ldap-rest 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/config/args.js +1 -1
  3. package/dist/lib/errors.js +1 -1
  4. package/dist/lib/instanceProviders.js +1 -1
  5. package/dist/lib/ldapActions.js +1 -1
  6. package/dist/plugins/auth/openidconnect.js +1 -1
  7. package/dist/plugins/ldap/groups.js +1 -1
  8. package/dist/plugins/ldap/trash.js +1 -1
  9. package/dist/plugins/scim/filter.js +1 -1
  10. package/dist/plugins/scim/groups.js +1 -1
  11. package/dist/plugins/scim/mapping.js +1 -1
  12. package/dist/plugins/scim/patch.js +1 -1
  13. package/dist/plugins/scim/users.js +1 -1
  14. package/dist/plugins/twake/groups.js +1 -0
  15. package/dist/plugins/twake/instances.js +1 -1
  16. package/dist/plugins/twake/lifecycleAttributes.js +1 -1
  17. package/dist/plugins/twake/lifecycleEvents.js +1 -1
  18. package/dist/plugins/twake/tombstone.js +1 -1
  19. package/dist/src/abstract/ldapFlat.d.ts +34 -6
  20. package/dist/src/config/args.d.ts +12 -0
  21. package/dist/src/lib/errors.d.ts +3 -0
  22. package/dist/src/lib/instanceProviders.d.ts +2 -0
  23. package/dist/src/plugins/auth/openidconnect.d.ts +15 -0
  24. package/dist/src/plugins/ldap/groups.d.ts +5 -0
  25. package/dist/src/plugins/scim/filter.d.ts +2 -1
  26. package/dist/src/plugins/scim/groups.d.ts +1 -0
  27. package/dist/src/plugins/scim/mapping.d.ts +18 -7
  28. package/dist/src/plugins/scim/patch.d.ts +2 -2
  29. package/dist/src/plugins/twake/groups.d.ts +89 -0
  30. package/dist/src/plugins/twake/instances.d.ts +14 -0
  31. package/dist/src/plugins/twake/lifecycleEvents.d.ts +20 -3
  32. package/docs/client-development/api/rest-api.md +32 -2
  33. package/docs/usage/configuration.md +22 -1
  34. package/docs/usage/plugins/README.md +1 -0
  35. package/docs/usage/plugins/auth/oidc.md +19 -0
  36. package/docs/usage/plugins/integrations/README.md +1 -0
  37. package/docs/usage/plugins/integrations/groups.md +225 -0
  38. package/docs/usage/plugins/integrations/instances.md +31 -3
  39. package/docs/usage/plugins/integrations/lifecycle-events.md +38 -5
  40. package/docs/usage/plugins/integrations/scim.md +44 -34
  41. package/docs/usage/plugins/integrations/tombstone.md +2 -10
  42. package/docs/usage/plugins/ldap/flat-generic.md +38 -2
  43. package/docs/usage/plugins/ldap/trash.md +4 -4
  44. package/docs/usage/upgrading.md +56 -0
  45. package/package.json +9 -1
  46. package/static/schemas/twake/organizationGroups.json +32 -0
@@ -51,7 +51,7 @@ All SCIM endpoints sit behind the auth middleware registered before the plugin
51
51
  | `--scim-base-map` | `DM_SCIM_BASE_MAP` | — | Path to JSON file mapping authenticated user → `{userBase, groupBase}` |
52
52
  | `--scim-user-object-class` | `DM_SCIM_USER_OBJECT_CLASSES` | `top, inetOrgPerson, organizationalPerson, person` | Object classes for created Users |
53
53
  | `--scim-user-rdn-attribute` | `DM_SCIM_USER_RDN_ATTRIBUTE` | `uid` | RDN attribute for Users |
