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
@@ -139,6 +139,178 @@ Fixed attributes:
139
139
  - Cannot be modified after creation
140
140
  - Cannot be deleted
141
141
 
142
+ ### Naming things, in the reader's language
143
+
144
+ `label` is what a client shows instead of the attribute name, and `entity.label`
145
+ / `entity.singularLabel` are what it calls the collection and one of its
146
+ entries. Each may be a plain string, or a map keyed by language tag:
147
+
148
+ ```json
149
+ {
150
+ "entity": {
151
+ "name": "twakeUser",
152
+ "singularName": "user",
153
+ "pluralName": "users",
154
+ "label": { "en": "Users", "fr": "Utilisateurs" },
155
+ "singularLabel": { "en": "user", "fr": "utilisateur" }
156
+ },
157
+ "attributes": {
158
+ "twakeDepartmentLink": {
159
+ "type": "pointer",
160
+ "role": "organizationLink",
161
+ "label": { "en": "Department", "fr": "Organisation" }
162
+ }
163
+ }
164
+ }
165
+ ```
166
+
167
+ These words name _your_ entities and attributes, so their translations belong
168
+ in your schemas rather than in a catalogue shipped with the product — which is
169
+ also why an interface can be fully translated without the product learning what
170
+ you call a department. A client picks the exact tag, then its language, then
171
+ English, then whatever the map does hold; with no label at all it falls back to
172
+ the attribute name made readable.
173
+
174
+ A nomenclature names its values too. `entity.valueLabels` maps the main
175
+ attribute value of an entry to what a client shows for it, so a group whose
176
+ type is `cn=teamMailbox,…` reads _Shared mailbox_ — or _Boîte partagée_ —
177
+ wherever a pointer lands on it:
178
+
179
+ ```json
180
+ {
181
+ "entity": {
182
+ "name": "twakeMailboxType",
183
+ "mainAttribute": "cn",
184
+ "valueLabels": {
185
+ "group": { "en": "Group", "fr": "Groupe" },
186
+ "teamMailbox": { "en": "Shared mailbox", "fr": "Boîte partagée" }
187
+ }
188
+ }
189
+ }
190
+ ```
191
+
192
+ A value the map does not hold is shown as it is stored: a directory with a
193
+ value of its own keeps working, and names it by adding it to its schema.
194
+
195
+ ### Explaining a pattern: `hint`
196
+
197
+ A `test` tells the server what to refuse. A `hint` tells the person what to
198
+ type, before they get it wrong:
199
+
200
+ ```json
201
+ {
202
+ "telephoneNumber": {
203
+ "type": "string",
204
+ "test": "^\\(\\+99\\)\\s\\d{3}\\s\\d{4}$",
205
+ "hint": "Expected pattern (+99) 999 9999"
206
+ }
207
+ }
208
+ ```
209
+
210
+ The hint travels with the schema, so a client shows it under the field and the
211
+ server repeats it when it rejects a value — the pattern and its explanation
212
+ cannot drift apart, and neither is hardcoded in the interface.
213
+
214
+ ### Attributes a client may not send
215
+
216
+ | Marker | Meaning |
217
+ | ----------- | ------------------------------------------------------------------------------- |
218
+ | `generated` | The server computes the value. A request that carries it is refused with a 400. |
219
+ | `readOnly` | The value is derived elsewhere — `memberOf` is driven from the group side. |
220
+
221
+ Both restrict the **request body** only: the core and its plugins still write
222
+ these attributes.
223
+
224
+ ### Attributes a client never receives
225
+
226
+ `neverReturn` strips an attribute from every API answer while leaving it
227
+ writable. It is what makes "a manager may reset a password without being able
228
+ to read it" a projection rather than an authorization engine:
229
+
230
+ ```json
231
+ {
232
+ "userPassword": { "type": "string", "role": "password", "neverReturn": true },
233
+ "pwdReset": {
234
+ "type": "boolean",
235
+ "role": "passwordReset",
236
+ "neverReturn": true
237
+ }
238
+ }
239
+ ```
240
+
241
+ ### Deriving the identifier
242
+
243
+ When the RDN is not something a person chooses, `generatedFrom` says where it
244
+ comes from. The client then posts without it:
245
+
246
+ ```json
247
+ {
248
+ "uid": {
249
+ "type": "string",
250
+ "role": "identifier",
251
+ "generated": true,
252
+ "generatedFrom": {
253
+ "attribute": "mail",
254
+ "extract": "^([^@]+)@",
255
+ "lowercase": true,
256
+ "onCollision": "suffix"
257
+ }
258
+ }
259
+ }
260
+ ```
261
+
262
+ `onCollision` decides what happens when the value is taken — two directories'
263
+ worth of `jean.dupont@a.example` and `jean.dupont@b.example` map to the same
264
+ local part. `suffix` appends `-2`, `-3`, …; `error` (the default) refuses the
265
+ creation with a 409.
266
+
267
+ ### Semantic roles
268
+
269
+ `role` says what an attribute _means_, so core code and plugins can find it
270
+ without knowing its name. `roleAttribute(schema, 'accountStatus')` returns
271
+ `twakeAccountStatus` in a Twake directory and whatever another deployment
272
+ chose in its own. The roles the core looks for:
273
+
274
+ | Role | Meaning |
275
+ | -------------------------------------- | --------------------------------------------- |
276
+ | `identifier`, `displayName` | RDN value, human-readable name |
277
+ | `primaryEmail`, `emailAliases` | Mail addresses of the entry |
278
+ | `emailQuota` | Mailbox size limit, in bytes |
279
+ | `organizationLink`, `organizationPath` | Organization the entry belongs to |
280
+ | `members`, `owners` | Members of a group, and who may write to it |
281
+ | `accountStatus`, `accountExpiry` | Lifecycle state and scheduled removal |
282
+ | `password`, `passwordReset` | Credential and "change it at next login" flag |
283
+ | `domainLink`, `domainName` | Mail domains an organization may use |
284
+
285
+ An attribute may carry several roles (`"role": ["identifier", "displayName"]`),
286
+ and a deployment is free to add its own for its plugins.
287
+
288
+ ### Which attributes a search covers: `searchable`
289
+
290
+ `GET /{pluralName}?match=…&attribute=…` takes one attribute name or several
291
+ separated by commas, and an entry matching any of them is returned. Which ones
292
+ a client should offer is a property of the deployment rather than of the code:
293
+ a substring filter on an attribute the directory has not indexed scans the
294
+ branch.
295
+
296
+ `"searchable": true` says so. A schema that marks even one attribute has
297
+ declared its list, and that list is what a client searches; a schema that
298
+ marks none leaves the client to guess — everything returnable, single-valued
299
+ and not a DN, which suits a small branch and scans a large one. The shipped
300
+ schemas mark the attributes an operator looks people up by:
301
+
302
+ ```json
303
+ "sn": { "type": "string", "searchable": true, "label": { … } }
304
+ ```
305
+
306
+ Align it with your indexes. In OpenLDAP that means `index sn,cn,mail sub` for
307
+ the attributes you mark.
308
+
309
+ The remaining markers — `unique`, `normalize`, `states`,
310
+ `referentialIntegrity`, `deleteGuard`, `mailDomainScope` — are read by
311
+ [enterprise-rules](enterprise-rules.md) and
312
+ [account-lifecycle](account-lifecycle.md).
313
+
142
314
  ## Generated APIs
