ldap-rest 0.4.7 → 0.5.0
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.
- package/bin/sync-james.mjs +6 -2
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/auth/base.js +1 -1
- package/dist/lib/ldapSchema.js +1 -0
- package/dist/plugins/auth/authzPerRoute.js +1 -1
- package/dist/plugins/auth/rateLimit.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/raw.js +1 -0
- package/dist/plugins/twake/appAccountsApi.js +1 -1
- package/dist/src/bin/index.d.ts +112 -0
- package/dist/src/browser/ldap-browser/LdapBrowser.d.ts +52 -0
- package/dist/src/browser/ldap-browser/api/RawApiClient.d.ts +64 -0
- package/dist/src/browser/ldap-browser/components/EntryTree.d.ts +75 -0
- package/dist/src/browser/ldap-browser/components/EntryView.d.ts +56 -0
- package/dist/src/browser/ldap-browser/index.d.ts +9 -0
- package/dist/src/browser/ldap-browser/schema.d.ts +62 -0
- package/dist/src/browser/ldap-browser/types.d.ts +96 -0
- package/dist/src/config/args.d.ts +6 -0
- package/dist/src/lib/auth/base.d.ts +82 -1
- package/dist/src/lib/ldapSchema.d.ts +159 -0
- package/dist/src/plugins/ldap/raw.d.ts +251 -0
- package/docs/client-development/browser/libraries.md +142 -1
- package/docs/usage/configuration.md +20 -0
- package/docs/usage/plugins/README.md +10 -9
- package/docs/usage/plugins/auth/README.md +57 -0
- package/docs/usage/plugins/auth/rate-limit.md +9 -0
- package/docs/usage/plugins/integrations/app-accounts.md +32 -0
- package/docs/usage/plugins/ldap/README.md +12 -0
- package/docs/usage/plugins/ldap/raw.md +226 -0
- package/examples/web/README.md +18 -0
- package/examples/web/ldap-browser.html +94 -0
- package/package.json +37 -1
- package/static/browser/ldap-browser/LdapBrowser.js +199 -0
- package/static/browser/ldap-browser/LdapBrowser.js.map +1 -0
- package/static/browser/ldap-browser/api/RawApiClient.js +103 -0
- package/static/browser/ldap-browser/api/RawApiClient.js.map +1 -0
- package/static/browser/ldap-browser/components/EntryTree.js +235 -0
- package/static/browser/ldap-browser/components/EntryTree.js.map +1 -0
- package/static/browser/ldap-browser/components/EntryView.js +183 -0
- package/static/browser/ldap-browser/components/EntryView.js.map +1 -0
- package/static/browser/ldap-browser/index.js +4 -0
- package/static/browser/ldap-browser/index.js.map +1 -0
- package/static/browser/ldap-browser/schema.js +135 -0
- package/static/browser/ldap-browser/schema.js.map +1 -0
- package/static/browser/ldap-browser/types.js +2 -0
- package/static/browser/ldap-browser/types.js.map +1 -0
- package/static/browser/ldap-browser.css +2 -0
- package/static/browser/ldap-browser.css.map +1 -0
- package/static/browser/ldap-browser.esm.js +915 -0
- package/static/browser/ldap-browser.esm.js.map +1 -0
- package/static/browser/ldap-browser.js +925 -0
- package/static/browser/ldap-browser.js.map +1 -0
- package/static/browser/ldap-browser.min.js +2 -0
- package/static/browser/ldap-browser.min.js.map +1 -0
- package/static/browser/ldap-unit-editor/components/UnitTree.js +0 -1
- package/static/browser/ldap-unit-editor/components/UnitTree.js.map +1 -1
- package/static/browser/ldap-unit-editor.esm.js +0 -1
- package/static/browser/ldap-unit-editor.esm.js.map +1 -1
- package/static/browser/ldap-unit-editor.js +0 -1
- package/static/browser/ldap-unit-editor.js.map +1 -1
- package/static/browser/ldap-unit-editor.min.js.map +1 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# LDAP Raw Plugin (low-level browsing)
|
|
2
|
+
|
|
3
|
+
Read-only, low-level access to the directory itself: root DSE, schema, and entries addressed by their DN. Where the other LDAP plugins expose business objects (users, groups, organizations), this one exposes the tree as it is stored — the server side of a phpLDAPadmin-like interface.
|
|
4
|
+
|
|
5
|
+
The companion browser library is [`ldap-browser`](../../../client-development/browser/libraries.md#ldapbrowser).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Root DSE**: naming contexts, supported controls and extensions
|
|
10
|
+
- **Schema**: object classes, attribute types, syntaxes and matching rules, parsed from the subschema entry (RFC 4512) and cached
|
|
11
|
+
- **Entry read**: every attribute, operational ones included, binary values base64-encoded
|
|
12
|
+
- **Tree navigation**: direct children of a DN, with an optional "has children" probe
|
|
13
|
+
- **Raw search**: arbitrary base, scope and LDAP filter
|
|
14
|
+
- **Read-only**: this plugin registers no write route
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
### Environment Variables
|
|
19
|
+
|
|
20
|
+
- `DM_LDAP_RAW_BASE`: comma-separated subtrees to expose (default: `--ldap-base`)
|
|
21
|
+
- `DM_LDAP_RAW_HIDDEN_ATTRIBUTES`: comma-separated attributes never returned, on top of the credential ones (default: none)
|
|
22
|
+
- `DM_LDAP_RAW_SHOW_SECRETS`: serve credential attributes instead of hiding them (default: `false`)
|
|
23
|
+
- `DM_LDAP_RAW_MAX_RESULTS`: maximum entries returned by a search or a children listing (default: `200`)
|
|
24
|
+
- `DM_LDAP_RAW_SCHEMA_CACHE_TTL`: schema cache lifetime in seconds (default: `3600`)
|
|
25
|
+
|
|
26
|
+
### Command Line
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
ldap-rest \
|
|
30
|
+
--plugin core/ldap/raw \
|
|
31
|
+
--ldap-raw-base "ou=users,dc=example,dc=com" \
|
|
32
|
+
--ldap-raw-base "ou=groups,dc=example,dc=com" \
|
|
33
|
+
--ldap-raw-hidden-attribute employeeNumber \
|
|
34
|
+
--ldap-raw-max-results 500
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
With no `--ldap-raw-base`, the plugin exposes `--ldap-base`.
|
|
38
|
+
|
|
39
|
+
## API Endpoints
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
GET /api/v1/ldap/raw/bases # Exposed subtrees (roots of the tree)
|
|
43
|
+
GET /api/v1/ldap/raw/rootdse # Server capabilities
|
|
44
|
+
GET /api/v1/ldap/raw/schema # Parsed directory schema
|
|
45
|
+
GET /api/v1/ldap/raw/entry/{dn} # One entry, operational attributes included
|
|
46
|
+
GET /api/v1/ldap/raw/children/{dn} # Direct children of an entry
|
|
47
|
+
GET /api/v1/ldap/raw/search # Arbitrary search
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`{dn}` is the URL-encoded fully-qualified DN.
|
|
51
|
+
|
|
52
|
+
### Entry format
|
|
53
|
+
|
|
54
|
+
Attribute values are always arrays. A `binary` flag tells whether the values are base64-encoded octets rather than text:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"dn": "uid=alice,ou=users,dc=example,dc=com",
|
|
59
|
+
"attributes": {
|
|
60
|
+
"objectClass": { "values": ["top", "inetOrgPerson"], "binary": false },
|
|
61
|
+
"uid": { "values": ["alice"], "binary": false },
|
|
62
|
+
"cn": { "values": ["Alice Smith"], "binary": false },
|
|
63
|
+
"jpegPhoto": { "values": ["/9j/4AAQSkZJRg…"], "binary": true }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A value is flagged binary when the directory returned octets that are not valid UTF-8, or when the attribute type is binary.
|
|
69
|
+
|
|
70
|
+
Credential attributes are **not** part of the response by default — see [Hidden attributes](#hidden-attributes).
|
|
71
|
+
|
|
72
|
+
### Children
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
curl "$URL/api/v1/ldap/raw/children/$(urlencode 'dc=example,dc=com')?children=1"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"children": [
|
|
81
|
+
{
|
|
82
|
+
"dn": "ou=users,dc=example,dc=com",
|
|
83
|
+
"rdn": "ou=users",
|
|
84
|
+
"objectClass": ["organizationalUnit"],
|
|
85
|
+
"hasChildren": true
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"truncated": false
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`hasChildren` is only computed when the `children` query parameter is set: it costs one extra search per child.
|
|
93
|
+
|
|
94
|
+
At most `--ldap-raw-max-results` children are returned. `truncated` is true when the branch holds more, so a browsing UI can say so instead of silently showing a partial list — a branch of 5000 users must not look like a branch of 200.
|
|
95
|
+
|
|
96
|
+
### Search
|
|
97
|
+
|
|
98
|
+
| Parameter | Default | Description |
|
|
99
|
+
| ------------ | ------------------------ | ---------------------------------------------- |
|
|
100
|
+
| `base` | first exposed base | Search base, must be inside an exposed subtree |
|
|
101
|
+
| `scope` | `sub` | `base`, `one` or `sub` |
|
|
102
|
+
| `filter` | `(objectClass=*)` | LDAP filter |
|
|
103
|
+
| `attributes` | `*` | Comma-separated attribute list |
|
|
104
|
+
| `limit` | `--ldap-raw-max-results` | Capped by `--ldap-raw-max-results` |
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
curl "$URL/api/v1/ldap/raw/search?base=ou=users,dc=example,dc=com&filter=(mail=*@example.com)&attributes=uid,mail"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{ "entries": [...], "truncated": false }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`truncated` is true when the limit cut the result set short.
|
|
115
|
+
|
|
116
|
+
The filter is parsed before the search runs, so a malformed one comes back as a 400 naming it rather than as a server error — a search box receives whatever the user typed, and `gov` is a typo, not a server fault:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"error": "Invalid LDAP filter \"gov\": Invalid expression: gov. A filter must be parenthesised, e.g. (cn=foo) or (|(cn=*foo*)(ou=*foo*))"
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Schema
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"objectClasses": [
|
|
129
|
+
{
|
|
130
|
+
"oid": "2.5.6.6",
|
|
131
|
+
"names": ["person"],
|
|
132
|
+
"desc": "RFC2256: a person",
|
|
133
|
+
"kind": "STRUCTURAL",
|
|
134
|
+
"sup": ["top"],
|
|
135
|
+
"must": ["sn", "cn"],
|
|
136
|
+
"may": ["userPassword", "telephoneNumber", "seeAlso", "description"],
|
|
137
|
+
"obsolete": false
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
"attributeTypes": [
|
|
141
|
+
{
|
|
142
|
+
"oid": "2.5.4.3",
|
|
143
|
+
"names": ["cn", "commonName"],
|
|
144
|
+
"sup": "name",
|
|
145
|
+
"singleValue": false,
|
|
146
|
+
"noUserModification": false,
|
|
147
|
+
"usage": "userApplications",
|
|
148
|
+
"obsolete": false,
|
|
149
|
+
"collective": false
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
"syntaxes": [...],
|
|
153
|
+
"matchingRules": [...]
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Inheritance is **not** flattened: a client resolving the attributes of an entry must walk the `sup` chains. `SchemaView` (browser) and `SchemaIndex` (`src/lib/ldapSchema.ts`, server) do it for you.
|
|
158
|
+
|
|
159
|
+
## Hidden attributes
|
|
160
|
+
|
|
161
|
+
Attributes holding credential material are stripped from **every** response — entry reads and searches alike — before it leaves the server. Asking for one explicitly (`?attributes=uid,userPassword`) does not get around the filter: the removal happens when the entry is serialised, not when the query is built.
|
|
162
|
+
|
|
163
|
+
The built-in list covers the usual OpenLDAP, Samba, Kerberos and Active Directory spellings:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
userPassword, authPassword, pwdHistory, sambaNTPassword, sambaLMPassword,
|
|
167
|
+
sambaPasswordHistory, krbPrincipalKey, krbExtraData, krbPwdHistory,
|
|
168
|
+
unicodePwd, dbcsPwd, lmPwdHistory, ntPwdHistory, supplementalCredentials,
|
|
169
|
+
userPKCS12
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
A password hash served over HTTP is an offline cracking target, and browsing a directory does not require reading one — hence hidden by default, visible only on request:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
--ldap-raw-show-secrets true # logs a warning at startup
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Site-specific attributes are added with `--ldap-raw-hidden-attribute`, which stacks on top of the built-in list rather than replacing it:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
--ldap-raw-hidden-attribute twakeApiKey --ldap-raw-hidden-attribute employeeNumber
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Note that hiding an attribute here keeps it out of the API, not out of the directory: an account able to bind to LDAP directly still reads it. This is a guard against casual exposure through the browsing UI, not an access control — use [authz-per-branch](../auth/authz-per-branch.md) or directory ACLs for that.
|
|
185
|
+
|
|
186
|
+
## Security
|
|
187
|
+
|
|
188
|
+
Access is restricted in three independent layers:
|
|
189
|
+
|
|
190
|
+
1. **Exposed bases** — any DN outside `--ldap-raw-base` is refused with a 403, whatever the caller's rights.
|
|
191
|
+
2. **Authorization hooks** — every entry read, children listing and search goes through `server.ldap`, so the authorization plugins ([authz-per-branch](../auth/authz-per-branch.md), authz-dynamic, …) apply exactly as on the high-level APIs.
|
|
192
|
+
3. **Hidden attributes** — credential attributes, plus anything in `--ldap-raw-hidden-attribute`, are removed from every response, including searches. See [Hidden attributes](#hidden-attributes).
|
|
193
|
+
|
|
194
|
+
Two exceptions, both deliberate: the **root DSE** and the **schema** are read with the service account, not with the caller's rights. They are directory metadata and carry no user data; reading them with a restricted account would leave the UI unable to name and type attributes. Do not expose this plugin publicly if your directory publishes sensitive information in its root DSE.
|
|
195
|
+
|
|
196
|
+
Combine with an authentication plugin ([token](../auth/token.md), [oidc](../auth/oidc.md), …) — this plugin adds no authentication of its own.
|
|
197
|
+
|
|
198
|
+
## How It Works
|
|
199
|
+
|
|
200
|
+
- **Schema fetch**: the root DSE is read for `subschemaSubentry` (falling back to `cn=Subschema`), then the subschema entry is fetched and parsed once per `--ldap-raw-schema-cache-ttl`. Concurrent callers share a single fetch.
|
|
201
|
+
- **Operational attributes**: entries are read with `*` and `+`. When the server rejects `+`, the read is retried with `*` alone.
|
|
202
|
+
- **Missing entries**: the LDAP "no such object" result (code 32) is mapped to a 404.
|
|
203
|
+
|
|
204
|
+
## Limitations
|
|
205
|
+
|
|
206
|
+
- **Read-only.** Creating, modifying, renaming and deleting entries is not exposed yet.
|
|
207
|
+
- **No LDIF** import or export.
|
|
208
|
+
- **No paging**: searches and children listings are capped by `--ldap-raw-max-results` and flagged as `truncated`; there is no cursor to fetch the rest. Use a narrower search to reach entries beyond the cap.
|
|
209
|
+
|
|
210
|
+
## Usage Example
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
ldap-rest \
|
|
214
|
+
--plugin core/auth/token --auth-token "$TOKEN" \
|
|
215
|
+
--plugin core/ldap/raw \
|
|
216
|
+
--plugin core/static
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The demo page ([`examples/web/ldap-browser.html`](../../../../examples/web/ldap-browser.html)) is then served at
|
|
220
|
+
`/static/examples/web/ldap-browser.html`.
|
|
221
|
+
|
|
222
|
+
## See Also
|
|
223
|
+
|
|
224
|
+
- [ldap-browser client library](../../../client-development/browser/libraries.md#ldapbrowser)
|
|
225
|
+
- [organizations plugin](organizations.md) — high-level tree of organizational units
|
|
226
|
+
- [authz-per-branch plugin](../auth/authz-per-branch.md) — branch-level access control
|
package/examples/web/README.md
CHANGED
|
@@ -82,6 +82,23 @@ Manage organizational units.
|
|
|
82
82
|
- Move units
|
|
83
83
|
- Create/delete units
|
|
84
84
|
|
|
85
|
+
### 🗂️ LDAP Browser (`ldap-browser.html`)
|
|
86
|
+
|
|
87
|
+
Low-level, read-only view of the directory, in the spirit of phpLDAPadmin.
|
|
88
|
+
|
|
89
|
+
**Features:**
|
|
90
|
+
|
|
91
|
+
- Lazy tree of the whole directory
|
|
92
|
+
- Raw entry content, operational attributes included
|
|
93
|
+
- Schema annotations: mandatory attributes, descriptions, unknown attributes flagged
|
|
94
|
+
- LDAP filter search with scope selection
|
|
95
|
+
|
|
96
|
+
Requires the `core/ldap/raw` plugin:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm start -- --plugin core/ldap/raw
|
|
100
|
+
```
|
|
101
|
+
|
|
85
102
|
## Running Examples
|
|
86
103
|
|
|
87
104
|
1. **Start the server:**
|
|
@@ -109,6 +126,7 @@ Manage organizational units.
|
|
|
109
126
|
http://localhost:8081/static/examples/web/ldap-user-editor.html
|
|
110
127
|
http://localhost:8081/static/examples/web/ldap-group-editor.html
|
|
111
128
|
http://localhost:8081/static/examples/web/ldap-unit-editor.html
|
|
129
|
+
http://localhost:8081/static/examples/web/ldap-browser.html
|
|
112
130
|
```
|
|
113
131
|
|
|
114
132
|
## Using Browser Libraries in Your Application
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>LDAP Browser Demo</title>
|
|
7
|
+
|
|
8
|
+
<!-- Google Fonts -->
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<link
|
|
14
|
+
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
|
15
|
+
rel="stylesheet"
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
<!-- Shared CSS -->
|
|
19
|
+
<link rel="stylesheet" href="/static/browser/common-demo.css" />
|
|
20
|
+
<!-- Library CSS -->
|
|
21
|
+
<link rel="stylesheet" href="/static/browser/ldap-browser.css" />
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
:root {
|
|
25
|
+
--primary-color: #3949ab;
|
|
26
|
+
--primary-color-dark: #283593;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
background: linear-gradient(135deg, #8ea7e9 0%, #3949ab 100%);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.demo-header h1 {
|
|
34
|
+
color: #3949ab;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#ldap-browser {
|
|
38
|
+
height: 70vh;
|
|
39
|
+
min-height: 520px;
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
44
|
+
<div class="demo-container">
|
|
45
|
+
<div class="demo-header">
|
|
46
|
+
<h1>🗂️ LDAP Browser</h1>
|
|
47
|
+
<p>
|
|
48
|
+
Low-level, read-only view of the directory: tree, raw entries and
|
|
49
|
+
schema
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div id="status-container"></div>
|
|
54
|
+
|
|
55
|
+
<div id="ldap-browser"></div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Shared utilities -->
|
|
59
|
+
<script src="/static/browser/shared.js"></script>
|
|
60
|
+
<!-- Library JS -->
|
|
61
|
+
<script src="/static/browser/ldap-browser.js"></script>
|
|
62
|
+
|
|
63
|
+
<script>
|
|
64
|
+
const { LdapBrowser } = window.LdapBrowser;
|
|
65
|
+
const { showStatus } = window.MiniDmShared;
|
|
66
|
+
|
|
67
|
+
async function initBrowser() {
|
|
68
|
+
try {
|
|
69
|
+
const browser = new LdapBrowser({
|
|
70
|
+
containerId: 'ldap-browser',
|
|
71
|
+
apiBaseUrl: window.location.origin,
|
|
72
|
+
onEntrySelected: dn => console.log('Entry selected:', dn),
|
|
73
|
+
onError: error => {
|
|
74
|
+
console.error('Browser error:', error);
|
|
75
|
+
showStatus(
|
|
76
|
+
'status-container',
|
|
77
|
+
`Error: ${error.message}`,
|
|
78
|
+
'error'
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await browser.init();
|
|
84
|
+
showStatus('status-container', 'LDAP Browser loaded', 'success');
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Failed to initialize browser:', error);
|
|
87
|
+
showStatus('status-container', `Error: ${error.message}`, 'error');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
initBrowser();
|
|
92
|
+
</script>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ldap-rest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Light directory manager",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Identity-Manager"
|
|
@@ -132,6 +132,10 @@
|
|
|
132
132
|
"types": "./dist/src/plugins/ldap/passwordPolicy.d.ts",
|
|
133
133
|
"import": "./dist/plugins/ldap/passwordPolicy.js"
|
|
134
134
|
},
|
|
135
|
+
"./plugin-ldap-raw": {
|
|
136
|
+
"types": "./dist/src/plugins/ldap/raw.d.ts",
|
|
137
|
+
"import": "./dist/plugins/ldap/raw.js"
|
|
138
|
+
},
|
|
135
139
|
"./plugin-ldap-trash": {
|
|
136
140
|
"types": "./dist/src/plugins/ldap/trash.d.ts",
|
|
137
141
|
"import": "./dist/plugins/ldap/trash.js"
|
|
@@ -232,6 +236,34 @@
|
|
|
232
236
|
"types": "./dist/src/abstract/twakePlugin.d.ts",
|
|
233
237
|
"import": "./dist/abstract/twakePlugin.js"
|
|
234
238
|
},
|
|
239
|
+
"./browser-ldap-browser-ldapbrowser": {
|
|
240
|
+
"types": "./dist/src/browser/ldap-browser/LdapBrowser.d.ts",
|
|
241
|
+
"import": "./static/browser/ldap-browser/LdapBrowser.js"
|
|
242
|
+
},
|
|
243
|
+
"./browser-ldap-browser-api-rawapiclient": {
|
|
244
|
+
"types": "./dist/src/browser/ldap-browser/api/RawApiClient.d.ts",
|
|
245
|
+
"import": "./static/browser/ldap-browser/api/RawApiClient.js"
|
|
246
|
+
},
|
|
247
|
+
"./browser-ldap-browser-components-entrytree": {
|
|
248
|
+
"types": "./dist/src/browser/ldap-browser/components/EntryTree.d.ts",
|
|
249
|
+
"import": "./static/browser/ldap-browser/components/EntryTree.js"
|
|
250
|
+
},
|
|
251
|
+
"./browser-ldap-browser-components-entryview": {
|
|
252
|
+
"types": "./dist/src/browser/ldap-browser/components/EntryView.d.ts",
|
|
253
|
+
"import": "./static/browser/ldap-browser/components/EntryView.js"
|
|
254
|
+
},
|
|
255
|
+
"./browser-ldap-browser-index": {
|
|
256
|
+
"types": "./dist/src/browser/ldap-browser/index.d.ts",
|
|
257
|
+
"import": "./static/browser/ldap-browser/index.js"
|
|
258
|
+
},
|
|
259
|
+
"./browser-ldap-browser-schema": {
|
|
260
|
+
"types": "./dist/src/browser/ldap-browser/schema.d.ts",
|
|
261
|
+
"import": "./static/browser/ldap-browser/schema.js"
|
|
262
|
+
},
|
|
263
|
+
"./browser-ldap-browser-types": {
|
|
264
|
+
"types": "./dist/src/browser/ldap-browser/types.d.ts",
|
|
265
|
+
"import": "./static/browser/ldap-browser/types.js"
|
|
266
|
+
},
|
|
235
267
|
"./browser-ldap-group-editor-ldapgroupeditor": {
|
|
236
268
|
"types": "./dist/src/browser/ldap-group-editor/LdapGroupEditor.d.ts",
|
|
237
269
|
"import": "./static/browser/ldap-group-editor/LdapGroupEditor.js"
|
|
@@ -578,6 +610,8 @@
|
|
|
578
610
|
"check": "npm run check:ts && npm run lint && npm run format:check",
|
|
579
611
|
"check:ts": "tsc --noEmit",
|
|
580
612
|
"clean": "rimraf dist/* static/browser/* coverage",
|
|
613
|
+
"coverage": "c8 --config .c8rc.json mocha",
|
|
614
|
+
"coverage:check": "npm run check:ts && c8 --config .c8rc.json --exclude 'src/browser/**' --check-coverage --statements 84 --lines 84 --branches 75 --functions 86 mocha",
|
|
581
615
|
"dev": "npm run start:dev",
|
|
582
616
|
"fix": "npm run lint:fix && npm run format:fix",
|
|
583
617
|
"format:check": "prettier --check .",
|
|
@@ -626,6 +660,7 @@
|
|
|
626
660
|
"@types/supertest": "^6.0.3",
|
|
627
661
|
"@typescript-eslint/eslint-plugin": "^8.44.0",
|
|
628
662
|
"@typescript-eslint/parser": "^8.44.0",
|
|
663
|
+
"c8": "^10.1.3",
|
|
629
664
|
"chai": "^6.0.1",
|
|
630
665
|
"eslint": "^9.35.0",
|
|
631
666
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -633,6 +668,7 @@
|
|
|
633
668
|
"eslint-plugin-node": "^11.1.0",
|
|
634
669
|
"eslint-plugin-prettier": "^5.5.4",
|
|
635
670
|
"fast-glob": "^3.3.3",
|
|
671
|
+
"globals": "^14.0.0",
|
|
636
672
|
"js-yaml": "^4.1.1",
|
|
637
673
|
"mocha": "^11.7.2",
|
|
638
674
|
"nock": "^14.0.10",
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { escapeHtml } from '../shared/utils/dom.js';
|
|
2
|
+
import { RawApiClient } from './api/RawApiClient.js';
|
|
3
|
+
import { EntryTree } from './components/EntryTree.js';
|
|
4
|
+
import { EntryView } from './components/EntryView.js';
|
|
5
|
+
import { SchemaView } from './schema.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Low-level LDAP browser: a tree of the directory on the left, the raw
|
|
9
|
+
* content of the selected entry on the right, and a filter search on top.
|
|
10
|
+
*
|
|
11
|
+
* Read-only: it renders what the `core/ldap/raw` plugin serves.
|
|
12
|
+
*
|
|
13
|
+
* @module browser/ldap-browser/LdapBrowser
|
|
14
|
+
*/
|
|
15
|
+
class LdapBrowser {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.container = null;
|
|
18
|
+
this.tree = null;
|
|
19
|
+
this.entryView = null;
|
|
20
|
+
this.schema = null;
|
|
21
|
+
this.bases = [];
|
|
22
|
+
this.currentDn = null;
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.api = new RawApiClient(options.apiBaseUrl || '', options.authToken);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build the interface, load the schema and open the first base.
|
|
28
|
+
*
|
|
29
|
+
* @throws Error when the container does not exist or the API is unreachable
|
|
30
|
+
*/
|
|
31
|
+
async init() {
|
|
32
|
+
const container = document.getElementById(this.options.containerId);
|
|
33
|
+
if (!container)
|
|
34
|
+
throw new Error(`Container element with id '${this.options.containerId}' not found`);
|
|
35
|
+
this.container = container;
|
|
36
|
+
this.bases = await this.api.getBases();
|
|
37
|
+
this.render();
|
|
38
|
+
// The schema only decorates the display: a directory that refuses it
|
|
39
|
+
// must not prevent browsing
|
|
40
|
+
try {
|
|
41
|
+
this.schema = new SchemaView(await this.api.getSchema());
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
this.reportError(err);
|
|
45
|
+
}
|
|
46
|
+
const treeContainer = container.querySelector('.ldap-browser__tree');
|
|
47
|
+
const entryContainer = container.querySelector('.ldap-browser__entry');
|
|
48
|
+
if (!treeContainer || !entryContainer)
|
|
49
|
+
return;
|
|
50
|
+
this.entryView = new EntryView(entryContainer, this.schema);
|
|
51
|
+
this.entryView.renderEmpty();
|
|
52
|
+
this.tree = new EntryTree(treeContainer, this.api, dn => this.showEntry(dn), error => this.reportError(error));
|
|
53
|
+
await this.tree.init(this.bases);
|
|
54
|
+
this.bindToolbar();
|
|
55
|
+
const initial = this.options.initialDn || this.bases[0];
|
|
56
|
+
if (initial)
|
|
57
|
+
await this.tree.revealAndSelect(initial);
|
|
58
|
+
}
|
|
59
|
+
/** Release the listeners held by the tree */
|
|
60
|
+
destroy() {
|
|
61
|
+
this.tree?.destroy();
|
|
62
|
+
this.tree = null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* DN currently displayed.
|
|
66
|
+
*
|
|
67
|
+
* @returns selected DN, or null when none
|
|
68
|
+
*/
|
|
69
|
+
getCurrentDn() {
|
|
70
|
+
return this.currentDn;
|
|
71
|
+
}
|
|
72
|
+
/** Draw the static layout */
|
|
73
|
+
render() {
|
|
74
|
+
if (!this.container)
|
|
75
|
+
return;
|
|
76
|
+
this.container.classList.add('ldap-browser-container');
|
|
77
|
+
this.container.innerHTML = `
|
|
78
|
+
<div class="ldap-browser">
|
|
79
|
+
<div class="ldap-browser__toolbar">
|
|
80
|
+
<input class="ldap-browser__filter" type="text"
|
|
81
|
+
placeholder="LDAP filter, e.g. (uid=alice)" />
|
|
82
|
+
<select class="ldap-browser__scope">
|
|
83
|
+
<option value="sub">sub</option>
|
|
84
|
+
<option value="one">one</option>
|
|
85
|
+
<option value="base">base</option>
|
|
86
|
+
</select>
|
|
87
|
+
<select class="ldap-browser__base">
|
|
88
|
+
${this.bases
|
|
89
|
+
.map(base => `<option value="${escapeHtml(base)}">${escapeHtml(base)}</option>`)
|
|
90
|
+
.join('')}
|
|
91
|
+
</select>
|
|
92
|
+
<button class="ldap-browser__search" type="button">
|
|
93
|
+
<span class="material-icons">search</span> Search
|
|
94
|
+
</button>
|
|
95
|
+
<button class="ldap-browser__refresh" type="button" title="Reload">
|
|
96
|
+
<span class="material-icons">refresh</span>
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="ldap-browser__body">
|
|
100
|
+
<div class="ldap-browser__tree"></div>
|
|
101
|
+
<div class="ldap-browser__entry"></div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="ldap-browser__results" hidden></div>
|
|
104
|
+
</div>`;
|
|
105
|
+
}
|
|
106
|
+
/** Wire the search and refresh controls */
|
|
107
|
+
bindToolbar() {
|
|
108
|
+
const filter = this.container?.querySelector('.ldap-browser__filter');
|
|
109
|
+
const searchButton = this.container?.querySelector('.ldap-browser__search');
|
|
110
|
+
const refreshButton = this.container?.querySelector('.ldap-browser__refresh');
|
|
111
|
+
const results = this.container?.querySelector('.ldap-browser__results');
|
|
112
|
+
searchButton?.addEventListener('click', () => void this.runSearch());
|
|
113
|
+
filter?.addEventListener('keydown', event => {
|
|
114
|
+
if (event.key === 'Enter')
|
|
115
|
+
void this.runSearch();
|
|
116
|
+
});
|
|
117
|
+
refreshButton?.addEventListener('click', () => {
|
|
118
|
+
this.tree?.invalidate();
|
|
119
|
+
if (this.currentDn)
|
|
120
|
+
void this.showEntry(this.currentDn);
|
|
121
|
+
});
|
|
122
|
+
results?.addEventListener('click', event => {
|
|
123
|
+
const target = event.target.closest('[data-dn]');
|
|
124
|
+
if (target?.dataset.dn)
|
|
125
|
+
void this.tree?.revealAndSelect(target.dataset.dn);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Load an entry and display it.
|
|
130
|
+
*
|
|
131
|
+
* @param dn DN of the entry
|
|
132
|
+
*/
|
|
133
|
+
async showEntry(dn) {
|
|
134
|
+
if (!this.entryView)
|
|
135
|
+
return;
|
|
136
|
+
this.currentDn = dn;
|
|
137
|
+
this.entryView.renderLoading();
|
|
138
|
+
try {
|
|
139
|
+
const entry = await this.api.getEntry(dn);
|
|
140
|
+
this.entryView.render(entry);
|
|
141
|
+
this.options.onEntrySelected?.(dn);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
this.entryView.renderError(err.message);
|
|
145
|
+
this.reportError(err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Run the search typed in the toolbar and list the matching DNs */
|
|
149
|
+
async runSearch() {
|
|
150
|
+
const filter = this.container?.querySelector('.ldap-browser__filter')
|
|
151
|
+
?.value || '';
|
|
152
|
+
const scope = this.container?.querySelector('.ldap-browser__scope')
|
|
153
|
+
?.value || 'sub';
|
|
154
|
+
const base = this.container?.querySelector('.ldap-browser__base')
|
|
155
|
+
?.value || this.bases[0];
|
|
156
|
+
const results = this.container?.querySelector('.ldap-browser__results');
|
|
157
|
+
if (!results)
|
|
158
|
+
return;
|
|
159
|
+
if (!filter.trim()) {
|
|
160
|
+
results.hidden = true;
|
|
161
|
+
results.innerHTML = '';
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
results.hidden = false;
|
|
165
|
+
results.innerHTML = '<p>Searching…</p>';
|
|
166
|
+
try {
|
|
167
|
+
const found = await this.api.search({
|
|
168
|
+
base,
|
|
169
|
+
scope: scope,
|
|
170
|
+
filter,
|
|
171
|
+
attributes: ['dn'],
|
|
172
|
+
});
|
|
173
|
+
results.innerHTML = found.entries.length
|
|
174
|
+
? `<p>${found.entries.length} result(s)${found.truncated ? ' (truncated)' : ''}</p>
|
|
175
|
+
<ul>${found.entries
|
|
176
|
+
.map(entry => `<li><a href="#" data-dn="${escapeHtml(entry.dn)}">${escapeHtml(entry.dn)}</a></li>`)
|
|
177
|
+
.join('')}</ul>`
|
|
178
|
+
: '<p>No result</p>';
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
results.innerHTML = `<p class="ldap-browser__error">${escapeHtml(err.message)}</p>`;
|
|
182
|
+
this.reportError(err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Forward an error to the host page, or log it when no handler was given.
|
|
187
|
+
*
|
|
188
|
+
* @param error error to report
|
|
189
|
+
*/
|
|
190
|
+
reportError(error) {
|
|
191
|
+
if (this.options.onError)
|
|
192
|
+
this.options.onError(error);
|
|
193
|
+
else
|
|
194
|
+
console.error('[ldap-browser]', error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { LdapBrowser };
|
|
199
|
+
//# sourceMappingURL=LdapBrowser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LdapBrowser.js","sources":["../../../../src/browser/ldap-browser/LdapBrowser.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|