ldap-rest 0.10.0 → 0.11.1
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/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/lib/storage/store.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/auth/authzLinid1.js +1 -1
- package/dist/plugins/auth/authzScope.js +1 -1
- package/dist/plugins/auth/fake.js +1 -0
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/priority.json.js +1 -1
- package/dist/plugins/storage/ldap.js +1 -1
- package/dist/plugins/storage/postgres.js +1 -0
- package/dist/plugins/storage/valkey.js +1 -0
- package/dist/plugins/storage.js +1 -1
- package/dist/src/config/args.d.ts +17 -3
- package/dist/src/lib/authz/base.d.ts +31 -0
- package/dist/src/lib/storage/store.d.ts +15 -0
- package/dist/src/lib/utils.d.ts +16 -0
- package/dist/src/plugins/auth/authzLinid1.d.ts +8 -0
- package/dist/src/plugins/auth/fake.d.ts +21 -0
- package/dist/src/plugins/ldap/groups.d.ts +6 -3
- package/dist/src/plugins/storage/ldap.d.ts +0 -3
- package/dist/src/plugins/storage/postgres.d.ts +54 -0
- package/dist/src/plugins/storage/valkey.d.ts +45 -0
- package/docs/usage/configuration.md +26 -12
- package/docs/usage/plugins/README.md +5 -5
- package/docs/usage/plugins/auth/README.md +2 -0
- package/docs/usage/plugins/auth/authz-linid1.md +55 -28
- package/docs/usage/plugins/auth/fake.md +28 -0
- package/docs/usage/plugins/utilities/storage.md +61 -10
- package/docs/usage/upgrading.md +41 -0
- package/package.json +16 -1
package/dist/src/lib/utils.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
6
6
|
import type { Config } from '../bin';
|
|
7
|
+
import type { AttributeValue } from './ldapActions';
|
|
7
8
|
/**
|
|
8
9
|
* Get a compiled RegExp from cache, or compile and cache it
|
|
9
10
|
* This avoids recompiling the same regex patterns repeatedly
|
|
@@ -268,6 +269,21 @@ export declare function normalizeDn(dn: string): string;
|
|
|
268
269
|
* ```
|
|
269
270
|
*/
|
|
270
271
|
export declare function isDummyMemberDn(member: unknown, dummy: string | undefined): boolean;
|
|
272
|
+
/**
|
|
273
|
+
* The organization an entry is attached to, read off its link attribute
|
|
274
|
+
* (`--ldap-organization-link-attribute`), or `undefined` when it is attached
|
|
275
|
+
* to none.
|
|
276
|
+
*
|
|
277
|
+
* "None" arrives in three forms: the attribute missing, an empty array —
|
|
278
|
+
* how ldapts answers an attribute that was asked for and is absent — and an
|
|
279
|
+
* empty value. The empty array read as the branch "undefined" in one copy of
|
|
280
|
+
* this logic, and nobody could write such an entry. Every reader of the link
|
|
281
|
+
* asks here, so the copies cannot drift apart again.
|
|
282
|
+
*
|
|
283
|
+
* @param value - The link attribute as a search returned it
|
|
284
|
+
* @returns its first value, or `undefined` when there is none
|
|
285
|
+
*/
|
|
286
|
+
export declare function organizationLink(value: AttributeValue | undefined): string | undefined;
|
|
271
287
|
/**
|
|
272
288
|
* Check whether a DN is a branch base itself or sits anywhere below it.
|
|
273
289
|
*
|
|
@@ -17,6 +17,14 @@ export default class AuthzLinid1 extends AuthzBase {
|
|
|
17
17
|
name: string;
|
|
18
18
|
permissionsCache: Map<string, CachedPermissions>;
|
|
19
19
|
constructor(server: DM);
|
|
20
|
+
/**
|
|
21
|
+
* Always: the branches this plugin grants are organizations, and what an
|
|
22
|
+
* administrator manages in them is the accounts and groups whose
|
|
23
|
+
* organization link names them, wherever they are stored. Judged by their
|
|
24
|
+
* parent instead, a flat `ou=users` is nobody's branch, and every
|
|
25
|
+
* administrator is refused every account.
|
|
26
|
+
*/
|
|
27
|
+
protected filtersAttachedEntries(): boolean;
|
|
20
28
|
/**
|
|
21
29
|
* Resolve user - for authzLinid1, convert uid to userDn via LDAP
|
|
22
30
|
*/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/auth/fake
|
|
3
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
+
*
|
|
5
|
+
* Development authentication: every request is served as the identity given
|
|
6
|
+
* on the command line, without asking for anything.
|
|
7
|
+
* @group Plugins
|
|
8
|
+
*/
|
|
9
|
+
import type { Response } from 'express';
|
|
10
|
+
import AuthBase, { type DmRequest } from '../../lib/auth/base';
|
|
11
|
+
import type { Role } from '../../abstract/plugin';
|
|
12
|
+
export default class AuthFake extends AuthBase {
|
|
13
|
+
name: string;
|
|
14
|
+
roles: Role[];
|
|
15
|
+
private readonly user;
|
|
16
|
+
constructor(...args: ConstructorParameters<typeof AuthBase>);
|
|
17
|
+
protected knownIdentities(): string[];
|
|
18
|
+
protected identitySource(): string;
|
|
19
|
+
afterLoad(): void;
|
|
20
|
+
authMethod(req: DmRequest, _res: Response, next: () => void): void;
|
|
21
|
+
}
|
|
@@ -148,23 +148,26 @@ export default class LdapGroups extends DmPlugin {
|
|
|
148
148
|
}>;
|
|
149
149
|
/**
|
|
150
150
|
* List groups from LDAP
|
|
151
|
-
* @param param0 {filter, attributes}:
|
|
151
|
+
* @param param0 {filter, attributes, req}:
|
|
152
152
|
* - filter: LDAP filter (default '(objectClass=*)')
|
|
153
153
|
* - attributes: attributes to fetch (default ['cn','member'])
|
|
154
|
+
* - req: incoming request, forwarded to the authorization hooks
|
|
154
155
|
* @returns LdapList (Record<string, AttributesList>)
|
|
155
156
|
*/
|
|
156
|
-
listGroups({ filter, attributes, }?: {
|
|
157
|
+
listGroups({ filter, attributes, req, }?: {
|
|
157
158
|
filter?: string;
|
|
158
159
|
attributes?: string[];
|
|
160
|
+
req?: Request;
|
|
159
161
|
}): Promise<LdapList>;
|
|
160
162
|
/**
|
|
161
163
|
* Simple formatter for listGroups()
|
|
162
164
|
* @param cn main attribute value (partial or full)
|
|
163
165
|
* @param partial boolean, true for partial search (default false)
|
|
164
166
|
* @param attributes array of attributes to return
|
|
167
|
+
* @param req incoming request, forwarded to the authorization hooks
|
|
165
168
|
* @returns LdapList (means Record<string, AttributesList> where key is the --ldap-user-main-attribute value [default: cn])
|
|
166
169
|
*/
|
|
167
|
-
searchGroupsByName(cn: string, partial?: boolean, attributes?: string[]): Promise<LdapList>;
|
|
170
|
+
searchGroupsByName(cn: string, partial?: boolean, attributes?: string[], req?: Request): Promise<LdapList>;
|
|
168
171
|
protected fixDn(dn: string): string | false;
|
|
169
172
|
/**
|
|
170
173
|
* Verify that each member exists in LDAP
|
|
@@ -7,9 +7,6 @@ export default class LdapStore extends Store {
|
|
|
7
7
|
private objectClass;
|
|
8
8
|
constructor(server: DM, base: string, objectClass: string);
|
|
9
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
10
|
protected load(key: string): Promise<StoredRecord | null>;
|
|
14
11
|
protected save(key: string, record: StoredRecord): Promise<void>;
|
|
15
12
|
protected drop(key: string): Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/storage/postgres
|
|
3
|
+
*
|
|
4
|
+
* A keyed store kept in one PostgreSQL table.
|
|
5
|
+
*
|
|
6
|
+
* `pg` is an optional dependency, loaded when this backend is chosen: a
|
|
7
|
+
* deployment that keeps its records elsewhere never needs it installed.
|
|
8
|
+
*
|
|
9
|
+
* The key is kept verbatim, split into the consumer's namespace and its own
|
|
10
|
+
* key: a `text` column has no reason to hash it the way a DN or a file name
|
|
11
|
+
* must, so a `SELECT` says what a record is about.
|
|
12
|
+
*
|
|
13
|
+
* @group Plugins
|
|
14
|
+
*/
|
|
15
|
+
import type winston from 'winston';
|
|
16
|
+
import Store, { type StoredRecord } from '../../lib/storage/store';
|
|
17
|
+
type Pg = typeof import('pg');
|
|
18
|
+
export default class PostgresStore extends Store {
|
|
19
|
+
name: string;
|
|
20
|
+
private url;
|
|
21
|
+
private table;
|
|
22
|
+
/** `schema.table` or `table`, each part quoted. */
|
|
23
|
+
private quoted;
|
|
24
|
+
private connection?;
|
|
25
|
+
constructor(logger: winston.Logger, url: string, table: string);
|
|
26
|
+
/** Split out so a test can stand in for a missing package. */
|
|
27
|
+
protected driver(): Promise<Pg>;
|
|
28
|
+
open(): Promise<void>;
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* The pool, once the table is known to be there. A failure is not kept:
|
|
32
|
+
* the next operation tries again, so a database down at startup does not
|
|
33
|
+
* leave the store unusable once it is back.
|
|
34
|
+
*/
|
|
35
|
+
private pool;
|
|
36
|
+
private connect;
|
|
37
|
+
/**
|
|
38
|
+
* Create the table when it is not there.
|
|
39
|
+
*
|
|
40
|
+
* Looked up first: `CREATE … IF NOT EXISTS` still needs the right to create
|
|
41
|
+
* in the schema, which a role granted only this table's rows does not have.
|
|
42
|
+
*/
|
|
43
|
+
private ensureTable;
|
|
44
|
+
/**
|
|
45
|
+
* The namespace and the consumer's own key. PostgreSQL refuses NUL in
|
|
46
|
+
* `text`, so a key holding one cannot be kept: `null` says so.
|
|
47
|
+
*/
|
|
48
|
+
private static parts;
|
|
49
|
+
protected load(key: string): Promise<StoredRecord | null>;
|
|
50
|
+
protected save(key: string, record: StoredRecord): Promise<void>;
|
|
51
|
+
protected drop(key: string): Promise<void>;
|
|
52
|
+
sweep(): Promise<number>;
|
|
53
|
+
}
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/storage/valkey
|
|
3
|
+
*
|
|
4
|
+
* A keyed store kept in Valkey, or Redis.
|
|
5
|
+
*
|
|
6
|
+
* `iovalkey` is an optional dependency, loaded when this backend is chosen: a
|
|
7
|
+
* deployment that keeps its records elsewhere never needs it installed.
|
|
8
|
+
*
|
|
9
|
+
* The only backend that expires its own records, so it has nothing to sweep.
|
|
10
|
+
* The value still carries its deadline, in the shape the LDAP store uses:
|
|
11
|
+
* reads judge expiry by it, as with every other backend, and the TTL only
|
|
12
|
+
* reclaims the memory.
|
|
13
|
+
*
|
|
14
|
+
* @group Plugins
|
|
15
|
+
*/
|
|
16
|
+
import type winston from 'winston';
|
|
17
|
+
import Store, { type StoredRecord } from '../../lib/storage/store';
|
|
18
|
+
type IoValkey = typeof import('iovalkey');
|
|
19
|
+
export default class ValkeyStore extends Store {
|
|
20
|
+
name: string;
|
|
21
|
+
private url;
|
|
22
|
+
private prefix;
|
|
23
|
+
private connection?;
|
|
24
|
+
/** Set once a lost connection is reported, so a reconnect loop says it once. */
|
|
25
|
+
private down;
|
|
26
|
+
constructor(logger: winston.Logger, url: string, prefix: string);
|
|
27
|
+
/** Split out so a test can stand in for a missing package. */
|
|
28
|
+
protected driver(): Promise<IoValkey>;
|
|
29
|
+
open(): Promise<void>;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* The client, once connected. A failed first connection is not kept: the
|
|
33
|
+
* next operation tries again. Past that one, the client reconnects on its
|
|
34
|
+
* own.
|
|
35
|
+
*/
|
|
36
|
+
private client;
|
|
37
|
+
private connect;
|
|
38
|
+
private key;
|
|
39
|
+
protected load(key: string): Promise<StoredRecord | null>;
|
|
40
|
+
protected save(key: string, record: StoredRecord): Promise<void>;
|
|
41
|
+
protected drop(key: string): Promise<void>;
|
|
42
|
+
/** The TTL has already reclaimed what expired. */
|
|
43
|
+
sweep(): Promise<number>;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
@@ -25,6 +25,7 @@ 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/auth/fake](#coreauthfake)
|
|
28
29
|
- [core/bcl](#corebcl)
|
|
29
30
|
- [Authorization Plugins](#authorization-plugins)
|
|
30
31
|
- [Common to every authorization plugin](#common-to-every-authorization-plugin)
|
|
@@ -238,6 +239,14 @@ Empty means the plugin guards every path. Scoping several instances to different
|
|
|
238
239
|
| -------------- | --------------- | ---------------- | ------- | --------------------- |
|
|
239
240
|
| `--auth-token` | `--auth-tokens` | `DM_AUTH_TOKENS` | `[]` | Authentication tokens |
|
|
240
241
|
|
|
242
|
+
#### `core/auth/fake`
|
|
243
|
+
|
|
244
|
+
| CLI | Env | Default | Description |
|
|
245
|
+
| ------------------ | ------------------- | ------- | ---------------------------------------------- |
|
|
246
|
+
| `--auth-fake-user` | `DM_AUTH_FAKE_USER` | | Identity every request is served as (required) |
|
|
247
|
+
|
|
248
|
+
Development only, refused with `NODE_ENV=production` — see [Fake Authentication](plugins/auth/fake.md).
|
|
249
|
+
|
|
241
250
|
#### `core/auth/totp`
|
|
242
251
|
|
|
243
252
|
| CLI | Plural | Env | Default | Description |
|
|
@@ -324,11 +333,12 @@ whichever of the two names it selects. A value other than `req.user` or
|
|
|
324
333
|
|
|
325
334
|
#### `core/auth/authzPerBranch`
|
|
326
335
|
|
|
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
|
|
336
|
+
| CLI | Env | Default | Description |
|
|
337
|
+
| --------------------------------- | ---------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
338
|
+
| `--authz-per-branch-config` | `DM_AUTHZ_PER_BRANCH_CONFIG` | `{default:{read:true,write:false,delete:false}}` | Authorization config (JSON) |
|
|
339
|
+
| `--authz-per-branch-cache-ttl` | `DM_AUTHZ_PER_BRANCH_CACHE_TTL` | `60` | Cache TTL (seconds) |
|
|
340
|
+
| `--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`; always on under `authzLinid1`; does nothing under `authzDynamic`. |
|
|
341
|
+
| `--authz-transit-branch` | `DM_AUTHZ_TRANSIT_BRANCH` | | Organization entries are handed over through when they are judged by attachment: every administrator sees and may claim what is there, and puts an entry there with write on the organization it leaves. An entry attached to no organization is in transit too. See [authzLinid1](plugins/auth/authz-linid1.md#transit-handing-an-entry-over). |
|
|
332
342
|
|
|
333
343
|
#### `core/auth/authzScope`
|
|
334
344
|
|
|
@@ -452,13 +462,17 @@ Web access logging plugin. No configuration options - just add the plugin to ena
|
|
|
452
462
|
Keyed storage with a deadline, for the plugins that need to keep something —
|
|
453
463
|
see [Storage](plugins/utilities/storage.md).
|
|
454
464
|
|
|
455
|
-
| CLI | Env | Default | Description
|
|
456
|
-
| ----------------------------- | ------------------------------ | -------------------- |
|
|
457
|
-
| `--storage-backend` | `DM_STORAGE_BACKEND` | | `ldap` or `
|
|
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
|
|
465
|
+
| CLI | Env | Default | Description |
|
|
466
|
+
| ----------------------------- | ------------------------------ | -------------------- | ------------------------------------------------ |
|
|
467
|
+
| `--storage-backend` | `DM_STORAGE_BACKEND` | | `ldap`, `file`, `postgres` or `valkey`; required |
|
|
468
|
+
| `--storage-sweep-interval` | `DM_STORAGE_SWEEP_INTERVAL` | `600` | Seconds between two expiry passes |
|
|
469
|
+
| `--storage-ldap-base` | `DM_STORAGE_LDAP_BASE` | | Branch the `ldap` backend writes to |
|
|
470
|
+
| `--storage-ldap-object-class` | `DM_STORAGE_LDAP_OBJECT_CLASS` | `applicationProcess` | Object class of the entries it writes |
|
|
471
|
+
| `--storage-file-directory` | `DM_STORAGE_FILE_DIRECTORY` | | Directory the `file` backend writes to |
|
|
472
|
+
| `--storage-postgres-url` | `DM_STORAGE_POSTGRES_URL` | | Connection string of the `postgres` backend |
|
|
473
|
+
| `--storage-postgres-table` | `DM_STORAGE_POSTGRES_TABLE` | `ldap_rest_storage` | Table it writes to |
|
|
474
|
+
| `--storage-valkey-url` | `DM_STORAGE_VALKEY_URL` | | URL of the `valkey` backend |
|
|
475
|
+
| `--storage-valkey-prefix` | `DM_STORAGE_VALKEY_PREFIX` | `ldap-rest:` | Prefix of the keys it writes |
|
|
462
476
|
|
|
463
477
|
#### `core/configApi`
|
|
464
478
|
|
|
@@ -62,11 +62,11 @@ Connect to external systems:
|
|
|
62
62
|
|
|
63
63
|
## Utility Plugins
|
|
64
64
|
|
|
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 |
|
|
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, in LDAP, files, PostgreSQL or Valkey |
|
|
70
70
|
|
|
71
71
|
## Plugin Dependencies
|
|
72
72
|
|
|
@@ -11,6 +11,7 @@ LDAP-Rest provides multiple authentication plugins to secure API access. These p
|
|
|
11
11
|
| [HMAC](hmac.md) | `core/auth/hmac` | HMAC-SHA256 request signing for backend services |
|
|
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
|
+
| [Fake](fake.md) | `core/auth/fake` | Development only: a fixed identity, no login |
|
|
14
15
|
|
|
15
16
|
## What a rule is keyed on
|
|
16
17
|
|
|
@@ -25,6 +26,7 @@ of what a caller is called. Every one of them publishes the caller twice:
|
|
|
25
26
|
| `core/auth/llng` | LLNG's `whatToTrace` | `--llng-username-header`, or the same |
|
|
26
27
|
| `core/auth/openidconnect` | the OIDC `sub` | `--oidc-username-claim` (default: `sub`) |
|
|
27
28
|
| `core/auth/authzDynamic` | the token's tenant | the same |
|
|
29
|
+
| `core/auth/fake` | `--auth-fake-user` | the same |
|
|
28
30
|
|
|
29
31
|
`req.user` is this server's identifier for the caller; `req.userName` is the
|
|
30
32
|
caller under a name a person would use. `--authz-identity` says which one is
|
|
@@ -48,6 +48,54 @@ twakeDepartmentLink: ou=HR,ou=organization,dc=example,dc=com
|
|
|
48
48
|
|
|
49
49
|
The HR admin (listed in `ou=HR`'s `twakeLocalAdminLink`) can manage John even though he's in `ou=users` because his `twakeDepartmentLink` points to the HR organization.
|
|
50
50
|
|
|
51
|
+
### What an administrator reads
|
|
52
|
+
|
|
53
|
+
`authzLinid1` always judges an attached entry this way, as
|
|
54
|
+
`--authz-filter-attached-entries` does for `authzPerBranch`, whatever that
|
|
55
|
+
option says: judged by its parent instead, an account in a flat `ou=users`
|
|
56
|
+
belongs to nobody's branch, and no administrator could list or change one.
|
|
57
|
+
|
|
58
|
+
- Listings of accounts and groups drop the entries attached to an
|
|
59
|
+
organization outside the caller's branches. A single read of one answers
|
|
60
|
+
`404`.
|
|
61
|
+
- Entries attached to nothing — the organizations, the nomenclature values, a
|
|
62
|
+
group or an account with no `twakeDepartmentLink` — are readable by every
|
|
63
|
+
administrator: the organization tree is reference data. Writes to them are
|
|
64
|
+
judged on their parent. The tree a console draws still starts at the
|
|
65
|
+
caller's own branches: see [getOrganisationTop Hook](#getorganisationtop-hook).
|
|
66
|
+
- A caller named in no `twakeLocalAdminLink` is refused every read (`403`)
|
|
67
|
+
but the top organization's own entry, which `organizations/top` answers.
|
|
68
|
+
|
|
69
|
+
### Transit: handing an entry over
|
|
70
|
+
|
|
71
|
+
An administrator can only move an entry between organizations they write, and
|
|
72
|
+
an account created by another tool often carries no `twakeDepartmentLink` at
|
|
73
|
+
all. The transit branch covers both cases: `--authz-transit-branch` names an
|
|
74
|
+
organization through which entries change hands.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
--authz-transit-branch "ou=Transit,ou=organization,dc=example,dc=com"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
An entry is **in transit** when it is attached to that organization, or to
|
|
81
|
+
none (with or without the option).
|
|
82
|
+
|
|
83
|
+
- Every administrator sees the entries in transit.
|
|
84
|
+
- **Claiming** one (`POST /api/v1/ldap/users/:id/move` towards an
|
|
85
|
+
organization) asks for write on that organization only.
|
|
86
|
+
- **Putting** one in transit asks for write on the organization it leaves,
|
|
87
|
+
and nothing on the transit branch: an administrator only lets go of what
|
|
88
|
+
they manage. Handing an account over to an organization one does not manage
|
|
89
|
+
is two steps: put it in transit, then its new administrator claims it.
|
|
90
|
+
- Any other change to an entry in transit (attributes, password, status,
|
|
91
|
+
deletion) is judged as before, so the entry is claimed first.
|
|
92
|
+
|
|
93
|
+
`GET /api/v1/authz/scope` returns the transit branch as `transit` (`null`
|
|
94
|
+
when there is none), so that a client can offer these moves. A group attached
|
|
95
|
+
to the transit branch follows the same rules through
|
|
96
|
+
`POST /api/v1/ldap/groups/:cn/move`; that endpoint does not move a group
|
|
97
|
+
attached to no organization.
|
|
98
|
+
|
|
51
99
|
## Configuration
|
|
52
100
|
|
|
53
101
|
No configuration file needed - permissions are read from LDAP. Simply load the plugin:
|
|
@@ -179,8 +227,9 @@ Intercepts all LDAP search operations:
|
|
|
179
227
|
2. Resolves user's DN from username
|
|
180
228
|
3. Allows base scope searches on top organization (for navigation)
|
|
181
229
|
4. Allows searches for `twakeLocalAdminLink` (for permission refresh)
|
|
182
|
-
5.
|
|
183
|
-
|
|
230
|
+
5. Refuses a caller who administers no branch; lets the others search, and
|
|
231
|
+
drops from the answer the entries attached outside their branches
|
|
232
|
+
(`ldapsearchfilter`)
|
|
184
233
|
|
|
185
234
|
**Special allowances:**
|
|
186
235
|
|
|
@@ -421,7 +470,10 @@ User exists in `ou=users,dc=example,dc=com` but local admin cannot manage them.
|
|
|
421
470
|
twakeDepartmentLink: ou=HR,ou=organization,dc=example,dc=com
|
|
422
471
|
```
|
|
423
472
|
|
|
424
|
-
2. Or
|
|
473
|
+
2. Or let the local admin claim it: an account with no `twakeDepartmentLink`
|
|
474
|
+
is in transit, see [Transit](#transit-handing-an-entry-over).
|
|
475
|
+
|
|
476
|
+
3. Or move user to organization branch:
|
|
425
477
|
```
|
|
426
478
|
From: uid=user,ou=users,dc=example,dc=com
|
|
427
479
|
To: uid=user,ou=users,ou=HR,ou=organization,dc=example,dc=com
|
|
@@ -461,31 +513,6 @@ User jdoe not found in LDAP
|
|
|
461
513
|
|
|
462
514
|
3. Verify authentication plugin sets `req.user` correctly
|
|
463
515
|
|
|
464
|
-
### Problem: Cannot Search Top Organization
|
|
465
|
-
|
|
466
|
-
**Symptoms:**
|
|
467
|
-
|
|
468
|
-
```json
|
|
469
|
-
{
|
|
470
|
-
"error": "User admin does not have read permission for branch ou=organization,dc=example,dc=com"
|
|
471
|
-
}
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
**Solutions:**
|
|
475
|
-
|
|
476
|
-
This is expected behavior - users can only see organizations they manage. To grant access to top organization:
|
|
477
|
-
|
|
478
|
-
```bash
|
|
479
|
-
ldapsearch -x -b "ou=organization,dc=example,dc=com" -s base "(objectClass=*)"
|
|
480
|
-
# Modify to add user:
|
|
481
|
-
ldapmodify <<EOF
|
|
482
|
-
dn: ou=organization,dc=example,dc=com
|
|
483
|
-
changetype: modify
|
|
484
|
-
add: twakeLocalAdminLink
|
|
485
|
-
twakeLocalAdminLink: uid=admin,ou=users,dc=example,dc=com
|
|
486
|
-
EOF
|
|
487
|
-
```
|
|
488
|
-
|
|
489
516
|
## Integration Examples
|
|
490
517
|
|
|
491
518
|
### With Token Authentication
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Fake Authentication
|
|
2
|
+
|
|
3
|
+
For development only: every request is served as the identity given on the
|
|
4
|
+
command line, without asking for anything. Restarting with another
|
|
5
|
+
`--auth-fake-user` shows an interface as another administrator sees it.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx ldap-rest \
|
|
9
|
+
--plugin core/auth/fake --auth-fake-user alice \
|
|
10
|
+
--plugin core/auth/authzLinid1 \
|
|
11
|
+
...
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Environment Variable:** `DM_AUTH_FAKE_USER=alice`
|
|
15
|
+
|
|
16
|
+
The identity goes to `req.user` and `req.userName` alike, so the
|
|
17
|
+
authorization plugins judge it as they would the same login coming from a
|
|
18
|
+
real authenticator.
|
|
19
|
+
|
|
20
|
+
## Safeguards
|
|
21
|
+
|
|
22
|
+
- The server refuses to start when `NODE_ENV` is `production`, as it is in
|
|
23
|
+
the Docker image.
|
|
24
|
+
- It refuses to start without `--auth-fake-user`.
|
|
25
|
+
- A warning naming the identity is logged at every start.
|
|
26
|
+
|
|
27
|
+
Loaded without an authorization plugin, it grants everything to anyone who
|
|
28
|
+
reaches the port. Listen on a local address only.
|
|
@@ -26,13 +26,17 @@ which is invisible in a configuration.
|
|
|
26
26
|
--storage-ldap-base ou=Records,dc=example,dc=com
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
| Option | Environment | Default | Description
|
|
30
|
-
| ----------------------------- | ------------------------------ | -------------------- |
|
|
31
|
-
| `--storage-backend` | `DM_STORAGE_BACKEND` | _(none)_ | `ldap` or `
|
|
32
|
-
| `--storage-sweep-interval` | `DM_STORAGE_SWEEP_INTERVAL` | `600` | Seconds between two passes dropping expired records
|
|
33
|
-
| `--storage-ldap-base` | `DM_STORAGE_LDAP_BASE` | _(none)_ | Branch the `ldap` backend writes to
|
|
34
|
-
| `--storage-ldap-object-class` | `DM_STORAGE_LDAP_OBJECT_CLASS` | `applicationProcess` | Object class of the entries it writes
|
|
35
|
-
| `--storage-file-directory` | `DM_STORAGE_FILE_DIRECTORY` | _(none)_ | Directory the `file` backend writes to
|
|
29
|
+
| Option | Environment | Default | Description |
|
|
30
|
+
| ----------------------------- | ------------------------------ | -------------------- | ------------------------------------------------------------------------------- |
|
|
31
|
+
| `--storage-backend` | `DM_STORAGE_BACKEND` | _(none)_ | `ldap`, `file`, `postgres` or `valkey`. Empty means the plugin refuses to start |
|
|
32
|
+
| `--storage-sweep-interval` | `DM_STORAGE_SWEEP_INTERVAL` | `600` | Seconds between two passes dropping expired records |
|
|
33
|
+
| `--storage-ldap-base` | `DM_STORAGE_LDAP_BASE` | _(none)_ | Branch the `ldap` backend writes to |
|
|
34
|
+
| `--storage-ldap-object-class` | `DM_STORAGE_LDAP_OBJECT_CLASS` | `applicationProcess` | Object class of the entries it writes |
|
|
35
|
+
| `--storage-file-directory` | `DM_STORAGE_FILE_DIRECTORY` | _(none)_ | Directory the `file` backend writes to |
|
|
36
|
+
| `--storage-postgres-url` | `DM_STORAGE_POSTGRES_URL` | _(none)_ | Connection string of the `postgres` backend |
|
|
37
|
+
| `--storage-postgres-table` | `DM_STORAGE_POSTGRES_TABLE` | `ldap_rest_storage` | Table it writes to, `schema.table` accepted |
|
|
38
|
+
| `--storage-valkey-url` | `DM_STORAGE_VALKEY_URL` | _(none)_ | URL of the `valkey` backend (`redis://…` or `rediss://…`) |
|
|
39
|
+
| `--storage-valkey-prefix` | `DM_STORAGE_VALKEY_PREFIX` | `ldap-rest:` | Prefix of the keys it writes |
|
|
36
40
|
|
|
37
41
|
`core/storage` is in the priority list, so it is registered before the
|
|
38
42
|
plugins that consume it whatever order they are written in. A consumer that
|
|
@@ -61,18 +65,65 @@ The directory is the plugin's own: nothing else should be writing there, and
|
|
|
61
65
|
the sweeper removes what it finds expired. It is local to one process, so it
|
|
62
66
|
suits a single instance, not a cluster sharing one verdict.
|
|
63
67
|
|
|
68
|
+
### `postgres`
|
|
69
|
+
|
|
70
|
+
One row per record in `--storage-postgres-table`, with the consumer's
|
|
71
|
+
namespace and key kept verbatim beside the value and an `expires_at` column,
|
|
72
|
+
so a `SELECT` tells what a record is about. Needs the optional `pg` package.
|
|
73
|
+
|
|
74
|
+
The table is created at the first connection when it is not there. A role
|
|
75
|
+
that may not create tables works once it exists:
|
|
76
|
+
|
|
77
|
+
```sql
|
|
78
|
+
CREATE TABLE ldap_rest_storage (
|
|
79
|
+
namespace text NOT NULL,
|
|
80
|
+
key text NOT NULL,
|
|
81
|
+
value text NOT NULL,
|
|
82
|
+
expires_at timestamptz NOT NULL,
|
|
83
|
+
PRIMARY KEY (namespace, key)
|
|
84
|
+
);
|
|
85
|
+
CREATE INDEX ldap_rest_storage_expires_at ON ldap_rest_storage (expires_at);
|
|
86
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ldap_rest_storage TO ldap_rest;
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Several instances can share the table and sweep it at once. PostgreSQL
|
|
90
|
+
refuses a NUL in `text`, so a key holding one cannot be kept: the write
|
|
91
|
+
fails.
|
|
92
|
+
|
|
93
|
+
### `valkey`
|
|
94
|
+
|
|
95
|
+
One key per record, `--storage-valkey-prefix` followed by the namespace and
|
|
96
|
+
the key, expiring with the record: nothing is left to sweep. Works with Redis
|
|
97
|
+
too. Needs the optional `iovalkey` package.
|
|
98
|
+
|
|
99
|
+
**The instance must not evict.** An instance configured as a cache
|
|
100
|
+
(`allkeys-lru`, `volatile-lru` and the like) drops keys under memory pressure,
|
|
101
|
+
and every key this backend writes has a TTL: a Back-Channel Logout mark
|
|
102
|
+
evicted early reopens the session it closed. Use an instance with
|
|
103
|
+
`maxmemory-policy noeviction`, which answers a full memory with a failed
|
|
104
|
+
write — reported to the identity provider — rather than a silent loss.
|
|
105
|
+
|
|
106
|
+
A connection lost while running is retried in the background; meanwhile, reads
|
|
107
|
+
answer "absent" with a warning and writes fail at once instead of waiting.
|
|
108
|
+
|
|
109
|
+
`postgres` and `valkey` connect at startup, and report there an address that
|
|
110
|
+
does not answer or a package that is not installed; the server starts anyway,
|
|
111
|
+
and every operation tries to connect again.
|
|
112
|
+
|
|
64
113
|
## Expiry
|
|
65
114
|
|
|
66
115
|
Two rules the backends do not get to reinterpret:
|
|
67
116
|
|
|
68
117
|
- **Expiry is enforced on read.** A record past its deadline is answered as
|
|
69
118
|
absent whatever the sweeper has done, so a sweeper running late is a
|
|
70
|
-
storage cost, never a wrong answer.
|
|
119
|
+
storage cost, never a wrong answer. Expiry is judged by this server's clock,
|
|
120
|
+
the sweep and the Valkey TTL included, so a database clock running ahead
|
|
121
|
+
cannot drop a record early.
|
|
71
122
|
- **The deadline belongs to the consumer.** How long a record lives is the
|
|
72
123
|
policy of whoever wrote it. The store is told an instant and respects it.
|
|
73
124
|
|
|
74
|
-
Keys are namespaced per consumer, so two of them cannot collide in one branch
|
|
75
|
-
or
|
|
125
|
+
Keys are namespaced per consumer, so two of them cannot collide in one branch,
|
|
126
|
+
directory, table or instance.
|
|
76
127
|
|
|
77
128
|
## For Plugin Authors
|
|
78
129
|
|
package/docs/usage/upgrading.md
CHANGED
|
@@ -4,6 +4,47 @@ What to check before deploying, newest first. Only releases that need a
|
|
|
4
4
|
decision or a configuration change appear here; see the
|
|
5
5
|
[CHANGELOG](../../CHANGELOG.md) for everything else.
|
|
6
6
|
|
|
7
|
+
## To 0.11.0
|
|
8
|
+
|
|
9
|
+
### Group reads are judged
|
|
10
|
+
|
|
11
|
+
**Who is affected:** a deployment loading `core/ldap/groups` with an
|
|
12
|
+
authorization plugin.
|
|
13
|
+
|
|
14
|
+
Listing the groups and reading one went to the directory without the
|
|
15
|
+
request, so no authorization plugin saw them: any authenticated caller read
|
|
16
|
+
every group with its members. They are judged like any other read now.
|
|
17
|
+
Under `core/auth/authzPerBranch` without `--authz-filter-attached-entries`,
|
|
18
|
+
that means a `read` grant on the group base (`ou=groups`, or
|
|
19
|
+
`--ldap-group-base`): **a caller without one is answered `403`** where the
|
|
20
|
+
list used to come back.
|
|
21
|
+
|
|
22
|
+
### authzLinid1 judges accounts and groups by their organization
|
|
23
|
+
|
|
24
|
+
**Who is affected:** every deployment using `core/auth/authzLinid1`.
|
|
25
|
+
|
|
26
|
+
It now works as with `--authz-filter-attached-entries`, whatever the option
|
|
27
|
+
says. Without it, a flat `ou=users` was nobody's branch: no administrator
|
|
28
|
+
could list or change an account.
|
|
29
|
+
|
|
30
|
+
- An administrator lists the accounts and groups attached to their branches,
|
|
31
|
+
and those attached to none.
|
|
32
|
+
- **The organization tree is readable by every administrator**, not only their
|
|
33
|
+
own branches: an organization is reference data. `organizations/top` still
|
|
34
|
+
answers their own branches, so a console starts there. Writes to the tree
|
|
35
|
+
are judged as before.
|
|
36
|
+
- **A caller named in no `twakeLocalAdminLink` is refused every read** but the
|
|
37
|
+
top organization's own entry.
|
|
38
|
+
- An account stored under the organization tree without a
|
|
39
|
+
`twakeDepartmentLink` becomes readable by every administrator. **Where
|
|
40
|
+
accounts sit under the tree, give them their link before upgrading.** This
|
|
41
|
+
lists the accounts that have none, under `twakeDepartmentLink: missing`:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run audit:directory -- --schema static/schemas/twake/users.json \
|
|
45
|
+
--base ou=users,<base> --filter '(!(twakeDepartmentLink=*))'
|
|
46
|
+
```
|
|
47
|
+
|
|
7
48
|
## To 0.10.0
|
|
8
49
|
|
|
9
50
|
### LDAP base is now required
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ldap-rest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Light directory manager",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Identity-Manager"
|
|
@@ -64,6 +64,10 @@
|
|
|
64
64
|
"types": "./dist/src/plugins/auth/crowdsec.d.ts",
|
|
65
65
|
"import": "./dist/plugins/auth/crowdsec.js"
|
|
66
66
|
},
|
|
67
|
+
"./plugin-auth-fake": {
|
|
68
|
+
"types": "./dist/src/plugins/auth/fake.d.ts",
|
|
69
|
+
"import": "./dist/plugins/auth/fake.js"
|
|
70
|
+
},
|
|
67
71
|
"./plugin-auth-hmac": {
|
|
68
72
|
"types": "./dist/src/plugins/auth/hmac.d.ts",
|
|
69
73
|
"import": "./dist/plugins/auth/hmac.js"
|
|
@@ -228,6 +232,14 @@
|
|
|
228
232
|
"types": "./dist/src/plugins/storage/ldap.d.ts",
|
|
229
233
|
"import": "./dist/plugins/storage/ldap.js"
|
|
230
234
|
},
|
|
235
|
+
"./plugin-storage-postgres": {
|
|
236
|
+
"types": "./dist/src/plugins/storage/postgres.d.ts",
|
|
237
|
+
"import": "./dist/plugins/storage/postgres.js"
|
|
238
|
+
},
|
|
239
|
+
"./plugin-storage-valkey": {
|
|
240
|
+
"types": "./dist/src/plugins/storage/valkey.d.ts",
|
|
241
|
+
"import": "./dist/plugins/storage/valkey.js"
|
|
242
|
+
},
|
|
231
243
|
"./plugin-twake-appaccountsapi": {
|
|
232
244
|
"types": "./dist/src/plugins/twake/appAccountsApi.d.ts",
|
|
233
245
|
"import": "./dist/plugins/twake/appAccountsApi.js"
|
|
@@ -723,6 +735,7 @@
|
|
|
723
735
|
"@types/mocha": "^10.0.10",
|
|
724
736
|
"@types/multer": "^2.0.0",
|
|
725
737
|
"@types/node": "^24.5.1",
|
|
738
|
+
"@types/pg": "^8.23.1",
|
|
726
739
|
"@types/supertest": "^6.0.3",
|
|
727
740
|
"@typescript-eslint/eslint-plugin": "^8.44.0",
|
|
728
741
|
"@typescript-eslint/parser": "^8.44.0",
|
|
@@ -751,7 +764,9 @@
|
|
|
751
764
|
"optionalDependencies": {
|
|
752
765
|
"@linagora/rabbitmq-client": "^0.1.2",
|
|
753
766
|
"express-openid-connect": "^2.19.2",
|
|
767
|
+
"iovalkey": "^0.4.0",
|
|
754
768
|
"lemonldap-ng-handler": "^0.7.3",
|
|
769
|
+
"pg": "^8.23.0",
|
|
755
770
|
"re2": "^1.22.1"
|
|
756
771
|
},
|
|
757
772
|
"engines": {
|