Skip to content

Instantly share code, notes, and snippets.

@vadv
Created September 22, 2026 06:22
Show Gist options
  • Select an option

  • Save vadv/e659e22674fd85752e05877aeee45a21 to your computer and use it in GitHub Desktop.

Select an option

Save vadv/e659e22674fd85752e05877aeee45a21 to your computer and use it in GitHub Desktop.
doorman + ldap

PgDoorman: native LDAP — спецификация требований

Версия 0.12, 2026-09-21. Статус: черновик, не согласован. LDAP-поддержка не реализована.

1. Введение

1.1 Назначение

Аутентификация клиентов PgDoorman в каталоге LDAP/LDAPS без PAM, с выбором роли PostgreSQL по имени клиента или по группе каталога.

1.2 Термины

Термин Значение
LDAP-клиент клиент, для которого первая подходящая строка pg_hba имеет метод ldap
логин имя пользователя из StartupMessage клиента, как введено
запись users элемент pools.<db>.users; описывает пул к PostgreSQL и его реквизиты
правило элемент pools.<db>.ldap.mapping
целевая запись запись users, в которую правило направляет LDAP-клиента
search+bind режим строки ldap с ldapbasedn: служебный bind, поиск, bind найденным DN
simple bind режим строки ldap с ldapprefix/ldapsuffix: bind собранным DN
персональная роль целевая запись с именем, равным логину
общая роль целевая запись, в которую направлены несколько логинов

1.3 Ссылки

Референс: PostgreSQL 18 — LDAP Authentication, hba.c, auth.c.

Код PgDoorman: src/config/{user,pool,general}.rs, src/auth/hba.rs, src/pool/mod.rs, src/server/server_backend.rs.

2. Ограничения

ID Требование
C-1 Синтаксис LDAP-строк pg_hba совместим с PostgreSQL 16–18; референс поведения — PostgreSQL 18. Исключения перечислены в CFG-HBA-3 и разделе 11. Собственных опций PgDoorman в строке нет.
C-2 Пароль клиента используется только для bind в каталог. В PostgreSQL он не передаётся ни в каком виде.
C-3 Запись users — единственное описание пула к PostgreSQL: имя, реквизиты backend, размеры, режим, хуки. Конфигурация LDAP не содержит копий этих полей.
C-4 Всё специфичное для PgDoorman в LDAP находится в pools.<db>.ldap. Глобального блока LDAP нет.
C-5 Пулы известны при загрузке конфигурации. Динамических пулов для LDAP-клиентов нет.
C-6 При отказе LDAP-аутентификации клиент получает 28000 или 28P01, подробная причина остаётся в логе. Ошибки backend и setup передаются по F-BE-3 и F-HOOK-4.
C-7 Некорректная или неоднозначная конфигурация отклоняется при загрузке, если ошибка определяется статически. При ошибке аутентификации или подготовки backend клиент получает отказ. Backend с незавершённым setup, ошибкой cleanup или нарушенным состоянием протокола закрывается и повторно не выдаётся.

3. Конфигурация

3.1 Строка pg_hba

ID Требование
CFG-HBA-1 Метод ldap с опциями ldapserver, ldapport, ldapscheme, ldaptls, ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, ldapprefix, ldapsuffix, ldapurl.
CFG-HBA-2 Проверки при загрузке — как в hba.c PostgreSQL 18, с теми же текстами ошибок: смешение режимов; отсутствие ldapbasedn/ldapprefix/ldapsuffix; ldapsearchattribute вместе с ldapsearchfilter; ldapport=0; неизвестная опция; опция не вида name=value; map= при ldap.
CFG-HBA-3 Строже PostgreSQL, ошибки загрузки: ldapscheme не из ldap/ldaps; пустой ldapserver; ldapscheme=ldaps вместе с ldaptls=1.
CFG-HBA-4 ldapsearchfilter действует как в PostgreSQL: все вхождения $username заменяются логином; без подстановки фильтр используется как задан, без дополнительного предупреждения. Если фильтр не задан, поиск идёт по ldapsearchattribute, по умолчанию uid.
CFG-HBA-5 ldapurl: схема ldap/ldaps, один хост, base, первый атрибут, scope (по умолчанию base, как в документации PostgreSQL), фильтр; поля участвуют в CFG-HBA-2 как отдельные опции.
CFG-HBA-6 ldapbindpasswd маскируется в SHOW CONFIG и логах.

3.2 Записи users

ID Требование
CFG-USR-1 password необязателен; для MD5/SCRAM это хеш для проверки пароля клиента. Без него локальная проверка MD5/SCRAM недоступна; запись может принимать LDAP-клиентов или подключения по trust. Существующее поведение PAM и других методов аутентификации сохраняется.
CFG-USR-2 server_password — пароль PostgreSQL в открытом виде; допускается без server_username. server_username по умолчанию равен username. Прежняя ошибка «server_password requires server_username» снимается для всех записей.
CFG-USR-3 Запись с password может быть целевой записью правила.
CFG-USR-4 Запись получает поля setup_server_query и cleanup_server_query (раздел 3.5).

