ldap-ui 0.9.6__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.6"
1
+ __version__ = "0.9.7"
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,17 +70,20 @@ class LdapConnectionMiddleware(BaseHTTPMiddleware):
71
70
 
72
71
  # Search for basic auth user
73
72
  if type(request.user) is LdapUser:
73
+ password = request.user.password
74
74
  dn = settings.GET_BIND_PATTERN(request.user.username)
75
75
  if dn is None:
76
- dn, _attrs = await unique(
77
- connection,
78
- connection.search(
79
- settings.BASE_DN,
80
- ldap.SCOPE_SUBTREE,
81
- settings.GET_BIND_DN_FILTER(request.user.username),
82
- ),
83
- )
84
- password = request.user.password
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
85
87
 
86
88
  # Hard-wired credentials
87
89
  if dn is None:
@@ -101,10 +103,10 @@ class LdapConnectionMiddleware(BaseHTTPMiddleware):
101
103
 
102
104
  except ldap.LDAPError as err:
103
105
  msg = ldap_exception_message(err)
104
- LOG.error(msg)
106
+ LOG.error(msg, exc_info=err)
105
107
  return PlainTextResponse(
106
108
  msg,
107
- status_code=500,
109
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,
108
110
  )
109
111
 
110
112
 
@@ -174,29 +176,16 @@ async def http_exception(_request: Request, exc: HTTPException) -> Response:
174
176
 
175
177
  async def forbidden(_request: Request, exc: ldap.LDAPError) -> Response:
176
178
  "HTTP 403 Forbidden"
177
- return PlainTextResponse(ldap_exception_message(exc), status_code=403)
179
+ return PlainTextResponse(
180
+ ldap_exception_message(exc),
181
+ status_code=HTTPStatus.FORBIDDEN.value,
182
+ )
178
183
 
179
184
 
180
185
  async def http_422(_request: Request, e: ValidationError) -> Response:
181
186
  "HTTP 422 Unprocessable Entity"
182
187
  LOG.warn("Invalid request body", exc_info=e)
183
- return Response(repr(e), status_code=422)
184
-
185
-
186
- @contextlib.asynccontextmanager
187
- async def lifespan(app):
188
- with ldap_connect() as connection:
189
- # See: https://hub.packtpub.com/python-ldap-applications-part-4-ldap-schema/
190
- _dn, sub_schema = await unique(
191
- connection,
192
- connection.search(
193
- settings.SCHEMA_DN,
194
- ldap.SCOPE_BASE,
195
- attrlist=WITH_OPERATIONAL_ATTRS,
196
- ),
197
- )
198
- app.state.schema = SubSchema(sub_schema, check_uniqueness=2)
199
- yield
188
+ return Response(repr(e), status_code=HTTPStatus.UNPROCESSABLE_ENTITY.value)
200
189
 
201
190
 
202
191
  # Main ASGI entry
@@ -207,7 +196,6 @@ app = Starlette(
207
196
  ldap.INSUFFICIENT_ACCESS: forbidden,
208
197
  ValidationError: http_422,
209
198
  },
210
- lifespan=lifespan,
211
199
  middleware=(
212
200
  Middleware(AuthenticationMiddleware, backend=BasicAuthBackend()),
213
201
  Middleware(LdapConnectionMiddleware),
@@ -215,7 +203,7 @@ app = Starlette(
215
203
  Middleware(GZipMiddleware, minimum_size=512, compresslevel=6),
216
204
  ),
217
205
  routes=[
218
- Mount("/api", routes=api.routes),
206
+ Mount("/api", app=api),
219
207
  Mount("/", StaticFiles(packages=["ldap_ui"], html=True)),
220
208
  ],
221
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
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ldap-ui
3
- Version: 0.9.6
3
+ Version: 0.9.7
4
4
  Summary: A fast and versatile LDAP editor
5
5
  Author: dnknth
6
6
  License: MIT License
@@ -1,9 +1,9 @@
1
- ldap_ui/__init__.py,sha256=IgVHjr-TeioZYLJSkvpT80LLGi6U3ONzR1cfYfd5XNQ,22
1
+ ldap_ui/__init__.py,sha256=RcQa7EaTs304WhxJ7lAJ_lelzmRpmed8pgcM1EYP3bo,22
2
2
  ldap_ui/__main__.py,sha256=s2jFbC2y2LpvcTY8yXOFVisKXSFG079hc9IVgrJ49vY,1849
3
- ldap_ui/app.py,sha256=sSZLmaZrIlBCXnED41k0wC7iu3g6bHDJMy2-JgGrpec,7086
4
- ldap_ui/ldap_api.py,sha256=l6Wnjzm8ycaY-xXT2JNuHTTvLEsOEXGJnJhDdD72N5Q,13267
5
- ldap_ui/ldap_helpers.py,sha256=DRpKtqEX_OvYJBuvzTNi0CcZAu446wwUiOezlkBAxrQ,3045
6
- ldap_ui/schema.py,sha256=apbdLK_WpED0IzmrdktWTu4ESz8GfdOoJuRicFC84YY,3327
3
+ ldap_ui/app.py,sha256=BgZGoKYHzQ74IuiEngGRiE-t7vgiyC096ncRkpn3trQ,6805
4
+ ldap_ui/ldap_api.py,sha256=tGFC8wWnnfpwYiP560K5cGElzEm5mgDmrVg_YDPIT08,14206
5
+ ldap_ui/ldap_helpers.py,sha256=KTgvwKH8ZNiO1Ccy8TVt8Rr9Q8D2ft334wQJMfBj6Ek,3236
6
+ ldap_ui/schema.py,sha256=gdfqIpRRgMj4CtrqFrzQcmfduJOyceOWqLD58dGFezE,4495
7
7
  ldap_ui/settings.py,sha256=NqtJrP_redV4fgQl7tMU25AY52HJyDLatFyh7xzAbHU,2413
8
8
  ldap_ui/statics/favicon.ico,sha256=_PMMM_C1ER5cpJTXZcRgISR4igj44kA4u8Trl-Ko3L0,4286
9
9
  ldap_ui/statics/index.html,sha256=KTF_WQSV58WQ4rRwMhz_21JKOxeTGrBWIOYiGBWY5AE,827
@@ -16,9 +16,9 @@ ldap_ui/statics/assets/index-CFsg5uEH.js,sha256=AJK_DIYMxldMovl4hQbot3aAN2KYqMzO
16
16
  ldap_ui/statics/assets/index-CFsg5uEH.js.gz,sha256=SsvrF3uvqhPE6i7YH1pSUT81DBtVOr_67SV8nGPCY3c,43709
17
17
  ldap_ui/statics/assets/index-Cw9TEv0d.css,sha256=sa0JhzpsjJhP3Bi2nJpG6Shn3yKI9hl_7I9kVY5E3Zs,48119
18
18
  ldap_ui/statics/assets/index-Cw9TEv0d.css.gz,sha256=qE_XQEa7HH54vGvQR78l5eeTcXVWmiqU_d7Go80X_S0,11533
19
- ldap_ui-0.9.6.dist-info/LICENSE.txt,sha256=UpJ0sDIqHxbOtzy1EG4bCHs9R_99ODxxPDK4NZ0g3I0,1042
20
- ldap_ui-0.9.6.dist-info/METADATA,sha256=sQp3ltHa5eMSiVco3MmEf5Usf4V1qYlq6AFbl0T2YOw,7557
21
- ldap_ui-0.9.6.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
22
- ldap_ui-0.9.6.dist-info/entry_points.txt,sha256=TGxMkXYeZP5m5NjZxWmgzITYWhSdj2mR_GGUYmHhGws,50
23
- ldap_ui-0.9.6.dist-info/top_level.txt,sha256=t9Agyig1nDdJuQvx_UVuk1n28pgswc1BIYw8E6pWado,8
24
- ldap_ui-0.9.6.dist-info/RECORD,,
19
+ ldap_ui-0.9.7.dist-info/LICENSE.txt,sha256=UpJ0sDIqHxbOtzy1EG4bCHs9R_99ODxxPDK4NZ0g3I0,1042
20
+ ldap_ui-0.9.7.dist-info/METADATA,sha256=_Lm0PkKzjb648F7YaeIvlohhWHZZawoXl20eCtcXmC4,7557
21
+ ldap_ui-0.9.7.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
22
+ ldap_ui-0.9.7.dist-info/entry_points.txt,sha256=TGxMkXYeZP5m5NjZxWmgzITYWhSdj2mR_GGUYmHhGws,50
23
+ ldap_ui-0.9.7.dist-info/top_level.txt,sha256=t9Agyig1nDdJuQvx_UVuk1n28pgswc1BIYw8E6pWado,8
24
+ ldap_ui-0.9.7.dist-info/RECORD,,