ldap-rest 0.26.0 → 1.0.1

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 (96) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/abstract/twakePlugin.js +1 -1
  3. package/dist/bin/index.js +1 -1
  4. package/dist/config/args.js +1 -1
  5. package/dist/lib/authz/base.js +1 -1
  6. package/dist/lib/authz/composition.js +1 -1
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/lib/mailDomains.js +1 -0
  9. package/dist/lib/requestContext.js +1 -0
  10. package/dist/lib/utils.js +1 -1
  11. package/dist/plugins/auth/authzDynamic.js +1 -1
  12. package/dist/plugins/auth/authzLinid1.js +1 -1
  13. package/dist/plugins/auth/authzPerBranch.js +1 -1
  14. package/dist/plugins/auth/authzScope.js +1 -1
  15. package/dist/plugins/auth/openidconnect.js +1 -1
  16. package/dist/plugins/bcl.js +1 -1
  17. package/dist/plugins/configApi.js +1 -1
  18. package/dist/plugins/ldap/bulkImport.js +1 -1
  19. package/dist/plugins/ldap/departmentSync.js +1 -1
  20. package/dist/plugins/ldap/enterpriseRules.js +1 -1
  21. package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
  22. package/dist/plugins/ldap/groups.js +1 -1
  23. package/dist/plugins/ldap/onChange.js +1 -1
  24. package/dist/plugins/ldap/organizations.js +1 -1
  25. package/dist/plugins/ldap/passwordPolicy.js +1 -1
  26. package/dist/plugins/ldap/raw.js +1 -1
  27. package/dist/plugins/ldap/trash.js +1 -1
  28. package/dist/plugins/rabbitmq.js +1 -1
  29. package/dist/plugins/scim/groups.js +1 -1
  30. package/dist/plugins/scim/users.js +1 -1
  31. package/dist/plugins/storage/ldap.js +1 -1
  32. package/dist/plugins/storage/postgres.js +1 -1
  33. package/dist/plugins/storage/valkey.js +1 -1
  34. package/dist/plugins/twake/appAccountsApi.js +1 -1
  35. package/dist/plugins/twake/appAccountsConsistency.js +1 -1
  36. package/dist/plugins/twake/clouderyProvision.js +1 -1
  37. package/dist/plugins/twake/groups.js +1 -1
  38. package/dist/plugins/twake/instances.js +1 -1
  39. package/dist/plugins/twake/james.js +1 -1
  40. package/dist/plugins/twake/lifecycleEvents.js +1 -1
  41. package/dist/plugins/twake/spaces.js +1 -1
  42. package/dist/plugins/twake/tombstone.js +1 -1
  43. package/dist/src/abstract/ldapFlat.d.ts +25 -6
  44. package/dist/src/abstract/plugin.d.ts +1 -1
  45. package/dist/src/bin/index.d.ts +12 -0
  46. package/dist/src/hooks.d.ts +12 -3
  47. package/dist/src/lib/authz/base.d.ts +87 -4
  48. package/dist/src/lib/authz/composition.d.ts +21 -0
  49. package/dist/src/lib/ldapActions.d.ts +139 -18
  50. package/dist/src/lib/mailDomains.d.ts +41 -0
  51. package/dist/src/lib/requestContext.d.ts +23 -0
  52. package/dist/src/lib/utils.d.ts +41 -2
  53. package/dist/src/plugins/auth/authzDynamic.d.ts +5 -12
  54. package/dist/src/plugins/auth/authzLinid1.d.ts +3 -1
  55. package/dist/src/plugins/auth/authzPerBranch.d.ts +5 -9
  56. package/dist/src/plugins/auth/openidconnect.d.ts +47 -0
  57. package/dist/src/plugins/bcl.d.ts +2 -1
  58. package/dist/src/plugins/ldap/accountLifecycle.d.ts +2 -2
  59. package/dist/src/plugins/ldap/bulkImport.d.ts +30 -5
  60. package/dist/src/plugins/ldap/enterpriseRules.d.ts +11 -1
  61. package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +20 -3
  62. package/dist/src/plugins/ldap/groups.d.ts +48 -1
  63. package/dist/src/plugins/ldap/organizations.d.ts +4 -4
  64. package/dist/src/plugins/twake/appAccountsApi.d.ts +3 -67
  65. package/dist/src/plugins/twake/groups.d.ts +46 -2
  66. package/dist/src/plugins/twake/james.d.ts +0 -11
  67. package/docs/README.md +25 -0
  68. package/docs/plugin-development/README.md +61 -15
  69. package/docs/plugin-development/dependencies.md +8 -2
  70. package/docs/plugin-development/hooks.md +4 -5
  71. package/docs/usage/configuration.md +11 -8
  72. package/docs/usage/plugins/auth/README.md +22 -3
  73. package/docs/usage/plugins/auth/authz-linid1.md +14 -5
  74. package/docs/usage/plugins/auth/authz-per-branch.md +60 -9
  75. package/docs/usage/plugins/auth/back-channel-logout.md +28 -0
  76. package/docs/usage/plugins/auth/oidc.md +8 -0
  77. package/docs/usage/plugins/integrations/app-accounts.md +5 -0
  78. package/docs/usage/plugins/integrations/groups.md +36 -3
  79. package/docs/usage/plugins/integrations/scim.md +7 -0
  80. package/docs/usage/plugins/integrations/spaces.md +3 -1
  81. package/docs/usage/plugins/ldap/bulk-import.md +20 -0
  82. package/docs/usage/plugins/ldap/enterprise-rules.md +12 -2
  83. package/docs/usage/plugins/ldap/external-users.md +8 -3
  84. package/docs/usage/plugins/ldap/flat-generic.md +3 -0
  85. package/docs/usage/plugins/ldap/groups.md +6 -0
  86. package/docs/usage/plugins/ldap/organizations.md +17 -0
  87. package/docs/usage/plugins/ldap/raw.md +1 -1
  88. package/docs/usage/upgrading.md +228 -0
  89. package/package.json +1 -1
  90. package/static/schemas/example/groups.json +1 -0
  91. package/static/schemas/example/organizations.json +1 -0
  92. package/static/schemas/example/users.json +3 -0
  93. package/static/schemas/twake/groups.json +2 -0
  94. package/static/schemas/twake/organizations.json +1 -0
  95. package/static/schemas/twake/teamMailboxes.json +3 -1
  96. package/static/schemas/twake/users.json +3 -0