3.3 Блок pools.<db>.ldap

ID Требование
CFG-LDAP-1 Пул принимает LDAP-клиентов, только если у него есть блок ldap; пустой блок допустим.
CFG-LDAP-2 Ключи блока: timeout, connections, cache_ttl, cache_failure_ttl, min_interval, group_attribute, mapping. Других ключей нет; неизвестный ключ — ошибка загрузки.
CFG-LDAP-3 timeout — общий бюджет соединения и операций для каждого сервера из ldapserver, по умолчанию 60s. Ожидание свободного соединения имеет отдельный бюджет той же длительности. При N серверах суммарный бюджет — до (N+1)×timeout.
CFG-LDAP-4 connections — максимальное число постоянных соединений с каталогом для пула; целое > 0, по умолчанию 2. Одно соединение обслуживает один вход одновременно. Ограничение при RELOAD описано в F-LOAD-5.
CFG-LDAP-5 cache_ttl, cache_failure_ttl, min_interval — имена и типы как у auth_query. Значения по умолчанию: 0s, 30s, 1s. Кэш успешных входов по умолчанию выключен.
CFG-LDAP-6 group_attribute — атрибут записи пользователя со списком групп. По умолчанию memberOf.

3.4 Список mapping

ID Требование
CFG-MAP-1 mapping — упорядоченный список правил. Если задан, не пуст.
CFG-MAP-2 Правило содержит ровно один селектор: user — логин, точное сравнение с учётом регистра; group — DN группы, содержит =.
CFG-MAP-3 Правило содержит цель as_user — имя записи users того же пула. Для селектора user по умолчанию равна логину; для group обязательна.
CFG-MAP-4 Цель существует среди записей users пула; иначе ошибка загрузки.
CFG-MAP-5 Два правила с одинаковым user или одинаковым group — ошибка загрузки с указанием недостижимого правила.
CFG-MAP-6 Подстановочных селекторов («любой») нет.

3.5 SQL при выдаче и возврате backend

ID Требование
CFG-HOOK-1 setup_server_query — новый ключ уровней general, pools.<db>, users[]. Отсутствие поля или null означает наследование; setup_server_query: "" отключает унаследованный setup. Приоритет: users[] → пул → general.
CFG-HOOK-2 cleanup_server_query дополнительно принимается на уровне users[], с приоритетом над пулом и general. Отсутствие поля или null означает наследование; пустая строка остаётся ошибкой, как в действующей конфигурации.
CFG-HOOK-3 В обоих ключах допустимы шаблоны {{username}} (логин) и {{group}} (DN из сработавшего group-правила, иначе пустая строка). Неизвестный плейсхолдер — ошибка загрузки.
CFG-HOOK-4 Подстановка текстовая, без экранирования; SQL и контекст подстановки задаёт администратор. Подставляемое значение не содержит ', ", \, ; и управляющих символов: для {{group}} проверяется при загрузке, для {{username}} — при входе. Эти проверки не гарантируют безопасность подстановки в произвольном SQL-контексте.

4. Функциональные требования

4.1 Выбор метода

ID Требование
F-HBA-1 Первая строка pg_hba, подходящая по типу соединения, базе, логину и адресу, определяет метод.
F-HBA-2 Метод ldap исключает остальные методы, users с паролем и auth_query для этого клиента.
F-HBA-3 Если у пула нет блока ldap, LDAP-клиент получает 28000; в лог pool "<db>" has no ldap block.

4.2 Проверка в каталоге

