Background:
I’ll start by saying we have a rather large AD with over a million users. #humblebrag
I had a ticket escalated to me that was quite odd. A user had logged into their Mac, but a number of applications and command line tools were reporting a different user account.
After a bit of trouble shooting and I found that both of these users had the same UID on the Mac. So I started to dig into how this could be and how the AD Plugin in generates its UID. The post below is a result of that work.
I think it is pretty well known that the AD Plugin uses the first 32 Bits of the 128 Bit objectGUID in AD to determine the UID on the Mac.
But after a bit more digging it turns out its not quite that simple.
I’ll work with a few examples here and show you how the AD Plugin determines the UID that will be used and provide a script that will allow you to determine the UID of your users accounts in AD. From here you can check to see if you have any UID collisions.
First lets start with a user in AD.
If we inspect the users account record in AD with something like Apache directory studio, we can see the objectGUID which is a 128 bit hex string.
Here we can see an objectGUID with a value of:
6C703CF1-B5D1-41F8-880B-317728CBD4F5
Now the AD Plugin will read this value and take the first 32 Bits which is: 6C703CF1
It then converts this hex value to decimal. This can be achieved by using the following command:
echo "ibase=16; 6C703CF1" | bc
Which will return: 1819294961
Now you might say, well thats easy!
And I thought so too. But theres a slight issue with this. The UID on the Mac has a maximum decimal value as it is a 32 Bit integer. So the maximum number the UID can be is: 2147483647
In this example the hex value 6C703CF1
converts nicely into a 32 Bit integer (as in, its value is less than or equal to 2147483647) and so can be used as the Mac UID without any further work.
But lets look at another example:
Here we can see an objectGUID with a value of:
BEB08781-0DAF-4B12-9EB6-AF33CBA90876
Now if we do our conversion on this as we did before:
echo "ibase=16; BEB08781" | bc
We end up with a result of: 3199240065
Unfortunately this number is larger than the maximum 32 Bit integer allowed by the Mac UID.
So what do we do?
Turns out Apple use a table to convert the first character of these 32 Bit GUID’s into a number and then recalculate the UID based on this new 32 Bit GUID.
For example with the GUID above, BEB08781
, we take the first character B
and replace it with the number 3 to end up with: 3EB08781
Now when we do the conversion:
echo "ibase=16; 3EB08781" | bc
We get a value of: 1051756417
Which fits perfectly into our 32 Bit integer Mac UID.
The table of conversion looks like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
case $FIRST_CHAR in | |
A) | |
NUMBER=2 ;; | |
B) | |
NUMBER=3 ;; | |
C) | |
NUMBER=4 ;; | |
D) | |
NUMBER=5 ;; | |
E) | |
NUMBER=6 ;; | |
F) | |
NUMBER=7 ;; | |
9) | |
NUMBER=1 ;; | |
8) | |
NUMBER=0 ;; | |
*) | |
esac |
Building a script
So now we know this, how do we build a script to do this conversion for us?
Interacting with records in AD is usually done with `ldapsearch`
and thats how i work with all my AD queries from my machine. It allows me to target specific OU’s and is generally easier to work with than dscl
for me, and I don’t need to have my machine bound to AD for it to work.
So first lets start with a basic ldapsearch to get the users objectGUID.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
## Start by loading up our ldap query variables | |
SVC_ACCOUNT_NAME="Username" | |
SVC_ACCOUNT_PASS="Password" | |
DOMAIN="my.domain" | |
LDAP_SERVER="dc.my.domain:389" | |
SEARCH_BASE="OU=Users,DC=My,DC=Domain" | |
ldapsearch -LLL -H ldap://$LDAP_SERVER -E pr=1000/noprompt -o ldif-wrap=no -x -D ${SVC_ACCOUNT_NAME}@$DOMAIN -w ${SVC_ACCOUNT_PASS} -b "${SEARCH_BASE}" \ | |
-s sub -a always "(objectClass=user)" "sAMAccountName" "objectGUID" |
This should return an output like this:
dn: CN=Smith\, John,OU=Accounts,OU=My Users,OU=My House,OU=My Room,DC=My,DC=Domain objectGUID:: TE0kyRyv8UCppPeXes5JTg== sAMAccountName: john.smith
Now the objectGUID here does not match what we see in AD with Apache Directory Studio, and that is because it is encoded in base64 as denoted by the double colons "::"
after the objectGUID label.
So to convert this into something we can work with we need to decode it from base64 and then hex dump it.
So to achieve that we use the following function:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
DECODE_BASE64(){ | |
# This function takes the encoded output from ldapsearch and decodes it | |
# It then needs to be "hex-dumped" in order to get it into regular text | |
# So that we can work with it | |
OBJECT_ID="$1" | |
BASE64_DECODED=$(echo $OBJECT_ID | base64 -D) | |
G=($(echo ${BASE64_DECODED} | hexdump -e '16/1 " %02X"')) | |
OBJECTGUID="${G[3]}${G[2]}${G[1]}${G[0]}-${G[5]}${G[4]}-${G[7]}${G[6]}-${G[8]}${G[9]}-${G[10]}${G[11]}${G[12]}${G[13]}${G[14]}${G[15]}" | |
} | |
# Set our objectGUID that we get from ldapsearch to a variable | |
TO_BE_DECODED="TE0kyRyv8UCppPeXes5JTg==" | |
# Send this to our decode function | |
DECODE_BASE64 $TO_BE_DECODED | |
# Now echo the result | |
echo $OBJECTGUID | |
exit 0 |
This then converts our objectGUID from ldapsearch into:
C9244D4C-AF1C-40F1-A9A4-F7977ACE494E
By now we should have all the bits we need to run a script to
1. Pull the objectGUID from AD using ldapsearch
2. Convert that objectGUID from Base64 into text
3. Convert the first 32 Bit from hex to decimal
4. Decide if that decimal value is larger than the maximum for a 32 Bit integer
5. If it is larger, we then know what number to replace the first character of that objectGUID with
6. Recalculate that new objectGUID into decimal to determine the UID the AD Plugin will set for that user on the Mac.
Completed script
With the script below you can target a user account DN in the search base and it will return that users DN
and ObjectGUID
in clear text and also the UID that will be used on a Mac when that user logs in.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# Author: Calum Hunter | |
# Date: 28/11/2016 | |
# Version: 1.0 | |
# Purpose: To generate a Mac UID from the objectGUID attribute | |
# (GeneratedUID) in AD. | |
# This uses the same method that the Apple | |
# AD Plugin uses | |
# | |
## Start by loading up our ldap query variables | |
SVC_ACCOUNT_NAME="Username" | |
SVC_ACCOUNT_PASS="Password" | |
DOMAIN="my.domain" | |
LDAP_SERVER="dc.my.domain:389" | |
SEARCH_BASE="CN=John\, Smith,OU=Users,DC=MY,DC=DOMAIN" | |
DECODE_BASE64(){ | |
# This function takes the encoded output from ldapsearch and decodes it | |
# It then needs to be "hex-dumped" in order to get it into regular text | |
# So that we can work with it | |
OBJECT_ID="$1" | |
BASE64_DECODED=$(echo $OBJECT_ID | base64 -D) | |
G=($(echo ${BASE64_DECODED} | hexdump -e '16/1 " %02X"')) | |
OBJECTGUID="${G[3]}${G[2]}${G[1]}${G[0]}-${G[5]}${G[4]}-${G[7]}${G[6]}-${G[8]}${G[9]}-${G[10]}${G[11]}${G[12]}${G[13]}${G[14]}${G[15]}" | |
} | |
# Search LDAP for our user account | |
RESULT=$(ldapsearch -LLL -H ldap://$LDAP_SERVER -o ldif-wrap=no -x -D ${SVC_ACCOUNT_NAME}@$DOMAIN -w ${SVC_ACCOUNT_PASS} -b "${SEARCH_BASE}" \ | |
-s sub -a always "(objectClass=user)" "objectGUID") | |
# Get our user DN and objectGUID from the result above. | |
USER_DN=$(echo "$RESULT" | grep "dn:") | |
USER_GUID_BASE64=$(echo "$RESULT" | awk -F "::" '/objectGUID/ {print $2}') | |
# Get our GeneratedUID from LDAPSEARCH by decoding and hex dumping it | |
DECODE_BASE64 "$USER_GUID_BASE64" | |
# Now lets get the first 32 bits of our GUID | |
GUID_32=${OBJECTGUID:0:8} | |
# Now convert this to decimal | |
GUID_32_DEC=$(echo "ibase=16; $GUID_32" | bc) | |
# Check if this is greater than the largest decimal figure allowed for a mac UID (32Bit Integer) | |
if [ $GUID_32_DEC -gt 2147483647 ]; then | |
# Get the first character of our 32bit GUID | |
FIRST_CHAR=${GUID_32:0:1} | |
# Use the below table to replace the first character with number it represents. ie: A=2 | |
case $FIRST_CHAR in | |
A) | |
NUMBER=2 ;; | |
B) | |
NUMBER=3 ;; | |
C) | |
NUMBER=4 ;; | |
D) | |
NUMBER=5 ;; | |
E) | |
NUMBER=6 ;; | |
F) | |
NUMBER=7 ;; | |
9) | |
NUMBER=1 ;; | |
8) | |
NUMBER=0 ;; | |
*) | |
esac | |
# Now lets replace the first character with our new number | |
A=$(echo $GUID_32 | cut -c2-) | |
NEW_32_GUID="${NUMBER}${A}" | |
GUID_32_DEC=$(echo "ibase=16; $NEW_32_GUID" | bc) | |
fi | |
# Echo our output | |
echo "User: $(echo $USER_DN | awk -F "dn:" '{print $2}')" | |
echo "ObjectGUID: $OBJECTGUID" | |
echo "Mac UID: $GUID_32_DEC" | |
Bonus points
For bonus points, you might want to target a container of Users, say OU=Users
and then iterate through that container outputting the UID’s for those users so you can then check for duplicates.
So here is an ugly bash script that does just that.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# | |
# Author: Calum Hunter | |
# Date: 28/11/2016 | |
# Version: 1.0 | |
# Purpose: To generate a Mac UID from the objectGUID attribute | |
# (GeneratedUID) in AD. | |
# This uses the same method that the Apple | |
# AD Plugin uses | |
# | |
## Start by loading up our ldap query variables | |
SVC_ACCOUNT_NAME="Username" | |
SVC_ACCOUNT_PASS="Password" | |
DOMAIN="my.domain" | |
LDAP_SERVER="dc.my.domain:389" | |
SEARCH_BASE="OU=Users,DC=My,DC=Domain" | |
DECODE_BASE64(){ | |
# This function takes the encoded output from ldapsearch and decodes it | |
# It then needs to be "hex-dumped" in order to get it into regular text | |
# So that we can work with it | |
OBJECT_ID="$1" | |
BASE64_DECODED=$(echo $OBJECT_ID | base64 -D) | |
G=($(echo ${BASE64_DECODED} | hexdump -e '16/1 " %02X"')) | |
OBJECTGUID="${G[3]}${G[2]}${G[1]}${G[0]}-${G[5]}${G[4]}-${G[7]}${G[6]}-${G[8]}${G[9]}-${G[10]}${G[11]}${G[12]}${G[13]}${G[14]}${G[15]}" | |
} | |
# Search LDAP for our user account | |
RESULT=$(ldapsearch -LLL -H ldap://$LDAP_SERVER -E pr=1000/noprompt -o ldif-wrap=no -x -D ${SVC_ACCOUNT_NAME}@$DOMAIN -w ${SVC_ACCOUNT_PASS} -b "${SEARCH_BASE}" \ | |
-s sub -a always "(objectClass=user)" "sAMAccountName" "objectGUID") | |
i=1 | |
s=1 | |
declare -a RESULT_ARRAY | |
while IFS= read -r line; do | |
# If we find an empty line, then we increase the counter (i), | |
# set the flag (s) to one, and skip to the next line | |
[[ $line == "" ]] && ((i++)) && s=1 && continue | |
# If the flag (s) is zero, then we are not in a new line of the block | |
# so we set the value of the array to be the previous value concatenated | |
# with the current line | |
[[ $s == 0 ]] && RESULT_ARRAY[$i]="${RESULT_ARRAY[$i]} | |
$line" || { | |
# Otherwise we are in the first line of the block, so we set the value | |
# of the array to the current line, and then we reset the flag (s) to zero | |
RESULT_ARRAY[$i]="$line" | |
s=0; | |
} | |
done <<< "$RESULT" | |
for USER in "${RESULT_ARRAY[@]}"; do | |
USER_DN=$(echo "$USER" | grep "dn:") | |
USER_GUID_BASE64=$(echo "$USER" | awk -F "::" '/objectGUID/ {print $2}') | |
# Get our GeneratedUID from LDAPSEARCH by decoding and hex dumping it | |
DECODE_BASE64 "$USER_GUID_BASE64" | |
# Now lets get the first 32 bits of our GUID | |
GUID_32=${OBJECTGUID:0:8} | |
# Now convert this to decimal | |
GUID_32_DEC=$(echo "ibase=16; $GUID_32" | bc) | |
if [ $GUID_32_DEC -gt 2147483647 ]; then | |
# Get the first character of our 32bit GUID | |
FIRST_CHAR=${GUID_32:0:1} | |
# Use the below table to replace the first character with number it represents. ie: A=2 | |
case $FIRST_CHAR in | |
A) | |
NUMBER=2 ;; | |
B) | |
NUMBER=3 ;; | |
C) | |
NUMBER=4 ;; | |
D) | |
NUMBER=5 ;; | |
E) | |
NUMBER=6 ;; | |
F) | |
NUMBER=7 ;; | |
9) | |
NUMBER=1 ;; | |
8) | |
NUMBER=0 ;; | |
*) | |
esac | |
# Now lets replace the first character with our new number | |
A=$(echo $GUID_32 | cut -c2-) | |
NEW_32_GUID="${NUMBER}${A}" | |
GUID_32_DEC=$(echo "ibase=16; $NEW_32_GUID" | bc) | |
fi | |
# Echo our output | |
USERNAME=$(echo $USER_DN | awk -F "dn:" '{print $2}') | |
echo "$USERNAME,$GUID_32_DEC" | |
echo "$USERNAME,$GUID_32_DEC" >> users_with_UID.csv | |
done |
I ran into a similar issue, though likely very different root cause. My environment had recently moved from using a 3rd party utility to bind, to instead using MDM profiles to initiate as dsconfigad bind. While the resulting mobile accounts had the same name, they had differing Unique Identifiers, as apparently the two bind systems used different algorithms to build the UID. The solution was relatively simple, to just read the UniqueID from the DSCL record of the user after they had migrated, and then use that in a find command to globally migrate file ownership.
LikeLike
Wouldn’t it be simpler to set UNIX Attributes in AD and use UID/GID?
LikeLike
In a small environment perhaps yes. Ours is extremely large, 2,000+ RODC’s alone.
2+ million user accounts.
Changing the schema for such an environment is not something that is done easily
Are Unix services even still available in Win2012?
LikeLike
No need for a conversion table. Just use the bitwise ‘and’ operator to mask out the high bit:
GUID_32_DEC=$(echo $(($(echo “ibase=16; $USER_GUID_BASE64” | bc) & 2147483647)))
LikeLike