Link to home
Start Free TrialLog in
Avatar of OnError_Fix
OnError_Fix

asked on

Using ActiveDirectory to return a list of all users in a given group

Hi,

I've seen many solutions to this but none actually work for me. All I want is a nice simple function as trying to get my head around the namespace is beginning to wear me down!

Using VB.NET (not C#), I want a function that will accept a string for the group name, and a string for the server. I then want it to connect, and output the list of users in that group as a string (or whatever).

Can anyone help?

Thanks.
Avatar of Howard Cantrell
Howard Cantrell
Flag of United States of America image

Avatar of ihenry
ihenry

Use Directory Services API under System.DirectoryServices namespace is the proper way to access information stored in AD. The basic idea is that each group object in active directory has "member" attribute which stores the list of users that belong to the group.

Given a group name, you can query to AD using DirectorySearcher class to get the group object as follows

    Private m_LdapPath As String = "LDAP://SERVER_NAME/..."
    Private m_AdminAccountName As String = "Domain\UserName"
    Private m_AdminPassword As String = "password"

    Public Function GetUserRoles(ByVal groupName As String) As String()

        Dim filter As String = String.Format("(&(objectCategory=group)(cn={0}))", groupName)
        Dim entry As New DirectoryEntry(m_LdapPath, m_AdminAccountName, m_AdminPassword, AuthenticationTypes.None)
        Dim searcher As New DirectorySearcher(entry, filter)
        Dim users As New ArrayList

        Try

            Dim result As SearchResult = searcher.FindOne()
            Dim member As Object = result.Properties("member")
            If IsNothing(member) Then
                For Each dn As String In CType(member, IEnumerable)
                    users.Add(dn)
                Next
                ' returns string array of users
                Return CType(users.ToArray(GetType(String)), String())
            End If

            ' returns empty string array
            Return New String() {}

        Catch
            Throw
        Finally
            If Not IsNothing(entry) Then
                entry.Dispose()
            End If
            If Not IsNothing(searcher) Then
                searcher.Dispose()
            End If
        End Try

    End Function

For you to read:
connection string format for ldap provider
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/adsi/adsi/ldap_adspath.asp

Notes: the code listed above uses AdminAccountName and AdminPassword password. You can however ignore the two parameters and pass as null (or empty string) value instead and the current logon user credentials will be used by .NET Framework to do the job. For any user account used make sure it has proper permission rights to query and view group information in Active Directory server.
Avatar of OnError_Fix

ASKER

Hi iHenry,

Thanks for your post. This appears to be the closest I have come to getting what I need. That said however, when I run the code, I get an error message:

LabelSystem.Runtime.InteropServices.COMException (0x80072020): An operations error occurred at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail) at System.DirectoryServices.DirectoryEntry.Bind() at System.DirectoryServices.DirectoryEntry.get_AdsObject() at System.DirectoryServices.DirectorySearcher.FindAll(Boolean findMoreThanOne) at System.DirectoryServices.DirectorySearcher.FindOne() at Incorpex.Olympia2004V1.ActiveDirectory.GetUserRoles(String groupName) in C:\Documents and Settings\r.parker\VSWebCache\intranet.domain.net\administration\ActiveDirectory.vb:line 23,

Now --- ActiveDirectory.vb line 23 reads:

Dim result As SearchResult = searcher.FindOne()

And here are the values of the following strings:

Dim m_LdapPath As String = "LDAP://DEV"
Dim m_AdminAccountName As String = ""
Dim m_AdminPassword As String = ""

I have also tried "LDAP://DEV.LOCAL/CN=Users" as well, to produce the same error.

Any ideas?

Thanks.

P.S. Points value increased to 200.


Hi,

I found this code on the web, but alas - it's all in C#. Would this code work if it were converted to VB.NET?