ID Требование
F-DIR-1 Клиенту отправляется AuthenticationCleartextPassword. Пустой пароль — 28P01 empty password returned by client.
F-DIR-2 В режиме search+bind логин с символами * ( ) \ / отклоняется без обращения к каталогу, как в PostgreSQL.
F-DIR-3 Search+bind: bind под ldapbinddn/ldapbindpasswd или анонимный; поиск в ldapbasedn по (<ldapsearchattribute>=<логин>) (по умолчанию uid) либо по ldapsearchfilter с заменой всех $username; ровно одна запись, иначе отказ; bind найденным DN и паролем клиента на том же сеансе.
F-DIR-4 Simple bind: DN = ldapprefix + логин + ldapsuffix; bind этим DN.
F-DIR-5 При наличии group-правил group_attribute запрашивается тем же поиском (search+bind) либо чтением собственной записи после bind (simple bind).
F-DIR-6 Новый LDAP-сеанс начинает перебор хостов ldapserver с первого. Здоровое соединение переиспользуется, в том числе со вторым сервером после failover; первый перед каждым входом не проверяется. Сетевая ошибка или истечение timeout переводит текущий вход к следующему хосту с полным бюджетом timeout. Ошибка каталога (неверный пароль, нет записи, нет прав) завершает вход отказом.
F-DIR-7 TLS по строке: ldapscheme=ldaps — TLS с соединения, ldaptls=1 — StartTLS. Доверие — системное хранилище CA. Имя хоста сверяется с ldapserver.
F-DIR-8 Referrals в ответах каталога не отслеживаются.
F-DIR-9 Входы сверх connections ждут свободное соединение в пределах timeout. Перед каждым поиском в search+bind выполняется служебный либо анонимный bind; в simple bind — bind текущего пользователя. Оборванное соединение не переиспользуется; дальнейший перебор хостов — по F-DIR-6.
F-DIR-10 По истечении timeout последнего хоста — 28000; в лог хост и стадия, на которой истёк бюджет.
F-DIR-11 Обработка LDAP не блокирует обслуживание других клиентов.

4.3 Выбор записи

ID Требование
F-MAP-1 mapping отсутствует: целевая запись — запись с именем, равным логину; её нет — отказ.
F-MAP-2 mapping задан: правила проверяются сверху вниз; первое совпавшее определяет целевую запись. Для LDAP-клиента список исчерпывающий: запись без правила недостижима.
F-MAP-3 Клиент, состоящий в нескольких группах, получает первое по списку group-правило со своей группой. Порядок значений group_attribute в ответе каталога не влияет.
F-MAP-4 Совпадений нет — 28000; в лог no match in ldap.mapping for user "<логин>"; groups=[<значения group_attribute>].
F-MAP-5 Сравнение DN групп: без учёта регистра, пробелы вокруг , и = игнорируются, других преобразований нет.
F-MAP-6 Ошибка чтения group_attribute — отказ на стадии groups; переход к следующим правилам не выполняется.
F-MAP-7 Вложенные группы не разворачиваются.

4.4 Кэш

ID Требование
F-CACHE-1 Ключ: пул, идентификатор LDAP-конфигурации выбранной строки pg_hba, логин, хеш пароля с солью процесса. Результат одной LDAP-строки не используется для другой. Значение: результат bind, DN, сработавшее правило. Пароль в открытом виде не сохраняется и обнуляется после bind.
F-CACHE-2 Успешный результат действует cache_ttl; при 0 успешные входы не кэшируются. Отказ bind запоминается на cache_failure_ttl в пределах ключа F-CACHE-1; повтор для того же логина раньше min_interval после отказа отклоняется без обращения к каталогу.
F-CACHE-3 Reload очищает кэш.
F-CACHE-4 Наличие свободного backend-соединения не отменяет проверку (кэш или каталог).

4.5 Backend

ID Требование
F-BE-1 Для LDAP-клиента реквизиты backend — только из целевой записи: server_username (по умолчанию username) и server_password в открытом виде. Хеш password проверяет frontend-клиента и не заменяет server_password в LDAP-пути.
F-BE-2 Без server_password LDAP-путь подключается к PostgreSQL без пароля; запрос пароля сервером — ошибка backend. Для backend с парольной аутентификацией администратор задаёт server_password.
F-BE-3 Ошибка backend передаётся клиенту как ошибка PostgreSQL.

4.6 Хуки

ID Требование
F-HOOK-1 pool_mode: session: setup_server_query — один раз при закреплении backend за клиентом; при отключении клиента действует политика очистки F-HOOK-3.
F-HOOK-2 pool_mode: transaction: setup_server_query — при каждой выдаче backend; при каждом возврате действует политика очистки F-HOOK-3.
F-HOOK-3 Семантика cleanup_server_connections сохраняется: off отключает очистку, adaptive выполняет её по необходимости, always выполняет заданный cleanup_server_query после использования backend. Пользовательский SQL заменяет встроенную очистку. Без него adaptive использует встроенный путь, а always отклоняется при загрузке конфигурации. ROLLBACK незавершённой транзакции не зависит от режима. Изменения от setup учитываются адаптивной очисткой, в том числе set_config.
F-HOOK-4 До успешного завершения setup backend не обслуживает запросы клиента. Ошибка setup возвращается клиенту и записывается в лог; backend закрывается. При ошибке cleanup backend также закрывается, уже полученный ответ на запрос клиента сохраняется. NOTICE сам по себе не считается ошибкой.
F-HOOK-5 Результаты SQL-хуков клиенту не передаются; ошибка setup обрабатывается по F-HOOK-4.
F-HOOK-6 Хук выполняется для любого клиента целевой записи независимо от метода аутентификации.

4.7 Ошибки и лог

