ldap-rest 0.8.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/bin/index.mjs +1 -1
  2. package/dist/abstract/ldapFlat.js +1 -1
  3. package/dist/bin/index.js +1 -1
  4. package/dist/config/args.js +1 -1
  5. package/dist/lib/auth/base.js +1 -1
  6. package/dist/lib/authz/base.js +1 -1
  7. package/dist/lib/authz/composition.js +1 -0
  8. package/dist/lib/changeContext.js +1 -0
  9. package/dist/lib/errors.js +1 -1
  10. package/dist/lib/expressFormatedResponses.js +1 -1
  11. package/dist/lib/ldapActions.js +1 -1
  12. package/dist/lib/ldapCodes.js +1 -0
  13. package/dist/lib/parseConfig.js +1 -1
  14. package/dist/lib/storage/store.js +1 -0
  15. package/dist/lib/utils.js +1 -1
  16. package/dist/plugins/auth/authzDynamic.js +1 -1
  17. package/dist/plugins/auth/authzLinid1.js +1 -1
  18. package/dist/plugins/auth/authzPerBranch.js +1 -1
  19. package/dist/plugins/auth/authzPerRoute.js +1 -1
  20. package/dist/plugins/auth/authzScope.js +1 -1
  21. package/dist/plugins/auth/hmac.js +1 -1
  22. package/dist/plugins/auth/llng.js +1 -1
  23. package/dist/plugins/auth/openidconnect.js +1 -1
  24. package/dist/plugins/auth/token.js +1 -1
  25. package/dist/plugins/auth/totp.js +1 -1
  26. package/dist/plugins/bcl.js +1 -0
  27. package/dist/plugins/ldap/departmentSync.js +1 -1
  28. package/dist/plugins/ldap/groups.js +1 -1
  29. package/dist/plugins/ldap/onChange.js +1 -1
  30. package/dist/plugins/ldap/organizations.js +1 -1
  31. package/dist/plugins/ldap/trash.js +1 -1
  32. package/dist/plugins/priority.json.js +1 -1
  33. package/dist/plugins/scim/baseResolver.js +1 -1
  34. package/dist/plugins/scim/errors.js +1 -1
  35. package/dist/plugins/scim/groups.js +1 -1
  36. package/dist/plugins/scim/list.js +1 -1
  37. package/dist/plugins/scim/scim.js +1 -1
  38. package/dist/plugins/scim/users.js +1 -1
  39. package/dist/plugins/storage/file.js +1 -0
  40. package/dist/plugins/storage/ldap.js +1 -0
  41. package/dist/plugins/storage.js +1 -0
  42. package/dist/plugins/twake/appAccountsApi.js +1 -1
  43. package/dist/plugins/twake/appAccountsConsistency.js +1 -1
  44. package/dist/plugins/twake/clouderyProvision.js +1 -1
  45. package/dist/plugins/twake/cozyProvision.js +1 -1
  46. package/dist/src/abstract/ldapFlat.d.ts +7 -0
  47. package/dist/src/abstract/plugin.d.ts +26 -0
  48. package/dist/src/bin/index.d.ts +50 -8
  49. package/dist/src/config/args.d.ts +36 -0
  50. package/dist/src/hooks.d.ts +70 -6
  51. package/dist/src/lib/auth/base.d.ts +175 -9
  52. package/dist/src/lib/authz/base.d.ts +159 -4
  53. package/dist/src/lib/authz/composition.d.ts +145 -0
  54. package/dist/src/lib/changeContext.d.ts +24 -0
  55. package/dist/src/lib/errors.d.ts +15 -0
  56. package/dist/src/lib/expressFormatedResponses.d.ts +9 -2
  57. package/dist/src/lib/ldapActions.d.ts +32 -1
  58. package/dist/src/lib/ldapCodes.d.ts +28 -0
  59. package/dist/src/lib/storage/store.d.ts +84 -0
  60. package/dist/src/lib/utils.d.ts +19 -0
  61. package/dist/src/plugins/auth/authzDynamic.d.ts +76 -0
  62. package/dist/src/plugins/auth/authzLinid1.d.ts +6 -0
  63. package/dist/src/plugins/auth/authzPerBranch.d.ts +46 -0
  64. package/dist/src/plugins/auth/authzPerRoute.d.ts +26 -0
  65. package/dist/src/plugins/auth/authzScope.d.ts +44 -3
  66. package/dist/src/plugins/auth/hmac.d.ts +2 -0
  67. package/dist/src/plugins/auth/llng.d.ts +39 -0
  68. package/dist/src/plugins/auth/openidconnect.d.ts +66 -3
  69. package/dist/src/plugins/auth/rateLimit.d.ts +3 -1
  70. package/dist/src/plugins/auth/token.d.ts +2 -0
  71. package/dist/src/plugins/auth/totp.d.ts +2 -0
  72. package/dist/src/plugins/bcl.d.ts +65 -0
  73. package/dist/src/plugins/ldap/groups.d.ts +7 -5
  74. package/dist/src/plugins/ldap/onChange.d.ts +17 -8
  75. package/dist/src/plugins/ldap/organizations.d.ts +92 -3
  76. package/dist/src/plugins/scim/baseResolver.d.ts +3 -1
  77. package/dist/src/plugins/scim/errors.d.ts +2 -10
  78. package/dist/src/plugins/storage/file.d.ts +19 -0
  79. package/dist/src/plugins/storage/ldap.d.ts +17 -0
  80. package/dist/src/plugins/storage.d.ts +44 -0
  81. package/docs/plugin-development/README.md +13 -19
  82. package/docs/plugin-development/dependencies.md +40 -15
  83. package/docs/plugin-development/hooks.md +6 -0
  84. package/docs/plugin-development/testing.md +1 -17
  85. package/docs/usage/configuration.md +90 -14
  86. package/docs/usage/plugins/README.md +17 -12
  87. package/docs/usage/plugins/auth/README.md +129 -0
  88. package/docs/usage/plugins/auth/authz-dynamic.md +42 -0
  89. package/docs/usage/plugins/auth/authz-linid1.md +33 -0
  90. package/docs/usage/plugins/auth/authz-per-branch.md +41 -11
  91. package/docs/usage/plugins/auth/authz-per-route.md +6 -0
  92. package/docs/usage/plugins/auth/authz-scope.md +74 -16
  93. package/docs/usage/plugins/auth/back-channel-logout.md +86 -0
  94. package/docs/usage/plugins/auth/llng.md +29 -0
  95. package/docs/usage/plugins/auth/oidc.md +52 -0
  96. package/docs/usage/plugins/ldap/on-change.md +51 -284
  97. package/docs/usage/plugins/ldap/organizations.md +37 -0
  98. package/docs/usage/plugins/utilities/storage.md +94 -0
  99. package/docs/usage/troubleshooting.md +1 -9
  100. package/docs/usage/upgrading.md +341 -0
  101. package/package.json +17 -1
