ldap-rest 0.9.0 → 0.11.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 (47) hide show
  1. package/bin/index.mjs +1 -1
  2. package/dist/abstract/ldapFlat.js +1 -1
  3. package/dist/config/args.js +1 -1
  4. package/dist/lib/authz/base.js +1 -1
  5. package/dist/lib/changeContext.js +1 -0
  6. package/dist/lib/errors.js +1 -1
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/plugins/auth/authzLinid1.js +1 -1
  9. package/dist/plugins/auth/authzPerBranch.js +1 -1
  10. package/dist/plugins/auth/fake.js +1 -0
  11. package/dist/plugins/ldap/departmentSync.js +1 -1
  12. package/dist/plugins/ldap/groups.js +1 -1
  13. package/dist/plugins/ldap/onChange.js +1 -1
  14. package/dist/plugins/ldap/organizations.js +1 -1
  15. package/dist/plugins/ldap/trash.js +1 -1
  16. package/dist/plugins/priority.json.js +1 -1
  17. package/dist/plugins/scim/scim.js +1 -1
  18. package/dist/plugins/twake/appAccountsApi.js +1 -1
  19. package/dist/plugins/twake/appAccountsConsistency.js +1 -1
  20. package/dist/plugins/twake/clouderyProvision.js +1 -1
  21. package/dist/plugins/twake/cozyProvision.js +1 -1
  22. package/dist/src/abstract/plugin.d.ts +6 -0
  23. package/dist/src/config/args.d.ts +5 -3
  24. package/dist/src/hooks.d.ts +15 -6
  25. package/dist/src/lib/authz/base.d.ts +32 -0
  26. package/dist/src/lib/changeContext.d.ts +24 -0
  27. package/dist/src/lib/errors.d.ts +15 -0
  28. package/dist/src/lib/ldapActions.d.ts +2 -1
  29. package/dist/src/plugins/auth/authzLinid1.d.ts +14 -0
  30. package/dist/src/plugins/auth/authzPerBranch.d.ts +35 -0
  31. package/dist/src/plugins/auth/fake.d.ts +21 -0
  32. package/dist/src/plugins/ldap/groups.d.ts +13 -8
  33. package/dist/src/plugins/ldap/onChange.d.ts +17 -8
  34. package/dist/src/plugins/ldap/organizations.d.ts +2 -2
  35. package/docs/plugin-development/README.md +1 -18
  36. package/docs/plugin-development/hooks.md +6 -0
  37. package/docs/plugin-development/testing.md +1 -17
  38. package/docs/usage/configuration.md +15 -6
  39. package/docs/usage/plugins/auth/README.md +2 -0
  40. package/docs/usage/plugins/auth/authz-linid1.md +29 -34
  41. package/docs/usage/plugins/auth/authz-per-branch.md +33 -11
  42. package/docs/usage/plugins/auth/authz-scope.md +5 -0
  43. package/docs/usage/plugins/auth/fake.md +28 -0
  44. package/docs/usage/plugins/ldap/on-change.md +51 -284
  45. package/docs/usage/troubleshooting.md +1 -9
  46. package/docs/usage/upgrading.md +93 -0
  47. package/package.json +5 -1
@@ -1,10 +1,7 @@
1
1
  # LDAP onChange Plugin
2
2
 
3
- Monitor LDAP attribute changes and trigger hooks for specific attributes.
4
-
5
- ## Overview
6
-
7
- The `onChange` plugin watches for LDAP modifications and generates specialized hooks when configured attributes change. This enables reactive integrations with external systems (mail servers, quota managers, etc.).
3
+ Publish the state of an entry before and after each write made through
4
+ LDAP-Rest, and hooks for the attributes other plugins follow.
8
5
 
9
6
  ## Configuration
10
7
 
@@ -14,305 +11,75 @@ The `onChange` plugin watches for LDAP modifications and generates specialized h
14
11
  --quota-attribute mailQuota
