ldap-rest 0.20.2 → 0.21.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/abstract/ldapFlat.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/config/schema.js +1 -1
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/lib/instanceProviders.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/plugins/ldap/accountLifecycle.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/passwordPolicy.js +1 -1
- package/dist/plugins/ldap/raw.js +1 -1
- package/dist/plugins/twake/clouderyProvision.js +1 -1
- package/dist/plugins/twake/instances.js +1 -1
- package/dist/src/browser/ldap-group-editor/api/GroupApiClient.d.ts +10 -5
- package/dist/src/browser/ldap-group-editor/types.d.ts +8 -0
- package/dist/src/browser/ldap-resource-editor/api/ResourceApiClient.d.ts +7 -1
- package/dist/src/browser/ldap-unit-editor/api/UnitApiClient.d.ts +7 -5
- package/dist/src/browser/ldap-user-editor/LdapUserEditor.d.ts +2 -0
- package/dist/src/browser/ldap-user-editor/api/UserApiClient.d.ts +7 -1
- package/dist/src/browser/shared/utils/form.d.ts +30 -0
- package/dist/src/config/args.d.ts +1 -0
- package/dist/src/config/schema.d.ts +30 -1
- package/dist/src/lib/instanceProviders.d.ts +11 -0
- package/dist/src/lib/ldapActions.d.ts +21 -3
- package/dist/src/plugins/ldap/accountLifecycle.d.ts +2 -0
- package/dist/src/plugins/ldap/passwordPolicy.d.ts +95 -18
- package/dist/src/plugins/twake/clouderyProvision.d.ts +7 -1
- package/docs/client-development/api/rest-api.md +19 -0
- package/docs/usage/configuration.md +16 -0
- package/docs/usage/plugins/integrations/instances.md +18 -6
- package/docs/usage/plugins/ldap/README.md +7 -1
- package/docs/usage/plugins/ldap/account-lifecycle.md +9 -1
- package/docs/usage/plugins/ldap/flat-generic.md +14 -12
- package/docs/usage/plugins/ldap/password-policy.md +88 -4
- package/docs/usage/upgrading.md +43 -13
- package/examples/web/README.md +5 -1
- package/examples/web/ldap-user-editor.html +9 -15
- package/examples/web/totp-client.html +51 -14
- package/package.json +1 -1
- package/static/browser/ldap-group-editor/api/GroupApiClient.js +27 -13
- package/static/browser/ldap-group-editor/api/GroupApiClient.js.map +1 -1
- package/static/browser/ldap-group-editor/components/CreateGroupModal.js +5 -17
- package/static/browser/ldap-group-editor/components/CreateGroupModal.js.map +1 -1
- package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js +28 -12
- package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js.map +1 -1
- package/static/browser/ldap-group-editor/components/MoveGroupModal.js.map +1 -1
- package/static/browser/ldap-group-editor.esm.js +114 -42
- package/static/browser/ldap-group-editor.esm.js.map +1 -1
- package/static/browser/ldap-group-editor.js +114 -42
- package/static/browser/ldap-group-editor.js.map +1 -1
- package/static/browser/ldap-group-editor.min.js +1 -1
- package/static/browser/ldap-group-editor.min.js.map +1 -1
- package/static/browser/ldap-resource-editor/api/ResourceApiClient.js +8 -2
- package/static/browser/ldap-resource-editor/api/ResourceApiClient.js.map +1 -1
- package/static/browser/ldap-unit-editor/api/UnitApiClient.js +8 -13
- package/static/browser/ldap-unit-editor/api/UnitApiClient.js.map +1 -1
- package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js +5 -2
- package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js.map +1 -1
- package/static/browser/ldap-unit-editor.esm.js +52 -15
- package/static/browser/ldap-unit-editor.esm.js.map +1 -1
- package/static/browser/ldap-unit-editor.js +52 -15
- package/static/browser/ldap-unit-editor.js.map +1 -1
- package/static/browser/ldap-unit-editor.min.js +1 -1
- package/static/browser/ldap-unit-editor.min.js.map +1 -1
- package/static/browser/ldap-user-editor/LdapUserEditor.js +22 -21
- package/static/browser/ldap-user-editor/LdapUserEditor.js.map +1 -1
- package/static/browser/ldap-user-editor/api/UserApiClient.js +8 -2
- package/static/browser/ldap-user-editor/api/UserApiClient.js.map +1 -1
- package/static/browser/ldap-user-editor/components/UserEditor.js +19 -9
- package/static/browser/ldap-user-editor/components/UserEditor.js.map +1 -1
- package/static/browser/ldap-user-editor.esm.js +88 -32
- package/static/browser/ldap-user-editor.esm.js.map +1 -1
- package/static/browser/ldap-user-editor.js +88 -32
- package/static/browser/ldap-user-editor.js.map +1 -1
- package/static/browser/ldap-user-editor.min.js +1 -1
- package/static/browser/ldap-user-editor.min.js.map +1 -1
- package/static/browser/shared/index.js +1 -1
- package/static/browser/shared/utils/form.js +51 -1
- package/static/browser/shared/utils/form.js.map +1 -1
- package/static/browser/shared.esm.js +51 -1
- package/static/browser/shared.esm.js.map +1 -1
- package/static/browser/shared.js +53 -0
- package/static/browser/shared.js.map +1 -1
- package/static/browser/shared.min.js +1 -1
- package/static/browser/shared.min.js.map +1 -1
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Password Policy plugin - Administration API for OpenLDAP ppolicy
|
|
3
|
-
*
|
|
4
|
-
* Provides REST endpoints to:
|
|
5
|
-
* - Query password status (expiration, lockout, etc.)
|
|
6
|
-
* - Unlock locked accounts
|
|
7
|
-
* - List expiring passwords and locked accounts
|
|
8
|
-
* - Validate password complexity (optional)
|
|
9
|
-
*
|
|
10
|
-
* @author Xavier Guimard <xguimard@linagora.com>
|
|
11
|
-
*/
|
|
12
1
|
import type { Express } from 'express';
|
|
13
2
|
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
14
3
|
/**
|
|
@@ -133,6 +122,60 @@ import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
|
133
122
|
* type: integer
|
|
134
123
|
* description: Number of grace logins already consumed.
|
|
135
124
|
* example: 0
|
|
125
|
+
* policy:
|
|
126
|
+
* type: string
|
|
127
|
+
* nullable: true
|
|
128
|
+
* description: |
|
|
129
|
+
* DN of the policy the status was computed with: the one the
|
|
130
|
+
* account's `pwdPolicySubentry` names, else the default one.
|
|
131
|
+
* example: cn=default,ou=policies,dc=example,dc=com
|
|
132
|
+
* PasswordPolicyEntry:
|
|
133
|
+
* type: object
|
|
134
|
+
* description: |
|
|
135
|
+
* A password policy entry. Only the attributes the entry holds are
|
|
136
|
+
* served; durations are in **seconds**. `pwdAttribute` and
|
|
137
|
+
* `pwdCheckModule` are read-only.
|
|
138
|
+
* required: [dn, cn, isDefault]
|
|
139
|
+
* properties:
|
|
140
|
+
* dn: { type: string, example: 'cn=default,ou=policies,dc=example,dc=com' }
|
|
141
|
+
* cn: { type: string, example: default }
|
|
142
|
+
* isDefault:
|
|
143
|
+
* type: boolean
|
|
144
|
+
* description: Whether this is the policy accounts without `pwdPolicySubentry` get.
|
|
145
|
+
* pwdAttribute: { type: string, example: userPassword }
|
|
146
|
+
* pwdMinAge: { type: integer }
|
|
147
|
+
* pwdMaxAge: { type: integer, example: 7776000 }
|
|
148
|
+
* pwdInHistory: { type: integer, example: 5 }
|
|
149
|
+
* pwdCheckQuality: { type: integer, minimum: 0, maximum: 2 }
|
|
150
|
+
* pwdMinLength: { type: integer, example: 12 }
|
|
151
|
+
* pwdMaxLength: { type: integer }
|
|
152
|
+
* pwdExpireWarning: { type: integer }
|
|
153
|
+
* pwdGraceAuthNLimit: { type: integer }
|
|
154
|
+
* pwdGraceExpiry: { type: integer }
|
|
155
|
+
* pwdLockout: { type: boolean }
|
|
156
|
+
* pwdLockoutDuration: { type: integer }
|
|
157
|
+
* pwdMaxFailure: { type: integer }
|
|
158
|
+
* pwdFailureCountInterval: { type: integer }
|
|
159
|
+
* pwdMustChange: { type: boolean }
|
|
160
|
+
* pwdAllowUserChange: { type: boolean }
|
|
161
|
+
* pwdSafeModify: { type: boolean }
|
|
162
|
+
* pwdMinDelay: { type: integer }
|
|
163
|
+
* pwdMaxDelay: { type: integer }
|
|
164
|
+
* pwdMaxIdle: { type: integer }
|
|
165
|
+
* pwdMaxRecordedFailure: { type: integer }
|
|
166
|
+
* pwdCheckModule: { type: string }
|
|
167
|
+
* pwdCheckModuleArg:
|
|
168
|
+
* type: string
|
|
169
|
+
* description: Configuration of the quality-check module, usually several lines.
|
|
170
|
+
* pwdUseCheckModule: { type: boolean }
|
|
171
|
+
* PasswordPolicyChanges:
|
|
172
|
+
* type: object
|
|
173
|
+
* description: |
|
|
174
|
+
* Attributes to set, by name as in `PasswordPolicyEntry`. `null` removes
|
|
175
|
+
* the attribute. On `PATCH`, `dn`, `cn`, `isDefault` and the read-only
|
|
176
|
+
* attributes are accepted only with the value the entry already has.
|
|
177
|
+
* additionalProperties: true
|
|
178
|
+
* example: { pwdMaxAge: 31536000, pwdLockout: true, pwdExpireWarning: null }
|
|
136
179
|
* ExpiringAccount:
|
|
137
180
|
* type: object
|
|
138
181
|
* description: User whose password is expiring within the warning window.
|
|
@@ -193,18 +236,22 @@ export default class PasswordPolicy extends DmPlugin {
|
|
|
193
236
|
name: string;
|
|
194
237
|
roles: Role[];
|
|
195
238
|
private policyCache;
|
|
196
|
-
private
|
|
239
|
+
private discoveredDefault;
|
|
240
|
+
private warnedAmbiguousDefault;
|
|
241
|
+
private warnedMissingBase;
|
|
242
|
+
private lastDefaultPolicy;
|
|
197
243
|
private readonly POLICY_CACHE_TTL;
|
|
198
244
|
api(app: Express): void;
|
|
199
245
|
/**
|
|
200
246
|
* Get password status for a user
|
|
201
247
|
*
|
|
202
|
-
* Every lookup
|
|
248
|
+
* Every account lookup and write carries the request, so an authorization
|
|
203
249
|
* plugin answers for the caller: without it, a route read as an internal
|
|
204
250
|
* call, and a delegated administrator could read or unlock any account of
|
|
205
251
|
* the directory (#325).
|
|
206
252
|
*/
|
|
207
253
|
private getPasswordStatus;
|
|
254
|
+
private readUser;
|
|
208
255
|
/**
|
|
209
256
|
* Calculate password status from LDAP entry and policy
|
|
210
257
|
*/
|
|
@@ -222,17 +269,47 @@ export default class PasswordPolicy extends DmPlugin {
|
|
|
222
269
|
*/
|
|
223
270
|
private findLockedAccounts;
|
|
224
271
|
/**
|
|
225
|
-
*
|
|
272
|
+
* The policy the overlay applies to an entry: the one its
|
|
273
|
+
* pwdPolicySubentry names, else the default one, which slapd also falls
|
|
274
|
+
* back to when the named entry holds no policy
|
|
275
|
+
*/
|
|
276
|
+
private policyFor;
|
|
277
|
+
/**
|
|
278
|
+
* Read the default ppolicy: --ppolicy-default-dn, else the only policy
|
|
279
|
+
* under the policy base
|
|
226
280
|
*/
|
|
227
281
|
private readPolicyConfig;
|
|
228
282
|
/**
|
|
229
|
-
*
|
|
283
|
+
* With several policies, the default one is named in slapd's
|
|
284
|
+
* configuration (olcPPolicyDefault), which this server cannot read
|
|
285
|
+
*/
|
|
286
|
+
private discoverDefaultDn;
|
|
287
|
+
/**
|
|
288
|
+
* Read the policy at `dn`, null when it holds none
|
|
289
|
+
*/
|
|
290
|
+
private readPolicyAt;
|
|
291
|
+
private forgetPolicies;
|
|
292
|
+
private policyBase;
|
|
293
|
+
/**
|
|
294
|
+
* The policy entries under the policy base matching `filter`, none when
|
|
295
|
+
* the base itself is missing
|
|
296
|
+
*/
|
|
297
|
+
private searchPolicies;
|
|
298
|
+
/**
|
|
299
|
+
* The policy named `cn` under the policy base
|
|
230
300
|
*/
|
|
231
|
-
private
|
|
301
|
+
private findPolicy;
|
|
302
|
+
private listPolicies;
|
|
303
|
+
private toPolicyRecord;
|
|
232
304
|
/**
|
|
233
|
-
*
|
|
305
|
+
* The directory form of a value a client sent for policy attribute `name`
|
|
234
306
|
*/
|
|
235
|
-
private
|
|
307
|
+
private policyValue;
|
|
308
|
+
private createPolicy;
|
|
309
|
+
private updatePolicy;
|
|
310
|
+
private deletePolicy;
|
|
311
|
+
private assignPolicy;
|
|
312
|
+
private param;
|
|
236
313
|
/**
|
|
237
314
|
* Parse GeneralizedTime (YYYYMMDDHHmmssZ) to Date
|
|
238
315
|
*/
|
|
@@ -52,7 +52,13 @@ export default class ClouderyProvision extends DmPlugin {
|
|
|
52
52
|
private createInstance;
|
|
53
53
|
/** Poll the workflow until it succeeds. Returns false on failure/timeout. */
|
|
54
54
|
private waitForWorkflow;
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* The instance at this FQDN, if any.
|
|
57
|
+
*
|
|
58
|
+
* @param fqdn instance address
|
|
59
|
+
* @param only the one field to ask for, when the others are not needed
|
|
60
|
+
*/
|
|
61
|
+
private findInstanceByFqdn;
|
|
56
62
|
private deleteInstance;
|
|
57
63
|
private publishUserCreated;
|
|
58
64
|
private publishUserDeleted;
|
|
@@ -1183,6 +1183,15 @@ move names both DNs:
|
|
|
1183
1183
|
}
|
|
1184
1184
|
```
|
|
1185
1185
|
|
|
1186
|
+
**DN that cannot be parsed** — an organization read or a move target that is
|
|
1187
|
+
not a DN (LDAP result code 34):
|
|
1188
|
+
|
|
1189
|
+
```json
|
|
1190
|
+
{
|
|
1191
|
+
"error": "garbage is not a valid DN"
|
|
1192
|
+
}
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1186
1195
|
#### 401 Unauthorized
|
|
1187
1196
|
|
|
1188
1197
|
**Missing Authentication:**
|
|
@@ -1227,6 +1236,16 @@ move names both DNs:
|
|
|
1227
1236
|
}
|
|
1228
1237
|
```
|
|
1229
1238
|
|
|
1239
|
+
**Entry or value the directory does not hold** — a modify or a delete of an
|
|
1240
|
+
entry that is not there, or the removal of a value the entry lacks (LDAP
|
|
1241
|
+
result codes 32 and 16). Removing a group member that is not one names both:
|
|
1242
|
+
|
|
1243
|
+
```json
|
|
1244
|
+
{
|
|
1245
|
+
"error": "uid=alice,ou=users,dc=example,dc=com is not a member of cn=backend-guild,ou=groups,dc=example,dc=com"
|
|
1246
|
+
}
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1230
1249
|
#### 409 Conflict
|
|
1231
1250
|
|
|
1232
1251
|
**Duplicate Entry:**
|
|
@@ -260,6 +260,22 @@ at `0` otherwise.
|
|
|
260
260
|
| `--trash-add-metadata` | `DM_TRASH_ADD_METADATA` | `true` | Add deletion metadata |
|
|
261
261
|
| `--trash-auto-create` | `DM_TRASH_AUTO_CREATE` | `true` | Auto-create trash container |
|
|
262
262
|
|
|
263
|
+
#### `core/ldap/passwordPolicy`
|
|
264
|
+
|
|
265
|
+
Policy management, password status and lockouts, see [password policy](plugins/ldap/password-policy.md).
|
|
266
|
+
|
|
267
|
+
| CLI | Env | Default | Description |
|
|
268
|
+
| ------------------------------- | -------------------------------- | ------------- | ------------------------------------------------------------------------------------------- |
|
|
269
|
+
| `--ppolicy-default-dn` | `DM_PPOLICY_DEFAULT_DN` | | Default policy, [needed with several](plugins/ldap/password-policy.md#which-policy-applies) |
|
|
270
|
+
| `--ppolicy-base` | `DM_PPOLICY_BASE` | `--ldap-base` | Where policies are looked for and created |
|
|
271
|
+
| `--ppolicy-warn-days` | `DM_PPOLICY_WARN_DAYS` | `14` | Days before expiration to flag as "expiring soon" |
|
|
272
|
+
| `--ppolicy-validate-complexity` | `DM_PPOLICY_VALIDATE_COMPLEXITY` | `false` | Enable `POST /password/validate` |
|
|
273
|
+
| `--ppolicy-min-length` | `DM_PPOLICY_MIN_LENGTH` | `12` | Minimum length checked by `/password/validate` |
|
|
274
|
+
| `--ppolicy-require-uppercase` | `DM_PPOLICY_REQUIRE_UPPERCASE` | `true` | Require an uppercase letter |
|
|
275
|
+
| `--ppolicy-require-lowercase` | `DM_PPOLICY_REQUIRE_LOWERCASE` | `true` | Require a lowercase letter |
|
|
276
|
+
| `--ppolicy-require-digit` | `DM_PPOLICY_REQUIRE_DIGIT` | `true` | Require a digit |
|
|
277
|
+
| `--ppolicy-require-special` | `DM_PPOLICY_REQUIRE_SPECIAL` | `true` | Require a special character |
|
|
278
|
+
|
|
263
279
|
#### `core/ldap/onChange`
|
|
264
280
|
|
|
265
281
|
Monitors LDAP modifications and triggers hooks for attribute changes. No configuration options - uses [Special Attributes](#special-attributes) settings.
|
|
@@ -36,6 +36,12 @@ gets no instance.
|
|
|
36
36
|
the publication and the mark sends it again on the next attempt, and two
|
|
37
37
|
replicas handling the same account at once may both send it. Within one
|
|
38
38
|
replica, the work on one account is done one step at a time.
|
|
39
|
+
- An entry another tool created, whose classes do not allow the address or
|
|
40
|
+
the mark, is given the auxiliary class of `--user-class` that allows it,
|
|
41
|
+
as the
|
|
42
|
+
[repair of entries created by other tools](../ldap/flat-generic.md#entries-created-by-other-tools)
|
|
43
|
+
does, whether the address comes from `workplace.created` or from making
|
|
44
|
+
sure of its instance.
|
|
39
45
|
- The server does not start without a reachable broker, and an event that
|
|
40
46
|
cannot be published fails its attempt rather than being marked as sent.
|
|
41
47
|
- With cozy-stack, deleting an account destroys its instance, when that
|
|
@@ -55,7 +61,9 @@ cannot write is refused before the provider or the broker hear of it.
|
|
|
55
61
|
|
|
56
62
|
Pending accounts are the entries under the `--twake-instance-dn` branches
|
|
57
63
|
with a mail and no address, for instance
|
|
58
|
-
`(&(mail=*)(!(twakeWorkspaceUrl=*)))`, technical accounts aside.
|
|
64
|
+
`(&(mail=*)(!(twakeWorkspaceUrl=*)))`, technical accounts aside. The route
|
|
65
|
+
can be called for each of them: an account already announced answers
|
|
66
|
+
`ready` and is left as it is.
|
|
59
67
|
|
|
60
68
|
## Organizations
|
|
61
69
|
|
|
@@ -122,11 +130,15 @@ organization entry, and is refused with a `403` otherwise.
|
|
|
122
130
|
already there is taken only when its email and OIDC id are this account's.
|
|
123
131
|
|
|
124
132
|
The instance id is `--twake-instance-id`, a template of the account's `uid`
|
|
125
|
-
and the named groups of the matching DN expression, for example
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
and the named groups of the matching DN expression, for example `{uid}{org}`.
|
|
134
|
+
It is lowercased and reduced to `[a-z0-9-]`: the Cloudery refuses other
|
|
135
|
+
characters in a slug, such as `_` or `@`, so `jane.doe_stg` gives
|
|
136
|
+
`janedoestg`. Two `uid`s that reduce to the same slug cannot both have an
|
|
137
|
+
instance: the second is refused, as an instance already there that is not its
|
|
138
|
+
own. A name no expression captures stops the server at startup. It is the slug
|
|
139
|
+
of the instance's address; the OIDC id is this slug with the Cloudery, and the
|
|
140
|
+
`uid` with cozy-stack. `user.created` names the account by its `uid`
|
|
141
|
+
(`twakeId`). A group named `org` is the organization id sent to the provider
|
|
130
142
|
and in `user.created`, with the mail's domain as its domain; with cozy-stack
|
|
131
143
|
and no such group, `--twake-instance-cozy-org-id` and
|
|
132
144
|
`--twake-instance-cozy-org-domain` stand for them.
|
|
@@ -15,7 +15,7 @@ Plugins for LDAP entity management.
|
|
|
15
15
|
| [on-change](on-change.md) | All | Change detection |
|
|
16
16
|
| [enterprise-rules](enterprise-rules.md) | All (business rules) | Uniqueness, Mail domains, Delete guards |
|
|
17
17
|
| [account-lifecycle](account-lifecycle.md) | Accounts | Enable/disable, Password reset |
|
|
18
|
-
| [password-policy](password-policy.md) | Password management | Expiration, Lockout, Unlock
|
|
18
|
+
| [password-policy](password-policy.md) | Password management | Policies, Expiration, Lockout, Unlock |
|
|
19
19
|
| [raw](raw.md) | Whole directory (read) | Root DSE, Schema, Tree, Raw search |
|
|
20
20
|
|
|
21
21
|
## API Endpoints
|
|
@@ -63,6 +63,12 @@ GET /api/v1/users/{id}/password-status # Get user password status
|
|
|
63
63
|
POST /api/v1/users/{id}/unlock # Unlock locked account
|
|
64
64
|
GET /api/v1/password-policy/expiring-soon # List expiring passwords
|
|
65
65
|
GET /api/v1/password-policy/locked-accounts # List locked accounts
|
|
66
|
+
GET /api/v1/password-policies # List policies
|
|
67
|
+
POST /api/v1/password-policies # Create a policy
|
|
68
|
+
GET /api/v1/password-policies/{cn} # Read a policy
|
|
69
|
+
PATCH /api/v1/password-policies/{cn} # Change a policy
|
|
70
|
+
DELETE /api/v1/password-policies/{cn} # Delete a policy
|
|
71
|
+
PUT /api/v1/users/{id}/password-policy # Assign a policy
|
|
66
72
|
POST /api/v1/password/validate # Validate complexity (optional)
|
|
67
73
|
```
|
|
68
74
|
|
|
@@ -99,7 +99,13 @@ It used to be taken as an absent one, so `{"password": ""}` answered
|
|
|
99
99
|
that emptied the field by accident believed it had set the password it typed.
|
|
100
100
|
Omitting the field is how a generated password is asked for.
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
A password longer than 256 characters, or holding a control character (a NUL
|
|
103
|
+
byte, a newline…), is refused too: nobody can type it at a login prompt. The
|
|
104
|
+
endpoint applies no minimum length or complexity rule of its own. These rules
|
|
105
|
+
belong to this endpoint: a password written as an attribute, through
|
|
106
|
+
`PUT /api/v1/ldap/{pluralName}/{id}` or a creation, is not checked.
|
|
107
|
+
|
|
108
|
+
`forceChange` (a boolean, true by default) also writes the `passwordReset`-role attribute
|
|
103
109
|
so the directory asks for a new password at next login. For a boolean
|
|
104
110
|
attribute the values written are `TRUE` and `FALSE`; a directory that spells
|
|
105
111
|
them otherwise says so through `states.required` and `states.cleared`.
|
|
@@ -120,6 +126,8 @@ them otherwise says so through `states.required` and `states.cleared`.
|
|
|
120
126
|
| ------ | -------------------------------------------------------------- |
|
|
121
127
|
| `400` | Unknown state, or a body with no `state` |
|
|
122
128
|
| `400` | A `password` that is present but empty, `null` or not a string |
|
|
129
|
+
| `400` | A `password` over 256 characters or with a control character |
|
|
130
|
+
| `400` | A `forceChange` that is not a boolean |
|
|
123
131
|
| `404` | No such account |
|
|
124
132
|
|
|
125
133
|
## See also
|
|
@@ -443,25 +443,27 @@ An entry written by another tool may lack an object class the schema
|
|
|
443
443
|
declares, and the directory then refuses an attribute that class carries: a
|
|
444
444
|
bare `inetOrgPerson` in the users branch cannot take `twakeDepartmentLink`,
|
|
445
445
|
which `twakeWhitePages` brings. When a modify through LDAP-Rest (this route,
|
|
446
|
-
`/move`, a state change…) writes such an attribute
|
|
447
|
-
|
|
448
|
-
the entry holds, or receives, that class's
|
|
449
|
-
the write needs is added, and a write the
|
|
450
|
-
changed: several entities may share a
|
|
451
|
-
`sshPublicKeys` all live in `ou=users`)
|
|
452
|
-
their classes.
|
|
446
|
+
`/move`, a state change…) writes such an attribute and the directory refuses
|
|
447
|
+
it, the write is sent again with the **auxiliary** class declared for the
|
|
448
|
+
branch that allows it, provided the entry holds, or receives, that class's
|
|
449
|
+
mandatory attributes. Only what the write needs is added, and a write the
|
|
450
|
+
directory would accept is never changed: several entities may share a
|
|
451
|
+
branch (`users`, `posixAccounts` and `sshPublicKeys` all live in `ou=users`)
|
|
452
|
+
without every entry being given all their classes.
|
|
453
453
|
|
|
454
454
|
A missing **structural** class, such as `twakeAccount`, cannot be added to an
|
|
455
455
|
existing entry: the write answers `409`, and the reason is logged once per
|
|
456
456
|
entry. The same applies to groups under `--ldap-group-base` (when it is set)
|
|
457
457
|
with `--group-class`, and to the organization tree with
|
|
458
|
-
`--ldap-organization-class
|
|
459
|
-
|
|
458
|
+
`--ldap-organization-class`, and to the accounts the Twake instance and
|
|
459
|
+
Cloudery provisioning plugins write, with `--user-class`. Nothing is added
|
|
460
|
+
when the change names `objectClass` itself.
|
|
460
461
|
|
|
461
462
|
The repair reads the directory schema, kept for
|
|
462
|
-
`--ldap-raw-schema-cache-ttl`, and the entry's classes
|
|
463
|
-
|
|
464
|
-
|
|
463
|
+
`--ldap-raw-schema-cache-ttl`, and the entry's classes only for a write the
|
|
464
|
+
directory refuses: a write it takes costs nothing more. That refused write
|
|
465
|
+
shows in the directory's logs (`err=65`) before the one adding the class.
|
|
466
|
+
`DM_LDAP_FLAT_AUTO_REPAIR=false` turns the repair off.
|
|
465
467
|
|
|
466
468
|
### Delete Entry
|
|
467
469
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Password Policy Plugin
|
|
2
2
|
|
|
3
|
-
The `passwordPolicy` plugin provides a REST API for managing OpenLDAP password policies (ppolicy overlay). It allows administrators to monitor password expiration, manage locked accounts, and optionally validate password complexity.
|
|
3
|
+
The `passwordPolicy` plugin provides a REST API for managing OpenLDAP password policies (ppolicy overlay). It allows administrators to edit the policies, monitor password expiration, manage locked accounts, and optionally validate password complexity.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -10,6 +10,7 @@ This plugin acts as an administration interface for OpenLDAP's ppolicy overlay.
|
|
|
10
10
|
- Account unlock functionality
|
|
11
11
|
- List users with expiring passwords
|
|
12
12
|
- List locked accounts
|
|
13
|
+
- [Policy management](#managing-policies): list, create, edit, delete, and assign to accounts
|
|
13
14
|
- Optional local password complexity validation
|
|
14
15
|
|
|
15
16
|
**Note:** ldap-rest is an administration backend. Users authenticate directly against OpenLDAP, not through ldap-rest. This plugin exposes ppolicy attributes via REST API for administrative purposes.
|
|
@@ -34,9 +35,24 @@ This plugin acts as an administration interface for OpenLDAP's ppolicy overlay.
|
|
|
34
35
|
| Argument | Environment Variable | Default | Description |
|
|
35
36
|
| ---------------------- | ----------------------- | ------------- | ------------------------------------------------- |
|
|
36
37
|
| `--ppolicy-default-dn` | `DM_PPOLICY_DEFAULT_DN` | (auto-detect) | DN of the default password policy entry |
|
|
38
|
+
| `--ppolicy-base` | `DM_PPOLICY_BASE` | `{ldap_base}` | Where policies are looked for and created |
|
|
37
39
|
| `--ppolicy-warn-days` | `DM_PPOLICY_WARN_DAYS` | `14` | Days before expiration to flag as "expiring soon" |
|
|
38
40
|
| `--ldap-users-base` | `DM_LDAP_USERS_BASE` | `{ldap_base}` | Base DN for user searches |
|
|
39
41
|
|
|
42
|
+
### Which policy applies
|
|
43
|
+
|
|
44
|
+
An account is judged by the policy its `pwdPolicySubentry` names, else by the
|
|
45
|
+
default one, as slapd does. The default policy is set in slapd's
|
|
46
|
+
configuration (`olcPPolicyDefault`), which this server cannot read: give it
|
|
47
|
+
with `--ppolicy-default-dn`. Without it, the plugin takes the only policy
|
|
48
|
+
under `--ppolicy-base`; when there are several, it takes none and logs a
|
|
49
|
+
warning, and accounts without `pwdPolicySubentry` are reported without
|
|
50
|
+
expiry.
|
|
51
|
+
|
|
52
|
+
The bind user must be able to read `pwdPolicySubentry`: hidden by an ACL, it
|
|
53
|
+
reads as absent and every account is judged by the default policy (see
|
|
54
|
+
[Cannot Read Operational Attributes](#cannot-read-operational-attributes)).
|
|
55
|
+
|
|
40
56
|
### Optional Complexity Validation
|
|
41
57
|
|
|
42
58
|
Enable local password complexity validation (for client-side feedback before submitting to LDAP):
|
|
@@ -124,7 +140,8 @@ curl "http://localhost:8081/api/v1/users/uid%3Djohn.doe%2Cou%3Dusers%2Cdc%3Dexam
|
|
|
124
140
|
"isLocked": false,
|
|
125
141
|
"lockedAt": null,
|
|
126
142
|
"failureCount": 0,
|
|
127
|
-
"graceLoginsUsed": 0
|
|
143
|
+
"graceLoginsUsed": 0,
|
|
144
|
+
"policy": "cn=default,ou=policies,dc=example,dc=com"
|
|
128
145
|
}
|
|
129
146
|
```
|
|
130
147
|
|
|
@@ -279,6 +296,69 @@ POST /api/v1/password/validate
|
|
|
279
296
|
}
|
|
280
297
|
```
|
|
281
298
|
|
|
299
|
+
## Managing policies
|
|
300
|
+
|
|
301
|
+
```http
|
|
302
|
+
GET /api/v1/password-policies # List
|
|
303
|
+
POST /api/v1/password-policies # Create
|
|
304
|
+
GET /api/v1/password-policies/{cn} # Read
|
|
305
|
+
PATCH /api/v1/password-policies/{cn} # Change
|
|
306
|
+
DELETE /api/v1/password-policies/{cn} # Delete
|
|
307
|
+
PUT /api/v1/users/{id}/password-policy # Assign to an account
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Policies are found by `cn` under `--ppolicy-base`. A policy is served with
|
|
311
|
+
the attributes it holds, typed: durations and counts as integers, `TRUE` /
|
|
312
|
+
`FALSE` as booleans, and `isDefault` marking the default one:
|
|
313
|
+
|
|
314
|
+
```json
|
|
315
|
+
{
|
|
316
|
+
"dn": "cn=software,ou=policies,dc=example,dc=com",
|
|
317
|
+
"cn": "software",
|
|
318
|
+
"isDefault": false,
|
|
319
|
+
"pwdAttribute": "userPassword",
|
|
320
|
+
"pwdMaxFailure": 10,
|
|
321
|
+
"pwdLockout": true,
|
|
322
|
+
"pwdMustChange": false
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Changing a policy:** `PATCH` sets the attributes it names; `null` removes
|
|
327
|
+
one. Sending back what `GET` served is accepted as long as `dn`, `cn`,
|
|
328
|
+
`isDefault` and the read-only attributes keep their value.
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
curl -X PATCH "http://localhost:8081/api/v1/password-policies/default" \
|
|
332
|
+
-H 'Content-Type: application/json' \
|
|
333
|
+
-d '{"pwdMaxAge": 31536000, "pwdExpireWarning": null}'
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
- `pwdAttribute` and `pwdCheckModule` are read-only. Before OpenLDAP 2.6,
|
|
337
|
+
`pwdCheckModule` is the path of a library slapd loads; set it in slapd.
|
|
338
|
+
- `pwdCheckModuleArg`, the configuration of the quality-check module (ppm,
|
|
339
|
+
for instance), is a string, usually of several lines. Writing it, or
|
|
340
|
+
`pwdUseCheckModule`, gives the entry the `pwdPolicyChecker` class. Both
|
|
341
|
+
need OpenLDAP 2.6; an older directory refuses them with a `400`.
|
|
342
|
+
- An attribute the directory's schema does not know is refused with a `400`
|
|
343
|
+
naming it.
|
|
344
|
+
|
|
345
|
+
**Creating a policy:** `POST` takes `cn` and the attributes to set. The entry
|
|
346
|
+
is created under `--ppolicy-base` as a `person` holding `pwdPolicy`, as in
|
|
347
|
+
the OpenLDAP administrator's guide, applied to `userPassword`. A policy of
|
|
348
|
+
the same name answers `409`. `--ppolicy-base` must exist: otherwise creating
|
|
349
|
+
answers `404` and listing finds nothing, with a warning in the log.
|
|
350
|
+
|
|
351
|
+
**Deleting a policy** is refused (`409`) while it is the default one or an
|
|
352
|
+
entry's `pwdPolicySubentry` names it. The default one is only known through
|
|
353
|
+
`--ppolicy-default-dn` when there are several: without it, nothing stops
|
|
354
|
+
deleting the policy slapd applies by default. Looking for the entries that
|
|
355
|
+
name the policy searches the whole of `--ldap-base`; unless
|
|
356
|
+
`pwdPolicySubentry` is indexed, that is a full scan on each delete.
|
|
357
|
+
|
|
358
|
+
**Assigning a policy:** `PUT /users/{id}/password-policy` with
|
|
359
|
+
`{"policy": "software"}` sets the account's `pwdPolicySubentry`; with
|
|
360
|
+
`{"policy": null}` the account gets the default policy again.
|
|
361
|
+
|
|
282
362
|
## OpenLDAP ppolicy Attributes
|
|
283
363
|
|
|
284
364
|
The plugin reads these operational attributes from user entries:
|
|
@@ -364,7 +444,8 @@ async function validatePassword(password) {
|
|
|
364
444
|
|
|
365
445
|
**Problem:** `GET /password-policy` returns `{}`
|
|
366
446
|
|
|
367
|
-
**Solution:**
|
|
447
|
+
**Solution:** With several policies, set `--ppolicy-default-dn` (see
|
|
448
|
+
[Which policy applies](#which-policy-applies)). Otherwise the ppolicy overlay may not be configured in OpenLDAP, or the ppolicy entry doesn't exist. Configure the overlay and create a pwdPolicy entry, through [`POST /password-policies`](#managing-policies) or:
|
|
368
449
|
|
|
369
450
|
```ldif
|
|
370
451
|
dn: cn=default,ou=policies,dc=example,dc=com
|
|
@@ -396,7 +477,7 @@ pwdLockoutDuration: 900
|
|
|
396
477
|
**Solution:** Ensure the LDAP bind user has permissions to read operational attributes. These require special ACLs in OpenLDAP:
|
|
397
478
|
|
|
398
479
|
```
|
|
399
|
-
access to attrs=pwdChangedTime,pwdAccountLockedTime,pwdFailureTime,pwdGraceUseTime,pwdReset
|
|
480
|
+
access to attrs=pwdChangedTime,pwdAccountLockedTime,pwdFailureTime,pwdGraceUseTime,pwdReset,pwdPolicySubentry
|
|
400
481
|
by dn="cn=admin,dc=example,dc=com" read
|
|
401
482
|
by self read
|
|
402
483
|
by * none
|
|
@@ -420,6 +501,9 @@ The plugin automatically exports its configuration via the `configApi` plugin. W
|
|
|
420
501
|
"unlockUser": "/api/v1/users/:id/unlock",
|
|
421
502
|
"getExpiringSoon": "/api/v1/password-policy/expiring-soon",
|
|
422
503
|
"getLockedAccounts": "/api/v1/password-policy/locked-accounts",
|
|
504
|
+
"listPolicies": "/api/v1/password-policies",
|
|
505
|
+
"policy": "/api/v1/password-policies/:cn",
|
|
506
|
+
"assignPolicy": "/api/v1/users/:id/password-policy",
|
|
423
507
|
"validatePassword": "/api/v1/password/validate"
|
|
424
508
|
},
|
|
425
509
|
"config": {
|
package/docs/usage/upgrading.md
CHANGED
|
@@ -4,6 +4,44 @@ What to check before deploying, newest first. Only releases that need a
|
|
|
4
4
|
decision or a configuration change appear here; see the
|
|
5
5
|
[CHANGELOG](../../CHANGELOG.md) for everything else.
|
|
6
6
|
|
|
7
|
+
## To 0.21.0
|
|
8
|
+
|
|
9
|
+
### Accounts another tool created
|
|
10
|
+
|
|
11
|
+
**Who is affected:** a deployment that loads `twake/instances` with the
|
|
12
|
+
Cloudery, where another tool creates accounts under `--twake-instance-dn`
|
|
13
|
+
without the classes of `--user-class` (a plain `inetOrgPerson`, say).
|
|
14
|
+
|
|
15
|
+
Those accounts have an instance but neither its address nor the mark: the
|
|
16
|
+
directory refused the write. Once deployed,
|
|
17
|
+
[make sure of each pending account](plugins/integrations/instances.md#making-sure-an-account-has-its-instance): the call gives it the class it needs.
|
|
18
|
+
|
|
19
|
+
### Several password policies
|
|
20
|
+
|
|
21
|
+
**Who is affected:** a deployment that loads `core/ldap/passwordPolicy`
|
|
22
|
+
without `--ppolicy-default-dn`, whose directory holds several `pwdPolicy`
|
|
23
|
+
entries.
|
|
24
|
+
|
|
25
|
+
`GET /password-policy` answers `{}` and accounts without
|
|
26
|
+
`pwdPolicySubentry` are reported without expiry, until
|
|
27
|
+
`--ppolicy-default-dn` names the policy slapd's `olcPPolicyDefault` names. A
|
|
28
|
+
warning in the log says so.
|
|
29
|
+
|
|
30
|
+
## To 0.20.3
|
|
31
|
+
|
|
32
|
+
### Members whose uid the Cloudery refused as a slug
|
|
33
|
+
|
|
34
|
+
**Who is affected:** a deployment that loads `twake/instances` with the
|
|
35
|
+
Cloudery, whose members were created through SCIM or the API with a `uid`
|
|
36
|
+
holding characters such as `_` or `@`.
|
|
37
|
+
|
|
38
|
+
Those members got no instance: the Cloudery refused their slug. Once
|
|
39
|
+
deployed,
|
|
40
|
+
[make sure of each pending account](plugins/integrations/instances.md#making-sure-an-account-has-its-instance).
|
|
41
|
+
Two members whose `uid`s reduce to the same slug cannot both have an
|
|
42
|
+
instance: the second call answers `409`,
|
|
43
|
+
[details](plugins/integrations/instances.md#providers).
|
|
44
|
+
|
|
7
45
|
## To 0.20.2
|
|
8
46
|
|
|
9
47
|
### Members of organizations created before 0.13.0
|
|
@@ -11,20 +49,12 @@ decision or a configuration change appear here; see the
|
|
|
11
49
|
**Who is affected:** a deployment that loads `twake/instances` with the
|
|
12
50
|
Cloudery and has organizations created before 0.13.0 that got members since.
|
|
13
51
|
|
|
14
|
-
Those members have neither an address
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
release them. Once deployed, find them under each organization entry with
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
(&(uid=*)(mail=*)(!(twakeWorkspaceUrl=*))(!(twakeCreatedEventAt=*)))
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
and call `POST /api/v1/twake/instances/ensure` with `{ "dn": … }` for each,
|
|
52
|
+
Those members have neither an address nor the mark, and adding another
|
|
53
|
+
member does not release them. Once deployed,
|
|
54
|
+
[make sure of each pending account](plugins/integrations/instances.md#making-sure-an-account-has-its-instance),
|
|
24
55
|
as a caller who may write the organization entry. The first call announces
|
|
25
|
-
the organization
|
|
26
|
-
|
|
27
|
-
[details](plugins/integrations/instances.md#organizations).
|
|
56
|
+
the organization, and each member's `user.created` follows once its instance
|
|
57
|
+
is built, [details](plugins/integrations/instances.md#organizations).
|
|
28
58
|
|
|
29
59
|
## To 0.20.0
|
|
30
60
|
|
package/examples/web/README.md
CHANGED
|
@@ -116,9 +116,13 @@ npm start -- --plugin core/ldap/raw
|
|
|
116
116
|
```bash
|
|
117
117
|
npm start -- \
|
|
118
118
|
--plugin core/auth/totp \
|
|
119
|
-
--auth-totp "JBSWY3DPEHPK3PXP:admin:6"
|
|
119
|
+
--auth-totp "JBSWY3DPEHPK3PXP:admin:6" \
|
|
120
|
+
--auth-path-prefix /api
|
|
120
121
|
```
|
|
121
122
|
|
|
123
|
+
`--auth-path-prefix /api` leaves the example pages themselves reachable
|
|
124
|
+
without a code.
|
|
125
|
+
|
|
122
126
|
3. **Open examples in browser:**
|
|
123
127
|
```
|
|
124
128
|
http://localhost:8081/static/examples/web/totp-client.html
|
|
@@ -97,7 +97,6 @@
|
|
|
97
97
|
processFormData,
|
|
98
98
|
loadPointerOptions,
|
|
99
99
|
replaceSchemaPlaceholders,
|
|
100
|
-
getOrganizationPath,
|
|
101
100
|
} = window.MiniDmShared;
|
|
102
101
|
|
|
103
102
|
let editor;
|
|
@@ -136,12 +135,6 @@
|
|
|
136
135
|
throw new Error('No schema available for users');
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
// Get organization path for twakeDepartmentPath
|
|
140
|
-
const orgPath = await getOrganizationPath(
|
|
141
|
-
currentOrgDn,
|
|
142
|
-
window.location.origin
|
|
143
|
-
);
|
|
144
|
-
|
|
145
138
|
// Replace placeholders in schema branches
|
|
146
139
|
const ldapBase = editor.getConfig().ldapBase;
|
|
147
140
|
userSchema = replaceSchemaPlaceholders(userSchema, {
|
|
@@ -170,9 +163,17 @@
|
|
|
170
163
|
userSchema.attributes
|
|
171
164
|
)) {
|
|
172
165
|
if (attribute.fixed) continue; // Skip fixed fields like 'dn'
|
|
166
|
+
// The server sets these, and refuses them from a client
|
|
167
|
+
if (
|
|
168
|
+
attribute.readOnly ||
|
|
169
|
+
(attribute.generated &&
|
|
170
|
+
(fieldName !== userSchema.entity?.mainAttribute ||
|
|
171
|
+
attribute.generatedFrom))
|
|
172
|
+
)
|
|
173
|
+
continue;
|
|
173
174
|
if (fieldName === 'objectClass') continue; // Already handled
|
|
174
175
|
|
|
175
|
-
//
|
|
176
|
+
// The organization is the one selected in the tree
|
|
176
177
|
if (fieldName === 'twakeDepartmentLink') {
|
|
177
178
|
fieldsHtml += `
|
|
178
179
|
<input type="hidden" name="twakeDepartmentLink" value="${escapeHtml(currentOrgDn)}" />
|
|
@@ -180,13 +181,6 @@
|
|
|
180
181
|
continue;
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
if (fieldName === 'twakeDepartmentPath') {
|
|
184
|
-
fieldsHtml += `
|
|
185
|
-
<input type="hidden" name="twakeDepartmentPath" value="${escapeHtml(orgPath)}" />
|
|
186
|
-
`;
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
184
|
fieldsHtml += await generateFormField(fieldName, attribute, {
|
|
191
185
|
pointerOptionsLoader: pointerLoader,
|
|
192
186
|
});
|