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.
Files changed (62) 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/plugins/auth/authzPerRoute.js +1 -1
  7. package/dist/plugins/auth/rateLimit.js +1 -1
  8. package/dist/plugins/ldap/organizations.js +1 -1
  9. package/dist/plugins/ldap/raw.js +1 -0
  10. package/dist/plugins/twake/appAccountsApi.js +1 -1
  11. package/dist/src/bin/index.d.ts +112 -0
  12. package/dist/src/browser/ldap-browser/LdapBrowser.d.ts +52 -0
  13. package/dist/src/browser/ldap-browser/api/RawApiClient.d.ts +64 -0
  14. package/dist/src/browser/ldap-browser/components/EntryTree.d.ts +75 -0
  15. package/dist/src/browser/ldap-browser/components/EntryView.d.ts +56 -0
  16. package/dist/src/browser/ldap-browser/index.d.ts +9 -0
  17. package/dist/src/browser/ldap-browser/schema.d.ts +62 -0
  18. package/dist/src/browser/ldap-browser/types.d.ts +96 -0
  19. package/dist/src/config/args.d.ts +6 -0
  20. package/dist/src/lib/auth/base.d.ts +82 -1
  21. package/dist/src/lib/ldapSchema.d.ts +159 -0
  22. package/dist/src/plugins/ldap/raw.d.ts +251 -0
  23. package/docs/client-development/browser/libraries.md +142 -1
  24. package/docs/usage/configuration.md +20 -0
  25. package/docs/usage/plugins/README.md +10 -9
  26. package/docs/usage/plugins/auth/README.md +57 -0
  27. package/docs/usage/plugins/auth/rate-limit.md +9 -0
  28. package/docs/usage/plugins/integrations/app-accounts.md +32 -0
  29. package/docs/usage/plugins/ldap/README.md +12 -0
  30. package/docs/usage/plugins/ldap/raw.md +226 -0
  31. package/examples/web/README.md +18 -0
  32. package/examples/web/ldap-browser.html +94 -0
  33. package/package.json +37 -1
  34. package/static/browser/ldap-browser/LdapBrowser.js +199 -0
  35. package/static/browser/ldap-browser/LdapBrowser.js.map +1 -0
  36. package/static/browser/ldap-browser/api/RawApiClient.js +103 -0
  37. package/static/browser/ldap-browser/api/RawApiClient.js.map +1 -0
  38. package/static/browser/ldap-browser/components/EntryTree.js +235 -0
  39. package/static/browser/ldap-browser/components/EntryTree.js.map +1 -0
  40. package/static/browser/ldap-browser/components/EntryView.js +183 -0
  41. package/static/browser/ldap-browser/components/EntryView.js.map +1 -0
  42. package/static/browser/ldap-browser/index.js +4 -0
  43. package/static/browser/ldap-browser/index.js.map +1 -0
  44. package/static/browser/ldap-browser/schema.js +135 -0
  45. package/static/browser/ldap-browser/schema.js.map +1 -0
  46. package/static/browser/ldap-browser/types.js +2 -0
  47. package/static/browser/ldap-browser/types.js.map +1 -0
  48. package/static/browser/ldap-browser.css +2 -0
  49. package/static/browser/ldap-browser.css.map +1 -0
  50. package/static/browser/ldap-browser.esm.js +915 -0
  51. package/static/browser/ldap-browser.esm.js.map +1 -0
  52. package/static/browser/ldap-browser.js +925 -0
  53. package/static/browser/ldap-browser.js.map +1 -0
  54. package/static/browser/ldap-browser.min.js +2 -0
  55. package/static/browser/ldap-browser.min.js.map +1 -0
  56. package/static/browser/ldap-unit-editor/components/UnitTree.js +0 -1
  57. package/static/browser/ldap-unit-editor/components/UnitTree.js.map +1 -1
  58. package/static/browser/ldap-unit-editor.esm.js +0 -1
  59. package/static/browser/ldap-unit-editor.esm.js.map +1 -1
  60. package/static/browser/ldap-unit-editor.js +0 -1
  61. package/static/browser/ldap-unit-editor.js.map +1 -1
  62. package/static/browser/ldap-unit-editor.min.js.map +1 -1
