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:

'
' 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 Function

Perl / 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];
}
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 => '<SID=' . $hexSID . '>',
      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: