ldap-ui 0.9.5__py3-none-any.whl → 0.9.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ldap_ui/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.9.5"
1
+ __version__ = "0.9.7"
ldap_ui/__main__.py CHANGED
@@ -23,7 +23,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
23
23
  "--base-dn",
24
24
  type=str,
25
25
  default=settings.BASE_DN,
26
- help="LDAP base DN. Required unless the BASE_DN environment variable is set.",
26
+ help="LDAP base DN (required). [default: BASE_DN environment variable]",
27
27
  )
28
28
  @click.option(
29
29
  "-h",
@@ -41,6 +41,12 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
41
41
  help="Bind socket to this port. If 0, an available port will be picked.",
42
42
  show_default=True,
43
43
  )
44
+ @click.option(
45
+ "-u",
46
+ "--ldap-url",
47
+ type=str,
48
+ help="LDAP directory connection URL. [default: LDAP_URL environment variable or 'ldap:///']",
49
+ )
44
50
  @click.option(
45
51
  "-l",
46
52
  "--log-level",
@@ -57,7 +63,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
57
63
  is_eager=True,
58
64
  help="Display the current version and exit.",
59
65
  )
60
- def main(base_dn, host, port, log_level):
66
+ def main(base_dn, host, port, ldap_url, log_level):
61
67
  logging.basicConfig(level=LOG_LEVELS[log_level])
62
68
  rootHandler = logging.getLogger().handlers[0]
63
69
  rootHandler.setFormatter(ColourizedFormatter(fmt="%(levelprefix)s %(message)s"))
@@ -65,9 +71,11 @@ def main(base_dn, host, port, log_level):
65
71
  if base_dn is not None:
66
72
  settings.BASE_DN = base_dn
67
73
 
74
+ if ldap_url is not None:
75
+ settings.LDAP_URL = ldap_url
76
+
68
77
  uvicorn.run(
69
78
  "ldap_ui.app:app",
70
- log_level=logging.INFO,
71
79
  host=host,
72
80
  port=port,
73
81
  )
ldap_ui/app.py CHANGED
@@ -10,13 +10,12 @@ No sessions, no cookies, nothing else.
10
10
 
11
11
  import base64
12
12
  import binascii
13
- import contextlib
14
13
  import logging
15
14
  import sys
15
+ from http import HTTPStatus
16
16
  from typing import AsyncGenerator
17
17
 
18
18
  import ldap
19
- from ldap.schema import SubSchema
20
19
  from pydantic import ValidationError
21
20
  from starlette.applications import Starlette
