ldap-rest 1.1.0 → 1.2.1

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 (115) hide show
  1. package/bin/index.mjs +8 -7
  2. package/dist/abstract/ldapFlat.js +1 -1
  3. package/dist/abstract/plugin.js +1 -1
  4. package/dist/abstract/twakePlugin.js +1 -1
  5. package/dist/bin/index.js +1 -1
  6. package/dist/config/args.js +1 -1
  7. package/dist/config/schema.js +1 -1
  8. package/dist/lib/changeContext.js +1 -1
  9. package/dist/lib/entitySchema/validator.js +1 -0
  10. package/dist/lib/expressFormatedResponses.js +1 -1
  11. package/dist/lib/health.js +1 -0
  12. package/dist/lib/instanceProviders.js +1 -1
  13. package/dist/lib/ldapActions.js +1 -1
  14. package/dist/lib/ldapSchema.js +1 -1
  15. package/dist/lib/metrics.js +1 -0
  16. package/dist/lib/requestContext.js +1 -1
  17. package/dist/lib/shutdown.js +1 -0
  18. package/dist/lib/staticFiles.js +1 -0
  19. package/dist/lib/storage/store.js +1 -1
  20. package/dist/lib/utils.js +1 -1
  21. package/dist/logger/winston.js +1 -1
  22. package/dist/plugins/auth/crowdsec.js +1 -1
  23. package/dist/plugins/auth/openidconnect.js +1 -1
  24. package/dist/plugins/demo/helloworld.js +1 -1
  25. package/dist/plugins/ldap/bulkImport.js +1 -1
  26. package/dist/plugins/ldap/departmentSync.js +1 -1
  27. package/dist/plugins/ldap/flatGeneric.js +1 -1
  28. package/dist/plugins/ldap/groups.js +1 -1
  29. package/dist/plugins/ldap/organizations.js +1 -1
  30. package/dist/plugins/metrics.js +1 -0
  31. package/dist/plugins/rabbitmq.js +1 -1
  32. package/dist/plugins/scim/baseResolver.js +1 -1
  33. package/dist/plugins/scim/bulk.js +1 -1
  34. package/dist/plugins/scim/errors.js +1 -1
  35. package/dist/plugins/scim/filter.js +1 -1
  36. package/dist/plugins/scim/groups.js +1 -1
  37. package/dist/plugins/scim/mapping.js +1 -1
  38. package/dist/plugins/scim/patch.js +1 -1
  39. package/dist/plugins/scim/projection.js +1 -1
  40. package/dist/plugins/scim/scim.js +1 -1
  41. package/dist/plugins/scim/users.js +1 -1
  42. package/dist/plugins/static.js +1 -1
  43. package/dist/plugins/storage/file.js +1 -1
  44. package/dist/plugins/storage/postgres.js +1 -1
  45. package/dist/plugins/storage/valkey.js +1 -1
  46. package/dist/plugins/storage.js +1 -1
  47. package/dist/plugins/twake/calendar.js +1 -1
  48. package/dist/plugins/twake/clouderyProvision.js +1 -1
  49. package/dist/plugins/twake/cozyProvision.js +1 -1
  50. package/dist/plugins/twake/drive.js +1 -1
  51. package/dist/plugins/twake/groups.js +1 -1
  52. package/dist/plugins/twake/james.js +1 -1
  53. package/dist/plugins/twake/migrationProxy.js +1 -0
  54. package/dist/plugins/twake/spaces.js +1 -1
  55. package/dist/plugins/weblogs.js +1 -1
  56. package/dist/src/abstract/ldapFlat.d.ts +3 -13
  57. package/dist/src/abstract/plugin.d.ts +20 -0
  58. package/dist/src/abstract/twakePlugin.d.ts +23 -1
  59. package/dist/src/bin/index.d.ts +46 -1
  60. package/dist/src/config/args.d.ts +12 -1
  61. package/dist/src/lib/changeContext.d.ts +11 -0
  62. package/dist/src/lib/entitySchema/validator.d.ts +64 -0
  63. package/dist/src/lib/expressFormatedResponses.d.ts +19 -0
  64. package/dist/src/lib/health.d.ts +63 -0
  65. package/dist/src/lib/instanceProviders.d.ts +4 -0
  66. package/dist/src/lib/ldapActions.d.ts +32 -2
  67. package/dist/src/lib/metrics.d.ts +44 -0
  68. package/dist/src/lib/requestContext.d.ts +40 -0
  69. package/dist/src/lib/shutdown.d.ts +20 -0
  70. package/dist/src/lib/staticFiles.d.ts +33 -0
  71. package/dist/src/lib/storage/store.d.ts +8 -0
  72. package/dist/src/lib/utils.d.ts +15 -0
  73. package/dist/src/logger/winston.d.ts +4 -0
  74. package/dist/src/plugins/ldap/groups.d.ts +18 -3
  75. package/dist/src/plugins/ldap/organizations.d.ts +3 -5
  76. package/dist/src/plugins/metrics.d.ts +17 -0
  77. package/dist/src/plugins/rabbitmq.d.ts +37 -1
  78. package/dist/src/plugins/scim/errors.d.ts +2 -0
  79. package/dist/src/plugins/scim/types.d.ts +2 -1
  80. package/dist/src/plugins/static.d.ts +12 -0
  81. package/dist/src/plugins/storage/file.d.ts +4 -0
  82. package/dist/src/plugins/storage/postgres.d.ts +7 -14
  83. package/dist/src/plugins/storage/valkey.d.ts +2 -15
  84. package/dist/src/plugins/storage.d.ts +8 -0
  85. package/dist/src/plugins/twake/drive.d.ts +8 -0
  86. package/dist/src/plugins/twake/groups.d.ts +1 -2
  87. package/dist/src/plugins/twake/james.d.ts +2 -0
  88. package/dist/src/plugins/twake/migrationProxy.d.ts +71 -0
  89. package/docs/client-development/api/reference.md +5 -9
  90. package/docs/client-development/api/rest-api.md +4 -10
  91. package/docs/client-development/schemas/README.md +60 -21
  92. package/docs/plugin-development/README.md +15 -13
  93. package/docs/plugin-development/dependencies.md +8 -0
  94. package/docs/plugin-development/hooks.md +1 -1
  95. package/docs/usage/README.md +1 -0
  96. package/docs/usage/configuration.md +62 -24
  97. package/docs/usage/operations.md +193 -0
  98. package/docs/usage/plugins/README.md +2 -0
  99. package/docs/usage/plugins/auth/authz-per-branch.md +2 -1
  100. package/docs/usage/plugins/integrations/README.md +8 -4
  101. package/docs/usage/plugins/integrations/groups.md +2 -2
  102. package/docs/usage/plugins/integrations/mail-migration.md +102 -0
  103. package/docs/usage/plugins/integrations/scim.md +16 -3
  104. package/docs/usage/plugins/integrations/spaces.md +1 -1
  105. package/docs/usage/plugins/ldap/bulk-import.md +3 -1
  106. package/docs/usage/plugins/ldap/flat-generic.md +5 -2
  107. package/docs/usage/plugins/ldap/on-change.md +2 -1
  108. package/docs/usage/plugins/ldap/organizations.md +7 -7
  109. package/docs/usage/plugins/utilities/metrics.md +21 -0
  110. package/docs/usage/plugins/utilities/static.md +16 -8
  111. package/docs/usage/plugins/utilities/storage.md +7 -0
  112. package/docs/usage/plugins/utilities/weblogs.md +51 -30
  113. package/docs/usage/upgrading.md +84 -0
  114. package/package.json +11 -2
  115. package/static/schemas/example/README.md +1 -0
