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.
Files changed (85) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/config/args.js +1 -1
  3. package/dist/config/schema.js +1 -1
  4. package/dist/lib/expressFormatedResponses.js +1 -1
  5. package/dist/lib/instanceProviders.js +1 -1
  6. package/dist/lib/ldapActions.js +1 -1
  7. package/dist/plugins/ldap/accountLifecycle.js +1 -1
  8. package/dist/plugins/ldap/groups.js +1 -1
  9. package/dist/plugins/ldap/organizations.js +1 -1
  10. package/dist/plugins/ldap/passwordPolicy.js +1 -1
  11. package/dist/plugins/ldap/raw.js +1 -1
  12. package/dist/plugins/twake/clouderyProvision.js +1 -1
  13. package/dist/plugins/twake/instances.js +1 -1
  14. package/dist/src/browser/ldap-group-editor/api/GroupApiClient.d.ts +10 -5
  15. package/dist/src/browser/ldap-group-editor/types.d.ts +8 -0
  16. package/dist/src/browser/ldap-resource-editor/api/ResourceApiClient.d.ts +7 -1
  17. package/dist/src/browser/ldap-unit-editor/api/UnitApiClient.d.ts +7 -5
  18. package/dist/src/browser/ldap-user-editor/LdapUserEditor.d.ts +2 -0
  19. package/dist/src/browser/ldap-user-editor/api/UserApiClient.d.ts +7 -1
  20. package/dist/src/browser/shared/utils/form.d.ts +30 -0
  21. package/dist/src/config/args.d.ts +1 -0
  22. package/dist/src/config/schema.d.ts +30 -1
  23. package/dist/src/lib/instanceProviders.d.ts +11 -0
  24. package/dist/src/lib/ldapActions.d.ts +21 -3
  25. package/dist/src/plugins/ldap/accountLifecycle.d.ts +2 -0
  26. package/dist/src/plugins/ldap/passwordPolicy.d.ts +95 -18
  27. package/dist/src/plugins/twake/clouderyProvision.d.ts +7 -1
  28. package/docs/client-development/api/rest-api.md +19 -0
  29. package/docs/usage/configuration.md +16 -0
  30. package/docs/usage/plugins/integrations/instances.md +18 -6
  31. package/docs/usage/plugins/ldap/README.md +7 -1
  32. package/docs/usage/plugins/ldap/account-lifecycle.md +9 -1
  33. package/docs/usage/plugins/ldap/flat-generic.md +14 -12
  34. package/docs/usage/plugins/ldap/password-policy.md +88 -4
  35. package/docs/usage/upgrading.md +43 -13
  36. package/examples/web/README.md +5 -1
  37. package/examples/web/ldap-user-editor.html +9 -15
  38. package/examples/web/totp-client.html +51 -14
  39. package/package.json +1 -1
  40. package/static/browser/ldap-group-editor/api/GroupApiClient.js +27 -13
  41. package/static/browser/ldap-group-editor/api/GroupApiClient.js.map +1 -1
  42. package/static/browser/ldap-group-editor/components/CreateGroupModal.js +5 -17
  43. package/static/browser/ldap-group-editor/components/CreateGroupModal.js.map +1 -1
  44. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js +28 -12
  45. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js.map +1 -1
  46. package/static/browser/ldap-group-editor/components/MoveGroupModal.js.map +1 -1
  47. package/static/browser/ldap-group-editor.esm.js +114 -42
  48. package/static/browser/ldap-group-editor.esm.js.map +1 -1
  49. package/static/browser/ldap-group-editor.js +114 -42
  50. package/static/browser/ldap-group-editor.js.map +1 -1
  51. package/static/browser/ldap-group-editor.min.js +1 -1
  52. package/static/browser/ldap-group-editor.min.js.map +1 -1
  53. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js +8 -2
  54. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js.map +1 -1
  55. package/static/browser/ldap-unit-editor/api/UnitApiClient.js +8 -13
  56. package/static/browser/ldap-unit-editor/api/UnitApiClient.js.map +1 -1
  57. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js +5 -2
  58. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js.map +1 -1
  59. package/static/browser/ldap-unit-editor.esm.js +52 -15
  60. package/static/browser/ldap-unit-editor.esm.js.map +1 -1
  61. package/static/browser/ldap-unit-editor.js +52 -15
  62. package/static/browser/ldap-unit-editor.js.map +1 -1
  63. package/static/browser/ldap-unit-editor.min.js +1 -1
  64. package/static/browser/ldap-unit-editor.min.js.map +1 -1
  65. package/static/browser/ldap-user-editor/LdapUserEditor.js +22 -21
  66. package/static/browser/ldap-user-editor/LdapUserEditor.js.map +1 -1
  67. package/static/browser/ldap-user-editor/api/UserApiClient.js +8 -2
  68. package/static/browser/ldap-user-editor/api/UserApiClient.js.map +1 -1
  69. package/static/browser/ldap-user-editor/components/UserEditor.js +19 -9
  70. package/static/browser/ldap-user-editor/components/UserEditor.js.map +1 -1
  71. package/static/browser/ldap-user-editor.esm.js +88 -32
  72. package/static/browser/ldap-user-editor.esm.js.map +1 -1
  73. package/static/browser/ldap-user-editor.js +88 -32
  74. package/static/browser/ldap-user-editor.js.map +1 -1
  75. package/static/browser/ldap-user-editor.min.js +1 -1
  76. package/static/browser/ldap-user-editor.min.js.map +1 -1
  77. package/static/browser/shared/index.js +1 -1
  78. package/static/browser/shared/utils/form.js +51 -1
  79. package/static/browser/shared/utils/form.js.map +1 -1
  80. package/static/browser/shared.esm.js +51 -1
  81. package/static/browser/shared.esm.js.map +1 -1
  82. package/static/browser/shared.js +53 -0
  83. package/static/browser/shared.js.map +1 -1
  84. package/static/browser/shared.min.js +1 -1
  85. 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 policyCacheTime;
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 of this plugin carries the request, so an authorization
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
- * Read ppolicy configuration from LDAP
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
- * Parse ppolicy entry to config object
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 parsePolicyEntry;
301
+ private findPolicy;
302
+ private listPolicies;
303
+ private toPolicyRecord;
232
304
  /**
233
- * Parse number from LDAP attribute value
305
+ * The directory form of a value a client sent for policy attribute `name`
234
306
  */