Стадия Причина Клиенту В лог
pool нет блока ldap 28000 pool "<db>" has no ldap block
password пустой пароль 28P01
name запрещённый символ в логине 28000 invalid character in user name
throttle min_interval или отказ в кэше 28000 cached failure
connect хосты недоступны, ошибка TLS 28000 хост, ошибка
service_bind отказ служебного bind 28000 could not perform initial LDAP bind + диагностика
search 0 или >1 записей 28000 does not exist / is not unique + фильтр
user_bind отказ bind пользователя 28000 LDAP login failed + диагностика
groups ошибка чтения групп 28000 ошибка каталога
map нет правила / нет записи 28000 no match in ldap.mapping …; groups=[…]
timeout истёк timeout 28000 стадия
template недопустимый символ для шаблона 28000 логин, запись
backend ошибка PostgreSQL или setup_server_query ошибка PostgreSQL backend + текст

Успех: connection authenticated: identity="<DN>" method=ldap, сработавшее правило, целевая запись.

5. Загрузка и reload

ID Требование
F-LOAD-1 Ошибка LDAP-конфигурации, ссылки mapping или шаблона хука отклоняет конфигурацию целиком. При RELOAD продолжает действовать прежняя конфигурация.
F-LOAD-3 По SIGHUP/RELOAD перечитываются строки pg_hba, записи users, блоки ldap. Начатый вход завершается со старым снимком.
F-LOAD-4 Изменение записи users пересоздаёт её пул. Изменение блока ldap применяется к следующим входам. Открытые сессии не пересматриваются.
F-LOAD-5 Пока завершаются входы со старой конфигурацией, старые и новые LDAP-соединения могут существовать одновременно; суммарное число входов может временно превышать connections. В первой версии общий лимит между поколениями конфигурации не обеспечивается.

6. Наблюдаемость

ID Требование
F-OBS-1 SHOW POOLS: пул целевой записи (<db>/<username>); SHOW CLIENTS: логин клиента и целевая запись.
F-OBS-2 Метрики: входы по результату и стадии; входы в каталоге сейчас; ожидание сеанса; попадания в кэш; выполнения и ошибки хуков.

7. Безопасность

ID Требование
SEC-1 Персональная роль без server_password требует, чтобы PostgreSQL принимал PgDoorman без пароля (trust по адресу, cert по клиентскому сертификату, peer). Граница доверия — сеть и PgDoorman.
SEC-2 Общая роль скрывает личность клиента от PostgreSQL; личность доступна через setup_server_query и лог PgDoorman. SET ROLE в общем пуле не является границей: RESET ROLE возвращает общую роль.
SEC-3 Администратор контролирует SQL хуков и отвечает за контекст текстовой подстановки. PgDoorman не экранирует значения; запрет символов CFG-HOOK-4 не является универсальной защитой SQL-шаблонов.
SEC-4 Пароль клиента и ldapbindpasswd не попадают в лог.

8. Требования к документации

ID Требование
DOC-1 Документация на русском и английском: authentication/ldap.md, порядок методов в overview.md, метод ldap в hba.md, актуализация PAM, comparison.md, changelog.md, пример конфигурации.
DOC-2 Правило первого совпадения показано на примере пользователя, состоящего в двух группах с разными правилами: bob в manager и analysts при порядке правил manager, analysts получает роль manager; при обратном порядке — analytics.
DOC-3 Объяснить выбор по имени без mapping и первое совпадение при заданном списке. Персональное правило получает приоритет только за счёт положения в списке; без совпадения — отказ.
DOC-4 Описать бюджет timeout на каждый сервер, переиспользование здорового соединения после failover, ldapsearchfilter по правилам PostgreSQL, ограничения вложенных групп и referrals.
DOC-5 Явно: строки ldap писать как hostssl; SEC-1 и SEC-2 с примерами pg_hba.conf/pg_ident.conf PostgreSQL для trust и cert. Различать frontend-хеш password и открытый пароль backend server_password: для LDAP-доступа к PostgreSQL с парольной аутентификацией нужен server_password в конфигурации.
DOC-6 Личность в общем пуле: пример set_config('app.ldap_user', '{{username}}', false) и current_setting. Описать текстовую подстановку под контролем администратора, отключение унаследованного setup через setup_server_query: "" и закрытие backend при ошибке setup.
DOC-7 Миграция с PostgreSQL/PgBouncer: совместимые LDAP-параметры и ограничения CFG-HBA-3, раздела 11. Пароль клиента используется только в LDAP; остальные клиенты пула продолжают использовать свои методы аутентификации, включая auth_query.
DOC-8 При cache_ttl: 0 успешный вход требует проверки в каталоге. При ненулевом TTL отзыв доступа, смена группы и прекращение приёма старого пароля могут задержаться до истечения cache_ttl. Новый пароль проверяется отдельно с учётом кэша отказов и min_interval.
DOC-9 Описать ограничения RELOAD: начатый вход использует старый снимок, открытые сессии не пересматриваются, старый и новый LDAP-пулы могут временно превысить connections.