54
- | `--scim-user-lock-attribute` | `DM_SCIM_USER_LOCK_ATTRIBUTE` | `pwdAccountLockedTime` | LDAP attribute whose presence marks a User as `active: false` |
54
+ | `--scim-user-lock-attribute` | `DM_SCIM_USER_LOCK_ATTRIBUTE` | `pwdAccountLockedTime` | LDAP attribute whose lock value marks a User as `active: false` |
55
55
  | `--scim-user-lock-value` | `DM_SCIM_USER_LOCK_VALUE` | — | Value written to that attribute when deactivating (required unless the attribute is the default) |
56
56
  | `--scim-group-object-class` | `DM_SCIM_GROUP_OBJECT_CLASSES` | `top, groupOfNames` | Object classes for created Groups |
57
57
  | `--scim-group-rdn-attribute` | `DM_SCIM_GROUP_RDN_ATTRIBUTE` | `cn` | RDN attribute for Groups |
@@ -113,23 +113,23 @@ All responses have `Content-Type: application/scim+json`.
113
113
 
114
114
  ### Default mapping (LDAP ⇄ SCIM User)
115
115
 
116
- | SCIM | LDAP |
117
- | ------------------------------------ | ------------------------------------------- |
118
- | `id` | `uid` (or `entryUUID`) |
119
- | `userName` | `uid` |
120
- | `externalId` | `employeeNumber` |
121
- | `name.familyName` | `sn` |
122
- | `name.givenName` | `givenName` |
123
- | `name.formatted` | `cn` |
124
- | `displayName` | `displayName` |
125
- | `title` | `title` |
126
- | `preferredLanguage` | `preferredLanguage` |
127
- | `emails[primary=true].value` | `mail` |
128
- | `emails[primary=false].value` | `mailAlternateAddress` |
129
- | `phoneNumbers[primary=true].value` | `telephoneNumber` |
130
- | `phoneNumbers[primary=false].value` | `mobile` |
131
- | `active` | pseudo (false if the lock attribute is set) |
132
- | `meta.created` / `meta.lastModified` | `createTimestamp` / `modifyTimestamp` |
116
+ | SCIM | LDAP |
117
+ | ------------------------------------ | --------------------------------------------------------- |
118
+ | `id` | `uid` (or `entryUUID`) |
119
+ | `userName` | `uid` |
120
+ | `externalId` | `employeeNumber` |
121
+ | `name.familyName` | `sn` |
122
+ | `name.givenName` | `givenName` |
123
+ | `name.formatted` | `cn` |
124
+ | `displayName` | `displayName` |
125
+ | `title` | `title` |
126
+ | `preferredLanguage` | `preferredLanguage` |
127
+ | `emails[primary=true].value` | `mail` |
128
+ | `emails[primary=false].value` | `mailAlternateAddress` |
129
+ | `phoneNumbers[primary=true].value` | `telephoneNumber` |
130
+ | `phoneNumbers[primary=false].value` | `mobile` |
131
+ | `active` | pseudo (false if the lock attribute holds the lock value) |
132
+ | `meta.created` / `meta.lastModified` | `createTimestamp` / `modifyTimestamp` |
133
133
 
134
134
  Override with `--scim-user-mapping /path/to/user-mapping.json` (same schema as
135
135
  `static/schemas/scim/default-mapping.json`).
@@ -140,14 +140,18 @@ SCIM models account status with the boolean `active` (RFC 7643 §4.1.1), and it
140
140
  is how Okta, Entra ID and JumpCloud disable a user — they PATCH `active` to
141
141
  `false` rather than DELETE the resource.
142
142
 
143
- LDAP has no single equivalent, so the plugin models it on the **presence of one
144
- attribute**: present means locked, absent means active.
143
+ LDAP has no single equivalent, so the plugin models it on **one attribute
144
+ holding one value**: `--scim-user-lock-value` in `--scim-user-lock-attribute`
145
+ means locked, anything else means active. A ppolicy lockout after failed binds
146
+ is a timestamp in `pwdAccountLockedTime`, so it reads as active, and
147
+ `"active": true` leaves it in place. It lasts `pwdLockoutDuration`, or until
148
+ an administrator clears it when that is 0.
145
149
 
