ldap-rest 0.8.2 → 0.10.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 (101) hide show
  1. package/bin/index.mjs +1 -1
  2. package/dist/abstract/ldapFlat.js +1 -1
  3. package/dist/bin/index.js +1 -1
  4. package/dist/config/args.js +1 -1
  5. package/dist/lib/auth/base.js +1 -1
  6. package/dist/lib/authz/base.js +1 -1
  7. package/dist/lib/authz/composition.js +1 -0
  8. package/dist/lib/changeContext.js +1 -0
  9. package/dist/lib/errors.js +1 -1
  10. package/dist/lib/expressFormatedResponses.js +1 -1
  11. package/dist/lib/ldapActions.js +1 -1
  12. package/dist/lib/ldapCodes.js +1 -0
  13. package/dist/lib/parseConfig.js +1 -1
  14. package/dist/lib/storage/store.js +1 -0
  15. package/dist/lib/utils.js +1 -1
  16. package/dist/plugins/auth/authzDynamic.js +1 -1
  17. package/dist/plugins/auth/authzLinid1.js +1 -1
  18. package/dist/plugins/auth/authzPerBranch.js +1 -1
  19. package/dist/plugins/auth/authzPerRoute.js +1 -1
  20. package/dist/plugins/auth/authzScope.js +1 -1
  21. package/dist/plugins/auth/hmac.js +1 -1
  22. package/dist/plugins/auth/llng.js +1 -1
  23. package/dist/plugins/auth/openidconnect.js +1 -1
  24. package/dist/plugins/auth/token.js +1 -1
  25. package/dist/plugins/auth/totp.js +1 -1
  26. package/dist/plugins/bcl.js +1 -0
  27. package/dist/plugins/ldap/departmentSync.js +1 -1
  28. package/dist/plugins/ldap/groups.js +1 -1
  29. package/dist/plugins/ldap/onChange.js +1 -1
  30. package/dist/plugins/ldap/organizations.js +1 -1
  31. package/dist/plugins/ldap/trash.js +1 -1
  32. package/dist/plugins/priority.json.js +1 -1
  33. package/dist/plugins/scim/baseResolver.js +1 -1
  34. package/dist/plugins/scim/errors.js +1 -1
  35. package/dist/plugins/scim/groups.js +1 -1
  36. package/dist/plugins/scim/list.js +1 -1
  37. package/dist/plugins/scim/scim.js +1 -1
  38. package/dist/plugins/scim/users.js +1 -1
  39. package/dist/plugins/storage/file.js +1 -0
  40. package/dist/plugins/storage/ldap.js +1 -0
  41. package/dist/plugins/storage.js +1 -0
  42. package/dist/plugins/twake/appAccountsApi.js +1 -1
  43. package/dist/plugins/twake/appAccountsConsistency.js +1 -1
  44. package/dist/plugins/twake/clouderyProvision.js +1 -1
  45. package/dist/plugins/twake/cozyProvision.js +1 -1
  46. package/dist/src/abstract/ldapFlat.d.ts +7 -0
  47. package/dist/src/abstract/plugin.d.ts +26 -0
  48. package/dist/src/bin/index.d.ts +50 -8
  49. package/dist/src/config/args.d.ts +36 -0
  50. package/dist/src/hooks.d.ts +70 -6
  51. package/dist/src/lib/auth/base.d.ts +175 -9
  52. package/dist/src/lib/authz/base.d.ts +159 -4
  53. package/dist/src/lib/authz/composition.d.ts +145 -0
  54. package/dist/src/lib/changeContext.d.ts +24 -0
  55. package/dist/src/lib/errors.d.ts +15 -0
  56. package/dist/src/lib/expressFormatedResponses.d.ts +9 -2
  57. package/dist/src/lib/ldapActions.d.ts +32 -1
  58. package/dist/src/lib/ldapCodes.d.ts +28 -0
  59. package/dist/src/lib/storage/store.d.ts +84 -0
  60. package/dist/src/lib/utils.d.ts +19 -0
  61. package/dist/src/plugins/auth/authzDynamic.d.ts +76 -0
  62. package/dist/src/plugins/auth/authzLinid1.d.ts +6 -0
  63. package/dist/src/plugins/auth/authzPerBranch.d.ts +46 -0
  64. package/dist/src/plugins/auth/authzPerRoute.d.ts +26 -0
  65. package/dist/src/plugins/auth/authzScope.d.ts +44 -3
  66. package/dist/src/plugins/auth/hmac.d.ts +2 -0
  67. package/dist/src/plugins/auth/llng.d.ts +39 -0
  68. package/dist/src/plugins/auth/openidconnect.d.ts +66 -3
  69. package/dist/src/plugins/auth/rateLimit.d.ts +3 -1
  70. package/dist/src/plugins/auth/token.d.ts +2 -0
  71. package/dist/src/plugins/auth/totp.d.ts +2 -0
  72. package/dist/src/plugins/bcl.d.ts +65 -0
  73. package/dist/src/plugins/ldap/groups.d.ts +7 -5
  74. package/dist/src/plugins/ldap/onChange.d.ts +17 -8
  75. package/dist/src/plugins/ldap/organizations.d.ts +92 -3
  76. package/dist/src/plugins/scim/baseResolver.d.ts +3 -1
  77. package/dist/src/plugins/scim/errors.d.ts +2 -10
  78. package/dist/src/plugins/storage/file.d.ts +19 -0
  79. package/dist/src/plugins/storage/ldap.d.ts +17 -0
  80. package/dist/src/plugins/storage.d.ts +44 -0
  81. package/docs/plugin-development/README.md +13 -19
  82. package/docs/plugin-development/dependencies.md +40 -15
  83. package/docs/plugin-development/hooks.md +6 -0
  84. package/docs/plugin-development/testing.md +1 -17
  85. package/docs/usage/configuration.md +90 -14
  86. package/docs/usage/plugins/README.md +17 -12
  87. package/docs/usage/plugins/auth/README.md +129 -0
  88. package/docs/usage/plugins/auth/authz-dynamic.md +42 -0
  89. package/docs/usage/plugins/auth/authz-linid1.md +33 -0
  90. package/docs/usage/plugins/auth/authz-per-branch.md +41 -11
  91. package/docs/usage/plugins/auth/authz-per-route.md +6 -0
  92. package/docs/usage/plugins/auth/authz-scope.md +74 -16
  93. package/docs/usage/plugins/auth/back-channel-logout.md +86 -0
  94. package/docs/usage/plugins/auth/llng.md +29 -0
  95. package/docs/usage/plugins/auth/oidc.md +52 -0
  96. package/docs/usage/plugins/ldap/on-change.md +51 -284
  97. package/docs/usage/plugins/ldap/organizations.md +37 -0
  98. package/docs/usage/plugins/utilities/storage.md +94 -0
  99. package/docs/usage/troubleshooting.md +1 -9
  100. package/docs/usage/upgrading.md +341 -0
  101. package/package.json +17 -1
