Как я могу изменить несколько паролей пользователей Active Directory? - PullRequest
0 голосов
/ 06 ноября 2018

Я пытаюсь программно изменить пароль нескольких пользователей, в частности без использования System.DirectoryServices.AccountManagement (PrincipalContext) У меня есть этот кусок рабочего кода:

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

Это пример с одним пользователем, но моя программа должна выполнять итерацию через ~ 120 тыс. Пользователей. Однако операция установки фильтра поиска, поиска одного результата и получения DirectoryEntry занимает около 2 или 3 секунд для каждого пользователя, поэтому я пытаюсь использовать структуру DirectoryEntries, заданную свойством DirectoryEntry.Children, что означает замену шести строк после "попробуй {" просто DirectoryEntry userentry = directoryEntry.Children.Find("CN=" + userName);

Таким образом, код примера будет выглядеть так:

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

Но этот код получает следующую ошибку в строке вызова (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)

, что на английском языке означает, что сервер RCP недоступен. Я застрял здесь на несколько дней, и я обнаружил, что это может быть из-за проблем с аутентификацией. Вызов метода «Группы» работает (userEntry.Invoke("Groups");), и администратор, который является пользователем, с которым я вхожу в ActiveDirectory, имеет все привилегии. Кроме того, политика паролей является полностью разрешающей, без минимальной длины или сложности.

Опять же, поскольку программа должна выполнять итерацию, реальная программа фактически выполняет итерацию дочерних элементов DirectoryEntry с помощью:

foreach(DirectoryEntry child in directoryEntry.Children) 
{
    child.Invoke("SetPassword", new object[] { newPassword });
    child.CommitChanges();
}

Большое спасибо!

1 Ответ

0 голосов
/ 06 ноября 2018

Я не думаю, что использование directoryEntry.Children.Find("CN=" + userName) значительно улучшит производительность. Атрибут sAMAccountName является индексированным атрибутом, поэтому поиск выполняется очень быстро. Это один из самых быстрых поисков, которые вы можете совершить.

Но учтите, что ваши два кодовых блока не равны. Find("CN=" + userName) пытается сопоставить userName с именем учетной записи: атрибут cn. Но ваш кодовый блок с DirectorySearcher соответствует userName атрибуту sAMAccountName. Атрибуты cn и sAMAccountName не обязательно совпадают (хотя они могут находиться в вашем домене).

Но, если вы все еще хотите использовать Children.Find(), я подозреваю, что проблема может быть в вашем Path из DirectoryEntry. Зачем ты это делаешь?

userEntry.Path = userEntry.Path.Replace(":389", "");

Ваш ADScriptService.Properties.Settings.Default.ActiveDirectoryPath имеет :389? В этом нет необходимости, если он начинается с LDAP:// (порт LDAP по умолчанию - 389).

Ваш userEntry.Path должен выглядеть примерно так (в зависимости от вашего домена) LDAP://CN=user,OU=Users,DC=domain,DC=com. Если это не так, то вам нужно это исправить.

Примечание: есть кое-что, что вы можете сделать, чтобы ускорить это намного больше, чем изменить поиск. Коллекция Properties использует кэш. Когда вы получаете доступ к свойству, оно проверяет, находится ли оно уже в кеше, и, если так, использует кеш. Но если свойство не находится в кэше, оно будет запрашивать у Active Directory каждый атрибут, имеющий значение . Это дорого и не нужно, если вы хотите прочитать только один или два атрибута (особенно если вы делаете это для тысяч учетных записей).

Чтобы обойти это, нужно указать только те атрибуты, которые вы хотите использовать, используя RefreshCache, прежде чем вы получите доступ к любому из Properties. Как это:

userEntry.RefreshCache(new [] { "sAMAccountName", "userAccountControl" });

Затем, когда вы получите доступ к этим свойствам, они уже будут храниться в кэше и не смогут связаться с AD, чтобы получить что-либо.

Кроме того, если вы выполняете это в большом цикле на тысячах учетных записей, тогда я предлагаю вам вставить DirectoryEntry в оператор using (или вызвать userEntry.Dispose(), когда вы закончите с ним). Обычно в этом нет необходимости, поскольку сборка мусора довольно хороша для их очистки. Но поскольку вы выполняете большой цикл, сборщик мусора не имеет возможности выполнить какую-либо очистку, поэтому ваш процесс может в конечном итоге занимать все больше и больше памяти до тех пор, пока цикл, наконец, не остановится. У меня были такие большие работы, которые занимали несколько ГБ памяти, пока я не начал избавляться от неиспользуемых DirectoryEntry объектов.

Обновление: На самом деле, забудьте о том, что я сказал о DirectoryEntry выше. Вам вообще не нужно использовать DirectoryEntry. Не используйте searchResult.GetDirectoryEntry(). Проблема в том, что вы уже выполнили поиск, чтобы найти аккаунт. Но теперь вы создаете DirectoryEntry, который просто сделает еще один вызов в AD, чтобы получить информацию, которая у вас уже есть. Это, вероятно, где ваш основной успех производительности.

Вместо этого используйте атрибуты, полученные в результате поиска. Теперь, как и DirectoryEntry.Properties, если вы не укажете, какие атрибуты вы хотите вернуть в поиске, он вернет все из них, которые вам не обязательно нужны. Таким образом, вы должны установить коллекцию PropertiesToLoad следующим образом:

search.PropertiesToLoad.AddRange(new [] { "sAMAccountName", "userAccountControl" });

Затем, после поиска, вы можете использовать это, чтобы получить sAMAccountName аккаунта, который вы нашли:

searchResult.Properties["sAMAccountName"][0]
...