@@ -61,12 +61,20 @@ Every plugin that judges LDAP operations — `core/auth/authzPerBranch`,
61
61
  `core/auth/authzLinid1`, `core/auth/authzDynamic` — registers the same hooks,
62
62
  and all of them run: loaded together, they compose as an **AND**, and the
63
63
  first refusal wins. The per-caller read filter (`ldapsearchfilter`) is one of
64
- those hooks: two plugins there compose as an intersection, which is the same
65
- AND. That is sound when it is meant. When it is not, it is a
64
+ the hooks they register: two plugins there compose as an intersection, which
65
+ is the same AND. That is sound when it is meant. When it is not, it is a
66
66
  refusal nobody can explain: `authzPerBranch` beside `authzDynamic` judges a
67
67
  machine token by a branch configuration written for administrators, finds
68
68
  no entry for the tenant, and refuses a write the token's own ACLs grant.
69
69
 
70
+ The authorization plugins' request hooks (`ldapsearchrequest`,
71
+ `ldapaddrequest`, `ldapmodifyrequest`, `ldapdeleterequest`,
72
+ `ldaprenamerequest`) run before any other plugin's, whatever order the
73
+ plugins loaded in: a caller who may not make an operation gets the 403, not
74
+ another plugin's 409, and the operation is not replaced (by
75
+ `core/ldap/trash`, for instance) before it is judged. `ldapsearchfilter`
76
+ keeps the load order.
77
+
70
78
  The AND needs no second plugin to bite. `--authz-per-branch-config` has a
71
79
  default — read everywhere, write nowhere — so a lone `authzPerBranch` judges