22
21
  from starlette.authentication import (
@@ -37,7 +36,7 @@ from starlette.staticfiles import StaticFiles
37
36
 
38
37
  from . import settings
39
38
  from .ldap_api import api
40
- from .ldap_helpers import WITH_OPERATIONAL_ATTRS, empty, ldap_connect, unique
39
+ from .ldap_helpers import empty, ldap_connect, unique
41
40
 
42
41
  LOG = logging.getLogger("ldap-ui")
43
42
 
@@ -49,8 +48,8 @@ LOG.debug("Base DN: %s", settings.BASE_DN)
49
48
 
50
49
  # Force authentication
51
50
  UNAUTHORIZED = Response(
52
- "Invalid credentials",
53
- status_code=401,
51
+ HTTPStatus.UNAUTHORIZED.phrase,
52
+ status_code=HTTPStatus.UNAUTHORIZED.value,
54
53
  headers={"WWW-Authenticate": 'Basic realm="Please log in", charset="UTF-8"'},
55
54
  )
56
55
 
@@ -71,15 +70,20 @@ class LdapConnectionMiddleware(BaseHTTPMiddleware):
71
70
 
72
71
  # Search for basic auth user
73
72
  if type(request.user) is LdapUser:
74
- dn, _attrs = await unique(
75
- connection,
76
- connection.search(
77
- settings.BASE_DN,
78
- ldap.SCOPE_SUBTREE,
79
- settings.GET_BIND_DN_FILTER(request.user.username),
80
- ),
81
- )
82
73
  password = request.user.password
74
+ dn = settings.GET_BIND_PATTERN(request.user.username)
75
+ if dn is None:
76
+ try:
77
+ dn, _attrs = await unique(
78
+ connection,
79
+ connection.search(
80
+ settings.BASE_DN,
81
+ ldap.SCOPE_SUBTREE,
82
+ settings.GET_BIND_DN_FILTER(request.user.username),
83
+ ),
84
+ )
85
+ except HTTPException:
86
+ pass
83
87
 
84
88
  # Hard-wired credentials
85
89
  if dn is None:
@@ -99,10 +103,10 @@ class LdapConnectionMiddleware(BaseHTTPMiddleware):
99
103
 
100
104
  except ldap.LDAPError as err:
101
105
  msg = ldap_exception_message(err)
102
- LOG.error(msg)
106
+ LOG.error(msg, exc_info=err)
103
107
  return PlainTextResponse(
104
108
  msg,
105
- status_code=500,
109
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,
106
110
  )
107
111
 
108
112
 
@@ -172,29 +176,16 @@ async def http_exception(_request: Request, exc: HTTPException) -> Response:
172
176
 
173
177
  async def forbidden(_request: Request, exc: ldap.LDAPError) -> Response:
174
178
  "HTTP 403 Forbidden"
175
- return PlainTextResponse(ldap_exception_message(exc), status_code=403)
179
+ return PlainTextResponse(
180
+ ldap_exception_message(exc),
181
+ status_code=HTTPStatus.FORBIDDEN.value,
182
+ )
176
183
 
177
184
 
178
185
  async def http_422(_request: Request, e: ValidationError) -> Response:
179
186
  "HTTP 422 Unprocessable Entity"
180
187
  LOG.warn("Invalid request body", exc_info=e)
181
- return Response(repr(e), status_code=422)
182
-
183
-
184
- @contextlib.asynccontextmanager
185
- async def lifespan(app):
186
- with ldap_connect() as connection:
187
- # See: https://hub.packtpub.com/python-ldap-applications-part-4-ldap-schema/
188
- _dn, sub_schema = await unique(
189
- connection,
190
- connection.search(
191
- settings.SCHEMA_DN,
192
- ldap.SCOPE_BASE,
193
- attrlist=WITH_OPERATIONAL_ATTRS,
194
- ),
195
- )
196
- app.state.schema = SubSchema(sub_schema, check_uniqueness=2)
197
- yield
188
+ return Response(repr(e), status_code=HTTPStatus.UNPROCESSABLE_ENTITY.value)
198
189
 
199
190
 
200
191
  # Main ASGI entry
@@ -205,7 +196,6 @@ app = Starlette(
205
196
  ldap.INSUFFICIENT_ACCESS: forbidden,
206
197
  ValidationError: http_422,
207
198
  },
208
- lifespan=lifespan,
209
199
  middleware=(
210
200
  Middleware(AuthenticationMiddleware, backend=BasicAuthBackend()),
211
201
  Middleware(LdapConnectionMiddleware),
@@ -213,7 +203,7 @@ app = Starlette(
213
203
  Middleware(GZipMiddleware, minimum_size=512, compresslevel=6),
214
204
  ),
215
205
  routes=[
216
- Mount("/api", routes=api.routes),
206
+ Mount("/api", app=api),
217
207
  Mount("/", StaticFiles(packages=["ldap_ui"], html=True)),
218
208
  ],
219
209
  )
ldap_ui/ldap_api.py CHANGED
@@ -2,13 +2,14 @@
2
2
  ReST endpoints for LDAP access.
3
3
 
4
4
  Directory operations are accessible to the frontend
5
- through a hand-knit API, responses are usually converted to JSON.
5
+ through a hand-knit ReST API, responses are usually converted to JSON.
6
6
 
7
7
  Asynchronous LDAP operations are used as much as possible.
8
8
  """
9
9
 
10
10
  import base64
11
11
  import io
12
+ from http import HTTPStatus
12
13
  from typing import Any, Optional, Tuple, Union
13
14
 
14
15
  import ldap
@@ -31,13 +32,15 @@ from .ldap_helpers import (
31
32
  get_entry_by_dn,
32
33
  ldap_connect,
33
34
  result,
35
+ unique,
34
36
  )
37
+ from .schema import ObjectClass as OC
35
38
  from .schema import frontend_schema
36
39
 
37
40
  __all__ = ("api",)
38
41
 
39
42
 
40
- NO_CONTENT = Response(status_code=204)
43
+ NO_CONTENT = Response(status_code=HTTPStatus.NO_CONTENT.value)
41
44
 
42
45
  # Special fields
43
46
  PHOTOS = ("jpegPhoto", "thumbnailPhoto")
@@ -87,7 +90,20 @@ async def _tree(request: Request, basedn: str, scope: int) -> list[dict[str, Any
87
90
  ]
88
91
 
89
92
 
90
- def _entry(schema: SubSchema, res: Tuple[str, Any]) -> dict[str, Any]:
93
+ class Meta(BaseModel):
94
+ dn: str
95
+ required: list[str]
96
+ aux: list[str]
97
+ binary: list[str]
98
+ autoFilled: list[str]
99
+
100
+
101
+ class Entry(BaseModel):
102
+ attrs: dict[str, list[str]]
103
+ meta: Meta
104
+
105
+
106
+ def _entry(schema: SubSchema, res: Tuple[str, Any]) -> Entry:
91
107
  "Prepare an LDAP entry for transmission"
92
108
 
93
109
  dn, attrs = res
@@ -96,7 +112,7 @@ def _entry(schema: SubSchema, res: Tuple[str, Any]) -> dict[str, Any]:
96
112
  soc = [
97
113
  oc.names[0]
98
114
  for oc in map(lambda o: schema.get_obj(ObjectClass, o), ocs)
99
- if oc.kind == 0
115
+ if oc.kind == OC.Kind.structural.value
100
116
  ]
101
117
  aux = set(
102
118
  schema.get_obj(ObjectClass, a).names[0]
@@ -126,26 +142,22 @@ def _entry(schema: SubSchema, res: Tuple[str, Any]) -> dict[str, Any]:
126
142
  if syntax.not_human_readable:
127
143
  binary.add(attr)
128
144
 
129
- return {
130
- "attrs": {
131
- k: [
132
- base64.b64encode(val).decode() if k in binary else val.decode()
133
- for val in values
134
- ]
145
+ return Entry(
146
+ attrs={
147
+ k: [base64.b64encode(val) if k in binary else val for val in values]
135
148
  for k, values in attrs.items()
136
149
  },
137
- "meta": {
138
- "dn": dn,
139
- "required": [schema.get_obj(AttributeType, a).names[0] for a in must_attrs],
140
- "aux": sorted(aux - ocs),
141
- "binary": sorted(binary),
142
- "hints": {}, # FIXME obsolete?
143
- "autoFilled": [],
144
- },
145
- }
150
+ meta=Meta(
151
+ dn=dn,
152
+ required=[schema.get_obj(AttributeType, a).names[0] for a in must_attrs],
153
+ aux=sorted(aux - ocs),
154
+ binary=sorted(binary),
155
+ autoFilled=[],
156
+ ),
157
+ )
146
158
 
147
159
 
148
- Entry = TypeAdapter(dict[str, list[bytes]])
160
+ Attributes = TypeAdapter(dict[str, list[bytes]])
149
161
 
150
162
 
151
163
  @api.route("/entry/{dn}", methods=("GET", "POST", "DELETE", "PUT"))
@@ -157,7 +169,9 @@ async def entry(request: Request) -> Response:
157
169
 
158
170
  if request.method == "GET":
159
171
  return JSONResponse(
160
- _entry(request.app.state.schema, await get_entry_by_dn(connection, dn))
172
+ _entry(
173
+ request.app.state.schema, await get_entry_by_dn(connection, dn)
174
+ ).model_dump()
161
175
  )
162
176
 
163
177
  if request.method == "DELETE":
@@ -168,7 +182,7 @@ async def entry(request: Request) -> Response:
168
182
  return NO_CONTENT
169
183
 
170
184
  # Copy JSON payload into a dictionary of non-empty byte strings
171
- json = Entry.validate_json(await request.body())
185
+ json = Attributes.validate_json(await request.body())
172
186
  req = {
173
187
  k: [s for s in filter(None, v)]
174
188
  for k, v in json.items()
@@ -206,7 +220,9 @@ async def blob(request: Request) -> Response:
206
220
 
207
221
  if request.method == "GET":
208
222
  if attr not in attrs or len(attrs[attr]) <= index:
209
- raise HTTPException(404, f"Attribute {attr} not found for DN {dn}")
223
+ raise HTTPException(
224
+ HTTPStatus.NOT_FOUND.value, f"Attribute {attr} not found for DN {dn}"
225
+ )
210
226
 
211
227
  return Response(
212
228
  attrs[attr][index],
@@ -234,7 +250,9 @@ async def blob(request: Request) -> Response:
234
250
 
235
251
  if request.method == "DELETE":
236
252
  if attr not in attrs or len(attrs[attr]) <= index:
237
- raise HTTPException(404, f"Attribute {attr} not found for DN {dn}")
253
+ raise HTTPException(
254
+ HTTPStatus.NOT_FOUND.value, f"Attribute {attr} not found for DN {dn}"
255
+ )
238
256
  await empty(connection, connection.modify(dn, [(1, attr, None)]))
239
257
  data = attrs[attr][:index] + attrs[attr][index + 1 :]
240
258
  if data:
@@ -420,7 +438,9 @@ async def attribute_range(request: Request) -> JSONResponse:
420
438
  )
421
439
 
422
440
  if not values:
423
- raise HTTPException(404, f"No values found for attribute {attribute}")
441
+ raise HTTPException(
442
+ HTTPStatus.NOT_FOUND.value, f"No values found for attribute {attribute}"
443
+ )
424
444
 
425
445
  minimum, maximum = min(values), max(values)
426
446
  return JSONResponse(
@@ -435,4 +455,18 @@ async def attribute_range(request: Request) -> JSONResponse:
435
455
  @api.route("/schema")
436
456
  async def json_schema(request: Request) -> JSONResponse:
437
457
  "Dump the LDAP schema as JSON"
438
- return JSONResponse(frontend_schema(request.app.state.schema))
458
+ if getattr(request.app.state, "schema", None) is None:
459
+ connection = request.state.ldap
460
+ # See: https://hub.packtpub.com/python-ldap-applications-part-4-ldap-schema/
461
+ _dn, sub_schema = await unique(
462
+ connection,
463
+ connection.search(
464
+ settings.SCHEMA_DN,
465
+ ldap.SCOPE_BASE,
466
+ attrlist=WITH_OPERATIONAL_ATTRS,
467
+ ),
468
+ )
469
+ request.app.state.schema = SubSchema(sub_schema, check_uniqueness=2)
470
+
471
+ schema = frontend_schema(request.app.state.schema)
472
+ return JSONResponse(schema.model_dump())
ldap_ui/ldap_helpers.py CHANGED
@@ -11,6 +11,7 @@ operation to complete without results.
11
11
  """
12
12
 
13
13
  import contextlib
14
+ from http import HTTPStatus
14
15
  from typing import AsyncGenerator, Generator, Tuple
15
16
 
16
17
  import ldap
@@ -83,9 +84,12 @@ async def unique(
83
84
  res = r
84
85
  else:
85
86
  connection.abandon(msgid)
86
- raise HTTPException(500, "Non-unique result")
87
+ raise HTTPException(
88
+ HTTPStatus.INTERNAL_SERVER_ERROR.value,
89
+ "Non-unique result",
90
+ )
87
91
  if res is None:
88
- raise HTTPException(404, "Empty search result")
92
+ raise HTTPException(HTTPStatus.NOT_FOUND.value, "Empty search result")
89
93
  return res
90
94
 
91
95
 
@@ -97,7 +101,7 @@ async def empty(
97
101
 
98
102
  async for r in result(connection, msgid):
99
103
  connection.abandon(msgid)
100
- raise HTTPException(500, "Unexpected result")
104
+ raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR.value, "Unexpected result")
101
105
 
102
106
 
103
107
  async def get_entry_by_dn(
@@ -109,4 +113,4 @@ async def get_entry_by_dn(
109
113
  try:
110
114
  return await unique(connection, connection.search(dn, ldap.SCOPE_BASE))
111
115
  except ldap.NO_SUCH_OBJECT:
112
- raise HTTPException(404, f"DN not found: {dn}")
116
+ raise HTTPException(HTTPStatus.NOT_FOUND.value, f"DN not found: {dn}")
ldap_ui/schema.py CHANGED
@@ -6,125 +6,155 @@ to determine how individual attributes should be presented
6
6
  to the user.
7
7
  """
8
8
 
9
- from typing import Any, Generator
9
+ from enum import IntEnum
10
+ from typing import Generator, Optional, Type, TypeVar, Union
10
11
 
11
12
  from ldap.schema import SubSchema
12
- from ldap.schema.models import AttributeType, LDAPSyntax, ObjectClass
13
+ from ldap.schema.models import AttributeType, SchemaElement
14
+ from ldap.schema.models import LDAPSyntax as LDAPSyntaxType
15
+ from ldap.schema.models import ObjectClass as ObjectClassType
16
+ from pydantic import BaseModel, Field, field_serializer
13
17
 
14
- __all__ = ("frontend_schema",)
18
+ __all__ = ("frontend_schema", "Attribute", "ObjectClass")
15
19
 
20
+ T = TypeVar("T", bound=SchemaElement)
16
21
 
17
- # Object class constants
18
- SCHEMA_OC_KIND = {
19
- 0: "structural",
20
- 1: "abstract",
21
- 2: "auxiliary",
22
- }
23
22
 
24
- # Attribute usage constants
25
- SCHEMA_ATTR_USAGE = {
26
- 0: "userApplications",
27
- 1: "directoryOperation",
28
- 2: "distributedOperation",
29
- 3: "dSAOperation",
30
- }
23
+ class Element(BaseModel):
24
+ "Common attributes od schema elements"
31
25
 
26
+ oid: str
27
+ name: str
28
+ names: list[str] = Field(min_length=1)
29
+ desc: Optional[str]
30
+ obsolete: bool
31
+ sup: list[str] # TODO check
32
32
 
33
- def element(obj) -> dict:
34
- "Basic information about an schema element"
33
+
34
+ def element(obj: Union[AttributeType, ObjectClassType]) -> Element:
35
35
  name = obj.names[0]
36
- return {
37
- "oid": obj.oid,
38
- "name": name[:1].lower() + name[1:],
39
- "names": obj.names,
40
- "desc": obj.desc,
41
- "obsolete": bool(obj.obsolete),
42
- "sup": sorted(obj.sup),
43
- }
44
-
45
-
46
- def object_class_dict(obj) -> dict:
47
- "Additional information about an object class"
48
- r = element(obj)
49
- r.update(
50
- {
51
- "may": sorted(obj.may),
52
- "must": sorted(obj.must),
53
- "kind": SCHEMA_OC_KIND[obj.kind],
54
- }
55
- )
56
- return r
57
-
58
-
59
- def attribute_dict(obj) -> dict:
60
- "Additional information about an attribute"
61
- r = element(obj)
62
- r.update(
63
- {
64
- "single_value": bool(obj.single_value),
65
- "no_user_mod": bool(obj.no_user_mod),
66
- "usage": SCHEMA_ATTR_USAGE[obj.usage],
67
- # FIXME avoid null values below
68
- "equality": obj.equality,
69
- "syntax": obj.syntax,
70
- "substr": obj.substr,
71
- "ordering": obj.ordering,
72
- }
36
+ return Element(
37
+ oid=obj.oid,
38
+ name=name[:1].lower() + name[1:],
39
+ names=obj.names,
40
+ desc=obj.desc,
41
+ obsolete=bool(obj.obsolete),
42
+ sup=sorted(obj.sup),
73
43
  )
74
- return r
75
44
 
76
45
 
77
- def syntax_dict(obj) -> dict:
78
- "Information about an attribute syntax"
79
- return {
80
- "oid": obj.oid,
81
- "desc": obj.desc,
82
- "not_human_readable": bool(obj.not_human_readable),
83
- }
46
+ class ObjectClass(Element):
47
+ class Kind(IntEnum):
48
+ structural = 0
49
+ abstract = 1
50
+ auxiliary = 2
51
+
52
+ may: list[str]
53
+ must: list[str]
54
+ kind: Kind
55
+
56
+ @field_serializer("kind")
57
+ def serialize_kind(self, kind: Kind, _info) -> str:
58
+ return kind.name
59
+
60
+
61
+ class Attribute(Element):
62
+ class Usage(IntEnum):
63
+ userApplications = 0
64
+ directoryOperation = 1
65
+ distributedOperation = 2
66
+ dSAOperation = 3
84
67
 
68
+ single_value: bool
69
+ no_user_mod: bool
70
+ usage: Usage
71
+ equality: Optional[str]
72
+ syntax: Optional[str]
73
+ substr: Optional[str]
74
+ ordering: Optional[str]
85
75
 
86
- def lowercase_dict(attr: str, items) -> dict:
76
+ @field_serializer("usage")
77
+ def serialize_kind(self, usage: Usage, _info) -> str:
78
+ return usage.name
79
+
80
+
81
+ class Syntax(BaseModel):
82
+ oid: str
83
+ desc: str
84
+ not_human_readable: bool
85
+
86
+
87
+ def lowercase_dict(attr: str, items: list[T]) -> dict[str, T]:
87
88
  "Create an dictionary with lowercased keys extracted from a given attribute"
88
- return {obj[attr].lower(): obj for obj in items}
89
+ return {getattr(obj, attr).lower(): obj for obj in items}
89
90
 
90
91
 
91
92
  def extract_type(
92
- sub_schema: SubSchema, schema_class: Any
93
- ) -> Generator[Any, None, None]:
93
+ sub_schema: SubSchema, schema_class: Type[T]
94
+ ) -> Generator[T, None, None]:
94
95
  "Get non-obsolete objects from the schema for a type"
95
96
 
96
97
  for oid in sub_schema.listall(schema_class):
97
98
  obj = sub_schema.get_obj(schema_class, oid)
98
- if schema_class is LDAPSyntax or not obj.obsolete:
99
+ if schema_class is LDAPSyntaxType or not obj.obsolete:
99
100
  yield obj
100
101
 
101
102
 
103
+ class Schema(BaseModel):
104
+ attributes: dict[str, Attribute]
105
+ objectClasses: dict[str, ObjectClass]
106
+ syntaxes: dict[str, Syntax]
107
+
108
+
102
109
  # See: https://www.python-ldap.org/en/latest/reference/ldap-schema.html
103
- def frontend_schema(sub_schema: SubSchema) -> dict[Any]:
110
+ def frontend_schema(sub_schema: SubSchema) -> Schema:
104
111
  "Dump an LDAP SubSchema"
105
112
 
106
- return dict(
113
+ return Schema(
107
114
  attributes=lowercase_dict(
108
115
  "name",
109
116
  sorted(
110
- map(
111
- attribute_dict,
112
- extract_type(sub_schema, AttributeType),
117
+ (
118
+ Attribute(
119
+ single_value=bool(attr.single_value),
120
+ no_user_mod=bool(attr.no_user_mod),
121
+ usage=Attribute.Usage(attr.usage),
122
+ # FIXME avoid null values below
123
+ equality=attr.equality,
124
+ syntax=attr.syntax,
125
+ substr=attr.substr,
126
+ ordering=attr.ordering,
127
+ **element(attr).model_dump(),
128
+ )
129
+ for attr in extract_type(sub_schema, AttributeType)
113
130
  ),
114
- key=lambda x: x["name"],
131
+ key=lambda x: x.name,
115
132
  ),
116
133
  ),
117
134
  objectClasses=lowercase_dict(
118
135
  "name",
119
136
  sorted(
120
- map(
121
- object_class_dict,
122
- extract_type(sub_schema, ObjectClass),
137
+ (
138
+ ObjectClass(
139
+ may=sorted(oc.may),
140
+ must=sorted(oc.must),
141
+ kind=ObjectClass.Kind(oc.kind),
142
+ **element(oc).model_dump(),
143
+ )
144
+ for oc in extract_type(sub_schema, ObjectClassType)
123
145
  ),
124
- key=lambda x: x["name"],
146
+ key=lambda x: x.name,
125
147
  ),
126
148
  ),
127
149
  syntaxes=lowercase_dict(
128
- "oid", map(syntax_dict, extract_type(sub_schema, LDAPSyntax))
150
+ "oid",
151
+ [
152
+ Syntax(
153
+ oid=stx.oid,
154
+ desc=stx.desc,
155
+ not_human_readable=bool(stx.not_human_readable),
156
+ )
157
+ for stx in extract_type(sub_schema, LDAPSyntaxType)
158
+ ],
129
159
  ),
130
160
  )
ldap_ui/settings.py CHANGED
@@ -39,11 +39,16 @@ def GET_BIND_DN(username) -> Optional[str]:
39
39
  if config("BIND_DN", default=None):
40
40
  return config("BIND_DN")
41
41
 
42
+ return GET_BIND_PATTERN(username)
43
+
44
+
45
+ def GET_BIND_PATTERN(username) -> Optional[str]:
46
+ "Determine the bind pattern from the environment and request"
42
47
  # Optional user DN pattern string for authentication,
43
48
  # e.g. "uid=%s,ou=people,dc=example,dc=com".
44
49
  # This can be used to authenticate with directories
45
50
  # that do not allow anonymous users to search.
46
- elif config("BIND_PATTERN", default=None) and username:
51
+ if config("BIND_PATTERN", default=None) and username:
47
52
  return config("BIND_PATTERN") % username
48
53
 
49
54