9. План BDD

Проверяем вход через PgDoorman с настоящими LDAP и PostgreSQL в существующем Rust Cucumber и Docker/Nix. Текущая ldap-infrastructure.feature проверяет только тестовый каталог через CLI; она не подтверждает LDAP-аутентификацию PgDoorman. Используем существующие шаги PostgreSQL-протокола и клиентские Python-скрипты внутри того же запуска BDD.

Таблицы ниже задают группы сценариев; независимые ошибки и варианты оформляются отдельными сценариями или строками Examples. Полное произведение транспортов, режимов пула, драйверов и политик очистки не требуется.

9.1 Стенд и повторяемость

  • Переиспользовать ldap_helper.rs: настоящий slapd, LDIF, временные сертификаты, смена пароля, изоляция и очистка процессов. Добавить варианты ACL для анонимного и служебного поиска, группы memberOf и изменение членства.
  • Для двух LDAP-серверов нужны разные loopback-адреса с одним портом: у ldapserver общий ldapport. Рестарт сохраняет endpoint. Нынешний helper выдаёт разные порты на одном адресе и при рестарте меняет их.
  • Экспортировать события slapd: соединение, bind, search, результат и DN, без паролей. Счётчики снимать после проверки готовности; служебные запросы самого теста не учитывать. Для ожидания и обрыва добавить управляемый посредник: операция получена → ответ удерживается → ответ разрешён либо соединение закрыто.
  • Для native LDAPS/StartTLS доверенный CA передавать только процессу PgDoorman через окружение теста. Проверять также посторонний CA и неверное имя в сертификате. Доверие на машине разработчика не менять.
  • Startup-ошибки читать как ErrorResponse с SQLSTATE; panic клиента или любое закрытие сокета не считать доказательством ожидаемого отказа.
  • Backend проверять SQL-запросом: session_user, current_user, current_setting, PID. Для доказательства повторного использования — pool_size: 1; для LDAP — connections: 1 и идентификатор соединения slapd. Ошибка входа дополнительно проверяется отсутствием выполнения клиентского SQL.
  • Синхронизация — по ответам протокола, событиям LDAP, состоянию очереди и наблюдаемому состоянию PostgreSQL с предельным временем ожидания. Барьер незавершённого setup можно построить на advisory lock; достигнутую стадию подтверждать через отдельное соединение наблюдателя. Счётчик попыток до SQL-ошибки — sequence, чтобы rollback не стёр свидетельство выполнения.
  • Точные границы TTL, min_interval и бюджетов времени проверять компонентными тестами с управляемыми часами. В BDD попадание в кэш проверять с большим TTL; истечение — ожиданием изменения наблюдаемого результата с deadline, без sleep(TTL + запас). В тестах кэша отключать остальные ограничения, если проверяется не их взаимодействие.

9.2 Вход и выбор метода

ID Сценарий Наблюдаемая проверка
T-1 Simple bind и search+bind; служебный и анонимный поиск Клиент получает AuthenticationCleartextPassword, входит и выполняет SQL под ожидаемой ролью. LDAP подтверждает bind нужного DN. Поддерживаемые HBA-строки проверяются также прямым входом в эталонный PostgreSQL.
T-6 Пустой пароль 28P01; пользовательский bind и клиентский SQL не выполняются.
T-7 Неверный пароль, отсутствующая или неуникальная запись, неверный пароль служебной учётки 28000 и соответствующая стадия в логе; исправные альтернативные HBA-строка и второй каталог не превращают отказ в успешный вход.
T-23 Поиск по умолчанию uid, явный атрибут, фильтр с несколькими $username, фильтр без подстановки, эквивалентный ldapurl На специально различающихся записях найден ожидаемый DN. Поведение совпадает с PostgreSQL; 0 или несколько результатов не допускают вход. Для URL проверить base, one, sub на разных уровнях LDIF.
T-26 Первая HBA-строка — LDAP; ниже разрешающие SCRAM/trust, настроен auth_query; обратный порядок строк Отказ LDAP не вызывает другой метод. При первой подходящей строке SCRAM/trust LDAP не вызывается. Счётчики LDAP и auth_query подтверждают выбранный путь.
T-27 Нет блока ldap; пустой блок; mapping отсутствует Без блока — 28000. С пустым блоком существующая одноимённая запись users работает; отсутствующая запись даёт отказ без создания динамического пула.
T-28 LDAP, LDAPS, StartTLS; для TLS — неверный CA, имя сертификата, отказ StartTLS На каждом поддерживаемом транспорте вход успешен. При ошибке TLS — отказ, без пользовательского bind по незащищённому соединению. Проверяется PgDoorman, а не только LDAP CLI.
T-13 Запрещённые символы в search-логине; отдельно логин с кавычкой при {{username}} в setup 28000, стадия name либо template; небезопасный фильтр или SQL не отправляется.
T-16 В записи нет password, выбран SCRAM, auth_query отсутствует Отказ; отсутствие локального пароля не превращается в trust или LDAP.

