ldap-rest 0.4.7 → 0.6.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/lib/utils.js +1 -1
- package/dist/plugins/auth/authzPerRoute.js +1 -1
- package/dist/plugins/auth/rateLimit.js +1 -1
- package/dist/plugins/ldap/groups.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/plugins/twake/appAccountsConsistency.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 +7 -0
- package/dist/src/lib/auth/base.d.ts +82 -1
- package/dist/src/lib/ldapSchema.d.ts +159 -0
- package/dist/src/lib/utils.d.ts +4 -0
- package/dist/src/plugins/ldap/raw.d.ts +251 -0
- package/dist/src/plugins/twake/appAccountsConsistency.d.ts +40 -2
- package/docs/client-development/browser/libraries.md +142 -1
- package/docs/usage/configuration.md +40 -5
- 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 +35 -2
- 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
package/dist/src/lib/utils.d.ts
CHANGED
|
@@ -73,6 +73,8 @@ export declare function escapeDnValue(value: string): string;
|
|
|
73
73
|
* unescapeDnValue('user\\+admin')
|
|
74
74
|
* // => 'user+admin'
|
|
75
75
|
* ```
|
|
76
|
+
*
|
|
77
|
+
* @throws BadRequestError if `value` is not a string
|
|
76
78
|
*/
|
|
77
79
|
export declare function unescapeDnValue(value: string): string;
|
|
78
80
|
/**
|
|
@@ -108,6 +110,8 @@ export declare function validateDnValue(value: string, fieldName: string): void;
|
|
|
108
110
|
* parseDn('cn=Smith\\, John,ou=users,dc=example,dc=com')
|
|
109
111
|
* // => ['cn=Smith\\, John', 'ou=users', 'dc=example', 'dc=com']
|
|
110
112
|
* ```
|
|
113
|
+
*
|
|
114
|
+
* @throws BadRequestError if `dn` is not a string
|
|
111
115
|
*/
|
|
112
116
|
export declare function parseDn(dn: string): string[];
|
|
113
117
|
/**
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/ldap/raw
|
|
3
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
+
*
|
|
5
|
+
* Low-level LDAP browsing API (read-only).
|
|
6
|
+
*
|
|
7
|
+
* Where the other plugins expose business objects (users, groups,
|
|
8
|
+
* organizations), this one exposes the directory itself: the root DSE, the
|
|
9
|
+
* schema, and entries addressed by their DN. It is the server side of a
|
|
10
|
+
* phpLDAPadmin-like interface.
|
|
11
|
+
*
|
|
12
|
+
* Every directory access goes through `server.ldap`, so the authorization,
|
|
13
|
+
* trash and logging hooks apply exactly as they do for the high-level APIs.
|
|
14
|
+
* Root DSE and schema are directory metadata rather than user data and are
|
|
15
|
+
* read with the service account, so that a UI can render attribute names and
|
|
16
|
+
* types even when the caller is restricted to a branch.
|
|
17
|
+
*/
|
|
18
|
+
import type { Express, Request } from 'express';
|
|
19
|
+
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
20
|
+
import type { DM } from '../../bin';
|
|
21
|
+
import { SchemaIndex, type LdapSchema } from '../../lib/ldapSchema';
|
|
22
|
+
/**
|
|
23
|
+
* Shared OpenAPI schemas surfaced by this plugin. Picked up by
|
|
24
|
+
* scripts/generate-openapi.ts and merged into `components.schemas`.
|
|
25
|
+
*
|
|
26
|
+
* @openapi-component
|
|
27
|
+
* RawAttribute:
|
|
28
|
+
* type: object
|
|
29
|
+
* description: |
|
|
30
|
+
* Values of a single attribute. When `binary` is true the values are
|
|
31
|
+
* base64-encoded octets rather than text.
|
|
32
|
+
* required: [values, binary]
|
|
33
|
+
* properties:
|
|
34
|
+
* values:
|
|
35
|
+
* type: array
|
|
36
|
+
* items: { type: string }
|
|
37
|
+
* example: [Alice Smith]
|
|
38
|
+
* binary: { type: boolean, example: false }
|
|
39
|
+
* RawEntry:
|
|
40
|
+
* type: object
|
|
41
|
+
* description: A directory entry with all its attributes, as stored.
|
|
42
|
+
* required: [dn, attributes]
|
|
43
|
+
* properties:
|
|
44
|
+
* dn:
|
|
45
|
+
* type: string
|
|
46
|
+
* example: uid=alice,ou=users,dc=example,dc=com
|
|
47
|
+
* attributes:
|
|
48
|
+
* type: object
|
|
49
|
+
* additionalProperties: { $ref: '#/components/schemas/RawAttribute' }
|
|
50
|
+
* example:
|
|
51
|
+
* dn: uid=alice,ou=users,dc=example,dc=com
|
|
52
|
+
* attributes:
|
|
53
|
+
* objectClass: { values: [top, inetOrgPerson], binary: false }
|
|
54
|
+
* uid: { values: [alice], binary: false }
|
|
55
|
+
* cn: { values: [Alice Smith], binary: false }
|
|
56
|
+
* RawChild:
|
|
57
|
+
* type: object
|
|
58
|
+
* description: Direct child of an entry, as needed to draw a tree node.
|
|
59
|
+
* required: [dn, rdn, objectClass, hasChildren]
|
|
60
|
+
* properties:
|
|
61
|
+
* dn: { type: string, example: uid=alice,ou=users,dc=example,dc=com }
|
|
62
|
+
* rdn: { type: string, example: uid=alice }
|
|
63
|
+
* objectClass:
|
|
64
|
+
* type: array
|
|
65
|
+
* items: { type: string }
|
|
66
|
+
* example: [top, inetOrgPerson]
|
|
67
|
+
* hasChildren:
|
|
68
|
+
* type: boolean
|
|
69
|
+
* description: |
|
|
70
|
+
* Whether the entry has at least one child. Only computed when the
|
|
71
|
+
* `children` query parameter is set, otherwise always false.
|
|
72
|
+
* example: false
|
|
73
|
+
*/
|
|
74
|
+
/** JSON representation of one attribute of an entry */
|
|
75
|
+
export interface RawAttribute {
|
|
76
|
+
/** Values, base64-encoded when `binary` is true */
|
|
77
|
+
values: string[];
|
|
78
|
+
binary: boolean;
|
|
79
|
+
}
|
|
80
|
+
/** JSON representation of a directory entry */
|
|
81
|
+
export interface RawEntry {
|
|
82
|
+
dn: string;
|
|
83
|
+
attributes: Record<string, RawAttribute>;
|
|
84
|
+
}
|
|
85
|
+
/** Direct child of an entry, as needed to draw a tree node */
|
|
86
|
+
export interface RawChild {
|
|
87
|
+
dn: string;
|
|
88
|
+
rdn: string;
|
|
89
|
+
objectClass: string[];
|
|
90
|
+
hasChildren: boolean;
|
|
91
|
+
}
|
|
92
|
+
/** Direct children of an entry, with the truncation flag */
|
|
93
|
+
export interface RawChildren {
|
|
94
|
+
children: RawChild[];
|
|
95
|
+
/** True when `ldap_raw_max_results` cut the listing short */
|
|
96
|
+
truncated: boolean;
|
|
97
|
+
}
|
|
98
|
+
/** Result of a raw search */
|
|
99
|
+
export interface RawSearchResult {
|
|
100
|
+
entries: RawEntry[];
|
|
101
|
+
/** True when `ldap_raw_max_results` cut the result set short */
|
|
102
|
+
truncated: boolean;
|
|
103
|
+
}
|
|
104
|
+
declare const VALID_SCOPES: readonly ["base", "one", "sub"];
|
|
105
|
+
type Scope = (typeof VALID_SCOPES)[number];
|
|
106
|
+
export default class LdapRaw extends DmPlugin {
|
|
107
|
+
name: string;
|
|
108
|
+
roles: Role[];
|
|
109
|
+
/** Subtrees the API is allowed to expose */
|
|
110
|
+
bases: string[];
|
|
111
|
+
/** Attributes never returned, whatever the request (lowercase) */
|
|
112
|
+
hiddenAttributes: Set<string>;
|
|
113
|
+
/** True when credential attributes are served instead of being hidden */
|
|
114
|
+
showSecrets: boolean;
|
|
115
|
+
/** Maximum entries returned by a search or a children listing */
|
|
116
|
+
maxResults: number;
|
|
117
|
+
private schemaCacheTtl;
|
|
118
|
+
private schemaCache?;
|
|
119
|
+
private schemaPromise?;
|
|
120
|
+
constructor(server: DM);
|
|
121
|
+
/**
|
|
122
|
+
* Reject any DN outside the configured bases. Authorization plugins narrow
|
|
123
|
+
* access further; this is the coarse boundary of the API itself.
|
|
124
|
+
*
|
|
125
|
+
* @param dn DN to check
|
|
126
|
+
* @throws BadRequestError when the DN is empty
|
|
127
|
+
* @throws ForbiddenError when the DN is outside every configured base
|
|
128
|
+
*/
|
|
129
|
+
checkDn(dn: string): void;
|
|
130
|
+
/**
|
|
131
|
+
* Reject a malformed LDAP filter before it reaches the directory. Parsing
|
|
132
|
+
* it here turns what would surface as an opaque server error into a 400
|
|
133
|
+
* naming the offending expression — a search box hands us whatever the
|
|
134
|
+
* user typed, and `gov` is a filter typo, not a server fault.
|
|
135
|
+
*
|
|
136
|
+
* @param filter filter to validate
|
|
137
|
+
* @throws BadRequestError when the filter cannot be parsed
|
|
138
|
+
*/
|
|
139
|
+
checkFilter(filter: string): void;
|
|
140
|
+
/**
|
|
141
|
+
* Convert an ldapts entry into its JSON representation: values are always
|
|
142
|
+
* arrays and hidden attributes are dropped.
|
|
143
|
+
*
|
|
144
|
+
* ldapts hands back a `Buffer` for values it could not decode as UTF-8 and
|
|
145
|
+
* for the attribute types it knows to be binary; those are the ones that
|
|
146
|
+
* need base64. A hashed `userPassword` is valid UTF-8 and stays readable,
|
|
147
|
+
* which is what an administration UI wants to display.
|
|
148
|
+
*
|
|
149
|
+
* @param entry entry as returned by ldapts
|
|
150
|
+
* @returns JSON-serializable entry
|
|
151
|
+
*/
|
|
152
|
+
formatEntry(entry: Record<string, unknown> & {
|
|
153
|
+
dn: string;
|
|
154
|
+
}): RawEntry;
|
|
155
|
+
/**
|
|
156
|
+
* Run a search and turn the directory's "no such object" result code (32)
|
|
157
|
+
* into a 404, instead of letting it surface as a server error.
|
|
158
|
+
*
|
|
159
|
+
* @param base search base
|
|
160
|
+
* @param options search options
|
|
161
|
+
* @param req incoming request, forwarded to the authorization hooks
|
|
162
|
+
* @returns the search result
|
|
163
|
+
* @throws NotFoundError when the base does not exist
|
|
164
|
+
*/
|
|
165
|
+
private searchDirectory;
|
|
166
|
+
/**
|
|
167
|
+
* Read the root DSE: naming contexts, supported controls and extensions,
|
|
168
|
+
* and the DN of the subschema entry.
|
|
169
|
+
*
|
|
170
|
+
* @returns root DSE as a JSON entry (its `dn` is the empty string)
|
|
171
|
+
*/
|
|
172
|
+
getRootDse(): Promise<RawEntry>;
|
|
173
|
+
/**
|
|
174
|
+
* Locate the subschema entry advertised by the root DSE, falling back to
|
|
175
|
+
* the conventional `cn=Subschema` when the server does not advertise one.
|
|
176
|
+
*
|
|
177
|
+
* @returns DN of the subschema entry
|
|
178
|
+
*/
|
|
179
|
+
getSubschemaDn(): Promise<string>;
|
|
180
|
+
/**
|
|
181
|
+
* Fetch and parse the directory schema, with a cache: it is large and
|
|
182
|
+
* changes rarely. Concurrent callers share a single fetch.
|
|
183
|
+
*
|
|
184
|
+
* @returns indexed schema
|
|
185
|
+
*/
|
|
186
|
+
getSchemaIndex(): Promise<SchemaIndex>;
|
|
187
|
+
/**
|
|
188
|
+
* Parsed schema, as served to clients.
|
|
189
|
+
*
|
|
190
|
+
* @returns object classes, attribute types, syntaxes and matching rules
|
|
191
|
+
*/
|
|
192
|
+
getSchema(): Promise<LdapSchema>;
|
|
193
|
+
/**
|
|
194
|
+
* Read a single entry by DN, operational attributes included.
|
|
195
|
+
*
|
|
196
|
+
* @param dn DN of the entry
|
|
197
|
+
* @param req incoming request, forwarded to the authorization hooks
|
|
198
|
+
* @returns the entry
|
|
199
|
+
* @throws NotFoundError when the DN does not exist
|
|
200
|
+
*/
|
|
201
|
+
getEntry(dn: string, req?: Request): Promise<RawEntry>;
|
|
202
|
+
/**
|
|
203
|
+
* List the direct children of an entry.
|
|
204
|
+
*
|
|
205
|
+
* @param dn DN of the parent entry
|
|
206
|
+
* @param withChildrenFlag when true, probe each child for its own children
|
|
207
|
+
* (one extra search per child)
|
|
208
|
+
* @param req incoming request, forwarded to the authorization hooks
|
|
209
|
+
* @returns children sorted by RDN, flagged as truncated when the branch
|
|
210
|
+
* holds more than `--ldap-raw-max-results` entries
|
|
211
|
+
*/
|
|
212
|
+
getChildren(dn: string, withChildrenFlag: boolean, req?: Request): Promise<RawChildren>;
|
|
213
|
+
/**
|
|
214
|
+
* Tell whether an entry has at least one child.
|
|
215
|
+
*
|
|
216
|
+
* @param dn DN of the entry
|
|
217
|
+
* @param req incoming request, forwarded to the authorization hooks
|
|
218
|
+
* @returns true when a child exists, false on error or empty result
|
|
219
|
+
*/
|
|
220
|
+
hasChildren(dn: string, req?: Request): Promise<boolean>;
|
|
221
|
+
/**
|
|
222
|
+
* Run an arbitrary search below one of the exposed bases.
|
|
223
|
+
*
|
|
224
|
+
* @param options base, scope, filter and attributes of the search
|
|
225
|
+
* @param req incoming request, forwarded to the authorization hooks
|
|
226
|
+
* @returns matching entries, flagged as truncated when the limit was hit
|
|
227
|
+
*/
|
|
228
|
+
search(options: {
|
|
229
|
+
base: string;
|
|
230
|
+
scope: Scope;
|
|
231
|
+
filter: string;
|
|
232
|
+
attributes?: string[];
|
|
233
|
+
limit?: number;
|
|
234
|
+
}, req?: Request): Promise<RawSearchResult>;
|
|
235
|
+
/**
|
|
236
|
+
* Parent DN of an entry, or null when it is one of the exposed bases.
|
|
237
|
+
*
|
|
238
|
+
* @param dn DN of the entry
|
|
239
|
+
* @returns parent DN, or null when the entry is a root of the API
|
|
240
|
+
*/
|
|
241
|
+
parentOf(dn: string): string | null;
|
|
242
|
+
api(app: Express): void;
|
|
243
|
+
/**
|
|
244
|
+
* Expose the API settings to the config API, so a UI can adapt itself
|
|
245
|
+
* (bases to display, read-only mode).
|
|
246
|
+
*
|
|
247
|
+
* @returns public configuration of this plugin
|
|
248
|
+
*/
|
|
249
|
+
getConfigApiData(): Record<string, unknown>;
|
|
250
|
+
}
|
|
251
|
+
export {};
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
*
|
|
21
21
|
* When a user is deleted, all corresponding applicative accounts are also deleted.
|
|
22
22
|
*
|
|
23
|
-
* When a user's mail changes,
|
|
23
|
+
* When a user's mail changes, the principal account moves to the new address and
|
|
24
|
+
* the app accounts are deleted: every client has to be reconfigured against the
|
|
25
|
+
* new address anyway.
|
|
24
26
|
*/
|
|
25
27
|
import DmPlugin from '../../abstract/plugin';
|
|
26
28
|
import type { Role } from '../../abstract/plugin';
|
|
@@ -35,13 +37,49 @@ export default class AppAccountsConsistency extends DmPlugin {
|
|
|
35
37
|
private mailAttr;
|
|
36
38
|
private applicativeAccountBase;
|
|
37
39
|
private operationalAttributes;
|
|
40
|
+
private copiedAttributes;
|
|
38
41
|
constructor(server: DM);
|
|
39
42
|
/**
|
|
40
|
-
* Check if an attribute should be excluded when
|
|
43
|
+
* Check if an attribute should be excluded when recreating an existing
|
|
44
|
+
* applicative entry from its own current state.
|
|
45
|
+
*
|
|
46
|
+
* This is a denylist because the input is an entry we wrote ourselves: what
|
|
47
|
+
* has to go is what the directory generates and would refuse on an `add`,
|
|
48
|
+
* plus `userPassword` (an app account is reconfigured on every client after
|
|
49
|
+
* a mail change anyway, so its password is deliberately not carried over).
|
|
50
|
+
*
|
|
51
|
+
* Attributes coming from the *user* entry are chosen by
|
|
52
|
+
* {@link pickCopiedAttributes} instead — an allowlist, so that a new
|
|
53
|
+
* attribute on the user schema is never propagated by accident.
|
|
54
|
+
*
|
|
41
55
|
* @param key - The attribute name to check
|
|
42
56
|
* @returns true if the attribute should be excluded
|
|
43
57
|
*/
|
|
44
58
|
private shouldExcludeAttribute;
|
|
59
|
+
/**
|
|
60
|
+
* Delete an entry of the applicative branch, tolerating its absence.
|
|
61
|
+
*
|
|
62
|
+
* A missing entry is the state we wanted, so `NoSuchObject` is not an error:
|
|
63
|
+
* it keeps the mail-change path idempotent when a hook fires twice.
|
|
64
|
+
*
|
|
65
|
+
* @param dn - DN of the applicative entry to remove
|
|
66
|
+
* @throws whatever the directory raised, except NoSuchObject
|
|
67
|
+
*/
|
|
68
|
+
private deleteApplicativeEntry;
|
|
69
|
+
/**
|
|
70
|
+
* Select the attributes to copy from a user entry into an applicative entry.
|
|
71
|
+
*
|
|
72
|
+
* An allowlist (`--applicative-account-attribute`, plus `objectClass` and the
|
|
73
|
+
* mail attribute, which are forced in by the constructor):
|
|
74
|
+
* the applicative branch is only ever read to bind with `uid` and
|
|
75
|
+
* `userPassword`, so anything else the user entry happens to carry —
|
|
76
|
+
* recovery addresses, e2ee key material, whatever the schema grows next —
|
|
77
|
+
* has no reason to be duplicated there.
|
|
78
|
+
*
|
|
79
|
+
* @param entry - The source user entry
|
|
80
|
+
* @returns The subset worth copying, empty values dropped
|
|
81
|
+
*/
|
|
82
|
+
private pickCopiedAttributes;
|
|
45
83
|
/**
|
|
46
84
|
* Whether a DN belongs to the applicative branch this plugin manages.
|
|
47
85
|
*
|
|
@@ -23,16 +23,23 @@ This guide covers LDAP-Rest's browser libraries for building LDAP management int
|
|
|
23
23
|
- [Installation](#ldapuniteditor-installation)
|
|
24
24
|
- [Basic Usage](#ldapuniteditor-basic-usage)
|
|
25
25
|
- [Features](#ldapuniteditor-features)
|
|
26
|
+
- [LdapBrowser](#ldapbrowser)
|
|
27
|
+
- [Installation](#ldapbrowser-installation)
|
|
28
|
+
- [Basic Usage](#ldapbrowser-basic-usage)
|
|
29
|
+
- [Configuration Options](#ldapbrowser-configuration-options)
|
|
30
|
+
- [Public Methods](#ldapbrowser-public-methods)
|
|
31
|
+
- [RawApiClient and SchemaView](#rawapiclient-and-schemaview)
|
|
26
32
|
|
|
27
33
|
---
|
|
28
34
|
|
|
29
35
|
## Overview
|
|
30
36
|
|
|
31
|
-
LDAP-Rest provides
|
|
37
|
+
LDAP-Rest provides four ready-to-use browser libraries for building LDAP management interfaces:
|
|
32
38
|
|
|
33
39
|
- **LdapTreeViewer** - Interactive hierarchical tree view of LDAP organizations
|
|
34
40
|
- **LdapUserEditor** - Complete user management interface with organization tree, user list, and edit form
|
|
35
41
|
- **LdapUnitEditor** - Organization management interface with create, move, edit, and delete operations
|
|
42
|
+
- **LdapBrowser** - Low-level, read-only directory browser (tree, raw entries, schema), in the spirit of phpLDAPadmin
|
|
36
43
|
|
|
37
44
|
All libraries are:
|
|
38
45
|
|
|
@@ -1174,6 +1181,140 @@ See [LDAP Organizations Plugin Documentation](../ldapOrganizations.md) for API d
|
|
|
1174
1181
|
|
|
1175
1182
|
---
|
|
1176
1183
|
|
|
1184
|
+
<a name="ldapbrowser"></a>
|
|
1185
|
+
|
|
1186
|
+
## LdapBrowser
|
|
1187
|
+
|
|
1188
|
+
A low-level, read-only browser of the directory: lazy tree on the left, raw content of the selected entry on the right, LDAP filter search on top. Attributes are annotated with the directory schema — mandatory attributes are marked, unknown ones are flagged, binary values are shown as base64 (photos as thumbnails).
|
|
1189
|
+
|
|
1190
|
+
It consumes the [raw plugin](../../usage/plugins/ldap/raw.md), which must be loaded server-side:
|
|
1191
|
+
|
|
1192
|
+
```bash
|
|
1193
|
+
ldap-rest --plugin core/ldap/raw --plugin core/static
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
<a name="ldapbrowser-installation"></a>
|
|
1197
|
+
|
|
1198
|
+
### Installation
|
|
1199
|
+
|
|
1200
|
+
#### Direct Browser Usage
|
|
1201
|
+
|
|
1202
|
+
```html
|
|
1203
|
+
<link rel="stylesheet" href="/static/browser/ldap-browser.css" />
|
|
1204
|
+
<script src="/static/browser/ldap-browser.js"></script>
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
The Material Icons font is used for the tree icons:
|
|
1208
|
+
|
|
1209
|
+
```html
|
|
1210
|
+
<link
|
|
1211
|
+
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
|
1212
|
+
rel="stylesheet"
|
|
1213
|
+
/>
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
#### With a Bundler
|
|
1217
|
+
|
|
1218
|
+
```typescript
|
|
1219
|
+
import { LdapBrowser } from 'ldap-rest/browser-ldap-browser-index';
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
<a name="ldapbrowser-basic-usage"></a>
|
|
1223
|
+
|
|
1224
|
+
### Basic Usage
|
|
1225
|
+
|
|
1226
|
+
```html
|
|
1227
|
+
<div id="ldap-browser" style="height: 70vh"></div>
|
|
1228
|
+
|
|
1229
|
+
<script>
|
|
1230
|
+
const { LdapBrowser } = window.LdapBrowser;
|
|
1231
|
+
|
|
1232
|
+
const browser = new LdapBrowser({
|
|
1233
|
+
containerId: 'ldap-browser',
|
|
1234
|
+
apiBaseUrl: window.location.origin,
|
|
1235
|
+
onEntrySelected: dn => console.log('Selected:', dn),
|
|
1236
|
+
onError: error => console.error(error),
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
await browser.init();
|
|
1240
|
+
</script>
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
A ready-made demo page ([`examples/web/ldap-browser.html`](../../../examples/web/ldap-browser.html))
|
|
1244
|
+
is served at `/static/examples/web/ldap-browser.html`.
|
|
1245
|
+
|
|
1246
|
+
<a name="ldapbrowser-configuration-options"></a>
|
|
1247
|
+
|
|
1248
|
+
### Configuration Options
|
|
1249
|
+
|
|
1250
|
+
| Option | Type | Default | Description |
|
|
1251
|
+
| ----------------- | ------------------------ | ------------------ | --------------------------------------- |
|
|
1252
|
+
| `containerId` | `string` | _required_ | id of the element to render into |
|
|
1253
|
+
| `apiBaseUrl` | `string` | `''` (same origin) | Origin of the API |
|
|
1254
|
+
| `authToken` | `string` | — | Sent as `Authorization: Bearer …` |
|
|
1255
|
+
| `initialDn` | `string` | first exposed base | DN selected on startup |
|
|
1256
|
+
| `onEntrySelected` | `(dn: string) => void` | — | Called whenever an entry is displayed |
|
|
1257
|
+
| `onError` | `(error: Error) => void` | `console.error` | Called on any error the browser handles |
|
|
1258
|
+
|
|
1259
|
+
<a name="ldapbrowser-public-methods"></a>
|
|
1260
|
+
|
|
1261
|
+
### Public Methods
|
|
1262
|
+
|
|
1263
|
+
#### `async init(): Promise<void>`
|
|
1264
|
+
|
|
1265
|
+
Loads the exposed bases and the schema, builds the interface and opens `initialDn`. Throws when the container does not exist or the API is unreachable. A schema that cannot be read is reported through `onError` but does not prevent browsing — the entry table is then displayed without annotations.
|
|
1266
|
+
|
|
1267
|
+
#### `getCurrentDn(): string | null`
|
|
1268
|
+
|
|
1269
|
+
DN currently displayed, or `null` before the first selection.
|
|
1270
|
+
|
|
1271
|
+
#### `destroy(): void`
|
|
1272
|
+
|
|
1273
|
+
Releases the listeners held by the tree.
|
|
1274
|
+
|
|
1275
|
+
<a name="rawapiclient-and-schemaview"></a>
|
|
1276
|
+
|
|
1277
|
+
### RawApiClient and SchemaView
|
|
1278
|
+
|
|
1279
|
+
Both are exported for applications that want the data without the interface:
|
|
1280
|
+
|
|
1281
|
+
```typescript
|
|
1282
|
+
import { RawApiClient, SchemaView } from 'ldap-rest/browser-ldap-browser-index';
|
|
1283
|
+
|
|
1284
|
+
const api = new RawApiClient(window.location.origin);
|
|
1285
|
+
const schema = new SchemaView(await api.getSchema());
|
|
1286
|
+
|
|
1287
|
+
const entry = await api.getEntry('uid=alice,ou=users,dc=example,dc=com');
|
|
1288
|
+
const { must, may } = schema.resolveAttributes(
|
|
1289
|
+
entry.attributes.objectClass.values
|
|
1290
|
+
);
|
|
1291
|
+
console.log('Mandatory attributes:', must);
|
|
1292
|
+
console.log('Allowed attributes:', may);
|
|
1293
|
+
|
|
1294
|
+
const { children, truncated } = await api.getChildren('dc=example,dc=com');
|
|
1295
|
+
const found = await api.search({
|
|
1296
|
+
base: 'ou=users,dc=example,dc=com',
|
|
1297
|
+
filter: '(mail=*@example.com)',
|
|
1298
|
+
attributes: ['uid', 'mail'],
|
|
1299
|
+
});
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
`SchemaView` resolves the `SUP` chains for you: `resolveAttributes()` walks object class inheritance, `getAttributeSyntax()` walks attribute type inheritance, and `isBinaryAttribute()` / `isImageAttribute()` tell how to render a value.
|
|
1303
|
+
|
|
1304
|
+
### API Integration
|
|
1305
|
+
|
|
1306
|
+
The browser uses these endpoints:
|
|
1307
|
+
|
|
1308
|
+
- `GET /api/v1/ldap/raw/bases` - Exposed subtrees, used as tree roots
|
|
1309
|
+
- `GET /api/v1/ldap/raw/schema` - Directory schema
|
|
1310
|
+
- `GET /api/v1/ldap/raw/entry/:dn` - Entry content
|
|
1311
|
+
- `GET /api/v1/ldap/raw/children/:dn` - Children of a node
|
|
1312
|
+
- `GET /api/v1/ldap/raw/search` - Filter search
|
|
1313
|
+
|
|
1314
|
+
See [LDAP Raw Plugin Documentation](../../usage/plugins/ldap/raw.md) for API details.
|
|
1315
|
+
|
|
1316
|
+
---
|
|
1317
|
+
|
|
1177
1318
|
## Next Steps
|
|
1178
1319
|
|
|
1179
1320
|
- **[REST API Documentation](../api/REST_API.md)** - Learn about the underlying API
|
|
@@ -190,8 +190,28 @@ Monitors LDAP modifications and triggers hooks for attribute changes. No configu
|
|
|
190
190
|
|
|
191
191
|
Maintains consistency of department links when organizations are renamed/moved. No configuration options - uses [core/ldap/organizations](#coreldaporganizations) settings.
|
|
192
192
|
|
|
193
|
+
#### `core/ldap/raw`
|
|
194
|
+
|
|
195
|
+
Read-only low-level browsing (root DSE, schema, entries). See [raw plugin](plugins/ldap/raw.md).
|
|
196
|
+
|
|
197
|
+
| CLI | Plural | Env | Default | Description |
|
|
198
|
+
| ----------------------------- | ------------------------------ | ------------------------------- | ------------- | ------------------------------- |
|
|
199
|
+
| `--ldap-raw-base` | `--ldap-raw-bases` | `DM_LDAP_RAW_BASE` | `--ldap-base` | Subtrees exposed by the API |
|
|
200
|
+
| `--ldap-raw-hidden-attribute` | `--ldap-raw-hidden-attributes` | `DM_LDAP_RAW_HIDDEN_ATTRIBUTES` | `[]` | Extra attributes never returned |
|
|
201
|
+
| `--ldap-raw-show-secrets` | | `DM_LDAP_RAW_SHOW_SECRETS` | `false` | Serve credential attributes |
|
|
202
|
+
| `--ldap-raw-max-results` | | `DM_LDAP_RAW_MAX_RESULTS` | `200` | Max entries per search/listing |
|
|
203
|
+
| `--ldap-raw-schema-cache-ttl` | | `DM_LDAP_RAW_SCHEMA_CACHE_TTL` | `3600` | Schema cache lifetime (seconds) |
|
|
204
|
+
|
|
193
205
|
### Authentication Plugins
|
|
194
206
|
|
|
207
|
+
#### Common to every authentication plugin
|
|
208
|
+
|
|
209
|
+
| CLI | Plural | Env | Default | Description |
|
|
210
|
+
| -------------------- | ---------------------- | --------------------- | ------- | ------------------------------------------- |
|
|
211
|
+
| `--auth-path-prefix` | `--auth-path-prefixes` | `DM_AUTH_PATH_PREFIX` | `[]` | Restrict this plugin to these path prefixes |
|
|
212
|
+
|
|
213
|
+
Empty means the plugin guards every path. Scoping several instances to different prefixes lets one server serve populations that authenticate differently — see [Serving several populations from one server](plugins/auth/README.md#serving-several-populations-from-one-server), including the routes it can leave unauthenticated.
|
|
214
|
+
|
|
195
215
|
#### `core/auth/token`
|
|
196
216
|
|
|
197
217
|
| CLI | Plural | Env | Default | Description |
|
|
@@ -302,14 +322,29 @@ Maintains consistency of department links when organizations are renamed/moved.
|
|
|
302
322
|
|
|
303
323
|
#### `core/twake/applicativeAccounts`
|
|
304
324
|
|
|
305
|
-
| CLI
|
|
306
|
-
|
|
|
307
|
-
| `--applicative-account-base`
|
|
308
|
-
| `--max-app-accounts`
|
|
309
|
-
| `--
|
|
325
|
+
| CLI | Plural | Env | Default | Description |
|
|
326
|
+
| --------------------------------- | ---------------------------------- | ----------------------------------- | ------------- | ------------------------------------- |
|
|
327
|
+
| `--applicative-account-base` | | `DM_APPLICATIVE_ACCOUNT_BASE` | | Applicative accounts base DN |
|
|
328
|
+
| `--max-app-accounts` | | `DM_MAX_APP_ACCOUNTS` | `5` | Max accounts per user |
|
|
329
|
+
| `--applicative-account-attribute` | `--applicative-account-attributes` | `DM_APPLICATIVE_ACCOUNT_ATTRIBUTES` | _(see below)_ | Attributes copied from the user entry |
|
|
330
|
+
| `--ldap-operational-attribute` | `--ldap-operational-attributes` | `DM_LDAP_OPERATIONAL_ATTRIBUTES` | _(see below)_ | Operational attributes to exclude |
|
|
331
|
+
|
|
332
|
+
Default copied attributes: `objectClass`, `cn`, `sn`, `givenName`, `displayName`, `description` — plus the configured mail attribute, always.
|
|
333
|
+
|
|
334
|
+
This is an **allowlist**: an attribute the user entry carries but that is not
|
|
335
|
+
named here is not copied into the applicative branch. That branch is only ever
|
|
336
|
+
read to bind with `uid` and `userPassword`, so anything else has no reason to
|
|
337
|
+
be duplicated there — and a new attribute added to the user schema later stays
|
|
338
|
+
out unless someone opts in.
|
|
310
339
|
|
|
311
340
|
Default operational attributes: `dn`, `controls`, `structuralObjectClass`, `entryUUID`, `entryDN`, `subschemaSubentry`, `modifyTimestamp`, `modifiersName`, `createTimestamp`, `creatorsName`, `userPassword`
|
|
312
341
|
|
|
342
|
+
Operational attributes are only used when an applicative entry is **recreated
|
|
343
|
+
from its own current state** (on a mail change): they are what the directory
|
|
344
|
+
generates and would refuse on an `add`. `userPassword` is in that list on
|
|
345
|
+
purpose — a mail change forces every client to be reconfigured anyway, so app
|
|
346
|
+
accounts are deliberately reissued without a password.
|
|
347
|
+
|
|
313
348
|
#### `core/twake/appAccountsConsistency`
|
|
314
349
|
|
|
315
350
|
Automatically creates/updates/deletes applicative account entries when users are created/modified/deleted. No configuration options - uses [core/twake/applicativeAccounts](#coretwakeapplicativeaccounts) settings.
|
|
@@ -6,15 +6,16 @@ LDAP-Rest is extensible through a plugin system. This section documents all avai
|
|
|
6
6
|
|
|
7
7
|
LDAP entity management:
|
|
8
8
|
|
|
9
|
-
| Plugin | Description
|
|
10
|
-
| ---------------------------------------- |
|
|
11
|
-
| [flat-generic](ldap/flat-generic.md) | Generic LDAP entity management (users, positions, etc.)
|
|
12
|
-
| [groups](ldap/groups.md) | LDAP group management with member validation
|
|
13
|
-
| [organizations](ldap/organizations.md) | Hierarchical organization management
|
|
14
|
-
| [bulk-import](ldap/bulk-import.md) | Bulk import from CSV
|
|
15
|
-
| [trash](ldap/trash.md) | Trash system (soft delete)
|
|
16
|
-
| [external-users](ldap/external-users.md) | Automatic external contact creation
|
|
17
|
-
| [on-change](ldap/on-change.md) | LDAP change detection
|
|
9
|
+
| Plugin | Description |
|
|
10
|
+
| ---------------------------------------- | --------------------------------------------------------- |
|
|
11
|
+
| [flat-generic](ldap/flat-generic.md) | Generic LDAP entity management (users, positions, etc.) |
|
|
12
|
+
| [groups](ldap/groups.md) | LDAP group management with member validation |
|
|
13
|
+
| [organizations](ldap/organizations.md) | Hierarchical organization management |
|
|
14
|
+
| [bulk-import](ldap/bulk-import.md) | Bulk import from CSV |
|
|
15
|
+
| [trash](ldap/trash.md) | Trash system (soft delete) |
|
|
16
|
+
| [external-users](ldap/external-users.md) | Automatic external contact creation |
|
|
17
|
+
| [on-change](ldap/on-change.md) | LDAP change detection |
|
|
18
|
+
| [raw](ldap/raw.md) | Low-level browsing: root DSE, schema, entries (read-only) |
|
|
18
19
|
|
|
19
20
|
## Authentication Plugins
|
|
20
21
|
|
|
@@ -43,6 +43,63 @@ LDAP-Rest provides multiple authentication plugins to secure API access. These p
|
|
|
43
43
|
| **Best For** | APIs, Scripts | APIs, Enhanced | Backend Services | Enterprises with LLNG | Cloud/SaaS, Enterprises with SSO |
|
|
44
44
|
| **Dependencies** | None | None | None | lemonldap-ng-handler | express-openid-connect |
|
|
45
45
|
|
|
46
|
+
## Serving several populations from one server
|
|
47
|
+
|
|
48
|
+
Populations rarely authenticate the same way: machines carry a token, administrators arrive with an SSO session. `--auth-path-prefix` restricts an authentication plugin to one or more path prefixes, so a single server can host both without either credential being valid on the other's branch of the API.
|
|
49
|
+
|
|
50
|
+
Any plugin can be loaded more than once under a distinct name with its own configuration (`module:name:{json}`), which is what makes this work:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
ldap-rest \
|
|
54
|
+
--plugin 'core/auth/token:auth-machines:{"auth_path_prefix":"/api/m","auth_token":"…:robot"}' \
|
|
55
|
+
--plugin 'core/ldap/raw:raw-machines:{"api_prefix":"/api/m"}' \
|
|
56
|
+
--plugin 'core/auth/openidconnect:auth-admins:{"auth_path_prefix":"/api/admin"}' \
|
|
57
|
+
--plugin 'core/ldap/raw:raw-admins:{"api_prefix":"/api/admin"}'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A token presented on `/api/admin` is refused, and a session presented on `/api/m` is refused too: each authentication plugin only sees the branch it was mounted on. Prefixes match on segment boundaries, so `/api/m` guards `/api/m` and `/api/m/entry` but never `/api/machines`.
|
|
61
|
+
|
|
62
|
+
A prefix must be a string starting with `/`. Anything else — a number left in the JSON, an empty entry, a prefix without its leading slash — refuses to start rather than being skipped: a dropped entry shrinks what the plugin guards, which is the failure that leaves a branch open while the configuration still reads as if it were covered. `/` is not a prefix but the whole server, so a list containing it makes the plugin a catch-all: `["/", "/api/admin"]` guards everything, not only `/api/admin`.
|
|
63
|
+
|
|
64
|
+
### One branch apart, a token on everything else
|
|
65
|
+
|
|
66
|
+
Leaving a plugin unscoped makes it the **catch-all**: it guards everything no scoped plugin claims. There is no list of "everything else" to write, and none to keep up to date when a route is added:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
ldap-rest \
|
|
70
|
+
--plugin 'core/auth/openidconnect:auth-admins:{"auth_path_prefix":"/api/admin"}' \
|
|
71
|
+
--plugin core/auth/token \
|
|
72
|
+
--auth-token "…:robot"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`/api/admin` requires the SSO session, everything else requires the token.
|
|
76
|
+
|
|
77
|
+
### How the choice is made
|
|
78
|
+
|
|
79
|
+
Authentication is not a middleware each plugin mounts for itself. The server mounts **one dispatcher**, positioned after the guards (`protect`: rate limiting, proxy trust, CrowdSec) and access logging, and before the first plugin able to register a route. Authentication plugins register with it instead of adding a layer.
|
|
80
|
+
|
|
81
|
+
Two properties follow, and both are load-bearing:
|
|
82
|
+
|
|
83
|
+
- **Order of declaration does not matter.** A plugin that registers after the API it guards still guards it, because the dispatcher — not the plugin — owns the layer. With per-plugin middlewares that was untrue and silent: a guard mounted after its routes never ran for them.
|
|
84
|
+
- **The most specific claim wins.** `/api/admin` is handled by the plugin scoped to it, `/api` by the plugin scoped to `/api`, and anything unclaimed by an unscoped plugin. Only the winner runs, so two plugins covering the same request never compose as an AND that no credential can satisfy.
|
|
85
|
+
|
|
86
|
+
Several plugins sharing the same winning prefix all run, in registration order: requiring two credentials on one branch is a legitimate ask, and an accident there fails closed rather than open.
|
|
87
|
+
|
|
88
|
+
### The gap to watch
|
|
89
|
+
|
|
90
|
+
A route registered **outside every prefix is served without authentication**. That is the price of scoping, and it is silent — the server starts, the API answers. To make it visible, the routes no plugin guards are listed at startup:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Authentication is restricted to /api/m, /api/admin, so these routes are
|
|
94
|
+
served without authentication: /api/v1/config, /api/v1/ldap/users
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Read that line on every configuration change. Either scope the missing routes to a prefix as well, add an unscoped plugin as the catch-all, or keep them public deliberately. Nothing is reported when no authentication is configured (the server is open on purpose) or when a catch-all covers what the scoped plugins do not.
|
|
98
|
+
|
|
99
|
+
Note the converse: with a catch-all there is no public branch at all. Making one API reachable without credentials means scoping every authentication plugin and leaving that branch out — the warning will then name it, which is correct but reads as an oversight rather than a decision.
|
|
100
|
+
|
|
101
|
+
Note that a prefix scopes **authentication**, not authorization: once past it, a caller reaches everything the branch exposes. Restrict what each population may do with [authz-per-route](authz-per-route.md) or [authz-per-branch](authz-per-branch.md).
|
|
102
|
+
|
|
46
103
|
## Combining with Authorization
|
|
47
104
|
|
|
48
105
|
All authentication plugins set `req.user` to the authenticated identity. You can use hooks to implement custom authorization:
|
|
@@ -54,6 +54,15 @@ For correct IP detection behind reverse proxies:
|
|
|
54
54
|
|
|
55
55
|
The rate limiter will use the real client IP from `X-Forwarded-For` when the request comes from a trusted proxy.
|
|
56
56
|
|
|
57
|
+
This pairing is not a refinement, it is what makes the limit hold. The limiter keys on `X-Forwarded-For`, a header any client can set to anything; `trustedProxy` is what strips it on requests that do not come from a declared proxy. Loaded alone, the limiter counts forged keys: rotating the header on every attempt gives an attacker a fresh counter each time, and the brute-force protection becomes decorative while still looking configured. The plugin therefore warns at startup when `trustedProxy` is absent:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Auth rate limiting keys on X-Forwarded-For but core/auth/trustedProxy is not
|
|
61
|
+
loaded: a client can forge that header and evade the limit.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Either load `trustedProxy`, or make sure the reverse proxy in front **overwrites** `X-Forwarded-For` rather than appending to it.
|
|
65
|
+
|
|
57
66
|
## Security Considerations
|
|
58
67
|
|
|
59
68
|
- Always use with `trustedProxy` plugin behind reverse proxies
|