ldap-rest 0.26.0 → 1.0.1
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/auth/openidconnect.js +1 -1
- package/dist/plugins/bcl.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/hooks.d.ts +12 -3
- 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/auth/openidconnect.d.ts +47 -0
- package/dist/src/plugins/bcl.d.ts +2 -1
- 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/auth/back-channel-logout.md +28 -0
- package/docs/usage/plugins/auth/oidc.md +8 -0
- 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
|
@@ -12,6 +12,21 @@ import type { DM } from '../../bin';
|
|
|
12
12
|
import type { BranchPermissions } from '../../config/args';
|
|
13
13
|
import { type DmRequest } from '../auth/base';
|
|
14
14
|
import type { AttributesList, ModifyRequest, SearchResult } from '../ldapActions';
|
|
15
|
+
/**
|
|
16
|
+
* The rule of the most specific configured branch holding `target`.
|
|
17
|
+
*
|
|
18
|
+
* Branches are compared as DNs, whatever the case and spacing a rule or the
|
|
19
|
+
* directory spells them in. Among one subject's rules, the narrowest branch
|
|
20
|
+
* holding the target applies: a rule on `ou=payroll,ou=acme` takes over from
|
|
21
|
+
* one on `ou=acme` for everything below it, whichever was written first.
|
|
22
|
+
* Rules of different subjects (a user and their groups) are combined by the
|
|
23
|
+
* caller.
|
|
24
|
+
*
|
|
25
|
+
* @param rules the subject's rules, by branch
|
|
26
|
+
* @param target the branch judged
|
|
27
|
+
* @returns the rule that applies, or null when no branch holds the target
|
|
28
|
+
*/
|
|
29
|
+
export declare function mostSpecificBranchRule<T>(rules: Iterable<[string, T]>, target: string): T | null;
|
|
15
30
|
/**
|
|
16
31
|
* Abstract base class for authorization plugins
|
|
17
32
|
* Provides common utility methods and interface for LDAP-based authorization
|
|
@@ -22,10 +37,21 @@ export default abstract class AuthzBase extends DmPlugin {
|
|
|
22
37
|
*
|
|
23
38
|
* The filter cannot ask `opts` whether the caller wanted the attribute: by
|
|
24
39
|
* then `opts` is the widened one and would answer yes about an attribute
|
|
25
|
-
* we added ourselves.
|
|
26
|
-
*
|
|
40
|
+
* we added ourselves. A mark on the options keeps the question answerable.
|
|
41
|
+
*
|
|
42
|
+
* A symbol key rather than a set of the objects touched: authorization
|
|
43
|
+
* judges a search before any other plugin, and one rewriting it after —
|
|
44
|
+
* `twake/tombstone` narrows the filter of a SCIM search — hands on a copy.
|
|
45
|
+
* An object spread copies the key; a set would no longer know the copy.
|
|
46
|
+
*/
|
|
47
|
+
private static readonly widenedForLink;
|
|
48
|
+
/**
|
|
49
|
+
* The search of the top organization by itself, which every caller may
|
|
50
|
+
* make (`getOrganisationTop`): granted by `ldapsearchrequest` whatever the
|
|
51
|
+
* rules say, it is not filtered by them either. A symbol key, for the
|
|
52
|
+
* reason given at {@link widenedForLink}.
|
|
27
53
|
*/
|
|
28
|
-
private static
|
|
54
|
+
private static readonly topOrganizationRead;
|
|
29
55
|
roles: Role[];
|
|
30
56
|
cacheTTL: number;
|
|
31
57
|
/** What `--authz-unresolved-user` accepts. */
|
|
@@ -58,6 +84,11 @@ export default abstract class AuthzBase extends DmPlugin {
|
|
|
58
84
|
* `--authz-filter-attached-entries`, off by default.
|
|
59
85
|
*/
|
|
60
86
|
protected filtersAttachedEntries(): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Whether this plugin judges entries by their organization link: a plugin
|
|
89
|
+
* writing entries in an organization's branch gives them one then.
|
|
90
|
+
*/
|
|
91
|
+
judgesByAttachment(): boolean;
|
|
61
92
|
/**
|
|
62
93
|
* Branches the broad read of `filtersAttachedEntries` leaves out: there,
|
|
63
94
|
* only a caller holding the branch reads. None unless a plugin names some.
|
|
@@ -87,6 +118,20 @@ export default abstract class AuthzBase extends DmPlugin {
|
|
|
87
118
|
* write.
|
|
88
119
|
*/
|
|
89
120
|
protected sourcePermission(_branch: string): keyof BranchPermissions;
|
|
121
|
+
/**
|
|
122
|
+
* `dn` as the directory names it, read as the server. OpenLDAP reads a DN
|
|
123
|
+
* through its schema and matching rules — `2.5.4.11=payroll`,
|
|
124
|
+
* `ou=payroll\20` and `ou=payroll` are one entry — further than a text
|
|
125
|
+
* comparison can follow, so a write is judged on the entry it acts on,
|
|
126
|
+
* by the name the rules know it by. An entry that is not there keeps the
|
|
127
|
+
* spelling given: the write fails on it anyway.
|
|
128
|
+
*/
|
|
129
|
+
protected storedDn(dn: string): Promise<string>;
|
|
130
|
+
/**
|
|
131
|
+
* A DN about to be written, under its parent as the directory names it:
|
|
132
|
+
* see {@link storedDn}.
|
|
133
|
+
*/
|
|
134
|
+
protected storedParent(dn: string): Promise<string>;
|
|
90
135
|
/**
|
|
91
136
|
* The branch a write is judged against.
|
|
92
137
|
*
|
|
@@ -178,6 +223,29 @@ export default abstract class AuthzBase extends DmPlugin {
|
|
|
178
223
|
* Must be implemented by subclasses
|
|
179
224
|
*/
|
|
180
225
|
abstract getAuthorizedBranches(user: string): Promise<string[]>;
|
|
226
|
+
/**
|
|
227
|
+
* Every branch one of the caller's rules names, whatever it grants: the
|
|
228
|
+
* branches where what applies differs from the branch above.
|
|
229
|
+
*/
|
|
230
|
+
abstract ruleBranches(user: string): Promise<string[]>;
|
|
231
|
+
/**
|
|
232
|
+
* Whether a write changes nothing but the organization link and the path
|
|
233
|
+
* that goes with it: what moving an entry between organizations writes.
|
|
234
|
+
*/
|
|
235
|
+
private onlyRelinks;
|
|
236
|
+
/**
|
|
237
|
+
* An entry a rule names is under that rule as well as under what judged
|
|
238
|
+
* the write, and moving it takes along the rules naming what lies below
|
|
239
|
+
* it, which name nothing afterwards (`departmentSync` relinks the
|
|
240
|
+
* accounts attached there to the new DN): the first of these branches
|
|
241
|
+
* where `user` lacks one of `permissions`.
|
|
242
|
+
*
|
|
243
|
+
* Only those: an entry no rule names is under the rule the caller judges.
|
|
244
|
+
*
|
|
245
|
+
* @param subtree whether the rules below `dn` count too: a move, or a
|
|
246
|
+
* delete, which `trash` turns into one
|
|
247
|
+
*/
|
|
248
|
+
private refusingRule;
|
|
181
249
|
/**
|
|
182
250
|
* Whether this request is none of this plugin's business.
|
|
183
251
|
*
|
|
@@ -256,6 +324,16 @@ export default abstract class AuthzBase extends DmPlugin {
|
|
|
256
324
|
* @returns the attribute name, or undefined when no domain is linked
|
|
257
325
|
*/
|
|
258
326
|
protected domainLinkAttribute(): string | undefined;
|
|
327
|
+
/**
|
|
328
|
+
* The organization a write attaches an entry to, among the link values it
|
|
329
|
+
* gives: one at most. The judgement reads one, so a second would attach
|
|
330
|
+
* the entry to an organization nobody judged; and an empty value, which
|
|
331
|
+
* the directory stores, would make the entry read as attached to none.
|
|
332
|
+
*
|
|
333
|
+
* @throws BadRequestError when a value is empty or the values name
|
|
334
|
+
* several organizations
|
|
335
|
+
*/
|
|
336
|
+
private static organizationLinks;
|
|
259
337
|
/**
|
|
260
338
|
* The values a write gives an attribute, whatever case its name is
|
|
261
339
|
* spelled in.
|
|
@@ -333,7 +411,12 @@ export default abstract class AuthzBase extends DmPlugin {
|
|
|
333
411
|
ldapaddrequest: ([dn, entry, req]: [string, AttributesList, DmRequest?]) => Promise<[string, AttributesList, DmRequest?]>;
|
|
334
412
|
ldapsearchrequest: ([base, opts, req]: [string, SearchOptions, DmRequest?]) => Promise<[string, SearchOptions, DmRequest?]>;
|
|
335
413
|
/**
|
|
336
|
-
* Drop
|
|
414
|
+
* Drop what this caller may not read from a search it was granted.
|
|
415
|
+
*
|
|
416
|
+
* Judged by branch, the search was granted on its base: each entry is
|
|
417
|
+
* kept only where the caller may read it, a rule narrower than the base
|
|
418
|
+
* refusing part of what it holds. Judged by attachment, the accounts
|
|
419
|
+
* attached outside the branches this caller manages are dropped.
|
|
337
420
|
*
|
|
338
421
|
* Runs after the cache and carries the request, so what one caller may
|
|
339
422
|
* not see never reaches another. An entry with no organization link —
|
|
@@ -142,4 +142,25 @@ export declare function describePopulation(population: Population, authenticator
|
|
|
142
142
|
* @throws Error naming the plugins when the composition is ambiguous
|
|
143
143
|
*/
|
|
144
144
|
export declare function assertAuthzComposition(server: DM, candidate?: DmPlugin): Promise<void>;
|
|
145
|
+
/**
|
|
146
|
+
* What the loaded plugins can enforce, decided once the configuration is
|
|
147
|
+
* loaded (#385).
|
|
148
|
+
*
|
|
149
|
+
* - No authentication nor authorization plugin: every request is served
|
|
150
|
+
* with every right. An open mode on purpose — local development, a
|
|
151
|
+
* trusted network — said once.
|
|
152
|
+
* - Authorization without authentication: no request carries an identity,
|
|
153
|
+
* so every authorization plugin steps aside and everything passes while
|
|
154
|
+
* the operator believes authorization is on. The server does not start.
|
|
155
|
+
* - Authentication without authorization: every authenticated caller has
|
|
156
|
+
* every right, which a single administration token legitimately wants.
|
|
157
|
+
* Said once.
|
|
158
|
+
*
|
|
159
|
+
* A plugin carrying both roles (`authzDynamic`) authenticates the requests
|
|
160
|
+
* it judges.
|
|
161
|
+
*
|
|
162
|
+
* @param server the server, its configuration loaded
|
|
163
|
+
* @throws Error naming the authorization plugins nothing authenticates for
|
|
164
|
+
*/
|
|
165
|
+
export declare function assertEnforceable(server: DM): void;
|
|
145
166
|
export {};
|
|
@@ -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
|
*
|
|
@@ -11,6 +11,10 @@ export default class OpenIDConnect extends AuthBase {
|
|
|
11
11
|
private router?;
|
|
12
12
|
/** Set once the missing `end_session_endpoint` has been reported. */
|
|
13
13
|
private warnedNoEndSession;
|
|
14
|
+
/** Set once a `/logout` with nothing to record it has been reported. */
|
|
15
|
+
private warnedNoRecord;
|
|
16
|
+
/** Set once a `/logout` of a session nothing names has been reported. */
|
|
17
|
+
private warnedUnnamed;
|
|
14
18
|
constructor(server: DM);
|
|
15
19
|
/**
|
|
16
20
|
* What is handed to `express-openid-connect`.
|
|
@@ -99,6 +103,49 @@ export default class OpenIDConnect extends AuthBase {
|
|
|
99
103
|
* one ends the session. Undefined when there is nothing to wait for.
|
|
100
104
|
*/
|
|
101
105
|
private renewExpired;
|
|
106
|
+
/**
|
|
107
|
+
* Whether the session in front of a request was ended — by a logout token,
|
|
108
|
+
* or by `/logout`.
|
|
109
|
+
*
|
|
110
|
+
* @param req the request, after the library has read its session
|
|
111
|
+
* @returns true when a subscriber of `oidcsessionvalid` refuses it
|
|
112
|
+
*/
|
|
113
|
+
private loggedOut;
|
|
114
|
+
/**
|
|
115
|
+
* The id a session gets at login.
|
|
116
|
+
*
|
|
117
|
+
* The same person logging in again over a live session keeps its id: the
|
|
118
|
+
* library merges the new tokens into that session, and a copy of its
|
|
119
|
+
* cookie taken before would otherwise carry an id the next `/logout` no
|
|
120
|
+
* longer names. Anyone else, or a session already ended — the callback
|
|
121
|
+
* runs before the library asks — gets a new one.
|
|
122
|
+
*
|
|
123
|
+
* @param req the callback request, its session as the cookie brought it
|
|
124
|
+
* @param sub the `sub` of the id token just received
|
|
125
|
+
* @returns the id to keep in the session
|
|
126
|
+
*/
|
|
127
|
+
private sessionId;
|
|
128
|
+
/**
|
|
129
|
+
* Record that the session in front of `/logout` is dead, as a logout token
|
|
130
|
+
* naming it would.
|
|
131
|
+
*
|
|
132
|
+
* Dropping the cookie ends the browser's copy only: the cookie holds the
|
|
133
|
+
* whole session, and a copy kept elsewhere — a shared computer, a backup
|
|
134
|
+
* of the browser profile, a stolen cookie — stayed valid until its tokens
|
|
135
|
+
* expired. The provider ends the sessions of the other applications
|
|
136
|
+
* through Back-Channel Logout, but does not tell the one that asked it to.
|
|
137
|
+
*
|
|
138
|
+
* The session is named by the id given at login, or by the provider's
|
|
139
|
+
* `sid` for a session from before that id existed — never by the `sub`,
|
|
140
|
+
* which would end every session of that person.
|
|
141
|
+
*
|
|
142
|
+
* A failure to record is logged and the logout goes on: the caller asked
|
|
143
|
+
* to leave, and refusing would keep the browser's copy alive as well.
|
|
144
|
+
*
|
|
145
|
+
* @param req the request, after the library's router
|
|
146
|
+
* @returns once the subscribers have run; never rejects
|
|
147
|
+
*/
|
|
148
|
+
private endHere;
|
|
102
149
|
/**
|
|
103
150
|
* Whether `/logout` failed because the provider offers no RP-Initiated
|
|
104
151
|
* Logout, rather than for a reason worth a 500.
|