using System;
using System.DirectoryServices;
using System.Runtime.InteropServices;
using System.Reflection;
using activeds; // Import activeds.tlb (%windir%\system32\activeds.tlb)
class Test {
static string bindUser = "administrator"; // binding user with sufficient privileges
static string bindPwd = "hispwd"; // password of the binding user
static void ListUserAndGroups(string machineName)
{
DirectoryEntry _compContainer = new DirectoryEntry("WinNT://" + machineName + ",computer", bindUser, bindPwd);
try
{
foreach(DirectoryEntry de in _compContainer.Children)
{
switch (de.SchemaClassName.ToLower())
{
case "group":
Console.WriteLine("---------- group - {0} ---------", de.Name);
ListMembersInGroup(de.Path);
break;
case "user":
Console.WriteLine("---------- user - {0} ---------", de.Name);
ListUserProp(de.Path);
break;
default:
break;
}
}
}
finally {
_compContainer.Dispose();
}
}

private static void ListMembersInGroup(string dirPath) {
IADsMembers MembersCollection = null;
DirectoryEntry _groupEntry = new DirectoryEntry(dirPath ,bindUser, bindPwd);
try {
// call native method "members" on the IADsGroup COM interface exposed by activeds.dll
IADsGroup gr = _groupEntry.NativeObject as IADsGroup;
MembersCollection = gr.Members();
// or call Invoke on the DirectoryEntry object passing the Method to call as arg.
// cast the retruned object to IADsMembers
// MembersCollection = _groupEntry.Invoke("Members") as IADsMembers;
object[] filter = {"user"};
MembersCollection.Filter = filter;
// enumerate members of collection object that supports the IADsMembers interface
// ADSI provider doesn't support count property!!
try {
foreach (IADsUser member in MembersCollection) {
Console.WriteLine("[{0}]", member.Name);
ListUserProp(member.ADsPath);
}
}
catch (COMException e) {
Console.WriteLine("Error: {0}",e.Message);
}
}
catch (COMException e) {
Console.WriteLine(e.Message);
}
finally {
_groupEntry.Dispose();
}
}
private static void ListUserProp(string dirPath) {
DirectoryEntry userEntry = null;
try {
userEntry = new DirectoryEntry(dirPath,bindUser, bindPwd);
PropertyCollection pcoll = userEntry.Properties;
foreach(string sc in pcoll.PropertyNames)
Console.WriteLine("\t" + sc + "\t" + pcoll[sc].Value);
}
catch (COMException e) {
Console.WriteLine(e.Message);
}
finally
{
userEntry.Dispose();
}
}

public static void Main() {
ListUserAndGroups("scenic");
}
}
>> I have also tried "LDAP://DEV.LOCAL/CN=Users" as well, to produce the same error.
Hm..are you sure the path is correct? do you really have an OU directly under you rootDSE?
I may have this bit wrong then.... I've tried several combinations. It's basically the standard setup straight "out-of-the-box". I seem to be getting confused constructing the LDAP:// string, if that is where the error is?

What would you suggest I try to retrieve (for example) just the users in the Domain Admins group?

Server name: DEV.DOMAIN.LOCAL (dns)

Thanks
Default AD instalation will have this kind of ldap path,

LDAP://<SERVER_NAME>:389/CN=Users,DC=<DOMAIN_NAME>,DC=local

Or you can ignore the SERVER_NAME if you want to do server-less binding. That a common case when web server is part of the AD domain. And port number defaults to 389 (you can ignore this as well).

And user account on which the code is running also determine whether you need to supply user name and password. Are you using ASP.NET or just a console application?
ASP.NET using Windows Forms Authentication.
(Integrated Logon is not enabled).
First, lets do some checking whether you can bind to AD in the first place. Run the code by commenting out line 3 and 4.
Then do the same thing with line 3 and 4.

Dim de As New DirectoryEntry()    ' ln 1
de.Path = ldapPath                      ' ln 2
de.User = userName                   ' ln 3
de.Password = password             ' ln 4
de.AuthenticationType = AuthenticationTypes.None  ' ln 5
de.RefreshCache()                      ' ln 6

Do some combination of AuthenticationTypes enum, use ServerBind if SERVER_NAME is specified in ldapPath. Then, post your ldap path and any error message occurs.
To make thing clearer, when using FormsAuthentication normally anonymous access in IIS is enabled and the app is running under IUSR_MACHINE security context. When enable anonymous access, the app is running under login user security context. So whatever user account used to bind to AD, it must be a domain user and has proper permission rights to view information in AD.
Hi IHenry:

When using a blank username and password, with the LDAP string "" I get:

System.Runtime.InteropServices.COMException (0x80072020): An operations error occurred at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail) at System.DirectoryServices.DirectoryEntry.Bind() at System.DirectoryServices.DirectoryEntry.RefreshCache() at Incorpex.Olympia2004V1.ActiveDirectory.DebugAD() in C:\Documents and Settings\r.parker\VSWebCache\intranet.domain.net\administration\ActiveDirectory.vb:line 25

When using the username and password of a local administrator account on the server (with the SAME LDAP string), I get:

