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.
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/authz/composition.js +1 -1
- package/dist/lib/instanceProviders.js +1 -0
- package/dist/lib/ldapActions.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/configApi.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/twake/instances.js +1 -0
- package/dist/plugins/twake/lifecycleAttributes.js +1 -0
- package/dist/plugins/twake/lifecycleEvents.js +1 -0
- package/dist/plugins/twake/tombstone.js +1 -0
- package/dist/src/abstract/plugin.d.ts +7 -4
- package/dist/src/config/args.d.ts +48 -0
- package/dist/src/hooks.d.ts +2 -0
- package/dist/src/lib/authz/composition.d.ts +1 -1
- package/dist/src/lib/instanceProviders.d.ts +86 -0
- package/dist/src/lib/ldapActions.d.ts +10 -2
- package/dist/src/plugins/auth/openidconnect.d.ts +24 -0
- package/dist/src/plugins/scim/mapping.d.ts +4 -1
- package/dist/src/plugins/scim/patch.d.ts +7 -0
- package/dist/src/plugins/twake/instances.d.ts +88 -0
- package/dist/src/plugins/twake/lifecycleAttributes.d.ts +29 -0
- package/dist/src/plugins/twake/lifecycleEvents.d.ts +45 -0
- package/dist/src/plugins/twake/tombstone.d.ts +86 -0
- package/docs/client-development/api/rest-api.md +2 -0
- package/docs/plugin-development/hooks.md +24 -0
- package/docs/usage/configuration.md +75 -0
- package/docs/usage/plugins/README.md +9 -6
- package/docs/usage/plugins/auth/oidc.md +27 -0
- package/docs/usage/plugins/integrations/README.md +11 -4
- package/docs/usage/plugins/integrations/instances.md +122 -0
- package/docs/usage/plugins/integrations/lifecycle-events.md +200 -0
- package/docs/usage/plugins/integrations/scim.md +11 -0
- package/docs/usage/plugins/integrations/tombstone.md +115 -0
- package/docs/usage/upgrading.md +58 -0
- 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;
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -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`
|