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.
- package/bin/index.mjs +1 -1
- 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/changeContext.js +1 -0
- package/dist/lib/errors.js +1 -1
- 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/authzLinid1.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/departmentSync.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/trash.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/scim.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/plugins/twake/appAccountsApi.js +1 -1
- package/dist/plugins/twake/appAccountsConsistency.js +1 -1
- package/dist/plugins/twake/clouderyProvision.js +1 -1
- package/dist/plugins/twake/cozyProvision.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +7 -0
- package/dist/src/abstract/plugin.d.ts +26 -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 +70 -6
- package/dist/src/lib/auth/base.d.ts +175 -9
- package/dist/src/lib/authz/base.d.ts +159 -4
- package/dist/src/lib/authz/composition.d.ts +145 -0
- package/dist/src/lib/changeContext.d.ts +24 -0
- package/dist/src/lib/errors.d.ts +15 -0
- package/dist/src/lib/expressFormatedResponses.d.ts +9 -2
- package/dist/src/lib/ldapActions.d.ts +32 -1
- 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/authzLinid1.d.ts +6 -0
- package/dist/src/plugins/auth/authzPerBranch.d.ts +46 -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/groups.d.ts +7 -5
- package/dist/src/plugins/ldap/onChange.d.ts +17 -8
- package/dist/src/plugins/ldap/organizations.d.ts +92 -3
- 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 +13 -19
- package/docs/plugin-development/dependencies.md +40 -15
- package/docs/plugin-development/hooks.md +6 -0
- package/docs/plugin-development/testing.md +1 -17
- package/docs/usage/configuration.md +90 -14
- 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 +33 -0
- package/docs/usage/plugins/auth/authz-per-branch.md +41 -11
- package/docs/usage/plugins/auth/authz-per-route.md +6 -0
- package/docs/usage/plugins/auth/authz-scope.md +74 -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/on-change.md +51 -284
- package/docs/usage/plugins/ldap/organizations.md +37 -0
- package/docs/usage/plugins/utilities/storage.md +94 -0
- package/docs/usage/troubleshooting.md +1 -9
- package/docs/usage/upgrading.md +341 -0
- 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
|
-
|
|
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
|
|
package/docs/usage/upgrading.md
CHANGED
|
@@ -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.
|
|
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"
|