@@ -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 {};
@@ -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 |
@@ -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
@@ -45,6 +45,33 @@ Instead of using a single primary password to access all services that authentic
45
45
  - Ensures automatic cleanup when users are deleted
46
46
  - Synchronizes mail changes across all app accounts
47
47
  3. **Applicative accounts base** configured in LDAP (e.g., `ou=applicative,dc=example,dc=com`)
48
+ 4. **Read access on `userPassword`** in that branch, for the bind DN
49
+ (`--ldap-dn`) - **Required for deletion**
50
+
51
+ Revoking an app account means removing _its_ value from the principal
52
+ account, which holds one `userPassword` per device. The server must read the
53
+ stored hash to know which value to remove — a salted hash cannot be
54
+ recomputed. Without that access, `DELETE` returns `500` and keeps the
55
+ account rather than reporting a revocation that did not happen.
56
+
57
+ A typical OpenLDAP ACL, scoped to the applicative branch so that the users'
58
+ primary passwords stay unreadable:
59
+
60
+ ```
61
+ olcAccess: to dn.subtree="ou=applicative,dc=example,dc=com" attrs=userPassword
62
+ by dn.exact="cn=ldap-rest,ou=tech,dc=example,dc=com" read
63
+ by * break
64
+ ```
65
+
66
+ It must precede any broader rule on `userPassword`; OpenLDAP stops at the
67
+ first matching directive, and `by * break` lets other identities fall
68
+ through to the following ones.
69
+
70
+ 5. **Uniqueness constraints must not cover the applicative branch.** The
71
+ principal account and its app accounts intentionally share the same `mail`,
72
+ so a `slapo-unique` overlay on `mail` has to be scoped to the users branch
73
+ (`olcUniqueURI: ldap:///ou=users,dc=example,dc=com?mail?sub?`) or every app
74
+ account creation is rejected.
48
75
 
49
76
  ## Configuration
50
77
 
@@ -181,6 +208,8 @@ Content-Type: application/json
181
208
  - `400 Bad Request` - Max accounts limit reached or user has no mail
182
209
  - `401 Unauthorized` - Missing or invalid token
183
210
  - `404 Not Found` - User not found
211
+ - `500 Internal Server Error` - The password could not be added to the principal
212
+ account; the app account is rolled back and no password is returned
184
213
 
185
214
  ### Delete Applicative Account
186
215
 
@@ -202,6 +231,9 @@ Authorization: Bearer {token}
202
231
  - `200 OK` - Account deleted (or already deleted - idempotent)
203
232
  - `401 Unauthorized` - Missing or invalid token
204
233
  - `403 Forbidden` - UID does not belong to user
234
+ - `500 Internal Server Error` - The account password could not be read, or could
235
+ not be removed from the principal account; the app account is kept so the
236
+ request stays replayable
205
237
 
206
238
  ## Password Generation
207
239
 
@@ -14,6 +14,7 @@ Plugins for LDAP entity management.
14
14
  | [external-users](external-users.md) | Contacts | Automatic creation |
15
15
  | [on-change](on-change.md) | All | Change detection |
16
16
  | [password-policy](password-policy.md) | Password management | Expiration, Lockout, Unlock |
17
+ | [raw](raw.md) | Whole directory (read) | Root DSE, Schema, Tree, Raw search |
17
18
 
18
19
  ## API Endpoints
19
20
 
@@ -62,6 +63,17 @@ GET /api/v1/password-policy/locked-accounts # List locked accounts
62
63
  POST /api/v1/password/validate # Validate complexity (optional)
63
64
  ```
64
65
 
66
+ ### Low-level browsing (ldapRaw)
67
+
68
+ ```
69
+ GET /api/v1/ldap/raw/bases # Exposed subtrees
70
+ GET /api/v1/ldap/raw/rootdse # Server capabilities
71
+ GET /api/v1/ldap/raw/schema # Parsed directory schema
72
+ GET /api/v1/ldap/raw/entry/{dn} # Read one entry
73
+ GET /api/v1/ldap/raw/children/{dn} # Direct children of an entry
74
+ GET /api/v1/ldap/raw/search # Raw search (base, scope, filter)
75
+ ```
76
+
65
77
  ### Bulk Import (ldapBulkImport)
66
78
 
67
79
  ```