ldap-rest 0.6.2 → 0.7.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/config/args.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/plugins/scim/bulk.js +1 -1
- package/dist/plugins/scim/errors.js +1 -1
- package/dist/plugins/scim/filter.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/list.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/scim/projection.js +1 -0
- package/dist/plugins/scim/scim.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/src/config/args.d.ts +3 -0
- package/dist/src/lib/ldapActions.d.ts +26 -0
- package/dist/src/plugins/scim/errors.d.ts +17 -4
- package/dist/src/plugins/scim/filter.d.ts +11 -1
- package/dist/src/plugins/scim/groups.d.ts +2 -0
- package/dist/src/plugins/scim/list.d.ts +3 -5
- package/dist/src/plugins/scim/mapping.d.ts +90 -9
- package/dist/src/plugins/scim/patch.d.ts +16 -1
- package/dist/src/plugins/scim/projection.d.ts +15 -0
- package/dist/src/plugins/scim/scim.d.ts +5 -0
- package/dist/src/plugins/scim/users.d.ts +21 -0
- package/docs/README.md +1 -0
- package/docs/plugin-development/README.md +22 -0
- package/docs/usage/README.md +1 -0
- package/docs/usage/plugins/integrations/scim.md +147 -38
- package/docs/usage/upgrading.md +122 -0
- package/package.json +5 -1
|
@@ -16,11 +16,10 @@
|
|
|
16
16
|
* `tooMany` RFC 7644 §3.12 provides for a filter yielding "many more results
|
|
17
17
|
* than the server is willing to calculate or process".
|
|
18
18
|
*/
|
|
19
|
-
import type
|
|
20
|
-
import type { AttributesList } from '../../lib/ldapActions';
|
|
21
|
-
import type { DmRequest } from '../../lib/auth/base';
|
|
19
|
+
import type { AttributesList, RequestBoundLdap } from '../../lib/ldapActions';
|
|
22
20
|
export interface PagedSearch {
|
|
23
|
-
|
|
21
|
+
/** The directory bound to the request, so authorization always applies. */
|
|
22
|
+
directory: RequestBoundLdap;
|
|
24
23
|
base: string;
|
|
25
24
|
filter: string;
|
|
26
25
|
attributes: string[];
|
|
@@ -30,7 +29,6 @@ export interface PagedSearch {
|
|
|
30
29
|
count: number;
|
|
31
30
|
/** Upper bound on entries walked before answering `tooMany`. */
|
|
32
31
|
maxScanned: number;
|
|
33
|
-
req?: DmRequest;
|
|
34
32
|
}
|
|
35
33
|
export interface PagedResult {
|
|
36
34
|
entries: AttributesList[];
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import type { AttributesList } from '../../lib/ldapActions';
|
|
2
2
|
import { type ScimUser, type ScimGroup, type ResourceMapping, type MultiValued } from './types';
|
|
3
3
|
export declare const DEFAULT_USER_MAPPING: ResourceMapping;
|
|
4
|
+
/**
|
|
5
|
+
* Group mapping.
|
|
6
|
+
*
|
|
7
|
+
* `externalId` is deliberately absent: RFC 7643 section 3.1 defines it as the
|
|
8
|
+
* *provisioning client's* identifier, so it cannot be served from a
|
|
9
|
+
* server-assigned value. groupOfNames has no attribute meant to hold one
|
|
10
|
+
* either, so the deployment names one with
|
|
11
|
+
* `--scim-group-external-id-attribute` and the entry is added at runtime by
|
|
12
|
+
* `withExternalId()`.
|
|
13
|
+
*/
|
|
4
14
|
export declare const DEFAULT_GROUP_MAPPING: ResourceMapping;
|
|
15
|
+
/**
|
|
16
|
+
* Add the `externalId` entry when the deployment named an attribute to store
|
|
17
|
+
* it in. An entry already present — from a mapping override — wins.
|
|
18
|
+
*/
|
|
19
|
+
export declare function withExternalId(mapping: ResourceMapping, ldapAttribute: string): ResourceMapping;
|
|
5
20
|
/**
|
|
6
21
|
* LDAP GeneralizedTime (RFC 4517 §3.3.13) → SCIM DateTime (RFC 7643 §2.3.5,
|
|
7
22
|
* an `xsd:dateTime`).
|
|
@@ -31,7 +46,81 @@ export interface MappingContext {
|
|
|
31
46
|
resourceType: 'User' | 'Group';
|
|
32
47
|
baseUrl?: string;
|
|
33
48
|
scimPrefix: string;
|
|
49
|
+
/**
|
|
50
|
+
* LDAP attribute whose *presence* marks the account as locked, and the
|
|
51
|
+
* value written to lock it. Both come from `resolveLockConfig()`, which is
|
|
52
|
+
* the only place the defaults live: re-defaulting here would let a context
|
|
53
|
+
* built outside the plugin reach the directory with a pair the startup
|
|
54
|
+
* guard never saw. Groups have no `active`, so they pass empty strings.
|
|
55
|
+
*/
|
|
56
|
+
lockAttribute: string;
|
|
57
|
+
lockValue: string;
|
|
34
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* SCIM `active` (RFC 7643 section 4.1.1) has no direct LDAP equivalent. We
|
|
61
|
+
* model it on the presence of one attribute: present means locked, absent
|
|
62
|
+
* means active.
|
|
63
|
+
*
|
|
64
|
+
* The default is the ppolicy overlay's `pwdAccountLockedTime`, whose
|
|
65
|
+
* conventional "locked forever" value is a GeneralizedTime in the distant
|
|
66
|
+
* past. A directory without that overlay points
|
|
67
|
+
* `--scim-user-lock-attribute` / `--scim-user-lock-value` somewhere else
|
|
68
|
+
* (`nsAccountLock` / `TRUE`, for instance).
|
|
69
|
+
*/
|
|
70
|
+
export declare const DEFAULT_LOCK_ATTRIBUTE = "pwdAccountLockedTime";
|
|
71
|
+
export declare const DEFAULT_LOCK_VALUE = "000001010000Z";
|
|
72
|
+
/**
|
|
73
|
+
* Where the configuration warnings go. A seam rather than a direct
|
|
74
|
+
* `console.warn` so the unit tests can read what was said instead of
|
|
75
|
+
* asserting on stderr.
|
|
76
|
+
*/
|
|
77
|
+
export declare let warn: (message: string) => void;
|
|
78
|
+
/** Testing seam: replace the warning sink, and get the previous one back. */
|
|
79
|
+
export declare function setLockConfigWarn(sink: (message: string) => void): (message: string) => void;
|
|
80
|
+
/**
|
|
81
|
+
* Settle the lock configuration, or refuse to start.
|
|
82
|
+
*
|
|
83
|
+
* Ways to get a deployment that answers 200 to every deactivation while the
|
|
84
|
+
* directory locks nothing:
|
|
85
|
+
*
|
|
86
|
+
* - naming an attribute without its value. `000001010000Z` is the ppolicy
|
|
87
|
+
* convention and means nothing to `nsAccountLock`, which 389-ds reads as
|
|
88
|
+
* locked only when it says `true` — so every deactivation would be
|
|
89
|
+
* written, read back as `active: false`, and the account would keep
|
|
90
|
+
* binding.
|
|
91
|
+
* - naming an attribute and pinning that same ppolicy value on it, which a
|
|
92
|
+
* deployment template that always sets both flags does by default. The
|
|
93
|
+
* guard above sees a value and passes; the outcome is identical.
|
|
94
|
+
* - the mirror: a value the default attribute cannot hold.
|
|
95
|
+
* `pwdAccountLockedTime` is a GeneralizedTime, so `TRUE` is refused by the
|
|
96
|
+
* directory's schema — or, where the attribute was redefined locally,
|
|
97
|
+
* stored and ignored.
|
|
98
|
+
* - a name that is not an attribute description. It is interpolated into the
|
|
99
|
+
* emitted LDAP filter for `active eq …`, where a metacharacter makes
|
|
100
|
+
* every list either malformed or quietly wrong.
|
|
101
|
+
*
|
|
102
|
+
* All are operator mistakes rather than attacks, and all are silent. The
|
|
103
|
+
* first and the last are refused outright. The two in the middle cannot be
|
|
104
|
+
* told apart from a deliberate local schema, so they are warned about: what
|
|
105
|
+
* the directory does with a value is beyond anything this can check.
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveLockConfig(attribute: string, value: string): {
|
|
108
|
+
attribute: string;
|
|
109
|
+
value: string;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Read a SCIM `active` value, wherever it came from.
|
|
113
|
+
*
|
|
114
|
+
* `undefined` means the body did not mention it — which is not the same as
|
|
115
|
+
* `true`. Only a boolean, or the two canonical strings some providers send
|
|
116
|
+
* instead, are accepted: guessing at `"0"`, `null` or `1` would let a
|
|
117
|
+
* deprovisioning request be executed as a re-activation, and answer 200
|
|
118
|
+
* doing it. The one rule lives here so POST, PUT, PATCH and the filter
|
|
119
|
+
* translator cannot drift apart.
|
|
120
|
+
*/
|
|
121
|
+
export declare function readActive(value: unknown): boolean | undefined;
|
|
122
|
+
/** Is this entry locked, per the configured lock attribute? */
|
|
123
|
+
export declare function isLocked(entry: AttributesList, ctx: MappingContext): boolean;
|
|
35
124
|
/** Resolve the SCIM id for an LDAP entry, per configuration. */
|
|
36
125
|
export declare function resolveScimId(entry: AttributesList, ctx: MappingContext): string | undefined;
|
|
37
126
|
export declare function buildLocation(id: string, ctx: MappingContext): string | undefined;
|
|
@@ -44,6 +133,7 @@ export declare function ldapToScimUser(entry: AttributesList, mapping: ResourceM
|
|
|
44
133
|
export declare function scimUserToLdap(user: ScimUser, mapping: ResourceMapping, ctx: MappingContext, objectClass: string[]): {
|
|
45
134
|
rdn: string;
|
|
46
135
|
attributes: AttributesList;
|
|
136
|
+
active?: boolean;
|
|
47
137
|
};
|
|
48
138
|
export declare function ldapToScimGroup(entry: AttributesList, mapping: ResourceMapping, ctx: MappingContext, memberResolver?: (dn: string) => MultiValued | undefined): ScimGroup;
|
|
49
139
|
/**
|
|
@@ -58,15 +148,6 @@ export declare function scimGroupToLdap(group: ScimGroup, mapping: ResourceMappi
|
|
|
58
148
|
* List of LDAP attributes to request from the directory so all
|
|
59
149
|
* mapped SCIM attributes can be populated.
|
|
60
150
|
*/
|
|
61
|
-
/**
|
|
62
|
-
* The ppolicy overlay's lock marker, whose presence means `active: false`.
|
|
63
|
-
*
|
|
64
|
-
* Operational, so a search that does not name it never answers it: the read
|
|
65
|
-
* path has to ask for it explicitly.
|
|
66
|
-
*/
|
|
67
|
-
export declare const LOCK_ATTRIBUTE = "pwdAccountLockedTime";
|
|
68
|
-
/** Is this entry locked, per the ppolicy lock marker? */
|
|
69
|
-
export declare function isLocked(entry: AttributesList): boolean;
|
|
70
151
|
export declare function requiredLdapAttributes(mapping: ResourceMapping, extra?: string[]): string[];
|
|
71
152
|
/**
|
|
72
153
|
* Given a SCIM attribute path like "emails.value" or "name.familyName",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* member operations for Groups are handled via the caller-provided
|
|
20
20
|
* `resolveMemberRef` hook (SCIM id → LDAP DN lookup).
|
|
21
21
|
*/
|
|
22
|
-
import type { ModifyRequest } from '../../lib/ldapActions';
|
|
22
|
+
import type { AttributesList, ModifyRequest } from '../../lib/ldapActions';
|
|
23
23
|
import { type ResourceMapping, type PatchRequest } from './types';
|
|
24
24
|
export interface PatchContext {
|
|
25
25
|
mapping: ResourceMapping;
|
|
@@ -27,6 +27,21 @@ export interface PatchContext {
|
|
|
27
27
|
resolveMemberRef?: (value: string) => Promise<string | undefined>;
|
|
28
28
|
/** The LDAP attribute holding members, default 'member'. */
|
|
29
29
|
memberAttribute?: string;
|
|
30
|
+
/**
|
|
31
|
+
* For Users: the attribute whose presence marks the account as locked, and
|
|
32
|
+
* the value to write when SCIM `active` is set to false.
|
|
33
|
+
*/
|
|
34
|
+
lockAttribute?: string;
|
|
35
|
+
lockValue?: string;
|
|
36
|
+
/** True when this resource type carries SCIM `active` (Users only). */
|
|
37
|
+
supportsActive?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The entry as it stands. RFC 7644 section 3.5.2 applies operations in
|
|
40
|
+
* order, so they are played against this and only the resulting difference
|
|
41
|
+
* is sent — without it, two operations touching one attribute would
|
|
42
|
+
* collapse into whichever the emitter happened to keep.
|
|
43
|
+
*/
|
|
44
|
+
current: AttributesList;
|
|
30
45
|
}
|
|
31
46
|
export declare function patchToModifyRequest(patch: PatchRequest, ctx: PatchContext): Promise<ModifyRequest>;
|
|
32
47
|
/**
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ProjectionQuery {
|
|
2
|
+
attributes?: string[];
|
|
3
|
+
excludedAttributes?: string[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Raise when both parameters are given: RFC 7644 §3.9 makes them mutually
|
|
7
|
+
* exclusive, and honouring one silently would hide the mistake.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertProjection(query: ProjectionQuery): void;
|
|
10
|
+
/** Apply `attributes` / `excludedAttributes` to one resource. */
|
|
11
|
+
export declare function projectResource<T extends Record<string, unknown>>(resource: T, query: ProjectionQuery): T;
|
|
12
|
+
/** Apply the projection to every resource of a ListResponse. */
|
|
13
|
+
export declare function projectList<T extends Record<string, unknown>, L extends {
|
|
14
|
+
Resources: T[];
|
|
15
|
+
}>(list: L, query: ProjectionQuery): L;
|
|
@@ -419,6 +419,11 @@ export default class Scim extends DmPlugin {
|
|
|
419
419
|
* `--scim-base-url` to make it absolute and trustworthy too.
|
|
420
420
|
*/
|
|
421
421
|
private scimResource;
|
|
422
|
+
/**
|
|
423
|
+
* `attributes` / `excludedAttributes` (RFC 7644 section 3.9). Accepted on
|
|
424
|
+
* every route that answers a resource, not only the list ones.
|
|
425
|
+
*/
|
|
426
|
+
private parseProjection;
|
|
422
427
|
private readScimBody;
|
|
423
428
|
api(app: Express): void;
|
|
424
429
|
private parseListQuery;
|
|
@@ -41,14 +41,35 @@ export declare class ScimUsers {
|
|
|
41
41
|
private readonly maxResults;
|
|
42
42
|
private readonly maxScanned;
|
|
43
43
|
private readonly scimPrefix;
|
|
44
|
+
private readonly lockAttribute;
|
|
45
|
+
private readonly lockValue;
|
|
44
46
|
constructor(opts: ScimUsersOptions);
|
|
45
47
|
private ctx;
|
|
48
|
+
/**
|
|
49
|
+
* LDAP attributes to fetch. The lock attribute backing SCIM `active` is
|
|
50
|
+
* operational on most directories, so it is never returned unless asked
|
|
51
|
+
* for by name.
|
|
52
|
+
*/
|
|
53
|
+
private ldapAttributes;
|
|
46
54
|
private dnForId;
|
|
47
55
|
get(req: DmRequest, id: string): Promise<ScimUser>;
|
|
48
56
|
list(req: DmRequest, query: ListQuery): Promise<ListResponse<ScimUser>>;
|
|
49
57
|
create(req: DmRequest, resource: ScimUser): Promise<ScimUser>;
|
|
50
58
|
replace(req: DmRequest, id: string, resource: ScimUser): Promise<ScimUser>;
|
|
51
59
|
patch(req: DmRequest, id: string, patch: PatchRequest): Promise<ScimUser>;
|
|
60
|
+
/**
|
|
61
|
+
* Re-raise a directory refusal that names the schema, pointing at the flag
|
|
62
|
+
* that caused it.
|
|
63
|
+
*
|
|
64
|
+
* The generic translation in `errors` cannot do this: it sees the error,
|
|
65
|
+
* not the configuration. Only this class knows which attribute was
|
|
66
|
+
* configured to back `active`, and the shipped default —
|
|
67
|
+
* `pwdAccountLockedTime` — exists only where slapd loads the ppolicy
|
|
68
|
+
* overlay, which is the common way to meet this.
|
|
69
|
+
*/
|
|
70
|
+
private lockSchemaError;
|
|
71
|
+
/** Fetch the raw LDAP entry backing a SCIM id, or raise 404. */
|
|
72
|
+
private currentEntry;
|
|
52
73
|
delete(req: DmRequest, id: string): Promise<void>;
|
|
53
74
|
/**
|
|
54
75
|
* Given a SCIM member value (typically an id), return the LDAP DN.
|
package/docs/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Installation, configuration and plugin usage.
|
|
|
10
10
|
|
|
11
11
|
- **[Getting Started](usage/README.md)** - Quick start guide
|
|
12
12
|
- **[Configuration](usage/configuration.md)** - CLI options and environment variables
|
|
13
|
+
- **[Upgrading](usage/upgrading.md)** - What to check before deploying a new version
|
|
13
14
|
- **[Plugins](usage/plugins/README.md)** - Documentation for all available plugins
|
|
14
15
|
|
|
15
16
|
### [Plugin Development](plugin-development/README.md)
|
|
@@ -455,6 +455,28 @@ export default class MyPlugin extends DmPlugin {
|
|
|
455
455
|
|
|
456
456
|
The `this.server.ldap` object provides access to all LDAP operations:
|
|
457
457
|
|
|
458
|
+
> **Serving an HTTP request? Bind it first.**
|
|
459
|
+
>
|
|
460
|
+
> The authorization plugins (`core/auth/authzPerBranch`,
|
|
461
|
+
> `core/auth/authzDynamic`, …) hook `ldapsearchrequest`, `ldapaddrequest`,
|
|
462
|
+
> `ldapmodifyrequest`, `ldaprenamerequest` and `ldapdeleterequest`, and they
|
|
463
|
+
> **skip every check when the request is missing**. The methods below take it as a trailing
|
|
464
|
+
> optional argument, so forgetting it does not fail — it silently authorizes
|
|
465
|
+
> the operation. Three such bugs shipped in the SCIM plugin.
|
|
466
|
+
>
|
|
467
|
+
> In a request handler, bind once and use the result:
|
|
468
|
+
>
|
|
469
|
+
> ```typescript
|
|
470
|
+
> const directory = this.server.ldap.forRequest(req);
|
|
471
|
+
> await directory.search({ scope: 'base' }, dn);
|
|
472
|
+
> await directory.modify(dn, changes);
|
|
473
|
+
> ```
|
|
474
|
+
>
|
|
475
|
+
> `forRequest()` returns a `RequestBoundLdap`, whose methods have no request
|
|
476
|
+
> parameter at all — the omission stops being expressible. Use the unbound
|
|
477
|
+
> methods only for work that genuinely belongs to no request, such as startup
|
|
478
|
+
> or scheduled tasks, where passing nothing is the deliberate answer.
|
|
479
|
+
|
|
458
480
|
#### Search
|
|
459
481
|
|
|
460
482
|
```typescript
|
package/docs/usage/README.md
CHANGED
|
@@ -86,4 +86,5 @@ The system will:
|
|
|
86
86
|
|
|
87
87
|
- **[Configuration](configuration.md)** - CLI options and environment variables
|
|
88
88
|
- **[Plugins](plugins/README.md)** - Choose and configure plugins
|
|
89
|
+
- **[Upgrading](upgrading.md)** - What to check before deploying a new version
|
|
89
90
|
- **[Troubleshooting](troubleshooting.md)** - Problem resolution
|
|
@@ -41,27 +41,30 @@ All SCIM endpoints sit behind the auth middleware registered before the plugin
|
|
|
41
41
|
|
|
42
42
|
### CLI / environment
|
|
43
43
|
|
|
44
|
-
| Argument
|
|
45
|
-
|
|
|
46
|
-
| `--scim-prefix`
|
|
47
|
-
| `--scim-user-base`
|
|
48
|
-
| `--scim-group-base`
|
|
49
|
-
| `--scim-user-base-template`
|
|
50
|
-
| `--scim-group-base-template`
|
|
51
|
-
| `--scim-base-map`
|
|
52
|
-
| `--scim-user-object-class`
|
|
53
|
-
| `--scim-user-rdn-attribute`
|
|
54
|
-
| `--scim-
|
|
55
|
-
| `--scim-
|
|
56
|
-
| `--scim-
|
|
57
|
-
| `--scim-
|
|
58
|
-
| `--scim-group-
|
|
59
|
-
| `--scim-
|
|
60
|
-
| `--scim-
|
|
61
|
-
| `--scim-
|
|
62
|
-
| `--scim-
|
|
63
|
-
| `--scim-
|
|
64
|
-
| `--scim-
|
|
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-user-lock-attribute` | `DM_SCIM_USER_LOCK_ATTRIBUTE` | `pwdAccountLockedTime` | LDAP attribute whose presence marks a User as `active: false` |
|
|
55
|
+
| `--scim-user-lock-value` | `DM_SCIM_USER_LOCK_VALUE` | — | Value written to that attribute when deactivating (required unless the attribute is the default) |
|
|
56
|
+
| `--scim-group-object-class` | `DM_SCIM_GROUP_OBJECT_CLASSES` | `top, groupOfNames` | Object classes for created Groups |
|
|
57
|
+
| `--scim-group-rdn-attribute` | `DM_SCIM_GROUP_RDN_ATTRIBUTE` | `cn` | RDN attribute for Groups |
|
|
58
|
+
| `--scim-group-external-id-attribute` | `DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE` | — | LDAP attribute storing a Group's `externalId` (unset: not supported) |
|
|
59
|
+
| `--scim-id-attribute` | `DM_SCIM_ID_ATTRIBUTE` | `rdn` | `rdn` (default) or `entryUUID` for SCIM `id` |
|
|
60
|
+
| `--scim-user-mapping` | `DM_SCIM_USER_MAPPING` | — | Path to JSON mapping override |
|
|
61
|
+
| `--scim-group-mapping` | `DM_SCIM_GROUP_MAPPING` | — | Same for Groups |
|
|
62
|
+
| `--scim-max-results` | `DM_SCIM_MAX_RESULTS` | `200` | Maximum page size (caps `count`) |
|
|
63
|
+
| `--scim-max-scanned` | `DM_SCIM_MAX_SCANNED` | `10000` | Entries a list may walk before answering `400 tooMany` |
|
|
64
|
+
| `--scim-bulk-max-operations` | `DM_SCIM_BULK_MAX_OPERATIONS` | `100` | Max `/Bulk` operations per request |
|
|
65
|
+
| `--scim-bulk-max-payload-size` | `DM_SCIM_BULK_MAX_PAYLOAD_SIZE` | `1048576` | Max `/Bulk` payload size in bytes |
|
|
66
|
+
| `--scim-etag` | `DM_SCIM_ETAG` | `false` | Advertise ETag support in discovery (not yet implemented) |
|
|
67
|
+
| `--scim-base-url` | `DM_SCIM_BASE_URL` | auto from request | Override external base URL for `meta.location` values |
|
|
65
68
|
|
|
66
69
|
### Authorization
|
|
67
70
|
|
|
@@ -110,27 +113,91 @@ All responses have `Content-Type: application/scim+json`.
|
|
|
110
113
|
|
|
111
114
|
### Default mapping (LDAP ⇄ SCIM User)
|
|
112
115
|
|
|
113
|
-
| SCIM | LDAP
|
|
114
|
-
| ------------------------------------ |
|
|
115
|
-
| `id` | `uid` (or `entryUUID`)
|
|
116
|
-
| `userName` | `uid`
|
|
117
|
-
| `externalId` | `employeeNumber`
|
|
118
|
-
| `name.familyName` | `sn`
|
|
119
|
-
| `name.givenName` | `givenName`
|
|
120
|
-
| `name.formatted` | `cn`
|
|
121
|
-
| `displayName` | `displayName`
|
|
122
|
-
| `title` | `title`
|
|
123
|
-
| `preferredLanguage` | `preferredLanguage`
|
|
124
|
-
| `emails[primary=true].value` | `mail`
|
|
125
|
-
| `emails[primary=false].value` | `mailAlternateAddress`
|
|
126
|
-
| `phoneNumbers[primary=true].value` | `telephoneNumber`
|
|
127
|
-
| `phoneNumbers[primary=false].value` | `mobile`
|
|
128
|
-
| `active` | pseudo (false if
|
|
129
|
-
| `meta.created` / `meta.lastModified` | `createTimestamp` / `modifyTimestamp`
|
|
116
|
+
| SCIM | LDAP |
|
|
117
|
+
| ------------------------------------ | ------------------------------------------- |
|
|
118
|
+
| `id` | `uid` (or `entryUUID`) |
|
|
119
|
+
| `userName` | `uid` |
|
|
120
|
+
| `externalId` | `employeeNumber` |
|
|
121
|
+
| `name.familyName` | `sn` |
|
|
122
|
+
| `name.givenName` | `givenName` |
|
|
123
|
+
| `name.formatted` | `cn` |
|
|
124
|
+
| `displayName` | `displayName` |
|
|
125
|
+
| `title` | `title` |
|
|
126
|
+
| `preferredLanguage` | `preferredLanguage` |
|
|
127
|
+
| `emails[primary=true].value` | `mail` |
|
|
128
|
+
| `emails[primary=false].value` | `mailAlternateAddress` |
|
|
129
|
+
| `phoneNumbers[primary=true].value` | `telephoneNumber` |
|
|
130
|
+
| `phoneNumbers[primary=false].value` | `mobile` |
|
|
131
|
+
| `active` | pseudo (false if the lock attribute is set) |
|
|
132
|
+
| `meta.created` / `meta.lastModified` | `createTimestamp` / `modifyTimestamp` |
|
|
130
133
|
|
|
131
134
|
Override with `--scim-user-mapping /path/to/user-mapping.json` (same schema as
|
|
132
135
|
`static/schemas/scim/default-mapping.json`).
|
|
133
136
|
|
|
137
|
+
## Deactivating an account (`active`)
|
|
138
|
+
|
|
139
|
+
SCIM models account status with the boolean `active` (RFC 7643 §4.1.1), and it
|
|
140
|
+
is how Okta, Entra ID and JumpCloud disable a user — they PATCH `active` to
|
|
141
|
+
`false` rather than DELETE the resource.
|
|
142
|
+
|
|
143
|
+
LDAP has no single equivalent, so the plugin models it on the **presence of one
|
|
144
|
+
attribute**: present means locked, absent means active.
|
|
145
|
+
|
|
146
|
+
| SCIM | LDAP |
|
|
147
|
+
| ----------------- | ----------------------------------------------- |
|
|
148
|
+
| `"active": false` | write `--scim-user-lock-value` to the attribute |
|
|
149
|
+
| `"active": true` | remove the attribute |
|
|
150
|
+
| attribute absent | `"active": true` |
|
|
151
|
+
|
|
152
|
+
The default targets the ppolicy overlay:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
--scim-user-lock-attribute pwdAccountLockedTime
|
|
156
|
+
--scim-user-lock-value 000001010000Z # "locked forever"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`pwdAccountLockedTime` only exists when slapd loads the ppolicy overlay. On a
|
|
160
|
+
directory without it — plain OpenLDAP, 389-ds, AD — the directory refuses the
|
|
161
|
+
write against its schema, and the plugin answers `400 invalidValue` naming
|
|
162
|
+
the attribute and the flags to change, on `/Bulk` as on the single-resource
|
|
163
|
+
routes. Point the two flags elsewhere:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
--scim-user-lock-attribute nsAccountLock
|
|
167
|
+
--scim-user-lock-value TRUE
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Name the value whenever you name the attribute.** `000001010000Z` is the
|
|
171
|
+
ppolicy convention and means nothing to any other attribute: 389-ds reads
|
|
172
|
+
`nsAccountLock` as locked only when it says `true`. Setting the attribute
|
|
173
|
+
alone would write a value the directory ignores — every deactivation answers
|
|
174
|
+
200 and reads back `active: false` while the account keeps binding. The
|
|
175
|
+
plugin refuses to start instead.
|
|
176
|
+
|
|
177
|
+
Two more pairings get a warning at startup rather than a refusal, because
|
|
178
|
+
neither can be told apart from a deliberate local schema:
|
|
179
|
+
|
|
180
|
+
- the ppolicy value `000001010000Z` pinned on some other attribute, which is
|
|
181
|
+
what a deployment template that always sets both flags produces by default
|
|
182
|
+
- a value `pwdAccountLockedTime` cannot hold, `TRUE` for instance: it holds a
|
|
183
|
+
GeneralizedTime
|
|
184
|
+
|
|
185
|
+
**None of this can tell whether your directory honours the value you chose.**
|
|
186
|
+
The checks are on the shape of the configuration. `active` is read back from
|
|
187
|
+
the mere presence of the attribute, so a value the directory stores and
|
|
188
|
+
ignores still reads as `false`. Verify once, by hand, that a deactivation
|
|
189
|
+
actually prevents a bind.
|
|
190
|
+
|
|
191
|
+
`active` is not a mapping entry and must not be added to a mapping file: it is
|
|
192
|
+
handled by these two flags on every path — create, PUT, PATCH and the
|
|
193
|
+
`active eq true|false` filter alike.
|
|
194
|
+
|
|
195
|
+
PUT is a full replacement (RFC 7644 §3.5.1) for every attribute except this
|
|
196
|
+
one: a body that omits `active` leaves the lock as it stands. Clearing it
|
|
197
|
+
would release locks SCIM never set — a ppolicy auto-lockout, or one an
|
|
198
|
+
administrator placed — so a routine profile sync would defeat the
|
|
199
|
+
brute-force control. Send `"active": true` to reactivate deliberately.
|
|
200
|
+
|
|
134
201
|
## Writing a custom mapping schema
|
|
135
202
|
|
|
136
203
|
The mapping file is a plain JSON document describing how each SCIM attribute is
|
|
@@ -247,6 +314,23 @@ adjust the loader). Exposing a schema there is **independent** from the
|
|
|
247
314
|
LDAP↔SCIM projection: the schema file describes the public SCIM contract, the
|
|
248
315
|
mapping file describes the internal translation.
|
|
249
316
|
|
|
317
|
+
### Group `externalId`
|
|
318
|
+
|
|
319
|
+
RFC 7643 §3.1 defines `externalId` as the **provisioning client's** identifier —
|
|
320
|
+
the id Okta or Entra ID keeps for its own copy of the resource, which it later
|
|
321
|
+
uses to find the group again (`filter=externalId eq "…"`).
|
|
322
|
+
|
|
323
|
+
For Users it lands in `employeeNumber`. groupOfNames has no attribute meant to
|
|
324
|
+
hold one, so nothing is stored unless the deployment names an attribute:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
--scim-group-external-id-attribute description
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Without it, `externalId` sent on a Group is ignored and a filter on it answers
|
|
331
|
+
`400 invalidFilter` — which is at least honest. Any free attribute of your
|
|
332
|
+
group entries works; use a mapping override if you would rather name it there.
|
|
333
|
+
|
|
250
334
|
## Filter syntax
|
|
251
335
|
|
|
252
336
|
SCIM filters are translated to LDAP filters (RFC 4515). Values are always escaped
|
|
@@ -259,6 +343,7 @@ via `escapeLdapFilter()` — no LDAP injection is possible.
|
|
|
259
343
|
| `emails.value co "@example.com"` | `(mail=*@example.com*)` |
|
|
260
344
|
| `displayName pr` | `(displayName=*)` |
|
|
261
345
|
| `active eq true` | `(!(pwdAccountLockedTime=*))` |
|
|
346
|
+
| `active pr` | `(objectClass=*)` |
|
|
262
347
|
| `a eq "x" and b eq "y"` | `(&(a=x)(b=y))` |
|
|
263
348
|
| `not (a eq "x")` | `(!(a=x))` |
|
|
264
349
|
|
|
@@ -297,6 +382,30 @@ willing to calculate or process". Raise the flag, or narrow the query.
|
|
|
297
382
|
If the directory's own size limit is lower than the collection, it refuses the
|
|
298
383
|
search first and the answer is the same `tooMany` — raise `sizelimit` in slapd.
|
|
299
384
|
|
|
385
|
+
## Partial representations (`attributes` / `excludedAttributes`)
|
|
386
|
+
|
|
387
|
+
Any route that answers a resource accepts the two RFC 7644 §3.9 parameters —
|
|
388
|
+
the lists, the single-resource GETs, and the POST/PUT/PATCH answers alike.
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
curl '.../Users/alice?attributes=userName,name.familyName'
|
|
392
|
+
curl '.../Users?excludedAttributes=emails'
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
- `attributes` replaces the default set: only what you name comes back.
|
|
396
|
+
- `excludedAttributes` keeps the default set minus what you name.
|
|
397
|
+
- Both accept standard attribute notation, so `name.familyName` and
|
|
398
|
+
`emails.value` narrow a complex or multi-valued attribute rather than
|
|
399
|
+
dropping it whole, and a core schema URN prefix is accepted and stripped.
|
|
400
|
+
- `id` and `schemas` are `returned: always` (RFC 7643 §7): they survive an
|
|
401
|
+
exclusion and are added back to an `attributes` list.
|
|
402
|
+
- `meta` is `returned: default`, so an `attributes` list that does not name it
|
|
403
|
+
does not get it. Ask for `meta` explicitly if you need `meta.location`.
|
|
404
|
+
- The two are mutually exclusive; sending both answers `400 invalidValue`.
|
|
405
|
+
|
|
406
|
+
The projection applies to the response only — the LDAP search still reads the
|
|
407
|
+
attributes the mapping needs.
|
|
408
|
+
|
|
300
409
|
## Multi-tenant: per-user LDAP bases
|
|
301
410
|
|
|
302
411
|
SCIM does not expose any tenant concept to clients — resource `id`s are opaque
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Upgrading
|
|
2
|
+
|
|
3
|
+
What to check before deploying, newest first. Only releases that need a
|
|
4
|
+
decision or a configuration change appear here; see the
|
|
5
|
+
[CHANGELOG](../../CHANGELOG.md) for everything else.
|
|
6
|
+
|
|
7
|
+
## To 0.7.0
|
|
8
|
+
|
|
9
|
+
### `externalId` on Groups is no longer served
|
|
10
|
+
|
|
11
|
+
**Who is affected:** anyone provisioning Groups from an identity provider and
|
|
12
|
+
reading `externalId` back, or filtering on it.
|
|
13
|
+
|
|
14
|
+
A Group's `externalId` used to answer its `entryUUID`. RFC 7643 section 3.1
|
|
15
|
+
makes `externalId` the _provisioning client's_ identifier, so the id Okta or
|
|
16
|
+
Entra ID sent was discarded on write, and `filter=externalId eq "<their id>"`
|
|
17
|
+
searched `entryUUID` and matched nothing.
|
|
18
|
+
|
|
19
|
+
Name an attribute to store it in:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
--scim-group-external-id-attribute description
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Left unset, `externalId` is simply not supported on Groups — absent from
|
|
26
|
+
answers, and refused in filters. That is the honest behaviour; the previous
|
|
27
|
+
one looked like support and was not.
|
|
28
|
+
|
|
29
|
+
Users are unaffected: theirs was already stored in `employeeNumber`.
|
|
30
|
+
|
|
31
|
+
**Migrating existing Groups.** Values sent before this release were never
|
|
32
|
+
stored, so there is nothing to convert — re-send them from the provisioning
|
|
33
|
+
side once the flag is set.
|
|
34
|
+
|
|
35
|
+
### `active` is now writable, and needs an attribute your directory holds
|
|
36
|
+
|
|
37
|
+
**Who is affected:** every SCIM deployment. Deactivations that used to be
|
|
38
|
+
silently dropped now reach the directory.
|
|
39
|
+
|
|
40
|
+
`active` is modelled on the presence of one LDAP attribute:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
--scim-user-lock-attribute pwdAccountLockedTime # default
|
|
44
|
+
--scim-user-lock-value 000001010000Z # default, "locked forever"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The default is the ppolicy overlay's convention, and **`pwdAccountLockedTime`
|
|
48
|
+
only exists where slapd loads that overlay**. On a directory without it —
|
|
49
|
+
plain OpenLDAP, 389-ds, AD — a deactivation is refused by the schema and
|
|
50
|
+
answers `400 invalidValue` naming the flags. Point them elsewhere:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
--scim-user-lock-attribute nsAccountLock
|
|
54
|
+
--scim-user-lock-value TRUE
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Two configuration mistakes are now refused at startup rather than accepted
|
|
58
|
+
and silently ineffective: naming an attribute without a value, and a name
|
|
59
|
+
that is not an LDAP attribute description. Two more are warned about. See
|
|
60
|
+
[the SCIM plugin documentation](plugins/integrations/scim.md#deactivating-an-account-active) for the
|
|
61
|
+
detail.
|
|
62
|
+
|
|
63
|
+
**Verify once, by hand, that a deactivation actually prevents a bind.**
|
|
64
|
+
Nothing here can check that your directory honours the value you chose:
|
|
65
|
+
`active` is read back from the mere presence of the attribute, so a value the
|
|
66
|
+
directory stores and ignores still reads as `false`.
|
|
67
|
+
|
|
68
|
+
**One deliberate deviation from RFC 7644 section 3.5.1:** a `PUT` that omits
|
|
69
|
+
`active` leaves the lock as it stands rather than clearing it. Clearing it
|
|
70
|
+
would release locks SCIM never set — a ppolicy auto-lockout after failed
|
|
71
|
+
binds, or one an administrator placed — so a routine profile sync would
|
|
72
|
+
defeat the brute-force control. Send `"active": true` to reactivate
|
|
73
|
+
deliberately.
|
|
74
|
+
|
|
75
|
+
### `forRequest()` for plugin authors
|
|
76
|
+
|
|
77
|
+
No action needed to upgrade. If you maintain a plugin that serves HTTP
|
|
78
|
+
requests, `this.server.ldap.forRequest(req)` returns a directory whose methods
|
|
79
|
+
carry the request, so the authorization hooks always see it. The unbound
|
|
80
|
+
methods take the request as a trailing optional argument, and omitting it
|
|
81
|
+
skips every authorization check silently. See the
|
|
82
|
+
[plugin development guide](../plugin-development/README.md#using-ldap-operations).
|
|
83
|
+
|
|
84
|
+
## To 0.6.2
|
|
85
|
+
|
|
86
|
+
### Writes that a bypass was letting through are now refused
|
|
87
|
+
|
|
88
|
+
**Who is affected:** deployments using `core/auth/authzPerBranch` or
|
|
89
|
+
`core/auth/authzLinid1` together with `plugins/ldap/onChange` or
|
|
90
|
+
`plugins/ldap/organizations`.
|
|
91
|
+
|
|
92
|
+
Both plugins rebuilt the hook tuple without the request, which made every
|
|
93
|
+
authorization plugin registered after them skip its check. Every `modify`,
|
|
94
|
+
and every `rename` for the second, went through unchecked.
|
|
95
|
+
|
|
96
|
+
**Check your grants before rolling this out widely.** An identity whose
|
|
97
|
+
grants were never quite right may start seeing `403` where it saw `200`. That
|
|
98
|
+
is the fix, not a regression.
|
|
99
|
+
|
|
100
|
+
Whether a given deployment was exposed was decided by plugin load order,
|
|
101
|
+
which is a race — it could differ from one restart to the next.
|
|
102
|
+
`core/auth/authzDynamic` was never affected: it reads its token from an
|
|
103
|
+
`AsyncLocalStorage` rather than from the request.
|
|
104
|
+
|
|
105
|
+
## To 0.6.1
|
|
106
|
+
|
|
107
|
+
### SCIM reads are authorized
|
|
108
|
+
|
|
109
|
+
**Who is affected:** any SCIM identity granted `write` without `read` on the
|
|
110
|
+
same branch.
|
|
111
|
+
|
|
112
|
+
Every search the SCIM plugin issued omitted the request, so
|
|
113
|
+
`ldapsearchrequest` — the hook the authorization plugins use — skipped its
|
|
114
|
+
check, and any authenticated identity could read any branch the plugin was
|
|
115
|
+
pointed at.
|
|
116
|
+
|
|
117
|
+
**An identity that writes now needs `read` on the same branch.** A SCIM write
|
|
118
|
+
answers with the resource it just changed, so it reads the entry back; a
|
|
119
|
+
grant of `write` without `read` no longer serves a write.
|
|
120
|
+
|
|
121
|
+
`GET /Users`, `GET /Users/{id}` and their Group counterparts answer `403`
|
|
122
|
+
where read is denied.
|