143
315
 
144
316
  For each schema, the plugin generates REST API endpoints:
@@ -41,7 +41,7 @@ This plugin manages groups stored as `groupOfNames` or similar object classes in
41
41
 
42
42
  ### Important Notes
43
43
 
44
- - **Empty Groups**: `groupOfNames` requires at least one member. The plugin uses a dummy member (configurable via `--group-dummy-user`) for empty groups.
44
+ - **Empty Groups**: `groupOfNames` requires at least one member. The plugin uses a dummy member (configurable via `--group-dummy-user`) for empty groups. That dummy member is hidden from `member` on both `GET /ldap/groups` and `GET /ldap/groups/{cn}`, so a client never sees it as a real one.
45
45
  - **Member Validation**: By default, the plugin validates that all members exist in LDAP before adding them. Set `--groups-allow-unexistent-members true` to disable validation.
46
46
  - **Automatic Cleanup**: When a user is deleted, the plugin automatically removes them from all groups via the `ldapdeleterequest` hook.
47
47
 
@@ -55,13 +55,17 @@ GET /api/v1/ldap/groups
55
55
 
56
56
  **Query Parameters:**
57
57
 
58
- - `match` (optional): Filter by group name (supports wildcards and LDAP filters)
58
+ - `match` (optional): Value to look for
59
+ - `attribute` (optional): Attribute(s) `match` is looked for in, as a
60
+ substring, several separated by commas — the same semantics as the flat
61
+ entity lists. Without it, `match` is either a raw LDAP filter (when it
62
+ contains `=`) or an exact group name.
59
63
  - `attributes` (optional): Comma-separated list of attributes to return
