ldap-rest 0.2.1 → 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.
Files changed (41) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/bin/index.js +1 -1
  3. package/dist/config/args.js +1 -1
  4. package/dist/lib/expressFormatedResponses.js +1 -1
  5. package/dist/plugins/auth/authzDynamic.js +1 -0
  6. package/dist/plugins/auth/authzDynamicHash.js +1 -0
  7. package/dist/plugins/scim/baseResolver.js +1 -0
  8. package/dist/plugins/scim/bulk.js +1 -0
  9. package/dist/plugins/scim/discovery.js +1 -0
  10. package/dist/plugins/scim/errors.js +1 -0
  11. package/dist/plugins/scim/filter.js +1 -0
  12. package/dist/plugins/scim/groups.js +1 -0
  13. package/dist/plugins/scim/mapping.js +1 -0
  14. package/dist/plugins/scim/patch.js +1 -0
  15. package/dist/plugins/scim/scim.js +1 -0
  16. package/dist/plugins/scim/types.js +1 -0
  17. package/dist/plugins/scim/users.js +1 -0
  18. package/dist/plugins/twake/drive.js +1 -1
  19. package/dist/src/abstract/ldapFlat.d.ts +20 -0
  20. package/dist/src/config/args.d.ts +24 -0
  21. package/dist/src/plugins/auth/authzDynamic.d.ts +117 -0
  22. package/dist/src/plugins/auth/authzDynamicHash.d.ts +27 -0
  23. package/dist/src/plugins/scim/baseResolver.d.ts +23 -0
  24. package/dist/src/plugins/scim/bulk.d.ts +43 -0
  25. package/dist/src/plugins/scim/discovery.d.ts +27 -0
  26. package/dist/src/plugins/scim/errors.d.ts +37 -0
  27. package/dist/src/plugins/scim/filter.d.ts +13 -0
  28. package/dist/src/plugins/scim/groups.d.ts +61 -0
  29. package/dist/src/plugins/scim/mapping.d.ts +52 -0
  30. package/dist/src/plugins/scim/patch.d.ts +39 -0
  31. package/dist/src/plugins/scim/scim.d.ts +23 -0
  32. package/dist/src/plugins/scim/types.d.ts +222 -0
  33. package/dist/src/plugins/scim/users.d.ts +58 -0
  34. package/dist/src/plugins/twake/drive.d.ts +7 -0
  35. package/docs/usage/plugins/auth/authz-dynamic.md +183 -0
  36. package/docs/usage/plugins/integrations/drive.md +22 -2
  37. package/docs/usage/plugins/integrations/scim.md +567 -0
  38. package/package.json +65 -1
  39. package/static/schemas/scim/Group.json +31 -0
  40. package/static/schemas/scim/User.json +147 -0
  41. 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
+ ```
@@ -357,6 +357,26 @@ async unblockInstance(dn: string): Promise<boolean>
357
357
  await drive.unblockInstance('uid=jdoe,ou=users,dc=example,dc=com');
358
358
  ```
359
359
 
360
+ ---
361
+
362
+ ### deleteInstance(dn)
363
+
364
+ Delete a Cozy instance and all its data.
365
+
366
+ **Signature:**
367
+
368
+ ```typescript
369
+ async deleteInstance(dn: string): Promise<boolean>
370
+ ```
371
+
372
+ Calls `DELETE /instances/:domain` on the Cozy Admin API. Returns `true` if the instance was successfully deleted or if the instance was not found (404). Returns `false` on error.
373
+
374
+ **Example:**
375
+
376
+ ```typescript
377
+ await drive.deleteInstance('uid=jdoe,ou=users,dc=example,dc=com');
378
+ ```
379
+
360
380
  ## Cozy Admin API
361
381
 
362
382
  ### Endpoints Used
@@ -367,6 +387,7 @@ await drive.unblockInstance('uid=jdoe,ou=users,dc=example,dc=com');
367
387
  | Update name | `/instances/{domain}?PublicName={name}&FromCloudery=true` | PATCH |
368
388
  | Update quota | `/instances/{domain}?DiskQuota={bytes}&FromCloudery=true` | PATCH |
369
389
  | Combined update | `/instances/{domain}?Email={email}&PublicName={name}&DiskQuota={bytes}&FromCloudery=true` | PATCH |
390
+ | Delete instance | `/instances/{domain}` | DELETE |
370
391
 
371
392
  ### Available Cozy Parameters
372
393
 
@@ -567,8 +588,7 @@ await drive.syncUserToCozy('uid=jdoe,ou=users,dc=example,dc=com');
567
588
 
568
589
  1. **One-way sync**: LDAP → Cozy only. Cozy changes don't sync back to LDAP.
569
590
  2. **No instance creation**: Plugin doesn't create Cozy instances, only updates existing ones.
570
- 3. **No user deletion**: Plugin doesn't delete Cozy instances when LDAP users are deleted.
571
- 4. **Domain required**: Users must have the `twakeCozyDomain` attribute to be synced.
591
+ 3. **Domain required**: Users must have the `twakeCozyDomain` attribute to be synced.
572
592
 
573
593
  ## Troubleshooting
574
594