ldap-rest 0.7.0 → 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 (79) 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/groups.js +1 -1
  20. package/dist/plugins/twake/calendar.js +1 -0
  21. package/dist/plugins/twake/calendarResources.js +1 -1
  22. package/dist/plugins/twake/james.js +1 -1
  23. package/dist/src/abstract/ldapFlat.d.ts +159 -1
  24. package/dist/src/abstract/plugin.d.ts +13 -0
  25. package/dist/src/config/args.d.ts +2 -0
  26. package/dist/src/config/schema.d.ts +382 -12
  27. package/dist/src/lib/expressFormatedResponses.d.ts +14 -0
  28. package/dist/src/lib/ldapActions.d.ts +33 -6
  29. package/dist/src/lib/utils.d.ts +65 -0
  30. package/dist/src/plugins/auth/authzScope.d.ts +53 -0
  31. package/dist/src/plugins/auth/llng.d.ts +30 -1
  32. package/dist/src/plugins/ldap/accountLifecycle.d.ts +56 -0
  33. package/dist/src/plugins/ldap/departmentSync.d.ts +43 -0
  34. package/dist/src/plugins/ldap/enterpriseRules.d.ts +436 -0
  35. package/dist/src/plugins/ldap/flatGeneric.d.ts +29 -0
  36. package/dist/src/plugins/ldap/groups.d.ts +22 -0
  37. package/dist/src/plugins/ldap/organizations.d.ts +25 -4
  38. package/dist/src/plugins/twake/calendar.d.ts +113 -0
  39. package/dist/src/plugins/twake/calendarResources.d.ts +9 -58
  40. package/docs/README.md +1 -0
  41. package/docs/client-development/api/openapi.md +1 -1
  42. package/docs/client-development/browser/libraries.md +1 -1
  43. package/docs/plugin-development/dependencies.md +10 -7
  44. package/docs/presentations/en.md +1 -1
  45. package/docs/presentations/fr.md +1 -1
  46. package/docs/usage/README.md +1 -0
  47. package/docs/usage/configuration.md +35 -15
  48. package/docs/usage/directory-audit.md +105 -0
  49. package/docs/usage/plugins/README.md +6 -6
  50. package/docs/usage/plugins/auth/README.md +6 -5
  51. package/docs/usage/plugins/auth/authz-scope.md +103 -0
  52. package/docs/usage/plugins/auth/llng.md +17 -0
  53. package/docs/usage/plugins/integrations/README.md +5 -5
  54. package/docs/usage/plugins/integrations/{calendar-resources.md → calendar.md} +70 -13
  55. package/docs/usage/plugins/ldap/README.md +13 -11
  56. package/docs/usage/plugins/ldap/account-lifecycle.md +129 -0
  57. package/docs/usage/plugins/ldap/bulk-import.md +8 -1
  58. package/docs/usage/plugins/ldap/enterprise-rules.md +167 -0
  59. package/docs/usage/plugins/ldap/flat-generic.md +172 -0
  60. package/docs/usage/plugins/ldap/groups.md +10 -4
  61. package/docs/usage/plugins/ldap/organizations.md +59 -33
  62. package/docs/usage/upgrading.md +322 -0
  63. package/package.json +50 -5
  64. package/static/schemas/example/README.md +36 -0
  65. package/static/schemas/example/groups.json +94 -0
  66. package/static/schemas/example/nomenclature/domains.json +49 -0
  67. package/static/schemas/example/organizations.json +106 -0
  68. package/static/schemas/example/positions.json +35 -0
  69. package/static/schemas/example/users.json +246 -0
  70. package/static/schemas/twake/groups.json +81 -15
  71. package/static/schemas/twake/nomenclature/twakeAccountStatus.json +26 -0
  72. package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +22 -0
  73. package/static/schemas/twake/nomenclature/twakeDomain.json +60 -0
  74. package/static/schemas/twake/nomenclature/twakeListType.json +22 -0
  75. package/static/schemas/twake/nomenclature/twakeMailboxType.json +22 -0
  76. package/static/schemas/twake/nomenclature/twakeTitle.json +42 -0
  77. package/static/schemas/twake/organizations.json +92 -12
  78. package/static/schemas/twake/positions.json +22 -4
  79. package/static/schemas/twake/users.json +269 -32
@@ -11,10 +11,10 @@ Plugins for connecting LDAP-Rest to external systems.
11
11
 
12
12
  ## Twake
13
13
 
14
- | Plugin | Description |
15
- | ------------------------------------------- | ---------------------------------------- |
16
- | [calendar-resources](calendar-resources.md) | Calendar resource synchronization |
17
- | [app-accounts](app-accounts.md) | Applicative accounts API (devices, apps) |
14
+ | Plugin | Description |
15
+ | ------------------------------- | ---------------------------------------- |
16
+ | [calendar](calendar.md) | Calendar resources and users sync |
17
+ | [app-accounts](app-accounts.md) | Applicative accounts API (devices, apps) |
18
18
 
19
19
  ## Prerequisites
20
20
 
