ldap-rest 0.11.2 → 0.13.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 (56) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/bin/index.js +1 -1
  3. package/dist/config/args.js +1 -1
  4. package/dist/lib/authz/composition.js +1 -1
  5. package/dist/lib/errors.js +1 -1
  6. package/dist/lib/instanceProviders.js +1 -0
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/plugins/auth/openidconnect.js +1 -1
  9. package/dist/plugins/configApi.js +1 -1
  10. package/dist/plugins/ldap/groups.js +1 -1
  11. package/dist/plugins/ldap/trash.js +1 -1
  12. package/dist/plugins/scim/filter.js +1 -1
  13. package/dist/plugins/scim/groups.js +1 -1
  14. package/dist/plugins/scim/mapping.js +1 -1
  15. package/dist/plugins/scim/patch.js +1 -1
  16. package/dist/plugins/scim/users.js +1 -1
  17. package/dist/plugins/twake/groups.js +1 -0
  18. package/dist/plugins/twake/instances.js +1 -0
  19. package/dist/plugins/twake/lifecycleAttributes.js +1 -0
  20. package/dist/plugins/twake/lifecycleEvents.js +1 -0
  21. package/dist/plugins/twake/tombstone.js +1 -0
  22. package/dist/src/abstract/ldapFlat.d.ts +34 -6
  23. package/dist/src/abstract/plugin.d.ts +7 -4
  24. package/dist/src/config/args.d.ts +60 -0
  25. package/dist/src/hooks.d.ts +2 -0
  26. package/dist/src/lib/authz/composition.d.ts +1 -1
  27. package/dist/src/lib/errors.d.ts +3 -0
  28. package/dist/src/lib/instanceProviders.d.ts +88 -0
  29. package/dist/src/lib/ldapActions.d.ts +10 -2
  30. package/dist/src/plugins/auth/openidconnect.d.ts +39 -0
  31. package/dist/src/plugins/ldap/groups.d.ts +5 -0
  32. package/dist/src/plugins/scim/filter.d.ts +2 -1
  33. package/dist/src/plugins/scim/groups.d.ts +1 -0
  34. package/dist/src/plugins/scim/mapping.d.ts +22 -8
  35. package/dist/src/plugins/scim/patch.d.ts +9 -2
  36. package/dist/src/plugins/twake/groups.d.ts +89 -0
  37. package/dist/src/plugins/twake/instances.d.ts +102 -0
  38. package/dist/src/plugins/twake/lifecycleAttributes.d.ts +29 -0
  39. package/dist/src/plugins/twake/lifecycleEvents.d.ts +62 -0
  40. package/dist/src/plugins/twake/tombstone.d.ts +86 -0
  41. package/docs/client-development/api/rest-api.md +34 -2
  42. package/docs/plugin-development/hooks.md +24 -0
  43. package/docs/usage/configuration.md +97 -1
  44. package/docs/usage/plugins/README.md +10 -6
  45. package/docs/usage/plugins/auth/oidc.md +46 -0
  46. package/docs/usage/plugins/integrations/README.md +12 -4
  47. package/docs/usage/plugins/integrations/groups.md +225 -0
  48. package/docs/usage/plugins/integrations/instances.md +150 -0
  49. package/docs/usage/plugins/integrations/lifecycle-events.md +233 -0
  50. package/docs/usage/plugins/integrations/scim.md +55 -34
  51. package/docs/usage/plugins/integrations/tombstone.md +107 -0
  52. package/docs/usage/plugins/ldap/flat-generic.md +38 -2
  53. package/docs/usage/plugins/ldap/trash.md +4 -4
  54. package/docs/usage/upgrading.md +114 -0
  55. package/package.json +25 -1
  56. package/static/schemas/twake/organizationGroups.json +32 -0
@@ -191,7 +191,7 @@ at `0` otherwise.
191
191
  | CLI | Env | Default | Description |
192
192
  | ----------------------- | ------------------------ | ------- | --------------------------- |
193
193
  | `--trash-base` | `DM_TRASH_BASE` | | Trash container DN |
194
- | `--trash-watched-bases` | `DM_TRASH_WATCHED_BASES` | | DNs to watch for deletions |
194
+ | `--trash-watched-bases` | `DM_TRASH_WATCHED_BASES` | | `;`-separated DNs to watch |
195
195
  | `--trash-add-metadata` | `DM_TRASH_ADD_METADATA` | `true` | Add deletion metadata |
196
196
  | `--trash-auto-create` | `DM_TRASH_AUTO_CREATE` | `true` | Auto-create trash container |
197
197
 
@@ -445,6 +445,102 @@ Automatically creates/updates/deletes applicative account entries when users are
445
445
 
446
446
  **Requires:** `core/ldap/onChange`
447
447
 