System.Runtime.InteropServices.COMException (0x8007202B): A referral was returned from the server at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail) at System.DirectoryServices.DirectoryEntry.Bind() at System.DirectoryServices.DirectoryEntry.RefreshCache() at Incorpex.Olympia2004V1.ActiveDirectory.DebugAD() in C:\Documents and Settings\r.parker\VSWebCache\intranet.domain.net\administration\ActiveDirectory.vb:line 25

NOTE: Line 25 is equal to Line 6 of the code you sent, that is: de.RefreshCache().

Back to the drawing board?!!!

Note: points increased to 300.
Of course it would help if I actually posted the LDAP String :)

It is:

"LDAP://DEV:389/CN=Users,DC=INCORPEX.LOCAL,DC=local"

;)
Hi iHenry:

Success?!

The following code DOES NOT produce any error:

 Dim ldapPath As String = "LDAP://DEV/CN=Users,DC=xxxx,DC=local"
            Dim username As String = "xxxxx"
            Dim password As String = "xxxxx"

            Dim de As New DirectoryEntry        ' ln 1

            de.Path = ldapPath                  ' ln 2
            de.Username = username              ' ln 3
            de.Password = password             ' ln 4
            de.AuthenticationType = AuthenticationTypes.ServerBind  ' ln 5
            de.RefreshCache()                      ' ln 6

So I think I've found an LDAP String that works. Shall I plug this into the existing code you submitted before?
Ok -- steaming along here.

Plugging the correct LDAP string into the code you gave me before now appears to NOT produce errors, but doesn't return any results.

Any ideas?
Yea, it seems to be working..
Sorry, I was busy today.lots of problems.

One more things, include this line to make sure you can retrieve some attributes from the OU object
Dim dn As String = CType( de.Properties("distinguishedName").Value, String )
Dim wc As DateTime = CType( de.Properties("whenCreated").Value, DateTime )

If everything's fine, use the same ldapPath, userName, password and AuthenticationType to the original code.
Hi iHentry, not to worry about being busy!! We all are!

I've done what you have said, but it doesn't return any users - and doesn't give an error message. Any ideas?
:o)
When do AD programming you might feel get intimidated with the error messages that do not give information about the nature of the failure. That's pretty much all you get with an LDAP bind.
True ---

The problem is no users are returned. Where do we go from here?
Hm..can you do some debugging?

'-> check if the member var is not null (nothing)
Dim member As Object = result.Properties("member")
If IsNothing(member) Then
   '-> check if it gets into the loop
   For Each dn As String In CType(member, IEnumerable)
         users.Add(dn)
   Next
   '-> check number of element of the users array
   ' returns string array of users
   Return CType(users.ToArray(GetType(String)), String())
End If
One question,

are you sure you can find any group under the "Users" OU in “Active Directory Users and Computers” snap-ins?
-> LDAP://DEV/CN=Users,DC=xxxx,DC=local
Hi,

If I change the groupname from "Users" to something else, it throws an error:

There is no such object on the server
at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail) at System.DirectoryServices.DirectoryEntry.Bind() at System.DirectoryServices.DirectoryEntry.RefreshCache() at Incorpex.Olympia2004V1.ActiveDirectory.DebugAD() in C:\Documents and Settings\r.parker\VSWebCache\intranet.domain.net\administration\ActiveDirectory.vb:line 25

Yet there is: I've tried changing the LDAP String to:

"LDAP://DEV/CN=Guests,DC=xxx,DC=local"
"LDAP://DEV/CN=Administrators,DC=xxx,DC=local"
"LDAP://DEV/CN=Domain Admins,DC=xxx,DC=local"

It only wants to execute if CN=Users. But if it does that, nothing is actually returned.

Let's say I wanted to view all the users who were a member of the "Administrators" group....
If I don't remember if there're ldap default path with names
LDAP://DEV/CN=Guests,DC=xxx,DC=local
LDAP://DEV/CN=Administrators,DC=xxx,DC=local or
LDAP://DEV/CN=Domain Admins,DC=xxx,DC=local

I think that should be
LDAP://DEV/CN=USERS,CN=Guests,DC=xxx,DC=local
LDAP://DEV/CN=USERS,CN=Administrators,DC=xxx,DC=local or
LDAP://DEV/CN=USERS,CN=Domain Admins,DC=xxx,DC=local

