LDAP support in KCML
LDAP or Lightweight Directory Access Protocol is an IETF standard for applications to access a directory across a network. These directories can hold information about people, network resources and other enterprise data. Examples of LDAP directory products are Microsoft Acrive Directory, the Novell eDirectory, Oracle's Internet Directory and OpenLDAP.
An LDAP directory is organized as a tree with the tagged with attributes which are defined in schemas. Each tree node has a Distinguished Name or DN which uniquely identifies it. A number of public schemas exist for managing people and resources and these can be extended locally if required.
LDAP for authentication
LDAP can be used by SSH, telnet and KCML Connection Manager to authenticate users from a central directory using PAM and the pam_ldap module available for Linux and most modern Unix variants. Multiple machines can shared a common directory of users. This means that each user need only have one centrally stored password to access all the machines configured this way. To control access, the user must exist in the local /etc/passwd file and the LDAP server supplies just the password. Generally it will check the local password file first then go out to the LDAP server if incorrect.
To configure this on Linux the system administrators should run authconfig and on the page about user authentication select LDAP and enter the LDAP server hostname (e.g. ldap.kerridge.com) and the base DN for the start of the people entries e.g. (ou=People,dc=kerrridge,dc=com). Don't enable TLS unless you know the ldap server supports SSL. If there are multiple, replicated ldap servers in your organization you can enter serveral space separated hostnames or IP addresses.
The connection manager also supports LDAP authentication directly, with some limitations. This is most useful for authenticating against Microsoft Active Directory which, by default, has a non-standard schema that will not support Unix/Posix authentication. See here for more details.
LDAP in KCML applications
KCML has support for accessing an LDAP directory through a built in dynamic object that is instantiated using CREATE. Microsoft Windows supports LDAP clients in Windows 2000 and later version though a patch is also available for Windows 98. On Linux, KCML requires OpenLDAP 2.3 to be installed. The libraries are loaded dynamically at CREATE time and it will fail if they are not supported on that system.
KCML has wrapped the original Netscape SDK API, as standardized in RFC1823, in a simple object wrapper described here. The simple_bind_s() method establishes a connection to the named server specified in CREATE. This can be anonomous or you can pass a userid and password. To perform a search use search_s() specifying the DN to start, whether you want to search below that point in the tree, a filter to define the search, an optional timeout, an optional maximum result size and a list of the attributes required from the search. The result object will contain a number of entries that match. These can be iterated with a FOR OBJECT loop. Each entry will have one or more attributes, available as a collection object and each attributes can have one or more values, again available as a collection object.
There are some limitations in this implementation
Here is an example program that gets some attributes about people from a conventional LDAP directory implementing the inetOrgPerson schema.
// Example of searching an LDAP directory DIM OBJECT ld, nErr, OBJECT res, nStatus, nTimeout, nMaxHits, n, OBJECT e, OBJECT attr, OBJECT a, OBJECT v, i // load library and define the server(s) to use DIM sLDAPserver$="ldap.kerridge.com" OBJECT ld = CREATE "dynamic", "LDAP", sLDAPserver$ // anon bind using simple authentication nErr = ld.simple_bind_s() 'CheckError(nErr) // List attributes required in the result, pass a NULL object for all of them OBJECT attr = ld.CreateArray() attr.Add("uid", "cn", "telePhoneNumber") // where to start searching, KCML defines constants for the scope DIM sBaseDN$="ou=People,dc=kerridge,dc=com" // a filter to select nodes DIM sFilter$="(givenName=Peter)" // The timeout is in ms nTimeout = 5000 // the maximum number of hits required nMaxHits = 5 // make search OBJECT res = ld.search_s(sBaseDN$, _LDAP_SCOPE_SUBTREE, sFilter$, OBJECT attr, FALSE, nTimeout, nMaxHits, BYREF nStatus) // The status should be zero for success or else it is an LDAP error code // It can be set for partial searches even though a result object is returned PRINT $PRINTF("Search returned status %d and the result had %d entries", nStatus, res.Count) OBJECT attr = NULL // iterate over the entries in the result, we are ignoring referrals FOR OBJECT e IN res // get DN for this entry and parse it PRINT "DN=";e.dn$ // iterate over the attributes returned for that DN FOR OBJECT a IN e.attributes // and iterate over the values for each attribute PRINT a.Name$, FOR OBJECT v IN a.values PRINT v.Value$, NEXT OBJECT v PRINT NEXT OBJECT a NEXT OBJECT e // tidy up OBJECT res = NULL OBJECT ld = NULL END DEFSUB 'CheckError(nErr) IF (nErr) PRINT $PRINTF("LDAP error %d = '%s'", nErr, ld.err2String$(nErr)) STOP END IF END SUB
Changing a users password on an LDAP server
Here is another program that shows how to change a users password on an RFC compliant LDAP server. Generally the password attribute is the only one that can be changed by a user and only then if he has bound the connection as that user. In this program we first locate the DN for the user using an anonymous binding and then rebind as that user using his existing password. Changing a password is a special case and must be done as a double operation of deleting the old and adding the new. The password is sent over the network already encrypted using one of the schemes from RFC2307 and to find the schemes in use we fetch and inspect the old password.
// Change password for a user on a standards compliant LDAP server DIM OBJECT ld, nErr, OBJECT res, nStatus, nTimeout, nMaxHits, n, OBJECT e, OBJECT attr, OBJECT a, OBJECT v, i DIM sUser$="Fred" DIM sPwd$="secret" DIM sNewPwd$="mostsecret" // load library and define the server(s) to use DIM sLDAPserver$="ldap.kerridge.com" OBJECT ld = CREATE "dynamic", "LDAP", sLDAPserver$ // anon bind using simple authentication, won't work on AD nErr = ld.simple_bind_s() 'CheckError(nErr) // Empty array as we don't need them on the first search OBJECT attr = ld.CreateArray() // where to start searching, KCML defines constants for the scope DIM sBaseDN$="ou=People,dc=kerridge,dc=com" // a filter to select nodes DIM sFilter$0 REDIM sFilter$ = "(uid=" & sUser$ & ")" // The timeout is in ms nTimeout = 5000 // the maximum number of hits required nMaxHits = 5 // make search OBJECT res = ld.search_s(sBaseDN$, _LDAP_SCOPE_SUBTREE, sFilter$, OBJECT attr, FALSE, nTimeout, nMaxHits, BYREF nStatus) // The status should be zero for success or else it is an LDAP error code // It can be set for partial searches even though a result object is returned PRINT $PRINTF("Search returned status %d and the result had %d entries", nStatus, res.Count) IF (res.Count <> 1) THEN STOP DIM dn$0 REDIM dn$ = res.First.dn$ // login as that user, this will verify the old password nErr = ld.simple_bind_s(dn$, sPwd$) 'CheckError(nErr) IF (nErr <> 0) THEN STOP // This is the attribute for the password in a Posix schema DIM pwdkey$="userPassword" OBJECT attr = ld.CreateArray(pwdkey$) // make search again in user context, password should now be visible OBJECT res = ld.search_s(dn$, _LDAP_SCOPE_SUBTREE, sFilter$, OBJECT attr, FALSE, nTimeout, nMaxHits, BYREF nStatus) PRINT $PRINTF("Search returned status %d and the result had %d entries", nStatus, res.Count) OBJECT attr = NULL IF (res.Count <> 1) THEN STOP // confirm old value of the password and encryption in use DIM sOldPwd$0 FOR OBJECT a IN res.First.attributes IF (a.Name$ == pwdkey$) REDIM sOldPwd$ = a.values.Value(1).Value$ BREAK END IF NEXT OBJECT a IF (sOldPwd$ == "") THEN STOP OBJECT res = NULL // encrypt new password using same algorithm DIM sEncrypted$64, sDigest$16 sEncrypted$ = sOldPwd$ SELECT CASE $LOWER(STR(sOldPwd$,, 4)) CASE "{cry" // Unix crypt with 2 character seed CALL KI_CRYPT sNewPwd$, STR(sOldPwd$, 8, 2) TO STR(sEncrypted$, 8) CASE "{md5" // MD5 hash 'KCML_MD5(sNewPwd$, sDigest$) $PACK(E="BASE64") STR(sEncrypted$, 6) FROM sDigest$ END SELECT // to change a password you need to delete the old and add the new as a double operation DIM OBJECT m, OBJECT v, OBJECT modsarray OBJECT modsarray = ld.CreateArray() // old pwd OBJECT v = ld.CreateArray(sOldPwd$) OBJECT m = ld.CreateMod(_LDAP_MOD_DELETE, pwdkey$, OBJECT v) modsarray.Add(OBJECT m) // new pwd OBJECT v = ld.CreateArray(sEncrypted$) OBJECT m = ld.CreateMod(_LDAP_MOD_ADD, pwdkey$, OBJECT v) modsarray.Add(OBJECT m) // make the update nErr = ld.ldap_modify_s(dn$, OBJECT modsarray) 'CheckError(nErr) IF (nErr <> 0) THEN STOP // tidy up OBJECT modsarray = NULL OBJECT v = NULL OBJECT m = NULL // disconnect OBJECT ld = NULL END DEFSUB 'CheckError(nErr) IF (nErr) PRINT $PRINTF("LDAP error %d = '%s'", nErr, ld.err2String$(nErr)) STOP END IF END SUB
A more robust program would also replace the attribute shadowLastChange with the current time in the Unix epoch, i.e. the value of $TIME as a UTF-8 string. This should be done in a separate operation in case it fails if that attribute is not in the schema or if the attribute's ACL does not allow write access by self. This attribute is defined in RFC2307 and is used for password expiry checking.
// update timestamp, ignore any error in case ACL or schema is wrong DIM s$64 s$ = $PRINTF("%d", VAL($TIME, 4)) OBJECT v = ld.CreateArray(s$) OBJECT m = ld.CreateMod(_LDAP_MOD_REPLACE, "shadowLastChange", OBJECT v) OBJECT modsarray = ld.CreateArray() modsarray.Add(OBJECT m) nErr = ld.ldap_modify_s(dn$, OBJECT modsarray)
Special considerations for Microsoft Active Directory
To send a binary value like the UTF-16 password you can create a BER encoded array by specifying the length after each string e.g.
REDIM sIn$ = HEX(22) & sOldPwd$ & HEX(22) nIn = LEN(sIn$) nOut = LEN(STR(sOut$)) 'KCML_encode("UTF-16LE", BYREF sIn$, BYREF nIn, BYREF sOut$, BYREF nOut) OBJECT v = ld.CreateArray(sOut$, nOut)
See also
KCML LDAP object documentation
Netscape LDAP SDK API documentation
LDAP authentication in the Connection Manager