ldap-rest 0.11.2 → 0.12.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 (40) hide show
  1. package/dist/bin/index.js +1 -1
  2. package/dist/config/args.js +1 -1
  3. package/dist/lib/authz/composition.js +1 -1
  4. package/dist/lib/instanceProviders.js +1 -0
  5. package/dist/lib/ldapActions.js +1 -1
  6. package/dist/plugins/auth/openidconnect.js +1 -1
  7. package/dist/plugins/configApi.js +1 -1
  8. package/dist/plugins/scim/groups.js +1 -1
  9. package/dist/plugins/scim/mapping.js +1 -1
  10. package/dist/plugins/scim/patch.js +1 -1
  11. package/dist/plugins/scim/users.js +1 -1
  12. package/dist/plugins/twake/instances.js +1 -0
  13. package/dist/plugins/twake/lifecycleAttributes.js +1 -0
  14. package/dist/plugins/twake/lifecycleEvents.js +1 -0
  15. package/dist/plugins/twake/tombstone.js +1 -0
  16. package/dist/src/abstract/plugin.d.ts +7 -4
  17. package/dist/src/config/args.d.ts +48 -0
  18. package/dist/src/hooks.d.ts +2 -0
  19. package/dist/src/lib/authz/composition.d.ts +1 -1
  20. package/dist/src/lib/instanceProviders.d.ts +86 -0
  21. package/dist/src/lib/ldapActions.d.ts +10 -2
  22. package/dist/src/plugins/auth/openidconnect.d.ts +24 -0
  23. package/dist/src/plugins/scim/mapping.d.ts +4 -1
  24. package/dist/src/plugins/scim/patch.d.ts +7 -0
  25. package/dist/src/plugins/twake/instances.d.ts +88 -0
  26. package/dist/src/plugins/twake/lifecycleAttributes.d.ts +29 -0
  27. package/dist/src/plugins/twake/lifecycleEvents.d.ts +45 -0
  28. package/dist/src/plugins/twake/tombstone.d.ts +86 -0
  29. package/docs/client-development/api/rest-api.md +2 -0
  30. package/docs/plugin-development/hooks.md +24 -0
  31. package/docs/usage/configuration.md +75 -0
  32. package/docs/usage/plugins/README.md +9 -6
  33. package/docs/usage/plugins/auth/oidc.md +27 -0
  34. package/docs/usage/plugins/integrations/README.md +11 -4
  35. package/docs/usage/plugins/integrations/instances.md +122 -0
  36. package/docs/usage/plugins/integrations/lifecycle-events.md +200 -0
  37. package/docs/usage/plugins/integrations/scim.md +11 -0
  38. package/docs/usage/plugins/integrations/tombstone.md +115 -0
  39. package/docs/usage/upgrading.md +58 -0
  40. package/package.json +17 -1
