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.
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/twakePlugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/lib/authz/composition.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/mailDomains.js +1 -0
- package/dist/lib/requestContext.js +1 -0
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/auth/authzDynamic.js +1 -1
- package/dist/plugins/auth/authzLinid1.js +1 -1
- package/dist/plugins/auth/authzPerBranch.js +1 -1
- package/dist/plugins/auth/authzScope.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/bcl.js +1 -1
- package/dist/plugins/configApi.js +1 -1
- package/dist/plugins/ldap/bulkImport.js +1 -1
- package/dist/plugins/ldap/departmentSync.js +1 -1
- package/dist/plugins/ldap/enterpriseRules.js +1 -1
- package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/passwordPolicy.js +1 -1
- package/dist/plugins/ldap/raw.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/rabbitmq.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/storage/ldap.js +1 -1
- package/dist/plugins/storage/postgres.js +1 -1
- package/dist/plugins/storage/valkey.js +1 -1
- package/dist/plugins/twake/appAccountsApi.js +1 -1
- package/dist/plugins/twake/appAccountsConsistency.js +1 -1
- package/dist/plugins/twake/clouderyProvision.js +1 -1
- package/dist/plugins/twake/groups.js +1 -1
- package/dist/plugins/twake/instances.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/plugins/twake/lifecycleEvents.js +1 -1
- package/dist/plugins/twake/spaces.js +1 -1
- package/dist/plugins/twake/tombstone.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +25 -6
- package/dist/src/abstract/plugin.d.ts +1 -1
- package/dist/src/bin/index.d.ts +12 -0
- package/dist/src/hooks.d.ts +12 -3
- package/dist/src/lib/authz/base.d.ts +87 -4
- package/dist/src/lib/authz/composition.d.ts +21 -0
- package/dist/src/lib/ldapActions.d.ts +139 -18
- package/dist/src/lib/mailDomains.d.ts +41 -0
- package/dist/src/lib/requestContext.d.ts +23 -0
- package/dist/src/lib/utils.d.ts +41 -2
- package/dist/src/plugins/auth/authzDynamic.d.ts +5 -12
- package/dist/src/plugins/auth/authzLinid1.d.ts +3 -1
- package/dist/src/plugins/auth/authzPerBranch.d.ts +5 -9
- package/dist/src/plugins/auth/openidconnect.d.ts +47 -0
- package/dist/src/plugins/bcl.d.ts +2 -1
- package/dist/src/plugins/ldap/accountLifecycle.d.ts +2 -2
- package/dist/src/plugins/ldap/bulkImport.d.ts +30 -5
- package/dist/src/plugins/ldap/enterpriseRules.d.ts +11 -1
- package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +20 -3
- package/dist/src/plugins/ldap/groups.d.ts +48 -1
- package/dist/src/plugins/ldap/organizations.d.ts +4 -4
- package/dist/src/plugins/twake/appAccountsApi.d.ts +3 -67
- package/dist/src/plugins/twake/groups.d.ts +46 -2
- package/dist/src/plugins/twake/james.d.ts +0 -11
- package/docs/README.md +25 -0
- package/docs/plugin-development/README.md +61 -15
- package/docs/plugin-development/dependencies.md +8 -2
- package/docs/plugin-development/hooks.md +4 -5
- package/docs/usage/configuration.md +11 -8
- package/docs/usage/plugins/auth/README.md +22 -3
- package/docs/usage/plugins/auth/authz-linid1.md +14 -5
- package/docs/usage/plugins/auth/authz-per-branch.md +60 -9
- package/docs/usage/plugins/auth/back-channel-logout.md +28 -0
- package/docs/usage/plugins/auth/oidc.md +8 -0
- package/docs/usage/plugins/integrations/app-accounts.md +5 -0
- package/docs/usage/plugins/integrations/groups.md +36 -3
- package/docs/usage/plugins/integrations/scim.md +7 -0
- package/docs/usage/plugins/integrations/spaces.md +3 -1
- package/docs/usage/plugins/ldap/bulk-import.md +20 -0
- package/docs/usage/plugins/ldap/enterprise-rules.md +12 -2
- package/docs/usage/plugins/ldap/external-users.md +8 -3
- package/docs/usage/plugins/ldap/flat-generic.md +3 -0
- package/docs/usage/plugins/ldap/groups.md +6 -0
- package/docs/usage/plugins/ldap/organizations.md +17 -0
- package/docs/usage/plugins/ldap/raw.md +1 -1
- package/docs/usage/upgrading.md +228 -0
- package/package.json +1 -1
- package/static/schemas/example/groups.json +1 -0
- package/static/schemas/example/organizations.json +1 -0
- package/static/schemas/example/users.json +3 -0
- package/static/schemas/twake/groups.json +2 -0
- package/static/schemas/twake/organizations.json +1 -0
- package/static/schemas/twake/teamMailboxes.json +3 -1
- 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
|
-
|
|
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
|
|
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,
|
|
91
|
-
deletion) is judged as before
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
416
|
-
-
|
|
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
|
|
42
|
-
`{ "error": "...", "code": "..." }`. A request an
|
|
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
|
|
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
|
|
67
|
-
|
|
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
|
|
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
|
|
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
|
|