ldap-rest 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/plugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/options.js +1 -1
- package/dist/config/schema.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/lib/errors.js +1 -1
- package/dist/lib/ldap/callerView.js +1 -0
- package/dist/lib/ldap/pool.js +1 -0
- package/dist/lib/ldap/schema.js +1 -0
- package/dist/lib/ldap/searchCache.js +1 -0
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/parseConfig.js +1 -1
- package/dist/lib/services/index.js +1 -0
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/ldap/accountLifecycle.js +1 -1
- package/dist/plugins/ldap/enterpriseRules.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/passwordPolicy.js +1 -1
- package/dist/plugins/ldap/raw.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/storage/file.js +1 -1
- package/dist/plugins/twake/instances.js +1 -1
- package/dist/plugins/twake/lifecycleAttributes.js +1 -1
- package/dist/plugins/twake/lifecycleEvents.js +1 -1
- package/dist/plugins/twake/tombstone.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +13 -0
- package/dist/src/abstract/plugin.d.ts +21 -0
- package/dist/src/bin/index.d.ts +18 -0
- package/dist/src/config/options.d.ts +16 -1
- package/dist/src/lib/errors.d.ts +2 -1
- package/dist/src/lib/ldap/callerView.d.ts +45 -0
- package/dist/src/lib/ldap/pool.d.ts +108 -0
- package/dist/src/lib/ldap/schema.d.ts +157 -0
- package/dist/src/lib/ldap/searchCache.d.ts +112 -0
- package/dist/src/lib/ldapActions.d.ts +25 -262
- package/dist/src/lib/parseConfig.d.ts +26 -2
- package/dist/src/lib/services/index.d.ts +56 -0
- package/dist/src/lib/utils.d.ts +72 -3
- package/dist/src/plugins/ldap/enterpriseRules.d.ts +0 -14
- package/dist/src/plugins/ldap/passwordPolicy.d.ts +3 -2
- package/dist/src/plugins/scim/mapping.d.ts +16 -3
- package/dist/src/plugins/scim/types.d.ts +2 -0
- package/dist/src/plugins/twake/instances.d.ts +31 -0
- package/dist/src/plugins/twake/lifecycleAttributes.d.ts +13 -2
- package/dist/src/plugins/twake/tombstone.d.ts +9 -2
- package/docs/README.md +16 -2
- package/docs/client-development/api/rest-api.md +4 -0
- package/docs/development/contracts.md +109 -0
- package/docs/plugin-development/README.md +8 -1
- package/docs/plugin-development/dependencies.md +13 -2
- package/docs/plugin-development/hooks.md +2 -1
- package/docs/plugin-development/options.md +40 -13
- package/docs/plugin-development/services.md +95 -0
- package/docs/usage/operations.md +9 -0
- package/docs/usage/plugins/integrations/instances.md +11 -3
- package/docs/usage/plugins/integrations/lifecycle-events.md +4 -1
- package/docs/usage/plugins/integrations/scim.md +20 -18
- package/docs/usage/plugins/integrations/tombstone.md +10 -6
- package/docs/usage/plugins/ldap/flat-generic.md +8 -1
- package/docs/usage/plugins/ldap/groups.md +4 -1
- package/docs/usage/plugins/ldap/password-policy.md +7 -0
- package/docs/usage/upgrading.md +59 -0
- package/package.json +17 -12
- package/static/browser/ldap-tree-viewer/LdapTreeViewer.js.map +1 -1
- package/static/browser/ldap-tree-viewer.esm.js.map +1 -1
- package/static/browser/ldap-tree-viewer.js.map +1 -1
- package/static/browser/ldap-tree-viewer.min.js.map +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed services between plugins: a token names the interface a consumer
|
|
3
|
+
* reads and the instance a provider offers, so the compiler checks both.
|
|
4
|
+
*
|
|
5
|
+
* @see docs/plugin-development/services.md
|
|
6
|
+
*/
|
|
7
|
+
declare const serviceType: unique symbol;
|
|
8
|
+
/**
|
|
9
|
+
* What identifies a service: a name for the logs, and the interface it
|
|
10
|
+
* answers. The type is carried by a phantom property and never exists at
|
|
11
|
+
* runtime; the object itself is the registry key.
|
|
12
|
+
*/
|
|
13
|
+
export interface ServiceToken<T> {
|
|
14
|
+
/** Names the service in the logs */
|
|
15
|
+
readonly name: string;
|
|
16
|
+
/** Type only, never set: carries the interface a consumer reads */
|
|
17
|
+
readonly [serviceType]?: T;
|
|
18
|
+
/**
|
|
19
|
+
* Bind the instance providing this service, as a plugin's `provides`
|
|
20
|
+
* declares. The argument must answer the interface the token names.
|
|
21
|
+
*
|
|
22
|
+
* @param provider the plugin instance providing the service
|
|
23
|
+
* @returns the token and its provider
|
|
24
|
+
*/
|
|
25
|
+
with(provider: T): ServiceProvider<T>;
|
|
26
|
+
}
|
|
27
|
+
/** A token with the instance providing it, as a plugin declares them. */
|
|
28
|
+
export interface ServiceProvider<T> {
|
|
29
|
+
readonly token: ServiceToken<T>;
|
|
30
|
+
readonly service: T;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Declare a service.
|
|
34
|
+
*
|
|
35
|
+
* Declare a token once, in a module both the provider and its consumers
|
|
36
|
+
* import: the object is the registry key, and a second declaration under the
|
|
37
|
+
* same name would be another service. The name only identifies it in the
|
|
38
|
+
* logs.
|
|
39
|
+
*
|
|
40
|
+
* @param name the service name
|
|
41
|
+
* @returns the token, carrying its interface
|
|
42
|
+
*/
|
|
43
|
+
export declare const serviceToken: <T>(name: string) => ServiceToken<T>;
|
|
44
|
+
/** What `DM.services` holds: the provided instance for each token. */
|
|
45
|
+
export type ServiceMap = Map<ServiceToken<unknown>, unknown>;
|
|
46
|
+
/**
|
|
47
|
+
* The instance provided for a token, when a plugin provides it.
|
|
48
|
+
*
|
|
49
|
+
* A provider always answers something: `undefined` means nobody provides it.
|
|
50
|
+
*
|
|
51
|
+
* @param services the server's registry
|
|
52
|
+
* @param token the service wanted
|
|
53
|
+
* @returns the provided instance, or undefined
|
|
54
|
+
*/
|
|
55
|
+
export declare const providedService: <T>(services: ReadonlyMap<ServiceToken<unknown>, unknown>, token: ServiceToken<T>) => T | undefined;
|
|
56
|
+
export {};
|
package/dist/src/lib/utils.d.ts
CHANGED
|
@@ -155,6 +155,30 @@ export declare function parseDn(dn: string): string[];
|
|
|
155
155
|
* @returns whether it reads as a DN
|
|
156
156
|
*/
|
|
157
157
|
export declare function isDnSyntax(dn: string): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Whether every escape in `dn` is one RFC 4514 §3 defines: an escaped
|
|
160
|
+
* special (`" + , ; < > \ # =` or a space) or a pair of hex digits.
|
|
161
|
+
*
|
|
162
|
+
* {@link parseDn} and {@link unescapeDnValue} read a DN permissively, as a
|
|
163
|
+
* directory may answer one; a DN a caller writes is held to the grammar.
|
|
164
|
+
*
|
|
165
|
+
* @param dn - The DN to check
|
|
166
|
+
* @returns whether its escapes are RFC 4514 escapes
|
|
167
|
+
*/
|
|
168
|
+
export declare function hasOnlyRfc4514Escapes(dn: string): boolean;
|
|
169
|
+
/**
|
|
170
|
+
* Refuse a DN a client wrote when it holds an escape RFC 4514 does not
|
|
171
|
+
* define.
|
|
172
|
+
*
|
|
173
|
+
* The readers here are permissive on purpose, because they also read what a
|
|
174
|
+
* directory answers. A DN a caller sends is held to the grammar instead, so
|
|
175
|
+
* the server only ever writes a spelling the directory would produce itself.
|
|
176
|
+
*
|
|
177
|
+
* @param dn - The DN the client wrote
|
|
178
|
+
* @param what - What to name in the refusal
|
|
179
|
+
* @throws BadRequestError when an escape is not one RFC 4514 defines
|
|
180
|
+
*/
|
|
181
|
+
export declare function assertRfc4514Escapes(dn: string, what: string): void;
|
|
158
182
|
/**
|
|
159
183
|
* Extract the parent DN from a DN
|
|
160
184
|
* Removes the first RDN component to get the parent branch
|
|
@@ -206,9 +230,11 @@ export declare function rdnValue(dn: string): string;
|
|
|
206
230
|
* Read the name of an entry written as a DN: the value of its first RDN when
|
|
207
231
|
* that RDN asserts `attribute` alone, under `base` when one is given.
|
|
208
232
|
*
|
|
209
|
-
* The value is decoded
|
|
210
|
-
*
|
|
211
|
-
* `attribute=…,base
|
|
233
|
+
* The value is decoded as {@link rdnValue} decodes it, then folded as the
|
|
234
|
+
* directory folds it ({@link foldRdnValue}): it is the naming attribute's
|
|
235
|
+
* value, the one a write puts back in `attribute=…,base`, and a caller
|
|
236
|
+
* spelling it in a way the directory considers the same name gets that name,
|
|
237
|
+
* not a second one.
|
|
212
238
|
*
|
|
213
239
|
* @param dn - The DN to read
|
|
214
240
|
* @param attribute - The attribute the first RDN must assert
|
|
@@ -249,6 +275,26 @@ export declare function namesByDn(id: string, attribute: string): boolean;
|
|
|
249
275
|
* ```
|
|
250
276
|
*/
|
|
251
277
|
export declare function isChildOf(dn: string, parentDn: string): boolean;
|
|
278
|
+
/**
|
|
279
|
+
* Whether `dn` names a direct child of `base`: its parent is `base`, compared
|
|
280
|
+
* as the directory compares DNs.
|
|
281
|
+
*
|
|
282
|
+
* `isChildOf` answers ancestry; this answers parent equality, which is what a
|
|
283
|
+
* route that only serves one branch requires of a DN a client writes. The
|
|
284
|
+
* comparison folds case, spacing and the escapes the directory reads, so a
|
|
285
|
+
* DN created under one spelling is addressed under another.
|
|
286
|
+
*
|
|
287
|
+
* @param dn - The DN to check
|
|
288
|
+
* @param base - The branch its parent has to be
|
|
289
|
+
* @returns True when the DN has a parent and that parent is `base`
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```typescript
|
|
293
|
+
* isDirectChildOf('uid=x,OU=Users , dc=example,dc=com', 'ou=users,dc=example,dc=com')
|
|
294
|
+
* // => true
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
export declare function isDirectChildOf(dn: string, base: string): boolean;
|
|
252
298
|
/**
|
|
253
299
|
* Teach {@link normalizeDn} the names and OIDs of attribute types: each is
|
|
254
300
|
* read as the type's first name.
|
|
@@ -299,6 +345,15 @@ export declare function rdnTypes(dn: string): string[];
|
|
|
299
345
|
* ```
|
|
300
346
|
*/
|
|
301
347
|
export declare function normalizeDn(dn: string): string;
|
|
348
|
+
/**
|
|
349
|
+
* A DN's structure normalized as {@link normalizeDn} does, with the case of
|
|
350
|
+
* its values kept.
|
|
351
|
+
*
|
|
352
|
+
* A configured expression compiled to ignore case reads a DN through this
|
|
353
|
+
* form: its values are folded as the directory folds them, but a value it
|
|
354
|
+
* captures keeps the spelling the caller wrote.
|
|
355
|
+
*/
|
|
356
|
+
export declare function canonicalDn(dn: string): string;
|
|
302
357
|
/**
|
|
303
358
|
* Whether a member DN is the placeholder a `groupOfNames` holds so it stays
|
|
304
359
|
* valid with no real member (`--group-dummy-user`).
|
|
@@ -374,6 +429,20 @@ export declare function isDnInBranch(dn: string, base: string): boolean;
|
|
|
374
429
|
* @returns true when the entry entered the trash branch
|
|
375
430
|
*/
|
|
376
431
|
export declare function movedToTrash(trashBase: string | undefined, oldDn: string, newDn: string): boolean;
|
|
432
|
+
/**
|
|
433
|
+
* Parse a directory date: an LDAP generalized time or anything `Date`
|
|
434
|
+
* understands.
|
|
435
|
+
*
|
|
436
|
+
* A generalized time (RFC 4517 section 3.3.13) is `yyyyMMddHH`, then optional
|
|
437
|
+
* minutes and seconds, an optional fraction of the last unit given, and a zone:
|
|
438
|
+
* `Z` or an offset of hours with optional minutes (`+02`, `-0530`). `Date` reads
|
|
439
|
+
* none of the shorter forms, and not an offset of bare hours, so they are
|
|
440
|
+
* computed here rather than rewritten into an ISO string it might refuse.
|
|
441
|
+
*
|
|
442
|
+
* @param raw value as stored or submitted
|
|
443
|
+
* @returns the date, or null when it cannot be read
|
|
444
|
+
*/
|
|
445
|
+
export declare function parseDirectoryDate(raw: AttributeValue): Date | null;
|
|
377
446
|
/**
|
|
378
447
|
* Wrapper for async Express route handlers to catch errors and pass them to error middleware
|
|
379
448
|
* This ensures that errors in async routes are properly handled and don't crash the server
|
|
@@ -107,20 +107,6 @@ export declare function parseByteSize(raw: AttributeValue): number;
|
|
|
107
107
|
* @returns true when the rules supply a value
|
|
108
108
|
*/
|
|
109
109
|
export declare function fillsGeneratedAttribute(name: string, attr: SchemaAttribute, schema: Schema): boolean;
|
|
110
|
-
/**
|
|
111
|
-
* Parse a directory date: an LDAP generalized time or anything `Date`
|
|
112
|
-
* understands.
|
|
113
|
-
*
|
|
114
|
-
* A generalized time (RFC 4517 section 3.3.13) is `yyyyMMddHH`, then optional
|
|
115
|
-
* minutes and seconds, an optional fraction of the last unit given, and a zone:
|
|
116
|
-
* `Z` or an offset of hours with optional minutes (`+02`, `-0530`). `Date` reads
|
|
117
|
-
* none of the shorter forms, and not an offset of bare hours, so they are
|
|
118
|
-
* computed here rather than rewritten into an ISO string it might refuse.
|
|
119
|
-
*
|
|
120
|
-
* @param raw value as stored or submitted
|
|
121
|
-
* @returns the date, or null when it cannot be read
|
|
122
|
-
*/
|
|
123
|
-
export declare function parseDirectoryDate(raw: AttributeValue): Date | null;
|
|
124
110
|
/**
|
|
125
111
|
* Start of a day, in UTC.
|
|
126
112
|
*
|
|
@@ -353,9 +353,10 @@ export default class PasswordPolicy extends DmPlugin {
|
|
|
353
353
|
private deletePolicy;
|
|
354
354
|
private assignPolicy;
|
|
355
355
|
/**
|
|
356
|
-
*
|
|
356
|
+
* A time the directory wrote, or null when the entry holds none, read as
|
|
357
|
+
* the rest of LDAP-Rest reads one: see {@link parseDirectoryDate}.
|
|
357
358
|
*/
|
|
358
|
-
private
|
|
359
|
+
private parsePolicyTime;
|
|
359
360
|
/**
|
|
360
361
|
* Validate password complexity (optional local validation)
|
|
361
362
|
*/
|
|
@@ -27,9 +27,11 @@ export declare function withExternalId(mapping: ResourceMapping, ldapAttribute:
|
|
|
27
27
|
* `xsd:dateTime` and reject anything else, so the raw directory value must not
|
|
28
28
|
* be forwarded as-is.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* The instant is read by {@link parseDirectoryDate}, as every other directory
|
|
31
|
+
* time is; this only writes it in the form SCIM asks for, in UTC. A value that
|
|
32
|
+
* does not look like a GeneralizedTime is returned untouched: a directory may
|
|
33
|
+
* already store an ISO 8601 string, and dropping it would lose more than
|
|
34
|
+
* passing it through.
|
|
33
35
|
*/
|
|
34
36
|
export declare function ldapTimeToIso(value: string | undefined): string | undefined;
|
|
35
37
|
export declare function loadMappingFile(path: string): ResourceMapping | undefined;
|
|
@@ -163,6 +165,17 @@ export declare function scimGroupToLdap(group: ScimGroup, mapping: ResourceMappi
|
|
|
163
165
|
* mapped SCIM attributes can be populated.
|
|
164
166
|
*/
|
|
165
167
|
export declare function requiredLdapAttributes(mapping: ResourceMapping, extra?: string[]): string[];
|
|
168
|
+
/**
|
|
169
|
+
* Refuse a DN a client wrote in a mapped reference when it holds an escape
|
|
170
|
+
* RFC 4514 does not define.
|
|
171
|
+
*
|
|
172
|
+
* The mapping says which sub-attributes carry a DN (`subDn`): a reference
|
|
173
|
+
* such as `manager.value` is written to the directory as sent, where the
|
|
174
|
+
* entity validator that holds the flat routes to RFC 4514 never sees it.
|
|
175
|
+
*/
|
|
176
|
+
export declare function assertReferenceDn(value: string, what: string): void;
|
|
177
|
+
/** Whether the mapping declares this sub-attribute a DN (`subDn`). */
|
|
178
|
+
export declare function isDnSubAttribute(mapping: ResourceMapping, top: string, sub: string): boolean;
|
|
166
179
|
/**
|
|
167
180
|
* Given a SCIM attribute path like "emails.value" or "name.familyName",
|
|
168
181
|
* return the corresponding LDAP attribute name (used by filter parser).
|
|
@@ -147,6 +147,8 @@ export interface MappingEntry {
|
|
|
147
147
|
ldapPrimary?: string;
|
|
148
148
|
ldapSecondary?: string;
|
|
149
149
|
sub?: Record<string, string>;
|
|
150
|
+
/** Sub-attributes whose value is a DN, held to RFC 4514 on write */
|
|
151
|
+
subDn?: string[];
|
|
150
152
|
multi?: 'single' | 'array';
|
|
151
153
|
readOnly?: boolean;
|
|
152
154
|
operational?: boolean;
|
|
@@ -12,6 +12,8 @@ export interface WorkplaceCreated {
|
|
|
12
12
|
/** An account's instance request, and the id `user.created` names it by. */
|
|
13
13
|
interface Account extends InstanceRequest {
|
|
14
14
|
twakeId: string;
|
|
15
|
+
/** The organization id came from the DN, not from the provider settings */
|
|
16
|
+
fromDn?: boolean;
|
|
15
17
|
}
|
|
16
18
|
export declare const EnsureInstanceBody: import("@sinclair/typebox").TObject<{
|
|
17
19
|
dn: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
@@ -128,12 +130,41 @@ export default class TwakeInstances extends DmPlugin {
|
|
|
128
130
|
private get organizationFqdn();
|
|
129
131
|
private organizationMail;
|
|
130
132
|
private isOrganization;
|
|
133
|
+
/**
|
|
134
|
+
* The id the organization's own entry spells, for an id captured from a
|
|
135
|
+
* client's DN: the caller may write the branch in another case, and the
|
|
136
|
+
* Cloudery branch and the slug should not follow it. The captured value
|
|
137
|
+
* stands when there is no entry or no organization base.
|
|
138
|
+
*/
|
|
139
|
+
private storedOrganizationId;
|
|
140
|
+
/** `account()`, with a captured organization id read from its entry. */
|
|
141
|
+
private storedAccount;
|
|
131
142
|
private findOrganization;
|
|
132
143
|
private read;
|
|
133
144
|
protected announce(exchange: string, key: string, message: Record<string, unknown>): Promise<void>;
|
|
134
145
|
/** What to ask for this account, when it gets an instance at all. */
|
|
135
146
|
protected account(dn: string, entry: AttributesList): Account | undefined;
|
|
147
|
+
/**
|
|
148
|
+
* The expression that takes this DN for an account's, and where it sits.
|
|
149
|
+
*
|
|
150
|
+
* Each expression reads the canonical DN first: a client may spell the
|
|
151
|
+
* branch differently, and a space or an escape inside a captured value
|
|
152
|
+
* would otherwise be captured as written. The canonical form keeps the
|
|
153
|
+
* caller's case, which an organization id is sent as. An expression written
|
|
154
|
+
* with the directory's own spelling — `\2C`, a doubled space — does not
|
|
155
|
+
* match that form, so the DN as sent is tried next.
|
|
156
|
+
*/
|
|
136
157
|
private match;
|
|
158
|
+
/**
|
|
159
|
+
* The DN with a comma inside a value written `\2C`, as the directory writes
|
|
160
|
+
* it: an expression's `[^,]` cannot then read that comma as an RDN
|
|
161
|
+
* separator and name the branch of an entry beside it.
|
|
162
|
+
*/
|
|
163
|
+
private separators;
|
|
164
|
+
/** The uid of an account's DN, under any name or alias of the type. */
|
|
165
|
+
private uid;
|
|
166
|
+
/** One queue per entry, whatever spelling its DN came in. */
|
|
167
|
+
private key;
|
|
137
168
|
private id;
|
|
138
169
|
protected value(entry: AttributesList, attribute: string): unknown;
|
|
139
170
|
protected findByMail(email: string, req?: Request): Promise<AttributesList | undefined>;
|
|
@@ -25,5 +25,16 @@ export declare function first(value: AttributeValue | null | undefined): string
|
|
|
25
25
|
export declare const isTombstone: (entry: AttributesList, attrs: LifecycleAttributes) => boolean;
|
|
26
26
|
export declare const isLocked: (entry: AttributesList, attrs: LifecycleAttributes) => boolean;
|
|
27
27
|
export declare function formatDeletedAt(date: Date, format: DeletedAtFormat): string;
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* A deletion date as the directory holds it, or undefined if unreadable.
|
|
30
|
+
*
|
|
31
|
+
* Read by the same reader as every other directory time: the format only says
|
|
32
|
+
* how this deployment writes the date, not how it is read.
|
|
33
|
+
*
|
|
34
|
+
* A value of another shape is refused here rather than handed to that
|
|
35
|
+
* reader, whose fallback to `new Date()` reads `1` or `Jan 1` as a date: the
|
|
36
|
+
* erase guard takes an unreadable date for a refusal. The same fallback rolls
|
|
37
|
+
* `2025-02-31` over to March, where the shared reader refuses that day
|
|
38
|
+
* spelled as a GeneralizedTime.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseDeletedAt(value: string): Date | undefined;
|
|
@@ -48,8 +48,8 @@ export default class TwakeTombstone extends DmPlugin {
|
|
|
48
48
|
*/
|
|
49
49
|
tombstone(dn: string, reason: string, req?: Request): Promise<void>;
|
|
50
50
|
/**
|
|
51
|
-
* Remove a tombstone and its group memberships. Refused
|
|
52
|
-
* is younger than the configured age, unless forced.
|
|
51
|
+
* Remove a tombstone and its group memberships. Refused when its deletion
|
|
52
|
+
* date is unreadable or younger than the configured age, unless forced.
|
|
53
53
|
*/
|
|
54
54
|
erase(dn: string, { force, req }?: {
|
|
55
55
|
force?: boolean;
|
|
@@ -77,6 +77,13 @@ export default class TwakeTombstone extends DmPlugin {
|
|
|
77
77
|
* group the erase left holding only the placeholder loses it again.
|
|
78
78
|
*/
|
|
79
79
|
private restore;
|
|
80
|
+
/**
|
|
81
|
+
* Whether this DN names an entry the expressions keep. A client may spell
|
|
82
|
+
* the DN differently from the expression; the directory stores the entry
|
|
83
|
+
* at its canonical DN, so the expressions read that too. The DN as sent is
|
|
84
|
+
* still tried: an expression written with the directory's own spelling
|
|
85
|
+
* (`\2C`, a doubled space) matches only that form.
|
|
86
|
+
*/
|
|
80
87
|
private matches;
|
|
81
88
|
private isScim;
|
|
82
89
|
private checkReason;
|
package/docs/README.md
CHANGED
|
@@ -20,6 +20,7 @@ Create and extend LDAP-Rest with custom plugins.
|
|
|
20
20
|
|
|
21
21
|
- **[Development Guide](plugin-development/README.md)** - Architecture and plugin creation
|
|
22
22
|
- **[Hooks](plugin-development/hooks.md)** - LDAP hooks system
|
|
23
|
+
- **[Services](plugin-development/services.md)** - Typed interfaces between plugins
|
|
23
24
|
- **[Routes](plugin-development/routes.md)** - Declaring routes and their input schemas
|
|
24
25
|
- **[Testing](plugin-development/testing.md)** - Testing with embedded LDAP
|
|
25
26
|
- **[Dependencies](plugin-development/dependencies.md)** - Plugin dependencies matrix
|
|
@@ -33,6 +34,13 @@ Integrate LDAP-Rest into your web applications.
|
|
|
33
34
|
- **[JSON Schemas](client-development/schemas/README.md)** - Schema structure and validation
|
|
34
35
|
- **[Examples](client-development/examples/README.md)** - React, Vue.js, Vanilla JS integration
|
|
35
36
|
|
|
37
|
+
### [Development](development/contracts.md)
|
|
38
|
+
|
|
39
|
+
Work on ldap-rest itself.
|
|
40
|
+
|
|
41
|
+
- **[Published contracts](development/contracts.md)** - What breaks a
|
|
42
|
+
contract, the snapshots that detect it and the deprecation window.
|
|
43
|
+
|
|
36
44
|
## API stability
|
|
37
45
|
|
|
38
46
|
From 1.0.0, ldap-rest follows semantic versioning. These are its public
|
|
@@ -44,8 +52,9 @@ API, which a minor or a patch release does not break:
|
|
|
44
52
|
[REST API](client-development/api/rest-api.md) and the plugin pages,
|
|
45
53
|
their statuses and their error `code`s.
|
|
46
54
|
- **Plugin API**: what `ldap-rest/plugin` exports — `DmPlugin` and its
|
|
47
|
-
roles (`auth`, `authz`, …), `
|
|
48
|
-
|
|
55
|
+
roles (`auth`, `authz`, …), `serviceToken`, `ServiceToken` and
|
|
56
|
+
`ServiceProvider`, `asyncHandler`, the DN and filter escaping helpers and
|
|
57
|
+
the error classes —, the `DM` type of `ldap-rest` and the
|
|
49
58
|
`Hooks` of `ldap-rest/hooks`, whose arguments are those typed in
|
|
50
59
|
`src/hooks.ts` ([hooks](plugin-development/hooks.md)), and the directory
|
|
51
60
|
operations of [the development guide](plugin-development/README.md):
|
|
@@ -58,6 +67,11 @@ API, which a minor or a patch release does not break:
|
|
|
58
67
|
Everything else — other exports, internal helpers, log messages — may
|
|
59
68
|
change in any release.
|
|
60
69
|
|
|
70
|
+
The options and the REST API are checked mechanically: committed snapshots
|
|
71
|
+
are compared with the base branch of each pull request, and a non-additive
|
|
72
|
+
change needs the `breaking` label. The plugin types are still reviewed by
|
|
73
|
+
hand. See [Published contracts](development/contracts.md).
|
|
74
|
+
|
|
61
75
|
## Presentations
|
|
62
76
|
|
|
63
77
|
Interactive presentations (using [presenterm](https://github.com/mfontanini/presenterm)):
|
|
@@ -1365,6 +1365,10 @@ const encoded = encodeURIComponent(dn);
|
|
|
1365
1365
|
// Result: uid%3Djohn.doe%2Cou%3Dusers%2Cdc%3Dexample%2Cdc%3Dcom
|
|
1366
1366
|
```
|
|
1367
1367
|
|
|
1368
|
+
A path parameter is percent-decoded once, as RFC 3986 spells it: an
|
|
1369
|
+
identifier holding `%` is sent as `%25`, so `100%` is addressed as
|
|
1370
|
+
`/api/v1/ldap/users/100%25`.
|
|
1371
|
+
|
|
1368
1372
|
### LDAP Attributes are Arrays
|
|
1369
1373
|
|
|
1370
1374
|
LDAP attributes are always returned as arrays, even for single-valued attributes:
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Published contracts
|
|
2
|
+
|
|
3
|
+
Other code depends on three things ldap-rest publishes. A change to one that
|
|
4
|
+
is not additive breaks that code, so it needs a major release and the pull
|
|
5
|
+
request says so before it merges.
|
|
6
|
+
|
|
7
|
+
- **Configuration**: the CLI options and `DM_*` variables of
|
|
8
|
+
[Configuration](../usage/configuration.md) and of each plugin's page.
|
|
9
|
+
- **REST API**: the routes, request bodies and answers of the
|
|
10
|
+
[REST API](../client-development/api/rest-api.md), their parameters,
|
|
11
|
+
responses (references resolved), statuses and response headers. The error
|
|
12
|
+
`code` values are reviewed by hand: the document types them as strings.
|
|
13
|
+
- **Plugin API**: what the npm sub-path exports declare — `ldap-rest`,
|
|
14
|
+
`./hooks`, `./plugin`, `./ldapactions`, `./expressformatedresponses`, the
|
|
15
|
+
`./plugin-*`, `./abstract-*` and `./browser-*` entries — and the JSON
|
|
16
|
+
schema format of the `./schema-*` entries.
|
|
17
|
+
|
|
18
|
+
[API stability](../README.md#api-stability) says what a minor or patch
|
|
19
|
+
release promises. This page says how a change is detected, and what to do
|
|
20
|
+
when it is not additive.
|
|
21
|
+
|
|
22
|
+
## Committed snapshots
|
|
23
|
+
|
|
24
|
+
Two snapshots are generated locally and checked in CI, so a change to a
|
|
25
|
+
contract shows up as a diff instead of a surprise after the release:
|
|
26
|
+
|
|
27
|
+
- `docs/usage/options.snapshot.json` lists every option's CLI name, plural
|
|
28
|
+
form, environment variable, type, default, kind, required flag and aliases.
|
|
29
|
+
Regenerate it with `npm run snapshot:options`; `npm run check:options`, part
|
|
30
|
+
of `npm run check`, fails when it is stale.
|
|
31
|
+
- `openapi.json` is the REST API document the published reference is built
|
|
32
|
+
from. Regenerate it with `npm run generate:openapi`; `npm run check:openapi`
|
|
33
|
+
fails when it is stale.
|
|
34
|
+
|
|
35
|
+
The plugin types have no snapshot yet: one api-extractor report per sub-path
|
|
36
|
+
export is a change of its own. Until it lands, a change to an exported type is
|
|
37
|
+
reviewed by hand.
|
|
38
|
+
|
|
39
|
+
The snapshot records what the parser reads. A narrowed `choices` list, a
|
|
40
|
+
raised `min` or a lowered `max` is checked when a plugin reads its options,
|
|
41
|
+
not by the snapshot, and is reviewed by hand, as is a custom error `code`.
|
|
42
|
+
|
|
43
|
+
## The pull request gate
|
|
44
|
+
|
|
45
|
+
The `contracts` job of `.github/workflows/contracts.yml` generates the
|
|
46
|
+
snapshots from the pull request and compares them with the base branch's
|
|
47
|
+
committed ones. It reports:
|
|
48
|
+
|
|
49
|
+
- additive changes — a new option, route or field — and lets them through;
|
|
50
|
+
- non-additive changes, and fails unless the pull request carries the
|
|
51
|
+
`breaking` label.
|
|
52
|
+
|
|
53
|
+
A change is non-additive when it removes or narrows what was published:
|
|
54
|
+
a removed option or environment variable, a changed type, default, bound,
|
|
55
|
+
`const`, enum or required flag, a request value that becomes an enum, a
|
|
56
|
+
request object that stops accepting additional properties, a removed route,
|
|
57
|
+
method, parameter, response status, response header or field, a request field
|
|
58
|
+
that becomes required, a response that may now be null, changed security
|
|
59
|
+
requirements, a removed union variant, a value an alternative of the type
|
|
60
|
+
closes with an enum or a `const` where the type used to accept any, an
|
|
61
|
+
untyped request value that becomes a union, an `allOf` that adds a
|
|
62
|
+
constraint, or — once the plugin snapshot exists — a removed export or a
|
|
63
|
+
narrowed signature. Renaming an option is a deprecation, not a removal, as
|
|
64
|
+
long as the old CLI name and environment variable stay in its `aliases` and
|
|
65
|
+
`envAliases`. Adding a union variant, a field or an optional parameter is
|
|
66
|
+
additive, and so is a request union with an alternative that still accepts
|
|
67
|
+
everything the base did — in a response the client reads every alternative,
|
|
68
|
+
so a field the others drop is still a removal. A `oneOf` is the exception:
|
|
69
|
+
it refuses a value two alternatives match, so an alternative as wide as the
|
|
70
|
+
base narrows the others away, and the alternatives are not told apart from
|
|
71
|
+
one another — a disjoint `oneOf` therefore asks for the label too.
|
|
72
|
+
Documentation-only edits, such as a summary or description, are ignored.
|
|
73
|
+
|
|
74
|
+
The job runs the scripts from the pull request, so a change to them is
|
|
75
|
+
reviewed like any other code, as `.github/CODEOWNERS` requires.
|
|
76
|
+
|
|
77
|
+
The `breaking` label records that the maintainers accepted the break, and the
|
|
78
|
+
`CHANGELOG.md` section must say so: the release gate below reads the section,
|
|
79
|
+
not the label, and refuses to ship the break as a minor or patch release. The
|
|
80
|
+
workflow listens to label events, so adding the label runs the job again; it
|
|
81
|
+
has its own concurrency group, so that event does not cancel the test matrix.
|
|
82
|
+
|
|
83
|
+
To try the comparison locally against your base branch:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx tsx scripts/contracts.ts --base origin/master
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## The release gate
|
|
90
|
+
|
|
91
|
+
`.github/workflows/release.yml` runs `scripts/check-release.ts` on the tag. A
|
|
92
|
+
non-major tag whose `CHANGELOG.md` section lists `### Breaking Changes` is
|
|
93
|
+
refused. For a 0.x version, a minor release counts as major, as semantic
|
|
94
|
+
versioning lets it break. When the tag has no section of its own, the gate
|
|
95
|
+
reads `## Unreleased`, so a tag cut before the section was renamed is still
|
|
96
|
+
checked; with neither section it fails. When the CHANGELOG lists no earlier
|
|
97
|
+
release, only a first `X.0.0` counts as major, so a trimmed file cannot open
|
|
98
|
+
the gate.
|
|
99
|
+
|
|
100
|
+
## Deprecating instead of breaking
|
|
101
|
+
|
|
102
|
+
The window and the rules are in
|
|
103
|
+
[CONTRIBUTING.md](../../CONTRIBUTING.md#deprecating-a-published-contract).
|
|
104
|
+
The mechanics of an option alias are in
|
|
105
|
+
[plugin options](../plugin-development/options.md#deprecating-an-option): the
|
|
106
|
+
old CLI name and environment variable go in `aliases` and `envAliases`, and
|
|
107
|
+
the server warns at startup while it still reads them. An array option's
|
|
108
|
+
plural form has no alias slot: renaming it is a breaking change, so it waits
|
|
109
|
+
for a major release.
|
|
@@ -7,6 +7,7 @@ This guide will help you create powerful, production-ready plugins for LDAP-Rest
|
|
|
7
7
|
- [Why Create a Plugin](#why-create-a-plugin)
|
|
8
8
|
- [Plugin Structure](#plugin-structure)
|
|
9
9
|
- [Creating Your Plugin](#creating-your-plugin)
|
|
10
|
+
- [Plugin services](./services.md)
|
|
10
11
|
- [Custom Configuration Options](#custom-configuration-options)
|
|
11
12
|
- [Plugin options](./options.md)
|
|
12
13
|
- [Loading Your Plugin](#loading-your-plugin)
|
|
@@ -54,6 +55,8 @@ export default class MyPlugin extends DmPlugin {
|
|
|
54
55
|
- **roles** (optional): Array categorizing plugin purpose
|
|
55
56
|
- **dependencies** (optional): Required plugins that must load first
|
|
56
57
|
- **hooks** (optional): Callbacks for LDAP operations
|
|
58
|
+
- **provides** (optional): Typed services this plugin offers, see
|
|
59
|
+
[plugin services](services.md)
|
|
57
60
|
- **api()** (optional): Method to register REST endpoints
|
|
58
61
|
|
|
59
62
|
### Available from Base Class
|
|
@@ -65,6 +68,8 @@ export default class MyPlugin extends DmPlugin {
|
|
|
65
68
|
- **this.logger**: Winston logger instance
|
|
66
69
|
- **this.hookBus**: The server's hooks: chain, notify and wait on them, see
|
|
67
70
|
[hooks](hooks.md#the-bus)
|
|
71
|
+
- **this.service(token)**: The instance another plugin provides under a
|
|
72
|
+
service token, or `null`, see [plugin services](services.md)
|
|
68
73
|
- **this.fetch**: `fetch` timed into the
|
|
69
74
|
[outbound metrics](../usage/operations.md#metrics) under the plugin's
|
|
70
75
|
name; the lint refuses a bare `fetch` or `node-fetch` under `src`, and a
|
|
@@ -452,7 +457,8 @@ const results = await this.server.ldap.search(
|
|
|
452
457
|
'ou=users,dc=example,dc=com'
|
|
453
458
|
);
|
|
454
459
|
|
|
455
|
-
// Search with pagination (returns AsyncGenerator
|
|
460
|
+
// Search with pagination (returns AsyncGenerator; the connection is taken
|
|
461
|
+
// when the first page is read)
|
|
456
462
|
const pagedResults = await this.server.ldap.search(
|
|
457
463
|
{
|
|
458
464
|
paged: true,
|
|
@@ -1320,6 +1326,7 @@ See [Tests and checks](../../CONTRIBUTING.md#tests-and-checks).
|
|
|
1320
1326
|
|
|
1321
1327
|
- [Contributing Guide](../../CONTRIBUTING.md) - Git workflow and PR guidelines
|
|
1322
1328
|
- [Hooks Reference](./hooks.md) - Complete list of available hooks
|
|
1329
|
+
- [Plugin services](./services.md) - Typed interfaces between plugins
|
|
1323
1330
|
- [Core Plugin Examples](../../src/plugins/) - Browse existing plugins
|
|
1324
1331
|
- [Test Examples](../../test/plugins/) - See how core plugins are tested
|
|
1325
1332
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Plugin Dependency Graph
|
|
2
2
|
|
|
3
|
-
This document describes the dependencies between LDAP-Rest plugins. There are
|
|
3
|
+
This document describes the dependencies between LDAP-Rest plugins. There are three types of dependencies:
|
|
4
4
|
|
|
5
5
|
1. **Explicit Dependencies**: Defined in the `dependencies` property of a plugin class
|
|
6
|
-
2. **
|
|
6
|
+
2. **Service Dependencies**: A plugin reaches a typed interface another one provides, see [plugin services](services.md)
|
|
7
|
+
3. **Hook Dependencies**: Implicit dependencies through hooks (one plugin provides a hook, another consumes it)
|
|
7
8
|
|
|
8
9
|
## Dependency Types
|
|
9
10
|
|
|
@@ -24,6 +25,16 @@ overrides — a dependency also given to `--plugin`, or an entry listed twice
|
|
|
24
25
|
is loaded once: the second request waits for the first. Only an entry with
|
|
25
26
|
overrides builds a second instance, and it needs a name of its own.
|
|
26
27
|
|
|
28
|
+
### Service Dependencies
|
|
29
|
+
|
|
30
|
+
A plugin provides a typed interface under a token, and a consumer reaches it
|
|
31
|
+
with `this.service(token)`. The token replaces the registry name as the way to
|
|
32
|
+
find another plugin's code; `dependencies` still decides what loads first. A
|
|
33
|
+
consumer whose provider is absent gets `null` and a single warning.
|
|
34
|
+
|
|
35
|
+
See [plugin services](services.md) for the token, the provided interface and
|
|
36
|
+
the registration rules.
|
|
37
|
+
|
|
27
38
|
### Hook Dependencies
|
|
28
39
|
|
|
29
40
|
**Hook Providers**: Plugins that implement hooks (via `hooks: Hooks = { ... }`)
|
|
@@ -60,7 +60,7 @@ await server.hookBus.settled();
|
|
|
60
60
|
- **hello**: raised by the [demo plugin](../../src/plugins/demo/helloworld.ts) for `/hello`, answered by the demo plugins
|
|
61
61
|
- **ldapsearchopts**: chained before any search, with its options
|
|
62
62
|
- **ldapsearchrequest**: chained before any search, with `[base, options, request]`; an authorization plugin refuses by throwing
|
|
63
|
-
- **ldapsearchresult**: chained after any search, with its result, before it is cached
|
|
63
|
+
- **ldapsearchresult**: chained after any search, with its result, before it is cached; for a paged search, when the first page is read
|
|
64
64
|
- **ldapsearchfilter**: chained on the result of a search after the cache, once per page, with `[result, request, options]`, to drop what the caller may not see
|
|
65
65
|
- **ldapaddrequest**: chained before any add, with `[dn, entry, request]`
|
|
66
66
|
- **ldapadddone**: notified once an add has written the entry, with the DN, the entry and the context, one by one; not awaited
|
|
@@ -118,6 +118,7 @@ await server.hookBus.settled();
|
|
|
118
118
|
- **scimgroupdeletedone**: notified once a SCIM group is deleted, with its id
|
|
119
119
|
- **scimbulkdone**: notified once a SCIM bulk request is answered
|
|
120
120
|
- **twakedeletionreplay**: notified with the DN, the entry and the context, one by one when a delete asks of an entry that is already a tombstone
|
|
121
|
+
|
|
121
122
|
<!-- /hooks -->
|
|
122
123
|
|
|
123
124
|
## Flat entities
|