Skip to content

LDAP провайдер аутентификации

Описание

Этот провайдер аутентификации проверяет учетные данные пользователя в LDAP, а затем выдает токен доступа.

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

После успешной аутентификации имя пользователя сохраняется в базе данных бэкенда. Затем оно используется для создания записей аудита при любом изменении объекта, см. поле changed_by.

Внимание!

Пока токен действителен, запросы к LDAP для проверки существования пользователя и его блокировки не выполняются. Поэтому не устанавливайте слишком длительное время истечения срока действия токена доступа (например, больше дня).

Стратегии

Примечание

Базовая терминология LDAP объясняется здесь: Обзор LDAP

Существуют 2 стратегии проверки пользователя в LDAP:

  • Попытка вызвать запрос bind в LDAP с DN (DistinguishedName) и паролем пользователя. DN генерируется с использованием bind_dn_template
  • Сначала попытка поиска пользователя (запрос search) в LDAP для получения DN пользователя с помощью некоторого запроса, а затем попытка вызвать bind с использованием этого DN. См. настройки поиска

По умолчанию используется стратегия поиска, так как она может найти пользователя в сложной среде LDAP/ActiveDirectory. Например:

  • вы можете искать пользователя по uid, например, (uid={login}) или (sAMAccountName={login})
  • вы можете искать пользователя по нескольким атрибутам, например, (|(uid={login})(mail={login}@domain.com))
  • вы можете фильтровать записи, например, (&(uid={login})(objectClass=person)
  • вы можете фильтровать пользователей, соответствующих определенной группе или другому условию, например, (&(uid={login})(memberOf=cn=MyPrettyGroup,ou=Groups,dc=mycompany,dc=com))

После того как пользователь найден в LDAP, его uid_attribute используется для записей аудита.

Схема взаимодействия

sequenceDiagram
participant "Клиент"
participant "Бэкенд"
participant "LDAP"

activate "Клиент"
alt Успешный кейс
"Клиент" ->> "Бэкенд"  : логин + пароль
"Бэкенд" ->> "Бэкенд" : DN = bind_dn_template(login)
"Бэкенд" ->> "LDAP"  : Вызов bind(DN, password)
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда,\nusername = login
"Бэкенд" ->> "Бэкенд" : Создание пользователя, если не существует

else Неверные учетные данныве | Пользователь заблокирован в LDAP
"Клиент" ->> "Бэкенд"  : логин + пароль
"Бэкенд" ->> "Бэкенд" : DN = bind_dn_template(login)
"Бэкенд" ->> "LDAP"  : Вызов bind(DN, password)
"LDAP" --x "Бэкенд"  : Ошибка связываения
"Бэкенд" --x "Клиент"  : 401 Unauthorized

else Пользователь заблокирован во внутренней базе данных бэкенда
"Клиент" ->> "Бэкенд"  : логин + пароль
"Бэкенд" ->> "Бэкенд" : DN = bind_dn_template(login)
"Бэкенд" ->> "LDAP"  : Вызов bind(DN, password)
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда,\nusername = login
"Бэкенд" --x "Клиент"  : 404 Не найден

else Пользователь удален из  внутренней базы данных Бэкенда
"Клиент" ->> "Бэкенд"  : логин + пароль
"Бэкенд" ->> "Бэкенд" : DN = bind_dn_template(login)
"Бэкенд" ->> "LDAP"  : Вызов bind(DN, password)
"LDAP" ->> "Бэкенд"  : Возвращение информации по пользователю
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда,\nusername = login
"Бэкенд" --x "Клиент"  : 404 Не найден

else LDAP недоступен
"Клиент" ->> "Бэкенд"  : логин + пароль
"Бэкенд" ->> "Бэкенд" : DN = bind_dn_template(login)
"Бэкенд" --x "LDAP" : вызов bind(DN, password)
"Бэкенд" --x "Клиент" : 503 сервис недоступен
end

alt Успешный кейс
"Клиент" ->> "Бэкенд"  : токен доступа
"Бэкенд" ->> "Бэкенд" : Проверка токена
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда
"Бэкенд" ->> "Бэкенд" : Получение данных
"Бэкенд" ->> "Клиент"  : Возвращение данных

else Токен истек
"Клиент" ->> "Бэкенд"  : токен доступа
"Бэкенд" ->> "Бэкенд" : Проверка токена
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда
"Бэкенд" --x "Клиент"  : 401 Неавторизован

else Пользователь заблокирован
"Клиент" ->> "Бэкенд"  : токен доступа
"Бэкенд" ->> "Бэкенд" : Проверка токена
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда
"Бэкенд" --x "Клиент"  : 401 неавторизован

else
"Клиент" ->> "Бэкенд"  : токен доступа
"Бэкенд" ->> "Бэкенд" : Проверка токена
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда
"Бэкенд" --x "Клиент"  : 401 неавторизован
end

deactivate "Клиент"
sequenceDiagram
participant "Клиент"
participant "Бэкенд"
participant "LDAP"

"Бэкенд" -> "LDAP"  : bind(lookup.username, lookup.password)
activate "LDAP"
Note right of "LDAP" : Открытие пула\nсоединений для\nпоисковых запросов\n(опционально, рекомендуется)

activate "Клиент"
alt Успешный кейс
"Клиент" ->> "Бэкенд"  : логин + пароль
"Бэкенд" ->> "Бэкенд" : query = query_template(login)
"Бэкенд" ->> "LDAP" : Вызов search(query, base_dn, attributes=*)
activate "LDAP"
"LDAP" ->> "Бэкенд" : Возвращение пользовательского DN and uid атрибутов
deactivate "LDAP"
"Бэкенд" ->> "LDAP"  : Вызов bind(DN, password)
"LDAP" ->> "Бэкенд"  : Успешно
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда,\nusername = uid_attribute из ответа LDAP
"Бэкенд" ->> "Бэкенд" : Создание пользователя, если не существует
"Бэкенд" ->> "Клиент"  : Генерация и возвращение токена доступа

else Неверные учетные данныве | Пользователь заблокирован в LDAP
"Клиент" ->> "Бэкенд"  : логин + пароль
"Бэкенд" ->> "Бэкенд" : query = query_template(login)
"Бэкенд" ->> "LDAP" : Вызов search(query, base_dn, attributes=*)
activate "LDAP"
"LDAP" ->> "Бэкенд" : Возвращение пользовательского DN and uid атрибутов
deactivate "LDAP"
"Бэкенд" ->> "LDAP"  : Вызов bind(DN, password)
"LDAP" --x "Бэкенд"  : Ошибка связывания
"Бэкенд" --x "Клиент"  : 401 неавторизован

else Пользователь заблокирован во внутренней базе данных бэкенда
"Клиент" ->> "Бэкенд"  : логин + пароль
"Бэкенд" ->> "Бэкенд" : query = query_template(login)
"Бэкенд" ->> "LDAP" : Вызов search(query, base_dn, attributes=*)
activate "LDAP"
"LDAP" ->> "Бэкенд" : Возвращение пользовательского DN and uid атрибутов
deactivate "LDAP"
"Бэкенд" ->> "LDAP"  : Вызов bind(DN, password)
"LDAP" ->> "Бэкенд"  : Успешно
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда,\nusername = uid_attribute из ответа LDAP
"Бэкенд" --x "Клиент"  : 404 не найден

else Пользователь удален из  внутренней базы данных Бэкенда
"Клиент" ->> "Бэкенд"  : логин + пароль
"Бэкенд" ->> "Бэкенд" : query = query_template(login)
"Бэкенд" ->> "LDAP" : Вызов search(query, base_dn, attributes=*)
activate "LDAP"
"LDAP" ->> "Бэкенд" : Return user DN and uid_attribute
deactivate "LDAP"
"Бэкенд" ->> "LDAP"  : Вызов bind(DN, password)
"LDAP" ->> "Бэкенд"  : Успешно
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда,\nusername = uid_attribute из ответа LDAP
"Бэкенд" --x "Клиент"  : 404 не найден

else LDAP недоступен
"Клиент" ->> "Бэкенд"  : логин + пароль
"Бэкенд" ->> "Бэкенд" : query = query_template(login)
"Бэкенд" --x "LDAP" : Вызов search(query, base_dn, attributes=*)
"Бэкенд" --x "Клиент" : 503 Сервис недоступен
end

alt Успешный кейс
"Клиент" ->> "Бэкенд"  : токен доступа
"Бэкенд" ->> "Бэкенд" : Проверка токена
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда
"Бэкенд" ->> "Бэкенд" : Получение данных
"Бэкенд" ->> "Клиент"  : Возвращение данных

else Токен просрочен
"Клиент" ->> "Бэкенд"  : токен доступа
"Бэкенд" ->> "Бэкенд" : Проверка токена
"Бэкенд" --x "Клиент"  : 401 неавторизован

else Пользователь заблокирован
"Клиент" ->> "Бэкенд"  : токен доступа
"Бэкенд" ->> "Бэкенд" : Проверка токена
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда
"Бэкенд" --x "Клиент"  : 401 неавторизован

else Пользователь удален
"Клиент" ->> "Бэкенд"  : токен доступа
"Бэкенд" ->> "Бэкенд" : Проверка токена
"Бэкенд" ->> "Бэкенд" : Проверка пользователя во внутренней базе данных бэкенда
"Бэкенд" --x "Клиент"  : 404 не найден
end

deactivate "LDAP"
deactivate "Клиент"

Базовая конфигурация

Bases: BaseModel

Settings for LDAPAuthProvider.

Examples

.. code-block:: bash

HORIZON__AUTH__PROVIDER=horizon.backend.providers.auth.ldap.LDAPAuthProvider
HORIZON__AUTH__ACCESS_KEY__SECRET_KEY=secret
HORIZON__AUTH__LDAP__URL=ldap://ldap.domain.com:389
HORIZON__AUTH__LDAP__LOOKUP__ENABLED=True
HORIZON__AUTH__LDAP__LOOKUP__POOL__ENABLED=True
HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__USER=uid=techuser,ou=users,dc=example,dc=com
HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__PASSWORD=somepassword
Source code in horizon/backend/settings/auth/ldap.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
class LDAPAuthProviderSettings(BaseModel):
    """Settings for LDAPAuthProvider.

    Examples
    --------

    .. code-block:: bash

        HORIZON__AUTH__PROVIDER=horizon.backend.providers.auth.ldap.LDAPAuthProvider
        HORIZON__AUTH__ACCESS_KEY__SECRET_KEY=secret
        HORIZON__AUTH__LDAP__URL=ldap://ldap.domain.com:389
        HORIZON__AUTH__LDAP__LOOKUP__ENABLED=True
        HORIZON__AUTH__LDAP__LOOKUP__POOL__ENABLED=True
        HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__USER=uid=techuser,ou=users,dc=example,dc=com
        HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__PASSWORD=somepassword
    """

    access_token: JWTSettings = Field(description="Access-token related settings")
    ldap: LDAPSettings = Field(description="LDAP related settings")

Bases: BaseModel

Settings related to LDAP interaction.

Examples

.. code-block:: bash

HORIZON__AUTH__LDAP__URL=ldap://ldap.domain.com:389
HORIZON__AUTH__LDAP__UID_ATTRIBUTE=sAMAccountName
Source code in horizon/backend/settings/auth/ldap.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
class LDAPSettings(BaseModel):
    """Settings related to LDAP interaction.

    Examples
    --------

    .. code-block:: bash

        HORIZON__AUTH__LDAP__URL=ldap://ldap.domain.com:389
        HORIZON__AUTH__LDAP__UID_ATTRIBUTE=sAMAccountName
    """

    url: LDAPUrl = Field(
        description="LDAP URL to connect to",
    )
    timeout_seconds: Optional[int] = Field(
        default=10,
        description="LDAP request timeout, in seconds. ``None`` means no timeout",
    )
    auth_mechanism: Literal["SIMPLE", "DIGEST-MD5"] = Field(
        default="SIMPLE",
        description="LDAP auth mechanism, used for ``bind`` request",
    )
    base_dn: str = Field(
        description="Organization DN, e.g. ``ou=users,dc=example,dc=com``",
    )
    uid_attribute: str = Field(
        default="uid",
        description=textwrap.dedent(
            """
            Attribute containing username.

            Usually ``uid`` (LDAP) or ``sAMAccountName`` (ActiveDirectory).
            """,
        ),
    )
    bind_dn_template: str = Field(
        default="{uid_attribute}={login},{base_dn}",
        description=textwrap.dedent(
            """
            Template for building DN string, used for checking credentials in LDAP.
            You can pass any DN value supported by LDAP.

            Supported substitution values:
              * ``{login}``
              * ``{uid_attribute}`` (see :obj:`~uid_attribute`)
              * ``{base_dn}`` (see :obj:`~base_dn`)
            """,
        ),
    )

    lookup: LDAPLookupSettings = Field(
        default_factory=LDAPLookupSettings,
        description="LDAP search options",
    )

Bases: BaseModel

Settings related to JWT tokens.

Examples

.. code-block:: bash

HORIZON__AUTH__ACCESS_KEY__SECRET_KEY=somesecret
HORIZON__AUTH__ACCESS_KEY__EXPIRE_SECONDS=3600  # 1 hour
Source code in horizon/backend/settings/auth/jwt.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class JWTSettings(BaseModel):
    """Settings related to JWT tokens.

    Examples
    --------

    .. code-block:: bash

        HORIZON__AUTH__ACCESS_KEY__SECRET_KEY=somesecret
        HORIZON__AUTH__ACCESS_KEY__EXPIRE_SECONDS=3600  # 1 hour
    """

    secret_key: SecretStr = Field(
        description=textwrap.dedent(
            """
            Secret key for signing JWT tokens.

            Can be any string. It is recommended to generate random value for every application instance.
            """,
        ),
    )
    security_algorithm: str = Field(
        default="HS256",
        description=textwrap.dedent(
            """
            Algorithm used for signing JWT tokens.

            See `authlib <https://docs.authlib.org/en/latest/specs/rfc7518.html>`_
            documentation.
            """,
        ),
    )
    expire_seconds: int = Field(
        default=10 * 60 * 60,
        description="Token expiration time, in seconds",
    )

Bases: BaseModel

Settings related to LDAP connection pool.

Examples

.. code-block:: bash

HORIZON__AUTH__LDAP__LOOKUP__POOL__ENABLED=True
HORIZON__AUTH__LDAP__LOOKUP__POOL__MAX=10
HORIZON__AUTH__LDAP__LOOKUP__POOL__CHECK_ON_STARTUP=True
Source code in horizon/backend/settings/auth/ldap.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class LDAPConnectionPoolSettings(BaseModel):
    """Settings related to LDAP connection pool.

    Examples
    --------

    .. code-block:: bash

        HORIZON__AUTH__LDAP__LOOKUP__POOL__ENABLED=True
        HORIZON__AUTH__LDAP__LOOKUP__POOL__MAX=10
        HORIZON__AUTH__LDAP__LOOKUP__POOL__CHECK_ON_STARTUP=True
    """

    enabled: bool = Field(
        default=True,
        description="Set to ``True`` to enable connection pool",
    )
    initial: int = Field(
        default=1,
        description="Initial size of connection pool",
    )
    max: int = Field(
        default=10,
        description="Maximum size of connection pool",
    )

Bases: BaseModel

Settings related to LDAP lookup.

Examples

.. code-block:: bash

HORIZON__AUTH__LDAP__LOOKUP__ENABLED=True
HORIZON__AUTH__LDAP__LOOKUP__POOL__ENABLED=True
HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__USER=uid=techuser,ou=users,dc=example,dc=com
HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__PASSWORD=somepassword
HORIZON__AUTH__LDAP__LOOKUP__QUERY=(uid={login})
Source code in horizon/backend/settings/auth/ldap.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
class LDAPLookupSettings(BaseModel):
    """Settings related to LDAP lookup.

    Examples
    --------

    .. code-block:: bash

        HORIZON__AUTH__LDAP__LOOKUP__ENABLED=True
        HORIZON__AUTH__LDAP__LOOKUP__POOL__ENABLED=True
        HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__USER=uid=techuser,ou=users,dc=example,dc=com
        HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__PASSWORD=somepassword
        HORIZON__AUTH__LDAP__LOOKUP__QUERY=(uid={login})
    """

    enabled: bool = Field(
        default=True,
        description="Set to ``True`` to enable lookup",
    )
    check_on_startup: bool = Field(
        default=True,
        description="If ``True``, and LDAP is not available during application start, abort application startup",
    )
    pool: LDAPConnectionPoolSettings = Field(
        default_factory=LDAPConnectionPoolSettings,
        description="LDAP connection pool settings",
    )
    credentials: Optional[LDAPCredentials] = Field(
        default=None,
        description="Credentials used for connecting to LDAP while performing user lookup",
    )
    query_template: str = Field(
        default="({uid_attribute}={login})",
        description=textwrap.dedent(
            """
            LDAP query send in lookup request.

            Usually lookup is performed against attributes ``uid`` (LDAP) or ``sAMAccountName`` (ActiveDirectory).
            You can also pass any query string supported by LDAP.
            See `Bonsai documentation <https://bonsai.readthedocs.io/en/latest/tutorial.html#searching>`_.

            Supported substitution values (see :obj:`horizon.backend.settings.auth.ldap.LDAPSettings`.):
              * ``{uid_attribute}``
              * ``{login}``
            """,
        ),
    )
    scope: LDAPSearchScope = Field(
        default=LDAPSearchScope.ONELEVEL,
        description=textwrap.dedent(
            """
            Lookup scope. Use ``SUBTREE`` for ActiveDirectory.

            See `Bonsai documentation <https://bonsai.readthedocs.io/en/latest/api.html#bonsai.LDAPSearchScope.ONE>`_.
            """,
        ),
    )

    @validator("scope", pre=True)
    def _convert_scope_to_enum(cls, value: Union[str, int, LDAPSearchScope]) -> LDAPSearchScope:  # noqa: N805
        if isinstance(value, str):
            return LDAPSearchScope[value.upper()]
        return LDAPSearchScope(value)

Bases: BaseModel

LDAP lookup query is executed using this credentials (instead of login and password provided by user).

Examples

.. code-block:: bash

HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__USER=uid=techuser,ou=users,dc=example,dc=com
HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__PASSWORD=somepassword
Source code in horizon/backend/settings/auth/ldap.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class LDAPCredentials(BaseModel):
    """LDAP lookup query is executed using this credentials
    (instead of login and password provided by user).

    Examples
    --------

    .. code-block:: bash

        HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__USER=uid=techuser,ou=users,dc=example,dc=com
        HORIZON__AUTH__LDAP__LOOKUP__CREDENTIALS__PASSWORD=somepassword
    """

    user: str = Field(
        description="DN of user which is used for calling ``lookup`` query in LDAP",
    )
    password: SecretStr = Field(
        description="This user password",
    )