1 /* Copyright (C) 2000-2006  Thomas Bopp, Thorsten Hampel, Ludger Merkens, Robert Hinn
     3  *  This program is free software; you can redistribute it and/or modify
     4  *  it under the terms of the GNU General Public License as published by
     5  *  the Free Software Foundation; either version 2 of the License, or
     6  *  (at your option) any later version.
     8  *  This program is distributed in the hope that it will be useful,
     9  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  *  GNU General Public License for more details.
    13  *  You should have received a copy of the GNU General Public License
    14  *  along with this program; if not, write to the Free Software
    15  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
    17  * $Id: ldap.pike,v 1.15 2010/08/18 20:32:45 astra Exp $
    19 inherit "/kernel/module";
    20 #include <configure.h>
    23 #include <attributes.h>
    27 //! This module is a ldap client inside sTeam. It reads configuration
    28 //! parameters of sTeam to contact a ldap server.
    29 //! All the user management can be done with ldap this way. Some
    30 //! special scenario might require to modify the modules code to 
    33 //! The configuration variables used are:
    34 //! server - the server name to connect to (if none is specified, then ldap will not be used)
    35 //! cacheTime - how long (in seconds) shall ldap entries be cached? (to reduce server requests)
    36 //! reuseConnection - "true" if you want to keep a single ldap connection open and reuse it, "false" if you want to close the connection after each request
    37 //! restrictAccess - "true" if the ldap module shall only allow certain modules
    38 //!   (persistence:ldap and auth) to call it (this is a privacy feature that
    39 //!   prevents users to query ldap data of other users)
    40 //! user   - ldap user for logging in
    41 //! password - the password for the user
    42 //! base_dc - ldap base dc, consult ldap documentation
    43 //! userdn - the dn path where new users are stored
    44 //! groupdn - the dn path where new groups are stored
    45 //! objectName - the ldap object name to be used for the search
    46 //! userAttr - the attribute containing the user's login name
    47 //! passwordAttr - the attribute containing the password
    48 //! passwordPrefix - a string to put before the password from passwordAttr, e.g. {sha}, {crypt}, {lm}, $1$
    49 //! emailAttr - the attribute containing the user's email address
    50 //! iconAttr - the attribute containing the user's icon
    51 //! fullnameAttr - the attribute containing the user's surname
    52 //! nameAttr - the attribute containing the user's first name
    53 //! userClass - an attribute value that identifies users
    54 //! userId - an attribute that contains a value that can be used to match users to groups
    55 //! userAttributes - a (comma-separated) list of ldap attributes that should be copied as attributes to the user object (the steam attributes will be called the same as the ldap attributes)
    56 //! groupAttr - the attribute (or comma-separated attributes) containing the group's name (must match the entries in groupClass)
    57 //! groupParentAttr - an attribute specifying an ldap attribute that contains the name of the parent group for a group (Note: this is only a workaround for two-level hierarchies in certain settings. Real group hierarchies in ldap will be implemented later!)
    58 //! groupClass - an attribute value (or comma-separated list of attribute values) that identifies groups (must match the entries in groupAttr)
    59 //! groupId - an attribute that contains a value that can be used to match users to groups
    60 //! subgroupMethod - determines how group hierarchies are done in ldap:
    61 //!           "structural" : subgroups are children in the ldap tree and the
    62 //!                          parent group thus appears in their dn
    63 //!           "attribute" : a single attribute specifies the name of the
    64 //!                         parent group (which doesn't have to exist in ldap),
    65 //!                         this method is considered deprecated and only
    66 //!                         allows one level group hierarchies
    67 //! subgroupIgnore - (may be specified multiple times) identifies dn parts
    68 //!                  that should not be considered parent groups if they appear
    69 //!                  in the dn of a group
    70 //! memberAttr - an attribute that contains a value that can be used to match users to groups
    71 //! groupAttributes - a (comma-separated) list of ldap attributes that should be copied as attributes to the group object (the steam attributes will be called the same as the ldap attributes)
    72 //! descriptionAttr - the attribute that contains a user or group description
    73 //! notfound - defines what should be done if a user or group could not be found in LDAP:
    74 //!            "create" : create and insert a new user in LDAP (at userdn)
    75 //!            "ignore" : do nothing
    76 //! sync - "true"/"false" : sync user or group data (false: read-only LDAP access)
    77 //! bindUser - "true" : when authorizing users, try ldap bind first (you will need this if the ldap lookup doesn't return a password for the user)
    78 //! bindUserWithoutDN - "false" : when binding users, only use the user name instead of the dn
    79 //! prefetchUsers : set to true to prefetch all users from ldap
    80 //! updateCacheInterval: interval in hours to update cache, set cacheTime to 0
    81 //! updateCacheStartHour: start hour for update as a string, e.g. 4 for 4am
    82 //! requiredAttr : required attributes for creating new users in LDAP
    83 //! adminAccount - "root" : sTeam account that will receive administrative mails about ldap
    84 //! charset - "utf-8" : charset used in ldap server
    85 //! suspendAttr - an ldap attribute that is used to suspend (lock out) users
    86 //! suspendAttrValue - the value (or values, separated by commas) of the suspendAttr ldap attribute. If the attribute has one of these values, the user will be suspended
    87 //! logAuthorization - switch on logging of authorization attempts by default,
    88 //!                    can be any combination of the following (separated by
    90 //!                    "failure" : log failed authorization attempts (and
    91 //!                                the first subsequent successful attempt)
    92 //!                    "success" : log successful authorization attempts
    93 //!                    "details" : output more details (crc checksums of
    95 class ldap : public module{
   105 #define LDAP_LOG(s, args...) werror("ldap: "+s+"\n", args)
   107 #define LDAP_LOG(s, args...)
   110 #define LOG_AUTH(s, args...) werror("ldap-auth (%s): "+s+"\n", Calendar.Second(time())->format_time(), args)
   112 #define LOG_TIME( start_time, msg, args... ) { int tmp_diff_time = get_time_millis() - start_time; if ( tmp_diff_time >= log_connection_times_min && tmp_diff_time <= log_connection_times_max ) MESSAGE( "LDAP took %d milliseconds: " + msg, tmp_diff_time, args ); }
   118  mapping    config = ([ ]);
   120  object charset_decoder;
   121  object charset_encoder;
   125  object authorize_cache;
   127  Thread.Local single_ldap;
   129  string last_bind_password_hash;
   130  int reconnection_time = 30;
   132  bool restrict_access = true;
   133  array valid_caller_objects = ({ });
   134  array valid_caller_programs = ({ });
   136  int log_connection_times_min = Int.NATIVE_MAX;
   137  int log_connection_times_max = 0;
   139  int log_level_auth = 0;
   140 constant LOG_AUTH_FAILURE = 1;
   141 constant LOG_AUTH_SUCCESS = 2;
   142 constant LOG_AUTH_DETAILS = 4;
   143  mapping log_failed_auth = ([ ]);
   145  object admin_account = 0;
   146  array ldap_conflicts = ({ });
   149 constant NO_ERROR = 0;
   150 constant ERROR_UNKNOWN = -1;
   151 constant ERROR_NOT_CONNECTED = -2;
   153  Thread.Queue invalidate_queue = Thread.Queue();
   155 string get_identifier() { return "ldap"; }
   160    mapping by_identifier = ([ ]);
   163    function last_modified_func;
   164    function multi_sync_func;
   166   object create ( int cache_time_seconds, function synchronize_function,
   167                   function|void last_modified_function, 
   168              function|void multi_synchronize_function ) {
   169     if ( !functionp(synchronize_function) )
   170       FATAL( "ldap: no synchronize function specified on cache creation" );
   171     sync_func = synchronize_function;
   172     multi_sync_func = multi_synchronize_function;
   173     last_modified_func = last_modified_function;
   174     cache_time = cache_time_seconds;
   179     return indices( by_identifier );
   182   mapping find ( string identifier ) {
   184     return by_identifier[ identifier ];
   188    void update_cache() {
   189     while (invalidate_queue->size() > 0) {
   190       mapping update_entry = invalidate_queue->read();
   191       LDAP_LOG("Updating cached entry of %O", update_entry);
   192       by_identifier[update_entry[config->userAttr]] = update_entry;
   198   mixed fetch ( string identifier, mixed ... args ) {
   200     if ( !stringp(identifier) ) return 0;
   202     // first try to update the cache if an update thread is running
   205     // check whether object is already cached:
   206     mapping cache_entry = by_identifier[ identifier ];
   207     if ( mappingp(cache_entry) ) {
   208       if ( cache_time > 0 && 
   209       time() - cache_entry->last_synchronized >= cache_time ) 
   212         if ( !functionp(last_modified_func) ||
   213              (cache_entry->last_modified == 0) ||
   214              ((last_modified = last_modified_func( identifier, @args )) >
   215               cache_entry->last_modified) ) {
   216           mixed tmp_data = sync_func( identifier, @args );
   217           if ( !tmp_data ) return 0;  // only cache values != 0
   218           if ( stringp(tmp_data) && sizeof(tmp_data) < 1 ) return 0;
   219           cache_entry->data = tmp_data;
   220           if ( last_modified ) cache_entry->last_modified = last_modified;
   221           cache_entry->last_synchronized = time();
   224       cache_entry->last_accessed = time();
   225       return cache_entry->data;
   227     // need to add object to cache:
   229     cache_entry->data = sync_func( identifier, @args );
   230     if ( !cache_entry->data ) return 0;  // only cache values != 0
   231     if ( stringp(cache_entry->data) && sizeof(cache_entry->data)<1 ) return 0;
   232     cache_entry->identifier = identifier;
   234     by_identifier[ identifier ] = cache_entry;
   236     if ( functionp(last_modified_func) )
   237       cache_entry->last_modified = last_modified_func( identifier );
   239     cache_entry->last_synchronized = time();
   240     cache_entry->last_accessed = time();
   242     return cache_entry->data;
   247     array userdata = multi_sync_func();
   248     foreach(userdata, mapping data) {
   249       mapping cache_entry = ([ ]);
   252       if (sizeof(data) == 0) {
   256       data = fix_charset( data );
   257       identifier = data[config->userAttr];
   258       LDAP_LOG("pre_fetch: %s  %O (%d rows)",identifier,data->cn,sizeof(data));
   259       cache_entry->data = data;
   260       cache_entry->identifier = identifier;
   261       by_identifier[identifier] = cache_entry;
   263       if ( functionp(last_modified_func) )
   264    cache_entry->last_modified = last_modified_func( identifier );
   265       cache_entry->last_synchronized = time();
   266       cache_entry->last_accessed = time();
   270   int drop ( void|string identifier ) {
   271     if ( !has_index( by_identifier, identifier ) )
   273     m_delete( by_identifier, identifier );
   278     return sizeof(by_identifier);
   282 bool ldap_activated ()
   284   array servers = Config.array_value( config["server"] );
   285   if ( arrayp(servers) ) {
   286     if ( sizeof(servers) < 1 ) return false;
   287     foreach ( servers, mixed server )
   288       if ( stringp(server) && sizeof(server) > 0 ) return true;
   295 int get_last_error () {
   300 string make_dn ( mixed ... parts ) {
   302   foreach ( parts, mixed part ) {
   303     if ( stringp(part) && sizeof(part) > 0 ) {
   304       if ( sizeof(dn) > 0 && dn[-1] != ',' ) dn += ",";
   312 mapping get_group_spec () {
   313   array groupAttrs = Config.array_value( config->groupAttr );
   314   array groupClasses = Config.array_value( config->groupClass );
   315   if ( !arrayp(groupAttrs) || sizeof(groupAttrs) == 0 ||
   316        !arrayp(groupClasses) || sizeof(groupClasses) == 0 ||
   317        sizeof(groupAttrs) != sizeof(groupClasses) )
   319   return mkmapping( groupAttrs, groupClasses );
   323 string get_group_attr ( mapping data, void|mapping group_spec ) {
   324   if ( !mappingp(group_spec) )
   325     group_spec = get_group_spec();
   326   if ( !mappingp(group_spec) ) return UNDEFINED;
   327   foreach ( indices(group_spec), string attr ) {
   328     mixed value = data[ attr ];
   329     if ( stringp(value) && value != "" )
   336 string dn_to_group_name ( string dn, void|int dont_strip_base_dc ) {
   337   string name = low_dn_to_group_name( dn );
   338   if ( !stringp(name) || sizeof(name) < 1 ) return name;
   339   if ( dont_strip_base_dc || !stringp(config->base_dc) ) return name;
   340   string base = low_dn_to_group_name( config->base_dc );
   341   if ( !has_prefix( lower_case(name), lower_case(base) ) ) return name;
   342   name = name[(sizeof(base)+1)..];
   343   if ( sizeof(name)<1 ) return 0;
   348  string low_dn_to_group_name ( string dn ) {
   349   if ( !stringp(dn) || sizeof(dn)<1 ) return 0;
   351   array ignore = Config.array_value(config->subgroupIgnore);
   352   if ( !arrayp(ignore) ) ignore = ({ });
   353   for ( int i=0; i<sizeof(ignore); i++ )
   354     ignore[i] = lower_case( String.trim_all_whites(ignore[i]) );
   355   foreach ( reverse( dn / "," ), string part ) {
   356     part = String.trim_all_whites( part );
   357     // TODO: handle wildcard *
   358     if ( search( ignore, lower_case( part ) ) >= 0 ) continue;
   359     sscanf( part, "%*s=%s", part );
   360     if ( stringp(part) && sizeof(part)>0 ) {
   361       if ( stringp(name) && sizeof(name)>0 )
   373 bool check_access () {
   374   if ( !restrict_access ) return true;
   376   if ( search( valid_caller_objects, CALLER ) >= 0 ) return true;
   378   string prog = sprintf( "%O",object_program(CALLER) );
   379   if ( search( valid_caller_programs, prog ) >= 0 ) return true;
   380   // if caller is not valid, throw an exception:
   381   steam_error( sprintf("ldap may not be accessed by caller %O\n", CALLER) );
   384 object connect ( void|string user, void|string password, void|bool dont_reconnect ) {
   386   last_error = ERROR_NOT_CONNECTED;
   387   if ( !ldap_activated() ) return 0;
   390   if ( Config.bool_value( config->bindUser ) && stringp(user) ) {
   391     if ( Config.bool_value(config->bindUserWithoutDN) )
   394       bind_dn = make_dn( config->userAttr + "=" + user, config["userdn"], config["base_dc"] );
   397   else if ( stringp(config["user"]) && sizeof(config["user"]) > 0 ) {
   398     // bind with default/root user
   399     if ( search( config["user"], "=" ) >= 0 )
   400       bind_dn = config["user"];
   402       bind_dn = make_dn( "cn="+config["user"] );
   403     password = config["password"];
   405   else {  // don't bind at all:
   409   LDAP_LOG("count of open file desciptors: " + sizeof(Stdio.get_all_active_fd()) );
   412     if ( Config.bool_value(config["reuseConnection"]) ||
   413          config["reconnectTime"] > 0 ) {
   414       if ( objectp(single_ldap) ) {
   419       disconnect( single_ldap );
   420     if ( !objectp(ldap) ) {
   421       array servers = Config.array_value( config["server"] );
   422       if ( arrayp(servers) ) {
   423         foreach ( servers, mixed server ) {
   424           if ( !stringp(server) || sizeof(server) < 1 ) continue;
   425           ldap = Protocols.LDAP.client( server );
   426           if ( objectp(ldap) ) break;
   427           LDAP_LOG( "failed connection to %s", server );
   431    LDAP_LOG( "new connection: %O", ldap );
   434   if ( err ) FATAL( "ldap: error while connecting: %O\n", err[0] );
   435   if ( !objectp(ldap) ) return 0;
   440     if ( stringp(bind_dn) && stringp(password) ) {
   441       if ( bind_dn != last_bind_dn ||
   442            !verify_crypt_md5( password, last_bind_password_hash ) ) {
   443         LDAP_LOG( "binding %O", bind_dn );
   444         if ( !ldap->bind( fix_query_string(bind_dn), password ) )
   445           throw( "bind failed on " + bind_dn );
   446         // remember dn and password hash, so we don't need to re-bind if
   447         // the user and password haven't changed:
   448         last_bind_dn = bind_dn;
   449         last_bind_password_hash = make_crypt_md5( password );
   452     else LDAP_LOG( "using without bind" );
   453     ldap->set_scope( 2 );
   454     if ( stringp(config["base_dc"]) && sizeof(config["base_dc"]) )
   455       ldap->set_basedn( fix_query_string(config["base_dc"]) );
   458     string error_msg = "";
   459     mixed ldap_error_nr = ldap->error_number();
   460     if ( ldap_error_nr > 0 )
   461       error_msg = "\n" + ldap->error_string() +
   462    sprintf( " (#%d)", ldap_error_nr );
   463     LDAP_LOG( "Failed to bind " + bind_dn + " on ldap" + error_msg );
   464     if ( ldap_error_nr == 0x31 || // invalid credentials
   465          ldap_error_nr == 0x30 ) {  // no such object
   470       disconnect( ldap, true );
   471       last_error = ERROR_NOT_CONNECTED;
   472       if ( dont_reconnect ) {
   473         FATAL("Failed to bind " + bind_dn + " on ldap" + error_msg);
   477         return connect( user, password, true );  // try to reconnect once
   480   last_error = NO_ERROR;
   485 void disconnect ( object ldap, void|bool force_disconnect ) {
   487   last_error = NO_ERROR;
   488   if ( !objectp(ldap) ) return;
   489   if ( Config.bool_value(config["reuseConnection"]) ||
   490        config["reconnectTime"] > 0 ) {
   491     if ( !force_disconnect ) return;
   495   last_bind_password_hash = 0;
   497   //removed for pike 7.8: method query_fd() on ldap no longer exists.
   498 //  int fd = ldap->query_fd();
   501 //    FATAL( "ldap: %d open file descriptors, calling garbage collector...\n",
   502 //            sizeof( Stdio.get_all_active_fd() ) );
   503 //    int time_start = get_time_millis();
   504 //    gc();  // force garbage collection to free ldap file descriptors
   505 //    FATAL( "ldap: garbage collector done, %d open file descriptors remain.\n",
   506 //            sizeof( Stdio.get_all_active_fd() ) );
   507 //    LOG_TIME( time_start, "disconnect[gc]" );
   509 //  LDAP_LOG( "connection closed (file descriptor was %d)", fd );
   513 mapping get_config ()
   520 object get_user_cache () {
   521   if ( !GROUP("admin")->is_member( this_user() ) ) return 0;
   526 object get_group_cache () {
   527   if ( !GROUP("admin")->is_member( this_user() ) ) return 0;
   532 object get_authorize_cache () {
   533   if ( !GROUP("admin")->is_member( this_user() ) ) return 0;
   534   return authorize_cache;
   538 mapping get_failed_authorize () {
   539   if ( !GROUP("admin")->is_member( this_user() ) ) return 0;
   540   return log_failed_auth;
   544  int hashcode(map userdata)
   547   if (!mappingp(userdata))
   550   foreach(values(userdata), mixed v) {
   552       hashvalue += hash(v);
   561  void update_cache() {
   563     // first check if the time is right
   564     int startUpdate = config->updateCacheStartHour;
   565     if (!intp(startUpdate))
   571     string current_time = ctime(time());
   572     sscanf(current_time, "%*s %*s %*d %d:%*d:%*d %*s\n", hour);
   573     LDAP_LOG("Checking update time: Hour = %O, update = %O\n", 
   576     if ( hour == startUpdate) {
   578    int updateInterval = config->updateCacheInterval;
   580    int time_start = get_time_millis();
   581    object ldap = connect();
   582    array(map) users = fetch_users_internal();
   584    foreach(users, mapping user_data) {
   585      if (sizeof(user_data) == 0)
   588      user_data = fix_charset( user_data );
   590      mapping cached_data = user_cache->fetch(user_data[config->userAttr]);
   591      if (hashcode(user_data) != hashcode(cached_data)) {
   592        LDAP_LOG("User updated in LDAP directory: invalidating user %O\n", 
   594        invalidate_queue->write(user_data);
   597    MESSAGE("Invalidating LDAP cache in %d ms, %d updates", 
   598            get_time_millis() - time_start, invalidate_queue->size());
   599    sleep(updateInterval*3600);
   611     valid_caller_objects = ({ master() });
   612     // since ldap is initialized before ldappersistence, we cannot fetch the
   613     // ldappersistence object yet... we just check against the program path:
   614     valid_caller_programs = ({ "/modules/ldappersistence", "/modules/auth" });
   616     last_error = NO_ERROR;
   617     config = Config.read_config_file( _Server.get_config_dir()+"/modules/ldap.cfg", "ldap" );
   618     if ( !mappingp(config) ) {
   620    MESSAGE("LDAP Service not started - missing configuration !");
   621         last_error = ERROR_NOT_CONNECTED;
   622    return; // ldap not started !
   624     if ( !ldap_activated() ) {
   625       MESSAGE("LDAP deactivated.");
   626       last_error = ERROR_NOT_CONNECTED;
   627       return;  // ldap deactivated
   630     LDAP_LOG("configuration is %O", config);
   633     if ( stringp(config["charset"]) && config["charset"] != "utf-8" ) {
   634       charset_decoder = Locale.Charset.decoder( config["charset"] );
   635       charset_encoder = Locale.Charset.encoder( "utf-8" );
   636       if ( !objectp(charset_decoder) || !objectp(charset_encoder) )
   637         FATAL( "LDAP: could not create a charset converter for %s\n",
   646     int cache_time = config["cacheTime"];
   647     user_cache = LDAPCache( cache_time, fetch_user_internal,
   648                             user_last_modified, fetch_users_internal );
   649     group_cache = LDAPCache( cache_time, fetch_group_internal,
   650                              group_last_modified );
   651     authorize_cache = LDAPCache( cache_time, authenticate_user_internal,
   652                                  user_last_modified );
   655     if ( stringp(config["logAuthorization"]) ) {
   656       array a = Config.array_value( config["logAuthorization"] );
   657       foreach ( a, string s ) {
   658         switch ( lower_case(s) ) {
   661             log_level_auth |= LOG_AUTH_FAILURE;
   665             log_level_auth |= LOG_AUTH_SUCCESS;
   669             log_level_auth |= LOG_AUTH_DETAILS;
   675     // restricted access:
   676     if ( !zero_type(config->restrictAccess) &&
   677          !Config.bool_value(config->restrictAccess) )
   678       restrict_access = false;
   680       restrict_access = true;
   681     valid_caller_objects += ({ user_cache, group_cache, authorize_cache });
   683     if ( stringp(config->notfound) && lower_case(config->notfound) == "create"
   684          && !config->objectName )
   685       steam_error("objectName configuration missing !");
   687     if ( Config.bool_value( config->sync ) ) {
   688       // if our main dc does not exist - create it
   689       object ldap = connect();
   691         ldap->add( config->base_dc, ([
   692           "objectclass": ({ "dcObject", "organization" }),
   693              "o": "sTeam Authorization Directory",
   696       if ( err ) FATAL( "ldap: failed to create main dc: %O\n", err[0] );
   700       // check whether the connection is working
   704    array servers = Config.array_value( config["server"] );
   705         if ( arrayp(servers) ) {
   706           foreach ( servers, mixed tmp_server ) {
   707             if ( !stringp(tmp_server) || sizeof(tmp_server) < 1 ) continue;
   709             ldap = Protocols.LDAP.client( server );
   710             if ( objectp(ldap) ) break;
   714       if ( err ) FATAL( "ldap: error while connecting: %O\n", err[0] );
   715       if ( objectp(ldap) ) {
   716    MESSAGE( "LDAP: connected to %s", server );
   720         last_error = ERROR_NOT_CONNECTED;
   721    MESSAGE( "LDAP: failed to connect to %s", server );
   722    werror( "LDAP: failed to connect to %s\n", server );
   726     if (stringp(config->prefetchUsers)) {
   727       int time_start = get_time_millis();
   728       user_cache->pre_fetch();
   729       MESSAGE("LDAP: pre-fetched " + user_cache->size() + " users in %d ms", 
   730          get_time_millis() - time_start);
   732     if (intp(config->updateCacheInterval)) {
   733       start_thread(update_cache);
   742   if ( ldap_activated() ) {
   743     add_global_event(EVENT_USER_CHANGE_PW, sync_password, PHASE_NOTIFY);
   744     add_global_event(EVENT_USER_NEW_TICKET, sync_ticket, PHASE_NOTIFY);
   745     add_global_event(EVENT_ADD_MEMBER, event_add_member, PHASE_NOTIFY);
   746     add_global_event(EVENT_REMOVE_MEMBER, event_remove_member, PHASE_NOTIFY);
   751 void event_add_member ( int e, object grp, object caller, object member, bool pw ) {
   752   if ( member->get_object_class() & CLASS_GROUP ) {
   753     uncache_group( member->get_identifier() );
   754     uncache_group( grp->get_identifier() );
   759 void event_remove_member ( int e, object grp, object caller, object member ) {
   760   if ( member->get_object_class() & CLASS_GROUP ) {
   761     uncache_group( member->get_identifier() );
   762     uncache_group( grp->get_identifier() );
   768 private  bool notify_admin ( string msg ) {
   769   if ( zero_type( config["adminAccount"] ) ) return false;
   770   object admin = USER( config["adminAccount"] );
   771   if ( !objectp(admin) ) admin = GROUP( config["adminAccount"] );
   772   if ( !objectp(admin) ) return false;
   773   string msg_text = "The following LDAP situation occured on the server "
   774     + _Server->get_server_name() +" at "+ (ctime(time())-"\n") + " :\n" + msg;
   775   admin->mail( msg_text, "LDAP on " + _Server->get_server_name(), 0, "text/plain" );
   783  mixed map_results(object results)
   785   array result = ({ });
   786   LDAP_LOG("map_results with " + results->num_entries() + " entries");
   787   for ( int i = 1; i <= results->num_entries(); i++ ) {
   788     mapping            data = ([ ]);
   789     mapping res = results->fetch(i);
   791     if (!mappingp(res)) {
   792       LDAP_LOG("unable to map result!");
   795     foreach(indices(res), string attr) {
   796       if ( arrayp(res[attr]) ) {
   797    if ( sizeof(res[attr]) == 1 )
   798      data[attr] = res[attr][0];
   800      data[attr] = res[attr];
   803     if ( results->num_entries() == 1 )
   805     result += ({ data });
   813 int uncache_user ( string user ) {
   814   if ( !objectp(user_cache) ) return 0;
   815   LDAP_LOG( "uncaching user %s from user cache", user );
   816   int dropped = user_cache->drop( user );
   817   if ( !objectp(authorize_cache) ) return 0;
   818   LDAP_LOG( "uncaching user %s from authorize cache", user );
   819   return dropped & authorize_cache->drop( user );
   823 int uncache_group ( string group ) {
   824   if ( !objectp(group_cache) ) return 0;
   825   LDAP_LOG( "uncaching group %s from group cache", group );
   826   return group_cache->drop( group );
   830 array(mapping) search_data ( string search_str, array result_attributes,
   831                 void|string base_dn, void|string user, void|string pass,
   834   last_error = NO_ERROR;
   835   if ( !ldap_activated() ) {
   836     last_error = ERROR_NOT_CONNECTED;
   840   if ( !stringp(search_str) || sizeof(search_str) < 1 )
   846   int time_start = get_time_millis();
   849   if ( Config.bool_value(config->bindUser) && stringp(user) && stringp(pass) )
   850     ldap = connect(user, pass);
   854   LDAP_LOG("searching data %s (user %O) : ldap is %O", search_str, user, ldap);
   856   if ( !objectp(ldap) ) {
   857     if ( nr_try < NR_RETRIES ) {
   858       disconnect( ldap, true );
   859       return search_data( search_str, result_attributes, base_dn,
   860                           user, pass, nr_try+1 );
   862     last_error = ERROR_NOT_CONNECTED;
   866   LDAP_LOG("searching data in LDAP: %s\n", search_str);
   868     ldap->set_basedn( fix_query_string(make_dn( base_dn, config->base_dc )) );
   869     if ( ldap->error_number() ) {
   870       if ( nr_try < NR_RETRIES ) {
   871         disconnect( ldap, true );
   872         return search_data( search_str, result_attributes, base_dn,
   873                             user, pass, nr_try+1 );
   875       FATAL( "ldap: error searching data: %s (#%d)\n",
   876          ldap->error_string(), ldap->error_number() );
   880   mixed err = catch( results = ldap->search( fix_query_string(search_str), result_attributes ) );
   882     if ( nr_try < NR_RETRIES ) {
   883       disconnect( ldap, true );
   884       return search_data( search_str, result_attributes, base_dn,
   885                           user, pass, nr_try+1 );
   887     FATAL( "ldap: error searching data: %s\n", err[0] );
   889     last_error = ERROR_UNKNOWN;
   893   LOG_TIME( time_start, "search_data( %s )", search_str );
   895   if ( objectp(ldap) && ldap->error_number() ) {
   896     if ( nr_try < NR_RETRIES ) {
   897       disconnect( ldap, true );
   898       return search_data( search_str, result_attributes, base_dn,
   899                           user, pass, nr_try+1 );
   901     FATAL( "ldap: Error while searching data: %s (#%d)\n",
   902        ldap->error_string(), ldap->error_number() );
   904     last_error = ERROR_UNKNOWN;
   908   if ( !objectp(results) ) {
   909     if ( nr_try < NR_RETRIES ) {
   910       disconnect( ldap, true );
   911       return search_data( search_str, result_attributes, base_dn,
   912                           user, pass, nr_try+1 );
   914     FATAL("ldap: Invalid results while searching data.\n");
   916     last_error = ERROR_UNKNOWN;
   920   if ( results->num_entries() == 0 ) {
   921     LDAP_LOG("No matching data found in LDAP directory: %s", search_str);
   926   udata = map_results( results );
   927   if ( mappingp(udata) ) udata = ({ udata });
   931   LOG_TIME( time_start, "search_data( %s )", search_str );
   933   if ( !arrayp(udata) ) return 0;
   938 array search_users ( mapping terms, bool any,
   939                              void|string user, void|string pass ) {
   941   if ( !mappingp(terms) || sizeof(terms)<1 ) {
   942     LDAP_LOG( "search_users: invalid terms: %O\n", terms );
   943     last_error = ERROR_UNKNOWN;
   947   string search_str = "";
   948   foreach ( indices(terms), mixed attr ) {
   949     mixed value = terms[attr];
   950     if ( !stringp(attr) || sizeof(attr) < 1 ||
   951          !stringp(value) || sizeof(value) < 1 ) continue;
   953       case "login" : attr = config->userAttr; break;
   954       case "firstname" : attr = config->nameAttr; break;
   955       case "lastname" : attr = config->fullnameAttr; break;
   956       case "email" : attr = config->emailAttr; break;
   958     search_str += "(" + attr + "=" + replace( value, ([ "(":"", ")":"", "=":"", ":":"" ]) ) + ")";
   960   if ( any ) search_str = "(&(objectclass=" + config->userClass + ")(|" +
   962   else search_str = "(&(objectclass=" + config->userClass + ")" +
   966   if ( stringp(config->user_dn) ) base_dn = config->user_dn;
   967   array result = search_data( search_str, ({ config->userAttr }),
   968                               base_dn, user, pass );
   970   foreach ( result, mixed res ) {
   971     if ( mappingp(res) ) {
   972       res = res[ config->userAttr ];
   973       if ( stringp(res) && sizeof(res) > 0 )
   982  * Returns an array of distinct names of all sub-groups of a group.
   984  * @param dn the dn of the group of which to return the sub-groups
   985  * @param recursive if 0 (default) then return only the immediate sub-groups of
   986  *   the group, otherwise return sub-groups recursively
   987  * @return an array of dn entries of the sub-groups
   989 array get_sub_groups ( string dn, int|void recursive, void|int nr_try ) {
   990   //check_access();  // group structures are no privacy data
   991   last_error = NO_ERROR;
   992   if ( !ldap_activated() ) {
   993     last_error = ERROR_NOT_CONNECTED;
   997   if ( !stringp(dn) || sizeof(dn)<1 ) {
   998     FATAL( "LDAP: get_subgroups: invalid dn: %O\n", dn );
   999     last_error = ERROR_UNKNOWN;
  1003   mapping group_spec = get_group_spec();
  1004   if ( !mappingp(group_spec) ) {
  1005     LDAP_LOG( "get_sub_groups: no groupAttr/groupClass configured" );
  1006     last_error = ERROR_UNKNOWN;
  1010   string group_identifier = dn_to_group_name( dn );
  1011   string group_name = (group_identifier / ".")[-1];
  1015   int time_start = get_time_millis();
  1017   object ldap = connect();
  1019   if ( !objectp(ldap) ) {
  1020     if ( nr_try < NR_RETRIES ) {
  1021       disconnect( ldap, true );
  1022       return get_sub_groups( dn, recursive, nr_try+1 );
  1024     last_error = ERROR_NOT_CONNECTED;
  1029   LDAP_LOG( "fetching subgroups of: %s%s\n",
  1030             dn, (recursive ? " (recursive)":"") );
  1031   ldap->set_basedn( fix_query_string(dn) );
  1032   if ( recursive ) old_scope = ldap->set_scope( 2 );
  1033   else old_scope = ldap->set_scope( 1 );
  1034   if ( ldap->error_number() ) {
  1035     if ( nr_try < NR_RETRIES ) {
  1036       disconnect( ldap, true );
  1037       return get_sub_groups( dn, recursive, nr_try+1 );
  1039     FATAL( "ldap: error fetching subgroups of %s : %s (#%d)\n", dn,
  1040            ldap->error_string(), ldap->error_number() );
  1043   string search_str = "";
  1044   foreach ( values(group_spec), string groupClass )
  1045     search_str += "(objectclass=" + groupClass + ")";
  1046   if ( sizeof(group_spec) > 1 )
  1047     search_str = "(|" + search_str + ")";
  1048   mixed err = catch( results = ldap->search( fix_query_string(search_str) ) );
  1050     if ( nr_try < NR_RETRIES ) {
  1051       disconnect( ldap, true );
  1052       return get_sub_groups( dn, recursive, nr_try+1 );
  1054     FATAL( "ldap: error fetching subgroups of %s : %s\n", dn, err[0] );
  1056     last_error = ERROR_UNKNOWN;
  1060   LOG_TIME( time_start, "get_subgroups( %s )", dn );
  1062   if ( objectp(ldap) && ldap->error_number() ) {
  1063     if ( nr_try < NR_RETRIES ) {
  1064       disconnect( ldap, true );
  1065       return get_sub_groups( dn, recursive, nr_try+1 );
  1067     FATAL( "ldap: Error while fetching subgroups of %s : %s (#%d)\n", dn,
  1068        ldap->error_string(), ldap->error_number() );
  1070     last_error = ERROR_UNKNOWN;
  1074   if ( !objectp(results) ) {
  1075     if ( nr_try < NR_RETRIES ) {
  1076       disconnect( ldap, true );
  1077       return get_sub_groups( dn, recursive, nr_try+1 );
  1079     FATAL("ldap: Invalid results while fetching subgroups of %s .\n", dn);
  1081     last_error = ERROR_UNKNOWN;
  1085   if ( results->num_entries() == 0 ) {
  1090   array data = map_results(results);
  1092   foreach ( data, mapping subgroup ) {
  1093     string sub_dn = subgroup["dn"];
  1094     if ( stringp(sub_dn) && sizeof(sub_dn) > 0 ) dns += ({ sub_dn });
  1096   LDAP_LOG( "group %s has %d subgroups", dn, sizeof(dns) );
  1098   if ( old_scope < 0 ) catch( ldap->set_scope( 2 ) );
  1099   else catch( ldap->set_scope( old_scope ) );
  1106 array search_groups ( mapping terms, bool any,
  1107                              void|string user, void|string pass ) {
  1109   if ( !mappingp(terms) || sizeof(terms)<1 ) {
  1110     LDAP_LOG( "search_groups: invalid terms: %O\n", terms );
  1111     last_error = ERROR_UNKNOWN;
  1115   mapping group_spec = get_group_spec();
  1116   if ( !mappingp(group_spec) ) {
  1117     LDAP_LOG( "search_groups: cannot search: no groupAttr/groupClass " +
  1119     last_error = ERROR_UNKNOWN;
  1123   string search_str = "";
  1124   foreach ( indices(terms), mixed attr ) {
  1125     mixed value = terms[attr];
  1126     if ( !stringp(attr) || sizeof(attr) < 1 ||
  1127          !stringp(value) || sizeof(value) < 1 ) continue;
  1128     value = replace( value, ([ "(":"", ")":"", "=":"", ":":"" ]) );
  1129     if ( attr == "name" ) {
  1130       string name_search_str = "";
  1131       foreach ( indices( group_spec ), string groupAttr ) {
  1132         string groupClass = group_spec[ groupAttr ];
  1133         name_search_str += "(&(" + groupAttr + "=" + value + ")(objectclass=" +
  1136       if ( sizeof(group_spec) > 1 )
  1137         name_search_str += "(|" + name_search_str + ")";
  1138       search_str += name_search_str;
  1141       search_str += "(" + attr + "=" + value + ")";
  1143   if ( any ) search_str = "(|" + search_str + ")";
  1144   else search_str = "(&" + search_str + ")";
  1147   if ( stringp(config->group_dn) ) base_dn = config->group_dn;
  1148   mixed result = search_data( search_str, ({ "dn" }) + indices(group_spec),
  1149                               base_dn, user, pass );
  1150   if ( !arrayp(result) )
  1152   array groups = ({ });
  1153   foreach ( result, mixed res ) {
  1154     if ( mappingp(res) ) {
  1155       if ( !stringp(get_group_attr( res, group_spec )) ||
  1156            !stringp(res["dn"]) || res["dn"] == "" )
  1158       groups += ({ dn_to_group_name( res["dn"] )  });
  1165 mapping search_user ( string search_str, void|string user, void|string pass,
  1169   last_error = NO_ERROR;
  1170   if ( !ldap_activated() ) {
  1171     last_error = ERROR_NOT_CONNECTED;
  1175   if ( !stringp(search_str) || sizeof(search_str)<1 ) {
  1176     FATAL( "LDAP: search_user: invalid search_str: %O\n", search_str );
  1177     last_error = ERROR_UNKNOWN;
  1181   mapping udata = ([ ]);
  1184   int time_start = get_time_millis();
  1187   if ( Config.bool_value(config->bindUser) && stringp(user) && stringp(pass) )
  1188     ldap = connect(user, pass);
  1192   LDAP_LOG("searching %s (user %O) : ldap is %O", search_str, user, ldap);
  1194   if ( !objectp(ldap) ) {
  1195     if ( nr_try < NR_RETRIES ) {
  1196       disconnect( ldap, true );
  1197       return search_user( search_str, user, pass, nr_try+1 );
  1199     last_error = ERROR_NOT_CONNECTED;
  1203   LDAP_LOG("looking up user in LDAP: %s\n", search_str);
  1204   if ( config->userdn ) {
  1205     ldap->set_basedn( fix_query_string(make_dn( config->userdn, config->base_dc )) );
  1206     if ( ldap->error_number() ) {
  1207       if ( nr_try < NR_RETRIES ) {
  1208         disconnect( ldap, true );
  1209         return search_user( search_str, user, pass, nr_try+1 );
  1211       FATAL( "ldap: error searching user: %s (#%d)\n",
  1212          ldap->error_string(), ldap->error_number() );
  1216   mixed err = catch( results = ldap->search( fix_query_string(search_str) ) );
  1218     if ( nr_try < NR_RETRIES ) {
  1219       disconnect( ldap, true );
  1220       return search_user( search_str, user, pass, nr_try+1 );
  1222     FATAL( "ldap: error searching user: %s\n", err[0] );
  1224     last_error = ERROR_UNKNOWN;
  1228   LOG_TIME( time_start, "search_user( %s )", search_str );
  1230   if ( objectp(ldap) && ldap->error_number() ) {
  1231     if ( nr_try < NR_RETRIES ) {
  1232       disconnect( ldap, true );
  1233       return search_user( search_str, user, pass, nr_try+1 );
  1235     if ( ldap->error_number() != 32 )  // 32 = no such object
  1236       FATAL( "ldap: Error while searching user: %s (#%d)\n",
  1237              ldap->error_string(), ldap->error_number() );
  1239     last_error = ERROR_UNKNOWN;
  1243   if ( !objectp(results) ) {
  1244     if ( nr_try < NR_RETRIES ) {
  1245       disconnect( ldap, true );
  1246       return search_user( search_str, user, pass, nr_try+1 );
  1248     FATAL("ldap: Invalid results while searching user.\n");
  1250     last_error = ERROR_UNKNOWN;
  1254   if ( results->num_entries() == 0 ) {
  1255     LDAP_LOG("User not found in LDAP directory: %s", search_str);
  1260   udata = map_results( results );
  1261   LDAP_LOG( "user %s has dn: %O and %d data entries",
  1262             search_str, udata["dn"], sizeof(indices(udata)) );
  1266   LOG_TIME( time_start, "search_user( %s )", search_str );
  1272 mapping search_group ( string search_str, void|int nr_try )
  1275   last_error = NO_ERROR;
  1276   if ( !ldap_activated() ) {
  1277     last_error = ERROR_NOT_CONNECTED;
  1281   if ( !stringp(search_str) || sizeof(search_str)<1 ) {
  1282     FATAL( "LDAP: search_group: invalid search_str: %O\n", search_str );
  1283     last_error = ERROR_UNKNOWN;
  1287   mapping gdata = ([ ]);
  1290   if ( !mappingp(get_group_spec()) ) {
  1291     last_error = ERROR_UNKNOWN;
  1295   int time_start = get_time_millis();
  1297   object ldap = connect();
  1299   if ( !objectp(ldap) ) {
  1300     if ( nr_try < NR_RETRIES ) {
  1301       disconnect( ldap, true );
  1302       return search_group( search_str, nr_try+1 );
  1304     last_error = ERROR_NOT_CONNECTED;
  1308   LDAP_LOG("looking up group in LDAP: %s\n", search_str);
  1309   if ( config->groupdn ) {
  1310     ldap->set_basedn( fix_query_string(make_dn( config->groupdn, config->base_dc )) );
  1311     if ( ldap->error_number() ) {
  1312       if ( nr_try < NR_RETRIES ) {
  1313         disconnect( ldap, true );
  1314         return search_group( search_str, nr_try+1 );
  1316       FATAL( "ldap: error searching group: %s (#%d)\n",
  1317              ldap->error_string(), ldap->error_number() );
  1321   mixed err = catch( results = ldap->search( fix_query_string(search_str) ) );
  1323     if ( nr_try < NR_RETRIES ) {
  1324       disconnect( ldap, true );
  1325       return search_group( search_str, nr_try+1 );
  1327     FATAL( "ldap: error searching group: %s\n", err[0] );
  1329     last_error = ERROR_UNKNOWN;
  1333   LOG_TIME( time_start, "search_group( %s )", search_str );
  1335   if ( objectp(ldap) && ldap->error_number() ) {
  1336     if ( nr_try < NR_RETRIES ) {
  1337       disconnect( ldap, true );
  1338       return search_group( search_str, nr_try+1 );
  1340     if ( ldap->error_number() != 32 )  // 32 = no such object
  1341       FATAL( "ldap: Error while searching group: %s (#%d)\n",
  1342              ldap->error_string(), ldap->error_number() );
  1344     last_error = ERROR_UNKNOWN;
  1348   if ( !objectp(results) ) {
  1349     if ( nr_try < NR_RETRIES ) {
  1350       disconnect( ldap, true );
  1351       return search_group( search_str, nr_try+1 );
  1353     FATAL("ldap: Invalid results while searching group.\n");
  1355     last_error = ERROR_UNKNOWN;
  1359   if ( results->num_entries() == 0 ) {
  1360     LDAP_LOG("Group not found in LDAP directory: %s", search_str);
  1365   gdata = map_results(results);
  1366   LDAP_LOG( "group %s has dn: %O", search_str, gdata["dn"] );
  1374 int user_last_modified ( string identifier, void|string pass )
  1377   //TODO: query ldap attribute modifyTimestamp
  1382 mapping fetch_user ( string identifier, void|string pass )
  1385   int time_start = get_time_millis();
  1386   last_error = NO_ERROR;
  1387   if ( !ldap_activated() ) {
  1388     last_error = ERROR_NOT_CONNECTED;
  1391   if ( !stringp(identifier) ) {
  1392     last_error = ERROR_UNKNOWN;
  1396   LDAP_LOG("fetch_user(%s) %d", identifier, stringp(pass));
  1398   mapping result = user_cache->fetch( identifier, pass );
  1399   if ( !mappingp(result) ) authorize_cache->drop( identifier );
  1400   LOG_TIME( time_start, "fetch_user( %s )", identifier );
  1406  mapping fetch_user_internal ( string identifier, void|string pass )
  1409   LDAP_LOG("fetch_user_internal(%s) %d", identifier, stringp(pass));
  1410   string search_str = "("+config->userAttr+"="+identifier+")";
  1411   if ( stringp(config->userClass) && sizeof(config->userClass)>0 )
  1412     search_str = "(&"+search_str+"(objectclass="+config->userClass+"))";
  1413   mapping result = fix_charset( search_user( search_str, identifier, pass ) );
  1414   if ( !mappingp(result) ) 
  1416   if ( lower_case(result[config->userAttr]) != lower_case(identifier) )
  1426  array(mapping) fetch_users_internal ()
  1429   LDAP_LOG("fetch_users_internal()");
  1430   string search_str = "("+config->userAttr+"=*)";
  1431   if ( stringp(config->userClass) && sizeof(config->userClass)>0 )
  1432     search_str = "(&"+search_str+"(objectclass="+config->userClass+"))";
  1433   object ldap = connect();
  1434   if ( !objectp(ldap) )
  1437   object results = ldap->search (fix_query_string(search_str));
  1438   if ( results->num_entries() == 0 ) {
  1439     LDAP_LOG("No matching data found in LDAP directory: %s", search_str);
  1443   LDAP_LOG("Found %d results", results->num_entries());
  1445   return map_results( results );
  1450 int group_last_modified ( string identifier )
  1453   //TODO: query ldap attribute modifyTimestamp
  1458 mapping fetch_group ( string identifier )
  1461   int time_start = get_time_millis();
  1462   last_error = NO_ERROR;
  1463   if ( !ldap_activated() ) {
  1464     last_error = ERROR_NOT_CONNECTED;
  1467   if ( !stringp(identifier) || sizeof(identifier) < 1 ) {
  1468     last_error = ERROR_UNKNOWN;
  1472   if ( !mappingp(get_group_spec()) ) {
  1473     last_error = ERROR_UNKNOWN;
  1477   LDAP_LOG("fetch_group(%s)", identifier);
  1478   mapping result = group_cache->fetch( identifier );
  1479   LOG_TIME( time_start, "fetch_group( %s )", identifier );
  1485  mapping fetch_group_internal ( string identifier )
  1488   LDAP_LOG("fetch_group_internal(%s)", identifier);
  1489   mapping group_spec = get_group_spec();
  1490   if ( !mappingp(group_spec) ) {
  1491     LDAP_LOG( "fetch_group_internal: could not fetch: no " +
  1492               "groupAttr/groupClass specified in config." );
  1493     last_error = ERROR_UNKNOWN;
  1497   array parts = identifier / ".";
  1498   string search_str = "(|";
  1499   foreach ( indices(group_spec), string groupAttr ) {
  1500     search_str += "(&(" + groupAttr + "=" + parts[-1] +
  1501       ")(objectclass=" + group_spec[groupAttr] + "))";
  1504   // sub-groups by structural method are already handled by that query
  1506   // pseudo sub-groups by attribute:
  1507   if ( config->subgroupMethod == "attribute" &&
  1508        stringp(config->groupParentAttr) &&
  1509        sizeof(config->groupParentAttr) > 0 && sizeof(parts) > 1 ) {
  1510     search_str = "(&" + search_str + "(" + config->groupParentAttr +
  1511       "=" + parts[-2] + "))";
  1514   mixed results = fix_charset( search_group( search_str ) );
  1516   if ( config->subgroupMethod == "structural" && arrayp(results) ) {
  1517     string lower_identifier = lower_case( identifier );
  1518     foreach ( results, mapping m ) {
  1519       string result_name = dn_to_group_name( m->dn );
  1520       if ( lower_case( result_name ) == lower_identifier ) {
  1527   // if there is more than one result, only return one:
  1529   if ( arrayp(results) ) result = results[0];
  1530   else if ( mappingp(results) ) result = results;
  1531   if ( !mappingp(result) ) return UNDEFINED;
  1532   string identifier_last_part = (identifier / ".")[-1];
  1533   string result_last_part = (dn_to_group_name( result->dn ) / ".")[-1];
  1534   if ( lower_case(result_last_part) != lower_case(identifier_last_part) )
  1541 mixed fetch ( string dn, string pattern )
  1543   return fetch_scope( dn, pattern, 2 );
  1546 mixed fetch_scope ( string dn, string pattern, int scope )
  1549   last_error = NO_ERROR;
  1550   if ( !ldap_activated() ) {
  1551     last_error = ERROR_NOT_CONNECTED;
  1554   if ( !stringp(dn) || !stringp(pattern) ) {
  1555     last_error = ERROR_UNKNOWN;
  1559   // caller must be module...
  1560   if ( !_Server->is_module( CALLER ) )
  1561     steam_error( "Access for non-module denied !" );
  1565   object ldap = connect();
  1567   if ( !objectp(ldap) ) {
  1568     last_error = ERROR_NOT_CONNECTED;
  1572   ldap->set_scope( scope );
  1573   if ( stringp(dn) && sizeof(dn)>0 ) {
  1574     if ( has_suffix( dn, config->base_dc ) )
  1575       ldap->set_basedn( fix_query_string(dn) );
  1577       ldap->set_basedn( fix_query_string(make_dn( dn, config->base_dc )) );
  1580     ldap->set_basedn( fix_query_string(config->base_dc) );
  1581   if ( ldap->error_number() )
  1582     FATAL( "ldap: error fetching: %s (#%d)\n",
  1583        ldap->error_string(), ldap->error_number() );
  1585   mixed err = catch( results = ldap->search( fix_query_string(pattern) ) );
  1587     FATAL( "ldap: error fetching: %s\n", err[0] );
  1589     last_error = ERROR_UNKNOWN;
  1593   if ( objectp(ldap) && ldap->error_number() ) {
  1594     if ( ldap->error_number() != 32 )  // 32 = no such object
  1595       FATAL( "ldap: Error while fetching: %s (#%d)\n",
  1596              ldap->error_string(), ldap->error_number() );
  1598     last_error = ERROR_UNKNOWN;
  1602   if ( !objectp(results) ) {
  1603     FATAL("ldap: Invalid results while fetching.\n");
  1605     last_error = ERROR_UNKNOWN;
  1609   if ( results->num_entries() == 0 ) {
  1610     LDAP_LOG("No results when fetching: %s", pattern);
  1617   return map_results( results );
  1621 array fetch_url ( string url, string additional_filter ) {
  1623   last_error = NO_ERROR;
  1624   if ( !ldap_activated() ) {
  1625     last_error = ERROR_NOT_CONNECTED;
  1628   if ( !stringp(url) || sizeof(url) < 1 ) {
  1629     last_error = ERROR_UNKNOWN;
  1632   // caller must be module...
  1633   if ( !_Server->is_module( CALLER ) )
  1634     steam_error( "Access for non-module denied !" );
  1638   object ldap = connect();
  1640   if ( !objectp(ldap) ) {
  1641     last_error = ERROR_NOT_CONNECTED;
  1645   mapping parts = ldap->parse_url( url );
  1646   if ( !mappingp(parts) ) {
  1647     last_error = ERROR_UNKNOWN;
  1651   string basedn = parts["basedn"];
  1652   if ( !stringp(basedn) ) basedn = "";
  1653   string filter = parts["filter"];
  1654   if ( !stringp(filter) ) filter = "";
  1655   if ( sizeof(filter) < 1 && sizeof(basedn) > 0 ) {
  1656     // basedn without filter:
  1657     array basedn_parts = basedn / ",";
  1658     filter = "(" + basedn_parts[0] + ")";
  1659     basedn = basedn_parts[1..] * ",";
  1661   else if ( sizeof(filter) < 1 && sizeof(basedn) < 1 ) {
  1662     last_error = ERROR_UNKNOWN;
  1665   ldap->set_scope( parts["scope"] );
  1666   ldap->set_basedn( fix_query_string(basedn) );
  1667   if ( stringp(additional_filter) && sizeof(additional_filter) > 0 ) {
  1668     if ( !has_prefix( additional_filter, "(" ) )
  1669       additional_filter = "(" + additional_filter;
  1670     if ( !has_suffix( additional_filter, ")" ) )
  1671       additional_filter += ")";
  1672     filter = "(&" + filter + additional_filter + ")";
  1674   mixed err = catch( results = ldap->search( fix_query_string(filter) ) );
  1676     FATAL( "ldap: error fetching url: %s\n", err[0] );
  1678     last_error = ERROR_UNKNOWN;
  1682   if ( objectp(ldap) && ldap->error_number() ) {
  1683     if ( ldap->error_number() != 32 )  // 32 = no such object
  1684       FATAL( "ldap: Error while fetching url: %s (#%d)\n",
  1685              ldap->error_string(), ldap->error_number() );
  1687     last_error = ERROR_UNKNOWN;
  1691   if ( !objectp(results) ) {
  1692     FATAL("ldap: Invalid results while fetching url.\n");
  1694     last_error = ERROR_UNKNOWN;
  1698   if ( results->num_entries() == 0 ) {
  1699     LDAP_LOG("No results when fetching url: %s", url);
  1706   mixed res = map_results( results );
  1707   if ( mappingp(res) ) res = ({ res });
  1712 string fix_query_string ( string s )
  1714   if ( !stringp(s) ) return s;
  1716   for ( int i=0; i<sizeof(s); i++ ) {
  1717     if ( (32 <= (int)s[i]) && ((int)s[i] < 128) ) s2 += s[i..i];
  1718     else s2 += sprintf( "\\%X", (int)s[i] );
  1724 mixed fix_charset ( string|mapping|array v )
  1726   if ( zero_type(v) ) return UNDEFINED;
  1727   if ( !objectp(charset_encoder) || !objectp(charset_decoder) ) return v;
  1729     if ( xml.utf8_check(v) ) return v;  // already utf-8
  1730     string tmp = charset_decoder->clear()->feed(v)->drain();
  1731     tmp = charset_encoder->clear()->feed(tmp)->drain();
  1732     // LDAP_LOG( "charset conversion: from \"%s\" to \"%s\".", v, tmp );
  1735   else if ( arrayp(v) ) {
  1737     foreach ( v, mixed i )
  1738       tmp += ({ fix_charset(i) });
  1741   else if ( mappingp(v) ) {
  1742     mapping tmp = ([ ]);
  1743     foreach ( indices(v), mixed i )
  1744       tmp += ([ fix_charset(i) : fix_charset(v[i]) ]);
  1747   else return UNDEFINED;
  1752  bool check_password(string pass, string user_pw)
  1754   if ( !stringp(pass) || !stringp(user_pw) )
  1757   LDAP_LOG("check_password()");
  1758   if ( strlen(user_pw) > 5 && lower_case(user_pw[0..4]) == "{sha}" )
  1759     return user_pw[5..] == MIME.encode_base64( sha_hash(pass) );
  1760   if ( strlen(user_pw) > 6 && lower_case(user_pw[0..5]) == "{ssha}" ) {
  1761     string salt = MIME.decode_base64( user_pw[6..] )[20..];  // last 8 bytes is the salt
  1762     return user_pw[6..] == MIME.encode_base64( sha_hash(pass+salt) );
  1766   if ( strlen(user_pw) > 7 && lower_case(user_pw[0..6]) == "{crypt}" )
  1767     return crypt(pass, user_pw[7..]);
  1768   if ( strlen(user_pw) > 4 && lower_case(user_pw[0..3]) == "{lm}" ) {
  1769     return user_pw[4..] == LanManHash.lanman_hash(pass);
  1771   if ( strlen(user_pw) < 3 || user_pw[0..2] != "$1$" ) 
  1772     return crypt(pass, user_pw); // normal crypt check
  1774   return verify_crypt_md5(pass, user_pw);
  1778 bool authorize_ldap ( object user, string pass )
  1780   return authenticate_user( user, pass );
  1784 bool authenticate_user ( object user, string pass )
  1787   int time_start = get_time_millis();
  1788   last_error = NO_ERROR;
  1789   if ( !ldap_activated() ) {
  1790     last_error = ERROR_NOT_CONNECTED;
  1794   if ( !objectp(user) )
  1795     steam_error("User object expected for authentication !");
  1797   if ( !stringp(pass) || sizeof(pass)<1 ) {
  1798     last_error = ERROR_UNKNOWN;
  1802   string uname = user->get_user_name();
  1804   // don't authenticate restricted users:
  1805   if ( _Persistence->user_restricted( uname ) ) return false;
  1807   string cached = authorize_cache->fetch( uname, pass );
  1809   if ( stringp(cached) && check_password( pass, cached ) ) {
  1810     // successfully authorized from cache
  1811     LDAP_LOG("user %s LDAP cache authorized", uname);
  1812     LOG_TIME( time_start, "authenticate_user[success]( %s )", uname );
  1813     bool log_auth = false;
  1814     if ( log_level_auth & LOG_AUTH_FAILURE ) {
  1815       if ( has_index( log_failed_auth, uname ) ) {
  1816         LOG_AUTH( "%s finally authorized by ldap cache after %d seconds "+
  1817           "(crc: %d)", uname, time()-log_failed_auth[uname], Gz.crc32(pass) );
  1818         m_delete( log_failed_auth, uname );
  1822     if ( log_level_auth & LOG_AUTH_SUCCESS ) {
  1823       LOG_AUTH( "%s authorized by ldap cache (crc: %d)",
  1824                 uname, Gz.crc32(pass) );
  1830   // failed to authorize from cache
  1831   if ( log_level_auth & LOG_AUTH_FAILURE ) {
  1832     if ( !has_index( log_failed_auth, uname ) ) {
  1833       log_failed_auth[ uname ] = time();
  1834       LOG_AUTH( "%s : %s failed", (ctime(time())-"\n"), uname );
  1836     LOG_AUTH( "%s not authorized by ldap cache (password check) (crc: %d)",
  1837             uname, Gz.crc32(pass) );
  1839   LDAP_LOG("user %s found in LDAP cache - password failed: %O",
  1841   LOG_TIME( time_start, "authenticate_user[failed]( %s )", uname );
  1847  string authenticate_user_internal ( string uname, string pass,
  1850   int time_start = get_time_millis();
  1852   // try authorizing via bind:
  1853   if ( Config.bool_value(config->bindUser) &&
  1854        stringp(uname) && sizeof(uname) > 0 &&
  1855        stringp(pass) && sizeof(pass) > 0) {
  1856     object ldap = connect( uname, pass );
  1857     if ( !objectp(ldap) ) {
  1858       if ( nr_try < NR_RETRIES ) {
  1859         disconnect( ldap, true );
  1860         return authenticate_user_internal( uname, pass, nr_try+1 );
  1863     if ( objectp(ldap) ) {
  1865       LDAP_LOG("authorized %s via bind", uname);
  1866       LOG_TIME( time_start, "authenticate_user[bind]( %s )", uname );
  1867       string cached_pw = make_crypt_md5( pass );
  1868       bool log_auth = false;
  1869       if ( (log_level_auth & LOG_AUTH_FAILURE) &&
  1870            has_index( log_failed_auth, uname ) ) {
  1871         LOG_AUTH( "%s finally authorized via ldap bind (after %d "+
  1872                 "seconds)", uname, time() - log_failed_auth[uname] );
  1875       if ( log_level_auth & LOG_AUTH_SUCCESS ) {
  1876         LOG_AUTH( "%s authorized via ldap bind", uname );
  1879       if ( log_auth && (log_level_auth & LOG_AUTH_DETAILS) )
  1880         LOG_AUTH( "%s password crc32 : %d", uname, Gz.crc32( pass ) );
  1884       LDAP_LOG("could not authorize %s via bind (no ldap connection)", uname);
  1885       if ( log_level_auth & LOG_AUTH_FAILURE ) {
  1886         LOG_AUTH( "%s not authorized via ldap bind", uname );
  1887         if ( log_level_auth & LOG_AUTH_DETAILS )
  1888           LOG_AUTH( "%s password crc32: %d", uname, Gz.crc32( pass ) );
  1893   // fetch user data and authorize via password:
  1894   mapping udata = fetch_user(uname, pass);
  1895   LDAP_LOG("trying to authorize user %s via password", uname);
  1897   LOG_TIME( time_start, "authenticate_user[fetch]( %s )", uname );
  1899   if ( mappingp(udata) ) {
  1900     object user = USER( uname );
  1901     string dn = udata["dn"];
  1902     if ( !stringp(dn) ) dn = "";
  1904     // check for conflicts (different user in sTeam than in LDAP):
  1905     if (config->checkConflicts && !stringp(user->query_attribute("ldap:dn"))) {
  1906       if ( search(ldap_conflicts,uname)<0 ) {
  1907    ldap_conflicts += ({ uname });
  1909        "Dear LDAP administrator at "+_Server->get_server_name()
  1910        +",\n\nthere has been a conflict between LDAP and sTeam:\n"
  1911        +"User \""+uname+"\" already exists in sTeam, but now "
  1912        +"there is also an LDAP user with the same name/id.\nYou "
  1913        +"will need to remove/rename one of them or, if they are "
  1914        +"the same user, you can overwrite the sTeam data from LDAP "
  1915        +"by adding a \"dn\" attribute to the sTeam user." ) );
  1917      FATAL( "ldap: user conflict: %s in sTeam vs. %s in LDAP\n", uname, dn );
  1921     else if ( search(ldap_conflicts,uname) >= 0 )
  1922       ldap_conflicts -= ({ uname });
  1924     string ldap_password = udata[config->passwordAttr] || "";
  1925     if ( stringp(config->passwordPrefix) && sizeof(config->passwordPrefix)>0 )
  1926       ldap_password = config->passwordPrefix + ldap_password;
  1928     if ( check_password( pass, ldap_password ) ) {
  1929       // need to synchronize passwords from ldap if ldap is down ?!
  1930       // this is only done when the ldap password is received
  1931       if ( ldap_password != user->get_user_password())
  1932    user->set_user_password(ldap_password, 1);
  1933       LDAP_LOG("user %s authorized via password from LDAP", uname);
  1934       string cached_pw = make_crypt_md5( pass );
  1935       bool log_auth = false;
  1936       if ( (log_level_auth & LOG_AUTH_FAILURE) &&
  1937            has_index( log_failed_auth, uname ) ) {
  1938         LOG_AUTH( "%s finally authorized via password (after %d seconds)",
  1939                   uname, time() - log_failed_auth[uname] );
  1942       if ( log_level_auth & LOG_AUTH_SUCCESS ) {
  1943         LOG_AUTH( "%s authorized via password", uname );
  1946       if ( log_auth && (log_level_auth & LOG_AUTH_DETAILS) )
  1947         LOG_AUTH( "%s password crc32: %d\n* ldap hash: %O",
  1948                 uname, Gz.crc32(pass), ldap_password );
  1952       if ( log_level_auth & LOG_AUTH_FAILURE ) {
  1953         LOG_AUTH( "%s not authorized via password", uname );
  1954         if ( log_level_auth & LOG_AUTH_DETAILS )
  1955           LOG_AUTH( "%s password crc32: %d\n* ldap hash: %O",
  1956                     uname, Gz.crc32(pass), ldap_password );
  1958       LDAP_LOG("user %s found in LDAP directory - password failed!", uname);
  1962   LDAP_LOG("user " + uname + " was not found in LDAP directory.");
  1963   // if notfound configuration is set to create, then we should create a user:
  1964   if ( config->notfound == "create" ) {
  1965     object user = USER( uname );
  1966     if ( add_user( uname, pass, user ) )
  1967       return make_crypt_md5( pass );
  1974 object sync_user(string name)
  1977   last_error = NO_ERROR;
  1978   if ( !ldap_activated() ) {
  1979     last_error = ERROR_NOT_CONNECTED;
  1983   // don't sync restricted users:
  1984   if ( _Persistence->user_restricted( name ) )
  1987   mapping udata = fetch_user( name );
  1988   if ( !mappingp(udata) )
  1991   LDAP_LOG("sync of ldap user \"%s\": %O", name, udata);
  1992   object user = get_module("users")->get_value(name);
  1993   string ldap_password = udata[config->passwordAttr];
  1994   if ( stringp(ldap_password) && stringp(config->passwordPrefix) &&
  1995        sizeof(config->passwordPrefix)>0 )
  1996     ldap_password = config->passwordPrefix + ldap_password;
  1997   if ( objectp(user) ) {
  1998     // update user date from LDAP
  1999     if ( ! user->set_attributes( ([
  2000              "pw" : ldap_password,
  2001         "email" : udata[config->emailAttr],
  2002         "fullname" : udata[config->fullnameAttr],
  2003         "firstname" : udata[config->nameAttr],
  2004         "OBJ_DESC" : udata[config->descriptionAttr],
  2006       FATAL( "LDAP: Could not sync user attributes with ldap for \"%s\".\n", name );
  2008     // create new user to match LDAP user
  2009     object factory = get_factory(CLASS_USER);
  2010     user = factory->execute( ([
  2012         "pw" : udata[config->passwordAttr],
  2013         "email" : udata[config->emailAttr],
  2014         "fullname" : udata[config->fullnameAttr],
  2015         "firstname" : udata[config->nameAttr],
  2016         "OBJ_DESC" : udata[config->descriptionAttr],
  2018     user->set_user_password( ldap_password, 1 );
  2019     user->activate_user( factory->get_activation() );
  2021   // sync group membership:
  2022   if ( objectp( user ) ) {
  2023     string primaryGroupId = udata[config->groupId];
  2024     if ( stringp( primaryGroupId ) ) {
  2025       mapping group = search_group("("+config->groupId+"="+primaryGroupId+")");
  2032 object sync_group(string name)
  2035   last_error = NO_ERROR;
  2036   if ( !ldap_activated() ) {
  2037     last_error = ERROR_NOT_CONNECTED;
  2041   // don't syncronize restricted groups:
  2042   if ( _Persistence->group_restricted( name ) )
  2045   mapping gdata = fetch_group( name );
  2046   if ( !mappingp(gdata) )
  2049   LDAP_LOG("sync of ldap group: %O", gdata);
  2050   //object group = get_module("groups")->lookup(name);
  2051   object group = get_module("groups")->get_value( name );
  2052   if ( objectp(group) ) {
  2053     // group memberships are handled by the persistence manager
  2054     // update group date from LDAP
  2055     group->set_attributes( ([
  2056         "OBJ_DESC" : gdata[config->descriptionAttr],
  2059     // create new group to match LDAP group
  2060     object factory = get_factory(CLASS_GROUP);
  2061     group = factory->execute( ([
  2063          "OBJ_DESC" : gdata[config->descriptionAttr],
  2070  void sync_password(int event, object user, object caller)
  2072   if ( !Config.bool_value(config->sync) )
  2074   string oldpw = user->get_old_password();
  2075   string crypted = user->get_user_password();
  2076   string name = user->get_user_name();
  2077   // don't sync password for restricted users:
  2078   if ( _Persistence->group_restricted( name ) ) return;
  2079   LDAP_LOG("password sync for " + user->get_user_name());
  2084   if ( Config.bool_value(config->bindUser) && oldpw &&
  2085        objectp(ldap = connect( name, oldpw )) ) {
  2086     if ( config->userdn )
  2087       dn = make_dn(config->userAttr+"="+name, config->userdn, config->base_dc);
  2089       dn = make_dn(config->userAttr+"="+name, config->base_dc);
  2091   else if ( ldap = connect() ) {
  2092     dn = make_dn( config->base_dc, config->userAttr+"="+name );
  2095   if ( !stringp(dn) ) {
  2096     LDAP_LOG("sync_password(): no dn");
  2102   if ( crypted[..2] == "$1$" )
  2103     crypted = "{crypt}" + crypted;
  2104   err = catch( ldap->modify(dn, ([ config->passwordAttr: ({ 2,crypted }),])) );
  2105   authorize_cache->drop( name );
  2106   user_cache->drop( name );
  2107   LDAP_LOG("sync_password(): %s - %s - %O\n", crypted, dn, ldap->error_string());
  2114  void sync_ticket(int event, object user, object caller, string ticket)
  2116   last_error = NO_ERROR;
  2118   if ( !ldap_activated() ) {
  2119     last_error = ERROR_NOT_CONNECTED;
  2122   if ( !Config.bool_value(config->sync) )
  2124   string name = user->get_user_name();
  2125   string dn = make_dn( config->base_dc, config->userAttr + "=" + name );
  2126   object ldap = connect();
  2127   err = catch( ldap->modify(dn, ([ "userCertificate": ({ 2,ticket }),])) );
  2133 bool is_user(string user)
  2136   last_error = NO_ERROR;
  2137   object ldap = connect();
  2138   if ( !objectp(ldap) ) {
  2139     last_error = ERROR_NOT_CONNECTED;
  2142   object results = ldap->search( "("+config["userAttr"]+"="+user+")" );
  2143   bool result = (objectp(results) && results->num_entries() > 0);
  2149  bool add_user(string name, string password, object user)
  2151   last_error = NO_ERROR;
  2152   if ( !ldap_activated() ) {
  2153     last_error = ERROR_NOT_CONNECTED;
  2156   if ( !Config.bool_value(config->sync) ) return false;
  2157   if ( !stringp(config->notfound) || lower_case(config->notfound) != "create" )
  2160   // don't add restricted users:
  2161   if ( _Persistence->user_restricted( name ) ) return false;
  2163   string fullname = user->get_name();
  2164   string firstname = user->query_attribute(USER_FIRSTNAME);
  2165   string email = user->query_attribute(USER_EMAIL);
  2167   mapping attributes = ([
  2168     config["userAttr"]: ({ name }),
  2169     config["fullnameAttr"]: ({ fullname }),
  2170     "objectClass": ({ config["objectName"] }),
  2171     config["passwordAttr"]: ({ make_crypt_md5(password) }),
  2173   if ( stringp(firstname) && strlen(firstname) > 0 )
  2174     config["nameAttr"] = ({ firstname });
  2175   if ( stringp(email) && strlen(email) > 0 )
  2176     config["emailAttr"] = ({ email });
  2178   array requiredAttributes =  config["requiredAttr"];
  2180   if ( arrayp(requiredAttributes) && sizeof(requiredAttributes) > 0 ) {
  2181     foreach(requiredAttributes, string attr) {
  2182       if ( zero_type(attributes[attr]) )
  2183    attributes[attr] = ({ "-" });
  2187   object ldap = connect();
  2188   if ( !objectp(ldap) ) {
  2189     last_error = ERROR_NOT_CONNECTED;
  2192   ldap->add( make_dn(config["userAttr"]+"="+name, config["base_dc"]),
  2194   int err = ldap->error_number();
  2196     FATAL( "Failed to add user , error is " + ldap->error_string() );
  2197   bool result = ldap->error_number() == 0;
  2206  * Returns the minimum number of milliseconds a connection request must take
  2207  * to appear in the log. Note: if logging is switched off, then this will be
  2208  * the maximum value an integer can take.
  2210  * @return the minimum threshold (in milliseconds) for connection times logging
  2212 int get_log_connection_times_min () {
  2213   return log_connection_times_min;
  2218  * Returns the maximum number of milliseconds a connection request must take
  2219  * to appear in the log. Note: if no upper threshold is used, then this will be
  2220  * the maximum value an integer can take.
  2222  * @return the maximum threshold (in milliseconds) for connection times logging
  2224 int get_log_connection_times_max () {
  2225   return log_connection_times_max;
  2230  * Activate or deactivate connection times logging and set the threshold for
  2233  * @param min_milliseconds the minimum number of milliseconds a connection
  2234  *   request must take to appear in the log (if -1, then logging will be
  2236  * @param max_milliseconds the maximum number of milliseconds a connection
  2237  *   request must take to appear in the log (if -1, then no upper limit will
  2239  * @return true if logging is now active, false if it is now not active
  2241 bool set_log_connection_times ( int min_milliseconds, void|int max_milliseconds ) {
  2242   if ( min_milliseconds < 0 )
  2243     log_connection_times_min = Int.NATIVE_MAX;
  2245     log_connection_times_min = min_milliseconds;
  2246   if ( zero_type(max_milliseconds) || max_milliseconds < 0 )
  2247     log_connection_times_max = Int.NATIVE_MAX;
  2249     log_connection_times_max = max_milliseconds;
  2250   return log_connection_times_min < Int.NATIVE_MAX &&
  2251     log_connection_times_max >= log_connection_times_min;
  2256  int get_time_millis () {
  2257   array tod = System.gettimeofday();
  2258   return tod[0]*1000 + tod[1]/1000;
  2265  * Set the log level for authentication logging during runtime.
  2266  * The log levels are:
  2268  * 1 : failed authentication,
  2269  * 2 : successful authentication,
  2270  * 3 : failed and successful authentication,
  2271  * 5 : detailed failed authentication (with password hashes in the log),
  2272  * 6 : detailed successful authentication (with password hashes in the log),
  2273  * 7 : detailed failed and successful authentication (with password hashes in
  2276  * @param log_level authentication log level
  2277  * @return the new log level
  2279 int set_authorize_log_level ( int log_level ) {
  2280   log_level_auth = log_level;
  2281   if ( log_level_auth < 1 ) log_failed_auth = ([ ]);
  2282   return log_level_auth;
  2287  * Query the authentication log level
  2288  * The log levels are:
  2290  * 1 : failed authentication,
  2291  * 2 : successful authentication,
  2292  * 3 : failed and successful authentication,
  2293  * 5 : detailed failed authentication (with password hashes in the log),
  2294  * 6 : detailed successful authentication (with password hashes in the log),
  2295  * 7 : detailed failed and successful authentication (with password hashes in
  2298  * @return the authentication log level
  2300 int get_authorize_log_level () {
  2301   return log_level_auth;
  2306 void synchronization_thread () {
  2308     int cache_time = config["cacheTime"];
  2309     int cache_live_time = config["cacheTime"] * 2;  //TODO: make this configurable
  2310     foreach ( indices( user_cache ), string identifier ) {
  2311       mapping entry = user_cache[ identifier ];
  2313       if ( t - entry->last_accessed > cache_live_time ) {
  2317       if ( t - entry->last_synchronized > cache_time ) {
  2321     foreach ( indices( group_cache ), string identifier ) {
  2323     foreach ( indices( authorize_cache ), string identifier ) {
  2325     sleep( cacheTime ); //TODO: substract the time needed for sync