Friday, February 11, 2011

Python + Active Directory + Linux

So, this is really pretty old, but I wanted to share it, since at the time, it took me a while to gather a lot of this information: Managing Active Directory (LDAP) via Linux + Python. There have probably been other posts on this since, but I wanted to put it out there.

I had managed OpenLDAP previously, and then we migrated to Active Directory (Windows Server 2008 at the time). There were a few gotchas that I wasn't expecting.

First, is the default 1,000 entry search results returned by AD. You can change this limit (setting) in Active Directory, but when you think about it, it makes sense to keep it -- if you have a huge directory and are running lots of searches that have huge results, this could definitely thrash your domain controllers.

For your programs to get around this, you need to do use "paged results" (otherwise you'll get an error stating the max returned entries is 1,000).

We are a RHEL shop, but at the time when I worked on this, the default python-ldap package didn't include paged results support (can't remember the version). So, on RHEL / CentOS, grab the latest python-ldap package, and do something like this:

yum remove python-ldap
yum groupinstall "Development Tools"
yum install python-devel
yum install openldap-devel
yum install openssl-devel
tar xvfz python-ldap-2.3.8.tar.gz
cd python-ldap-2.3.8
python setup.py build
python setup.py bdist_rpm
rpm -ivh dist/python-ldap-2.3.8-0.x86_64.rpm

Here is an example in Python of one of those paged searches. I'm pretty sure I grabbed the original from another post somewhere else on the 'net, but I wanted to share it for completeness:

import ldap
from ldap.controls import SimplePagedResultsControl
import sys
import ldap.modlist as modlist

LDAP_SERVER = "ldaps://dc.host.com"
BIND_DN = "Operator@host.com"
BIND_PASS = "password"
USER_FILTER = "(&(objectClass=person)(primaryGroupID=7235))"
USER_BASE = "ou=Special Peeps,ou=My Users,dc=host,dc=com"
PAGE_SIZE = 10

# LDAP connection
try:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, 0)
ldap_connection = ldap.initialize(LDAP_SERVER)
ldap_connection.simple_bind_s(BIND_DN, BIND_PASS)
except ldap.LDAPError, e:
sys.stderr.write('Error connecting to LDAP server: ' + str(e) + '\n')
sys.exit(1)

# Lookup usernames from LDAP via paged search
paged_results_control = SimplePagedResultsControl(
ldap.LDAP_CONTROL_PAGE_OID, True, (PAGE_SIZE, ''))
accounts = []
pages = 0
while True:
serverctrls = [paged_results_control]
try:
msgid = ldap_connection.search_ext(USER_BASE,
ldap.SCOPE_ONELEVEL,
USER_FILTER,
attrlist=['employeeID',
'sAMAccountName'],
serverctrls=serverctrls)
except ldap.LDAPError, e:
sys.stderr.write('Error performing user paged search: ' +
str(e) + '\n')
sys.exit(1)
try:
unused_code, results, unused_msgid, serverctrls = \
ldap_connection.result3(msgid)
except ldap.LDAPError, e:
sys.stderr.write('Error getting user paged search results: ' +
str(e) + '\n')
sys.exit(1)
for result in results:
pages += 1
accounts.append(result)
cookie = None
for serverctrl in serverctrls:
if serverctrl.controlType == ldap.LDAP_CONTROL_PAGE_OID:
unused_est, cookie = serverctrl.controlValue
if cookie:
paged_results_control.controlValue = (PAGE_SIZE, cookie)
break
if not cookie:
break

# LDAP unbind
ldap_connection.unbind_s()

# Make dictionary with user data
user_map = {}
for entry in accounts:
if entry[1].has_key('employeeID') and \
entry[1].has_key('sAMAccountName'):
user_map[entry[1]['employeeID'][0]] = entry[1]['sAMAccountName'][0]

In the above block, I included an example of the connection setup, and retrieving attributes from the result set (not really specific to AD, but someone might find it helpful). Below are some more Python-AD examples, but they are just little snippets (not necessarily complete) of the action.

Changing an Active Directory user's password:

PASSWORD_ATTR = "unicodePwd"
user_dn = user_results[0][1]['distinguishedName'][0]
username = sys.argv[1]
password = getpass.getpass("New password: ")

# Set AD password
unicode_pass = unicode("\"" + password + "\"", "iso-8859-1")
password_value = unicode_pass.encode("utf-16-le")
add_pass = [(ldap.MOD_REPLACE, PASSWORD_ATTR, [password_value])]