What's the Administrators group's parent container? sorry, I don't have access to AD now.
You can check using “Active Directory Users and Computers” console. Or do you have other utility to browse active directory object like ADSI viewer or ldap browser for windows (download for free). Such utilities can make your life much easier.
The parent container for the Administrators group is "Builtin". I'm just trying to solve this now using the ldap browser program you suggested.
Hi,

I am increasing the reward for this question to 750 points. I need an answer, and soon!

Points go to s/he who can give me a VB.NET function that has the server to bind to and the group name to find as parameters (strings), and search the Active Directory server for that group, and then return a list of all the group's members.

Thank you.

Note to iHenry: still no joy. I'm not having any luck here.

In ASP classic this was so simple. What happened in .NET?!
Thanks.
Whoops - I cannot increase to 750 so I am setting to the max of 500.
I think you don't get the idea here, query groups to AD via DirectorySearcher should be the easiest way.

This is a user object, not a container object. A user object cannot contain any object and does not have "member" attribute.
LDAP://DEV/CN=Guests,DC=xxx,DC=local

These two are group objects, not container objects. A group object cannot contain any object and has "member" attribute.
LDAP://DEV/CN=Administrators,DC=xxx,DC=local
LDAP://DEV/CN=Domain Admins,DC=xxx,DC=local

When you create a DirectorySearcher object the constructor needs two parameters. First, the DirectoryEntry which represents a node (a container) in the Active Directory hierarchy where the search starts.

Let say you are searching for Administrators group which is under a container with this dn format "CN=BuiltIn, DC=<domain>, DC=", so your ldapPath should looks something like this "LDAP://DEV/CN=BuiltIn,DC=xxx,DC=local"

And the query filter is the search criteria, so "(&(objectCategory=group)(cn=Administrator)" means you are searching in AD for any object with objectCategory as group and has name exactly match to "Administrator".

And finally, this is how to call the function
Dim groups As String = GetUserRoles("Administrators")

Furthermore, path of an AD object actually depends on your AD structure. So knowing your own structure is the first priority here.

Active Directory default groups naming is pretty well documented here..
http://www.microsoft.com/resources/documentation/WindowsServ/2003/all/techref/en-us/Default.asp?url=/Resources/Documentation/windowsserv/2003/all/techref/en-us/w2k3tr_princ_how.asp
Right,

You're right: I don't quite understand the ins and outs of the DirectoryServices namespace.... hence my desire to find an answer here on EE.

I know how to CALL the function. It is the function itself which is returning an error for me. So my question is the same: who can write a simple function that will show me the users in the given group? I have tried the code you have sent me, and quite a few variations on it, and I either get an error message, or no users returned.

If you're able to continue trying I'd appreciate it.

Thank you.

ASKER CERTIFIED SOLUTION
Avatar of ihenry
ihenry

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
iHenry,

No offence was taken so don't worry! I was getting quite frustrated; I thought the code you'd given me was OK but I was doing something wrong! Still, never mind, all that's history now because you've cracked it; it works brilliantly. I will award points to you. Just one mini question (still related) which you may be able to answer.

When the output comes back, it returns the users Full Name instead of their "username". For example:

CN=Test User,OU=HighRisk,DC=xxx,DC=local

"Test User" is actually the full name of the user (the concatenation of Firstname and Lastname), and the username should be "Test.User".

Any ideas?

If not no worries - I really appreciate your help so far.


The return value is actually "distinguishedName" attribute value of each user object.

CN=Test User,OU=HighRisk,DC=xxx,DC=local

The "Test User" is taken from cn attribute, it is not always as full name. That because it depends heavily on how the user object is created. For instance, you create one user object using the “Active Directory Users and Computers” console, the names default as follows. You specify the first name, initials, and last name of the user and it is displayed as <firstname> <initials>. <last name>. The source of confusion is when you create a user object programmatically, it is displayed as the cn attribute's value. You can overwrite this behaviour by set the cn attributes the same way as when you create a user via “Active Directory Users and Computers” console.

And also given a "distinguishedName" attribute you can get the associated object, example

Dim dn As String = "CN=white.trevor,CN=Users,DC=theDomainName,DC=local"
Dim de As New DirectoryEntry()
de.Path = "LDAP://THE_SERVER_NAME/" + dn
de.Username = userName
de.Password = password
de.RefreshCache()

Dim firstName = de.Properties("givenName").Value
Dim lastName = de.Properties("sn").Value

Just concatenate the two string whatever you like.
At long last!