146
150
  | SCIM | LDAP |
147
151
  | ----------------- | ----------------------------------------------- |
148
152
  | `"active": false` | write `--scim-user-lock-value` to the attribute |
149
- | `"active": true` | remove the attribute |
150
- | attribute absent | `"active": true` |
153
+ | `"active": true` | remove the attribute if it holds the lock value |
154
+ | lock value absent | `"active": true` |
151
155
 
152
156
  The default targets the ppolicy overlay:
153
157
 
@@ -184,8 +188,8 @@ neither can be told apart from a deliberate local schema:
184
188
 
185
189
  **None of this can tell whether your directory honours the value you chose.**
186
190
  The checks are on the shape of the configuration. `active` is read back from
187
- the mere presence of the attribute, so a value the directory stores and
188
- ignores still reads as `false`. Verify once, by hand, that a deactivation
191
+ the value this plugin wrote, so a value the directory stores and ignores still
192
+ reads as `false`. Verify once, by hand, that a deactivation
189
193
  actually prevents a bind.
190
194
 
191
195
  `active` is not a mapping entry and must not be added to a mapping file: it is
@@ -347,16 +351,16 @@ group entries works; use a mapping override if you would rather name it there.
347
351
  SCIM filters are translated to LDAP filters (RFC 4515). Values are always escaped
348
352
  via `escapeLdapFilter()` — no LDAP injection is possible.
349
353
 
350
- | SCIM | LDAP |
351
- | -------------------------------- | ----------------------------- |
352
- | `userName eq "alice"` | `(uid=alice)` |
353
- | `name.familyName sw "Du"` | `(sn=Du*)` |
354
- | `emails.value co "@example.com"` | `(mail=*@example.com*)` |
355
- | `displayName pr` | `(displayName=*)` |
356
- | `active eq true` | `(!(pwdAccountLockedTime=*))` |
357
- | `active pr` | `(objectClass=*)` |
358
- | `a eq "x" and b eq "y"` | `(&(a=x)(b=y))` |
359
- | `not (a eq "x")` | `(!(a=x))` |
354
+ | SCIM | LDAP |
355
+ | -------------------------------- | ----------------------------------------- |
356
+ | `userName eq "alice"` | `(uid=alice)` |
357
+ | `name.familyName sw "Du"` | `(sn=Du*)` |
358
+ | `emails.value co "@example.com"` | `(mail=*@example.com*)` |
359
+ | `displayName pr` | `(displayName=*)` |
360
+ | `active eq true` | `(!(pwdAccountLockedTime=000001010000Z))` |
361
+ | `active pr` | `(objectClass=*)` |
362
+ | `a eq "x" and b eq "y"` | `(&(a=x)(b=y))` |
363
+ | `not (a eq "x")` | `(!(a=x))` |
360
364
 
361
365
  Unknown SCIM attributes raise `400 invalidFilter`.
362
366
 
@@ -616,6 +620,12 @@ Other plugins can observe and transform SCIM operations via the dynamic hooks
616
620
  `scimuserdelete`, `scimuserdeletedone`, and their `scimgroup*` equivalents, plus
617
621
  `scimbulkdone` for Bulk.
618
622
 
623
+ `scimgroupcreate` receives the group, the request and the group base. A
624
+ group's RDN value is its `displayName`, unless the hook sets `id` on the group
625
+ and the group mapping stores nothing in the RDN attribute: that `id` is then
626
+ the RDN value. An `id` sent by the client is ignored. A hook returns all
627
+ three, so that the hooks after it still get the base.
628
+
619
629
  ## Exposing the configuration to client apps (`configApi`)
620
630
 
621
631
  Load `core/configApi` alongside the SCIM plugin, and `GET /api/v1/config` will
@@ -38,16 +38,8 @@ a delete that an authorization plugin refuses writes nothing.
38
38
 
39
39
  `core/ldap/trash` moves a deleted entry before this plugin sees the delete,
40
40
  so no tombstone would be written in a branch it watches. The server refuses
