Add second patch from Novell for creating a postauth method in order to
authorkkalev <kkalev>
Fri, 28 Jan 2005 07:21:43 +0000 (07:21 +0000)
committerkkalev <kkalev>
Fri, 28 Jan 2005 07:21:43 +0000 (07:21 +0000)
implement the Novell eDirectory account policy check

INSTALL
doc/ldap_howto.txt
doc/rlm_ldap
raddb/radiusd.conf.in
src/modules/rlm_ldap/rlm_ldap.c

diff --git a/INSTALL b/INSTALL
index 647c57e..8c59af0 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -47,6 +47,8 @@ following list is a selection from the available flags:
   --with-experimental-modules      Use experimental and unstable modules. (default=no) 
   --enable-developer      Turns on super-duper-extra-compile-warnings
                               when using gcc.
+  --with-edir             Compile with support for Novell eDirectory
+                              integration.
 
   To get the defaults that Cistron Radius used up to 1.5.4.3-beta18, use:
 
index 9ea421f..f53804c 100644 (file)
@@ -985,7 +985,15 @@ ldap {
   ldap_cache_size = 0
   ldap_connections_number = 10
   #password_header = {clear}
+#While integrating FreeRADIUS with Novell eDirectory, set
+#'password_attribute = nspmpassword' in order to use the universal password
+#of the eDirectory users for RADIUS authentication. This will work only if
+#FreeRADIUS is configured to build with --with-edir option.
   password_attribute = userPassword
+#Comment out the following to disable the eDirectory account policy check and
+#intruder detection. This will work only if FreeRADIUS is configured to build
+#with --with-edir option.
+#edir_account_policy_check=no
   groupname_attribute = radiusGroupName
   groupmembership_filter = (&(uid=%{Stripped-User-Name:-%{User-Name}})
   (objectclass=radiusprofile))
index 79c9d0a..5c870a3 100644 (file)
@@ -178,11 +178,24 @@ the rlm_ldap module:
 
 #      password_attribute: Define the attribute which contains the user
 #      password.
+#      While integrating FreeRADIUS with Novell eDirectory, set
+#      'password_attribute = nspmpassword' in order to use the universal
+#      password of the eDirectory users for RADIUS authentication. This will
+#      work only if FreeRADIUS is configured to build with --with-edir option.
 #
 #      default: NULL - don't add password
 #
 #      password_attribute = "userPassword"
 
+#      edir_account_policy_check: Specifies if the module has to enforce
+#      Novell eDirectory account policy check and intruder detection for
+#      RADIUS users.  This will work only if FreeRADIUS is configured to build
+#      with --with-edir option.
+#
+#      default: yes - eDirectory account policy check enabled
+#
+#      edir_account_policy_check = no
+
 #      groupname_attribute: The attribute containing group name in the LDAP
 #      server. It is used to search group by name.
 #
index 5c97c9f..4715913 100644 (file)
@@ -803,6 +803,13 @@ $INCLUDE ${confdir}/eap.conf
                # freeRADIUS is configured to build with --with-edir option.
                #                                                                               
                # password_attribute = userPassword
+               #
+               # Un-comment the following to disable Novell eDirectory account
+               # policy check and intruder detection. This will work *only if*
+               # FreeRADIUS is configured to build with --with-edir option.
+               #
+               # edir_account_policy_check=no
+               #
                # groupname_attribute = cn
                # groupmembership_filter = "(|(&(objectClass=GroupOfNames)(member=%{Ldap-UserDn}))(&(objectClass=GroupOfUniqueNames)(uniquemember=%{Ldap-UserDn})))"
                # groupmembership_attribute = radiusGroupName
@@ -1947,8 +1954,17 @@ post-auth {
 #      sql
 
        #
-       #  Access-Reject packets are sent through the REJECT sub-section
-       #  of the post-auth section.
+       #  Un-comment the following if you have set
+       #  'edir_account_policy_check = yes' in the ldap module sub-section of
+       #  the 'modules' section.
+       #
+#      ldap
+       #
+       #  Access-Reject packets are sent through the REJECT sub-section of the
+       #  post-auth section.
+       #  Uncomment the following and set the module name to the ldap instance
+       #  name if you have set 'edir_account_policy_check = yes' in the ldap
+       #  module sub-section of the 'modules' section.
        #
 #      Post-Auth-Type REJECT {
 #              insert-module-name-here
index fbe2668..ff02c91 100644 (file)
@@ -155,6 +155,9 @@ typedef struct {
        TLDAP_RADIUS   *check_item_map;
        TLDAP_RADIUS   *reply_item_map;
        LDAP_CONN       *conns;
+#ifdef NOVELL
+       LDAP_CONN *apc_conns;
+#endif
        int             ldap_debug; /* Debug flag for LDAP SDK */
        char            *xlat_name; /* name used to xlat */
        char            *tls_cacertfile;
@@ -163,6 +166,9 @@ typedef struct {
        char            *tls_keyfile;
        char            *tls_randfile;
        char            *tls_require_cert;
+#ifdef NOVELL
+       int                     edir_account_policy_check;
+#endif
 }  ldap_instance;
 
 /* The default setting for TLS Certificate Verification */
@@ -206,6 +212,9 @@ static CONF_PARSER module_config[] = {
        {"compare_check_items", PW_TYPE_BOOLEAN, offsetof(ldap_instance,do_comp), NULL, "no"},
        {"access_attr_used_for_allow", PW_TYPE_BOOLEAN, offsetof(ldap_instance,default_allow), NULL, "yes"},
        {"do_xlat", PW_TYPE_BOOLEAN, offsetof(ldap_instance,do_xlat), NULL, "yes"},
+#ifdef NOVELL
+       {"edir_account_policy_check", PW_TYPE_BOOLEAN, offsetof(ldap_instance,edir_account_policy_check), NULL, "yes"},
+#endif
 
        {NULL, -1, 0, NULL, NULL}
 };
@@ -220,7 +229,7 @@ static void     fieldcpy(char *, char **);
 static VALUE_PAIR *ldap_pairget(LDAP *, LDAPMessage *, TLDAP_RADIUS *,VALUE_PAIR **,char);
 static int ldap_groupcmp(void *, REQUEST *, VALUE_PAIR *, VALUE_PAIR *, VALUE_PAIR *, VALUE_PAIR **);
 static int ldap_xlat(void *,REQUEST *, char *, char *,int, RADIUS_ESCAPE_STRING);
-static LDAP    *ldap_connect(void *instance, const char *, const char *, int, int *);
+static LDAP    *ldap_connect(void *instance, const char *, const char *, int, int *, char **);
 static int     read_mappings(ldap_instance* inst);
 
 static inline int ldap_get_conn(LDAP_CONN *conns,LDAP_CONN **ret,void *instance)
@@ -343,6 +352,22 @@ ldap_instantiate(CONF_SECTION * conf, void **instance)
        DEBUG("rlm_ldap: Registering ldap_xlat with xlat_name %s",xlat_name);
        xlat_register(xlat_name,ldap_xlat,inst);
 
+#ifdef NOVELL
+       /*
+        * (LDAP_Instance, V1) attribute-value pair in the config items list means
+        * that the 'authorize' method of the instance 'V1' of the LDAP module has
+        * processed this request.
+        */
+       dict_addattr("LDAP-Instance", 0, PW_TYPE_STRING, -1, flags);
+       /*
+        * ('eDir-APC', '1') in config items list => Do not perform eDirectory account
+        *                                           policy check (APC)
+        * ('eDir-APC', '2') in config items list => Perform eDirectory APC
+        * ('eDir-APC', '3') in config items list => eDirectory APC has been completed
+        */
+       dict_addattr("eDir-APC", 0, PW_TYPE_INTEGER, -1, flags);
+#endif
+
        if (inst->num_conns <= 0){
                radlog(L_ERR, "rlm_ldap: Invalid ldap connections number passed.");
                free(inst);
@@ -362,6 +387,28 @@ ldap_instantiate(CONF_SECTION * conf, void **instance)
                pthread_mutex_init(&inst->conns[i].mutex, NULL);
        }
 
+#ifdef NOVELL
+       /*
+        * 'inst->apc_conns' is a separate connection pool to be used for performing
+        * eDirectory account policy check in the 'postauth' method. This avoids
+        * changing the (RADIUS server) credentials associated with the 'inst->conns'
+        * connection pool.
+        */
+       inst->apc_conns = (LDAP_CONN *)malloc(sizeof(LDAP_CONN)*inst->num_conns);
+       if (inst->apc_conns == NULL){
+               radlog(L_ERR, "rlm_ldap: Could not allocate memory. Aborting.");
+               free(inst);
+               return -1;
+       }
+       for(i = 0; i < inst->num_conns; i++){
+               inst->apc_conns[i].bound = 0;
+               inst->apc_conns[i].locked = 0;
+               inst->apc_conns[i].failed_conns = 0;
+               inst->apc_conns[i].ld = NULL;
+               pthread_mutex_init(&inst->apc_conns[i].mutex, NULL);
+       }
+#endif
+
        if (read_mappings(inst) != 0) {
                radlog(L_ERR, "rlm_ldap: Reading dictionary mappings from file %s failed",
                       inst->dictionary_mapping);
@@ -593,7 +640,7 @@ retry:
                        ldap_unbind_s(conn->ld);
                }
                if ((conn->ld = ldap_connect(instance, inst->login,
-                                            inst->password, 0, &res)) == NULL) {
+                                            inst->password, 0, &res, NULL)) == NULL) {
                        radlog(L_ERR, "rlm_ldap: (re)connection attempt failed");
                        if (search_retry == 0)
                                conn->failed_conns++;
@@ -1309,6 +1356,63 @@ ldap_authorize(void *instance, REQUEST * request)
                                                strncpy(passwd_item->strvalue,passwd_val,MAX_STRING_LEN - 1);
                                                passwd_item->length = (passwd_len > (MAX_STRING_LEN - 1)) ? (MAX_STRING_LEN - 1) : passwd_len;
                                                pairadd(&request->config_items,passwd_item);
+
+#ifdef NOVELL
+                                               {
+                                                       DICT_ATTR *dattr;
+                                                       VALUE_PAIR      *vp_inst, *vp_apc;
+                                                       int inst_attr, apc_attr;
+
+                                                       dattr = dict_attrbyname("LDAP-Instance");
+                                                       inst_attr = dattr->attr;
+                                                       dattr = dict_attrbyname("eDir-APC");
+                                                       apc_attr = dattr->attr;
+
+                                                       vp_inst = pairfind(request->config_items, inst_attr);
+                                                       if(vp_inst == NULL){
+                                                               /*
+                                                                * The authorize method of no other LDAP module instance has
+                                                                * processed this request.
+                                                                */
+                                                               if ((vp_inst = paircreate(inst_attr, PW_TYPE_STRING)) == NULL){
+                                                                       radlog(L_ERR, "rlm_ldap: Could not allocate memory. Aborting.");
+                                                                       ldap_msgfree(result);
+                                                                       ldap_release_conn(conn_id, inst->conns);
+                                                                       memset(universal_password, 0, universal_password_len);
+                                                                       free(universal_password);
+                                                                       return RLM_MODULE_FAIL;
+                                                               }
+                                                               strcpy(vp_inst->strvalue, inst->xlat_name);
+                                                               vp_inst->length = strlen(vp_inst->strvalue);
+                                                               pairadd(&request->config_items, vp_inst);
+
+                                                               /*
+                                                                * Inform the authenticate / post-auth method about the presence
+                                                                * of UP in the config items list and whether eDirectory account
+                                                                * policy check is to be performed or not.
+                                                                */
+                                                               if ((vp_apc = paircreate(apc_attr, PW_TYPE_INTEGER)) == NULL){
+                                                                       radlog(L_ERR, "rlm_ldap: Could not allocate memory. Aborting.");
+                                                                       ldap_msgfree(result);
+                                                                       ldap_release_conn(conn_id, inst->conns);
+                                                                       memset(universal_password, 0, universal_password_len);
+                                                                       free(universal_password);
+                                                                       return RLM_MODULE_FAIL;
+                                                               }
+
+                                                               if(!inst->edir_account_policy_check){
+                                                                       /* Do nothing */
+                                                                       strcpy(vp_apc->strvalue, "1");
+                                                               }else{
+                                                                       /* Perform eDirectory account-policy check */
+                                                                       strcpy(vp_apc->strvalue, "2");
+                                                               }
+                                                               vp_apc->length = 1;
+                                                               pairadd(&request->config_items, vp_apc);
+                                                       }
+                                               }
+#endif
+
                                                DEBUG("rlm_ldap: Added the eDirectory password in check items");
                                        }
                                }
