ldap-ui 0.9.2__py3-none-any.whl → 0.9.3__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 +1 -1
- ldap_ui/__main__.py +7 -7
- ldap_ui/app.py +13 -3
- ldap_ui/ldap_api.py +38 -16
- {ldap_ui-0.9.2.dist-info → ldap_ui-0.9.3.dist-info}/METADATA +19 -2
- {ldap_ui-0.9.2.dist-info → ldap_ui-0.9.3.dist-info}/RECORD +10 -10
- {ldap_ui-0.9.2.dist-info → ldap_ui-0.9.3.dist-info}/LICENSE.txt +0 -0
- {ldap_ui-0.9.2.dist-info → ldap_ui-0.9.3.dist-info}/WHEEL +0 -0
- {ldap_ui-0.9.2.dist-info → ldap_ui-0.9.3.dist-info}/entry_points.txt +0 -0
- {ldap_ui-0.9.2.dist-info → ldap_ui-0.9.3.dist-info}/top_level.txt +0 -0
ldap_ui/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.9.
|
|
1
|
+
__version__ = "0.9.3"
|
ldap_ui/__main__.py
CHANGED
|
@@ -14,6 +14,13 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@click.command()
|
|
17
|
+
@click.option(
|
|
18
|
+
"-b",
|
|
19
|
+
"--base-dn",
|
|
20
|
+
type=str,
|
|
21
|
+
default=settings.BASE_DN,
|
|
22
|
+
help="LDAP base DN. Required unless the BASE_DN environment variable is set.",
|
|
23
|
+
)
|
|
17
24
|
@click.option(
|
|
18
25
|
"-h",
|
|
19
26
|
"--host",
|
|
@@ -22,13 +29,6 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
|
|
22
29
|
help="Bind socket to this host.",
|
|
23
30
|
show_default=True,
|
|
24
31
|
)
|
|
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
32
|
@click.option(
|
|
33
33
|
"-p",
|
|
34
34
|
"--port",
|
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,8 +39,10 @@ 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
|
|
|
42
|
+
LOG = logging.getLogger("ldap-ui")
|
|
43
|
+
|
|
41
44
|
if settings.BASE_DN is None:
|
|
42
|
-
|
|
45
|
+
LOG.critical("An LDAP base DN is required!")
|
|
43
46
|
sys.exit(1)
|
|
44
47
|
|
|
45
48
|
# Force authentication
|
|
@@ -149,7 +152,7 @@ class CacheBustingMiddleware(BaseHTTPMiddleware):
|
|
|
149
152
|
return response
|
|
150
153
|
|
|
151
154
|
|
|
152
|
-
async def http_exception(_request: Request, exc: HTTPException):
|
|
155
|
+
async def http_exception(_request: Request, exc: HTTPException) -> Response:
|
|
153
156
|
"Send error responses"
|
|
154
157
|
assert exc.status_code >= 400
|
|
155
158
|
return PlainTextResponse(
|
|
@@ -159,11 +162,17 @@ async def http_exception(_request: Request, exc: HTTPException):
|
|
|
159
162
|
)
|
|
160
163
|
|
|
161
164
|
|
|
162
|
-
async def forbidden(_request: Request, exc: ldap.LDAPError):
|
|
165
|
+
async def forbidden(_request: Request, exc: ldap.LDAPError) -> Response:
|
|
163
166
|
"HTTP 403 Forbidden"
|
|
164
167
|
return PlainTextResponse(ldap_exception_message(exc), status_code=403)
|
|
165
168
|
|
|
166
169
|
|
|
170
|
+
async def http_422(_request: Request, e: ValidationError) -> Response:
|
|
171
|
+
"HTTP 422 Unprocessable Entity"
|
|
172
|
+
LOG.exception("Invalid request body", exc_info=e)
|
|
173
|
+
return Response(repr(e), status_code=422)
|
|
174
|
+
|
|
175
|
+
|
|
167
176
|
@contextlib.asynccontextmanager
|
|
168
177
|
async def lifespan(app):
|
|
169
178
|
with ldap_connect() as connection:
|
|
@@ -186,6 +195,7 @@ app = Starlette(
|
|
|
186
195
|
exception_handlers={
|
|
187
196
|
HTTPException: http_exception,
|
|
188
197
|
ldap.INSUFFICIENT_ACCESS: forbidden,
|
|
198
|
+
ValidationError: http_422,
|
|
189
199
|
},
|
|
190
200
|
lifespan=lifespan,
|
|
191
201
|
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.
|
|
171
|
+
json = Entry.validate_json(await request.body())
|
|
168
172
|
req = {
|
|
169
|
-
k: [s
|
|
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
|
-
|
|
281
|
-
|
|
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.
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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.
|
|
322
|
+
args = PasswordRequest.validate_json(await request.body())
|
|
307
323
|
|
|
308
|
-
if
|
|
324
|
+
if type(args) is CheckPasswordRequest:
|
|
309
325
|
with ldap_connect() as con:
|
|
310
326
|
try:
|
|
311
|
-
con.simple_bind_s(dn, args
|
|
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
|
-
|
|
332
|
+
else:
|
|
317
333
|
connection = request.state.ldap
|
|
318
|
-
if args
|
|
334
|
+
if args.new1:
|
|
319
335
|
await empty(
|
|
320
336
|
connection,
|
|
321
|
-
connection.passwd(dn, args.
|
|
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.
|
|
3
|
+
Version: 0.9.3
|
|
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=
|
|
2
|
-
ldap_ui/__main__.py,sha256=
|
|
3
|
-
ldap_ui/app.py,sha256=
|
|
4
|
-
ldap_ui/ldap_api.py,sha256=
|
|
1
|
+
ldap_ui/__init__.py,sha256=xKd3pzbczuMsdB08eLAOqZDUd_q1IRxwZ_ccAFL4c4A,22
|
|
2
|
+
ldap_ui/__main__.py,sha256=RzzkUuwqEk9bPG9pPaBZHA6EvFhXFKI89cF7jdX9RPw,1345
|
|
3
|
+
ldap_ui/app.py,sha256=2gy0khq8whbrxJbA-jNo2Ss7oiW-XxQKWM6Jbxp5d7I,6759
|
|
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.
|
|
20
|
-
ldap_ui-0.9.
|
|
21
|
-
ldap_ui-0.9.
|
|
22
|
-
ldap_ui-0.9.
|
|
23
|
-
ldap_ui-0.9.
|
|
24
|
-
ldap_ui-0.9.
|
|
19
|
+
ldap_ui-0.9.3.dist-info/LICENSE.txt,sha256=UpJ0sDIqHxbOtzy1EG4bCHs9R_99ODxxPDK4NZ0g3I0,1042
|
|
20
|
+
ldap_ui-0.9.3.dist-info/METADATA,sha256=tDswFFkkDqk_wc40dnfq455_lWXmUmaGFntAgNcbiQc,7557
|
|
21
|
+
ldap_ui-0.9.3.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
|
22
|
+
ldap_ui-0.9.3.dist-info/entry_points.txt,sha256=TGxMkXYeZP5m5NjZxWmgzITYWhSdj2mR_GGUYmHhGws,50
|
|
23
|
+
ldap_ui-0.9.3.dist-info/top_level.txt,sha256=t9Agyig1nDdJuQvx_UVuk1n28pgswc1BIYw8E6pWado,8
|
|
24
|
+
ldap_ui-0.9.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|