72
80
  **every** authenticated identity, whatever authenticated it: a token that no
@@ -108,6 +116,16 @@ aside — `authzDynamic` letting a request through on
108
116
 
109
117
  ### What is refused at startup
110
118
 
119
+ - An authorization plugin with no authentication plugin is refused: no
120
+ request carries an identity, so every authorization plugin would step
121
+ aside and everything would pass while authorization looks on.
122
+ `authzDynamic`, which authenticates the requests it judges, needs nothing
123
+ else. With neither kind of plugin, the server serves every request with
124
+ every right, on purpose (local development, a trusted network); with
125
+ authentication alone, every authenticated caller has every right (a
126
+ single administration token, say). The log says which, once. This is
127
+ checked over the plugins the configuration loads (`--plugin`); a host
128
+ registering plugins itself answers for what they enforce.
111
129
  - Two plugins judging LDAP operations for the same authenticator's requests
112
130
  — two branch-level plugins with no `authz_for`, or a branch-level plugin
113
131
  judging `authzDynamic`'s tokens — are refused, with both names, unless
@@ -124,7 +142,8 @@ aside — `authzDynamic` letting a request through on
124
142
  already.
125
143
 
126
144
  A host that assembles the server itself — `registerPlugin` after `ready`,
127
- rather than `--plugin` — gets the same checks, as each plugin arrives: the
145
+ rather than `--plugin` — gets the same checks of the composition, as each
146
+ plugin arrives (not the one asking for an authentication plugin, above): the
128
147
  refusal comes out of `registerPlugin`, before any of the plugin's routes or
129
148
  hooks exist. An arriving plugin is judged against what is already there, so
130
149
  the authentication plugins an `authz_for` or `--authz-scope-source` names
