Search in global catalog of AD Forest - c#

I need to do LDAP query across all DCs of Forest. I found similar post, from which is following code:
using (Forest currentForest = Forest.GetCurrentForest())
{
using (GlobalCatalog globalCatalog = currentForest.FindGlobalCatalog())
{
using (DirectorySearcher searcher = globalCatalog.GetDirectorySearcher())
{
searcher.Filter = "filter to verify existence of user account according to supplied sid";
SearchResult result = searcher.FindOne();
return (result != null);
}
}
}
Above code works in my test scenario. Is this the correct way to query information across domains? There are a lot of other posts where they manually enter GC location or search through list of domains. Therefore is there anything I should pay attention to or that could go wrong with this code?

Use the Domains property of System.DirectoryServices.ActiveDirectory.Forest to iterate through the domains and then the DomainControllers property of System.DirectoryServices.ActiveDirectory.Domain to iterate though domain controllers, running whatever query you want on each.

Related

Get AD users and groups from client

Is there any way possible to access a clients AD users and groups outsite an intranet setting?
I am not looking for azure solutions.
The tech i am working with is .Net Core web api and Angular as frontend.
Retrieving AD information on my own domain is achievable and i could get UserPrincipals,
but if the web API and AD is not hosted on the same server, how does that work?
I can't tell you exactly how to do it, since I haven't done it with .NET Core yet, but I can tell you what you need to do and you can look up more details about each part.
Use forms authentication. You will need a login page that asks for their username and password.
Validate the credentials. There are several ways to do this. My favourite is in this answer, which uses LdapConnection from System.DirectoryServices.Protocols because it's the least amount of network requests needed to do the job and it will tell your why credentials fail (which would let you take the user to a "change password" page if their password has expired, for example). However, using DirectoryEntry/DirectorySearcher is easier for looking up groups, so you might want to also use that for validating too, by using the user's credentials in the constructor of DirectoryEntry (but you'd lose knowing the reason for failed attempts).
Look up the user's account. I prefer using DirectoryEntry/DirectorySearcher from System.DirectoryServices for this. Eldar's answer shows how to do that.
Find the user's groups. I wrote a whole article about this: Finding all of a user's groups. Assuming you only have one domain in your environment, and you already have a DirectoryEntry object for the user's account, this code will work:
private static IEnumerable<string> GetUserMemberOf(DirectoryEntry de) {
var groups = new List<string>();
//retrieve only the memberOf attribute from the user
de.RefreshCache(new[] {"memberOf"});
while (true) {
var memberOf = de.Properties["memberOf"];
foreach (string group in memberOf) {
var groupDe = new DirectoryEntry($"LDAP://{group.Replace("/", "\\/")}");
groupDe.RefreshCache(new[] {"cn"});
groups.Add(groupDe.Properties["cn"].Value as string);
}
//AD only gives us 1000 or 1500 at a time (depending on the server version)
//so if we've hit that, go see if there are more
if (memberOf.Count != 1500 && memberOf.Count != 1000) break;
try {
de.RefreshCache(new[] {$"memberOf;range={groups.Count}-*"});
} catch (COMException e) {
if (e.ErrorCode == unchecked((int) 0x80072020)) break; //no more results
throw;
}
}
return groups;
}
If you have more than one domain in your environment, then it'll be a bit more complicated.
var entry = new DirectoryEntry("LDAP://DC=DomainController,DC=com","UserName","P4$$w0Rd!???");
// userName password must be valid
var searcher = new DirectorySearcher(entry);
searcher.PropertiesToLoad.Add("sn");
var accName = "accNameToSearch"; // you can also use wildcart
// https://learn.microsoft.com/en-us/windows/win32/adsi/search-filter-syntax
searcher.Filter = $"(&(objectCategory=person)(objectClass=user)(sAMAccountName={accName}))";
var result = searcher.FindOne();
var sn = result.Properties["sn"];
There is no UserPrincipal class for that nuget package yet. But you can still query users and other stuff with ad query syntax like above.

Active Directory groups not immediately available after creation

I am creating Active Directory groups in my app. I make security and distribution groups. The groups will get created just fine, but it takes about 10-15 minutes to show up in the Active Directory Users and Computers.
Is there some kind of forced sync I can perform in C# to make this happen sooner? Or maybe some setting I can change in my directory to change this behavior?
Example code
DirectoryEntry ou1= topLevel.Children.Find("OU=ou1");
DirectoryEntry secGroups = ou1.Children.Find("OU=Security Groups");
DirectoryEntry newGroup = secGroups.Children.Add("CN=" + name + "", "group");
newGroup.CommitChanges();
GroupPrincipal createdGroup = GroupPrincipal.FindByIdentity(this._context, name);
createdGroup.SamAccountName = name;
createdGroup.DisplayName = name;
createdGroup.GroupScope = GroupScope.Universal;
createdGroup.Save();
if (members.Any())
{
foreach (var item in members)
{
createdGroup.Members.Add(this._context, IdentityType.SamAccountName, item);
}
createdGroup.Save();
}
Using ASP.NET MVC, C#, System.DirectoryServices.AccountManagement, System.DirectoryServices.ActiveDirectory.
The most likely answer is that it takes time to propagate to all domain controllers on your network. You may be connected to a different DC via ADUC from the one your application updated.
Something that might help in this situation where you have multiple domain controllers replicating is to target a specific DC for each call you make to the AD server.
So instead of "LDAP://mydomain.com" it becomes something like "LDAP://myDC.mydomain.com"

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.

Is there a way to enable referral chasing for UserPrincipal.FindByIdentity()?

I have a .NET 3.5 web application that uses the System.DirectoryServices.AccountManagement classes. When I search for some users I get a PrincipalOperationException: A referral was returned from the server. If I did this the old school way with my own LDAP code I could enable chasing of referrals. Do I need to rewrite my code?
My code looks like this:
using (var principalContext = new PrincipalContext(ContextType.Domain, null, adPath))
{
// Find the principal object for which you wish to enumerate group
// membership.
using (var userPrincipal = UserPrincipal.FindByIdentity(principalContext, identity))
{
if (userPrincipal != null)
{
Name = userPrincipal.DisplayName;
DistinguishedName = userPrincipal.DistinguishedName;
EmailAddress = userPrincipal.EmailAddress;
Sid = userPrincipal.Sid.Value;
}
}
}
My adPath can be one of 2 values. One of the values is a domain that was recently joined, and can be accessed using different tools. I believe this is a problem with how this .NET library makes the LDAP calls.
Here is a partial Answer, as it's too long for a comment.
According to this Microsoft documentation, as you even know, Referrals are a hint that the client can chase. But concerning RODC they add "For example, in the case of an LDAP application, if chase referrals is enabled on the LDAP connection between the client and the RODC, the application never knows that the client received a referral from the RODC. The client is automatically redirected to the writable domain controller that is specified in the referral. ".
So I look how to enable LDAP chasing on a connexion in Microsoft site and I found this which means ADSI use. I'am very interested in the answer.
Do you try to query the global catalog like this :
/* Retreiving a principal context
*/
PrincipalContext domainContext = new PrincipalContext(ContextType.Domain, "YourGCServer:3268", "dc=dom,dc=fr", "User", "Password");
It's supposed to contains all the forest domain's datas.
I hope it helps.
Have you tried code of the form(put the domain in as the second argument):
var principalContext = new PrincipalContext(ContextType.Domain, "office.local", "OU=Users, DC=office, DC=local" ))
Also make sure that the adPath is from most specific to least specific.

