ldap-rest 0.2.2 → 0.3.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/README.md +1 -1
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/plugins/auth/authzDynamic.js +1 -0
- package/dist/plugins/auth/authzDynamicHash.js +1 -0
- package/dist/plugins/ldap/organization.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -0
- package/dist/plugins/scim/baseResolver.js +1 -0
- package/dist/plugins/scim/bulk.js +1 -0
- package/dist/plugins/scim/discovery.js +1 -0
- package/dist/plugins/scim/errors.js +1 -0
- package/dist/plugins/scim/filter.js +1 -0
- package/dist/plugins/scim/groups.js +1 -0
- package/dist/plugins/scim/mapping.js +1 -0
- package/dist/plugins/scim/patch.js +1 -0
- package/dist/plugins/scim/scim.js +1 -0
- package/dist/plugins/scim/types.js +1 -0
- package/dist/plugins/scim/users.js +1 -0
- package/dist/src/abstract/ldapFlat.d.ts +127 -0
- package/dist/src/config/args.d.ts +24 -0
- package/dist/src/plugins/auth/authzDynamic.d.ts +117 -0
- package/dist/src/plugins/auth/authzDynamicHash.d.ts +27 -0
- package/dist/src/plugins/configApi.d.ts +51 -0
- package/dist/src/plugins/demo/helloworld.d.ts +21 -0
- package/dist/src/plugins/ldap/bulkImport.d.ts +65 -0
- package/dist/src/plugins/ldap/groups.d.ts +68 -0
- package/dist/src/plugins/ldap/organization.d.ts +8 -65
- package/dist/src/plugins/ldap/organizations.d.ts +155 -0
- package/dist/src/plugins/ldap/passwordPolicy.d.ts +178 -0
- package/dist/src/plugins/scim/baseResolver.d.ts +23 -0
- package/dist/src/plugins/scim/bulk.d.ts +43 -0
- package/dist/src/plugins/scim/discovery.d.ts +27 -0
- package/dist/src/plugins/scim/errors.d.ts +37 -0
- package/dist/src/plugins/scim/filter.d.ts +13 -0
- package/dist/src/plugins/scim/groups.d.ts +61 -0
- package/dist/src/plugins/scim/mapping.d.ts +52 -0
- package/dist/src/plugins/scim/patch.d.ts +39 -0
- package/dist/src/plugins/scim/scim.d.ts +412 -0
- package/dist/src/plugins/scim/types.d.ts +222 -0
- package/dist/src/plugins/scim/users.d.ts +58 -0
- package/dist/src/plugins/static.d.ts +33 -0
- package/dist/src/plugins/twake/appAccountsApi.d.ts +63 -0
- package/dist/src/plugins/twake/james.d.ts +47 -0
- package/docs/plugin-development/README.md +2 -1
- package/docs/plugin-development/openapi.md +144 -0
- package/docs/usage/plugins/auth/authz-dynamic.md +183 -0
- package/docs/usage/plugins/integrations/scim.md +567 -0
- package/package.json +72 -1
- package/static/schemas/scim/Group.json +31 -0
- package/static/schemas/scim/User.json +147 -0
- package/static/schemas/scim/default-mapping.json +48 -0
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
import type { Express } from 'express';
|
|
2
2
|
import DmPlugin, { type Role } from '../abstract/plugin';
|
|
3
|
+
/**
|
|
4
|
+
* @openapi-component
|
|
5
|
+
* LdapSchema:
|
|
6
|
+
* type: object
|
|
7
|
+
* description: |
|
|
8
|
+
* A JSON schema file served from the `schemas/` sub-directory of the
|
|
9
|
+
* static path. These schemas describe LDAP entity types (users, groups,
|
|
10
|
+
* organizations, …) and are consumed by browser-embedded editors to
|
|
11
|
+
* render attribute forms and validate input before submission.
|
|
12
|
+
*
|
|
13
|
+
* The server replaces `__FOO_BAR__` placeholders on the fly with the
|
|
14
|
+
* value of the corresponding `--foo-bar` CLI option so that a single
|
|
15
|
+
* schema file can be shared between server-side validation and
|
|
16
|
+
* client-side rendering.
|
|
17
|
+
* additionalProperties: true
|
|
18
|
+
* example:
|
|
19
|
+
* entity:
|
|
20
|
+
* name: standardUser
|
|
21
|
+
* mainAttribute: uid
|
|
22
|
+
* objectClass: [top, inetOrgPerson]
|
|
23
|
+
* singularName: user
|
|
24
|
+
* pluralName: users
|
|
25
|
+
* base: ou=users,dc=example,dc=com
|
|
26
|
+
* strict: true
|
|
27
|
+
* attributes:
|
|
28
|
+
* uid:
|
|
29
|
+
* type: string
|
|
30
|
+
* required: true
|
|
31
|
+
* role: identifier
|
|
32
|
+
* cn:
|
|
33
|
+
* type: string
|
|
34
|
+
* required: true
|
|
35
|
+
*/
|
|
3
36
|
export default class Static extends DmPlugin {
|
|
4
37
|
name: string;
|
|
5
38
|
roles: Role[];
|
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
import type { Express } from 'express';
|
|
2
2
|
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
3
3
|
import type { DM } from '../../bin';
|
|
4
|
+
/**
|
|
5
|
+
* @openapi-component
|
|
6
|
+
* AppAccount:
|
|
7
|
+
* type: object
|
|
8
|
+
* description: |
|
|
9
|
+
* An application-specific identity attached to a user. App accounts
|
|
10
|
+
* allow a single principal user to authenticate as different named
|
|
11
|
+
* "devices" or "applications" each with its own credential, while
|
|
12
|
+
* sharing the same mail address.
|
|
13
|
+
* required: [uid]
|
|
14
|
+
* properties:
|
|
15
|
+
* uid:
|
|
16
|
+
* type: string
|
|
17
|
+
* description: |
|
|
18
|
+
* Unique identifier of the app account. Format:
|
|
19
|
+
* `<username>_c<8-digits>` (e.g. `alice_c04729183`).
|
|
20
|
+
* example: alice_c04729183
|
|
21
|
+
* name:
|
|
22
|
+
* type: string
|
|
23
|
+
* description: Human-readable label (stored as LDAP `description`).
|
|
24
|
+
* example: Work laptop
|
|
25
|
+
* example:
|
|
26
|
+
* uid: alice_c04729183
|
|
27
|
+
* name: Work laptop
|
|
28
|
+
* AppAccountCreated:
|
|
29
|
+
* type: object
|
|
30
|
+
* description: |
|
|
31
|
+
* Response returned when a new app account is successfully created.
|
|
32
|
+
* The one-time cleartext password is included here and never returned
|
|
33
|
+
* again — callers must store it immediately.
|
|
34
|
+
* required: [uid, pwd, mail]
|
|
35
|
+
* properties:
|
|
36
|
+
* uid:
|
|
37
|
+
* type: string
|
|
38
|
+
* description: Identifier of the newly created app account.
|
|
39
|
+
* example: alice_c04729183
|
|
40
|
+
* pwd:
|
|
41
|
+
* type: string
|
|
42
|
+
* description: |
|
|
43
|
+
* One-time cleartext password generated for this account.
|
|
44
|
+
* Format: 6 blocks of 4 characters separated by `-`
|
|
45
|
+
* (e.g. `Ab3@-xYz!-9pQ#-Sv4$-mN8!-pQ5@`).
|
|
46
|
+
* OpenLDAP hashes it via ppolicy before storage.
|
|
47
|
+
* example: Ab3@-xYz!-9pQ#-Sv4$-mN8!-pQ5@
|
|
48
|
+
* mail:
|
|
49
|
+
* type: string
|
|
50
|
+
* description: Mail address of the principal account this app account belongs to.
|
|
51
|
+
* example: alice@example.com
|
|
52
|
+
* example:
|
|
53
|
+
* uid: alice_c04729183
|
|
54
|
+
* pwd: Ab3@-xYz!-9pQ#-Sv4$-mN8!-pQ5@
|
|
55
|
+
* mail: alice@example.com
|
|
56
|
+
* AppAccountCreate:
|
|
57
|
+
* type: object
|
|
58
|
+
* description: Request body for creating a new app account.
|
|
59
|
+
* properties:
|
|
60
|
+
* name:
|
|
61
|
+
* type: string
|
|
62
|
+
* description: Optional human-readable label for the new account.
|
|
63
|
+
* example: Work laptop
|
|
64
|
+
* example:
|
|
65
|
+
* name: Work laptop
|
|
66
|
+
*/
|
|
4
67
|
export default class AppAccountsApi extends DmPlugin {
|
|
5
68
|
name: string;
|
|
6
69
|
roles: Role[];
|
|
@@ -5,6 +5,53 @@ import type { DM } from '../../bin';
|
|
|
5
5
|
import type { AttributeValue } from '../../lib/ldapActions';
|
|
6
6
|
import { Hooks } from '../../hooks';
|
|
7
7
|
import type { ChangesToNotify } from '../ldap/onChange';
|
|
8
|
+
/**
|
|
9
|
+
* OpenAPI schemas specific to the James integration.
|
|
10
|
+
*
|
|
11
|
+
* @openapi-component
|
|
12
|
+
* JamesQuotaUsage:
|
|
13
|
+
* type: object
|
|
14
|
+
* description: |
|
|
15
|
+
* Quota information for a mailbox user as returned by the James
|
|
16
|
+
* WebAdmin API (`GET /quota/users/{mail}`). Contains separate
|
|
17
|
+
* counters for message count and storage size.
|
|
18
|
+
* properties:
|
|
19
|
+
* count:
|
|
20
|
+
* type: object
|
|
21
|
+
* description: Message-count quota.
|
|
22
|
+
* properties:
|
|
23
|
+
* used:
|
|
24
|
+
* type: integer
|
|
25
|
+
* description: Number of messages currently stored.
|
|
26
|
+
* example: 142
|
|
27
|
+
* max:
|
|
28
|
+
* type: integer
|
|
29
|
+
* nullable: true
|
|
30
|
+
* description: |
|
|
31
|
+
* Maximum number of messages allowed. `null` means unlimited.
|
|
32
|
+
* example: 10000
|
|
33
|
+
* size:
|
|
34
|
+
* type: object
|
|
35
|
+
* description: Storage-size quota (bytes).
|
|
36
|
+
* properties:
|
|
37
|
+
* used:
|
|
38
|
+
* type: integer
|
|
39
|
+
* description: Bytes currently used.
|
|
40
|
+
* example: 524288000
|
|
41
|
+
* max:
|
|
42
|
+
* type: integer
|
|
43
|
+
* nullable: true
|
|
44
|
+
* description: |
|
|
45
|
+
* Maximum bytes allowed. `null` means unlimited.
|
|
46
|
+
* example: 2147483648
|
|
47
|
+
* example:
|
|
48
|
+
* count:
|
|
49
|
+
* used: 142
|
|
50
|
+
* max: 10000
|
|
51
|
+
* size:
|
|
52
|
+
* used: 524288000
|
|
53
|
+
* max: 2147483648
|
|
54
|
+
*/
|
|
8
55
|
export default class James extends TwakePlugin {
|
|
9
56
|
name: string;
|
|
10
57
|
roles: Role[];
|
|
@@ -13,6 +13,7 @@ This guide will help you create powerful, production-ready plugins for LDAP-Rest
|
|
|
13
13
|
- [Configuration Best Practices](#configuration-best-practices)
|
|
14
14
|
- [Complete Plugin Example](#complete-plugin-example)
|
|
15
15
|
- [Testing Your Plugin](#testing-your-plugin)
|
|
16
|
+
- [Documenting REST endpoints (OpenAPI)](./openapi.md)
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
@@ -1282,7 +1283,7 @@ source ~/.test-env && npm run test:dev
|
|
|
1282
1283
|
## Additional Resources
|
|
1283
1284
|
|
|
1284
1285
|
- [Contributing Guide](../../CONTRIBUTING.md) - Git workflow and PR guidelines
|
|
1285
|
-
- [Hooks Reference](
|
|
1286
|
+
- [Hooks Reference](./hooks.md) - Complete list of available hooks
|
|
1286
1287
|
- [Core Plugin Examples](../../src/plugins/) - Browse existing plugins
|
|
1287
1288
|
- [Test Examples](../../test/plugins/) - See how core plugins are tested
|
|
1288
1289
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Documenting plugin endpoints (OpenAPI)
|
|
2
|
+
|
|
3
|
+
The published API reference at <https://linagora.github.io/ldap-rest/api/> is
|
|
4
|
+
generated from `openapi.json`, which itself is built statically by
|
|
5
|
+
[`scripts/generate-openapi.ts`](../../scripts/generate-openapi.ts) — it walks
|
|
6
|
+
every plugin's `api()` method and reads the JSDoc comments above each route.
|
|
7
|
+
|
|
8
|
+
You don't need to touch the generator to enrich your plugin's documentation.
|
|
9
|
+
Two JSDoc directives drive the entire pipeline:
|
|
10
|
+
|
|
11
|
+
- `@openapi` — placed **just above an `app.method(path, handler)` call**,
|
|
12
|
+
describes that single route.
|
|
13
|
+
- `@openapi-component` — placed anywhere in the file, registers reusable
|
|
14
|
+
schemas under `components.schemas` so multiple routes can `$ref` them.
|
|
15
|
+
|
|
16
|
+
Both directives carry inline **YAML** that follows the
|
|
17
|
+
[OpenAPI 3.0 Operation Object](https://spec.openapis.org/oas/v3.0.3#operation-object)
|
|
18
|
+
(or [Schema Object](https://spec.openapis.org/oas/v3.0.3#schema-object) for
|
|
19
|
+
components). The generator parses the YAML and merges it on top of the
|
|
20
|
+
defaults it would have emitted, so you only describe what you want to enrich.
|
|
21
|
+
|
|
22
|
+
## Per-route metadata: `@openapi`
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
/**
|
|
26
|
+
* @openapi
|
|
27
|
+
* summary: Get group by CN
|
|
28
|
+
* description: |
|
|
29
|
+
* The `:cn` segment may be either the group's RDN value or its full DN.
|
|
30
|
+
* Returns the raw LDAP entry.
|
|
31
|
+
* responses:
|
|
32
|
+
* '200':
|
|
33
|
+
* description: Group entry.
|
|
34
|
+
* content:
|
|
35
|
+
* application/json:
|
|
36
|
+
* schema: { $ref: '#/components/schemas/Group' }
|
|
37
|
+
* example:
|
|
38
|
+
* dn: cn=admins,ou=groups,dc=example,dc=com
|
|
39
|
+
* cn: admins
|
|
40
|
+
* description: Server administrators
|
|
41
|
+
* '404':
|
|
42
|
+
* description: Group not found.
|
|
43
|
+
*/
|
|
44
|
+
app.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`, asyncHandler(...));
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Anything below the `@openapi` line (up to the next `@directive` or the end of
|
|
48
|
+
the JSDoc block) is YAML. The keys you provide override the auto-generated
|
|
49
|
+
ones; everything you omit keeps the generator's defaults.
|
|
50
|
+
|
|
51
|
+
### What you can override
|
|
52
|
+
|
|
53
|
+
Any field of an OpenAPI Operation Object, including:
|
|
54
|
+
|
|
55
|
+
| Field | Notes |
|
|
56
|
+
| ------------- | -------------------------------------------------------------------------------- |
|
|
57
|
+
| `summary` | Short, one-line title. |
|
|
58
|
+
| `description` | Long description (markdown). |
|
|
59
|
+
| `tags` | Array of strings; defaults to the plugin's tag. |
|
|
60
|
+
| `parameters` | Concatenated with auto-generated path params (your entries win on duplicates). |
|
|
61
|
+
| `requestBody` | Replace the generic `application/json` placeholder with a real schema + example. |
|
|
62
|
+
| `responses` | Replace per status code. |
|
|
63
|
+
| `security` | List of security requirement objects (works as-is). |
|
|
64
|
+
| `deprecated` | `true` to mark the operation as deprecated. |
|
|
65
|
+
| `operationId` | Stable identifier for code generators. |
|
|
66
|
+
|
|
67
|
+
### What is autodetected
|
|
68
|
+
|
|
69
|
+
The generator already extracts:
|
|
70
|
+
|
|
71
|
+
- the **HTTP method** (`get`, `post`, …) from the call expression;
|
|
72
|
+
- the **path** (substituting local `const prefix = ...` variables, the
|
|
73
|
+
`${this.config.api_prefix}` template, and a few well-known config knobs);
|
|
74
|
+
- **path parameters** from `:param` segments;
|
|
75
|
+
- a default **summary** built from the verb and the last path segment;
|
|
76
|
+
- the plugin's **tag** (configured in `pluginTags` in the generator).
|
|
77
|
+
|
|
78
|
+
So a minimal, correct `@openapi` block is often only two or three keys.
|
|
79
|
+
|
|
80
|
+
## Reusable schemas: `@openapi-component`
|
|
81
|
+
|
|
82
|
+
Place this anywhere in your plugin file (the convention is right above the
|
|
83
|
+
class). The body is a YAML map of `<SchemaName>: <SchemaDefinition>` pairs:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
/**
|
|
87
|
+
* @openapi-component
|
|
88
|
+
* Group:
|
|
89
|
+
* type: object
|
|
90
|
+
* required: [dn, cn]
|
|
91
|
+
* properties:
|
|
92
|
+
* dn: { type: string, example: cn=admins,ou=groups,dc=example,dc=com }
|
|
93
|
+
* cn: { type: string, example: admins }
|
|
94
|
+
* member:
|
|
95
|
+
* type: array
|
|
96
|
+
* items: { type: string }
|
|
97
|
+
* Error:
|
|
98
|
+
* type: object
|
|
99
|
+
* properties:
|
|
100
|
+
* error: { type: string }
|
|
101
|
+
* code: { type: integer }
|
|
102
|
+
*/
|
|
103
|
+
export default class LdapGroups extends DmPlugin {
|
|
104
|
+
// ...
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Refer to them from any route using `$ref`:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
schema: { $ref: '#/components/schemas/Group' }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Component names are global to the spec, so prefix them with the plugin's
|
|
115
|
+
domain (`Group`, `ScimUser`, `PasswordPolicy`) to avoid collisions.
|
|
116
|
+
|
|
117
|
+
## Tips
|
|
118
|
+
|
|
119
|
+
- **Examples beat specs.** A single concrete `example:` block is worth a
|
|
120
|
+
dozen `description:` lines for someone trying the API for the first time.
|
|
121
|
+
- **YAML alignment.** The generator strips the leading `*` from each line
|
|
122
|
+
before parsing, then dedents the block, so you can use any consistent
|
|
123
|
+
indentation as long as it's even within the block.
|
|
124
|
+
- **Multiline strings.** Use YAML block scalars (`description: |`) for
|
|
125
|
+
paragraphs and code samples — they round-trip cleanly through Redoc.
|
|
126
|
+
- **Annotation is opt-in.** Routes without an `@openapi` block are
|
|
127
|
+
**excluded** from the published spec — the generator prints a
|
|
128
|
+
`⚠️ Skipping undocumented route` warning so the author notices, but
|
|
129
|
+
the route does not surface in Redoc. The published doc therefore
|
|
130
|
+
reflects intentionally-documented API surface only, not every Express
|
|
131
|
+
call we happened to find.
|
|
132
|
+
- **A plugin with zero annotated routes disappears entirely** from the
|
|
133
|
+
spec, by the same rule.
|
|
134
|
+
|
|
135
|
+
## Regenerate locally
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
npm run generate:openapi # writes openapi.json
|
|
139
|
+
npm run build:pages # rebuilds the static site under _site/
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Open `_site/api/index.html` to preview the Redoc rendering. The published
|
|
143
|
+
site at <https://linagora.github.io/ldap-rest/api/> is rebuilt automatically
|
|
144
|
+
on every `v*` tag push.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# authzDynamic — LDAP-backed tokens + per-branch authorization
|
|
2
|
+
|
|
3
|
+
The `authzDynamic` plugin combines **bearer-token authentication** and
|
|
4
|
+
**per-branch authorization** in a single plugin, sourced entirely from a
|
|
5
|
+
dedicated LDAP branch. Adding, rotating, or revoking a token — or changing its
|
|
6
|
+
ACL — is a matter of writing to LDAP: no process restart, no config file.
|
|
7
|
+
|
|
8
|
+
This is the recommended building block for multi-tenant deployments where
|
|
9
|
+
each SCIM client (or REST API consumer) is scoped to its own subtree.
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
1. The plugin loads all entries under `--authz-dynamic-base` and caches them.
|
|
14
|
+
2. On every incoming request, it reads `Authorization: Bearer <token>` and
|
|
15
|
+
verifies the token against the `userPassword` hash of each cached entry
|
|
16
|
+
(timing-safe comparison).
|
|
17
|
+
3. On a match, the plugin sets `req.user` to the tenant name and records the
|
|
18
|
+
token's ACL in an `AsyncLocalStorage` context.
|
|
19
|
+
4. Downstream LDAP operations (via `ldapActions.search / add / modify / delete / rename`) trigger authz hooks that read the active token from the async context and throw if the requested DN is outside the allowed branches.
|
|
20
|
+
5. The cache refreshes on a TTL (default 60 s) or on demand via a protected
|
|
21
|
+
reload endpoint.
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
node lib/bin/index.js \
|
|
27
|
+
--ldap-url ldap://ldap.internal \
|
|
28
|
+
--ldap-base dc=example,dc=com \
|
|
29
|
+
--ldap-dn cn=admin,dc=example,dc=com --ldap-pwd '***' \
|
|
30
|
+
\
|
|
31
|
+
--plugin core/auth/authzDynamic \
|
|
32
|
+
--authz-dynamic-base ou=authz-tokens,dc=example,dc=com \
|
|
33
|
+
--authz-dynamic-cache-ttl 60 \
|
|
34
|
+
--authz-dynamic-reload-endpoint
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### CLI / environment
|
|
38
|
+
|
|
39
|
+
| Argument | Environment | Default | Purpose |
|
|
40
|
+
| ---------------------------------- | ----------------------------------- | -------------- | --------------------------------------------------------------------- |
|
|
41
|
+
| `--authz-dynamic-base` | `DM_AUTHZ_DYNAMIC_BASE` | — (required) | LDAP branch that contains the token entries |
|
|
42
|
+
| `--authz-dynamic-cache-ttl` | `DM_AUTHZ_DYNAMIC_CACHE_TTL` | `60` | Cache refresh interval in seconds |
|
|
43
|
+
| `--authz-dynamic-token-attribute` | `DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE` | `userPassword` | Attribute holding the hashed bearer token |
|
|
44
|
+
| `--authz-dynamic-config-attribute` | `DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE` | `description` | Attribute holding the JSON ACL document |
|
|
45
|
+
| `--authz-dynamic-tenant-attribute` | `DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE` | `cn` | Attribute from which `req.user` is read |
|
|
46
|
+
| `--authz-dynamic-reload-endpoint` | `DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT` | `false` | Register `POST /api/v1/authz-dynamic/reload` for manual cache refresh |
|
|
47
|
+
|
|
48
|
+
## Token entry shape
|
|
49
|
+
|
|
50
|
+
Each token is a plain LDAP entry under `--authz-dynamic-base`. Minimal
|
|
51
|
+
requirements: `cn` (token identifier), `userPassword` (the hashed secret),
|
|
52
|
+
and a JSON ACL document in `description` (by default — override via
|
|
53
|
+
`--authz-dynamic-config-attribute`).
|
|
54
|
+
|
|
55
|
+
```ldif
|
|
56
|
+
dn: cn=acme,ou=authz-tokens,dc=example,dc=com
|
|
57
|
+
objectClass: top
|
|
58
|
+
objectClass: inetOrgPerson
|
|
59
|
+
cn: acme
|
|
60
|
+
sn: acme
|
|
61
|
+
userPassword: {SSHA}eFt2wP4ykczHzV9pzE3CH3k1t9M=
|
|
62
|
+
description: {
|
|
63
|
+
"tenant": "acme",
|
|
64
|
+
"bases": [
|
|
65
|
+
{ "dn": "ou=users,ou=acme,dc=example,dc=com", "read": true, "write": true, "delete": true },
|
|
66
|
+
{ "dn": "ou=groups,ou=acme,dc=example,dc=com", "read": true, "write": true, "delete": true }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### ACL JSON
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"tenant": "acme",
|
|
76
|
+
"bases": [
|
|
77
|
+
{ "dn": "<branch DN>", "read": true, "write": true, "delete": true }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- **Sub-branch matching**: a permission on `ou=acme,dc=example,dc=com`
|
|
83
|
+
automatically covers `ou=users,ou=acme,…` and any sub-branch.
|
|
84
|
+
- Absent flags default to `false`.
|
|
85
|
+
- `tenant` is optional; if omitted, `req.user` falls back to the value of the
|
|
86
|
+
configured `tenantAttribute` (default `cn`).
|
|
87
|
+
|
|
88
|
+
### Supported userPassword schemes
|
|
89
|
+
|
|
90
|
+
Handled by a constant-time verifier (`authzDynamicHash.ts`):
|
|
91
|
+
|
|
92
|
+
| Scheme | Notes |
|
|
93
|
+
| ------------------------------------- | ---------------------------------------------------- |
|
|
94
|
+
| `{SSHA}` | salted SHA-1 — OpenLDAP default, recommended minimum |
|
|
95
|
+
| `{SHA}` | unsalted SHA-1 — legacy |
|
|
96
|
+
| `{SSHA256}` / `{SHA256}` | salted / unsalted SHA-256 |
|
|
97
|
+
| `{SSHA512}` / `{SHA512}` | salted / unsalted SHA-512 |
|
|
98
|
+
| `{SMD5}` / `{MD5}` | salted / unsalted MD5 — legacy, avoid |
|
|
99
|
+
| `{CLEARTEXT}` / `{PLAIN}` / no prefix | cleartext — test environments only |
|
|
100
|
+
|
|
101
|
+
To generate an `{SSHA}` hash with OpenLDAP tools: `slappasswd -h '{SSHA}'`.
|
|
102
|
+
To generate programmatically, `ssha(token)` is also exported from
|
|
103
|
+
`authzDynamicHash.ts` for scripts.
|
|
104
|
+
|
|
105
|
+
## Usage with SCIM
|
|
106
|
+
|
|
107
|
+
This is the combination the plugin was designed for:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
node lib/bin/index.js \
|
|
111
|
+
--ldap-base dc=example,dc=com \
|
|
112
|
+
--ldap-dn cn=admin,dc=example,dc=com --ldap-pwd '***' \
|
|
113
|
+
\
|
|
114
|
+
--plugin core/auth/authzDynamic \
|
|
115
|
+
--authz-dynamic-base ou=authz-tokens,dc=example,dc=com \
|
|
116
|
+
\
|
|
117
|
+
--plugin core/scim \
|
|
118
|
+
--scim-user-base-template 'ou=users,ou={user},dc=example,dc=com' \
|
|
119
|
+
--scim-group-base-template 'ou=groups,ou={user},dc=example,dc=com'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Each token's `tenant` field populates `req.user`, which the SCIM plugin uses
|
|
123
|
+
to resolve its LDAP bases via the `{user}` template. Combined with the token's
|
|
124
|
+
ACL, the client is cryptographically constrained to its own SCIM subtree.
|
|
125
|
+
|
|
126
|
+
## Operational notes
|
|
127
|
+
|
|
128
|
+
- **Cache**: read entries are kept in memory. A failed reload (LDAP hiccup)
|
|
129
|
+
keeps the previous snapshot in use — no request is ever served with an
|
|
130
|
+
empty cache unless the very first load fails.
|
|
131
|
+
- **Reload endpoint**: when `--authz-dynamic-reload-endpoint` is set, any
|
|
132
|
+
**already-authenticated** token can call `POST /api/v1/authz-dynamic/reload`
|
|
133
|
+
to force an immediate refresh. Useful after provisioning a new token.
|
|
134
|
+
- **Deletion**: deleting a token entry from LDAP invalidates it within
|
|
135
|
+
`--authz-dynamic-cache-ttl` seconds (or immediately if a reload is
|
|
136
|
+
triggered). Rotate compromised secrets this way.
|
|
137
|
+
- **Audit**: every unauthorized attempt is logged at `warn` with a masked
|
|
138
|
+
token (first 8 chars + `...`). Every authorization failure is logged at
|
|
139
|
+
`error` via the DM error middleware.
|
|
140
|
+
- **configApi**: when `core/configApi` is loaded, the plugin publishes a
|
|
141
|
+
compact status under `features.authzDynamic` (token count, base DN, cache
|
|
142
|
+
TTL, reload endpoint URL) — never hashes or per-token DNs.
|
|
143
|
+
|
|
144
|
+
## Security considerations
|
|
145
|
+
|
|
146
|
+
- **Secret storage**: always hash `userPassword` (`{SSHA}` or better). The
|
|
147
|
+
plugin refuses unknown scheme prefixes, but accepts cleartext — do not
|
|
148
|
+
deploy cleartext secrets in production.
|
|
149
|
+
- **Timing**: comparisons run through `crypto.timingSafeEqual` or a buffer
|
|
150
|
+
wrapper, preventing timing oracles on the secret.
|
|
151
|
+
- **Scope escape**: the authz hooks check every LDAP operation's DN against
|
|
152
|
+
the token's ACL. The active token is carried via `AsyncLocalStorage`, so it
|
|
153
|
+
applies even to plugins that do not thread `req` down to `ldapActions`.
|
|
154
|
+
- **Binding credentials**: the DM bind user still has admin access — the
|
|
155
|
+
plugin's isolation is logical (path prefix check), not LDAP-layer ACL. For
|
|
156
|
+
a second layer of defence, configure OpenLDAP ACLs to match.
|
|
157
|
+
|
|
158
|
+
## Example: listing a tenant's configuration
|
|
159
|
+
|
|
160
|
+
With `core/configApi` loaded:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
curl -H 'Authorization: Bearer <token>' http://localhost:8081/api/v1/config
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The response contains (trimmed):
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"features": {
|
|
171
|
+
"authzDynamic": {
|
|
172
|
+
"enabled": true,
|
|
173
|
+
"base": "ou=authz-tokens,dc=example,dc=com",
|
|
174
|
+
"cacheTtlSeconds": 60,
|
|
175
|
+
"tokenCount": 17,
|
|
176
|
+
"tokenAttribute": "userPassword",
|
|
177
|
+
"configAttribute": "description",
|
|
178
|
+
"tenantAttribute": "cn",
|
|
179
|
+
"reloadEndpoint": "/api/v1/authz-dynamic/reload"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|