ldap-rest 0.11.2 → 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 (56) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/bin/index.js +1 -1
  3. package/dist/config/args.js +1 -1
  4. package/dist/lib/authz/composition.js +1 -1
  5. package/dist/lib/errors.js +1 -1
  6. package/dist/lib/instanceProviders.js +1 -0
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/plugins/auth/openidconnect.js +1 -1
  9. package/dist/plugins/configApi.js +1 -1
  10. package/dist/plugins/ldap/groups.js +1 -1
  11. package/dist/plugins/ldap/trash.js +1 -1
  12. package/dist/plugins/scim/filter.js +1 -1
  13. package/dist/plugins/scim/groups.js +1 -1
  14. package/dist/plugins/scim/mapping.js +1 -1
  15. package/dist/plugins/scim/patch.js +1 -1
  16. package/dist/plugins/scim/users.js +1 -1
  17. package/dist/plugins/twake/groups.js +1 -0
  18. package/dist/plugins/twake/instances.js +1 -0
  19. package/dist/plugins/twake/lifecycleAttributes.js +1 -0
  20. package/dist/plugins/twake/lifecycleEvents.js +1 -0
  21. package/dist/plugins/twake/tombstone.js +1 -0
  22. package/dist/src/abstract/ldapFlat.d.ts +34 -6
  23. package/dist/src/abstract/plugin.d.ts +7 -4
  24. package/dist/src/config/args.d.ts +60 -0
  25. package/dist/src/hooks.d.ts +2 -0
  26. package/dist/src/lib/authz/composition.d.ts +1 -1
  27. package/dist/src/lib/errors.d.ts +3 -0
  28. package/dist/src/lib/instanceProviders.d.ts +88 -0
  29. package/dist/src/lib/ldapActions.d.ts +10 -2
  30. package/dist/src/plugins/auth/openidconnect.d.ts +39 -0
  31. package/dist/src/plugins/ldap/groups.d.ts +5 -0
  32. package/dist/src/plugins/scim/filter.d.ts +2 -1
  33. package/dist/src/plugins/scim/groups.d.ts +1 -0
  34. package/dist/src/plugins/scim/mapping.d.ts +22 -8
  35. package/dist/src/plugins/scim/patch.d.ts +9 -2
  36. package/dist/src/plugins/twake/groups.d.ts +89 -0
  37. package/dist/src/plugins/twake/instances.d.ts +102 -0
  38. package/dist/src/plugins/twake/lifecycleAttributes.d.ts +29 -0
  39. package/dist/src/plugins/twake/lifecycleEvents.d.ts +62 -0
  40. package/dist/src/plugins/twake/tombstone.d.ts +86 -0
  41. package/docs/client-development/api/rest-api.md +34 -2
  42. package/docs/plugin-development/hooks.md +24 -0
  43. package/docs/usage/configuration.md +97 -1
  44. package/docs/usage/plugins/README.md +10 -6
  45. package/docs/usage/plugins/auth/oidc.md +46 -0
  46. package/docs/usage/plugins/integrations/README.md +12 -4
  47. package/docs/usage/plugins/integrations/groups.md +225 -0
  48. package/docs/usage/plugins/integrations/instances.md +150 -0
  49. package/docs/usage/plugins/integrations/lifecycle-events.md +233 -0
  50. package/docs/usage/plugins/integrations/scim.md +55 -34
  51. package/docs/usage/plugins/integrations/tombstone.md +107 -0
  52. package/docs/usage/plugins/ldap/flat-generic.md +38 -2
  53. package/docs/usage/plugins/ldap/trash.md +4 -4
  54. package/docs/usage/upgrading.md +114 -0
  55. package/package.json +25 -1
  56. package/static/schemas/twake/organizationGroups.json +32 -0