235
- private parseNumber;
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
- private findInstanceUuidByFqdn;
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
- `{uid}{org}`; dots are dropped. A name no expression captures stops the
127
- server at startup. It is the slug of the instance's address; the OIDC id is
128
- this slug with the Cloudery, and the `uid` with cozy-stack. `user.created`
129
- names the account by its `uid` (`twakeId`). A group named `org` is the organization id sent to the provider
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
- `forceChange` (true by default) also writes the `passwordReset`-role attribute
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, the **auxiliary** class
447
- declared for the branch that allows it is added in the same write, provided
448
- the entry holds, or receives, that class's mandatory attributes. Only what
449
- the write needs is added, and a write the directory would accept is never
450
- changed: several entities may share a branch (`users`, `posixAccounts` and
451
- `sshPublicKeys` all live in `ou=users`) without every entry being given all
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`. Nothing is added when the change names
459
- `objectClass` itself.
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 when a written
463
- attribute belongs to a declared auxiliary class. `DM_LDAP_FLAT_AUTO_REPAIR=false`
464
- turns it off.
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:** The ppolicy overlay may not be configured in OpenLDAP, or the ppolicy entry doesn't exist. Configure the overlay and create a pwdPolicy entry:
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": {
@@ -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 (`twakeWorkspaceUrl`, or
15
- `--twake-instance-fqdn-attribute`) nor the mark (`twakeCreatedEventAt`, or
16
- `--twake-instance-sent-attribute`), and adding another member does not
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. Each call asks for the member's instance, and the member's
26
- `user.created` follows once it is built,
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
 
@@ -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
- // Auto-fill twakeDepartmentLink and twakeDepartmentPath
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
  });