Active Directory group name uniqueness requirement (query from C# client) - c#

Our company's C# product uses System.DirectoryServices.AccountManagement to query Active Directory for users and groups. We use the following method to get the principal:
...
PrincipalContext principalContext = new PrincipalContext(ContextType.Domain);
return principalContext;
...
We get Active Directory groups using (e.g. groupName = "Devs"):
...
GroupPrincipal groupPrincipal = GroupPrincipal.FindByIdentity(this.principalContext, groupName);
...
Everything works fine with this setup when we run it on a simple, one domain Active Directory database.
My question is, what will happen when we run this code on a big forest with more than one "Devs" group? Can there be more than one "Devs" security group in a forest? If so, how will it resolve "Devs"? Do I have to switch to using the method:
public static GroupPrincipal FindByIdentity(
PrincipalContext context,
IdentityType identityType,
string identityValue
)
I cannot simulate this currently (lack of resources and lack of time) and I have been reading a lot about this. I know there are local, global and universal security groups, spread among domain trees. But domain trees in a forest have some sort of trust among the roots, so they are not completely ignorant of each other. What is the worst case of having "Devs" duplicates in the forest and how could the application handle it?

It's pretty common task to search through domain hierarchy. With AccountManagement classes you can do the following:
// Connect to global catalog of the forest
var context = new PrincipalContext(ContextType.Domain, "contoso.com:3268", "DC=contoso,DC=com");
// Build a filter principal by name and context
var groupFilter = new GroupPrincipal(context) {Name = "Devs"};
// Build a searcher with a filter applied
var searcher = new PrincipalSearcher(groupFilter);
// This should return all groups in all subdomains matching specified name
var groups = searcher.FindAll().ToList();
foreach (var group in groups)
{
Console.WriteLine(group.DistinguishedName);
}
You will not have any duplicates cause there can't be more than one group with this name ("Devs") in domain. In AccountManagement terms you create GroupPrincipal object with context and name parameters and can't have more than one in context with the same name.
If you connect to the domain controller (new PrincipalContext(ContextType.Domain)) then FindByIdentity will search this single domain. If you connect to global catalog of the forest (like in my example, port 3268) then FindByIdentity will search entire forest. The DistinguishedName property will show which domain a group belongs to.
As to cross-forest access there you need to connect to global catalog in every forest separately, because there's no user/group data replication between forests global catalogs.

Related

Manipulate Windows "Users" group without access to domain?

I have a C#/WPF application that is used to manipulate local users and groups on a system. We only care about local users and groups, regardless of whether the machine is joined to a domain or not. When we create a user in our application, I want to add the user to the "Users" group. Normally this works fine, but if the machine is domain-joined and NOT connected to the network (e.g. a laptop out of the office), I get "the network path is not found" errors when trying to add a local user to the "Users" group.
I think the reason is because the "Users" group contains domain users, as shown in this screenshot.
And this is essentially my code:
public static void AddUserToGroup(UserPrincipal oUserPrincipal, string groupName)
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Machine))
{
GroupPrincipal group = GroupPrincipal.FindByIdentity(pc, groupName);
if (group == null)
{
group = CreateLocalWindowsGroup(groupName);
}
if (!group.Members.Contains(oUserPrincipal)) // this line throws "network path not found" exception if the machine is domain joined, but can't contact the domain controller
group.Members.Add(oUserPrincipal);
group.Save();
}
}
I can't figure out how to approach this with the API, but it seems like it should be possible because I can add the exact same user to the same group manually with the "Local Users and Groups" tool with no issues, regardless of network connectivity. How can I get around this issue?
This is one reason I don't like using the AccountManagement namespace.
The GroupPrincipal.Members property returns a PrincipalCollection, which is just a collection of Principal objects. The actual type will be UserPrincipal or GroupPrincipal depending on what the actual member is.
But, those Principal classes, when they're created, load all the details for that object. So just the act of creating a UserPrincipal for a domain user triggers it to go out to the domain and get all the details for the user.
You're better off using DirectoryEntry directly, which is what the AccountManagement namespace uses in the background anyway. It gives you more control over what's actually happening.
var usersGroup = new DirectoryEntry($"WinNT://{Environment.MachineName}/{groupName}");
usersGroup.Invoke("Add", new object[] { $"WinNT://{Environment.MachineName}/{userName}" });
This assumes a userName variable with the name of the local user. If the user is already in the group, it will throw an exception, so you may want to catch that.
Besides actually working in this case, this will run faster since you're not wasting time collecting details for all the existing members when you have no intention of using any of that data.
Update: To read all the members of a local group, use .Invoke("Members"). Then you have to create a new DirectoryEntry with each member in the collection. For example:
foreach (var member in (IEnumerable) usersGroup.Invoke("Members")) {
using (var memberDe = new DirectoryEntry(member)) {
Console.WriteLine(memberDe.Name);
}
}
The DirectoryEntry class is really a wrapper around the Windows native ADSI Interfaces. For a group, the underlying object will really be IADsGroup. When you call .Invoke on a DirectoryEntry object, that lets you call the IADsGroup methods (you'll see the Members method listed in the documentation there). All of the object-specific classes like IADsGroup and IADsUser all inherit from IADs, so the methods from that are usable too.
This only applies to local groups. With Active Directory groups, you don't have to resort to using the IADs methods.

Determine usergroups/claims of user given LDAP server details in C#

We have a test active directory LDAP server. I also have some user names and passwords. I would like to determine the claims/user groups of a particular user, whilst logged into another domain. Can this be done with some C# code? I presume I will have to use System.DirectoryServices.dll
If you can use .Net 3.5 of higher, then try System.DirectoryServices.AccountManagement.dll assembly. It provides System.DirectoryServices.AccountManagement namespace and Principal-based classes, such as UserPrincipal and GroupPrincipal. They represent higher level of abstraction and are easier to use.
For example, to connect to LDAP server in another domain (get Principal Context in terms of this abstraction) you need to create an instance of PrincipalContext class with this constructor:
PrincipalContext anotherDomainContext = new PrincipalContext(ContextType.Domain, DomainDnsName, RootOU, ContextOptions.SimpleBind, QueryUserName, QueryUserPassword);
RootOU is something like "DC=Company,DC=COM", therefore DomainDnsName will be like "company.com" or "ldapserver.company.com". If you have serveral domains in your AD forest then try to connect to global catalog (DomainDnsName = "ldapserver.company.com:3268"). QueryUserName and QueryUserPassword are plain strings with username and password which are used to connect to LDAP server. Username may include domain name, for example:
string QueryUserName = #"company\username";
Once connected to LDAP server you can search for users:
UserPrincipal user = UserPrincipal.FindByIdentity(anotherDomainContext , IdentityType.SamAccountName, samAccountName);
where you supply samAccountName and context (connection).
With an instance of UserPrincipal at hands you gain access to its properties and methods. For example, get security groups for user:
PrincipalSearchResult<Principal> searchResults = user.GetGroups();
List<GroupPrincipal> groupsList = searchResults.Select(result => result as GroupPrincipal).
Where(group => (group != null) &&
(group.IsSecurityGroup.HasValue) &&
(group.IsSecurityGroup.Value))
Note that GetGroups returns only groups to which user belongs directrly. To get all user groups including nested, call GetAuthorizationGroups. Also, you can avoid using LINQ, it's just for filtering security groups from GetGroups.
With GroupPrincipal you can check Name property, or Members collecion.

Searching for users across multiple Active Directory domains

I'm using the System.DirectoryServices.AccountManagement to provide user lookup functionality.
The business has several region specific AD domains: AMR, EUR, JPN etc.
The following works for the EUR domain, but doesn't return users from the other domains (naturally):
var context = new PrincipalContext(ContextType.Domain, "mycorp.com", "DC=eur,DC=mycorp,DC=com");
var query = new UserPrincipal(GetContext());
query.Name = "*Bloggs*";
var users = new PrincipalSearcher(query).FindAll().ToList();
However, if I target the entire directory, it doesn't return users from any of the region specific domains:
var context = new PrincipalContext(ContextType.Domain, "mycorp.com", "DC=mycorp,DC=com");
How do I search the entire directory?
Update
Read up on "How Active Directory Searches Work":
http://technet.microsoft.com/en-us/library/cc755809(v=ws.10).aspx
If I suffix the server name with port 3268 it searches against the Global Catalog:
var context = new PrincipalContext(ContextType.Domain, "mycorp.com:3268", "DC=mycorp,DC=com");
However it's very, very slow. Any suggestions on how to improve performance?
Queries which have initial wildcards (*Bloggs*) will be slow unless you have a tuple index on the attribute being queries. None of the attributes in AD have this set by default. Better to not do initial wildcards.

Any way to distinguish between "people user accounts" and "computer user accounts"?

When querying Active Directory for users - is there a way to filter out user accounts created for computers? Ideally a way which is common across most typical networks. e.g.:
DirectorySearcher ds = new DirectorySearcher(new DirectoryEntry([Users_OU_root]));
ds.filter = "(&(objectClass=User)([CRITERIA_TO_FILTER_OUT_COMPUTER_USER_ACCOUNTS]))";
ds.FindAll();
...
If you're on .NET 3.5 and up, you should check out the System.DirectoryServices.AccountManagement (S.DS.AM) namespace. Read all about it here:
Managing Directory Security Principals in the .NET Framework 3.5
MSDN docs on System.DirectoryServices.AccountManagement
Basically, you can define a domain context and easily find users and/or groups in AD:
// set up domain context
PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
// find a user
UserPrincipal user = UserPrincipal.FindByIdentity(ctx, "SomeUserName");
if(user != null)
{
// do something here....
}
// find the group in question
GroupPrincipal group = GroupPrincipal.FindByIdentity(ctx, "YourGroupNameHere");
// if found....
if (group != null)
{
// iterate over members
foreach (Principal p in group.GetMembers())
{
Console.WriteLine("{0}: {1}", p.StructuralObjectClass, p.DisplayName);
// do whatever you need to do to those members
}
}
The new S.DS.AM makes it really easy to play around with users and groups in AD:
Computer accounts will show up as ComputerPrincipal (derived from Principal) - so you can easily keep users and computer accounts apart.
If you cannot or don't want to move to S.DS.AM - you can also keep user and computers apart by using the objectCategory instead of the objectClass in your LDAP filter. objectCategory is beneficial anyway, since it's indexed, and not multi-valued - so query performance will be much better.
For a real-life user, use objectCategory = Person, while for a computer, use objectCategory = Computer in your LDAP filter.
Even if I agree with the answer. Active-Directory remain an LDAP server. Here is the filter you are looking for :
(&(objectCategory=user)(objectClass=user)(...))
'objectCategory=user' is a shortcut for 'objectCategory=CN=User,CN=Schema,CN=Configuration,DC=dom,DC=fr' understood by Active-Directory but it's also a way in others Directories, that's why I put an answer, even if another answer is accepted.

How to enumerate per-Forest Active Directory domains in C#?

This code enumerate Active Directory domains, if the mahine on which is running is part of the forest.
public static ArrayList EnumerateDomains()
{
ArrayList alDomains = new ArrayList();
Forest currentForest = Forest.GetCurrentForest();
DomainCollection myDomains = currentForest.Domains;
foreach (Domain objDomain in myDomains)
{
alDomains.Add(objDomain.Name);
}
return alDomains;
}
Is it posible to enumerate domains which are part of some other forest ?
What is the difference between forest and global catalog ?
The logic above should work (provided permissions are OK) if you replace the setting of currentForest with a call to Forest.GetForest that identifies the forest whose domains you wish to enumerate.
DirectoryContext context = new DirectoryContext(DirectoryContextType.Forest,
"dns-name-of-target-forest");
Forest currentForest = Forest.GetForest(context);
If you don't have permission but do know someone who does, there are DirectoryContext constructor overrides that allow you to specify an alternate name and password.
The relationship of global catalog to forest is detailed here. In short, a forest is an Active Directory (AD) abstraction for grouping of AD objects. A global catalog (if the forest has one) is a distributed data repository that is required in order for certain types of operations to be done on that forest.

Categories