It's here. And it works. Thanks to the very kind member iHenry and all the support given to me from EE, I am posting here my final solution to the initial problem for the world to see. It's basically two functions. One which iterates through ActiveDirectory to return all the objects within a given DN. The second, for each DN given to it, binds to it, checks to see if it is a user, and then adds it to a list which can be used elsewhere.

Triumph!

Enjoy the code :)

Public Function GetUserGroups(ByVal groupName As String)

        Dim ldapPath = "LDAP://" & ServerName & "/CN=" & CN & ",DC=" & DomainName & ",DC=" & DomainSuffix
        Dim filter As String = String.Format("(&(objectCategory=group)(cn={0}))", groupName)
        Dim entry As New DirectoryEntry(ldapPath, adminUserName, adminPassword, AuthenticationTypes.None)
        Dim searcher As New DirectorySearcher(entry, filter)
        Dim users As New ArrayList

        Try

            Dim result As SearchResult = searcher.FindOne()
            Dim member As Object = result.Properties("member")
            If Not IsNothing(member) Then
                For Each dn As String In CType(member, IEnumerable)
                    Dim sAMAccountName As String = GetAccountName(dn)
                    If Len(sAMAccountName) > 0 Then
                        users.Add(sAMAccountName)
                    End If
                Next
                ' returns string array of users
                Return CType(users.ToArray(GetType(String)), String())
            End If


        Catch e As Exception
            Return e.ToString

        End Try

        entry.Dispose()
        searcher.Dispose()


    End Function


    Public Function GetAccountName(ByVal DN As String)

        Dim strOutput As String

        Dim DE As New DirectoryEntry
        DE.Path = "LDAP://" & ServerName & "/" & DN
        DE.Username = AdminUsername
        DE.Password = AdminPassword
        DE.RefreshCache()

        Try

            For Each DV As String In CType(DE.Properties("objectClass"), IEnumerable)
                If DV = "user" Then
                    strOutput = DE.Properties("sAMAccountName").Value
                End If
            Next

            If Not Len(strOutput) > 0 Then
                Exit Function
            End If

        Catch ex As Exception
            'TODO: Raise an error
            strOutput = "There was an exception of some kind. It was: " & ex.ToString
        End Try

        Return strOutput

        DE.Dispose()


    End Function
you are very welcome :0) and well done!

just two very trivial suggestions to the GetAccountName function,
put the DE.RefreshCache() method call in the try and catch block and use DE.SchemaClassName property to get the object class name.

iHenry, you deserve a medal mate :)

I've taken your suggestions and reorganised the function. It now reads:

Public Function GetAccountName(ByVal DN As String)

        Dim strOutput As String

        Dim DE As New DirectoryEntry
        DE.Path = "LDAP://" & ServerName & "/" & DN
        DE.Username = AdminUsername
        DE.Password = AdminPassword


        Try
            DE.RefreshCache()

            If DE.SchemaClassName = "user" Then
                strOutput = DE.Properties("sAMAccountName").Value
            End If

            If Not Len(strOutput) > 0 Then
                Exit Function
            End If

        Catch ex As Exception
            'TODO: Raise an error
            strOutput = "There was an exception and I died. It was: " & ex.ToString
        End Try

        Return strOutput

        DE.Dispose()


    End Function

And it even executes slightly faster, too.

NICE ONE!!

Thanks again.
This is a nice clean way to find them
Public Shared Function ADAllUsers(ByVal strGroupName As String) As ArrayList
        ' ** This will return Active Directory Info for a ALL USERS belonging to a GROUP
        Dim adDS As DirectorySearcher = New DirectorySearcher("LDAP://yourdomain.local") With {.Filter = "(&(objectClass=group)(cn=" & strGroupName & "))"}
        Dim results As DirectoryServices.SearchResultCollection = adDS.FindAll()
        Dim result As DirectoryServices.SearchResult
        Dim WorkList As New ArrayList
        If (results.Count > 0) Then
            Dim strManipulateName As String
            Dim intConstLocation As Int16
            For Each result In results
                For Each member As String In result.Properties("member")
                    strManipulateName = member.Replace("CN=", "").Replace("\", "")
                    intConstLocation = InStr(strManipulateName, "OU=", CompareMethod.Text)
                    strManipulateName = Mid(strManipulateName, 1, intConstLocation - 2)
                    WorkList.Add(strManipulateName)
                Next
            Next
        Else
            WorkList.Add("NOTHING FOUND!!")
        End If
        WorkList.Sort()
        Return WorkList
    End Function