@@ -78,17 +78,26 @@ organization through which entries change hands.
78
78
  ```
79
79
 
80
80
  An entry is **in transit** when it is attached to that organization, or to
81
- none (with or without the option).
81
+ none (with or without the option). An entry carrying no link is in transit
82
+ on purpose: it is how the entries other tools insert without one are
83
+ caught. Nothing ties such an entry to an organization: any administrator
84
+ may claim it into one they manage, and the first to claim it takes it.
82
85
 
83
- - Every administrator sees the entries in transit.
86
+ - Every administrator sees the entries in transit, but in a global branch
87
+ (`--authz-global-branch`), which only its holders read.
84
88
  - **Claiming** one (`POST /api/v1/ldap/users/:id/move` towards an
85
- organization) asks for write on that organization only.
89
+ organization) asks for write on that organization only. The write
90
+ claiming it may carry other changes to the entry — its password, say —
91
+ judged with it: the entry lands in the organization that write is
92
+ allowed in.
86
93
  - **Putting** one in transit asks for write on the organization it leaves,
87
94
  and nothing on the transit branch: an administrator only lets go of what
88
95
  they manage. Handing an account over to an organization one does not manage
89
96
  is two steps: put it in transit, then its new administrator claims it.
90
- - Any other change to an entry in transit (attributes, password, status,
91
- deletion) is judged as before, so the entry is claimed first.
97
+ - Any other change to an entry in transit, not claiming it (attributes,
98
+ password, status, deletion), is judged as before: on the transit branch
99
+ for an entry attached to it, on its parent branch for one attached to
100
+ nothing.
92
101
 
93
102
  `GET /api/v1/authz/scope` returns the transit branch as `transit` (`null`
94
103
  when there is none), so that a client can offer these moves. A group attached
@@ -106,13 +106,44 @@ Permissions applied to all group members:
106
106
 
107
107
  ## Permission Resolution
108
108
 
109
- Permissions are resolved in this order:
110
-
111
- 1. **User-specific permissions** - If user has direct branch permissions, use them
112
- 2. **Group permissions** - Check all groups user belongs to
113
- 3. **Default permissions** - Fall back to defaults if no specific rules match
114
-
115
- For each permission type (read/write/delete), the **most permissive** rule wins.
109
+ The rules holding the target, the user's own and those of every group they
110
+ belong to, are combined:
111
+
112
+ 1. **Within one source** (the user's own rules, or one group's), the rule of
113
+ the **most specific** branch holding the target applies, whatever order
114
+ the rules are written in, broader rules of that source aside. Branches
115
+ are compared as DNs, whatever their case and spacing.
116
+ 2. **Between sources**, for each permission type (read/write/delete), the
117
+ **most permissive** wins.
118
+ 3. **Default permissions** apply to a permission none of these rules sets. They are a fallback, not a floor: `"default": {"read": true}` does
119
+ not undo a rule refusing read.
120
+
121
+ Creating or writing an entry a rule names also asks for that rule's
122
+ permission, and
123
+ moving or deleting one holding such an entry asks for read and write, or
124
+ delete, at every rule below it: a caller granted `ou=organization` cannot
125
+ rename `ou=payroll` from under the rule refusing it, nor change or delete
126
+ the `ou=payroll` entry itself. This holds under
127
+ `--authz-filter-attached-entries` too, where `departmentSync` would relink
128
+ the accounts attached there. A write setting the organization link
129
+ (`--ldap-organization-link-attribute`) is judged on the organizations it
130
+ leaves and joins; anything else it carries still needs write where the
131
+ entry stands: its branch, or, judged by attachment, the organization it
132
+ leaves.
133
+
134
+ A rule's branch matches a DN however it is spelled, as OpenLDAP reads it:
135
+ case, spaces, escapes (`\2C` and `\,`, `\20` and a space, `\C3\A9` and
136
+ `é`), an attribute type by any of its names or its OID
137
+ (`organizationalUnitName=` and `2.5.4.11=` are `ou=`, learned from the
138
+ directory schema at startup), the spaces its matching rules ignore, and
139
+ the LDAPv2 forms (`;` between RDNs, a value in double quotes). A write is
140
+ judged on the entry as the directory names it, read as the server before
141
+ the write. A caller cannot step around a rule by spelling its branch
142
+ another way.
143
+
144
+ A move is judged on the mover's own rules. The rules of other users and
145
+ groups name the branch by its DN: once a caller allowed to move it has done
146
+ so, they no longer apply to it, and need rewriting for the new DN.
116
147
 
117
148
  ## How It Works
118
149
 
@@ -212,6 +243,25 @@ With this config, user `manager` can read:
212
243
  - `ou=team1,ou=dept1,ou=organization,dc=example,dc=com`
213
244
  - ... all descendants
214
245
 
246
+ A rule on a sub-branch takes over below it. To keep `manager` out of one
247
+ department:
248
+
249
+ ```json
250
+ {
251
+ "users": {
252
+ "manager": {
253
+ "ou=organization,dc=example,dc=com": { "read": true },
254
+ "ou=payroll,ou=organization,dc=example,dc=com": { "read": false }
255
+ }
256
+ }
257
+ }
258
+ ```
259
+
260
+ A search granted on `ou=organization` then returns nothing from
261
+ `ou=payroll`: each entry it finds is kept only where the caller may read it.
262
+ A group the user belongs to may still grant the department, as the most
263
+ permissive source wins.
264
+
215
265
  ## Examples
216
266
 
217
267
  ### Example 1: Department-Based Access
@@ -412,8 +462,9 @@ Cache hit/miss logging:
412
462
 
413
463
  ### Permission Inheritance
414
464
 
415
- - Sub-branch permissions inherit from parent branches
416
- - Cannot grant more restrictive permissions on sub-branches
465
+ - Sub-branch rules take over from parent branch rules, for what they set
466
+ - A rule may restrict a sub-branch as well as widen it, but another source
467
+ (a group) granting more still wins
417
468
  - Plan branch structure carefully
418
469
 
419
470
  ### Default Permissions
@@ -18,6 +18,29 @@ saying that one `sid`, or every session of one `sub`, is dead. Sessions stay
18
18
  in their cookie, and the storage holds only what died — which is what keeps
19
19
  it small.
20
20
 
21
+ ## The Server's Own `/logout`
22
+
23
+ A provider sends logout tokens to the other applications, not to the one
24
+ that asked it to log out. So `core/auth/openidconnect` records the session
25
+ its own `/logout` ends, as a logout token naming it would: by the id it gave
26
+ that session at login, kept in the cookie, which ends that one session and
27
+ leaves the person's others alone. The same person logging in again over a
28
+ live session keeps its id, so a copy taken before is ended with it.
29
+
30
+ A session from before that id existed is named by the provider's `sid`, when
31
+ it sent one. A provider that sends none (Dex) leaves such a session
32
+ unnamed: its copies keep working until its tokens expire, and the first
33
+ `/logout` of one logs a warning. That lasts until those sessions have
34
+ expired, a week at most.
35
+
36
+ Without `core/bcl`, `/logout` drops the browser's cookie and nothing else,
37
+ and a copy of it keeps working until its tokens expire.
38
+
39
+ A plugin subscribing to `oidclogouttoken` receives `{ iss, session }` from
40
+ `/logout`, with neither `sid` nor `sub`: `session` names one session of this
41
+ server, and is matched against the `session` that `oidcsessionvalid`
42
+ carries.
43
+
21
44
  ## Configuration
22
45
 
23
46
  ```bash
