How to Query Active Directory Security Group Membership

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:

  • Determine the current logged in user
  • Do a quick group membership check with IsMemberOf()
  • Enumerate all group memberships with GetTokenGroups() and GetIDBySid()
  • Use enumerated SIDs for repeated group membership lookups with GetSidByID()

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:

[code language="vb"]
'
' 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: = IsMemberOf(, )
Function IsMemberOf(dnObject, dnGroup)
IsMemberOf = TokenListFindSid(GetTokenGroups(dnObject), GetSidByDN(dnGroup))
End Function

' Gets tokenGroups attribute from the provided DN
' Usage: = GetTokenGroups()
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: = TokenListFindSid(,

) 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: = GetSidByDN() 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: = GetSidByID() 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: = GetDNByID() 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: = GetIDBySid() Function GetIDBySid(objectSid) If TypeName(objectSid) = "Byte()" Then GetIDBySid = GetObject("LDAP://").Get("sAMAccountName") End If End Function ' ===================================== ' LDAP routines ' ===================================== ' Get Root DN of logged in domain (e.g. DC=yourdomain,DC=com) ' Usage: = GetRootDN Function GetRootDN GetRootDN = GetObject("LDAP://RootDSE").Get("defaultNamingContext") End Function ' Get/create singleton LDAP ADODB connection object ' Usage: = LDAPConnection ' or: LDAPConnection. 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: = QueryLDAP(, , , ' 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 Function [/code]
Perl / LDAP script:
[code language="perl"] #!/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) = ('', '', ''); # 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: = IsMemberOf(, ) 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: = GetTokenGroups(, ) 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: = GetSidByDN(, ) 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: = GetSidByID(, ) 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: = GetDNByID(, ) 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: = GetIDBySid(, ) sub GetIDBySid($$) { my ($ldap, $objectSid) = @_; my $results = $ldap->search( base => '', 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: = GetRootDN() sub GetRootDN($) { my ($ldap) = @_; ($ldap->root_dse->get_value('namingContexts'))[0]; } [/code]
AttachmentSize
AD query group membership example code (zip)3.7 KB

Comments

Thanks for the brilliant

Thanks for the brilliant post. I appreciate your great ideas and knowledge you've shared to us. God Bless and keep on posting. Very interesting topic.

Truly it can help us especially student like me.

Just incase anyone is in the

Just incase anyone is in the situation where they have a SID ( in the form of S-1-5-21-2127521184-1604012920-1887927527-72713) and you would like to get the samAccountName based on that value, I came up with the following (somewhat sloppy) perl code which expands on the example above. I relied heavily on the following website for this:

http://blogs.msdn.com/b/oldnewthing/archive/2004/03/15/89753.aspx

I wrote a method to convert the SID String into a Hex representation, and another method which is an adjusted version of GetIDBySid method (which uses a binary representation of the Sid), to take the Hex representation which is returned by convertSIDStrToHex.

I tested this on an Intel RedHat box, not sure if endian issues will come into play with the hex conversions on sparc Solaris or not.


sub convertSIDStrToHex(){
my ($sidSTR) = @_;

## Split the SID based on dashes
my @SID_PARTS = split(/\-/,$sidSTR);
my $convertedString = "";

## First 2 parts (S and #)
shift(@SID_PARTS);
$convertedString .= sprintf("%02d",hex(shift(@SID_PARTS)));

## Get the number of dashes minus 2
my $dashCount = $sidSTR =~ tr/-/-/;
$dashCount = $dashCount-2;
$convertedString .= sprintf("%02d",hex($dashCount));

## 3rd Part (should b 5)
$convertedString .= sprintf("%012d",sprintf("%X",shift(@SID_PARTS)));

## 4th part should be 21
$convertedString .= unpack("H*",(pack('I*',shift(@SID_PARTS))));

## the rest of the parts
$convertedString .= unpack("H*",(pack('I*',shift(@SID_PARTS))));
$convertedString .= unpack("H*",(pack('I*',shift(@SID_PARTS))));
$convertedString .= unpack("H*",(pack('I*',shift(@SID_PARTS))));
$convertedString .= unpack("H*",(pack('I*',shift(@SID_PARTS))));

return $convertedString;
}

sub GetIDByHexSid(){
my ($ldap,$hexSID) = @_;
my $results = $ldap->search(
base => '',
scope => 'base',
filter => '(objectCategory=*)',
attrs => ['sAMAccountName']
);

if ($results->count) {
return $results->entry(0)->get_value('sAMAccountName');
}
}

my $samAccountName = &GetIDByHexSid(convertSIDStrToHex($ldap,$SID));

Modularized version

Great example Shawn. I was inspired to modularize this Perl example for my own use in a web application.

This was the first Perl module I have written. So it was merely an exercise in forcing myself to learn something new, and to produce something useful and re-usable. If you or your readers, are interested in another approach to the problem, check out the code here.

I basically wanted two methods. The first, $obj->ismember to return a boolean, given any user's SAM or UPN name, and a group name. The second $obj->allgroups returns an array of group names, given a SAM or UPN name. Much thanks for the logic behind accomplishing this task.

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...

About Shawn Poulson / Exploding Coder
Software developer and designer

I'm a software developer in the Philadelphia/Baltimore metro area that loves web technology and creating compelling and useful applications. This blog was created to showcase some of my ideas and share my passion and experiences with others in the same field. Read more...

My portfolio...

Other places you can find me online: