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...
Great work, i found your blog very interesting
ReplyDeleteI miss something
user_ldif = modlist.addModlist(user_attrs)
Where are you getting modlist.addModlist ?
Regards Hubert
Sorry, forgot an import: import ldap.modlist as modlist
ReplyDeletethanks to your tread i managed to add users, set passwords and so on.
ReplyDeleteThe 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?
Does it throw an exception (when trying modify_s)?
ReplyDeleteNo 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.
ReplyDeleteA 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
Set the password expiry time to a time in the past and the system will set ADS_UF_PASSWORD_EXPIRED for you.
ReplyDeleteVery handy reference. Thanks!
ReplyDeleteHi 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.
ReplyDeleteRegards
Navendu
Hi Navendu. Sorry for the delay -- I have never attempted to manipulate group policy via LDAP. Sorry!
ReplyDeletenp. thanks. i will post my findings when i get it going.
ReplyDeleteDo you have a way to search for and list specific attributes of a user object in AD?
ReplyDeleteThank you very much, I just stumbled on this. Everyone has been recommending VBS and PowerShell and I figured there must be a Python mod.
ReplyDeleteAwesome 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
ReplyDeleteThe same can be used for ubuntu..?What all the packages we might need for doing so..?
ReplyDeleteHi, Marc~ I used you blog for password management and it don't works.
ReplyDeletePython 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!
Hi,
DeleteCan 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
Yes, you're right.
Deletehow 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"}
Hi,
DeleteYou 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
Hi Marc, i used your blog, it works fine. i am trying to use ldap3 to read group and its attribute from specific searchbase and writing result it into CSV file. it would be really helpful if can you provide me some tips/references related to it.
ReplyDeleteHi Marc,
ReplyDeletejust reading through this Using the templates I'm able to get a user to be created, but I cannot get any of the subsequent changes to activate the user account to complete. I've imported ldap.modlist as modlist. Any suggestions? code below.
attrs = {}
attrs['objectclass'] = ['Top','person','organizationalPerson','user']
attrs['cn'] = built_name
attrs['displayName'] = built_name
attrs['name'] = lastName
attrs['givenName'] = firstName
attrs['mail'] = email
attrs['ou'] = "Users"
#attrs['pwdLastSet'] = "-1"
attrs['userPrincipalName'] = username + "@domain.name"
attrs['sAMAccountName'] = username
attrs['userPassword'] = password
try:
ldif = modlist.addModlist(attrs)
ldap_client.add_s(user_dn, ldif)
except ldap.LDAPError, error_message:
print "Error enabling user: %s" % error_message
return False
try:
mod_acct = [(ldap.MOD_REPLACE, 'userAccountControl', '66048')]
ldap_client.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:
add_member = [(ldap.MOD_ADD, 'member', user_dn)]
ldap_client.modify_s(group_dn, add_member)
except ldap.LDAPError, error_message:
print "Error adding user to group: %s" % error_message
return False
success_string = "success %s" % str(ldif)
return success_string
When you say "activate" do you mean "enable" the user account? Do you get any error messages? In your own words, what are you trying to accomplish with the Python code?
Delete--Marc
Hi Marc, yes when i say activate i mean enable, sorry. I get no error messages and accounts are created but they are created in a disable state.
ReplyDeleteI'd like it to :
1. create the user
2. enable the account
3. add them as having rights to another groups
So, if you're using the same example as CreateUser() above, maybe double-check everything there. I'm not sure what flags '66048' equates to in this: mod_acct = [(ldap.MOD_REPLACE, 'userAccountControl', '66048')]
DeleteDid you try just the simple '512' that I demonstrated above? Just to see if it at least works with that, then check your flags.
--Marc
This was the only example code I found on the internet that actually works for modifying "userAccountControl" as well as creating accounts. Have never worked with LDAP/AD in my career until now, so you're a life saver! Thanks1
ReplyDeleteI've to stop by and say thank you, I finally make my code work with your help and one of the comments were particularly useful, to set the password you require an SSL connection, that finally make the trick and make my problem go away, thanks for putting everything in one very useful and concise post.
ReplyDeleteHi all,
ReplyDeletewhen i tried to set password, exception occurs
Error enabling user: {'info': '0000052D: SvcErr: DSID-031A12D2, problem 5003 (WILL_NOT_PERFORM), data 0\n', 'desc': 'Server is unwilling to perform'}
Please let me know if anyway to solve this
hi all,
ReplyDeleteit's possible to modify the attribute accountExpires to put a new date for expiration account?
It is very useful information. Thanks for sharing with us. I would like share my website about LDAP Integeration Module.
ReplyDeleteCheckout the best marketplace extension in the town
ReplyDeletehttp://magento-development.medma.net/medma-magento2-marketplace.html