@@ -45,6 +68,11 @@ A tombstone has to outlive the session it kills, or a cookie older than the
45
68
  mark would be honoured again. The default matches `express-openid-connect`'s
46
69
  own seven-day session; shorten it only alongside the session lifetime.
47
70
 
71
+ A copy of a session `/logout` ended is refused, so no longer renewed: it
72
+ dies a day after its last use at the latest, the cookie's rolling lifetime.
73
+ While the store cannot answer, though, a copy is still renewed, up to the
74
+ week; keep the retention at the session lifetime.
75
+
48
76
  Expiry is enforced when the mark is read, not only when the sweeper passes,
49
77
  so a late sweeper never keeps out someone who has since logged in again.
50
78
 
@@ -121,6 +121,14 @@ A provider whose discovery document names no `end_session_endpoint` gets a
121
121
  local logout — the cookie is dropped, the browser goes to `--base-url` — and
122
122
  a warning saying the provider may log the caller back in.
123
123
 
124
+ The session itself ends here only with [`core/bcl`](back-channel-logout.md)
125
+ loaded. The cookie holds the whole session, so dropping it ends the
126
+ browser's copy: a copy kept elsewhere — a shared computer, a backup of the
127
+ browser profile, a stolen cookie — keeps working until its tokens expire.
128
+ With `core/bcl`, `/logout` records the session it ends, named by an id given
129
+ at login, and that copy is refused. Without it, the first `/logout` logs a
130
+ warning saying so.
131
+
124
132
  `GET /v1/config` (with `core/configApi`) publishes the route, so a front-end
125
133
  knows it can offer a sign-out control. The key is `openidconnect` whatever
126
134
  the instance is named:
@@ -100,6 +100,11 @@ DM_MAIL_ATTRIBUTE="mail"
100
100
 
101
101
  - `--applicative-account-base`: LDAP base DN for applicative accounts (required)
102
102
  - `--max-app-accounts`: Maximum number of app accounts per user (default: 5)
103
+ - `--app-accounts-user-attribute`: attribute the `:user` of the routes is
104
+ matched against (default: the mail attribute, unique directory-wide).
105
+ `uid` is safe only where a uid is unique across the whole directory: a
106
+ uid shared by users the caller can read is answered `409`, and one
107
+ shared with a user it cannot read resolves to the one it can
103
108
  - `--mail-attribute`: LDAP attribute for email (default: `mail`)
104
109
  - `--api-prefix`: API endpoint prefix (default: `/api`)
105
110
 
@@ -38,9 +38,42 @@ branch per organization. It serves the groups of an organization under
38
38
  ## Routes
39
39
 
40
40
  Every route answers 404 `ORGANIZATION_NOT_FOUND` when the organization entry
