ldap-rest 0.11.1 → 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 (49) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/bin/index.js +1 -1
  3. package/dist/config/args.js +1 -1
  4. package/dist/lib/authz/composition.js +1 -1
  5. package/dist/lib/instanceProviders.js +1 -0
  6. package/dist/lib/ldapActions.js +1 -1
  7. package/dist/lib/ldapSchema.js +1 -1
  8. package/dist/plugins/auth/openidconnect.js +1 -1
  9. package/dist/plugins/configApi.js +1 -1
  10. package/dist/plugins/ldap/groups.js +1 -1
  11. package/dist/plugins/ldap/organizations.js +1 -1
  12. package/dist/plugins/ldap/raw.js +1 -1
  13. package/dist/plugins/scim/groups.js +1 -1
  14. package/dist/plugins/scim/mapping.js +1 -1
  15. package/dist/plugins/scim/patch.js +1 -1
  16. package/dist/plugins/scim/users.js +1 -1
  17. package/dist/plugins/twake/instances.js +1 -0
  18. package/dist/plugins/twake/lifecycleAttributes.js +1 -0
  19. package/dist/plugins/twake/lifecycleEvents.js +1 -0
  20. package/dist/plugins/twake/tombstone.js +1 -0
  21. package/dist/src/abstract/plugin.d.ts +7 -4
  22. package/dist/src/config/args.d.ts +50 -0
  23. package/dist/src/hooks.d.ts +2 -0
  24. package/dist/src/lib/authz/composition.d.ts +1 -1
  25. package/dist/src/lib/instanceProviders.d.ts +86 -0
  26. package/dist/src/lib/ldapActions.d.ts +79 -2
  27. package/dist/src/lib/ldapSchema.d.ts +14 -0
  28. package/dist/src/plugins/auth/openidconnect.d.ts +24 -0
  29. package/dist/src/plugins/ldap/raw.d.ts +3 -6
  30. package/dist/src/plugins/scim/mapping.d.ts +4 -1
  31. package/dist/src/plugins/scim/patch.d.ts +7 -0
  32. package/dist/src/plugins/twake/instances.d.ts +88 -0
  33. package/dist/src/plugins/twake/lifecycleAttributes.d.ts +29 -0
  34. package/dist/src/plugins/twake/lifecycleEvents.d.ts +45 -0
  35. package/dist/src/plugins/twake/tombstone.d.ts +86 -0
  36. package/docs/client-development/api/rest-api.md +2 -0
  37. package/docs/plugin-development/hooks.md +24 -0
  38. package/docs/usage/configuration.md +86 -10
  39. package/docs/usage/plugins/README.md +9 -6
  40. package/docs/usage/plugins/auth/oidc.md +27 -0
  41. package/docs/usage/plugins/integrations/README.md +11 -4
  42. package/docs/usage/plugins/integrations/instances.md +122 -0
  43. package/docs/usage/plugins/integrations/lifecycle-events.md +200 -0
  44. package/docs/usage/plugins/integrations/scim.md +11 -0
  45. package/docs/usage/plugins/integrations/tombstone.md +115 -0
  46. package/docs/usage/plugins/ldap/flat-generic.md +26 -0
  47. package/docs/usage/plugins/ldap/raw.md +1 -1
  48. package/docs/usage/upgrading.md +58 -0
  49. package/package.json +17 -1
@@ -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
  }
@@ -18,7 +18,7 @@
18
18
  import type { Express, Request } from 'express';
19
19
  import DmPlugin, { type Role } from '../../abstract/plugin';
20
20
  import type { DM } from '../../bin';
21
- import { SchemaIndex, type LdapSchema } from '../../lib/ldapSchema';
21
+ import type { LdapSchema, SchemaIndex } from '../../lib/ldapSchema';
22
22
  /**
23
23
  * Shared OpenAPI schemas surfaced by this plugin. Picked up by
24
24
  * scripts/generate-openapi.ts and merged into `components.schemas`.
@@ -114,9 +114,6 @@ export default class LdapRaw extends DmPlugin {
114
114
  showSecrets: boolean;
115
115
  /** Maximum entries returned by a search or a children listing */
116
116
  maxResults: number;
117
- private schemaCacheTtl;
118
- private schemaCache?;
119
- private schemaPromise?;
120
117
  constructor(server: DM);