@@ -0,0 +1,233 @@
1
+ # Lifecycle Events
2
+
3
+ `core/twake/lifecycleEvents` publishes an account's or a group's lifecycle to
4
+ RabbitMQ from the directory write itself. REST, SCIM and any other plugin writing through
5
+ LDAP-Rest announce the same events, with the same payloads.
6
+
7
+ Which entries are accounts or groups, which attributes carry the role, the
8
+ lock, the deletion and the members, and where each event goes are
9
+ configuration.
10
+
11
+ ## Events
12
+
13
+ Each event is a change of state, read from the entry before and after a write
14
+ (`onLdapEntryChange`, from `core/ldap/onChange`). A write that changes nothing
15
+ publishes nothing.
16
+
17
+ - `created`: an entry is added. An entry added locked, as a SCIM create with
18
+ `active: false` does, publishes `created` then `disabled`.
19
+ - `roleChanged`: the role attribute gets different values. Case and order are
20
+ ignored: `Admin` and `admin` are one role. A multi-valued role attribute is
21
+ compared as a set, but `$title` carries its first value only, so
22
+ `[x, y]` to `[x, z]` publishes `x` as both role and previous role: keep the
23
+ role in a single-valued attribute.
24
+ - `disabled`: the lock attribute takes the lock value.
25
+ - `enabled`: the lock attribute no longer holds the lock value.
26
+ - `updated`: a target whose payload has `$changed.` sources publishes when one
27
+ of them changed, with those that did not left out. A target without them
28
+ publishes on every change of the entry.
29
+ - `memberAdded`, `memberRemoved`: the member attribute gains or loses values.
30
+ The old and new lists are compared as sets, so a whole list replaced
31
+ publishes only the members that came or went. The group placeholder
32
+ (`--group-dummy-user`) is never a member here. A group created with
33
+ members publishes `created` only, its members in `$members`.
34
+ - `deleted`: the entry becomes a tombstone (the deleted attribute takes the
35
+ deleted value), or an entry that is not a tombstone is removed.
36
+
37
+ An account is locked when its lock attribute holds the lock value, by default
38
+ the ppolicy administrative lock `000001010000Z`. Any other value, such as the
39
+ timestamp of a ppolicy lockout after failed binds, is not a lock here.
40
+
41
+ A tombstone announces nothing else: the write that makes it does not also
42
+ publish `disabled`, later changes to it (a lock written afterwards included)
43
+ publish nothing, and removing it publishes nothing, since its deletion was
44
+ already announced.
45
+
46
+ A publish that fails is logged as an error, and the next targets are still
47
+ published. It never fails the write that caused it, and the event is not
48
+ replayed: writing the same value again changes nothing, so it publishes
49
+ nothing. With `core/twake/tombstone`, a lost `deleted` is published again by
50
+ deleting the tombstone once more (see below); a lost `disabled`, `enabled`,
51
+ `created` or `roleChanged` has no such path.
52
+
53
+ The lock attribute is requested by name when the entry is read, so an
54
+ operational one such as `pwdAccountLockedTime` is seen on both sides.
55
+
56
+ ## What is not published
57
+
58
+ - A rename or a move publishes nothing. The `$dn.*` groups of the new DN may
59
+ then differ from the ones consumers know, and an entry moved into or out of
60
+ a rule's pattern publishes neither `created` nor `deleted`.
61
+ - A tombstone cannot be restored. Clearing its deleted attribute is not a
62
+ supported operation: it publishes nothing, whatever else the same write
63
+ changes, and later changes are published again as for any account, to
64
+ consumers that consider it deleted.
65
+ - A change made outside LDAP-Rest is not seen. A ppolicy lockout after failed
66
+ binds is written by the directory itself, and holds a timestamp rather than
67
+ the lock value: it publishes nothing, and neither does clearing it.
68
+
69
+ ## Deployments with other plugins
70
+
71
+ - `core/twake/tombstone` answers a second delete of a tombstone without
72
+ writing anything, and fires `twakedeletionreplay`; this plugin then
73
+ publishes `deleted` again, with the tombstone as it is (same deletion date).
74
+ This explicit delete is how a `deleted` event a broker outage lost is
75
+ replayed.
76
+ - `core/twake/cozyProvision` and `core/twake/clouderyProvision` publish their
77
+ own deletion event on `--cozy-user-deleted-routing-key`. When a `deleted`
78
+ rule here publishes it, set that option to `""`. Turning it off without a
79
+ `deleted` rule leaves no deletion event at all; keeping both publishes it
80
+ twice.
81
+ - `core/ldap/trash` moves a deleted entry instead of removing it, and a move
82
+ made by the trash is not seen by `core/ldap/onChange`. A deletion in a
83
+ branch the trash watches publishes nothing.
84
+
85
+ ## Configuration
86
+
87
+ ```bash
88
+ --plugin core/twake/lifecycleEvents \
89
+ --twake-lifecycle-role-attribute title \
90
+ --twake-lifecycle-lock-attribute pwdAccountLockedTime \
91
+ --twake-lifecycle-lock-value 000001010000Z \
92
+ --twake-lifecycle-deleted-attribute employeeType \
93
+ --twake-lifecycle-deleted-value deleted \
94
+ --twake-lifecycle-deleted-at-attribute roomNumber \
95
+ --twake-lifecycle-deleted-at-format iso8601 \
96
+ --twake-lifecycle-rules /etc/ldap-rest/lifecycle-rules.json
97
+ ```
98
+
99
+ - `--twake-lifecycle-role-attribute`: empty means no `roleChanged`.
100
+ - `--twake-lifecycle-lock-attribute`, `--twake-lifecycle-lock-value`: default
101
+ to `--scim-user-lock-attribute` and `--scim-user-lock-value`, so SCIM
102
+ `active` and the events agree; SCIM's value only goes with SCIM's
103
+ attribute. `pwdAccountLockedTime` defaults to `000001010000Z`; any other
104
+ attribute needs its value, or the server does not start, as for
105
+ [SCIM](scim.md).
106
+ Only changes written through LDAP-Rest are seen: a lockout set by the
107
+ directory publishes nothing.
108
+ - `--twake-lifecycle-deleted-attribute`, `--twake-lifecycle-deleted-value`
109
+ (default `TRUE`, compared case-insensitively): what marks a tombstone. Empty
110
+ means entries are never tombstones, and only a removal publishes `deleted`.
111
+ - `--twake-lifecycle-deleted-at-attribute`,
112
+ `--twake-lifecycle-deleted-at-format` (`iso8601`, the default, or
113
+ `generalizedTime`): the deletion date. It is always published as ISO 8601.
114
+ - `--twake-lifecycle-member-attribute` (default `member`): the attribute
115
+ `memberAdded`, `memberRemoved` and member lists read.
116
+ - `--twake-lifecycle-rules`: a JSON file, or the JSON itself.
117
+
118
+ Each option has a `DM_` environment variable, for example
119
+ `DM_TWAKE_LIFECYCLE_RULES`. The events go through `core/rabbitmq`. With rules
120
+ configured, the server does not start without `--rabbitmq-url`, nor when
121
+ that broker cannot be reached. An event that finds no broker later on is
122
+ lost, and logged as an error.
123
+
124
+ ## Rules
125
+
126
+ The rules are an array. The first rule whose `dn` regular expression matches
127
+ the entry's DN (case-insensitively) decides what is published; an entry no
128
+ rule matches publishes nothing.
129
+
130
+ The DN is matched without spaces around its separators, and with escaped
131
+ commas written `\2C`: `uid=a\, b, ou=users,…` is read as `uid=a\2C b,ou=users,…`,
132
+ so `(?<id>[^,]+)` takes the whole value. A `$dn.name` group is unescaped:
133
+ `a, b`.
134
+
135
+ ```json
136
+ [
137
+ {
138
+ "dn": "^uid=(?<id>[^,]+),ou=users,dc=example,dc=com$",
139
+ "exchange": "accounts",
140
+ "payload": {
141
+ "id": "$dn.id",
142
+ "email": "$mail",
143
+ "domain": "$mail|domain",
144
+ "role": "$title",
145
+ "timestamp": "$now"
146
+ },
147
+ "events": {
148
+ "created": "account.created",
149
+ "roleChanged": {
150
+ "routingKey": "account.role.changed",
151
+ "payload": {
152
+ "id": "$dn.id",
153
+ "role": "$title",
154
+ "previousRole": "$previous.title"
155
+ }
156
+ },
157
+ "disabled": "account.disabled",
158
+ "enabled": "account.enabled",
159
+ "deleted": [
160
+ "account.deleted",
161
+ {
162
+ "exchange": "notifications",
163
+ "routingKey": "account.deleted",
164
+ "when": { "$businessCategory": "user_request" },
165
+ "payload": { "id": "$dn.id", "mobile": "$previous.mobile" }
166
+ }
167
+ ]
168
+ }
169
+ }
170
+ ]
171
+ ```
172
+
173
+ An event takes one target or a list of them, published in order. A target is
174
+ a routing key, or an object with:
175
+
176
+ - `routingKey`
177
+ - `exchange`: defaults to the rule's
178
+ - `payload`: replaces the rule's `payload`
179
+ - `when`: publish only if every source equals its value; a value starting with
180
+ `!` means "differs from"
181
+
182
+ Payload and `when` values are sources:
183
+
184
+ - `$attr`: the attribute after the write; for a removed entry, which has no
185
+ "after", the attribute before it
186
+ - `$previous.attr`: the attribute before the write
187
+ - `$dn.name`: a named group of the rule's `dn` expression
188
+ - `$context.actor`, `$context.requestId`, `$context.source`: who made the
189
+ write, the request it belongs to, and the API it came through (`rest`,
190
+ `scim`); left out for a write no request made, such as a scheduled task
191
+ - `$changed.attr`: the attribute after the write, only if the write changed
192
+ it; an attribute the write removed gives `""`
193
+ - `$attr|domain`, `$previous.attr|domain`: what follows the `@`
194
+ - `$now`: the current time, ISO 8601
195
+ - anything else: the value itself
196
+
197
+ `$attr` and `$previous.attr` carry the first value of a multi-valued
198
+ attribute. A source with no value leaves its field out.
199
+
200
+ A payload field can also list members, as an object with one key:
201
+
202
+ - `$members`: the members after the write
203
+ - `$added`, `$removed`: the members `memberAdded` and `memberRemoved` are about
204
+
205
+ ```json
206
+ "members": { "$added": { "username": "$uid", "email": "$mail" } }
207
+ ```
208
+
209
+ Each member is read once, and shaped by the object it maps to, whose
210
+ sources are its own attributes (`$attr`) or plain values; any other source is
211
+ refused at startup. A tombstone is left
212
+ out, and a target whose `$added` or `$removed` is left empty publishes
213
+ nothing. A member no longer in the directory is known by its RDN alone:
214
+ `uid=jdoe,…` gives `{ "username": "jdoe" }` above. A group of 1,000 members
215
+ takes 1,000 reads, one after the other, off the path of the write's
216
+ response. A member that cannot be read, for a reason other than being gone,
217
+ drops that target, logged as an error: a member the service account may not
218
+ read silences every target of the group that lists its members. Every message carries
219
+ a random AMQP `messageId`.
220
+
221
+ A source can read any attribute of the entry: `$userPassword` would put the
222
+ password hash in the message.
223
+
224
+ The rules are checked at startup: a malformed one, such as a payload value
225
+ that is not a string, stops the server with an error naming its `dn`.
226
+
227
+ ## Dependencies
228
+
229
+ ```
230
+ core/twake/lifecycleEvents
231
+ ├─ requires: core/ldap/onChange
232
+ └─ requires: core/rabbitmq
233
+ ```
@@ -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
@@ -305,6 +309,17 @@ mapping contains an entry with `scim: "department"`; otherwise the server return
305
309
  `400 invalidFilter`. Same for PATCH: operations on unmapped paths produce
