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
|
@@ -25,8 +25,11 @@ All LDAP-Rest configuration options.
|
|
|
25
25
|
- [core/auth/hmac](#coreauthhmac)
|
|
26
26
|
- [core/auth/llng](#coreauthllng)
|
|
27
27
|
- [core/auth/openidconnect](#coreauthopenidconnect)
|
|
28
|
+
- [core/bcl](#corebcl)
|
|
28
29
|
- [Authorization Plugins](#authorization-plugins)
|
|
30
|
+
- [Common to every authorization plugin](#common-to-every-authorization-plugin)
|
|
29
31
|
- [core/auth/authzPerBranch](#coreauthauthzperbranch)
|
|
32
|
+
- [core/auth/authzScope](#coreauthauthzscope)
|
|
30
33
|
- [core/auth/authzPerRoute](#coreauthauthzperroute)
|
|
31
34
|
- [core/auth/authzLinid1](#coreauthauthzlinid1)
|
|
32
35
|
- [Security Plugins](#security-plugins)
|
|
@@ -41,6 +44,7 @@ All LDAP-Rest configuration options.
|
|
|
41
44
|
- [Utility Plugins](#utility-plugins)
|
|
42
45
|
- [core/static](#corestatic)
|
|
43
46
|
- [core/weblogs](#coreweblogs)
|
|
47
|
+
- [core/storage](#corestorage)
|
|
44
48
|
- [core/configApi](#coreconfigapi)
|
|
45
49
|
- [Configuration File](#configuration-file)
|
|
46
50
|
- [LDAP Failover](#ldap-failover)
|
|
@@ -97,7 +101,7 @@ export DM_PLUGINS="core/auth/token,core/ldap/flatGeneric,core/ldap/groups"
|
|
|
97
101
|
| `--ldap-url` | `--ldap-urls` | `DM_LDAP_URL` | `ldap://localhost` | LDAP server URL(s) |
|
|
98
102
|
| `--ldap-dn` | | `DM_LDAP_DN` | `cn=admin,dc=example,dc=com` | Bind DN |
|
|
99
103
|
| `--ldap-pwd` | | `DM_LDAP_PWD` | `admin` | Password |
|
|
100
|
-
| `--ldap-base` | | `DM_LDAP_BASE` | | Base DN for searches
|
|
104
|
+
| `--ldap-base` | | `DM_LDAP_BASE` | | Base DN for searches (required) |
|
|
101
105
|
| `--ldap-user-main-attribute` | | `DM_LDAP_USER_ATTRIBUTE` | `uid` | User identifier attribute |
|
|
102
106
|
| `--ldap-cache-max` | | `DM_LDAP_CACHE_MAX` | `1000` | Max cache entries |
|
|
103
107
|
| `--ldap-cache-ttl` | | `DM_LDAP_CACHE_TTL` | `0` | Search cache TTL (seconds), `0` disables it |
|
|
@@ -251,27 +255,86 @@ Empty means the plugin guards every path. Scoping several instances to different
|
|
|
251
255
|
|
|
252
256
|
#### `core/auth/llng`
|
|
253
257
|
|
|
254
|
-
| CLI
|
|
255
|
-
|
|
|
256
|
-
| `--llng-ini`
|
|
258
|
+
| CLI | Env | Default | Description |
|
|
259
|
+
| ------------------------ | ------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------- |
|
|
260
|
+
| `--llng-ini` | `DM_LLNG_INI` | `/etc/lemonldap-ng/lemonldap-ng.ini` | LemonLDAP::NG config path |
|
|
261
|
+
| `--llng-username-header` | `DM_LLNG_USERNAME_HEADER` | | Header LLNG exports with the login, published as `req.userName` (see [llng](plugins/auth/llng.md)) |
|
|
257
262
|
|
|
258
263
|
#### `core/auth/openidconnect`
|
|
259
264
|
|
|
260
|
-
| CLI
|
|
261
|
-
|
|
|
262
|
-
| `--oidc-server`
|
|
263
|
-
| `--oidc-client-id`
|
|
264
|
-
| `--oidc-client-secret`
|
|
265
|
-
| `--
|
|
265
|
+
| CLI | Env | Default | Description |
|
|
266
|
+
| ----------------------- | ------------------------ | ------- | --------------------------------------------------------------------------------------- |
|
|
267
|
+
| `--oidc-server` | `DM_OIDC_SERVER` | | OIDC server URL |
|
|
268
|
+
| `--oidc-client-id` | `DM_OIDC_CLIENT_ID` | | OIDC Client ID |
|
|
269
|
+
| `--oidc-client-secret` | `DM_OIDC_CLIENT_SECRET` | | OIDC Client Secret |
|
|
270
|
+
| `--oidc-username-claim` | `DM_OIDC_USERNAME_CLAIM` | `sub` | Claim naming the caller, published as `req.userName` (see [OIDC](plugins/auth/oidc.md)) |
|
|
271
|
+
| `--base-url` | `DM_BASE_URL` | | Public URL for callbacks |
|
|
272
|
+
|
|
273
|
+
#### `core/bcl`
|
|
274
|
+
|
|
275
|
+
Back-Channel Logout: a logout performed at the provider ends the session here
|
|
276
|
+
on the next request. Needs `core/storage`, and refuses to start without it —
|
|
277
|
+
see [Back-Channel Logout](plugins/auth/back-channel-logout.md).
|
|
278
|
+
|
|
279
|
+
| CLI | Env | Default | Description |
|
|
280
|
+
| ----------------- | ------------------ | -------- | --------------------------- |
|
|
281
|
+
| `--bcl-retention` | `DM_BCL_RETENTION` | `604800` | Seconds a tombstone is kept |
|
|
266
282
|
|
|
267
283
|
### Authorization Plugins
|
|
268
284
|
|
|
285
|
+
#### Common to every authorization plugin
|
|
286
|
+
|
|
287
|
+
| CLI | Env | Default | Description |
|
|
288
|
+
| ------------------------- | -------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
289
|
+
| `--authz-unresolved-user` | `DM_AUTHZ_UNRESOLVED_USER` | `deny` | What an authenticated identity the plugin cannot resolve means: `deny` refuses the operation, `allow` lets it through with a warning |
|
|
290
|
+
| `--authz-identity` | `DM_AUTHZ_IDENTITY` | `req.user` | Which value rules are keyed on: `req.user`, this server's identifier for the caller, or `req.userName`, the caller under a name a person would use. See [what a rule is keyed on](plugins/auth/README.md#what-a-rule-is-keyed-on) |
|
|
291
|
+
| `--authz-for` | `DM_AUTHZ_FOR` | | The authentication plugins, by instance name, whose requests the plugin judges; unset, every authenticated request. Meant for a plugin's own overrides: `{"authz_for":["oidc"]}` |
|
|
292
|
+
| `--authz-combine` | `DM_AUTHZ_COMBINE` | `false` | Let two plugins judging the LDAP operations of the same requests start together, as an AND |
|
|
293
|
+
|
|
294
|
+
`--authz-unresolved-user` is read by the plugins that resolve an identity
|
|
295
|
+
before judging it — `core/auth/authzPerBranch` and `core/auth/authzLinid1`.
|
|
296
|
+
`core/auth/authzPerRoute` matches the identity as it stands and
|
|
297
|
+
`core/auth/authzDynamic` judges a token, so neither resolves anything and
|
|
298
|
+
neither reads it. A value other than `deny` or `allow` is refused at startup.
|
|
299
|
+
|
|
300
|
+
An **anonymous** request is not this case: it carries no identity, and every
|
|
301
|
+
authorization plugin skips it as before. This is about a caller the
|
|
302
|
+
authenticator admitted whose identity the authorization model cannot place —
|
|
303
|
+
`authzLinid1` looking up a `uid` that does not exist, typically because the
|
|
304
|
+
authenticator publishes something else. `allow` is the behaviour every plugin
|
|
305
|
+
had before 0.8.3, and it is an open door where `authzLinid1` is loaded: see
|
|
306
|
+
[the note](upgrading.md#an-identity-that-does-not-resolve-is-refused).
|
|
307
|
+
|
|
308
|
+
`--authz-for` and `--authz-combine` say how plugins loaded together compose:
|
|
309
|
+
without them, two plugins judging the LDAP operations of the same requests
|
|
310
|
+
are refused at startup — see [several authorization
|
|
311
|
+
plugins](plugins/auth/README.md#several-authorization-plugins).
|
|
312
|
+
|
|
313
|
+
`--authz-for` takes one name per occurrence: repeat it
|
|
314
|
+
(`--authz-for oidc --authz-for authToken`), or give the environment variable
|
|
315
|
+
a list (`DM_AUTHZ_FOR="oidc authToken"`). A second word after the value —
|
|
316
|
+
`--authz-for oidc authToken` — is refused rather than dropped, as it is after
|
|
317
|
+
every option taking a list.
|
|
318
|
+
|
|
319
|
+
`--authz-identity` is a different question, and the paragraphs above are not
|
|
320
|
+
about it: `core/auth/authzPerRoute` and the SCIM base map read it as well,
|
|
321
|
+
since a route rule and the base a SCIM operation is served from are keyed on
|
|
322
|
+
whichever of the two names it selects. A value other than `req.user` or
|
|
323
|
+
`req.userName` is refused at startup.
|
|
324
|
+
|
|
269
325
|
#### `core/auth/authzPerBranch`
|
|
270
326
|
|
|
271
|
-
| CLI
|
|
272
|
-
|
|
|
273
|
-
| `--authz-per-branch-config`
|
|
274
|
-
| `--authz-per-branch-cache-ttl`
|
|
327
|
+
| CLI | Env | Default | Description |
|
|
328
|
+
| --------------------------------- | ---------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
329
|
+
| `--authz-per-branch-config` | `DM_AUTHZ_PER_BRANCH_CONFIG` | `{default:{read:true,write:false,delete:false}}` | Authorization config (JSON) |
|
|
330
|
+
| `--authz-per-branch-cache-ttl` | `DM_AUTHZ_PER_BRANCH_CACHE_TTL` | `60` | Cache TTL (seconds) |
|
|
331
|
+
| `--authz-filter-attached-entries` | `DM_AUTHZ_FILTER_ATTACHED_ENTRIES` | `false` | Judge an attached entry by the organization it hangs off, and filter listings accordingly. Honoured by `authzPerBranch` and `authzLinid1`; does nothing under `authzDynamic`. |
|
|
332
|
+
|
|
333
|
+
#### `core/auth/authzScope`
|
|
334
|
+
|
|
335
|
+
| CLI | Env | Default | Description |
|
|
336
|
+
| ---------------------- | ----------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
337
|
+
| `--authz-scope-source` | `DM_AUTHZ_SCOPE_SOURCE` | | The plugin, by instance name, that describes a caller's scope when several judge them; unset, the first loaded. See [authz-scope](plugins/auth/authz-scope.md) |
|
|
275
338
|
|
|
276
339
|
#### `core/auth/authzPerRoute`
|
|
277
340
|
|
|
@@ -384,6 +447,19 @@ Automatically creates/updates/deletes applicative account entries when users are
|
|
|
384
447
|
|
|
385
448
|
Web access logging plugin. No configuration options - just add the plugin to enable access logs.
|
|
386
449
|
|
|
450
|
+
#### `core/storage`
|
|
451
|
+
|
|
452
|
+
Keyed storage with a deadline, for the plugins that need to keep something —
|
|
453
|
+
see [Storage](plugins/utilities/storage.md).
|
|
454
|
+
|
|
455
|
+
| CLI | Env | Default | Description |
|
|
456
|
+
| ----------------------------- | ------------------------------ | -------------------- | -------------------------------------- |
|
|
457
|
+
| `--storage-backend` | `DM_STORAGE_BACKEND` | | `ldap` or `file`; required |
|
|
458
|
+
| `--storage-sweep-interval` | `DM_STORAGE_SWEEP_INTERVAL` | `600` | Seconds between two expiry passes |
|
|
459
|
+
| `--storage-ldap-base` | `DM_STORAGE_LDAP_BASE` | | Branch the `ldap` backend writes to |
|
|
460
|
+
| `--storage-ldap-object-class` | `DM_STORAGE_LDAP_OBJECT_CLASS` | `applicationProcess` | Object class of the entries it writes |
|
|
461
|
+
| `--storage-file-directory` | `DM_STORAGE_FILE_DIRECTORY` | | Directory the `file` backend writes to |
|
|
462
|
+
|
|
387
463
|
#### `core/configApi`
|
|
388
464
|
|
|
389
465
|
Exposes API configuration at `/api/v1/config` for client applications. Returns available features, schemas, and endpoints. No configuration options.
|
|
@@ -21,13 +21,14 @@ LDAP entity management:
|
|
|
21
21
|
|
|
22
22
|
Secure API access:
|
|
23
23
|
|
|
24
|
-
| Plugin
|
|
25
|
-
|
|
|
26
|
-
| [token](auth/token.md)
|
|
27
|
-
| [totp](auth/totp.md)
|
|
28
|
-
| [hmac](auth/hmac.md)
|
|
29
|
-
| [llng](auth/llng.md)
|
|
30
|
-
| [oidc](auth/oidc.md)
|
|
24
|
+
| Plugin | Description |
|
|
25
|
+
| -------------------------------------------------- | ---------------------------------------------- |
|
|
26
|
+
| [token](auth/token.md) | Bearer token authentication |
|
|
27
|
+
| [totp](auth/totp.md) | TOTP authentication (time-based codes) |
|
|
28
|
+
| [hmac](auth/hmac.md) | HMAC-SHA256 signing for backend services |
|
|
29
|
+
| [llng](auth/llng.md) | LemonLDAP::NG SSO integration |
|
|
30
|
+
| [oidc](auth/oidc.md) | OpenID Connect / OAuth 2.0 |
|
|
31
|
+
| [back-channel-logout](auth/back-channel-logout.md) | Honour a logout performed at the OIDC provider |
|
|
31
32
|
|
|
32
33
|
## Authorization Plugins
|
|
33
34
|
|
|
@@ -61,10 +62,11 @@ Connect to external systems:
|
|
|
61
62
|
|
|
62
63
|
## Utility Plugins
|
|
63
64
|
|
|
64
|
-
| Plugin | Description
|
|
65
|
-
| ------------------------------- |
|
|
66
|
-
| [static](utilities/static.md) | Static file server
|
|
67
|
-
| [weblogs](utilities/weblogs.md) | HTTP logging
|
|
65
|
+
| Plugin | Description |
|
|
66
|
+
| ------------------------------- | ----------------------------- |
|
|
67
|
+
| [static](utilities/static.md) | Static file server |
|
|
68
|
+
| [weblogs](utilities/weblogs.md) | HTTP logging |
|
|
69
|
+
| [storage](utilities/storage.md) | Keyed storage with a deadline |
|
|
68
70
|
|
|
69
71
|
## Plugin Dependencies
|
|
70
72
|
|
|
@@ -79,10 +81,13 @@ core/ldap/externalUsersInGroups
|
|
|
79
81
|
|
|
80
82
|
core/auth/authzPerBranch
|
|
81
83
|
└─ requires: An authentication plugin
|
|
84
|
+
|
|
85
|
+
core/bcl
|
|
86
|
+
└─ requires: core/storage (refuses to start without it)
|
|
82
87
|
```
|
|
83
88
|
|
|
84
89
|
See [the complete dependencies matrix](../../plugin-development/dependencies.md).
|
|
85
90
|
|
|
86
91
|
## Load Order
|
|
87
92
|
|
|
88
|
-
Authentication plugins are loaded first to
|
|
93
|
+
Authentication plugins are loaded first, so their middleware sits ahead of the routes it guards. The list also carries the plugins other plugins depend on, which have to be registered before the batch their consumers land in — `core/storage` is one. The order is defined in `src/plugins/priority.json`.
|
|
@@ -12,6 +12,135 @@ LDAP-Rest provides multiple authentication plugins to secure API access. These p
|
|
|
12
12
|
| [LemonLDAP::NG](llng.md) | `core/auth/llng` | Integration with LemonLDAP::NG SSO |
|
|
13
13
|
| [OpenID Connect](oidc.md) | `core/auth/openidconnect` | OAuth 2.0 / OpenID Connect authentication |
|
|
14
14
|
|
|
15
|
+
## What a rule is keyed on
|
|
16
|
+
|
|
17
|
+
Authorization rules name a caller, and each authenticator has its own idea
|
|
18
|
+
of what a caller is called. Every one of them publishes the caller twice:
|
|
19
|
+
|
|
20
|
+
| Authenticator | `req.user` | `req.userName` |
|
|
21
|
+
| ------------------------- | ----------------------------------- | ---------------------------------------- |
|
|
22
|
+
| `core/auth/token` | the name given in `--auth-token` | the same |
|
|
23
|
+
| `core/auth/totp` | the name given in `--auth-totp` | the same |
|
|
24
|
+
| `core/auth/hmac` | the service name from `--auth-hmac` | the same |
|
|
25
|
+
| `core/auth/llng` | LLNG's `whatToTrace` | `--llng-username-header`, or the same |
|
|
26
|
+
| `core/auth/openidconnect` | the OIDC `sub` | `--oidc-username-claim` (default: `sub`) |
|
|
27
|
+
| `core/auth/authzDynamic` | the token's tenant | the same |
|
|
28
|
+
|
|
29
|
+
`req.user` is this server's identifier for the caller; `req.userName` is the
|
|
30
|
+
caller under a name a person would use. `--authz-identity` says which one is
|
|
31
|
+
keyed on by `core/auth/authzPerBranch`, `core/auth/authzLinid1`,
|
|
32
|
+
`core/auth/authzPerRoute` and the SCIM base map — every plugin that names a
|
|
33
|
+
caller in its configuration:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
--authz-identity req.user # the default: every existing rule is unchanged
|
|
37
|
+
--authz-identity req.userName # rules written on logins
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The second is what makes a rule portable. Under OpenID Connect the `sub` is
|
|
41
|
+
opaque — `auth0|…`, a UUID — so rules keyed on it have to be looked up per
|
|
42
|
+
administrator and rewritten when the provider changes; keyed on
|
|
43
|
+
`preferred_username` they read as the logins they are.
|
|
44
|
+
|
|
45
|
+
A value other than these two is refused at startup. When the option asks for
|
|
46
|
+
`req.userName` and the authenticator published none, permissions are read for
|
|
47
|
+
`req.user` and a warning says so once — a rule that silently stops matching
|
|
48
|
+
is worse than one that refuses.
|
|
49
|
+
|
|
50
|
+
At startup each authenticator logs what it will publish, and each
|
|
51
|
+
authorization plugin whose configured identities none of them can produce
|
|
52
|
+
says so: token, TOTP and HMAC names are in the configuration, and an
|
|
53
|
+
identity provider's claims are not, in which case the line says it cannot be
|
|
54
|
+
checked before a login rather than staying silent.
|
|
55
|
+
|
|
56
|
+
## Several authorization plugins
|
|
57
|
+
|
|
58
|
+
Every plugin that judges LDAP operations — `core/auth/authzPerBranch`,
|
|
59
|
+
`core/auth/authzLinid1`, `core/auth/authzDynamic` — registers the same hooks,
|
|
60
|
+
and all of them run: loaded together, they compose as an **AND**, and the
|
|
61
|
+
first refusal wins. The per-caller read filter (`ldapsearchfilter`) is one of
|
|
62
|
+
those hooks: two plugins there compose as an intersection, which is the same
|
|
63
|
+
AND. That is sound when it is meant. When it is not, it is a
|
|
64
|
+
refusal nobody can explain: `authzPerBranch` beside `authzDynamic` judges a
|
|
65
|
+
machine token by a branch configuration written for administrators, finds
|
|
66
|
+
no entry for the tenant, and refuses a write the token's own ACLs grant.
|
|
67
|
+
|
|
68
|
+
The AND needs no second plugin to bite. `--authz-per-branch-config` has a
|
|
69
|
+
default — read everywhere, write nowhere — so a lone `authzPerBranch` judges
|
|
70
|
+
**every** authenticated identity, whatever authenticated it: a token that no
|
|
71
|
+
rule names reads, and every write it attempts is refused.
|
|
72
|
+
|
|
73
|
+
### Whose requests a plugin judges
|
|
74
|
+
|
|
75
|
+
The dispatcher records which authentication plugins vouched for a request,
|
|
76
|
+
by instance name, in `req.authenticators`. An authorization plugin given
|
|
77
|
+
`authz_for` judges the requests of those plugins and skips the others:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
--plugin core/auth/authzDynamic
|
|
81
|
+
--plugin 'core/auth/openidconnect:oidc:{"auth_path_prefix":"/api/admin"}'
|
|
82
|
+
--plugin 'core/auth/authzPerBranch:authzPerBranch:{"authz_for":["oidc"]}'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Here the tokens are held to their own ACLs, and the administrators signed in
|
|
86
|
+
through OpenID Connect to the branch configuration: no request is judged by
|
|
87
|
+
both. Unset, `authz_for` means every authenticated request — what every
|
|
88
|
+
plugin did before the option existed. It is read by `authzPerBranch`,
|
|
89
|
+
`authzLinid1` and `authzPerRoute`; `authzDynamic` judges the requests
|
|
90
|
+
carrying its own tokens, and no others, whatever the option says — an
|
|
91
|
+
`authz_for` of its own there is said at startup rather than ignored in
|
|
92
|
+
silence.
|
|
93
|
+
|
|
94
|
+
Two cases stay apart:
|
|
95
|
+
|
|
96
|
+
- a request **another** population's authenticator vouched for is skipped —
|
|
97
|
+
it is not this model's business;
|
|
98
|
+
- a request of **this** population whose identity does not resolve is
|
|
99
|
+
refused — a configuration error, not an absence of one
|
|
100
|
+
(`--authz-unresolved-user`).
|
|
101
|
+
|
|
102
|
+
A request carrying an identity that no authenticator vouched for is judged:
|
|
103
|
+
"not mine" is never inferred from an absence. A plugin that only stepped
|
|
104
|
+
aside — `authzDynamic` letting a request through on
|
|
105
|
+
`--authz-dynamic-bypass` — did not vouch for it and is not recorded.
|
|
106
|
+
|
|
107
|
+
### What is refused at startup
|
|
108
|
+
|
|
109
|
+
- Two plugins judging LDAP operations for the same authenticator's requests
|
|
110
|
+
— two branch-level plugins with no `authz_for`, or a branch-level plugin
|
|
111
|
+
judging `authzDynamic`'s tokens — are refused, with both names, unless
|
|
112
|
+
`--authz-combine` says the AND is meant. `authzPerRoute` registers no LDAP
|
|
113
|
+
hook and is never part of it: route plus branch, and route plus token, are
|
|
114
|
+
deliberate ANDs on identities each side can name.
|
|
115
|
+
- An `authz_for` naming no loaded authentication plugin is refused: the
|
|
116
|
+
plugin would judge nobody, which reads as a working configuration. A
|
|
117
|
+
server-wide `--authz-for` that no loaded plugin reads — only
|
|
118
|
+
`authzDynamic` there — is said: it scopes nothing.
|
|
119
|
+
- Two plugins whose populations only meet through two authenticators on the
|
|
120
|
+
same path prefix — a request presenting both credentials is judged by
|
|
121
|
+
both — are said, not refused: asking for two credentials is a decision
|
|
122
|
+
already.
|
|
123
|
+
|
|
124
|
+
A host that assembles the server itself — `registerPlugin` after `ready`,
|
|
125
|
+
rather than `--plugin` — gets the same checks, as each plugin arrives: the
|
|
126
|
+
refusal comes out of `registerPlugin`, before any of the plugin's routes or
|
|
127
|
+
hooks exist. An arriving plugin is judged against what is already there, so
|
|
128
|
+
the authentication plugins an `authz_for` or `--authz-scope-source` names
|
|
129
|
+
are registered before the plugin naming them.
|
|
130
|
+
|
|
131
|
+
### What the log says
|
|
132
|
+
|
|
133
|
+
At startup, one line per plugin names the hooks it takes part in and whose
|
|
134
|
+
requests it judges — `every authenticated request (authToken, oidc)` when
|
|
135
|
+
`authz_for` is unset, so a model judging an authenticator nobody wrote it
|
|
136
|
+
for is visible before a request arrives. When a hook refuses, the log line
|
|
137
|
+
names the plugin that refused; the 403 body does not, as it never discloses
|
|
138
|
+
the model.
|
|
139
|
+
|
|
140
|
+
With two plugins combined on purpose, `--authz-filter-attached-entries` only
|
|
141
|
+
broadens a listing while both honour it — both do, being built on the same
|
|
142
|
+
base class, but nothing obliges a third implementation to.
|
|
143
|
+
|
|
15
144
|
## Authorization Plugins
|
|
16
145
|
|
|
17
146
|
| Method | Plugin | Description |
|
|
@@ -44,6 +44,44 @@ node lib/bin/index.js \
|
|
|
44
44
|
| `--authz-dynamic-config-attribute` | `DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE` | `description` | Attribute holding the JSON ACL document |
|
|
45
45
|
| `--authz-dynamic-tenant-attribute` | `DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE` | `cn` | Attribute from which `req.user` is read |
|
|
46
46
|
| `--authz-dynamic-reload-endpoint` | `DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT` | `false` | Register `POST /api/v1/authz-dynamic/reload` for manual cache refresh |
|
|
47
|
+
| `--authz-dynamic-bypass` | `DM_AUTHZ_DYNAMIC_BYPASS` | _(empty)_ | Who may reach the directory without one of these tokens (see below) |
|
|
48
|
+
|
|
49
|
+
### Loading this plugin beside another authenticator
|
|
50
|
+
|
|
51
|
+
The dispatcher runs every authentication plugin claiming a request's path,
|
|
52
|
+
so a request can arrive here already identified by another one. Until 0.9.0
|
|
53
|
+
this plugin stepped aside whenever that happened, and the token ACLs were
|
|
54
|
+
then applied to nothing: with `core/auth/token` loaded beside it, a static
|
|
55
|
+
token reached the whole directory as an unscoped administrator — or was
|
|
56
|
+
refused for lacking a dynamic token, depending on which plugin had been
|
|
57
|
+
registered first.
|
|
58
|
+
|
|
59
|
+
The ACLs now apply to whatever token the request carries, whoever else
|
|
60
|
+
identified it, and anything allowed past them without a token is named:
|
|
61
|
+
|
|
62
|
+
| `--authz-dynamic-bypass` | Effect |
|
|
63
|
+
| ------------------------ | --------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| _(empty, the default)_ | Nobody. A request must carry one of these tokens |
|
|
65
|
+
| `any-authenticated` | Anything another plugin authenticated — the behaviour before 0.9.0 |
|
|
66
|
+
| `trusted-proxy` | A request from a trusted proxy **that named a caller** (`req.trustedProxy` and `req.proxyAuthUser`) |
|
|
67
|
+
| any other value | The identity another authenticator publishes in `req.user`, e.g. a name from `--auth-token` |
|
|
68
|
+
|
|
69
|
+
Values combine: `--authz-dynamic-bypass trusted-proxy --authz-dynamic-bypass ops-admin`.
|
|
70
|
+
|
|
71
|
+
`trusted-proxy` requires the proxy to have named someone. `trustedProxy`
|
|
72
|
+
marks the _address_ it trusts and fills `proxyAuthUser` only when the
|
|
73
|
+
request carried the authentication header it expects, so accepting the mark
|
|
74
|
+
alone would hand the whole directory to every host of the `--trusted-proxy`
|
|
75
|
+
range — `10.0.0.0/8` being all of it.
|
|
76
|
+
|
|
77
|
+
A bypassed request carries **no** token, so no ACL is enforced on it — that
|
|
78
|
+
is what a bypass is. Each one is logged at `info` with the reason that let it
|
|
79
|
+
through, and a configuration loading this plugin beside another
|
|
80
|
+
authenticator says so once at startup.
|
|
81
|
+
|
|
82
|
+
The answer no longer depends on registration order: when the bypass names an
|
|
83
|
+
identity another plugin has not published yet, the verdict waits until every
|
|
84
|
+
authenticator has run.
|
|
47
85
|
|
|
48
86
|
## Token entry shape
|
|
49
87
|
|
|
@@ -148,6 +186,10 @@ ACL, the client is cryptographically constrained to its own SCIM subtree.
|
|
|
148
186
|
deploy cleartext secrets in production.
|
|
149
187
|
- **Timing**: comparisons run through `crypto.timingSafeEqual` or a buffer
|
|
150
188
|
wrapper, preventing timing oracles on the secret.
|
|
189
|
+
- **Composition**: loading another authenticator on the same paths means a
|
|
190
|
+
request can arrive already identified. It is still checked against the
|
|
191
|
+
token it carries; what may pass without one is `--authz-dynamic-bypass`,
|
|
192
|
+
and nothing else.
|
|
151
193
|
- **Scope escape**: the authz hooks check every LDAP operation's DN against
|
|
152
194
|
the token's ACL. The active token is carried via `AsyncLocalStorage`, so it
|
|
153
195
|
applies even to plugins that do not thread `req` down to `ldapActions`.
|
|
@@ -60,6 +60,39 @@ No configuration file needed - permissions are read from LDAP. Simply load the p
|
|
|
60
60
|
|
|
61
61
|
The plugin caches permissions for 5 minutes by default. This is hardcoded but can be modified in the source if needed.
|
|
62
62
|
|
|
63
|
+
### The identity has to be a `uid`
|
|
64
|
+
|
|
65
|
+
The plugin resolves `req.user` by searching
|
|
66
|
+
`(<ldap_user_main_attribute>=<identity>)` under `--ldap-base`, so it only
|
|
67
|
+
works behind an authenticator that publishes what the directory calls a user:
|
|
68
|
+
|
|
69
|
+
| Authenticator | `req.user` | Resolves here |
|
|
70
|
+
| ------------------------- | -------------------------------- | -------------------------------------- |
|
|
71
|
+
| `core/auth/token` | the name given in `--auth-token` | only if that name is a `uid` |
|
|
72
|
+
| `core/auth/totp` | the name given in `--auth-totp` | only if that name is a `uid` |
|
|
73
|
+
| `core/auth/hmac` | the service name | usually not |
|
|
74
|
+
| `core/auth/llng` | LLNG's `whatToTrace` | when it traces the `uid` |
|
|
75
|
+
| `core/auth/openidconnect` | the OIDC `sub` | **no** — an opaque provider identifier |
|
|
76
|
+
|
|
77
|
+
An identity that does not resolve is **refused** (403), and the refusal is
|
|
78
|
+
logged with the plugin's name; `--authz-unresolved-user` sets it otherwise
|
|
79
|
+
([notes](../../upgrading.md#an-identity-that-does-not-resolve-is-refused)).
|
|
80
|
+
An identity naming **several** entries is refused (403) whatever that
|
|
81
|
+
option says, and a warning gives the number of entries. A lookup the
|
|
82
|
+
directory fails fails the request and is not cached.
|
|
83
|
+
|
|
84
|
+
Both answers are cached for the plugin's TTL (5 minutes), negatives
|
|
85
|
+
included, so a mismatch does not turn every request into a directory search.
|
|
86
|
+
The TTL cuts both ways: an identity added to the directory after being
|
|
87
|
+
refused waits it out, and an administrator whose **own entry is renamed or
|
|
88
|
+
moved** keeps resolving to the former DN — where no organization names them,
|
|
89
|
+
so their own operations are refused. A rename, a move to the trash or a
|
|
90
|
+
delete performed through this server drops what was resolved; a change made
|
|
91
|
+
directly in the directory waits out the TTL.
|
|
92
|
+
|
|
93
|
+
`GET /api/v1/authz/scope` resolves the identity outside that cache, so
|
|
94
|
+
during such a window it can describe a scope the hooks no longer grant.
|
|
95
|
+
|
|
63
96
|
## Permission Resolution
|
|
64
97
|
|
|
65
98
|
For each LDAP operation, the plugin:
|
|
@@ -174,10 +174,20 @@ Modifies the organization tree API:
|
|
|
174
174
|
|
|
175
175
|
Groups are resolved dynamically:
|
|
176
176
|
|
|
177
|
-
1.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
1. Resolves the caller to its DN, searching
|
|
178
|
+
`(<ldap_user_main_attribute>=<uid>)` under the LDAP base (`--ldap-base`)
|
|
179
|
+
2. Searches for all groups where that DN is a member
|
|
180
|
+
3. Matches each group found against the `groups` keys **as DNs**: case and
|
|
181
|
+
the spaces around `,` and `=` do not matter, so
|
|
182
|
+
`CN=Editors, OU=Groups,dc=example,dc=com` names the same group as
|
|
183
|
+
`cn=editors,ou=groups,dc=example,dc=com`
|
|
184
|
+
4. Caches results (see [Group Caching](#group-caching)); concurrent
|
|
185
|
+
requests for the same user share one lookup
|
|
186
|
+
|
|
187
|
+
A uid that names **more than one** entry gets **no group rule**, since its
|
|
188
|
+
groups cannot be told apart from its homonym's; a warning is logged, and the
|
|
189
|
+
uid's own `users` rule still applies. A lookup that fails answers `500` and
|
|
190
|
+
is not cached.
|
|
181
191
|
|
|
182
192
|
## Sub-branch Inheritance
|
|
183
193
|
|
|
@@ -379,6 +389,10 @@ Group memberships are cached to reduce LDAP queries:
|
|
|
379
389
|
- **Configurable**: `--authz-per-branch-cache-ttl`
|
|
380
390
|
- **Per-user cache**: Each user's groups cached separately
|
|
381
391
|
- **Automatic expiry**: Cache entries expire after TTL
|
|
392
|
+
- **Dropped on writes**: any add, delete, rename or move made through
|
|
393
|
+
ldap-rest empties the cache, and so does a modify touching the group member
|
|
394
|
+
attribute or `--ldap-user-main-attribute`. A change made directly in the
|
|
395
|
+
directory waits for the TTL
|
|
382
396
|
|
|
383
397
|
Cache hit/miss logging:
|
|
384
398
|
|
|
@@ -408,11 +422,18 @@ Cache hit/miss logging:
|
|
|
408
422
|
- **Open by default**: Set `read: true` for general access
|
|
409
423
|
- Choose based on your security posture
|
|
410
424
|
|
|
425
|
+
`--authz-per-branch-config` is never empty: unset, it is
|
|
426
|
+
`{"default":{"read":true,"write":false,"delete":false}}`, and it applies to
|
|
427
|
+
**every** authenticated identity, whatever authenticated it. Loaded beside a
|
|
428
|
+
token plugin, `authzDynamic` or an identity provider whose callers no rule
|
|
429
|
+
names, this plugin lets them read and refuses every write. Give it an
|
|
430
|
+
`authz_for` naming the authentication plugins it was written for — see
|
|
431
|
+
[several authorization plugins](README.md#several-authorization-plugins).
|
|
432
|
+
|
|
411
433
|
### Group-Based Security
|
|
412
434
|
|
|
413
435
|
- Group DNs must be fully qualified
|
|
414
436
|
- Group resolution uses LDAP search - ensure proper indexing
|
|
415
|
-
- Cache can delay permission revocation (up to TTL duration)
|
|
416
437
|
|
|
417
438
|
### Authentication Required
|
|
418
439
|
|
|
@@ -523,7 +544,8 @@ Changed permissions don't take effect immediately.
|
|
|
523
544
|
|
|
524
545
|
**Solutions:**
|
|
525
546
|
|
|
526
|
-
1.
|
|
547
|
+
1. A membership changed directly in the directory waits for the
|
|
548
|
+
[group cache](#group-caching) TTL
|
|
527
549
|
2. Restart server to clear all caches
|
|
528
550
|
3. Reduce `--authz-per-branch-cache-ttl` for testing
|
|
529
551
|
|
|
@@ -534,17 +556,25 @@ User should inherit group permissions but doesn't.
|
|
|
534
556
|
|
|
535
557
|
**Solutions:**
|
|
536
558
|
|
|
537
|
-
1. Verify user is member of group:
|
|
559
|
+
1. Verify the user's DN is a member of the group:
|
|
538
560
|
|
|
539
561
|
```bash
|
|
540
|
-
ldapsearch -x -b "
|
|
562
|
+
ldapsearch -x -b "dc=example,dc=com" \
|
|
563
|
+
"(member=uid=jdoe,ou=users,dc=example,dc=com)"
|
|
541
564
|
```
|
|
542
565
|
|
|
543
|
-
2. Check group DN in config
|
|
566
|
+
2. Check the group DN in config names the group: it is compared as a DN
|
|
567
|
+
(case and spaces are ignored), but every RDN must be there
|
|
568
|
+
|
|
569
|
+
3. Check the uid names a single entry:
|
|
570
|
+
|
|
571
|
+
```bash
|
|
572
|
+
ldapsearch -x -b "dc=example,dc=com" "(uid=jdoe)" dn
|
|
573
|
+
```
|
|
544
574
|
|
|
545
|
-
|
|
575
|
+
4. Ensure group membership uses correct attribute (default: `member`)
|
|
546
576
|
|
|
547
|
-
|
|
577
|
+
5. Configure custom member attribute if needed:
|
|
548
578
|
```bash
|
|
549
579
|
--ldap-group-member-attribute uniqueMember
|
|
550
580
|
```
|
|
@@ -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
|
|