Related
I am trying to build a collection of all local groups and their respected members, however the challenge I am having with the listed code bellow is that "Administrators" group members is empty, all other groups on the server returns their members just not administrators. any ideas?
private void BuildGroupMembership(string remoteHost, string targetdomain, string userName, string password, bool domainOnly)
{
var groupsList = new List<string>();
PrincipalContext pContext = null;
PrincipalContext searchContext = null;
if (string.IsNullOrEmpty(remoteHost))
{
remoteHost = Environment.MachineName;
}
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
{
pContext = new PrincipalContext(ContextType.Machine, remoteHost, null, ContextOptions.Negotiate, userName, password);
searchContext = new PrincipalContext(ContextType.Domain, targetdomain, null, ContextOptions.Negotiate, userName, password);
}
else
{
pContext = new PrincipalContext(ContextType.Machine, remoteHost, null, ContextOptions.Negotiate);
searchContext = new PrincipalContext(ContextType.Domain, targetdomain, null, ContextOptions.Negotiate);
}
try
{
var pSearcher = new PrincipalSearcher(new GroupPrincipal(pContext));
foreach (var principal in pSearcher.FindAll().Where(principal => !groupsList.Contains(principal.Name))) groupsList.Add(principal.Name);
foreach (var group in groupsList)
try
{
var groupItem = new Group {GroupName = group};
Groups.Add(groupItem);
var grp = GroupPrincipal.FindByIdentity(pContext, group);
if (grp != null)
{
var allmembers = grp.GetMembers(false).ToList();
var members = domainOnly ? allmembers.Where(x => x.ContextType == ContextType.Domain).ToList() : allmembers.ToList();
foreach (var p in members)
try
{
var adGroup = GroupPrincipal.FindByIdentity(searchContext, IdentityType.Sid, p.Sid.Value);
if (adGroup != null)
{
groupItem.GroupMembers.Add(new GroupMember
{
MemberDomain = adGroup.DistinguishedName.Substring(adGroup.DistinguishedName.IndexOf("DC="), adGroup.DistinguishedName.Length - adGroup.DistinguishedName.IndexOf("DC=")).Replace("DC=", "").Replace(",", "."),
MemberName = p.SamAccountName,
MemberSID = p.Sid.ToString(),
IsGroup = true
});
continue;
}
var adUser = UserPrincipal.FindByIdentity(searchContext, IdentityType.Sid, p.Sid.ToString());
if (adUser != null)
{
groupItem.GroupMembers.Add(new GroupMember
{
MemberDomain = adUser.DistinguishedName.Substring(adUser.DistinguishedName.IndexOf("DC="), adUser.DistinguishedName.Length - adUser.DistinguishedName.IndexOf("DC=")).Replace("DC=", "").Replace(",", "."),
MemberName = p.SamAccountName,
MemberSID = p.Sid.ToString(),
IsGroup = false
});
}
}
catch
{
// ignored
}
grp.Dispose();
}
}
catch
{
}
pContext.Dispose();
searchContext.Dispose();
}
catch (COMException ex)
{
throw new AuthenticationException(ex.Message);
}
}
This doen't answer completely your question but might help you. Using WMI is so much faster than using PrincipalContext, ... (at least in my case). In my app only Administrators and Users where needed.
static Regex partComponentRegex = new Regex("^[^:]+:Win32_UserAccount.Domain=\"(?<Domain>.+?)\",Name=\"(?<Name>.+?)\"$");
static IEnumerable<User> GetUsersFromSidType(WellKnownSidType wellKnownSidType)
{
string gName = GetGroupName(wellKnownSidType);
using (ManagementObjectSearcher groupSearcher = new ManagementObjectSearcher(
string.Format("SELECT * FROM Win32_GroupUser WHERE GroupComponent =\"Win32_Group.Domain='{0}',Name='{1}'\"",
Environment.MachineName,
gName)))
{
foreach (var group in groupSearcher.Get())
{
Match m = partComponentRegex.Match(group["PartComponent"].ToString());
if (m.Success)
{
using (ManagementObjectSearcher userSearcher = new ManagementObjectSearcher(
string.Format("SELECT * FROM Win32_UserAccount WHERE Name='{0}' AND Domain='{1}'",
m.Groups["Name"], m.Groups["Domain"])))
{
foreach (var user in userSearcher.Get())
{
yield return new User()
{
Disabled = (bool)user["Disabled"],
Domain = user["Domain"].ToString(),
FullName = user["FullName"].ToString(),
Name = user["Name"].ToString(),
SID = user["SID"].ToString()
};
}
}
}
}
}
}
static string GetGroupName(WellKnownSidType wellKnownSidType)
{
SecurityIdentifier sid = new SecurityIdentifier(wellKnownSidType, null);
using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(
string.Format("SELECT * FROM Win32_Group WHERE SID='{0}'",
sid.Value)))
{
var e = searcher.Get().GetEnumerator();
if (e.MoveNext())
return e.Current["Name"].ToString();
return null;
}
}
I am working on a module where I need to fetch members of an Active Directory group. This functionality already exists in the project but it was built for .Net3.5. The same is not working for .Net4.5. After some googling I found that I need to use "Principal Context" object to get the Directory entry object.
The problem here is, I need to do the testing in Test AD, which is different from my production AD.
The old way I used was allowing me to specify the test AD server path,
DirectoryEntry entry = new DirectoryEntry(ADLdapPath, ADAdminUser, ADAdminPassword, AuthenticationTypes.Secure);
Can anyone please help me find a way to specify LDAP path(AD server path) while creating "Principal Context" so that I can do the testing in Test environment.
I've used the following helper (modified) which is part of my AD tool belt to create PrincipalContext for working with AD. This should get you started. Modify it to suit your needs. Hope it helps.
public class ADHelper {
public static PrincipalContext CreatePrincipalContext(string domain = null) {
string container = null;
if (IsNullOrWhiteSpace(domain)) {
domain = GetCurrentDnsSuffix();
if (domain != null && domain.EndsWith(".com", StringComparison.InvariantCultureIgnoreCase)) {
container = GetContainers(domain);
} else {
domain = null;
}
}
var hostName = GetHostName();
if (IsNullOrWhiteSpace(domain)) {
domain = hostName;
}
ContextType contextType;
if (domain.Equals(hostName, StringComparison.InvariantCultureIgnoreCase) &&
domain.Equals(Environment.MachineName, StringComparison.InvariantCultureIgnoreCase)) {
contextType = ContextType.Machine;
} else {
contextType = ContextType.Domain;
}
PrincipalContext principalContext = null;
if (contextType == ContextType.Machine) {
principalContext = new PrincipalContext(contextType, domain);
} else {
principalContext = new PrincipalContext(contextType, domain, container, Constants.LDAPUser, Constants.LDAPPassword);
}
return principalContext;
}
public static string GetCurrentDnsSuffix() {
string dnsHostName = null;
if (NetworkInterface.GetIsNetworkAvailable()) {
var nics = NetworkInterface.GetAllNetworkInterfaces()
.Where(ni => ni.OperationalStatus == OperationalStatus.Up);
foreach (var ni in nics) {
var networkConfiguration = ni.GetIPProperties();
var dnsSuffix = networkConfiguration.DnsSuffix;
if (dnsSuffix != null) {
dnsHostName = dnsSuffix;
break;
}
var address = networkConfiguration.DnsAddresses.FirstOrDefault();
if (address != null) {
try {
var dnsHost = Dns.GetHostEntry(address.ToString());
dnsHostName = dnsHost.HostName;
} catch (System.Net.Sockets.SocketException e) {
traceError(e);
} catch (Exception e) {
traceError(e);
}
}
}
}
return dnsHostName;
}
private static string GetContainers(string ADServer) {
string[] LDAPDC = ADServer.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < LDAPDC.GetUpperBound(0) + 1; i++) {
LDAPDC[i] = string.Format("DC={0}", LDAPDC[i]);
}
String ldapdomain = Join(",", LDAPDC);
return ldapdomain;
}
public static string GetHostName() {
var ipProperties = IPGlobalProperties.GetIPGlobalProperties();
return ipProperties.HostName;
}
}
I can then use it in something like this
public static List<string> GetAllUserNames(string domain = null) {
List<string> userNames = new List<string>();
using (var principalContext = createPrincipalContext(domain)) {
//Get a list of user names in MyDomain that match filter
using (UserPrincipal userPrincipal = new UserPrincipal(principalContext)) {
using (PrincipalSearcher principalSearcher = new PrincipalSearcher(userPrincipal)) {
var results = principalSearcher
.FindAll()
.Where(c =>
(c is UserPrincipal) &&
(c as UserPrincipal).Enabled.GetValueOrDefault(false) &&
!string.IsNullOrEmpty(c.DisplayName)
);
foreach (UserPrincipal p in results) {
var temp = p.StructuralObjectClass;
string value = string.Format("{0} ({1})", p.DisplayName, p.EmailAddress ?? Join("\\", p.Context.Name, p.SamAccountName));
userNames.Add(value);
}
}
}
}
return userNames;
}
Basically I found a post that has a solution for a problem we are having in our application and the solution was:
private static void listGroupMembers(string groupDistinguishedName, PrincipalContext ctx, List<UserPrincipal> users)
{
DirectoryEntry group = new DirectoryEntry("LDAP://" + groupDistinguishedName);
foreach (string dn in group.Properties["member"])
{
DirectoryEntry gpMemberEntry = new DirectoryEntry("LDAP://" + dn);
System.DirectoryServices.PropertyCollection userProps = gpMemberEntry.Properties;
object[] objCls = (userProps["objectClass"].Value) as object[];
if (objCls.Contains("group"))
listGroupMembers(userProps["distinguishedName"].Value as string, ctx, users);
if (!objCls.Contains("foreignSecurityPrincipal"))
{
UserPrincipal u = UserPrincipal.FindByIdentity(ctx, IdentityType.DistinguishedName, dn);
if(u!=null) // u==null for any other types except users
users.Add(u);
}
}
}
However I am trying to search a Local group so if I change the directory entry to say:
DirectoryEntry groupEntry =
new DirectoryEntry(string.Format("WinNT://{0}/{1},group", Environment.MachineName, groupName));
Then it doesn't work and it says that the property doesn't exist. How can I do the above but for a local group and user?
Basically to fix this I ended up doing:
protected bool IsUserInLocalGroup(string userName, string group)
{
using (DirectoryEntry computerEntry = new DirectoryEntry("WinNT://{0},computer".FormatWith(Environment.MachineName)))
using(DirectoryEntry groupEntry = computerEntry.Children.Find(group, "Group"))
{
foreach (object o in (IEnumerable)groupEntry.Invoke("Members"))
{
using (DirectoryEntry entry = new DirectoryEntry(o))
{
if (entry.SchemaClassName.Equals("User", StringComparison.OrdinalIgnoreCase) && entry.Name.Equals(userName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
}
I checked this post already. But it doesn't answer my question. I want to get all the active directory groups in which a particular user is a member.
I've written the following code. But I'm not able to proceed further as I don't know how to give the filter and how to access the properties.
class Program
{
static void Main(string[] args)
{
DirectoryEntry de = new DirectoryEntry("LDAP://mydomain.com");
DirectorySearcher searcher = new DirectorySearcher(de);
searcher.Filter = "(&(ObjectClass=group))";
searcher.PropertiesToLoad.Add("distinguishedName");
searcher.PropertiesToLoad.Add("sAMAccountName");
searcher.PropertiesToLoad.Add("name");
searcher.PropertiesToLoad.Add("objectSid");
SearchResultCollection results = searcher.FindAll();
int i = 1;
foreach (SearchResult res in results)
{
Console.WriteLine("Result" + Convert.ToString(i++));
DisplayProperties("distinguishedName", res);
DisplayProperties("sAMAccouontName", res);
DisplayProperties("name", res);
DisplayProperties("objectSid", res);
Console.WriteLine();
}
Console.ReadKey();
}
private static void DisplayProperties(string property, SearchResult res)
{
Console.WriteLine("\t" + property);
ResultPropertyValueCollection col = res.Properties[property];
foreach (object o in col)
{
Console.WriteLine("\t\t" + o.ToString());
}
}
}
Any ideas?
You should use System.DirectoryServices.AccountManagement. It's much easier. Here is a nice code project article giving you an overview on all the classes in this DLL.
As you pointed out, your current approach doesn't find out the primary group. Actually, it's much worse than you thought. There are some more cases that it doesn't work, like the domain local group from another domain. You can check here for details. Here is how the code looks like if you switch to use System.DirectoryServices.AccountManagement. The following code can find the immediate groups this user assigned to, which includes the primary group.
UserPrincipal user = UserPrincipal.FindByIdentity(new PrincipalContext (ContextType.Domain, "mydomain.com"), IdentityType.SamAccountName, "username");
foreach (GroupPrincipal group in user.GetGroups())
{
Console.Out.WriteLine(group);
}
Use tokenGroups:
DirectorySearcher ds = new DirectorySearcher();
ds.Filter = String.Format("(&(objectClass=user)(sAMAccountName={0}))", username);
SearchResult sr = ds.FindOne();
DirectoryEntry user = sr.GetDirectoryEntry();
user.RefreshCache(new string[] { "tokenGroups" });
for (int i = 0; i < user.Properties["tokenGroups"].Count; i++) {
SecurityIdentifier sid = new SecurityIdentifier((byte[]) user.Properties["tokenGroups"][i], 0);
NTAccount nt = (NTAccount)sid.Translate(typeof(NTAccount));
//do something with the SID or name (nt.Value)
}
Note: this only gets security groups
Just query the "memberOf" property and iterate though the return, example:
search.PropertiesToLoad.Add("memberOf");
StringBuilder groupNames = new StringBuilder(); //stuff them in | delimited
SearchResult result = search.FindOne();
int propertyCount = result.Properties["memberOf"].Count;
String dn;
int equalsIndex, commaIndex;
for (int propertyCounter = 0; propertyCounter < propertyCount;
propertyCounter++)
{
dn = (String)result.Properties["memberOf"][propertyCounter];
equalsIndex = dn.IndexOf("=", 1);
commaIndex = dn.IndexOf(",", 1);
if (-1 == equalsIndex)
{
return null;
}
groupNames.Append(dn.Substring((equalsIndex + 1),
(commaIndex - equalsIndex) - 1));
groupNames.Append("|");
}
return groupNames.ToString();
This just stuffs the group names into the groupNames string, pipe delimited, but when you spin through you can do whatever you want with them
This code works even faster (two 1.5 faster than my previous version):
public List<String> GetUserGroups(WindowsIdentity identity)
{
List<String> groups = new List<String>();
String userName = identity.Name;
int pos = userName.IndexOf(#"\");
if (pos > 0) userName = userName.Substring(pos + 1);
PrincipalContext domain = new PrincipalContext(ContextType.Domain, "riomc.com");
UserPrincipal user = UserPrincipal.FindByIdentity(domain, IdentityType.SamAccountName, userName); // NGeodakov
DirectoryEntry de = new DirectoryEntry("LDAP://RIOMC.com");
DirectorySearcher search = new DirectorySearcher(de);
search.Filter = "(&(objectClass=group)(member=" + user.DistinguishedName + "))";
search.PropertiesToLoad.Add("cn");
search.PropertiesToLoad.Add("samaccountname");
search.PropertiesToLoad.Add("memberOf");
SearchResultCollection results = search.FindAll();
foreach (SearchResult sr in results)
{
GetUserGroupsRecursive(groups, sr, de);
}
return groups;
}
public void GetUserGroupsRecursive(List<String> groups, SearchResult sr, DirectoryEntry de)
{
if (sr == null) return;
String group = (String)sr.Properties["cn"][0];
if (String.IsNullOrEmpty(group))
{
group = (String)sr.Properties["samaccountname"][0];
}
if (!groups.Contains(group))
{
groups.Add(group);
}
DirectorySearcher search;
SearchResult sr1;
String name;
int equalsIndex, commaIndex;
foreach (String dn in sr.Properties["memberof"])
{
equalsIndex = dn.IndexOf("=", 1);
if (equalsIndex > 0)
{
commaIndex = dn.IndexOf(",", equalsIndex + 1);
name = dn.Substring(equalsIndex + 1, commaIndex - equalsIndex - 1);
search = new DirectorySearcher(de);
search.Filter = "(&(objectClass=group)(|(cn=" + name + ")(samaccountname=" + name + ")))";
search.PropertiesToLoad.Add("cn");
search.PropertiesToLoad.Add("samaccountname");
search.PropertiesToLoad.Add("memberOf");
sr1 = search.FindOne();
GetUserGroupsRecursive(groups, sr1, de);
}
}
}
This is how I list all the groups (direct and indirect) for a specific Distinguished Name:
The string 1.2.840.113556.1.4.1941 specifies LDAP_MATCHING_RULE_IN_CHAIN.
This rule is limited to filters that apply to the DN. This is a special "extended" match operator that walks the chain of ancestry in objects all the way to the root until it finds a match.
This method is 25 times faster than the UserPrincipal.GetGroups() method in my testing.
Note: The primary group (typically Domain Users) is not returned by this or GetGroups() method. To get the primary group name too, I've confirmed this method works.
Additionally, I found this list of LDAP filters extremely useful.
private IEnumerable<string> GetGroupsForDistinguishedName(DirectoryEntry domainDirectoryEntry, string distinguishedName)
{
var groups = new List<string>();
if (!string.IsNullOrEmpty(distinguishedName))
{
var getGroupsFilterForDn = $"(&(objectCategory=group)(member:1.2.840.113556.1.4.1941:={distinguishedName}))";
using (DirectorySearcher dirSearch = new DirectorySearcher(domainDirectoryEntry))
{
dirSearch.Filter = getGroupsFilterForDn;
dirSearch.PropertiesToLoad.Add("name");
using (var results = dirSearch.FindAll())
{
foreach (SearchResult result in results)
{
if (result.Properties.Contains("name"))
groups.Add((string)result.Properties["name"][0]);
}
}
}
}
return groups;
}
The following example is from the Code Project article, (Almost) Everything In Active Directory via C#:
// userDn is a Distinguished Name such as:
// "LDAP://CN=Joe Smith,OU=Sales,OU=domain,OU=com"
public ArrayList Groups(string userDn, bool recursive)
{
ArrayList groupMemberships = new ArrayList();
return AttributeValuesMultiString("memberOf", userDn,
groupMemberships, recursive);
}
public ArrayList AttributeValuesMultiString(string attributeName,
string objectDn, ArrayList valuesCollection, bool recursive)
{
DirectoryEntry ent = new DirectoryEntry(objectDn);
PropertyValueCollection ValueCollection = ent.Properties[attributeName];
IEnumerator en = ValueCollection.GetEnumerator();
while (en.MoveNext())
{
if (en.Current != null)
{
if (!valuesCollection.Contains(en.Current.ToString()))
{
valuesCollection.Add(en.Current.ToString());
if (recursive)
{
AttributeValuesMultiString(attributeName, "LDAP://" +
en.Current.ToString(), valuesCollection, true);
}
}
}
}
ent.Close();
ent.Dispose();
return valuesCollection;
}
Just call the Groups method with the Distinguished Name for the user, and pass in the bool flag to indicate if you want to include nested / child groups memberships in your resulting ArrayList:
ArrayList groups = Groups("LDAP://CN=Joe Smith,OU=Sales,OU=domain,OU=com", true);
foreach (string groupName in groups)
{
Console.WriteLine(groupName);
}
If you need to do any serious level of Active Directory programming in .NET I highly recommend bookmarking & reviewing the Code Project article I mentioned above.
Here is the code that worked for me:
public ArrayList GetBBGroups(WindowsIdentity identity)
{
ArrayList groups = new ArrayList();
try
{
String userName = identity.Name;
int pos = userName.IndexOf(#"\");
if (pos > 0) userName = userName.Substring(pos + 1);
PrincipalContext domain = new PrincipalContext(ContextType.Domain, "riomc.com");
UserPrincipal user = UserPrincipal.FindByIdentity(domain, IdentityType.SamAccountName, userName);
DirectoryEntry de = new DirectoryEntry("LDAP://RIOMC.com");
DirectorySearcher search = new DirectorySearcher(de);
search.Filter = "(&(objectClass=group)(member=" + user.DistinguishedName + "))";
search.PropertiesToLoad.Add("samaccountname");
search.PropertiesToLoad.Add("cn");
String name;
SearchResultCollection results = search.FindAll();
foreach (SearchResult result in results)
{
name = (String)result.Properties["samaccountname"][0];
if (String.IsNullOrEmpty(name))
{
name = (String)result.Properties["cn"][0];
}
GetGroupsRecursive(groups, de, name);
}
}
catch
{
// return an empty list...
}
return groups;
}
public void GetGroupsRecursive(ArrayList groups, DirectoryEntry de, String dn)
{
DirectorySearcher search = new DirectorySearcher(de);
search.Filter = "(&(objectClass=group)(|(samaccountname=" + dn + ")(cn=" + dn + ")))";
search.PropertiesToLoad.Add("memberof");
String group, name;
SearchResult result = search.FindOne();
if (result == null) return;
group = #"RIOMC\" + dn;
if (!groups.Contains(group))
{
groups.Add(group);
}
if (result.Properties["memberof"].Count == 0) return;
int equalsIndex, commaIndex;
foreach (String dn1 in result.Properties["memberof"])
{
equalsIndex = dn1.IndexOf("=", 1);
if (equalsIndex > 0)
{
commaIndex = dn1.IndexOf(",", equalsIndex + 1);
name = dn1.Substring(equalsIndex + 1, commaIndex - equalsIndex - 1);
GetGroupsRecursive(groups, de, name);
}
}
}
I measured it's performance in a loop of 200 runs against the code that uses the AttributeValuesMultiString recursive method; and it worked 1.3 times faster.
It might be so because of our AD settings. Both snippets gave the same result though.
I would like to say that Microsoft LDAP has some special ways to search recursively for all of memberships of a user.
The Matching Rule you can specify for the "member" attribute. In particular, using the Microsoft Exclusive LDAP_MATCHING_RULE_IN_CHAIN rule for "member" attribute allows recursive/nested membership searching. The rule is used when you add it after the member attribute. Ex. (member:1.2.840.113556.1.4.1941:= XXXXX )
For the same Domain as the Account, The filter can use <SID=S-1-5-21-XXXXXXXXXXXXXXXXXXXXXXX> instead of an Accounts DistinguishedName attribute which is very handy to use cross domain if needed. HOWEVER it appears you need to use the ForeignSecurityPrincipal <GUID=YYYY> as it will not resolve your SID as it appears the <SID=> tag does not consider ForeignSecurityPrincipal object type. You can use the ForeignSecurityPrincipal DistinguishedName as well.
Using this knowledge, you can LDAP query those hard to get memberships, such as the "Domain Local" groups an Account is a member of but unless you looked at the members of the group, you wouldn't know if user was a member.
//Get Direct+Indirect Memberships of User (where SID is XXXXXX)
string str = "(& (objectCategory=group)(member:1.2.840.113556.1.4.1941:=<SID=XXXXXX>) )";
//Get Direct+Indirect **Domain Local** Memberships of User (where SID is XXXXXX)
string str2 = "(& (objectCategory=group)(|(groupType=-2147483644)(groupType=4))(member:1.2.840.113556.1.4.1941:=<SID=XXXXXX>) )";
//TAA DAA
Feel free to try these LDAP queries after substituting the SID of a user you want to retrieve all group memberships of. I figure this is similiar if not the same query as what the PowerShell Command Get-ADPrincipalGroupMembership uses behind the scenes. The command states "If you want to search for local groups in another domain, use the ResourceContextServer parameter to specify the alternate server in the other domain."
If you are familiar enough with C# and Active Directory, you should know how to perform an LDAP search using the LDAP queries provided.
Additional Documentation:
<SID> Binding String
<GUID> Binding String
If you have a LDAP connection with a username and password to connect to Active Directory, here is the code I used to connect properly:
using System.DirectoryServices.AccountManagement;
// ...
// Connection information
var connectionString = "LDAP://domain.com/DC=domain,DC=com";
var connectionUsername = "your_ad_username";
var connectionPassword = "your_ad_password";
// Get groups for this user
var username = "myusername";
// Split the LDAP Uri
var uri = new Uri(connectionString);
var host = uri.Host;
var container = uri.Segments.Count() >=1 ? uri.Segments[1] : "";
// Create context to connect to AD
var princContext = new PrincipalContext(ContextType.Domain, host, container, connectionUsername, connectionPassword);
// Get User
UserPrincipal user = UserPrincipal.FindByIdentity(princContext, IdentityType.SamAccountName, username);
// Browse user's groups
foreach (GroupPrincipal group in user.GetGroups())
{
Console.Out.WriteLine(group.Name);
}
there is a helpers class based in curtisk response:
public static class ActiveDirectoryHelpers
{
private static readonly Regex keyValuePair = new Regex($"(?<key>[^=]+)=(?<value>[^,]+),?");
public enum X500DirectorySpecification
{
/// <summary>Common Name</summary>
CN,
/// <summary>Organizational Unit</summary>
OU,
/// <summary>Domain Component</summary>
DC
}
public static IEnumerable<string> GetUserMemberOfNodeValue(this PrincipalContext principalContext, string userName, X500DirectorySpecification node)
{
return principalContext.GetUserMemberOf(userName)
.SelectMany(memberOf =>
GetUserMemberOfKeyValues(memberOf).Where(item => item.Key == node.ToString()).Select(item => item.Value));
}
private static IEnumerable<string> GetUserMemberOf(this PrincipalContext principalContext, string userName)
{
using var user = UserPrincipal.FindByIdentity(principalContext, userName);
IEnumerable<string> result = null;
if (user != null)
{
var directoryEntry = (DirectoryEntry)user.GetUnderlyingObject();
var directorySearcher = new DirectorySearcher(directoryEntry);
directorySearcher.PropertiesToLoad.Add("memberOf");
result = directorySearcher.FindOne().Properties["memberOf"].Cast<string>();
}
return result ?? Enumerable.Empty<string>();
}
private static IEnumerable<KeyValuePair<string, string>> GetUserMemberOfKeyValues(string memberOfValue)
{
return keyValuePair.Matches(memberOfValue).OfType<Match>()
.Select(item => new KeyValuePair<string, string>(item.Groups["key"].Value.Trim(), item.Groups["value"].Value));
}
}
PrincipalContext pc1 = new PrincipalContext(ContextType.Domain, "DomainName", UserAccountOU, UserName, Password);
UserPrincipal UserPrincipalID = UserPrincipal.FindByIdentity(pc1, IdentityType.SamAccountName, UserID);
searcher.Filter = "(&(ObjectClass=group)(member = " + UserPrincipalID.DistinguishedName + "));
Given a group like this in Active Directory:
MainGroup
GroupA
User1
User2
GroupB
User3
User4
I can easily determine if User3 is member of MainGroup or any of its subgroups with code like this:
using System;
using System.DirectoryServices;
static class Program {
static void Main() {
DirectoryEntry user = new DirectoryEntry("LDAP://CN=User3,DC=X,DC=y");
string filter = "(memberOf:1.2.840.113556.1.4.1941:=CN=MainGroup,DC=X,DC=y)";
DirectorySearcher searcher = new DirectorySearcher(user, filter);
searcher.SearchScope = SearchScope.Subtree;
var r = searcher.FindOne();
bool isMember = (r != null);
}
}
I would like to know if there is a similar way to get all the users that are member of a group or any of its subgroups, i.e. in the example for MainGroup get User1, User2, User3 and User4.
The obvious way of getting all the users is to recursively query each subgroup, but I was wondering if there is an easier way to do it.
Using the same approach with the memberOf:1.2.840.113556.1.4.1941: filter, but using the domain root instead of the user as a search base is not feasible, as the query takes too long (probably it computes all the group memberships recursively for all users in the domain and checks if they are member of the given group).
Which is the best way to get all members of a group, including its subgroups?
Just in case this might benefit someone else: here is the solution I ended up with. It is just a recursive search, with some extra checks to avoid checking the same group or user twice, e.g. if groupA is member of groupB and groupB is member of groupA or a user is member of more than one group.
using System;
using System.DirectoryServices;
using System.Collections.Generic;
static class Program {
static IEnumerable<SearchResult> GetMembers(DirectoryEntry searchRoot, string groupDn, string objectClass) {
using (DirectorySearcher searcher = new DirectorySearcher(searchRoot)) {
searcher.Filter = "(&(objectClass=" + objectClass + ")(memberOf=" + groupDn + "))";
searcher.PropertiesToLoad.Clear();
searcher.PropertiesToLoad.AddRange(new string[] {
"objectGUID",
"sAMAccountName",
"distinguishedName"});
searcher.Sort = new SortOption("sAMAccountName", SortDirection.Ascending);
searcher.PageSize = 1000;
searcher.SizeLimit = 0;
foreach (SearchResult result in searcher.FindAll()) {
yield return result;
}
}
}
static IEnumerable<SearchResult> GetUsersRecursively(DirectoryEntry searchRoot, string groupDn) {
List<string> searchedGroups = new List<string>();
List<string> searchedUsers = new List<string>();
return GetUsersRecursively(searchRoot, groupDn, searchedGroups, searchedUsers);
}
static IEnumerable<SearchResult> GetUsersRecursively(
DirectoryEntry searchRoot,
string groupDn,
List<string> searchedGroups,
List<string> searchedUsers) {
foreach (var subGroup in GetMembers(searchRoot, groupDn, "group")) {
string subGroupName = ((string)subGroup.Properties["sAMAccountName"][0]).ToUpperInvariant();
if (searchedGroups.Contains(subGroupName)) {
continue;
}
searchedGroups.Add(subGroupName);
string subGroupDn = ((string)subGroup.Properties["distinguishedName"][0]);
foreach (var user in GetUsersRecursively(searchRoot, subGroupDn, searchedGroups, searchedUsers)) {
yield return user;
}
}
foreach (var user in GetMembers(searchRoot, groupDn, "user")) {
string userName = ((string)user.Properties["sAMAccountName"][0]).ToUpperInvariant();
if (searchedUsers.Contains(userName)) {
continue;
}
searchedUsers.Add(userName);
yield return user;
}
}
static void Main(string[] args) {
using (DirectoryEntry searchRoot = new DirectoryEntry("LDAP://DC=x,DC=y")) {
foreach (var user in GetUsersRecursively(searchRoot, "CN=MainGroup,DC=x,DC=y")) {
Console.WriteLine((string)user.Properties["sAMAccountName"][0]);
}
}
}
}
static List<SearchResult> ad_find_all_members(string a_sSearchRoot, string a_sGroupDN, string[] a_asPropsToLoad)
{
using (DirectoryEntry de = new DirectoryEntry(a_sSearchRoot))
return ad_find_all_members(de, a_sGroupDN, a_asPropsToLoad);
}
static List<SearchResult> ad_find_all_members(DirectoryEntry a_SearchRoot, string a_sGroupDN, string[] a_asPropsToLoad)
{
string sDN = "distinguishedName";
string sOC = "objectClass";
string sOC_GROUP = "group";
string[] asPropsToLoad = a_asPropsToLoad;
Array.Sort<string>(asPropsToLoad);
if (Array.BinarySearch<string>(asPropsToLoad, sDN) < 0)
{
Array.Resize<string>(ref asPropsToLoad, asPropsToLoad.Length+1);
asPropsToLoad[asPropsToLoad.Length-1] = sDN;
}
if (Array.BinarySearch<string>(asPropsToLoad, sOC) < 0)
{
Array.Resize<string>(ref asPropsToLoad, asPropsToLoad.Length+1);
asPropsToLoad[asPropsToLoad.Length-1] = sOC;
}
List<SearchResult> lsr = new List<SearchResult>();
using (DirectorySearcher ds = new DirectorySearcher(a_SearchRoot))
{
ds.Filter = "(&(|(objectClass=group)(objectClass=user))(memberOf=" + a_sGroupDN + "))";
//ds.PropertiesToLoad.Clear();
ds.PropertiesToLoad.AddRange(asPropsToLoad);
//ds.PageSize = 1000;
//ds.SizeLimit = 0;
foreach (SearchResult sr in ds.FindAll())
lsr.Add(sr);
}
for(int i=0;i<lsr.Count;i++)
if (lsr[i].Properties.Contains(sOC) && lsr[i].Properties[sOC].Contains(sOC_GROUP))
lsr.AddRange(ad_find_all_members(a_SearchRoot, (string)lsr[i].Properties[sDN][0], asPropsToLoad));
return lsr;
}
static void Main(string[] args)
{
foreach (var sr in ad_find_all_members("LDAP://DC=your-domain,DC=com", "CN=your-group-name,OU=your-group-ou,DC=your-domain,DC=com", new string[] { "sAMAccountName" }))
Console.WriteLine((string)sr.Properties["distinguishedName"][0] + " : " + (string)sr.Properties["sAMAccountName"][0]);
}
To get the members recursively take this (the trick is GetMembers(true) instead of false which is the default value):
private List<string> GetGroupMembers(string groupName)
{
var members = new List<string>();
try
{
using (var pc = new PrincipalContext(ContextType.Domain, Common.THE_DOMAIN))
{
var gp = GroupPrincipal.FindByIdentity(pc, groupName);
if (gp == null) return members;
foreach (Principal p in gp.GetMembers(true))
members.Add(p.Name);
members.Sort();
}
}
catch (Exception)
{
return new List<string>();
}
return members;
}
I also wanted to know if a user or a computer is in an ActiveDirectory group. And this worked for me also with nested groups (it is part of a WebService but you can use the code also in a standalone application):
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
[HttpGet]
[Route("IsUserInGroup")]
public HttpResponseMessage IsUserInGroup(string userName, string groupName)
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.BadRequest);
try
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, Common.THE_DOMAIN))
{
var gp = GroupPrincipal.FindByIdentity(pc, groupName);
var up = UserPrincipal.FindByIdentity(pc, userName);
if (gp == null)
{
response.Content = Common.ConvertToJsonContent($"Group '{groupName}' not found in Active Directory");
response.StatusCode = HttpStatusCode.NotFound;
return response;
}
if (up == null)
{
response.Content = Common.ConvertToJsonContent($"User '{userName}' not found in Active Directory");
response.StatusCode = HttpStatusCode.NotFound;
return response;
}
DirectoryEntry user = new DirectoryEntry($"LDAP://{up.DistinguishedName}");
DirectorySearcher mySearcher = new DirectorySearcher(user)
{
SearchScope = System.DirectoryServices.SearchScope.Subtree,
Filter = $"(memberOf:1.2.840.113556.1.4.1941:={gp.DistinguishedName})"
};
SearchResult result = mySearcher.FindOne();
response.StatusCode = HttpStatusCode.OK;
response.Content = Common.ConvertToJsonContent(result != null);
}
}
catch (Exception ex)
{
response.StatusCode = HttpStatusCode.BadRequest;
response.Content = Common.ConvertToJsonContent($"{MethodBase.GetCurrentMethod().Name}: {ex.Message}");
}
return response;
}
[HttpGet]
[Route("IsComputerInGroup")]
public HttpResponseMessage IsComputerInGroup(string computerName, string groupName)
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.BadRequest);
try
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, Common.THE_DOMAIN))
{
var gp = GroupPrincipal.FindByIdentity(pc, groupName);
var cp = ComputerPrincipal.FindByIdentity(pc, computerName);
if (gp == null)
{
response.Content = Common.ConvertToJsonContent($"Group '{groupName}' not found in Active Directory");
response.StatusCode = HttpStatusCode.NotFound;
return response;
}
if (cp == null)
{
response.Content = Common.ConvertToJsonContent($"Computer '{computerName}' not found in Active Directory");
response.StatusCode = HttpStatusCode.NotFound;
return response;
}
DirectoryEntry computer = new DirectoryEntry($"LDAP://{cp.DistinguishedName}");
DirectorySearcher mySearcher = new DirectorySearcher(computer)
{
SearchScope = System.DirectoryServices.SearchScope.Subtree,
Filter = $"(memberOf:1.2.840.113556.1.4.1941:={gp.DistinguishedName})"
};
SearchResult result = mySearcher.FindOne();
response.StatusCode = HttpStatusCode.OK;
response.Content = Common.ConvertToJsonContent(result != null);
}
}
catch (Exception ex)
{
response.StatusCode = HttpStatusCode.BadRequest;
response.Content = Common.ConvertToJsonContent($"{MethodBase.GetCurrentMethod().Name}: {ex.Message}");
}
return response;
}