9.3 Mapping и реквизиты PostgreSQL

ID Сценарий Наблюдаемая проверка
T-2 Пользователь состоит в двух группах; меняем порядок правил и отдельно порядок значений атрибута Роль PostgreSQL определяется первым совпавшим правилом; порядок значений от LDAP результата не меняет.
T-3 Совпадают персональное и групповое правила, в обоих порядках Выбрано первое правило, без скрытого приоритета персональной записи.
T-4 При заданном mapping совпадения нет, но одноимённая запись users существует 28000; возврата к собственному имени нет.
T-29 Группы в search+bind и simple bind; memberOf и явно выбранный атрибут; только вложенное членство Прямое членство выбирает ожидаемую роль. PgDoorman сам не разворачивает вложенные группы.
T-30 Ошибка LDAP-операции чтения групп при наличии следующего разрешающего правила Отказ на стадии groups; следующее правило не разрешает вход. Fixture должна давать ошибку операции, а не просто скрывать атрибут через ACL.
T-34 Пароли LDAP, frontend SCRAM и PostgreSQL различаются; server_username задан или отсутствует Вход LDAP и SQL успешны только с правильным server_password; session_user соответствует явной backend-роли либо имени целевой записи.
T-35 server_password отсутствует; PostgreSQL использует trust либо требует пароль, совпадающий с LDAP-паролем Trust работает. Парольная аутентификация PostgreSQL отклоняется: LDAP-пароль и frontend-хеш не используются как запасные реквизиты.
T-14 SCRAM- и LDAP-клиенты используют одну запись users; отдельный клиент входит через auth_query Все разрешённые методы работают с ожидаемыми ролями и setup; LDAP не подменяет остальные пути.

9.4 Кэш

ID Сценарий Наблюдаемая проверка
T-10 Повторный успешный вход при cache_ttl: 0 и большом TTL В первом случае есть новый пользовательский bind, во втором — попадание в кэш без bind. Роль и setup остаются правильными.
T-11 После успешного входа меняем пароль; отдельно членство в группе При TTL=0 следующий вход отражает изменение. С положительным TTL старый пароль/выбранная роль действуют до истечения; затем результат обновляется. Новый пароль имеет отдельную запись кэша.
T-22 Одинаковые логин и пароль в разных пулах; отдельно две HBA-строки одного пула, выбирающие разные каталоги по адресу клиента Каталоги содержат разные группы; в отдельном варианте пароль верен только в одном. Успех, отказ и роль не переносятся между ключами. Для обеих HBA-строк действительно используются разные адреса клиента, без RELOAD между входами.
T-31 Повтор неверного пароля при включённом кэше отказов и min_interval: 0; затем другой пароль и истечение TTL отказа Повтор не вызывает bind; другой пароль проверяется независимо; после истечения прежний пароль снова проверяется в каталоге.
T-32 Кэш отказов выключен; после отказа повтор того же логина с другим паролем раньше min_interval; другой логин Первый повтор отклоняется без bind, другой логин проходит проверку. Точная граница интервала проверяется компонентным тестом.
T-33 После успешного клиента остаётся свободный backend; следующий вход с неверным паролем, кэш успехов выключен LDAP отклоняет вход независимо от наличия backend; клиентский SQL не выполняется.

9.5 Соединения с каталогом и отказы

ID Сценарий Наблюдаемая проверка
T-8 Первый endpoint отказывает в соединении либо удерживает ответ LDAP-операции Вход обслуживает второй сервер. Для закрытого endpoint подтверждение — ошибка connect в логе PgDoorman и bind на втором сервере; для удержанного ответа — события обоих серверов. Точные бюджеты проверяются компонентно, без требования уложиться ровно в 2×timeout.
T-9 connections: 1, первый вход удерживается барьером, второй ждёт; затем барьер снимается После роста gauge ожидающих LDAP-входов до 1 (F-OBS-2) второй ещё не выполняет LDAP-операцию. После освобождения оба завершаются; одновременно занят не более одного LDAP-сеанса. Уже открытый обычный клиент продолжает выполнять SQL.
T-21 Первый LDAP-сервер восстановлен на прежнем endpoint, но соединение со вторым здорово; connections: 1 Следующий вход использует прежний connection ID второго сервера; первый не получает новых попыток.
T-36 Два последовательных пользователя, затем обрыв переиспользуемого соединения; кэш успехов выключен До обрыва один connection ID; перед новым search — служебный/анонимный bind, при simple bind — bind нового пользователя. После обрыва старое соединение не используется; новый вход либо проходит полный bind, либо получает отказ.
T-37 Каталог возвращает referral на другой контролируемый endpoint PgDoorman не подключается к адресату и не передаёт ему пароль; ссылка не позволяет обойти поиск и bind выбранного каталога.

