ldap-rest 0.6.2 → 0.8.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/abstract/plugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/auth/authzScope.js +1 -0
- package/dist/plugins/auth/llng.js +1 -1
- package/dist/plugins/ldap/accountLifecycle.js +1 -0
- package/dist/plugins/ldap/bulkImport.js +1 -1
- package/dist/plugins/ldap/departmentSync.js +1 -1
- package/dist/plugins/ldap/enterpriseRules.js +1 -0
- package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
- package/dist/plugins/ldap/flatGeneric.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/scim/bulk.js +1 -1
- package/dist/plugins/scim/errors.js +1 -1
- package/dist/plugins/scim/filter.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/list.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/scim/projection.js +1 -0
- package/dist/plugins/scim/scim.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/twake/calendar.js +1 -0
- package/dist/plugins/twake/calendarResources.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +159 -1
- package/dist/src/abstract/plugin.d.ts +13 -0
- package/dist/src/config/args.d.ts +5 -0
- package/dist/src/config/schema.d.ts +382 -12
- package/dist/src/lib/expressFormatedResponses.d.ts +14 -0
- package/dist/src/lib/ldapActions.d.ts +59 -6
- package/dist/src/lib/utils.d.ts +65 -0
- package/dist/src/plugins/auth/authzScope.d.ts +53 -0
- package/dist/src/plugins/auth/llng.d.ts +30 -1
- package/dist/src/plugins/ldap/accountLifecycle.d.ts +56 -0
- package/dist/src/plugins/ldap/departmentSync.d.ts +43 -0
- package/dist/src/plugins/ldap/enterpriseRules.d.ts +436 -0
- package/dist/src/plugins/ldap/flatGeneric.d.ts +29 -0
- package/dist/src/plugins/ldap/groups.d.ts +22 -0
- package/dist/src/plugins/ldap/organizations.d.ts +25 -4
- package/dist/src/plugins/scim/errors.d.ts +17 -4
- package/dist/src/plugins/scim/filter.d.ts +11 -1
- package/dist/src/plugins/scim/groups.d.ts +2 -0
- package/dist/src/plugins/scim/list.d.ts +3 -5
- package/dist/src/plugins/scim/mapping.d.ts +90 -9
- package/dist/src/plugins/scim/patch.d.ts +16 -1
- package/dist/src/plugins/scim/projection.d.ts +15 -0
- package/dist/src/plugins/scim/scim.d.ts +5 -0
- package/dist/src/plugins/scim/users.d.ts +21 -0
- package/dist/src/plugins/twake/calendar.d.ts +113 -0
- package/dist/src/plugins/twake/calendarResources.d.ts +9 -58
- package/docs/README.md +2 -0
- package/docs/client-development/api/openapi.md +1 -1
- package/docs/client-development/browser/libraries.md +1 -1
- package/docs/plugin-development/README.md +22 -0
- package/docs/plugin-development/dependencies.md +10 -7
- package/docs/presentations/en.md +1 -1
- package/docs/presentations/fr.md +1 -1
- package/docs/usage/README.md +2 -0
- package/docs/usage/configuration.md +35 -15
- package/docs/usage/directory-audit.md +105 -0
- package/docs/usage/plugins/README.md +6 -6
- package/docs/usage/plugins/auth/README.md +6 -5
- package/docs/usage/plugins/auth/authz-scope.md +103 -0
- package/docs/usage/plugins/auth/llng.md +17 -0
- package/docs/usage/plugins/integrations/README.md +5 -5
- package/docs/usage/plugins/integrations/{calendar-resources.md → calendar.md} +70 -13
- package/docs/usage/plugins/integrations/scim.md +147 -38
- package/docs/usage/plugins/ldap/README.md +13 -11
- package/docs/usage/plugins/ldap/account-lifecycle.md +129 -0
- package/docs/usage/plugins/ldap/bulk-import.md +8 -1
- package/docs/usage/plugins/ldap/enterprise-rules.md +167 -0
- package/docs/usage/plugins/ldap/flat-generic.md +172 -0
- package/docs/usage/plugins/ldap/groups.md +10 -4
- package/docs/usage/plugins/ldap/organizations.md +59 -33
- package/docs/usage/upgrading.md +444 -0
- package/package.json +54 -5
- package/static/schemas/example/README.md +36 -0
- package/static/schemas/example/groups.json +94 -0
- package/static/schemas/example/nomenclature/domains.json +49 -0
- package/static/schemas/example/organizations.json +106 -0
- package/static/schemas/example/positions.json +35 -0
- package/static/schemas/example/users.json +246 -0
- package/static/schemas/twake/groups.json +81 -15
- package/static/schemas/twake/nomenclature/twakeAccountStatus.json +26 -0
- package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +22 -0
- package/static/schemas/twake/nomenclature/twakeDomain.json +60 -0
- package/static/schemas/twake/nomenclature/twakeListType.json +22 -0
- package/static/schemas/twake/nomenclature/twakeMailboxType.json +22 -0
- package/static/schemas/twake/nomenclature/twakeTitle.json +42 -0
- package/static/schemas/twake/organizations.json +92 -12
- package/static/schemas/twake/positions.json +22 -4
- package/static/schemas/twake/users.json +269 -32
|
@@ -41,27 +41,30 @@ All SCIM endpoints sit behind the auth middleware registered before the plugin
|
|
|
41
41
|
|
|
42
42
|
### CLI / environment
|
|
43
43
|
|
|
44
|
-
| Argument
|
|
45
|
-
|
|
|
46
|
-
| `--scim-prefix`
|
|
47
|
-
| `--scim-user-base`
|
|
48
|
-
| `--scim-group-base`
|
|
49
|
-
| `--scim-user-base-template`
|
|
50
|
-
| `--scim-group-base-template`
|
|
51
|
-
| `--scim-base-map`
|
|
52
|
-
| `--scim-user-object-class`
|
|
53
|
-
| `--scim-user-rdn-attribute`
|
|
54
|
-
| `--scim-
|
|
55
|
-
| `--scim-
|
|
56
|
-
| `--scim-
|
|
57
|
-
| `--scim-
|
|
58
|
-
| `--scim-group-
|
|
59
|
-
| `--scim-
|
|
60
|
-
| `--scim-
|
|
61
|
-
| `--scim-
|
|
62
|
-
| `--scim-
|
|
63
|
-
| `--scim-
|
|
64
|
-
| `--scim-
|
|
44
|
+
| Argument | Environment | Default | Description |
|
|
45
|
+
| ------------------------------------ | ------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
46
|
+
| `--scim-prefix` | `DM_SCIM_PREFIX` | `/scim/v2` | Base URL path for all SCIM endpoints |
|
|
47
|
+
| `--scim-user-base` | `DM_SCIM_USER_BASE` | `{ldap_base}` | LDAP branch containing users |
|
|
48
|
+
| `--scim-group-base` | `DM_SCIM_GROUP_BASE` | `{ldap_base}` | LDAP branch containing groups |
|
|
49
|
+
| `--scim-user-base-template` | `DM_SCIM_USER_BASE_TEMPLATE` | — | DN template with `{user}` placeholder (see _multi-tenant_) |
|
|
50
|
+
| `--scim-group-base-template` | `DM_SCIM_GROUP_BASE_TEMPLATE` | — | Same for groups |
|
|
51
|
+
| `--scim-base-map` | `DM_SCIM_BASE_MAP` | — | Path to JSON file mapping authenticated user → `{userBase, groupBase}` |
|
|
52
|
+
| `--scim-user-object-class` | `DM_SCIM_USER_OBJECT_CLASSES` | `top, inetOrgPerson, organizationalPerson, person` | Object classes for created Users |
|
|
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` |
|
|
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
|
+
| `--scim-group-object-class` | `DM_SCIM_GROUP_OBJECT_CLASSES` | `top, groupOfNames` | Object classes for created Groups |
|
|
57
|
+
| `--scim-group-rdn-attribute` | `DM_SCIM_GROUP_RDN_ATTRIBUTE` | `cn` | RDN attribute for Groups |
|
|
58
|
+
| `--scim-group-external-id-attribute` | `DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE` | — | LDAP attribute storing a Group's `externalId` (unset: not supported) |
|
|
59
|
+
| `--scim-id-attribute` | `DM_SCIM_ID_ATTRIBUTE` | `rdn` | `rdn` (default) or `entryUUID` for SCIM `id` |
|
|
60
|
+
| `--scim-user-mapping` | `DM_SCIM_USER_MAPPING` | — | Path to JSON mapping override |
|
|
61
|
+
| `--scim-group-mapping` | `DM_SCIM_GROUP_MAPPING` | — | Same for Groups |
|
|
62
|
+
| `--scim-max-results` | `DM_SCIM_MAX_RESULTS` | `200` | Maximum page size (caps `count`) |
|
|
63
|
+
| `--scim-max-scanned` | `DM_SCIM_MAX_SCANNED` | `10000` | Entries a list may walk before answering `400 tooMany` |
|
|
64
|
+
| `--scim-bulk-max-operations` | `DM_SCIM_BULK_MAX_OPERATIONS` | `100` | Max `/Bulk` operations per request |
|
|
65
|
+
| `--scim-bulk-max-payload-size` | `DM_SCIM_BULK_MAX_PAYLOAD_SIZE` | `1048576` | Max `/Bulk` payload size in bytes |
|
|
66
|
+
| `--scim-etag` | `DM_SCIM_ETAG` | `false` | Advertise ETag support in discovery (not yet implemented) |
|
|
67
|
+
| `--scim-base-url` | `DM_SCIM_BASE_URL` | auto from request | Override external base URL for `meta.location` values |
|
|
65
68
|
|
|
66
69
|
### Authorization
|
|
67
70
|
|
|
@@ -110,27 +113,91 @@ All responses have `Content-Type: application/scim+json`.
|
|
|
110
113
|
|
|
111
114
|
### Default mapping (LDAP ⇄ SCIM User)
|
|
112
115
|
|
|
113
|
-
| SCIM | LDAP
|
|
114
|
-
| ------------------------------------ |
|
|
115
|
-
| `id` | `uid` (or `entryUUID`)
|
|
116
|
-
| `userName` | `uid`
|
|
117
|
-
| `externalId` | `employeeNumber`
|
|
118
|
-
| `name.familyName` | `sn`
|
|
119
|
-
| `name.givenName` | `givenName`
|
|
120
|
-
| `name.formatted` | `cn`
|
|
121
|
-
| `displayName` | `displayName`
|
|
122
|
-
| `title` | `title`
|
|
123
|
-
| `preferredLanguage` | `preferredLanguage`
|
|
124
|
-
| `emails[primary=true].value` | `mail`
|
|
125
|
-
| `emails[primary=false].value` | `mailAlternateAddress`
|
|
126
|
-
| `phoneNumbers[primary=true].value` | `telephoneNumber`
|
|
127
|
-
| `phoneNumbers[primary=false].value` | `mobile`
|
|
128
|
-
| `active` | pseudo (false if
|
|
129
|
-
| `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 is set) |
|
|
132
|
+
| `meta.created` / `meta.lastModified` | `createTimestamp` / `modifyTimestamp` |
|
|
130
133
|
|
|
131
134
|
Override with `--scim-user-mapping /path/to/user-mapping.json` (same schema as
|
|
132
135
|
`static/schemas/scim/default-mapping.json`).
|
|
133
136
|
|
|
137
|
+
## Deactivating an account (`active`)
|
|
138
|
+
|
|
139
|
+
SCIM models account status with the boolean `active` (RFC 7643 §4.1.1), and it
|
|
140
|
+
is how Okta, Entra ID and JumpCloud disable a user — they PATCH `active` to
|
|
141
|
+
`false` rather than DELETE the resource.
|
|
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.
|
|
145
|
+
|
|
146
|
+
| SCIM | LDAP |
|
|
147
|
+
| ----------------- | ----------------------------------------------- |
|
|
148
|
+
| `"active": false` | write `--scim-user-lock-value` to the attribute |
|
|
149
|
+
| `"active": true` | remove the attribute |
|
|
150
|
+
| attribute absent | `"active": true` |
|
|
151
|
+
|
|
152
|
+
The default targets the ppolicy overlay:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
--scim-user-lock-attribute pwdAccountLockedTime
|
|
156
|
+
--scim-user-lock-value 000001010000Z # "locked forever"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`pwdAccountLockedTime` only exists when slapd loads the ppolicy overlay. On a
|
|
160
|
+
directory without it — plain OpenLDAP, 389-ds, AD — the directory refuses the
|
|
161
|
+
write against its schema, and the plugin answers `400 invalidValue` naming
|
|
162
|
+
the attribute and the flags to change, on `/Bulk` as on the single-resource
|
|
163
|
+
routes. Point the two flags elsewhere:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
--scim-user-lock-attribute nsAccountLock
|
|
167
|
+
--scim-user-lock-value TRUE
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Name the value whenever you name the attribute.** `000001010000Z` is the
|
|
171
|
+
ppolicy convention and means nothing to any other attribute: 389-ds reads
|
|
172
|
+
`nsAccountLock` as locked only when it says `true`. Setting the attribute
|
|
173
|
+
alone would write a value the directory ignores — every deactivation answers
|
|
174
|
+
200 and reads back `active: false` while the account keeps binding. The
|
|
175
|
+
plugin refuses to start instead.
|
|
176
|
+
|
|
177
|
+
Two more pairings get a warning at startup rather than a refusal, because
|
|
178
|
+
neither can be told apart from a deliberate local schema:
|
|
179
|
+
|
|
180
|
+
- the ppolicy value `000001010000Z` pinned on some other attribute, which is
|
|
181
|
+
what a deployment template that always sets both flags produces by default
|
|
182
|
+
- a value `pwdAccountLockedTime` cannot hold, `TRUE` for instance: it holds a
|
|
183
|
+
GeneralizedTime
|
|
184
|
+
|
|
185
|
+
**None of this can tell whether your directory honours the value you chose.**
|
|
186
|
+
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
|
|
189
|
+
actually prevents a bind.
|
|
190
|
+
|
|
191
|
+
`active` is not a mapping entry and must not be added to a mapping file: it is
|
|
192
|
+
handled by these two flags on every path — create, PUT, PATCH and the
|
|
193
|
+
`active eq true|false` filter alike.
|
|
194
|
+
|
|
195
|
+
PUT is a full replacement (RFC 7644 §3.5.1) for every attribute except this
|
|
196
|
+
one: a body that omits `active` leaves the lock as it stands. Clearing it
|
|
197
|
+
would release locks SCIM never set — a ppolicy auto-lockout, or one an
|
|
198
|
+
administrator placed — so a routine profile sync would defeat the
|
|
199
|
+
brute-force control. Send `"active": true` to reactivate deliberately.
|
|
200
|
+
|
|
134
201
|
## Writing a custom mapping schema
|
|
135
202
|
|
|
136
203
|
The mapping file is a plain JSON document describing how each SCIM attribute is
|
|
@@ -247,6 +314,23 @@ adjust the loader). Exposing a schema there is **independent** from the
|
|
|
247
314
|
LDAP↔SCIM projection: the schema file describes the public SCIM contract, the
|
|
248
315
|
mapping file describes the internal translation.
|
|
249
316
|
|
|
317
|
+
### Group `externalId`
|
|
318
|
+
|
|
319
|
+
RFC 7643 §3.1 defines `externalId` as the **provisioning client's** identifier —
|
|
320
|
+
the id Okta or Entra ID keeps for its own copy of the resource, which it later
|
|
321
|
+
uses to find the group again (`filter=externalId eq "…"`).
|
|
322
|
+
|
|
323
|
+
For Users it lands in `employeeNumber`. groupOfNames has no attribute meant to
|
|
324
|
+
hold one, so nothing is stored unless the deployment names an attribute:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
--scim-group-external-id-attribute description
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Without it, `externalId` sent on a Group is ignored and a filter on it answers
|
|
331
|
+
`400 invalidFilter` — which is at least honest. Any free attribute of your
|
|
332
|
+
group entries works; use a mapping override if you would rather name it there.
|
|
333
|
+
|
|
250
334
|
## Filter syntax
|
|
251
335
|
|
|
252
336
|
SCIM filters are translated to LDAP filters (RFC 4515). Values are always escaped
|
|
@@ -259,6 +343,7 @@ via `escapeLdapFilter()` — no LDAP injection is possible.
|
|
|
259
343
|
| `emails.value co "@example.com"` | `(mail=*@example.com*)` |
|
|
260
344
|
| `displayName pr` | `(displayName=*)` |
|
|
261
345
|
| `active eq true` | `(!(pwdAccountLockedTime=*))` |
|
|
346
|
+
| `active pr` | `(objectClass=*)` |
|
|
262
347
|
| `a eq "x" and b eq "y"` | `(&(a=x)(b=y))` |
|
|
263
348
|
| `not (a eq "x")` | `(!(a=x))` |
|
|
264
349
|
|
|
@@ -297,6 +382,30 @@ willing to calculate or process". Raise the flag, or narrow the query.
|
|
|
297
382
|
If the directory's own size limit is lower than the collection, it refuses the
|
|
298
383
|
search first and the answer is the same `tooMany` — raise `sizelimit` in slapd.
|
|
299
384
|
|
|
385
|
+
## Partial representations (`attributes` / `excludedAttributes`)
|
|
386
|
+
|
|
387
|
+
Any route that answers a resource accepts the two RFC 7644 §3.9 parameters —
|
|
388
|
+
the lists, the single-resource GETs, and the POST/PUT/PATCH answers alike.
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
curl '.../Users/alice?attributes=userName,name.familyName'
|
|
392
|
+
curl '.../Users?excludedAttributes=emails'
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
- `attributes` replaces the default set: only what you name comes back.
|
|
396
|
+
- `excludedAttributes` keeps the default set minus what you name.
|
|
397
|
+
- Both accept standard attribute notation, so `name.familyName` and
|
|
398
|
+
`emails.value` narrow a complex or multi-valued attribute rather than
|
|
399
|
+
dropping it whole, and a core schema URN prefix is accepted and stripped.
|
|
400
|
+
- `id` and `schemas` are `returned: always` (RFC 7643 §7): they survive an
|
|
401
|
+
exclusion and are added back to an `attributes` list.
|
|
402
|
+
- `meta` is `returned: default`, so an `attributes` list that does not name it
|
|
403
|
+
does not get it. Ask for `meta` explicitly if you need `meta.location`.
|
|
404
|
+
- The two are mutually exclusive; sending both answers `400 invalidValue`.
|
|
405
|
+
|
|
406
|
+
The projection applies to the response only — the LDAP search still reads the
|
|
407
|
+
attributes the mapping needs.
|
|
408
|
+
|
|
300
409
|
## Multi-tenant: per-user LDAP bases
|
|
301
410
|
|
|
302
411
|
SCIM does not expose any tenant concept to clients — resource `id`s are opaque
|
|
@@ -4,17 +4,19 @@ Plugins for LDAP entity management.
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
| Plugin
|
|
8
|
-
|
|
|
9
|
-
| [flat-generic](flat-generic.md)
|
|
10
|
-
| [groups](groups.md)
|
|
11
|
-
| [organizations](organizations.md)
|
|
12
|
-
| [bulk-import](bulk-import.md)
|
|
13
|
-
| [trash](trash.md)
|
|
14
|
-
| [external-users](external-users.md)
|
|
15
|
-
| [on-change](on-change.md)
|
|
16
|
-
| [
|
|
17
|
-
| [
|
|
7
|
+
| Plugin | Entity | Features |
|
|
8
|
+
| ----------------------------------------- | ------------------------ | --------------------------------------- |
|
|
9
|
+
| [flat-generic](flat-generic.md) | Users, Positions, Custom | Schema-driven, Validation, Pointers |
|
|
10
|
+
| [groups](groups.md) | Groups | Member validation, Nested groups |
|
|
11
|
+
| [organizations](organizations.md) | Organizational Units | Tree navigation, Search |
|
|
12
|
+
| [bulk-import](bulk-import.md) | All (bulk operations) | CSV import, Template generation |
|
|
13
|
+
| [trash](trash.md) | All (soft delete) | Trash system, Recovery |
|
|
14
|
+
| [external-users](external-users.md) | Contacts | Automatic creation |
|
|
15
|
+
| [on-change](on-change.md) | All | Change detection |
|
|
16
|
+
| [enterprise-rules](enterprise-rules.md) | All (business rules) | Uniqueness, Mail domains, Delete guards |
|
|
17
|
+
| [account-lifecycle](account-lifecycle.md) | Accounts | Enable/disable, Password reset |
|
|
18
|
+
| [password-policy](password-policy.md) | Password management | Expiration, Lockout, Unlock |
|
|
19
|
+
| [raw](raw.md) | Whole directory (read) | Root DSE, Schema, Tree, Raw search |
|
|
18
20
|
|
|
19
21
|
## API Endpoints
|
|
20
22
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# LDAP Account Lifecycle Plugin
|
|
2
|
+
|
|
3
|
+
The two account operations an administrator performs most often — change an
|
|
4
|
+
account's state, and reset its password — as first-class endpoints.
|
|
5
|
+
|
|
6
|
+
Both are expressed against semantic roles, never attribute names. What
|
|
7
|
+
"disabled" _is_ in a given directory — a DN in a nomenclature, a string, a
|
|
8
|
+
boolean — is schema configuration.
|
|
9
|
+
|
|
10
|
+
## Configuration
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
--plugin core/ldap/accountLifecycle
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The plugin loads `core/ldap/flatGeneric` if it is not already there, and adds
|
|
17
|
+
endpoints to each entity whose schema asks for them:
|
|
18
|
+
|
|
19
|
+
- `POST /api/v1/ldap/{pluralName}/{id}/status` when the schema declares an
|
|
20
|
+
`accountStatus` attribute carrying named `states`
|
|
21
|
+
- `POST /api/v1/ldap/{pluralName}/{id}/password` when it declares a `password`
|
|
22
|
+
attribute
|
|
23
|
+
|
|
24
|
+
Changes go through the entity's own modify path, so schema validation, hooks
|
|
25
|
+
and authorization apply exactly as they do to any other update.
|
|
26
|
+
|
|
27
|
+
## Declaring the states
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"twakeAccountStatus": {
|
|
32
|
+
"type": "pointer",
|
|
33
|
+
"branch": ["ou=twakeAccountStatus,ou=nomenclature,dc=example,dc=com"],
|
|
34
|
+
"role": "accountStatus",
|
|
35
|
+
"generated": true,
|
|
36
|
+
"default": "cn=active,ou=twakeAccountStatus,ou=nomenclature,dc=example,dc=com",
|
|
37
|
+
"states": {
|
|
38
|
+
"enabled": "cn=active,ou=twakeAccountStatus,ou=nomenclature,dc=example,dc=com",
|
|
39
|
+
"disabled": "cn=disabled,ou=twakeAccountStatus,ou=nomenclature,dc=example,dc=com",
|
|
40
|
+
"noAccess": "cn=noaccess,ou=twakeAccountStatus,ou=nomenclature,dc=example,dc=com",
|
|
41
|
+
"toDelete": "cn=todelete,ou=twakeAccountStatus,ou=nomenclature,dc=example,dc=com"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`enabled` and `disabled` are the names the API speaks; a deployment adds its
|
|
48
|
+
own freely. `default` is what a new account gets, applied by
|
|
49
|
+
[enterprise-rules](enterprise-rules.md).
|
|
50
|
+
|
|
51
|
+
## Changing the state
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
curl -X POST http://localhost:8081/api/v1/ldap/users/alice/status \
|
|
55
|
+
-H 'Content-Type: application/json' \
|
|
56
|
+
-d '{"state": "disabled"}'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{ "success": true, "state": "disabled" }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
An unknown state is refused with a `400` that names the ones this directory
|
|
64
|
+
knows, so a client never has to hardcode the list:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"error": "Unknown state \"retired\" for user; known states: enabled, disabled, noAccess, toDelete"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Resetting the password
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
curl -X POST http://localhost:8081/api/v1/ldap/users/alice/password \
|
|
76
|
+
-H 'Content-Type: application/json' -d '{}'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{ "success": true, "generated": true, "forceChange": true, "password": "xY3k…" }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
With no `password` in the body, one is generated and returned **once** — the
|
|
84
|
+
credential attributes are marked `neverReturn` in the schema precisely so a
|
|
85
|
+
later read cannot hand it back. Supply `password` to set a chosen one; the
|
|
86
|
+
answer then carries `"generated": false` and does not echo it.
|
|
87
|
+
|
|
88
|
+
A `password` that is present has to be a non-empty string. An empty one — or
|
|
89
|
+
`null`, or anything that is not text — is refused with a `400`:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"error": "Field password must be a non-empty string; omit it to have one generated"
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
It used to be taken as an absent one, so `{"password": ""}` answered
|
|
98
|
+
`success` after setting a random credential the caller never saw: a client
|
|
99
|
+
that emptied the field by accident believed it had set the password it typed.
|
|
100
|
+
Omitting the field is how a generated password is asked for.
|
|
101
|
+
|
|
102
|
+
`forceChange` (true by default) also writes the `passwordReset`-role attribute
|
|
103
|
+
so the directory asks for a new password at next login. For a boolean
|
|
104
|
+
attribute the values written are `TRUE` and `FALSE`; a directory that spells
|
|
105
|
+
them otherwise says so through `states.required` and `states.cleared`.
|
|
106
|
+
|
|
107
|
+
| Field | Meaning |
|
|
108
|
+
| ------------- | --------------------------------------------------- |
|
|
109
|
+
| `generated` | The server chose the password |
|
|
110
|
+
| `password` | Present only when generated |
|
|
111
|
+
| `forceChange` | Whether the "change at next login" flag was written |
|
|
112
|
+
|
|
113
|
+
> **Note** — `pwdReset` is an operational attribute of the OpenLDAP `ppolicy`
|
|
114
|
+
> overlay. Writing it requires the overlay to be loaded; without it the
|
|
115
|
+
> directory answers `attribute type undefined`.
|
|
116
|
+
|
|
117
|
+
## Errors
|
|
118
|
+
|
|
119
|
+
| Status | When |
|
|
120
|
+
| ------ | -------------------------------------------------------------- |
|
|
121
|
+
| `400` | Unknown state, or a body with no `state` |
|
|
122
|
+
| `400` | A `password` that is present but empty, `null` or not a string |
|
|
123
|
+
| `404` | No such account |
|
|
124
|
+
|
|
125
|
+
## See also
|
|
126
|
+
|
|
127
|
+
- [flat-generic](flat-generic.md) — `role`, `states` and `neverReturn`
|
|
128
|
+
- [enterprise-rules](enterprise-rules.md) — the default state of a new account
|
|
129
|
+
- [password-policy](password-policy.md) — expiry, lockout and complexity
|
|
@@ -137,6 +137,13 @@ jdoe,John Doe,Doe,John,john.doe@example.com,password123,ou=IT,ou=organization,dc
|
|
|
137
137
|
asmith,Alice Smith,Smith,Alice,alice.smith@example.com,secret456,ou=HR,ou=organization,dc=example,dc=com
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
A value must sit under a column whose name is a syntactically valid LDAP
|
|
141
|
+
attribute name (`mail`, `2.5.4.3`, `cn;lang-fr`). Otherwise its line fails
|
|
142
|
+
with `Invalid column name` and is counted in `failed`. The check is on syntax
|
|
143
|
+
only: an unknown attribute such as `maill` passes it and fails on the
|
|
144
|
+
directory side. Empty cells are ignored whatever their column is called, so a
|
|
145
|
+
comment column left empty, or a trailing comma, does not prevent the import.
|
|
146
|
+
|
|
140
147
|
### Special Column: `organizationDn`
|
|
141
148
|
|
|
142
149
|
The `organizationDn` column is special and triggers automatic calculation of:
|
|
@@ -289,7 +296,7 @@ mail: john.doe@example.com
|
|
|
289
296
|
userPassword: {SSHA}...
|
|
290
297
|
telephoneNumber: +1-555-0100
|
|
291
298
|
twakeDepartmentLink: ou=Engineering,ou=organization,dc=example,dc=com
|
|
292
|
-
twakeDepartmentPath: Engineering
|
|
299
|
+
twakeDepartmentPath: Engineering
|
|
293
300
|
```
|
|
294
301
|
|
|
295
302
|
## Operation Modes
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# LDAP Enterprise Rules Plugin
|
|
2
|
+
|
|
3
|
+
The business rules an enterprise directory needs on top of per-attribute
|
|
4
|
+
validation: uniqueness across a shared namespace, mail addresses confined to
|
|
5
|
+
the domains an organization owns, computed organization paths, normalised
|
|
6
|
+
quotas, and referential-integrity guards on deletion.
|
|
7
|
+
|
|
8
|
+
Every rule is driven by the loaded entity schemas. The plugin contains no
|
|
9
|
+
attribute name, no domain and no nomenclature value, so the same code serves a
|
|
10
|
+
directory laid out differently without a change.
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
--plugin core/ldap/enterpriseRules
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
It reads the schemas already loaded by `core/ldap/flatGeneric`,
|
|
19
|
+
`core/ldap/groups` and `core/ldap/organizations`, so load it alongside them.
|
|
20
|
+
The organizations plugin only exposes its schema when one is given:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
--organization-schema ./static/schemas/twake/organizations.json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Option | Default | Purpose |
|
|
27
|
+
| ------------------------------------ | ------------------- | ------------------------------------------------------------------- |
|
|
28
|
+
| `--enterprise-domain-name-attribute` | `associatedDomain` | Fallback when no loaded schema declares a `domainName` role |
|
|
29
|
+
| `--enterprise-domain-link-attribute` | _(from the schema)_ | Fallback when the organization schema declares no `domainLink` role |
|
|
30
|
+
| `--group-dummy-user` | `cn=fakeuser` | Placeholder member that does not count towards `deleteGuard` |
|
|
31
|
+
|
|
32
|
+
## Order of evaluation
|
|
33
|
+
|
|
34
|
+
Checks run **before** transformations, inside the same hook. A check therefore
|
|
35
|
+
always sees the payload as the client sent it: uniqueness and domain rules
|
|
36
|
+
validate the address that was submitted, never one the server has already
|
|
37
|
+
rewritten, and an identifier derived from that address cannot influence the
|
|
38
|
+
check that justified it.
|
|
39
|
+
|
|
40
|
+
## Rules
|
|
41
|
+
|
|
42
|
+
### Uniqueness
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"employeeNumber": { "type": "string", "unique": true },
|
|
47
|
+
"mail": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"role": "primaryEmail",
|
|
50
|
+
"unique": {
|
|
51
|
+
"attributes": ["mailAlternateAddress"],
|
|
52
|
+
"branches": ["dc=example,dc=com"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- `attributes` names the other attributes sharing the value namespace — a mail
|
|
59
|
+
address must not already be someone's alias.
|
|
60
|
+
- `branches` widens the search beyond the entity's own branch, which is what
|
|
61
|
+
makes an address unique across accounts _and_ distribution lists.
|
|
62
|
+
- `sentinel` exempts one value, for directories that use a placeholder on
|
|
63
|
+
non-individual entries.
|
|
64
|
+
- `filter` narrows the search, e.g. `(objectClass=twakeAccount)`.
|
|
65
|
+
|
|
66
|
+
On an update the entry being modified is excluded from the search, so
|
|
67
|
+
re-sending an unchanged value never conflicts with itself.
|
|
68
|
+
|
|
69
|
+
### Mail domains
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mail": {
|
|
74
|
+
"mailDomainScope": "organization",
|
|
75
|
+
"allowSubdomains": true
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`organization` walks up from the entry's `organizationLink`, collecting the
|
|
81
|
+
domains each ancestor declares through its `domainLink` role, up to the root of
|
|
82
|
+
the tree. `directory` accepts any domain declared anywhere. When nobody
|
|
83
|
+
declares a domain, no restriction applies; a domain entry whose name is `*`
|
|
84
|
+
authorises every domain.
|
|
85
|
+
|
|
86
|
+
The host is taken after the **last** `@`, so a domain that merely ends with an
|
|
87
|
+
authorised one (`evil-example.org` against `example.org`) is refused.
|
|
88
|
+
`allowSubdomains` is what lets a distribution list live at
|
|
89
|
+
`all@lists.example.org` under an organization that owns `example.org`.
|
|
90
|
+
|
|
91
|
+
### Computed organization path
|
|
92
|
+
|
|
93
|
+
An entity declaring both an `organizationLink` and an `organizationPath` role
|
|
94
|
+
gets its path filled from the organization it points at, on creation and
|
|
95
|
+
whenever the link changes. Mark the path `generated` so a client cannot set it.
|
|
96
|
+
|
|
97
|
+
### Normalised sizes
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mailQuotaSize": {
|
|
102
|
+
"type": "number",
|
|
103
|
+
"normalize": "byteSize",
|
|
104
|
+
"default": 1000000000,
|
|
105
|
+
"hint": "Expected pattern xGB, xMB, xKB or a number of bytes"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`5GB`, `500 MB`, `2KB` and `2048` all become a number of bytes, with decimal
|
|
111
|
+
multipliers (1 GB = 10⁹ bytes) as mail servers count them. The same conversion
|
|
112
|
+
applies to creations and to updates.
|
|
113
|
+
|
|
114
|
+
### Server-owned defaults
|
|
115
|
+
|
|
116
|
+
On creation, an attribute that is `generated` or `normalize`d and carries a
|
|
117
|
+
`default` is filled when the client said nothing — which is how a new account
|
|
118
|
+
lands in its initial state:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"twakeAccountStatus": {
|
|
123
|
+
"type": "pointer",
|
|
124
|
+
"role": "accountStatus",
|
|
125
|
+
"generated": true,
|
|
126
|
+
"default": "cn=active,ou=twakeAccountStatus,ou=nomenclature,dc=example,dc=com"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Scheduled removal
|
|
132
|
+
|
|
133
|
+
An `accountExpiry` attribute must not carry a date already past. The comparison
|
|
134
|
+
is against the start of the current day, so scheduling something for today is
|
|
135
|
+
allowed.
|
|
136
|
+
|
|
137
|
+
### Deletion guards
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"title": { "type": "pointer", "referentialIntegrity": "restrict" },
|
|
142
|
+
"member": { "type": "array", "role": "members", "deleteGuard": "nonEmpty" }
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- `referentialIntegrity: "restrict"` refuses to delete an entry while another
|
|
147
|
+
entry still points at it — a position somebody still holds, a nomenclature
|
|
148
|
+
value still in use. Declared on the **referencing** attribute, and the
|
|
149
|
+
pointer's `branch` tells the plugin which deletions to check.
|
|
150
|
+
- `deleteGuard: "nonEmpty"` refuses to delete a group that still has members.
|
|
151
|
+
The placeholder member some directories keep to satisfy `groupOfNames`
|
|
152
|
+
(`--group-dummy-user`) does not count.
|
|
153
|
+
|
|
154
|
+
Both default to off: a deployment turns them on where it wants them.
|
|
155
|
+
|
|
156
|
+
## Errors
|
|
157
|
+
|
|
158
|
+
| Status | When |
|
|
159
|
+
| ------ | ----------------------------------------------------------------------------- |
|
|
160
|
+
| `400` | A size that is not a size, a date already past |
|
|
161
|
+
| `409` | A duplicate value, a mail domain outside the authorised set, a guarded delete |
|
|
162
|
+
|
|
163
|
+
## See also
|
|
164
|
+
|
|
165
|
+
- [flat-generic](flat-generic.md) — the schema format these markers extend
|
|
166
|
+
- [account-lifecycle](account-lifecycle.md) — status changes and password resets
|
|
167
|
+
- [organizations](organizations.md) — the tree the domain rule walks up
|