60
64
 
61
65
  **Example:**
62
66
 
63
67
  ```bash
64
- curl "http://localhost:8081/api/v1/ldap/groups?match=admin*&attributes=cn,member,description"
68
+ curl "http://localhost:8081/api/v1/ldap/groups?match=admin&attribute=cn,mail&attributes=cn,member,description"
65
69
  ```
66
70
 
67
71
  **Response (200):**
@@ -150,7 +154,9 @@ POST /api/v1/ldap/groups
150
154
 
151
155
  - `cn` is required
152
156
  - `member` can be a string (single member) or array (multiple members)
153
- - If no members provided, a dummy member is automatically added
157
+ - If no members provided, a dummy member is automatically added, and it is
158
+ left out of `member` when the group is read back, on both the list and the
159
+ single-group endpoint
154
160
  - Additional attributes can be included based on schema
155
161
 
156
162
  **Response (200):**
@@ -51,8 +51,10 @@ Organizations are hierarchical LDAP entries:
51
51
 
52
52
  - Use LDAP DN structure for hierarchy
53
53
  - Have a `twakeDepartmentPath` attribute showing their position in the tree
54
- - Example: `ou=IT,ou=Departments,dc=example,dc=com`
55
- - Path: `IT / Departments / Top Organization`
54
+ - The path reads from the root down, the entry's own name last, and stops
55
+ below the top organization: a top-level organization's path is its own name
56
+ - Example: `ou=IT,ou=Departments,ou=organization,dc=example,dc=com`
57
+ - Path: `Departments / IT`
56
58
 
57
59
  ### Users and Groups
58
60
 
@@ -64,8 +66,8 @@ Users and groups reference organizations via:
64
66
  ```json
65
67
  {
66
68
  "uid": "john.doe",
67
- "twakeDepartmentLink": "ou=IT,ou=Departments,dc=example,dc=com",
68
- "twakeDepartmentPath": "IT / Departments / Top Organization"
69
+ "twakeDepartmentLink": "ou=IT,ou=Departments,ou=organization,dc=example,dc=com",
70
+ "twakeDepartmentPath": "Departments / IT"
69
71
  }
70
72
  ```
71
73
 
@@ -118,7 +120,7 @@ curl "http://localhost:8081/api/v1/ldap/organizations/ou%3DIT%2Co%3Dgov%2Cc%3Dmu
118
120
  "dn": "ou=IT,dc=example,dc=com",
119
121
  "ou": "IT",
120
122
  "description": "Information Technology Department",
121
- "twakeDepartmentPath": "IT / Government"
123
+ "twakeDepartmentPath": "Government / IT"
122
124
  }
123
125
  ```
124
126
 
@@ -185,7 +187,7 @@ POST /api/v1/ldap/organizations
185
187
  "ou": "IT",
186
188
  "parentDn": "dc=example,dc=com",
187
189
  "description": "Information Technology Department",
188
- "twakeDepartmentPath": "IT / Government"
190
+ "twakeDepartmentPath": "Government / IT"
189
191
  }
190
192
  ```