@@ -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)
@@ -89,15 +89,7 @@ DM_LOG_LEVEL=debug ldap-rest
89
89
 
90
90
  ## Testing
91
91
 
92
- Run the test suite:
93
-
94
- ```bash
95
- # All tests
96
- source ~/.test-env && npm run test:dev
97
-
98
- # Single file
99
- source ~/.test-env && npm run test:one test/path/to/file.test.ts
100
- ```
92
+ See [Tests and checks](../../CONTRIBUTING.md#tests-and-checks).
101
93
 
102
94
  ## Support
103
95
 
@@ -4,6 +4,347 @@ What to check before deploying, newest first. Only releases that need a
4
4
  decision or a configuration change appear here; see the
5
5
  [CHANGELOG](../../CHANGELOG.md) for everything else.
6
6
 
7
+ ## To 0.10.0
8
+
9
+ ### LDAP base is now required
10
+
11
+ **Who is affected:** a deployment that starts `ldap-rest` without
12
+ `--ldap-base` (or `DM_LDAP_BASE`).
13
+
14
+ The server refuses to start without it. It used to take the second RDN of
15
+ `--ldap-dn` — `dc=example` for `cn=admin,dc=example,dc=com` — so subtree
16
+ searches failed or found nothing. Set `--ldap-base` to the DN under which
17
+ the entries are searched.
18
+
19
+ ### Group rules now apply
20
+
21
+ **Who is affected:** a deployment whose `--authz-per-branch-config` declares
22
+ a `groups` section.
23
+
24
+ That section granted nothing: permissions came from `users` and `default`
25
+ alone. A member of a configured group now also gets what its rule grants.
26
+ **Check the `groups` rules before upgrading.**
27
+
28
+ ### An identity naming several entries is refused
29
+
30
+ **Who is affected:** a directory where the same `uid` appears twice — one
31
+ without a `unique` overlay, or one where a homonym was left in the trash
32
+ while the uid was reused.
33
+
34
+ `authzLinid1` refuses every request of such a caller with a `403`.
35
+ `authzPerBranch` keeps the uid's `users` rules and withholds the `groups`
36
+ rules. Both log a warning naming the identity and the number of entries it
37
+ matched.
38
+
39
+ **Check your directory before upgrading:**
40
+
41
+ ```bash
42
+ ldapsearch -x -b "<base>" "(uid=<uid>)" dn
43
+ ```
44
+
45
+ must answer one entry. Where it answers two, remove the duplicate — the
46
+ trash included — or restore the uniqueness the `unique` overlay enforces.
47
+
48
+ ### `onLdapChange` gives full values
49
+
50
+ **Who is affected:** plugins listening to `onLdapChange` that read the values
51
+ of an `add`, or of a `delete` naming values, as the values added or removed.
52
+
53
+ Each attribute now maps to all its values before and after the write. Adding
54
+ `c` to `member: [a, b]` gave `[null, [c]]` and gives `[[a, b], [a, b, c]]`.
55
+ Compute what was added or removed as the difference of the two sides, as the
56
+ plugins shipped with LDAP-Rest do. A `replace` with the current value no
57
+ longer fires the hook.
58
+
59
+ ## To 0.9.0
60
+
61
+ ### Two authorization plugins judging the same requests no longer start
62
+
63
+ **Who is affected:** anyone loading two of `core/auth/authzPerBranch`,
64
+ `core/auth/authzLinid1` and `core/auth/authzDynamic` together — including
65
+ `authzPerBranch` beside `authzDynamic`, where the branch plugin judged the
66
+ tokens too.
67
+
68
+ Every plugin judging LDAP operations registers the same hooks, and all of
69
+ them run: the first refusal wins. Loaded together, they composed as an AND
70
+ nobody had decided, and the 403 did not say whose refusal it was. The server
71
+ now refuses to start, naming both plugins. Two ways out:
72
+
73
+ - **Scope each plugin to its own callers** with `authz_for`, the instance
74
+ names of the authentication plugins whose requests it judges — so that no
75
+ request is judged by both:
76
+
77
+ ```bash
78
+ --plugin core/auth/authzDynamic
79
+ --plugin 'core/auth/openidconnect:oidc:{"auth_path_prefix":"/api/admin"}'
80
+ --plugin 'core/auth/authzPerBranch:authzPerBranch:{"authz_for":["oidc"]}'
81
+ ```
82
+
83
+ - **Keep the AND**, if it was meant, with `--authz-combine`. Behaviour is
84
+ then what it was, and the startup log says the two compose.
85
+
86
+ `authzPerRoute` is not concerned: it registers no LDAP hook, and route plus
87
+ branch stays a combination that starts as before.
88
+
89
+ **Hosts assembling the server themselves** — `registerPlugin` after `ready`
90
+ rather than `--plugin` — get the same refusal, out of the `registerPlugin`
91
+ call that brings the second plugin. A plugin arriving is judged against what
92
+ is already there: register the authentication plugins an `authz_for` names,
93
+ and the plugins `--authz-scope-source` names, before the plugin naming them.
94
+
95
+ A lone `authzPerBranch` is not refused, but mind its default: unset,
96
+ `--authz-per-branch-config` grants read everywhere and write nowhere to
97
+ **every** authenticated identity, whatever authenticated it. The startup
98
+ line `authzPerBranch judges … for every authenticated request (…)` names the
99
+ authenticators it judges; one it was not written for needs an `authz_for`.
100
+
101
+ `GET /v1/authz/scope` changes in two ways. It answers with a plugin that
102
+ judges the caller, lists every plugin judging them under `sources`, and says
103
+ which one answered under `source` — `--authz-scope-source` chooses when
104
+ several can. And when the only plugins judging the caller cannot describe a
105
+ scope — `authzPerRoute`, `authzDynamic` — it answers `described: false` with
106
+ no entity instead of `unrestricted: true` with `create: true` everywhere. A
107
+ client that enumerates the entities it may create offers nothing there
108
+ rather than everything. An anonymous caller is answered `401` as soon as any
109
+ authorization plugin is loaded, where it used to be told `unrestricted`.
110
+
111
+ ### A second word after an option taking a list is refused
112
+
113
+ **Who is affected:** anyone whose command line puts two values after one
114
+ `--plugin`, `--authz-for`, `--mail-domain` or other list option.
115
+
116
+ `--authz-for oidc authToken` read as `["oidc"]`: the second word was dropped
117
+ in silence, and for `authz_for` that is a population smaller than the
118
+ command line says — requests of `authToken` that nothing judges. The server
119
+ now refuses to start on it, naming the option. Repeat the option for each
120
+ value (`--authz-for oidc --authz-for authToken`), or use the plural form
121
+ where there is one (`--plugins "a b"`), or the environment variable, which
122
+ splits on spaces and commas.
123
+
124
+ ### `getLogger()` may return `undefined`, and its type says so
125
+
126
+ **Who is affected:** anyone building a plugin or a host in TypeScript
127
+ against the package's `expressformatedresponses` export and calling
128
+ `getLogger()`.
129
+
130
+ The logger is installed by the `DM` constructor, and before that there is
131
+ none. The type used to say `winston.Logger`, so a reader without a guard
132
+ compiled and threw `Cannot read properties of undefined` at runtime — from
133
+ inside an error path, which is how #182 and #199 happened. It now says
134
+ `winston.Logger | undefined`, and such a reader stops compiling:
135
+
136
+ ```ts
137
+ getLogger().info('…'); // no longer compiles
138
+ getLogger()?.info('…'); // logs when there is a logger
139
+ ```
140
+
141
+ A plugin holds `this.logger`, which is always set; `getLogger()` is for a
142
+ helper with no server in hand. `setLogger` accepts `undefined` as well, so a
143
+ test restoring the state it found no longer needs a cast.
144
+
145
+ ### OpenID Connect honours `auth_path_prefix`
146
+
147
+ **Who is affected:** anyone running `core/auth/openidconnect`, and in
148
+ particular anyone who gave it an `auth_path_prefix`.
149
+
150
+ The plugin did not go through the authentication dispatcher: it mounted its
151
+ own middleware in `api()`, which kept its registration order, and it never
152
+ read `auth_path_prefix`. Three consequences, all of them now gone:
153
+
154
+ - **Scoping did nothing.** An instance scoped to `/api/admin` guarded the
155
+ whole server. It now guards `/api/admin` — and **the paths it used to
156
+ guard by accident are no longer guarded by it**. Check that another
157
+ authentication plugin covers them, or drop the prefix to keep guarding
158
+ everything. The startup line naming unguarded routes works again and is
159
+ the fastest way to tell.
160
+ - **Authorization could run before the identity.** With
161
+ `core/auth/authzPerRoute` loaded, a named OIDC instance landed in the
162
+ parallel batch and mounted after it, so every rule was judged with no
163
+ `req.user` — and an unidentified request is passed, not refused. Rules
164
+ that read as enforced were inert.
165
+ - **Two authentications composed as an AND.** A valid `Authorization:
166
+ Bearer` answered by a catch-all token plugin was then redirected to the
167
+ provider anyway. Only the plugin whose claim is most specific runs now.
168
+
169
+ `/login`, `/logout`, `/callback` and `/backchannel-logout` are claimed by
170
+ the plugin whatever its scope, so a catch-all authentication no longer
171
+ answers the provider's redirect with a 401.
172
+
173
+ Unauthenticated requests are unchanged: a browser is redirected, an API
174
+ client receives 401.
175
+
176
+ ### `core/auth/llng` refuses a request the handler named nobody for
177
+
178
+ **Who is affected:** anyone whose LemonLDAP::NG configuration has a `skip`
179
+ rule on a path this plugin guards.
180
+
181
+ `skip` makes the handler return immediately — no session read, no
182
+ `Lm-Remote-User` header — and the plugin published `req.user = undefined`.
183
+ Every authorization plugin skips its check when a request carries no
184
+ identity, so the rule meant "authenticated, and scoped by nobody" rather
185
+ than "no authentication on this path". Such a request now answers 401.
186
+
187
+ Serve those paths from a virtual host this plugin does not guard, or scope
188
+ the plugin with `auth_path_prefix` so they fall outside it.
189
+
190
+ ### `authzDynamic` no longer steps aside for another authenticator
191
+
192
+ **Who is affected:** anyone loading `core/auth/authzDynamic` beside another
193
+ authentication plugin on the same paths — `core/auth/token`,
194
+ `core/auth/openidconnect`, `core/auth/llng`.
195
+
196
+ The plugin returned early as soon as a request already carried a `req.user`,
197
+ so the token ACLs were applied to nothing. With `core/auth/token` registered
198
+ first, its static tokens reached the whole directory as unscoped
199
+ administrators; registered the other way round, the same configuration
200
+ demanded both credentials. Which one a deployment got was plugin import
201
+ order.
202
+
203
+ The ACLs now apply to the token a request carries, whoever else identified
204
+ it, and a request carrying none is refused. **A deployment relying on the
205
+ old behaviour will start answering 401** — its callers authenticate with the
206
+ other plugin and have no dynamic token.
207
+
208
+ `--authz-dynamic-bypass` names what may pass without a token:
209
+
210
+ ```
211
+ --authz-dynamic-bypass any-authenticated # what the plugin did before 0.9.0
212
+ --authz-dynamic-bypass ops-admin # one identity, as another plugin publishes it
213
+ --authz-dynamic-bypass trusted-proxy # what core/auth/trustedProxy vouched for
214
+ ```
215
+
216
+ `any-authenticated` restores the previous behaviour in one line. The
217
+ difference is that it is now written down, logged per request with the
218
+ reason, and announced at startup when this plugin shares its paths with
219
+ another authenticator.
220
+
221
+ `trusted-proxy` is the composition the old code's comment described and
222
+ never performed: `core/auth/trustedProxy` sets `req.trustedProxy` and
223
+ `req.proxyAuthUser`, never `req.user`, so that pair never took the early
224
+ return — while every authenticator that does set it, did.
225
+
226
+ ### An identity that does not resolve is refused
227
+
228
+ **Who is affected:** anyone running an authorization plugin that resolves
229
+ identities against the directory — `core/auth/authzLinid1` — behind an
230
+ authenticator that publishes something other than the directory's
231
+ `ldap_user_main_attribute`.
232
+
233
+ Every authorization hook skipped its check when `resolveUser` answered
234
+ null: a `warn`, then the read, the write or the delete went ahead. With
235
+ `authzLinid1`, whose `resolveUser` searches
236
+ `(<ldap_user_main_attribute>=<identity>)`, an authenticator publishing
237
+ anything else resolved to nothing on every request — `core/auth/openidconnect`
238
+ publishes the OIDC `sub`, `core/auth/llng` whatever `whatToTrace` traces — so
239
+ anyone the identity provider admitted read, wrote and deleted across the
240
+ whole tree, behind a line a `notice` production log does not show.
241
+
242
+ An authenticated identity that does not resolve is now refused, with the same
243
+ 403 as any other refusal. **Check your logs before upgrading**: a deployment
244
+ in this state looks healthy, and the requests that were passing will start
245
+ answering 403.
246
+
247
+ ```
248
+ grep 'could not be resolved' <your log>
249
+ ```
250
+
251
+ If that matches, the caller's identity and the plugin's model disagree, and
252
+ the upgrade turns an open door into a refusal — which may be a service
253
+ outage where the door was being used. `--authz-unresolved-user allow`
254
+ restores the old behaviour while you fix the pairing; it keeps the warning,
255
+ and it keeps the door open.
256
+
257
+ An **anonymous** request is unchanged: no identity at all is still skipped,
258
+ and the route's own authentication is what answers it.
259
+
260
+ Both answers are cached now, negatives included, for the plugin's own cache
261
+ TTL. An identity added to the directory after being refused waits out that
262
+ TTL.
263
+
264
+ ### `/subnodes/search` caps what it returns
265
+
266
+ **Who is affected:** a client reading the whole answer of
267
+ `/api/v1/ldap/organizations/<dn>/subnodes/search` and expecting every match.
268
+
269
+ It returned every attached entry it found, which is the one shape a directory
270
+ with a size limit cannot answer: past that limit the server refuses the
271
+ search rather than shortening it. It now caps them at
272
+ `--ldap-organization-max-subnodes` (default 50) and ends the list with the
273
+ `moreIndicator` row `/subnodes` already used, so the two endpoints answer the
274
+ same shape.
275
+
276
+ A client that treats every row as an entry will show that row as one. Drop
277
+ whatever carries `_isMoreIndicator` — see
278
+ [the endpoint's notes](plugins/ldap/organizations.md#get-organization-subnodes).
279
+
280
+ The same row can now also appear among the **child organizations**, which
281
+ were never capped and still are not: it says the directory refused to list
282
+ them all, and it carries no `_totalCount`, nothing having counted them.
283
+
284
+ ### `/subnodes/search` is authorized, and lists the children by page
285
+
286
+ **Who is affected:** anyone whose callers reach that route with a token that
287
+ does not hold the whole directory, and anyone running against Active
288
+ Directory.
289
+
290
+ Neither of its searches carried the request, and an authorization plugin
291
+ skips its check when there is none — the same gap the flat routes had until
292
+ 0.8.2. A caller now sees what its branch grants it, where it used to see
293
+ everything the directory held; a caller outside its branch is refused. This
294
+ is a fix, but it changes what an existing client receives.
295
+
296
+ The child organizations of both routes are also searched by page now. On
297
+ OpenLDAP that changes nothing — the server's size limit bounds a paged
298
+ search as it does an unpaged one. On **Active Directory** it does: an
299
+ unpaged search answered at most `MaxPageSize` entries (1000 by default) and
300
+ said nothing about the rest, so a node with more children than that now
301
+ returns all of them, and a client holding the answer in memory receives a
302
+ larger one than before.
303
+
304
+ ### Back-Channel Logout keeps its marks in `core/storage`
305
+
306
+ `core/bcl/ldap` and `core/bcl/file` are gone, and with them
307
+ `--bcl-ldap-base`, `--bcl-ldap-object-class`, `--bcl-file-directory` and
308
+ `--bcl-sweep-interval`. One plugin, `core/bcl`, keeps what a logout killed in
309
+ `core/storage`, so a deployment chooses where records live once and every
310
+ consumer of that store follows. Sweeping is now `--storage-sweep-interval`;
311
+ `--bcl-retention` stays, because how long a mark is kept is Back-Channel
312
+ Logout's policy rather than the store's. See
313
+ [Storage](plugins/utilities/storage.md) and
314
+ [Back-Channel Logout](plugins/auth/back-channel-logout.md).
315
+
316
+ **Marks written before this are not read.** The key is namespaced now, and
317
+ the LDAP branch is the store's rather than the plugin's. Back-Channel Logout
318
+ shipped in no release — 0.8.2 predates it — so this can only affect an
319
+ instance running a build from `master`. If yours is one, everything logged out
320
+ within the retention window stops counting once you upgrade, and the old
321
+ `ou=BclTombstones` branch, or the old `--bcl-file-directory`, is left behind
322
+ for you to remove.
323
+
324
+ ### Judging an account by what it is attached to
325
+
326
+ `--authz-filter-attached-entries` changes what a branch grant means, so it is
327
+ off by default and nothing moves until you set it.
328
+
329
+ Every account of a directory lives in the same `ou=users`, so a branch check
330
+ on its parent says the same thing about all of them: either every
331
+ administrator reaches every account, or none does. With the option on, an
332
+ account is judged by the organization its `twakeDepartmentLink` names —
333
+ listings drop the accounts attached elsewhere, and a write is refused on an
334
+ account outside the branch you hold. Entries attached to nothing, which is
335
+ what an organization or a nomenclature value is, stay readable by every
336
+ administrator.
337
+
338
+ **Turn it on only where a branch is a department.** Where a branch is a
339
+ tenant, the option also stops refusing a read on the branch it targets — the
340
+ organization tree and the reference data become readable across branches, and
341
+ what keeps one customer out of another's data is then the per-entry filter
342
+ alone.
343
+
344
+ It is honoured by `core/auth/authzPerBranch` and `core/auth/authzLinid1`.
345
+ `core/auth/authzDynamic` registers its own hooks and never sees the filter:
346
+ setting the option there does nothing at all.
347
+
7
348
  ## To 0.8.0
8
349
 
9
350
  ### Node 20 is the floor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"
@@ -92,6 +92,10 @@
92
92
  "types": "./dist/src/plugins/auth/trustedProxy.d.ts",
93
93
  "import": "./dist/plugins/auth/trustedProxy.js"
94
94
  },
95
+ "./plugin-bcl": {
96
+ "types": "./dist/src/plugins/bcl.d.ts",
97
+ "import": "./dist/plugins/bcl.js"
98
+ },
95
99
  "./plugin-configapi": {
96
100
  "types": "./dist/src/plugins/configApi.d.ts",
97
101
  "import": "./dist/plugins/configApi.js"
@@ -212,6 +216,18 @@
212
216
  "types": "./dist/src/plugins/static.d.ts",
213
217
  "import": "./dist/plugins/static.js"
214
218
  },
219
+ "./plugin-storage": {
220
+ "types": "./dist/src/plugins/storage.d.ts",
221
+ "import": "./dist/plugins/storage.js"
222
+ },
223
+ "./plugin-storage-file": {
224
+ "types": "./dist/src/plugins/storage/file.d.ts",
225
+ "import": "./dist/plugins/storage/file.js"
226
+ },
227
+ "./plugin-storage-ldap": {
228
+ "types": "./dist/src/plugins/storage/ldap.d.ts",
229
+ "import": "./dist/plugins/storage/ldap.js"
230
+ },
215
231
  "./plugin-twake-appaccountsapi": {
216
232
  "types": "./dist/src/plugins/twake/appAccountsApi.d.ts",
217
233
  "import": "./dist/plugins/twake/appAccountsApi.js"