15
12
  ```
16
13
 
17
- **Environment Variables:**
18
-
19
- ```bash
20
- DM_MAIL_ATTRIBUTE="mail"
21
- DM_QUOTA_ATTRIBUTE="mailQuota"
22
- ```
23
-
24
- ## Generated Hooks
25
-
26
- The plugin generates hooks based on configured attributes:
27
-
28
- | Config Parameter | Generated Hook | Parameters |
29
- | ------------------- | ------------------- | -------------------------------- |
30
- | `--mail-attribute` | `onLdapMailChange` | `(dn, oldMail, newMail)` |
31
- | `--quota-attribute` | `onLdapQuotaChange` | `(dn, mail, oldQuota, newQuota)` |
32
-
33
- Generic hook for any change:
34
-
35
- - `onLdapChange` - `(dn, changes)` where `changes` is `Record<attr, [oldValue, newValue]>`
36
-
37
- ## How It Works
38
-
39
- 1. **Hooks into LDAP modify operations** via `ldapUsersmodify` and `ldapGroupsmodify` hooks
40
- 2. **Detects attribute changes** by comparing old and new values
41
- 3. **Triggers specialized hooks** for configured attributes
42
- 4. **Provides old and new values** to downstream hooks
43
-
44
- ## Use Cases
45
-
46
- ### Mail Server Synchronization
47
-
48
- When user email changes, update external mail server:
49
-
50
- ```javascript
51
- // In another plugin
52
- hooks: {
53
- onLdapMailChange: async (dn, oldMail, newMail) => {
54
- await mailServer.renameAccount(oldMail, newMail);
55
- console.log(`Renamed mail account: ${oldMail} → ${newMail}`);
56
- };
57
- }
58
- ```
59
-
60
- ### Quota Management
61
-
62
- When quota changes, update mail server quota:
63
-
64
- ```javascript
65
- hooks: {
66
- onLdapQuotaChange: async (dn, mail, oldQuota, newQuota) => {
67
- await mailServer.setQuota(mail, newQuota);
68
- console.log(`Updated quota for ${mail}: ${oldQuota} → ${newQuota}`);
69
- };
70
- }
71
- ```
72
-
73
- ### Generic Change Tracking
74
-
75
- Track all attribute changes:
76
-
77
- ```javascript
78
- hooks: {
79
- onLdapChange: async (dn, changes) => {
80
- for (const [attr, [oldVal, newVal]] of Object.entries(changes)) {
81
- console.log(`${dn}: ${attr} changed from ${oldVal} to ${newVal}`);
82
- }
83
- };
84
- }
85
- ```
86
-
87
- ## Examples
88
-
89
- ### Example 1: Mail Attribute Monitoring
90
-
91
- ```bash
92
- --plugin core/ldap/onChange \
93
- --plugin core/ldap/flatGeneric \
94
- --ldap-flat-schema ./schemas/users.json \
95
- --mail-attribute mail
96
- ```
97
-
98
- When a user's mail attribute changes via API:
99
-
100
- ```bash
101
- PUT /api/v1/ldap/users/jdoe
102
- {
103
- "replace": {
104
- "mail": "john.doe@newdomain.com"
105
- }
106
- }
107
- ```
108
-
109
- The `onLdapMailChange` hook fires with:
110
-
111
- - `dn`: `"uid=jdoe,ou=users,dc=example,dc=com"`
112
- - `oldMail`: `"jdoe@olddomain.com"`
113
- - `newMail`: `"john.doe@newdomain.com"`
114
-
115
- ### Example 2: Multiple Attributes
116
-
117
- ```bash
118
- --plugin core/ldap/onChange \
119
- --mail-attribute mail \
120
- --quota-attribute mailQuota
121
- ```
122
-
123
- When both attributes change:
14
+ ## Hooks
124
15
 
125
- ```bash
126
- PUT /api/v1/ldap/users/jdoe
127
- {
128
- "replace": {
129
- "mail": "jdoe@company.com",
130
- "mailQuota": "5000000000"
131
- }
132
- }
133
- ```
16
+ `onLdapEntryChange(dn, before, after, context)` fires after every add,
17
+ modify, rename and delete, with the entry as the directory held it on each
18
+ side:
134
19
 
135
- Both hooks fire:
20
+ | Operation | `before` | `after` |
21
+ | --------- | ------------------------ | ------------------------ |
22
+ | add | `null` | the new entry |
23
+ | modify | the entry before | the entry after |
24
+ | rename | the entry, at its old DN | the entry, at its new DN |
25
+ | delete | the entry before | `null` |
136
26
 
137
- 1. `onLdapMailChange(dn, "old@mail.com", "jdoe@company.com")`
138
- 2. `onLdapQuotaChange(dn, "jdoe@company.com", "1000000000", "5000000000")`
27
+ `dn` is the entry's DN after the write, its old one on a delete. `context`
28
+ says who made the write, and through which door:
139
29
 
140
- ### Example 3: Generic Change Listener
30
+ - `actor`: the caller's `userName`, or its `user` when it has none;
31
+ - `requestId`: generated, the same for every write of one request;
32
+ - `source`: `scim` for the SCIM API, `rest` otherwise.
141
33
 
142
- ```bash
143
- --plugin core/ldap/onChange
144
- ```
34
+ A write no request is behind, such as a scheduled task's, gets `{}`. The LDAP
35
+ `ldap*done` hooks receive the same context after their arguments.
145
36
 
146
- All modifications trigger `onLdapChange`:
37
+ The other hooks are derived from it:
147
38
 
148
- ```bash
149
- PUT /api/v1/ldap/users/jdoe
150
- {
151
- "replace": {
152
- "cn": "John A. Doe",
153
- "telephoneNumber": "+1234567890"
154
- },
155
- "delete": ["description"]
156
- }
157
- ```
39
+ | Hook | Fires when | Parameters |
40
+ | ------------------------- | ---------------------------------------------------------- | -------------------------------------- |
41
+ | `onLdapChange` | any attribute changed | `(dn, changes)` |
42
+ | `onLdapMailChange` | `--mail-attribute` changed | `(dn, oldMail, newMail)` |
43
+ | `onLdapQuotaChange` | `--quota-attribute` changed | `(dn, mail, oldQuota, newQuota)` |
44
+ | `onLdapAliasChange` | `--alias-attribute` changed | `(dn, mail, oldAliases, newAliases)` |
45
+ | `onLdapForwardChange` | `--forward-attribute` changed | `(dn, mail, oldForwards, newForwards)` |
46
+ | `onLdapDriveQuotaChange` | `--drive-quota-attribute` changed | `(dn, oldQuota, newQuota)` |
47
+ | `onLdapDisplayNameChange` | the name built from `cn`, or `givenName` and `sn`, changed | `(dn, oldName, newName)` |
158
48
 
159
- Hook receives:
49
+ `changes` maps each attribute that changed to `[oldValues, newValues]`, the
50
+ full values on each side, `null` where the attribute is absent:
160
51
 
161
- ```javascript
162
- {
163
- "cn": ["John Doe", "John A. Doe"],
164
- "telephoneNumber": [null, "+1234567890"],
165
- "description": ["Old description", null]
166
- }
52
+ ```text
53
+ member was [a, b], the request added c:
54
+ { member: [['a', 'b'], ['a', 'b', 'c']] }
167
55
  ```
