ldap-ui 0.9.2__py3-none-any.whl → 0.9.4__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.2"
1
+ __version__ = "0.9.4"
ldap_ui/__main__.py CHANGED
@@ -1,5 +1,9 @@
1
+ import logging
2
+
1
3
  import click
2
4
  import uvicorn
5
+ from uvicorn.config import LOG_LEVELS
6
+ from uvicorn.logging import ColourizedFormatter
3
7
  from uvicorn.main import LEVEL_CHOICES
4
8
 
5
9
  import ldap_ui
@@ -14,6 +18,13 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
14
18
 
15
19
 
16
20
  @click.command()
21
+ @click.option(
22
+ "-b",
23
+ "--base-dn",
24
+ type=str,
25
+ default=settings.BASE_DN,
26
+ help="LDAP base DN. Required unless the BASE_DN environment variable is set.",
27
+ )
17
28
  @click.option(
18
29
  "-h",
19
30
  "--host",
@@ -22,13 +33,6 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
22
33
  help="Bind socket to this host.",
23
34
  show_default=True,
24
35
  )
25
- @click.option(
26
- "-b",
27
- "--base-dn",
28
- type=str,
29
- default=settings.BASE_DN,
30
- help="LDAP base DN. Required unless the BASE_DN environment variable is set.",
31
- )
32
36
  @click.option(
33
37
  "-p",
34
38
  "--port",
@@ -54,12 +58,16 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
54
58
  help="Display the current version and exit.",
55
59
  )
56
60
  def main(base_dn, host, port, log_level):
57
- if base_dn:
61
+ logging.basicConfig(level=LOG_LEVELS[log_level])
62
+ rootHandler = logging.getLogger().handlers[0]
63
+ rootHandler.setFormatter(ColourizedFormatter(fmt="%(levelprefix)s %(message)s"))
64
+
65
+ if base_dn is not None:
58
66
  settings.BASE_DN = base_dn
59
67
 
60
68
  uvicorn.run(
61
69
  "ldap_ui.app:app",
62
- log_level=log_level,
70
+ log_level=logging.INFO,
63
71
  host=host,
64
72
  port=port,
65
73
  )
ldap_ui/app.py CHANGED
@@ -17,6 +17,7 @@ from typing import AsyncGenerator
17
17
 
18
18
  import ldap
19
19
  from ldap.schema import SubSchema
20
+ from pydantic import ValidationError
20
21
  from starlette.applications import Starlette
