ldap-rest 0.26.0 → 1.0.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/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/twakePlugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/lib/authz/composition.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/mailDomains.js +1 -0
- package/dist/lib/requestContext.js +1 -0
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/auth/authzDynamic.js +1 -1
- package/dist/plugins/auth/authzLinid1.js +1 -1
- package/dist/plugins/auth/authzPerBranch.js +1 -1
- package/dist/plugins/auth/authzScope.js +1 -1
- package/dist/plugins/configApi.js +1 -1
- package/dist/plugins/ldap/bulkImport.js +1 -1
- package/dist/plugins/ldap/departmentSync.js +1 -1
- package/dist/plugins/ldap/enterpriseRules.js +1 -1
- package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/passwordPolicy.js +1 -1
- package/dist/plugins/ldap/raw.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/rabbitmq.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/storage/ldap.js +1 -1
- package/dist/plugins/storage/postgres.js +1 -1
- package/dist/plugins/storage/valkey.js +1 -1
- package/dist/plugins/twake/appAccountsApi.js +1 -1
- package/dist/plugins/twake/appAccountsConsistency.js +1 -1
- package/dist/plugins/twake/clouderyProvision.js +1 -1
- package/dist/plugins/twake/groups.js +1 -1
- package/dist/plugins/twake/instances.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/plugins/twake/lifecycleEvents.js +1 -1
- package/dist/plugins/twake/spaces.js +1 -1
- package/dist/plugins/twake/tombstone.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +25 -6
- package/dist/src/abstract/plugin.d.ts +1 -1
- package/dist/src/bin/index.d.ts +12 -0
- package/dist/src/lib/authz/base.d.ts +87 -4
- package/dist/src/lib/authz/composition.d.ts +21 -0
- package/dist/src/lib/ldapActions.d.ts +139 -18
- package/dist/src/lib/mailDomains.d.ts +41 -0
- package/dist/src/lib/requestContext.d.ts +23 -0
- package/dist/src/lib/utils.d.ts +41 -2
- package/dist/src/plugins/auth/authzDynamic.d.ts +5 -12
- package/dist/src/plugins/auth/authzLinid1.d.ts +3 -1
- package/dist/src/plugins/auth/authzPerBranch.d.ts +5 -9
- package/dist/src/plugins/ldap/accountLifecycle.d.ts +2 -2
- package/dist/src/plugins/ldap/bulkImport.d.ts +30 -5
- package/dist/src/plugins/ldap/enterpriseRules.d.ts +11 -1
- package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +20 -3
- package/dist/src/plugins/ldap/groups.d.ts +48 -1
- package/dist/src/plugins/ldap/organizations.d.ts +4 -4
- package/dist/src/plugins/twake/appAccountsApi.d.ts +3 -67
- package/dist/src/plugins/twake/groups.d.ts +46 -2
- package/dist/src/plugins/twake/james.d.ts +0 -11
- package/docs/README.md +25 -0
- package/docs/plugin-development/README.md +61 -15
- package/docs/plugin-development/dependencies.md +8 -2
- package/docs/plugin-development/hooks.md +4 -5
- package/docs/usage/configuration.md +11 -8
- package/docs/usage/plugins/auth/README.md +22 -3
- package/docs/usage/plugins/auth/authz-linid1.md +14 -5
- package/docs/usage/plugins/auth/authz-per-branch.md +60 -9
- package/docs/usage/plugins/integrations/app-accounts.md +5 -0
- package/docs/usage/plugins/integrations/groups.md +36 -3
- package/docs/usage/plugins/integrations/scim.md +7 -0
- package/docs/usage/plugins/integrations/spaces.md +3 -1
- package/docs/usage/plugins/ldap/bulk-import.md +20 -0
- package/docs/usage/plugins/ldap/enterprise-rules.md +12 -2
- package/docs/usage/plugins/ldap/external-users.md +8 -3
- package/docs/usage/plugins/ldap/flat-generic.md +3 -0
- package/docs/usage/plugins/ldap/groups.md +6 -0
- package/docs/usage/plugins/ldap/organizations.md +17 -0
- package/docs/usage/plugins/ldap/raw.md +1 -1
- package/docs/usage/upgrading.md +228 -0
- package/package.json +1 -1
- package/static/schemas/example/groups.json +1 -0
- package/static/schemas/example/organizations.json +1 -0
- package/static/schemas/example/users.json +3 -0
- package/static/schemas/twake/groups.json +2 -0
- package/static/schemas/twake/organizations.json +1 -0
- package/static/schemas/twake/teamMailboxes.json +3 -1
- package/static/schemas/twake/users.json +3 -0
|
@@ -4,6 +4,7 @@ import type winston from 'winston';
|
|
|
4
4
|
import pLimit from 'p-limit';
|
|
5
5
|
import { type Config } from '../config/args';
|
|
6
6
|
import { type DM } from '../bin';
|
|
7
|
+
import type { Hooks } from '../hooks';
|
|
7
8
|
import { type ChangeContext } from './changeContext';
|
|
8
9
|
import { SchemaIndex } from './ldapSchema';
|
|
9
10
|
export type AttributeValue = Buffer | Buffer[] | string[] | string;
|
|
@@ -14,15 +15,13 @@ export type { SearchOptions, SearchResult };
|
|
|
14
15
|
* The directory as seen by one HTTP request.
|
|
15
16
|
*
|
|
16
17
|
* `ldapActions`'s own methods take the request as a trailing optional
|
|
17
|
-
* argument,
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* argument, which the authorization plugins judge by. One naming none is
|
|
19
|
+
* made for the request being served, if any (#385); this says which
|
|
20
|
+
* request is meant rather than leaving it to the context.
|
|
20
21
|
*
|
|
21
22
|
* These methods take no request parameter: it is captured once by
|
|
22
|
-
* `ldapActions.forRequest()
|
|
23
|
-
*
|
|
24
|
-
* for work that genuinely belongs to no request, such as startup and cron
|
|
25
|
-
* tasks, where passing nothing is the deliberate answer.
|
|
23
|
+
* `ldapActions.forRequest()`. Request handlers should bind once and use
|
|
24
|
+
* this; the server's own work goes through {@link ldapActions.system}.
|
|
26
25
|
*/
|
|
27
26
|
export interface RequestBoundLdap {
|
|
28
27
|
search(options: SearchOptions, base?: string): Promise<SearchResult | AsyncGenerator<SearchResult>>;
|
|
@@ -65,6 +64,8 @@ export interface ModifyOptions {
|
|
|
65
64
|
*/
|
|
66
65
|
declaredClasses?: string[];
|
|
67
66
|
}
|
|
67
|
+
/** The request hooks an operation is judged in, see `ldapActions.authorize` */
|
|
68
|
+
type AuthorizedHook = 'ldapaddrequest' | 'ldapmodifyrequest' | 'ldapdeleterequest' | 'ldaprenamerequest';
|
|
68
69
|
declare class ldapActions {
|
|
69
70
|
config: Config;
|
|
70
71
|
options: ClientOptions;
|
|
@@ -106,6 +107,13 @@ declare class ldapActions {
|
|
|
106
107
|
private schemaLoading?;
|
|
107
108
|
/** Whether the last schema read failed, so a failure is logged once. */
|
|
108
109
|
private schemaUnavailable;
|
|
110
|
+
/**
|
|
111
|
+
* How this server spells attribute names, by name in lower case: the
|
|
112
|
+
* configuration's, then the loaded schemas'. See {@link canonicalName}.
|
|
113
|
+
*/
|
|
114
|
+
private spellings;
|
|
115
|
+
/** The first name given for each attribute type, to warn of another. */
|
|
116
|
+
private spelledTypes;
|
|
109
117
|
/** Class problems already reported, per entry. See {@link warnOnce}. */
|
|
110
118
|
private classWarnings;
|
|
111
119
|
constructor(server: DM);
|
|
@@ -156,31 +164,91 @@ declare class ldapActions {
|
|
|
156
164
|
* below it and the extra test costs nothing.
|
|
157
165
|
*/
|
|
158
166
|
invalidateCache(dn: string): void;
|
|
167
|
+
/**
|
|
168
|
+
* Drop every cached read holding a value that names `dn`, or an entry
|
|
169
|
+
* below it.
|
|
170
|
+
*
|
|
171
|
+
* The directory mirrors some writes onto other entries than the one
|
|
172
|
+
* written: the `memberof` overlay keeps each member's `memberOf` in step
|
|
173
|
+
* with the group's `member` / `uniqueMember`, and `refint` rewrites or
|
|
174
|
+
* removes the references to an entry renamed or deleted. Those entries'
|
|
175
|
+
* cached reads are what {@link invalidateCache} cannot find, and they name
|
|
176
|
+
* the entry written: the members of a group name it in their `memberOf`.
|
|
177
|
+
* A member a write adds names it only once the write has landed; the
|
|
178
|
+
* caller drops those by their own DN.
|
|
179
|
+
*
|
|
180
|
+
* Every attribute is looked at, not `memberOf` alone: which attributes an
|
|
181
|
+
* overlay maintains is the deployment's configuration. A read naming `dn`
|
|
182
|
+
* for another reason is dropped too, which costs a miss.
|
|
183
|
+
*/
|
|
184
|
+
private invalidateReferences;
|
|
159
185
|
/**
|
|
160
186
|
* The directory as the server itself, with no request behind it.
|
|
161
187
|
*
|
|
162
|
-
* Every authorization plugin
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
188
|
+
* Every authorization plugin judges an operation by its request, and
|
|
189
|
+
* judges nothing without one. Some work belongs to nobody in particular:
|
|
190
|
+
* a startup task, a cron job, a uniqueness check, a referential-integrity
|
|
191
|
+
* check. Those must see the
|
|
166
192
|
* whole directory to be correct: a caller who cannot *read* `uid=jdoe`
|
|
167
193
|
* must still not be handed it as a free identifier, and a reference to an
|
|
168
194
|
* organization they cannot read is still a valid reference.
|
|
169
195
|
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
196
|
+
* Writes included, since a scheduled task writes: the examples above are
|
|
197
|
+
* reads because that is where the ambiguity lives, not because a write
|
|
198
|
+
* cannot belong to nobody.
|
|
199
|
+
*
|
|
200
|
+
* While a request is being served, an operation that names none is made
|
|
201
|
+
* for it ({@link currentRequest}), so `ldap.search(…)` is judged as the
|
|
202
|
+
* caller. These operations step out of the request instead: they are the
|
|
203
|
+
* server's, whatever plugin serves the request — `authzDynamic` included.
|
|
204
|
+
* Each use says why the work is the server's.
|
|
205
|
+
*
|
|
206
|
+
* Only those six step out: `system.readableBy` and `system.forRequest`
|
|
207
|
+
* still judge the request they are given or find.
|
|
177
208
|
*/
|
|
178
209
|
get system(): this;
|
|
210
|
+
/** {@link system}, built once. */
|
|
211
|
+
private systemView?;
|
|
179
212
|
/**
|
|
180
213
|
* Bind every directory operation to one request, so the authorization
|
|
181
214
|
* hooks always see it. See {@link RequestBoundLdap}.
|
|
182
215
|
*/
|
|
183
216
|
forRequest(req: Request): RequestBoundLdap;
|
|
217
|
+
/**
|
|
218
|
+
* Whether the caller of a request can read an entry.
|
|
219
|
+
*
|
|
220
|
+
* For a route that finds an entry as the server — by an attribute, across
|
|
221
|
+
* branches — and then answers about it: an entry the caller cannot read
|
|
222
|
+
* must get the answer an entry that is not there gets, or the route tells
|
|
223
|
+
* them what the directory holds where they cannot read. Refused or
|
|
224
|
+
* filtered out by an authorization plugin, the entry reads as absent.
|
|
225
|
+
* Without a request, as for any operation, nobody judges.
|
|
226
|
+
*
|
|
227
|
+
* @param dn the entry
|
|
228
|
+
* @param req the request whose caller asks
|
|
229
|
+
* @returns whether a read of the entry, as that caller, finds it
|
|
230
|
+
*/
|
|
231
|
+
readableBy(dn: string, req?: Request): Promise<boolean>;
|
|
232
|
+
/**
|
|
233
|
+
* Have the authorization plugins judge an operation, without making it.
|
|
234
|
+
*
|
|
235
|
+
* A route that looks in the directory before it writes — whether an entry
|
|
236
|
+
* is there, which organization holds it, whether a pointer leads
|
|
237
|
+
* somewhere — answers what it found, as the server, to whoever asked. A
|
|
238
|
+
* caller who may not make the write then learns, one request at a time,
|
|
239
|
+
* what the directory holds where they cannot read. Judged first, they get
|
|
240
|
+
* the 403 the write itself would get, whether the entry is there or not.
|
|
241
|
+
*
|
|
242
|
+
* Only the hooks of the plugins with the `authz` role run, and what they
|
|
243
|
+
* return is dropped: the operation, when it is made, runs every hook
|
|
244
|
+
* again. Without a request in the arguments, the one being served is
|
|
245
|
+
* judged, as for the operation itself; outside any, nobody judges.
|
|
246
|
+
*
|
|
247
|
+
* @param hook the request hook the operation launches
|
|
248
|
+
* @param args what the operation hands that hook
|
|
249
|
+
* @throws the refusal of the first authorization plugin refusing
|
|
250
|
+
*/
|
|
251
|
+
authorize<H extends AuthorizedHook>(hook: H, args: Parameters<NonNullable<Hooks[H]>>[0]): Promise<void>;
|
|
184
252
|
/**
|
|
185
253
|
* Declare the object classes an entity gives the entries of its branch.
|
|
186
254
|
*
|
|
@@ -243,6 +311,55 @@ declare class ldapActions {
|
|
|
243
311
|
* @returns indexed schema
|
|
244
312
|
*/
|
|
245
313
|
schemaIndex(): Promise<SchemaIndex>;
|
|
314
|
+
/**
|
|
315
|
+
* Say how this server spells attribute names: a schema loaded by a plugin
|
|
316
|
+
* names its attributes as the plugins compare them. The first spelling of
|
|
317
|
+
* a name wins.
|
|
318
|
+
*
|
|
319
|
+
* @param names attribute names
|
|
320
|
+
*/
|
|
321
|
+
knowAttributeNames(names: Iterable<string>): void;
|
|
322
|
+
/**
|
|
323
|
+
* Name each attribute of a write as this server does, in place.
|
|
324
|
+
*
|
|
325
|
+
* The directory takes `MAIL`, `rfc822Mailbox` and `0.9.2342.19200300.100.1.3`
|
|
326
|
+
* for `mail`, while the authorization plugins and the hooks look for
|
|
327
|
+
* `mail`: a write naming the organization link by its OID was judged as
|
|
328
|
+
* if it left the link alone. Each name is resolved through the directory
|
|
329
|
+
* schema — case, aliases, OIDs — and written as the configuration or a
|
|
330
|
+
* loaded schema spells it, or as the directory declares it first.
|
|
331
|
+
*
|
|
332
|
+
* An option names a value of its own (`member;lang-en` is not `member` to
|
|
333
|
+
* the hooks guarding `member`), and is refused, `;binary` aside. Two names
|
|
334
|
+
* for one attribute in one part of a write are refused too: on a modify
|
|
335
|
+
* the directory applies both, and only one would have been judged.
|
|
336
|
+
*
|
|
337
|
+
* Without the schema, the case is still set, and a name given as an OID,
|
|
338
|
+
* which nothing here could resolve, is refused.
|
|
339
|
+
*
|
|
340
|
+
* @param entry attributes of an add, or one part of a modify
|
|
341
|
+
* @throws BadRequestError on an option, or on one attribute named twice
|
|
342
|
+
*/
|
|
343
|
+
canonicalAttributes(entry: AttributesList | undefined): Promise<void>;
|
|
344
|
+
/**
|
|
345
|
+
* {@link canonicalAttributes} for every part of a modify, in place.
|
|
346
|
+
*
|
|
347
|
+
* @param changes the modify
|
|
348
|
+
* @throws BadRequestError as {@link canonicalAttributes}
|
|
349
|
+
*/
|
|
350
|
+
canonicalChanges(changes: ModifyRequest | undefined): Promise<void>;
|
|
351
|
+
/**
|
|
352
|
+
* The names of one part of a write, as {@link canonicalAttributes} writes
|
|
353
|
+
* them.
|
|
354
|
+
*/
|
|
355
|
+
private canonicalNames;
|
|
356
|
+
/**
|
|
357
|
+
* One attribute description, named as this server does.
|
|
358
|
+
*
|
|
359
|
+
* @param key the name as written, options included
|
|
360
|
+
* @param index the directory schema, when it could be read
|
|
361
|
+
*/
|
|
362
|
+
private canonicalName;
|
|
246
363
|
/**
|
|
247
364
|
* Hand a search result to `ldapsearchfilter`, which may drop entries the
|
|
248
365
|
* caller is not allowed to see.
|
|
@@ -267,6 +384,10 @@ declare class ldapActions {
|
|
|
267
384
|
* Move an entry to a new location (different parent)
|
|
268
385
|
* Uses LDAP modifyDN with full DN to change both RDN and parent
|
|
269
386
|
*
|
|
387
|
+
* Not judged, request or not: a move is the server's, made for a write
|
|
388
|
+
* already judged — the trash turning a delete into one. A route moving an
|
|
389
|
+
* entry for its caller uses {@link rename}, which is judged.
|
|
390
|
+
*
|
|
270
391
|
* Note: ldapts provides a high-level API that accepts a full DN as the second parameter,
|
|
271
392
|
* unlike the standard LDAP modifyDN which expects (newRDN, deleteOldRDN, newSuperior).
|
|
272
393
|
* ldapts automatically parses the full DN and extracts the newRDN and newSuperior components
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module lib/mailDomains
|
|
3
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
+
*
|
|
5
|
+
* The mail domains a directory declares: entries in the domain branches,
|
|
6
|
+
* each naming its domain in an attribute.
|
|
7
|
+
* @group Libraries
|
|
8
|
+
*/
|
|
9
|
+
import type { DM } from '../bin';
|
|
10
|
+
/** Where mail domain entries live, and the attribute naming their domain */
|
|
11
|
+
export interface DomainBranch {
|
|
12
|
+
base: string;
|
|
13
|
+
attribute: string;
|
|
14
|
+
}
|
|
15
|
+
/** The mail domains among values, lower-cased as James stores them */
|
|
16
|
+
export declare const mailDomains: (values: string[] | undefined) => string[];
|
|
17
|
+
/**
|
|
18
|
+
* Where the mail domain entries live, and the attribute naming their
|
|
19
|
+
* domain: each loaded flat schema declaring the `domainName` role, the
|
|
20
|
+
* lookup `core/ldap/enterpriseRules` makes. Without one, the branch of the
|
|
21
|
+
* shipped `twake/nomenclature/twakeDomain.json` and the attribute
|
|
22
|
+
* `--enterprise-domain-name-attribute` names.
|
|
23
|
+
*
|
|
24
|
+
* Rebuilt on each call, as the enterprise rules do theirs: a list taken in
|
|
25
|
+
* a constructor could predate the schemas.
|
|
26
|
+
*
|
|
27
|
+
* @param server the server, whose flat entities are read
|
|
28
|
+
* @returns the domain branches
|
|
29
|
+
*/
|
|
30
|
+
export declare function domainBranches(server: DM): DomainBranch[];
|
|
31
|
+
/**
|
|
32
|
+
* Every mail domain the domain branches declare, lower-cased.
|
|
33
|
+
*
|
|
34
|
+
* Read as the server: which domains the directory serves is not a matter of
|
|
35
|
+
* who asks. A branch that does not exist declares none; any other failure
|
|
36
|
+
* is thrown, since an answer missing domains reads as "not ours".
|
|
37
|
+
*
|
|
38
|
+
* @param server the server
|
|
39
|
+
* @returns the declared domains, each once
|
|
40
|
+
*/
|
|
41
|
+
export declare function declaredMailDomains(server: DM): Promise<string[]>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Serve a request: what runs inside, synchronously or not, is made for it.
|
|
4
|
+
*
|
|
5
|
+
* @param req the authenticated request
|
|
6
|
+
* @param fn what serves it
|
|
7
|
+
* @returns what `fn` returns
|
|
8
|
+
*/
|
|
9
|
+
export declare function runForRequest<T>(req: Request, fn: () => T): T;
|
|
10
|
+
/**
|
|
11
|
+
* Run something as the server, outside the request being served: its
|
|
12
|
+
* operations are judged by nobody. What `ldap.system` does.
|
|
13
|
+
*
|
|
14
|
+
* @param fn what to run
|
|
15
|
+
* @returns what it returns
|
|
16
|
+
*/
|
|
17
|
+
export declare function outsideRequest<T>(fn: () => T): T;
|
|
18
|
+
/**
|
|
19
|
+
* The request being served.
|
|
20
|
+
*
|
|
21
|
+
* @returns the request, or undefined outside any, or as the server
|
|
22
|
+
*/
|
|
23
|
+
export declare function currentRequest(): Request | undefined;
|
package/dist/src/lib/utils.d.ts
CHANGED
|
@@ -147,7 +147,9 @@ export declare function unescapeDnValue(value: string): string;
|
|
|
147
147
|
export declare function validateDnValue(value: string, fieldName: string): void;
|
|
148
148
|
/**
|
|
149
149
|
* Parse a Distinguished Name (DN) into its component parts (RDNs)
|
|
150
|
-
* Handles escaped commas and other special characters
|
|
150
|
+
* Handles escaped commas and other special characters, and what OpenLDAP
|
|
151
|
+
* still reads from LDAPv2 (RFC 1779): `;` separating RDNs as `,` does, and
|
|
152
|
+
* a value in double quotes holding either
|
|
151
153
|
*
|
|
152
154
|
* @param dn - The DN to parse
|
|
153
155
|
* @returns Array of RDN components
|
|
@@ -164,6 +166,15 @@ export declare function validateDnValue(value: string, fieldName: string): void;
|
|
|
164
166
|
* @throws BadRequestError if `dn` is not a string
|
|
165
167
|
*/
|
|
166
168
|
export declare function parseDn(dn: string): string[];
|
|
169
|
+
/**
|
|
170
|
+
* Whether a string has the shape of a DN: RDNs of `type=value` assertions,
|
|
171
|
+
* a type being a name or an OID. What the directory would refuse as
|
|
172
|
+
* `invalidDNSyntax` is answered without asking it.
|
|
173
|
+
*
|
|
174
|
+
* @param dn - The string
|
|
175
|
+
* @returns whether it reads as a DN
|
|
176
|
+
*/
|
|
177
|
+
export declare function isDnSyntax(dn: string): boolean;
|
|
167
178
|
/**
|
|
168
179
|
* Extract the parent DN from a DN
|
|
169
180
|
* Removes the first RDN component to get the parent branch
|
|
@@ -228,6 +239,32 @@ export declare function rdnValue(dn: string): string;
|
|
|
228
239
|
* ```
|
|
229
240
|
*/
|
|
230
241
|
export declare function isChildOf(dn: string, parentDn: string): boolean;
|
|
242
|
+
/**
|
|
243
|
+
* Teach {@link normalizeDn} the names and OIDs of attribute types: each is
|
|
244
|
+
* read as the type's first name.
|
|
245
|
+
*
|
|
246
|
+
* @param types attribute types, as `ldapSchema` parses them
|
|
247
|
+
*/
|
|
248
|
+
export declare function registerAttributeTypes(types: Iterable<{
|
|
249
|
+
oid: string;
|
|
250
|
+
names: string[];
|
|
251
|
+
}>): void;
|
|
252
|
+
/**
|
|
253
|
+
* The name an attribute type is known by, spelled as declared, from the
|
|
254
|
+
* names and OIDs {@link registerAttributeTypes} was given: `COMMONNAME`,
|
|
255
|
+
* `2.5.4.3` and `OID.2.5.4.3` are `cn`.
|
|
256
|
+
*
|
|
257
|
+
* @param type attribute name or OID
|
|
258
|
+
* @returns the name, or undefined for a type it was not given
|
|
259
|
+
*/
|
|
260
|
+
export declare function knownAttributeName(type: string): string | undefined;
|
|
261
|
+
/**
|
|
262
|
+
* The attribute types the first RDN of a DN names, as {@link normalizeDn}
|
|
263
|
+
* reads them: `CN=a+2.5.4.11=b,…` names `cn` and `ou`.
|
|
264
|
+
*
|
|
265
|
+
* @param dn the DN
|
|
266
|
+
*/
|
|
267
|
+
export declare function rdnTypes(dn: string): string[];
|
|
231
268
|
/**
|
|
232
269
|
* Normalize a DN into a canonical, comparable form.
|
|
233
270
|
*
|
|
@@ -238,7 +275,9 @@ export declare function isChildOf(dn: string, parentDn: string): boolean;
|
|
|
238
275
|
* `,`. Empty components (e.g. from a trailing separator) are dropped.
|
|
239
276
|
*
|
|
240
277
|
* This makes equality / suffix comparisons robust to case, surrounding
|
|
241
|
-
* whitespace
|
|
278
|
+
* whitespace, multi-valued RDN ordering, the escapes a directory answers
|
|
279
|
+
* with ({@link canonicalValue}) and the names and OIDs of an attribute type
|
|
280
|
+
* ({@link registerAttributeTypes}) — unlike a raw string match.
|
|
242
281
|
*
|
|
243
282
|
* @param dn - The DN to normalize
|
|
244
283
|
* @returns The canonical form
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
* Sub-branch matching is used (a permission on `ou=acme` covers
|
|
25
25
|
* `ou=users,ou=acme`).
|
|
26
26
|
*/
|
|
27
|
-
import { AsyncLocalStorage } from 'async_hooks';
|
|
28
27
|
import type { Express, Response } from 'express';
|
|
29
28
|
import { type Role } from '../../abstract/plugin';
|
|
30
29
|
import type { DM } from '../../bin';
|
|
@@ -51,16 +50,6 @@ export interface TokenEntry {
|
|
|
51
50
|
export interface AuthzDynamicRequest extends DmRequest {
|
|
52
51
|
authzToken?: TokenEntry;
|
|
53
52
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Per-request async context holding the authenticated token.
|
|
56
|
-
*
|
|
57
|
-
* Core plugins (ldapGroups, ldapFlat, etc.) do not always forward the Express
|
|
58
|
-
* `req` object down to `ldapActions.search()` / `.modify()`. Relying solely
|
|
59
|
-
* on `req` would therefore let un-tracked operations bypass authorization.
|
|
60
|
-
* Using AsyncLocalStorage gives us a request-scoped, ambient handle that the
|
|
61
|
-
* authz hooks can read regardless of whether the caller threaded `req`.
|
|
62
|
-
*/
|
|
63
|
-
export declare const authzContext: AsyncLocalStorage<TokenEntry>;
|
|
64
53
|
export default class AuthzDynamic extends AuthBase {
|
|
65
54
|
name: string;
|
|
66
55
|
roles: Role[];
|
|
@@ -175,7 +164,11 @@ export default class AuthzDynamic extends AuthBase {
|
|
|
175
164
|
* request.
|
|
176
165
|
*/
|
|
177
166
|
hooks: Hooks;
|
|
178
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* The token of the request an operation is made for: the one it names,
|
|
169
|
+
* or the one being served (`ldapActions` fills it in). An operation the
|
|
170
|
+
* server makes as itself (`ldap.system`) has none, and is not judged.
|
|
171
|
+
*/
|
|
179
172
|
private activeToken;
|
|
180
173
|
/**
|
|
181
174
|
* Throw a ForbiddenError (403) if the token cannot perform `permission` on
|
|
@@ -82,8 +82,10 @@ export default class AuthzLinid1 extends AuthzBase {
|
|
|
82
82
|
* Get list of branches user has access to
|
|
83
83
|
*/
|
|
84
84
|
getAuthorizedBranches(userDn: string): Promise<string[]>;
|
|
85
|
+
/** The organizations naming the user an administrator */
|
|
86
|
+
ruleBranches(userDn: string): Promise<string[]>;
|
|
85
87
|
/**
|
|
86
|
-
*
|
|
88
|
+
* The caller's rights on a branch: see {@link mostSpecificBranchRule}.
|
|
87
89
|
*/
|
|
88
90
|
private findBranchPermissions;
|
|
89
91
|
}
|
|
@@ -65,22 +65,18 @@ export default class AuthzPerBranch extends AuthzBase {
|
|
|
65
65
|
* Get list of branches user has read access to (base class implementation)
|
|
66
66
|
*/
|
|
67
67
|
getAuthorizedBranches(uid: string): Promise<string[]>;
|
|
68
|
+
/**
|
|
69
|
+
* The branches named by the user's rules and their groups'
|
|
70
|
+
*/
|
|
71
|
+
ruleBranches(uid: string): Promise<string[]>;
|
|
68
72
|
/**
|
|
69
73
|
* Get list of branches user has access to for a given permission type
|
|
70
74
|
*/
|
|
71
75
|
getAuthorizedBranchesForPermission(uid: string, permissionType: 'read' | 'write' | 'delete'): Promise<string[]>;
|
|
72
76
|
/**
|
|
73
|
-
*
|
|
77
|
+
* The subject's rule on a branch: see {@link mostSpecificBranchRule}.
|
|
74
78
|
*/
|
|
75
79
|
private findBranchPermissions;
|
|
76
|
-
/**
|
|
77
|
-
* Merge permissions (OR logic - grant if any source grants)
|
|
78
|
-
*/
|
|
79
|
-
private mergePermissions;
|
|
80
|
-
/**
|
|
81
|
-
* Build LDAP filter for authorized branches
|
|
82
|
-
*/
|
|
83
|
-
private buildBranchFilter;
|
|
84
80
|
/**
|
|
85
81
|
* The rules configured for a group, compared as DNs.
|
|
86
82
|
*
|
|
@@ -51,8 +51,8 @@ export default class LdapAccountLifecycle extends DmPlugin {
|
|
|
51
51
|
* @param entity flat entity holding the account
|
|
52
52
|
* @param id identifier or DN of the entry
|
|
53
53
|
* @param changes modification to apply
|
|
54
|
-
* @param req request this comes from
|
|
55
|
-
*
|
|
54
|
+
* @param req request this comes from, which the authorization plugins
|
|
55
|
+
* judge the write by
|
|
56
56
|
*/
|
|
57
57
|
private modify;
|
|
58
58
|
}
|
|
@@ -25,11 +25,11 @@ import type { DM } from '../../bin';
|
|
|
25
25
|
* example: 42
|
|
26
26
|
* created:
|
|
27
27
|
* type: integer
|
|
28
|
-
* description: Entries successfully created (or
|
|
28
|
+
* description: Entries successfully created (or that would be, in a dry run).
|
|
29
29
|
* example: 38
|
|
30
30
|
* updated:
|
|
31
31
|
* type: integer
|
|
32
|
-
* description: Existing entries updated.
|
|
32
|
+
* description: Existing entries updated (or that would be, in a dry run).
|
|
33
33
|
* example: 2
|
|
34
34
|
* skipped:
|
|
35
35
|
* type: integer
|
|
@@ -58,7 +58,8 @@ import type { DM } from '../../bin';
|
|
|
58
58
|
* type: string
|
|
59
59
|
* description: |
|
|
60
60
|
* `unique` or `mailDomain`, with `attribute`, `value` and
|
|
61
|
-
* `domains`, as in the `Error` answer of a single write
|
|
61
|
+
* `domains`, as in the `Error` answer of a single write;
|
|
62
|
+
* `forbidden` when the caller may not write the row.
|
|
62
63
|
* attribute: { type: string }
|
|
63
64
|
* value: { type: string }
|
|
64
65
|
* domains:
|
|
@@ -97,9 +98,33 @@ export default class LdapBulkImport extends DmPlugin {
|
|
|
97
98
|
*/
|
|
98
99
|
private bulkImport;
|
|
99
100
|
/**
|
|
100
|
-
*
|
|
101
|
+
* Whether the caller may write a row: create its entry, or change the one
|
|
102
|
+
* it names. Which of the two the row turns into is known only once the
|
|
103
|
+
* directory has been read, so the caller is judged first on both.
|
|
104
|
+
*
|
|
105
|
+
* The creation alone is not enough: judged by attachment, an entry is
|
|
106
|
+
* judged on the organization the row links it to, or on its parent when
|
|
107
|
+
* the row links it to none, while a change is judged on the organization
|
|
108
|
+
* it is attached to. An administrator updating their own accounts from a
|
|
109
|
+
* file naming no organization would be refused.
|
|
110
|
+
*
|
|
111
|
+
* No oracle: the change is judged on the entry as stored, and an entry
|
|
112
|
+
* that is not there is judged on its parent, as a creation without a link
|
|
113
|
+
* is.
|
|
114
|
+
*
|
|
115
|
+
* @throws the refusal of the creation when neither is allowed
|
|
101
116
|
*/
|
|
102
|
-
private
|
|
117
|
+
private authorizeRow;
|
|
118
|
+
/**
|
|
119
|
+
* Build the entry of a CSV record, reading nothing from the directory: the
|
|
120
|
+
* write it makes is judged on it before anything is looked for.
|
|
121
|
+
*/
|
|
122
|
+
private buildRecord;
|
|
123
|
+
/**
|
|
124
|
+
* Complete a record once its write has been judged: the organization path,
|
|
125
|
+
* read from the organization, then the required attributes.
|
|
126
|
+
*/
|
|
127
|
+
private completeRecord;
|
|
103
128
|
/**
|
|
104
129
|
* Get editable attributes from schema (exclude fixed and calculated)
|
|
105
130
|
*/
|
|
@@ -214,11 +214,19 @@ export default class LdapEnterpriseRules extends DmPlugin {
|
|
|
214
214
|
* compared by content, and the entry being updated is excluded from the
|
|
215
215
|
* search — so re-sending an unchanged name no longer conflicts with itself.
|
|
216
216
|
*
|
|
217
|
+
* Nor with its copies: a value the entry already holds, as stored, is not
|
|
218
|
+
* looked for again, or the copies other plugins keep of the entry — the
|
|
219
|
+
* applicative accounts of `appAccountsConsistency` carry its mail — would
|
|
220
|
+
* hold it against it. Held under another attribute, it is skipped only
|
|
221
|
+
* when that attribute's own constraint looked for it wherever this one
|
|
222
|
+
* looks: a value accepted under a narrower one is checked here.
|
|
223
|
+
*
|
|
217
224
|
* @param entity entity the entry belongs to
|
|
218
225
|
* @param name attribute being checked
|
|
219
226
|
* @param attr its schema definition
|
|
220
227
|
* @param values values submitted for it
|
|
221
|
-
* @param
|
|
228
|
+
* @param dn entry being written
|
|
229
|
+
* @param current entry as stored, on a modification; its DN is excluded
|
|
222
230
|
* @throws ConflictError naming the entry that already holds the value
|
|
223
231
|
*/
|
|
224
232
|
private checkUnique;
|
|
@@ -341,6 +349,8 @@ export default class LdapEnterpriseRules extends DmPlugin {
|
|
|
341
349
|
* @param dn DN to look for
|
|
342
350
|
* @returns the matching entries, carrying that attribute
|
|
343
351
|
*/
|
|
352
|
+
/** The bases found missing, said once each */
|
|
353
|
+
private readonly missingBases;
|
|
344
354
|
private searchReferrers;
|
|
345
355
|
/**
|
|
346
356
|
* Every reference the directory holds to a DN.
|
|
@@ -9,6 +9,12 @@ import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
|
9
9
|
import type { DM } from '../../bin';
|
|
10
10
|
import { Hooks } from '../../hooks';
|
|
11
11
|
import ldapActions from '../../lib/ldapActions';
|
|
12
|
+
/**
|
|
13
|
+
* The domain of an address, as the managed domains are compared: after its
|
|
14
|
+
* last `@` (the local part may hold one, quoted), lower-cased, without the
|
|
15
|
+
* trailing dot DNS allows. Empty for an address without one.
|
|
16
|
+
*/
|
|
17
|
+
export declare const mailDomainOf: (mail: string) => string;
|
|
12
18
|
export default class ExternalUsersInGroups extends DmPlugin {
|
|
13
19
|
name: string;
|
|
14
20
|
roles: Role[];
|
|
@@ -23,10 +29,21 @@ export default class ExternalUsersInGroups extends DmPlugin {
|
|
|
23
29
|
* right under the branch. Null for any other member.
|
|
24
30
|
*/
|
|
25
31
|
externalAddress(member: string): string | null;
|
|
26
|
-
/**
|
|
27
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The domains no external member may use, lower-cased: those of
|
|
34
|
+
* `--mail-domain` and those the directory declares in its domain branches,
|
|
35
|
+
* the ones `core/twake/james` follows. A deployment declaring its domains
|
|
36
|
+
* in the directory only would otherwise take an address of its own for an
|
|
37
|
+
* external one, and so would a client reading the list from `/config`.
|
|
38
|
+
*
|
|
39
|
+
* A subdomain of a managed domain is not managed: it is external unless
|
|
40
|
+
* declared itself.
|
|
41
|
+
*/
|
|
42
|
+
managedDomains(): Promise<string[]>;
|
|
43
|
+
/** `--mail-domain`, lower-cased */
|
|
44
|
+
private configuredDomains;
|
|
28
45
|
/**
|
|
29
46
|
* Where a client sends an external member: `mail=<escaped address>,<branch>`
|
|
30
47
|
*/
|
|
31
|
-
getConfigApiData(): Record<string, unknown
|
|
48
|
+
getConfigApiData(): Promise<Record<string, unknown>>;
|
|
32
49
|
}
|
|
@@ -164,6 +164,17 @@ export default class LdapGroups extends DmPlugin {
|
|
|
164
164
|
* LDAP group methods
|
|
165
165
|
*/
|
|
166
166
|
addGroup(cn: string, members?: string[], additional?: AttributesList, req?: Request): Promise<boolean>;
|
|
167
|
+
/**
|
|
168
|
+
* The entry `addGroup` writes, as the plugin's own hooks receive it. Built
|
|
169
|
+
* in one place, so that a creation judged before it is made — here, or by
|
|
170
|
+
* a plugin checking something first — is the creation made.
|
|
171
|
+
*
|
|
172
|
+
* @param cn the group's cn
|
|
173
|
+
* @param members member DNs
|
|
174
|
+
* @param additional attributes sent
|
|
175
|
+
* @returns the entry
|
|
176
|
+
*/
|
|
177
|
+
candidateGroup(cn: string, members: string[], additional: AttributesList): AttributesList;
|
|
167
178
|
modifyGroup(cn: string, changes: {
|
|
168
179
|
add?: AttributesList;
|
|
169
180
|
replace?: AttributesList;
|
|
@@ -239,7 +250,43 @@ export default class LdapGroups extends DmPlugin {
|
|
|
239
250
|
* @returns LdapList (means Record<string, AttributesList> where key is the --ldap-user-main-attribute value [default: cn])
|
|
240
251
|
*/
|
|
241
252
|
searchGroupsByName(cn: string, partial?: boolean, attributes?: string[], req?: Request): Promise<LdapList>;
|
|
242
|
-
|
|
253
|
+
/**
|
|
254
|
+
* The DN of a group named by its `cn`, or given as a DN: one with a comma.
|
|
255
|
+
* A DN the directory would refuse is answered 400 rather than its error.
|
|
256
|
+
*
|
|
257
|
+
* @param cn the group's name, or its DN
|
|
258
|
+
* @returns its DN
|
|
259
|
+
*/
|
|
260
|
+
protected toGroupDn(cn: string): string;
|
|
261
|
+
/**
|
|
262
|
+
* {@link toGroupDn} for a route serving the groups branch: what is not
|
|
263
|
+
* below it, nor named as these groups are (`cn=`), is no group of these
|
|
264
|
+
* routes, whatever entry it is (#395). The branch may be the whole
|
|
265
|
+
* directory, which is the default: the naming is what tells an account
|
|
266
|
+
* from a group there. A multi-valued RDN holding the main attribute
|
|
267
|
+
* passes here; the group class, checked next, is what settles it.
|
|
268
|
+
*
|
|
269
|
+
* @param cn the group's name, or its DN
|
|
270
|
+
* @returns its DN
|
|
271
|
+
*/
|
|
272
|
+
protected routeGroupDn(cn: string): string;
|
|
273
|
+
/**
|
|
274
|
+
* A filter matching a group: one of the classes this plugin gives groups
|
|
275
|
+
* (`--group-class`), or a standard group class.
|
|
276
|
+
*/
|
|
277
|
+
private groupFilter;
|
|
278
|
+
/**
|
|
279
|
+
* That the entry a route writes is a group, once the write is judged:
|
|
280
|
+
* another entry named alike is not one, and is not written as one (#395).
|
|
281
|
+
*
|
|
282
|
+
* Judged first, a caller who may not make the write gets the refusal
|
|
283
|
+
* whether the entry is there, a group or not; only one who may learns
|
|
284
|
+
* that it is none.
|
|
285
|
+
*
|
|
286
|
+
* @param dn the entry, from {@link routeGroupDn}
|
|
287
|
+
* @param judge the authorization of the write, as `ldap.authorize`
|
|
288
|
+
*/
|
|
289
|
+
protected judgedGroup(dn: string, judge: () => Promise<void>): Promise<void>;
|
|
243
290
|
/**
|
|
244
291
|
* Verify that each member exists in LDAP
|
|
245
292
|
*
|