Ожидание очереди и операция LDAP имеют одинаковый timeout. Поэтому один зависший bind не доказывает timeout очереди: он сам освободит слот. Точную границу очереди проверять с управляемым временем; BDD T-9 доказывает ограничение параллелизма и освобождение слота.

9.6 Setup и изоляция backend

ID Сценарий Наблюдаемая проверка
T-12 Alice и bob по очереди используют одну общую запись с pool_size: 1; режимы session и transaction При том же PID app.ldap_user всегда соответствует текущему клиенту. В session setup выполняется один раз на закрепление, в transaction — на каждую выдачу; счётчик не растёт от каждого SQL внутри одной транзакции.
T-19 Setup изменил состояние, затем получил SQL-ошибку; отдельно клиент отключился, пока setup удерживался барьером Клиент получает ошибку либо уже отключён; его SQL не выполнен. Наблюдатель подтверждает закрытие backend; следующий клиент не получает его PID или частичное состояние.
T-20 Разные setup на уровнях general, pool, user; у пользователя отсутствие, null, "" SQL показывает приоритет user → pool → general; отсутствие/null наследуют, пустая строка отключает.
T-25 Setup выдаёт NOTICE, включая 0A000 и 0AM01, затем завершается успешно SQL клиента выполняется; NOTICE не становится ошибкой. При повторной выдаче backend сохраняет PID.
T-38 Setup удерживается барьером, клиентский запрос уже отправлен После подтверждения ожидания setup маркер клиентского запроса не изменён. После снятия барьера запрос видит результат полного setup.
T-39 Пользовательские setup/cleanup с {{username}} и {{group}}; после группового LDAP-клиента — персональный либо SCRAM-клиент Подстановки относятся к нужному клиенту и правилу. Cleanup использует личность уходящего клиента; у клиента без группового правила {{group}} пуст.
T-40 Setup вызывает set_config; adaptive без custom SQL, custom cleanup на уровне user, режим off При том же PID встроенная очистка сбрасывает параметр, который следующий setup не перезаписывает. Custom SQL имеет приоритет user → pool → general; его выполнение подтверждает счётчик, а отсутствие встроенного RESET ALL — сохранённый GUC-маркер, заранее установленный и не изменяемый custom SQL. При off счётчик cleanup не меняется.
T-41 Setup возвращает строки и CommandComplete; cleanup после успешного COMMIT завершается ошибкой Клиент получает только ответы своего SQL, включая успешный COMMIT. Данные видны наблюдателю; backend с ошибкой cleanup закрыт. Переиспользовать существующий cleanup-сценарий, добавив setup и LDAP-клиента.

9.7 Валидация и RELOAD

ID Сценарий Наблюдаемая проверка
T-5 Дубликат селектора, неизвестная цель mapping, оба селектора в правиле; неизвестный шаблон хука Ошибка конфигурации указывает проблемное поле; конфигурация не применяется. Полную таблицу значений проверять unit-тестами.
T-17 Несовместимые LDAP-опции, ldapscheme=foo, пустой сервер, connections: 0 Запуск отклонён. Тексты ошибок PostgreSQL проверяются для перечисленных в CFG-HBA-2 случаев; дополнительные ограничения — отдельно.
T-24 Ошибочная конфигурация при запуске и RELOAD При запуске процесс завершается с ошибкой; при RELOAD работающий клиент и новый вход продолжают использовать прежнюю конфигурацию.
T-15 RELOAD меняет порядок mapping, HBA, служебный пароль LDAP или server_password Новые входы используют новые настройки; уже открытая сессия сохраняет прежнюю роль и работоспособность. Барьер применения — ответ RELOAD и наблюдаемая активная конфигурация, без sleep.
T-42 Перед RELOAD заполнены положительный и отрицательный кэши, затем изменены пароль/доступ После RELOAD первый вход снова проверяется в LDAP и отражает актуальное состояние.
T-43 Положительный TTL включён, кэш отказов и min_interval выключены; удерживаем успешный BindResponse, меняем пароль, выполняем RELOAD и освобождаем ответ Старый вход завершается по старому снимку. Его результат не попадает в новый кэш: новый вход со старым паролем делает bind и получает отказ; с новым паролем — успешен.
T-44 RELOAD уменьшает connections, пока старый вход удерживается барьером Новый лимит действует на новые входы; старый вход завершается по своему снимку. Временное превышение общего числа соединений допускается по F-LOAD-5.

