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.
Files changed (99) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/abstract/plugin.js +1 -1
  3. package/dist/bin/index.js +1 -1
  4. package/dist/config/args.js +1 -1
  5. package/dist/config/schema.js +1 -0
  6. package/dist/lib/expressFormatedResponses.js +1 -1
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/lib/utils.js +1 -1
  9. package/dist/plugins/auth/authzScope.js +1 -0
  10. package/dist/plugins/auth/llng.js +1 -1
  11. package/dist/plugins/ldap/accountLifecycle.js +1 -0
  12. package/dist/plugins/ldap/bulkImport.js +1 -1
  13. package/dist/plugins/ldap/departmentSync.js +1 -1
  14. package/dist/plugins/ldap/enterpriseRules.js +1 -0
  15. package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
  16. package/dist/plugins/ldap/flatGeneric.js +1 -1
  17. package/dist/plugins/ldap/groups.js +1 -1
  18. package/dist/plugins/ldap/organizations.js +1 -1
  19. package/dist/plugins/scim/bulk.js +1 -1
  20. package/dist/plugins/scim/errors.js +1 -1
  21. package/dist/plugins/scim/filter.js +1 -1
  22. package/dist/plugins/scim/groups.js +1 -1
  23. package/dist/plugins/scim/list.js +1 -1
  24. package/dist/plugins/scim/mapping.js +1 -1
  25. package/dist/plugins/scim/patch.js +1 -1
  26. package/dist/plugins/scim/projection.js +1 -0
  27. package/dist/plugins/scim/scim.js +1 -1
  28. package/dist/plugins/scim/users.js +1 -1
  29. package/dist/plugins/twake/calendar.js +1 -0
  30. package/dist/plugins/twake/calendarResources.js +1 -1
  31. package/dist/plugins/twake/james.js +1 -1
  32. package/dist/src/abstract/ldapFlat.d.ts +159 -1
  33. package/dist/src/abstract/plugin.d.ts +13 -0
  34. package/dist/src/config/args.d.ts +5 -0
  35. package/dist/src/config/schema.d.ts +382 -12
  36. package/dist/src/lib/expressFormatedResponses.d.ts +14 -0
  37. package/dist/src/lib/ldapActions.d.ts +59 -6
  38. package/dist/src/lib/utils.d.ts +65 -0
  39. package/dist/src/plugins/auth/authzScope.d.ts +53 -0
  40. package/dist/src/plugins/auth/llng.d.ts +30 -1
  41. package/dist/src/plugins/ldap/accountLifecycle.d.ts +56 -0
  42. package/dist/src/plugins/ldap/departmentSync.d.ts +43 -0
  43. package/dist/src/plugins/ldap/enterpriseRules.d.ts +436 -0
  44. package/dist/src/plugins/ldap/flatGeneric.d.ts +29 -0
  45. package/dist/src/plugins/ldap/groups.d.ts +22 -0
  46. package/dist/src/plugins/ldap/organizations.d.ts +25 -4
  47. package/dist/src/plugins/scim/errors.d.ts +17 -4
  48. package/dist/src/plugins/scim/filter.d.ts +11 -1
  49. package/dist/src/plugins/scim/groups.d.ts +2 -0
  50. package/dist/src/plugins/scim/list.d.ts +3 -5
  51. package/dist/src/plugins/scim/mapping.d.ts +90 -9
  52. package/dist/src/plugins/scim/patch.d.ts +16 -1
  53. package/dist/src/plugins/scim/projection.d.ts +15 -0
  54. package/dist/src/plugins/scim/scim.d.ts +5 -0
  55. package/dist/src/plugins/scim/users.d.ts +21 -0
  56. package/dist/src/plugins/twake/calendar.d.ts +113 -0
  57. package/dist/src/plugins/twake/calendarResources.d.ts +9 -58
  58. package/docs/README.md +2 -0
  59. package/docs/client-development/api/openapi.md +1 -1
  60. package/docs/client-development/browser/libraries.md +1 -1
  61. package/docs/plugin-development/README.md +22 -0
  62. package/docs/plugin-development/dependencies.md +10 -7
  63. package/docs/presentations/en.md +1 -1
  64. package/docs/presentations/fr.md +1 -1
  65. package/docs/usage/README.md +2 -0
  66. package/docs/usage/configuration.md +35 -15
  67. package/docs/usage/directory-audit.md +105 -0
  68. package/docs/usage/plugins/README.md +6 -6
  69. package/docs/usage/plugins/auth/README.md +6 -5
  70. package/docs/usage/plugins/auth/authz-scope.md +103 -0
  71. package/docs/usage/plugins/auth/llng.md +17 -0
  72. package/docs/usage/plugins/integrations/README.md +5 -5
  73. package/docs/usage/plugins/integrations/{calendar-resources.md → calendar.md} +70 -13
  74. package/docs/usage/plugins/integrations/scim.md +147 -38
  75. package/docs/usage/plugins/ldap/README.md +13 -11
  76. package/docs/usage/plugins/ldap/account-lifecycle.md +129 -0
  77. package/docs/usage/plugins/ldap/bulk-import.md +8 -1
  78. package/docs/usage/plugins/ldap/enterprise-rules.md +167 -0
  79. package/docs/usage/plugins/ldap/flat-generic.md +172 -0
  80. package/docs/usage/plugins/ldap/groups.md +10 -4
  81. package/docs/usage/plugins/ldap/organizations.md +59 -33
  82. package/docs/usage/upgrading.md +444 -0
  83. package/package.json +54 -5
  84. package/static/schemas/example/README.md +36 -0
  85. package/static/schemas/example/groups.json +94 -0
  86. package/static/schemas/example/nomenclature/domains.json +49 -0
  87. package/static/schemas/example/organizations.json +106 -0
  88. package/static/schemas/example/positions.json +35 -0
  89. package/static/schemas/example/users.json +246 -0
  90. package/static/schemas/twake/groups.json +81 -15
  91. package/static/schemas/twake/nomenclature/twakeAccountStatus.json +26 -0
  92. package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +22 -0
  93. package/static/schemas/twake/nomenclature/twakeDomain.json +60 -0
  94. package/static/schemas/twake/nomenclature/twakeListType.json +22 -0
  95. package/static/schemas/twake/nomenclature/twakeMailboxType.json +22 -0
  96. package/static/schemas/twake/nomenclature/twakeTitle.json +42 -0
  97. package/static/schemas/twake/organizations.json +92 -12
  98. package/static/schemas/twake/positions.json +22 -4
  99. 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 | 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-group-object-class` | `DM_SCIM_GROUP_OBJECT_CLASSES` | `top, groupOfNames` | Object classes for created Groups |
55
- | `--scim-group-rdn-attribute` | `DM_SCIM_GROUP_RDN_ATTRIBUTE` | `cn` | RDN attribute for Groups |
56
- | `--scim-id-attribute` | `DM_SCIM_ID_ATTRIBUTE` | `rdn` | `rdn` (default) or `entryUUID` for SCIM `id` |
57
- | `--scim-user-mapping` | `DM_SCIM_USER_MAPPING` | — | Path to JSON mapping override |
58
- | `--scim-group-mapping` | `DM_SCIM_GROUP_MAPPING` | — | Same for Groups |
59
- | `--scim-max-results` | `DM_SCIM_MAX_RESULTS` | `200` | Maximum page size (caps `count`) |
60
- | `--scim-max-scanned` | `DM_SCIM_MAX_SCANNED` | `10000` | Entries a list may walk before answering `400 tooMany` |
61
- | `--scim-bulk-max-operations` | `DM_SCIM_BULK_MAX_OPERATIONS` | `100` | Max `/Bulk` operations per request |
62
- | `--scim-bulk-max-payload-size` | `DM_SCIM_BULK_MAX_PAYLOAD_SIZE` | `1048576` | Max `/Bulk` payload size in bytes |
63
- | `--scim-etag` | `DM_SCIM_ETAG` | `false` | Advertise ETag support in discovery (not yet implemented) |
64
- | `--scim-base-url` | `DM_SCIM_BASE_URL` | auto from request | Override external base URL for `meta.location` values |
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 `pwdAccountLockedTime` is set) |
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 | 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
- | [password-policy](password-policy.md) | Password management | Expiration, Lockout, Unlock |
17
- | [raw](raw.md) | Whole directory (read) | Root DSE, Schema, Tree, Raw search |
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 / organization
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