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.
Files changed (79) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/bin/index.js +1 -1
  3. package/dist/config/args.js +1 -1
  4. package/dist/lib/auth/base.js +1 -1
  5. package/dist/lib/authz/base.js +1 -1
  6. package/dist/lib/authz/composition.js +1 -0
  7. package/dist/lib/expressFormatedResponses.js +1 -1
  8. package/dist/lib/ldapActions.js +1 -1
  9. package/dist/lib/ldapCodes.js +1 -0
  10. package/dist/lib/parseConfig.js +1 -1
  11. package/dist/lib/storage/store.js +1 -0
  12. package/dist/lib/utils.js +1 -1
  13. package/dist/plugins/auth/authzDynamic.js +1 -1
  14. package/dist/plugins/auth/authzPerBranch.js +1 -1
  15. package/dist/plugins/auth/authzPerRoute.js +1 -1
  16. package/dist/plugins/auth/authzScope.js +1 -1
  17. package/dist/plugins/auth/hmac.js +1 -1
  18. package/dist/plugins/auth/llng.js +1 -1
  19. package/dist/plugins/auth/openidconnect.js +1 -1
  20. package/dist/plugins/auth/token.js +1 -1
  21. package/dist/plugins/auth/totp.js +1 -1
  22. package/dist/plugins/bcl.js +1 -0
  23. package/dist/plugins/ldap/organizations.js +1 -1
  24. package/dist/plugins/priority.json.js +1 -1
  25. package/dist/plugins/scim/baseResolver.js +1 -1
  26. package/dist/plugins/scim/errors.js +1 -1
  27. package/dist/plugins/scim/groups.js +1 -1
  28. package/dist/plugins/scim/list.js +1 -1
  29. package/dist/plugins/scim/users.js +1 -1
  30. package/dist/plugins/storage/file.js +1 -0
  31. package/dist/plugins/storage/ldap.js +1 -0
  32. package/dist/plugins/storage.js +1 -0
  33. package/dist/src/abstract/ldapFlat.d.ts +7 -0
  34. package/dist/src/abstract/plugin.d.ts +20 -0
  35. package/dist/src/bin/index.d.ts +50 -8
  36. package/dist/src/config/args.d.ts +36 -0
  37. package/dist/src/hooks.d.ts +55 -0
  38. package/dist/src/lib/auth/base.d.ts +175 -9
  39. package/dist/src/lib/authz/base.d.ts +133 -4
  40. package/dist/src/lib/authz/composition.d.ts +145 -0
  41. package/dist/src/lib/expressFormatedResponses.d.ts +9 -2
  42. package/dist/src/lib/ldapActions.d.ts +30 -0
  43. package/dist/src/lib/ldapCodes.d.ts +28 -0
  44. package/dist/src/lib/storage/store.d.ts +84 -0
  45. package/dist/src/lib/utils.d.ts +19 -0
  46. package/dist/src/plugins/auth/authzDynamic.d.ts +76 -0
  47. package/dist/src/plugins/auth/authzPerBranch.d.ts +11 -0
  48. package/dist/src/plugins/auth/authzPerRoute.d.ts +26 -0
  49. package/dist/src/plugins/auth/authzScope.d.ts +44 -3
  50. package/dist/src/plugins/auth/hmac.d.ts +2 -0
  51. package/dist/src/plugins/auth/llng.d.ts +39 -0
  52. package/dist/src/plugins/auth/openidconnect.d.ts +66 -3
  53. package/dist/src/plugins/auth/rateLimit.d.ts +3 -1
  54. package/dist/src/plugins/auth/token.d.ts +2 -0
  55. package/dist/src/plugins/auth/totp.d.ts +2 -0
  56. package/dist/src/plugins/bcl.d.ts +65 -0
  57. package/dist/src/plugins/ldap/organizations.d.ts +90 -1
  58. package/dist/src/plugins/scim/baseResolver.d.ts +3 -1
  59. package/dist/src/plugins/scim/errors.d.ts +2 -10
  60. package/dist/src/plugins/storage/file.d.ts +19 -0
  61. package/dist/src/plugins/storage/ldap.d.ts +17 -0
  62. package/dist/src/plugins/storage.d.ts +44 -0
  63. package/docs/plugin-development/README.md +12 -1
  64. package/docs/plugin-development/dependencies.md +40 -15
  65. package/docs/usage/configuration.md +89 -13
  66. package/docs/usage/plugins/README.md +17 -12
  67. package/docs/usage/plugins/auth/README.md +129 -0
  68. package/docs/usage/plugins/auth/authz-dynamic.md +42 -0
  69. package/docs/usage/plugins/auth/authz-linid1.md +32 -0
  70. package/docs/usage/plugins/auth/authz-per-branch.md +8 -0
  71. package/docs/usage/plugins/auth/authz-per-route.md +6 -0
  72. package/docs/usage/plugins/auth/authz-scope.md +69 -16
  73. package/docs/usage/plugins/auth/back-channel-logout.md +86 -0
  74. package/docs/usage/plugins/auth/llng.md +29 -0
  75. package/docs/usage/plugins/auth/oidc.md +52 -0
  76. package/docs/usage/plugins/ldap/organizations.md +37 -0
  77. package/docs/usage/plugins/utilities/storage.md +94 -0
  78. package/docs/usage/upgrading.md +289 -0
  79. 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 has no options. It answers through whichever loaded plugin carries the
19
- `authz` role _and_ can say who may do what where — one implementing
20
- `resolveUser`, `getAuthorizedBranches` and `getUserPermissions`. Both
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
- The role alone is not enough. [authz-per-route](authz-per-route.md) gates URLs
25
- and [authz-dynamic](authz-dynamic.md) reads a token: both carry the `authz`
26
- role and implement none of those three methods, so this endpoint passes them
27
- over — loading one beside a branch-level plugin changes the answer not at all.
28
- A server whose only authorization plugin is one of those has nothing to
29
- describe a scope with, and the answer is the unrestricted one shown below: a
30
- client built on it shows every button, and a refusal, when it comes, comes from
31
- the route or the token rather than from the scope.
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 loaded the server grants everything, and the
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 the directory cannot resolve |
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)