LDAP
LDAP (Lightweight Directory Access Protocol) — открытый протокол прикладного уровня с множеством реализаций для доступа к службе каталогов, использующий TCP/IP и позволяющий производить операции аутентификации (bind), поиска (search) и сравнения (compare), а также операции добавления, изменения или удаления записей об организации, ее активах и пользователях.
Основные понятия LDAP
DN (Distinguished Name) – Уникальное имя; отличительное имя. Всякая запись в каталоге LDAP состоит из одного или нескольких атрибутов и обладает уникальным именем. Примеры уникальных имен:
CN=Администратор,CN=Users;CN=Иван Петров,OU=Сотрудники,DC=example,DC=com.
Уникальное имя состоит из одного или нескольких относительных уникальных имён (RDN). RDN (Relative Distinguished Name) – Относительное уникальное имя. Уникальное имя (DN) состоит из одного или нескольких относительных уникальных имён (RDN), разделённых запятыми. Относительное уникальное имя имеет вид ИмяАтрибута=значение. На одном уровне каталога не может существовать двух записей с одинаковыми относительными уникальными именами. Примеры RDN:
CN=Иван Петров;OU=Сотрудники;DC=example;DC=com.
Атрибуты записи. Каждая запись в LDAP-системе обладает уникальным именем (DN) и хранится как совокупность элементов, называемых атрибутами. Атрибуты являются парами «ключ-значение», например: «mail: admin@example.com», или «CN: Иван Петров». Каждый атрибут определён в наборе схемы данных.
-
DIT (Data Information Tree) – Информационное дерево Каталога (Информационное дерево справочника). Все записи в системе LDAP иерархически упорядочены в виде веток на деревьях, называемых DIT. DIT представляет собой организационную структуру (Каталог), где каждая запись (кроме корневой записи верхнего уровня) имеет ровно одну родительскую запись и под ней может находиться любое количество дочерних записей. Запись по ее DN (distinguished name) используется для идентификации в DIT в целом и функционирует как полный путь до "корня" DIT. «Корнем» является само доменное имя (например: foiv.local; citadel.com; example.com и т.д.).
-
Base DN:
- Корневая точка входа в LDAP, с которой начинается поиск всех объектов (пользователей, групп, компьютеров и т. д.).
-
Group DN:
- Путь к контейнеру/подразделению, где хранятся группы.
-
User DN:
- Путь к контейнеру/подразделению, где хранятся учетные записи пользователей.
Base DN, Group DN, User DN определяют, где искать пользователей, группы и другие объекты в структуре LDAP-каталога
Также частями спецификации Каталога LDAP, которые определяют его узлы, являются:
- CN = Common Name (Общее Имя);
- OU = Organizational Unit (Подразделение).
- DC = Domain Component (Компонент Домена).
Настройка подключения к LDAP
Настройка LDAP выполняется на странице Главная / Настройки / Пользователь / LDAP.
Памир поддерживает одновременную работу с несколькими доменами. Ниже приведены параметры настройки подключения к LDAP с пояснениями и примерами заполнения.
Основные параметры подключения
| Параметр | Описание | Пример заполнения |
|---|---|---|
| Название | Произвольное имя подключения (для идентификации в системе). | LDAP-подключение к Active Directory |
| Протокол | Тип шифрования соединения: | SSL, TLS, Без шифрования |
| Хост | IP-адрес или FQDN LDAP-сервера. | ldap.example.com, 192.168.1.100 |
| Порт | Порт LDAP-сервера (по умолчанию: 389 для незашифрованного, 636 для SSL). | 389, 636 |
| Анонимное подключение | Если включено, логин и пароль не требуются. | ☑ Включено / ☐ Выключено |
Учетные данные для поиска в LDAP
| Параметр | Описание | Пример заполнения |
|---|---|---|
| Логин | Учетная запись для поиска в LDAP (в формате DN). | CN=Администратор,CN=Users,DC=foiv,DC=local |
| Пароль | Пароль для указанного пользователя. | ******** |
Пути поиска объектов в LDAP
| Параметр | Описание | Пример заполнения |
|---|---|---|
| Base DN | Корневая точка входа в LDAP (определяет начало поиска). | DC=foiv,DC=local |
| User DN | Подпуть для поиска пользователей (добавляется к Base DN). | CN=Employees,OU=Users |
| Group DN | Подпуть для поиска групп (добавляется к Base DN). | CN=Security,OU=Groups |
Примечание:
- Полный путь для поиска пользователей:
User DN + Base DN→CN=Employees,OU=Users,DC=foiv,DC=local - Полный путь для поиска групп:
Group DN + Base DN→CN=Security,OU=Groups,DC=foiv,DC=local
Шаблоны поиска
| Параметр | Описание | Пример шаблона |
|---|---|---|
| Шаблон поиска пользователя | LDAP-запрос для поиска пользователя (должен содержать {login}). | (&(|(objectClass=person)(objectClass=inetOrgPerson))(|(uid={login})(cn={login}))) |
| Шаблон поиска группы | LDAP-запрос для поиска групп пользователя (должен содержать {dn}). | (&(|(objectClass=group)(objectClass=groupOfNames))(member={dn})) |
Пояснение:
- В
{login}подставляется логин пользователя при аутентификации. - В
{dn}подставляется Distinguished Name пользователя.
Приведённый в таблице шаблон поиска пользователя рассчитан на каталоги OpenLDAP. Для Microsoft
Active Directory он не подходит: там учётные записи имеют классы person и user, а логин хранится
в атрибуте sAMAccountName. Проверенный на Active Directory шаблон:
(&(|(objectClass=person)(objectClass=user))(sAMAccountName={login}))
Сопоставление атрибутов пользователя
При входе через LDAP система сначала находит запись пользователя в каталоге, а затем вычисляет из неё логин и значения атрибутов. И логин, и каждый атрибут задаются отдельным выражением на языке jq, которое применяется к одному и тому же JSON-объекту с результатами поиска.
Регулярные выражения и выбор типа выражения («Пользователь» или «Группа») больше не используются — всё сопоставление выполняется выражениями jq.
Данные, к которым применяется выражение
Перед вычислением выражений система:
- Подключается к серверу под учётной записью из настроек подключения.
- Ищет пользователя по шаблону поиска пользователя, подставив в
{login}логин, введённый на странице входа. Поиск идёт в Base DN, а если заданы User DN — последовательно по каждому из них, до первого пути, в котором что-то нашлось. - Ищет группы пользователя по шаблону поиска группы, подставив в
{dn}уникальное имя найденной записи.
Результат собирается в один JSON-объект — именно он подаётся на вход jq:
| Ключ | Что содержит |
|---|---|
login | Логин, который пользователь ввёл на странице входа (то же значение подставлялось в {login}) |
user_dn | Уникальное имя (DN) найденной записи пользователя |
data | Список найденных записей пользователя. Каждая запись — объект с ключами dn и attributes |
groups | Список найденных групп пользователя. Устроен так же: dn и attributes |
Три правила, без которых выражение не заработает:
- Значения атрибутов — всегда массивы строк, даже если атрибут хранит единственное значение.
Поэтому к значению обращаются по индексу:
[0]— первое значение атрибута. - Имена атрибутов чувствительны к регистру и пишутся ровно так, как их вернул сервер:
sAMAccountName, а неsamaccountname. - Двоичные атрибуты в выражениях непригодны.
objectGUIDиobjectSidв Active Directory приходят нечитаемой строкой. Идентификатор записи система берёт из них сама, настраивать это не требуется.
JQ-выражение для логина
Поле «JQ‑выражение для логина» в блоке «Сопоставление атрибутов» отвечает за то, как из данных каталога получить логин пользователя в системе.
- Выражение применяется к описанному выше JSON-объекту целиком.
- Результат должен быть непустой строкой. Если выражение вернуло
null, число, массив или объект, вход не состоится: система сообщит, что логин не найден. - Полученное значение записывается в поле
loginпользователя и по нему учётная запись сопоставляется с уже существующими — подробнее в разделе Единая учетная запись пользователя.
Раз в пять минут система обновляет атрибуты активных сессий, повторяя поиск в каталоге по уже вычисленному логину. Поэтому логин должен оставаться значением, по которому шаблон поиска находит того же пользователя. Если выражение меняет значение — например, отрезает часть почтового адреса, — убедитесь, что результат по-прежнему находится шаблоном; иначе после входа атрибуты обновляться не будут, а сессия завершится.
Пример для Microsoft Active Directory
Шаблон поиска пользователя:
(&(|(objectClass=person)(objectClass=user))(sAMAccountName={login}))
Выражение для логина:
.data[0].attributes.sAMAccountName[0]
Данные, которые получает выражение (пример сокращён — реальная запись Active Directory содержит несколько десятков атрибутов, включая служебные):
{
"login": "test.user",
"user_dn": "CN=Тестовый Пользователь,OU=Users,OU=PAMIR,DC=example,DC=local",
"data": [
{
"attributes": {
"cn": ["Тестовый Пользователь"],
"displayName": ["Тестовый Пользователь"],
"givenName": ["Тестовый"],
"sn": ["Пользователь"],
"mail": ["test.user@example.local"],
"memberOf": ["CN=Admins,OU=Groups,OU=PAMIR,DC=example,DC=local"],
"objectClass": ["top", "person", "organizationalPerson", "user"],
"sAMAccountName": ["test.user"],
"userPrincipalName": ["test.user@example.local"]
},
"dn": "CN=Тестовый Пользователь,OU=Users,OU=PAMIR,DC=example,DC=local"
}
],
"groups": [
{
"attributes": { "memberOf": [] },
"dn": "CN=Admins,OU=Groups,OU=PAMIR,DC=example,DC=local"
}
]
}
Выражение читается слева направо:
| Шаг | Что происходит |
|---|---|
.data | Берётся список найденных записей пользователя |
[0] | Из списка берётся первая запись |
.attributes | Берётся набор атрибутов этой записи |
.sAMAccountName | Берётся атрибут с таким именем — это массив ["test.user"] |
[0] | Из массива берётся первое значение — строка "test.user" |
Результат для приведённого JSON — "test.user", он и становится логином пользователя в системе.
Пример для OpenLDAP
Шаблон поиска пользователя и выражение для логина отличаются: в каталогах на основе OpenLDAP
учётные записи обычно имеют класс inetOrgPerson, а логин хранится в атрибуте uid.
(&(|(objectClass=person)(objectClass=inetOrgPerson))(|(uid={login})(cn={login})))
.data[0].attributes.uid[0]
Состав атрибутов в OpenLDAP заметно скромнее, чем в Active Directory, а идентификатор записи
приходит в entryUUID:
{
"login": "test.user",
"user_dn": "cn=Тестовый Пользователь,ou=Users,ou=PAMIR,dc=example,dc=int",
"data": [
{
"attributes": {
"cn": ["Тестовый Пользователь"],
"entryUUID": ["3f895c10-8721-1040-9c3b-33d3d656972d"],
"objectClass": ["inetOrgPerson", "top", "person"],
"sn": ["Пользователь"]
},
"dn": "cn=Тестовый Пользователь,ou=Users,ou=PAMIR,dc=example,dc=int"
}
],
"groups": []
}
Обратите внимание: в этом примере атрибута uid у записи нет, и выражение .data[0].attributes.uid[0]
вернёт null. Состав атрибутов зависит от того, как заполнен конкретный каталог, поэтому выражение
всегда нацеливают на атрибут, который реально виден в данных — как их посмотреть, описано ниже в
разделе Проверка выражения. Если логин хранится, например, в sn, выражение
будет .data[0].attributes.sn[0].
Сопоставление дополнительных атрибутов
Ниже поля с выражением для логина находится список атрибутов пользователя, которые заполняются из каталога. Каждая строка списка состоит из трёх частей.
Название
Атрибут пользователя в системе, в который записывается результат.
Выражение
Выражение jq, применяемое к тому же JSON-объекту. Результат должен быть строкой; значение другого
типа система считает отсутствующим. Примеры:
.data[0].attributes.mail[0]— почта;.data[0].attributes.displayName[0]— отображаемое имя;.data[0].attributes.givenName[0] + " " + .data[0].attributes.sn[0]— имя и фамилия одной строкой;.groups[0].dn— уникальное имя первой из найденных групп.
Тип синхронизации
Определяет, когда обновлять атрибут:
| Значение | Поведение |
|---|---|
DISABLE | Синхронизация выключена, значение атрибута из каталога не берётся |
ON_CREATE | Атрибут заполняется один раз, при первом входе пользователя через это подключение |
PERIODIC | Атрибут пересчитывается при каждом входе и при обновлении данных активной сессии |
IF_EXIST | То же, что PERIODIC, но пустой результат не стирает уже сохранённое в системе значение |
Данные активных сессий обновляются раз в пять минут: система повторяет поиск пользователя в
каталоге и пересчитывает выражения. Разница между PERIODIC и IF_EXIST проявляется, когда
выражение не дало значения: в режиме PERIODIC атрибут в системе очищается, в режиме IF_EXIST
сохраняется прежнее значение.
Результаты поиска групп
Группы ищет отдельный запрос к каталогу, но обрабатываются они не в стороне: результат попадает в
тот же JSON-объект, в ключ groups. Он доступен выражениям наравне с data, обращение такое же —
.groups[0].dn, .groups[0].attributes.memberOf[0].
Пустой список групп — штатная ситуация: она означает, что по шаблону поиска группы ничего не нашлось. На вычисление логина это не влияет, вход состоится.
Членство пользователя в группах Active Directory дополнительно видно в его собственном атрибуте
memberOf внутри data — это независимый от поиска групп источник, к которому тоже можно
обращаться выражением.
Группы и OU пользователя
Памир содержит два системных атрибута пользователя: ous — список OU пользователя, и
groups — список групп, в которых он состоит. Атрибуты остаются в системе, но автоматически при
входе через LDAP они не заполняются: значения из каталога попадают в атрибуты только через
сопоставление, описанное выше.
Выражение всегда возвращает одну строку, поэтому список собирают через join. Группы — по
результатам поиска групп:
[.groups[].dn] | join(", ")
Для приведённого выше примера с Active Directory результат —
CN=Admins,OU=Groups,OU=PAMIR,DC=example,DC=local.
Если поиск групп ничего не вернул, те же данные обычно есть в атрибуте memberOf самой записи
пользователя:
[.data[0].attributes.memberOf[]?] | join(", ")
Список OU собирается из уникального имени пользователя. Регистр ou= в разных каталогах
отличается, поэтому сравнение идёт без учёта регистра:
[.user_dn | split(",")[] | select(test("^ou=";"i")) | sub("^[oO][uU]=";"")] | join(", ")
Для CN=Тестовый Пользователь,OU=Users,OU=PAMIR,DC=example,DC=local результат — Users, PAMIR.
Эти выражения заполняют атрибуты строкового типа. У системных ous и groups тип «Список
строк», и результат выражения в них не запишется: строку система не принимает по схеме атрибута и
отклоняет вход с ошибкой Incorrect attributes: groups: is not valid by schema, а выражение,
возвращающее массив ([.groups[].dn] без join), считает пустым результатом. Пока это не изменено
на стороне сервера, списки групп и OU складывайте в атрибуты строкового типа.
Особые случаи
| Ситуация | Что происходит |
|---|---|
| Пользователь не найден | Выражения не вычисляются вовсе, вход отклоняется с сообщением, что пользователь не найден на сервере |
| Атрибута нет в записи | Выражение возвращает null. Для логина это ошибка входа, для атрибута — отсутствие значения |
| Атрибут есть, но пуст | Значение приходит пустым, обращение по [0] снова даёт null — результат тот же, что и при отсутствии атрибута |
| Найдено несколько записей | Все они попадают в data, [0] берёт первую в том порядке, в каком их вернул сервер. Уточните шаблон поиска пользователя |
Нет objectGUID или entryUUID | Вход отклоняется: системе нечем однозначно связать учётную запись с записью каталога |
| Выражение написано с ошибкой | Вход отклоняется с сообщением о неверном формате данных и текстом ошибки jq |
Проверка выражения
Проверить выражение можно, не заходя в систему под учётной записью пользователя и не изучая исходный код.
- Сохраните подключение.
- Нажмите «Протестировать соединение» и введите логин и пароль любого пользователя каталога.
- На странице результата разверните блок «Исходные данные» — это в точности тот JSON, к которому применяются выражения, со всеми атрибутами, которые вернул сервер.
- Блок «Сопоставленный пользователь» показывает, какой пользователь системы получился после применения выражений.
Проверка не создаёт и не изменяет учётные записи: результат нужен только для настройки.
«Исходные данные» содержат все атрибуты записи, которые вернул сервер, в том числе служебные. Не публикуйте этот вывод целиком.