@@ -210,6 +210,54 @@ export interface Config {
210
210
  cloudery_default_locale?: string;
211
211
  cloudery_workflow_poll_interval_ms?: number;
212
212
  cloudery_workflow_max_attempts?: number;
213
+ twake_lifecycle_role_attribute?: string;
214
+ twake_lifecycle_lock_attribute?: string;
215
+ twake_lifecycle_lock_value?: string;
216
+ twake_lifecycle_deleted_attribute?: string;
217
+ twake_lifecycle_deleted_value?: string;
218
+ twake_lifecycle_deleted_at_attribute?: string;
219
+ twake_lifecycle_deleted_at_format?: string;
220
+ twake_lifecycle_rules?: string;
221
+ twake_instance_dn?: string[];
222
+ twake_instance_skip_attribute?: string;
223
+ twake_instance_skip_value?: string;
224
+ twake_instance_provider?: string;
225
+ twake_instance_id?: string;
226
+ twake_instance_fqdn_attribute?: string;
227
+ twake_instance_sent_attribute?: string;
228
+ twake_instance_locale?: string;
229
+ twake_instance_timeout?: number;
230
+ twake_instance_cloudery_url?: string;
231
+ twake_instance_cloudery_token?: string;
232
+ twake_instance_cloudery_offer?: string;
233
+ twake_instance_cloudery_domain?: string;
234
+ twake_instance_cloudery_organization_offer?: string;
235
+ twake_instance_cozy_url?: string;
236
+ twake_instance_cozy_user?: string;
237
+ twake_instance_cozy_passphrase?: string;
238
+ twake_instance_cozy_domain?: string;
239
+ twake_instance_cozy_context?: string;
240
+ twake_instance_cozy_apps?: string;
241
+ twake_instance_cozy_org_id?: string;
242
+ twake_instance_cozy_org_domain?: string;
243
+ twake_instance_auth_exchange?: string;
244
+ twake_instance_b2b_exchange?: string;
245
+ twake_instance_user_created_key?: string;
246
+ twake_instance_organization_created_key?: string;
247
+ twake_instance_workplace_created_key?: string;
248
+ twake_instance_queue?: string;
249
+ twake_instance_organization_base?: string;
250
+ twake_instance_organization_id_attribute?: string;
251
+ twake_instance_organization_domain_attribute?: string;
252
+ twake_instance_organization_fqdn_attribute?: string;
253
+ twake_lifecycle_reason_attribute?: string;
254
+ twake_tombstone_dn?: string[];
255
+ twake_tombstone_default_reason?: string;
256
+ twake_tombstone_reason_header?: string;
257
+ twake_tombstone_reasons?: string[];
258
+ twake_tombstone_clear_attributes?: string[];
259
+ twake_tombstone_erase_min_age?: number;
260
+ twake_tombstone_group_bases?: string[];
213
261
  rabbitmq_url?: string;
214
262
  applicative_account_base?: string;
215
263
  max_app_accounts?: number;
@@ -51,6 +51,8 @@ export interface Hooks {
51
51
  ldapsearchfilter?: ChainedHook<[SearchResult, Request?, SearchOptions?]>;
52
52
  ldapaddrequest?: ChainedHook<[string, AttributesList, Request?]>;
53
53
  ldapadddone?: (args: [string, AttributesList], context?: ChangeContext) => MaybePromise<void>;
54
+ /** See docs/plugin-development/hooks.md#ldapaddafter */
55
+ ldapaddafter?: (args: [string, AttributesList], context?: ChangeContext) => MaybePromise<void>;
54
56
  ldapmodifyrequest?: ChainedHook<[string, ModifyRequest, number, Request?]>;
55
57
  ldapmodifydone?: (args: [string, ModifyRequest, number], context?: ChangeContext) => MaybePromise<void>;
56
58
  ldapdeleterequest?: ChainedHook<[string | string[], Request?]>;
@@ -141,5 +141,5 @@ export declare function describePopulation(population: Population, authenticator
141
141
  * `server.loadedPlugins`; only what concerns it is checked
142
142
  * @throws Error naming the plugins when the composition is ambiguous
143
143
  */
144
- export declare function assertAuthzComposition(server: DM, candidate?: DmPlugin): void;
144
+ export declare function assertAuthzComposition(server: DM, candidate?: DmPlugin): Promise<void>;
145
145
  export {};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @module lib/instanceProviders
3
+ *
4
+ * The two places a workplace instance can be created: the Cloudery, which
5
+ * answers at once and builds the instance afterwards, and the cozy-stack
6
+ * admin API, which builds it before answering.
7
+ */
8
+ import { type RequestInit, type Response } from 'node-fetch';
9
+ export interface InstanceRequest {
10
+ /** Slug of the instance, and its OIDC id unless `oidc` says otherwise */
11
+ id: string;
12
+ oidc?: string;
13
+ email: string;
14
+ publicName: string;
15
+ locale: string;
16
+ phone?: string;
17
+ orgId?: string;
18
+ orgDomain?: string;
19
+ offer?: string;
20
+ }
21
+ export interface FoundInstance {
22
+ fqdn: string;
23
+ /** Built and usable, not only accepted */
24
+ ready: boolean;
25
+ }
26
+ export interface InstanceProvider {
27
+ /**
28
+ * Ask for an instance. Answers the address when the instance already
29
+ * exists by then; otherwise it comes later, with `workplace.created`.
30
+ */
31
+ create(request: InstanceRequest): Promise<string | undefined>;
32
+ /**
33
+ * The instance at this request's address, if any. One that belongs to
34
+ * another account is a conflict, not an answer.
35
+ */
36
+ find(request: InstanceRequest, at?: string): Promise<FoundInstance | undefined>;
37
+ /** The address this request's instance gets. */
38
+ address(request: InstanceRequest): string;
39
+ }
40
+ /**
41
+ * HTTP with a deadline per call: these calls sit inside the add request.
42
+ * The deadline covers the answer's body too.
43
+ */
44
+ declare abstract class HttpProvider {
45
+ protected readonly domain: string;
46
+ protected readonly timeout: number;
47
+ constructor(domain: string, timeout: number);
48
+ address(request: InstanceRequest): string;
49
+ protected call(url: string, init?: RequestInit): Promise<Response>;
50
+ protected json<T>(res: Response): Promise<T>;
51
+ private timedOut;
52
+ }
53
+ export declare class ClouderyProvider extends HttpProvider implements InstanceProvider {
54
+ private readonly url;
55
+ private readonly token;
56
+ private readonly offer;
57
+ constructor(url: string, token: string, domain: string, offer: string, timeout: number);
58
+ private headers;
59
+ create(request: InstanceRequest): Promise<undefined>;
60
+ find(request: InstanceRequest, at?: string): Promise<FoundInstance | undefined>;
61
+ createOrganization(org: {
62
+ id: string;
63
+ name: string;
64
+ domain: string;
65
+ }): Promise<void>;
66
+ }
67
+ export declare class CozyStackProvider extends HttpProvider implements InstanceProvider {
68
+ private readonly url;
69
+ private readonly context;
70
+ private readonly apps;
71
+ private readonly auth;
72
+ constructor(url: string, user: string, passphrase: string, domain: string, context: string, apps: string, timeout: number);
73
+ private headers;
74
+ create(request: InstanceRequest): Promise<string>;
75
+ /** Also finishes the onboarding of an instance left unfinished. */
76
+ find(request: InstanceRequest, at?: string): Promise<FoundInstance | undefined>;
77
+ /**
78
+ * Destroy the instance of a deleted account, the entry being gone: one
79
+ * another OIDC id owns is left alone.
80
+ */
81
+ destroy(id: string, oidc: string): Promise<void>;
82
+ private read;
83
+ /** Without it, the first login lands on the setup wizard. */
84
+ private finishOnboarding;
85
+ }
86
+ export {};
@@ -26,7 +26,7 @@ export type { SearchOptions, SearchResult };
26
26
  export interface RequestBoundLdap {
27
27
  search(options: SearchOptions, base?: string): Promise<SearchResult | AsyncGenerator<SearchResult>>;
28
28
  add(dn: string, entry: AttributesList): Promise<boolean>;
29
- modify(dn: string, changes: ModifyRequest): Promise<boolean>;
29
+ modify(dn: string, changes: ModifyRequest, options?: ModifyOptions): Promise<boolean>;
30
30
  rename(dn: string, newRdn: string): Promise<boolean>;
31
31
  delete(dn: string | string[]): Promise<boolean>;
32
32
  }
@@ -35,6 +35,14 @@ export interface ModifyRequest {
35
35
  replace?: AttributesList;
36
36
  delete?: string[] | AttributesList;
37
37
  }
38
+ export interface ModifyOptions {
39
+ /**
40
+ * Send the permissive modify control, not critical: a directory that
41
+ * honours it accepts an add of a value already there (OpenLDAP 2.4 still
42
+ * refuses a delete of an absent one); one that does not ignores it.
43
+ */
44
+ permissive?: boolean;
45
+ }
38
46
  declare class ldapActions {
39
47
  config: Config;
40
48
  options: ClientOptions;
@@ -227,7 +235,7 @@ declare class ldapActions {
227
235
  */
228
236
  private wrapPaginatedSearch;
229
237
  add(dn: string, entry: AttributesList, req?: Request): Promise<boolean>;
230
- modify(dn: string, changes: ModifyRequest, req?: Request): Promise<boolean>;
238
+ modify(dn: string, changes: ModifyRequest, req?: Request, options?: ModifyOptions): Promise<boolean>;
231
239
  rename(dn: string, newRdn: string, req?: Request): Promise<boolean>;
232
240
  /**
233
241
  * Move an entry to a new location (different parent)
@@ -5,9 +5,12 @@ import { type Role } from '../../abstract/plugin';
5
5
  import { DM } from '../../bin';
6
6
  export default class OpenIDConnect extends AuthBase {
7
7
  name: string;
8
+ configApiKey: string;
8
9
  roles: Role[];
9
10
  /** The library's router, built once: `auth()` returns a fresh one per call. */
10
11
  private router?;
12
+ /** Set once the missing `end_session_endpoint` has been reported. */
13
+ private warnedNoEndSession;
11
14
  constructor(server: DM);
12
15
  /**
13
16
  * What is handed to `express-openid-connect`.
@@ -19,6 +22,16 @@ export default class OpenIDConnect extends AuthBase {
19
22
  */
20
23
  buildConfig(): ConfigParams;
21
24
  protected identitySource(): string;
25
+ /**
26
+ * What `GET /v1/config` publishes under `features`.
27
+ *
28
+ * A front-end served by this server cannot tell otherwise that a session
29
+ * can be ended here: the logout route is this plugin's, and exists only
30
+ * when it is loaded.
31
+ *
32
+ * @returns the logout route, relative to the server root
33
+ */
34
+ getConfigApiData(): Record<string, unknown>;
22
35
  /**
23
36
  * Paths this authentication claims.
24
37
  *
@@ -71,4 +84,15 @@ export default class OpenIDConnect extends AuthBase {
71
84
  * @param next called once the request carries an identity
72
85
  */
73
86
  authMethod(req: DmRequest, res: Response, next: () => void): void;
87
+ /**
88
+ * Whether `/logout` failed because the provider offers no RP-Initiated
89
+ * Logout, rather than for a reason worth a 500.
90
+ *
91
+ * Warned once: the provider's discovery document does not change while
92
+ * the server runs.
93
+ *
94
+ * @param err what the library handed back
95
+ * @returns true when the provider has no `end_session_endpoint`
96
+ */
97
+ private noEndSession;
74
98
  }
@@ -104,7 +104,10 @@ export declare function setLockConfigWarn(sink: (message: string) => void): (mes
104
104
  * told apart from a deliberate local schema, so they are warned about: what
105
105
  * the directory does with a value is beyond anything this can check.
106
106
  */
107
- export declare function resolveLockConfig(attribute: string, value: string): {
107
+ export declare function resolveLockConfig(attribute: string, value: string, options?: {
108
+ attribute: string;
109
+ value: string;
110
+ }): {
108
111
  attribute: string;
109
112
  value: string;
110
113
  };
@@ -44,6 +44,13 @@ export interface PatchContext {
44
44
  current: AttributesList;
45
45
  }
46
46
  export declare function patchToModifyRequest(patch: PatchRequest, ctx: PatchContext): Promise<ModifyRequest>;
47
+ /**
48
+ * Write a PATCH, read and computed anew by `attempt` each time. A delete
49
+ * only names values the read showed, so noSuchAttribute means a concurrent
50
+ * write removed one of them: the PATCH is played once more on a fresh read,
51
+ * then refused with 409.
52
+ */
53
+ export declare function writePatch(dn: string, attempt: () => Promise<void>): Promise<void>;
47
54
  /**
48
55
  * Apply PATCH to a SCIM resource object in-memory (used for PUT-equivalent
49
56
  * or when the resource handler prefers object-level manipulation before
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @module plugins/twake/instances
3
+ *
4
+ * Gives every account this server creates its workplace instance, once its
5
+ * entry is written, from the Cloudery or from the cozy-stack admin API. The
6
+ * address is written once the instance exists, then `user.created` is
7
+ * published and the entry marked. The event is sent at least once.
8
+ *
9
+ * See `docs/usage/plugins/integrations/instances.md`.
10
+ */
11
+ import type { Express, Request } from 'express';
12
+ import DmPlugin, { type Role } from '../../abstract/plugin';
13
+ import type { DM } from '../../bin';
14
+ import type { Hooks } from '../../hooks';
15
+ import { type InstanceProvider, type InstanceRequest } from '../../lib/instanceProviders';
16
+ import type { AttributesList } from '../../lib/ldapActions';
17
+ export interface WorkplaceCreated {
18
+ twakeId?: string;
19
+ internalEmail?: string;
20
+ workplaceFqdn?: string;
21
+ }
22
+ /** An account's instance request, and the id `user.created` names it by. */
23
+ interface Account extends InstanceRequest {
24
+ twakeId: string;
25
+ }
26
+ export default class TwakeInstances extends DmPlugin {
27
+ name: string;
28
+ roles: Role[];
29
+ dependencies: {
30
+ rabbitmq: string;
31
+ };
32
+ protected readonly patterns: RegExp[];
33
+ protected readonly provider: InstanceProvider;
34
+ protected readonly fqdnAttribute: string;
35
+ protected readonly sentAttribute: string;
36
+ private readonly locale;
37
+ private readonly lifecycle;
38
+ /** Work on one account, or organization, waits for the work before it */
39
+ private readonly queues;
40
+ constructor(server: DM);
41
+ hooks: Hooks;
42
+ /**
43
+ * Without a broker `user.created` could not be sent, and with the Cloudery
44
+ * no address would ever arrive: the server does not start.
45
+ */
46
+ api(app: Express): Promise<void>;
47
+ /**
48
+ * Make sure this account has its instance: covers a refused or lost
49
+ * creation, and a `workplace.created` that never arrived. With a request,
50
+ * the account is read and written with the caller's rights.
51
+ */
52
+ ensureInstance(dn: string, req?: Request): Promise<'ready' | 'pending'>;
53
+ /**
54
+ * Ask for an organization's own instance, which the Cloudery also needs as
55
+ * an organization. `organization.created` follows once it exists. For the
56
+ * application that creates organizations to call.
57
+ */
58
+ ensureOrganization(org: {
59
+ id: string;
60
+ name: string;
61
+ domain: string;
62
+ }): Promise<'ready' | 'pending'>;
63
+ onWorkplaceCreated(message: WorkplaceCreated): Promise<void>;
64
+ /** Find the instance, or ask for it; write it down once it exists. */
65
+ private provide;
66
+ protected ready(dn: string, entry: AttributesList, account: Account, fqdn: string, req?: Request): Promise<void>;
67
+ private organizationReady;
68
+ /**
69
+ * Run `work` once the work already queued for `key` is over. One replica
70
+ * only: across replicas, `user.created` may still be sent twice.
71
+ */
72
+ private serialized;
73
+ private get organizationId();
74
+ private get organizationDomain();
75
+ private get organizationFqdn();
76
+ private organizationMail;
77
+ private isOrganization;
78
+ private findOrganization;
79
+ private read;
80
+ protected announce(exchange: string, key: string, message: Record<string, unknown>): Promise<void>;
81
+ /** What to ask for this account, when it gets an instance at all. */
82
+ protected account(dn: string, entry: AttributesList): Account | undefined;
83
+ private match;
84
+ private id;
85
+ protected value(entry: AttributesList, attribute: string): unknown;
86
+ protected findByMail(email: string, req?: Request): Promise<AttributesList | undefined>;
87
+ }
88
+ export {};
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @module plugins/twake/lifecycleAttributes
3
+ *
4
+ * The attributes that carry an account's lifecycle (its role, its lock, its
5
+ * deletion), read from the `--twake-lifecycle-*` options: the ones
6
+ * `core/twake/lifecycleEvents` reads, and `core/twake/tombstone` writes.
7
+ */
8
+ import type { Config } from '../../bin';
9
+ import type { AttributesList, AttributeValue } from '../../lib/ldapActions';
10
+ export type DeletedAtFormat = 'iso8601' | 'generalizedTime';
11
+ export interface LifecycleAttributes {
12
+ role: string;
13
+ lock: string;
14
+ lockValue: string;
15
+ deleted: string;
16
+ deletedValue: string;
17
+ deletedAt: string;
18
+ deletedAtFormat: DeletedAtFormat;
19
+ reason: string;
20
+ }
21
+ export declare function lifecycleAttributes(config: Config): LifecycleAttributes;
22
+ /** An attribute of an entry, whatever case the caller spelled its name in. */
23
+ export declare function valueOf<T>(entry: Record<string, T>, attribute: string): T | undefined;
24
+ export declare function first(value: AttributeValue | null | undefined): string | undefined;
25
+ export declare const isTombstone: (entry: AttributesList, attrs: LifecycleAttributes) => boolean;
26
+ export declare const isLocked: (entry: AttributesList, attrs: LifecycleAttributes) => boolean;
27
+ export declare function formatDeletedAt(date: Date, format: DeletedAtFormat): string;
28
+ /** A deletion date as the directory holds it, or undefined if unreadable. */
29
+ export declare function parseDeletedAt(value: string, format: DeletedAtFormat): Date | undefined;
@@ -0,0 +1,45 @@
1
+ import DmPlugin, { type Role } from '../../abstract/plugin';
2
+ import type { DM } from '../../bin';
3
+ import type { Hooks } from '../../hooks';
4
+ export declare const LIFECYCLE_EVENTS: readonly ["created", "roleChanged", "disabled", "enabled", "deleted"];
5
+ export type LifecycleEvent = (typeof LIFECYCLE_EVENTS)[number];
6
+ type Payload = Record<string, string>;
7
+ interface Target {
8
+ exchange: string;
9
+ routingKey: string;
10
+ payload: Payload;
11
+ when: Payload;
12
+ }
13
+ interface Rule {
14
+ dn: RegExp;
15
+ targets: Partial<Record<LifecycleEvent, Target[]>>;
16
+ }
17
+ export declare function parseRules(source: string): Rule[];
18
+ export default class TwakeLifecycleEvents extends DmPlugin {
19
+ name: string;
20
+ roles: Role[];
21
+ dependencies: {
22
+ onLdapChange: string;
23
+ rabbitmq: string;
24
+ };
25
+ private readonly rules;
26
+ private readonly attrs;
27
+ constructor(server: DM);
28
+ /**
29
+ * core/rabbitmq connects lazily and hands back no client when it cannot:
30
+ * every event would then be lost, so the server does not start.
31
+ */
32
+ assertComposition(): Promise<void>;
33
+ /**
34
+ * SCIM deactivates by writing its own lock: on another attribute or
35
+ * value, `disabled` and `enabled` never follow a SCIM `active` change.
36
+ */
37
+ afterLoad(): void;
38
+ hooks: Hooks;
39
+ private replay;
40
+ private match;
41
+ private onEntryChange;
42
+ private resolve;
43
+ private publish;
44
+ }
45
+ export {};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @module plugins/twake/tombstone
3
+ *
4
+ * Turns the delete of an account into a tombstone: the entry stays, flagged
5
+ * as deleted, dated, locked, with the reason and without the attributes the
6
+ * deployment wants gone. The address it holds cannot be taken again until the
7
+ * tombstone is erased.
8
+ *
9
+ * SCIM treats a tombstone as gone. Erasing removes the entry and its group
10
+ * memberships, once the deletion is old enough or when forced, and announces
11
+ * nothing: the deletion already did.
12
+ *
13
+ * See `docs/usage/plugins/integrations/tombstone.md`.
14
+ */
15
+ import type { Express, Request } from 'express';
16
+ import DmPlugin, { type Role } from '../../abstract/plugin';
17
+ import type { DM } from '../../bin';
18
+ import type { Hooks } from '../../hooks';
19
+ export default class TwakeTombstone extends DmPlugin {
20
+ name: string;
21
+ roles: Role[];
22
+ private readonly attrs;
23
+ private readonly patterns;
24
+ private readonly defaultReason;
25
+ private readonly reasonHeader;
26
+ private readonly reasons;
27
+ private readonly clearAttributes;
28
+ private readonly eraseMinAge;
29
+ private readonly groupBases;
30
+ private readonly scimPrefix;
31
+ /** Deletes asked for through {@link tombstone}, with their reason. */
32
+ private readonly reasonFor;
33
+ /** Entries being erased: their delete must reach the directory. */
34
+ private readonly erasing;
35
+ constructor(server: DM);
36
+ /**
37
+ * Last in the delete and add chains: a plugin that may refuse the request
38
+ * judges it before a tombstone is written or erased.
39
+ */
40
+ afterLoad(): void;
41
+ hooks: Hooks;
42
+ api(app: Express): void;
43
+ /**
44
+ * Delete an entry as a tombstone recording this reason. Goes through the
45
+ * directory's delete, so every plugin judging deletes judges this one.
46
+ */
47
+ tombstone(dn: string, reason: string, req?: Request): Promise<void>;
48
+ /**
49
+ * Remove a tombstone and its group memberships. Refused while the deletion
50
+ * is younger than the configured age, unless forced.
51
+ */
52
+ erase(dn: string, { force, req }?: {
53
+ force?: boolean;
54
+ req?: Request;
55
+ }): Promise<void>;
56
+ /**
57
+ * core/ldap/trash moves a deleted entry away before this plugin, last in
58
+ * the delete chain, sees it: no tombstone would be written, and an erase
59
+ * would move the tombstone instead of removing it.
60
+ */
61
+ assertComposition(): void;
62
+ /**
63
+ * Erase a tombstone so a SCIM create can take its identity. The directory
64
+ * may still refuse the add; the tombstone and its memberships are then put
65
+ * back once the response is sent.
66
+ */
67
+ private replace;
68
+ /**
69
+ * Put back a tombstone the add did not replace, with its memberships. A
70
+ * group the erase left holding only the placeholder loses it again.
71
+ */
72
+ private restore;
73
+ private matches;
74
+ private isScim;
75
+ private checkReason;
76
+ private reasonOf;
77
+ private read;
78
+ private writeTombstone;
79
+ private groupsOf;
80
+ private leaveGroups;
81
+ /**
82
+ * core/ldap/groups and refint clean the same memberships once the delete
83
+ * lands, so each outcome of a race with them counts as done.
84
+ */
85
+ private leaveGroup;
86
+ }
@@ -244,6 +244,8 @@ curl -H "Accept: application/json" \
244
244
  - `features.ldapFlatGeneric.flatResources`: Array of flat LDAP resources (users, positions, etc.)
245
245
  - `features.ldapGroups`: Group management configuration (if enabled)
246
246
  - `features.ldapOrganizations`: Organization tree configuration (if enabled)
247
+ - `features.openidconnect.endpoints.logout`: the logout route, when OpenID Connect
248
+ guards the server ([details](../../usage/plugins/auth/oidc.md#logout))
247
249
 
248
250
  ---
249
251
 
@@ -13,10 +13,34 @@ Typescript definitions into [hooks.ts](./src/hooks.ts)
13
13
  - **ldapopts**: called before any ldapsearch to modify search options
14
14
  - **ldapsearchresult**: called after any ldapsearch to modify search result
15
15
  - **ldapaddrequest**: called before any ldapadd
16
+ - **ldapadddone**: launched once an ldapadd has written the entry, not awaited
17
+ - **ldapaddafter**: awaited once an ldapadd has written the entry, before it
18
+ answers, see [below](#ldapaddafter)
16
19
  - **ldapmodifyrequest**: called before any ldapmodify
17
20
  - **ldapdeleterequest**: called before any ldapdelete
18
21
  - **ldaprenamerequest**: called before any ldap rename/modifyDN operation
19
22
 
23
+ ### ldapaddafter
24
+
25
+ For what must follow an add within the same request, such as provisioning
26
+ the account the entry describes.
27
+
28
+ - Subscribers get `[dn, entry]` and the change context, as `ldapadddone`
29
+ does. They run one after the other, and `add()` answers once the last has
30
+ returned.
31
+ - An error is logged and goes no further: the add has succeeded, and the
32
+ next subscriber still runs. A subscriber that can fail owns its repair
33
+ path, and should be safe to run again on the same entry.
34
+ - It fires for every add, including those made without a request (trash,
35
+ storage, appAccountsConsistency, externalUsersInGroups), whose context is
36
+ empty. Some of these follow an API call all the same, so select entries by
37
+ their DN or attributes rather than by an empty context.
38
+ - The entry is the one submitted, after `ldapaddrequest`, not what the
39
+ directory holds: no operational attributes, and the password as given.
40
+ - `ldapadddone` and `onLdapEntryChange` fire whatever this hook does, and in
41
+ no set order with it: an add event may or may not include what a
42
+ subscriber here wrote.
43
+
20
44
  ## [onChange](../usage/plugins/ldap/on-change.md) hooks
21
45
 
22
46
  - **onLdapEntryChange**: called after any write, with the entry before and
@@ -445,6 +445,81 @@ Automatically creates/updates/deletes applicative account entries when users are
445
445
 
446
446
  **Requires:** `core/ldap/onChange`
447
447
 
448
+ #### `core/twake/lifecycleEvents`
449
+
450
+ | CLI | Env | Default | Description |
451
+ | ---------------------------------------- | ----------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------- |
452
+ | `--twake-lifecycle-role-attribute` | `DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE` | | Role attribute |
453
+ | `--twake-lifecycle-lock-attribute` | `DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE` | `--scim-user-lock-attribute` | Lock attribute |
454
+ | `--twake-lifecycle-lock-value` | `DM_TWAKE_LIFECYCLE_LOCK_VALUE` | see description | Value marking an account locked: SCIM's with SCIM's attribute, required with another than `pwdAccountLockedTime` |
455
+ | `--twake-lifecycle-deleted-attribute` | `DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE` | | Attribute marking a tombstone |
456
+ | `--twake-lifecycle-deleted-value` | `DM_TWAKE_LIFECYCLE_DELETED_VALUE` | `TRUE` | Value marking a tombstone |
457
+ | `--twake-lifecycle-deleted-at-attribute` | `DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE` | | Deletion date attribute |
458
+ | `--twake-lifecycle-deleted-at-format` | `DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT` | `iso8601` | `iso8601` or `generalizedTime` |
459
+ | `--twake-lifecycle-rules` | `DM_TWAKE_LIFECYCLE_RULES` | | Rules: a JSON file, or the JSON itself |
460
+
461
+ See [lifecycle events](plugins/integrations/lifecycle-events.md).
462
+
463
+ **Requires:** `core/ldap/onChange`, `core/rabbitmq`
464
+
465
+ #### `core/twake/instances`
466
+
467
+ | CLI | Env | Default | Description |
468
+ | ------------------------------------------------ | ------------------------------------------------- | ------------------------------------- | --------------------------------------------------------- |
469
+ | `--twake-instance-dn` | `DM_TWAKE_INSTANCE_DN` | `[]` | DN expressions of the accounts to equip |
470
+ | `--twake-instance-skip-attribute` | `DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE` | | Attribute marking an account with no instance |
471
+ | `--twake-instance-skip-value` | `DM_TWAKE_INSTANCE_SKIP_VALUE` | | Its value |
472
+ | `--twake-instance-provider` | `DM_TWAKE_INSTANCE_PROVIDER` | `cloudery` | `cloudery` or `cozy-stack` |
473
+ | `--twake-instance-id` | `DM_TWAKE_INSTANCE_ID` | `{uid}` | Instance id template |
474
+ | `--twake-instance-fqdn-attribute` | `DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE` | `twakeWorkspaceUrl` | Address attribute |
475
+ | `--twake-instance-sent-attribute` | `DM_TWAKE_INSTANCE_SENT_ATTRIBUTE` | `twakeCreatedEventAt` | Date the creation event was sent |
476
+ | `--twake-instance-locale` | `DM_TWAKE_INSTANCE_LOCALE` | `en`, `fr` with cozy-stack | Locale when the entry has none |
477
+ | `--twake-instance-timeout` | `DM_TWAKE_INSTANCE_TIMEOUT` | `30000` | Milliseconds a provider call may take |
478
+ | `--twake-instance-cloudery-url` | `DM_TWAKE_INSTANCE_CLOUDERY_URL` | | Cloudery API |
479
+ | `--twake-instance-cloudery-token` | `DM_TWAKE_INSTANCE_CLOUDERY_TOKEN` | | Its token |
480
+ | `--twake-instance-cloudery-domain` | `DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN` | | Domain of the instances |
481
+ | `--twake-instance-cloudery-offer` | `DM_TWAKE_INSTANCE_CLOUDERY_OFFER` | `b2b_twake_default` | Offer of an account's instance |
482
+ | `--twake-instance-cloudery-organization-offer` | `DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER` | | Offer of an organization's instance |
483
+ | `--twake-instance-cozy-url` | `DM_TWAKE_INSTANCE_COZY_URL` | | cozy-stack admin API |
484
+ | `--twake-instance-cozy-user` | `DM_TWAKE_INSTANCE_COZY_USER` | `admin` | Its user |
485
+ | `--twake-instance-cozy-passphrase` | `DM_TWAKE_INSTANCE_COZY_PASSPHRASE` | | Its passphrase |
486
+ | `--twake-instance-cozy-domain` | `DM_TWAKE_INSTANCE_COZY_DOMAIN` | | Domain of the instances |
487
+ | `--twake-instance-cozy-context` | `DM_TWAKE_INSTANCE_COZY_CONTEXT` | `default` | cozy-stack context |
488
+ | `--twake-instance-cozy-apps` | `DM_TWAKE_INSTANCE_COZY_APPS` | `home,drive,settings,notes,dataproxy` | Apps installed on a new instance |
489
+ | `--twake-instance-cozy-org-id` | `DM_TWAKE_INSTANCE_COZY_ORG_ID` | | Organization id when the DN gives none |
490
+ | `--twake-instance-cozy-org-domain` | `DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN` | | Organization domain when the DN gives none |
491
+ | `--twake-instance-auth-exchange` | `DM_TWAKE_INSTANCE_AUTH_EXCHANGE` | `auth` | Exchange of account events |
492
+ | `--twake-instance-b2b-exchange` | `DM_TWAKE_INSTANCE_B2B_EXCHANGE` | `b2b` | Exchange of organization events |
493
+ | `--twake-instance-user-created-key` | `DM_TWAKE_INSTANCE_USER_CREATED_KEY` | `user.created` | Routing key of an account's creation |
494
+ | `--twake-instance-organization-created-key` | `DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY` | `organization.created` | Routing key of an organization's creation |
495
+ | `--twake-instance-workplace-created-key` | `DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY` | `workplace.created` | Routing key the Cloudery announces on |
496
+ | `--twake-instance-queue` | `DM_TWAKE_INSTANCE_QUEUE` | `workplace.created.ldap-rest` | Queue shared by the replicas |
497
+ | `--twake-instance-organization-base` | `DM_TWAKE_INSTANCE_ORGANIZATION_BASE` | | Branch of the organization entries |
498
+ | `--twake-instance-organization-id-attribute` | `DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE` | `ou` | Attribute holding an organization's id |
499
+ | `--twake-instance-organization-domain-attribute` | `DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE` | `twakeDomain` | Attribute holding an organization's domain |
500
+ | `--twake-instance-organization-fqdn-attribute` | `DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE` | | Address attribute of an organization (empty: not written) |
501
+
502
+ See [instances](plugins/integrations/instances.md).
503
+
504
+ **Requires:** `core/rabbitmq`
505
+
506
+ #### `core/twake/tombstone`
507
+
508
+ Reads the `--twake-lifecycle-*` attributes above, and:
509
+
510
+ | CLI | Env | Default | Description |
511
+ | ------------------------------------ | ------------------------------------- | ------------------- | ----------------------------------------- |
512
+ | `--twake-lifecycle-reason-attribute` | `DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE` | | Deletion reason attribute |
513
+ | `--twake-tombstone-dn` | `DM_TWAKE_TOMBSTONE_DN` | `[]` | DN patterns of entries kept as tombstones |
514
+ | `--twake-tombstone-default-reason` | `DM_TWAKE_TOMBSTONE_DEFAULT_REASON` | `deleted` | Reason when none is given |
515
+ | `--twake-tombstone-reason-header` | `DM_TWAKE_TOMBSTONE_REASON_HEADER` | `x-deletion-reason` | Request header carrying the reason |
516
+ | `--twake-tombstone-reasons` | `DM_TWAKE_TOMBSTONE_REASONS` | `[]` | Accepted reasons (empty: any) |
517
+ | `--twake-tombstone-clear-attributes` | `DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES` | `[]` | Attributes removed from a tombstone |
518
+ | `--twake-tombstone-erase-min-age` | `DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE` | `2592000` | Seconds before a tombstone may be erased |
519
+ | `--twake-tombstone-group-bases` | `DM_TWAKE_TOMBSTONE_GROUP_BASES` | `--ldap-group-base` | Where memberships are removed at erase |
520
+
521
+ See [tombstone](plugins/integrations/tombstone.md).
522
+
448
523
  ### Utility Plugins
449
524
 
450
525
  #### `core/static`