With WMI: How can I get the name of the user account who's running my program?

I need to restrict access to my application to only one specific user account. I have found classes under WMI to find users accounts, but I donĀ“t know how to recognize which one is running my app.
There are simpler ways to get the current username than using WMI.
WindowsIdentity.GetCurrent().Name will get you the name of the current Windows user.
Environment.Username will get you the name of the currently logged on user.
The difference between these two is that WindowsIdentity.GetCurrent().Name will also include the domain name as well as the username (ie. MYDOMAIN\adrian instead of adrian). If you need the domain name from Environment, you can use Environment.UserDomainName.
EDIT
If you really want to do it using WMI, you can do this:
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT UserName FROM Win32_ComputerSystem");
ManagementObjectCollection collection = searcher.Get();
string username = (string) collection.Cast<ManagementBaseObject>().First()["UserName"];
Unfortunately, there is no indexer property on ManagementObjectCollection so you have to enumerate it to get the first (and only) result.
You don't necessarily need to use WMI. Check out WindowsIdentity.
var identity = WindowsIdentity.GetCurrent();
var username = identity.Name;
The simplest approach is through the Environment class:
string user = Environment.UserDomainName + "\\" + Environment.UserName;
There also are several ways to restrict to a certain user (although checking for a role is more common).
Apart from the obvious
if (userName == "domain\\john")
{ }
You can also use the following attribute on an entire class or specific methods:
[PrincipalPermission(SecurityAction.Demand, Name = "domain\\john",
Authenticated = true)]
void MyMethod()
{
}
Which could be a bit more reliable for low-level, reusable methods.
Note that you could use both, checking for a user with if() as part of the normal flow of the program and the attribute as a safeguard on critical methods.

Categories