Creating User Accounts Using LDAP
I’ve expanded my RADUM sync() method to create Windows user accounts using LDAP alone. This means I won’t have to resort to using the Windows command line. I thought this would be possible, especially after successfully creating Windows groups. I found out that I was in for a world of pain (at least initially). I kept getting error code 53: “Unwilling to perform”. I figured that I was violating some Windows security rules somewhere. I searched online like mad, and I was able to get some hints on what the problems might be, though none of those solutions were Ruby related in any way :-)
So, how does one create a Windows user account using only LDAP? There are a couple of potential problems to deal with which I will discuss inline. The basic steps I am using are:
- Add the user account as a normal account, disabled, and with no password required.
- Replace the unicodePwd and userAccountControl AD attributes to at least remove the no password required attribute (and enable the account if it should be, which it should most of the time).
- Set the pwdLastSet AD attribute to 0 if the user must change his password on the first login.
- Fix the primaryGroupGID AD attribute if the primary Windows group is not Domain Users. Note that in the first step, the account was created with Domain Users as the primary Windows group. That seems like the easiest way to accomplish this because there is additional group logic needed if the primary Windows group is not the default Domain Users group. Most of the time this would not be required, but I want to make sure it is possible.
I am not going to post all the code from my create_user() method. It’s in the RADUM Git repository, and it is not production ready yet. It does work though. I will outline one sticky problem it took me a while to figure out though. The unicodePwd AD attribute can be set using LDAP to set the user’s Windows password. This attribute expects a UTF-16LE encoded value. This is actually Base64 encoded when it is handled over LDAP as well, but when specifying the value in the LDAP method you can just give it the UTF-16LE value. There are numerous sources online that point out the UTF-16LE format for this attribute. Microsoft has some documentation here. The first thing to note about the password value is that it has to be surrounded by explicit double quotes. Ruby has an NKF class that can easily encode values as UTF-16BE:
NKF.nkf('-w16m0', '"foo"')
But I could not get anything to work with respect to getting UTF-16LE using the NKF class. I did some digging and figured out that UTF-16LE for ASCII characters is simply the character itself followed by a NULL byte. That’s very convenient since I only care about handling ASCII characters for the password (sorry everyone who wants to use more characters :-). More information about this can be found on the UTF-16/UCS-2 Wikipedia page and in this extremely helpful email. This is how I encode the user’s password for the unicodePwd AD attribute:
# Convert a string to UTF-16LE. For ASCII characters, the result should be
# each character followed by a NULL, so this is very easy. Windows expects
# a UTF-16LE string for the unicodePwd attribute. Note that the password
# Active Directory is expecting for the unicodePwd attribute has to be
# explicitly quoted.
def str2utf16le(str)
('"' + str + '"').gsub(/./) { |c| "#{c}\0" }
end
Note that setting the unicodePwd AD attribute also requires using TLS as well or else error code 53 will be returned. I was able to do everything else except set the password unless I switched to TLS, so for now I’ve made my code require TLS. This means that a domain must have a certificate server. On my test server I added the role. Without a certificate server, connections to port 636 on my server would simply disconnect, even though the system was “listening”.
The second pain was dealing with setting the primaryGroupID AD attribute. Most of the time this would not be necessary. Users normally have Domain Users as their primaryGroupID, but as I’ve mentioned before, I want to be able to set this to whatever (there are restrictions that I check in the code). You can’t just set the primaryGroupID AD attribute to the right group. That also returned the dreaded error code 53. When using the GUI tools in Windows, I noted that you have to first add the user as a member of the target group, then you can set their primary Windows group to be that group. This also handles the magic where the user is removed from the member AD attribute for the target group and added to the member AD attribute for the previous primary Windows group. Recall that users are members of their primary Windows group by way of their primaryGroupID AD attribute alone. So, I simply did the same thing:
# The user already has the primary Windows group as Domain Users
# based on the default actions above. If the user has a different
# primary Windows group, it is necessary to add the user to that
# group first (as a member in the member attribute for the group)
# before attempting to set their primaryGroupID attribute or Active
# Directory will refuse to do it.
unless rid == find_group("Domain Users").rid
ops = [
[:add, :member, user.distinguished_name]
]
puts ops.to_yaml
@ldap.modify :dn => user.primary_group.distinguished_name,
:operations => ops
check_ldap_result
ops = [
[:replace, :primaryGroupID, rid.to_s]
]
puts ops.to_yaml
@ldap.modify :dn => user.distinguished_name, :operations => ops
check_ldap_result
end
I am outputting the ops array to YAML format for now. That will be removed eventually of course, but it is useful for now.
My general algorithm for synchronizing with Active Directory seems to be something along the lines of: create all the groups that do not already exist (sans any user memberships since those users might not exist yet), create all user accounts that don’t already exist yet, then deal with group membership issues. I’ve not gotten to the last part yet, but at least I can now create a Windows user and set their password using LDAP alone. That’s great!
Oh yeah, I forgot that I also have to deal with any modified attributes for accounts that were not created by the first part of the algorithm. I am still working on this, but first I had to figure out if I could create users and groups using only LDAP.