41
- to start when `--trash-watched-bases` covers a `--twake-tombstone-dn`
42
- pattern, read the way the trash reads it today, and an empty list watches
43
- every branch.
44
-
45
- The trash splits `--trash-watched-bases` on every comma, a bug
46
- ([#226](https://github.com/linagora/ldap-rest/issues/226)): a base such as
47
- `ou=users,dc=example,dc=com` becomes the suffixes `ou=users`, `dc=example`
48
- and `dc=com`, and `dc=com` watches every branch below it. The check follows
49
- the trash as it behaves, so such a value refuses to start next to any
50
- tombstone pattern under `dc=com`.
41
+ to start when one of the `;`-separated `--trash-watched-bases` covers or sits under
42
+ a `--twake-tombstone-dn` pattern, and an empty list watches every branch.
51
43
 
52
44
  ## Reason
53
45
 
@@ -318,13 +318,13 @@ For each schema, the plugin generates REST API endpoints:
318
318
  ### List Entries
319
319
 
320
320
  ```bash
321
- GET /api/v1/ldap/{pluralName}?match={filter}&attributes={attrs}
321
+ GET /api/v1/ldap/{pluralName}?match={value}&attribute={attrs}&attributes={attrs}&limit={n}
322
322
  ```
323
323
 
324
324
  **Example:**
325
325
 
326
326
  ```bash
327
- curl http://localhost:8081/api/v1/ldap/users?match=uid=*smith*&attributes=uid,cn,mail
327
+ curl 'http://localhost:8081/api/v1/ldap/users?match=smith&attribute=uid&attributes=uid,cn,mail&limit=50'
328
328
  ```
329
329
 
330
330
  **Response:**
@@ -340,6 +340,42 @@ curl http://localhost:8081/api/v1/ldap/users?match=uid=*smith*&attributes=uid,cn
340
340
  }
341
341
  ```
342
342
 
343
+ #### Long lists
344
+
345
+ A directory lists at most a set number of entries per search for the account
346
+ LDAP-Rest binds as (OpenLDAP: `olcSizeLimit`, default 500, or `olcLimits`;
347
+ `size.prtotal` bounds paged searches). OpenLDAP refuses a longer list as a
348
+ whole rather than cutting it short:
349
+
350
+ - **Without `limit`**, the answer is every match or `422`:
351
+
352
+ ```json
353
+ {
354
+ "error": "Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`."
355
+ }
356
+ ```
357
+
358
+ - **With `limit`** (a positive integer, `400` otherwise), at most `limit`
359
+ entries come back, with the header `X-Result-Truncated: true` when others
360
+ were left out. When the directory refuses the list, LDAP-Rest asks again
361
+ for `limit + 1` entries and answers what it gets, at most `limit` —
362
+ possibly fewer, if the account's own limit is lower — always with the
363
+ header: the directory does not say whether that answer was cut, so a
364
+ shorter one is not taken for complete. A request with `limit` is never
365
+ answered `422`.
366
+
367
+ The header is absent from a complete answer. LDAP-Rest sets no CORS headers:
368
+ a browser client on another origin sees `X-Result-Truncated` only if the
369
+ proxy answering for it lists the header in `Access-Control-Expose-Headers`.
370
+
371
+ A small `limit` costs a page of the directory's answer. A `limit` larger than
372
+ the directory's own limit costs a walk up to that limit, then the second
373
+ search: memory stays bounded by the directory, not by `limit`.
374
+
375
+ Only the lists of the entities this plugin serves honour `limit`,
376
+ `X-Result-Truncated` and the `422` for now. The groups list
377
+ (`GET /api/v1/ldap/groups`) ignores `limit` and still fails with `500`.
378
+
343
379
  ### Get Entry
344
380
 
345
381
  ```bash
@@ -16,7 +16,7 @@ Plugin to intercept LDAP delete operations and move entries to a trash branch in
16
16
  ### Environment Variables
17
17
 
18
18
  - `DM_TRASH_BASE`: LDAP base DN for trash (default: `ou=trash,dc=example,dc=com`)
19
- - `DM_TRASH_WATCHED_BASES`: Comma-separated list of branches to watch (default: all branches except trash)
19
+ - `DM_TRASH_WATCHED_BASES`: branches to watch, each a full DN, separated by `;` (default: all branches except trash)
20
20
  - `DM_TRASH_ADD_METADATA`: Add metadata to trashed entries (default: `true`)
21
21
  - `DM_TRASH_AUTO_CREATE`: Auto-create trash branch if missing (default: `true`)
22
22
 
@@ -25,7 +25,7 @@ Plugin to intercept LDAP delete operations and move entries to a trash branch in
25
25
  ```bash
26
26
  --plugin core/ldap/trash \
27
27
  --trash-base "ou=trash,dc=example,dc=com" \
28
- --trash-watched-bases "ou=users,dc=example,dc=com,ou=groups,dc=example,dc=com" \
28
+ --trash-watched-bases "ou=users,dc=example,dc=com;ou=groups,dc=example,dc=com" \
29
29
  --trash-add-metadata true \
30
30
  --trash-auto-create true
31
31
  ```
@@ -68,7 +68,7 @@ This watches all LDAP branches except the trash branch itself.
68
68
  ldap-rest \
69
69
  --plugin core/ldap/trash \
70
70
  --trash-base "ou=trash,dc=example,dc=com" \
71
- --trash-watched-bases "ou=users,dc=example,dc=com,ou=groups,dc=example,dc=com"
71
+ --trash-watched-bases "ou=users,dc=example,dc=com;ou=groups,dc=example,dc=com"
72
72
  ```
73
73
 
74
74
  This only intercepts deletes from `ou=users` and `ou=groups`. Deletes from other branches proceed normally.
@@ -248,7 +248,7 @@ ldap-rest \
248
248
  --ldap-pwd "admin" \
249
249
  --ldap-base "dc=example,dc=com" \
250
250
  --trash-base "ou=trash,dc=example,dc=com" \
251
- --trash-watched-bases "ou=users,dc=example,dc=com,ou=groups,dc=example,dc=com" \
251
+ --trash-watched-bases "ou=users,dc=example,dc=com;ou=groups,dc=example,dc=com" \
252
252
  --trash-add-metadata true \
253
253
  --auth-token "secret-token"
254
254
  ```
@@ -4,6 +4,62 @@ What to check before deploying, newest first. Only releases that need a
4
4
  decision or a configuration change appear here; see the
5
5
  [CHANGELOG](../../CHANGELOG.md) for everything else.
6
6
 
7
+ ## To 0.13.0
8
+
9
+ ### An OpenID Connect session ends with its access token
10
+
11
+ **Who is affected:** a deployment loading `core/auth/openidconnect`.
12
+
13
+ A session used to last as long as its cookie, a day of inactivity and a week
14
+ at most. It now ends when its access token expires, unless the provider gave
15
+ a refresh token to renew it: a browser then goes back through the provider,
16
+ silently while the provider's own session lasts, and a client asking for
17
+ JSON only (`Accept: application/json`) is answered `401`.
18
+
19
+ To keep sessions going without that round trip, have the provider issue
20
+ refresh tokens to the client
21
+ ([details](plugins/auth/oidc.md#session-lifetime)).
22
+
23
+ ### SCIM `active` reads the lock value
24
+
25
+ **Who is affected:** a SCIM client of a directory with ppolicy lockouts. An
26
+ account locked out after failed binds now reads `active: true`, is left out
27
+ of `active eq false`, and keeps its lockout on `"active": true`; only the
28
+ configured lock value reads as inactive. See
29
+ [the SCIM plugin documentation](plugins/integrations/scim.md#deactivating-an-account-active).
30
+
31
+ ### twake/instances links an organization before its members
32
+
33
+ **Who is affected:** a deployment running 0.12.0 with
34
+ `--twake-instance-provider cloudery` and `--twake-instance-organization-base`.
35
+
36
+ 0.12.0 sent each member's `user.created` at once and marked the member, while
37
+ the Cloudery refused it because the organization was not linked to its
38
+ instance. The plugin now links the organization and holds its members until
39
+ then, but only releases members that are not marked, so those already marked
40
+ are not sent again on their own.
41
+
42
+ To repair an organization, clear `--twake-instance-sent-attribute` on its
43
+ marked members, then replay its `workplace.created` or call
44
+ `ensureOrganization` for it ([details](plugins/integrations/instances.md#organizations)).
45
+
46
+ ### Trash watched bases are separated by `;`
47
+
48
+ **Who is affected:** a deployment loading `core/ldap/trash` with
49
+ `--trash-watched-bases` set.
50
+
51
+ The list was split on commas, so a DN such as `ou=users,dc=example,dc=com`
52
+ became the suffixes `ou=users`, `dc=example` and `dc=com`, and the trash
53
+ caught deletes in every branch under `dc=com`. Each base is now a whole DN,
54
+ and bases are separated by `;`:
55
+ `ou=users,dc=example,dc=com;ou=groups,dc=example,dc=com`. A single DN needs
56
+ no change. A list of full DNs joined by commas, such as
57
+ `ou=users,dc=example,dc=com,ou=groups,dc=example,dc=com`, or a list of RDNs,
58
+ such as `ou=users,ou=groups`, now reads as one DN that no entry sits under,
59
+ so the trash catches nothing and deletes there are permanent: rewrite it with
60
+ `;`, each branch as its full DN. The server logs a warning at startup for a
61
+ watched base that is not one DN under `--ldap-base`.
62
+
7
63
  ## To 0.12.0
8
64
 
9
65
  ### twake/instances replaces cozyProvision and clouderyProvision
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"
@@ -268,6 +268,10 @@
268
268
  "types": "./dist/src/plugins/twake/drive.d.ts",
269
269
  "import": "./dist/plugins/twake/drive.js"
270
270
  },
271
+ "./plugin-twake-groups": {
272
+ "types": "./dist/src/plugins/twake/groups.d.ts",
273
+ "import": "./dist/plugins/twake/groups.js"
274
+ },
271
275
  "./plugin-twake-instances": {
272
276
  "types": "./dist/src/plugins/twake/instances.d.ts",
273
277
  "import": "./dist/plugins/twake/instances.js"
@@ -660,6 +664,10 @@
660
664
  "import": "./static/schemas/twake/nomenclature/twakeTitle.json",
661
665
  "require": "./static/schemas/twake/nomenclature/twakeTitle.json"
662
666
  },
667
+ "./schema-twake-organizationgroups": {
668
+ "import": "./static/schemas/twake/organizationGroups.json",
669
+ "require": "./static/schemas/twake/organizationGroups.json"
670
+ },
663
671
  "./schema-twake-organizations": {
664
672
  "import": "./static/schemas/twake/organizations.json",
665
673
  "require": "./static/schemas/twake/organizations.json"
@@ -0,0 +1,32 @@
1
+ {
2
+ "strict": true,
3
+ "attributes": {
4
+ "objectClass": {
5
+ "type": "array",
6
+ "items": {
7
+ "type": "string",
8
+ "test": "^[a-zA-Z][a-zA-Z0-9-]*$"
9
+ },
10
+ "default": ["top", "groupOfNames", "twakeGroup"],
11
+ "required": true,
12
+ "fixed": true
13
+ },
14
+ "cn": {
15
+ "type": "string",
16
+ "test": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
17
+ "required": true
18
+ },
19
+ "member": {
20
+ "type": "array",
21
+ "items": {
22
+ "type": "string",
23
+ "test": "^[a-zA-Z]+=.*,__LDAP_BASE__$"
24
+ },
25
+ "required": false
26
+ },
27
+ "description": {
28
+ "type": "string",
29
+ "required": false
30
+ }
31
+ }
32
+ }