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