41
- is missing, and 410 `ORGANIZATION_DELETED` when it is deleted. Errors are
42
- `{ "error": "...", "code": "..." }`. A request an authorization plugin refuses
43
- answers 403 `REFUSED`.
41
+ is missing or the caller cannot read it, and 410 `ORGANIZATION_DELETED` when
42
+ it is deleted. Errors are `{ "error": "...", "code": "..." }`. A request an
43
+ authorization plugin refuses answers 403 `REFUSED`.
44
+
45
+ The routes read as the caller: the authorization plugins decide which groups
46
+ and members it gets. A group it cannot read answers 404, a user it cannot
47
+ read is not found (404 `USER_NOT_FOUND` when adding it), and a member it
48
+ cannot read lists as `{ "uid": "..." }`. A write is judged before anything
49
+ is looked for, so a caller who may not make it gets 403 whether the name is
50
+ taken, the users exist or they are already members.
51
+
52
+ What the caller may read is the authorization model's to say:
53
+
54
+ - A model judging by branch (`core/auth/authzPerBranch`) gives an
55
+ organization's groups and users to the callers granted its branch.
56
+ - A model judging entries by their organization link (`core/auth/authzLinid1`,
57
+ or `--authz-filter-attached-entries`) gives an entry carrying no link to
58
+ every administrator. Under such a model only, every group written in an
59
+ organization's branch — through these routes, SCIM or another plugin — is
60
+ given the organization link (`--ldap-organization-link-attribute`, with the
61
+ auxiliary class `twakeGroup` when it holds neither it nor
62
+ `twakeStaticGroup`), and follows it when moved to another organization; a
63
+ link naming another organization is refused (400). A group is then judged
64
+ on its organization: creating or changing one needs write on the
65
+ organization, not on its group branch only. What stays every
66
+ administrator's: whether an organization exists or is deleted, its spaces,
67
+ and users carrying no link, whose link is the provisioning tool's to set.
68
+ Such users are in transit: any administrator may claim them into an
69
+ organization they manage, the first to do so taking them — see
70
+ [transit](../auth/authz-linid1.md#transit-handing-an-entry-over).
71
+ With `core/ldap/organizations` loaded too, the organizations must lie under
72
+ `--ldap-top-organization`, where it accepts a link.
73
+ - A plugin judging every read made for the request, as
74
+ `core/auth/authzDynamic` does, judges the organization check too: an
75
+ organization its token does not reach answers 404, as one that is not
76
+ there.
44
77
 
45
78
  A group reads:
46
79
 
@@ -320,6 +320,13 @@ the caller sent, and so does a group `PUT` for its members, hidden ones
320
320
  included. A group left with no member the caller saw gets the
321
321
  `--group-dummy-user` placeholder.
322
322
 
323
+ A member is resolved as the caller sees the directory, whether it is given
324
+ by id or by DN: an account the caller cannot read is left out of the group,
325
+ as one that does not exist is. Under an authorization model judging by
326
+ branch, a member in a branch the caller may not read refuses the whole write
327
+ instead, whether the account exists or not. A DN outside the user base is
328
+ left out.
329
+
323
330
  ### Schema advertisement
324
331
 
325
332
  Schemas listed under `GET /scim/v2/Schemas` are read from
@@ -179,7 +179,9 @@ Member events compare each moved user's roles before and after a change:
179
179
  ## Routes
180
180
 
181
181
  Every route answers 404 `ORGANIZATION_NOT_FOUND` when the organization entry
182
- is missing, and 410 `ORGANIZATION_DELETED` when it is deleted. Errors are
182
+ is missing or the caller cannot read it, and 410 `ORGANIZATION_DELETED` when
183
+ it is deleted. A user the caller cannot read is not found
184
+ (`USER_NOT_FOUND`). Errors are
183
185
  `{ "error": "...", "code": "..." }`. A request an authorization plugin refuses
184
186
  answers 403 `REFUSED`. A write answers `{ "success": true }` unless stated
185
187
  otherwise.
@@ -318,6 +318,26 @@ curl -X POST "http://localhost:8081/api/v1/ldap/bulk-import/users" \
318
318
  - Check organization DNs exist
319
319
  - Estimate import duration
320
320
 
321
+ A dry run is judged as the import is: a row the caller may not write fails,
322
+ and a row updating an existing entry counts as `updated`.
323
+
324
+ ### Authorization
325
+
326
+ Each row's write is judged by the authorization plugins before anything is
327
+ looked for in the directory: the entry, its organization, whether it exists.
328
+ A row is let through when the caller may create its entry, or change the
329
+ entry it names (an administrator updating the accounts of their organization
330
+ from a file naming none). Otherwise it fails with
331
+ `Token does not have permission on this branch` (`code: forbidden`), whether
332
+ its entry and its organization are there or not.
333
+
334
+ A row the caller may write tells, as a single creation does, whether its
335
+ identifier is taken: it is then skipped, or updated with `updateExisting`.
336
+
337
+ The organization link of a row names one organization: a row naming several,
338
+ or holding an empty value (`;` in a cell), fails as invalid, reported in
339
+ `errors` (a 400 answer with `continueOnError=false`).
340
+
321
341
  ### Update Existing Mode
322
342
 
323
343
  Update entries that already exist:
@@ -63,8 +63,18 @@ check that justified it.
63
63
  non-individual entries.
64
64
  - `filter` narrows the search, e.g. `(objectClass=twakeAccount)`.
65
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.
66
+ On an update, a value the entry already holds, as stored and spelled the
67
+ same, is not looked for again; only the values it does not hold yet are.
68
+ Re-sending an unchanged value conflicts neither with the entry itself nor
69
+ with the copies other plugins keep of it — the applicative accounts of
70
+ `appAccountsConsistency` carry the account's mail. A value held under
71
+ another attribute the constraint spans, an alias becoming the mail, is
72
+ skipped only when that attribute's own constraint looks for it everywhere
73
+ this one does; otherwise it is checked. A duplicate the directory already
74
+ holds, from before the rule or another tool, is therefore not reported on a
75
+ re-save. A value spelled differently is a new one, and looked for: changing
76
+ only the case of an account's mail is refused while an applicative copy
77
+ holds the old spelling, which the directory takes for the same address.
68
78
 
69
79
  ### Mail domains
70
80
 
@@ -37,7 +37,7 @@ This plugin intercepts group member validation and automatically creates missing
37
37
  the address escaped as any DN value (RFC 4514): `mail=a\+b@partner.com,…`
38
38
  creates an entry whose `mail` is `a+b@partner.com`. Only the direct children of the
39
39
  branch are external members.
40
- - **Domain Protection**: External users cannot be created if their mail are in managed domains (configured via `--mail-domain`, compared case-insensitively)
40
+ - **Domain Protection**: External users cannot be created if their mail is in a managed domain, compared case-insensitively. The managed domains are those of `--mail-domain` and those the directory declares in its domain branches: the entries of a flat schema declaring the `domainName` role, or else `ou=domains,ou=nomenclature,<base>` with `--enterprise-domain-name-attribute` (`associatedDomain`). A subdomain of a managed domain is not managed: `user@sub.example.com` is external unless `sub.example.com` is declared itself
41
41
  - **Automatic Creation**: Users are created automatically when added to a group if they don't exist
42
42
 
43
43
  ### Config API
@@ -56,7 +56,8 @@ where to send them:
56
56
 
57
57
  A client turns an address into a member with
58
58
  `rdnAttribute=<escaped address>,<branch>`, and can refuse a `managedDomains`
59
- address before sending it.
59
+ address before sending it. The list holds the domains declared in the
60
+ directory as well as those of `--mail-domain`.
60
61
 
61
62
  ## How It Works
62
63
 
@@ -225,6 +226,9 @@ Protect multiple domains from external user creation:
225
226
  --mail-domain example.com,company.org,subsidiary.net
226
227
  ```
227
228
 
229
+ The domains declared in the directory are protected without being listed
230
+ here.
231
+
228
232
  ### Custom Attributes via Hooks
229
233
 
230
234
  Add organization-specific attributes to external users:
@@ -261,7 +265,8 @@ hooks: {
261
265
  **Solutions:**
262
266
 
263
267
  1. Use the correct branch for internal users: `ou=users,dc=example,dc=com`
264
- 2. Remove domain from `--mail-domain` if it should allow external users
268
+ 2. Remove the domain from `--mail-domain`, or from the directory's domain
269
+ branches, if it should allow external users
265
270
  3. This is expected behavior for internal domains - create user in users branch instead
266
271
 
267
272
  ### Malformed Member DN
@@ -120,6 +120,9 @@ Each schema file must include an `entity` metadata section and attribute definit
120
120
  ```
121
121
 
122
122
  Pointer validation ensures the DN exists and is within allowed branches.
123
+ The DN is looked for as the server, whoever writes: a caller may point at
124
+ an entry it cannot read, and learns from the answer whether one is there,
125
+ never what it holds.
123
126
 
124
127
  #### Fixed Attributes
125
128
 
@@ -410,6 +410,12 @@ The plugin supports JSON schema validation for group attributes. Example schema:
410
410
  }
