Convert list of samaccount names to distinguished names - c#

I have a list of account names (thousands) that I need to add as members of a group, so I need to look up the distinguished names of those accounts so I can add them as members of a group.
There are 21000 user accounts. It takes hours to look up the distinguished names, is there a faster way?
DirectoryEntry deDomain = new DirectoryEntry();
deDomain.Path = "LDAP://DC=my,DC=ca;
deDomain.Username = "me";
deDomain.Password = "mypassword";
DirectorySearcher dsSearch = new DirectorySearcher(deDomain);
dsSearch.CacheResults = false;
dsSearch.PropertiesToLoad.Add("distinguishedname");
foreach (string sam in lstSamAccountNames)
{
dsSearch.Filter = $"(&(objectCategory=person)(objectClass=user)(SAMAccountName={ReplaceSpecialCharacters(sSamAccountName)}))";
SearchResult srPerson = dsSearch.FindOne();
yield return srPerson.Properties["distinguishedname"][0].ToString();
}

I don't have an AD domain handy that I can do performance testing with at the moment, but have you tried the AccountManagement library instead of using DirectorySearcher?
using System.DirectoryServices.AccountManagement;
// ...
var domainName = "MyDomainName";
using (var context = new PrincipalContext(ContextType.Domain, domainName))
{
foreach (var sam in lstSamAccountNames)
{
var userName = ReplaceSpecialCharacters(sam);
using (var user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, $#"{domainName}\{userName}"))
{
yield return user.DistinguishedName;
}
}
}

Related

Get multiple user with similar names from Active Directory using C#

I need to display a list of users (in autocomplete) with similar names from the AD. For example, if I search Arthur and there are 2 people with same name (first name or last name), I should display both the names in the autocomplete. I tried to get the names but it is taking around 2-4 minutes to get the names. I am using the following code:
string[] domainNames = new string[] { "domain1", "domain2" };
List<string> userNames = new List<string>();
string user = string.Empty;
foreach (string domain in domainNames)
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, domain))
{
GroupPrincipal group = GroupPrincipal.FindByIdentity(pc, IdentityType.SamAccountName, "Domain Users");
if (group != null)
{
foreach (Principal p in group.GetMembers(false))
{
if (p.Name.ToLower().Contains(key.ToLower()))
{
if (!userNames.Contains(p.Name))
userNames.Add(p.Name);
}
}
}
}
}
Any way I can speed up the process? I am already using ajax call.
The cn= filter isn't really valid - there's nothing guaranteeing that format. Instead, look up Ambigous Name Resolution - it's designed for this: http://social.technet.microsoft.com/wiki/contents/articles/22653.active-directory-ambiguous-name-resolution.aspx.
DirectorySearcher ds = new DirectorySearcher();
ds.SearchRoot = new DirectoryEntry("LDAP://" + domain, domain + #"\" + userName, password);
ds.Filter = "(&(objectClass=user)(cn=*" + key + "*))";
ds.PropertyNamesOnly = true;
ds.PropertiesToLoad.Add("name");
ds.PropertiesToLoad.Add("cn");
foreach (SearchResult searchResults in ds.FindAll())
{
foreach (string propertyName in searchResults.Properties.PropertyNames)
{
foreach (Object retEntry in searchResults.Properties[propertyName])
{
var user = retEntry.ToString().Split('/').Where(x => x.Contains("CN")).Select(y => y).FirstOrDefault().Split(',').Where(z => z.Contains("CN")).Select(c => c).FirstOrDefault().Split(',').FirstOrDefault().Split('=')[1];
if(!string.IsNullOrWhiteSpace(user))
userNames.Add(user);
}
}
}
Reduced to 30-40 seconds.

PrincipalSearchResult<T> with PrincipalSearcher FindAll, why does T have to be Principal and not UserPrincipal

I'm just curious:
List<string> ADUsers = new List<string>();
using (PrincipalContext principle_context = new PrincipalContext(ContextType.Domain, "MYDOMAIN"))
using (UserPrincipal user_principal = new UserPrincipal(principle_context) { Enabled = true, Name = "*", EmailAddress = "*" })
using (PrincipalSearcher user_searcher = new PrincipalSearcher(user_principal))
using (PrincipalSearchResult<Principal> results = user_searcher.FindAll())
{
foreach (Principal p in results)
{
ADUsers.Add(p.Name + " " + ((UserPrincipal)p).EmailAddress);
}
}
...is there a way to avoid having to cast my results here? I wanted to do something like:
using (PrincipalSearchResult<UserPrincipal> results = user_searcher.FindAll())
...so that my search result would be of the type I needed it, but it seems the FindAll method only allows using the <Principal> type. Is there a better way?
Thank you.
Actually foreach will cast the enumerated values for you so you could do this
foreach (UserPrincipal p in results)
{
ADUsers.Add(p.Name + " " + p.EmailAddress);
}
assuming that Name is defined within UserPrincipal as well as Principal.
You could try adding a Cast. Change
PrincipalSearchResult<Principal> results = user_searcher.FindAll()
to
var results = user_searcher.FindAll().Cast<UserPrincipal>()

Using PrincipalSearcher to find users with "or" parameters

Is it possible to use System.DirectoryServices.AccountManagement.PrincipalSearcher to search based on multiple parameters using "or" (not "and").
i.e.
// This uses an and
//(&(objectCategory=person)(!UserAccountControl:1.2.840.113556.1.4.803:=2)(&(SAMAccountName=tom*)(DisplayName=tom*)))
var searchPrinciple = new UserPrincipal(context);
searchPrinciple.DisplayName = "tom*";
searchPrinciple.SamAccountName = "tom*";
var searcher = new PrincipalSearcher();
searcher.QueryFilter = searchPrinciple;
var results = searcher.FindAll();
and I would like a search similar to this (in LDAP) using PrincipalSearcher (not DirectorySearcher)
// (&(objectCategory=person)(!UserAccountControl:1.2.840.113556.1.4.803:=2)(|(SAMAccountName=tom*)(DisplayName=tom*)))
It's obviously not possible, here is a workaround:
List<UserPrincipal> searchPrinciples = new List<UserPrincipal>();
searchPrinciples.Add(new UserPrincipal(context) { DisplayName="tom*"});
searchPrinciples.Add(new UserPrincipal(context) { SamAccountName = "tom*" });
searchPrinciples.Add(new UserPrincipal(context) { MiddleName = "tom*" });
searchPrinciples.Add(new UserPrincipal(context) { GivenName = "tom*" });
List<Principal> results = new List<Principal>();
var searcher = new PrincipalSearcher();
foreach (var item in searchPrinciples)
{
searcher = new PrincipalSearcher(item);
results.AddRange(searcher.FindAll());
}
Not necessarily as clean as some of the other answers but here is how I've implemented this in a project I'm working on. I wanted both searches to be run async to try and reduce any slow down due to running two AD queries.
public async static Task<List<ADUserEntity>> FindUsers(String searchString)
{
searchString = String.Format("*{0}*", searchString);
List<ADUserEntity> users = new List<ADUserEntity>();
using (UserPrincipal searchMaskDisplayname = new UserPrincipal(domainContext) { DisplayName = searchString })
using (UserPrincipal searchMaskUsername = new UserPrincipal(domainContext) { SamAccountName = searchString })
using (PrincipalSearcher searcherDisplayname = new PrincipalSearcher(searchMaskDisplayname))
using (PrincipalSearcher searcherUsername = new PrincipalSearcher(searchMaskUsername))
using (Task<PrincipalSearchResult<Principal>> taskDisplayname = Task.Run<PrincipalSearchResult<Principal>>(() => searcherDisplayname.FindAll()))
using (Task<PrincipalSearchResult<Principal>> taskUsername = Task.Run<PrincipalSearchResult<Principal>>(() => searcherUsername.FindAll()))
{
foreach (UserPrincipal userPrincipal in (await taskDisplayname).Union(await taskUsername))
using (userPrincipal)
{
users.Add(new ADUserEntity(userPrincipal));
}
}
return users.Distinct().ToList();
}
My ADUserEntity class has an equality check based on the SID. I tried to add the Distinct() on to the Union() of the two searcher results but that didn't work.
I welcome any constructive criticism on my answer as I'd like to know if there is any way I can improve it.
I know this is kind of late, but this is the construct I use when searching AD:
public static Task<IEnumerable<SomeUserModelClass>> GetUsers(//Whatever filters you want)
{
return Task.Run(() =>
{
PrincipalContext context = new PrincipalContext(ContextType.Domain);
UserPrincipal principal = new UserPrincipal(context);
principal.Enabled = true;
PrincipalSearcher searcher = new PrincipalSearcher(principal);
var users = searcher.FindAll().Cast<UserPrincipal>()
.Where(x => x.SomeProperty... // Perform queries)
.Select(x => new SomeUserModelClass
{
userName = x.SamAccountName,
email = x.UserPrincipalName,
guid = x.Guid.Value
}).OrderBy(x => x.userName).AsEnumerable();
return users;
});
}
The FindAll method searches the domain specified in the principal
context for objects that have identical properties to those set on the
query filter. The FindAll method returns all objects that match the
supplied object whereas the FindOne method returns only a single
matching principal object.
http://msdn.microsoft.com/en-us/library/bb384378(v=vs.90).aspx
I don't know what you need, but you could do a search by 1 proprety and 1 by other and then use LINQ on the lists to merge,filter and etc...
PrincipalContext pContext = new PrincipalContext(ContextType.Machine, Environment.MachineName);
GroupPrincipal gp = GroupPrincipal.FindByIdentity(pContext, "Administrators");
bool isMember = UserPrincipal.Current.IsMemberOf(gp);

How to retrieve SAMAccountName from Active Directory

I implemented a method that returns a list of Active Directory users, I would like to retrieve SAMAccountName like this Domain\Administrator.
This is the method I use:
public Collection<software_user> GetUsersFromAD(String adConnectionString)
{
var users = new Collection<software_user>();
using (var directoryEntry = new DirectoryEntry(adConnectionString))
{
var directorySearcher = new DirectorySearcher(directoryEntry);
directorySearcher.Filter = "(&(objectClass=user))";
var propertiesToLoad = new[]
{
"SAMAccountName",
"displayName",
"givenName",
"sn",
"mail",
"userAccountControl",
"objectSid"
};
directorySearcher.PropertiesToLoad.AddRange(propertiesToLoad);
foreach (SearchResult searchEntry in directorySearcher.FindAll())
{
var userEntry = searchEntry.GetDirectoryEntry();
var ldapUser = new software_user();
ldapUser.User_name = NullHandler.GetString(userEntry.Properties["displayName"].Value);
if (string.IsNullOrEmpty(ldapUser.User_name))
continue;
ldapUser.User_name = NullHandler.GetString(userEntry.Properties["SAMAccountName"].Value);
ldapUser.email = NullHandler.GetString(userEntry.Properties["mail"].Value);
ldapUser.user_shortname = NullHandler.GetString(userEntry.Properties["givenName"].Value);
var userAccountControl = (int)userEntry.Properties["userAccountControl"].Value;
//ldapUser.IsActive = (userAccountControl & UF_ACCOUNTDISABLE) != UF_ACCOUNTDISABLE;
var sid = new SecurityIdentifier((byte[])userEntry.Properties["objectSid"][0], 0).Value;
//ldapUser.SId = sid;
users.Add(ldapUser);
}
}
return users;
}
First off: Domain\Administrator is NOT a SAM account name! The SAM account name is a unique (over the whole domain) name of up to 20 characters in length - typically it's your "Windows user name" (e.g. Administrator) - but it does NOT include the domain name. That value made up of domain\username is NOT stored in Active Directory anywhere!
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....
string samAccountName = user.SamAccountName;
}
The new S.DS.AM makes it really easy to play around with users and groups in AD!
If you want to search for a whole group of users (or groups or computers), you can use a PrincipalSearcher and a "query-by-example" principal to do your searching:
// create your domain context
PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
// define a "query-by-example" principal - here, we search for a UserPrincipal
// and with the last name (Surname) of "Miller"
UserPrincipal qbeUser = new UserPrincipal(ctx);
qbeUser.Surname = "Miller";
// create your principal searcher passing in the QBE principal
PrincipalSearcher srch = new PrincipalSearcher(qbeUser);
// find all matches
foreach(var found in srch.FindAll())
{
// do whatever here - "found" is of type "Principal" - it could be user, group, computer.....
}
You can translate a user as a Distinguished name to DOMAIN\SAMaccount form using the Object's SID, and the System.Security.Principal.SecurityIdentifier.Translate command.
public Collection<software_user> GetUsersFromAD(String adConnectionString)
{
var users = new Collection<software_user>();
using (var directoryEntry = new DirectoryEntry(adConnectionString))
{
var directorySearcher = new DirectorySearcher(directoryEntry);
directorySearcher.Filter = "(&(objectClass=user))";
var propertiesToLoad = new[]
{
"SAMAccountName",
"displayName",
"givenName",
"sn",
"mail",
"userAccountControl",
"objectSid"
};
directorySearcher.PropertiesToLoad.AddRange(propertiesToLoad);
foreach (SearchResult searchEntry in directorySearcher.FindAll())
{
var userEntry = searchEntry.GetDirectoryEntry();
var ldapUser = new software_user();
ldapUser.User_name = NullHandler.GetString(userEntry.Properties["displayName"].Value);
if (string.IsNullOrEmpty(ldapUser.User_name))
continue;
ldapUser.User_name = NullHandler.GetString(userEntry.Properties["SAMAccountName"].Value);
ldapUser.email = NullHandler.GetString(userEntry.Properties["mail"].Value);
ldapUser.user_shortname = NullHandler.GetString(userEntry.Properties["givenName"].Value);
var userAccountControl = (int)userEntry.Properties["userAccountControl"].Value;
//ldapUser.IsActive = (userAccountControl & UF_ACCOUNTDISABLE) != UF_ACCOUNTDISABLE;
SecurityIdentifier sid = new SecurityIdentifier((byte[])userEntry.Properties["objectSid"][0], 0).Value;
--> NTAccount account = (NTAccount) sid.Translate(typeof(NTAccount));
--> ldapUser.User_name = account.ToString();
//ldapUser.SId = sid;
users.Add(ldapUser);
}
}
return users;
}

How to find users in AD that belong to a group, and just get their SAMAccountName, and SID?

I just want a user to be able to type in a group name in a textbox, and return just their login name and their SID.
So far i have this, and that loads the users in the group but im unsure how to extract the login and SID.
SearchResult result;
DirectorySearcher search = new DirectorySearcher();
search.Filter = String.Format("(cn={0})", txtGroup.Text);
search.PropertiesToLoad.Add("member");
search.PropertiesToLoad.Add("cn");
search.PropertiesToLoad.Add("objectGUID");
result = search.FindOne();
StringBuilder userNames = new StringBuilder();
if (result != null)
{
for (int counter = 0; counter <
result.Properties["member"].Count; counter++)
{
string user = (string)result.Properties["member"][counter];
userNames.AppendLine(user);
}
}
lblResults.Text = userNames.ToString();
The propertie wich contains SID is called objectSid, and the propertie wich contain th login is sAMAccountName for the NT4 compatible version and userPrincipalName. You'd better work with #Virkkunen advice.
static void Main(string[] args)
{
/* Connection to Active Directory
*/
DirectoryEntry deBase = new DirectoryEntry("LDAP://192.168.183.138:389/dc=societe,dc=fr", "administrateur", "pwd");
/* Directory Search
*/
DirectorySearcher dsLookForGrp = new DirectorySearcher(deBase);
dsLookForGrp.Filter = String.Format("(cn={0})", "yourgroup");
dsLookForGrp.SearchScope = SearchScope.Subtree;
dsLookForGrp.PropertiesToLoad.Add("distinguishedName");
SearchResult srcGrp = dsLookForGrp.FindOne();
/* Directory Search
*/
DirectorySearcher dsLookForUsers = new DirectorySearcher(deBase);
dsLookForUsers.Filter = String.Format("(&(objectCategory=person)(memberOf={0}))", srcGrp.Properties["distinguishedName"][0]);
dsLookForUsers.SearchScope = SearchScope.Subtree;
dsLookForUsers.PropertiesToLoad.Add("objectSid");
dsLookForUsers.PropertiesToLoad.Add("userPrincipalName ");
dsLookForUsers.PropertiesToLoad.Add("sAMAccountName");
SearchResultCollection srcLstUsers = dsLookForUsers.FindAll();
foreach (SearchResult sruser in srcLstUsers)
{
Console.WriteLine("{0}", sruser.Path);
SecurityIdentifier sid = new SecurityIdentifier((byte[]) sruser.Properties["objectSid"][0], 0);
Console.WriteLine(sid.ToString());
foreach (string property in sruser.Properties.PropertyNames)
{
Console.WriteLine("\t{0} : {1} ", property, sruser.Properties[property][0]);
}
}
}
I think it would work better if you reverse your query:
(&(objectClass=user)(memberOf={0}))
This way you'll directly get back a list of users by using FindAll. Don't forget to add sAMAccountName etc into PropertiesToLoad.

Categories