Using PrincipalSearcher to find users with "or" parameters - c#

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);

Related

Active Directory Query Users with contains ids from an array using C#

I am trying to perform a query to Active Directory to obtain all users where id is contained in the input array using a single query.
Is it possible? It
public List<Principal> Get(IEnumerable<string> ids)
{
var context = new PrincipalContext(ContextType.Domain);
var userPrincipal = new UserPrincipal(context)
{
// SOME logic with input array
};
var searcher = new PrincipalSearcher(userPrincipal);
return searcher.FindAll().ToList();
}
It is not a problem to query single user by id:
public static UserPrincipal? Get(string id)
{
var context = new PrincipalContext(ContextType.Domain);
var userPrincipal = UserPrincipal.FindByIdentity(context, id);
return userPrincipal;
}
Also, I can not find any information in docs
You can't do it with PrincipalSearcher. You'll have to use DirectorySearcher (which is what PrincipalSearcher uses behind the scenes anyway).
I actually already wrote a method that does exactly this in an article I wrote about getting better performance when programming withActive Directory. That method takes usernames and returns a list of email addresses:
public IEnumerable<string> GetEmailAddresses(IEnumerable<string> usernames) {
var filter = new StringBuilder();
var numUsernames = 0;
var e = usernames.GetEnumerator();
var hasMore = e.MoveNext();
while (hasMore) {
var username = e.Current;
filter.Append($"(sAMAccountName={username})");
numUsernames++;
hasMore = e.MoveNext();
if (numUsernames == 50 || !hasMore) {
var search = new DirectorySearcher(new DirectoryEntry()) {
PageSize = 1000,
Filter = $"(&(objectClass=user)(objectCategory=person)(|{filter}))"
};
search.PropertiesToLoad.Add("mail");
using (var results = search.FindAll()) {
foreach (SearchResult result in results) {
yield return (string) result.Properties["mail"][0];
}
}
filter.Clear();
numUsernames = 0;
}
}
}
This method builds an LDAP query in the format:
(&(objectClass=user)(objectCategory=person)(|(sAMAccountName={username1})(sAMAccountName={username2})))
It does them in batches of 50 just to make sure you never hit the size limit on LDAP requests, although you could probably push that number higher.
You can adapt it to return whatever you'd like by changing the method signature and the yield return line.
In that same article I discuss the benefits of using DirectoryEntry/DirectorySearcher instead of the AccountManagement namespace. You can get far better performance.

Convert list of samaccount names to distinguished names

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;
}
}
}

Active Directory group search C#

I'm finding a group that is under a domain to see the group entered by user is valid or not. I have checked with few groups. it is working fine. But there is one group which is in the domain(I'm able to see it from AD) but still not succeding my code condition.
Can you please review if I'm missing something?
PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "mydomain");
GroupPrincipal qbeGroup = new GroupPrincipal(ctx);
PrincipalSearcher srch = new PrincipalSearcher(qbeGroup);
IsValidGroup = 0;
foreach (var found in srch.FindAll())
{
if (found.ToString() == TxtAccName.Text)
{
IsValidGroup = 1;
break;
}
}
You've got the essence actually. But your code is not feaseble. Try below, let me know.
bool groupExists;
using( var ctx = new PrincipalContext(ContextType.Domain, "mydomain") )
{
var filter = new GroupPrincipal(ctx) { Name = TxtAccName.Text }
using ( var searcher = new PrincipalSearcher(filter) )
{
groupExists = searcher.FindAll().Any();
}
}

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>()

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;
}

Categories