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.
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/authz/composition.js +1 -1
- package/dist/lib/errors.js +1 -1
- package/dist/lib/instanceProviders.js +1 -0
- package/dist/lib/ldapActions.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/configApi.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/scim/filter.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/twake/groups.js +1 -0
- package/dist/plugins/twake/instances.js +1 -0
- package/dist/plugins/twake/lifecycleAttributes.js +1 -0
- package/dist/plugins/twake/lifecycleEvents.js +1 -0
- package/dist/plugins/twake/tombstone.js +1 -0
- package/dist/src/abstract/ldapFlat.d.ts +34 -6
- package/dist/src/abstract/plugin.d.ts +7 -4
- package/dist/src/config/args.d.ts +60 -0
- package/dist/src/hooks.d.ts +2 -0
- package/dist/src/lib/authz/composition.d.ts +1 -1
- package/dist/src/lib/errors.d.ts +3 -0
- package/dist/src/lib/instanceProviders.d.ts +88 -0
- package/dist/src/lib/ldapActions.d.ts +10 -2
- package/dist/src/plugins/auth/openidconnect.d.ts +39 -0
- package/dist/src/plugins/ldap/groups.d.ts +5 -0
- package/dist/src/plugins/scim/filter.d.ts +2 -1
- package/dist/src/plugins/scim/groups.d.ts +1 -0
- package/dist/src/plugins/scim/mapping.d.ts +22 -8
- package/dist/src/plugins/scim/patch.d.ts +9 -2
- package/dist/src/plugins/twake/groups.d.ts +89 -0
- package/dist/src/plugins/twake/instances.d.ts +102 -0
- package/dist/src/plugins/twake/lifecycleAttributes.d.ts +29 -0
- package/dist/src/plugins/twake/lifecycleEvents.d.ts +62 -0
- package/dist/src/plugins/twake/tombstone.d.ts +86 -0
- package/docs/client-development/api/rest-api.md +34 -2
- package/docs/plugin-development/hooks.md +24 -0
- package/docs/usage/configuration.md +97 -1
- package/docs/usage/plugins/README.md +10 -6
- package/docs/usage/plugins/auth/oidc.md +46 -0
- package/docs/usage/plugins/integrations/README.md +12 -4
- package/docs/usage/plugins/integrations/groups.md +225 -0
- package/docs/usage/plugins/integrations/instances.md +150 -0
- package/docs/usage/plugins/integrations/lifecycle-events.md +233 -0
- package/docs/usage/plugins/integrations/scim.md +55 -34
- package/docs/usage/plugins/integrations/tombstone.md +107 -0
- package/docs/usage/plugins/ldap/flat-generic.md +38 -2
- package/docs/usage/plugins/ldap/trash.md +4 -4
- package/docs/usage/upgrading.md +114 -0
- package/package.json +25 -1
- 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
|
|
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
|
|
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
|
|
144
|
-
|
|
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
|
-
|
|
|
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
|
|
188
|
-
|
|
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={
|
|
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
|
|
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`:
|
|
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
|
|
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
|
|
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
|
|
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
|
```
|