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.
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/authz/composition.js +1 -1
- package/dist/lib/errors.js +1 -1
- package/dist/lib/instanceProviders.js +1 -0
- package/dist/lib/ldapActions.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/configApi.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/scim/filter.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/twake/groups.js +1 -0
- package/dist/plugins/twake/instances.js +1 -0
- package/dist/plugins/twake/lifecycleAttributes.js +1 -0
- package/dist/plugins/twake/lifecycleEvents.js +1 -0
- package/dist/plugins/twake/tombstone.js +1 -0
- package/dist/src/abstract/ldapFlat.d.ts +34 -6
- package/dist/src/abstract/plugin.d.ts +7 -4
- package/dist/src/config/args.d.ts +60 -0
- package/dist/src/hooks.d.ts +2 -0
- package/dist/src/lib/authz/composition.d.ts +1 -1
- package/dist/src/lib/errors.d.ts +3 -0
- package/dist/src/lib/instanceProviders.d.ts +88 -0
- package/dist/src/lib/ldapActions.d.ts +10 -2
- package/dist/src/plugins/auth/openidconnect.d.ts +39 -0
- package/dist/src/plugins/ldap/groups.d.ts +5 -0
- package/dist/src/plugins/scim/filter.d.ts +2 -1
- package/dist/src/plugins/scim/groups.d.ts +1 -0
- package/dist/src/plugins/scim/mapping.d.ts +22 -8
- package/dist/src/plugins/scim/patch.d.ts +9 -2
- package/dist/src/plugins/twake/groups.d.ts +89 -0
- package/dist/src/plugins/twake/instances.d.ts +102 -0
- package/dist/src/plugins/twake/lifecycleAttributes.d.ts +29 -0
- package/dist/src/plugins/twake/lifecycleEvents.d.ts +62 -0
- package/dist/src/plugins/twake/tombstone.d.ts +86 -0
- package/docs/client-development/api/rest-api.md +34 -2
- package/docs/plugin-development/hooks.md +24 -0
- package/docs/usage/configuration.md +97 -1
- package/docs/usage/plugins/README.md +10 -6
- package/docs/usage/plugins/auth/oidc.md +46 -0
- package/docs/usage/plugins/integrations/README.md +12 -4
- package/docs/usage/plugins/integrations/groups.md +225 -0
- package/docs/usage/plugins/integrations/instances.md +150 -0
- package/docs/usage/plugins/integrations/lifecycle-events.md +233 -0
- package/docs/usage/plugins/integrations/scim.md +55 -34
- package/docs/usage/plugins/integrations/tombstone.md +107 -0
- package/docs/usage/plugins/ldap/flat-generic.md +38 -2
- package/docs/usage/plugins/ldap/trash.md +4 -4
- package/docs/usage/upgrading.md +114 -0
- package/package.json +25 -1
- 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
|
|
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
|
|
57
|
-
|
|
|
58
|
-
| [james-mail](integrations/james-mail.md)
|
|
59
|
-
| [james-mailboxes](integrations/james-mailboxes.md)
|
|
60
|
-
| [calendar](integrations/calendar.md)
|
|
61
|
-
| [app-accounts](integrations/app-accounts.md)
|
|
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
|
|
15
|
-
|
|
|
16
|
-
| [calendar](calendar.md)
|
|
17
|
-
| [app-accounts](app-accounts.md)
|
|
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.
|