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.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/dist/abstract/ldapFlat.js +1 -1
  3. package/dist/bin/index.js +1 -1
  4. package/dist/config/args.js +1 -1
  5. package/dist/lib/expressFormatedResponses.js +1 -1
  6. package/dist/plugins/auth/authzDynamic.js +1 -0
  7. package/dist/plugins/auth/authzDynamicHash.js +1 -0
  8. package/dist/plugins/ldap/organization.js +1 -1
  9. package/dist/plugins/ldap/organizations.js +1 -0
  10. package/dist/plugins/scim/baseResolver.js +1 -0
  11. package/dist/plugins/scim/bulk.js +1 -0
  12. package/dist/plugins/scim/discovery.js +1 -0
  13. package/dist/plugins/scim/errors.js +1 -0
  14. package/dist/plugins/scim/filter.js +1 -0
  15. package/dist/plugins/scim/groups.js +1 -0
  16. package/dist/plugins/scim/mapping.js +1 -0
  17. package/dist/plugins/scim/patch.js +1 -0
  18. package/dist/plugins/scim/scim.js +1 -0
  19. package/dist/plugins/scim/types.js +1 -0
  20. package/dist/plugins/scim/users.js +1 -0
  21. package/dist/src/abstract/ldapFlat.d.ts +127 -0
  22. package/dist/src/config/args.d.ts +24 -0
  23. package/dist/src/plugins/auth/authzDynamic.d.ts +117 -0
  24. package/dist/src/plugins/auth/authzDynamicHash.d.ts +27 -0
  25. package/dist/src/plugins/configApi.d.ts +51 -0
  26. package/dist/src/plugins/demo/helloworld.d.ts +21 -0
  27. package/dist/src/plugins/ldap/bulkImport.d.ts +65 -0
  28. package/dist/src/plugins/ldap/groups.d.ts +68 -0
  29. package/dist/src/plugins/ldap/organization.d.ts +8 -65
  30. package/dist/src/plugins/ldap/organizations.d.ts +155 -0
  31. package/dist/src/plugins/ldap/passwordPolicy.d.ts +178 -0
  32. package/dist/src/plugins/scim/baseResolver.d.ts +23 -0
  33. package/dist/src/plugins/scim/bulk.d.ts +43 -0
  34. package/dist/src/plugins/scim/discovery.d.ts +27 -0
  35. package/dist/src/plugins/scim/errors.d.ts +37 -0
  36. package/dist/src/plugins/scim/filter.d.ts +13 -0
  37. package/dist/src/plugins/scim/groups.d.ts +61 -0
  38. package/dist/src/plugins/scim/mapping.d.ts +52 -0
  39. package/dist/src/plugins/scim/patch.d.ts +39 -0
  40. package/dist/src/plugins/scim/scim.d.ts +412 -0
  41. package/dist/src/plugins/scim/types.d.ts +222 -0
  42. package/dist/src/plugins/scim/users.d.ts +58 -0
  43. package/dist/src/plugins/static.d.ts +33 -0
  44. package/dist/src/plugins/twake/appAccountsApi.d.ts +63 -0
  45. package/dist/src/plugins/twake/james.d.ts +47 -0
  46. package/docs/plugin-development/README.md +2 -1
  47. package/docs/plugin-development/openapi.md +144 -0
  48. package/docs/usage/plugins/auth/authz-dynamic.md +183 -0
  49. package/docs/usage/plugins/integrations/scim.md +567 -0
  50. package/package.json +72 -1
  51. package/static/schemas/scim/Group.json +31 -0
  52. package/static/schemas/scim/User.json +147 -0
  53. package/static/schemas/scim/default-mapping.json +48 -0