168
56
 
169
- ## Integration Examples
170
-
171
- ### With Twake James Mail Server
172
-
173
- ```bash
174
- --plugin core/ldap/onChange \
175
- --plugin twake/james \
176
- --plugin core/ldap/flatGeneric \
177
- --ldap-flat-schema ./schemas/twake/users.json \
178
- --mail-attribute mail \
179
- --quota-attribute mailQuota \
180
- --james-webadmin-url http://james:8000
181
- ```
57
+ ## Operational attributes
182
58
 
183
- The James plugin listens to `onLdapMailChange` and `onLdapQuotaChange` hooks and automatically updates the James mail server.
184
-
185
- ### Custom Integration Plugin
186
-
187
- Create a custom plugin that reacts to changes:
59
+ A search returns no operational attribute unless asked by name, so neither
60
+ side holds `pwdAccountLockedTime` or its kind by default, and a write changing
61
+ only such an attribute fires no hook. A plugin that follows some declares them:
188
62
 
189
63
  ```typescript
190
- import DmPlugin from '../abstract/plugin';
191
- import { Hooks } from '../hooks';
192
-
193
- export default class CustomSync extends DmPlugin {
194
- name = 'customSync';
195
-
196
- dependencies = {
197
- onChange: 'core/ldap/onChange',
198
- };
199
-
200
- hooks: Hooks = {
201
- onLdapMailChange: async (dn, oldMail, newMail) => {
202
- this.logger.info(`Mail changed: ${oldMail} → ${newMail}`);
203
-
204
- try {
205
- await fetch('https://api.example.com/update-email', {
206
- method: 'POST',
207
- headers: { 'Content-Type': 'application/json' },
208
- body: JSON.stringify({ dn, oldMail, newMail }),
209
- });
210
- } catch (err) {
211
- this.logger.error(`Failed to sync mail change: ${err}`);
212
- }
213
- },
214
- };
215
- }
64
+ followedOperationalAttributes = ['pwdAccountLockedTime'];
216
65
  ```
