ldap-rest 0.7.0 → 0.8.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/plugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/auth/authzScope.js +1 -0
- package/dist/plugins/auth/llng.js +1 -1
- package/dist/plugins/ldap/accountLifecycle.js +1 -0
- package/dist/plugins/ldap/bulkImport.js +1 -1
- package/dist/plugins/ldap/departmentSync.js +1 -1
- package/dist/plugins/ldap/enterpriseRules.js +1 -0
- package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
- package/dist/plugins/ldap/flatGeneric.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/twake/calendar.js +1 -0
- package/dist/plugins/twake/calendarResources.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +159 -1
- package/dist/src/abstract/plugin.d.ts +13 -0
- package/dist/src/config/args.d.ts +2 -0
- package/dist/src/config/schema.d.ts +382 -12
- package/dist/src/lib/expressFormatedResponses.d.ts +14 -0
- package/dist/src/lib/ldapActions.d.ts +33 -6
- package/dist/src/lib/utils.d.ts +65 -0
- package/dist/src/plugins/auth/authzScope.d.ts +53 -0
- package/dist/src/plugins/auth/llng.d.ts +30 -1
- package/dist/src/plugins/ldap/accountLifecycle.d.ts +56 -0
- package/dist/src/plugins/ldap/departmentSync.d.ts +43 -0
- package/dist/src/plugins/ldap/enterpriseRules.d.ts +436 -0
- package/dist/src/plugins/ldap/flatGeneric.d.ts +29 -0
- package/dist/src/plugins/ldap/groups.d.ts +22 -0
- package/dist/src/plugins/ldap/organizations.d.ts +25 -4
- package/dist/src/plugins/twake/calendar.d.ts +113 -0
- package/dist/src/plugins/twake/calendarResources.d.ts +9 -58
- package/docs/README.md +1 -0
- package/docs/client-development/api/openapi.md +1 -1
- package/docs/client-development/browser/libraries.md +1 -1
- package/docs/plugin-development/dependencies.md +10 -7
- package/docs/presentations/en.md +1 -1
- package/docs/presentations/fr.md +1 -1
- package/docs/usage/README.md +1 -0
- package/docs/usage/configuration.md +35 -15
- package/docs/usage/directory-audit.md +105 -0
- package/docs/usage/plugins/README.md +6 -6
- package/docs/usage/plugins/auth/README.md +6 -5
- package/docs/usage/plugins/auth/authz-scope.md +103 -0
- package/docs/usage/plugins/auth/llng.md +17 -0
- package/docs/usage/plugins/integrations/README.md +5 -5
- package/docs/usage/plugins/integrations/{calendar-resources.md → calendar.md} +70 -13
- package/docs/usage/plugins/ldap/README.md +13 -11
- package/docs/usage/plugins/ldap/account-lifecycle.md +129 -0
- package/docs/usage/plugins/ldap/bulk-import.md +8 -1
- package/docs/usage/plugins/ldap/enterprise-rules.md +167 -0
- package/docs/usage/plugins/ldap/flat-generic.md +172 -0
- package/docs/usage/plugins/ldap/groups.md +10 -4
- package/docs/usage/plugins/ldap/organizations.md +59 -33
- package/docs/usage/upgrading.md +322 -0
- package/package.json +50 -5
- package/static/schemas/example/README.md +36 -0
- package/static/schemas/example/groups.json +94 -0
- package/static/schemas/example/nomenclature/domains.json +49 -0
- package/static/schemas/example/organizations.json +106 -0
- package/static/schemas/example/positions.json +35 -0
- package/static/schemas/example/users.json +246 -0
- package/static/schemas/twake/groups.json +81 -15
- package/static/schemas/twake/nomenclature/twakeAccountStatus.json +26 -0
- package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +22 -0
- package/static/schemas/twake/nomenclature/twakeDomain.json +60 -0
- package/static/schemas/twake/nomenclature/twakeListType.json +22 -0
- package/static/schemas/twake/nomenclature/twakeMailboxType.json +22 -0
- package/static/schemas/twake/nomenclature/twakeTitle.json +42 -0
- package/static/schemas/twake/organizations.json +92 -12
- package/static/schemas/twake/positions.json +22 -4
- package/static/schemas/twake/users.json +269 -32
|
@@ -1,18 +1,388 @@
|
|
|
1
1
|
import type { AttributeValue } from '../lib/ldapActions';
|
|
2
|
+
/**
|
|
3
|
+
* Scalar and container types an entity schema may declare.
|
|
4
|
+
*
|
|
5
|
+
* `pointer` is a DN reference validated against `branch`; `date` is an LDAP
|
|
6
|
+
* generalized time (`yyyyMMddHHmmss[.SSS]Z`).
|
|
7
|
+
*/
|
|
8
|
+
export type SchemaAttributeType = 'string' | 'number' | 'integer' | 'boolean' | 'date' | 'array' | 'pointer';
|
|
9
|
+
/**
|
|
10
|
+
* Semantic role of an attribute: what it *means*, independent of the concrete
|
|
11
|
+
* LDAP attribute holding it. Core code is written against roles so that a
|
|
12
|
+
* deployment with a different directory layout reuses it unchanged — the name
|
|
13
|
+
* of the attribute and the values it takes are configuration.
|
|
14
|
+
*
|
|
15
|
+
* The type is a plain string: the list below is what the core itself looks
|
|
16
|
+
* for, and a deployment is free to add its own for its plugins.
|
|
17
|
+
*
|
|
18
|
+
* | Role | Meaning |
|
|
19
|
+
* | ------------------ | ---------------------------------------------------- |
|
|
20
|
+
* | `identifier` | RDN value of the entry |
|
|
21
|
+
* | `displayName` | Human-readable name |
|
|
22
|
+
* | `primaryEmail` | Main mail address |
|
|
23
|
+
* | `emailAliases` | Further addresses of the same mailbox |
|
|
24
|
+
* | `emailQuota` | Mailbox size limit, in bytes |
|
|
25
|
+
* | `organizationLink` | DN of the organization the entry belongs to |
|
|
26
|
+
* | `organizationPath` | Human-readable path of that organization |
|
|
27
|
+
* | `members` | DNs of the members of a group |
|
|
28
|
+
* | `owners` | DNs allowed to write to a restricted group |
|
|
29
|
+
* | `accountStatus` | Lifecycle state of an account (enabled / disabled…) |
|
|
30
|
+
* | `password` | Credential |
|
|
31
|
+
* | `passwordReset` | Flag forcing a password change at next login |
|
|
32
|
+
* | `accountExpiry` | Date after which the account is to be removed |
|
|
33
|
+
* | `domainLink` | DNs of the mail domains an organization may use |
|
|
34
|
+
* | `domainName` | Domain name carried by a domain entry |
|
|
35
|
+
*/
|
|
36
|
+
export type SchemaRole = string;
|
|
37
|
+
/**
|
|
38
|
+
* A piece of text a client shows, in one or several languages.
|
|
39
|
+
*
|
|
40
|
+
* A plain string is the text itself. A map is keyed by language tag, and a
|
|
41
|
+
* client picks the closest one it can:
|
|
42
|
+
*
|
|
43
|
+
* ```json
|
|
44
|
+
* "label": { "en": "Department", "fr": "Département" }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* These words belong to the deployment, not to the product — they name *its*
|
|
48
|
+
* entities and attributes — so their translations belong in its schemas
|
|
49
|
+
* rather than in a catalogue shipped with the code.
|
|
50
|
+
*/
|
|
51
|
+
export type LocalizedText = string | Record<string, string>;
|
|
52
|
+
/** Roles the core itself looks for. See {@link SchemaRole}. */
|
|
53
|
+
export declare const CORE_ROLES: readonly ["identifier", "displayName", "primaryEmail", "emailAliases", "emailQuota", "organizationLink", "organizationPath", "members", "owners", "accountStatus", "password", "passwordReset", "accountExpiry", "domainLink", "domainName"];
|
|
54
|
+
/**
|
|
55
|
+
* How a uniqueness constraint is evaluated.
|
|
56
|
+
*
|
|
57
|
+
* By default the value must be unique among the entries of the entity's own
|
|
58
|
+
* branch. `branches` widens the search to a shared namespace — a mail address
|
|
59
|
+
* belongs to accounts *and* to distribution lists — and `attributes` names the
|
|
60
|
+
* other attributes that hold values of that same namespace.
|
|
61
|
+
*/
|
|
62
|
+
export interface UniqueConstraint {
|
|
63
|
+
/**
|
|
64
|
+
* Extra branches searched besides the entity's own base. Each entry may use
|
|
65
|
+
* the `__ldap_base__` placeholder, like `branch`.
|
|
66
|
+
*/
|
|
67
|
+
branches?: string[];
|
|
68
|
+
/**
|
|
69
|
+
* Other attributes sharing the value namespace, searched alongside the
|
|
70
|
+
* attribute itself (e.g. `mailAlternateAddress` for `mail`).
|
|
71
|
+
*/
|
|
72
|
+
attributes?: string[];
|
|
73
|
+
/**
|
|
74
|
+
* Value exempted from the check. A directory may use a placeholder for
|
|
75
|
+
* non-individual entries (a shared payroll number, say); that value is
|
|
76
|
+
* allowed to repeat.
|
|
77
|
+
*/
|
|
78
|
+
sentinel?: string;
|
|
79
|
+
/** Extra LDAP filter narrowing the search, e.g. `(objectClass=twakeAccount)` */
|
|
80
|
+
filter?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Derivation of a generated attribute from another attribute of the same
|
|
84
|
+
* entry. Used for identifiers built from the mail address, where the client
|
|
85
|
+
* never supplies the value.
|
|
86
|
+
*/
|
|
87
|
+
export interface GeneratedFrom {
|
|
88
|
+
/** Source attribute, e.g. `mail` */
|
|
89
|
+
attribute: string;
|
|
90
|
+
/**
|
|
91
|
+
* Regex whose first capturing group is the generated value. Applied to the
|
|
92
|
+
* source value; when it does not match, the whole source value is used.
|
|
93
|
+
*/
|
|
94
|
+
extract?: string;
|
|
95
|
+
/** Lowercase the result */
|
|
96
|
+
lowercase?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Characters to drop from the extracted value, as an extended regular
|
|
99
|
+
* expression. A mail local part may legally hold `+`, `'` or `!`, which an
|
|
100
|
+
* identifier charset usually refuses; without this the entry becomes
|
|
101
|
+
* uncreatable, since the client may not send the generated attribute
|
|
102
|
+
* either. `[^a-zA-Z0-9._-]` keeps what a `uid` accepts.
|
|
103
|
+
*/
|
|
104
|
+
strip?: string;
|
|
105
|
+
/**
|
|
106
|
+
* What to do when the generated value is already taken. `error` (the
|
|
107
|
+
* default) refuses the creation; `suffix` appends `-2`, `-3`, … until a free
|
|
108
|
+
* value is found.
|
|
109
|
+
*/
|
|
110
|
+
onCollision?: 'error' | 'suffix';
|
|
111
|
+
}
|
|
112
|
+
export interface SchemaAttribute {
|
|
113
|
+
type: SchemaAttributeType;
|
|
114
|
+
items?: {
|
|
115
|
+
type: string;
|
|
116
|
+
test?: string | RegExp;
|
|
117
|
+
hint?: string;
|
|
118
|
+
branch?: string[];
|
|
119
|
+
};
|
|
120
|
+
default?: AttributeValue;
|
|
121
|
+
required?: boolean;
|
|
122
|
+
test?: string | RegExp;
|
|
123
|
+
/**
|
|
124
|
+
* Plain-language description of `test`, shown to the user *before* they get
|
|
125
|
+
* a value wrong — "Expected pattern 999 9999". It travels with the
|
|
126
|
+
* schema so the pattern and its explanation cannot drift apart, and so a
|
|
127
|
+
* client never has to hardcode either.
|
|
128
|
+
*/
|
|
129
|
+
hint?: string;
|
|
130
|
+
branch?: string[];
|
|
131
|
+
fixed?: boolean;
|
|
132
|
+
/** Semantic role(s) — see {@link SchemaRole} */
|
|
133
|
+
role?: SchemaRole | SchemaRole[];
|
|
134
|
+
/**
|
|
135
|
+
* Computed server-side. Refused when a client supplies it; still writable by
|
|
136
|
+
* the core and by plugins.
|
|
137
|
+
*
|
|
138
|
+
* The refusal is about *attribute names in a payload*, not about the entry
|
|
139
|
+
* being immutable: an endpoint parameter that happens to end up in a
|
|
140
|
+
* generated attribute is not one of them. `POST /ldap/{resource}/{id}/rename`
|
|
141
|
+
* takes a `newId` and `POST /ldap/{resource}/{id}/move` a `targetOrgDn`, and
|
|
142
|
+
* both then write a `generated` attribute themselves — while
|
|
143
|
+
* `PUT /ldap/{resource}/{id}` naming that attribute is still refused.
|
|
144
|
+
*/
|
|
145
|
+
generated?: boolean;
|
|
146
|
+
/** How a `generated` value is derived from another attribute */
|
|
147
|
+
generatedFrom?: GeneratedFrom;
|
|
148
|
+
/**
|
|
149
|
+
* Operational or derived attribute: refused in input and never written.
|
|
150
|
+
* `memberOf` is the archetype — membership is driven from the group side.
|
|
151
|
+
*/
|
|
152
|
+
readOnly?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Never returned to a client, though it may still be written. Covers the
|
|
155
|
+
* write-only attributes of a password reset.
|
|
156
|
+
*/
|
|
157
|
+
neverReturn?: boolean;
|
|
158
|
+
/** Value must be unique; `true` means "unique within this entity" */
|
|
159
|
+
unique?: boolean | UniqueConstraint;
|
|
160
|
+
/**
|
|
161
|
+
* Canonicalisation applied to an incoming value before it is stored.
|
|
162
|
+
*
|
|
163
|
+
* `byteSize` reads a human-readable size (`5GB`, `500MB`, `2048`) and stores
|
|
164
|
+
* the number of bytes, so the same value means the same thing whether it
|
|
165
|
+
* arrived on a creation or an update.
|
|
166
|
+
*/
|
|
167
|
+
normalize?: 'byteSize';
|
|
168
|
+
/**
|
|
169
|
+
* Whether a client's "search everywhere" should include this attribute.
|
|
170
|
+
*
|
|
171
|
+
* Which attributes can be searched at all is a property of the deployment,
|
|
172
|
+
* not of the code: a substring filter on an attribute the directory has not
|
|
173
|
+
* indexed scans the branch. A schema that marks none leaves the client to
|
|
174
|
+
* guess — every returnable, single-valued, non-pointer attribute — which is
|
|
175
|
+
* right for a small branch and wrong for a large one. Marking even one
|
|
176
|
+
* attribute replaces the guess entirely, so the list is exactly what the
|
|
177
|
+
* deployment indexed.
|
|
178
|
+
*/
|
|
179
|
+
searchable?: boolean;
|
|
180
|
+
/**
|
|
181
|
+
* Named states of a lifecycle attribute, mapping a semantic name the API
|
|
182
|
+
* speaks (`enabled`, `disabled`) to the concrete value this directory
|
|
183
|
+
* stores. What "disabled" *is* — a DN in a nomenclature, a string, a
|
|
184
|
+
* boolean — is a property of the deployment, never of the code.
|
|
185
|
+
*/
|
|
186
|
+
states?: Record<string, AttributeValue>;
|
|
187
|
+
/**
|
|
188
|
+
* For a `pointer`: refuse to delete the entry it points at while any entry
|
|
189
|
+
* still references it. `ignore` (the default) leaves the reference dangling.
|
|
190
|
+
*/
|
|
191
|
+
referentialIntegrity?: 'restrict' | 'ignore';
|
|
192
|
+
/**
|
|
193
|
+
* For a `members`-role attribute: refuse to delete the entry while it still
|
|
194
|
+
* has members. The placeholder member some directories need to keep a
|
|
195
|
+
* `groupOfNames` valid (`--group-dummy-user`) does not count.
|
|
196
|
+
*/
|
|
197
|
+
deleteGuard?: 'nonEmpty';
|
|
198
|
+
/**
|
|
199
|
+
* For a mail attribute: which set of domains the address must belong to.
|
|
200
|
+
*
|
|
201
|
+
* `organization` walks up from the entry's organization, collecting the
|
|
202
|
+
* domains declared on it and on each of its ancestors; `directory` accepts
|
|
203
|
+
* any domain declared anywhere in the directory. When no domain is declared
|
|
204
|
+
* at all, any address passes.
|
|
205
|
+
*/
|
|
206
|
+
mailDomainScope?: 'organization' | 'directory';
|
|
207
|
+
/**
|
|
208
|
+
* Accept a subdomain of an authorised domain — `list@lists.example.org`
|
|
209
|
+
* under `example.org`. Off by default: the domain must match exactly.
|
|
210
|
+
*/
|
|
211
|
+
allowSubdomains?: boolean;
|
|
212
|
+
/** Grouping hint for a form, e.g. `Mailbox Settings` */
|
|
213
|
+
group?: string;
|
|
214
|
+
/** Name a client shows for this attribute, in one or several languages */
|
|
215
|
+
label?: LocalizedText;
|
|
216
|
+
}
|
|
2
217
|
export interface Schema {
|
|
3
218
|
strict: boolean;
|
|
4
219
|
attributes: {
|
|
5
|
-
[key: string]:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
220
|
+
[key: string]: SchemaAttribute;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Metadata about the entity itself, present in a schema file loaded by
|
|
224
|
+
* `ldapFlatGeneric` (the whole file is parsed as a `Schema`, so this is what
|
|
225
|
+
* that parse actually carries — narrowing to the declared type must not
|
|
226
|
+
* silently drop it). A schema attached to a plugin without its own file
|
|
227
|
+
* carries none of this.
|
|
228
|
+
*/
|
|
229
|
+
entity?: {
|
|
230
|
+
/** Name a client shows for the collection, in one or several languages */
|
|
231
|
+
label?: LocalizedText;
|
|
232
|
+
/** Same, for a single entry */
|
|
233
|
+
singularLabel?: LocalizedText;
|
|
234
|
+
/**
|
|
235
|
+
* What a client shows for an entry, keyed by its main attribute value:
|
|
236
|
+
* the readable name of a nomenclature value a pointer lands on
|
|
237
|
+
*/
|
|
238
|
+
valueLabels?: Record<string, LocalizedText>;
|
|
17
239
|
};
|
|
18
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Tell whether an attribute carries a given semantic role.
|
|
243
|
+
*
|
|
244
|
+
* @param attr attribute definition
|
|
245
|
+
* @param role role to look for
|
|
246
|
+
* @returns true when the attribute declares that role
|
|
247
|
+
*/
|
|
248
|
+
export declare function hasRole(attr: SchemaAttribute | undefined, role: SchemaRole): boolean;
|
|
249
|
+
/**
|
|
250
|
+
* Find the attribute of a schema carrying a given semantic role.
|
|
251
|
+
*
|
|
252
|
+
* This is the lookup that keeps core code free of concrete attribute names:
|
|
253
|
+
* `roleAttribute(schema, 'accountStatus')` returns `twakeAccountStatus` for a
|
|
254
|
+
* Twake directory and whatever another deployment chose for its own.
|
|
255
|
+
*
|
|
256
|
+
* @param schema entity schema
|
|
257
|
+
* @param role role to look for
|
|
258
|
+
* @returns attribute name, or undefined when no attribute declares that role
|
|
259
|
+
*/
|
|
260
|
+
export declare function roleAttribute(schema: Schema | undefined, role: SchemaRole): string | undefined;
|
|
261
|
+
/**
|
|
262
|
+
* Find every attribute of a schema carrying a given semantic role.
|
|
263
|
+
*
|
|
264
|
+
* @param schema entity schema
|
|
265
|
+
* @param role role to look for
|
|
266
|
+
* @returns attribute names, in declaration order
|
|
267
|
+
*/
|
|
268
|
+
export declare function roleAttributes(schema: Schema | undefined, role: SchemaRole): string[];
|
|
269
|
+
/**
|
|
270
|
+
* Find an attribute definition by the name a request or the directory uses.
|
|
271
|
+
*
|
|
272
|
+
* LDAP attribute names are case-insensitive and may carry options
|
|
273
|
+
* (`cn;lang-fr`), while schema keys are written in one spelling. Looking a
|
|
274
|
+
* name up verbatim lets `TWAKEDEPARTMENTPATH` slip past a rule written for
|
|
275
|
+
* `twakeDepartmentPath`, though the directory treats both as the same.
|
|
276
|
+
*
|
|
277
|
+
* @param schema entity schema
|
|
278
|
+
* @param name attribute name, in any case, with or without options
|
|
279
|
+
* @returns the schema's own spelling and the definition, or undefined
|
|
280
|
+
*/
|
|
281
|
+
export declare function schemaAttribute(schema: Schema | undefined, name: string): [string, SchemaAttribute] | undefined;
|
|
282
|
+
/**
|
|
283
|
+
* Every attribute name a modify request touches: added, replaced or deleted.
|
|
284
|
+
*
|
|
285
|
+
* A delete counts as much as a replace: removing a computed attribute is
|
|
286
|
+
* setting it, to nothing, and nothing the client does afterwards can put it
|
|
287
|
+
* back. `delete` comes as a list of names or as an object of values to remove.
|
|
288
|
+
*
|
|
289
|
+
* @param body modify request
|
|
290
|
+
* @returns attribute names, as the client spelt them
|
|
291
|
+
*/
|
|
292
|
+
export declare function modifiedAttributeNames(body: {
|
|
293
|
+
add?: object;
|
|
294
|
+
replace?: object;
|
|
295
|
+
delete?: string[] | object;
|
|
296
|
+
}): string[];
|
|
297
|
+
/**
|
|
298
|
+
* Lowercased names of the attributes marked `neverReturn` in any of the given
|
|
299
|
+
* schemas.
|
|
300
|
+
*
|
|
301
|
+
* @param schemas schemas to read, undefined entries skipped
|
|
302
|
+
* @returns lowercased attribute names
|
|
303
|
+
*/
|
|
304
|
+
export declare function neverReturnAttributes(schemas: Iterable<Schema | undefined>): Set<string>;
|
|
305
|
+
/**
|
|
306
|
+
* Copy an entry without the attributes of a hidden set.
|
|
307
|
+
*
|
|
308
|
+
* @param entry entry about to be serialised
|
|
309
|
+
* @param hidden lowercased attribute names to leave out
|
|
310
|
+
* @returns the entry itself when nothing is hidden, a filtered copy otherwise
|
|
311
|
+
*/
|
|
312
|
+
export declare function withoutAttributes<T extends object>(entry: T, hidden: Set<string>): T;
|
|
313
|
+
/**
|
|
314
|
+
* Check the DNs an attribute carries: a `pointer`, or an array whose `items`
|
|
315
|
+
* are pointers or declare a `branch`.
|
|
316
|
+
*
|
|
317
|
+
* Each DN has to sit inside one of the declared branches, compared RDN by RDN
|
|
318
|
+
* (a suffix match reads `uid=x,xou=users,…` as inside `ou=users,…`), and a
|
|
319
|
+
* pointer has to name an entry that exists. The flat entities checked the
|
|
320
|
+
* branch of a single pointer; organizations and groups read neither `items`
|
|
321
|
+
* rule, so an organization accepted a domain link to a DN outside the domains
|
|
322
|
+
* branch, or to none at all — and lost its mail-domain restriction with it.
|
|
323
|
+
*
|
|
324
|
+
* @param field attribute name, for the messages
|
|
325
|
+
* @param attr its schema definition
|
|
326
|
+
* @param value value submitted
|
|
327
|
+
* @param entryExists tells whether a DN names an existing entry
|
|
328
|
+
* @throws BadRequestError naming the first DN that breaks a rule
|
|
329
|
+
*/
|
|
330
|
+
export declare function checkDnValues(field: string, attr: SchemaAttribute, value: AttributeValue, entryExists: (dn: string) => Promise<boolean>): Promise<void>;
|
|
331
|
+
/**
|
|
332
|
+
* Tell whether every value of an attribute matches its pattern: `test`, or
|
|
333
|
+
* `items.test` for the elements of an array.
|
|
334
|
+
*
|
|
335
|
+
* @param attr schema definition
|
|
336
|
+
* @param value value submitted
|
|
337
|
+
* @returns false when one value does not match
|
|
338
|
+
*/
|
|
339
|
+
export declare function matchesPattern(attr: SchemaAttribute, value: AttributeValue): boolean;
|
|
340
|
+
/**
|
|
341
|
+
* Refuse a payload naming an attribute the server owns.
|
|
342
|
+
*
|
|
343
|
+
* `generated` and `readOnly` say a value is not the client's to set. The flat
|
|
344
|
+
* entities enforce it on their own paths; organizations and groups have their
|
|
345
|
+
* own routes and need the same guard, or an endpoint promising that a computed
|
|
346
|
+
* path "cannot be changed here" quietly accepts one.
|
|
347
|
+
*
|
|
348
|
+
* @param schema entity schema, when one is loaded
|
|
349
|
+
* @param names attribute names carried by the request
|
|
350
|
+
* @param mainAttribute RDN attribute, exempt when nothing derives it
|
|
351
|
+
* @throws BadRequestError naming the first attribute the client may not set
|
|
352
|
+
*/
|
|
353
|
+
export declare function assertClientMaySet(schema: Schema | undefined, names: string[], mainAttribute?: string): void;
|
|
354
|
+
/**
|
|
355
|
+
* A plugin able to say whether it fills a given generated attribute.
|
|
356
|
+
*
|
|
357
|
+
* Declaring the method is the whole contract: an entity asks before it
|
|
358
|
+
* exempts a required attribute from its check, so the answer has to be about
|
|
359
|
+
* that attribute, not about a role the plugin happens to carry.
|
|
360
|
+
*/
|
|
361
|
+
export interface GeneratedAttributeFiller {
|
|
362
|
+
fillsGeneratedAttribute?: (name: string, attr: SchemaAttribute, schema: Schema) => boolean;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Name the first required attribute a new entry fails to provide.
|
|
366
|
+
*
|
|
367
|
+
* A `generated` attribute is exempt: it is filled by a hook *after*
|
|
368
|
+
* validation, on purpose — checks must see the payload as the client sent it,
|
|
369
|
+
* never a value the server has already rewritten.
|
|
370
|
+
*
|
|
371
|
+
* Exempt only when something will actually fill it. `generatedFrom` is
|
|
372
|
+
* derived by the entity itself; for the rest, ask the loaded plugins about
|
|
373
|
+
* *this* attribute rather than trusting a role a dozen of them carry. With no
|
|
374
|
+
* filler — a server upgraded without adding `core/ldap/enterpriseRules` —
|
|
375
|
+
* exempting would write an entry missing an attribute its own schema calls
|
|
376
|
+
* required, silently, and the client could not supply it either since
|
|
377
|
+
* `generated` is refused as input. The entry would be unwritable by any
|
|
378
|
+
* route, so demand it and say so instead.
|
|
379
|
+
*
|
|
380
|
+
* Every entity runs this check: the flat branches, the organizations and the
|
|
381
|
+
* groups, each wording the refusal in its own terms.
|
|
382
|
+
*
|
|
383
|
+
* @param schema entity schema, when one is loaded
|
|
384
|
+
* @param entry entry as the client sent it
|
|
385
|
+
* @param plugins loaded plugins, asked about each generated attribute
|
|
386
|
+
* @returns the attribute name to complain about, or undefined
|
|
387
|
+
*/
|
|
388
|
+
export declare function missingRequiredAttribute(schema: Schema | undefined, entry: Record<string, unknown>, plugins: Record<string, unknown>): string | undefined;
|
|
@@ -11,6 +11,20 @@ export declare const tryMethodData: (res: Response, method: Function, ...args: u
|
|
|
11
11
|
export declare const ok: (res: Response, data?: object) => void;
|
|
12
12
|
export declare const created: (res: Response, data?: object) => void;
|
|
13
13
|
export declare const noContent: (res: Response) => void;
|
|
14
|
+
/**
|
|
15
|
+
* A request whose parts did not all succeed, and whose failure cannot be
|
|
16
|
+
* undone (RFC 4918 section 13).
|
|
17
|
+
*
|
|
18
|
+
* An entry rename rewrites the references other entries hold to it. The
|
|
19
|
+
* rename itself is the only rollback point: once the directory has accepted
|
|
20
|
+
* it, a reference that could not be rewritten leaves the directory partly
|
|
21
|
+
* inconsistent, and answering 200 would hide it while answering 500 would
|
|
22
|
+
* claim nothing happened. The body says what was done and what was not.
|
|
23
|
+
*
|
|
24
|
+
* @param res express response
|
|
25
|
+
* @param data report of what succeeded and what did not
|
|
26
|
+
*/
|
|
27
|
+
export declare const multiStatus: (res: Response, data: object) => void;
|
|
14
28
|
export declare const badRequest: (res: Response, message?: string) => void;
|
|
15
29
|
export declare const unauthorized: (res: Response, message?: string) => void;
|
|
16
30
|
export declare const forbidden: (res: Response, message?: string) => void;
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LDAP low-level library
|
|
3
|
-
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
-
*/
|
|
5
1
|
import type { Request } from 'express';
|
|
6
2
|
import type { ClientOptions, SearchResult, SearchOptions } from 'ldapts';
|
|
7
3
|
import type winston from 'winston';
|
|
@@ -47,6 +43,22 @@ declare class ldapActions {
|
|
|
47
43
|
parent: DM;
|
|
48
44
|
logger: winston.Logger;
|
|
49
45
|
private searchCache;
|
|
46
|
+
private cacheEnabled;
|
|
47
|
+
/**
|
|
48
|
+
* How many writes this instance has made since it was built.
|
|
49
|
+
*
|
|
50
|
+
* A read can be overtaken by a write: the write lands and `invalidateCache`
|
|
51
|
+
* finds nothing to drop, because the read has not stored its answer yet —
|
|
52
|
+
* and the read then stores what it read *before* the write, an entry
|
|
53
|
+
* nothing will drop until its TTL runs out. `search()` remembers the count
|
|
54
|
+
* it started at and stores only if no write happened in between, so a read
|
|
55
|
+
* a write overtook costs a cache entry, never a stale answer.
|
|
56
|
+
*
|
|
57
|
+
* The count is per instance and per write, not per DN: a write to another
|
|
58
|
+
* branch keeps a concurrent read out of the cache too. That is hit rate
|
|
59
|
+
* given up, not freshness.
|
|
60
|
+
*/
|
|
61
|
+
private cacheGeneration;
|
|
50
62
|
queryLimit: ReturnType<typeof pLimit>;
|
|
51
63
|
private connectionPool;
|
|
52
64
|
private poolSize;
|
|
@@ -83,11 +95,26 @@ declare class ldapActions {
|
|
|
83
95
|
private getAttributeSignature;
|
|
84
96
|
/**
|
|
85
97
|
* Generate cache key for LDAP search
|
|
98
|
+
*
|
|
99
|
+
* Every option that changes the answer belongs in here, or a search that
|
|
100
|
+
* asked for more is served what a search that asked for less got: the
|
|
101
|
+
* requested attributes above all, but also the ones that change the shape
|
|
102
|
+
* of what comes back. `timeLimit` is deliberately absent — it bounds how
|
|
103
|
+
* long the server may spend, not what it returns.
|
|
86
104
|
*/
|
|
87
105
|
private getCacheKey;
|
|
88
106
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
107
|
+
* Drop every cached read of `dn`, and of everything below it.
|
|
108
|
+
*
|
|
109
|
+
* Called after each write, so the next read of what just changed goes to
|
|
110
|
+
* the directory. Two things this does that a `key.startsWith(dn)` test did
|
|
111
|
+
* not: it compares DNs the way the directory does (see {@link foldDn}), so
|
|
112
|
+
* a write spelling a DN differently from the read that cached it still
|
|
113
|
+
* drops it; and it drops the subtree, because `rename()` and `move()`
|
|
114
|
+
* re-parent a whole branch in one operation — renaming an organization
|
|
115
|
+
* changes the DN of every entry under it — leaving the cached reads of
|
|
116
|
+
* those children filed under DNs that no longer exist. A leaf has nothing
|
|
117
|
+
* below it and the extra test costs nothing.
|
|
91
118
|
*/
|
|
92
119
|
invalidateCache(dn: string): void;
|
|
93
120
|
/**
|
package/dist/src/lib/utils.d.ts
CHANGED
|
@@ -41,6 +41,30 @@ export declare const transformSchemas: (schemas: string | Buffer, config: Config
|
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
43
|
export declare function escapeLdapFilter(value: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Build the LDAP filter of a list search: a substring match on one attribute,
|
|
46
|
+
* or on any of several.
|
|
47
|
+
*
|
|
48
|
+
* The value is escaped, so nothing a client types can reach the filter as
|
|
49
|
+
* syntax; the attribute names cannot be escaped — they *are* syntax — so they
|
|
50
|
+
* are checked against what an LDAP attribute name may contain and refused
|
|
51
|
+
* otherwise.
|
|
52
|
+
*
|
|
53
|
+
* Every list route builds the same filter, and a client asking for a mail
|
|
54
|
+
* address expects the same answer whichever entity it is asking about.
|
|
55
|
+
*
|
|
56
|
+
* @param match - The substring to look for
|
|
57
|
+
* @param attribute - One attribute name, or several separated by commas
|
|
58
|
+
* @returns The LDAP filter
|
|
59
|
+
* @throws BadRequestError when an attribute name is not one
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* substringSearchFilter('ali', 'cn,mail')
|
|
64
|
+
* // => '(|(cn=*ali*)(mail=*ali*))'
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function substringSearchFilter(match: string, attribute: string): string;
|
|
44
68
|
/**
|
|
45
69
|
* Escape special characters in LDAP DN attribute values according to RFC 4514
|
|
46
70
|
* Prevents LDAP injection attacks by escaping characters that have special meaning in DNs
|
|
@@ -145,6 +169,22 @@ export declare function getParentDn(dn: string): string;
|
|
|
145
169
|
* ```
|
|
146
170
|
*/
|
|
147
171
|
export declare function getRdn(dn: string): string;
|
|
172
|
+
/**
|
|
173
|
+
* Extract the *value* of the first RDN of a DN, with its escapes removed
|
|
174
|
+
*
|
|
175
|
+
* Where {@link getRdn} returns `ou=Test Org`, this returns `Test Org`: the
|
|
176
|
+
* name the entry is known by, which is what a path or a label is built from.
|
|
177
|
+
*
|
|
178
|
+
* @param dn - The DN to read
|
|
179
|
+
* @returns The value, or an empty string when the DN has no RDN
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* rdnValue('ou=Test\\, Org,ou=organization,dc=example,dc=com')
|
|
184
|
+
* // => 'Test, Org'
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export declare function rdnValue(dn: string): string;
|
|
148
188
|
/**
|
|
149
189
|
* Check if a DN is a child of another DN
|
|
150
190
|
*
|
|
@@ -184,6 +224,31 @@ export declare function isChildOf(dn: string, parentDn: string): boolean;
|
|
|
184
224
|
* ```
|
|
185
225
|
*/
|
|
186
226
|
export declare function normalizeDn(dn: string): string;
|
|
227
|
+
/**
|
|
228
|
+
* Whether a member DN is the placeholder a `groupOfNames` holds so it stays
|
|
229
|
+
* valid with no real member (`--group-dummy-user`).
|
|
230
|
+
*
|
|
231
|
+
* Compared as DNs rather than as text: the directory answers with its own
|
|
232
|
+
* spelling, so the configured `uid=fakeUser,ou=users,…` has to match the
|
|
233
|
+
* `uid=fakeuser, ou=users,…` a group may actually hold. A textual comparison
|
|
234
|
+
* counted such a member as a real one — enough to make an empty group refuse
|
|
235
|
+
* deletion, and to show the placeholder to SCIM clients.
|
|
236
|
+
*
|
|
237
|
+
* Every reader of the member list asks here, so the four of them cannot drift
|
|
238
|
+
* apart again.
|
|
239
|
+
*
|
|
240
|
+
* @param member - The member DN to test
|
|
241
|
+
* @param dummy - The configured placeholder DN, if any
|
|
242
|
+
* @returns true when `member` is that placeholder; false when none is
|
|
243
|
+
* configured, as there is then no placeholder to hide
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* isDummyMemberDn('uid=fakeuser, ou=users,dc=e,dc=c', 'uid=fakeUser,ou=users,dc=e,dc=c')
|
|
248
|
+
* // => true
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export declare function isDummyMemberDn(member: unknown, dummy: string | undefined): boolean;
|
|
187
252
|
/**
|
|
188
253
|
* Check whether a DN is a branch base itself or sits anywhere below it.
|
|
189
254
|
*
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/auth/authzScope
|
|
3
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
+
*
|
|
5
|
+
* Tells a client what the signed-in administrator may actually do, before it
|
|
6
|
+
* offers them a button that will fail.
|
|
7
|
+
*
|
|
8
|
+
* In a local-administration model the scope *is* the interface: a manager
|
|
9
|
+
* administers a few branches of the tree and nothing else, and an application
|
|
10
|
+
* that cannot name those branches leaves them guessing. This endpoint answers
|
|
11
|
+
* both halves — which branches, and which entities can be created in them —
|
|
12
|
+
* by asking the authorization plugin in force, whichever one that is.
|
|
13
|
+
* @group Plugins
|
|
14
|
+
*/
|
|
15
|
+
import type { Express } from 'express';
|
|
16
|
+
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
17
|
+
export default class AuthzScope extends DmPlugin {
|
|
18
|
+
name: string;
|
|
19
|
+
roles: Role[];
|
|
20
|
+
/**
|
|
21
|
+
* Register `GET {api_prefix}/v1/authz/scope`.
|
|
22
|
+
*
|
|
23
|
+
* @param app Express application
|
|
24
|
+
*/
|
|
25
|
+
api(app: Express): void;
|
|
26
|
+
/**
|
|
27
|
+
* The authorization plugin in force, if any.
|
|
28
|
+
*
|
|
29
|
+
* @returns the plugin, or undefined when the server is unrestricted
|
|
30
|
+
*/
|
|
31
|
+
private authz;
|
|
32
|
+
/**
|
|
33
|
+
* Entities a client can create, as declared by the loaded plugins.
|
|
34
|
+
*
|
|
35
|
+
* @returns entity name and branch, one per creatable entity
|
|
36
|
+
*/
|
|
37
|
+
private entities;
|
|
38
|
+
/**
|
|
39
|
+
* Answer the scope request.
|
|
40
|
+
*
|
|
41
|
+
* @param req Express request, carrying the authenticated user
|
|
42
|
+
* @param res Express response
|
|
43
|
+
*/
|
|
44
|
+
private scope;
|
|
45
|
+
/**
|
|
46
|
+
* Read the display name and path of a branch, so a client can show the
|
|
47
|
+
* scope in the words the directory uses rather than as a raw DN.
|
|
48
|
+
*
|
|
49
|
+
* @param dn branch to describe
|
|
50
|
+
* @returns name and path when the entry carries them
|
|
51
|
+
*/
|
|
52
|
+
private branchLabel;
|
|
53
|
+
}
|
|
@@ -1,8 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/auth/llng
|
|
3
|
+
* @group Plugins
|
|
4
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
5
|
+
*
|
|
6
|
+
* Lemonldap::NG authentication plugin
|
|
7
|
+
* This plugin enables authentication and authorization using Lemonldap::NG.
|
|
8
|
+
*/
|
|
9
|
+
import type { Express, Response } from 'express';
|
|
2
10
|
import AuthBase, { DmRequest } from '../../lib/auth/base';
|
|
3
11
|
import type { Role } from '../../abstract/plugin';
|
|
12
|
+
type LlngHandler = typeof import('lemonldap-ng-handler');
|
|
4
13
|
export default class AuthLLNG extends AuthBase {
|
|
5
14
|
name: string;
|
|
6
15
|
roles: Role[];
|
|
16
|
+
private handler?;
|
|
17
|
+
/**
|
|
18
|
+
* Load the optional `lemonldap-ng-handler` dependency before the server
|
|
19
|
+
* starts serving requests, rather than importing it statically.
|
|
20
|
+
*
|
|
21
|
+
* A static import would throw the moment this file is loaded — as soon as
|
|
22
|
+
* this plugin is configured — with Node's own opaque "Cannot find
|
|
23
|
+
* package" error. Loading it here instead means a server that never
|
|
24
|
+
* configures this plugin never touches the package at all, and one that
|
|
25
|
+
* does configure it gets a clear failure naming both the plugin and the
|
|
26
|
+
* missing dependency, at startup, instead of on the first request an auth
|
|
27
|
+
* plugin that cannot run must not pretend to succeed.
|
|
28
|
+
*/
|
|
29
|
+
api(app: Express): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Load the handler. Split out so tests can override it to simulate the
|
|
32
|
+
* dependency being absent, without actually uninstalling it.
|
|
33
|
+
*/
|
|
34
|
+
protected loadHandler(): Promise<LlngHandler>;
|
|
7
35
|
authMethod(req: DmRequest, res: Response, next: () => void): void;
|
|
8
36
|
}
|
|
37
|
+
export {};
|