191
193
 
@@ -264,7 +266,10 @@ POST /api/v1/ldap/organizations/:dn/move
264
266
 
265
267
  - Moves an organization to a different parent organization
266
268
  - The organization will become a child of the target organization
267
- - All sub-organizations and linked entities (users/groups) move with it
269
+ - All sub-organizations and linked entities (users/groups) move with it. With
270
+ `core/ldap/departmentSync` loaded, the path of the organization and of every
271
+ sub-organization is recomputed, then the linked entities take the new link
272
+ and path; without it, those paths keep naming the former parent
268
273
  - Cannot move an organization into itself or its own descendants (circular reference prevention)
269
274
  - Cannot move to the same parent (no-op)
270
275
  - Target must be a valid organizational unit
@@ -340,10 +345,14 @@ The plugin enforces several validation rules through hooks:
340
345
  ### For Organizations
341
346
 
342
347
  1. **Path Validation**: The `twakeDepartmentPath` must:
343
- - Start with the organization's own `ou` name
344
- - Be followed by the path separator
345
- - Reference an existing parent organization in the hierarchy
346
- - Match the actual LDAP DN structure
348
+ - End with the organization's own `ou` name, preceded by the path separator
349
+ - Be preceded by the stored path of an existing organization
350
+ - Be its own name alone only when the entry hangs directly from the top
351
+ organization, which its DN has to say
352
+ - A path written the other way round, the entry's own name first and the
353
+ top organization's name last, is accepted as it stands: it is what
354
+ directories written before this convention hold. The server never
355
+ computes one.
347
356
 
348
357
  2. **Deletion Protection**: Organizations can only be deleted if:
349
358
  - No users have `twakeDepartmentLink` pointing to it
@@ -385,9 +394,10 @@ hooks: {
385
394
  if (!isOrganization(entry)) {
386
395
  await checkDeptLink(entry); // Validates link exists
387
396
  }
388
- // If entry is an organization, validate path
397
+ // If entry is an organization, validate path against the DN it is
398
+ // written at: that is what says where the entry hangs from
389
399
  if (isOrganization(entry)) {
390
- await checkDeptPath(entry); // Validates path matches hierarchy
400
+ await checkDeptPath(entry, dn); // Validates path matches hierarchy
391
401
  }
392
402
  return [dn, entry];
393
403
  };
@@ -417,13 +427,13 @@ curl -X POST http://localhost:8081/api/v1/ldap/organizations \
417
427
  -H "Content-Type: application/json" \
418
428
  -d '{
419
429
  "ou": "Recruitment",
420
- "parentDn": "ou=HR,dc=example,dc=com",
421
- "description": "Recruitment Team",
422
- "twakeDepartmentPath": "Recruitment / HR / Government"
430
+ "parentDn": "ou=HR,ou=organization,dc=example,dc=com",
431
+ "description": "Recruitment Team"
423
432
  }'
433
+ # `twakeDepartmentPath` is computed by the server: `HR / Recruitment`
424
434
  ```
425
435
 
426
- This creates: `ou=Recruitment,ou=HR,dc=example,dc=com`
436
+ This creates: `ou=Recruitment,ou=HR,ou=organization,dc=example,dc=com`
427
437
 
428
438
  ### Example 3: Get Organization Hierarchy
429
439
 
@@ -493,7 +503,7 @@ curl -X POST http://localhost:8081/api/v1/ldap/users \
493
503
  "sn": "Doe",
494
504
  "mail": "john.doe@example.com",
495
505
  "twakeDepartmentLink": "ou=IT,dc=example,dc=com",
496
- "twakeDepartmentPath": "IT / Government"
506
+ "twakeDepartmentPath": "Government / IT"
497
507
  }'
498
508
  ```
499
509
 
@@ -513,7 +523,7 @@ curl -X POST http://localhost:8081/api/v1/ldap/groups \
513
523
  "cn": "it-admins",