306
310
  `400 invalidPath`.
307
311
 
312
+ A PATCH `add`, or a `remove` naming values, changes only the values it names:
313
+ values the caller's read did not show, such as group members an
314
+ `ldapsearchfilter` hides, stay. Two PATCHes changing different values of
315
+ the same attribute both apply. Adding a value already there succeeds where
316
+ the directory honours the permissive modify control; removing one another
317
+ write just removed replays the PATCH on a fresh read, then answers `409`.
318
+ A `replace`, or a `remove` of a whole attribute, sets the attribute to what
319
+ the caller sent, and so does a group `PUT` for its members, hidden ones
320
+ included. A group left with no member the caller saw gets the
321
+ `--group-dummy-user` placeholder.
322
+
308
323
  ### Schema advertisement
309
324
 
310
325
  Schemas listed under `GET /scim/v2/Schemas` are read from
@@ -336,16 +351,16 @@ group entries works; use a mapping override if you would rather name it there.
336
351
  SCIM filters are translated to LDAP filters (RFC 4515). Values are always escaped
337
352
  via `escapeLdapFilter()` — no LDAP injection is possible.
338
353
 
339
- | SCIM | LDAP |
340
- | -------------------------------- | ----------------------------- |
341
- | `userName eq "alice"` | `(uid=alice)` |
342
- | `name.familyName sw "Du"` | `(sn=Du*)` |
343
- | `emails.value co "@example.com"` | `(mail=*@example.com*)` |
344
- | `displayName pr` | `(displayName=*)` |
345
- | `active eq true` | `(!(pwdAccountLockedTime=*))` |
346
- | `active pr` | `(objectClass=*)` |
347
- | `a eq "x" and b eq "y"` | `(&(a=x)(b=y))` |
348
- | `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))` |
349
364
 