448
+ #### `core/twake/lifecycleEvents`
449
+
450
+ | CLI | Env | Default | Description |
451
+ | ---------------------------------------- | ----------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------- |
452
+ | `--twake-lifecycle-role-attribute` | `DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE` | | Role attribute |
453
+ | `--twake-lifecycle-lock-attribute` | `DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE` | `--scim-user-lock-attribute` | Lock attribute |
454
+ | `--twake-lifecycle-lock-value` | `DM_TWAKE_LIFECYCLE_LOCK_VALUE` | see description | Value marking an account locked: SCIM's with SCIM's attribute, required with another than `pwdAccountLockedTime` |
455
+ | `--twake-lifecycle-deleted-attribute` | `DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE` | | Attribute marking a tombstone |
456
+ | `--twake-lifecycle-deleted-value` | `DM_TWAKE_LIFECYCLE_DELETED_VALUE` | `TRUE` | Value marking a tombstone |
457
+ | `--twake-lifecycle-deleted-at-attribute` | `DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE` | | Deletion date attribute |
458
+ | `--twake-lifecycle-deleted-at-format` | `DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT` | `iso8601` | `iso8601` or `generalizedTime` |
459
+ | `--twake-lifecycle-rules` | `DM_TWAKE_LIFECYCLE_RULES` | | Rules: a JSON file, or the JSON itself |
460
+ | `--twake-lifecycle-member-attribute` | `DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE` | `member` | Attribute holding a group's members |
461
+
462
+ See [lifecycle events](plugins/integrations/lifecycle-events.md).
463
+
464
+ **Requires:** `core/ldap/onChange`, `core/rabbitmq`
465
+
466
+ #### `core/twake/instances`
467
+
468
+ | CLI | Env | Default | Description |
469
+ | ------------------------------------------------ | ------------------------------------------------- | ------------------------------------- | --------------------------------------------------------- |
470
+ | `--twake-instance-dn` | `DM_TWAKE_INSTANCE_DN` | `[]` | DN expressions of the accounts to equip |
471
+ | `--twake-instance-skip-attribute` | `DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE` | | Attribute marking an account with no instance |
472
+ | `--twake-instance-skip-value` | `DM_TWAKE_INSTANCE_SKIP_VALUE` | | Its value |
473
+ | `--twake-instance-provider` | `DM_TWAKE_INSTANCE_PROVIDER` | `cloudery` | `cloudery` or `cozy-stack` |
474
+ | `--twake-instance-id` | `DM_TWAKE_INSTANCE_ID` | `{uid}` | Instance id template |
475
+ | `--twake-instance-fqdn-attribute` | `DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE` | `twakeWorkspaceUrl` | Address attribute |
476
+ | `--twake-instance-sent-attribute` | `DM_TWAKE_INSTANCE_SENT_ATTRIBUTE` | `twakeCreatedEventAt` | Date the creation event was sent |
477
+ | `--twake-instance-locale` | `DM_TWAKE_INSTANCE_LOCALE` | `en`, `fr` with cozy-stack | Locale when the entry has none |
478
+ | `--twake-instance-timeout` | `DM_TWAKE_INSTANCE_TIMEOUT` | `30000` | Milliseconds a provider call may take |
479
+ | `--twake-instance-cloudery-url` | `DM_TWAKE_INSTANCE_CLOUDERY_URL` | | Cloudery API |
480
+ | `--twake-instance-cloudery-token` | `DM_TWAKE_INSTANCE_CLOUDERY_TOKEN` | | Its token |
481
+ | `--twake-instance-cloudery-domain` | `DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN` | | Domain of the instances |
482
+ | `--twake-instance-cloudery-offer` | `DM_TWAKE_INSTANCE_CLOUDERY_OFFER` | `b2b_twake_default` | Offer of an account's instance |
483
+ | `--twake-instance-cloudery-organization-offer` | `DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER` | | Offer of an organization's instance |
484
+ | `--twake-instance-cozy-url` | `DM_TWAKE_INSTANCE_COZY_URL` | | cozy-stack admin API |
485
+ | `--twake-instance-cozy-user` | `DM_TWAKE_INSTANCE_COZY_USER` | `admin` | Its user |
486
+ | `--twake-instance-cozy-passphrase` | `DM_TWAKE_INSTANCE_COZY_PASSPHRASE` | | Its passphrase |
487
+ | `--twake-instance-cozy-domain` | `DM_TWAKE_INSTANCE_COZY_DOMAIN` | | Domain of the instances |
488
+ | `--twake-instance-cozy-context` | `DM_TWAKE_INSTANCE_COZY_CONTEXT` | `default` | cozy-stack context |
489
+ | `--twake-instance-cozy-apps` | `DM_TWAKE_INSTANCE_COZY_APPS` | `home,drive,settings,notes,dataproxy` | Apps installed on a new instance |
490
+ | `--twake-instance-cozy-org-id` | `DM_TWAKE_INSTANCE_COZY_ORG_ID` | | Organization id when the DN gives none |
491
+ | `--twake-instance-cozy-org-domain` | `DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN` | | Organization domain when the DN gives none |
492
+ | `--twake-instance-auth-exchange` | `DM_TWAKE_INSTANCE_AUTH_EXCHANGE` | `auth` | Exchange of account events |
493
+ | `--twake-instance-b2b-exchange` | `DM_TWAKE_INSTANCE_B2B_EXCHANGE` | `b2b` | Exchange of organization events |
494
+ | `--twake-instance-user-created-key` | `DM_TWAKE_INSTANCE_USER_CREATED_KEY` | `user.created` | Routing key of an account's creation |
495
+ | `--twake-instance-organization-created-key` | `DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY` | `organization.created` | Routing key of an organization's creation |
496
+ | `--twake-instance-workplace-created-key` | `DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY` | `workplace.created` | Routing key the Cloudery announces on |
497
+ | `--twake-instance-queue` | `DM_TWAKE_INSTANCE_QUEUE` | `workplace.created.ldap-rest` | Queue shared by the replicas |
498
+ | `--twake-instance-organization-base` | `DM_TWAKE_INSTANCE_ORGANIZATION_BASE` | | Branch of the organization entries |
499
+ | `--twake-instance-organization-id-attribute` | `DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE` | `ou` | Attribute holding an organization's id |
500
+ | `--twake-instance-organization-domain-attribute` | `DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE` | `twakeDomain` | Attribute holding an organization's domain |
501
+ | `--twake-instance-organization-name-attribute` | `DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE` | `description` | Attribute holding an organization's name |
502
+ | `--twake-instance-organization-fqdn-attribute` | `DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE` | | Address attribute of an organization (empty: not written) |
503
+
504
+ See [instances](plugins/integrations/instances.md).
505
+
506
+ **Requires:** `core/rabbitmq`
507
+
508
+ #### `core/twake/tombstone`
509
+
510
+ Reads the `--twake-lifecycle-*` attributes above, and:
511
+
512
+ | CLI | Env | Default | Description |
513
+ | ------------------------------------ | ------------------------------------- | ------------------- | ----------------------------------------- |
514
+ | `--twake-lifecycle-reason-attribute` | `DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE` | | Deletion reason attribute |
515
+ | `--twake-tombstone-dn` | `DM_TWAKE_TOMBSTONE_DN` | `[]` | DN patterns of entries kept as tombstones |
516
+ | `--twake-tombstone-default-reason` | `DM_TWAKE_TOMBSTONE_DEFAULT_REASON` | `deleted` | Reason when none is given |
517
+ | `--twake-tombstone-reason-header` | `DM_TWAKE_TOMBSTONE_REASON_HEADER` | `x-deletion-reason` | Request header carrying the reason |
518
+ | `--twake-tombstone-reasons` | `DM_TWAKE_TOMBSTONE_REASONS` | `[]` | Accepted reasons (empty: any) |
519
+ | `--twake-tombstone-clear-attributes` | `DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES` | `[]` | Attributes removed from a tombstone |
520
+ | `--twake-tombstone-erase-min-age` | `DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE` | `2592000` | Seconds before a tombstone may be erased |
521
+ | `--twake-tombstone-group-bases` | `DM_TWAKE_TOMBSTONE_GROUP_BASES` | `--ldap-group-base` | Where memberships are removed at erase |
522
+
523
+ See [tombstone](plugins/integrations/tombstone.md).
524
+
525
+ #### `core/twake/groups`
526
+
527
+ Extends `core/ldap/groups`, whose options it reads too.
528
+
529
+ | CLI | Env | Default | Description |
530
+ | --------------------------------------------- | ---------------------------------------------- | ------------------ | ----------------------------------------------------- |
531
+ | `--twake-group-base` | `DM_TWAKE_GROUP_BASE` | | Group branch of an organization, with `{org}` |
532
+ | `--twake-group-user-base` | `DM_TWAKE_GROUP_USER_BASE` | | User branch of an organization, with `{org}` |
533
+ | `--twake-group-organization-dn` | `DM_TWAKE_GROUP_ORGANIZATION_DN` | | Organization entry, with `{org}` (empty: not checked) |
534
+ | `--twake-group-organization-status-attribute` | `DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE` | `twakeOrgStatus` | Organization status attribute |
535
+ | `--twake-group-organization-deleted-value` | `DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE` | `deleted` | Status of a deleted organization |
536
+ | `--twake-group-display-name-attribute` | `DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE` | `twakeDisplayName` | Group display name attribute |
537
+ | `--twake-group-color-attribute` | `DM_TWAKE_GROUP_COLOR_ATTRIBUTE` | `twakeGroupColor` | Group color attribute |
538
+ | `--twake-group-created-at-attribute` | `DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE` | `twakeCreatedAt` | Group creation date attribute |
539
+ | `--twake-group-max-page-limit` | `DM_TWAKE_GROUP_MAX_PAGE_LIMIT` | `1000` | Largest page, and most users added at once |
540
+ | `--twake-group-member-fields` | `DM_TWAKE_GROUP_MEMBER_FIELDS` | `{}` | Member profile fields over the defaults, as JSON |
541
+
542
+ See [groups](plugins/integrations/groups.md).
543
+
448
544
  ### Utility Plugins
