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
|
@@ -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
|
|
|
@@ -61,12 +61,20 @@ Every plugin that judges LDAP operations — `core/auth/authzPerBranch`,
|
|
|
61
61
|
`core/auth/authzLinid1`, `core/auth/authzDynamic` — registers the same hooks,
|
|
62
62
|
and all of them run: loaded together, they compose as an **AND**, and the
|
|
63
63
|
first refusal wins. The per-caller read filter (`ldapsearchfilter`) is one of
|
|
64
|
-
|
|
65
|
-
AND. That is sound when it is meant. When it is not, it is a
|
|
64
|
+
the hooks they register: two plugins there compose as an intersection, which
|
|
65
|
+
is the same AND. That is sound when it is meant. When it is not, it is a
|
|
66
66
|
refusal nobody can explain: `authzPerBranch` beside `authzDynamic` judges a
|
|
67
67
|
machine token by a branch configuration written for administrators, finds
|
|
68
68
|
no entry for the tenant, and refuses a write the token's own ACLs grant.
|
|
69
69
|
|
|
70
|
+
The authorization plugins' request hooks (`ldapsearchrequest`,
|
|
71
|
+
`ldapaddrequest`, `ldapmodifyrequest`, `ldapdeleterequest`,
|
|
72
|
+
`ldaprenamerequest`) run before any other plugin's, whatever order the
|
|
73
|
+
plugins loaded in: a caller who may not make an operation gets the 403, not
|
|
74
|
+
another plugin's 409, and the operation is not replaced (by
|
|
75
|
+
`core/ldap/trash`, for instance) before it is judged. `ldapsearchfilter`
|
|
76
|
+
keeps the load order.
|
|
77
|
+
|
|
70
78
|
The AND needs no second plugin to bite. `--authz-per-branch-config` has a
|
|
71
79
|
default — read everywhere, write nowhere — so a lone `authzPerBranch` judges
|
|
72
80
|
**every** authenticated identity, whatever authenticated it: a token that no
|
|
@@ -108,6 +116,16 @@ aside — `authzDynamic` letting a request through on
|
|
|
108
116
|
|
|
109
117
|
### What is refused at startup
|
|
110
118
|
|
|
119
|
+
- An authorization plugin with no authentication plugin is refused: no
|
|
120
|
+
request carries an identity, so every authorization plugin would step
|
|
121
|
+
aside and everything would pass while authorization looks on.
|
|
122
|
+
`authzDynamic`, which authenticates the requests it judges, needs nothing
|
|
123
|
+
else. With neither kind of plugin, the server serves every request with
|
|
124
|
+
every right, on purpose (local development, a trusted network); with
|
|
125
|
+
authentication alone, every authenticated caller has every right (a
|
|
126
|
+
single administration token, say). The log says which, once. This is
|
|
127
|
+
checked over the plugins the configuration loads (`--plugin`); a host
|
|
128
|
+
registering plugins itself answers for what they enforce.
|
|
111
129
|
- Two plugins judging LDAP operations for the same authenticator's requests
|
|
112
130
|
— two branch-level plugins with no `authz_for`, or a branch-level plugin
|
|
113
131
|
judging `authzDynamic`'s tokens — are refused, with both names, unless
|
|
@@ -124,7 +142,8 @@ aside — `authzDynamic` letting a request through on
|
|
|
124
142
|
already.
|
|
125
143
|
|
|
126
144
|
A host that assembles the server itself — `registerPlugin` after `ready`,
|
|
127
|
-
rather than `--plugin` — gets the same checks, as each
|
|
145
|
+
rather than `--plugin` — gets the same checks of the composition, as each
|
|
146
|
+
plugin arrives (not the one asking for an authentication plugin, above): the
|
|
128
147
|
refusal comes out of `registerPlugin`, before any of the plugin's routes or
|
|
129
148
|
hooks exist. An arriving plugin is judged against what is already there, so
|
|
130
149
|
the authentication plugins an `authz_for` or `--authz-scope-source` names
|
|
@@ -78,17 +78,26 @@ organization through which entries change hands.
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
An entry is **in transit** when it is attached to that organization, or to
|
|
81
|
-
none (with or without the option).
|
|
81
|
+
none (with or without the option). An entry carrying no link is in transit
|
|
82
|
+
on purpose: it is how the entries other tools insert without one are
|
|
83
|
+
caught. Nothing ties such an entry to an organization: any administrator
|
|
84
|
+
may claim it into one they manage, and the first to claim it takes it.
|
|
82
85
|
|
|
83
|
-
- Every administrator sees the entries in transit
|
|
86
|
+
- Every administrator sees the entries in transit, but in a global branch
|
|
87
|
+
(`--authz-global-branch`), which only its holders read.
|
|
84
88
|
- **Claiming** one (`POST /api/v1/ldap/users/:id/move` towards an
|
|
85
|
-
organization) asks for write on that organization only.
|
|
89
|
+
organization) asks for write on that organization only. The write
|
|
90
|
+
claiming it may carry other changes to the entry — its password, say —
|
|
91
|
+
judged with it: the entry lands in the organization that write is
|
|
92
|
+
allowed in.
|
|
86
93
|
- **Putting** one in transit asks for write on the organization it leaves,
|
|
87
94
|
and nothing on the transit branch: an administrator only lets go of what
|
|
88
95
|
they manage. Handing an account over to an organization one does not manage
|
|
89
96
|
is two steps: put it in transit, then its new administrator claims it.
|
|
90
|
-
- Any other change to an entry in transit (attributes,
|
|
91
|
-
deletion) is judged as before
|
|
97
|
+
- Any other change to an entry in transit, not claiming it (attributes,
|
|
98
|
+
password, status, deletion), is judged as before: on the transit branch
|
|
99
|
+
for an entry attached to it, on its parent branch for one attached to
|
|
100
|
+
nothing.
|
|
92
101
|
|
|
93
102
|
`GET /api/v1/authz/scope` returns the transit branch as `transit` (`null`
|
|
94
103
|
when there is none), so that a client can offer these moves. A group attached
|
|
@@ -106,13 +106,44 @@ Permissions applied to all group members:
|
|
|
106
106
|
|
|
107
107
|
## Permission Resolution
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
The rules holding the target, the user's own and those of every group they
|
|
110
|
+
belong to, are combined:
|
|
111
|
+
|
|
112
|
+
1. **Within one source** (the user's own rules, or one group's), the rule of
|
|
113
|
+
the **most specific** branch holding the target applies, whatever order
|
|
114
|
+
the rules are written in, broader rules of that source aside. Branches
|
|
115
|
+
are compared as DNs, whatever their case and spacing.
|
|
116
|
+
2. **Between sources**, for each permission type (read/write/delete), the
|
|
117
|
+
**most permissive** wins.
|
|
118
|
+
3. **Default permissions** apply to a permission none of these rules sets. They are a fallback, not a floor: `"default": {"read": true}` does
|
|
119
|
+
not undo a rule refusing read.
|
|
120
|
+
|
|
121
|
+
Creating or writing an entry a rule names also asks for that rule's
|
|
122
|
+
permission, and
|
|
123
|
+
moving or deleting one holding such an entry asks for read and write, or
|
|
124
|
+
delete, at every rule below it: a caller granted `ou=organization` cannot
|
|
125
|
+
rename `ou=payroll` from under the rule refusing it, nor change or delete
|
|
126
|
+
the `ou=payroll` entry itself. This holds under
|
|
127
|
+
`--authz-filter-attached-entries` too, where `departmentSync` would relink
|
|
128
|
+
the accounts attached there. A write setting the organization link
|
|
129
|
+
(`--ldap-organization-link-attribute`) is judged on the organizations it
|
|
130
|
+
leaves and joins; anything else it carries still needs write where the
|
|
131
|
+
entry stands: its branch, or, judged by attachment, the organization it
|
|
132
|
+
leaves.
|
|
133
|
+
|
|
134
|
+
A rule's branch matches a DN however it is spelled, as OpenLDAP reads it:
|
|
135
|
+
case, spaces, escapes (`\2C` and `\,`, `\20` and a space, `\C3\A9` and
|
|
136
|
+
`é`), an attribute type by any of its names or its OID
|
|
137
|
+
(`organizationalUnitName=` and `2.5.4.11=` are `ou=`, learned from the
|
|
138
|
+
directory schema at startup), the spaces its matching rules ignore, and
|
|
139
|
+
the LDAPv2 forms (`;` between RDNs, a value in double quotes). A write is
|
|
140
|
+
judged on the entry as the directory names it, read as the server before
|
|
141
|
+
the write. A caller cannot step around a rule by spelling its branch
|
|
142
|
+
another way.
|
|
143
|
+
|
|
144
|
+
A move is judged on the mover's own rules. The rules of other users and
|
|
145
|
+
groups name the branch by its DN: once a caller allowed to move it has done
|
|
146
|
+
so, they no longer apply to it, and need rewriting for the new DN.
|
|
116
147
|
|
|
117
148
|
## How It Works
|
|
118
149
|
|
|
@@ -212,6 +243,25 @@ With this config, user `manager` can read:
|
|
|
212
243
|
- `ou=team1,ou=dept1,ou=organization,dc=example,dc=com`
|
|
213
244
|
- ... all descendants
|
|
214
245
|
|
|
246
|
+
A rule on a sub-branch takes over below it. To keep `manager` out of one
|
|
247
|
+
department:
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"users": {
|
|
252
|
+
"manager": {
|
|
253
|
+
"ou=organization,dc=example,dc=com": { "read": true },
|
|
254
|
+
"ou=payroll,ou=organization,dc=example,dc=com": { "read": false }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
A search granted on `ou=organization` then returns nothing from
|
|
261
|
+
`ou=payroll`: each entry it finds is kept only where the caller may read it.
|
|
262
|
+
A group the user belongs to may still grant the department, as the most
|
|
263
|
+
permissive source wins.
|
|
264
|
+
|
|
215
265
|
## Examples
|
|
216
266
|
|
|
217
267
|
### Example 1: Department-Based Access
|
|
@@ -412,8 +462,9 @@ Cache hit/miss logging:
|
|
|
412
462
|
|
|
413
463
|
### Permission Inheritance
|
|
414
464
|
|
|
415
|
-
- Sub-branch
|
|
416
|
-
-
|
|
465
|
+
- Sub-branch rules take over from parent branch rules, for what they set
|
|
466
|
+
- A rule may restrict a sub-branch as well as widen it, but another source
|
|
467
|
+
(a group) granting more still wins
|
|
417
468
|
- Plan branch structure carefully
|
|
418
469
|
|
|
419
470
|
### Default Permissions
|
|
@@ -100,6 +100,11 @@ DM_MAIL_ATTRIBUTE="mail"
|
|
|
100
100
|
|
|
101
101
|
- `--applicative-account-base`: LDAP base DN for applicative accounts (required)
|
|
102
102
|
- `--max-app-accounts`: Maximum number of app accounts per user (default: 5)
|
|
103
|
+
- `--app-accounts-user-attribute`: attribute the `:user` of the routes is
|
|
104
|
+
matched against (default: the mail attribute, unique directory-wide).
|
|
105
|
+
`uid` is safe only where a uid is unique across the whole directory: a
|
|
106
|
+
uid shared by users the caller can read is answered `409`, and one
|
|
107
|
+
shared with a user it cannot read resolves to the one it can
|
|
103
108
|
- `--mail-attribute`: LDAP attribute for email (default: `mail`)
|
|
104
109
|
- `--api-prefix`: API endpoint prefix (default: `/api`)
|
|
105
110
|
|
|
@@ -38,9 +38,42 @@ branch per organization. It serves the groups of an organization under
|
|
|
38
38
|
## Routes
|
|
39
39
|
|
|
40
40
|
Every route answers 404 `ORGANIZATION_NOT_FOUND` when the organization entry
|
|
41
|
-
is missing, and 410 `ORGANIZATION_DELETED` when
|
|
42
|
-
`{ "error": "...", "code": "..." }`. A request an
|
|
43
|
-
answers 403 `REFUSED`.
|
|
41
|
+
is missing or the caller cannot read it, and 410 `ORGANIZATION_DELETED` when
|
|
42
|
+
it is deleted. Errors are `{ "error": "...", "code": "..." }`. A request an
|
|
43
|
+
authorization plugin refuses answers 403 `REFUSED`.
|
|
44
|
+
|
|
45
|
+
The routes read as the caller: the authorization plugins decide which groups
|
|
46
|
+
and members it gets. A group it cannot read answers 404, a user it cannot
|
|
47
|
+
read is not found (404 `USER_NOT_FOUND` when adding it), and a member it
|
|
48
|
+
cannot read lists as `{ "uid": "..." }`. A write is judged before anything
|
|
49
|
+
is looked for, so a caller who may not make it gets 403 whether the name is
|
|
50
|
+
taken, the users exist or they are already members.
|
|
51
|
+
|
|
52
|
+
What the caller may read is the authorization model's to say:
|
|
53
|
+
|
|
54
|
+
- A model judging by branch (`core/auth/authzPerBranch`) gives an
|
|
55
|
+
organization's groups and users to the callers granted its branch.
|
|
56
|
+
- A model judging entries by their organization link (`core/auth/authzLinid1`,
|
|
57
|
+
or `--authz-filter-attached-entries`) gives an entry carrying no link to
|
|
58
|
+
every administrator. Under such a model only, every group written in an
|
|
59
|
+
organization's branch — through these routes, SCIM or another plugin — is
|
|
60
|
+
given the organization link (`--ldap-organization-link-attribute`, with the
|
|
61
|
+
auxiliary class `twakeGroup` when it holds neither it nor
|
|
62
|
+
`twakeStaticGroup`), and follows it when moved to another organization; a
|
|
63
|
+
link naming another organization is refused (400). A group is then judged
|
|
64
|
+
on its organization: creating or changing one needs write on the
|
|
65
|
+
organization, not on its group branch only. What stays every
|
|
66
|
+
administrator's: whether an organization exists or is deleted, its spaces,
|
|
67
|
+
and users carrying no link, whose link is the provisioning tool's to set.
|
|
68
|
+
Such users are in transit: any administrator may claim them into an
|
|
69
|
+
organization they manage, the first to do so taking them — see
|
|
70
|
+
[transit](../auth/authz-linid1.md#transit-handing-an-entry-over).
|
|
71
|
+
With `core/ldap/organizations` loaded too, the organizations must lie under
|
|
72
|
+
`--ldap-top-organization`, where it accepts a link.
|
|
73
|
+
- A plugin judging every read made for the request, as
|
|
74
|
+
`core/auth/authzDynamic` does, judges the organization check too: an
|
|
75
|
+
organization its token does not reach answers 404, as one that is not
|
|
76
|
+
there.
|
|
44
77
|
|
|
45
78
|
A group reads:
|
|
46
79
|
|