I currently have a method that queries for Canonical Name and the CN of a specified machine. Although, when validating the data within the method, nothing is outputted. I'm not sure if I've formatted something incorrectly or didn't filter properly. I've tried a number of things (changing the format of search.Filter), etc., to no avail. Below is the latest code I have. I'm hoping someone will see something that I am missing.
public class ComputersCheck
{
public string OU { get; set; }
public string ComputerName { get; set; }
public override string ToString()
{
return OU + " " + " " + ComputerName;
}
}
public List<ComputersCheck> GetOU(string PCName)
{
List<ComputersCheck> Computers = new List<ComputersCheck>().ToList();
string DomainPath = "LDAP://DC=DOMAIN,DC=COM";
DirectoryEntry searchRoot = new DirectoryEntry(DomainPath);
DirectorySearcher search = new DirectorySearcher(searchRoot);
search.Filter = "(&(objectClass=Computer)(computerName=MACHINE_NAME))";
search.PropertiesToLoad.Add("CN");
search.PropertiesToLoad.Add("Canonical Name");
SearchResult result;
SearchResultCollection resultCol = search.FindAll();
if (resultCol != null)
{
for (int counter = 0; counter < resultCol.Count; counter++)
{
string ComputerNameOUString = string.Empty;
result = resultCol[counter];
if(result.Properties.Contains("CN") &&
result.Properties.Contains("Canonical Name"))
{
ComputersCheck objSurveyComputers = new ComputersCheck();
objSurveyComputers.ComputerName = (String)result.Properties["CN"][0];
objSurveyComputers.OU = (String)result.Properties["Canonical Name"][0];
Computers.Add(objSurveyComputers);
}
}
}
foreach (ComputersCheck computer in Computers)
{
Console.WriteLine(computer);
}
search.Dispose();
searchRoot.Dispose();
return Computers;
}
You do have a few things that need to be corrected:
There is no computerName attribute. The attribute you're after is called sAMAccountName, which is the same attribute used for the username for user accounts. However, for computers, it is the computer name followed by the $ character. So your search should look like this:
(&(objectClass=computer)(sAMAccountName=MACHINE_NAME$))
Attribute names never have spaces. You want the canonicalName attribute. I see you are assigning it to a property called OU. Just be aware that the canonicalName includes the name of the object itself at the end, not just the OU.
search.PropertiesToLoad.Add("canonicalName");
...
objSurveyComputers.OU = (String)result.Properties["canonicalName"][0];
You are calling .Dispose() on a couple objects, which is fine (although not absolutely needed). But the most important one to dispose is resultCol, because the documentation for SearchResultCollection says:
Due to implementation restrictions, the SearchResultCollection class cannot release all of its unmanaged resources when it is garbage collected. To prevent a memory leak, you must call the Dispose method when the SearchResultCollection object is no longer needed.
You can also put it in a using statement instead of calling .Dispose() manually, which would have the same effect.
Related
I've tryed a lot of others solutions, and I still didn't make it work. Can someone help me please.
my code is like that:
I saw something about secureString, I tryed to use it but it still didn't work.
I saw too another solution that says to use var rather than string in the variables. Didn't work
I dont know if I'm doing something wrong or if those solutions that dosoen't work.
public bool RedefinirSenha(string pUsuario, string pSenhaAtual, string pNovaSenha)
{
var NovaSenha = pNovaSenha;
var SenhaAtual = pSenhaAtual;
var Usuario = pUsuario;
//string Pwd = String.Format(#"""{0}""", NovaSenha);
//byte[] pwdCerto = System.Text.Encoding.Unicode.GetBytes(Pwd);
try
{
string LDAP = myLDAPpath;
DirectoryEntry ADcon = new DirectoryEntry(LDAP, Usuario, SenhaAtual, AuthenticationTypes.Secure);
if (ADcon != null)
{
DirectorySearcher search = new DirectorySearcher(ADcon);
search.Filter = "(SAMAccountName=" + Usuario + ")";
SearchResult result = search.FindOne();
if (result != null)
{
DirectoryEntry userEntry = result.GetDirectoryEntry();
if (userEntry != null)
{
try
{
userEntry.Invoke("ChangePassword", new object[] { SenhaAtual, NovaSenha }, AuthenticationTypes.Secure);
userEntry.Properties["LockOutTime"].Value = 0;
userEntry.CommitChanges();
userEntry.Close();
return true;
}
catch (Exception INex)
{
this.Erro = INex.Message + "COD:\r\n" + INex.InnerException;
userEntry.Close();
return false;
}
}
}
}
return true;
}
catch (Exception ex)
{
this.Erro = ex.Message;
return false;
}
}
First, there will be no difference at runtime if you declare the variables as var or string. Using the var keyword lets the compiler decide what the type is. Because you're assigning a string to it, then it is a string too. In most cases, var is fine. There are only very rare cases when you need to explicitly specify the type.
Second, DirectoryEntry.Invoke is defined like this:
public object Invoke (string methodName, params object[] args);
That may seem like you need to pass an object array, but that is not the case. The params keyword is a way to allow you to pass multiple parameters that get used inside the method as an array. So when you call it like this:
userEntry.Invoke("ChangePassword", new object[] { SenhaAtual, NovaSenha }, AuthenticationTypes.Secure);
The first parameter is an object array and the second parameter is AuthenticationTypes.Secure, then both of those get put inside the args array for use inside the Invoke method. But that is not what ChangePassword looks for. If this doesn't make sense to you, read the documentation for the params keyword and it should help.
When you call .Invoke("ChangePassword", ...), it calls the native Windows IADsUser.ChangePassword method. That takes two parameters: a string with the old password and a string with the new password - not an object array and an AuthenticationTypes value. So you should be calling it like this:
userEntry.Invoke("ChangePassword", SenhaAtual, NovaSenha);
You don't need to worry about the authentication because the password can only be changed over a secure connection. In the documentation, it says it behaves the same way as (IADsUser.SetPassword](https://learn.microsoft.com/en-ca/windows/win32/api/iads/nf-iads-iadsuser-setpassword), where it attempts several different ways to achieve a secure connection for you.
There is another way to change the password if the DirectoryEntry connection is already over a secure connection. A secure connection can either be using Kerberos, which can be done using AuthenticationTypes.Sealing (this is best if you are on the same network as the domain controller):
var ADcon = new DirectoryEntry(LDAP, Usuario, SenhaAtual, AuthenticationTypes.Secure | AuthenticationTypes.Sealing);
Or if by using LDAPS (LDAP over SSL), which you can use just by specifying port 636 in the LDAP path (this is the only way if you are not on the same network as the domain controller):
var ADcon = new DirectoryEntry("LDAP://example.com:636", Usuario, SenhaAtual);
If you do that, then you can change the password by updating the unicodePwd attribute directly, in the very specific way it wants it (enclosed in quotes and encoded in UTF-16), like this:
userEntry.Properties["unicodePwd"].Remove(Encoding.Unicode.GetBytes($"\"{SenhaAtual}\""));
userEntry.Properties["unicodePwd"].Add(Encoding.Unicode.GetBytes($"\"{NovaSenha}\""));
This should perform slightly faster since all of the work (changing the password and setting lockOutTime) is done over one network request instead of two.
I would like to get some additional AD attributes from the AD user. I want to use PrincipalSearcher instead of DirectorySearcher.
What I did was get the underlyingSearcher of the PrincipalSearcher and called FindOne() method. I don't like to use GetUnderlyingSearcher from PrincipalSearcher, but apparently it works.
The code works, but I'm wondering if there is a way to read additional properties/Ad attributes from Principal
Principal match = principalSearcher.FindOne();
Additionally could below code have some problems because we are using the underlyingSearcher (DirectorySearcher) methods.
I find PrincipalSearcher more higher level and also if I would use DirectorySearcher the ldapPath must be defined e.g. var ldapPath = "DC=corp,DC=ad,DC=example,DC=com"; It's just less lines of code.
public AdSimpleObject GetUser(string userName, string domainName)
{
PrincipalContext principalContext = new PrincipalContext(ContextType.Domain, domainName);
UserPrincipal userPrincipal = new UserPrincipal(principalContext);
userPrincipal.SamAccountName = userName; // where condition
using (PrincipalSearcher principalSearcher = new PrincipalSearcher(userPrincipal))
{
using (DirectorySearcher ds = (DirectorySearcher)principalSearcher.GetUnderlyingSearcher())
{
// get only properties we need, therefore search performance is increased
ds.PropertiesToLoad.Clear();
ds.PropertiesToLoad.AddRange(new string[]
{
"sAMAccountType",
"sAMAccountName",
"userPrincipalName",
"msDS-PrincipalName"
});
SearchResult match = ds.FindOne();
if (match != null)
{
AdSimpleObject ado = this.CreateAdSimpleObject(match);
return ado;
}
return null;
}
}
}
public class AdSimpleObject
{
public string AdsPath { get; set; }
public int SamAccountType { get; set; }
public string SamAccountName { get; set; }
public string MsDsPrincipalName { get; set; }
public string UserPrincipalName { get; set; }
}
private AdSimpleObject CreateAdSimpleObject(SearchResult searchItem)
{
ResultPropertyCollection props = searchItem.Properties;
string adsPath = props["adspath"]?.OfType<string>().ToList().FirstOrDefault() ?? string.Empty;
int samAccountType = props["sAMAccountType"]?.OfType<int>().ToList().FirstOrDefault() ?? 0;
string samAccountName = props["sAMAccountName"]?.OfType<string>().ToList().FirstOrDefault() ?? string.Empty;
string userPrincipalName = props["userPrincipalName"]?.OfType<string>().ToList().FirstOrDefault() ?? string.Empty;
string msDsPrincipalName = props["msDS-PrincipalName"]?.OfType<string>().ToList().FirstOrDefault() ?? string.Empty;
return new AdSimpleObject
{
AdsPath = adsPath,
SamAccountType = samAccountType,
SamAccountName = samAccountName,
UserPrincipalName = userPrincipalName,
MsDsPrincipalName = msDsPrincipalName
};
}
I'm wondering if there is a way to read additional properties/Ad attributes from Principal
You would use the DirectoryEntry object returned from Principal.GetUnderlyingObject:
Principal match = principalSearcher.FindOne();
var de = (DirectoryEntry) match.GetUnderlyingObject();
de.RefreshCache(new string[] {"someAttribute"});
var someAttribute = de.Properties["someAttribute"].Value;
Doing this will go back out to AD to get the attributes, instead of using the data that was retrieved in the search. The use of RefreshCache is to tell it to only get the attributes you are interested in. Otherwsie, if you use DirectoryEntry.Properties right away, it will go out to AD and get all attributes that have a value, which is unlikely what you need, and will just take extra time for no reason.
Additionally could below code have some problems because we are using the underlyingSearcher (DirectorySearcher) methods.
Not at all. That code is no different than just making your own DirectorySearcher and using it. In fact, with the way you've written this code, there is no real point to using UserPrincipal/PrincipalSearcher at all. The only thing it's done for you is build the query string.
The whole AccountManagement namespace is just a wrapper around DirectoryEntry/DirectorySearcher. It makes things easier for the programmer (in some cases, not all), but it does so at the cost of performance.
If you use DirectoryEntry/DirectorySearcher yourself directly, you have far more control over how much data is retrieved from AD, and how often network requests are made. Those translate into less time taken. If you're just searching for one account, then it won't make much of a difference. But if you're searching for large groups of users, or looping over a large list of users, it can make a huge difference.
I wrote an article about optimizing performance when talking to AD (specifically with DirectoryEntry/DirectorySearcher): Active Directory: Better performance
I've read plenty of similar StackOverflow questions but none seem to address the issue I'm seeing. If I query for a user using userprincipalname I get back a search result with exactly 34 properties. None of the custom properties are returned. If I query again using a custom property like employeeNumber I get back a result with 71 properties. All custom properties are included.
My issue is that I don't have the employeeNumber at run time, just the userprincipalname. I need to get back all of the custom properties all of the time. Hopefully that makes sense. Here's my practice code:
string sid = "";
using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
{
UserPrincipal user = UserPrincipal.Current;
//sid = user.SamAccountName;
sid = user.UserPrincipalName;
//sid = user.Sid.ToString();
DirectoryEntry entry = user.GetUnderlyingObject() as DirectoryEntry;
if (entry.Properties.Contains("employeeNumber"))
{
//this doesn't work
}
}
DirectoryEntry ldapConnection = new DirectoryEntry("companyname.com");
ldapConnection.Path = "LDAP://DC=companyname,DC=com";
ldapConnection.AuthenticationType = AuthenticationTypes.Secure;
DirectorySearcher search = new DirectorySearcher(ldapConnection);
search.Filter = string.Format("(&(ObjectClass=user)(userprincipalname={0}))", sid); // <-- this doesn't get custom properties
//search.Filter = string.Format("(employeeNumber={0})", "11663"); <-- this works
var result = search.FindOne(); // FindOne();
if (result.Properties.Contains("employeeNumber"))
{
//this never happens either :(
}
The above never returns the employeeNumber field, but if I uncomment the second search.Filter line and manually search by employeeNumber I find a result and it contains all of the fields that I need.
EDIT: I found a very good MSDN article Here which describes how to extend the UserPrincipal object to get custom attributes. The only issue is that it's giving me an empty string every time I access it even though I have verified that the property IS set in AD! Any help is appreciated.
EDIT 2: Here's the code for the custom principal extension:
[DirectoryRdnPrefix("CN")]
[DirectoryObjectClass("Person")]
public class UserPrincipalExtension : UserPrincipal
{
public UserPrincipalExtension(PrincipalContext context)
: base(context)
{
}
public UserPrincipalExtension(PrincipalContext context, string samAccountName, string password, bool enabled)
: base(context, samAccountName, password, enabled)
{
}
public static new UserPrincipalExtension FindByIdentity(PrincipalContext context, IdentityType type, string identityValue)
{
return (UserPrincipalExtension)FindByIdentityWithType(context, typeof(UserPrincipalExtension), type, identityValue);
}
PersonSearchFilter searchFilter;
new public PersonSearchFilter AdvancedSearchFilter
{
get
{
if (searchFilter == null)
searchFilter = new PersonSearchFilter(this);
return searchFilter;
}
}
[DirectoryProperty("employeeNumber")]
public string EmployeeNumber
{
get
{
if (ExtensionGet("employeeNumber").Length != 1)
return string.Empty;
return (string)ExtensionGet("employeeNumber")[0];
}
set
{
ExtensionSet("employeeNumber", value);
}
}
}
And the custom search filter:
public class PersonSearchFilter : AdvancedFilters
{
public PersonSearchFilter(Principal p)
: base(p)
{
}
public void SAMAccountName(string value, MatchType type)
{
this.AdvancedFilterSet("sAMAccountName", value, typeof(string), type);
}
}
Usage:
UserPrincipalExtension filter = new UserPrincipalExtension(context);
filter.AdvancedSearchFilter.SAMAccountName(UserPrincipal.Current.SamAccountName, MatchType.Equals);
PrincipalSearcher search = new PrincipalSearcher(filter);
foreach (var result in search.FindAll())
{
var q = (UserPrincipalExtension)result;
var m = q.EmployeeNumber;
}
var m is always an empty string even though ALL AD entries have an employeeNumber.
EDIT: From active directory:
I am more confidant in how to fix your second method so I will answer that first. You need to specify the property in DirectorySearcher.PropertiesToLoad when you do your search for the property to show up.
DirectorySearcher search = new DirectorySearcher(ldapConnection);
search.Filter = string.Format("(&(ObjectClass=user)(userprincipalname={0}))", sid);
search.PropertiesToLoad.Add("employeeNumber");
var result = search.FindOne(); // FindOne();
if (result.Properties.Contains("employeeNumber"))
{
//This should now work.
}
The reason it worked for string.Format("(employeeNumber={0})", "11663"); is because any search clause you add automatically gets put in to the PropertiesToLoad collection.
For your first method, I think you need to call DirectoryEntry.RefreshCache and pass in the property to make it show up.
string sid = "";
using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
{
UserPrincipal user = UserPrincipal.Current;
//sid = user.SamAccountName;
sid = user.UserPrincipalName;
//sid = user.Sid.ToString();
DirectoryEntry entry = user.GetUnderlyingObject() as DirectoryEntry;
entry.RefreshCache(new[] {"employeeNumber"});
if (entry.Properties.Contains("employeeNumber"))
{
}
}
But I am not 100% sure if that will work or not.
For your custom user principal, I am not sure what is wrong. The only difference I see between what you are doing and what I have in a project that does it that I know works is you use [DirectoryObjectClass("Person")] but I have [DirectoryObjectClass("user")]. Perhaps that is the issue
[DirectoryRdnPrefix("CN")]
[DirectoryObjectClass("user")] //Maybe this will fix it???
public class UserPrincipalExtension : UserPrincipal
{
I need to run a foreach loop in DirectorySearcher.FindAll() and get the displayname property. It seems like there are memory issues with that (referred link: Memory Leak when using DirectorySearcher.FindAll()).
My code is as follows:
List<string> usersList = new List<string>();
string displayName = string.Empty;
try
{
using (DirectoryEntry directoryEntry = new DirectoryEntry(ldap, userName, password))
{
DirectorySearcher directorySearcher = new DirectorySearcher(directoryEntry);
directorySearcher.PageSize = 500; // ADD THIS LINE HERE !
string strFilter = "(&(objectCategory=User))";
directorySearcher.PropertiesToLoad.Add("displayname");//first name
directorySearcher.Filter = strFilter;
directorySearcher.CacheResults = false;
SearchResult result;
var resultOne = directorySearcher.FindOne();
using (var resultCol = directorySearcher.FindAll())
{
for (int counter = 0; counter < resultCol.Count; counter++)
{
result = resultCol[counter];
if (result.Properties.Contains("displayname"))
{
displayName = (String)result.Properties["displayname"][0];
usersList.Add(displayName);
}
}
}
}
}
Is there any possible way to looping. I have also tried calling Dispose() method but it doesn't work. Any help is really appreciated.
Here is a solution that I came up with for a project I'm working on that I think would work for you. My solution loops through the search results and returns the display name in a list of strings up to 20.
using (DirectorySearcher ds = new DirectorySearcher())
{
//My original filter
//ds.Filter = string.Format("(|(&(objectClass=group)(|(samaccountname=*{0}*)(displayname=*{0}*)))(&(objectCategory=person)(objectClass=user)(|(samaccountname=*{0}*)(displayname=*{0}*))))", name);
//Your Modified filter
ds.filter = "(objectCategory=User)"
ds.PropertiesToLoad.Add("displayname");
ds.SizeLimit = 20;
SearchResultCollection result = ds.FindAll();
List<string> names = new List<string>();
foreach (SearchResult r in result)
{
var n = r.Properties["displayname"][0].ToString();
if (!names.Contains(n))
names.Add(n);
}
return Json(names, JsonRequestBehavior.AllowGet);
}
If it's not valid solution in managed code and your code depend hard on not-managed things such Active Directory i think it's good idea to move logic of directory searching to isolate such logic from your main application domain. It's way to not run against wind.
For me - better is to use outer console process, send parameters to argument string and catch StandardOutput.
It will be slower, but memory will be not leaked because it will be freed by system after console process die.
Additionally in this case u can not to make self-made ad_searcher.ex, but use standard dsquery and so on. For example:
[https://serverfault.com/questions/49405/command-line-to-list-users-in-a-windows-active-directory-group]
Another approach - usage of managed sub-domains, BUT i think it's harder and i'm not sure that all unmanaged resources will be freed.
The 'Users' on the network were moved to another domain recently. This caused a local method that gets the user's email address to break for a few of our web applications. This problem would likely fix itself after the web server is moved to the same domain, but would request help in fixing the code to allow this method to work in this split domain network (ex. Domain1.ad.com, Domain2.ad.com).
// Method below gets users email address.
public static string GetUserEmailAddress()
{
// Grab users network name. It will be in the form domain\username.
System.Security.Principal.WindowsPrincipal p = System.Threading.Thread.CurrentPrincipal as System.Security.Principal.WindowsPrincipal;
string NetworkID = p.Identity.Name;
// Remove the domain.
string[] networkIDparts = NetworkID.Split('\\');
string UserID = networkIDparts[1];
DirectorySearcher objsearch = new DirectorySearcher();
objsearch.Filter = "(& (cn="+ UserID.Trim() + ")(objectClass=user))";
objsearch.SearchScope = System.DirectoryServices.SearchScope.Subtree;
objsearch.PropertiesToLoad.Add("mail");
objsearch.PropertyNamesOnly = true ;
objsearch.Sort.Direction = System.DirectoryServices.SortDirection.Ascending;
SearchResultCollection colresults = objsearch.FindAll() ;
ArrayList arrGal = new ArrayList();
foreach(SearchResult objresult in colresults)
{
arrGal.Add(objresult.GetDirectoryEntry().Properties["mail"].Value);
}
objsearch.Dispose();
String[] myArray = (String[]) arrGal.ToArray(typeof(string));
if (myArray.Length == 0)
{ return "NoAddressAssigned#domain2.ad.com";}
else
{ return myArray[0] ;}
}
I'm also open to any ideas that would improve the functionality of this method.