Нередко перед разработчиками 1С встает задача синхронизации пользователей базы данных с корпоративным каталогом Active Directory (AD): автоматическое отключение учетных записей уволенных сотрудников, блокировка доступа по окончании срока действия учетной записи и контроль прав. В процессе реализации этого механизма на стороне сервера разработчики часто сталкиваются сразу с двумя неочевидными проблемами: полной неработоспособностью инструкций препроцессора внутри динамически выполняемого кода (Выполнить) и системной ошибкой Windows 0x80070005 (Отказано в доступе) при обращении к объектам COM.
Разберем детально, почему возникают эти ошибки, как платформа 1С обрабатывает динамические контексты и какие существуют надежные решения для чтения данных Active Directory.
Проанализируем типовую ситуацию: разработчик пытается выяснить контекст выполнения кода, помещая в динамический фрагмент инструкции препроцессора вида #Если Сервер Тогда ... #Иначе ... #КонецЕсли, однако управление неизменно передается в ветку #Иначе.
Посмотрим на пример такого кода:
#Если Сервер Тогда
Сообщить("Выполняется на сервере");
#ИначеЕсли Клиент Тогда
Сообщить("Выполняется на клиенте");
#ИначеЕсли ВнешнееСоединение Тогда
Сообщить("Выполняется в COM соединении");
#Иначе
Сообщить("???");
#КонецЕсли
Выясним причину такого поведения. Инструкции препроцессора платформы 1С:Предприятие отрабатывают строго на этапе синтаксического анализа и компиляции исходного модуля при старте сеанса или загрузке конфигурации. В этот момент препроцессор удаляет неактуальные блоки кода и оставляет только тот фрагмент, который предназначен для текущего типа приложения.
Когда мы передаем строку в процедуру Выполнить(), код компилируется «на лету» во время исполнения (в runtime). В динамическом компиляторе инструкции препроцессора не обрабатываются — условия просто игнорируются, а выполнение проваливается в секцию #Иначе. Сам же динамический код отрабатывает строго в том контексте, в котором была вызвана процедура Выполнить(): если вызов произошел на сервере, то и все операции выполняются на сервере.
Для гарантированного разделения серверного и клиентского контекстов без препроцессора следует вызывать процедуры и функции из общих модулей с жестко установленными свойствами компиляции (флаги «Сервер» или «Клиент»).
Следующая проблема возникает при попытке получить доступ к контроллеру домена через COM-объект на стороне сервера 1С:
мОб = "WinNT://" + Домен + "/" + Логин + ",user";
objNameSpace = ПолучитьCOMОбъект(мОб);
Платформа перехватывает системное исключение: Ошибка получения объекта COM: -2147024891(0x80070005): Отказано в доступе. Рассмотрим ключевые факторы, вызывающие эту неполадку:
ragent, rphost) по умолчанию устанавливаются под специальным локальным пользователем операционной системы (например, usr1cv8). Локальный пользователь сервера не имеет прав сетевой аутентификации в домене Active Directory через RPC/NetBIOS-провайдер WinNT, поэтому попытка чтения каталога блокируется на уровне операционной системы.БезопасныйРежим = Истина), платформа 1С блокирует создание и получение любых внешних COM-объектов.rphost.Разберем основные варианты устранения ошибки прав доступа.
Вариант 1. Запуск службы сервера 1С от доменной учетной записи
Наиболее простое административное решение — перевести службу 1С:Предприятия на запуск от имени доменного пользователя. Для этого системный администратор выделяет доменную служебную учетную запись (Service Account), наделяет ее правами «Вход в качестве службы» (Log on as a service) на сервере 1С, а также локальными правами на каталоги данных 1С (файлы конфигурации, журналы регистрации, временные файлы). В этом случае все серверные вызовы к Active Directory через WinNT:// будут прозрачно использовать контекст безопасности этой доменной учетной записи.
Вариант 2. Явная доменная аутентификация через интерфейс OpenDSObject
Если изменить учетную запись службы кластера невозможно, мы можем передать учетные данные доменного пользователя напрямую из кода 1С, используя COM-интерфейс ADsNameSpaces и метод OpenDSObject:
DSO = Новый COMОбъект("ADsNameSpaces");
// Значение 1 соответствует флагу ADS_SECURE_AUTHENTICATION
ОбъектAD = DSO.OpenDSObject("LDAP://DC=corp,DC=local", "corp\UserReader", "SecretPassword", 1);
Такой подход позволяет серверной службе с локальными правами авторизоваться в домене с правами выделенного технического пользователя, имеющего права только на чтение учетных записей.
Проанализируем практический сценарий: нам требуется получить список учетных записей домена, разделив их на активные и заблокированные. При работе через провайдер WinNT часто возникает ошибка при обращении к свойству AccountExpirationDate: если у пользователя в Active Directory установлена опция «Срок действия учетной записи не ограничен» (бессрочно), COM-интерфейс выбрасывает исключение 0x8000500D (свойство отсутствует в кэше каталога).
Посмотрим на рабочий пример кода, где эта особенность корректно обойдена с помощью конструкции Попытка ... Исключение, а также используется фильтрация по типу объектов через COMSafeArray:
МассивАктивныхУчеток = Новый Массив;
МассивЗаблокированыхУчеток = Новый Массив;
Попытка
мТекущаяДата = ТекущаяДата();
ИмяДомена = "MYDOMAIN";
мОб = "WinNT://" + ИмяДомена;
objNameSpace = ПолучитьCOMОбъект(мОб);
// Накладываем фильтр, чтобы выбирать только объекты типа User
м = Новый Массив();
м.Добавить("User");
мФильтр = Новый COMSafeArray(м, "VT_VARIANT");
objNameSpace.Filter = мФильтр;
Для Каждого item Из objNameSpace Цикл
ЕстьЗапретПоДате = Ложь;
// Обработка свойства с ограничением срока действия
Попытка
ЕстьЗапретПоДате = (item.AccountExpirationDate < мТекущаяДата);
Исключение
// Если дата истечения не задана (бессрочная учетка), возникает исключение
ЕстьЗапретПоДате = Ложь;
КонецПопытки;
мУчетка = ВРег(ИмяДомена + "\" + item.Name);
Если ЕстьЗапретПоДате Тогда
МассивЗаблокированыхУчеток.Добавить(мУчетка);
ИначеЕсли item.AccountDisabled = Ложь Тогда
МассивАктивныхУчеток.Добавить(мУчетка);
Иначе
МассивЗаблокированыхУчеток.Добавить(мУчетка);
КонецЕсли;
КонецЦикла;
objNameSpace = Неопределено;
Сообщить("Активных учетных записей: " + МассивАктивныхУчеток.Количество());
Сообщить("Заблокированных учетных записей: " + МассивЗаблокированыхУчеток.Количество());
Исключение
Сообщить("Ошибка при работе с Active Directory: " + ОписаниеОшибки());
КонецПопытки;
Использование интерфейса WinNT:// при большом количестве учетных записей работает медленно, так как перебор объектов выполняется последовательно. Разберем более быстрый способ прямого запроса к Active Directory через ADO и протокол LDAP://. В этом случае мы получаем битовую маску флагов учетной записи (userAccountControl), где бит со значением 2 указывает на отключенную учетную запись:
Соединение = Новый COMОбъект("ADODB.Connection");
Соединение.Provider = "ADsDSOObject";
Соединение.Open("Active Directory Provider");
Команда = Новый COMОбъект("ADODB.Command");
Команда.ActiveConnection = Соединение;
// LDAP-запрос: выбираем всех заблокированных пользователей каталога
ТекстФильтра = "(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=2))";
Команда.CommandText = "<LDAP://DC=mydomain,DC=local>;" + ТекстФильтра + ";sAMAccountName,userAccountControl;subtree";
Выборка = Команда.Execute();
Пока НЕ Выборка.EOF Цикл
Логин = Выборка.Fields("sAMAccountName").Value;
Сообщить("Заблокирован в AD: " + Логин);
Выборка.MoveNext();
КонецЦикла;
Соединение.Close();
Запрос через ADODB выполняется на порядок быстрее, не требует последовательного перебора всего каталога и позволяет точно отбирать целевые учетные записи за один сетевой запрос к контроллеру домена.