ldap-rest 0.2.2 → 0.3.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/expressFormatedResponses.js +1 -1
- package/dist/plugins/auth/authzDynamic.js +1 -0
- package/dist/plugins/auth/authzDynamicHash.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 +20 -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/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 +23 -0
- package/dist/src/plugins/scim/types.d.ts +222 -0
- package/dist/src/plugins/scim/users.d.ts +58 -0
- package/docs/usage/plugins/auth/authz-dynamic.md +183 -0
- package/docs/usage/plugins/integrations/scim.md +567 -0
- package/package.json +65 -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
|
@@ -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
|
+
```
|