ldap-rest 0.25.1 → 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/instanceProviders.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/instanceProviders.d.ts +8 -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/instances.md +6 -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
|
@@ -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
|
*
|
|
@@ -219,7 +219,8 @@ export default class LdapOrganizations extends DmPlugin {
|
|
|
219
219
|
* search just learned to survive: a directory that will not walk the whole
|
|
220
220
|
* link filter is told about rather than turned into an empty list.
|
|
221
221
|
*
|
|
222
|
-
* @param baseDn branch the attached entries live in
|
|
222
|
+
* @param baseDn branch the attached entries live in; the organization's
|
|
223
|
+
* own, for a caller who may not search it
|
|
223
224
|
* @param filter what links them to the organization
|
|
224
225
|
* @param cap how many to return
|
|
225
226
|
* @param dn organization they are attached to, for the indicator's DN
|
|
@@ -253,9 +254,8 @@ export default class LdapOrganizations extends DmPlugin {
|
|
|
253
254
|
* The same two searches as `getOrganisationSubnodes`, narrowed by a query.
|
|
254
255
|
*
|
|
255
256
|
* Both go through the helpers that classify a failure rather than reading
|
|
256
|
-
* every one of them as emptiness (#179), and both
|
|
257
|
-
*
|
|
258
|
-
* there is none — the same gap the flat routes had until 0.8.2. The
|
|
257
|
+
* every one of them as emptiness (#179), and both carry the request the
|
|
258
|
+
* authorization plugins judge them by. The
|
|
259
259
|
* attached entries are capped like the other endpoint's, since an answer
|
|
260
260
|
* the directory refuses to finish is what this is about.
|
|
261
261
|
*
|
|
@@ -1,71 +1,6 @@
|
|
|
1
1
|
import type { Express } from 'express';
|
|
2
2
|
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
3
3
|
import type { DM } from '../../bin';
|
|
4
|
-
/**
|
|
5
|
-
* @openapi-component
|
|
6
|
-
* AppAccount:
|
|
7
|
-
* type: object
|
|
8
|
-
* description: |
|
|
9
|
-
* An application-specific identity attached to a user. App accounts
|
|
10
|
-
* allow a single principal user to authenticate as different named
|
|
11
|
-
* "devices" or "applications" each with its own credential, while
|
|
12
|
-
* sharing the same mail address.
|
|
13
|
-
* required: [uid]
|
|
14
|
-
* properties:
|
|
15
|
-
* uid:
|
|
16
|
-
* type: string
|
|
17
|
-
* description: |
|
|
18
|
-
* Unique identifier of the app account. Format:
|
|
19
|
-
* `<prefix>_c<8-digits>`, where `<prefix>` is the `:user` path param
|
|
20
|
-
* (the resolution value — the mail by default) sanitized into a
|
|
21
|
-
* uid-safe token (e.g. `alice@example.com` -> `alice_example_com`).
|
|
22
|
-
* example: alice_example_com_c04729183
|
|
23
|
-
* name:
|
|
24
|
-
* type: string
|
|
25
|
-
* description: Human-readable label (stored as LDAP `description`).
|
|
26
|
-
* example: Work laptop
|
|
27
|
-
* example:
|
|
28
|
-
* uid: alice_c04729183
|
|
29
|
-
* name: Work laptop
|
|
30
|
-
* AppAccountCreated:
|
|
31
|
-
* type: object
|
|
32
|
-
* description: |
|
|
33
|
-
* Response returned when a new app account is successfully created.
|
|
34
|
-
* The one-time cleartext password is included here and never returned
|
|
35
|
-
* again — callers must store it immediately.
|
|
36
|
-
* required: [uid, pwd, mail]
|
|
37
|
-
* properties:
|
|
38
|
-
* uid:
|
|
39
|
-
* type: string
|
|
40
|
-
* description: Identifier of the newly created app account.
|
|
41
|
-
* example: alice_c04729183
|
|
42
|
-
* pwd:
|
|
43
|
-
* type: string
|
|
44
|
-
* description: |
|
|
45
|
-
* One-time cleartext password generated for this account.
|
|
46
|
-
* Format: 6 blocks of 4 characters separated by `-`
|
|
47
|
-
* (e.g. `Ab3@-xYz!-9pQ#-Sv4$-mN8!-pQ5@`).
|
|
48
|
-
* OpenLDAP hashes it via ppolicy before storage.
|
|
49
|
-
* example: Ab3@-xYz!-9pQ#-Sv4$-mN8!-pQ5@
|
|
50
|
-
* mail:
|
|
51
|
-
* type: string
|
|
52
|
-
* description: Mail address of the principal account this app account belongs to.
|
|
53
|
-
* example: alice@example.com
|
|
54
|
-
* example:
|
|
55
|
-
* uid: alice_c04729183
|
|
56
|
-
* pwd: Ab3@-xYz!-9pQ#-Sv4$-mN8!-pQ5@
|
|
57
|
-
* mail: alice@example.com
|
|
58
|
-
* AppAccountCreate:
|
|
59
|
-
* type: object
|
|
60
|
-
* description: Request body for creating a new app account.
|
|
61
|
-
* properties:
|
|
62
|
-
* name:
|
|
63
|
-
* type: string
|
|
64
|
-
* description: Optional human-readable label for the new account.
|
|
65
|
-
* example: Work laptop
|
|
66
|
-
* example:
|
|
67
|
-
* name: Work laptop
|
|
68
|
-
*/
|
|
69
4
|
export default class AppAccountsApi extends DmPlugin {
|
|
70
5
|
name: string;
|
|
71
6
|
roles: Role[];
|
|
@@ -87,8 +22,9 @@ export default class AppAccountsApi extends DmPlugin {
|
|
|
87
22
|
* `:user` is matched against the configured resolution attribute
|
|
88
23
|
* (`app_accounts_user_attribute`, default the mail attribute). Mail is
|
|
89
24
|
* globally unique; resolving by the LDAP `uid` instead is only safe when uid
|
|
90
|
-
* is unique directory-wide
|
|
91
|
-
*
|
|
25
|
+
* is unique directory-wide: a `uid` repeated under different subtrees is
|
|
26
|
+
* answered 409 when the caller reads several of its holders, and resolves
|
|
27
|
+
* to the one it reads otherwise (see #88).
|
|
92
28
|
*
|
|
93
29
|
* On failure (no match, or an ambiguous one) this writes the HTTP error
|
|
94
30
|
* response and returns null, so callers can simply `return` when it does.
|
|
@@ -81,6 +81,40 @@ export default class TwakeGroups extends LdapGroups {
|
|
|
81
81
|
* SCIM group mapping leaves the RDN attribute alone.
|
|
82
82
|
*/
|
|
83
83
|
afterLoad(): void;
|
|
84
|
+
/**
|
|
85
|
+
* A group of `org` with its organization link, under an authorization
|
|
86
|
+
* model judging entries by that link (`authzLinid1`,
|
|
87
|
+
* `--authz-filter-attached-entries`): there, a group without one is every
|
|
88
|
+
* administrator's. Elsewhere the group is left as it is, and judged by its
|
|
89
|
+
* branch as before.
|
|
90
|
+
*
|
|
91
|
+
* The auxiliary class `twakeGroup` is added when the group holds neither
|
|
92
|
+
* it nor `twakeStaticGroup`. A link the writer gave is kept when it names
|
|
93
|
+
* the group's organization, refused otherwise: a group in one
|
|
94
|
+
* organization's branch belonging to another would mix their members.
|
|
95
|
+
*
|
|
96
|
+
* @param dn the group's DN
|
|
97
|
+
* @param org its organization
|
|
98
|
+
* @param entry the group as written
|
|
99
|
+
* @returns the group, linked
|
|
100
|
+
* @throws BadRequestError when the link given names another organization
|
|
101
|
+
*/
|
|
102
|
+
linked(dn: string, org: string, entry: AttributesList): AttributesList;
|
|
103
|
+
/**
|
|
104
|
+
* Refuse a change taking a group's link away from its organization, under
|
|
105
|
+
* a model judging by attachment: as on a creation, a group in one
|
|
106
|
+
* organization's branch cannot belong to another, nor to none.
|
|
107
|
+
*
|
|
108
|
+
* @param dn the group's DN
|
|
109
|
+
* @param changes the modification
|
|
110
|
+
* @throws BadRequestError when the change links it elsewhere or unlinks it
|
|
111
|
+
*/
|
|
112
|
+
private checkLinkChange;
|
|
113
|
+
/**
|
|
114
|
+
* Whether an authorization plugin judges entries by their organization
|
|
115
|
+
* link: then a group needs one.
|
|
116
|
+
*/
|
|
117
|
+
private judgedByAttachment;
|
|
84
118
|
/** The organization a group DN belongs to, if it is a group at all. */
|
|
85
119
|
organizationOf(dn: string): string | undefined;
|
|
86
120
|
groupBaseOf(org: string): string;
|
|
@@ -112,14 +146,24 @@ export default class TwakeGroups extends LdapGroups {
|
|
|
112
146
|
groupDn(org: string, id: string): string;
|
|
113
147
|
get userAttribute(): string;
|
|
114
148
|
userDn(org: string, username: string): string;
|
|
115
|
-
/**
|
|
116
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Every route answers 404 for a missing organization, 410 for a deleted one.
|
|
151
|
+
*
|
|
152
|
+
* The status is read as the server, whoever asks; an organization the
|
|
153
|
+
* caller of `req` cannot read answers as one that is not there, deleted or
|
|
154
|
+
* not.
|
|
155
|
+
*/
|
|
156
|
+
checkOrganization(org: string, req?: Request): Promise<void>;
|
|
117
157
|
/** Page, search and sort of a list route, or a refusal of them. */
|
|
118
158
|
page(req: Request, sortable: string[]): Page;
|
|
119
159
|
sorted<T extends Record<string, unknown>>(items: T[], sortBy: string | undefined, desc: boolean): T[];
|
|
120
160
|
/** A group as the routes answer it: members named by their RDN value. */
|
|
121
161
|
groupOf(org: string, entry: AttributesList): Record<string, unknown>;
|
|
122
162
|
groups(org: string, filter: string, attributes?: string[], req?: Request): Promise<AttributesList[]>;
|
|
163
|
+
/**
|
|
164
|
+
* A group, read as the caller of `req` when there is one: a group they
|
|
165
|
+
* cannot read answers as one that is not there.
|
|
166
|
+
*/
|
|
123
167
|
private readGroup;
|
|
124
168
|
/** The group of that display name in the organization, if any. */
|
|
125
169
|
private named;
|
|
@@ -37,17 +37,6 @@ export default class James extends TwakePlugin {
|
|
|
37
37
|
*/
|
|
38
38
|
private getUserQuota;
|
|
39
39
|
hooks: Hooks;
|
|
40
|
-
/**
|
|
41
|
-
* Where the mail domain entries live, and the attribute naming their
|
|
42
|
-
* domain: each loaded flat schema declaring the `domainName` role, the
|
|
43
|
-
* lookup `core/ldap/enterpriseRules` makes. Without one, the branch of the
|
|
44
|
-
* shipped `twake/nomenclature/twakeDomain.json` and the attribute
|
|
45
|
-
* `--enterprise-domain-name-attribute` names.
|
|
46
|
-
*
|
|
47
|
-
* Rebuilt on each call, as the enterprise rules do theirs: a list taken in
|
|
48
|
-
* the constructor could predate the schemas.
|
|
49
|
-
*/
|
|
50
|
-
private domainBranches;
|
|
51
40
|
/**
|
|
52
41
|
* The attribute naming the mail domain of the entry at a DN, when it lies
|
|
53
42
|
* in a domain branch: at any depth, as a deployment may nest its domains
|
package/docs/README.md
CHANGED
|
@@ -32,6 +32,31 @@ Integrate LDAP-Rest into your web applications.
|
|
|
32
32
|
- **[JSON Schemas](client-development/schemas/README.md)** - Schema structure and validation
|
|
33
33
|
- **[Examples](client-development/examples/README.md)** - React, Vue.js, Vanilla JS integration
|
|
34
34
|
|
|
35
|
+
## API stability
|
|
36
|
+
|
|
37
|
+
From 1.0.0, ldap-rest follows semantic versioning. These are its public
|
|
38
|
+
API, which a minor or a patch release does not break:
|
|
39
|
+
|
|
40
|
+
- **Configuration**: the options and environment variables of
|
|
41
|
+
[Configuration](usage/configuration.md) and of each plugin's page.
|
|
42
|
+
- **Routes**: the paths, bodies and answers of the
|
|
43
|
+
[REST API](client-development/api/rest-api.md) and the plugin pages,
|
|
44
|
+
their statuses and their error `code`s.
|
|
45
|
+
- **Plugin API**: what `ldap-rest/plugin` exports — `DmPlugin` and its
|
|
46
|
+
roles (`auth`, `authz`, …), `asyncHandler`, the DN and filter escaping
|
|
47
|
+
helpers and the error classes —, the `DM` type of `ldap-rest` and the
|
|
48
|
+
`Hooks` of `ldap-rest/hooks`, whose arguments are those typed in
|
|
49
|
+
`src/hooks.ts` ([hooks](plugin-development/hooks.md)), and the directory
|
|
50
|
+
operations of [the development guide](plugin-development/README.md):
|
|
51
|
+
`ldap.search`, `add`, `modify`, `rename`, `delete`, `ldap.forRequest`,
|
|
52
|
+
`ldap.system`, `ldap.authorize`, `ldap.readableBy`,
|
|
53
|
+
`ldap.canonicalAttributes`, `ldap.canonicalChanges` and
|
|
54
|
+
`ldap.knowAttributeNames`.
|
|
55
|
+
- **Schemas**: the JSON schema format and the shipped schemas' attributes.
|
|
56
|
+
|
|
57
|
+
Everything else — other exports, internal helpers, log messages — may
|
|
58
|
+
change in any release.
|
|
59
|
+
|
|
35
60
|
## Presentations
|
|
36
61
|
|
|
37
62
|
Interactive presentations (using [presenterm](https://github.com/mfontanini/presenterm)):
|
|
@@ -455,14 +455,13 @@ export default class MyPlugin extends DmPlugin {
|
|
|
455
455
|
|
|
456
456
|
The `this.server.ldap` object provides access to all LDAP operations:
|
|
457
457
|
|
|
458
|
-
> **
|
|
458
|
+
> **Say whose each operation is.**
|
|
459
459
|
>
|
|
460
460
|
> The authorization plugins (`core/auth/authzPerBranch`,
|
|
461
461
|
> `core/auth/authzDynamic`, …) hook `ldapsearchrequest`, `ldapaddrequest`,
|
|
462
|
-
> `ldapmodifyrequest`, `ldaprenamerequest` and `ldapdeleterequest`, and
|
|
463
|
-
>
|
|
464
|
-
> optional argument
|
|
465
|
-
> the operation. Three such bugs shipped in the SCIM plugin.
|
|
462
|
+
> `ldapmodifyrequest`, `ldaprenamerequest` and `ldapdeleterequest`, and judge
|
|
463
|
+
> an operation by the request it carries. The methods below take it as a
|
|
464
|
+
> trailing optional argument.
|
|
466
465
|
>
|
|
467
466
|
> In a request handler, bind once and use the result:
|
|
468
467
|
>
|
|
@@ -472,21 +471,50 @@ The `this.server.ldap` object provides access to all LDAP operations:
|
|
|
472
471
|
> await directory.modify(dn, changes);
|
|
473
472
|
> ```
|
|
474
473
|
>
|
|
475
|
-
>
|
|
476
|
-
>
|
|
477
|
-
>
|
|
478
|
-
>
|
|
479
|
-
>
|
|
474
|
+
> An operation that names no request while an authenticated request is being
|
|
475
|
+
> served is made for that request all the same, and judged as its caller: a
|
|
476
|
+
> forgotten argument fails closed. Work that is the server's — a uniqueness
|
|
477
|
+
> check has to see a whole branch, and a reference to an entry the caller
|
|
478
|
+
> cannot read is still a valid reference — goes through `ldap.system`, which
|
|
479
|
+
> steps out of the request, with a comment saying why:
|
|
480
480
|
>
|
|
481
481
|
> ```typescript
|
|
482
|
+
> // As the server: a name is taken whoever may read the entry holding it
|
|
482
483
|
> await this.server.ldap.system.search({ scope: 'base' }, dn);
|
|
483
484
|
> ```
|
|
484
485
|
>
|
|
485
|
-
>
|
|
486
|
-
>
|
|
487
|
-
>
|
|
488
|
-
>
|
|
489
|
-
>
|
|
486
|
+
> A lint rule refuses an operation that names neither a request nor
|
|
487
|
+
> `ldap.system`. Hooks run outside the request: what a hook reads or writes
|
|
488
|
+
> to validate, complete or follow an operation is the server's, and a hook
|
|
489
|
+
> acting as the caller passes the request it is handed. Startup and
|
|
490
|
+
> scheduled tasks run outside any request.
|
|
491
|
+
|
|
492
|
+
> **Read as the server? Judge the caller first.**
|
|
493
|
+
>
|
|
494
|
+
> A read the server makes for a request answers the caller with what it
|
|
495
|
+
> found: a 404 for an entry that is not there, a 400 for a pointer leading
|
|
496
|
+
> nowhere, a 409 for a name taken. Made before the write it prepares is
|
|
497
|
+
> judged, it tells a caller who may not make that write what the directory
|
|
498
|
+
> holds where they cannot read. Have the authorization plugins judge the
|
|
499
|
+
> write first, without making it:
|
|
500
|
+
>
|
|
501
|
+
> ```typescript
|
|
502
|
+
> await this.server.ldap.authorize('ldaprenamerequest', [dn, newDn, req]);
|
|
503
|
+
> // only now: does dn exist, is newDn free, …
|
|
504
|
+
> ```
|
|
505
|
+
>
|
|
506
|
+
> It runs the hooks of the plugins with the `authz` role only, and refuses
|
|
507
|
+
> with their 403 whether the entry is there or not. The write, when made,
|
|
508
|
+
> is judged again with every other hook.
|
|
509
|
+
>
|
|
510
|
+
> A route that finds an entry as the server, across branches, and then
|
|
511
|
+
> answers about it, checks that the caller can read what it found, and
|
|
512
|
+
> answers as for an entry that is not there otherwise:
|
|
513
|
+
>
|
|
514
|
+
> ```typescript
|
|
515
|
+
> if (!(await this.server.ldap.readableBy(dn, req)))
|
|
516
|
+
> throw new NotFoundError(`User ${id} not found`);
|
|
517
|
+
> ```
|
|
490
518
|
|
|
491
519
|
#### Search
|
|
492
520
|
|
|
@@ -549,6 +577,24 @@ await this.server.ldap.modify('uid=john,ou=users,dc=example,dc=com', {
|
|
|
549
577
|
});
|
|
550
578
|
```
|
|
551
579
|
|
|
580
|
+
#### Attribute names
|
|
581
|
+
|
|
582
|
+
An add or a modify names its attributes as the server does before the
|
|
583
|
+
authorization plugins and the hooks see them: `MAIL`, `rfc822Mailbox` or
|
|
584
|
+
the OID of `mail` reach a hook as `mail`, spelled as the configuration or a
|
|
585
|
+
loaded schema spells it. A hook may therefore look an attribute up by that
|
|
586
|
+
name. An option other than `;binary` is refused, as is one attribute named
|
|
587
|
+
twice in one part of a write.
|
|
588
|
+
|
|
589
|
+
A route that checks a body before handing it to `add` or `modify` names
|
|
590
|
+
its attributes the same way first, and a plugin loading a schema of its
|
|
591
|
+
own says how it spells them:
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
await this.server.ldap.canonicalChanges(changes); // or canonicalAttributes(entry)
|
|
595
|
+
this.server.ldap.knowAttributeNames(Object.keys(schema.attributes));
|
|
596
|
+
```
|
|
597
|
+
|
|
552
598
|
#### Delete
|
|
553
599
|
|
|
554
600
|
```typescript
|
|
@@ -581,7 +581,13 @@ guards.
|
|
|
581
581
|
|
|
582
582
|
## Hook Execution Order
|
|
583
583
|
|
|
584
|
-
Hooks are executed in the order plugins are loaded
|
|
584
|
+
Hooks are executed in the order plugins are loaded, with one exception: the
|
|
585
|
+
request hooks of authorization plugins (`ldapsearchrequest`, `ldapaddrequest`,
|
|
586
|
+
`ldapmodifyrequest`, `ldapdeleterequest`, `ldaprenamerequest`) run before any
|
|
587
|
+
other plugin's, in the order the authorization plugins loaded. The first hook
|
|
588
|
+
to throw answers the request, so a caller who may not make an operation is
|
|
589
|
+
refused before a plugin judges its content, or replaces it.
|
|
590
|
+
`ldapsearchfilter` keeps the load order. For chained hooks:
|
|
585
591
|
|
|
586
592
|
1. First plugin in chain can modify/validate data
|
|
587
593
|
2. Returns modified data to next plugin
|
|
@@ -595,7 +601,7 @@ For non-chained hooks:
|
|
|
595
601
|
**Example**: `ldapdeleterequest` chain
|
|
596
602
|
|
|
597
603
|
```
|
|
598
|
-
Request → trash (moves to trash) → groups (checks members) → organizations (checks empty) → LDAP
|
|
604
|
+
Request → authorization → trash (moves to trash) → groups (checks members) → organizations (checks empty) → LDAP
|
|
599
605
|
```
|
|
600
606
|
|
|
601
607
|
## Creating Plugins with Dependencies
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# Core hooks
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Typescript definitions into [hooks.ts](./src/hooks.ts)
|
|
3
|
+
The hooks a plugin can register. Their arguments and return values are
|
|
4
|
+
those typed in [hooks.ts](../../src/hooks.ts), which is the reference.
|
|
6
5
|
|
|
7
6
|
## Demo hooks
|
|
8
7
|
|
|
9
|
-
- **hello**: called by [helloworld demo plugin](
|
|
8
|
+
- **hello**: called by [helloworld demo plugin](../../src/plugins/demo/helloworld.ts)
|
|
10
9
|
|
|
11
|
-
## [LDAP](
|
|
10
|
+
## [LDAP](../../src/lib/ldapActions.ts) hooks
|
|
12
11
|
|
|
13
12
|
- **ldapopts**: called before any ldapsearch to modify search options
|
|
14
13
|
- **ldapsearchresult**: called after any ldapsearch to modify search result
|
|
@@ -186,8 +186,10 @@ deployment has run with it on, and a cache that silently serves stale entries
|
|
|
186
186
|
is not something to switch on for everyone at once.
|
|
187
187
|
|
|
188
188
|
Writes made through this service (add, modify, rename, move, delete) drop what
|
|
189
|
-
they change, the moved subtree included
|
|
190
|
-
|
|
189
|
+
they change, the moved subtree included, and what the directory's overlays
|
|
190
|
+
change with them: the `memberOf` of the members a group gains or loses, and
|
|
191
|
+
the entries naming an entry renamed or deleted. Writes made anywhere else do
|
|
192
|
+
not: another replica of this service, an LSC synchronisation, a hand-run
|
|
191
193
|
`ldapmodify`. Each process caches on its own, and only its own writes drop what
|
|
192
194
|
it cached. Turn the cache on when this service is the only writer, or when
|
|
193
195
|
serving an entry up to `--ldap-cache-ttl` seconds old is acceptable; leave it
|
|
@@ -509,12 +511,13 @@ Formerly `core/twake/calendarResources`, still accepted as a deprecated alias.
|
|
|
509
511
|
|
|
510
512
|
#### `core/twake/applicativeAccounts`
|
|
511
513
|
|
|
512
|
-
| CLI | Plural | Env | Default
|
|
513
|
-
| --------------------------------- | ---------------------------------- | ----------------------------------- |
|
|
514
|
-
| `--applicative-account-base` | | `DM_APPLICATIVE_ACCOUNT_BASE` |
|
|
515
|
-
| `--max-app-accounts` | | `DM_MAX_APP_ACCOUNTS` | `5`
|
|
516
|
-
| `--
|
|
517
|
-
| `--
|
|
514
|
+
| CLI | Plural | Env | Default | Description |
|
|
515
|
+
| --------------------------------- | ---------------------------------- | ----------------------------------- | ------------------ | ------------------------------------------------------ |
|
|
516
|
+
| `--applicative-account-base` | | `DM_APPLICATIVE_ACCOUNT_BASE` | | Applicative accounts base DN |
|
|
517
|
+
| `--max-app-accounts` | | `DM_MAX_APP_ACCOUNTS` | `5` | Max accounts per user |
|
|
518
|
+
| `--app-accounts-user-attribute` | | `DM_APP_ACCOUNTS_USER_ATTRIBUTE` | the mail attribute | Attribute the `:user` of the routes is matched against |
|
|
519
|
+
| `--applicative-account-attribute` | `--applicative-account-attributes` | `DM_APPLICATIVE_ACCOUNT_ATTRIBUTES` | _(see below)_ | Attributes copied from the user entry |
|
|
520
|
+
| `--ldap-operational-attribute` | `--ldap-operational-attributes` | `DM_LDAP_OPERATIONAL_ATTRIBUTES` | _(see below)_ | Operational attributes to exclude |
|
|
518
521
|
|
|
519
522
|
Default copied attributes: `objectClass`, `cn`, `sn`, `givenName`, `displayName`, `description` — plus the configured mail attribute, always.
|
|
520
523
|
|