What commands will change Open Directory passwords

mac-osx-serveropendirectorypassword

I understand Open Directory to be OpenLDAP + SASL (Password Server) + Kerberos. It appears that OpenLDAP defers to SASL for authentication; I don't know about Kerberos.

I want to change user passwords from a script, preferably remotely, and I want the password to be changed properly. (ie. in no event do I want a user to have a different password depending upon which of the three services that go into Open Directory they authenticate against.)

I can do a dsimport over the network just fine from a machine that is not bound to the directory, but, when you try to import a password (depite setting the AuthType to dsAuthMethodStandard:dsAuthClearText), it will work only if the password has not been set before. (I believe it is possible to set a Crypt password, but I fear that means that only the LDAP portion of OD will know the current password.)

Is there anything I can do short of initiating an ssh session to the server and changing the passwords there? If I do that, is there any command that will let me specify a number of users and their new passwords on one line?

Which commands work to change all open directory passwords, and is there any one to prefer?

apropos password gives me these interesting results:

  • kpasswd(1) – change a user's Kerberos password
  • ldappasswd(1) – change the password of an LDAP entry
  • lppasswd(1) – add, change, or delete digest passwords
  • passwd(1) – modify a user's password
  • pwpolicy(8) – gets and sets password policies
  • saslpasswd2(8) – set a user's sasl password
  • slappasswd(8) – OpenLDAP password utility

I'll look at some of the man pages, and I'm under the impression that pwpolicy is the best choice, but I'd love to know if there are any subtleties to using these (such as, don't change the Kerberos password without also changing the LDAP and SASL passwords), and if any of them work remotely without an ssh session.

Best Answer

The handiest answer I've come across is to use the passwd command in conjunction with dscl. Here is the output from an interactive session (with the passwords replaced by asterices):

$ dscl -u diradmin -p ces 
Password: 
 > cd /LDAPv3/127.0.0.1/
/LDAPv3/127.0.0.1 > auth diradmin *****
/LDAPv3/127.0.0.1 > passwd Users/Atwo807 *****
/LDAPv3/127.0.0.1 > passwd Users/Atwo249 *****
/LDAPv3/127.0.0.1 > passwd Users/doesnotexist foobar
passwd: Invalid Path
<dscl_cmd> DS Error: -14009 (eDSUnknownNodeName)
/LDAPv3/127.0.0.1 > exit
Goodbye

Here is a python script to make the changes. You will need the pexpect module (sudo easy_install pexpect should get it for you; I don't think you need the dev tools installed).

#!/usr/bin/env python

import pexpect

def ChangePasswords(host, path, diradmin, diradmin_password, user_passwords, record_type='Users'):
    """Changes passwords in a Open Directory or similar directory service.

    host = the dns name or IP of the computer hosting the directory
    path = the pathname to the directory (ex. '/LDAPv3/127.0.0.1')
    diradmin = the directory administrator's shortname (ex. 'diradmin')
    diradmin_password = the directory administrator's password
    user_passwords = a dictionary mapping record names (typically, user's short
                     names) onto their new password
    record_type = the sort of records you are updating.  Typically 'Users'

    Returns a tuple.  The first entry is a list of all records (users) who
        failed to update.  The second entry is a list of all records (users)
        who successfully updated.
    """

    failed_list = []
    succeeded_list = []
    prompt = " > "

    child = pexpect.spawn("dscl -u %s -p %s" % (diradmin, host))

    if not (ReplyOnGoodResult(child, "Password:", diradmin_password) and
       ReplyOnGoodResult(child, prompt, "cd %s" % path) and
       ReplyOnGoodResult(child, prompt,
                        "auth %s %s"  % (diradmin, diradmin_password)) and
       ReplyOnGoodResult(child, prompt, None)):
        print "Failed to log in and authenticate"
        failed_list = user_passwords.keys()
        return (failed_list, succeeded_list)

    # We are now logged in, and have a prompt waiting for us
    expected_list = [ pexpect.EOF, pexpect.TIMEOUT,
                     '(?i)error', 'Invalid Path', prompt ]
    desired_index = len(expected_list) - 1
    for record_name in user_passwords:        
        #print "Updating password for %s" % record_name,

        child.sendline("passwd %s/%s %s" % (record_type, record_name,
                                            user_passwords[record_name]))
        if child.expect(expected_list) == desired_index:
            #print ": Succeeded"
            succeeded_list.append(record_name)
        else:
            #print ": Failed"
            failed_list.append(record_name)
            child.expect(prompt)

    child.sendline("exit")
    child.expect(pexpect.EOF)

    return (failed_list, succeeded_list)


def ReplyOnGoodResult(child, desired, reply):
    """Helps analyze the results as we try to set passwords.

    child = a pexpect child process
    desired = The value we hope to see 
    reply = text to send if we get the desired result (or None for no reply)
    If we do get the desired result, we send the reply and return true.
    If not, we return false."""

    expectations = [ pexpect.EOF, pexpect.TIMEOUT, '(?i)error', desired ]
    desired_index = len(expectations) - 1

    index = child.expect(expectations)
    if index == desired_index:
        if reply:
            child.sendline(reply)
        return True
    else:
        return False

You can use it as follows:

# This example assumes that you have named the script given above 'pwchange.py'
# and that it is in the current working directory
import pwchange 

(failed, succeeded) = pwchange.ChangePasswords("ces", "/LDAPv3/127.0.0.1", 
     "diradmin", "******", 
     { 'Atwo807' : '*****', 'Atwo249' : '*****', 
       'Nonexist' : 'foobar', 'Bad' : 'bad' })

print failed, succeeded
['Bad', 'Nonexist'] ['Atwo249', 'Atwo807']