21
22
  from starlette.authentication import (
22
23
  AuthCredentials,
@@ -38,10 +39,14 @@ from . import settings
38
39
  from .ldap_api import api
39
40
  from .ldap_helpers import WITH_OPERATIONAL_ATTRS, empty, ldap_connect, unique
40
41
 
41
- if settings.BASE_DN is None:
42
- logging.getLogger("ldap-ui").critical("An LDAP base DN is required!")
42
+ LOG = logging.getLogger("ldap-ui")
43
+
44
+ if not settings.BASE_DN:
45
+ LOG.critical("An LDAP base DN is required!")
43
46
  sys.exit(1)
44
47
 
48
+ LOG.debug("Base DN: %s", settings.BASE_DN)
49
+
45
50
  # Force authentication
46
51
  UNAUTHORIZED = Response(
47
52
  "Invalid credentials",
@@ -93,8 +98,10 @@ class LdapConnectionMiddleware(BaseHTTPMiddleware):
93
98
  return UNAUTHORIZED
94
99
 
95
100
  except ldap.LDAPError as err:
101
+ msg = ldap_exception_message(err)
102
+ LOG.error(msg)
96
103
  return PlainTextResponse(
97
- ldap_exception_message(err),
104
+ msg,
98
105
  status_code=500,
99
106
  )
100
107
 
@@ -149,9 +156,13 @@ class CacheBustingMiddleware(BaseHTTPMiddleware):
149
156
  return response
150
157
 
151
158
 
152
- async def http_exception(_request: Request, exc: HTTPException):
159
+ async def http_exception(_request: Request, exc: HTTPException) -> Response:
153
160
  "Send error responses"
154
161
  assert exc.status_code >= 400
162
+ if exc.status_code < 500:
163
+ LOG.warning(exc.detail)
164
+ else:
165
+ LOG.error(exc.detail)
155
166
  return PlainTextResponse(
156
167
  exc.detail,
157
168
  status_code=exc.status_code,
@@ -159,11 +170,17 @@ async def http_exception(_request: Request, exc: HTTPException):
159
170
  )
160
171
 
161
172
 
162
- async def forbidden(_request: Request, exc: ldap.LDAPError):
173
+ async def forbidden(_request: Request, exc: ldap.LDAPError) -> Response:
163
174
  "HTTP 403 Forbidden"
164
175
  return PlainTextResponse(ldap_exception_message(exc), status_code=403)
165
176
 
166
177
 
178
+ async def http_422(_request: Request, e: ValidationError) -> Response:
179
+ "HTTP 422 Unprocessable Entity"
180
+ LOG.warn("Invalid request body", exc_info=e)
181
+ return Response(repr(e), status_code=422)
182
+
183
+
167
184
  @contextlib.asynccontextmanager
168
185
  async def lifespan(app):
169
186
  with ldap_connect() as connection:
@@ -186,6 +203,7 @@ app = Starlette(
186
203
  exception_handlers={
187
204
  HTTPException: http_exception,
188
205
  ldap.INSUFFICIENT_ACCESS: forbidden,
206
+ ValidationError: http_422,
189
207
  },
190
208
  lifespan=lifespan,
191
209
  middleware=(
ldap_ui/ldap_api.py CHANGED
@@ -9,7 +9,7 @@ Asynchronous LDAP operations are used as much as possible.
9
9
 
10
10
  import base64
11
11
  import io
12
- from typing import Any, Optional, Tuple
12
+ from typing import Any, Optional, Tuple, Union
13
13
 
14
14
  import ldap
15
15
  import ldif
@@ -17,6 +17,7 @@ from ldap.ldapobject import LDAPObject
17
17
  from ldap.modlist import addModlist, modifyModlist
18
18
  from ldap.schema import SubSchema
19
19
  from ldap.schema.models import AttributeType, LDAPSyntax, ObjectClass
20
+ from pydantic import BaseModel, Field, TypeAdapter
20
21
  from starlette.datastructures import UploadFile
21
22
  from starlette.exceptions import HTTPException
22
23
  from starlette.requests import Request
@@ -144,6 +145,9 @@ def _entry(schema: SubSchema, res: Tuple[str, Any]) -> dict[str, Any]:
144
145
  }
145
146
 
146
147
 
148
+ Entry = TypeAdapter(dict[str, list[bytes]])
149
+
150
+
147
151
  @api.route("/entry/{dn}", methods=("GET", "POST", "DELETE", "PUT"))
148
152
  async def entry(request: Request) -> Response:
149
153
  "Edit directory entries"
@@ -164,9 +168,9 @@ async def entry(request: Request) -> Response:
164
168
  return NO_CONTENT
165
169
 
166
170
  # Copy JSON payload into a dictionary of non-empty byte strings
167
- json = await request.json()
171
+ json = Entry.validate_json(await request.body())
168
172
  req = {
169
- k: [s.encode() for s in filter(None, v)]
173
+ k: [s for s in filter(None, v)]
170
174
  for k, v in json.items()
171
175
  if k not in PHOTOS and (k not in PASSWORDS or request.method == "PUT")
172
176
  }
@@ -277,8 +281,14 @@ async def ldifUpload(
277
281
  "Import LDIF"
278
282
 
279
283
  reader = LDIFReader(await request.body(), request.state.ldap)
280
- reader.parse()
281
- return NO_CONTENT
284
+ try:
285
+ reader.parse()
286
+ return NO_CONTENT
287
+ except ValueError as e:
288
+ return Response(e.args[0], status_code=422)
289
+
290
+
291
+ Rdn = TypeAdapter(str)
282
292
 
283
293
 
284
294
  @api.route("/rename/{dn}", methods=("POST",))
@@ -286,16 +296,22 @@ async def rename(request: Request) -> JSONResponse:
286
296
  "Rename an entry"
287
297
 
288
298
  dn = request.path_params["dn"]
289
- rdn = await request.json()
299
+ rdn = Rdn.validate_json(await request.body())
290
300
  connection = request.state.ldap
291
301
  await empty(connection, connection.rename(dn, rdn, delold=0))
292
302
  return NO_CONTENT
293
303
 
294
304
 
295
- def _cn(entry: dict) -> Optional[str]:
296
- "Try to extract a CN"
297
- if "cn" in entry and entry["cn"]:
298
- return entry["cn"][0].decode()
305
+ class ChangePasswordRequest(BaseModel):
306
+ old: str
307
+ new1: str
308
+
309
+
310
+ class CheckPasswordRequest(BaseModel):
311
+ check: str = Field(min_length=1)
312
+
313
+
314
+ PasswordRequest = TypeAdapter(Union[ChangePasswordRequest, CheckPasswordRequest])
299
315
 
300
316
 
301
317
  @api.route("/entry/password/{dn}", methods=("POST",))
@@ -303,22 +319,22 @@ async def passwd(request: Request) -> JSONResponse:
303
319
  "Update passwords"
304
320
 
305
321
  dn = request.path_params["dn"]
306
- args = await request.json()
322
+ args = PasswordRequest.validate_json(await request.body())
307
323
 
308
- if "check" in args:
324
+ if type(args) is CheckPasswordRequest:
309
325
  with ldap_connect() as con:
310
326
  try:
311
- con.simple_bind_s(dn, args["check"])
327
+ con.simple_bind_s(dn, args.check)
312
328
  return JSONResponse(True)
313
329
  except ldap.INVALID_CREDENTIALS:
314
330
  return JSONResponse(False)
315
331
 
316
- if "old" in args and "new1" in args:
332
+ else:
317
333
  connection = request.state.ldap
318
- if args["new1"]:
334
+ if args.new1:
319
335
  await empty(
320
336
  connection,
321
- connection.passwd(dn, args.get("old") or None, args["new1"]),
337
+ connection.passwd(dn, args.old or None, args.new1),
322
338
  )
323
339
  _dn, attrs = await get_entry_by_dn(connection, dn)
324
340
  return JSONResponse(attrs["userPassword"][0].decode())
@@ -328,6 +344,12 @@ async def passwd(request: Request) -> JSONResponse:
328
344
  return JSONResponse(None)
329
345
 
330
346
 
347
+ def _cn(entry: dict) -> Optional[str]:
348
+ "Try to extract a CN"
349
+ if "cn" in entry and entry["cn"]:
350
+ return entry["cn"][0].decode()
351
+
352
+
331
353
  @api.route("/search/{query:path}")
332
354
  async def search(request: Request) -> JSONResponse:
333
355
  "Search the directory"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ldap-ui
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: A fast and versatile LDAP editor
5
5
  Author: dnknth
6
6
  License: MIT License
@@ -15,6 +15,7 @@ Classifier: Topic :: Software Development :: Libraries
15
15
  Requires-Python: >=3.7
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE.txt
18
+ Requires-Dist: pydantic
18
19
  Requires-Dist: python-ldap
19
20
  Requires-Dist: python-multipart
20
21
  Requires-Dist: starlette
@@ -85,7 +86,23 @@ python3 -m venv --system-site-packages venv
85
86
  pip3 install ldap-ui
86
87
  ```
87
88
 
88
- Possibly after a shell `rehash`, it is available as `ldap-ui`.
89
+ Possibly after a shell `rehash`, it is available as `ldap-ui`:
90
+
91
+ ```text
92
+ Usage: ldap-ui [OPTIONS]
93
+
94
+ Options:
95
+ -b, --base-dn TEXT LDAP base DN. Required unless the BASE_DN
96
+ environment variable is set.
97
+ -h, --host TEXT Bind socket to this host. [default:
98
+ 127.0.0.1]
99
+ -p, --port INTEGER Bind socket to this port. If 0, an available
100
+ port will be picked. [default: 5000]
101
+ -l, --log-level [critical|error|warning|info|debug|trace]
102
+ Log level. [default: info]
103
+ --version Display the current version and exit.
104
+ --help Show this message and exit.
105
+ ```
89
106
 
90
107
  ## Development
91
108
 
@@ -1,7 +1,7 @@
1
- ldap_ui/__init__.py,sha256=gqT-BGoeEItda9fICQDvLbxEjWRIBhFJxPxxKvmHLUo,22
2
- ldap_ui/__main__.py,sha256=XEW0IGh-BZ0MUnzHmLRgnqSee8r0o_oRgcQ_8WSNC0g,1345
3
- ldap_ui/app.py,sha256=8iYzv92zl6XeqeF8JjaYT-68wgYSrcsq9CQK0O0bsCE,6443
4
- ldap_ui/ldap_api.py,sha256=ecK8K-7-tkoA2df2dbQT8y61I70r7NukE3BQFNN1BsE,12776
1
+ ldap_ui/__init__.py,sha256=e56AvHfJCtG2ZwwINqsxINVbehWdKxMYgIDbjd7P-II,22
2
+ ldap_ui/__main__.py,sha256=m1rmS8LgWag5KP0tf0bZ5VskJ3eWhB4rhCViC66lzUI,1651
3
+ ldap_ui/app.py,sha256=I_JuRLYYy-zeMzvDMqVQMshzP_PFDLMAMlFoRgEgR6M,6945
4
+ ldap_ui/ldap_api.py,sha256=l6Wnjzm8ycaY-xXT2JNuHTTvLEsOEXGJnJhDdD72N5Q,13267
5
5
  ldap_ui/ldap_helpers.py,sha256=DRpKtqEX_OvYJBuvzTNi0CcZAu446wwUiOezlkBAxrQ,3045
6
6
  ldap_ui/schema.py,sha256=apbdLK_WpED0IzmrdktWTu4ESz8GfdOoJuRicFC84YY,3327
7
7
  ldap_ui/settings.py,sha256=PmugPMAmFmNa3bM4XKsur89ELORZ103DxrP7ZbsuopY,2260
@@ -16,9 +16,9 @@ ldap_ui/statics/assets/index-CA45Sb-q.js,sha256=qejoldzV9wbUVar1ljURYwyTb2DYn3HQ
16
16
  ldap_ui/statics/assets/index-CA45Sb-q.js.gz,sha256=1yJTHdcLZLU2d6DkPKbmNEFiOlPo7vIWXYyg_fidSdE,43710
17
17
  ldap_ui/statics/assets/index-DlTKbnmq.css,sha256=Rpthz_HvUybqmodfPCnOXFsSwGd7v8hhh-p-duAzf1E,48119
18
18
  ldap_ui/statics/assets/index-DlTKbnmq.css.gz,sha256=Ctq3hMh_BBVt9zsh9CYlHpVtDDHanicyGPAdcGXIFXw,11532
19
- ldap_ui-0.9.2.dist-info/LICENSE.txt,sha256=UpJ0sDIqHxbOtzy1EG4bCHs9R_99ODxxPDK4NZ0g3I0,1042
20
- ldap_ui-0.9.2.dist-info/METADATA,sha256=7NuYPD10C_50VRimJO-besonFetFEETpZmdFm13MSEg,6824
21
- ldap_ui-0.9.2.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
22
- ldap_ui-0.9.2.dist-info/entry_points.txt,sha256=TGxMkXYeZP5m5NjZxWmgzITYWhSdj2mR_GGUYmHhGws,50
23
- ldap_ui-0.9.2.dist-info/top_level.txt,sha256=t9Agyig1nDdJuQvx_UVuk1n28pgswc1BIYw8E6pWado,8
24
- ldap_ui-0.9.2.dist-info/RECORD,,
19
+ ldap_ui-0.9.4.dist-info/LICENSE.txt,sha256=UpJ0sDIqHxbOtzy1EG4bCHs9R_99ODxxPDK4NZ0g3I0,1042
20
+ ldap_ui-0.9.4.dist-info/METADATA,sha256=zNGp6KEaiXTr5ym0eWo8GwzIjy_oTTsXEBABYaDXuUE,7557
21
+ ldap_ui-0.9.4.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
22
+ ldap_ui-0.9.4.dist-info/entry_points.txt,sha256=TGxMkXYeZP5m5NjZxWmgzITYWhSdj2mR_GGUYmHhGws,50
23
+ ldap_ui-0.9.4.dist-info/top_level.txt,sha256=t9Agyig1nDdJuQvx_UVuk1n28pgswc1BIYw8E6pWado,8
24
+ ldap_ui-0.9.4.dist-info/RECORD,,