@@ -0,0 +1,567 @@
1
+ # SCIM 2.0 Plugin
2
+
3
+ The `scim` plugin exposes a [RFC 7643](https://datatracker.ietf.org/doc/html/rfc7643) /
4
+ [RFC 7644](https://datatracker.ietf.org/doc/html/rfc7644) compliant endpoint at
5
+ `/scim/v2/*`, allowing identity providers like Okta, Microsoft Entra ID (Azure AD),
6
+ Google Workspace, and JumpCloud to provision users and groups against the underlying
7
+ LDAP directory.
8
+
9
+ ## Overview
10
+
11
+ - Resources: `/Users`, `/Groups`
12
+ - Operations: GET, POST, PUT, PATCH, DELETE, Bulk
13
+ - Discovery: `/ServiceProviderConfig`, `/ResourceTypes`, `/Schemas`
14
+ - SCIM filter syntax (RFC 7644 §3.4.2.2) translated to LDAP filters
15
+ - SCIM PATCH (RFC 7644 §3.5.2) translated to LDAP `ModifyRequest`
16
+ - Bulk (RFC 7644 §3.7) with cross-operation `bulkId` references
17
+ - Per-request base resolution (multi-tenant) via template or JSON map
18
+
19
+ The plugin sits on top of `ldapActions` and can operate fully independently from the
20
+ generic `ldap/users` / `ldap/groups` plugins — it only needs LDAP credentials and
21
+ target branches.
22
+
23
+ ## Quick start
24
+
25
+ ```bash
26
+ node lib/bin/index.js \
27
+ --ldap-url ldap://localhost:389 \
28
+ --ldap-base dc=example,dc=com \
29
+ --ldap-dn cn=admin,dc=example,dc=com \
30
+ --ldap-pwd admin \
31
+ --plugin core/auth/token --auth-token 'my-secret:idp1' \
32
+ --plugin core/scim \
33
+ --scim-user-base ou=users,dc=example,dc=com \
34
+ --scim-group-base ou=groups,dc=example,dc=com
35
+ ```
36
+
37
+ All SCIM endpoints sit behind the auth middleware registered before the plugin
38
+ (Bearer token in this example).
39
+
40
+ ## Configuration
41
+
42
+ ### CLI / environment
43
+
44
+ | Argument | Environment | Default | Description |
45
+ | ------------------------------ | ------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------- |
46
+ | `--scim-prefix` | `DM_SCIM_PREFIX` | `/scim/v2` | Base URL path for all SCIM endpoints |
47
+ | `--scim-user-base` | `DM_SCIM_USER_BASE` | `{ldap_base}` | LDAP branch containing users |
48
+ | `--scim-group-base` | `DM_SCIM_GROUP_BASE` | `{ldap_base}` | LDAP branch containing groups |
49
+ | `--scim-user-base-template` | `DM_SCIM_USER_BASE_TEMPLATE` | — | DN template with `{user}` placeholder (see _multi-tenant_) |
50
+ | `--scim-group-base-template` | `DM_SCIM_GROUP_BASE_TEMPLATE` | — | Same for groups |
51
+ | `--scim-base-map` | `DM_SCIM_BASE_MAP` | — | Path to JSON file mapping authenticated user → `{userBase, groupBase}` |
52
+ | `--scim-user-object-class` | `DM_SCIM_USER_OBJECT_CLASSES` | `top, inetOrgPerson, organizationalPerson, person` | Object classes for created Users |
53
+ | `--scim-user-rdn-attribute` | `DM_SCIM_USER_RDN_ATTRIBUTE` | `uid` | RDN attribute for Users |
54
+ | `--scim-group-object-class` | `DM_SCIM_GROUP_OBJECT_CLASSES` | `top, groupOfNames` | Object classes for created Groups |
55
+ | `--scim-group-rdn-attribute` | `DM_SCIM_GROUP_RDN_ATTRIBUTE` | `cn` | RDN attribute for Groups |
56
+ | `--scim-id-attribute` | `DM_SCIM_ID_ATTRIBUTE` | `rdn` | `rdn` (default) or `entryUUID` for SCIM `id` |
57
+ | `--scim-user-mapping` | `DM_SCIM_USER_MAPPING` | — | Path to JSON mapping override |
58
+ | `--scim-group-mapping` | `DM_SCIM_GROUP_MAPPING` | — | Same for Groups |
59
+ | `--scim-max-results` | `DM_SCIM_MAX_RESULTS` | `200` | Hard cap on list results |
60
+ | `--scim-bulk-max-operations` | `DM_SCIM_BULK_MAX_OPERATIONS` | `100` | Max `/Bulk` operations per request |
61
+ | `--scim-bulk-max-payload-size` | `DM_SCIM_BULK_MAX_PAYLOAD_SIZE` | `1048576` | Max `/Bulk` payload size in bytes |
62
+ | `--scim-etag` | `DM_SCIM_ETAG` | `false` | Advertise ETag support in discovery (not yet implemented) |
63
+ | `--scim-base-url` | `DM_SCIM_BASE_URL` | auto from request | Override external base URL for `meta.location` values |
64
+
65
+ ### Authentication
66
+
67
+ SCIM piggybacks on whichever auth plugin you load. The discovery endpoint
68
+ `/ServiceProviderConfig` reflects the active scheme automatically:
69
+
70
+ - `core/auth/token` → `oauthbearertoken`
71
+ - `core/auth/openidconnect` → `oauth2`
72
+ - `core/auth/hmac` → `httpbasic`
73
+
74
+ For Okta / Entra, load `core/auth/token` with a provisioning token.
75
+
76
+ ## Endpoints
77
+
78
+ All responses have `Content-Type: application/scim+json`.
79
+
80
+ | Method | Path | Description |
81
+ | ------------------------- | ------------------------------------------ | ------------------------------------- |
82
+ | GET | `/Users` | List Users (filter, pagination, sort) |
83
+ | GET | `/Users/{id}` | Get a User |
84
+ | POST | `/Users` | Create a User |
85
+ | PUT | `/Users/{id}` | Replace a User |
86
+ | PATCH | `/Users/{id}` | Partial update (add/remove/replace) |
87
+ | DELETE | `/Users/{id}` | Delete a User |
88
+ | GET/POST/PUT/PATCH/DELETE | `/Groups[/{id}]` | Same operations on Groups |
89
+ | POST | `/Bulk` | Batch operations with `bulkId` refs |
90
+ | GET | `/ServiceProviderConfig` | Capabilities |
91
+ | GET | `/ResourceTypes` / `/ResourceTypes/{name}` | Resource metadata |
92
+ | GET | `/Schemas` / `/Schemas/{urn}` | Schema definitions |
93
+
94
+ ### Default mapping (LDAP ⇄ SCIM User)
95
+
96
+ | SCIM | LDAP |
97
+ | ------------------------------------ | ----------------------------------------------- |
98
+ | `id` | `uid` (or `entryUUID`) |
99
+ | `userName` | `uid` |
100
+ | `externalId` | `employeeNumber` |
101
+ | `name.familyName` | `sn` |
102
+ | `name.givenName` | `givenName` |
103
+ | `name.formatted` | `cn` |
104
+ | `displayName` | `displayName` |
105
+ | `title` | `title` |
106
+ | `preferredLanguage` | `preferredLanguage` |
107
+ | `emails[primary=true].value` | `mail` |
108
+ | `emails[primary=false].value` | `mailAlternateAddress` |
109
+ | `phoneNumbers[primary=true].value` | `telephoneNumber` |
110
+ | `phoneNumbers[primary=false].value` | `mobile` |
111
+ | `active` | pseudo (false if `pwdAccountLockedTime` is set) |
112
+ | `meta.created` / `meta.lastModified` | `createTimestamp` / `modifyTimestamp` |
113
+
114
+ Override with `--scim-user-mapping /path/to/user-mapping.json` (same schema as
115
+ `static/schemas/scim/default-mapping.json`).
116
+
117
+ ## Writing a custom mapping schema
118
+
119
+ The mapping file is a plain JSON document describing how each SCIM attribute is
120
+ projected to LDAP, and vice versa. A separate file is loaded for `--scim-user-mapping`
121
+ and `--scim-group-mapping`.
122
+
123
+ ### File shape
124
+
125
+ ```json
126
+ {
127
+ "resourceType": "User",
128
+ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
129
+ "entries": [
130
+ { "scim": "<scim attribute>", "ldap": "<ldap attribute>" },
131
+ ...
132
+ ]
133
+ }
134
+ ```
135
+
136
+ Each element of `entries` is a `MappingEntry` with the following optional fields:
137
+
138
+ | Field | Purpose |
139
+ | --------------- | -------------------------------------------------------------------------------------------------------------------- |
140
+ | `scim` | **Required.** Top-level SCIM attribute name (`userName`, `displayName`, `emails`, …). |
141
+ | `ldap` | Simple 1:1 mapping to an LDAP attribute. |
142
+ | `ldapPrimary` | For multi-valued SCIM attributes (`emails`, `phoneNumbers`, …), the LDAP attribute that stores the _primary_ value. |
143
+ | `ldapSecondary` | LDAP attribute that stores the _non-primary_ values of the same SCIM attribute (array). |
144
+ | `sub` | Object that maps SCIM sub-attributes to LDAP attributes. Used for complex SCIM values like `name.familyName` → `sn`. |
145
+ | `multi` | `"array"` or `"single"` (default) — hints array serialization for `ldap`. |
146
+ | `readOnly` | `true` to skip this attribute on write (create/update ignore it). |
147
+ | `operational` | `true` for LDAP operational attributes (e.g. `entryUUID`) — loaded on read only. |
148
+
149
+ ### Merging
150
+
151
+ The file you provide is **merged on top of the default mapping**: entries whose
152
+ `scim` key matches a default entry replace it; new entries are appended. So you can
153
+ override only what's different and inherit the rest.
154
+
155
+ ### Example — adding the Enterprise User extension
156
+
157
+ ```json
158
+ {
159
+ "resourceType": "User",
160
+ "schemas": [
161
+ "urn:ietf:params:scim:schemas:core:2.0:User",
162
+ "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
163
+ ],
164
+ "entries": [
165
+ { "scim": "userName", "ldap": "sAMAccountName" },
166
+ {
167
+ "scim": "name",
168
+ "sub": {
169
+ "familyName": "sn",
170
+ "givenName": "givenName",
171
+ "formatted": "displayName"
172
+ }
173
+ },
174
+ {
175
+ "scim": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber",
176
+ "ldap": "employeeNumber"
177
+ },
178
+ {
179
+ "scim": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department",
180
+ "ldap": "department"
181
+ },
182
+ {
183
+ "scim": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager",
184
+ "sub": { "value": "manager" }
185
+ }
186
+ ]
187
+ }
188
+ ```
189
+
190
+ Save it as `/etc/ldap-rest/scim-enterprise.json` and load with
191
+ `--scim-user-mapping /etc/ldap-rest/scim-enterprise.json`.
192
+
193
+ ### Example — multi-valued phone numbers with a custom LDAP attribute
194
+
195
+ ```json
196
+ {
197
+ "resourceType": "User",
198
+ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
199
+ "entries": [
200
+ {
201
+ "scim": "phoneNumbers",
202
+ "ldapPrimary": "telephoneNumber",
203
+ "ldapSecondary": "otherTelephone",
204
+ "multi": "array"
205
+ }
206
+ ]
207
+ }
208
+ ```
209
+
210
+ On write, the entry marked `primary: true` (or the first one) lands in
211
+ `telephoneNumber`; the remaining values are written as an array to
212
+ `otherTelephone`. On read, both attributes are collected back into a SCIM
213
+ `phoneNumbers` array with the primary flag set on the `telephoneNumber` value.
214
+
215
+ ### Filter and PATCH behavior
216
+
217
+ The mapping is authoritative for the **SCIM filter parser** (RFC 7644 §3.4.2.2)
218
+ and the **PATCH applicator** (RFC 7644 §3.5.2). A SCIM filter like
219
+ `department eq "Engineering"` will only translate to an LDAP filter if your
220
+ mapping contains an entry with `scim: "department"`; otherwise the server returns
221
+ `400 invalidFilter`. Same for PATCH: operations on unmapped paths produce
222
+ `400 invalidPath`.
223
+
224
+ ### Schema advertisement
225
+
226
+ Schemas listed under `GET /scim/v2/Schemas` are read from
227
+ `static/schemas/scim/User.json` and `Group.json`. To expose a custom schema to
228
+ clients, extend those JSON files (or drop additional files in the same folder and
229
+ adjust the loader). Exposing a schema there is **independent** from the
230
+ LDAP↔SCIM projection: the schema file describes the public SCIM contract, the
231
+ mapping file describes the internal translation.
232
+
233
+ ## Filter syntax
234
+
235
+ SCIM filters are translated to LDAP filters (RFC 4515). Values are always escaped
236
+ via `escapeLdapFilter()` — no LDAP injection is possible.
237
+
238
+ | SCIM | LDAP |
239
+ | -------------------------------- | ----------------------------- |
240
+ | `userName eq "alice"` | `(uid=alice)` |
241
+ | `name.familyName sw "Du"` | `(sn=Du*)` |
242
+ | `emails.value co "@example.com"` | `(mail=*@example.com*)` |
243
+ | `displayName pr` | `(displayName=*)` |
244
+ | `active eq true` | `(!(pwdAccountLockedTime=*))` |
245
+ | `a eq "x" and b eq "y"` | `(&(a=x)(b=y))` |
246
+ | `not (a eq "x")` | `(!(a=x))` |
247
+
248
+ Unknown SCIM attributes raise `400 invalidFilter`.
249
+
250
+ ## Multi-tenant: per-user LDAP bases
251
+
252
+ SCIM does not expose any tenant concept to clients — resource `id`s are opaque
253
+ strings. Internally the plugin can route each authenticated identity to a
254
+ different LDAP subtree, so one `ldap-rest` instance can serve many tenants
255
+ without any client-visible distinction.
256
+
257
+ ### How `req.user` is populated
258
+
259
+ Whichever auth plugin runs before SCIM must set `req.user` to a string that
260
+ identifies the caller. Supported out of the box:
261
+
262
+ | Auth plugin | `req.user` value |
263
+ | ------------------------- | ---------------------------------------------------------------- |
264
+ | `core/auth/token` | the `name` field of `--auth-token "id:secret:name"` |
265
+ | `core/auth/openidconnect` | the subject / preferred_username from the OIDC token |
266
+ | `core/auth/hmac` | the HMAC key id |
267
+ | `core/auth/trustedProxy` | the value of `--trusted-proxy-auth-header` (default `Auth-User`) |
268
+ | `core/auth/llng` | the LemonLDAP::NG user id |
269
+
270
+ In the examples below we use `core/auth/token`, but any of the above works the
271
+ same way — the SCIM plugin only reads `req.user`.
272
+
273
+ ### Three modes, evaluated in order
274
+
275
+ The plugin resolves `userBase` and `groupBase` on **every request**, in this
276
+ precedence:
277
+
278
+ 1. Explicit entry in a JSON map keyed by `req.user` (`--scim-base-map`).
279
+ 2. Wildcard `"*"` entry in the same JSON map (applied when no explicit match).
280
+ 3. DN template with `{user}` placeholder (`--scim-user-base-template` /
281
+ `--scim-group-base-template`).
282
+ 4. Static value (`--scim-user-base` / `--scim-group-base`).
283
+ 5. Global fallback (`--ldap-base`).
284
+
285
+ ### Mode 1 — JSON map (explicit, arbitrary routing)
286
+
287
+ Use when different tenants live in unrelated branches of the tree, or when you
288
+ need to override group base independently of user base.
289
+
290
+ Create `/etc/ldap-rest/scim-tenants.json`:
291
+
292
+ ```json
293
+ {
294
+ "okta-acme": {
295
+ "userBase": "ou=users,ou=acme,dc=example,dc=com",
296
+ "groupBase": "ou=groups,ou=acme,dc=example,dc=com"
297
+ },
298
+ "entra-globex": {
299
+ "userBase": "ou=users,ou=globex,dc=example,dc=com",
300
+ "groupBase": "ou=groups,ou=globex,dc=example,dc=com"
301
+ },
302
+ "*": {
303
+ "userBase": "ou=users,dc=example,dc=com",
304
+ "groupBase": "ou=groups,dc=example,dc=com"
305
+ }
306
+ }
307
+ ```
308
+
309
+ Run with:
310
+
311
+ ```bash
312
+ node lib/bin/index.js \
313
+ --ldap-base dc=example,dc=com \
314
+ --ldap-dn cn=admin,dc=example,dc=com --ldap-pwd admin \
315
+ --plugin core/auth/token \
316
+ --auth-token 'okta-token:okta-acme' \
317
+ --auth-token 'entra-token:entra-globex' \
318
+ --plugin core/scim \
319
+ --scim-base-map /etc/ldap-rest/scim-tenants.json
320
+ ```
321
+
322
+ Okta authenticates with `Bearer okta-token` → `req.user = "okta-acme"` →
323
+ SCIM operates under `ou=acme`. Entra ID authenticates with `Bearer entra-token`
324
+ → `req.user = "entra-globex"` → operates under `ou=globex`. Any other caller
325
+ falls back to the `"*"` entry.
326
+
327
+ ### Mode 2 — Template (symmetric, naming-based)
328
+
329
+ Use when all tenants follow the same branch pattern and the tenant id happens to
330
+ be a safe DN value.
331
+
332
+ ```bash
333
+ node lib/bin/index.js \
334
+ ... \
335
+ --plugin core/auth/token \
336
+ --auth-token 'acme-secret:acme' \
337
+ --auth-token 'globex-secret:globex' \
338
+ --plugin core/scim \
339
+ --scim-user-base-template 'ou=users,ou={user},dc=example,dc=com' \
340
+ --scim-group-base-template 'ou=groups,ou={user},dc=example,dc=com'
341
+ ```
342
+
343
+ The `{user}` placeholder is substituted with `req.user` **after being sanitized
344
+ by `escapeDnValue`**, so any DN-special characters in the identity are properly
345
+ escaped and cannot break out of the template. A caller authenticating as `acme`
346
+ will see `ou=users,ou=acme,dc=example,dc=com` as its user base.
347
+
348
+ The template is also honoured for the `"*"` wildcard values in a JSON map, if
349
+ those contain `{user}`.
350
+
351
+ ### Mode 3 — Static (single-tenant)
352
+
353
+ If every caller targets the same subtree, just set:
354
+
355
+ ```bash
356
+ --scim-user-base ou=users,dc=example,dc=com
357
+ --scim-group-base ou=groups,dc=example,dc=com
358
+ ```
359
+
360
+ No template, no map.
361
+
362
+ ### Mixing modes
363
+
364
+ Nothing prevents combining them. A common pattern is: explicit map for a few
365
+ named partners, template for the common case, static fallback for untagged
366
+ tokens.
367
+
368
+ ```bash
369
+ --scim-base-map /etc/ldap-rest/partners.json # explicit overrides
370
+ --scim-user-base-template 'ou=users,ou={user},dc=example,dc=com'
371
+ --scim-user-base ou=users,dc=example,dc=com # absolute fallback
372
+ ```
373
+
374
+ ### Debugging the resolution
375
+
376
+ Enable `--log-level debug` and look for messages such as `LDAP search` with the
377
+ computed `base` — you will see the per-request base chosen for each SCIM call.
378
+ If your token is missing a `name` part, `req.user` will be `"token <index>"`,
379
+ which is unlikely to match any map key — always use `id:secret:name`.
380
+
381
+ ## Examples
382
+
383
+ ### Create a User
384
+
385
+ ```bash
386
+ curl -X POST http://localhost:8081/scim/v2/Users \
387
+ -H 'Authorization: Bearer my-secret' \
388
+ -H 'Content-Type: application/scim+json' \
389
+ -d '{
390
+ "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
391
+ "userName":"alice",
392
+ "name":{"familyName":"Doe","givenName":"Alice"},
393
+ "emails":[{"value":"alice@example.com","primary":true}]
394
+ }'
395
+ ```
396
+
397
+ ### PATCH
398
+
399
+ ```bash
400
+ curl -X PATCH http://localhost:8081/scim/v2/Users/alice \
401
+ -H 'Authorization: Bearer my-secret' \
402
+ -H 'Content-Type: application/scim+json' \
403
+ -d '{
404
+ "schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
405
+ "Operations":[
406
+ {"op":"replace","path":"displayName","value":"Alice Doe"}
407
+ ]
408
+ }'
409
+ ```
410
+
411
+ ### Bulk with `bulkId` cross-reference
412
+
413
+ ```bash
414
+ curl -X POST http://localhost:8081/scim/v2/Bulk \
415
+ -H 'Authorization: Bearer my-secret' \
416
+ -H 'Content-Type: application/scim+json' \
417
+ -d '{
418
+ "schemas":["urn:ietf:params:scim:api:messages:2.0:BulkRequest"],
419
+ "Operations":[
420
+ {"method":"POST","bulkId":"u1","path":"/Users",
421
+ "data":{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"alice"}},
422
+ {"method":"POST","path":"/Groups",
423
+ "data":{"schemas":["urn:ietf:params:scim:schemas:core:2.0:Group"],
424
+ "displayName":"admins","members":[{"value":"bulkId:u1"}]}}
425
+ ]
426
+ }'
427
+ ```
428
+
429
+ ## Error envelope
430
+
431
+ All errors conform to RFC 7644 §3.12:
432
+
433
+ ```json
434
+ {
435
+ "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
436
+ "status": "400",
437
+ "scimType": "invalidFilter",
438
+ "detail": "Unknown attribute path 'foo'"
439
+ }
440
+ ```
441
+
442
+ ## Hooks
443
+
444
+ Other plugins can observe and transform SCIM operations via the dynamic hooks
445
+ `scimusercreate`, `scimusercreatedone`, `scimuserupdate`, `scimuserupdatedone`,
446
+ `scimuserdelete`, `scimuserdeletedone`, and their `scimgroup*` equivalents, plus
447
+ `scimbulkdone` for Bulk.
448
+
449
+ ## Exposing the configuration to client apps (`configApi`)
450
+
451
+ Load `core/configApi` alongside the SCIM plugin, and `GET /api/v1/config` will
452
+ auto-discover SCIM and publish its metadata under `features.scim`. This lets
453
+ browser-based editors and dashboards build SCIM-aware UIs without re-parsing
454
+ server flags.
455
+
456
+ ```bash
457
+ node lib/bin/index.js \
458
+ ... \
459
+ --plugin core/scim \
460
+ --plugin core/static \
461
+ --plugin core/configApi
462
+ ```
463
+
464
+ Response shape (truncated):
465
+
466
+ ```json
467
+ {
468
+ "apiPrefix": "/api",
469
+ "ldapBase": "dc=example,dc=com",
470
+ "features": {
471
+ "scim": {
472
+ "enabled": true,
473
+ "version": "2.0",
474
+ "prefix": "/scim/v2",
475
+ "endpoints": {
476
+ "users": "/scim/v2/Users",
477
+ "groups": "/scim/v2/Groups",
478
+ "bulk": "/scim/v2/Bulk",
479
+ "serviceProviderConfig": "/scim/v2/ServiceProviderConfig",
480
+ "resourceTypes": "/scim/v2/ResourceTypes",
481
+ "schemas": "/scim/v2/Schemas"
482
+ },
483
+ "schemaUrls": {
484
+ "user": "/static/schemas/scim/User.json",
485
+ "group": "/static/schemas/scim/Group.json",
486
+ "defaultMapping": "/static/schemas/scim/default-mapping.json"
487
+ },
488
+ "capabilities": {
489
+ "patch": true,
490
+ "bulk": true,
491
+ "filter": true,
492
+ "sort": true,
493
+ "etag": false,
494
+ "changePassword": false,
495
+ "maxResults": 200,
496
+ "bulkMaxOperations": 100,
497
+ "bulkMaxPayloadSize": 1048576
498
+ },
499
+ "resourceTypes": [
500
+ {
501
+ "id": "User",
502
+ "endpoint": "/scim/v2/Users",
503
+ "schema": "urn:ietf:params:scim:schemas:core:2.0:User",
504
+ "rdnAttribute": "uid",
505
+ "objectClass": [
506
+ "top",
507
+ "inetOrgPerson",
508
+ "organizationalPerson",
509
+ "person"
510
+ ],
511
+ "filterableAttributes": [
512
+ "userName",
513
+ "externalId",
514
+ "name",
515
+ "displayName",
516
+ "...",
517
+ "id",
518
+ "active"
519
+ ],
520
+ "mapping": [
521
+ /* same shape as default-mapping.json */
522
+ ]
523
+ },
524
+ {
525
+ "id": "Group",
526
+ "endpoint": "/scim/v2/Groups",
527
+ "schema": "urn:ietf:params:scim:schemas:core:2.0:Group",
528
+ "rdnAttribute": "cn",
529
+ "mapping": [
530
+ /* ... */
531
+ ]
532
+ }
533
+ ],
534
+ "idStrategy": "rdn",
535
+ "baseResolution": {
536
+ "userBaseTemplate": "ou=users,ou={user},dc=example,dc=com",
537
+ "groupBaseTemplate": null,
538
+ "hasBaseMap": false
539
+ }
540
+ }
541
+ }
542
+ }
543
+ ```
544
+
545
+ Notable fields:
546
+
547
+ - `schemaUrls` — direct download URLs when `core/static` is loaded (editor UIs
548
+ typically fetch them to render attribute pickers).
549
+ - `resourceTypes[].mapping` — the live SCIM↔LDAP mapping used server-side, so
550
+ UI builders can render filter helpers / attribute selectors without second
551
+ round-trip.
552
+ - `resourceTypes[].filterableAttributes` — the list of attribute paths accepted
553
+ by the SCIM filter parser.
554
+ - `baseResolution` — advertises template/map presence but NOT concrete base
555
+ DNs: those vary per authenticated identity and remain server-side.
556
+
557
+ No configuration is needed on the SCIM plugin to enable this — it advertises
558
+ the `configurable` role automatically, and `core/configApi` picks it up.
559
+
560
+ ## Known limitations (v1)
561
+
562
+ - No `/Me` endpoint (RFC 7644 §3.11).
563
+ - ETag / `If-Match` advertised but not enforced.
564
+ - No `/.search` POST endpoint (use GET with `?filter=…`).
565
+ - `totalResults` is computed from a single LDAP search capped at
566
+ `--scim-max-results`; switch to paged search for directories with many thousands
567
+ of entries.