I haven't posted in a while, so I wanted to get back into a routine by sharing this tidbit.
A common task a developer may encounter is the need to find out what security group a user is a member of. This is critical information for an app to utilize a role-based authorization mechanism in web apps, client/server apps, login scripts, etc. When querying LDAP, this is as easy as enumerating the 'memberOf' attribute of the user account, right?
Not quite. The memberOf attribute lists distinguished names of all groups the user is an immediate member of. Additionally, memberOf will list both distribution and security groups as well as disabled groups, so it's important to check for these conditions. Most importantly, this does not include nested group membership. For example, say the user is a member of "IT Operations", and that group is a member of "IT Department". If we grant authorization to "IT Department", wouldn't we expect the user to inherit that right?
Ok, so we scan for the groups' parents recursively, right? Sure, but there's a much better way.
User accounts have a 'tokenGroups' attribute that contains the SIDs of all member enabled security groups AND their parents. Knowing the SID of a group, it is very fast to look it up from this attribute to check membership, taking only one query for the tokenGroups and another for each group SID lookup.
I've attached example code using two APIs: VBScript using ADSI, and Perl using LDAP. Both examples do essentially the exact same things:
The nice part about the Perl/LDAP approach is that it should work (though untested) against LDAP directories other than Active Directory, like say OpenLDAP, and from Unix/Linux machines. I'm curious to hear if anyone is able to use this code in non-AD environments.
VBScript / ADSI script:
'
' Example code to scan Active Directory for user group membership
' using VBScript under Windows Script Host and ADSI.
'
' Usage: cscript ADGroupsExample.vbs
'
' Shawn Poulson, 2009.05.18
' explodingcoder.com
'
' Get current logged in user info
Set oADSysInfo = CreateObject("ADSystemInfo")
userDN = oADSysInfo.UserName ' Get DN of user
WScript.Echo "User DN: " & userDN
' Quick check if user is a member of a group
WScript.Echo "User is a member of 'IT': " & IsMemberOf(userDN, GetDNByID("IT"))
WScript.Echo "User is a member of 'Foobar': " & IsMemberOf(userDN, GetDNByID("Foobar"))
' Enumerate all member group names
tkUser = GetTokenGroups(userDN) ' Get tokens of member groups
WScript.Echo "User is a member of " & (UBound(tkUser) + 1) & " groups:"
For Each tk in tkUser
WScript.Echo " " & GetIDBySid(tk)
Next
' Scan token list for groups
WScript.Echo "User is a member of 'IT': " & TokenListFindSid(tkUser, GetSidByID("IT"))
WScript.Echo "User is a member of 'Foobar': " & TokenListFindSid(tkUser, GetSidByID("Foobar"))
' Done
' =====================================
' Token query routines
' =====================================
' Is DN a member of security group?
' Usage: <bool> = IsMemberOf(<DN of object>, <DN of group>)
Function IsMemberOf(dnObject, dnGroup)
IsMemberOf = TokenListFindSid(GetTokenGroups(dnObject), GetSidByDN(dnGroup))
End Function
' Gets tokenGroups attribute from the provided DN
' Usage: <Array of tokens> = GetTokenGroups(<DN of object>)
Function GetTokenGroups(dnObject)
Dim adsObject
' Setup query of tokenGroup SIDs from dnObject
Set adsObject = GetObject(LdapUri(dnObject))
adsObject.GetInfoEx Array("tokenGroups"), 0
GetTokenGroups = adsObject.GetEx("tokenGroups")
End Function
' Checks if the SID of a DN is found in an array of tokens.
' Usage: <bool> = TokenListFindSid(<Array of tokens>, <Object SID Byte()>)
Function TokenListFindSid(arrTokens, objectSid)
Dim nSidSize, vSidHex, e
TokenListFindSid = False
If TypeName(objectSid) = "Byte()" Then
' Scan token array for object SID
nSidSize = UBound(objectSid)
vSidHex = ByteArrToHexString(objectSid)
For Each e in arrTokens
If UBound(e) = nSidSize Then
If ByteArrToHexString(e) = vSidHex Then
TokenListFindSid = True
Exit For
End If
End If
Next
End If
End Function
' Encode Byte() to hex string
Function ByteArrToHexString(bytes)
Dim i
ByteArrToHexString = ""
For i = 1 to Lenb(bytes)
ByteArrToHexString = ByteArrToHexString & Right("0" & Hex(Ascb(Midb(bytes, i, 1))), 2)
Next
End Function
' Format a DN into a valid LDAP URI
Function LdapUri(DN)
LdapUri = "LDAP://" & Replace(DN, "/", "\/")
End Function
' =====================================
' Query helper routines
' =====================================
' Get object's SID by DN
' Usage: <SID Byte()> = GetSidByDN(<DN>)
Function GetSidByDN(objectDN)
On Error Resume Next
GetSidByDN = GetObject(LdapUri(objectDN)).Get("objectSid")
On Error GoTo 0
End Function
' Get object's SID by ID
' Usage: <SID Byte()> = GetSidByID(<Object ID>)
Function GetSidByID(ID)
Dim rs
Set rs = QueryLDAP(GetRootDN, "(sAMAccountName=" & ID & ")", "objectSid", "subtree")
If Not rs.EOF Then GetSidByID = rs("objectSid")
rs.Close
Set rs = Nothing
End Function
' Get DN by sAMAccountName
' Usage: <DN> = GetDNByID(<User or Group ID>)
Function GetDNByID(ID)
Dim rs
Set rs = QueryLDAP(GetRootDN, "(sAMAccountName=" & ID & ")", "distinguishedName", "subtree")
If Not rs.EOF Then GetDNByID = rs("distinguishedName")
rs.Close
Set rs = Nothing
End Function
' Get sAMAccountName by object's SID
' Usage: <sAMAccountName> = GetIDBySid(<SID Byte()>)
Function GetIDBySid(objectSid)
If TypeName(objectSid) = "Byte()" Then
GetIDBySid = GetObject("LDAP://<SID=" & ByteArrToHexString(objectSid) & ">").Get("sAMAccountName")
End If
End Function
' =====================================
' LDAP routines
' =====================================
' Get Root DN of logged in domain (e.g. DC=yourdomain,DC=com)
' Usage: <DN> = GetRootDN
Function GetRootDN
GetRootDN = GetObject("LDAP://RootDSE").Get("defaultNamingContext")
End Function
' Get/create singleton LDAP ADODB connection object
' Usage: <Connection object ref> = LDAPConnection
' or: LDAPConnection.<property or method>
Dim l_LDAPConnection
Function LDAPConnection
If IsEmpty(l_LDAPConnection) Then
Set l_LDAPConnection = CreateObject("ADODB.Connection")
l_LDAPConnection.Provider = "ADSDSOObject"
l_LDAPConnection.Open "ADs Provider"
End If
Set LDAPConnection = l_LDAPConnection
End Function
' Close the LDAPConnection singleton object
' Usage: CloseLDAPConnection
Sub CloseLDAPConnection
If IsObject(l_LDAPConnection) Then
If l_LDAPConnection.State = 1 Then l_LDAPConnection.Close
End If
l_LDAPConnection = Empty
End Sub
' Query LDAP helper, return RecordSet
' Usage: <RecordSet object ref> = QueryLDAP(<DN>, <LDAP Filter>, <Attributes CSV>, <Scope>
' Scope can be: "subtree", "onelevel", or "base"
' Be sure to close the RecordSet object when done with it
Function QueryLDAP(DN, Filter, AttributeList, Scope)
Set QueryLDAP = LDAPConnection.Execute("<" & LdapUri(DN) & ">;" & Filter & ";" & AttributeList & ";" & Scope)
End FunctionPerl / LDAP script:
#!/bin/perl # # Example code to scan Active Directory for user group membership # using Perl and Net::LDAP. # # Usage: perl ADGroupsExample.pl # # Shawn Poulson, 2009.05.18 # explodingcoder.com # use Net::LDAP; my ($ldap_server, $ldap_username, $ldap_password) = ('<LDAP hostname>', '<service_account>', '<password>'); # Login to LDAP print "Connecting to LDAP..."; my $ldap = Net::LDAP->new($ldap_server, async => 0) or die $@; print "Binding... "; $_ = $ldap->bind($ldap_username, password => $ldap_password) or die $@; print $_->error_text(); # Get currently logged in user my $userDN = GetDNByID($ldap, $ENV{USERNAME}); print "User DN: $userDN\n"; # Quick check if user is a member of a group print "User is a member of 'IT': ", IsMemberOf($ldap, $userDN, GetDNByID($ldap, 'IT')) ? 'True' : 'False', "\n"; print "User is a member of 'Foobar': ", IsMemberOf($ldap, $userDN, GetDNByID($ldap, 'Foobar')) ? 'True' : 'False', "\n"; # Enumerate all member group names my @tkUser = GetTokenGroups($ldap, $userDN); # Get tokens of member groups print "User is a member of ", scalar @tkUser, " groups:\n"; foreach my $tk (@tkUser) { print " ", GetIDBySid($ldap, $tk), "\n"; } # Scan token list for groups my $ITSid = GetSidByID($ldap, 'IT'); my $isMemberIT = !!scalar grep { $ITSid eq $_ } @tkUser; print "User is a member of 'IT': ", $isMemberIT ? 'True' : 'False', "\n"; my $FoobarSid = GetSidByID($ldap, 'Foobar'); my $isMemberFoobar = !!scalar grep { $FoobarSid eq $_ } @tkUser; print "User is a member of 'Foobar': ", $isMemberFoobar ? 'True' : 'False', "\n"; # Done $ldap->unbind; exit; # ===================================== # Token query routines # ===================================== # Is DN a member of security group? # Usage: <bool> = IsMemberOf(<DN of object>, <DN of group>) sub IsMemberOf($$$) { my ($ldap, $objectDN, $groupDN) = @_; return if ($groupDN eq ""); my $groupSid = GetSidByDN($ldap, $groupDN); return if ($groupSid eq ""); my @matches = grep { $_ eq $groupSid } GetTokenGroups($ldap, $objectDN); @matches > 0; } # Gets tokenGroups attribute from the provided DN # Usage: <Array of tokens> = GetTokenGroups(<LDAP ref>, <DN of object>) sub GetTokenGroups($$) { my ($ldap, $objectDN) = @_; my $results = $ldap->search( base => $objectDN, scope => 'base', filter => '(objectCategory=*)', attrs => ['tokenGroups'] ); if ($results->count) { return $results->entry(0)->get_value('tokenGroups'); } } # ===================================== # Query helper routines # ===================================== # Get object's SID by DN # Usage: <SID> = GetSidByDN(<LDAP ref>, <DN>) sub GetSidByDN($$) { my ($ldap, $objectDN) = @_; my $results = $ldap->search( base => $objectDN, scope => 'base', filter => '(objectCategory=*)', attrs => ['objectSid'] ); if ($results->count) { return $results->entry(0)->get_value('objectSid'); } } # Get object's SID by sAMAccountName # Usage: <SID> = GetSidByID(<LDAP ref>, <sAMAccountName>) sub GetSidByID($$) { my ($ldap, $ID) = @_; my $results = $ldap->search( base => GetRootDN($ldap), filter => "(sAMAccountName=$ID)", attrs => ['objectSid'] ); if ($results->count) { return $results->entry(0)->get_value('objectSid'); } } # Get DN by sAMAccountName # Usage: <DN> = GetDNByID(<LDAP ref>, <ID>) sub GetDNByID($$) { my ($ldap, $ID) = @_; my $results = $ldap->search( base => GetRootDN($ldap), filter => "(sAMAccountName=$ID)", attrs => ['distinguishedName'] ); if ($results->count) { return $results->entry(0)->get_value('distinguishedName'); } } # Get sAMAccountName by object's SID # Usage: <ID> = GetIDBySid(<LDAP ref>, <SID>) sub GetIDBySid($$) { my ($ldap, $objectSid) = @_; my $results = $ldap->search( base => '<SID=' . unpack('H*', $objectSid) . '>', scope => 'base', filter => '(objectCategory=*)', attrs => ['sAMAccountName'] ); if ($results->count) { return $results->entry(0)->get_value('sAMAccountName'); } } # ===================================== # LDAP routines # ===================================== # Get Root DN of logged in domain (e.g. DC=yourdomain,DC=com) # Usage: <DN> = GetRootDN(<LDAP ref>) sub GetRootDN($) { my ($ldap) = @_; ($ldap->root_dse->get_value('namingContexts'))[0]; }
| Attachment | Size |
|---|---|
| AD query group membership example code (zip) | 3.7 KB |
Comments
Should have used it
Gee... if we had known how to do this and had used it for testing group membership in SASSy, then we would have a *much* simpler user-groups-and-roles system. It is probably not worth rewriting it now...