217
66
 
218
- ## Change Detection
67
+ The entry is then read with `*` and every attribute a loaded plugin declares.
219
68
 
220
- ### Replace Operations
69
+ ## What counts as a change
221
70
 
222
- ```javascript
223
- // Before: mail = "old@example.com"
224
- {
225
- replace: {
226
- mail: 'new@example.com';
227
- }
228
- }
229
- // Triggers: onLdapMailChange("uid=user,ou=users,dc=example,dc=com", "old@example.com", "new@example.com")
230
- ```
231
-
232
- ### Add Operations
71
+ Values are compared as sets: a single value and a one-element array are equal,
72
+ and so are two orderings of the same values. A write that leaves every value
73
+ as it was, such as a `replace` with the current value, fires no hook.
233
74
 
234
- ```javascript
235
- // Before: mail = undefined
236
- {
237
- add: {
238
- mail: 'new@example.com';
239
- }
240
- }
241
- // Triggers: onLdapMailChange("uid=user,ou=users,dc=example,dc=com", null, "new@example.com")
242
- ```
75
+ ## Timing and errors
243
76
 
244
- ### Delete Operations
245
-
246
- ```javascript
247
- // Before: mail = "old@example.com"
248
- { delete: ["mail"] }
249
- // Triggers: onLdapMailChange("uid=user,ou=users,dc=example,dc=com", "old@example.com", null)
250
- ```
251
-
252
- ## Performance Considerations
253
-
254
- ### Hook Execution
255
-
256
- - Hooks run **synchronously** within the modify operation
257
- - Slow hooks delay the LDAP response
258
- - Consider async operations with fire-and-forget for non-critical updates
259
-
260
- ### Optimization Tips
261
-
262
- 1. **Keep hooks fast**: Defer heavy work to background jobs
263
- 2. **Use error handling**: Don't let hook failures block LDAP operations
264
- 3. **Batch updates**: If possible, batch multiple hook calls
265
- 4. **Cache external state**: Avoid redundant external API calls
266
-
267
- ## Error Handling
268
-
269
- Hooks can throw errors to abort the LDAP modification:
270
-
271
- ```javascript
272
- hooks: {
273
- onLdapMailChange: async (dn, oldMail, newMail) => {
274
- // Validate mail domain
275
- if (!newMail.endsWith('@company.com')) {
276
- throw new Error('Mail must be @company.com domain');
277
- }
278
-
279
- // Update mail server
280
- try {
281
- await mailServer.renameAccount(oldMail, newMail);
282
- } catch (err) {
283
- throw new Error(`Mail server update failed: ${err.message}`);
284
- }
285
- };
286
- }
287
- ```
288
-
289
- If the hook throws, the LDAP modification is rolled back (if supported by LDAP server).
290
-
291
- ## Debugging
292
-
293
- Enable debug logging to see change detection:
294
-
295
- ```bash
296
- --log-level debug
297
- ```
298
-
299
- Output:
300
-
301
- ```
302
- [debug] Detected mail change for uid=jdoe,ou=users,dc=example,dc=com: old@domain.com → new@domain.com
303
- [debug] Triggering onLdapMailChange hook
304
- [info] onLdapMailChange: Mail account renamed successfully
305
- ```
77
+ Hooks run after the directory has accepted the write, and the API answers
78
+ without waiting for them. A hook that throws is logged; the write stays.
306
79
 