@@ -37,6 +37,6 @@ James plugins require:
37
37
  core/twake/james
38
38
  └─ requires: core/ldap/onChange
39
39
 
40
- core/twake/calendarResources
40
+ core/twake/calendar
41
41
  └─ requires: core/ldap/onChange
42
42
  ```
@@ -1,6 +1,13 @@
1
- # Twake Calendar Resources Plugin
1
+ # Twake Calendar Plugin
2
2
 
3
- Plugin to automatically synchronize LDAP resources (meeting rooms, equipment, etc.) with Twake Calendar via its WebAdmin API.
3
+ Plugin to keep Twake Calendar in sync with LDAP via its WebAdmin API:
4
+
5
+ - resources (meeting rooms, equipment, etc.) stored in an LDAP branch
6
+ - registered users, whose email, first name and last name follow LDAP
7
+
8
+ > Formerly `core/twake/calendarResources`. That name still loads the plugin,
9
+ > registered as `calendarResources`, but logs a deprecation warning and will
10
+ > be removed in a future major release.
4
11
 
5
12
  ## Features
6
13
 
@@ -8,6 +15,7 @@ Plugin to automatically synchronize LDAP resources (meeting rooms, equipment, et
8
15
  - **Automatic Updates**: When a resource is modified in LDAP, changes are synced to Twake Calendar
9
16
  - **Automatic Deletion**: When a resource is removed from LDAP, it's deleted from Twake Calendar
10
17
  - **Flexible Configuration**: Configure which LDAP branch and objectClass to monitor
18
+ - **User Identity Propagation**: When a user's email, first name or last name changes in LDAP, the matching Calendar registered user is updated
11
19
 
12
20
  ## Configuration
13
21
 
@@ -15,10 +23,15 @@ Plugin to automatically synchronize LDAP resources (meeting rooms, equipment, et
15
23
 
16
24
  - `DM_CALENDAR_WEBADMIN_URL`: URL of the Twake Calendar WebAdmin API (default: `http://localhost:8080`)
17
25
  - `DM_CALENDAR_WEBADMIN_TOKEN`: Bearer token for WebAdmin API authentication
