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.
Files changed (67) hide show
  1. package/bin/sync-james.mjs +6 -2
  2. package/dist/bin/index.js +1 -1
  3. package/dist/config/args.js +1 -1
  4. package/dist/lib/auth/base.js +1 -1
  5. package/dist/lib/ldapSchema.js +1 -0
  6. package/dist/lib/utils.js +1 -1
  7. package/dist/plugins/auth/authzPerRoute.js +1 -1
  8. package/dist/plugins/auth/rateLimit.js +1 -1
  9. package/dist/plugins/ldap/groups.js +1 -1
  10. package/dist/plugins/ldap/organizations.js +1 -1
  11. package/dist/plugins/ldap/raw.js +1 -0
  12. package/dist/plugins/twake/appAccountsApi.js +1 -1
  13. package/dist/plugins/twake/appAccountsConsistency.js +1 -1
  14. package/dist/src/bin/index.d.ts +112 -0
  15. package/dist/src/browser/ldap-browser/LdapBrowser.d.ts +52 -0
  16. package/dist/src/browser/ldap-browser/api/RawApiClient.d.ts +64 -0
  17. package/dist/src/browser/ldap-browser/components/EntryTree.d.ts +75 -0
  18. package/dist/src/browser/ldap-browser/components/EntryView.d.ts +56 -0
  19. package/dist/src/browser/ldap-browser/index.d.ts +9 -0
  20. package/dist/src/browser/ldap-browser/schema.d.ts +62 -0
  21. package/dist/src/browser/ldap-browser/types.d.ts +96 -0
  22. package/dist/src/config/args.d.ts +7 -0
  23. package/dist/src/lib/auth/base.d.ts +82 -1
  24. package/dist/src/lib/ldapSchema.d.ts +159 -0
  25. package/dist/src/lib/utils.d.ts +4 -0
  26. package/dist/src/plugins/ldap/raw.d.ts +251 -0
  27. package/dist/src/plugins/twake/appAccountsConsistency.d.ts +40 -2
  28. package/docs/client-development/browser/libraries.md +142 -1
  29. package/docs/usage/configuration.md +40 -5
  30. package/docs/usage/plugins/README.md +10 -9
  31. package/docs/usage/plugins/auth/README.md +57 -0
  32. package/docs/usage/plugins/auth/rate-limit.md +9 -0
  33. package/docs/usage/plugins/integrations/app-accounts.md +35 -2
  34. package/docs/usage/plugins/ldap/README.md +12 -0
  35. package/docs/usage/plugins/ldap/raw.md +226 -0
  36. package/examples/web/README.md +18 -0
  37. package/examples/web/ldap-browser.html +94 -0
  38. package/package.json +37 -1
  39. package/static/browser/ldap-browser/LdapBrowser.js +199 -0
  40. package/static/browser/ldap-browser/LdapBrowser.js.map +1 -0
  41. package/static/browser/ldap-browser/api/RawApiClient.js +103 -0
  42. package/static/browser/ldap-browser/api/RawApiClient.js.map +1 -0
  43. package/static/browser/ldap-browser/components/EntryTree.js +235 -0
  44. package/static/browser/ldap-browser/components/EntryTree.js.map +1 -0
  45. package/static/browser/ldap-browser/components/EntryView.js +183 -0
  46. package/static/browser/ldap-browser/components/EntryView.js.map +1 -0
  47. package/static/browser/ldap-browser/index.js +4 -0
  48. package/static/browser/ldap-browser/index.js.map +1 -0
  49. package/static/browser/ldap-browser/schema.js +135 -0
  50. package/static/browser/ldap-browser/schema.js.map +1 -0
  51. package/static/browser/ldap-browser/types.js +2 -0
  52. package/static/browser/ldap-browser/types.js.map +1 -0
  53. package/static/browser/ldap-browser.css +2 -0
  54. package/static/browser/ldap-browser.css.map +1 -0
  55. package/static/browser/ldap-browser.esm.js +915 -0
  56. package/static/browser/ldap-browser.esm.js.map +1 -0
  57. package/static/browser/ldap-browser.js +925 -0
  58. package/static/browser/ldap-browser.js.map +1 -0
  59. package/static/browser/ldap-browser.min.js +2 -0
  60. package/static/browser/ldap-browser.min.js.map +1 -0
  61. package/static/browser/ldap-unit-editor/components/UnitTree.js +0 -1
  62. package/static/browser/ldap-unit-editor/components/UnitTree.js.map +1 -1
  63. package/static/browser/ldap-unit-editor.esm.js +0 -1
  64. package/static/browser/ldap-unit-editor.esm.js.map +1 -1
  65. package/static/browser/ldap-unit-editor.js +0 -1
  66. package/static/browser/ldap-unit-editor.js.map +1 -1
  67. package/static/browser/ldap-unit-editor.min.js.map +1 -1
@@ -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, all applicative accounts are updated with the new mail.
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 copying LDAP entry attributes
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 three ready-to-use browser libraries for building LDAP management interfaces:
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 | Plural | Env | Default | Description |
306
- | ------------------------------ | ------------------------------- | -------------------------------- | ------------- | --------------------------------- |
307
- | `--applicative-account-base` | | `DM_APPLICATIVE_ACCOUNT_BASE` | | Applicative accounts base DN |
308
- | `--max-app-accounts` | | `DM_MAX_APP_ACCOUNTS` | `5` | Max accounts per user |
309
- | `--ldap-operational-attribute` | `--ldap-operational-attributes` | `DM_LDAP_OPERATIONAL_ATTRIBUTES` | _(see below)_ | Operational attributes to exclude |
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