350
365
  Unknown SCIM attributes raise `400 invalidFilter`.
351
366
 
@@ -605,6 +620,12 @@ Other plugins can observe and transform SCIM operations via the dynamic hooks
605
620
  `scimuserdelete`, `scimuserdeletedone`, and their `scimgroup*` equivalents, plus
606
621
  `scimbulkdone` for Bulk.
607
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
+
608
629
  ## Exposing the configuration to client apps (`configApi`)
609
630
 
610
631
  Load `core/configApi` alongside the SCIM plugin, and `GET /api/v1/config` will
@@ -0,0 +1,107 @@
1
+ # Tombstone
2
+
3
+ `core/twake/tombstone` turns the delete of an account into a tombstone. The
4
+ entry stays, so its identifiers cannot be taken by a new account, but it is
5
+ flagged as deleted, dated, locked, carries the reason, and loses the
6
+ attributes the deployment names.
7
+
8
+ A tombstone is erased later, from an operator's decision: once the deletion
9
+ is old enough, or right away when forced.
10
+
11
+ ## Behaviour
12
+
13
+ - A delete of an entry whose DN matches `--twake-tombstone-dn` writes, in one
14
+ modify: the deleted attribute, the deletion date (now), the reason, the
15
+ lock, and removes `--twake-tombstone-clear-attributes`. Other entries are
16
+ deleted as usual.
17
+ - A delete of an existing tombstone writes nothing and answers as a delete.
18
+ It fires the `twakedeletionreplay` hook with the tombstone, so
19
+ [lifecycle events](lifecycle-events.md) publishes the deletion again, with
20
+ the date of the first one. This is the way to replay a `deleted` event that
21
+ was lost, for instance while the broker was down.
22
+ - The tombstone is written by the server, not as the caller: the caller's
23
+ delete right is what was judged, and a write right is not required on top.
24
+ So the `deleted` event of that write has no `$context.actor`,
25
+ `$context.requestId` or `$context.source`. A replay has them.
26
+ - The tombstone keeps its group memberships until it is erased.
27
+ - SCIM treats a tombstone as gone: reads answer 404, lists leave it out, and
28
+ a create with the same identity erases the tombstone and creates a new
29
+ entry. If the directory then refuses the new entry, the tombstone and its
30
+ memberships are put back once the response is sent.
31
+ - Erasing removes the entry, then its group memberships. It publishes
32
+ nothing.
33
+ - A tombstone cannot be restored. Clearing its deleted attribute by hand is
34
+ not a supported operation.
35
+
36
+ The tombstone is written after every other plugin has judged the delete, so
37
+ a delete that an authorization plugin refuses writes nothing.
38
+
39
+ `core/ldap/trash` moves a deleted entry before this plugin sees the delete,
40
+ so no tombstone would be written in a branch it watches. The server refuses
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.
43
+
44
+ ## Reason
45
+
46
+ The reason is, in order:
47
+
48
+ - the one given to `tombstone(dn, reason, req)` by another plugin;
49
+ - the `--twake-tombstone-reason-header` request header;
50
+ - `--twake-tombstone-default-reason`.
51
+
52
+ When `--twake-tombstone-reasons` is set, any other reason is refused with a 400. Otherwise a reason is letters, digits, `_`, `.` or `-`.
53
+
54
+ ## Erase
55
+
56
+ ```http
57
+ POST /api/v1/twake/tombstones/erase
58
+ Content-Type: application/json
59
+
60
+ { "dn": "uid=jdoe,ou=users,dc=example,dc=com", "force": false }
61
+ ```
62
+
63
+ - `200`: erased.
64
+ - `404`: no tombstone at this DN.
65
+ - `409`: the deletion is younger than `--twake-tombstone-erase-min-age` and
66
+ `force` is not true.
67
+
68
+ Other plugins call `erase(dn, { force, req })`.
69
+
70
+ ## Configuration
71
+
72
+ The tombstone attributes are the lifecycle ones, shared with
73
+ [lifecycle events](lifecycle-events.md):
74
+
75
+ ```bash
76
+ --plugin core/twake/tombstone \
77
+ --twake-lifecycle-lock-attribute pwdAccountLockedTime \
78
+ --twake-lifecycle-lock-value 000001010000Z \
79
+ --twake-lifecycle-deleted-attribute employeeType \
80
+ --twake-lifecycle-deleted-value deleted \
81
+ --twake-lifecycle-deleted-at-attribute roomNumber \
82
+ --twake-lifecycle-deleted-at-format iso8601 \
83
+ --twake-lifecycle-reason-attribute businessCategory \
84
+ --twake-tombstone-dn '^uid=[^,]+,ou=users,dc=example,dc=com$' \
85
+ --twake-tombstone-dn '^uid=[^,]+,ou=[^,]+,ou=organizations,dc=example,dc=com$' \
86
+ --twake-tombstone-reasons deleted,user_request \
87
+ --twake-tombstone-clear-attributes mobile,telephoneNumber \
88
+ --twake-tombstone-erase-min-age 2592000 \
89
+ --twake-tombstone-group-bases ou=groups,dc=example,dc=com
90
+ ```
91
+
92
+ - `--twake-lifecycle-deleted-attribute`: required.
93
+ - `--twake-lifecycle-lock-attribute`, `--twake-lifecycle-lock-value`: the
94
+ lock written on a tombstone, read as
95
+ [lifecycle events](lifecycle-events.md) reads it. A lock attribute other
96
+ than `pwdAccountLockedTime` needs its value (`TRUE` for `nsAccountLock` on
97
+ 389-ds), or the server does not start: `000001010000Z` means nothing there.
98
+ - `--twake-lifecycle-reason-attribute`: empty means the reason is not
99
+ recorded.
100
+ - `--twake-tombstone-dn`: regular expressions, matched case-insensitively. A
101
+ DN holds commas, so in `DM_TWAKE_TOMBSTONE_DN` end each one with `;`.
102
+ - `--twake-tombstone-default-reason`: default `deleted`.
103
+ - `--twake-tombstone-reason-header`: default `x-deletion-reason`.
104
+ - `--twake-tombstone-erase-min-age`: seconds, default 30 days.
105
+ - `--twake-tombstone-group-bases`: where memberships are looked for at erase,
106
+ by `member`. Defaults to `--ldap-group-base`, then `--ldap-base`. In the
107
+ environment variable, end each base with `;`.
@@ -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
  ```