ldap-rest 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/sync-james.mjs +6 -2
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/auth/base.js +1 -1
- package/dist/lib/ldapSchema.js +1 -0
- package/dist/plugins/auth/authzPerRoute.js +1 -1
- package/dist/plugins/auth/rateLimit.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/raw.js +1 -0
- package/dist/plugins/twake/appAccountsApi.js +1 -1
- package/dist/plugins/weblogs.js +1 -1
- package/dist/src/bin/index.d.ts +112 -0
- package/dist/src/browser/ldap-browser/LdapBrowser.d.ts +52 -0
- package/dist/src/browser/ldap-browser/api/RawApiClient.d.ts +64 -0
- package/dist/src/browser/ldap-browser/components/EntryTree.d.ts +75 -0
- package/dist/src/browser/ldap-browser/components/EntryView.d.ts +56 -0
- package/dist/src/browser/ldap-browser/index.d.ts +9 -0
- package/dist/src/browser/ldap-browser/schema.d.ts +62 -0
- package/dist/src/browser/ldap-browser/types.d.ts +96 -0
- package/dist/src/config/args.d.ts +6 -0
- package/dist/src/lib/auth/base.d.ts +82 -1
- package/dist/src/lib/ldapSchema.d.ts +159 -0
- package/dist/src/plugins/ldap/raw.d.ts +251 -0
- package/dist/src/plugins/weblogs.d.ts +7 -0
- package/docs/client-development/browser/libraries.md +142 -1
- package/docs/usage/configuration.md +20 -0
- package/docs/usage/plugins/README.md +10 -9
- package/docs/usage/plugins/auth/README.md +57 -0
- package/docs/usage/plugins/auth/rate-limit.md +9 -0
- package/docs/usage/plugins/integrations/app-accounts.md +32 -0
- package/docs/usage/plugins/ldap/README.md +12 -0
- package/docs/usage/plugins/ldap/raw.md +226 -0
- package/docs/usage/plugins/utilities/weblogs.md +47 -0
- package/examples/web/README.md +18 -0
- package/examples/web/ldap-browser.html +94 -0
- package/package.json +37 -1
- package/static/browser/ldap-browser/LdapBrowser.js +199 -0
- package/static/browser/ldap-browser/LdapBrowser.js.map +1 -0
- package/static/browser/ldap-browser/api/RawApiClient.js +103 -0
- package/static/browser/ldap-browser/api/RawApiClient.js.map +1 -0
- package/static/browser/ldap-browser/components/EntryTree.js +235 -0
- package/static/browser/ldap-browser/components/EntryTree.js.map +1 -0
- package/static/browser/ldap-browser/components/EntryView.js +183 -0
- package/static/browser/ldap-browser/components/EntryView.js.map +1 -0
- package/static/browser/ldap-browser/index.js +4 -0
- package/static/browser/ldap-browser/index.js.map +1 -0
- package/static/browser/ldap-browser/schema.js +135 -0
- package/static/browser/ldap-browser/schema.js.map +1 -0
- package/static/browser/ldap-browser/types.js +2 -0
- package/static/browser/ldap-browser/types.js.map +1 -0
- package/static/browser/ldap-browser.css +2 -0
- package/static/browser/ldap-browser.css.map +1 -0
- package/static/browser/ldap-browser.esm.js +915 -0
- package/static/browser/ldap-browser.esm.js.map +1 -0
- package/static/browser/ldap-browser.js +925 -0
- package/static/browser/ldap-browser.js.map +1 -0
- package/static/browser/ldap-browser.min.js +2 -0
- package/static/browser/ldap-browser.min.js.map +1 -0
- package/static/browser/ldap-unit-editor/components/UnitTree.js +0 -1
- package/static/browser/ldap-unit-editor/components/UnitTree.js.map +1 -1
- package/static/browser/ldap-unit-editor.esm.js +0 -1
- package/static/browser/ldap-unit-editor.esm.js.map +1 -1
- package/static/browser/ldap-unit-editor.js +0 -1
- package/static/browser/ldap-unit-editor.js.map +1 -1
- package/static/browser/ldap-unit-editor.min.js.map +1 -1
|
@@ -0,0 +1,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 {};
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { Express, Request, Response } from 'express';
|
|
2
2
|
import DmPlugin, { type Role } from '../abstract/plugin';
|
|
3
|
+
declare global {
|
|
4
|
+
namespace Express {
|
|
5
|
+
interface Request {
|
|
6
|
+
logDetails?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
3
10
|
export default class WebLogs extends DmPlugin {
|
|
4
11
|
name: string;
|
|
5
12
|
roles: Role[];
|
|
@@ -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 |
|
|
@@ -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
|
```
|