Ive wrote a function to get all Groups with the Members from AD:
public static void getGroupsWithUsers() {
String currentDomain = Domain.GetCurrentDomain().ToString();
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, currentDomain))
{
using (PrincipalSearcher searcher = new PrincipalSearcher(new UserPrincipal(context)))
{
foreach (var result in searcher.FindAll())
{
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
var SID = WindowsIdentity.GetCurrent().User.ToString();
// find a user
UserPrincipal user = UserPrincipal.FindByIdentity(context, de.Properties["samAccountName"].Value.ToString());
if (user != null)
{
// get the user's groups
var groups = user.GetAuthorizationGroups();
foreach (GroupPrincipal group in groups)
{
Console.WriteLine("User: " + user + " is in Group: " + group);
}
}
}
}
}
}
The execution time is around 1,5 seconds for a small amount of data.
Are there any improvements I could make to get the method faster?
I ask because if I execute the function for 1 million users or groups that it will take forever.
The bulk of your problem is what you're doing in the loop. You're taking the search result, converting it to a DirectoryEntry, then going back out to the network to find that account that you already just found.
The PrincipalSearcher.FindAll method returns a collection of Principal objects that you can cast to the type you know they are. In this case, you know it will only return users, so you can cast the result to UserPrincipal in your loop.
You're also assigning a SID variable that you're not using (although this is likely not taking any significant time).
A side note: You don't need that currentDomain variable here, since if you create a PrincipalContext with ContextType.Domain and no domain name, it will automatically use the current domain.
So your code can be simplified to this:
using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
using (PrincipalSearcher searcher = new PrincipalSearcher(new UserPrincipal(context)))
{
foreach (UserPrincipal user in searcher.FindAll())
{
// get the user's groups
var groups = user.GetAuthorizationGroups();
foreach (GroupPrincipal group in groups)
{
Console.WriteLine("User: " + user + " is in Group: " + group);
}
}
}
That will likely cut the time quite a bit. But if you really want to find the best performance possible, you need to use DirectorySearcher and DirectoryEntry directly. The PrincipalSearcher/UserPrincipal classes use those in the background anyway, but you lose a lot of control over what is happening in the background, and that's the key to performance. For maximum performance, you need to cut down on:
the number of network requests, and
how much data is being sent back.
I wrote a couple of articles on this subject that can help you with that, if you'd like to go down that route:
Active Directory: Better performance
Finding all of a user’s groups
Try to split operations: first - get info to string builder, second - print it.
Like this
public static void getGroupsWithUsers()
{
String currentDomain = Domain.GetCurrentDomain().ToString();
var stringBuilder = new StringBuilder();
using (PrincipalContext context = new PrincipalContext(ContextType.Domain, currentDomain))
{
using (PrincipalSearcher searcher = new PrincipalSearcher(new UserPrincipal(context)))
{
foreach (var result in searcher.FindAll())
{
DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
var SID = WindowsIdentity.GetCurrent().User.ToString();
// find a user
UserPrincipal user = UserPrincipal.FindByIdentity(context, de.Properties["samAccountName"].Value.ToString());
if (user != null)
{
// get the user's groups
var groups = user.GetAuthorizationGroups();
foreach (GroupPrincipal group in groups)
{
stringBuilder.AppendLine($"User: {user} is in Group: {group}");
}
}
}
}
}
string result = stringBuilder.Length > 0
? stringBuilder.ToString()
: "No users were found";
Console.WriteLine(result);
}
Related
I'll try to explain myself better.
I'm using C# to build a mini-program to myself. I want to search a user within my active directory, but I want to search a user without a full display name. Let me explain myself.
For example, my display name can be: "David Holonka\Jeramy".
Is there a way to search the letters "lonka" and it'll find me all the users that has these combinations of letters within their display name?
My current code:
using (var pc = new PrinicpalContext(ContextType.Domain, "MyDomain"))
{
UserPrincipal user = new UserPrinicpal(pc);
User.DisplayName = "Holonka";
PrinicpalSearcher scrh = new PrinicpalSearcher(user);
Prinicpal found = scrh.FindOne();
}
}
Right now it doesn't find anything becasue there isn't a user that its display name is only "Holonka", but I want it to find the user that i've mentioned before
Thank you very much!
You can use query string in the PrinicpalSearcher:
UserPrincipal user = new UserPrinicpal(pc);
User.DisplayName = "*Holonka*";
PrinicpalSearcher searcher = new PrinicpalSearcher(user);
var results = searcher.FindAll();
You can also using PrincipalSearcher to find users with “or” parameters like in the example below:
List<UserPrincipal> searchPrinciples = new List<UserPrincipal>();
searchPrinciples.Add(new UserPrincipal(context) { DisplayName="*Holonka*"});
searchPrinciples.Add(new UserPrincipal(context) { SamAccountName = "*Holonka*" });
searchPrinciples.Add(new UserPrincipal(context) { MiddleName = "*Holonka*" });
searchPrinciples.Add(new UserPrincipal(context) { GivenName = "*Holonka*" });
List<Principal> results = new List<Principal>();
foreach (var item in searchPrinciples)
{
var searcher = new PrincipalSearcher(item);
// Results may contains duplicate values because of separate searchers can handle the same user
results.AddRange(searcher.FindAll());
}
You can also try using Ambiguous Name Resolution, which is a special AD query that looks for partial matches in several attributes (the list of those attributes is in that article).
You can't use PrincipalSearcher do use ANR though. You have to use DirectorySearcher (which is what PrincipalSearcher uses in the background anyway).
Here's an example:
var searchRoot = new DirectoryEntry("LDAP://domain.com");
var searcher = new DirectorySearcher(searchRoot) {
Filter = "(anr=Holonka)"
};
searcher.PropertiesToLoad.Add("displayName");
using (var results in = searcher.FindAll()) {
foreach (SearchResult result in results) {
if (result.Properties.Contains("displayName")) {
var displayName = (string) result.Properties["displayName"][0];
//do something else
}
}
}
When using DirectorySearcher, it's wise to use PropertiesToLoad. If you don't, it will return every attribute that has a value, which is likely way more data than you need. It's a waste of network traffic and time.
The using statement is also wise, since SearchResultCollection can't be completely cleaned up by the garbage collector.
I wrote some more about that in an article I wrote: Active Directory: Better Performance
I need to get user details of a particular Active Directory group. I am using this code:
var result = grpResponse.Entries[0];
if (result.Attributes["member"] != null)
{
for (var i = 0; i < result.Attributes["member"].Count; i++)
{
var filter = result.Attributes["member"][i].ToString();
var query = "(&(objectClass=user)(" + filter + "))"; // Here I need username to use like cn=username
var userRequest = new SearchRequest(distinguishedName, query,
SearchScope.Subtree);
In filter I am getting something like
CN=Name,OU=something,DC=example
How can I take this cn value i.e user name alone?
If you're on .NET 3.5 and up, you should check out the System.DirectoryServices.AccountManagement (S.DS.AM) namespace.
Basically, you can define a domain context and easily find users and/or groups in AD:
// set up domain context - limit to the OU you're interested in
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain, null, "OU=YourOU,DC=YourCompany,DC=Com"))
{
// find the group in question
GroupPrincipal group = GroupPrincipal.FindByIdentity(ctx, "YourGroupNameHere");
// if found....
if (group != null)
{
// iterate over the group's members
foreach (Principal p in group.GetMembers())
{
Console.WriteLine("{0}: {1}", p.StructuralObjectClass, p.DisplayName);
// do whatever else 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!
Read more about it here:
MSDN docs on System.DirectoryServices.AccountManagement
The below is exactly what I needed.
The OuString you use like ours may have has multiple parts - both OU & DC
bstring OUString = "OU=Groups,OU=Accounts,DC=nw,DC=nos,DC=ourcompanyName,DC=com" ;
using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain, null, OUString))
I have an application that needs to have a list of names and email addresses of users in a specific security group. I am currently doing this with the code below. When I run on VPN it comes back right away within a second or two usually but when I run on either on ethernet or wireless (both on the domain), it takes about 40 seconds for this to come back. Is there any way I can improve the time of this method on ethernet or wireless?
...
DirectoryEntry entry = new DirectoryEntry(ldap);
DirectorySearcher mySearcher = new DirectorySearcher(entry);
mySearcher.Filter = "(&(objectCategory=group)(objectClass=group)(groupType:1.2.840.113556.1.4.803:=2147483648))";
mySearcher.PropertiesToLoad.Add("member");
SearchResultCollection results = mySearcher.FindAll();
foreach (SearchResult result in results)
{
foreach (string distinguishedMember in result.Properties["member"])
{
string memberPath = "LDAP://" + distinguishedMember;
DirectoryEntry member = new DirectoryEntry(memberPath);
DirectorySearcher Searcher = new DirectorySearcher(member);
Searcher.Filter = "(&(objectCategory=user))";
Searcher.PropertiesToLoad.Add("mail");
Searcher.PropertiesToLoad.Add("name");
SearchResult memberFound = Searcher.FindOne();
if (memberFound != null)
{
String memberEmail = memberFound.Properties["mail"][0].ToString();
String memberName = memberFound.Properties["name"][0].ToString();
users.Add(new KeyValuePair<String, String>(memberName, memberEmail));
}
}
}
Maybe it would help to get all of the users in one go, instead of fetching them one by one*:
Searcher.Filter = "(&(objectCategory=user)(memberOf=" + myGroupsDistinguishedName + "))"
Searcher.PropertiesToLoad.Add("mail");
Searcher.PropertiesToLoad.Add("name");
var allMembers = Searcher.FindAll();
var users = allMembers.Cast<SearchResult>().ToDictionary(sr=>sr.Properties["name"].ToString(), sr=>sr.Properties["mail"].ToString());
*This doesn't handle scenarios with over 1000 users.
Do not fetch all members at one time. Instead, I recommend using the pagesize property of the DirectorySearcher class:
mySearcher.PageSize = 10;
I have a user query that can't be done at an OU level. I am trying to only return users where they are a member of a group (I need to filter by the group string value, not the actual group object). Here is what it currently looks like:
using (var entry = new DirectoryEntry("LDAP://foo.net/DC=appName,DC=domainName,DC=com", Username, Password)) {
using (var searcher = new DirectorySearcher()) {
searcher.Filter = "(&(objectClass=user))";
searcher.SearchRoot = entry;
searcher.PropertiesToLoad.Add("memberOf");
foreach (SearchResult sr in searcher.FindAll()) {
var member = GetSearchResultProperty(sr, "memberOf");
}
}
}
This query goes across multiple OU's where there are different user and group containers.
Is there a way I can filter on the memberOf property when I execute my query (without specifying any sort of OU)?
Just add another term to the filter:
searcher.Filter = "(&(objectClass=user)(memberOf=myGroup))";
It looks like what I am trying to do isn't possible at the time of query execution because the memberOf property needs to be a full path to a group. In my case, I don't actually care about the group object, but, rather the group name (each OU will have different groups, but, could have the same group name). I had to take a different approach to solve this which could have performance implications later on. Just in case someone else finds this question looking for an answer, here is how I am solving it as of now:
using (var entry = new DirectoryEntry("LDAP://foo.net/DC=appName,DC=domainName,DC=com", Username, Password)) {
using (var searcher = new DirectorySearcher()) {
searcher.Filter = "(&(objectClass=user))";
searcher.SearchRoot = entry;
searcher.PageSize = 5000;
searcher.PropertiesToLoad.Add(DirectoryConstants.AD_PROPERTY_MEMBER_OF);
searcher.PropertiesToLoad.Add(DirectoryConstants.AD_PROPERTY_DISPLAY_NAME);
foreach (SearchResult sr in searcher.FindAll()) {
//The memberOf property will be a string array if the user is in multiple groups.
string[] memberOf = GetSearchResultProperties(sr, DirectoryConstants.AD_PROPERTY_MEMBER_OF);
//Check if the user is in the group by analyzing the string.
var group = memberOf.FirstOrDefault(m => m.StartsWith(string.Format("CN={0},", groupName)));
if (group != null) {
//The user is in the group!
}
}
}
}
You need to include the full distinguished name of the group in your search filter, as the memberOf property will contain that value.
searcher.Filter = "(&(objectClass=user)(memberOf=CN=myGroup,CN=groups,OU=theOU,DC=appName,DC=domainName,DC=com))";
EDIT :
My apologies, as I misread the question. Without including an OU, the only other way of doing this is taking the opposite approach, and locating the group object first :
searcher.Filter = "(&(objectClass=group)(name=Your Group Name)"
Then iterate through the DirectoryEntry using it's properties :
foreach(object dn in group.Properties["member"] )
{
string DistinguishedName = dn.ToString();
}
If you're retrieving more than 1,000+ users though, you'll need to take a more segmented approach, which is detailed in this article.
if (DomainHelpers.DomainExists(ConnectionString))
{
using(var baseDirectory = new DirectoryEntry(ConnectionString))
{
baseDirectory.Username = Username;
baseDirectory.Password = Password;
using (DirectorySearcher searcher = new DirectorySearcher())
{
searcher.SearchRoot = baseDirectory;
searcher.Filter = "(objectCategory=user)";
searcher.SearchScope = SearchScope.Subtree;
var userResults = searcher.FindAll();
foreach (SearchResult user in userResults)
{
var newUser = new User();
newUser.Name = user.Properties["name"][0].ToString();
newUser.Path = user.Path;
//.Groups is just a List<string>.
newUser.Groups = user.Properties?????
_users.Add(newUser);
}
}
}
}
How do I retrieve a collection of groups the user belongs to?
Thank you! :)
user.Properties["memberOf"]
don't forget to add searcher.PropertiesToLoad.Add("memberOf"); before ...searcher.FindAll()
To populate your property:
//.Groups is just a List<string>.
foreach(object group in user.Properties["memberOf"])
newUser.Groups.Add((string)group);
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.
It's really hard to get it right using DirectoryEntry. First of all, getting memberOf attribute doesn't give you primary group. Also, if the user has a domain local group from another domain, it won't show up in memberOf attribute. 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);
}