I'm trying to programaticaly change several user's password, specifically without using System.DirectoryServices.AccountManagement (PrincipalContext) I have this piece of working code:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.DirectoryServices;
namespace ADScriptService.core
{
class ExportTool
{
const AuthenticationTypes = AuthenticationTypes.Secure | AuthenticationTypes.Sealing | AuthenticationTypes.ServerBind;
private static DirectoryEntry directoryEntry = new DirectoryEntry(ADScriptService.Properties.Settings.Default.ActiveDirectoryPath, ADScriptService.Properties.Settings.Default.ServerAdminUser, ADScriptService.Properties.Settings.Default.ServerAdminPwd, AuthenticationTypes.Secure);
private static DirectorySearcher search = new DirectorySearcher(directoryEntry);
public void Export()
{
string path = ADScriptService.Properties.Settings.Default.ActiveDirectoryPath;
string adminUser = ADScriptService.Properties.Settings.Default.ServerAdminUser;
string adminPassword = ADScriptService.Properties.Settings.Default.ServerAdminPwd;
string userName = "exampleUser";
string newPassword = "P455w0rd";
try
{
search.Filter = String.Format("sAMAccountName={0}", userName);
search.SearchScope = SearchScope.Subtree;
search.CacheResults = false;
SearchResult searchResult = search.FindOne();
if (searchResult == null) Console.WriteLine("User Not Found In This Domain");
DirectoryEntry userEntry = searchResult.GetDirectoryEntry();
userEntry.Path = userEntry.Path.Replace(":389", "");
Console.WriteLine(String.Format("sAMAccountName={0}, User={1}, path={2}", userEntry.Properties["sAMAccountName"].Value, userEntry.Username, userEntry.Path));
userEntry.Invoke("SetPassword", new object[] { newPassword });
userEntry.Properties["userAccountControl"].Value = 0x0200 | 0x10000;
userEntry.CommitChanges();
Console.WriteLine("Se ha cambiado la contraseña");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
This is an example with a single user, but what my program should do is iterate through ~120k users.
However, the operation of setting the search filter, finding one result and getting the DirectoryEntry takes about 2 or 3 seconds per user so I'm trying to use the DirectoryEntries structure given by the DirectoryEntry.Children property, which means replacing the six lines after "try{" with simply DirectoryEntry userentry = directoryEntry.Children.Find("CN=" + userName);
So the example's code would look like this:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.DirectoryServices;
namespace ADScriptService.core
{
class ExportTool
{
const AuthenticationTypes = AuthenticationTypes.Secure | AuthenticationTypes.Sealing | AuthenticationTypes.ServerBind;
private static DirectoryEntry directoryEntry = new DirectoryEntry(ADScriptService.Properties.Settings.Default.ActiveDirectoryPath, ADScriptService.Properties.Settings.Default.ServerAdminUser, ADScriptService.Properties.Settings.Default.ServerAdminPwd, AuthenticationTypes.Secure);
private static DirectorySearcher search = new DirectorySearcher(directoryEntry);
public void Export()
{
string path = ADScriptService.Properties.Settings.Default.ActiveDirectoryPath;
string adminUser = ADScriptService.Properties.Settings.Default.ServerAdminUser;
string adminPassword = ADScriptService.Properties.Settings.Default.ServerAdminPwd;
string userName = "exampleUser";
string newPassword = "P455w0rd";
try
{
DirectoryEntry userEntry = directoryEntry.Children.Find("CN=" + userName);
userEntry.Path = userEntry.Path.Replace(":389", "");
Console.WriteLine(String.Format("sAMAccountName={0}, User={1}, path={2}", userEntry.Properties["sAMAccountName"].Value, userEntry.Username, userEntry.Path));
userEntry.Invoke("SetPassword", new object[] { newPassword });
userEntry.Properties["userAccountControl"].Value = 0x0200 | 0x10000;
userEntry.CommitChanges();
Console.WriteLine("Se ha cambiado la contraseña");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
But this code gets the following error in the invocation line (userEntry.Invoke("SetPassword", new object[] { newPassword });
:
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Runtime.InteropServices.COMException: The RPC server is unavailable. (Excepción de HRESULT: 0x800706BA)
which in english means RCP server unavailable. I've been stuck here for some days, and I've only found that it can be because of some authentication problem.
Invoking the method "Groups" works (userEntry.Invoke("Groups");
) and the administrator, which is the user I'm loging in with to the ActiveDirectory has all the privileges. Also the password policy is completly permissive with no minimum lenght or complexity.
Again, because the program must iterate through, the real program actually iterates through the DirectoryEntry's children with:
foreach(DirectoryEntry child in directoryEntry.Children)
{
child.Invoke("SetPassword", new object[] { newPassword });
child.CommitChanges();
}
Thank you very much!
I don't think using directoryEntry.Children.Find("CN=" + userName)
will give you much performance improvement. The sAMAccountName
attribute is an indexed attribute, so the search is very fast. That's one of the fastest searches you can make.
But note that your two code blocks are not equal. Find("CN=" + userName)
is trying to match userName
to the name of the account: the cn
attribute. But your code block with the DirectorySearcher
is matching userName
to the sAMAccountName
attribute. The cn
and sAMAccountName
attributes are not necessarily the same (although they might be in your domain).
But, if you still want to use Children.Find()
, I suspect that the problem might be in your Path
of the DirectoryEntry
. Why are you doing this?
userEntry.Path = userEntry.Path.Replace(":389", "");
Does your ADScriptService.Properties.Settings.Default.ActiveDirectoryPath
have :389
? It doesn't need to if it starts with LDAP://
(the default LDAP port is 389).
Your userEntry.Path
should look something like (depending on your domain) LDAP://CN=user,OU=Users,DC=domain,DC=com
. If it's not, then you need to fix that.
A side note: There is something you can do to speed this up a lot more than changing the search. The Properties
collection uses a cache. When you access a property, it checks if it is already in the cache and, if so, uses the cache. But if the property is not in the cache, then it will ask Active Directory for every attribute that has a value. That is expensive and unnecessary if you only want to read one or two attributes (especially if you're doing it for thousands of accounts).
A way around this is to tell it to get only the attributes that you want using RefreshCache
, before you access any of the Properties
. Like this:
userEntry.RefreshCache(new [] { "sAMAccountName", "userAccountControl" });
Then when you access those properties, it will already have them in the cache and not reach out to AD to get anything.
Also, if you are running this in a big loop over thousands of accounts, then I suggest you put the DirectoryEntry
in a using
statement (or call userEntry.Dispose()
when you're done with it). Usually you don't need to since garbage collection is pretty good at cleaning them up. But because you're running a big loop, the garbage collector doesn't have a chance to do any cleaning, so your process can end up taking up more and more and more memory until the loop finally stops. I have had big jobs like this take several GB of memory until I started disposing the unused DirectoryEntry
objects.
Update: Actually, forget about what I said about the DirectoryEntry
above. You don't actually need to use the DirectoryEntry
at all. Don't use searchResult.GetDirectoryEntry()
. The problem here is that you've already made a search to find the account. But now you're creating a DirectoryEntry
, which is just going to make another call out to AD to get information you already have. That's probably where your main performance hit is.
Instead, use the attributes returned from your search. Now, just like DirectoryEntry.Properties
, if you don't specify which attributes you want returned in the search, then it will return all of them, which you don't necessarily need. So you should set the PropertiesToLoad
collection, like this:
search.PropertiesToLoad.AddRange(new [] { "sAMAccountName", "userAccountControl" });
Then, after the search, you can use this to get the sAMAccountName
of the account you found:
searchResult.Properties["sAMAccountName"][0]
User contributions licensed under CC BY-SA 3.0