OK, working with active directory, my one remaining requirement is to pull back all the users in a given group and get their details - first name, last name, username, email.
Having read up around this I was surprised to discover that there doesn't seem to be a way of doing this in an efficient fashion. I've worked out two ways of getting the results, but both seem absurdly wasteful.
The first is to search members of a group, like this:
DirectoryEntry searchRoot = new DirectoryEntry("LDAP://" + server, username, password);
DirectorySearcher search = new DirectorySearcher(searchRoot);
search.Filter = string.Format("(|(CN={0})(CN={1}))", "Group1", "Group2");
search.PropertiesToLoad.Add("member");
SearchResultCollection results = search.FindAll();
Which brings back a single result containing usernames, and usernames only. You can dissect this into an array of individual usernames, but then to get the details of each user you have to search AD again on a per-name basis.
The other approach is to do this:
DirectoryEntry searchRoot = new DirectoryEntry("LDAP://" + server, username, password);
DirectorySearcher search = new DirectorySearcher(searchRoot);
search.Filter = string.Format("(&(&(objectClass=user)(objectCategory=person))(memberOf=*))");
search.PropertiesToLoad.Add("memberOf");
search.PropertiesToLoad.Add("name");
search.PropertiesToLoad.Add("mail");
search.PropertiesToLoad.Add("displayName");
SearchResultCollection mySearchResultColl = search.FindAll();
foreach (SearchResult result in mySearchResultColl)
{
foreach (string prop in result.Properties["memberOf"])
{
if (prop.Contains("Group1") || prop.Contain("Group2"))
{
//add user to list
}
}
}
Which gets me what I want but involves retrieving every active directory user and then iterating through the collection to find ones that match. That works fine on my little test directory, but I shudder to think how slow it's going to be on a system with thousands of users.
I'm aware you can do what I need using the PrinicpalContext object, but as best I can tell that only works if the code is running in the same domain, which I can't guarantee. I need to be able to query across domains.
Is there a better way of doing this? Or am I just going to have to suck on the performance issues?
If you only need to find direct members, you can use Attribute Scope Query (ASQ).
This requires domain/forest functional level of 2003 (forgot domain or forest).
DirectoryEntry groupEntry = new DirectoryEntry("LDAP://<server>/<group DN>", "user", "pwd");
DirectorySearcher searcher = new DirectorySearcher(groupEntry);
searcher.SearchScope = SearchScope.Base;
searcher.AttributeScopeQuery = "member";
searcher.Filter = "(&(objectCategory=person)(objectClass=user))";
searcher.PropertiesToLoad.Clear();
searcher.PropertiesToLoad.Add("name");
searcher.PropertiesToLoad.Add("mail");
searcher.PropertiesToLoad.Add("displayName");
foreach (SearchResult result in searcher.FindAll())
{
Console.WriteLine(result.Path);
}
For nested group members, you may use the LDAP_MATCHING_RULE_IN_CHAIN matching rule.
This requires domain/forest functional level of 2008 R2 (again, forgot domain or forest).
DirectoryEntry rootEntry = new DirectoryEntry("GC://<server>", "user", "pwd");
DirectorySearcher searcher = new DirectorySearcher(rootEntry);
searcher.SearchScope = SearchScope.Subtree;
searcher.Filter = "(&(objectCategory=person)(objectClass=user)(memberOf:1.2.840.113556.1.4.1941:=<group DN>))";
searcher.PropertiesToLoad.Clear();
searcher.PropertiesToLoad.Add("name");
searcher.PropertiesToLoad.Add("mail");
searcher.PropertiesToLoad.Add("displayName");
foreach (SearchResult result in searcher.FindAll())
{
Console.WriteLine(result.Path);
}
Limitations:
Both methods don't handle primary group membership
ASQ don't work across domain, an exception will be thrown if member from another domain is found. So normally you can only perform this for global groups.
You could utilize an ANR search for some of those attributes For more information, see this article - the functionality has been there since 2000:
http://support.microsoft.com/kb/243299
In order to search for other attributes not in the default set, you'd need to tweak the schema (which may not be suitable in your situation).
Related
I need to fetch active directory records and insert in SQL database. There are approximately 10,000 records. I have used this code:
List<ADUser> users = new List<ADUser>();
DirectoryEntry entry = new DirectoryEntry("LDAP://xyz.com");
ADUser userToAdd = null;
IList<string> dict = new List<string>();
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(&(objectClass=user))";
search.PropertiesToLoad.Add("samaccountname");
search.PageSize = 1000;
foreach (SearchResult result in search.FindAll())
{
DirectoryEntry user = result.GetDirectoryEntry();
if (user != null && user.Properties["displayName"].Value!=null)
{
userToAdd = new ADUser
{
FullName = Convert.ToString(user.Properties["displayName"].Value),
LanId = Convert.ToString(user.Properties["sAMAccountName"].Value)
};
users.Add(userToAdd);
}
}
How can I optimize the above code in terms of speed and space complexity? Can I use traversal in binary tree as Active Directory structure looks similar to binary tree.
The list returned by DirectorySearcher.FindAll() is just a list. So you can't traverse it any better than you already are.
To optimize this, don't use GetDirectoryEntry(). That is doing two things:
Making another network request to AD that is unnecessary since the search can return any attributes you want, and
Taking up memory since all those DirectoryEntry objects will stay in memory until you call Dispose() or the GC has time to run (which it won't till your loop finally ends).
First, add displayName to your PropertiesToLoad to make sure that gets returned too. Then you can access each property by using result.Properties[propertyName][0]. Used this way, every property is returned as an array, even if it is a single-value attribute in AD, hence you need the [0].
Also, if your app is staying open after this search completes, make sure you call Dispose() on the SearchResultCollection that comes out of FindAll(). In the documentation for FindAll(), the "Remarks" section indicates you could have a memory leak if you don't. Or you can just put it in a using block:
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(&(objectClass=user))";
search.PropertiesToLoad.Add("sAMAccountName");
search.PropertiesToLoad.Add("displayName");
search.PageSize = 1000;
using (SearchResultCollection results = search.FindAll()) {
foreach (SearchResult result in results) {
if (result.Properties["displayName"][0] != null) {
userToAdd = new ADUser {
FullName = (string) result.Properties["displayName"][0],
LanId = (string) result.Properties["sAMAccountName"][0]
};
users.Add(userToAdd);
}
}
}
I'd like to search my active directory for all users that belong to a particular physicalDeliveryOfficeName (LDAP) and store them into an array of type SearchResult. Can I do this with a DirectorySearcher filter? Or is there a better approach?
I'm using asp.net, visual c#. Thanks!
Using the DirectorySeacher class, your query could be
(&(objectClass=user)(physicalDeliveryOfficeName=Kalkutta))
where objectClass=user to get only user entries and physicalDeliveryOfficeName=Kalkutta is your query for an office.
DirectoryEntry entry = new DirectoryEntry("LDAP://...");
DirectorySearcher search = new DirectorySearcher(entry)
{
SearchScope = SearchScope.Subtree,
Filter = "(&(objectClass=user)(physicalDeliveryOfficeName=Kalkutta))"
};
search.PropertiesToLoad.Add("cn");
SearchResultCollection result = search.FindAll();
foreach (SearchResult r in result)
Response.Write(r.Properties["cn"][0]);
As I'm not very experienced in terms of retrieving information from the Active Directory I hope to get some pointers into the right direction.
I'd like to list all ous (organizational units) below a specified ou, but unfortunately I don't know how to get things working.
Let's say that the structure in our AD is as follows
SomeStartingPoint
|-MySpecifiedOuName
|-SubOuName1
|-SubOuName2
What I've got so far is
DirectoryEntry rootDSE = new DirectoryEntry( "LDAP://RootDSE" );
string defaultNamingContext = rootDSE.Proeprties[ "defaultNamingContext" ].Value.ToString();
DirectoryEntry entry = new DirectoryEntry( "LDAP://" + defaultNamingContext );
DirectorySearcher ouSearch =
new DirectorySearcher( entry.Path ) { Filter = "(objectCategory=organizationalUnit)", SearchScope = SearchScope.Subtree };
ouSearch.PropertiesToLoad.Add( "name" );
ouSearch.PropertiesToLoad.Add( "adspath" );
SearchResultCollection allOUs = ouSearch.FindAll();
Now I can loop over the allOUs and access .Properties[ "name" ][ 0 ] and .Properties[ "adspath" ][ 0 ] to list the values for all OUs.
Now when I try to use a different filter as in Filter = (&(objectCategory=organizationalUnit)(ou=MySpecifiedOuName)), I do get the single entry corresponding to exactly MySpecifiedOuName, but not the underlying OUs below, even though they contain MySpecifiedOuName within their path. This is probably down to the fact that in my example I query the wrong thing (the OU directly), but I don't know any other way.
Any ideas?
Just try with this filter :
"(objectCategory=CN=Organizational-Unit,CN=Schema,CN=Configuration,DC=dom,DC=fr)"
Adapted to your domain with configurationNamingContext from the RootDSE for CN=Configuration,DC=dom,DC=fr
This may be explained by the fact that objectCategory is a distinguich name, I know that Microsoft tools are making the translation but it seems not to work for you here.
-----Edited-----
As #Desmond insist on the fact "(objectCategory=organizationalUnit)" I just test it an it works. "(objectCategory=CN=Organizational-Unit,CN=Schema,CN=Configuration,DC=dom,DC=fr)" also works.
DirectoryEntry deBase = new DirectoryEntry("LDAP://WM2008R2ENT:389/dc=dom,dc=fr", "jpb", "Pwd");
/* Directory Search
*/
DirectorySearcher dsLookForOUs = new DirectorySearcher(deBase);
dsLookForOUs.Filter = "(objectCategory=organizationalUnit)";
dsLookForOUs.SearchScope = SearchScope.Subtree;
dsLookForOUs.PropertiesToLoad.Add("cn");
dsLookForOUs.PropertiesToLoad.Add("ou");
SearchResultCollection srcOUs = dsLookForOUs.FindAll();
foreach (SearchResult srOU in srcOUs)
{
Console.WriteLine("{0}", srOU.Path);
}
The only way to do what you want is in a recursive fashion. ou is the RDN attribute for the OU (much like CN is for users). Thus doing a search for ou=foo will only get you OUs with their OU attribute set to foo.
In order to walk the chain, you're going to need to do a search for all the OUs at the current level (use a OneLevel search instead of SubTree), and then recurse through there. This is not at all efficient, though, since you're going to be issuing numerous queries to AD.
Instead, you could do what you're doing and then build your hierarchy by sorting based on DN/depth. This is more complex but it will be more efficient from a resource access point of view.
You most likley are only missing the subtree option:
ouSearch.SearchScope = SearchScope.Subtree;
Our institution has a large LDAP system which we access over LDAPS on port 636. I'm attempting to create an IQueryable interface to this LDAP which maps onto existing People entities but am having trouble.
I mention the above so folks know my end goal but I'd be satisified today if someone could help me simply run a successful query against this non-AD LDAP, even if its outside of the IQueryable realm. Here's what I have so far (I've edited the password, uid and ou for security):
var url = #"ldaps://ldap.ucdavis.edu:636/uid=s1,ou=s2,dc=ucdavis,dc=edu";
var password = #"something";
DirectoryEntry entry = new DirectoryEntry(url);
entry.Password = password;
entry.AuthenticationType = AuthenticationTypes.Secure;
DirectorySearcher mySearcher = new DirectorySearcher(entry);
SearchResultCollection results;
results = mySearcher.FindAll();
foreach(SearchResult resEnt in results) {
ResultPropertyCollection propcoll = resEnt.Properties;
foreach (string key in propcoll.PropertyNames)
{
foreach (object values in propcoll[key])
{
switch (key)
{
case "sn":
//sb.Append(key.ToString() + "<surname>"
//+ values.ToString() + "</surname>");
break;
case "cn":
//sb.Append(key.ToString() + "<cn>"
//+ values.ToString() + "</cn>");
break;
case "name":
//sb.Append(key.ToString() + "<name>"
//+ values.ToString() + "</name>");
break;
}
}
}
}
But I keep getting an "unknown error" on the line mySearcher.FindAll(). Anybody see an obvious problem here? Am I specifying the ou and uid correctly?
>see an obvious problem here?
Where's the search query itself?
DirectorySearcher(entry);
It initializes the searcher given your base path.
Try setting the "filter" property to a valid LDAP search string, such as "(objectClass=inetOrgPerson)".
Also, try setting the SearchScope to OneLevel.
Anyway, I don't really think that will help.
AFAIK DirectorySearcher is merely a wrapper over the ADSI. What you should use instead is the classes from System.DirectoryServices.Protocols namespace, see this article: http://msdn.microsoft.com/en-us/library/bb332056.aspx
I wonder if the LDAP DN is correct? AD used dc=acme, dc=com, but other LDAP servers might use ou=ucdavis, o=edu perhaps.
I have some code that queries Active Directory to verify user existence. I am trying to verify a long list of about 1300 ids. I've tried several methods to verify if a user account (LINQ to AD, DirectorySearcher (with and without a parent DirectoryEntry) and also a DirectoryEntry that links to the WinNT:// path). Every time it will come back and say that several users do not exist. If I hardcode their userids in the code and execute for individually, it validates existence. If I try and do it in a foreach loop, I get several false negatives.
Here's the code I am using right now..
static string[] userIDs = new string[] "user1","user2","user3","user4","user5","user6","user7","user8"...,"user1300"};
List<string> nonExistingUsers = new List<string>();
List<string> ExistingUsers = new List<string>();
foreach (string s in userIDs)
{
DirectorySearcher search = new DirectorySearcher();
search.Filter = String.Format("(SAMAccountName={0})", s);
search.PropertiesToLoad.Add("cn");
DirectorySearcher ds = new DirectorySearcher(de, "(&(objectClass=user)(cn=" + s + "))", new string[] { "Name" }, SearchScope.Subtree);
SearchResultCollection resultCollection = ds.FindAll();
SearchResult result = search.FindOne();
if (result != null)
ExistingUsers.Add(s);
else
nonExistingUsers.Add(s);
}
Any suggestions or reasons why I am getting the false negatives?
Couple of things:
first of all, try using the "anr=" (ambiguous name resolution) in your LDAP filter - it searches several name-related attributes and make searching easier. The UserID might not be part of the actual "common name" (CN=user1)
secondly, use the objectCategory instead of objectClass - the objectCategory is single-valued and indexed and thus a fair bit faster on searches
thirdly: why are you first calling .FindAll() and then .FindOne() on the next line? Doesn't seem really necessary at all....
WinNT:// really is only for backward compatibility and if you need to deal with local computer accounts - try to avoid it whenever possible, it also exposes a lot less properties than LDAP
Here's my code I'd write:
static string[] userIDs = new string[] "user1","user2","user3","user4","user5","user6","user7","user8"...,"user1300"};
DirectoryEntry searchRoot = new DirectoryEntry("LDAP://cn=Users,dc=YourComp,dc=com");
List<string> nonExistingUsers = new List<string>();
List<string> ExistingUsers = new List<string>();
foreach (string s in userIDs)
{
DirectorySearcher search = new DirectorySearcher(searchRoot);
search.SearchScope = SearchScope.Subtree;
search.Filter = string.Format("(&(objectCategory=person)(anr={0}))", s);
SearchResultCollection resultCollection = ds.FindAll();
if(resultCollection != null && resultCollection.Count > 0)
ExistingUsers.Add(s);
else
nonExistingUsers.Add(s);
}
Does that work in your scenario??
Also, if you're using .NET 3.5 or higher, things got a lot easier - see:
Managing Directory Security Principals in the .NET Framework 3.5