ldap-rest 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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/organizations.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/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/src/abstract/ldapFlat.d.ts +7 -0
- package/dist/src/abstract/plugin.d.ts +20 -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 +55 -0
- package/dist/src/lib/auth/base.d.ts +175 -9
- package/dist/src/lib/authz/base.d.ts +133 -4
- package/dist/src/lib/authz/composition.d.ts +145 -0
- package/dist/src/lib/expressFormatedResponses.d.ts +9 -2
- package/dist/src/lib/ldapActions.d.ts +30 -0
- 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/authzPerBranch.d.ts +11 -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/organizations.d.ts +90 -1
- 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 +12 -1
- package/docs/plugin-development/dependencies.md +40 -15
- package/docs/usage/configuration.md +89 -13
- 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 +32 -0
- package/docs/usage/plugins/auth/authz-per-branch.md +8 -0
- package/docs/usage/plugins/auth/authz-per-route.md +6 -0
- package/docs/usage/plugins/auth/authz-scope.md +69 -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/organizations.md +37 -0
- package/docs/usage/plugins/utilities/storage.md +94 -0
- package/docs/usage/upgrading.md +289 -0
- package/package.json +17 -1
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
8
8
|
import { HttpError } from '../../lib/errors';
|
|
9
|
+
import { extractLdapCode } from '../../lib/ldapCodes';
|
|
10
|
+
export { extractLdapCode };
|
|
9
11
|
import { type ScimErrorResponse, type ScimErrorType } from './types';
|
|
10
12
|
export declare const SCIM_CONTENT_TYPE = "application/scim+json";
|
|
11
13
|
export declare class ScimError extends HttpError {
|
|
@@ -22,16 +24,6 @@ export declare const scimUniqueness: (detail?: string) => ScimError;
|
|
|
22
24
|
export declare const scimTooMany: (detail?: string) => ScimError;
|
|
23
25
|
export declare const scimNotFound: (detail?: string) => ScimError;
|
|
24
26
|
export declare function writeScimError(res: Response, status: number, detail: string, scimType?: ScimErrorType): void;
|
|
25
|
-
/**
|
|
26
|
-
* Extract an LDAP numeric error code from a thrown ldapts error.
|
|
27
|
-
*
|
|
28
|
-
* `.code` first: ldapts sets it, and `ldapActions` now carries it across its
|
|
29
|
-
* own wrapping rather than reducing the failure to a sentence. The message
|
|
30
|
-
* patterns below stay as a fallback for the paths that still throw a bare
|
|
31
|
-
* `Error` — they are why a driver that reworded its messages could quietly
|
|
32
|
-
* turn a 400 into a 500, so they are the last resort, not the first.
|
|
33
|
-
*/
|
|
34
|
-
export declare function extractLdapCode(err: unknown): number | undefined;
|
|
35
27
|
/**
|
|
36
28
|
* Turn any thrown thing into the SCIM error body it deserves.
|
|
37
29
|
*
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type winston from 'winston';
|
|
2
|
+
import Store, { type StoredRecord } from '../../lib/storage/store';
|
|
3
|
+
export default class FileStore extends Store {
|
|
4
|
+
name: string;
|
|
5
|
+
private dir;
|
|
6
|
+
/** How long a temporary file must sit before it counts as abandoned. */
|
|
7
|
+
private abandonedAfter;
|
|
8
|
+
constructor(logger: winston.Logger, dir: string, abandonedAfterSeconds: number);
|
|
9
|
+
/** The directory is created on demand, readable by this user alone. */
|
|
10
|
+
ready(): Promise<void>;
|
|
11
|
+
private path;
|
|
12
|
+
/** `<deadline> <key>\n<value>` — the deadline first, so a short read still has it. */
|
|
13
|
+
private static encode;
|
|
14
|
+
private static decode;
|
|
15
|
+
protected load(key: string): Promise<StoredRecord | null>;
|
|
16
|
+
protected save(key: string, record: StoredRecord): Promise<void>;
|
|
17
|
+
protected drop(key: string): Promise<void>;
|
|
18
|
+
sweep(): Promise<number>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Store, { type StoredRecord } from '../../lib/storage/store';
|
|
2
|
+
import type { DM } from '../../bin';
|
|
3
|
+
export default class LdapStore extends Store {
|
|
4
|
+
name: string;
|
|
5
|
+
private server;
|
|
6
|
+
private base;
|
|
7
|
+
private objectClass;
|
|
8
|
+
constructor(server: DM, base: string, objectClass: string);
|
|
9
|
+
private dn;
|
|
10
|
+
/** `<deadline> <key>\n<value>`, the deadline first as in the file store. */
|
|
11
|
+
private static encode;
|
|
12
|
+
private static decode;
|
|
13
|
+
protected load(key: string): Promise<StoredRecord | null>;
|
|
14
|
+
protected save(key: string, record: StoredRecord): Promise<void>;
|
|
15
|
+
protected drop(key: string): Promise<void>;
|
|
16
|
+
sweep(): Promise<number>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/storage
|
|
3
|
+
*
|
|
4
|
+
* Keyed storage, for whoever needs to keep something with a deadline.
|
|
5
|
+
*
|
|
6
|
+
* One plugin, one backend, chosen by `--storage-backend`. Consumers ask this
|
|
7
|
+
* one for its store and work through a single interface; where the records
|
|
8
|
+
* actually live is a deployment decision, not theirs.
|
|
9
|
+
*
|
|
10
|
+
* Having exactly one is the point. The alternative — each consumer loading
|
|
11
|
+
* the backend it likes — puts the same record in two places, and makes
|
|
12
|
+
* whichever plugin was registered first decide what happens when one of them
|
|
13
|
+
* is unwell. Registration order is invisible in a configuration, which is a
|
|
14
|
+
* poor place to keep a behaviour.
|
|
15
|
+
*
|
|
16
|
+
* A consumer reaches it with `requirePlugin('storage')` and works through
|
|
17
|
+
* `store`; it should not index `loadedPlugins` itself. And one that makes a
|
|
18
|
+
* security decision must not read "no storage" as "nothing to check":
|
|
19
|
+
* `requirePlugin` answers null and warns once, which is right for an optional
|
|
20
|
+
* feature and wrong for, say, a logout check — such a consumer refuses to
|
|
21
|
+
* load instead.
|
|
22
|
+
*
|
|
23
|
+
* What a consumer must not expect of this layer: it does not decide how long
|
|
24
|
+
* anything lives. A deadline comes in with the record, because retention is
|
|
25
|
+
* the policy of whoever wrote it — the moment a backend decides for itself,
|
|
26
|
+
* two of them can disagree about when the same record died.
|
|
27
|
+
*
|
|
28
|
+
* @group Plugins
|
|
29
|
+
*/
|
|
30
|
+
import DmPlugin from '../abstract/plugin';
|
|
31
|
+
import type { DM } from '../bin';
|
|
32
|
+
import type Store from '../lib/storage/store';
|
|
33
|
+
export default class Storage extends DmPlugin {
|
|
34
|
+
name: string;
|
|
35
|
+
store: Store;
|
|
36
|
+
constructor(server: DM);
|
|
37
|
+
/**
|
|
38
|
+
* Release the single-instance hold.
|
|
39
|
+
*
|
|
40
|
+
* For a test building several servers in one process; a running one has no
|
|
41
|
+
* reason to call it.
|
|
42
|
+
*/
|
|
43
|
+
static release(instance: Storage): void;
|
|
44
|
+
}
|
|
@@ -475,7 +475,18 @@ The `this.server.ldap` object provides access to all LDAP operations:
|
|
|
475
475
|
> `forRequest()` returns a `RequestBoundLdap`, whose methods have no request
|
|
476
476
|
> parameter at all — the omission stops being expressible. Use the unbound
|
|
477
477
|
> methods only for work that genuinely belongs to no request, such as startup
|
|
478
|
-
> or scheduled tasks, where passing nothing is the deliberate answer
|
|
478
|
+
> or scheduled tasks, where passing nothing is the deliberate answer — and
|
|
479
|
+
> say so by reaching for them through `ldap.system`:
|
|
480
|
+
>
|
|
481
|
+
> ```typescript
|
|
482
|
+
> await this.server.ldap.system.search({ scope: 'base' }, dn);
|
|
483
|
+
> ```
|
|
484
|
+
>
|
|
485
|
+
> It is the same object, and that is the point: `ldap.system.search(…)` reads
|
|
486
|
+
> as a decision where `ldap.search(…)` reads as a forgotten argument. Some
|
|
487
|
+
> reads are the server's by nature — a uniqueness check has to see a whole
|
|
488
|
+
> branch, and a reference to an entry the caller cannot read is still a valid
|
|
489
|
+
> reference — and those are the ones it names.
|
|
479
490
|
|
|
480
491
|
#### Search
|
|
481
492
|
|
|
@@ -76,6 +76,16 @@ dependencies = {
|
|
|
76
76
|
- **Consumes Hooks**: None
|
|
77
77
|
- **Notes**: LinID 1.x authorization integration
|
|
78
78
|
|
|
79
|
+
#### `bcl` (core/bcl)
|
|
80
|
+
|
|
81
|
+
- **Dependencies**: `core/storage` — **required**: a logout check with
|
|
82
|
+
nowhere to record what died would leave every session outliving the logout
|
|
83
|
+
that closed it, so the plugin refuses to start
|
|
84
|
+
- **Provides Hooks**: None
|
|
85
|
+
- **Consumes Hooks**: `oidclogouttoken`, `oidclogin`, `oidcsessionvalid`
|
|
86
|
+
- **Notes**: Back-Channel Logout. Records only the sessions a provider
|
|
87
|
+
declared dead; needs `core/auth/openidconnect` to raise those hooks
|
|
88
|
+
|
|
79
89
|
### LDAP Core Plugins
|
|
80
90
|
|
|
81
91
|
#### `onLdapChange` (core/ldap/onChange)
|
|
@@ -238,6 +248,15 @@ dependencies = {
|
|
|
238
248
|
- **Consumes Hooks**: None
|
|
239
249
|
- **Notes**: HTTP request logging
|
|
240
250
|
|
|
251
|
+
#### `storage` (core/storage)
|
|
252
|
+
|
|
253
|
+
- **Dependencies**: None (the `ldap` backend uses the server's connection)
|
|
254
|
+
- **Provides Hooks**: None
|
|
255
|
+
- **Consumes Hooks**: None
|
|
256
|
+
- **Notes**: Keyed storage with a deadline. Only one instance may be loaded —
|
|
257
|
+
a second one is refused rather than leaving registration order to decide
|
|
258
|
+
which answers
|
|
259
|
+
|
|
241
260
|
#### `configApi` (core/configApi)
|
|
242
261
|
|
|
243
262
|
- **Dependencies**: None (reads configuration from other loaded plugins)
|
|
@@ -531,24 +550,30 @@ graph TB
|
|
|
531
550
|
- `static` - Static files
|
|
532
551
|
- `weblogs` - Logging
|
|
533
552
|
- `configApi` - Configuration API
|
|
553
|
+
- `storage` - Keyed storage. Written anywhere in the configuration, but
|
|
554
|
+
loaded before the others: its consumers have to find it registered, so it
|
|
555
|
+
is in the priority list
|
|
534
556
|
|
|
535
557
|
## Plugin Priority System
|
|
536
558
|
|
|
537
|
-
LDAP-Rest uses a priority system defined in
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
559
|
+
LDAP-Rest uses a priority system defined in
|
|
560
|
+
[`src/plugins/priority.json`](../../src/plugins/priority.json), which is the
|
|
561
|
+
list itself — copying it here is how this section came to name two plugins
|
|
562
|
+
the file does not and to omit four it does.
|
|
563
|
+
|
|
564
|
+
Plugins in that list are loaded one at a time, in its order, before every
|
|
565
|
+
other plugin is loaded in parallel. Two reasons put a plugin there: a
|
|
566
|
+
security plugin whose middleware must sit ahead of the routes it guards, and
|
|
567
|
+
a plugin others depend on, which has to be registered before the batch its
|
|
568
|
+
consumers land in — `core/storage` is there for the second reason.
|
|
569
|
+
|
|
570
|
+
The list matches on the **module**, so a named instance
|
|
571
|
+
(`core/auth/token:authAdmins:{…}`) keeps its rank, and every instance of a
|
|
572
|
+
priority module is loaded in that first pass, in configuration order. It
|
|
573
|
+
used to be matched on the whole `--plugin` string, which sent exactly the
|
|
574
|
+
configurations that carry per-instance options into the parallel batch —
|
|
575
|
+
`core/auth/trustedProxy` named that way registered after the routes it
|
|
576
|
+
guards.
|
|
552
577
|
|
|
553
578
|
## Hook Execution Order
|
|
554
579
|
|
|
@@ -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)
|
|
@@ -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,38 @@ 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. Until 0.9.0 it was let through instead, which
|
|
79
|
+
made this pairing an open door rather than a misconfiguration; see
|
|
80
|
+
[the note](../../upgrading.md#an-identity-that-does-not-resolve-is-refused)
|
|
81
|
+
and `--authz-unresolved-user` for the transition.
|
|
82
|
+
|
|
83
|
+
Both answers are cached for the plugin's TTL (5 minutes), negatives
|
|
84
|
+
included, so a mismatch does not turn every request into a directory search.
|
|
85
|
+
The TTL cuts both ways: an identity added to the directory after being
|
|
86
|
+
refused waits it out, and an administrator whose **own entry is renamed or
|
|
87
|
+
moved** keeps resolving to the former DN — where no organization names them,
|
|
88
|
+
so their own operations are refused. A rename or a delete performed through
|
|
89
|
+
this server drops what was resolved; a change made directly in the directory
|
|
90
|
+
waits out the TTL.
|
|
91
|
+
|
|
92
|
+
`GET /api/v1/authz/scope` resolves the identity outside that cache, so
|
|
93
|
+
during such a window it can describe a scope the hooks no longer grant.
|
|
94
|
+
|
|
63
95
|
## Permission Resolution
|
|
64
96
|
|
|
65
97
|
For each LDAP operation, the plugin:
|