9.8 Драйверы и диагностика

ID Сценарий Наблюдаемая проверка
T-18 libpq/psql, pgx, JDBC, psycopg: вход, неверный пароль, повторное использование backend Каждый драйвер выполняет SQL после LDAP-входа и получает ожидаемый отказ. Один подробный сценарий Simple/Extended protocol проверяет порядок сообщений, скрытые ответы хуков и повторный Bind подготовленного запроса после cleanup.
T-45 Разные секреты-маркеры в LDAP-пароле, ldapbindpasswd, server_password; успешные и неуспешные входы В логах PgDoorman и SHOW CONFIG секретов нет. Проверять отсутствие конкретных маркеров без печати их в диагностике теста.
T-46 Клиент перенаправлен в общую запись; успешный вход, отказ, попадание в кэш и ошибка setup SHOW CLIENTS показывает логин и цель, SHOW POOLS — целевой пул. Дельты метрик соответствуют событиям; причина отказа не теряется и не дублируется обёртками.

9.9 Выполнение

Все новые native LDAP-сценарии входят в существующую группу @ldap; дополнительный тег @ldap-native позволяет запускать только их. Базовый стенд и образ переиспользуются, PgDoorman собирается один раз на набор. Проверки драйверов вызываются из этого же набора без дублирования сценариев в нескольких CI-группах.

Текущий Nix-стенд содержит PostgreSQL 16. Для прямой сверки с референсом PostgreSQL 18 в T-1/T-23 нужен отдельный тестовый PostgreSQL 18 с LDAP-поддержкой; это зависимость плана, сейчас такого прогона нет. Входы по LDAP, LDAPS и StartTLS проверяются на настоящем LDAP. Вспомогательный управляемый сервер/посредник нужен только для воспроизводимых задержек, обрывов и ошибок.

Существующие наборы SCRAM/MD5, PAM, auth_query, cleanup и Greengage остаются регрессионными проверками. Для Greengage добавить узкий сценарий нового setup: установка личности, успешный NOTICE и повторное использование backend; всю LDAP-матрицу на Greengage не повторять.

10. Пример конфигурации

general:
  pg_hba:
    content: |
      hostssl all all 10.0.0.0/8 ldap ldapserver="dc1.example.org dc2.example.org" ldapscheme=ldaps ldapbasedn="ou=people,dc=example,dc=org" ldapbinddn="cn=pg-doorman,ou=services,dc=example,dc=org" ldapbindpasswd="<secret>" ldapsearchattribute=sAMAccountName

pools:
  reports:
    server_host: pg-bi.example.org
    server_database: analytics
    users:
      - username: alice              # без password: пароль проверяет каталог
        pool_size: 5                 # соединений с PostgreSQL для alice
      - username: manager            # без password; server_username по умолчанию = username
        server_password: "<pg password of manager>"
        pool_size: 20
        setup_server_query: "SELECT set_config('app.ldap_user', '{{username}}', false)"
      - username: analytics
        server_password: "<pg password of analytics>"
        pool_size: 20
    ldap:
      # timeout: 60s                 # бюджет на каждый LDAP-сервер
      connections: 2                 # постоянных соединений с каталогом
      # cache_ttl: 10m               # по умолчанию кэш успешных входов выключен
      mapping:                       # сверху вниз, первое совпадение
        - user: alice
        - group: "cn=manager,ou=groups,dc=example,dc=org"
          as_user: manager
        - group: "cn=analysts,ou=groups,dc=example,dc=org"
          as_user: analytics
Клиент Группы Правило Роль в PostgreSQL
alice manager 1 alice
bob manager, analysts 2 — первое совпадение manager, app.ldap_user = 'bob'
dave analysts 3 analytics
erin нет отказ 28000

Роли PostgreSQL:

CREATE ROLE manager   LOGIN PASSWORD '...';
CREATE ROLE analytics LOGIN PASSWORD '...';
CREATE ROLE alice LOGIN;

pg_hba.conf PostgreSQL для адреса PgDoorman:

host analytics manager,analytics 10.0.0.5/32 scram-sha-256
host analytics alice             10.0.0.5/32 trust

11. Вне области первой версии

Регулярные выражения в user с \1 в as_user; селектор по DN и поддереву; несколько групп в одном правиле; флаг case_insensitive; отдельный поиск групп для каталогов без memberOf; вложенные группы силами PgDoorman; динамические пулы для не перечисленных логинов; DNS SRV; пароль служебной учётки из файла; отдельный CA-файл каталога; ленивый режим хуков; setup_server_query у auth_query; SHOW LDAP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment