Система нотификации изменения пароля [Windows]

в 20:44, , рубрики: .net, .NET service, C#, mssql, Администрирование доменных имен, ит-инфраструктура

Однажды для нашего корпоративной информационной системы понадобилось оперативно менять сохраненыe пароли пользователей, чьи имена были импортированы из LDAP.

Система нотификации изменения пароля [Windows] - 1

Традиционно корпоративные клиенты для изменения пароля пользутся готовыми системами типа: Microsoft Forefront Identity Manager (FIM), Oracle Identity Manager, IBM Security Identity Manager и прочие другие.

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

Тем более что Microsoft AD сервис позволяет иметь раcширения. И все бы хорошо, пока некоторые кастомеры не сказали — все хорошо что вы большая и известная международная корпорация и мы десятки лет пользуемся вашим оборудованием и программным обеспечением, но ставить на наш сервер с AD контроллером ваш криворукий специальный софт не будем, — только хардкор решения от Microsoft.

Основными такими возможностями обладает Password Change Notification Service (PCNS) Configuration Utility. Эта утилита входит в состав FIM. Начав изучать как мне добится желаемой цели, я перечитал все возможные статьи от Microsoft и с перепугу недопонимания, как это работает поставил сам FIM, имеюший порядка десятка зависимостей от различных продуктов, включая SharePoint, который еше сам имеет пару десятков зависимостей, в числе которых MSSQL server, голосовой поиск и массу прочего ненужного и тяжеловесного софта, ради то чтобы понять, что в итоге FIM — это две примитивных HTML странички, каждая из которых имеет пару примитивных контролов для сброса пароля.

И чтобы поставить все это требуется некислый сервер (Microsoft рекомендует 3-4 сервера для этого) и примерно рабочий день. Угробив кучу времени и опять перечитав половину интернета, я понял что в представлении Microsoft есть как минимум 3! пути движения паролей:
— IM то DC (xxx identification manager to Domain Controller);
— Mail system to DC (Exchange/GroupWise/Domino to Domain Controller);
— DC to Mail system

То что я хотел в этот список не входило, поскольку ни IM ни почтовые системы мне не хотелoсь трогать, в виду чрезвычайного разноообразия оных. Все это движение паролей проходило через FIM Synchronization Service и на аппаратном уровне для всех моих безобразий хватало одного сервера минимальной конфигурации.

Формально FIMSS представляет из себя продвинутую среду для автоматизированной синхронизации данных чего угодно с чем угодно (в разумных пределах конечно) и имеет ряд готовых агентов для DC и всех известных почтовых агентов, и конечно ряда пользовательских расширений.

Microsoft предлагает несколько весьма запутанных step by step руководств, но осилить вот так блондинке с улицы будет весьма непросто, и более того, — это все не то что нужно для моего простого, как я думал случая. Вдоволь намучавшись с примерами Еxtension Projects и неполучив нужного мне результата, вновь обратил свой взор в Google в поисках нужного решния — и о чудо — нашелся один таки один! (скорее всего я опять плохо искал) пример где парнишка на бэсийке что-то сделал и оно работает (с его слов)! Самое главное скормить гуглю правильное кодовое слово: IMAPasswordManagement.

В обшем, получив волшебный пендель в заданном направлении я быстро наваял похожую конфигурацию, почeму похожую, потому что повторив это, — оно не заработало, пока я вдоволь не намучался, и, методом научно тыка не понял, как работают основные принципы передачи пароля.

Что бы пароль приходил в мой код написанный на C#, а там я могу резвится делать с ним что хочу — надо:

1. Инсталлировать FIM Synchronization Service и в нем установить возможность синхронизации паролей:
Система нотификации изменения пароля [Windows] - 2

MVExtension.DLL как и пользовательские агенты синхронизации генерируются (тексты на C# или на VB.NET) прямо из FIMSS.

RPC сервис должен быть разрешен (используется для связи с Password Change Notification Service (PCNS) Configuration Utility).

2.Завести в FIMSS 2 агента — оба уже готовые:

Система нотификации изменения пароля [Windows] - 3

Все агенты имеют 3 основных свойства — импортировать, экспортироват и синхронизировать. В нашем случае достаточно 2: импорт и синхронизация.
Почему — скажу позже.

1. DCAgent: импортирует список пользователей из DC
2. RNAgent: импортирует список пользователей из нашего самописного корпоративного сервиса (где уменьшенный список тех же пользователей, то есть по именам они совпадают — мой агент получает только имена, полученные из LDAP). Этот агент создается на базе SQL агента (то есть данные (список пользователей) получает из MSSQL)

Тут небольшое вступление — пароль будет передатся только если имена из обоих агентов совпадут по имени.
Чтобы это произошло у агентов FIMSS есть профайлы где настраиваются, a потом запускаются профайлы с нужными командами — сначала импорт, потом синхронизация.

Внутри себя FIMSS держит все в MSSQL базе и если вам не нужно заботится o ограниченном списке из вашего софта, то можно порекомендовать (думаю Microsoft будет отговаривать от такого хака) читать список пользователей прямо из внyтренних таблиц FIMSS, где список был создан агентом DCAgent: select accountName from [FIMSynchronizationService].[dbo].[mms_metaverse]:

Система нотификации изменения пароля [Windows] - 4

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

Настройка агентов тоже нетривиальное дело и наверное потребует отдельной статьи, но ничего такого чего нет в статьях от Microsoft. Укажу только несколько моментов, которые критически важны:

1. Чтобы RNAgent мог дергать наш Password Extension (текст на C# тоже генерится из FIMSS) нужно его указать:

Система нотификации изменения пароля [Windows] - 5

Этот в метод позволяет получить имя аккаунта и пароль в открытом виде и вызвать метод из нашей DLL на С++ которая шифрует результат и передает REST сервису нашего корпоративного сервера. Бинго.

        public void SetPassword( CSEntry csentry,  string  NewPassword )
        {
            Log(String.Format("SetPassword entered: [{0}] is : [{1}]", csentry.DN.ToString(), NewPassword));

            UNICODE_STRING User = InitLsaString(csentry.RDN);
            UNICODE_STRING Pwd = InitLsaString(NewPassword);

            SendPasswordToMyServer(User, Pwd);
        }

2. DCAgent: Указать кому передаем пароль
Система нотификации изменения пароля [Windows] - 6

После того как агенты созданы нужно что бы их кто-то пинал, чтобы импорт и синхронизация происходили регулярно. Мы для себя установили что раз в сутки вполне достаточно, но можно и сделать раз 5 минут. Основная проблема, что в большой компании с десятками тысяч сотрудников импорт из DC может идти очень долгое время и поэтому для регулярного импорта лучше использовать не Full Import а Delta Import. Microsoft предлагает следуюшее решение — дергать профайлы с помошью TASK SCHEDULER.
Если сходите по ссылке то сможете увидеть что в диалоге создания профайла можно сгенерить C#/VB который и запускать по расписанию.

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

То есть на той же машине иметь C# Windows Service, который будет по таймеру читать xml, раскодировать его и вставлять в нашу базу-табличку, из которой RNAgent уже и будет читать.

Основной код сервиса

        protected bool Process()
        {
            IntPtr ptrUserList = new IntPtr();
            bool result = false;
            try 
            {
                ptrUserList = GetUserList();

                string  userList = StringFromNativeUtf8(ptrUserList);

                 int cnt_imported = -1;

                 result = SaveAccounts(userList, ref cnt_imported);          // LDAP users will appear in [RNService].[dbo].[CoreAccount]
                 result = result ? DCAgent(m_dcagent_guid) : false;          // sync users from DC
                 result = result ? RNAgent(m_rnagent_guid) : false;          // sync users from [RNService].[dbo].[CoreAccount]
                  eventLogRN.WriteEntry("Imported users: " + cnt_imported );
            }
            catch (Exception ex)
            {
                eventLogRN.WriteEntry("Process: " + ex.Message, EventLogEntryType.Error);
                return false;
            }
            DisposeUserList(ptrUserList);
            
            m_timer.Interval = m_servicePollInterval;  // use interval from registry

            return result;
        }
        public bool SaveAccounts(string userList, ref int cnt_imported)
        {
            string con_str = @"Data Source=" + m_serverName + ";Initial Catalog=RNService;Integrated Security=True";
            cnt_imported = -1;

            using (SqlConnection cnn = new SqlConnection(con_str))
            {
                cnn.Open();

                    using (SqlCommand cmd = new SqlCommand())
                    {
                        cmd.Connection = cnn;
                        cmd.CommandType = CommandType.Text;
                        cmd.CommandText = "TRUNCATE TABLE  CoreAccount;"
                                        + " INSERT INTO CoreAccount SELECT X.C.value(N'@name', N'nvarchar(448)') FROM "
                                        + " (SELECT @data AS XML_DATA) DATA CROSS APPLY DATA.XML_DATA.nodes(N'/response/users/user') as X(C); "
                                        + " SELECT @@ROWCOUNT AS usercnt; ";

                        cmd.Parameters.Add("@data", SqlDbType.Xml);

                        try
                        {
                                cmd.Parameters[0].Value = userList;

                                SqlDataReader SqlDataReader = cmd.ExecuteReader();

                                if (!SqlDataReader.IsClosed && SqlDataReader.HasRows)
                                {
                                    if( SqlDataReader.Read() )
                                    {
                                        cnt_imported = SqlDataReader.GetInt32(0);
                                    }
                                }
                        }
                        catch (Exception ex)
                        {
                            eventLogRN.WriteEntry("UpdateCoreAccounts: " + ex.Message, EventLogEntryType.Error);
                            cnn.Close();
                            return false;
                        }
                }
                cnn.Close();
                return true;
            }
        }

В этом же сервисе и дергать профайлы агентов которые мы сгенерили, чем избавим наших клиентов мучений с TASK SCHEDULER.
В моем случае они просто вынимают гуиды профайлов из скрипта и кладут в регистри.

        public bool Agent(string agentName, string guid)
        {
            if (guid == null_guid)
            {
                return true;            // required manual syncronization
            }
            try
            {
                ConnectionOptions opt = new ConnectionOptions();
                opt.Authentication = AuthenticationLevel.PacketPrivacy;
                ManagementScope myScope = new ManagementScope("root\MicrosoftIdentityIntegrationServer", opt);
                string sQuery = "GUID='{" + guid + "}'";
                SelectQuery myQuery = new SelectQuery("MIIS_ManagementAgent", sQuery);
                ManagementObjectSearcher searcher = new ManagementObjectSearcher(myScope, myQuery);
                foreach (ManagementObject ma in searcher.Get())
                {
                    eventLogRN.WriteEntry(agentName + ".Execute( "ImportSync" )...");
                    ma.InvokeMethod("Execute", new object[1] { "ImportSync" });
                }
            }
            catch (Exception ex)
            {
                eventLogRN.WriteEntry( ex.Message, EventLogEntryType.Error);
            }
            return true;
        }

Автор: BalinTomsk

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js