ldap-rest 0.8.2 → 0.9.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.
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/auth/base.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/lib/authz/composition.js +1 -0
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/ldapCodes.js +1 -0
- package/dist/lib/parseConfig.js +1 -1
- package/dist/lib/storage/store.js +1 -0
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/auth/authzDynamic.js +1 -1
- package/dist/plugins/auth/authzPerBranch.js +1 -1
- package/dist/plugins/auth/authzPerRoute.js +1 -1
- package/dist/plugins/auth/authzScope.js +1 -1
- package/dist/plugins/auth/hmac.js +1 -1
- package/dist/plugins/auth/llng.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/auth/token.js +1 -1
- package/dist/plugins/auth/totp.js +1 -1
- package/dist/plugins/bcl.js +1 -0
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/priority.json.js +1 -1
- package/dist/plugins/scim/baseResolver.js +1 -1
- package/dist/plugins/scim/errors.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/list.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/storage/file.js +1 -0
- package/dist/plugins/storage/ldap.js +1 -0
- package/dist/plugins/storage.js +1 -0
- package/dist/src/abstract/ldapFlat.d.ts +7 -0
- package/dist/src/abstract/plugin.d.ts +20 -0
- package/dist/src/bin/index.d.ts +50 -8
- package/dist/src/config/args.d.ts +36 -0
- package/dist/src/hooks.d.ts +55 -0
- package/dist/src/lib/auth/base.d.ts +175 -9
- package/dist/src/lib/authz/base.d.ts +133 -4
- package/dist/src/lib/authz/composition.d.ts +145 -0
- package/dist/src/lib/expressFormatedResponses.d.ts +9 -2
- package/dist/src/lib/ldapActions.d.ts +30 -0
- package/dist/src/lib/ldapCodes.d.ts +28 -0
- package/dist/src/lib/storage/store.d.ts +84 -0
- package/dist/src/lib/utils.d.ts +19 -0
- package/dist/src/plugins/auth/authzDynamic.d.ts +76 -0
- package/dist/src/plugins/auth/authzPerBranch.d.ts +11 -0
- package/dist/src/plugins/auth/authzPerRoute.d.ts +26 -0
- package/dist/src/plugins/auth/authzScope.d.ts +44 -3
- package/dist/src/plugins/auth/hmac.d.ts +2 -0
- package/dist/src/plugins/auth/llng.d.ts +39 -0
- package/dist/src/plugins/auth/openidconnect.d.ts +66 -3
- package/dist/src/plugins/auth/rateLimit.d.ts +3 -1
- package/dist/src/plugins/auth/token.d.ts +2 -0
- package/dist/src/plugins/auth/totp.d.ts +2 -0
- package/dist/src/plugins/bcl.d.ts +65 -0
- package/dist/src/plugins/ldap/organizations.d.ts +90 -1
- package/dist/src/plugins/scim/baseResolver.d.ts +3 -1
- package/dist/src/plugins/scim/errors.d.ts +2 -10
- package/dist/src/plugins/storage/file.d.ts +19 -0
- package/dist/src/plugins/storage/ldap.d.ts +17 -0
- package/dist/src/plugins/storage.d.ts +44 -0
- package/docs/plugin-development/README.md +12 -1
- package/docs/plugin-development/dependencies.md +40 -15
- package/docs/usage/configuration.md +89 -13
- package/docs/usage/plugins/README.md +17 -12
- package/docs/usage/plugins/auth/README.md +129 -0
- package/docs/usage/plugins/auth/authz-dynamic.md +42 -0
- package/docs/usage/plugins/auth/authz-linid1.md +32 -0
- package/docs/usage/plugins/auth/authz-per-branch.md +8 -0
- package/docs/usage/plugins/auth/authz-per-route.md +6 -0
- package/docs/usage/plugins/auth/authz-scope.md +69 -16
- package/docs/usage/plugins/auth/back-channel-logout.md +86 -0
- package/docs/usage/plugins/auth/llng.md +29 -0
- package/docs/usage/plugins/auth/oidc.md +52 -0
- package/docs/usage/plugins/ldap/organizations.md +37 -0
- package/docs/usage/plugins/utilities/storage.md +94 -0
- package/docs/usage/upgrading.md +289 -0
- package/package.json +17 -1
|
@@ -408,6 +408,14 @@ Cache hit/miss logging:
|
|
|
408
408
|
- **Open by default**: Set `read: true` for general access
|
|
409
409
|
- Choose based on your security posture
|
|
410
410
|
|
|
411
|
+
`--authz-per-branch-config` is never empty: unset, it is
|
|
412
|
+
`{"default":{"read":true,"write":false,"delete":false}}`, and it applies to
|
|
413
|
+
**every** authenticated identity, whatever authenticated it. Loaded beside a
|
|
414
|
+
token plugin, `authzDynamic` or an identity provider whose callers no rule
|
|
415
|
+
names, this plugin lets them read and refuses every write. Give it an
|
|
416
|
+
`authz_for` naming the authentication plugins it was written for — see
|
|
417
|
+
[several authorization plugins](README.md#several-authorization-plugins).
|
|
418
|
+
|
|
411
419
|
### Group-Based Security
|
|
412
420
|
|
|
413
421
|
- Group DNs must be fully qualified
|
|
@@ -107,6 +107,12 @@ one-level:GET:/api/v1/ldap/users/*
|
|
|
107
107
|
| User authenticated but has no rules configured | 403 Forbidden |
|
|
108
108
|
| User has at least one matching rule | 200 / pass through |
|
|
109
109
|
| User has rules but none match the current method+path | 403 Forbidden |
|
|
110
|
+
| Authenticated by a plugin outside `authz_for` | Pass through — another population, judged elsewhere |
|
|
111
|
+
|
|
112
|
+
`authz_for` lists the authentication plugins, by instance name, whose
|
|
113
|
+
requests the rules are for; unset, they apply to every authenticated
|
|
114
|
+
request. See [several authorization
|
|
115
|
+
plugins](README.md#several-authorization-plugins).
|
|
110
116
|
|
|
111
117
|
## Full Example
|
|
112
118
|
|
|
@@ -15,20 +15,60 @@ whichever authorization plugin is in force.
|
|
|
15
15
|
--plugin core/auth/authzScope
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
It
|
|
19
|
-
|
|
20
|
-
`
|
|
18
|
+
It answers through a loaded plugin that carries the `authz` role _and_ can
|
|
19
|
+
say who may do what where — one implementing `resolveUser`,
|
|
20
|
+
`getAuthorizedBranches` and `getUserPermissions`. Both
|
|
21
21
|
[authz-linid1](authz-linid1.md) and [authz-per-branch](authz-per-branch.md)
|
|
22
22
|
do, and this endpoint works with either without knowing which.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
| CLI | Env | Default | Description |
|
|
25
|
+
| ---------------------- | ----------------------- | ------- | ------------------------------------------------------------- |
|
|
26
|
+
| `--authz-scope-source` | `DM_AUTHZ_SCOPE_SOURCE` | | The plugin, by instance name, that describes a caller's scope |
|
|
27
|
+
|
|
28
|
+
### Which plugin answers
|
|
29
|
+
|
|
30
|
+
The caller is resolved under the name the hooks key on — `req.user`, or
|
|
31
|
+
`req.userName` with `--authz-identity req.userName` — so the scope described
|
|
32
|
+
is the one enforced.
|
|
33
|
+
|
|
34
|
+
Only a plugin that judges the caller answers for them: one scoped with
|
|
35
|
+
`authz_for` to other authentication plugins is not this caller's model, and
|
|
36
|
+
is passed over (see [several authorization
|
|
37
|
+
plugins](README.md#several-authorization-plugins)).
|
|
38
|
+
|
|
39
|
+
When more than one judges the caller — two branch-level plugins combined with
|
|
40
|
+
`--authz-combine` — the hooks apply both and this endpoint can describe only
|
|
41
|
+
one. `--authz-scope-source` names it. Without it, the first loaded answers,
|
|
42
|
+
and for plugins outside the priority list that is import order, which nobody
|
|
43
|
+
chose: the server says so at startup, and the answer lists every plugin
|
|
44
|
+
judging the caller under `sources`, the one it comes from under `source`. A
|
|
45
|
+
name that is not a loaded plugin able to describe a scope is refused at
|
|
46
|
+
startup.
|
|
47
|
+
|
|
48
|
+
### When nothing can describe the scope
|
|
49
|
+
|
|
50
|
+
[authz-per-route](authz-per-route.md) gates URLs and
|
|
51
|
+
[authz-dynamic](authz-dynamic.md) reads a token: both carry the `authz` role,
|
|
52
|
+
neither resolves a user or a branch. When they judge the caller and no
|
|
53
|
+
branch-level plugin does, no model can say what the caller may do, and the
|
|
54
|
+
answer says that rather than guessing:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"user": "alice",
|
|
59
|
+
"unrestricted": false,
|
|
60
|
+
"described": false,
|
|
61
|
+
"source": null,
|
|
62
|
+
"sources": ["authzPerRoute"],
|
|
63
|
+
"branches": [],
|
|
64
|
+
"entities": []
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This endpoint used to answer `unrestricted: true` with `create: true` on
|
|
69
|
+
every entity there — an authorization judgement handed to a client that acts
|
|
70
|
+
on it, while the route or the token refused. A client that reads an entity
|
|
71
|
+
missing from `entities` as not creatable offers nothing.
|
|
32
72
|
|
|
33
73
|
## Endpoint
|
|
34
74
|
|
|
@@ -40,6 +80,9 @@ GET /api/v1/authz/scope
|
|
|
40
80
|
{
|
|
41
81
|
"user": "uid=alice,ou=users,dc=example,dc=com",
|
|
42
82
|
"unrestricted": false,
|
|
83
|
+
"described": true,
|
|
84
|
+
"source": "authzLinid1",
|
|
85
|
+
"sources": ["authzLinid1"],
|
|
43
86
|
"branches": [
|
|
44
87
|
{
|
|
45
88
|
"dn": "ou=Sales,ou=organization,dc=example,dc=com",
|
|
@@ -77,13 +120,16 @@ is where a new organization goes when the client names no parent. A local admini
|
|
|
77
120
|
`create: false` for organizations, and creates sub-organizations under their own
|
|
78
121
|
node by naming it as `parentDn`.
|
|
79
122
|
|
|
80
|
-
With no authorization plugin
|
|
81
|
-
answer says so:
|
|
123
|
+
With no authorization plugin judging the caller the server grants
|
|
124
|
+
everything, and the answer says so:
|
|
82
125
|
|
|
83
126
|
```json
|
|
84
127
|
{
|
|
85
128
|
"user": "alice",
|
|
86
129
|
"unrestricted": true,
|
|
130
|
+
"described": true,
|
|
131
|
+
"source": null,
|
|
132
|
+
"sources": [],
|
|
87
133
|
"branches": [],
|
|
88
134
|
"entities": [
|
|
89
135
|
{ "name": "users", "base": "ou=users,dc=example,dc=com", "create": true }
|
|
@@ -93,9 +139,16 @@ answer says so:
|
|
|
93
139
|
|
|
94
140
|
## Errors
|
|
95
141
|
|
|
96
|
-
| Status | When
|
|
97
|
-
| ------ |
|
|
98
|
-
| `401` | No authenticated user, or a user
|
|
142
|
+
| Status | When |
|
|
143
|
+
| ------ | ------------------------------------------------------------------------------------------- |
|
|
144
|
+
| `401` | No authenticated user while any authorization plugin is loaded, or a user it cannot resolve |
|
|
145
|
+
|
|
146
|
+
An anonymous caller is refused as soon as anything authorizes: every
|
|
147
|
+
authorization plugin skips a request without an identity, so nothing would
|
|
148
|
+
judge them and the answer would be `unrestricted: true` — more than an
|
|
149
|
+
identified caller of the same server is told. A server loading no
|
|
150
|
+
authorization plugin at all answers `unrestricted` to anyone, anonymous
|
|
151
|
+
included, which is what it does.
|
|
99
152
|
|
|
100
153
|
## See also
|
|
101
154
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Back-Channel Logout
|
|
2
|
+
|
|
3
|
+
Honouring a logout the provider performed elsewhere.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
When someone logs out at the OpenID Provider — or an administrator ends their
|
|
8
|
+
sessions — the provider sends a **logout token** to every application that
|
|
9
|
+
asked for one. Without it, an application keeps honouring its own cookie: the
|
|
10
|
+
session stays alive until it expires on its own, which is exactly what a
|
|
11
|
+
logout is supposed to prevent.
|
|
12
|
+
|
|
13
|
+
`core/bcl` records what such a token killed, and `core/auth/openidconnect`
|
|
14
|
+
asks, on every request, whether the session it holds is still alive.
|
|
15
|
+
|
|
16
|
+
**Nothing here stores a session.** A logout token only leaves a _tombstone_
|
|
17
|
+
saying that one `sid`, or every session of one `sub`, is dead. Sessions stay
|
|
18
|
+
in their cookie, and the storage holds only what died — which is what keeps
|
|
19
|
+
it small.
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
--plugin core/auth/openidconnect \
|
|
25
|
+
--plugin core/storage --storage-backend file --storage-file-directory /var/lib/ldap-rest/storage \
|
|
26
|
+
--plugin core/bcl --bcl-retention 604800
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Option | Environment | Default | Description |
|
|
30
|
+
| ----------------- | ------------------ | -------- | --------------------------- |
|
|
31
|
+
| `--bcl-retention` | `DM_BCL_RETENTION` | `604800` | Seconds a tombstone is kept |
|
|
32
|
+
|
|
33
|
+
Where the marks live, and how often expired ones are reclaimed, belongs to
|
|
34
|
+
[`core/storage`](../utilities/storage.md) — `core/bcl` keeps only the policy.
|
|
35
|
+
|
|
36
|
+
At the provider, register the back-channel logout URI:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
https://ldap-rest.example.com/backchannel-logout
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Retention
|
|
43
|
+
|
|
44
|
+
A tombstone has to outlive the session it kills, or a cookie older than the
|
|
45
|
+
mark would be honoured again. The default matches `express-openid-connect`'s
|
|
46
|
+
own seven-day session; shorten it only alongside the session lifetime.
|
|
47
|
+
|
|
48
|
+
Expiry is enforced when the mark is read, not only when the sweeper passes,
|
|
49
|
+
so a late sweeper never keeps out someone who has since logged in again.
|
|
50
|
+
|
|
51
|
+
## `core/bcl` Refuses to Start Without Storage
|
|
52
|
+
|
|
53
|
+
`core/storage` must be loaded and `--storage-backend` set, or the plugin
|
|
54
|
+
throws at startup.
|
|
55
|
+
|
|
56
|
+
That is deliberate. A logout check that quietly does nothing would leave
|
|
57
|
+
every session outliving the logout that closed it, and nothing would say so.
|
|
58
|
+
For a feature that can be skipped, a warning is the right answer; for this
|
|
59
|
+
one, a refusal to start is the honest failure.
|
|
60
|
+
|
|
61
|
+
The refusal answers for the configuration, not for the order it is written
|
|
62
|
+
in. `core/bcl` declares `core/storage` as a dependency, so a configuration
|
|
63
|
+
naming only `core/bcl` loads the store anyway; and `core/storage` is in the
|
|
64
|
+
priority list, so one that names both gets a single instance whichever comes
|
|
65
|
+
first.
|
|
66
|
+
|
|
67
|
+
## What a Login Forgets
|
|
68
|
+
|
|
69
|
+
A mark on the `sub` kills every session of that person. Left in place it
|
|
70
|
+
would also kill the sessions created _after_ it, for the whole retention —
|
|
71
|
+
so a successful login clears the marks matching its own claims.
|
|
72
|
+
|
|
73
|
+
This has a consequence worth knowing, which the SDK's own default shares: a
|
|
74
|
+
login also revives sessions that a "log out everywhere" token killed without
|
|
75
|
+
naming a `sid`.
|
|
76
|
+
|
|
77
|
+
## Failures Are Not Swallowed
|
|
78
|
+
|
|
79
|
+
If the store cannot record a tombstone, the provider is answered `400` rather
|
|
80
|
+
than `204`. A provider retries a `400`; a `204` tells it a logout was honoured
|
|
81
|
+
that nothing kept.
|
|
82
|
+
|
|
83
|
+
## See Also
|
|
84
|
+
|
|
85
|
+
- [Storage](../utilities/storage.md) — where the marks live
|
|
86
|
+
- [OpenID Connect](oidc.md)
|
|
@@ -15,6 +15,35 @@ Integration with [LemonLDAP::NG](https://lemonldap-ng.org/) (LLNG) Web SSO solut
|
|
|
15
15
|
DM_LLNG_INI="/etc/lemonldap-ng/lemonldap-ng.ini"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Rules that name nobody
|
|
19
|
+
|
|
20
|
+
The plugin serves a request only when the handler named a user. A LemonLDAP::NG
|
|
21
|
+
location rule set to `skip` makes the handler return at once — no session
|
|
22
|
+
read, no `Lm-Remote-User` written — and such a request is refused with 401
|
|
23
|
+
rather than served with no identity: every authorization plugin skips its
|
|
24
|
+
check when a request carries no identity, so "no authentication on this path"
|
|
25
|
+
would have become "authenticated, and scoped by nobody".
|
|
26
|
+
|
|
27
|
+
Serve such a path from a virtual host this plugin does not guard, or scope
|
|
28
|
+
the plugin with `auth_path_prefix` so the path falls outside it.
|
|
29
|
+
|
|
30
|
+
The identity header is read whatever case the handler wrote it in, and
|
|
31
|
+
whatever the client sent under that name is dropped before the handler runs:
|
|
32
|
+
only the handler may name the caller.
|
|
33
|
+
|
|
34
|
+
`req.user` is what `whatToTrace` traces, which is often a mail or a display
|
|
35
|
+
name. `--llng-username-header` names a second header LemonLDAP::NG exports
|
|
36
|
+
carrying the login, published as `req.userName`; with
|
|
37
|
+
`--authz-identity req.userName`, rules are then written on logins. Export it
|
|
38
|
+
from the LLNG configuration as you would any other header.
|
|
39
|
+
|
|
40
|
+
Whatever the request carried under that name is dropped before the handler
|
|
41
|
+
runs, exactly as the identity header is: a rule can be keyed on
|
|
42
|
+
`req.userName`, so a client supplying it would otherwise take over the
|
|
43
|
+
identity that rule names. The consequence is that the value must come from
|
|
44
|
+
the handler — a header set by an upstream proxy does not survive, and would
|
|
45
|
+
only have been trustworthy if that proxy stripped the client's copy itself.
|
|
46
|
+
|
|
18
47
|
## Prerequisites
|
|
19
48
|
|
|
20
49
|
1. **LemonLDAP::NG Handler**: The `lemonldap-ng-handler` npm package (optional dependency)
|
|
@@ -35,6 +35,56 @@ Install the optional dependency:
|
|
|
35
35
|
npm install express-openid-connect
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## Scope
|
|
39
|
+
|
|
40
|
+
Like every other authentication plugin, this one registers with the server's
|
|
41
|
+
authentication dispatcher, so `auth_path_prefix` says which paths it guards:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
--plugin 'core/auth/openidconnect:oidc:{"auth_path_prefix":"/api/admin"}'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Until 0.9.0 the option was accepted and ignored — the plugin mounted its own
|
|
48
|
+
middleware and guarded the whole server — and the layers it mounted kept
|
|
49
|
+
their registration order, so a route plugin loading before it was served
|
|
50
|
+
without a session and an authorization plugin loading after it judged rules
|
|
51
|
+
before `req.user` existed. See
|
|
52
|
+
[the note](../../upgrading.md#openid-connect-honours-auth_path_prefix).
|
|
53
|
+
|
|
54
|
+
Its own routes come with it: `/login`, `/logout`, `/callback` and
|
|
55
|
+
`/backchannel-logout` are claimed whatever the scope, so the provider can
|
|
56
|
+
always reach the callback and post a logout token, and a catch-all
|
|
57
|
+
authentication never answers them with a 401.
|
|
58
|
+
|
|
59
|
+
Two instances are supported — `auth()` builds a fresh router per instance —
|
|
60
|
+
but `--base-url` and the callback path are global, so both share one
|
|
61
|
+
redirect URI. Scope by prefix, not by provider, unless you also front them
|
|
62
|
+
with different base URLs.
|
|
63
|
+
|
|
64
|
+
## The identity it publishes
|
|
65
|
+
|
|
66
|
+
`req.user` is the OIDC `sub` — an opaque provider identifier, `auth0|…` or a
|
|
67
|
+
UUID — which is what authorization rules are keyed on by default, and what
|
|
68
|
+
nobody can write a rule for without looking it up per administrator.
|
|
69
|
+
|
|
70
|
+
`--oidc-username-claim` names the claim published as `req.userName`:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
--oidc-username-claim preferred_username
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Ask the provider for the scope that carries it (`profile`, or `email` for
|
|
77
|
+
`email`), then key the authorization plugins on it with
|
|
78
|
+
`--authz-identity req.userName`. A session missing the configured claim falls
|
|
79
|
+
back to the `sub`, with a warning naming the claim.
|
|
80
|
+
|
|
81
|
+
## What an unauthenticated request receives
|
|
82
|
+
|
|
83
|
+
The library's own answer: a browser is redirected to the provider, an API
|
|
84
|
+
client (`Accept: application/json`) receives 401. Every other authentication
|
|
85
|
+
plugin in this repository answers JSON 401 throughout — the difference is
|
|
86
|
+
kept because it is what makes a login work at all.
|
|
87
|
+
|
|
38
88
|
## How It Works
|
|
39
89
|
|
|
40
90
|
1. Uses `express-openid-connect` for OAuth2/OIDC flow
|
|
@@ -163,5 +213,7 @@ npm install express-openid-connect
|
|
|
163
213
|
|
|
164
214
|
## See Also
|
|
165
215
|
|
|
216
|
+
- [Back-Channel Logout](back-channel-logout.md) — honouring a logout the
|
|
217
|
+
provider performed elsewhere
|
|
166
218
|
- [OpenID Connect Specification](https://openid.net/connect/)
|
|
167
219
|
- [express-openid-connect](https://github.com/auth0/express-openid-connect)
|
|
@@ -174,6 +174,43 @@ curl "http://localhost:8081/api/v1/ldap/organizations/ou%3DIT%2Co%3Dgov%2Cc%3Dmu
|
|
|
174
174
|
]
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
+
**When the answer is partial**
|
|
178
|
+
|
|
179
|
+
Two things bound what comes back, and both are announced in the list rather
|
|
180
|
+
than left for the client to guess:
|
|
181
|
+
|
|
182
|
+
- The attached entries are capped at `--ldap-organization-max-subnodes`
|
|
183
|
+
(default 50). Child organizations are not capped.
|
|
184
|
+
- The directory has a size limit of its own, and a branch holding more
|
|
185
|
+
entries than it will list in one answer makes it refuse rather than
|
|
186
|
+
truncate. The endpoint then asks again for a bounded number and returns
|
|
187
|
+
what it gets, writing a `warn` line naming the node.
|
|
188
|
+
|
|
189
|
+
Either way the list ends with a row that is not an entry:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"dn": "more-ou=IT,dc=example,dc=com",
|
|
194
|
+
"cn": ["... 412 more elements"],
|
|
195
|
+
"objectClass": ["moreIndicator"],
|
|
196
|
+
"_isMoreIndicator": "true",
|
|
197
|
+
"_totalCount": "462",
|
|
198
|
+
"_displayedCount": "50"
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Its DN answers nothing — `more-<the organization's DN>` for the attached
|
|
203
|
+
entries, `more-organizations-<the organization's DN>` for the child
|
|
204
|
+
organizations — and a client should drop any row carrying
|
|
205
|
+
`_isMoreIndicator` before treating the rest as entries. `_totalCount` is
|
|
206
|
+
present only when the directory listed everything and the cap did the
|
|
207
|
+
trimming; when the directory itself refused, nothing knows the total.
|
|
208
|
+
|
|
209
|
+
A refusal is not emptiness: a node whose children cannot be listed answers
|
|
210
|
+
its first fifty and says there are more, and any other directory failure is
|
|
211
|
+
raised rather than returned as `[]`. Raise the size limit for the account
|
|
212
|
+
ldap-rest binds as (`olcLimits`, or `olcSizeLimit`) to see a whole branch.
|
|
213
|
+
|
|
177
214
|
### Create Organization
|
|
178
215
|
|
|
179
216
|
```http
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Storage Plugin
|
|
2
|
+
|
|
3
|
+
Keyed storage for whoever needs to keep something with a deadline.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`core/storage` gives other plugins a key, a value and an instant past which
|
|
8
|
+
the value stops being returned. It knows nothing about why a record exists:
|
|
9
|
+
Back-Channel Logout tombstones were the first use, a refresh-token lock and a
|
|
10
|
+
shared counter are the ones in sight.
|
|
11
|
+
|
|
12
|
+
One plugin, one backend, named by `--storage-backend`. Consumers ask this one
|
|
13
|
+
for its store and work through a single interface; where the records actually
|
|
14
|
+
live is a deployment decision, not theirs.
|
|
15
|
+
|
|
16
|
+
Having exactly one is the point. The alternative — each consumer loading the
|
|
17
|
+
backend it likes — puts the same record in two places, and makes whichever
|
|
18
|
+
plugin was registered first decide what happens when one of them is unwell.
|
|
19
|
+
Loading a second instance is refused rather than left to registration order,
|
|
20
|
+
which is invisible in a configuration.
|
|
21
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
--plugin core/storage --storage-backend ldap \
|
|
26
|
+
--storage-ldap-base ou=Records,dc=example,dc=com
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Option | Environment | Default | Description |
|
|
30
|
+
| ----------------------------- | ------------------------------ | -------------------- | --------------------------------------------------------- |
|
|
31
|
+
| `--storage-backend` | `DM_STORAGE_BACKEND` | _(none)_ | `ldap` or `file`. Empty means the plugin refuses to start |
|
|
32
|
+
| `--storage-sweep-interval` | `DM_STORAGE_SWEEP_INTERVAL` | `600` | Seconds between two passes dropping expired records |
|
|
33
|
+
| `--storage-ldap-base` | `DM_STORAGE_LDAP_BASE` | _(none)_ | Branch the `ldap` backend writes to |
|
|
34
|
+
| `--storage-ldap-object-class` | `DM_STORAGE_LDAP_OBJECT_CLASS` | `applicationProcess` | Object class of the entries it writes |
|
|
35
|
+
| `--storage-file-directory` | `DM_STORAGE_FILE_DIRECTORY` | _(none)_ | Directory the `file` backend writes to |
|
|
36
|
+
|
|
37
|
+
`core/storage` is in the priority list, so it is registered before the
|
|
38
|
+
plugins that consume it whatever order they are written in. A consumer that
|
|
39
|
+
declares it as a dependency — `core/bcl` does — also loads it on its own when
|
|
40
|
+
the configuration does not name it.
|
|
41
|
+
|
|
42
|
+
## Backends
|
|
43
|
+
|
|
44
|
+
### `ldap`
|
|
45
|
+
|
|
46
|
+
One entry per record under `--storage-ldap-base`. The branch holds records
|
|
47
|
+
and nothing else, so it belongs outside the branches the directory serves —
|
|
48
|
+
the plugin does not create it, and will not write into a branch that is not
|
|
49
|
+
there.
|
|
50
|
+
|
|
51
|
+
Suited to a deployment that already replicates its directory and wants the
|
|
52
|
+
records to follow, and to one where no other store is available.
|
|
53
|
+
|
|
54
|
+
### `file`
|
|
55
|
+
|
|
56
|
+
One file per record under `--storage-file-directory`. A write goes to a
|
|
57
|
+
temporary file and is renamed into place, so a reader never sees half a
|
|
58
|
+
record; the directory and its files are created `0700` and `0600`.
|
|
59
|
+
|
|
60
|
+
The directory is the plugin's own: nothing else should be writing there, and
|
|
61
|
+
the sweeper removes what it finds expired. It is local to one process, so it
|
|
62
|
+
suits a single instance, not a cluster sharing one verdict.
|
|
63
|
+
|
|
64
|
+
## Expiry
|
|
65
|
+
|
|
66
|
+
Two rules the backends do not get to reinterpret:
|
|
67
|
+
|
|
68
|
+
- **Expiry is enforced on read.** A record past its deadline is answered as
|
|
69
|
+
absent whatever the sweeper has done, so a sweeper running late is a
|
|
70
|
+
storage cost, never a wrong answer.
|
|
71
|
+
- **The deadline belongs to the consumer.** How long a record lives is the
|
|
72
|
+
policy of whoever wrote it. The store is told an instant and respects it.
|
|
73
|
+
|
|
74
|
+
Keys are namespaced per consumer, so two of them cannot collide in one branch
|
|
75
|
+
or one directory.
|
|
76
|
+
|
|
77
|
+
## For Plugin Authors
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const storage = this.requirePlugin<Storage>('storage');
|
|
81
|
+
await storage.store.set('myplugin', key, value, Date.now() + 3600_000);
|
|
82
|
+
const kept = await storage.store.get('myplugin', key); // null once expired
|
|
83
|
+
await storage.store.delete('myplugin', key);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`requirePlugin` warns and answers `null` when storage is absent, which is
|
|
87
|
+
right for a feature that can be skipped. A plugin making a **security**
|
|
88
|
+
decision must not read "no storage" as "nothing to check": it refuses to load
|
|
89
|
+
instead, as [Back-Channel Logout](../auth/back-channel-logout.md) does.
|
|
90
|
+
|
|
91
|
+
## See Also
|
|
92
|
+
|
|
93
|
+
- [Back-Channel Logout](../auth/back-channel-logout.md) — the first consumer
|
|
94
|
+
- [OpenID Connect](../auth/oidc.md)
|