514
524
  "description": "IT Administrators",
515
525
  "twakeDepartmentLink": "ou=IT,dc=example,dc=com",
516
- "twakeDepartmentPath": "IT / Government",
526
+ "twakeDepartmentPath": "Government / IT",
517
527
  "member": ["uid=john.doe,ou=users,dc=example,dc=com"]
518
528
  }'
519
529
  ```
@@ -538,31 +548,47 @@ You must first:
538
548
 
539
549
  ### Path Format
540
550
 
541
- The path attribute follows this pattern:
551
+ The path attribute follows this pattern, the top organization excluded:
542
552
 
543
553
  ```
544
- {current_ou} / {parent_ou} / {grandparent_ou} / ... / {top_org}
554
+ {parent_of_parent_ou} / {parent_ou} / {current_ou}
545
555
  ```
546
556
 
547
557
  ### Validation Process
548
558
 
549
- 1. **Extract Components**: Split path by separator
550
- 2. **Verify First Component**: Must match organization's own `ou`
551
- 3. **Validate Parent Path**: Search for parent organization with matching path
552
- 4. **Verify Hierarchy**: Ensure path matches actual LDAP DN structure
559
+ 1. **Verify Last Component**: Must match the organization's own `ou`
560
+ 2. **Validate Parent Path**: What precedes it must be the stored path of an
561
+ existing organization
562
+ 3. **Verify Hierarchy**: An organization whose path is its own name alone must
563
+ hang directly from the top organization
564
+ 4. **Accept The Old Form**: A path reading the other way round, ending in the
565
+ top organization's own name, is left alone — the directories that hold
566
+ them predate this convention and their entries have to stay writable
553
567
 
554
568
  ### Example Path Validation
555
569
 
556
- For organization: `ou=Recruitment,ou=HR,dc=example,dc=com`
570
+ For organization: `ou=Recruitment,ou=HR,ou=organization,dc=example,dc=com`
557
571
 
558
- Valid path: `Recruitment / HR / Government`
572
+ Valid path: `HR / Recruitment`
559
573
 
560
- - `Recruitment` matches current ou
561
- - `HR / Government` must exist as path of `ou=HR,dc=example,dc=com`
574
+ - it ends with `Recruitment`, the entry's own `ou`
575
+ - what precedes it, `HR`, must be the stored path of an existing organization
562
576
 
563
- Invalid path: `Recruitment / IT / Government`
577
+ Invalid path: `IT / Recruitment`
564
578
 
565
- - `IT / Government` doesn't match parent's actual path
579
+ - no organization holds the path `IT`
580
+
581
+ Invalid path: `Recruitment / HR`
582
+
583
+ - it ends with `HR`, not with the entry's own name
584
+
585
+ Invalid path: `Recruitment`
586
+
587
+ - it names no parent, and the entry does not hang from the top organization
588
+
589
+ Accepted path: `Recruitment / HR / organization`
590
+
591
+ - the old form, the top organization last: read, not written
566
592
 
567
593
  ## Troubleshooting
568
594
 
@@ -576,7 +602,7 @@ Invalid path: `Recruitment / IT / Government`
576
602
  # Create organization first
577
603
  curl -X POST http://localhost:8081/api/v1/ldap/organizations \
578
604
  -H "Content-Type: application/json" \
579
- -d '{"ou": "IT", "twakeDepartmentPath": "IT / Government"}'
605
+ -d '{"ou": "IT"}' # the path is computed by the server
580
606
 
581
607
  # Then create user with link
582
608
  curl -X POST http://localhost:8081/api/v1/ldap/users \
@@ -591,7 +617,7 @@ curl -X POST http://localhost:8081/api/v1/ldap/users \
591
617
 
592
618
  1. Ensure parent path exists and matches LDAP hierarchy
593
619
  2. Verify separator matches configured separator (default: `" / "`)
594
- 3. Check that path starts with organization's own `ou` name
620
+ 3. Check that the path ends with the organization's own `ou` name
595
621
 
596
622
  ### Cannot Delete Organization
597
623