18
- - `DM_CALENDAR_RESOURCE_BASE`: LDAP branch to monitor for resources (e.g., `ou=resources,dc=example,dc=com`)
19
- - `DM_CALENDAR_RESOURCE_OBJECTCLASS`: Optional objectClass filter (only process entries with this objectClass)
26
+ - `DM_CALENDAR_RESOURCE_BASE`: LDAP branch to monitor for resources, as a full DN (e.g., `ou=resources,dc=example,dc=com`). It is compared to an entry's DN part by part, so a partial value such as `ou=resources` names no branch and matches nothing (the plugin warns at startup when the value is not a DN of the directory).
27
+ - `DM_CALENDAR_RESOURCE_OBJECTCLASS`: Optional objectClass filter, applied when a resource is **created** (see [Resource identity](#resource-identity))
20
28
  - `DM_CALENDAR_RESOURCE_CREATOR`: Default creator email for resources (default: `admin@example.com`)
21
29
  - `DM_CALENDAR_RESOURCE_DOMAIN`: Default domain for resources (extracted from DN if not specified)
30
+ - `DM_CALENDAR_FIRSTNAME_ATTRIBUTE`: LDAP attribute holding a user's first name (default: `givenName`)
31
+ - `DM_CALENDAR_LASTNAME_ATTRIBUTE`: LDAP attribute holding a user's last name (default: `sn`)
32
+
33
+ The resource variables are only needed for resource synchronization; user
34
+ identity propagation only needs the WebAdmin URL and token.
22
35
 
23
36
  ### Required Dependencies
24
37
 
@@ -66,6 +79,49 @@ The plugin uses the following WebAdmin API endpoints:
66
79
  - `POST /resources` - Create a new resource
67
80
  - `PATCH /resources/{id}` - Update an existing resource
68
81
  - `DELETE /resources/{id}` - Delete a resource
82
+ - `GET /registeredUsers?email={mail}` - Find the registered user to update
83
+ - `PATCH /registeredUsers?id={id}` - Update a registered user's email, first and last name
84
+ - `POST /users/{mail}?action=deleteData` - Delete a user's data (see `deleteUserData`)
85
+
86
+ The resource id goes into the path percent-encoded, so a `/` or a `#` in it
87
+ cannot build a path naming something else. The user's address is left as it is
88
+ written — `user@example.com`, `@` being legal in a path segment — which is what
89
+ `plugins/twake/james` does with the same WebAdmin; whether the two should
90
+ encode it is issue #175.
91
+
92
+ ### Registered Users
93
+
94
+ A change of the configured mail attribute updates the registered user found by
95
+ its **previous** address; a change of the first or last name attribute updates
96
+ the one found by its current address. In both cases the email, first name and
97
+ last name are re-read from LDAP and sent together. Adding or removing the mail
98
+ attribute, and users not registered in Calendar, are skipped.
99
+
100
+ Calendar lower-cases the address before looking it up, which is also how it
101
+ stores addresses, so a case difference between LDAP and Calendar does not
102
+ matter. The exception is a legacy record whose stored address kept upper case
103
+ (written without Calendar's normalisation, e.g. migrated data): it is not
104
+ found, and its user is logged as not registered. Calendar releases before
105
+ 1.0.0.1 ignore the `email` parameter and answer the full list of registered
106
+ users; the plugin then picks the user from it, which works but costs a full
107
+ listing per change.
108
+
109
+ ### Resource identity
110
+
111
+ A resource is known to Calendar by an `id`, which is the value of its LDAP
112
+ entry's own RDN with the escapes removed: `cn=Meeting Room 1,ou=resources,…`
113
+ gives `Meeting Room 1`, and `o=Room 12,ou=resources,…` gives `Room 12`. The
114
+ creation, the update and the deletion hooks all read it that way, so the three
115
+ name the same resource; an entry whose DN yields no value is not synchronised.
116
+
117
+ Only entries in `DM_CALENDAR_RESOURCE_BASE` are synchronised, on all three
118
+ hooks. `DM_CALENDAR_RESOURCE_OBJECTCLASS` filters creations only: a
119
+ modification carries the attributes that changed, so an untouched objectClass
120
+ is not there to compare, and a deleted entry cannot be read any more. The
121
+ branch is the guard that remains on those two paths.
122
+
123
+ Renaming an entry changes its RDN, hence its id: Calendar is not told, and the
124
+ resource keeps its former id there.
69
125
 
70
126
  ### Resource Data Format
71
127
 
@@ -103,7 +159,7 @@ DM_LDAP_FLAT_SCHEMA="/path/to/calendar-resources-schema.json"
103
159
  3. Add the plugin to `DM_PLUGINS`:
104
160
 
105
161
  ```bash
106
- DM_PLUGINS="core/ldap/onChange,core/ldap/flatGeneric,twake/calendarResources"
162
+ DM_PLUGINS="core/ldap/onChange,core/ldap/flatGeneric,twake/calendar"
107
163
  ```
108
164
 
109
165
  4. Start ldap-rest - resources will be automatically synced
@@ -121,7 +177,7 @@ Example log:
121
177
 
122
178
  ```json
123
179
  {
124
- "plugin": "calendarResources",
180
+ "plugin": "calendar",
125
181
  "event": "ldapcalendarResourceadddone",
126
182
  "result": "success",
127
183
  "http_status": 201,
@@ -138,6 +194,7 @@ Example log:
138
194
  2. Verify that `DM_CALENDAR_WEBADMIN_TOKEN` is valid
139
195
  3. Check logs for API errors
140
196
  4. Ensure the ldapFlat schema has `entity.name` set to `calendarResource`
197
+ 5. Ensure `DM_CALENDAR_RESOURCE_BASE` is the full DN of the branch the entries are in: a partial value matches no entry, and the plugin says so at startup
141
198
 
142
199
  ### Authentication errors
143
200
 
@@ -145,22 +202,21 @@ Ensure `DM_CALENDAR_WEBADMIN_TOKEN` is set and valid. The token is sent as a Bea
145
202
 
146
203
  ### Wrong resources being synced
147
204
 
148
- Use `DM_CALENDAR_RESOURCE_BASE` and `DM_CALENDAR_RESOURCE_OBJECTCLASS` to filter which LDAP entries are considered resources.
205
+ Use `DM_CALENDAR_RESOURCE_BASE` and `DM_CALENDAR_RESOURCE_OBJECTCLASS` to filter which LDAP entries are considered resources. Note that the objectClass filter only applies to creations, so an entry of the resource entity that must never reach Calendar has to sit outside the branch.
149
206
 
150
207
  ## Public Methods
151
208
 
152
- Public methods available on the CalendarResources plugin instance.
209
+ Public methods available on the Calendar plugin instance.
153
210
 
154
211
  ### Usage
155
212
 
156
213
  ```typescript
157
214
  import type { DM } from 'ldap-rest';
158
- import type CalendarResources from 'ldap-rest/plugin-twake-calendarresources';
215
+ import type Calendar from 'ldap-rest/plugin-twake-calendar';
159
216
 
160
217
  // Get the plugin instance from another plugin
161
- const calendar = this.server.getPlugin(
162
- 'calendarResources'
163
- ) as CalendarResources;
218
+ // (declare `dependencies = { calendar: 'core/twake/calendar' }` so it loads first)
219
+ const calendar = this.requirePlugin<Calendar>('calendar');
164
220
  ```
165
221
 
166
222
  ### deleteUserData(mail)
@@ -182,7 +238,8 @@ async deleteUserData(mail: string): Promise<{ taskId: string } | null>
182
238
  **Example:**
183
239
 
184
240
  ```typescript
185
- const result = await calendar.deleteUserData('user@example.com');
241
+ // requirePlugin() returns null when the plugin is not loaded
242
+ const result = await calendar?.deleteUserData('user@example.com');
186
243
  if (result) {
187
244
  console.log(`Deletion task started: ${result.taskId}`);
188
245
  }
@@ -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