@@ -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 | Env | Default | Description |
255
- | ------------ | ------------- | ------------------------------------ | ------------------------- |
256
- | `--llng-ini` | `DM_LLNG_INI` | `/etc/lemonldap-ng/lemonldap-ng.ini` | LemonLDAP::NG config path |
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 | Env | Default | Description |
261
- | ---------------------- | ----------------------- | ------- | ------------------------ |
262
- | `--oidc-server` | `DM_OIDC_SERVER` | | OIDC server URL |
263
- | `--oidc-client-id` | `DM_OIDC_CLIENT_ID` | | OIDC Client ID |
264
- | `--oidc-client-secret` | `DM_OIDC_CLIENT_SECRET` | | OIDC Client Secret |
265
- | `--base-url` | `DM_BASE_URL` | | Public URL for callbacks |
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 | Env | Default | Description |
272
- | ------------------------------ | ------------------------------- | ------------------------------------------------ | --------------------------- |
273
- | `--authz-per-branch-config` | `DM_AUTHZ_PER_BRANCH_CONFIG` | `{default:{read:true,write:false,delete:false}}` | Authorization config (JSON) |
274
- | `--authz-per-branch-cache-ttl` | `DM_AUTHZ_PER_BRANCH_CACHE_TTL` | `60` | Cache TTL (seconds) |
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 | 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 |
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 secure API endpoints. The order is defined in `src/plugins/priority.json`.
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. Searches for all groups where user is a member
178
- 2. Uses wildcard DN pattern: `(member=uid={user},*)`
179
- 3. Caches results for configured TTL (default: 60 seconds)
180
- 4. Cache prevents repeated LDAP queries for same user
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. Wait for group cache TTL to expire
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 "ou=groups,dc=example,dc=com" "(member=uid=jdoe,*)"
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 matches LDAP exactly
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
- 3. Ensure group membership uses correct attribute (default: `member`)
575
+ 4. Ensure group membership uses correct attribute (default: `member`)
546
576
 
547
- 4. Configure custom member attribute if needed:
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