ldap-rest 0.11.2 → 0.12.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/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/authz/composition.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/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/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/plugin.d.ts +7 -4
- package/dist/src/config/args.d.ts +48 -0
- package/dist/src/hooks.d.ts +2 -0
- package/dist/src/lib/authz/composition.d.ts +1 -1
- package/dist/src/lib/instanceProviders.d.ts +86 -0
- package/dist/src/lib/ldapActions.d.ts +10 -2
- package/dist/src/plugins/auth/openidconnect.d.ts +24 -0
- package/dist/src/plugins/scim/mapping.d.ts +4 -1
- package/dist/src/plugins/scim/patch.d.ts +7 -0
- package/dist/src/plugins/twake/instances.d.ts +88 -0
- package/dist/src/plugins/twake/lifecycleAttributes.d.ts +29 -0
- package/dist/src/plugins/twake/lifecycleEvents.d.ts +45 -0
- package/dist/src/plugins/twake/tombstone.d.ts +86 -0
- package/docs/client-development/api/rest-api.md +2 -0
- package/docs/plugin-development/hooks.md +24 -0
- package/docs/usage/configuration.md +75 -0
- package/docs/usage/plugins/README.md +9 -6
- package/docs/usage/plugins/auth/oidc.md +27 -0
- package/docs/usage/plugins/integrations/README.md +11 -4
- package/docs/usage/plugins/integrations/instances.md +122 -0
- package/docs/usage/plugins/integrations/lifecycle-events.md +200 -0
- package/docs/usage/plugins/integrations/scim.md +11 -0
- package/docs/usage/plugins/integrations/tombstone.md +115 -0
- package/docs/usage/upgrading.md +58 -0
- package/package.json +17 -1
|
@@ -53,12 +53,15 @@ 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 |
|
|
62
65
|
|
|
63
66
|
## Utility Plugins
|
|
64
67
|
|
|
@@ -85,6 +85,33 @@ 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
|
+
## Logout
|
|
89
|
+
|
|
90
|
+
`/logout` ends the session at the provider as well (RP-Initiated Logout): the
|
|
91
|
+
browser is sent to the provider's `end_session_endpoint` with the id token as
|
|
92
|
+
`id_token_hint`, and the provider closes its own session — and, with
|
|
93
|
+
[Back-Channel Logout](back-channel-logout.md), tells the other applications.
|
|
94
|
+
No `post_logout_redirect_uri` is sent: the caller ends on the provider's own
|
|
95
|
+
logout page, and nothing has to be registered there.
|
|
96
|
+
|
|
97
|
+
Ending the cookie alone would not log anyone out: every guarded path goes
|
|
98
|
+
through the provider, which still holds its session and logs the caller back
|
|
99
|
+
in without asking anything.
|
|
100
|
+
|
|
101
|
+
A provider whose discovery document names no `end_session_endpoint` gets a
|
|
102
|
+
local logout — the cookie is dropped, the browser goes to `--base-url` — and
|
|
103
|
+
a warning saying the provider may log the caller back in.
|
|
104
|
+
|
|
105
|
+
`GET /v1/config` (with `core/configApi`) publishes the route, so a front-end
|
|
106
|
+
knows it can offer a sign-out control. The key is `openidconnect` whatever
|
|
107
|
+
the instance is named:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
"features": {
|
|
111
|
+
"openidconnect": { "enabled": true, "endpoints": { "logout": "/logout" } }
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
88
115
|
## How It Works
|
|
89
116
|
|
|
90
117
|
1. Uses `express-openid-connect` for OAuth2/OIDC flow
|
|
@@ -11,10 +11,13 @@ 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 |
|
|
18
21
|
|
|
19
22
|
## Prerequisites
|
|
20
23
|
|
|
@@ -39,4 +42,8 @@ core/twake/james
|
|
|
39
42
|
|
|
40
43
|
core/twake/calendar
|
|
41
44
|
└─ requires: core/ldap/onChange
|
|
45
|
+
|
|
46
|
+
core/twake/lifecycleEvents
|
|
47
|
+
├─ requires: core/ldap/onChange
|
|
48
|
+
└─ requires: core/rabbitmq
|
|
42
49
|
```
|
|
@@ -0,0 +1,122 @@
|
|
|
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, then
|
|
69
|
+
`organization.created` is published on the b2b exchange, and the entry is
|
|
70
|
+
marked. The mark, not the address, says the organization is done.
|
|
71
|
+
|
|
72
|
+
## Providers
|
|
73
|
+
|
|
74
|
+
- `cloudery`: the Cloudery accepts the request and builds the instance
|
|
75
|
+
afterwards. An instance already at the account's address is taken when it
|
|
76
|
+
is this account's, so a re-created account keeps its instance. The address
|
|
77
|
+
arrives with `workplace.created`, consumed on `--twake-instance-queue`,
|
|
78
|
+
with a dead-letter queue. The message goes to an organization when its
|
|
79
|
+
`twakeId` is an organization id and its `internalEmail` that
|
|
80
|
+
organization's own (`<id>@<domain>`); otherwise to the account with that
|
|
81
|
+
`internalEmail`.
|
|
82
|
+
- `cozy-stack`: the admin API builds the instance before answering, and the
|
|
83
|
+
address is written in the request. The instance's onboarding is marked as
|
|
84
|
+
finished, so its first login does not open the setup wizard. An instance
|
|
85
|
+
already there is taken only when its email and OIDC id are this account's.
|
|
86
|
+
|
|
87
|
+
The instance id is `--twake-instance-id`, a template of the account's `uid`
|
|
88
|
+
and the named groups of the matching DN expression, for example
|
|
89
|
+
`{uid}{org}`; dots are dropped. A name no expression captures stops the
|
|
90
|
+
server at startup. It is the slug of the instance's address; the OIDC id is
|
|
91
|
+
this slug with the Cloudery, and the `uid` with cozy-stack. `user.created`
|
|
92
|
+
names the account by its `uid` (`twakeId`). A group named `org` is the organization id sent to the provider
|
|
93
|
+
and in `user.created`, with the mail's domain as its domain; with cozy-stack
|
|
94
|
+
and no such group, `--twake-instance-cozy-org-id` and
|
|
95
|
+
`--twake-instance-cozy-org-domain` stand for them.
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
--plugin core/twake/instances \
|
|
101
|
+
--twake-instance-dn '^uid=(?<uid>[^,]+),ou=users,ou=(?<org>[^,]+),ou=organizations,dc=example,dc=com$' \
|
|
102
|
+
--twake-instance-id '{uid}{org}' \
|
|
103
|
+
--twake-instance-skip-attribute employeeType \
|
|
104
|
+
--twake-instance-skip-value technical \
|
|
105
|
+
--twake-instance-provider cloudery \
|
|
106
|
+
--twake-instance-cloudery-url https://manager.example.com \
|
|
107
|
+
--twake-instance-cloudery-token … \
|
|
108
|
+
--twake-instance-cloudery-domain example.com \
|
|
109
|
+
--twake-instance-cloudery-organization-offer organization \
|
|
110
|
+
--twake-instance-organization-base ou=organizations,dc=example,dc=com
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A DN holds commas, so in `DM_TWAKE_INSTANCE_DN` end each expression with `;`.
|
|
114
|
+
Every option and its default is listed in the
|
|
115
|
+
[configuration reference](../../configuration.md). The plugin needs
|
|
116
|
+
`--rabbitmq-url`.
|
|
117
|
+
|
|
118
|
+
The directory schema must define the address and mark attributes, and with
|
|
119
|
+
`--twake-instance-organization-base` the organization domain (and address)
|
|
120
|
+
attributes: the defaults `twakeWorkspaceUrl`, `twakeCreatedEventAt` and
|
|
121
|
+
`twakeDomain` come from the Twake schema, which this repository does not
|
|
122
|
+
ship. The server refuses to start when one is missing.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Lifecycle Events
|
|
2
|
+
|
|
3
|
+
`core/twake/lifecycleEvents` publishes an account's lifecycle to RabbitMQ from
|
|
4
|
+
the directory write itself. REST, SCIM and any other plugin writing through
|
|
5
|
+
LDAP-Rest announce the same events, with the same payloads.
|
|
6
|
+
|
|
7
|
+
Which entries are accounts, which attributes carry the role, the lock and the
|
|
8
|
+
deletion, and where each event goes are configuration.
|
|
9
|
+
|
|
10
|
+
## Events
|
|
11
|
+
|
|
12
|
+
Each event is a change of state, read from the entry before and after a write
|
|
13
|
+
(`onLdapEntryChange`, from `core/ldap/onChange`). A write that changes nothing
|
|
14
|
+
publishes nothing.
|
|
15
|
+
|
|
16
|
+
- `created`: an entry is added. An entry added locked, as a SCIM create with
|
|
17
|
+
`active: false` does, publishes `created` then `disabled`.
|
|
18
|
+
- `roleChanged`: the role attribute gets different values. Case and order are
|
|
19
|
+
ignored: `Admin` and `admin` are one role. A multi-valued role attribute is
|
|
20
|
+
compared as a set, but `$title` carries its first value only, so
|
|
21
|
+
`[x, y]` to `[x, z]` publishes `x` as both role and previous role: keep the
|
|
22
|
+
role in a single-valued attribute.
|
|
23
|
+
- `disabled`: the lock attribute takes the lock value.
|
|
24
|
+
- `enabled`: the lock attribute no longer holds the lock value.
|
|
25
|
+
- `deleted`: the entry becomes a tombstone (the deleted attribute takes the
|
|
26
|
+
deleted value), or an entry that is not a tombstone is removed.
|
|
27
|
+
|
|
28
|
+
An account is locked when its lock attribute holds the lock value, by default
|
|
29
|
+
the ppolicy administrative lock `000001010000Z`. Any other value, such as the
|
|
30
|
+
timestamp of a ppolicy lockout after failed binds, is not a lock here.
|
|
31
|
+
|
|
32
|
+
A tombstone announces nothing else: the write that makes it does not also
|
|
33
|
+
publish `disabled`, later changes to it (a lock written afterwards included)
|
|
34
|
+
publish nothing, and removing it publishes nothing, since its deletion was
|
|
35
|
+
already announced.
|
|
36
|
+
|
|
37
|
+
A publish that fails is logged as an error, and the next targets are still
|
|
38
|
+
published. It never fails the write that caused it, and the event is not
|
|
39
|
+
replayed: writing the same value again changes nothing, so it publishes
|
|
40
|
+
nothing. With `core/twake/tombstone`, a lost `deleted` is published again by
|
|
41
|
+
deleting the tombstone once more (see below); a lost `disabled`, `enabled`,
|
|
42
|
+
`created` or `roleChanged` has no such path.
|
|
43
|
+
|
|
44
|
+
The lock attribute is requested by name when the entry is read, so an
|
|
45
|
+
operational one such as `pwdAccountLockedTime` is seen on both sides.
|
|
46
|
+
|
|
47
|
+
## What is not published
|
|
48
|
+
|
|
49
|
+
- A rename or a move publishes nothing. The `$dn.*` groups of the new DN may
|
|
50
|
+
then differ from the ones consumers know, and an entry moved into or out of
|
|
51
|
+
a rule's pattern publishes neither `created` nor `deleted`.
|
|
52
|
+
- A tombstone cannot be restored. Clearing its deleted attribute is not a
|
|
53
|
+
supported operation: it publishes nothing, whatever else the same write
|
|
54
|
+
changes, and later changes are published again as for any account, to
|
|
55
|
+
consumers that consider it deleted.
|
|
56
|
+
- A change made outside LDAP-Rest is not seen. A ppolicy lockout after failed
|
|
57
|
+
binds is written by the directory itself, and holds a timestamp rather than
|
|
58
|
+
the lock value: it publishes nothing, and neither does clearing it.
|
|
59
|
+
|
|
60
|
+
## Deployments with other plugins
|
|
61
|
+
|
|
62
|
+
- `core/twake/tombstone` answers a second delete of a tombstone without
|
|
63
|
+
writing anything, and fires `twakedeletionreplay`; this plugin then
|
|
64
|
+
publishes `deleted` again, with the tombstone as it is (same deletion date).
|
|
65
|
+
This explicit delete is how a `deleted` event a broker outage lost is
|
|
66
|
+
replayed.
|
|
67
|
+
- `core/twake/cozyProvision` and `core/twake/clouderyProvision` publish their
|
|
68
|
+
own deletion event on `--cozy-user-deleted-routing-key`. When a `deleted`
|
|
69
|
+
rule here publishes it, set that option to `""`. Turning it off without a
|
|
70
|
+
`deleted` rule leaves no deletion event at all; keeping both publishes it
|
|
71
|
+
twice.
|
|
72
|
+
- `core/ldap/trash` moves a deleted entry instead of removing it, and a move
|
|
73
|
+
made by the trash is not seen by `core/ldap/onChange`. A deletion in a
|
|
74
|
+
branch the trash watches publishes nothing.
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
--plugin core/twake/lifecycleEvents \
|
|
80
|
+
--twake-lifecycle-role-attribute title \
|
|
81
|
+
--twake-lifecycle-lock-attribute pwdAccountLockedTime \
|
|
82
|
+
--twake-lifecycle-lock-value 000001010000Z \
|
|
83
|
+
--twake-lifecycle-deleted-attribute employeeType \
|
|
84
|
+
--twake-lifecycle-deleted-value deleted \
|
|
85
|
+
--twake-lifecycle-deleted-at-attribute roomNumber \
|
|
86
|
+
--twake-lifecycle-deleted-at-format iso8601 \
|
|
87
|
+
--twake-lifecycle-rules /etc/ldap-rest/lifecycle-rules.json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- `--twake-lifecycle-role-attribute`: empty means no `roleChanged`.
|
|
91
|
+
- `--twake-lifecycle-lock-attribute`, `--twake-lifecycle-lock-value`: default
|
|
92
|
+
to `--scim-user-lock-attribute` and `--scim-user-lock-value`, so SCIM
|
|
93
|
+
`active` and the events agree; SCIM's value only goes with SCIM's
|
|
94
|
+
attribute. `pwdAccountLockedTime` defaults to `000001010000Z`; any other
|
|
95
|
+
attribute needs its value, or the server does not start, as for
|
|
96
|
+
[SCIM](scim.md).
|
|
97
|
+
Only changes written through LDAP-Rest are seen: a lockout set by the
|
|
98
|
+
directory publishes nothing.
|
|
99
|
+
- `--twake-lifecycle-deleted-attribute`, `--twake-lifecycle-deleted-value`
|
|
100
|
+
(default `TRUE`, compared case-insensitively): what marks a tombstone. Empty
|
|
101
|
+
means entries are never tombstones, and only a removal publishes `deleted`.
|
|
102
|
+
- `--twake-lifecycle-deleted-at-attribute`,
|
|
103
|
+
`--twake-lifecycle-deleted-at-format` (`iso8601`, the default, or
|
|
104
|
+
`generalizedTime`): the deletion date. It is always published as ISO 8601.
|
|
105
|
+
- `--twake-lifecycle-rules`: a JSON file, or the JSON itself.
|
|
106
|
+
|
|
107
|
+
Each option has a `DM_` environment variable, for example
|
|
108
|
+
`DM_TWAKE_LIFECYCLE_RULES`. The events go through `core/rabbitmq`. With rules
|
|
109
|
+
configured, the server does not start without `--rabbitmq-url`, nor when
|
|
110
|
+
that broker cannot be reached. An event that finds no broker later on is
|
|
111
|
+
lost, and logged as an error.
|
|
112
|
+
|
|
113
|
+
## Rules
|
|
114
|
+
|
|
115
|
+
The rules are an array. The first rule whose `dn` regular expression matches
|
|
116
|
+
the entry's DN (case-insensitively) decides what is published; an entry no
|
|
117
|
+
rule matches publishes nothing.
|
|
118
|
+
|
|
119
|
+
The DN is matched without spaces around its separators, and with escaped
|
|
120
|
+
commas written `\2C`: `uid=a\, b, ou=users,…` is read as `uid=a\2C b,ou=users,…`,
|
|
121
|
+
so `(?<id>[^,]+)` takes the whole value. A `$dn.name` group is unescaped:
|
|
122
|
+
`a, b`.
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
[
|
|
126
|
+
{
|
|
127
|
+
"dn": "^uid=(?<id>[^,]+),ou=users,dc=example,dc=com$",
|
|
128
|
+
"exchange": "accounts",
|
|
129
|
+
"payload": {
|
|
130
|
+
"id": "$dn.id",
|
|
131
|
+
"email": "$mail",
|
|
132
|
+
"domain": "$mail|domain",
|
|
133
|
+
"role": "$title",
|
|
134
|
+
"timestamp": "$now"
|
|
135
|
+
},
|
|
136
|
+
"events": {
|
|
137
|
+
"created": "account.created",
|
|
138
|
+
"roleChanged": {
|
|
139
|
+
"routingKey": "account.role.changed",
|
|
140
|
+
"payload": {
|
|
141
|
+
"id": "$dn.id",
|
|
142
|
+
"role": "$title",
|
|
143
|
+
"previousRole": "$previous.title"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"disabled": "account.disabled",
|
|
147
|
+
"enabled": "account.enabled",
|
|
148
|
+
"deleted": [
|
|
149
|
+
"account.deleted",
|
|
150
|
+
{
|
|
151
|
+
"exchange": "notifications",
|
|
152
|
+
"routingKey": "account.deleted",
|
|
153
|
+
"when": { "$businessCategory": "user_request" },
|
|
154
|
+
"payload": { "id": "$dn.id", "mobile": "$previous.mobile" }
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
An event takes one target or a list of them, published in order. A target is
|
|
163
|
+
a routing key, or an object with:
|
|
164
|
+
|
|
165
|
+
- `routingKey`
|
|
166
|
+
- `exchange`: defaults to the rule's
|
|
167
|
+
- `payload`: replaces the rule's `payload`
|
|
168
|
+
- `when`: publish only if every source equals its value; a value starting with
|
|
169
|
+
`!` means "differs from"
|
|
170
|
+
|
|
171
|
+
Payload and `when` values are sources:
|
|
172
|
+
|
|
173
|
+
- `$attr`: the attribute after the write; for a removed entry, which has no
|
|
174
|
+
"after", the attribute before it
|
|
175
|
+
- `$previous.attr`: the attribute before the write
|
|
176
|
+
- `$dn.name`: a named group of the rule's `dn` expression
|
|
177
|
+
- `$context.actor`, `$context.requestId`, `$context.source`: who made the
|
|
178
|
+
write, the request it belongs to, and the API it came through (`rest`,
|
|
179
|
+
`scim`); left out for a write no request made, such as a scheduled task
|
|
180
|
+
- `$attr|domain`, `$previous.attr|domain`: what follows the `@`
|
|
181
|
+
- `$now`: the current time, ISO 8601
|
|
182
|
+
- anything else: the value itself
|
|
183
|
+
|
|
184
|
+
`$attr` and `$previous.attr` carry the first value of a multi-valued
|
|
185
|
+
attribute. A source with no value leaves its field out. Every message carries
|
|
186
|
+
a random AMQP `messageId`.
|
|
187
|
+
|
|
188
|
+
A source can read any attribute of the entry: `$userPassword` would put the
|
|
189
|
+
password hash in the message.
|
|
190
|
+
|
|
191
|
+
The rules are checked at startup: a malformed one, such as a payload value
|
|
192
|
+
that is not a string, stops the server with an error naming its `dn`.
|
|
193
|
+
|
|
194
|
+
## Dependencies
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
core/twake/lifecycleEvents
|
|
198
|
+
├─ requires: core/ldap/onChange
|
|
199
|
+
└─ requires: core/rabbitmq
|
|
200
|
+
```
|
|
@@ -305,6 +305,17 @@ mapping contains an entry with `scim: "department"`; otherwise the server return
|
|
|
305
305
|
`400 invalidFilter`. Same for PATCH: operations on unmapped paths produce
|
|
306
306
|
`400 invalidPath`.
|
|
307
307
|
|
|
308
|
+
A PATCH `add`, or a `remove` naming values, changes only the values it names:
|
|
309
|
+
values the caller's read did not show, such as group members an
|
|
310
|
+
`ldapsearchfilter` hides, stay. Two PATCHes changing different values of
|
|
311
|
+
the same attribute both apply. Adding a value already there succeeds where
|
|
312
|
+
the directory honours the permissive modify control; removing one another
|
|
313
|
+
write just removed replays the PATCH on a fresh read, then answers `409`.
|
|
314
|
+
A `replace`, or a `remove` of a whole attribute, sets the attribute to what
|
|
315
|
+
the caller sent, and so does a group `PUT` for its members, hidden ones
|
|
316
|
+
included. A group left with no member the caller saw gets the
|
|
317
|
+
`--group-dummy-user` placeholder.
|
|
318
|
+
|
|
308
319
|
### Schema advertisement
|
|
309
320
|
|
|
310
321
|
Schemas listed under `GET /scim/v2/Schemas` are read from
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Tombstone
|
|
2
|
+
|
|
3
|
+
`core/twake/tombstone` turns the delete of an account into a tombstone. The
|
|
4
|
+
entry stays, so its identifiers cannot be taken by a new account, but it is
|
|
5
|
+
flagged as deleted, dated, locked, carries the reason, and loses the
|
|
6
|
+
attributes the deployment names.
|
|
7
|
+
|
|
8
|
+
A tombstone is erased later, from an operator's decision: once the deletion
|
|
9
|
+
is old enough, or right away when forced.
|
|
10
|
+
|
|
11
|
+
## Behaviour
|
|
12
|
+
|
|
13
|
+
- A delete of an entry whose DN matches `--twake-tombstone-dn` writes, in one
|
|
14
|
+
modify: the deleted attribute, the deletion date (now), the reason, the
|
|
15
|
+
lock, and removes `--twake-tombstone-clear-attributes`. Other entries are
|
|
16
|
+
deleted as usual.
|
|
17
|
+
- A delete of an existing tombstone writes nothing and answers as a delete.
|
|
18
|
+
It fires the `twakedeletionreplay` hook with the tombstone, so
|
|
19
|
+
[lifecycle events](lifecycle-events.md) publishes the deletion again, with
|
|
20
|
+
the date of the first one. This is the way to replay a `deleted` event that
|
|
21
|
+
was lost, for instance while the broker was down.
|
|
22
|
+
- The tombstone is written by the server, not as the caller: the caller's
|
|
23
|
+
delete right is what was judged, and a write right is not required on top.
|
|
24
|
+
So the `deleted` event of that write has no `$context.actor`,
|
|
25
|
+
`$context.requestId` or `$context.source`. A replay has them.
|
|
26
|
+
- The tombstone keeps its group memberships until it is erased.
|
|
27
|
+
- SCIM treats a tombstone as gone: reads answer 404, lists leave it out, and
|
|
28
|
+
a create with the same identity erases the tombstone and creates a new
|
|
29
|
+
entry. If the directory then refuses the new entry, the tombstone and its
|
|
30
|
+
memberships are put back once the response is sent.
|
|
31
|
+
- Erasing removes the entry, then its group memberships. It publishes
|
|
32
|
+
nothing.
|
|
33
|
+
- A tombstone cannot be restored. Clearing its deleted attribute by hand is
|
|
34
|
+
not a supported operation.
|
|
35
|
+
|
|
36
|
+
The tombstone is written after every other plugin has judged the delete, so
|
|
37
|
+
a delete that an authorization plugin refuses writes nothing.
|
|
38
|
+
|
|
39
|
+
`core/ldap/trash` moves a deleted entry before this plugin sees the delete,
|
|
40
|
+
so no tombstone would be written in a branch it watches. The server refuses
|
|
41
|
+
to start when `--trash-watched-bases` covers a `--twake-tombstone-dn`
|
|
42
|
+
pattern, read the way the trash reads it today, and an empty list watches
|
|
43
|
+
every branch.
|
|
44
|
+
|
|
45
|
+
The trash splits `--trash-watched-bases` on every comma, a bug
|
|
46
|
+
([#226](https://github.com/linagora/ldap-rest/issues/226)): a base such as
|
|
47
|
+
`ou=users,dc=example,dc=com` becomes the suffixes `ou=users`, `dc=example`
|
|
48
|
+
and `dc=com`, and `dc=com` watches every branch below it. The check follows
|
|
49
|
+
the trash as it behaves, so such a value refuses to start next to any
|
|
50
|
+
tombstone pattern under `dc=com`.
|
|
51
|
+
|
|
52
|
+
## Reason
|
|
53
|
+
|
|
54
|
+
The reason is, in order:
|
|
55
|
+
|
|
56
|
+
- the one given to `tombstone(dn, reason, req)` by another plugin;
|
|
57
|
+
- the `--twake-tombstone-reason-header` request header;
|
|
58
|
+
- `--twake-tombstone-default-reason`.
|
|
59
|
+
|
|
60
|
+
When `--twake-tombstone-reasons` is set, any other reason is refused with a 400. Otherwise a reason is letters, digits, `_`, `.` or `-`.
|
|
61
|
+
|
|
62
|
+
## Erase
|
|
63
|
+
|
|
64
|
+
```http
|
|
65
|
+
POST /api/v1/twake/tombstones/erase
|
|
66
|
+
Content-Type: application/json
|
|
67
|
+
|
|
68
|
+
{ "dn": "uid=jdoe,ou=users,dc=example,dc=com", "force": false }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `200`: erased.
|
|
72
|
+
- `404`: no tombstone at this DN.
|
|
73
|
+
- `409`: the deletion is younger than `--twake-tombstone-erase-min-age` and
|
|
74
|
+
`force` is not true.
|
|
75
|
+
|
|
76
|
+
Other plugins call `erase(dn, { force, req })`.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
The tombstone attributes are the lifecycle ones, shared with
|
|
81
|
+
[lifecycle events](lifecycle-events.md):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
--plugin core/twake/tombstone \
|
|
85
|
+
--twake-lifecycle-lock-attribute pwdAccountLockedTime \
|
|
86
|
+
--twake-lifecycle-lock-value 000001010000Z \
|
|
87
|
+
--twake-lifecycle-deleted-attribute employeeType \
|
|
88
|
+
--twake-lifecycle-deleted-value deleted \
|
|
89
|
+
--twake-lifecycle-deleted-at-attribute roomNumber \
|
|
90
|
+
--twake-lifecycle-deleted-at-format iso8601 \
|
|
91
|
+
--twake-lifecycle-reason-attribute businessCategory \
|
|
92
|
+
--twake-tombstone-dn '^uid=[^,]+,ou=users,dc=example,dc=com$' \
|
|
93
|
+
--twake-tombstone-dn '^uid=[^,]+,ou=[^,]+,ou=organizations,dc=example,dc=com$' \
|
|
94
|
+
--twake-tombstone-reasons deleted,user_request \
|
|
95
|
+
--twake-tombstone-clear-attributes mobile,telephoneNumber \
|
|
96
|
+
--twake-tombstone-erase-min-age 2592000 \
|
|
97
|
+
--twake-tombstone-group-bases ou=groups,dc=example,dc=com
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- `--twake-lifecycle-deleted-attribute`: required.
|
|
101
|
+
- `--twake-lifecycle-lock-attribute`, `--twake-lifecycle-lock-value`: the
|
|
102
|
+
lock written on a tombstone, read as
|
|
103
|
+
[lifecycle events](lifecycle-events.md) reads it. A lock attribute other
|
|
104
|
+
than `pwdAccountLockedTime` needs its value (`TRUE` for `nsAccountLock` on
|
|
105
|
+
389-ds), or the server does not start: `000001010000Z` means nothing there.
|
|
106
|
+
- `--twake-lifecycle-reason-attribute`: empty means the reason is not
|
|
107
|
+
recorded.
|
|
108
|
+
- `--twake-tombstone-dn`: regular expressions, matched case-insensitively. A
|
|
109
|
+
DN holds commas, so in `DM_TWAKE_TOMBSTONE_DN` end each one with `;`.
|
|
110
|
+
- `--twake-tombstone-default-reason`: default `deleted`.
|
|
111
|
+
- `--twake-tombstone-reason-header`: default `x-deletion-reason`.
|
|
112
|
+
- `--twake-tombstone-erase-min-age`: seconds, default 30 days.
|
|
113
|
+
- `--twake-tombstone-group-bases`: where memberships are looked for at erase,
|
|
114
|
+
by `member`. Defaults to `--ldap-group-base`, then `--ldap-base`. In the
|
|
115
|
+
environment variable, end each base with `;`.
|
package/docs/usage/upgrading.md
CHANGED
|
@@ -4,6 +4,64 @@ 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.12.0
|
|
8
|
+
|
|
9
|
+
### twake/instances replaces cozyProvision and clouderyProvision
|
|
10
|
+
|
|
11
|
+
**Who is affected:** a deployment loading `core/twake/cozyProvision` or
|
|
12
|
+
`core/twake/clouderyProvision`.
|
|
13
|
+
|
|
14
|
+
Replace the plugin with `core/twake/instances`
|
|
15
|
+
([doc](plugins/integrations/instances.md)); the server refuses to start with
|
|
16
|
+
both. Its options are its own. From cozyProvision, with
|
|
17
|
+
`--twake-instance-provider cozy-stack` and `--twake-instance-dn` set to the
|
|
18
|
+
accounts' branch:
|
|
19
|
+
|
|
20
|
+
- `--cozy-admin-url` → `--twake-instance-cozy-url`
|
|
21
|
+
- `--cozy-admin-user` → `--twake-instance-cozy-user`
|
|
22
|
+
- `--cozy-admin-passphrase` → `--twake-instance-cozy-passphrase`
|
|
23
|
+
- `--cozy-org-domain` → `--twake-instance-cozy-domain` (the instances'
|
|
24
|
+
domain) and `--twake-instance-cozy-org-domain`
|
|
25
|
+
- `--cozy-org-id` → `--twake-instance-cozy-org-id`
|
|
26
|
+
- `--cozy-context-name` → `--twake-instance-cozy-context`
|
|
27
|
+
- `--cozy-apps` → `--twake-instance-cozy-apps`
|
|
28
|
+
- `--cozy-default-locale` → `--twake-instance-locale`
|
|
29
|
+
- `--cozy-auth-exchange` → `--twake-instance-auth-exchange`
|
|
30
|
+
- `--cozy-b2b-exchange` → `--twake-instance-b2b-exchange`
|
|
31
|
+
- `--cozy-user-created-routing-key` → `--twake-instance-user-created-key`
|
|
32
|
+
|
|
33
|
+
From clouderyProvision, with `--twake-instance-provider cloudery`:
|
|
34
|
+
|
|
35
|
+
- `--cloudery-manager-url` → `--twake-instance-cloudery-url`
|
|
36
|
+
- `--cloudery-manager-token` → `--twake-instance-cloudery-token`
|
|
37
|
+
- `--cloudery-offer` → `--twake-instance-cloudery-offer`
|
|
38
|
+
- `--cloudery-domain` → `--twake-instance-cloudery-domain`
|
|
39
|
+
- `--cloudery-default-locale` → `--twake-instance-locale`
|
|
40
|
+
- `--cloudery-fqdn-attribute` → `--twake-instance-fqdn-attribute`
|
|
41
|
+
- `--cloudery-user-branch` → `--twake-instance-dn`, with an `org` group for
|
|
42
|
+
the organization id; `--twake-instance-id '{uid}{org}'` keeps the slugs
|
|
43
|
+
|
|
44
|
+
What no longer happens:
|
|
45
|
+
|
|
46
|
+
- An account gets its instance on any add under `--twake-instance-dn`, not
|
|
47
|
+
only a SCIM create.
|
|
48
|
+
- `domain.user.deleted` is not published, and `--cozy-user-deleted-routing-key`
|
|
49
|
+
has no plugin to read it. With the Cloudery, deleting an account no longer
|
|
50
|
+
deletes its instance; cozy-stack instances still are.
|
|
51
|
+
- clouderyProvision's writes on a SCIM create are gone:
|
|
52
|
+
`twakeOrganizationId`, `twakeOrganizationRole`, `twakeInvited`,
|
|
53
|
+
`twakePhones`, `cn` set to the userName, and the lower-casing of the SCIM
|
|
54
|
+
`userName`. The `x-cloudery-org-*` headers are not read; the organization
|
|
55
|
+
comes from the DN. `--cloudery-org-*`, `--cloudery-phones-attribute`,
|
|
56
|
+
`--cloudery-invited-attribute` and the workflow polling options are gone:
|
|
57
|
+
the address arrives with `workplace.created` instead.
|
|
58
|
+
- With cozy-stack, the instance address drops the uid's dots and lower-cases
|
|
59
|
+
it, as the Cloudery's slugs do: an account whose uid holds a dot or a
|
|
60
|
+
capital gets a different address than cozyProvision gave it. Its OIDC id
|
|
61
|
+
is still the uid.
|
|
62
|
+
|
|
63
|
+
`user.created` keeps its fields, `twakeId` still being the account's uid.
|
|
64
|
+
|
|
7
65
|
## To 0.11.0
|
|
8
66
|
|
|
9
67
|
### Group reads are judged
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ldap-rest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Light directory manager",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Identity-Manager"
|
|
@@ -268,10 +268,26 @@
|
|
|
268
268
|
"types": "./dist/src/plugins/twake/drive.d.ts",
|
|
269
269
|
"import": "./dist/plugins/twake/drive.js"
|
|
270
270
|
},
|
|
271
|
+
"./plugin-twake-instances": {
|
|
272
|
+
"types": "./dist/src/plugins/twake/instances.d.ts",
|
|
273
|
+
"import": "./dist/plugins/twake/instances.js"
|
|
274
|
+
},
|
|
271
275
|
"./plugin-twake-james": {
|
|
272
276
|
"types": "./dist/src/plugins/twake/james.d.ts",
|
|
273
277
|
"import": "./dist/plugins/twake/james.js"
|
|
274
278
|
},
|
|
279
|
+
"./plugin-twake-lifecycleattributes": {
|
|
280
|
+
"types": "./dist/src/plugins/twake/lifecycleAttributes.d.ts",
|
|
281
|
+
"import": "./dist/plugins/twake/lifecycleAttributes.js"
|
|
282
|
+
},
|
|
283
|
+
"./plugin-twake-lifecycleevents": {
|
|
284
|
+
"types": "./dist/src/plugins/twake/lifecycleEvents.d.ts",
|
|
285
|
+
"import": "./dist/plugins/twake/lifecycleEvents.js"
|
|
286
|
+
},
|
|
287
|
+
"./plugin-twake-tombstone": {
|
|
288
|
+
"types": "./dist/src/plugins/twake/tombstone.d.ts",
|
|
289
|
+
"import": "./dist/plugins/twake/tombstone.js"
|
|
290
|
+
},
|
|
275
291
|
"./plugin-weblogs": {
|
|
276
292
|
"types": "./dist/src/plugins/weblogs.d.ts",
|
|
277
293
|
"import": "./dist/plugins/weblogs.js"
|