@@ -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
- private registerShutdown;
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 {};
@@ -14,6 +14,8 @@ export declare class ScimError extends HttpError {
14
14
  scimType?: ScimErrorType;
15
15
  constructor(status: number, detail: string, scimType?: ScimErrorType);
16
16
  }
17
+ /** The kind of a client value, for an error that quotes none of it */
18
+ export declare const kindOf: (v: unknown) => string;
17
19
  export declare const scimInvalidFilter: (detail?: string) => ScimError;
18
20
  export declare const scimInvalidPath: (detail?: string) => ScimError;
19
21
  export declare const scimInvalidValue: (detail?: string) => ScimError;
@@ -120,7 +120,8 @@ export interface BulkRequest {
120
120
  Operations: BulkOperationRequest[];
121
121
  }
122
122
  export interface BulkOperationResponse {
123
- method: 'POST' | 'PUT' | 'PATCH' | 'DELETE';
123
+ /** Absent when the operation was not even an object */
124
+ method?: 'POST' | 'PUT' | 'PATCH' | 'DELETE';
124
125
  bulkId?: string;
125
126
  version?: string;
126
127
  location?: string;
@@ -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
@@ -30,10 +30,18 @@
30
30
  import DmPlugin from '../abstract/plugin';
31
31
  import type { DM } from '../bin';
32
32
  import type Store from '../lib/storage/store';
33
+ import type { HealthStatus } from '../lib/health';
33
34
  export default class Storage extends DmPlugin {
34
35
  name: string;
35
36
  store: Store;
36
37
  constructor(server: DM);
38
+ close(): Promise<void>;
39
+ /** Set while the backend fails the probe, so the reason is logged once */
40
+ private degraded;
41
+ /** Counts the probes: only the latest says what the log lines say */
42
+ private probes;
43
+ /** The backend's own answer; a backend that cannot answer is degraded */
44
+ health(): Promise<HealthStatus>;
37
45
  /**
38
46
  * Release the single-instance hold.
39
47
  *
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Twake Drive Plugin
3
+ *
4
+ * Synchronizes LDAP user changes with Twake Drive via Admin API.
5
+ * Propagates email address, display name, and disk quota changes.
6
+ *
7
+ * @see https://docs.cozy.io/en/cozy-stack/admin/
8
+ */
1
9
  import TwakePlugin from '../../abstract/twakePlugin';
2
10
  import { type Role } from '../../abstract/plugin';
3
11
  import type { DM } from '../../bin';
@@ -11,6 +11,7 @@ import { type LifecycleAttributes } from './lifecycleAttributes';
11
11
  */
12
12
  export declare const read: (entry: AttributesList, attribute: string) => string | undefined;
13
13
  export declare const ORG = "{org}";
14
+ export declare function withOrg(pattern: string, org: string): string;
14
15
  /** A refusal the routes answer as `{ error, code }`. */
15
16
  export declare class RouteError extends Error {
16
17
  readonly status: number;
@@ -36,8 +37,6 @@ export declare function values(value: AttributeValue | undefined): string[];
36
37
  /** A DN spelled one way, to compare two spellings of it. */
37
38
  export declare function dnKey(dn: string): string;
38
39
  export declare function parentOf(dn: string): string;
39
- /** The RDN value of a DN, unescaped, or the DN itself if it has none. */
40
- export declare function rdnValue(dn: string): string;
41
40
  /**
42
41
  * The entries one level under a DN pattern holding `{org}`, the organization
43
42
  * captured as one RDN value: `(?:\\.|[^,])+` keeps an escaped comma inside it.
@@ -1,5 +1,6 @@
1
1
  import type { Express } from 'express';
2
2
  import TwakePlugin from '../../abstract/twakePlugin';
3
+ import type { HealthStatus } from '../../lib/health';
3
4
  import { type Role } from '../../abstract/plugin';
4
5
  import type { DM } from '../../bin';
5
6
  import type { AttributeValue } from '../../lib/ldapActions';
@@ -22,6 +23,7 @@ export default class James extends TwakePlugin {
22
23
  onLdapChange: string;
23
24
  ldapGroups: string;
24
25
  };
26
+ health(): Promise<HealthStatus>;
25
27
  private quotaAttr;
26
28
  private aliasAttr;
27
29
  private mailboxTypeAttr;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @module plugins/twake/migrationProxy
3
+ *
4
+ * Keeps the migrated-users list of the Twake Mail migration proxy in step
5
+ * with the accounts of the directory: an account created during the
6
+ * migration has no mailbox on the old system, so the proxy has to route it
7
+ * to the new one from its first login, and the addresses of a migrated
8
+ * account have to follow it.
9
+ *
10
+ * See `docs/usage/plugins/integrations/mail-migration.md`.
11
+ */
12
+ import type { Express } from 'express';
13
+ import TwakePlugin from '../../abstract/twakePlugin';
14
+ import { type Role } from '../../abstract/plugin';
15
+ import type { DM } from '../../bin';
16
+ import type { Hooks } from '../../hooks';
17
+ import type { HealthStatus } from '../../lib/health';
18
+ /**
19
+ * @openapi
20
+ * components:
21
+ * schemas:
22
+ * MailMigrationStatus:
23
+ * type: object
24
+ * required: [migrated]
25
+ * properties:
26
+ * migrated:
27
+ * type: boolean
28
+ * description: Whether the proxy routes the user to the new backend
29
+ */
30
+ export default class MigrationProxy extends TwakePlugin {
31
+ name: string;
32
+ roles: Role[];
33
+ dependencies: {
34
+ onLdapChange: string;
35
+ };
36
+ private readonly branches;
37
+ private readonly aliasAttr;
38
+ /** The work on one entry waits for the work before it */
39
+ private readonly queues;
40
+ constructor(server: DM);
41
+ health(): Promise<HealthStatus>;
42
+ api(app: Express): void;
43
+ hooks: Hooks;
44
+ /**
45
+ * One entry at a time: two writes close together would otherwise race to
46
+ * the proxy, the second reading a list the first has not written yet
47
+ */
48
+ private inOrder;
49
+ /**
50
+ * The address the entry logs in with, and the ones that reach it, each
51
+ * once: an alias repeating the mail would otherwise be put and taken off
52
+ * in the same breath
53
+ */
54
+ private addresses;
55
+ /**
56
+ * An entry gaining its address is put on the list with all of them, one
57
+ * losing it is taken off; one keeping it has its addresses follow it when
58
+ * the proxy says it is migrated, and is left to the migration otherwise.
59
+ * The proxy folds usernames to lower case, so a change of case is none.
60
+ */
61
+ private follow;
62
+ private userUrl;
63
+ private call;
64
+ /**
65
+ * `HEAD /migratedUsers/{user}`: 204 when migrated, 404 otherwise. With
66
+ * `retry`, an answer of {@link PROXY_UNAVAILABLE} is asked again as a
67
+ * write would be; throws once the proxy has not said
68
+ */
69
+ isMigrated(address: string, retry?: boolean): Promise<boolean>;
70
+ private getMigrationStatus;
71
+ }
@@ -93,11 +93,13 @@ Complete technical reference for the LDAP-Rest REST API including detailed speci
93
93
  | 500 | Internal Server Error | Server-side failure | LDAP connection issues, database errors, plugin failures |
94
94
  | 503 | Service Unavailable | Server temporarily unavailable | LDAP server down, maintenance mode |
95
95
 
96
- **Example 500 Response:**
96
+ **Example 500 Response** (the failure is in the server's logs; `message`
97
+ names it only when the server runs with `--debug`):
97
98
 
98
99
  ```json
99
100
  {
100
- "error": "LDAP bind error"
101
+ "error": "Internal Server Error",
102
+ "message": "An error occurred"
101
103
  }
102
104
  ```
103
105
 
@@ -805,13 +807,7 @@ Failed to fetch
805
807
 
806
808
  #### Problem: LDAP Connection Failed
807
809
 
808
- **Error:**
809
-
810
- ```json
811
- {
812
- "error": "LDAP bind error"
813
- }
814
- ```
810
+ **Error:** a 500 answer, with `LDAP bind error` in the server's logs.
815
811
 
816
812
  **Checklist:**
817
813
 
@@ -1335,19 +1335,13 @@ no `limit` was given —
1335
1335
 
1336
1336
  #### 500 Internal Server Error
1337
1337
 
1338
- **LDAP Connection Error:**
1338
+ The failure is in the server's logs, not in the response; the message names
1339
+ it only when the server runs with `--debug`:
1339
1340
 
1340
1341
  ```json
1341
1342
  {
1342
- "error": "LDAP bind error"
1343
- }
1344
- ```
1345
-
1346
- **General Server Error:**
1347
-
1348
- ```json
1349
- {
1350
- "error": "Internal server error: <details>"
1343
+ "error": "Internal Server Error",
1344
+ "message": "An error occurred"
1351
1345
  }
1352
1346
  ```
1353
1347
 
@@ -169,7 +169,10 @@ This will be replaced with the actual LDAP base at runtime (e.g., `ou=users,dc=e
169
169
 
170
170
  ## Attribute Types
171
171
 
172
- LDAP-Rest supports three main attribute types: `string`, `array`, and `pointer`.
172
+ LDAP-Rest supports three main attribute types: `string`, `array`, and `pointer`,
173
+ plus `number`, `integer`, `boolean` and `date` for single values. What a
174
+ client sends has to have the shape of the type, see
175
+ [what a value must look like](#what-a-value-must-look-like).
173
176
 
174
177
  ### String Type
175
178
 
@@ -337,7 +340,7 @@ Complete reference for all attribute properties.
337
340
 
338
341
  | Property | Type | Applies To | Description | Example |
339
342
  | ---------- | --------------- | ------------------- | ------------------------------------------------------------------------------------- | ----------------------------------- |
340
- | `type` | string | All | Attribute data type: `"string"`, `"array"`, `"pointer"`, `"number"`, `"integer"` | `"string"` |
343
+ | `type` | string | All | `"string"`, `"array"`, `"pointer"`, `"number"`, `"integer"`, `"boolean"`, `"date"` | `"string"` |
341
344
  | `required` | boolean | All | Whether the attribute must be present | `true` |
342
345
  | `fixed` | boolean | All | Whether the attribute value cannot be changed after creation | `true` |
343
346
  | `role` | string/string[] | String | Semantic role, or roles, for special handling (see [Semantic Roles](#semantic-roles)) | `"identifier"` |
@@ -364,6 +367,32 @@ Defines the data type of the attribute. Supported values:
364
367
  - `"pointer"`: Reference to another LDAP entry (DN)
365
368
  - `"number"`: Numeric value (integer or float)
366
369
  - `"integer"`: Integer value only
370
+ - `"boolean"`: `true` or `false`, stored as `TRUE` / `FALSE`
371
+ - `"date"`: LDAP generalized time, as text
372
+
373
+ #### What a value must look like
374
+
375
+ A single-valued attribute takes one value, an `array` a list. A list of one
376
+ value stands for that value and one value for a list of one, so what a read
377
+ answers, one value where the attribute holds one, can be sent back as is;
378
+ two values or more for a single-valued attribute are refused. Each element
379
+ of an `array` follows `items.type`, text without `items`. A `number` or
380
+ `integer` is a JSON number or its text (`42`, `"42"`), a `boolean` a JSON
381
+ boolean or the directory's `TRUE` / `FALSE`; anything else for them, and
382
+ anything but text for a `string`, `date` or `pointer`, is refused; an
383
+ attribute with `normalize` takes any text, read after validation. In a
384
+ `replace`, `null` or `[]` clears an attribute; in an `add` or on a creation
385
+ they are refused as no value. A `delete` naming the attribute, or naming it
386
+ with no value, removes it, unless it is `required`; naming values removes
387
+ those, every value it holds included, as the server does not read what
388
+ remains, and `''` names no value. An empty string sent as a value is
389
+ checked against `test`, refused for a pointer, and missing for a `required`
390
+ attribute. Every refusal answers 400 with the attribute's name.
391
+
392
+ Every entity route applies these rules; `test/lib/entitySchema.contract.test.ts`
393
+ asserts them on a flat entity, a group and an organization alike. In the
394
+ server they are enforced by `EntityValidator` (`src/lib/entitySchema/validator.ts`)
395
+ alone: ESLint refuses the import of the schema helpers anywhere else.
367
396
 
368
397
  #### `required` (Optional, default: `false`)
369
398
 
@@ -674,13 +703,8 @@ Common validation patterns:
674
703
 
675
704
  ### Type Validation
676
705
 
677
- Attribute values must match their declared type:
678
-
679
- - **String**: Single text value
680
- - **Array**: Multiple values (may be empty unless required)
681
- - **Number**: Numeric value (integer or float)
682
- - **Integer**: Integer value only
683
- - **Pointer**: Valid DN string
706
+ Attribute values must have the shape of their declared type, see
707
+ [what a value must look like](#what-a-value-must-look-like).
684
708
 
685
709
  ### Fixed Attributes
686
710
 
@@ -780,6 +804,7 @@ twake/
780
804
  ```bash
781
805
  npx ldap-rest \
782
806
  --ldap-base 'dc=example,dc=com' \
807
+ --ldap-dn 'cn=admin,dc=example,dc=com' --ldap-pwd secret \
783
808
  --plugin core/ldap/flatGeneric \
784
809
  --ldap-flat-schema ./static/schemas/twake/users.json \
785
810
  --plugin core/ldap/groups \
@@ -842,6 +867,7 @@ ad/
842
867
  ```bash
843
868
  npx ldap-rest \
844
869
  --ldap-base 'dc=example,dc=com' \
870
+ --ldap-dn 'cn=admin,dc=example,dc=com' --ldap-pwd secret \
845
871
  --plugin core/ldap/flatGeneric \
846
872
  --ldap-flat-schema ./static/schemas/ad/users.json \
847
873
  --plugin core/ldap/groups \
@@ -910,6 +936,7 @@ standard/
910
936
  ```bash
911
937
  npx ldap-rest \
912
938
  --ldap-base 'dc=example,dc=com' \
939
+ --ldap-dn 'cn=admin,dc=example,dc=com' --ldap-pwd secret \
913
940
  --plugin core/ldap/flatGeneric \
914
941
  --ldap-flat-schema ./static/schemas/standard/users.json \
915
942
  --plugin core/ldap/groups \
@@ -1055,6 +1082,7 @@ Save your schema and reference it when starting LDAP-Rest:
1055
1082
  ```bash
1056
1083
  npx ldap-rest \
1057
1084
  --ldap-base 'dc=example,dc=com' \
1085
+ --ldap-dn 'cn=admin,dc=example,dc=com' --ldap-pwd secret \
1058
1086
  --plugin core/ldap/flatGeneric \
1059
1087
  --ldap-flat-schema ./path/to/custom-users.json
1060
1088
  ```
@@ -1131,18 +1159,29 @@ it with a 500.
1131
1159
  ### Served Schemas
1132
1160
 
1133
1161
  `core/static` serves the merged schema. The configuration API gives each
1134
- entity its merged schema inline (`schema`) and, with `core/static`, a
1135
- `schemaUrl` made of the file path from its first `schemas` directory:
1136
- `/etc/ldap-rest/schemas/users.json` gets `/static/schemas/users.json`.
1137
- `core/static` serves a configured schema file at that URL even when it is
1138
- outside `--static-path`, and before a shipped schema of the same URL:
1139
- `/etc/ldap-rest/schemas/twake/users.json` is what
1140
- `/static/schemas/twake/users.json` returns. An option left at its default
1141
- does not count: without `--group-schema`, a `--static-path` of your own
1142
- still serves its `schemas/twake/groups.json`. If two configured files get
1143
- the same URL, the first one is served there and a warning names the other.
1144
- A file outside any `schemas` directory has no `schemaUrl`; its schema is
1145
- still inline.
1162
+ entity its merged schema inline (`schema`) and, where a static plugin's
1163
+ schema routes serve that very file, its `schemaUrl`, under the plugin's own
1164
+ `--static-name`:
1165
+
1166
+ - a schema file of the configuration gets the file path from its first
1167
+ `schemas` directory, `/etc/ldap-rest/schemas/users.json` getting
1168
+ `/static/schemas/users.json`. `core/static` serves it at that URL even
1169
+ when it is outside `--static-path`, and before a shipped schema of the
1170
+ same URL: `/etc/ldap-rest/schemas/twake/users.json` is what
1171
+ `/static/schemas/twake/users.json` returns. If two configured files get
1172
+ the same URL, the first one is served there and a warning names the
1173
+ other;
1174
+ - a file the `schemas/` directory of `--static-path` holds, that directory
1175
+ being a link or not, gets its path in it, if it is named `.json`.
1176
+
1177
+ An option left at its default does not count as configured: without
1178
+ `--group-schema`, a `--static-path` of your own still serves its
1179
+ `schemas/twake/groups.json`, and no `schemaUrl` is given unless that file is
1180
+ the package's, through a link. A static plugin loaded under another name, or with a
1181
+ `static_name` of its own, is asked as well, the one registered as `static`
1182
+ first. A file no static plugin serves merged has no `schemaUrl`, one
1183
+ express.static would send as it stands included (outside `schemas/`, or not
1184
+ named `.json`); its schema is still inline.
1146
1185
 
1147
1186
  ---
1148
1187
 
@@ -61,20 +61,22 @@ export default class MyPlugin extends DmPlugin {
61
61
  - **this.config**: Complete configuration object
62
62
  - **this.logger**: Winston logger instance
63
63
  - **this.registeredHooks**: Access to global hook registry
64
+ - **this.fetch**: `fetch` timed into the
65
+ [outbound metrics](../usage/operations.md#metrics) under the plugin's
66
+ name; the lint refuses a bare `fetch` or `node-fetch` under `src`, and a
67
+ plugin with a `fetch` member of its own has to rename it
64
68
 
65
69
  ### Error Handling and Server Resilience
66
70
 
67
- LDAP-Rest includes robust error handling to ensure the server remains operational even when plugins encounter errors:
68
-
69
- #### Global Error Handling
70
-
71
- The server automatically catches and handles:
72
-
73
- - **Uncaught exceptions** in plugin code
74
- - **Unhandled promise rejections**
75
- - **Errors in async route handlers**
76
-
77
- All errors are logged with full stack traces without crashing the server.
71
+ An error a route handler or a hook throws is logged with its stack; the
72
+ request is answered with a 5xx when the hook ran before the operation, and
73
+ the server keeps serving. An exception nothing catches, in a timer or an
74
+ event handler, is not: the binary shuts down and exits with 1, and a
75
+ promise rejection nothing handles is logged and counted, see
76
+ [shutdown](../usage/operations.md#shutdown); a host assembling the server
77
+ chooses with `exitOnCrash()`. A plugin holding a connection, a timer or a
78
+ queue implements `close()`, see
79
+ [for plugin authors](../usage/operations.md#for-plugin-authors).
78
80
 
79
81
  #### Using asyncHandler
80
82
 
@@ -175,7 +177,7 @@ export default class WebhookNotifier extends DmPlugin {
175
177
  // Private methods
176
178
  private async sendWebhook(event: string, data: unknown): Promise<void> {
177
179
  try {
178
- await fetch(this.config.webhook_url as string, {
180
+ await this.fetch(this.config.webhook_url as string, {
179
181
  method: 'POST',
180
182
  headers: { 'Content-Type': 'application/json' },
181
183
  body: JSON.stringify({ event, data }),
@@ -1153,7 +1155,7 @@ export default class NotificationPlugin extends DmPlugin {
1153
1155
  const timeoutId = setTimeout(() => controller.abort(), this.webhookTimeout);
1154
1156
 
1155
1157
  try {
1156
- const response = await fetch(this.webhookUrl, {
1158
+ const response = await this.fetch(this.webhookUrl, {
1157
1159
  method: 'POST',
1158
1160
  headers: {
1159
1161
  'Content-Type': 'application/json',
@@ -204,6 +204,14 @@ overrides builds a second instance, and it needs a name of its own.
204
204
  - **Consumes Hooks**: (Hooks provided by `onLdapChange`)
205
205
  - **Notes**: Synchronizes LDAP users/groups with Apache James mail server
206
206
 
207
+ #### `migrationProxy` (twake/migrationProxy)
208
+
209
+ - **Dependencies**:
210
+ - `onLdapChange: 'core/ldap/onChange'` ⚠️ **Required**
211
+ - **Provides Hooks**: None
212
+ - **Consumes Hooks**: `onLdapEntryChange` (provided by `onLdapChange`)
213
+ - **Notes**: Keeps the migrated-users list of the Twake Mail migration proxy, see [mail-migration](../usage/plugins/integrations/mail-migration.md)
214
+
207
215
  #### `calendar` (twake/calendar)
208
216
 
209
217
  - **Dependencies**:
@@ -9,7 +9,7 @@ those typed in [hooks.ts](../../src/hooks.ts), which is the reference.
9
9
 
10
10
  ## [LDAP](../../src/lib/ldapActions.ts) hooks
11
11
 
12
- - **ldapopts**: called before any ldapsearch to modify search options
12
+ - **ldapsearchopts**: called before any ldapsearch to modify search options
13
13
  - **ldapsearchresult**: called after any ldapsearch to modify search result
14
14
  - **ldapaddrequest**: called before any ldapadd
15
15
  - **ldapadddone**: launched once an ldapadd has written the entry, not awaited
@@ -88,4 +88,5 @@ The system will:
88
88
  - **[Plugins](plugins/README.md)** - Choose and configure plugins
89
89
  - **[Upgrading](upgrading.md)** - What to check before deploying a new version
90
90
  - **[Directory audit](directory-audit.md)** - What a schema would refuse in an existing directory
91
+ - **[Operations](operations.md)** - Health and readiness probes
91
92
  - **[Troubleshooting](troubleshooting.md)** - Problem resolution