ldap-rest 0.11.0 → 0.11.2

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.
@@ -62,11 +62,11 @@ Connect to external systems:
62
62
 
63
63
  ## Utility Plugins
64
64
 
65
- | Plugin | Description |
66
- | ------------------------------- | ----------------------------- |
67
- | [static](utilities/static.md) | Static file server |
68
- | [weblogs](utilities/weblogs.md) | HTTP logging |
69
- | [storage](utilities/storage.md) | Keyed storage with a deadline |
65
+ | Plugin | Description |
66
+ | ------------------------------- | ------------------------------------------------------------------- |
67
+ | [static](utilities/static.md) | Static file server |
68
+ | [weblogs](utilities/weblogs.md) | HTTP logging |
69
+ | [storage](utilities/storage.md) | Keyed storage with a deadline, in LDAP, files, PostgreSQL or Valkey |
70
70
 
71
71
  ## Plugin Dependencies
72
72
 
@@ -66,6 +66,36 @@ belongs to nobody's branch, and no administrator could list or change one.
66
66
  - A caller named in no `twakeLocalAdminLink` is refused every read (`403`)
67
67
  but the top organization's own entry, which `organizations/top` answers.
68
68
 
69
+ ### Transit: handing an entry over
70
+
71
+ An administrator can only move an entry between organizations they write, and
72
+ an account created by another tool often carries no `twakeDepartmentLink` at
73
+ all. The transit branch covers both cases: `--authz-transit-branch` names an
74
+ organization through which entries change hands.
75
+
76
+ ```bash
77
+ --authz-transit-branch "ou=Transit,ou=organization,dc=example,dc=com"
78
+ ```
79
+
80
+ An entry is **in transit** when it is attached to that organization, or to
81
+ none (with or without the option).
82
+
83
+ - Every administrator sees the entries in transit.
84
+ - **Claiming** one (`POST /api/v1/ldap/users/:id/move` towards an
85
+ organization) asks for write on that organization only.
86
+ - **Putting** one in transit asks for write on the organization it leaves,
87
+ and nothing on the transit branch: an administrator only lets go of what
88
+ they manage. Handing an account over to an organization one does not manage
89
+ is two steps: put it in transit, then its new administrator claims it.
90
+ - Any other change to an entry in transit (attributes, password, status,
91
+ deletion) is judged as before, so the entry is claimed first.
92
+
93
+ `GET /api/v1/authz/scope` returns the transit branch as `transit` (`null`
94
+ when there is none), so that a client can offer these moves. A group attached
95
+ to the transit branch follows the same rules through
96
+ `POST /api/v1/ldap/groups/:cn/move`; that endpoint does not move a group
97
+ attached to no organization.
98
+
69
99
  ## Configuration
70
100
 
71
101
  No configuration file needed - permissions are read from LDAP. Simply load the plugin:
@@ -440,7 +470,10 @@ User exists in `ou=users,dc=example,dc=com` but local admin cannot manage them.
440
470
  twakeDepartmentLink: ou=HR,ou=organization,dc=example,dc=com
441
471
  ```
442
472
 
443
- 2. Or move user to organization branch:
473
+ 2. Or let the local admin claim it: an account with no `twakeDepartmentLink`
474
+ is in transit, see [Transit](#transit-handing-an-entry-over).
475
+
476
+ 3. Or move user to organization branch:
444
477
  ```
445
478
  From: uid=user,ou=users,dc=example,dc=com
446
479
  To: uid=user,ou=users,ou=HR,ou=organization,dc=example,dc=com
@@ -383,6 +383,32 @@ Content-Type: application/json
383
383
  }
384
384
  ```
385
385
 
386
+ #### Entries created by other tools
387
+
388
+ An entry written by another tool may lack an object class the schema
389
+ declares, and the directory then refuses an attribute that class carries: a
390
+ bare `inetOrgPerson` in the users branch cannot take `twakeDepartmentLink`,
391
+ which `twakeWhitePages` brings. When a modify through LDAP-Rest (this route,
392
+ `/move`, a state change…) writes such an attribute, the **auxiliary** class
393
+ declared for the branch that allows it is added in the same write, provided
394
+ the entry holds, or receives, that class's mandatory attributes. Only what
395
+ the write needs is added, and a write the directory would accept is never
396
+ changed: several entities may share a branch (`users`, `posixAccounts` and
397
+ `sshPublicKeys` all live in `ou=users`) without every entry being given all
398
+ their classes.
399
+
400
+ A missing **structural** class, such as `twakeAccount`, cannot be added to an
401
+ existing entry: the write answers `409`, and the reason is logged once per
402
+ entry. The same applies to groups under `--ldap-group-base` (when it is set)
403
+ with `--group-class`, and to the organization tree with
404
+ `--ldap-organization-class`. Nothing is added when the change names
405
+ `objectClass` itself.
406
+
407
+ The repair reads the directory schema, kept for
408
+ `--ldap-raw-schema-cache-ttl`, and the entry's classes when a written
409
+ attribute belongs to a declared auxiliary class. `DM_LDAP_FLAT_AUTO_REPAIR=false`
410
+ turns it off.
411
+
386
412
  ### Delete Entry
387
413
 
388
414
  ```bash