# Replace password
try:
ldap_connection.modify_s(user_dn, add_pass)
print "Active Directory password for", username, \
"was set successfully!"
except ldap.LDAPError, e:
sys.stderr.write('Error setting AD password for: ' + username + '\n')
sys.stderr.write('Message: ' + str(e) + '\n')
sys.exit(1)

Create an Active Directory user account:

def CreateUser(username, password, base_dn, fname, lname, domain, employee_num):
"""
Create a new user account in Active Directory.
"""
# LDAP connection
try:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, 0)
ldap_connection = ldap.initialize(LDAP_SERVER)
ldap_connection.simple_bind_s(BIND_DN, BIND_PASS)
except ldap.LDAPError, error_message:
print "Error connecting to LDAP server: %s" % error_message
return False

# Check and see if user exists
try:
user_results = ldap_connection.search_s(base_dn, ldap.SCOPE_SUBTREE,
'(&(sAMAccountName=' +
username +
')(objectClass=person))',
['distinguishedName'])
except ldap.LDAPError, error_message:
print "Error finding username: %s" % error_message
return False

# Check the results
if len(user_results) != 0:
print "User", username, "already exists in AD:", \
user_results[0][1]['distinguishedName'][0]
return False

# Lets build our user: Disabled to start (514)
user_dn = 'cn=' + fname + ' ' + lname + ',' + base_dn
user_attrs = {}
user_attrs['objectClass'] = \
['top', 'person', 'organizationalPerson', 'user']
user_attrs['cn'] = fname + ' ' + lname
user_attrs['userPrincipalName'] = username + '@' + domain
user_attrs['sAMAccountName'] = username
user_attrs['givenName'] = fname
user_attrs['sn'] = lname
user_attrs['displayName'] = fname + ' ' + lname
user_attrs['userAccountControl'] = '514'
user_attrs['mail'] = username + '@host.com'
user_attrs['employeeID'] = employee_num
user_attrs['homeDirectory'] = '\\\\server\\' + username
user_attrs['homeDrive'] = 'H:'
user_attrs['scriptPath'] = 'logon.vbs'
user_ldif = modlist.addModlist(user_attrs)

# Prep the password
unicode_pass = unicode('\"' + password + '\"', 'iso-8859-1')
password_value = unicode_pass.encode('utf-16-le')
add_pass = [(ldap.MOD_REPLACE, 'unicodePwd', [password_value])]
# 512 will set user account to enabled
mod_acct = [(ldap.MOD_REPLACE, 'userAccountControl', '512')]
# New group membership
add_member = [(ldap.MOD_ADD, 'member', user_dn)]
# Replace the primary group ID
mod_pgid = [(ldap.MOD_REPLACE, 'primaryGroupID', GROUP_TOKEN)]
# Delete the Domain Users group membership
del_member = [(ldap.MOD_DELETE, 'member', user_dn)]

# Add the new user account
try:
ldap_connection.add_s(user_dn, user_ldif)
except ldap.LDAPError, error_message:
print "Error adding new user: %s" % error_message
return False

# Add the password
try:
ldap_connection.modify_s(user_dn, add_pass)
except ldap.LDAPError, error_message:
print "Error setting password: %s" % error_message
return False

# Change the account back to enabled
try:
ldap_connection.modify_s(user_dn, mod_acct)
except ldap.LDAPError, error_message:
print "Error enabling user: %s" % error_message
return False

# Add user to their primary group
try:
ldap_connection.modify_s(GROUP_DN, add_member)
except ldap.LDAPError, error_message:
print "Error adding user to group: %s" % error_message
return False

# Modify user's primary group ID
try:
ldap_connection.modify_s(user_dn, mod_pgid)
except ldap.LDAPError, error_message:
print "Error changing user's primary group: %s" % error_message
return False

# Remove user from the Domain Users group
try:
ldap_connection.modify_s(DU_GROUP_DN, del_member)
except ldap.LDAPError, error_message:
print "Error removing user from group: %s" % error_message
return False

# LDAP unbind
ldap_connection.unbind_s()

# Setup user's home directory
os.system('mkdir -p /home/' + username + '/public_html')
os.system('cp /etc/skel/.bashrc /etc/skel/.bash_profile ' +
'/etc/skel/.bash_logout /home/' + username)
os.system('chown -R ' + username + ' /home/' + username)
os.system('chmod 0701 /home/' + username)

# All is good
return True

More to come...