449
545
 
450
546
  #### `core/static`
@@ -53,12 +53,16 @@ Protection and rate limiting:
53
53
 
54
54
  Connect to external systems:
55
55
 
56
- | Plugin | Description |
57
- | -------------------------------------------------- | ---------------------------------- |
58
- | [james-mail](integrations/james-mail.md) | Apache James synchronization |
59
- | [james-mailboxes](integrations/james-mailboxes.md) | Apache James team mailboxes |
60
- | [calendar](integrations/calendar.md) | Twake Calendar resources and users |
61
- | [app-accounts](integrations/app-accounts.md) | Applicative accounts API |
56
+ | Plugin | Description |
57
+ | ---------------------------------------------------- | ------------------------------------ |
58
+ | [james-mail](integrations/james-mail.md) | Apache James synchronization |
59
+ | [james-mailboxes](integrations/james-mailboxes.md) | Apache James team mailboxes |
60
+ | [calendar](integrations/calendar.md) | Twake Calendar resources and users |
61
+ | [app-accounts](integrations/app-accounts.md) | Applicative accounts API |
62
+ | [lifecycle-events](integrations/lifecycle-events.md) | Account lifecycle events to RabbitMQ |
63
+ | [instances](integrations/instances.md) | Workplace instance of each account |
64
+ | [tombstone](integrations/tombstone.md) | Deleted accounts kept as tombstones |
65
+ | [groups](integrations/groups.md) | Organization groups |
62
66
 
