ldap-rest 1.1.0 → 1.2.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/bin/index.mjs +8 -7
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/plugin.js +1 -1
- package/dist/abstract/twakePlugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/config/schema.js +1 -1
- package/dist/lib/changeContext.js +1 -1
- package/dist/lib/entitySchema/validator.js +1 -0
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/lib/health.js +1 -0
- package/dist/lib/instanceProviders.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/metrics.js +1 -0
- package/dist/lib/requestContext.js +1 -1
- package/dist/lib/shutdown.js +1 -0
- package/dist/lib/staticFiles.js +1 -0
- package/dist/lib/storage/store.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/logger/winston.js +1 -1
- package/dist/plugins/auth/crowdsec.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/demo/helloworld.js +1 -1
- package/dist/plugins/ldap/bulkImport.js +1 -1
- package/dist/plugins/ldap/flatGeneric.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/metrics.js +1 -0
- package/dist/plugins/rabbitmq.js +1 -1
- package/dist/plugins/scim/scim.js +1 -1
- package/dist/plugins/static.js +1 -1
- package/dist/plugins/storage/file.js +1 -1
- package/dist/plugins/storage/postgres.js +1 -1
- package/dist/plugins/storage/valkey.js +1 -1
- package/dist/plugins/storage.js +1 -1
- package/dist/plugins/twake/calendar.js +1 -1
- package/dist/plugins/twake/clouderyProvision.js +1 -1
- package/dist/plugins/twake/cozyProvision.js +1 -1
- package/dist/plugins/twake/drive.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/plugins/twake/migrationProxy.js +1 -0
- package/dist/plugins/weblogs.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +3 -13
- package/dist/src/abstract/plugin.d.ts +20 -0
- package/dist/src/abstract/twakePlugin.d.ts +23 -1
- package/dist/src/bin/index.d.ts +46 -1
- package/dist/src/config/args.d.ts +12 -1
- package/dist/src/lib/changeContext.d.ts +11 -0
- package/dist/src/lib/entitySchema/validator.d.ts +64 -0
- package/dist/src/lib/expressFormatedResponses.d.ts +19 -0
- package/dist/src/lib/health.d.ts +63 -0
- package/dist/src/lib/instanceProviders.d.ts +4 -0
- package/dist/src/lib/ldapActions.d.ts +28 -0
- package/dist/src/lib/metrics.d.ts +44 -0
- package/dist/src/lib/requestContext.d.ts +40 -0
- package/dist/src/lib/shutdown.d.ts +20 -0
- package/dist/src/lib/staticFiles.d.ts +33 -0
- package/dist/src/lib/storage/store.d.ts +8 -0
- package/dist/src/lib/utils.d.ts +9 -0
- package/dist/src/logger/winston.d.ts +4 -0
- package/dist/src/plugins/ldap/groups.d.ts +18 -3
- package/dist/src/plugins/ldap/organizations.d.ts +3 -5
- package/dist/src/plugins/metrics.d.ts +17 -0
- package/dist/src/plugins/rabbitmq.d.ts +37 -1
- package/dist/src/plugins/static.d.ts +12 -0
- package/dist/src/plugins/storage/file.d.ts +4 -0
- package/dist/src/plugins/storage/postgres.d.ts +7 -14
- package/dist/src/plugins/storage/valkey.d.ts +2 -15
- package/dist/src/plugins/storage.d.ts +8 -0
- package/dist/src/plugins/twake/drive.d.ts +8 -0
- package/dist/src/plugins/twake/james.d.ts +2 -0
- package/dist/src/plugins/twake/migrationProxy.d.ts +71 -0
- package/docs/client-development/api/reference.md +5 -9
- package/docs/client-development/api/rest-api.md +4 -10
- package/docs/client-development/schemas/README.md +60 -21
- package/docs/plugin-development/README.md +15 -13
- package/docs/plugin-development/dependencies.md +8 -0
- package/docs/plugin-development/hooks.md +1 -1
- package/docs/usage/README.md +1 -0
- package/docs/usage/configuration.md +49 -11
- package/docs/usage/operations.md +193 -0
- package/docs/usage/plugins/README.md +2 -0
- package/docs/usage/plugins/auth/authz-per-branch.md +2 -1
- package/docs/usage/plugins/integrations/README.md +8 -4
- package/docs/usage/plugins/integrations/mail-migration.md +102 -0
- package/docs/usage/plugins/integrations/scim.md +4 -2
- package/docs/usage/plugins/ldap/bulk-import.md +3 -1
- package/docs/usage/plugins/ldap/flat-generic.md +5 -2
- package/docs/usage/plugins/ldap/on-change.md +2 -1
- package/docs/usage/plugins/ldap/organizations.md +7 -7
- package/docs/usage/plugins/utilities/metrics.md +21 -0
- package/docs/usage/plugins/utilities/static.md +16 -8
- package/docs/usage/plugins/utilities/storage.md +7 -0
- package/docs/usage/plugins/utilities/weblogs.md +51 -30
- package/docs/usage/upgrading.md +71 -0
- package/package.json +11 -2
- package/static/schemas/example/README.md +1 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AttributesList, AttributeValue, ModifyRequest } from '../ldapActions';
|
|
2
|
+
import type { Schema } from '../../config/schema';
|
|
3
|
+
export interface EntityValidatorDeps {
|
|
4
|
+
/**
|
|
5
|
+
* Tells whether a DN names an entry. Asked as the server: a reference to
|
|
6
|
+
* an entry the caller cannot read is still a valid reference.
|
|
7
|
+
*/
|
|
8
|
+
entryExists: (dn: string) => Promise<boolean>;
|
|
9
|
+
/** Asked whether a `generated` attribute will be filled after validation */
|
|
10
|
+
loadedPlugins: Record<string, unknown>;
|
|
11
|
+
/** Names the entity in the messages */
|
|
12
|
+
singularName: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Enforces an entity schema on what a client sends: the entry of a creation
|
|
16
|
+
* and the parts of a change. Every refusal is a `BadRequestError`.
|
|
17
|
+
*
|
|
18
|
+
* See `docs/client-development/schemas/README.md` for the rules.
|
|
19
|
+
*/
|
|
20
|
+
export declare class EntityValidator {
|
|
21
|
+
private readonly schema;
|
|
22
|
+
private readonly deps;
|
|
23
|
+
constructor(schema: Schema | undefined, deps: EntityValidatorDeps);
|
|
24
|
+
/**
|
|
25
|
+
* Check a new entry, then convert it in place for the directory.
|
|
26
|
+
*
|
|
27
|
+
* @param entry entry as the client sent it, `objectClass` included
|
|
28
|
+
*/
|
|
29
|
+
validateNew(entry: AttributesList): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Check the parts of a change, then convert them in place. No value under
|
|
32
|
+
* a name is the empty list for the directory layer, which clears the
|
|
33
|
+
* attribute in a `replace` and refuses it in an `add`.
|
|
34
|
+
*
|
|
35
|
+
* @param changes change as the client sent it
|
|
36
|
+
*/
|
|
37
|
+
validateChanges(changes: ModifyRequest): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Check one value against its attribute's rules: its shape, then the
|
|
40
|
+
* entries a pointer names, then the pattern.
|
|
41
|
+
*
|
|
42
|
+
* @param field attribute name, as the client wrote it
|
|
43
|
+
* @param value value sent; no value clears the attribute
|
|
44
|
+
* @returns the value in the shape of the type: one value for a
|
|
45
|
+
* single-valued attribute, a list for an `array`
|
|
46
|
+
*/
|
|
47
|
+
validateValue(field: string, value: AttributeValue | null | undefined): Promise<AttributeValue | null | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* The schema's definition of an attribute, under any case.
|
|
50
|
+
*
|
|
51
|
+
* @throws BadRequestError for a name a strict schema does not declare
|
|
52
|
+
*/
|
|
53
|
+
private attribute;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Write every fixed attribute with a default as the schema says, whatever
|
|
57
|
+
* the request carried. `objectClass` is left to the entity's configuration.
|
|
58
|
+
* Run on the entry an authorization judges, so that it judges the entry
|
|
59
|
+
* written.
|
|
60
|
+
*
|
|
61
|
+
* @param schema entity schema, nothing to apply without one
|
|
62
|
+
* @param entry entry about to be created, changed in place
|
|
63
|
+
*/
|
|
64
|
+
export declare function applyFixedDefaults(schema: Schema | undefined, entry: AttributesList): void;
|
|
@@ -13,6 +13,25 @@ import type winston from 'winston';
|
|
|
13
13
|
export declare const setLogger: (logger: winston.Logger | undefined) => void;
|
|
14
14
|
/** @returns the installed logger, or undefined before any `DM` exists */
|
|
15
15
|
export declare const getLogger: () => winston.Logger | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Whether `serverError` names the failure in its answer: the `--debug` of
|
|
18
|
+
* the server, installed as its logger is.
|
|
19
|
+
*
|
|
20
|
+
* @param debug true to name the failure in the response
|
|
21
|
+
*/
|
|
22
|
+
export declare const setDebug: (debug: boolean) => void;
|
|
23
|
+
/**
|
|
24
|
+
* The body of a 5xx answer, for the error middleware of `DM` and
|
|
25
|
+
* `serverError` alike. The failure is for the logs: the message names it
|
|
26
|
+
* only under `--debug`.
|
|
27
|
+
*
|
|
28
|
+
* @param err what failed
|
|
29
|
+
* @param debug whether to name the failure; `serverError`'s setting by default
|
|
30
|
+
*/
|
|
31
|
+
export declare const serverErrorBody: (err: unknown, debug?: boolean) => {
|
|
32
|
+
error: string;
|
|
33
|
+
message: string;
|
|
34
|
+
};
|
|
16
35
|
export declare const tryMethod: (res: Response, method: Function, ...args: unknown[]) => Promise<void>;
|
|
17
36
|
export declare const tryMethodData: (res: Response, method: Function, ...args: unknown[]) => Promise<void>;
|
|
18
37
|
export declare const ok: (res: Response, data?: object) => void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import type { MaybePromise } from '../hooks';
|
|
3
|
+
/** What a plugin answers about the resources it depends on */
|
|
4
|
+
export type HealthStatus = 'ok' | 'degraded';
|
|
5
|
+
/** The paths of the two probes, served before authentication */
|
|
6
|
+
export declare const HEALTH_PATHS: readonly ["/healthz", "/readyz"];
|
|
7
|
+
/** How long one readiness answer serves */
|
|
8
|
+
export declare const PROBE_TTL_MS = 5000;
|
|
9
|
+
/**
|
|
10
|
+
* How long a readiness answer may take, the directory and every plugin
|
|
11
|
+
* asked at once: under the 3 s a kubelet waits, so that a hanging resource
|
|
12
|
+
* is reported, not timed out
|
|
13
|
+
*/
|
|
14
|
+
export declare const PROBE_TIMEOUT_MS = 2000;
|
|
15
|
+
/** What the readiness probe reads of the server */
|
|
16
|
+
export interface Probed {
|
|
17
|
+
/** Every plugin loaded and the composition checked */
|
|
18
|
+
readonly composed: boolean;
|
|
19
|
+
/** Told to stop: what is in flight finishes, nothing new is taken */
|
|
20
|
+
readonly stopping: boolean;
|
|
21
|
+
readonly ldap: {
|
|
22
|
+
ping: (timeoutMs: number) => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
readonly loadedPlugins: {
|
|
25
|
+
[name: string]: {
|
|
26
|
+
health?: () => MaybePromise<HealthStatus>;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface Readiness {
|
|
31
|
+
/** `starting`, `stopping` and `unavailable` answer 503, the two others 200 */
|
|
32
|
+
status: 'starting' | 'stopping' | 'ok' | 'degraded' | 'unavailable';
|
|
33
|
+
ldap?: 'ok' | 'failed';
|
|
34
|
+
/** The plugins implementing `health()`; `failed` when it threw or hung */
|
|
35
|
+
plugins?: {
|
|
36
|
+
[name: string]: HealthStatus | 'failed';
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Tell whether the server can serve: loaded, with a directory that answers.
|
|
41
|
+
* A plugin's resource being down degrades the answer without failing it,
|
|
42
|
+
* so that one broken outbound service does not take every replica out of
|
|
43
|
+
* rotation.
|
|
44
|
+
*
|
|
45
|
+
* One answer serves `PROBE_TTL_MS`, and the probes arriving while it is
|
|
46
|
+
* made share it: a probe every few seconds from every replica, or a flood
|
|
47
|
+
* on an unauthenticated route, must not become a load on the directory nor
|
|
48
|
+
* on what the plugins ask.
|
|
49
|
+
*
|
|
50
|
+
* @param server what is probed
|
|
51
|
+
* @param now the moment, for the answer's age
|
|
52
|
+
* @param timeoutMs how long the whole answer may take
|
|
53
|
+
*/
|
|
54
|
+
export declare function readiness(server: Probed, now?: number, timeoutMs?: number): Promise<Readiness>;
|
|
55
|
+
/**
|
|
56
|
+
* Serve `GET /healthz`, which answers as long as the process serves, and
|
|
57
|
+
* `GET /readyz`, which answers 200 when `readiness` says the server can
|
|
58
|
+
* serve. Both are outside the API prefix and carry no identity.
|
|
59
|
+
*
|
|
60
|
+
* @param app the application, before its authentication
|
|
61
|
+
* @param server what the readiness probe reads
|
|
62
|
+
*/
|
|
63
|
+
export declare function mountHealth(app: Express, server: Probed): void;
|
|
@@ -69,6 +69,8 @@ export interface InstanceProvider {
|
|
|
69
69
|
declare abstract class HttpProvider {
|
|
70
70
|
protected readonly domain: string;
|
|
71
71
|
protected readonly timeout: number;
|
|
72
|
+
/** The `target` label of this provider's calls in the outbound metrics */
|
|
73
|
+
protected abstract readonly target: string;
|
|
72
74
|
constructor(domain: string, timeout: number);
|
|
73
75
|
address(request: InstanceRequest): string;
|
|
74
76
|
protected call(url: string, init?: RequestInit): Promise<Response>;
|
|
@@ -79,6 +81,7 @@ export declare class ClouderyProvider extends HttpProvider implements InstancePr
|
|
|
79
81
|
private readonly url;
|
|
80
82
|
private readonly token;
|
|
81
83
|
private readonly offer;
|
|
84
|
+
protected readonly target = "cloudery";
|
|
82
85
|
constructor(url: string, token: string, domain: string, offer: string, timeout: number);
|
|
83
86
|
private headers;
|
|
84
87
|
create(request: InstanceRequest): Promise<undefined>;
|
|
@@ -97,6 +100,7 @@ export declare class CozyStackProvider extends HttpProvider implements InstanceP
|
|
|
97
100
|
private readonly url;
|
|
98
101
|
private readonly context;
|
|
99
102
|
private readonly apps;
|
|
103
|
+
protected readonly target = "cozy-stack";
|
|
100
104
|
private readonly auth;
|
|
101
105
|
constructor(url: string, user: string, passphrase: string, domain: string, context: string, apps: string, timeout: number);
|
|
102
106
|
private headers;
|
|
@@ -126,11 +126,39 @@ declare class ldapActions {
|
|
|
126
126
|
* Uses a flag to prevent concurrent cleanup operations
|
|
127
127
|
*/
|
|
128
128
|
private cleanupExpiredConnections;
|
|
129
|
+
/**
|
|
130
|
+
* Ask the directory for its root DSE on a connection of its own, bound
|
|
131
|
+
* as the server and closed after: the readiness probe. Neither the pool
|
|
132
|
+
* nor the search cache nor the hooks answer for a directory that does
|
|
133
|
+
* not, and a directory that stops answering holds nothing of the pool.
|
|
134
|
+
* The URLs are tried as `createConnection` tries them.
|
|
135
|
+
*
|
|
136
|
+
* @param timeoutMs how long the connection, the bind and the search may
|
|
137
|
+
* each take; the client closes the socket when it passes
|
|
138
|
+
* @throws when no URL answers in time
|
|
139
|
+
*/
|
|
140
|
+
ping(timeoutMs: number): Promise<void>;
|
|
141
|
+
/** Set by `close`: no connection is handed out any more */
|
|
142
|
+
private closed;
|
|
143
|
+
/**
|
|
144
|
+
* Close every pooled connection, the ones in use included: an operation
|
|
145
|
+
* still running on one is rejected by the client, and a waiter is refused
|
|
146
|
+
* rather than kept.
|
|
147
|
+
*/
|
|
148
|
+
close(): Promise<void>;
|
|
129
149
|
/**
|
|
130
150
|
* Acquire a connection from the pool or create a new one
|
|
131
151
|
* Optimized for O(1) lookup using separate available connections queue
|
|
132
152
|
*/
|
|
133
153
|
private acquireConnection;
|
|
154
|
+
/** The pool as it is, for the metrics */
|
|
155
|
+
poolStats(): {
|
|
156
|
+
busy: number;
|
|
157
|
+
idle: number;
|
|
158
|
+
waiting: number;
|
|
159
|
+
max: number;
|
|
160
|
+
};
|
|
161
|
+
private takeConnection;
|
|
134
162
|
/**
|
|
135
163
|
* Release a connection back to the pool
|
|
136
164
|
* If there are waiting requests, hand off the connection directly
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the server measures of itself, for `core/metrics` to expose; see
|
|
3
|
+
* docs/usage/operations.md#metrics
|
|
4
|
+
* @module lib/metrics
|
|
5
|
+
*/
|
|
6
|
+
import type { Request } from 'express';
|
|
7
|
+
import { Counter, Histogram, Registry } from 'prom-client';
|
|
8
|
+
/** The instruments below register here; `core/metrics` adds its own */
|
|
9
|
+
export declare const registry: Registry<"text/plain; version=0.0.4; charset=utf-8">;
|
|
10
|
+
export declare const httpRequestDuration: Histogram<"status" | "method" | "route">;
|
|
11
|
+
export type LdapOperation = 'bind' | 'search' | 'add' | 'modify' | 'rename' | 'delete';
|
|
12
|
+
export declare const ldapOperationDuration: Histogram<"operation" | "result">;
|
|
13
|
+
export declare const ldapPoolWait: Histogram<string>;
|
|
14
|
+
export declare const ldapSearchCache: Counter<"result">;
|
|
15
|
+
export declare const hookFailures: Counter<"plugin" | "hook">;
|
|
16
|
+
export declare const outboundRequestDuration: Histogram<"status" | "target">;
|
|
17
|
+
export declare const unhandledRejections: Counter<string>;
|
|
18
|
+
/**
|
|
19
|
+
* The `result` label of a directory call that threw: a refusal is the
|
|
20
|
+
* directory answering, by its result code (32 for an entry that is not
|
|
21
|
+
* there); `error` is for not answering.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ldapResult: (err: unknown) => string;
|
|
24
|
+
/** @returns the seconds since `started`, a `process.hrtime.bigint()` */
|
|
25
|
+
export declare const secondsSince: (started: bigint) => number;
|
|
26
|
+
/**
|
|
27
|
+
* Time a directory call into the operation histogram, whatever becomes of
|
|
28
|
+
* it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function timedLdap<T>(operation: LdapOperation, call: () => Promise<T>): Promise<T>;
|
|
31
|
+
/**
|
|
32
|
+
* Time a call to another service into the outbound histogram, to its
|
|
33
|
+
* answer's headers: the body is read by the caller, at its own pace.
|
|
34
|
+
*/
|
|
35
|
+
export declare function timedOutbound<T extends {
|
|
36
|
+
status: number;
|
|
37
|
+
}>(target: string, call: () => Promise<T>): Promise<T>;
|
|
38
|
+
/**
|
|
39
|
+
* The template of the route that answered, never the URL: a label per URL
|
|
40
|
+
* would grow without bound. See docs/usage/operations.md#metrics.
|
|
41
|
+
*
|
|
42
|
+
* @returns the template, `none` when no route matched
|
|
43
|
+
*/
|
|
44
|
+
export declare const routeTemplate: (req: Request) => string;
|
|
@@ -21,3 +21,43 @@ export declare function outsideRequest<T>(fn: () => T): T;
|
|
|
21
21
|
* @returns the request, or undefined outside any, or as the server
|
|
22
22
|
*/
|
|
23
23
|
export declare function currentRequest(): Request | undefined;
|
|
24
|
+
/** What a log line carries beside what it says */
|
|
25
|
+
export interface LogContext {
|
|
26
|
+
/** The request being served */
|
|
27
|
+
requestId?: string;
|
|
28
|
+
/** The plugin whose hook is running */
|
|
29
|
+
plugin?: string;
|
|
30
|
+
/** That hook */
|
|
31
|
+
op?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Run something that outlives the request being served, a connection
|
|
35
|
+
* opened for the pool or a client kept for the process: its operations are
|
|
36
|
+
* judged by nobody, and what it logs later is nobody's request.
|
|
37
|
+
*
|
|
38
|
+
* @param fn what to run
|
|
39
|
+
* @returns what it returns
|
|
40
|
+
*/
|
|
41
|
+
export declare function outsideAnyRequest<T>(fn: () => T): T;
|
|
42
|
+
/**
|
|
43
|
+
* Serve a request with its log context: what runs inside, synchronously or
|
|
44
|
+
* not, logs with it.
|
|
45
|
+
*
|
|
46
|
+
* @param context what the lines carry
|
|
47
|
+
* @param fn what serves the request
|
|
48
|
+
* @returns what `fn` returns
|
|
49
|
+
*/
|
|
50
|
+
export declare function runWithLogContext<T>(context: LogContext, fn: () => T): T;
|
|
51
|
+
/**
|
|
52
|
+
* Run a hook: what it logs names its plugin and itself, under the request
|
|
53
|
+
* being served; a hook no plugin registered names none, even inside
|
|
54
|
+
* another's.
|
|
55
|
+
*
|
|
56
|
+
* @param plugin the plugin's instance name, none for an unregistered hook
|
|
57
|
+
* @param op the hook
|
|
58
|
+
* @param fn the hook's run
|
|
59
|
+
* @returns what `fn` returns
|
|
60
|
+
*/
|
|
61
|
+
export declare function runAsPlugin<T>(plugin: string | undefined, op: string | undefined, fn: () => T): T;
|
|
62
|
+
/** The log context of what is running, if any: request, plugin, hook */
|
|
63
|
+
export declare function currentLogContext(): LogContext | undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ShutdownCallback = () => void | Promise<void>;
|
|
2
|
+
/**
|
|
3
|
+
* How long the shutdown callbacks get before the signal is raised again.
|
|
4
|
+
* The binary sets it from `--shutdown-timeout`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const shutdownTimeout: {
|
|
7
|
+
ms: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Run a callback when the process is told to stop. The first registration
|
|
11
|
+
* installs the signal listeners; a callback registered while the shutdown
|
|
12
|
+
* runs is awaited with the others.
|
|
13
|
+
*
|
|
14
|
+
* Once every callback has settled, or the timeout has passed, the signal is
|
|
15
|
+
* raised again for its default action, unless another listener is there to
|
|
16
|
+
* decide.
|
|
17
|
+
*
|
|
18
|
+
* @param cb what to run, awaited when it answers a promise
|
|
19
|
+
*/
|
|
20
|
+
export declare function registerProcessShutdown(cb: ShutdownCallback): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the static plugins serve a schema file, for another plugin to
|
|
3
|
+
* announce it
|
|
4
|
+
* @module lib/staticFiles
|
|
5
|
+
*/
|
|
6
|
+
import type { DM } from '../bin';
|
|
7
|
+
/** A static plugin, as another plugin asks it where it serves a file */
|
|
8
|
+
export interface StaticFiles {
|
|
9
|
+
schemaUrlOf(file: string): string | undefined;
|
|
10
|
+
servedPath(): string;
|
|
11
|
+
}
|
|
12
|
+
/** Told by its methods, not its class: another copy of ldap-rest has its own */
|
|
13
|
+
export declare const isStaticFiles: (plugin: unknown) => plugin is StaticFiles;
|
|
14
|
+
/**
|
|
15
|
+
* The static plugins loaded, whatever their class or name: one loaded under
|
|
16
|
+
* another name, or with a name or a directory of its own, serves files
|
|
17
|
+
* elsewhere or not at all. The one registered as `static` comes first; one
|
|
18
|
+
* whose path an earlier plugin took, lies under it or holds it, is left
|
|
19
|
+
* out, the earlier one's routes answering first.
|
|
20
|
+
*
|
|
21
|
+
* @param server the server whose plugins are asked
|
|
22
|
+
* @returns the plugins, in the order to ask them
|
|
23
|
+
*/
|
|
24
|
+
export declare function staticPlugins(server: Pick<DM, 'loadedPlugins'>): StaticFiles[];
|
|
25
|
+
/**
|
|
26
|
+
* URL a static plugin serves a schema file at, merged, asked of
|
|
27
|
+
* {@link staticPlugins} in their order
|
|
28
|
+
*
|
|
29
|
+
* @param server the server whose plugins are asked
|
|
30
|
+
* @param file the file, as the plugin announcing it reads it
|
|
31
|
+
* @returns its URL, or undefined when no static plugin serves it merged
|
|
32
|
+
*/
|
|
33
|
+
export declare function staticSchemaUrl(server: Pick<DM, 'loadedPlugins'>, file: string): string | undefined;
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* @group Libraries
|
|
27
27
|
*/
|
|
28
28
|
import type winston from 'winston';
|
|
29
|
+
import type { HealthStatus } from '../health';
|
|
29
30
|
/** What a backend keeps: a value, and when it stops counting. */
|
|
30
31
|
export interface StoredRecord {
|
|
31
32
|
value: string;
|
|
@@ -45,6 +46,11 @@ export default abstract class Store {
|
|
|
45
46
|
protected abstract drop(key: string): Promise<void>;
|
|
46
47
|
/** Drop every expired record. Returns how many went. */
|
|
47
48
|
abstract sweep(): Promise<number>;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the backend answers, asked by the readiness probe; a backend
|
|
51
|
+
* with a resource of its own asks it and throws when it does not answer.
|
|
52
|
+
*/
|
|
53
|
+
health(): Promise<HealthStatus>;
|
|
48
54
|
/**
|
|
49
55
|
* The key a backend actually sees, namespaced so two consumers cannot
|
|
50
56
|
* collide.
|
|
@@ -72,6 +78,8 @@ export default abstract class Store {
|
|
|
72
78
|
* operator reads it, instead of the first request that needs the store.
|
|
73
79
|
*/
|
|
74
80
|
open(): Promise<void>;
|
|
81
|
+
/** Set by `close`: a backend opens nothing past it */
|
|
82
|
+
protected closed: boolean;
|
|
75
83
|
/** Stop sweeping and let go of any connection. */
|
|
76
84
|
close(): Promise<void>;
|
|
77
85
|
/**
|
package/dist/src/lib/utils.d.ts
CHANGED
|
@@ -22,6 +22,15 @@ export declare function getCompiledRegex(pattern: string, flags?: string): RegEx
|
|
|
22
22
|
* @returns The escaped string safe for use in RegExp
|
|
23
23
|
*/
|
|
24
24
|
export declare function escapeRegex(str: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Run a hook under its plugin's name and its own, for the lines it logs:
|
|
27
|
+
* what `launchHooks` does, for a caller walking the hooks itself.
|
|
28
|
+
*
|
|
29
|
+
* @param hook the registered function
|
|
30
|
+
* @param fn its run
|
|
31
|
+
* @returns what `fn` returns
|
|
32
|
+
*/
|
|
33
|
+
export declare const runAsHookOwner: <T>(hook: Function, fn: () => T) => T;
|
|
25
34
|
export declare const launchHooks: (hooks: Function[] | undefined, ...args: unknown[]) => Promise<void>;
|
|
26
35
|
/**
|
|
27
36
|
* Record the plugin a hook function belongs to.
|
|
@@ -5,4 +5,8 @@ declare module 'winston' {
|
|
|
5
5
|
notice: winston.LeveledLogMethod;
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* @throws when `--logger` names no known logger, or `--logger file` has no
|
|
10
|
+
* file: never a file nobody asked for
|
|
11
|
+
*/
|
|
8
12
|
export declare const buildLogger: (config: Config) => winston.Logger;
|
|
@@ -14,6 +14,7 @@ import type { Hooks } from '../../hooks';
|
|
|
14
14
|
import type ldapActions from '../../lib/ldapActions';
|
|
15
15
|
import type { AttributesList, AttributeValue, LdapList, ModifyRequest } from '../../lib/ldapActions';
|
|
16
16
|
import type { Schema } from '../../config/schema';
|
|
17
|
+
import { EntityValidator } from '../../lib/entitySchema/validator';
|
|
17
18
|
export interface postAdd {
|
|
18
19
|
cn?: string;
|
|
19
20
|
[key: string]: AttributeValue | undefined;
|
|
@@ -308,9 +309,23 @@ export default class LdapGroups extends DmPlugin {
|
|
|
308
309
|
* @returns nothing, throw if error
|
|
309
310
|
*/
|
|
310
311
|
validateMembers(dn: string, members: string[], req?: Request): Promise<void>;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
/**
|
|
313
|
+
* Check a new group's entry, after the `ldapgroupvalidatenew` hooks, and
|
|
314
|
+
* convert it in place.
|
|
315
|
+
*
|
|
316
|
+
* @returns the entry the hooks handed on; `addGroup` writes its
|
|
317
|
+
* attributes over the entry it judged, the members aside
|
|
318
|
+
*/
|
|
319
|
+
validateNewGroup(dn: string, entry: AttributesList): Promise<AttributesList>;
|
|
320
|
+
/**
|
|
321
|
+
* Check a change, after the `ldapgroupvalidatechanges` hooks, and convert
|
|
322
|
+
* it in place.
|
|
323
|
+
*
|
|
324
|
+
* @returns the change the hooks handed on
|
|
325
|
+
*/
|
|
326
|
+
validateChanges(dn: string, changes: ModifyRequest): Promise<ModifyRequest>;
|
|
327
|
+
/** The schema's rules; the pointer lookups run as the server */
|
|
328
|
+
validator(): EntityValidator;
|
|
314
329
|
/**
|
|
315
330
|
* Provide configuration for config API
|
|
316
331
|
*/
|
|
@@ -2,7 +2,7 @@ import type { Express, Request, Response } from 'express';
|
|
|
2
2
|
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
3
3
|
import { DM } from '../../bin';
|
|
4
4
|
import { Hooks } from '../../hooks';
|
|
5
|
-
import { AttributesList,
|
|
5
|
+
import { AttributesList, ModifyRequest } from '../../lib/ldapActions';
|
|
6
6
|
import type { Schema } from '../../config/schema';
|
|
7
7
|
/**
|
|
8
8
|
* Shared OpenAPI schemas surfaced by this plugin. Picked up by
|
|
@@ -231,10 +231,8 @@ export default class LdapOrganizations extends DmPlugin {
|
|
|
231
231
|
private linkedEntities;
|
|
232
232
|
addOrganization(dn: string, entry: AttributesList, req?: Request): Promise<boolean>;
|
|
233
233
|
modifyOrganization(dn: string, changes: ModifyRequest, req?: Request): Promise<boolean>;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
validateChanges(dn: string, changes: ModifyRequest): Promise<boolean>;
|
|
237
|
-
_validateOneChange(field: string, value: AttributeValue | null): Promise<boolean>;
|
|
234
|
+
/** The schema's rules; the pointer lookups run as the server */
|
|
235
|
+
private validator;
|
|
238
236
|
/**
|
|
239
237
|
* Tell whether a DN names an existing entry.
|
|
240
238
|
*
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import DmPlugin from '../abstract/plugin';
|
|
3
|
+
import type { DM } from '../bin';
|
|
4
|
+
export default class Metrics extends DmPlugin {
|
|
5
|
+
name: string;
|
|
6
|
+
/** The gauges read at scrape time and the process metrics */
|
|
7
|
+
private readonly own;
|
|
8
|
+
private readonly exposed;
|
|
9
|
+
private listener?;
|
|
10
|
+
constructor(server: DM);
|
|
11
|
+
/** Open the listener; the server does not start on a port it cannot take */
|
|
12
|
+
api(_app: Express): Promise<void>;
|
|
13
|
+
/** @returns the port the metrics are served on, once `api` opened it */
|
|
14
|
+
port(): number | undefined;
|
|
15
|
+
private answer;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import DmPlugin from '../abstract/plugin';
|
|
21
21
|
import type { DM } from '../bin';
|
|
22
|
+
import type { HealthStatus } from '../lib/health';
|
|
22
23
|
/** JSON-serialisable message payload. */
|
|
23
24
|
export type RabbitMessage = Record<string, unknown>;
|
|
24
25
|
/** Async handler invoked for each consumed message. */
|
|
@@ -46,6 +47,18 @@ interface RabbitClient {
|
|
|
46
47
|
subscribe(exchange: string, routingKey: string, queue: string, handler: RabbitMessageHandler, options?: SubscribeOptions): Promise<void>;
|
|
47
48
|
unsubscribe(queue: string): Promise<void>;
|
|
48
49
|
close(clearSubscriptions?: boolean): Promise<void>;
|
|
50
|
+
isConnected(): boolean;
|
|
51
|
+
}
|
|
52
|
+
/** The logging contract of `@linagora/rabbitmq-client`: message first, then a context */
|
|
53
|
+
interface ClientLogger {
|
|
54
|
+
debug(message: string, context?: unknown): void;
|
|
55
|
+
info(message: string, context?: unknown): void;
|
|
56
|
+
warn(message: string, context?: unknown): void;
|
|
57
|
+
error(message: string, context?: unknown): void;
|
|
58
|
+
}
|
|
59
|
+
interface RabbitClientOptions {
|
|
60
|
+
url: string;
|
|
61
|
+
logger: ClientLogger;
|
|
49
62
|
}
|
|
50
63
|
export default class RabbitMq extends DmPlugin {
|
|
51
64
|
name: string;
|
|
@@ -59,6 +72,13 @@ export default class RabbitMq extends DmPlugin {
|
|
|
59
72
|
* the broker is reachable — the connection is still lazy.
|
|
60
73
|
*/
|
|
61
74
|
isAvailable(): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Degraded when a broker is configured and no connection to it stands:
|
|
77
|
+
* the state as it is, nothing opened for the probe, which would run the
|
|
78
|
+
* client's connection attempts on every probe while the broker is away.
|
|
79
|
+
* A plugin without a broker has nothing to report.
|
|
80
|
+
*/
|
|
81
|
+
health(): HealthStatus;
|
|
62
82
|
/**
|
|
63
83
|
* Publish a JSON message to a topic exchange. No-op (returns silently) when
|
|
64
84
|
* no broker is configured or the client is unavailable.
|
|
@@ -89,6 +109,22 @@ export default class RabbitMq extends DmPlugin {
|
|
|
89
109
|
* retry policy in `getClient()` can be tested without a live broker.
|
|
90
110
|
*/
|
|
91
111
|
protected connect(): Promise<RabbitClient>;
|
|
92
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Build and initialise a client of the given class: a test hands in a stub
|
|
114
|
+
* to see what the client is built with.
|
|
115
|
+
*/
|
|
116
|
+
protected open(Client: new (options: RabbitClientOptions) => unknown): Promise<RabbitClient>;
|
|
117
|
+
/**
|
|
118
|
+
* The client's lines go through the plugin's logger, under its name, so
|
|
119
|
+
* they follow `--logger` and `--log-level`; without one the client writes
|
|
120
|
+
* to the console. An `Error` in the context is written as text: the JSON
|
|
121
|
+
* format keeps only its enumerable fields, which for a channel error is
|
|
122
|
+
* the code without the broker's message.
|
|
123
|
+
*/
|
|
124
|
+
private clientLogger;
|
|
125
|
+
/** Set by `close`: a publish of what outlives the drain opens nothing */
|
|
126
|
+
private closed;
|
|
127
|
+
/** Close the broker connection; nothing opens one after */
|
|
128
|
+
close(): Promise<void>;
|
|
93
129
|
}
|
|
94
130
|
export {};
|
|
@@ -65,6 +65,18 @@ export default class Static extends DmPlugin {
|
|
|
65
65
|
* @returns its URL, or undefined when this plugin does not serve it
|
|
66
66
|
*/
|
|
67
67
|
urlOf(file: string): string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* The URL its schema routes serve a schema file at, merged with what it
|
|
70
|
+
* extends and its placeholders replaced, for another plugin to announce
|
|
71
|
+
* it: one of the configuration's schema files it serves, or a file its
|
|
72
|
+
* `schemas/` directory reaches, through a link included. One that
|
|
73
|
+
* express.static would send as it stands, outside `schemas/` or not named
|
|
74
|
+
* `.json`, has none.
|
|
75
|
+
*
|
|
76
|
+
* @param file the schema file, as the other plugin reads it
|
|
77
|
+
* @returns its URL, or undefined when its schema routes do not serve it
|
|
78
|
+
*/
|
|
79
|
+
schemaUrlOf(file: string): string | undefined;
|
|
68
80
|
/** The path it serves under, which an earlier plugin may have taken */
|
|
69
81
|
servedPath(): string;
|
|
70
82
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type winston from 'winston';
|
|
2
2
|
import Store, { type StoredRecord } from '../../lib/storage/store';
|
|
3
|
+
import type { HealthStatus } from '../../lib/health';
|
|
3
4
|
export default class FileStore extends Store {
|
|
4
5
|
name: string;
|
|
5
6
|
private dir;
|
|
@@ -8,6 +9,9 @@ export default class FileStore extends Store {
|
|
|
8
9
|
constructor(logger: winston.Logger, dir: string, abandonedAfterSeconds: number);
|
|
9
10
|
/** The directory is created on demand, readable by this user alone. */
|
|
10
11
|
ready(): Promise<void>;
|
|
12
|
+
open(): Promise<void>;
|
|
13
|
+
/** A directory this user may write a record in: entered and written */
|
|
14
|
+
health(): Promise<HealthStatus>;
|
|
11
15
|
private path;
|
|
12
16
|
/** `<deadline> <key>\n<value>` — the deadline first, so a short read still has it. */
|
|
13
17
|
private static encode;
|
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module plugins/storage/postgres
|
|
3
|
-
*
|
|
4
|
-
* A keyed store kept in one PostgreSQL table.
|
|
5
|
-
*
|
|
6
|
-
* `pg` is an optional dependency, loaded when this backend is chosen: a
|
|
7
|
-
* deployment that keeps its records elsewhere never needs it installed.
|
|
8
|
-
*
|
|
9
|
-
* The key is kept verbatim, split into the consumer's namespace and its own
|
|
10
|
-
* key: a `text` column has no reason to hash it the way a DN or a file name
|
|
11
|
-
* must, so a `SELECT` says what a record is about.
|
|
12
|
-
*
|
|
13
|
-
* @group Plugins
|
|
14
|
-
*/
|
|
15
1
|
import type winston from 'winston';
|
|
16
2
|
import Store, { type StoredRecord } from '../../lib/storage/store';
|
|
3
|
+
import { type HealthStatus } from '../../lib/health';
|
|
17
4
|
type Pg = typeof import('pg');
|
|
18
5
|
export default class PostgresStore extends Store {
|
|
19
6
|
name: string;
|
|
@@ -26,6 +13,12 @@ export default class PostgresStore extends Store {
|
|
|
26
13
|
/** Split out so a test can stand in for a missing package. */
|
|
27
14
|
protected driver(): Promise<Pg>;
|
|
28
15
|
open(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* On a client of its own, closed after, with the budget as its timeouts:
|
|
18
|
+
* a query the pool never got an answer to would hold one of its
|
|
19
|
+
* connections for as long as the socket lives
|
|
20
|
+
*/
|
|
21
|
+
health(): Promise<HealthStatus>;
|
|
29
22
|
close(): Promise<void>;
|
|
30
23
|
/**
|
|
31
24
|
* The pool, once the table is known to be there. A failure is not kept:
|
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module plugins/storage/valkey
|
|
3
|
-
*
|
|
4
|
-
* A keyed store kept in Valkey, or Redis.
|
|
5
|
-
*
|
|
6
|
-
* `iovalkey` is an optional dependency, loaded when this backend is chosen: a
|
|
7
|
-
* deployment that keeps its records elsewhere never needs it installed.
|
|
8
|
-
*
|
|
9
|
-
* The only backend that expires its own records, so it has nothing to sweep.
|
|
10
|
-
* The value still carries its deadline, in the shape the LDAP store uses:
|
|
11
|
-
* reads judge expiry by it, as with every other backend, and the TTL only
|
|
12
|
-
* reclaims the memory.
|
|
13
|
-
*
|
|
14
|
-
* @group Plugins
|
|
15
|
-
*/
|
|
16
1
|
import type winston from 'winston';
|
|
17
2
|
import Store, { type StoredRecord } from '../../lib/storage/store';
|
|
3
|
+
import type { HealthStatus } from '../../lib/health';
|
|
18
4
|
type IoValkey = typeof import('iovalkey');
|
|
19
5
|
export default class ValkeyStore extends Store {
|
|
20
6
|
name: string;
|
|
@@ -27,6 +13,7 @@ export default class ValkeyStore extends Store {
|
|
|
27
13
|
/** Split out so a test can stand in for a missing package. */
|
|
28
14
|
protected driver(): Promise<IoValkey>;
|
|
29
15
|
open(): Promise<void>;
|
|
16
|
+
health(): Promise<HealthStatus>;
|
|
30
17
|
close(): Promise<void>;
|
|
31
18
|
/**
|
|
32
19
|
* The client, once connected. A failed first connection is not kept: the
|