@@ -1350,6 +1454,20 @@ ldap_authorize(void *instance, REQUEST * request)
        }
 
        if (inst->do_comp && paircmp(request,request->packet->vps,*check_pairs,reply_pairs) != 0){
+#ifdef NOVELL
+               /* Don't perform eDirectory APC if RADIUS authorize fails */
+               int apc_attr;
+               VALUE_PAIR *vp_apc;
+               DICT_ATTR *dattr;
+
+               dattr = dict_attrbyname("eDir-APC");
+               apc_attr = dattr->attr;
+
+               vp_apc = pairfind(request->config_items, apc_attr);
+               if(vp_apc)
+                       vp_apc->strvalue[0] = '1';
+#endif
+
                DEBUG("rlm_ldap: Pairs do not match. Rejecting user.");
                snprintf(module_fmsg,sizeof(module_fmsg),"rlm_ldap: Pairs do not match");
                module_fmsg_vp = pairmake("Module-Failure-Message", module_fmsg, T_OP_EQ);
@@ -1391,7 +1509,7 @@ ldap_authenticate(void *instance, REQUEST * request)
        LDAP           *ld_user;
        LDAPMessage    *result, *msg;
        ldap_instance  *inst = instance;
-       char           *user_dn, *attrs[] = {"uid", NULL};
+       char           *user_dn, *attrs[] = {"uid", NULL}, *err = NULL;
        char            filter[MAX_FILTER_STR_LEN];
        char            basedn[MAX_FILTER_STR_LEN];
        int             res;
@@ -1496,8 +1614,35 @@ ldap_authenticate(void *instance, REQUEST * request)
 
        DEBUG("rlm_ldap: user DN: %s", user_dn);
 
+#ifndef NOVELL
+       ld_user = ldap_connect(instance, user_dn, request->password->strvalue,
+                              1, &res, NULL);
+#else
+       
        ld_user = ldap_connect(instance, user_dn, request->password->strvalue,
-                              1, &res);
+                       1, &res, &err);
+
+       if(err != NULL){
+               /* 'err' contains the LDAP connection error description */
+               DEBUG("rlm_ldap: %s", err);
+               pairadd(&request->reply->vps, pairmake("Reply-Message", err, T_OP_EQ));
+               ldap_memfree((void *)err);
+       }
+
+       /* Don't perform eDirectory APC again after attempting to bind here. */
+       {
+               int apc_attr;
+               DICT_ATTR *dattr;
+               VALUE_PAIR *vp_apc;
+
+               dattr = dict_attrbyname("eDir-APC");
+               apc_attr = dattr->attr;
+               vp_apc = pairfind(request->config_items, apc_attr);
+               if(vp_apc && vp_apc->strvalue[0] == '2')
+                       vp_apc->strvalue[0] = '3';
+       }
+#endif
+
        if (ld_user == NULL){
                if (res == RLM_MODULE_REJECT){
                        inst->failed_conns = 0;
@@ -1520,8 +1665,151 @@ ldap_authenticate(void *instance, REQUEST * request)
        return RLM_MODULE_OK;
 }
 
+#ifdef NOVELL
+/*****************************************************************************
+ *
+ *     Function: rlm_ldap_postauth
+ *
+ *     Purpose: Perform eDirectory account policy check and failed-login reporting
+ *     to eDirectory.
+ *
+ *****************************************************************************/
+static int
+ldap_postauth(void *instance, REQUEST * request)
+{
+       int res = RLM_MODULE_FAIL;
+       int inst_attr, apc_attr;
+       char password[UNIVERSAL_PASS_LEN];
+       ldap_instance  *inst = instance;
+       LDAP_CONN       *conn;
+       VALUE_PAIR *vp_inst, *vp_apc;
+       DICT_ATTR *dattr;
+
+       dattr = dict_attrbyname("LDAP-Instance");
+       inst_attr = dattr->attr;
+       dattr = dict_attrbyname("eDir-APC");
+       apc_attr = dattr->attr;
+
+       vp_inst = pairfind(request->config_items, inst_attr);
+
+       /*
+        * Check if the password in the config items list is the user's UP which has
+        * been read in the authorize method of this instance of the LDAP module.
+        */
+       if((vp_inst == NULL) || strcmp(vp_inst->strvalue, inst->xlat_name))
+               return RLM_MODULE_NOOP;
+
+       vp_apc = pairfind(request->config_items, apc_attr);
+
+       switch(vp_apc->strvalue[0]){
+               case '1':
+                       /* Account policy check not enabled */
+               case '3':
+                       /* Account policy check has been completed */
+                       res = RLM_MODULE_NOOP;
+                       break;
+               case '2':
+                       {
+                               int err, conn_id = -1, i;
+                               char *error_msg = NULL;
+                               LDAP *ld;
+                               VALUE_PAIR *vp_fdn, *vp_pwd;
+                               DICT_ATTR *da;
+                               ldap_instance   *inst = instance;
+                               LDAP_CONN       *conn;
+
+                               if(request->reply->code == PW_AUTHENTICATION_REJECT){
+                                       /* Bind to eDirectory as the RADIUS user with a wrong password. */
+                                       vp_pwd = pairfind(request->config_items, PW_PASSWORD);
+                                       strcpy(password, vp_pwd->strvalue);
+                                       if(strlen(password) > 0){
+                                               if(password[0] != 'a'){
+                                                       password[0] = 'a';
+                                               }else{
+                                                       password[0] = 'b';
+                                               }
+                                       }else{
+                                               strcpy(password, "dummy_password");
+                                       }
+                                       res = RLM_MODULE_REJECT;
+                               }else{
+                                       /* Bind to eDirectory as the RADIUS user using the user's UP */
+                                       vp_pwd = pairfind(request->config_items, PW_PASSWORD);
+                                       if(vp_pwd == NULL){
+                                               DEBUG("rlm_ldap: User's Universal Password not in config items list.");
+                                               return RLM_MODULE_FAIL;
+                                       }
+                                       strcpy(password, vp_pwd->strvalue);
+                               }
+
+                               if ((da = dict_attrbyname("Ldap-UserDn")) == NULL) {
+                                       DEBUG("rlm_ldap: Attribute for user FDN not found in dictionary. Unable to proceed");
+                                       return RLM_MODULE_FAIL;
+                               }
+
+                               vp_fdn = pairfind(request->packet->vps, da->attr);
+                               if(vp_fdn == NULL){
+                                       DEBUG("rlm_ldap: User's FQDN not in config items list.");
+                                       return RLM_MODULE_FAIL;
+                               }
+
+                               if ((conn_id = ldap_get_conn(inst->apc_conns, &conn, inst)) == -1){
+                                       radlog(L_ERR, "rlm_ldap: All ldap connections are in use");
+                                       return RLM_MODULE_FAIL;
+                               }
+
+                               /*
+                                * If there is an existing LDAP connection to the directory, bind over
+                                * it. Otherwise, establish a new connection.
+                                */
+postauth_reconnect:
+                               if (!conn->bound || conn->ld == NULL) {
+                                       DEBUG2("rlm_ldap: attempting LDAP reconnection");
+                                       if (conn->ld){
+                                               DEBUG2("rlm_ldap: closing existing LDAP connection");
+                                               ldap_unbind_s(conn->ld);
+                                       }
+                                       if ((conn->ld = ldap_connect(instance, (char *)vp_fdn->strvalue, password, 0, &res, &error_msg)) == NULL) {
+                                               radlog(L_ERR, "rlm_ldap: eDirectory account policy check failed.");
+
+                                               if(error_msg != NULL){
+                                                       DEBUG("rlm_ldap: %s", error_msg);
+                                                       pairadd(&request->reply->vps, pairmake("Reply-Message", error_msg, T_OP_EQ));
+                                                       ldap_memfree((void *)error_msg);
+                                               }
+
+                                               vp_apc->strvalue[0] = '3';
+                                               ldap_release_conn(conn_id, inst->apc_conns);
+                                               return RLM_MODULE_REJECT;
+                                       }
+                                       conn->bound = 1;
+                               }else if((err = ldap_simple_bind_s(conn->ld, (char *)vp_fdn->strvalue, password)) != LDAP_SUCCESS){
+                                       if(err == LDAP_SERVER_DOWN){
+                                               conn->bound = 0;
+                                               goto postauth_reconnect;
+                                       }
+                                       DEBUG("rlm_ldap: eDirectory account policy check failed.");
+                                       ldap_get_option(conn->ld, LDAP_OPT_ERROR_STRING, &error_msg);
+                                       if(error_msg != NULL){
+                                               DEBUG("rlm_ldap: %s", error_msg);
+                                               pairadd(&request->reply->vps, pairmake("Reply-Message", error_msg, T_OP_EQ));
+                                               ldap_memfree((void *)error_msg);
+                                       }
+                                       vp_apc->strvalue[0] = '3';
+                                       ldap_release_conn(conn_id, inst->apc_conns);
+                                       return RLM_MODULE_REJECT;
+                               }
+                               vp_apc->strvalue[0] = '3';
+                               ldap_release_conn(conn_id, inst->apc_conns);
+                               return RLM_MODULE_OK;
+                       }
+       }
+       return res;
+}
+#endif
+
 static LDAP    *
-ldap_connect(void *instance, const char *dn, const char *password, int auth, int *result)
+ldap_connect(void *instance, const char *dn, const char *password, int auth, int *result, char **err)
 {
        ldap_instance  *inst = instance;
        LDAP           *ld = NULL;
@@ -1677,6 +1965,9 @@ ldap_connect(void *instance, const char *dn, const char *password, int auth, int
        msgid = ldap_bind(ld, dn, password,LDAP_AUTH_SIMPLE);
        if (msgid == -1) {
                ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &ldap_errno);
+               if(err != NULL){
+                       ldap_get_option(ld, LDAP_OPT_ERROR_STRING, err);
+               }
                if (inst->is_url) {
                        radlog(L_ERR, "rlm_ldap: %s bind to %s failed: %s",
                                dn, inst->server, ldap_err2string(ldap_errno));
@@ -1696,6 +1987,9 @@ ldap_connect(void *instance, const char *dn, const char *password, int auth, int
        if (rc < 1) {
                DEBUG("rlm_ldap: ldap_result()");
                ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &ldap_errno);
+               if(err != NULL){
+                       ldap_get_option(ld, LDAP_OPT_ERROR_STRING, err);
+               }
                if (inst->is_url) {
                        radlog(L_ERR, "rlm_ldap: %s bind to %s failed: %s",
                                dn, inst->server, (rc == 0) ? "timeout" : ldap_err2string(ldap_errno));
@@ -1724,6 +2018,9 @@ ldap_connect(void *instance, const char *dn, const char *password, int auth, int
                        radlog(L_ERR, "rlm_ldap: LDAP login failed: check identity, password settings in ldap section of radiusd.conf");
                        *result = RLM_MODULE_FAIL;
                }
+               if(err != NULL){
+                       ldap_get_option(ld, LDAP_OPT_ERROR_STRING, err);
+               }
                break;
 
        default:
@@ -1736,6 +2033,9 @@ ldap_connect(void *instance, const char *dn, const char *password, int auth, int
                                ldap_err2string(ldap_errno));
                }
                *result = RLM_MODULE_FAIL;
+               if(err != NULL){
+                       ldap_get_option(ld, LDAP_OPT_ERROR_STRING, err);
+               }
        }
 
        if (*result != RLM_MODULE_OK) {
@@ -1797,6 +2097,20 @@ ldap_detach(void *instance)
                free(inst->conns);
        }
 
+#ifdef NOVELL
+       if (inst->apc_conns){ 
+               int i;
+
+               for(i = 0; i < inst->num_conns; i++){
+                       if (inst->apc_conns[i].ld){
+                               ldap_unbind_s(inst->apc_conns[i].ld);
+                       }
+                       pthread_mutex_destroy(&inst->apc_conns[i].mutex);
+               }
+               free(inst->apc_conns);
+       }
+#endif
+
        pair = inst->check_item_map;
 
        while (pair != NULL) {
@@ -1963,7 +2277,11 @@ module_t        rlm_ldap = {
                NULL,                   /* checksimul            */
                NULL,                   /* pre-proxy             */
                NULL,                   /* post-proxy            */
-               NULL                    /* post-auth             */
+#ifdef NOVELL
+               ldap_postauth                   /* post-auth             */
+#else
+               NULL
+#endif
        },
        ldap_detach,            /* detach                */
        NULL,                   /* destroy               */