18 comments:

  1. Great work, i found your blog very interesting
    I miss something
    user_ldif = modlist.addModlist(user_attrs)
    Where are you getting modlist.addModlist ?

    Regards Hubert

    ReplyDelete
  2. Sorry, forgot an import: import ldap.modlist as modlist

    ReplyDelete
  3. thanks to your tread i managed to add users, set passwords and so on.
    The method to set expired account-passwords seems not working, at least not in windows 2008
    We shall create expired useraccounts with a defaultpassword.

    Here the code shred:
    mod_acct = [(ldap.MOD_REPLACE, 'userAccountControl', '544')]
    ad.modify_s(user_dn,mod_acct)

    Is there a way?

    ReplyDelete
  4. Does it throw an exception (when trying modify_s)?

    ReplyDelete
  5. No there is no exception and the flag is written. AdsiEdit outputs "PASSWD_NOT_REQD|NORMAL_ACCOUNT. So 544 seems no more adequate. The right one is 8388608 (0x800000), but it's set read only.

    A msd-article states:

    ADS_UF_PASSWORD_EXPIRED
    The user password has expired. This flag is created by the system using data from the password last set attribute and the domain policy. It is read-only and cannot be set. To manually set a user password as expired, use the NetUserSetInfo function with the USER_INFO_3 (usri3_password_expired member) or USER_INFO_4 (usri4_password_expired member) structure.

    NetUserSetInfo ... is a function of the win32net-Library from Active-Python supposedly not available for linux-systems.

    Meanwhile i will resolve my problem with cygwin-ssh-Shell and command 'dsmod user "userdn..." -d domain -mustchpwd yes' but it's clearly a bad workaround

    ReplyDelete
  6. Set the password expiry time to a time in the past and the system will set ADS_UF_PASSWORD_EXPIRED for you.

    ReplyDelete
  7. Very handy reference. Thanks!

    ReplyDelete
  8. Hi Marc. i used you blog for password management and it works perfectly. i am trying to use ldap to modify group policy in AD. it would be really helpful if can you provide me some tips/references related to it. eagerly waiting for your reply.

    Regards
    Navendu

    ReplyDelete
  9. Hi Navendu. Sorry for the delay -- I have never attempted to manipulate group policy via LDAP. Sorry!

    ReplyDelete
  10. np. thanks. i will post my findings when i get it going.

    ReplyDelete
  11. Do you have a way to search for and list specific attributes of a user object in AD?

    ReplyDelete
  12. Thank you very much, I just stumbled on this. Everyone has been recommending VBS and PowerShell and I figured there must be a Python mod.

    ReplyDelete
  13. Awesome example of LDAP paging. I reused (and credited) your code to query AD for valid SMTP recipients and format for Postfix relay_recipients: https://gist.github.com/4503265

    ReplyDelete
  14. The same can be used for ubuntu..?What all the packages we might need for doing so..?

    ReplyDelete
  15. Hi, Marc~ I used you blog for password management and it don't works.
    Python change domain(Microsoft Active Directory) user's password.
    ...requires certification services between python and domain?
    Could you have any good ways to deal with it?
    Thank you very much!

    ReplyDelete
    Replies
    1. Hi,

      Can you provide more information and example code you're trying? Do you mean it requires SSL (eg, LDAPS) when changing passwords? I believe the answer is yes -- if I remember correctly AD won't let you change passwords via no-SSL LDAP.


      --Marc

      Delete
    2. Yes, you're right.
      how can i setup ldaps? thank you
      ----------------------------------------------------------------------------
      import ldap
      import sys

      host = 'ldap://10.172.0.79'

      con = ldap.initialize(host)
      BIND_DN = "administrator@biztalk.com"
      BIND_PASS = "a-123456"
      con.set_option( ldap.OPT_X_TLS_DEMAND, True )
      con.set_option( ldap.OPT_DEBUG_LEVEL, 255 )

      PASSWORD_ATTR = "unicodePwd"
      username="bizadmin"
      user_dn = "CN=%s,OU=User,OU=biztalk,DC=biz-talk,DC=com" % username
      password = 'New12345'

      # Set AD password
      unicode_pass = unicode("\"" + password + "\"", "iso-8859-1")
      password_value = unicode_pass.encode("utf-16-le")
      add_pass = [(ldap.MOD_REPLACE, PASSWORD_ATTR, [password_value])]

      # Replace password
      try:
      con.modify_s(user_dn, add_pass)
      print "Active Directory password for", username, "was set successfully!"
      except ldap.LDAPError, e:
      sys.stderr.write('Error setting AD password for: ' + username + '\n')
      sys.stderr.write('Message: ' + str(e) + '\n')
      sys.exit(1)
      -------------------------------------------------------------
      Error setting AD password for: bizadmin

      Message: {'desc': "Can't contact LDAP server"}

      Delete
    3. Hi,

      You would need to enable LDAP SSL (LDAPS) on Active Directory. I don't remember right off the top of my head for setting it up, but if you Google it, you should find some good articles / answers.


      --Marc

      Delete