411
411
  ```
412
412
 
413
+ A `pointer` attribute is checked as in
414
+ [flat schemas](flat-generic.md#pointer-dn-reference): the DN must name an
415
+ entry in the allowed branches, looked for as the server, whoever writes.
416
+ A caller may point at an entry it cannot read, and learns from the answer
417
+ whether one is there, never what it holds.
418
+
413
419
  ## Hooks
414
420
 
415
421
  The plugin emits and listens to lifecycle hooks:
@@ -174,6 +174,14 @@ curl "http://localhost:8081/api/v1/ldap/organizations/ou%3DIT%2Co%3Dgov%2Cc%3Dmu
174
174
  ]
175
175
  ```
176
176
 
177
+ **Where the attached entries are looked for**
178
+
179
+ Below the parent of `--ldap-top-organization`, as the caller. A caller an
180
+ authorization plugin does not let search there — `authzPerBranch` judging
181
+ by DN, a token of `authzDynamic` — gets the entries attached within the
182
+ organization's own branch; those attached from elsewhere are not listed.
183
+ `/subnodes/search` looks the same way.
184
+
177
185
  **When the answer is partial**
178
186
 
179
187
  Two things bound what comes back, and both are announced in the list rather
@@ -715,6 +723,15 @@ The plugin validates organizations against configured object classes. Example sc
715
723
  }
