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.
Files changed (99) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/abstract/plugin.js +1 -1
  3. package/dist/bin/index.js +1 -1
  4. package/dist/config/args.js +1 -1
  5. package/dist/config/schema.js +1 -0
  6. package/dist/lib/expressFormatedResponses.js +1 -1
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/lib/utils.js +1 -1
  9. package/dist/plugins/auth/authzScope.js +1 -0
  10. package/dist/plugins/auth/llng.js +1 -1
  11. package/dist/plugins/ldap/accountLifecycle.js +1 -0
  12. package/dist/plugins/ldap/bulkImport.js +1 -1
  13. package/dist/plugins/ldap/departmentSync.js +1 -1
  14. package/dist/plugins/ldap/enterpriseRules.js +1 -0
  15. package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
  16. package/dist/plugins/ldap/flatGeneric.js +1 -1
  17. package/dist/plugins/ldap/groups.js +1 -1
  18. package/dist/plugins/ldap/organizations.js +1 -1
  19. package/dist/plugins/scim/bulk.js +1 -1
  20. package/dist/plugins/scim/errors.js +1 -1
  21. package/dist/plugins/scim/filter.js +1 -1
  22. package/dist/plugins/scim/groups.js +1 -1
  23. package/dist/plugins/scim/list.js +1 -1
  24. package/dist/plugins/scim/mapping.js +1 -1
  25. package/dist/plugins/scim/patch.js +1 -1
  26. package/dist/plugins/scim/projection.js +1 -0
  27. package/dist/plugins/scim/scim.js +1 -1
  28. package/dist/plugins/scim/users.js +1 -1
  29. package/dist/plugins/twake/calendar.js +1 -0
  30. package/dist/plugins/twake/calendarResources.js +1 -1
  31. package/dist/plugins/twake/james.js +1 -1
  32. package/dist/src/abstract/ldapFlat.d.ts +159 -1
  33. package/dist/src/abstract/plugin.d.ts +13 -0
  34. package/dist/src/config/args.d.ts +5 -0
  35. package/dist/src/config/schema.d.ts +382 -12
  36. package/dist/src/lib/expressFormatedResponses.d.ts +14 -0
  37. package/dist/src/lib/ldapActions.d.ts +59 -6
  38. package/dist/src/lib/utils.d.ts +65 -0
  39. package/dist/src/plugins/auth/authzScope.d.ts +53 -0
  40. package/dist/src/plugins/auth/llng.d.ts +30 -1
  41. package/dist/src/plugins/ldap/accountLifecycle.d.ts +56 -0
  42. package/dist/src/plugins/ldap/departmentSync.d.ts +43 -0
  43. package/dist/src/plugins/ldap/enterpriseRules.d.ts +436 -0
  44. package/dist/src/plugins/ldap/flatGeneric.d.ts +29 -0
  45. package/dist/src/plugins/ldap/groups.d.ts +22 -0
  46. package/dist/src/plugins/ldap/organizations.d.ts +25 -4
  47. package/dist/src/plugins/scim/errors.d.ts +17 -4
  48. package/dist/src/plugins/scim/filter.d.ts +11 -1
  49. package/dist/src/plugins/scim/groups.d.ts +2 -0
  50. package/dist/src/plugins/scim/list.d.ts +3 -5
  51. package/dist/src/plugins/scim/mapping.d.ts +90 -9
  52. package/dist/src/plugins/scim/patch.d.ts +16 -1
  53. package/dist/src/plugins/scim/projection.d.ts +15 -0
  54. package/dist/src/plugins/scim/scim.d.ts +5 -0
  55. package/dist/src/plugins/scim/users.d.ts +21 -0
  56. package/dist/src/plugins/twake/calendar.d.ts +113 -0
  57. package/dist/src/plugins/twake/calendarResources.d.ts +9 -58
  58. package/docs/README.md +2 -0
  59. package/docs/client-development/api/openapi.md +1 -1
  60. package/docs/client-development/browser/libraries.md +1 -1
  61. package/docs/plugin-development/README.md +22 -0
  62. package/docs/plugin-development/dependencies.md +10 -7
  63. package/docs/presentations/en.md +1 -1
  64. package/docs/presentations/fr.md +1 -1
  65. package/docs/usage/README.md +2 -0
  66. package/docs/usage/configuration.md +35 -15
  67. package/docs/usage/directory-audit.md +105 -0
  68. package/docs/usage/plugins/README.md +6 -6
  69. package/docs/usage/plugins/auth/README.md +6 -5
  70. package/docs/usage/plugins/auth/authz-scope.md +103 -0
  71. package/docs/usage/plugins/auth/llng.md +17 -0
  72. package/docs/usage/plugins/integrations/README.md +5 -5
  73. package/docs/usage/plugins/integrations/{calendar-resources.md → calendar.md} +70 -13
  74. package/docs/usage/plugins/integrations/scim.md +147 -38
  75. package/docs/usage/plugins/ldap/README.md +13 -11
  76. package/docs/usage/plugins/ldap/account-lifecycle.md +129 -0
  77. package/docs/usage/plugins/ldap/bulk-import.md +8 -1
  78. package/docs/usage/plugins/ldap/enterprise-rules.md +167 -0
  79. package/docs/usage/plugins/ldap/flat-generic.md +172 -0
  80. package/docs/usage/plugins/ldap/groups.md +10 -4
  81. package/docs/usage/plugins/ldap/organizations.md +59 -33
  82. package/docs/usage/upgrading.md +444 -0
  83. package/package.json +54 -5
  84. package/static/schemas/example/README.md +36 -0
  85. package/static/schemas/example/groups.json +94 -0
  86. package/static/schemas/example/nomenclature/domains.json +49 -0
  87. package/static/schemas/example/organizations.json +106 -0
  88. package/static/schemas/example/positions.json +35 -0
  89. package/static/schemas/example/users.json +246 -0
  90. package/static/schemas/twake/groups.json +81 -15
  91. package/static/schemas/twake/nomenclature/twakeAccountStatus.json +26 -0
  92. package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +22 -0
  93. package/static/schemas/twake/nomenclature/twakeDomain.json +60 -0
  94. package/static/schemas/twake/nomenclature/twakeListType.json +22 -0
  95. package/static/schemas/twake/nomenclature/twakeMailboxType.json +22 -0
  96. package/static/schemas/twake/nomenclature/twakeTitle.json +42 -0
  97. package/static/schemas/twake/organizations.json +92 -12
  98. package/static/schemas/twake/positions.json +22 -4
  99. 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
- renameEntry(id: string, newId: string): Promise<boolean>;
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
- type: 'string' | 'array' | 'pointer';
7
- items?: {
8
- type: string;
9
- test?: string | RegExp;
10
- };
11
- default?: AttributeValue;
12
- required?: boolean;
13
- test?: string | RegExp;
14
- branch?: string[];
15
- fixed?: boolean;
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;