ldap-ui 0.10.0__py3-none-any.whl → 0.10.2__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.10.0"
1
+ __version__ = "0.10.2"
ldap_ui/app.py CHANGED
@@ -9,8 +9,6 @@ No sessions, no cookies, nothing else.
9
9
  """
10
10
 
11
11
  import logging
12
- import sys
13
- from contextlib import contextmanager
14
12
  from http import HTTPStatus
15
13
 
16
14
  from fastapi import FastAPI, Request, Response
@@ -23,67 +21,16 @@ from ldap import ( # type: ignore
23
21
  INVALID_CREDENTIALS, # type: ignore
24
22
  NO_SUCH_OBJECT, # type: ignore
25
23
  OBJECT_CLASS_VIOLATION, # type: ignore
26
- SCOPE_BASE, # type: ignore
27
- UNWILLING_TO_PERFORM, # type: ignore
24
+ UNWILLING_TO_PERFORM, # type: ignore # type: ignore
28
25
  LDAPError, # type: ignore
29
26
  )
30
27
 
31
- from . import __version__, settings
32
- from .ldap_api import api
33
- from .ldap_helpers import WITH_OPERATIONAL_ATTRS, ldap_connect
34
-
35
- LOG = logging.getLogger("ldap-ui")
36
-
37
- LDAP_ERROR_TO_STATUS = {
38
- ALREADY_EXISTS: HTTPStatus.CONFLICT,
39
- INSUFFICIENT_ACCESS: HTTPStatus.FORBIDDEN,
40
- INVALID_CREDENTIALS: HTTPStatus.UNAUTHORIZED,
41
- NO_SUCH_OBJECT: HTTPStatus.NOT_FOUND,
42
- OBJECT_CLASS_VIOLATION: HTTPStatus.BAD_REQUEST,
43
- UNWILLING_TO_PERFORM: HTTPStatus.FORBIDDEN,
44
- }
45
-
46
-
47
- def format_ldap_error(exc: LDAPError) -> str:
48
- cause = exc.args[0] if exc.args else {}
49
- desc = cause.get("desc", "LDAP error").strip().capitalize()
50
- if "info" in cause:
51
- return f"{desc}: {cause['info'].strip().capitalize()}"
52
- return f"{desc}"
53
-
54
-
55
- if not settings.BASE_DN or not settings.SCHEMA_DN:
56
- try: # Auto-detection from root DSE
57
- connect = contextmanager(ldap_connect)
58
- with connect() as connection:
59
- _dn, attrs = connection.search_s( # type: ignore
60
- "",
61
- SCOPE_BASE,
62
- attrlist=WITH_OPERATIONAL_ATTRS,
63
- )[0]
64
- base_dns = attrs.get("namingContexts", [])
65
- if len(base_dns) == 1:
66
- settings.BASE_DN = settings.BASE_DN or base_dns[0].decode()
67
- else:
68
- LOG.warning("No unique base DN: %s", base_dns)
69
- schema_dns = attrs.get("subschemaSubentry", [])
70
- settings.SCHEMA_DN = settings.SCHEMA_DN or schema_dns[0].decode()
71
- except LDAPError as err:
72
- LOG.error(format_ldap_error(err), exc_info=err)
73
-
74
- if not settings.BASE_DN:
75
- LOG.critical("An LDAP base DN is required!")
76
- sys.exit(1)
77
-
78
- if not settings.SCHEMA_DN:
79
- LOG.critical("An LDAP schema DN is required!")
80
- sys.exit(1)
81
-
28
+ from . import __version__, ldap_api, settings
82
29
 
83
30
  # Main ASGI entry
84
31
 
85
32
  app = FastAPI(debug=settings.DEBUG, title="LDAP UI", version=__version__)
86
- app.include_router(api)
33
+ app.include_router(ldap_api.api)
87
34
  app.mount("/", StaticFiles(packages=["ldap_ui"], html=True))
88
35
 
89
36
  app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=5)
@@ -102,31 +49,42 @@ async def cache_buster(request: Request, call_next) -> Response:
102
49
 
103
50
  # API error handling
104
51
 
105
-
106
- def error_response(
107
- exc: LDAPError, status: HTTPStatus, headers: dict[str, str] | None = None
108
- ) -> JSONResponse:
109
- return JSONResponse({"detail": [format_ldap_error(exc)]}, status, headers=headers)
52
+ LDAP_ERROR_TO_STATUS = {
53
+ ALREADY_EXISTS: HTTPStatus.CONFLICT,
54
+ INSUFFICIENT_ACCESS: HTTPStatus.FORBIDDEN,
55
+ INVALID_CREDENTIALS: HTTPStatus.UNAUTHORIZED,
56
+ NO_SUCH_OBJECT: HTTPStatus.NOT_FOUND,
57
+ OBJECT_CLASS_VIOLATION: HTTPStatus.BAD_REQUEST,
58
+ UNWILLING_TO_PERFORM: HTTPStatus.FORBIDDEN,
59
+ }
110
60
 
111
61
 
112
- @app.exception_handler(INVALID_CREDENTIALS)
113
- def handle_invalid_credentials(_request: Request, exc: INVALID_CREDENTIALS):
114
- return error_response(
115
- exc,
116
- HTTPStatus.UNAUTHORIZED,
117
- headers={"WWW-Authenticate": 'Basic realm="Please log in", charset="UTF-8"'},
118
- )
62
+ @app.exception_handler(LDAPError)
63
+ def handle_ldap_error(request: Request, exc: LDAPError) -> Response:
64
+ "General handler for LDAP errors"
119
65
 
66
+ exc_type = type(exc)
67
+ if exc_type is UNWILLING_TO_PERFORM:
68
+ logging.critical("Need BIND_DN or BIND_PATTERN to authenticate")
120
69
 
121
- @app.exception_handler(UNWILLING_TO_PERFORM)
122
- def handle_unwilling_to_perform(_request: Request, exc: UNWILLING_TO_PERFORM):
123
- LOG.warning("Need BIND_DN or BIND_PATTERN to authenticate")
124
- return error_response(exc, HTTPStatus.FORBIDDEN)
70
+ if exc_type is INVALID_CREDENTIALS:
71
+ return Response(
72
+ status_code=HTTPStatus.UNAUTHORIZED,
73
+ headers={
74
+ "WWW-Authenticate": 'Basic realm="Please log in", charset="UTF-8"'
75
+ },
76
+ )
125
77
 
78
+ if exc_type not in LDAP_ERROR_TO_STATUS:
79
+ # Unknown error --> log it since FastApi won't do it for us
80
+ logging.exception("Error in %s %s:", request.method, request.url, exc_info=exc)
126
81
 
127
- @app.exception_handler(LDAPError)
128
- def handle_ldap_error(_request: Request, exc: LDAPError):
129
- "General handler for other LDAP errors"
130
- return error_response(
131
- exc, LDAP_ERROR_TO_STATUS.get(type(exc), HTTPStatus.INTERNAL_SERVER_ERROR)
82
+ cause = exc.args[0] if exc.args else {}
83
+ desc = cause.get("desc", "LDAP error").capitalize()
84
+ msg = f"{desc}" if "info" not in cause else f"{desc}: {cause['info'].capitalize()}"
85
+ return JSONResponse(
86
+ {"detail": [msg]},
87
+ status_code=LDAP_ERROR_TO_STATUS.get(
88
+ exc_type, HTTPStatus.INTERNAL_SERVER_ERROR
89
+ ),
132
90
  )
ldap_ui/entities.py CHANGED
@@ -1,6 +1,6 @@
1
1
  "Data types for ReST endpoints"
2
2
 
3
- from pydantic import BaseModel, Field
3
+ from pydantic import BaseModel
4
4
 
5
5
 
6
6
  class TreeItem(BaseModel):
@@ -12,32 +12,20 @@ class TreeItem(BaseModel):
12
12
  level: int
13
13
 
14
14
 
15
- class Meta(BaseModel):
16
- "Attribute classification for an entry"
17
-
18
- dn: str
19
- required: list[str]
20
- aux: list[str]
21
- binary: list[str]
22
- autoFilled: list[str]
23
- isNew: bool = False
24
-
25
-
26
15
  Attributes = dict[str, list[str]]
27
16
 
17
+ AttributeNames = list[str] # Names of modified attributes
18
+
28
19
 
29
20
  class Entry(BaseModel):
30
21
  "Directory entry"
31
22
 
23
+ dn: str
32
24
  attrs: Attributes
33
- meta: Meta
34
- changed: list[str] = Field(default_factory=list)
35
-
36
-
37
- class ChangedAttributes(BaseModel):
38
- "List of modified attributes"
39
-
40
- changed: list[str]
25
+ binary: AttributeNames
26
+ autoFilled: AttributeNames
27
+ changed: AttributeNames
28
+ isNew: bool = False
41
29
 
42
30
 
43
31
  class ChangePasswordRequest(BaseModel):