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
|
@@ -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.
|