Привет, Хабр! В прошлых статьях я упоминал, что Яндекс 360 для бизнеса позволяет использовать IdP заказчика и синхронизировать учётные записи пользователей из LDAP. Заказчики могут настроить для своих сотрудников SSO со своим IdP на основе ADFS или Keycloak. Дополнительно можно настроить синхронизацию пользователей, например, со своей Active Directory по протоколу SCIM, используя утилиту Yandex AD SCIM.
В этой статье я приоткрою капот и расскажу детальнее, как работает SSO и синхронизация. Вы узнаете, как связываются объекты типа «пользователь» из LDAP (Active Directory) с учётными записями в директории Яндекс 360. А ещё я разберу сценарий, когда в качестве уникального идентификатора пользователя в Active Directory выбирается атрибут ObjectGUID и модифицируется преобразование типов данных при формировании утверждений SAML (claims), а именно Base64 — Claim. В этом мне помогут изыскания моего коллеги, архитектора Андрея Лаврецкого, и библиотеки для ADFS.
Перед прочтением статьи рекомендую базово разобраться, как работает SSO в Яндекс 360 и синхронизация с Active Directory. Для этого достаточно ознакомиться со статьёй «Синхронизация учётных записей пользователей в Организацию Яндекс 360 для бизнеса» или документацией. Этот материал поможет сделать оптимальный выбор, какой уникальный идентификатор использовать в вашей организации.
Уникальный идентификатор пользователя
Когда настраивается синхронизация, учётные записи пользователей в директории Яндекс 360 для бизнеса должны быть сцеплены с учётными записями соответствующих пользователей в Active Directory (AD). Цель этой связи — чтобы последующие изменения бизнес-свойств учётных записей AD, таких как «Фамилия», «Имя», «Должность», «Контакты», успешно синхронизировались в учётные записи тех же пользователей в директорию Яндекс 360 для бизнеса.
Связь устанавливается с помощью уникального идентификатора пользователя. Это заранее заданный атрибут пользователя, например UserPrincipalName, ObjectGUID, EmplyeeID или любой другой. Важно, чтобы в LDAP значение этого атрибута было уникальным для каждого пользователя и не повторялось.
Уникальный идентификатор пользователя определяется в двух местах:
При настройке утилиты синхронизации Yandex AD SCIM в ключе PropertyLoginName.
При настройке SSO, когда вы задаёте, чему будет равен NameID.
Очевидно, эти значения в обоих компонентах должны быть одинаковыми.
Как задать идентификатор в утилите синхронизации Yandex AD SCIM
В файле конфигурации найдите строчку #PropertyLoginName, раскомментируйте её и укажите, какому атрибуту в AD равен этот ключ. Если ключ не задан в файле, значит, по умолчанию он будет равен UserPrincipalName. Следовательно, ему же будет равен идентификатор.
Как Yandex AD SCIM использует идентификатор
Каждый цикл синхронизации утилита Yandex AD SCIM:
Получает список пользователей в Active Directory.
Смотрит на значение идентификатора, например UPN пользователя в AD вида user@example.com.
Ищет учётную запись в директории Яндекс 360, у которой propertloginname равно user@example.com.
Синхронизирует любые изменения в других атрибутах этой учётной записи в директории.
Как заменить значение в Yandex AD SCIM
Допустим, вы определили уникальный идентификатор и синхронизировали пользователей. Если захотите переопределить параметр, например вместо UPN решите выбрать EmployeeID, то придётся подставить в атрибут EmployeeID то же значение, что в атрибуте UPN у всех пользователей. После этого необходимо заново запустить цикл синхронизации. Но не забудьте изменить и настройки SSO, чтобы NameID брал параметры из нового атрибута.
Что случится, если администратор изменит атрибут пользователя в AD, который сконфигурирован как уникальный идентификатор пользователя? Например, propertyloginname был задан UPN, который у пользователя равен user@example.com, а стал manager@example.com. В таком случае произойдёт следующее:
Утилита синхронизации найдёт пользователя user@example.com в директории Яндекс 360, но не найдёт никого с таким UPN в AD. Пользователь будет заблокирован в директории Яндекс 360.
Утилита синхронизации найдёт в AD пользователя с UPN manager@example.com, но не найдёт в директории Яндекс 360 учётной записи со значением propertyloginname, равным manager@example.com. Она попытается создать новую учётную запись в директории Яндекс 360.
Дальше могут быть варианты. Всё зависит от того, будут ли повторяться значения других параметров, такие как email, alias, с уже существующими в директории Яндекс 360.
Если да, то будет сообщение об ошибке, что пользователь с аналогичным email уже есть.
Если нет, то будет успешно создана новая учётная запись. Так произойдёт, если поля типа email у пользователя с UPN = manager@example.com не будут пересекаться с другими.
Вот так будет выглядеть сообщение об ошибке в логах службы синхронизации Yandex AD SCIM.
После строчки с попыткой добавить нового пользователя получите и распишитесь в (409) Conflict.
240712-090425.122 [I] CORE Add user: manager@example.com (Active=True, DisplayName=Василий Пупкин, Name.GivenName=Василий, Name.FamilyName=Пупкин, Emails=[user@example.com (work)], Aliases=[])
240712-090425.232 [E] SCIM Add Users: Exception: The remote server returned an error: (409) Conflict.
Как задаётся уникальный идентификатор пользователя при настройке SSO
Разберём на примере конфигурации SSO для ADFS. Это делается в мастере настройки на шаге «Сопоставления утверждений». Когда мы задаём UPN как идентификатор, то в разделе «Тип входящего утверждения» должны явно выбрать UPN, а в «Типе исходящего утверждения» указать Name ID.
Как это будет выглядеть — смотрите на скрине ниже.
Подробная документация доступна по ссылке.
Мы видим:
Name ID в настройках SSO.
Propertyloginname в настройках Yandex AD SCIM.
Уникальный идентификатор пользователя.
Всё это один и тот же атрибут.
Какой атрибут в AD использовать для уникального идентификатора пользователя
Исходить надо из постоянства атрибута или вероятности/частотности смены этого атрибута.
Если выбрать UserPrincipalName, то мы получаем:
Наглядно читаемый атрибут. Его значение нередко красноречиво говорит, кто это.
Простую настройку. Для Yandex AD SCIM это настройка по умолчанию, а в ADFS буквально Next → Next и в Production.
Адаптивность. Есть рекомендованные шаги, как продолжить обеспечивать SSO и синхронизацию, когда нужно поменять Ф. И. О. пользователя после похода в ЗАГС. Про это читайте в статье «Синхронизация учётных записей пользователей в Организацию Яндекс 360 для бизнеса» в разделе «Переименование пользователя в Active Directory».
Почти идеальный атрибут, но что, если...
У вас в организации 5 000 пользователей. Точнее, у вас группа компаний — пять штук по 1 000 сотрудников в каждой. И вот в одной компании планируется изменение домена и, соответственно, изменение UPN у всех пользователей. А может, в рамках сценариев слияния и поглощения (Merge & Acquisitions) есть планы, что появятся другие компании, где постепенно будут заменяться UPN у большого числа пользователей? Тогда настраивать связь между AD и Яндекс 360 на основе UPN не будет оптимальным решением. Гораздо лучше для разовых задач подходит способ с переименованием.
ObjectGUID как уникальный атрибут идентификации
Какой атрибут у пользователя никогда не меняется? Да, это ObjectGUID. Он действительно уникален и никогда не меняется, но по его значению сложно понять, о ком идёт речь. Возьмём за основу этот атрибут и узнаем, к чему нас это приведёт.
Типичный сценарий: у компании в ИТ-инфраструктуре всё ещё есть Active Directory и ADFS. В Яндекс 360 настраивают SSO и интегрируют ADFS в качестве IdP. Как это настроить, указано в документации.
Сотрудники, ответственные за Identity, покумекали и решили связать всё на ObjectGUID. Именно поэтому немного отошли от документации при настройке правил и пошли так:
Нажали «Добавить правило».
Выбрали в нём Send LDAP Attributes as Claims.
Назвали его красноречиво — ObjectGUID into Name ID.
Attribute Store указали Active Directory. А в маппинге в столбце LDAP атрибуты указали objectGUID (обязательно с маленькой буквы, так как в LDAP он именно так называется).
В Outgoing Claim Type напечатали Name ID, тоже case-sensitive.
Далее администратор настраивает синхронизацию с помощью ADSCIM. В файле конфигурации он указывает PropertyLoginName = ObjectGUID.
Создаём нового пользователя в AD, который ещё не синхронизирован. Например:
First Name: Вася.
LastName: Пупкин.
Email: a@example.com.
ObjectGUID: 8bd5c172-091a-495b-9f0f-911f53217f67.
Пытаемся зайти в Яндекс 360. Ожидаем, что пользователь успешно сможет зайти, даже если мы его ещё не синхронизировали. Но в лоб получаем ошибку вида:
И на этом всё. Пояснений нет, думайте-гадайте.
Если залезть в SAMLResponse, который браузер получил от ADFS и передаёт в Яндекс 360, то он окажется successful:
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
Если проверить поле NameID, то оно есть и заполнено:
<Subject>
<NameID>
csHVixoJW0mfD5EfUyF/Zw==
</NameID>
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<SubjectConfirmationData InResponseTo="track_23a6eeb75c7816adec16132d040a231c29" NotOnOrAfter="2024-07-12T10:53:39.492Z" Recipient="https://passport.yandex.ru/auth/sso/commit"/>
</SubjectConfirmation>
</Subject>
Давайте думать.
Во-первых, NameID должен содержать ObjectGUID, а содержит абракадабру csHVixoJW0mfD5EfUyF/Zw== далеко не в формате GUID. Что с этим сделать — скажу чуть позже.
Во-вторых, надо разобраться, почему авторизация не удалась. Пускай значение поля не соответствует GUID, но оно же выглядит по-людски. Так, да не так. На стороне Яндекс 360 идёт проверка на наличие символов в поле Name ID:
Там должно быть не более 80 символов.
Символы должны быть из диапазона a-zA-Z0-9@_-.
В описанном случае наличие символов «/» и «=» приводит к такой малоинформативной ошибке. Если бы их не было, то пользователь успешно создался бы в Яндекс 360 с таким уникальным идентификатором. Хотя и этот успех был бы временным: как только мы попытаемся синхронизировать такого пользователя с помощью Yandex AD SCIM, то получим заблокированного пользователя.
Мы выбрали атрибут, в котором отсутствует какое-либо упоминание о домене пользователя. Именно поэтому в конфигурации Yandex AD SCIM нужно явно указать «игнорировать наличие домена в уникальном идентификаторе пользователя». Для этого в файл конфигурации раскомментируйте строчку с ключом:
IgnoreUsernameDomain = true
С моей точки зрения, это избыточная проверка. Но надо учитывать её наличие и добавлять этот ключ в конфигурацию, если выбрали не UPN, а objectGUID.
Теперь расскажу, что же делать, если вы выбрали ObjectGUID, а ADFS подсовывает вместо него абракадабру.
Откуда берётся абракадабра? Сначала ADFS стучится в Active Directory, чтобы забрать ObjectGUID пользователя. Затем он применяет встроенное кодирование в Base64 к значению ObjectGUID. В SAMLResponse он передаёт именно результат кодировки вместо чистого значения ObjectGUID.
Как я написал выше, даже если бы учётка создалась, то потом всё поломалось бы при синхронизации. Всё потому, что когда Yandex AD SCIM запрашивает ObjectGUID, он получает его как есть и передаёт в Яндекс 360, а не трансформирует в Base64. Если бы в облаке была версия Base64, то учётные записи одного пользователя в директории Яндекс 360 и Active Directory не смогли бы связаться.
Ситуацию можно исправить, если научить ADFS конвертировать Base64-строку обратно в Plain Text. Так в NameID всё будет передаваться красиво и читабельно. Для этого нужно сбацать модуль с функцией преобразования значения атрибута.
Первоисточник расположен на GitHub моего коллеги — здесь я приведу описание из его инструкции. Исходный код взят из документации для ADFS.
Технически данный модуль кастомизации ADFS-сервера можно расширить для произвольной модификации значений утверждений (claims). Исходный код как раз и показывает, как можно манипулировать строками. Но в целях безопасности в предлагаемом модуле в дополнение к трансформации convertToObjectGUID реализованы только трансформации toUpper, toLower, trim. Также по сравнению с оригинальным кодом входящий параметр, который определяет тип запроса/трансформации, перед сравнением со значениями внутри кода приводится к нижнему регистру. Это нужно, чтобы исключить ошибки, которые могут возникнуть в результате вариаций букв нижнего и верхнего регистра при написании правил выпуска утверждений в ADFS-сервере.
Вот что содержит атрибут objectGUID в свойствах пользователя в Active Directory:
Пример SAMLResponse, когда значение атрибута ObjectGUID передаётся ADFS-сервером как есть:
Пример SAMLResponse, когда значение атрибута ObjectGUID передаётся ADFS-сервером с использованием этой библиотеки:
Вы можете самостоятельно скомпилировать библиотеку в Visual Studio или использовать скомпилированную DLL, расположенную в папке /dll для теста и проверки работоспособности решения.
Использование готовой DLL
DLL была скомпилирована для .Net Framework версии 4.8. Проверьте, что на вашем ADFS-сервере установлена данная версия, иначе работоспособность решения не гарантируется.
1. Скачайте скомпилированную DLL из папки /dll (download link) и поместите в корневой каталог сервера ADFS. Для ADFS версии 2016 и выше этот каталог находится по адресу c:\windows\ADFS\.
2. Откройте консоль ADFS-сервера и перейдите в раздел Services → Attribute Stores и в панели Actions нажмите на Add Custom Attribute Store:
3. Введите название хранилища, на которое вы будете ссылаться при написании правил. Например, OriginalObjectGIUD.
Во втором поле укажите информацию, откуда брать код для преобразования в формате Namespace.ClassName,DllName. Обратите внимание, что первым разделителем идёт точка, вторым — запятая. В случае данной скомпилированной dll строка будет такой:
StringProcessingNamespace.StringProcessingClass,StringProcessingAttributeStore
Warning! Иногда при добавлении или удалении Custom Attribute Store возникает ошибка. Для продолжения работы перейдите в корень консоли управления ADFS (самый верхний / первый элемент с именем AD FS) и выберите пункт меню Action → Refresh. После этого повторите попытку добавления или удаления.
Tip. Для проверки успешности добавления Custom Attribute Store обратитесь к системному журналу Event Viewer. Выберите Application and Services Logs → ADFS → Admin и найдите событие с Event ID = 251.
Ещё с помощью записей в этом логе можно диагностировать ошибки при написании своих собственных функций преобразования утверждений в ADFS-сервере через механизм подключаемых Custom Attribute Store.
4. Добавьте правило для передачи в SAMLResponse оригинального ObjectGUID.
Для выбранного приложения в разделе Ralying Party Trust (у меня используется passport.yandex.ru) в панели Actions активируйте Edit Claim Insurance Policy:
Чтобы выполнить трансформацию нужного утверждения (claim), его нужно добавить в конвейер управления утверждениями. Для этого необходимо добавить правила, каждое из которых отвечает за один этап работы с запрошенным утверждением. Правила обрабатываются по очереди сверху вниз.
Warning! Синтаксис правил ADFS чувствителен к регистру. Особенно это может проявляться при написании типов утверждений, в которых находятся данные для трансформаций. Например, строки с типом types = ("objectGUID") и types = ("ОbjectGUID") ссылаются на два разных утверждения. При отсутствии значения запрошенного утверждения в выводе SAMLResponse проверяйте регистр букв в правилах.
Для создания правил нажимаем на кнопку Add Rule и в появившемся мастере выбираем пункт Send Claims using Custom Rule. Затем нажимаем Next, вводим имя правила (по желанию) и точный текст правила.
Создаём два вспомогательных правила для добавления двух типов утверждений.
Первое — правило с именем Add ObjectGUID. Это правило ищет в каталоге LDAP значение атрибута ObjectGUID для пользователя, который выполнил аутентификацию через ADFS-сервер. Значение этого атрибута в формате Base64-строки добавляется в список существующих утверждений в виде нового типа утверждения — types = ("objectGUID").
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]
=> add(store = "Active Directory", types = ("objectGUID"), query = ";objectGUID;{0}", param = c.Value);
Второе — правило с именем Add origObjectGUID. Это правило выполняет трансформацию полученного на предыдущем этапе значения ObjectGUID в формате Base64 в формат строкового представления байтового массива с использованием нашей подключаемой скомпилированной библиотеки (Custom Attribute Store). Вызываемая функция — convertToObjectGUID.
c:[Type == "objectGUID"]
=> add(store = "OriginalObjectGIUD", types = ("origObjectGUID"), query = "convertToObjectGUID", param = c.Value);
Таким образом, после выполнения этого шага у нас появится ёще одно утверждение с типом types = ("origObjectGUID"), которое будет содержать оригинальное значение ObjectGUID пользователя из нашей Active Directory.
Теперь мы можем создать третье правило, которое будет выпускать нужное нам утверждение, используя в качестве значения содержимое утверждения с типом types = ("origObjectGUID").
Например, если мы хотим выпустить утверждение NameID с этим значением, мы можем сделать следующим образом:
Нажимаем на кнопку Add Rule и в появившемся окне мастера выбираем пункт Transform an Incomming Claim. Нажимаем Next.
Вводим имя правила, например Issue NameID. Затем в поле Incomming Claim Type с клавиатуры печатаем origObjectGUID, а в поле Outgoing Claim Type выбираем из списка Name ID (помним о зависимости типа утверждения от регистра). Больше в этом окне ничего не меняем. Нажимаем кнопку Finish.
Можно проверить, что правило было создано правильно: либо с помощью кнопки Edit Rule для этого правила, либо в появившемся окне просмотра кода правила по кнопке View Rule Language. Оно должно быть таким:
c:[Type == "origObjectGUID"]
=> issue(Type = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", Issuer = c.Issuer, OriginalIssuer = c.OriginalIssuer, Value = c.Value, ValueType = c.ValueType, Properties["http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format"] = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified");
В результате всех действий у вас будет три дополнительных правила. При запросе ADFS вы получите SAMLResponse, в котором Name ID будет содержать текстовое значение ObjectGUID без всяких кодировок, состоящих из символов в нижнем регистре.
После этого пользователь успешно залогинится, а синхронизация будет работать исправно.
Какой же атрибут AD использовать в качестве уникального идентификатора
Мы посмотрели, как удобно, быстро и легко настроить UPN, а также как сложно и с творческими изысками настроить ObjectGUID. Что же лучше?
По умолчанию мы предлагаем UserPrincipalName. Для большинства компаний, которые имеют один домен и не состоят из группы компаний, это самый простой и нативный способ. Есть поддержка изменения UPN в AD в случае штучных событий.
Если планируете массово менять домен у пользователей, это потянет массовые изменения в UPN. То есть смысла выбирать UPN нет. В таком случае можно взяться за системный атрибут, например ObjectGUID. С чем вы столкнётесь, я подробно описал в статье. Если выберете похожий, учитывайте, что могут быть другие подводные камни. Но по следам этой статьи, я думаю, вы сможете преодолеть любые сложности.
Есть ещё одна палочка-выручалочка. В крупных компаниях обычно используют уникальный идентификатор пользователя из системы HR. Этот номер копируют в AD — например, в атрибут EmployeeID у пользователя. Обычно это порядковый номер — от 1 и далее по возрастанию. Компаний, в которых количество сотрудников переваливает за 1 млн, не так много. Следовательно:
Это будет очень простое и короткое значение.
Очень редко, когда этот параметр меняется. Даже когда сотрудник увольняется, а через некоторое восстанавливается, то в HR за ним остаётся тот же номер.
Такой атрибут никак не зависит от смены Ф. И. О. или домена учётной записи пользователей. Сотрудник может ходить ежемесячно в ЗАГС, а у администратора работы не прибавится.
Даже если в будущем будет миграция доменов и учётная запись поедет в другой лес, то значение идентификатора из HR-системы останется за сотрудником.
Это прямо как ИНН: один раз и навсегда. Вот, кстати, тоже идея — хранить ИНН в AD и на его основе связываться. Или скопировать ObjectGUID в другой атрибут AD, и тогда всю конструкцию, которая описана в статье, можно не применять. Так, всё, хватит генерить идеи, заканчиваем.
Заключение
В этой статье мы рассмотрели, как работает реализация SSO в Яндекс 360, разобрали некоторые ошибки и выяснили, откуда у них растут ноги. Теперь вы знаете, как можно решать такие проблемы. А ещё оценили разные варианты, как можно подойти к вопросу выбора уникального идентификатора пользователя.
Буду рад ответить на ваши вопросы — задавайте их в комментариях. Обсудим, как быть с SSO в вашем случае.