ldap-rest 0.6.2 → 0.8.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/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/bulk.js +1 -1
- package/dist/plugins/scim/errors.js +1 -1
- package/dist/plugins/scim/filter.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/list.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/scim/projection.js +1 -0
- package/dist/plugins/scim/scim.js +1 -1
- package/dist/plugins/scim/users.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 +5 -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 +59 -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/scim/errors.d.ts +17 -4
- package/dist/src/plugins/scim/filter.d.ts +11 -1
- package/dist/src/plugins/scim/groups.d.ts +2 -0
- package/dist/src/plugins/scim/list.d.ts +3 -5
- package/dist/src/plugins/scim/mapping.d.ts +90 -9
- package/dist/src/plugins/scim/patch.d.ts +16 -1
- package/dist/src/plugins/scim/projection.d.ts +15 -0
- package/dist/src/plugins/scim/scim.d.ts +5 -0
- package/dist/src/plugins/scim/users.d.ts +21 -0
- package/dist/src/plugins/twake/calendar.d.ts +113 -0
- package/dist/src/plugins/twake/calendarResources.d.ts +9 -58
- package/docs/README.md +2 -0
- package/docs/client-development/api/openapi.md +1 -1
- package/docs/client-development/browser/libraries.md +1 -1
- package/docs/plugin-development/README.md +22 -0
- 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 +2 -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/integrations/scim.md +147 -38
- 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 +444 -0
- package/package.json +54 -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
|
@@ -144,6 +144,50 @@ export interface LdapFlatConfig {
|
|
|
144
144
|
* example: ou=engineering,ou=departments,dc=example,dc=com
|
|
145
145
|
* example:
|
|
146
146
|
* targetOrgDn: ou=engineering,ou=departments,dc=example,dc=com
|
|
147
|
+
* FlatRenameRequest:
|
|
148
|
+
* type: object
|
|
149
|
+
* description: |
|
|
150
|
+
* Body for changing the identifier of an entry. The field is named
|
|
151
|
+
* `newId` for every resource, whatever that resource calls its
|
|
152
|
+
* `mainAttribute`.
|
|
153
|
+
* required: [newId]
|
|
154
|
+
* properties:
|
|
155
|
+
* newId:
|
|
156
|
+
* type: string
|
|
157
|
+
* description: |
|
|
158
|
+
* New value of the resource's `mainAttribute`. A value, not a DN.
|
|
159
|
+
* example: bob
|
|
160
|
+
* example:
|
|
161
|
+
* newId: bob
|
|
162
|
+
* FlatRenameResult:
|
|
163
|
+
* type: object
|
|
164
|
+
* description: |
|
|
165
|
+
* Outcome of a rename: the entry's new DN, and how many references to
|
|
166
|
+
* the old one were rewritten.
|
|
167
|
+
* properties:
|
|
168
|
+
* success:
|
|
169
|
+
* type: boolean
|
|
170
|
+
* description: False when a reference could not be rewritten.
|
|
171
|
+
* dn:
|
|
172
|
+
* type: string
|
|
173
|
+
* description: DN the entry now has.
|
|
174
|
+
* referencesUpdated:
|
|
175
|
+
* type: integer
|
|
176
|
+
* description: References rewritten to the new DN.
|
|
177
|
+
* referencesFailed:
|
|
178
|
+
* type: array
|
|
179
|
+
* description: |
|
|
180
|
+
* References left naming the old DN, counted per attribute. The
|
|
181
|
+
* referring entries are named in the server log, not here.
|
|
182
|
+
* items:
|
|
183
|
+
* type: object
|
|
184
|
+
* properties:
|
|
185
|
+
* attribute: { type: string }
|
|
186
|
+
* count: { type: integer }
|
|
187
|
+
* example:
|
|
188
|
+
* success: true
|
|
189
|
+
* dn: uid=bob,ou=users,dc=example,dc=com
|
|
190
|
+
* referencesUpdated: 3
|
|
147
191
|
*/
|
|
148
192
|
export default abstract class LdapFlat extends DmPlugin {
|
|
149
193
|
base: string;
|
|
@@ -156,6 +200,85 @@ export default abstract class LdapFlat extends DmPlugin {
|
|
|
156
200
|
pluralName: string;
|
|
157
201
|
hookPrefix: string;
|
|
158
202
|
constructor(server: DM, config: LdapFlatConfig);
|
|
203
|
+
/**
|
|
204
|
+
* Attributes a client is not allowed to supply: those the server computes
|
|
205
|
+
* (`generated`) and those it only ever derives from elsewhere (`readOnly`,
|
|
206
|
+
* such as `memberOf`, which is driven from the group side).
|
|
207
|
+
*
|
|
208
|
+
* The core and its plugins still write them — the restriction is on the
|
|
209
|
+
* request body, not on the entry.
|
|
210
|
+
*
|
|
211
|
+
* @returns lowercased attribute names
|
|
212
|
+
*/
|
|
213
|
+
protected clientForbiddenAttributes(): Set<string>;
|
|
214
|
+
/**
|
|
215
|
+
* Attributes never sent back over the API. A manager may reset a password
|
|
216
|
+
* without being able to read it back: the asymmetry is a projection, not an
|
|
217
|
+
* authorization rule, so it lives with the schema.
|
|
218
|
+
*
|
|
219
|
+
* @returns lowercased attribute names
|
|
220
|
+
*/
|
|
221
|
+
protected hiddenAttributes(): Set<string>;
|
|
222
|
+
/**
|
|
223
|
+
* Remove the `neverReturn` attributes from an entry about to be serialised.
|
|
224
|
+
* LDAP attribute names are case-insensitive and may carry options
|
|
225
|
+
* (`userPassword;binary`), so both are normalised before comparison.
|
|
226
|
+
*
|
|
227
|
+
* @param entry entry as read from the directory
|
|
228
|
+
* @returns a copy without the hidden attributes
|
|
229
|
+
*/
|
|
230
|
+
protected project<T extends Record<string, unknown>>(entry: T): T;
|
|
231
|
+
/**
|
|
232
|
+
* Same as {@link project}, for a map of entries keyed by identifier.
|
|
233
|
+
*
|
|
234
|
+
* @param list entries as read from the directory
|
|
235
|
+
* @returns a copy without the hidden attributes
|
|
236
|
+
*/
|
|
237
|
+
protected projectList(list: LdapList): LdapList;
|
|
238
|
+
/**
|
|
239
|
+
* Refuse a request body that carries an attribute the client may not set.
|
|
240
|
+
*
|
|
241
|
+
* Failing loudly matters more than ignoring the value: a client that sends
|
|
242
|
+
* `twakeDepartmentPath` believes it is setting the path, and silently
|
|
243
|
+
* overwriting it with the computed one would leave it thinking otherwise.
|
|
244
|
+
*
|
|
245
|
+
* @param names attribute names present in the request
|
|
246
|
+
* @throws BadRequestError naming the first offending attribute
|
|
247
|
+
*/
|
|
248
|
+
protected rejectForbiddenInput(names: string[]): void;
|
|
249
|
+
/**
|
|
250
|
+
* Derive the RDN value of a new entry when the schema says the server
|
|
251
|
+
* generates it — an account identifier taken from the local part of its mail
|
|
252
|
+
* address, typically.
|
|
253
|
+
*
|
|
254
|
+
* The derivation itself (source attribute, extraction pattern, collision
|
|
255
|
+
* strategy) is schema configuration; this method only applies it.
|
|
256
|
+
*
|
|
257
|
+
* @param body creation payload
|
|
258
|
+
* @returns the generated value, or undefined when nothing is generated
|
|
259
|
+
* @throws BadRequestError when the source attribute is missing, or when the
|
|
260
|
+
* value collides and the schema asks for an error
|
|
261
|
+
*/
|
|
262
|
+
protected generateMainAttribute(body: Record<string, AttributeValue>): Promise<string | undefined>;
|
|
263
|
+
/**
|
|
264
|
+
* Tell whether an entry already uses this RDN value in the branch.
|
|
265
|
+
*
|
|
266
|
+
* @param value candidate RDN value
|
|
267
|
+
* @returns true when the DN is taken
|
|
268
|
+
*/
|
|
269
|
+
protected rdnExists(value: string): Promise<boolean>;
|
|
270
|
+
/**
|
|
271
|
+
* Build the rejection message for a value that failed its schema `test`.
|
|
272
|
+
*
|
|
273
|
+
* When the schema carries a `hint`, the message says what a valid value
|
|
274
|
+
* looks like instead of only that this one was not. The hint lives next to
|
|
275
|
+
* the pattern so the two cannot drift apart, and so a client can show it
|
|
276
|
+
* under the field *before* the user gets it wrong.
|
|
277
|
+
*
|
|
278
|
+
* @param field attribute name
|
|
279
|
+
* @returns message for a `BadRequestError`
|
|
280
|
+
*/
|
|
281
|
+
protected invalidValueMessage(field: string): string;
|
|
159
282
|
/**
|
|
160
283
|
* API routes
|
|
161
284
|
*/
|
|
@@ -165,6 +288,26 @@ export default abstract class LdapFlat extends DmPlugin {
|
|
|
165
288
|
apiDelete(req: Request, res: Response): Promise<void>;
|
|
166
289
|
apiModify(req: Request, res: Response): Promise<void>;
|
|
167
290
|
apiMove(req: Request, res: Response): Promise<void>;
|
|
291
|
+
/**
|
|
292
|
+
* Change an entry's identifier, and make the directory consistent again
|
|
293
|
+
* before answering.
|
|
294
|
+
*
|
|
295
|
+
* Shaped like `apiMove`, and deliberately not on `tryMethod`: that helper
|
|
296
|
+
* flattens every outcome to `{success: true}`, which cannot say how many
|
|
297
|
+
* references were rewritten nor that one was not.
|
|
298
|
+
*
|
|
299
|
+
* @param req request, carrying `newId`
|
|
300
|
+
* @param res response
|
|
301
|
+
*/
|
|
302
|
+
apiRename(req: Request, res: Response): Promise<void>;
|
|
303
|
+
/**
|
|
304
|
+
* Whether an entry exists, asked without narrowing by the caller's own
|
|
305
|
+
* permissions — as `apiGet` asks it.
|
|
306
|
+
*
|
|
307
|
+
* @param dn entry to look for
|
|
308
|
+
* @returns true when the directory holds it
|
|
309
|
+
*/
|
|
310
|
+
private entryExists;
|
|
168
311
|
/**
|
|
169
312
|
* Resolve an id (either an RDN value or a full DN) into a DN inside this
|
|
170
313
|
* instance's base branch.
|
|
@@ -187,7 +330,22 @@ export default abstract class LdapFlat extends DmPlugin {
|
|
|
187
330
|
protected resolveDn(id: string): string;
|
|
188
331
|
addEntry(id: string, additional?: AttributesList, req?: Request): Promise<boolean>;
|
|
189
332
|
modifyEntry(id: string, changes: ModifyRequest): Promise<boolean>;
|
|
190
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Change the RDN of an entry.
|
|
335
|
+
*
|
|
336
|
+
* The request is not optional in practice: `ldapActions` hands it to the
|
|
337
|
+
* `ldaprenamerequest` hook, and every authorization plugin skips its check
|
|
338
|
+
* when there is none (`AuthzBase.shouldSkipAuthorization`). Renaming
|
|
339
|
+
* without one therefore renames without authorization — silently. It stays
|
|
340
|
+
* optional in the signature for the callers that genuinely belong to no
|
|
341
|
+
* request, as the rest of this class does.
|
|
342
|
+
*
|
|
343
|
+
* @param id current identifier, or the entry's DN
|
|
344
|
+
* @param newId new identifier, or the entry's new DN
|
|
345
|
+
* @param req request this comes from, so the branch check runs
|
|
346
|
+
* @returns true when the directory accepted the rename
|
|
347
|
+
*/
|
|
348
|
+
renameEntry(id: string, newId: string, req?: Request): Promise<boolean>;
|
|
191
349
|
deleteEntry(id: string): Promise<boolean>;
|
|
192
350
|
/**
|
|
193
351
|
* Move entry to a different organization
|
|
@@ -42,6 +42,19 @@ export default abstract class DmPlugin {
|
|
|
42
42
|
* @returns uniq operation number
|
|
43
43
|
*/
|
|
44
44
|
opNumber(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Leave out of data about to be sent every attribute a loaded schema marks
|
|
47
|
+
* `neverReturn`.
|
|
48
|
+
*
|
|
49
|
+
* A flat entity hides its own attributes, but an organization's subnodes are
|
|
50
|
+
* users, groups, positions…, each described by another plugin's schema: an
|
|
51
|
+
* answer mixing entities has to honour all of them, or `/users/<uid>` hides
|
|
52
|
+
* a password hash that `/organizations/<org>/subnodes` hands out.
|
|
53
|
+
*
|
|
54
|
+
* @param data an entry, a list of entries, or entries keyed by identifier
|
|
55
|
+
* @returns the same shape, without the hidden attributes
|
|
56
|
+
*/
|
|
57
|
+
protected hideNeverReturn<T>(data: T): T;
|
|
45
58
|
/** Names already warned about by requirePlugin, so each warns at most once. */
|
|
46
59
|
private missingPluginsWarned?;
|
|
47
60
|
/**
|
|
@@ -58,6 +58,8 @@ export interface Config {
|
|
|
58
58
|
ldap_organization_max_subnodes?: number;
|
|
59
59
|
organization_schema?: string;
|
|
60
60
|
ldap_flat_schema?: string[];
|
|
61
|
+
enterprise_domain_name_attribute?: string;
|
|
62
|
+
enterprise_domain_link_attribute?: string;
|
|
61
63
|
ldap_raw_base?: string[];
|
|
62
64
|
ldap_raw_hidden_attribute?: string[];
|
|
63
65
|
ldap_raw_show_secrets?: boolean;
|
|
@@ -186,8 +188,11 @@ export interface Config {
|
|
|
186
188
|
scim_base_map?: string;
|
|
187
189
|
scim_user_object_class?: string[];
|
|
188
190
|
scim_user_rdn_attribute?: string;
|
|
191
|
+
scim_user_lock_attribute?: string;
|
|
192
|
+
scim_user_lock_value?: string;
|
|
189
193
|
scim_group_object_class?: string[];
|
|
190
194
|
scim_group_rdn_attribute?: string;
|
|
195
|
+
scim_group_external_id_attribute?: string;
|
|
191
196
|
scim_id_attribute?: string;
|
|
192
197
|
scim_user_mapping?: string;
|
|
193
198
|
scim_group_mapping?: string;
|
|
@@ -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;
|