@@ -197,7 +197,7 @@ Combine with an authentication plugin ([token](../auth/token.md), [oidc](../auth
197
197
 
198
198
  ## How It Works
199
199
 
200
- - **Schema fetch**: the root DSE is read for `subschemaSubentry` (falling back to `cn=Subschema`), then the subschema entry is fetched and parsed once per `--ldap-raw-schema-cache-ttl`. Concurrent callers share a single fetch.
200
+ - **Schema fetch**: the root DSE is read for `subschemaSubentry` (falling back to `cn=Subschema`), then the subschema entry is fetched and parsed once per `--ldap-raw-schema-cache-ttl`. Concurrent callers share a single fetch, and so does the [object class repair](flat-generic.md#entries-created-by-other-tools), which loads it even without this plugin.
201
201
  - **Operational attributes**: entries are read with `*` and `+`. When the server rejects `+`, the read is retried with `*` alone.
202
202
  - **Missing entries**: the LDAP "no such object" result (code 32) is mapped to a 404.
203
203
 
@@ -26,13 +26,17 @@ which is invisible in a configuration.
26
26
  --storage-ldap-base ou=Records,dc=example,dc=com
27
27
  ```
28
28
 
29
- | Option | Environment | Default | Description |
30
- | ----------------------------- | ------------------------------ | -------------------- | --------------------------------------------------------- |
31
- | `--storage-backend` | `DM_STORAGE_BACKEND` | _(none)_ | `ldap` or `file`. Empty means the plugin refuses to start |
32
- | `--storage-sweep-interval` | `DM_STORAGE_SWEEP_INTERVAL` | `600` | Seconds between two passes dropping expired records |
33
- | `--storage-ldap-base` | `DM_STORAGE_LDAP_BASE` | _(none)_ | Branch the `ldap` backend writes to |
34
- | `--storage-ldap-object-class` | `DM_STORAGE_LDAP_OBJECT_CLASS` | `applicationProcess` | Object class of the entries it writes |
35
- | `--storage-file-directory` | `DM_STORAGE_FILE_DIRECTORY` | _(none)_ | Directory the `file` backend writes to |
29
+ | Option | Environment | Default | Description |
30
+ | ----------------------------- | ------------------------------ | -------------------- | ------------------------------------------------------------------------------- |
31
+ | `--storage-backend` | `DM_STORAGE_BACKEND` | _(none)_ | `ldap`, `file`, `postgres` or `valkey`. Empty means the plugin refuses to start |
32
+ | `--storage-sweep-interval` | `DM_STORAGE_SWEEP_INTERVAL` | `600` | Seconds between two passes dropping expired records |
33
+ | `--storage-ldap-base` | `DM_STORAGE_LDAP_BASE` | _(none)_ | Branch the `ldap` backend writes to |
34
+ | `--storage-ldap-object-class` | `DM_STORAGE_LDAP_OBJECT_CLASS` | `applicationProcess` | Object class of the entries it writes |
35
+ | `--storage-file-directory` | `DM_STORAGE_FILE_DIRECTORY` | _(none)_ | Directory the `file` backend writes to |
36
+ | `--storage-postgres-url` | `DM_STORAGE_POSTGRES_URL` | _(none)_ | Connection string of the `postgres` backend |
37
+ | `--storage-postgres-table` | `DM_STORAGE_POSTGRES_TABLE` | `ldap_rest_storage` | Table it writes to, `schema.table` accepted |
38
+ | `--storage-valkey-url` | `DM_STORAGE_VALKEY_URL` | _(none)_ | URL of the `valkey` backend (`redis://…` or `rediss://…`) |
39
+ | `--storage-valkey-prefix` | `DM_STORAGE_VALKEY_PREFIX` | `ldap-rest:` | Prefix of the keys it writes |
36
40
 
37
41
  `core/storage` is in the priority list, so it is registered before the
38
42
  plugins that consume it whatever order they are written in. A consumer that
@@ -61,18 +65,65 @@ The directory is the plugin's own: nothing else should be writing there, and
61
65
  the sweeper removes what it finds expired. It is local to one process, so it
62
66
  suits a single instance, not a cluster sharing one verdict.
63
67
 
68
+ ### `postgres`
69
+
70
+ One row per record in `--storage-postgres-table`, with the consumer's
71
+ namespace and key kept verbatim beside the value and an `expires_at` column,
72
+ so a `SELECT` tells what a record is about. Needs the optional `pg` package.
73
+
74
+ The table is created at the first connection when it is not there. A role
75
+ that may not create tables works once it exists:
76
+
77
+ ```sql
78
+ CREATE TABLE ldap_rest_storage (
79
+ namespace text NOT NULL,
80
+ key text NOT NULL,
81
+ value text NOT NULL,
82
+ expires_at timestamptz NOT NULL,
83
+ PRIMARY KEY (namespace, key)
84
+ );
85
+ CREATE INDEX ldap_rest_storage_expires_at ON ldap_rest_storage (expires_at);
86
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ldap_rest_storage TO ldap_rest;
87
+ ```
88
+
89
+ Several instances can share the table and sweep it at once. PostgreSQL
90
+ refuses a NUL in `text`, so a key holding one cannot be kept: the write
91
+ fails.
92
+
93
+ ### `valkey`
94
+
95
+ One key per record, `--storage-valkey-prefix` followed by the namespace and
96
+ the key, expiring with the record: nothing is left to sweep. Works with Redis
97
+ too. Needs the optional `iovalkey` package.
98
+
99
+ **The instance must not evict.** An instance configured as a cache
100
+ (`allkeys-lru`, `volatile-lru` and the like) drops keys under memory pressure,
101
+ and every key this backend writes has a TTL: a Back-Channel Logout mark
102
+ evicted early reopens the session it closed. Use an instance with
103
+ `maxmemory-policy noeviction`, which answers a full memory with a failed
104
+ write — reported to the identity provider — rather than a silent loss.
105
+
106
+ A connection lost while running is retried in the background; meanwhile, reads
107
+ answer "absent" with a warning and writes fail at once instead of waiting.
108
+
109
+ `postgres` and `valkey` connect at startup, and report there an address that
110
+ does not answer or a package that is not installed; the server starts anyway,
111
+ and every operation tries to connect again.
112
+
64
113
  ## Expiry
65
114
 
66
115
  Two rules the backends do not get to reinterpret:
67
116
 
68
117
  - **Expiry is enforced on read.** A record past its deadline is answered as
69
118
  absent whatever the sweeper has done, so a sweeper running late is a
70
- storage cost, never a wrong answer.
119
+ storage cost, never a wrong answer. Expiry is judged by this server's clock,
120
+ the sweep and the Valkey TTL included, so a database clock running ahead
121
+ cannot drop a record early.
71
122
  - **The deadline belongs to the consumer.** How long a record lives is the
72
123
  policy of whoever wrote it. The store is told an instant and respects it.
73
124
 
74
- Keys are namespaced per consumer, so two of them cannot collide in one branch
75
- or one directory.
125
+ Keys are namespaced per consumer, so two of them cannot collide in one branch,
126
+ directory, table or instance.
76
127
 
77
128
  ## For Plugin Authors
78
129
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"
@@ -232,6 +232,14 @@
232
232
  "types": "./dist/src/plugins/storage/ldap.d.ts",
233
233
  "import": "./dist/plugins/storage/ldap.js"
234
234
  },
235
+ "./plugin-storage-postgres": {
236
+ "types": "./dist/src/plugins/storage/postgres.d.ts",
237
+ "import": "./dist/plugins/storage/postgres.js"
238
+ },
239
+ "./plugin-storage-valkey": {
240
+ "types": "./dist/src/plugins/storage/valkey.d.ts",
241
+ "import": "./dist/plugins/storage/valkey.js"
242
+ },
235
243
  "./plugin-twake-appaccountsapi": {
236
244
  "types": "./dist/src/plugins/twake/appAccountsApi.d.ts",
237
245
  "import": "./dist/plugins/twake/appAccountsApi.js"
@@ -727,6 +735,7 @@
727
735
  "@types/mocha": "^10.0.10",
728
736
  "@types/multer": "^2.0.0",
729
737
  "@types/node": "^24.5.1",
738
+ "@types/pg": "^8.23.1",
730
739
  "@types/supertest": "^6.0.3",
731
740
  "@typescript-eslint/eslint-plugin": "^8.44.0",
732
741
  "@typescript-eslint/parser": "^8.44.0",
@@ -755,7 +764,9 @@
755
764
  "optionalDependencies": {
756
765
  "@linagora/rabbitmq-client": "^0.1.2",
757
766
  "express-openid-connect": "^2.19.2",
767
+ "iovalkey": "^0.4.0",
758
768
  "lemonldap-ng-handler": "^0.7.3",
769
+ "pg": "^8.23.0",
759
770
  "re2": "^1.22.1"
760
771
  },
761
772
  "engines": {