63
67
  ## Utility Plugins
64
68
 
@@ -85,6 +85,52 @@ client (`Accept: application/json`) receives 401. Every other authentication
85
85
  plugin in this repository answers JSON 401 throughout — the difference is
86
86
  kept because it is what makes a login work at all.
87
87
 
88
+ ## Session lifetime
89
+
90
+ A session lasts no longer than its access token. Once the token has expired,
91
+ the next request renews it with the refresh token, when the provider gave
92
+ one; without a refresh token, or when the provider refuses the renewal, the
93
+ session ends and the request is answered as an unauthenticated one: the
94
+ browser goes back through the provider, which signs the caller in again
95
+ without asking anything while its own session lasts.
96
+
97
+ So that renewals do not cost a round trip through the provider, have it
98
+ issue refresh tokens to this client (LemonLDAP::NG: _Issue refresh tokens_
99
+ in the relying party's options). A token the provider gives no expiry keeps
100
+ the session for the cookie's own lifetime.
101
+
102
+ Nothing serialises the renewals: requests arriving together with the same
103
+ expired cookie each send the refresh token. A provider that rotates refresh
104
+ tokens refuses all but one, and the requests it refused end their session:
105
+ the caller signs in again, silently while the provider's session lasts.
106
+
107
+ ## Logout
108
+
109
+ `/logout` ends the session at the provider as well (RP-Initiated Logout): the
110
+ browser is sent to the provider's `end_session_endpoint` with the id token as
111
+ `id_token_hint`, and the provider closes its own session — and, with
112
+ [Back-Channel Logout](back-channel-logout.md), tells the other applications.
113
+ No `post_logout_redirect_uri` is sent: the caller ends on the provider's own
114
+ logout page, and nothing has to be registered there.
115
+
116
+ Ending the cookie alone would not log anyone out: every guarded path goes
117
+ through the provider, which still holds its session and logs the caller back
118
+ in without asking anything.
119
+
120
+ A provider whose discovery document names no `end_session_endpoint` gets a
121
+ local logout — the cookie is dropped, the browser goes to `--base-url` — and
122
+ a warning saying the provider may log the caller back in.
123
+
124
+ `GET /v1/config` (with `core/configApi`) publishes the route, so a front-end
125
+ knows it can offer a sign-out control. The key is `openidconnect` whatever
126
+ the instance is named:
127
+
128
+ ```json
129
+ "features": {
130
+ "openidconnect": { "enabled": true, "endpoints": { "logout": "/logout" } }
131
+ }
132
+ ```
133
+
88
134
  ## How It Works
89
135
 
90
136
  1. Uses `express-openid-connect` for OAuth2/OIDC flow
@@ -11,10 +11,14 @@ Plugins for connecting LDAP-Rest to external systems.
11
11
 
12
12
  ## Twake
13
13
 
14
- | Plugin | Description |
15
- | ------------------------------- | ---------------------------------------- |
16
- | [calendar](calendar.md) | Calendar resources and users sync |
17
- | [app-accounts](app-accounts.md) | Applicative accounts API (devices, apps) |
14
+ | Plugin | Description |
15
+ | --------------------------------------- | ---------------------------------------- |
16
+ | [calendar](calendar.md) | Calendar resources and users sync |
17
+ | [app-accounts](app-accounts.md) | Applicative accounts API (devices, apps) |
18
+ | [lifecycle-events](lifecycle-events.md) | Account lifecycle events to RabbitMQ |
19
+ | [instances](instances.md) | Workplace instance of each account |
20
+ | [tombstone](tombstone.md) | Deleted accounts kept as tombstones |
21
+ | [groups](groups.md) | Organization groups |
18
22
 
19
23
  ## Prerequisites
20
24
 
@@ -39,4 +43,8 @@ core/twake/james
39
43
 
40
44
  core/twake/calendar
41
45
  └─ requires: core/ldap/onChange
46
+
47
+ core/twake/lifecycleEvents
48
+ ├─ requires: core/ldap/onChange
49
+ └─ requires: core/rabbitmq
42
50
  ```
@@ -0,0 +1,225 @@
1
+ # Organization Groups
2
+
3
+ `core/twake/groups` is [`core/ldap/groups`](../ldap/groups.md) with one group
4
+ branch per organization. It serves the groups of an organization under
5
+ `/api/v1/organizations/:id/groups`, in place of the flat `/api/v1/ldap/groups`.
6
+
7
+ ## Behaviour
8
+
9
+ - An organization's groups and users live in branches given by DN patterns,
10
+ where `{org}` stands for the organization id.
11
+ - A group is a `groupOfNames` with a generated `cn` (a UUID), its name in the
12
+ display name attribute, an optional color and its creation date.
13
+ - A group's members belong to its organization: adding a user of another
14
+ organization, or of no organization, is refused with a 400, whichever API
15
+ writes the group (REST, SCIM, another plugin). The placeholder
16
+ (`--group-dummy-user`) is the one exception.
17
+ - An empty group holds the placeholder, because `groupOfNames` needs a member.
18
+ The placeholder is never listed.
19
+ - A tombstone (see [tombstone](tombstone.md)) keeps its memberships until it
20
+ is erased, and is hidden from every member list read through LDAP-Rest.
21
+ A SCIM `PUT` of a group replaces its whole member list, so it drops the
22
+ hidden tombstones too; the routes below add and remove one member at a
23
+ time and keep them. Hiding them costs one search of each organization's
24
+ user branch per search that returns a group with its members, whichever
25
+ plugin makes it.
26
+ - An erased member leaves its groups through the directory: enable the
27
+ `refint` overlay on `member`. Unlike `core/ldap/groups`, this plugin does
28
+ not remove a deleted entry from its groups itself, since that write would
29
+ announce a member removed on an erase.
30
+ - Every route of an organization whose status says deleted answers 410.
31
+
32
+ ## Routes
33
+
34
+ Every route answers 404 `ORGANIZATION_NOT_FOUND` when the organization entry
35
+ is missing, and 410 `ORGANIZATION_DELETED` when it is deleted. Errors are
36
+ `{ "error": "...", "code": "..." }`.
37
+
38
+ A group reads:
39
+
40
+ ```json
41
+ {
42
+ "id": "8f14e45f-ceea-467d-9a3e-7c2b0a1d5e10",
43
+ "cn": "8f14e45f-ceea-467d-9a3e-7c2b0a1d5e10",
44
+ "displayName": "Engineering",
45
+ "description": "Engineering team",
46
+ "color": "#3366FF",
47
+ "organizationId": "acme",
48
+ "baseDN": "cn=8f14e45f-ceea-467d-9a3e-7c2b0a1d5e10,ou=groups,ou=acme,dc=example,dc=com",
49
+ "members": ["jdoe"],
50
+ "createdAt": "2026-01-23T11:00:00.000Z"
51
+ }
52
+ ```
53
+
54
+ `members` are the RDN values of the organization's users. A group without a
55
+ display name reads its `cn`.
56
+
57
+ - `GET /groups?page&limit&search&sortBy&sortOrder`: `search` (2 characters
58
+ or more) matches the display name and the description; `sortBy` is
59
+ `displayName`, `description` or `createdAt`. Answers
60
+ `{ organizationId, groups, pagination: { page, limit, total, totalPages } }`.
61
+ - `POST /groups` with `{ name, description?, color? }`: `name` is at most 256
62
+ characters, not blank, without control characters, and unique in the
63
+ organization among groups made here (409 `GROUP_EXISTS`); `color` is `#RGB`
64
+ or `#RRGGBB`. Answers 201 with the group.
65
+ - `GET /groups/:groupId`: the group, or 404 `GROUP_NOT_FOUND`.
66
+ - `PATCH /groups/:groupId` with any of `name`, `description`, `color`: an
67
+ empty `description` or `color` clears it; any other field is a 400.
68
+ - `DELETE /groups/:groupId`.
69
+ - `GET /groups/:groupId/members?page&limit&search&sortBy&sortOrder`: the
70
+ members' public profiles, `sortBy` `uid` (default), `displayName`, `mail`
71
+ or `jobTitle`. A member that cannot be read is `{ "uid": "..." }`.
72
+ - `POST /groups/:groupId/members` with `{ usernames: [...] }`: every user must
73
+ exist in the organization (404 `USER_NOT_FOUND`, nothing added), at most
74
+ `--twake-group-max-page-limit` per request.
75
+ - `DELETE /groups/:groupId/members/:userId`: 404 `MEMBER_NOT_FOUND` for a user
76
+ who is not a member.
77
+
78
+ Name uniqueness holds for groups made through these routes only: an identity
79
+ provider may push two groups of one name, and a SCIM create is never refused
80
+ for it.
81
+
82
+ ## Configuration
83
+
84
+ ```bash
85
+ --plugin core/twake/groups \
86
+ --twake-group-base 'ou=groups,ou={org},dc=example,dc=com' \
87
+ --twake-group-user-base 'ou=users,ou={org},dc=example,dc=com' \
88
+ --twake-group-organization-dn 'ou={org},dc=example,dc=com' \
89
+ --group-class top,groupOfNames,twakeGroup \
90
+ --group-dummy-user cn=placeholder \
91
+ --group-schema static/schemas/twake/organizationGroups.json
92
+ ```
93
+
94
+ - `--group-schema`: `static/schemas/twake/organizationGroups.json` declares
95
+ the groups' `cn`, members and description. The default group schema
96
+ describes department groups and requires `twakeDepartmentLink`, which these
97
+ groups do not have. Groups created through SCIM are written directly and
98
+ are not checked against this schema.
99
+ - `--twake-group-base`, `--twake-group-user-base`: required, each with
100
+ `{org}`.
101
+ - `--twake-group-organization-dn`: the organization entry. Empty means no
102
+ organization is checked.
103
+ - `--twake-group-organization-status-attribute` (default `twakeOrgStatus`),
104
+ `--twake-group-organization-deleted-value` (default `deleted`).
105
+ - `--twake-group-display-name-attribute` (default `twakeDisplayName`),
106
+ `--twake-group-color-attribute` (default `twakeGroupColor`),
107
+ `--twake-group-created-at-attribute` (default `twakeCreatedAt`): the
108
+ group's attributes. The group classes (`--group-class`) must allow them.
109
+ The plugin adds them to the loaded group schema under these names, in any
110
+ case, so a renamed attribute needs no schema of its own.
111
+ - `--twake-group-max-page-limit` (default 1000): the largest page, and the
112
+ most users added in one request.
113
+ - `--twake-group-member-fields`: JSON of `{ field: attribute }` for the member
114
+ profile, over the defaults `uid`, `_id` (`entryUUID`), `cn`, `sn`,
115
+ `givenName`, `displayName`, `mail`, `mobile`, `jobTitle`
116
+ (`twakeJobTitle`), `company` (`twakeCompany`), `organizationRole`
117
+ (`twakeOrganizationRole`) and `organizationId` (`twakeOrganizationId`).
118
+ A profile also has `name` from `sn`, `givenName`, `twakeAdditionalName` and
119
+ `twakeNamePrefix`, and `isTechnical` from `twakeIsTechnical`.
120
+ - Tombstones are recognized by `--twake-lifecycle-deleted-attribute` and
121
+ `--twake-lifecycle-deleted-value`, as [tombstone](tombstone.md) writes them.
122
+
123
+ ## Events
124
+
125
+ [Lifecycle events](lifecycle-events.md) publishes a group's changes from the
126
+ directory writes, whichever API made them. A rule for the pattern above:
127
+
128
+ ```json
129
+ {
130
+ "dn": "^cn=(?<id>[^,]+),ou=groups,ou=(?<org>[^,]+),dc=example,dc=com$",
131
+ "exchange": "groups",
132
+ "events": {
133
+ "created": {
134
+ "routingKey": "group.created",
135
+ "payload": {
136
+ "organizationId": "$dn.org",
137
+ "id": "$dn.id",
138
+ "name": "$twakeDisplayName",
139
+ "description": "$description",
140
+ "color": "$twakeGroupColor",
141
+ "createdAt": "$twakeCreatedAt",
142
+ "members": { "$members": { "username": "$uid", "email": "$mail" } },
143
+ "timestamp": "$now"
144
+ }
145
+ },
146
+ "updated": {
147
+ "routingKey": "group.updated",
148
+ "payload": {
149
+ "organizationId": "$dn.org",
150
+ "id": "$dn.id",
151
+ "name": "$changed.twakeDisplayName",
152
+ "description": "$changed.description",
153
+ "color": "$changed.twakeGroupColor",
154
+ "timestamp": "$now"
155
+ }
156
+ },
157
+ "memberAdded": {
158
+ "routingKey": "group.member.added",
159
+ "payload": {
160
+ "organizationId": "$dn.org",
161
+ "id": "$dn.id",
162
+ "members": {
163
+ "$added": {
164
+ "username": "$uid",
165
+ "email": "$mail",
166
+ "firstName": "$givenName",
167
+ "lastName": "$sn"
168
+ }
169
+ },
170
+ "timestamp": "$now"
171
+ }
172
+ },
173
+ "memberRemoved": {
174
+ "routingKey": "group.member.removed",
175
+ "payload": {
176
+ "organizationId": "$dn.org",
177
+ "id": "$dn.id",
178
+ "members": { "$removed": { "username": "$uid", "email": "$mail" } },
179
+ "timestamp": "$now"
180
+ }
181
+ },
182
+ "deleted": {
183
+ "routingKey": "group.deleted",
184
+ "payload": {
185
+ "organizationId": "$dn.org",
186
+ "id": "$dn.id",
187
+ "timestamp": "$now"
188
+ }
189
+ }
190
+ }
191
+ }
192
+ ```
193
+
194
+ Erasing a member publishes no `memberRemoved` when the directory's `refint`
195
+ removes it from its groups, since that write does not go through LDAP-Rest.
196
+ Without `refint`, the [tombstone](tombstone.md) erase takes the member out
197
+ of the groups under `--twake-tombstone-group-bases` itself, through
198
+ LDAP-Rest: each of those groups then publishes a `memberRemoved` naming the
199
+ member by its RDN. That option defaults to the flat group base, which holds
200
+ no group here; point it at the organizations' group branches.
201
+
202
+ ## SCIM groups
203
+
204
+ SCIM groups take the organization's group branch through SCIM's per-request
205
+ group base (`--scim-group-base-header`, `--scim-base-header-root`), the same
206
+ classes through `--scim-group-object-class`, and the provider's `externalId`
207
+ through `--scim-group-external-id-attribute`. Map SCIM `displayName` to the
208
+ display name attribute in `--scim-group-mapping`: the name is kept there,
209
+ SCIM reads, updates and `displayName` filters go through it, and a SCIM group
210
+ of an organization gets a generated `cn` (a UUID), which is also its SCIM
211
+ `id`, and a creation date, as a group made by the routes does. The date is
212
+ added only to a group that holds the display name attribute, so classes that
213
+ allow the display name must allow the creation date too: the default
214
+ attributes both come with `twakeGroup`, but a display name kept in an
215
+ attribute `groupOfNames` allows, such as `description`, next to the default
216
+ `twakeCreatedAt` makes every SCIM create fail. Without that mapping, a SCIM
217
+ group's `cn` is its `displayName`, and the plugin logs a warning at startup. A group whose `cn` is already its `displayName` keeps
218
+ that `cn` and `id`.
219
+
220
+ ## Dependencies
221
+
222
+ ```
223
+ core/twake/groups
224
+ └─ extends: core/ldap/groups
225
+ ```
@@ -0,0 +1,150 @@
1
+ # Workplace Instances
2
+
3
+ `core/twake/instances` gives every account this server creates its workplace
4
+ instance. It replaces `core/twake/cozyProvision` and
5
+ `core/twake/clouderyProvision`, and refuses to start when either is loaded
6
+ too. What changes when moving to it is in the
7
+ [upgrade notes](../../upgrading.md#twakeinstances-replaces-cozyprovision-and-clouderyprovision).
8
+
9
+ Every account must be created through this server (REST, SCIM, the LSC
10
+ plugin, scripts through the API). An entry written straight to the directory
11
+ gets no instance.
12
+
13
+ ## Behaviour
14
+
15
+ - Once an add has written an entry whose DN matches `--twake-instance-dn`,
16
+ the instance is looked up, then asked for if missing, before the add
17
+ answers (through the
18
+ [`ldapaddafter`](../../../plugin-development/hooks.md#ldapaddafter) hook).
19
+ An entry holding `--twake-instance-skip-value` in
20
+ `--twake-instance-skip-attribute`, a technical account for instance, gets
21
+ none, and neither does a tombstone
22
+ (`--twake-lifecycle-deleted-attribute`) put back in place.
23
+ - The address (`--twake-instance-fqdn-attribute`) and the mark
24
+ (`--twake-instance-sent-attribute`) are this plugin's to write: an add that
25
+ carries them has them removed, and an address written by a modify is taken
26
+ only when it is the one the provider gives the account, or once the
27
+ provider confirms it.
28
+ - A refused, unreachable or slow provider leaves the entry written and
29
+ without an address: it is pending, and the error is logged. The add itself
30
+ succeeds, whatever the door (REST, SCIM, bulk import).
31
+ `--twake-instance-timeout` bounds each call, so a lookup, a creation and,
32
+ with cozy-stack, the onboarding can hold an add up to three times as long.
33
+ - Once the instance exists, its address is written, `user.created` is
34
+ published on the auth exchange, without a password hash, and the entry is
35
+ marked with the date. The event is sent at least once: a failure between
36
+ the publication and the mark sends it again on the next attempt, and two
37
+ replicas handling the same account at once may both send it. Within one
38
+ replica, the work on one account is done one step at a time.
39
+ - The server does not start without a reachable broker, and an event that
40
+ cannot be published fails its attempt rather than being marked as sent.
41
+ - With cozy-stack, deleting an account destroys its instance, when that
42
+ instance's OIDC id is this account's.
43
+
44
+ ## Making sure an account has its instance
45
+
46
+ `POST /api/v1/twake/instances/ensure` with `{ "dn": … }` or `{ "mail": … }`
47
+ (or `ensureInstance(dn)` from another plugin) writes a missing address and
48
+ announces the account when its instance exists, and asks for the instance
49
+ when it does not. It answers `ready` or `pending`, and `400` when the body
50
+ names no account.
51
+
52
+ The route reads and writes the account with the caller's rights: an account
53
+ the caller cannot read answers `404`, as a missing one does, and one it
54
+ cannot write is refused before the provider or the broker hear of it.
55
+
56
+ Pending accounts are the entries under the `--twake-instance-dn` branches
57
+ with a mail and no address, for instance
58
+ `(&(mail=*)(!(twakeWorkspaceUrl=*)))`, technical accounts aside.
59
+
60
+ ## Organizations
61
+
62
+ `ensureOrganization({ id, name, domain })` asks for an organization's own
63
+ instance, for the application that creates organizations to call once it
64
+ has written the organization entry. With the Cloudery it creates the
65
+ Cloudery organization first. The organization entry, found under
66
+ `--twake-instance-organization-base` by
67
+ `--twake-instance-organization-id-attribute`, gets the address when
68
+ `--twake-instance-organization-fqdn-attribute` names where. With the
69
+ Cloudery the organization is then linked to its instance (`PATCH
70
+ /api/v2/organizations/<id>` with `instance_fqdn`), which the Cloudery needs
71
+ before it takes its members' `user.created`. Then `organization.created` is
72
+ published on the b2b exchange, and the entry is marked. The message carries
73
+ the organization's id, address, domain, and its name as `organization`, read
74
+ from `--twake-instance-organization-name-attribute`, which
75
+ `ensureOrganization` fills from `name` when the entry has none. The mark, not
76
+ the address, says the organization is done.
77
+
78
+ With the Cloudery, a member's `user.created` waits for its organization's: a
79
+ member under an organization entry that is not marked yet gets its address
80
+ but no event and no mark. A member outside its organization entry is not
81
+ held, since nothing would find it to release it: it is announced with a
82
+ warning, and needs sending again once the organization is linked. A stored
83
+ address the provider does not confirm keeps a member held, with a warning,
84
+ until `ensureInstance` sends it. Once the organization is
85
+ marked, every member under its entry with an address and no mark gets its
86
+ `user.created`, each on its own: one that fails is logged, the others are
87
+ still sent, and the message is redelivered. A stored address is sent only
88
+ if it is the one the provider gives the account, or once the provider
89
+ confirms it. Nothing of this is kept in memory: the organization's
90
+ `workplace.created` again, or `ensureOrganization` on a marked organization,
91
+ links the organization again and sends whatever is still held, from any
92
+ replica. That also links an organization marked before the link existed.
93
+
94
+ The Cloudery builds no member instance for an organization it does not
95
+ have. Before asking for a member's instance, the plugin asks for its
96
+ organization and the organization's instance when the Cloudery has neither,
97
+ so making sure of a member also repairs an organization whose creation
98
+ failed.
99
+
100
+ ## Providers
101
+
102
+ - `cloudery`: the Cloudery accepts the request and builds the instance
103
+ afterwards. An instance already at the account's address is taken when it
104
+ is this account's, so a re-created account keeps its instance. The address
105
+ arrives with `workplace.created`, consumed on `--twake-instance-queue`,
106
+ with a dead-letter queue. The message goes to an organization when its
107
+ `twakeId` is an organization id and its `internalEmail` that
108
+ organization's own (`<id>@<domain>`); otherwise to the account with that
109
+ `internalEmail`.
110
+ - `cozy-stack`: the admin API builds the instance before answering, and the
111
+ address is written in the request. The instance's onboarding is marked as
112
+ finished, so its first login does not open the setup wizard. An instance
113
+ already there is taken only when its email and OIDC id are this account's.
114
+
115
+ The instance id is `--twake-instance-id`, a template of the account's `uid`
116
+ and the named groups of the matching DN expression, for example
117
+ `{uid}{org}`; dots are dropped. A name no expression captures stops the
118
+ server at startup. It is the slug of the instance's address; the OIDC id is
119
+ this slug with the Cloudery, and the `uid` with cozy-stack. `user.created`
120
+ names the account by its `uid` (`twakeId`). A group named `org` is the organization id sent to the provider
121
+ and in `user.created`, with the mail's domain as its domain; with cozy-stack
122
+ and no such group, `--twake-instance-cozy-org-id` and
123
+ `--twake-instance-cozy-org-domain` stand for them.
124
+
125
+ ## Configuration
126
+
127
+ ```bash
128
+ --plugin core/twake/instances \
129
+ --twake-instance-dn '^uid=(?<uid>[^,]+),ou=users,ou=(?<org>[^,]+),ou=organizations,dc=example,dc=com$' \
130
+ --twake-instance-id '{uid}{org}' \
131
+ --twake-instance-skip-attribute employeeType \
132
+ --twake-instance-skip-value technical \
133
+ --twake-instance-provider cloudery \
134
+ --twake-instance-cloudery-url https://manager.example.com \
135
+ --twake-instance-cloudery-token … \
136
+ --twake-instance-cloudery-domain example.com \
137
+ --twake-instance-cloudery-organization-offer organization \
138
+ --twake-instance-organization-base ou=organizations,dc=example,dc=com
139
+ ```
140
+
141
+ A DN holds commas, so in `DM_TWAKE_INSTANCE_DN` end each expression with `;`.
142
+ Every option and its default is listed in the
143
+ [configuration reference](../../configuration.md). The plugin needs
144
+ `--rabbitmq-url`.
145
+
146
+ The directory schema must define the address and mark attributes, and with
147
+ `--twake-instance-organization-base` the organization domain (and address)
148
+ attributes: the defaults `twakeWorkspaceUrl`, `twakeCreatedEventAt` and
149
+ `twakeDomain` come from the Twake schema, which this repository does not
150
+ ship. The server refuses to start when one is missing.