307
80
  ## Limitations
308
81
 
309
- 1. **Only monitors modify operations**: Does not detect changes from direct LDAP clients
310
- 2. **Requires old value lookup**: Fetches entry before modification for comparison
311
- 3. **Single-valued attributes**: Best suited for single-valued attributes like mail
312
- 4. **Synchronous execution**: Hooks block the LDAP response
313
-
314
- ## See Also
315
-
316
- - [Twake James Plugin](twakeJames.md) - Mail server integration using onChange
317
- - [ldapFlatGeneric.md](ldapFlatGeneric.md) - Generic LDAP entity management
318
- - [Hooks Documentation](../src/hooks.ts) - All available hooks
82
+ - Only writes made through LDAP-Rest are seen; a direct LDAP client's are not.
83
+ - Entries moved by the [trash](trash.md) plugin are not seen, nor the entries
84
+ under a renamed container, which move with it.
85
+ - Each write reads the entry before and after it: one search on each side.
@@ -89,15 +89,7 @@ DM_LOG_LEVEL=debug ldap-rest
89
89
 
90
90
  ## Testing
91
91
 
92
- Run the test suite:
93
-
94
- ```bash
95
- # All tests
96
- source ~/.test-env && npm run test:dev
97
-
98
- # Single file
99
- source ~/.test-env && npm run test:one test/path/to/file.test.ts
100
- ```
92
+ See [Tests and checks](../../CONTRIBUTING.md#tests-and-checks).
101
93
 
102
94
  ## Support
103
95
 
@@ -4,6 +4,99 @@ 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
+
48
+ ## To 0.10.0
49
+
50
+ ### LDAP base is now required
51
+
52
+ **Who is affected:** a deployment that starts `ldap-rest` without
53
+ `--ldap-base` (or `DM_LDAP_BASE`).
54
+
55
+ The server refuses to start without it. It used to take the second RDN of
56
+ `--ldap-dn` — `dc=example` for `cn=admin,dc=example,dc=com` — so subtree
57
+ searches failed or found nothing. Set `--ldap-base` to the DN under which
58
+ the entries are searched.
59
+
60
+ ### Group rules now apply
61
+
62
+ **Who is affected:** a deployment whose `--authz-per-branch-config` declares
63
+ a `groups` section.
64
+
65
+ That section granted nothing: permissions came from `users` and `default`
66
+ alone. A member of a configured group now also gets what its rule grants.
67
+ **Check the `groups` rules before upgrading.**
68
+
69
+ ### An identity naming several entries is refused
70
+
71
+ **Who is affected:** a directory where the same `uid` appears twice — one
72
+ without a `unique` overlay, or one where a homonym was left in the trash
73
+ while the uid was reused.
74
+
75
+ `authzLinid1` refuses every request of such a caller with a `403`.
76
+ `authzPerBranch` keeps the uid's `users` rules and withholds the `groups`
77
+ rules. Both log a warning naming the identity and the number of entries it
78
+ matched.
79
+
80
+ **Check your directory before upgrading:**
81
+
82
+ ```bash
83
+ ldapsearch -x -b "<base>" "(uid=<uid>)" dn
84
+ ```
85
+
86
+ must answer one entry. Where it answers two, remove the duplicate — the
87
+ trash included — or restore the uniqueness the `unique` overlay enforces.
88
+
89
+ ### `onLdapChange` gives full values
90
+
91
+ **Who is affected:** plugins listening to `onLdapChange` that read the values
92
+ of an `add`, or of a `delete` naming values, as the values added or removed.
93
+
94
+ Each attribute now maps to all its values before and after the write. Adding
95
+ `c` to `member: [a, b]` gave `[null, [c]]` and gives `[[a, b], [a, b, c]]`.
96
+ Compute what was added or removed as the difference of the two sides, as the
97
+ plugins shipped with LDAP-Rest do. A `replace` with the current value no
98
+ longer fires the hook.
99
+
7
100
  ## To 0.9.0
8
101
 
9
102
  ### Two authorization plugins judging the same requests no longer start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
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"