716
724
  ```
717
725
 
726
+ A `pointer` attribute is checked as in
727
+ [flat schemas](flat-generic.md#pointer-dn-reference): the DN must name an
728
+ entry in the allowed branches, looked for as the server, whoever writes.
729
+ A caller may point at an entry it cannot read, and learns from the answer
730
+ whether one is there, never what it holds. A pointer that grants rights
731
+ follows the same rule: an organization administrator may name, in
732
+ `twakeLocalAdminLink`, an account they cannot read, and so hand the
733
+ administration of their organization to it.
734
+
718
735
  ## See Also
719
736
 
720
737
  - [LDAP Flat Plugin](./ldapFlat.md) - For managing flat LDAP entities (users, positions, nomenclature) through schema-driven approach
@@ -199,7 +199,7 @@ Combine with an authentication plugin ([token](../auth/token.md), [oidc](../auth
199
199
 
200
200
  - **Schema fetch**: the root DSE is read for `subschemaSubentry` (falling back to `cn=Subschema`), then the subschema entry is fetched and parsed once per `--ldap-raw-schema-cache-ttl`. Concurrent callers share a single fetch, and so does the [object class repair](flat-generic.md#entries-created-by-other-tools), which loads it even without this plugin.
201
201
  - **Operational attributes**: entries are read with `*` and `+`. When the server rejects `+`, the read is retried with `*` alone.
202
- - **Missing entries**: the LDAP "no such object" result (code 32) is mapped to a 404.
202
+ - **Missing entries**: the LDAP "no such object" result (code 32) is mapped to a 404. A search whose base the caller cannot read answers the same 404 as one whose base does not exist.
203
203
 
204
204
  ## Limitations
205
205