121
118
  /**
122
119
  * Reject any DN outside the configured bases. Authorization plugins narrow
@@ -178,8 +175,8 @@ export default class LdapRaw extends DmPlugin {
178
175
  */
179
176
  getSubschemaDn(): Promise<string>;
180
177
  /**
181
- * Fetch and parse the directory schema, with a cache: it is large and
182
- * changes rarely. Concurrent callers share a single fetch.
178
+ * The parsed directory schema, cached for `--ldap-raw-schema-cache-ttl`
179
+ * and shared with the object class repair.
183
180
  *
184
181
  * @returns indexed schema
185
182
  */
@@ -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
@@ -173,9 +173,10 @@ at `0` otherwise.
173
173
 
174
174
  #### `core/ldap/flatGeneric`
175
175
 
176
- | CLI | Plural | Env | Default | Description |
177
- | -------------------- | --------------------- | --------------------- | ------- | --------------------- |
178
- | `--ldap-flat-schema` | `--ldap-flat-schemas` | `DM_LDAP_FLAT_SCHEMA` | `[]` | Entity schema path(s) |
176
+ | CLI | Plural | Env | Default | Description |
177
+ | ------------------------- | --------------------- | -------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
178
+ | `--ldap-flat-schema` | `--ldap-flat-schemas` | `DM_LDAP_FLAT_SCHEMA` | `[]` | Entity schema path(s) |
179
+ | `--ldap-flat-auto-repair` | | `DM_LDAP_FLAT_AUTO_REPAIR` | `true` | Add the auxiliary class a write needs to an entry lacking it — [details](plugins/ldap/flat-generic.md#entries-created-by-other-tools) |
179
180
 
180
181
  #### `core/ldap/bulkImport`
181
182
 
@@ -215,13 +216,13 @@ Maintains consistency of department links when organizations are renamed/moved:
215
216
 
216
217
  Read-only low-level browsing (root DSE, schema, entries). See [raw plugin](plugins/ldap/raw.md).
217
218
 
218
- | CLI | Plural | Env | Default | Description |
219
- | ----------------------------- | ------------------------------ | ------------------------------- | ------------- | ------------------------------- |
220
- | `--ldap-raw-base` | `--ldap-raw-bases` | `DM_LDAP_RAW_BASE` | `--ldap-base` | Subtrees exposed by the API |
221
- | `--ldap-raw-hidden-attribute` | `--ldap-raw-hidden-attributes` | `DM_LDAP_RAW_HIDDEN_ATTRIBUTES` | `[]` | Extra attributes never returned |
222
- | `--ldap-raw-show-secrets` | | `DM_LDAP_RAW_SHOW_SECRETS` | `false` | Serve credential attributes |
223
- | `--ldap-raw-max-results` | | `DM_LDAP_RAW_MAX_RESULTS` | `200` | Max entries per search/listing |
224
- | `--ldap-raw-schema-cache-ttl` | | `DM_LDAP_RAW_SCHEMA_CACHE_TTL` | `3600` | Schema cache lifetime (seconds) |
219
+ | CLI | Plural | Env | Default | Description |
220
+ | ----------------------------- | ------------------------------ | ------------------------------- | ------------- | -------------------------------------------------------------------- |
221
+ | `--ldap-raw-base` | `--ldap-raw-bases` | `DM_LDAP_RAW_BASE` | `--ldap-base` | Subtrees exposed by the API |
222
+ | `--ldap-raw-hidden-attribute` | `--ldap-raw-hidden-attributes` | `DM_LDAP_RAW_HIDDEN_ATTRIBUTES` | `[]` | Extra attributes never returned |
223
+ | `--ldap-raw-show-secrets` | | `DM_LDAP_RAW_SHOW_SECRETS` | `false` | Serve credential attributes |
224
+ | `--ldap-raw-max-results` | | `DM_LDAP_RAW_MAX_RESULTS` | `200` | Max entries per search/listing |
225
+ | `--ldap-raw-schema-cache-ttl` | | `DM_LDAP_RAW_SCHEMA_CACHE_TTL` | `3600` | Schema cache lifetime (seconds), shared with the object class repair |
225
226
 
226
227
  ### Authentication Plugins
227
228
 
@@ -444,6 +445,81 @@ Automatically creates/updates/deletes applicative account entries when users are
444
445
 
445
446
  **Requires:** `core/ldap/onChange`
446
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
+
447
523
  ### Utility Plugins
448
524
 
449
525
  #### `core/static`
@@ -53,12 +53,15 @@ Protection and rate limiting:
53
53
 
54
54
  Connect to external systems:
55
55
 
56
- | Plugin | Description |
57
- | -------------------------------------------------- | ---------------------------------- |
58
- | [james-mail](integrations/james-mail.md) | Apache James synchronization |
59
- | [james-mailboxes](integrations/james-mailboxes.md) | Apache James team mailboxes |
60
- | [calendar](integrations/calendar.md) | Twake Calendar resources and users |
61
- | [app-accounts](integrations/app-accounts.md) | Applicative accounts API |
56
+ | Plugin | Description |
57
+ | ---------------------------------------------------- | ------------------------------------ |
58
+ | [james-mail](integrations/james-mail.md) | Apache James synchronization |
59
+ | [james-mailboxes](integrations/james-mailboxes.md) | Apache James team mailboxes |
60
+ | [calendar](integrations/calendar.md) | Twake Calendar resources and users |
61
+ | [app-accounts](integrations/app-accounts.md) | Applicative accounts API |
62
+ | [lifecycle-events](integrations/lifecycle-events.md) | Account lifecycle events to RabbitMQ |
63
+ | [instances](integrations/instances.md) | Workplace instance of each account |
64
+ | [tombstone](integrations/tombstone.md) | Deleted accounts kept as tombstones |
62
65
 
63
66
  ## Utility Plugins
64
67
 
@@ -85,6 +85,33 @@ client (`Accept: application/json`) receives 401. Every other authentication
85
85
  plugin in this repository answers JSON 401 throughout — the difference is
86
86
  kept because it is what makes a login work at all.
87
87
 
88
+ ## Logout
89
+
90
+ `/logout` ends the session at the provider as well (RP-Initiated Logout): the
91
+ browser is sent to the provider's `end_session_endpoint` with the id token as
92
+ `id_token_hint`, and the provider closes its own session — and, with
93
+ [Back-Channel Logout](back-channel-logout.md), tells the other applications.
94
+ No `post_logout_redirect_uri` is sent: the caller ends on the provider's own
95
+ logout page, and nothing has to be registered there.
96
+
97
+ Ending the cookie alone would not log anyone out: every guarded path goes
98
+ through the provider, which still holds its session and logs the caller back
99
+ in without asking anything.
100
+
101
+ A provider whose discovery document names no `end_session_endpoint` gets a
102
+ local logout — the cookie is dropped, the browser goes to `--base-url` — and
103
+ a warning saying the provider may log the caller back in.
104
+
105
+ `GET /v1/config` (with `core/configApi`) publishes the route, so a front-end
106
+ knows it can offer a sign-out control. The key is `openidconnect` whatever
107
+ the instance is named:
108
+
109
+ ```json
110
+ "features": {
111
+ "openidconnect": { "enabled": true, "endpoints": { "logout": "/logout" } }
112
+ }
113
+ ```
114
+
88
115
  ## How It Works
89
116
 
90
117
  1. Uses `express-openid-connect` for OAuth2/OIDC flow
@@ -11,10 +11,13 @@ Plugins for connecting LDAP-Rest to external systems.
11
11
 
12
12
  ## Twake
13
13
 
14
- | Plugin | Description |
15
- | ------------------------------- | ---------------------------------------- |
16
- | [calendar](calendar.md) | Calendar resources and users sync |
17
- | [app-accounts](app-accounts.md) | Applicative accounts API (devices, apps) |
14
+ | Plugin | Description |
15
+ | --------------------------------------- | ---------------------------------------- |
16
+ | [calendar](calendar.md) | Calendar resources and users sync |
17
+ | [app-accounts](app-accounts.md) | Applicative accounts API (devices, apps) |
18
+ | [lifecycle-events](lifecycle-events.md) | Account lifecycle events to RabbitMQ |
19
+ | [instances](instances.md) | Workplace instance of each account |
20
+ | [tombstone](tombstone.md) | Deleted accounts kept as tombstones |
18
21
 
19
22
  ## Prerequisites
20
23
 
@@ -39,4 +42,8 @@ core/twake/james
39
42
 
40
43
  core/twake/calendar
41
44
  └─ requires: core/ldap/onChange
45
+
46
+ core/twake/lifecycleEvents
47
+ ├─ requires: core/ldap/onChange
48
+ └─ requires: core/rabbitmq
42
49
  ```