ldap-rest 0.12.0 → 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/config/args.js +1 -1
- package/dist/lib/errors.js +1 -1
- package/dist/lib/instanceProviders.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/plugins/auth/openidconnect.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 -1
- package/dist/plugins/twake/lifecycleAttributes.js +1 -1
- package/dist/plugins/twake/lifecycleEvents.js +1 -1
- package/dist/plugins/twake/tombstone.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +34 -6
- package/dist/src/config/args.d.ts +12 -0
- package/dist/src/lib/errors.d.ts +3 -0
- package/dist/src/lib/instanceProviders.d.ts +2 -0
- package/dist/src/plugins/auth/openidconnect.d.ts +15 -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 +18 -7
- package/dist/src/plugins/scim/patch.d.ts +2 -2
- package/dist/src/plugins/twake/groups.d.ts +89 -0
- package/dist/src/plugins/twake/instances.d.ts +14 -0
- package/dist/src/plugins/twake/lifecycleEvents.d.ts +20 -3
- package/docs/client-development/api/rest-api.md +32 -2
- package/docs/usage/configuration.md +22 -1
- package/docs/usage/plugins/README.md +1 -0
- package/docs/usage/plugins/auth/oidc.md +19 -0
- package/docs/usage/plugins/integrations/README.md +1 -0
- package/docs/usage/plugins/integrations/groups.md +225 -0
- package/docs/usage/plugins/integrations/instances.md +31 -3
- package/docs/usage/plugins/integrations/lifecycle-events.md +38 -5
- package/docs/usage/plugins/integrations/scim.md +44 -34
- package/docs/usage/plugins/integrations/tombstone.md +2 -10
- package/docs/usage/plugins/ldap/flat-generic.md +38 -2
- package/docs/usage/plugins/ldap/trash.md +4 -4
- package/docs/usage/upgrading.md +56 -0
- package/package.json +9 -1
- package/static/schemas/twake/organizationGroups.json +32 -0
|
@@ -60,11 +60,24 @@ export default class TwakeInstances extends DmPlugin {
|
|
|
60
60
|
name: string;
|
|
61
61
|
domain: string;
|
|
62
62
|
}): Promise<'ready' | 'pending'>;
|
|
63
|
+
private organizationRequest;
|
|
64
|
+
/**
|
|
65
|
+
* The Cloudery builds no member instance for an organization it does not
|
|
66
|
+
* have, so a member asks for a missing one first. The organization is
|
|
67
|
+
* marked when its own workplace.created arrives. Not serialized on the
|
|
68
|
+
* organization: its work releases members, which would wait on this one.
|
|
69
|
+
*/
|
|
70
|
+
private requestOrganization;
|
|
63
71
|
onWorkplaceCreated(message: WorkplaceCreated): Promise<void>;
|
|
72
|
+
private settle;
|
|
64
73
|
/** Find the instance, or ask for it; write it down once it exists. */
|
|
65
74
|
private provide;
|
|
66
75
|
protected ready(dn: string, entry: AttributesList, account: Account, fqdn: string, req?: Request): Promise<void>;
|
|
67
76
|
private organizationReady;
|
|
77
|
+
/** Send the user.created held while the organization at `dn` was pending */
|
|
78
|
+
private releaseMembers;
|
|
79
|
+
/** Announce a held member, with the address rule ensureInstance applies */
|
|
80
|
+
private release;
|
|
68
81
|
/**
|
|
69
82
|
* Run `work` once the work already queued for `key` is over. One replica
|
|
70
83
|
* only: across replicas, `user.created` may still be sent twice.
|
|
@@ -72,6 +85,7 @@ export default class TwakeInstances extends DmPlugin {
|
|
|
72
85
|
private serialized;
|
|
73
86
|
private get organizationId();
|
|
74
87
|
private get organizationDomain();
|
|
88
|
+
private get organizationName();
|
|
75
89
|
private get organizationFqdn();
|
|
76
90
|
private organizationMail;
|
|
77
91
|
private isOrganization;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
2
2
|
import type { DM } from '../../bin';
|
|
3
3
|
import type { Hooks } from '../../hooks';
|
|
4
|
-
export declare const LIFECYCLE_EVENTS: readonly ["created", "roleChanged", "disabled", "enabled", "deleted"];
|
|
4
|
+
export declare const LIFECYCLE_EVENTS: readonly ["created", "roleChanged", "disabled", "enabled", "updated", "memberAdded", "memberRemoved", "deleted"];
|
|
5
5
|
export type LifecycleEvent = (typeof LIFECYCLE_EVENTS)[number];
|
|
6
|
-
|
|
6
|
+
declare const MEMBER_LISTS: readonly ["$members", "$added", "$removed"];
|
|
7
|
+
/** A payload field listing members, each read and shaped by `fields`. */
|
|
8
|
+
interface MemberList {
|
|
9
|
+
list: (typeof MEMBER_LISTS)[number];
|
|
10
|
+
fields: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
type Payload = Record<string, string | MemberList>;
|
|
13
|
+
type Condition = Record<string, string>;
|
|
7
14
|
interface Target {
|
|
8
15
|
exchange: string;
|
|
9
16
|
routingKey: string;
|
|
10
17
|
payload: Payload;
|
|
11
|
-
when:
|
|
18
|
+
when: Condition;
|
|
12
19
|
}
|
|
13
20
|
interface Rule {
|
|
14
21
|
dn: RegExp;
|
|
@@ -24,6 +31,7 @@ export default class TwakeLifecycleEvents extends DmPlugin {
|
|
|
24
31
|
};
|
|
25
32
|
private readonly rules;
|
|
26
33
|
private readonly attrs;
|
|
34
|
+
private readonly memberAttribute;
|
|
27
35
|
constructor(server: DM);
|
|
28
36
|
/**
|
|
29
37
|
* core/rabbitmq connects lazily and hands back no client when it cannot:
|
|
@@ -39,6 +47,15 @@ export default class TwakeLifecycleEvents extends DmPlugin {
|
|
|
39
47
|
private replay;
|
|
40
48
|
private match;
|
|
41
49
|
private onEntryChange;
|
|
50
|
+
/** The members of an entry, the group placeholder left out. */
|
|
51
|
+
private members;
|
|
52
|
+
private memberDiff;
|
|
53
|
+
/**
|
|
54
|
+
* A member list of a payload: one read per member, a tombstone left out,
|
|
55
|
+
* and a member no longer in the directory given its RDN alone.
|
|
56
|
+
*/
|
|
57
|
+
private memberPayloads;
|
|
58
|
+
private readMember;
|
|
42
59
|
private resolve;
|
|
43
60
|
private publish;
|
|
44
61
|
}
|
|
@@ -538,6 +538,12 @@ Retrieve all users or filter by attributes.
|
|
|
538
538
|
- `match` - Filter value (partial match with wildcards)
|
|
539
539
|
- `attribute` - Attribute name to filter on
|
|
540
540
|
- `attributes` - Comma-separated list of attributes to return
|
|
541
|
+
- `limit` - Return at most this many users; `X-Result-Truncated: true` then
|
|
542
|
+
says others were left out. Without it, a list longer than the directory's
|
|
543
|
+
size limit is answered `422`
|
|
544
|
+
([details](../../usage/plugins/ldap/flat-generic.md#long-lists)). Only the
|
|
545
|
+
flat entities' lists (users and the other schema-driven entities) take
|
|
546
|
+
`limit` for now; the [groups list](#list-groups) does not.
|
|
541
547
|
|
|
542
548
|
**Example Request (all users):**
|
|
543
549
|
|
|
@@ -764,6 +770,11 @@ Retrieve all groups or filter by name.
|
|
|
764
770
|
- `match` - Filter groups by CN (supports LDAP filter format or simple string)
|
|
765
771
|
- `attributes` - Comma-separated list of attributes to return
|
|
766
772
|
|
|
773
|
+
This list does not take `limit` yet: `?limit=` is ignored, no
|
|
774
|
+
`X-Result-Truncated` header is sent, and a list longer than the directory's
|
|
775
|
+
size limit fails with `500` rather than the `422` of the
|
|
776
|
+
[users list](#list-users).
|
|
777
|
+
|
|
767
778
|
**Example Request (all groups):**
|
|
768
779
|
|
|
769
780
|
```bash
|
|
@@ -1046,6 +1057,7 @@ curl -X DELETE \
|
|
|
1046
1057
|
| 403 | Forbidden - Insufficient permissions |
|
|
1047
1058
|
| 404 | Not Found - Resource doesn't exist |
|
|
1048
1059
|
| 409 | Conflict - Resource already exists |
|
|
1060
|
+
| 422 | Unprocessable - see below |
|
|
1049
1061
|
| 500 | Internal Server Error |
|
|
1050
1062
|
|
|
1051
1063
|
### Error Response Format
|
|
@@ -1140,6 +1152,18 @@ All errors return a JSON object with an error message:
|
|
|
1140
1152
|
}
|
|
1141
1153
|
```
|
|
1142
1154
|
|
|
1155
|
+
#### 422 Unprocessable Entity
|
|
1156
|
+
|
|
1157
|
+
**List longer than the directory's size limit** (flat entities' lists, when
|
|
1158
|
+
no `limit` was given —
|
|
1159
|
+
[details](../../usage/plugins/ldap/flat-generic.md#long-lists)):
|
|
1160
|
+
|
|
1161
|
+
```json
|
|
1162
|
+
{
|
|
1163
|
+
"error": "Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`."
|
|
1164
|
+
}
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1143
1167
|
#### 500 Internal Server Error
|
|
1144
1168
|
|
|
1145
1169
|
**LDAP Connection Error:**
|
|
@@ -1202,9 +1226,15 @@ Use the correct operation type:
|
|
|
1202
1226
|
- **add**: Add values to multi-valued attributes (error if already exists)
|
|
1203
1227
|
- **delete**: Remove attributes or specific values
|
|
1204
1228
|
|
|
1205
|
-
###
|
|
1229
|
+
### Long Lists
|
|
1206
1230
|
|
|
1207
|
-
|
|
1231
|
+
The API has no pagination. A flat entity's list (users and the other
|
|
1232
|
+
schema-driven entities) takes `limit` to bound its answer, and says with
|
|
1233
|
+
`X-Result-Truncated: true` that entries were left out: narrow the search with
|
|
1234
|
+
`match` and `attribute` to reach them. Without `limit`, a list longer than the
|
|
1235
|
+
directory's size limit is answered `422`. See
|
|
1236
|
+
[long lists](../../usage/plugins/ldap/flat-generic.md#long-lists); the groups
|
|
1237
|
+
list does not take `limit` yet.
|
|
1208
1238
|
|
|
1209
1239
|
### Schema Validation
|
|
1210
1240
|
|
|
@@ -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
|
|
|
@@ -457,6 +457,7 @@ Automatically creates/updates/deletes applicative account entries when users are
|
|
|
457
457
|
| `--twake-lifecycle-deleted-at-attribute` | `DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE` | | Deletion date attribute |
|
|
458
458
|
| `--twake-lifecycle-deleted-at-format` | `DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT` | `iso8601` | `iso8601` or `generalizedTime` |
|
|
459
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 |
|
|
460
461
|
|
|
461
462
|
See [lifecycle events](plugins/integrations/lifecycle-events.md).
|
|
462
463
|
|
|
@@ -497,6 +498,7 @@ See [lifecycle events](plugins/integrations/lifecycle-events.md).
|
|
|
497
498
|
| `--twake-instance-organization-base` | `DM_TWAKE_INSTANCE_ORGANIZATION_BASE` | | Branch of the organization entries |
|
|
498
499
|
| `--twake-instance-organization-id-attribute` | `DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE` | `ou` | Attribute holding an organization's id |
|
|
499
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 |
|
|
500
502
|
| `--twake-instance-organization-fqdn-attribute` | `DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE` | | Address attribute of an organization (empty: not written) |
|
|
501
503
|
|
|
502
504
|
See [instances](plugins/integrations/instances.md).
|
|
@@ -520,6 +522,25 @@ Reads the `--twake-lifecycle-*` attributes above, and:
|
|
|
520
522
|
|
|
521
523
|
See [tombstone](plugins/integrations/tombstone.md).
|
|
522
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
|
+
|
|
523
544
|
### Utility Plugins
|
|
524
545
|
|
|
525
546
|
#### `core/static`
|
|
@@ -62,6 +62,7 @@ Connect to external systems:
|
|
|
62
62
|
| [lifecycle-events](integrations/lifecycle-events.md) | Account lifecycle events to RabbitMQ |
|
|
63
63
|
| [instances](integrations/instances.md) | Workplace instance of each account |
|
|
64
64
|
| [tombstone](integrations/tombstone.md) | Deleted accounts kept as tombstones |
|
|
65
|
+
| [groups](integrations/groups.md) | Organization groups |
|
|
65
66
|
|
|
66
67
|
## Utility Plugins
|
|
67
68
|
|
|
@@ -85,6 +85,25 @@ 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
|
+
|
|
88
107
|
## Logout
|
|
89
108
|
|
|
90
109
|
`/logout` ends the session at the provider as well (RP-Initiated Logout): the
|
|
@@ -18,6 +18,7 @@ Plugins for connecting LDAP-Rest to external systems.
|
|
|
18
18
|
| [lifecycle-events](lifecycle-events.md) | Account lifecycle events to RabbitMQ |
|
|
19
19
|
| [instances](instances.md) | Workplace instance of each account |
|
|
20
20
|
| [tombstone](tombstone.md) | Deleted accounts kept as tombstones |
|
|
21
|
+
| [groups](groups.md) | Organization groups |
|
|
21
22
|
|
|
22
23
|
## Prerequisites
|
|
23
24
|
|
|
@@ -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
|
+
```
|
|
@@ -65,9 +65,37 @@ has written the organization entry. With the Cloudery it creates the
|
|
|
65
65
|
Cloudery organization first. The organization entry, found under
|
|
66
66
|
`--twake-instance-organization-base` by
|
|
67
67
|
`--twake-instance-organization-id-attribute`, gets the address when
|
|
68
|
-
`--twake-instance-organization-fqdn-attribute` names where
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
71
99
|
|
|
72
100
|
## Providers
|
|
73
101
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Lifecycle Events
|
|
2
2
|
|
|
3
|
-
`core/twake/lifecycleEvents` publishes an account's lifecycle to
|
|
4
|
-
the directory write itself. REST, SCIM and any other plugin writing through
|
|
3
|
+
`core/twake/lifecycleEvents` publishes an account's or a group's lifecycle to
|
|
4
|
+
RabbitMQ from the directory write itself. REST, SCIM and any other plugin writing through
|
|
5
5
|
LDAP-Rest announce the same events, with the same payloads.
|
|
6
6
|
|
|
7
|
-
Which entries are accounts, which attributes carry the role, the
|
|
8
|
-
deletion, and where each event goes are
|
|
7
|
+
Which entries are accounts or groups, which attributes carry the role, the
|
|
8
|
+
lock, the deletion and the members, and where each event goes are
|
|
9
|
+
configuration.
|
|
9
10
|
|
|
10
11
|
## Events
|
|
11
12
|
|
|
@@ -22,6 +23,14 @@ publishes nothing.
|
|
|
22
23
|
role in a single-valued attribute.
|
|
23
24
|
- `disabled`: the lock attribute takes the lock value.
|
|
24
25
|
- `enabled`: the lock attribute no longer holds the lock value.
|
|
26
|
+
- `updated`: a target whose payload has `$changed.` sources publishes when one
|
|
27
|
+
of them changed, with those that did not left out. A target without them
|
|
28
|
+
publishes on every change of the entry.
|
|
29
|
+
- `memberAdded`, `memberRemoved`: the member attribute gains or loses values.
|
|
30
|
+
The old and new lists are compared as sets, so a whole list replaced
|
|
31
|
+
publishes only the members that came or went. The group placeholder
|
|
32
|
+
(`--group-dummy-user`) is never a member here. A group created with
|
|
33
|
+
members publishes `created` only, its members in `$members`.
|
|
25
34
|
- `deleted`: the entry becomes a tombstone (the deleted attribute takes the
|
|
26
35
|
deleted value), or an entry that is not a tombstone is removed.
|
|
27
36
|
|
|
@@ -102,6 +111,8 @@ operational one such as `pwdAccountLockedTime` is seen on both sides.
|
|
|
102
111
|
- `--twake-lifecycle-deleted-at-attribute`,
|
|
103
112
|
`--twake-lifecycle-deleted-at-format` (`iso8601`, the default, or
|
|
104
113
|
`generalizedTime`): the deletion date. It is always published as ISO 8601.
|
|
114
|
+
- `--twake-lifecycle-member-attribute` (default `member`): the attribute
|
|
115
|
+
`memberAdded`, `memberRemoved` and member lists read.
|
|
105
116
|
- `--twake-lifecycle-rules`: a JSON file, or the JSON itself.
|
|
106
117
|
|
|
107
118
|
Each option has a `DM_` environment variable, for example
|
|
@@ -177,12 +188,34 @@ Payload and `when` values are sources:
|
|
|
177
188
|
- `$context.actor`, `$context.requestId`, `$context.source`: who made the
|
|
178
189
|
write, the request it belongs to, and the API it came through (`rest`,
|
|
179
190
|
`scim`); left out for a write no request made, such as a scheduled task
|
|
191
|
+
- `$changed.attr`: the attribute after the write, only if the write changed
|
|
192
|
+
it; an attribute the write removed gives `""`
|
|
180
193
|
- `$attr|domain`, `$previous.attr|domain`: what follows the `@`
|
|
181
194
|
- `$now`: the current time, ISO 8601
|
|
182
195
|
- anything else: the value itself
|
|
183
196
|
|
|
184
197
|
`$attr` and `$previous.attr` carry the first value of a multi-valued
|
|
185
|
-
attribute. A source with no value leaves its field out.
|
|
198
|
+
attribute. A source with no value leaves its field out.
|
|
199
|
+
|
|
200
|
+
A payload field can also list members, as an object with one key:
|
|
201
|
+
|
|
202
|
+
- `$members`: the members after the write
|
|
203
|
+
- `$added`, `$removed`: the members `memberAdded` and `memberRemoved` are about
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
"members": { "$added": { "username": "$uid", "email": "$mail" } }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Each member is read once, and shaped by the object it maps to, whose
|
|
210
|
+
sources are its own attributes (`$attr`) or plain values; any other source is
|
|
211
|
+
refused at startup. A tombstone is left
|
|
212
|
+
out, and a target whose `$added` or `$removed` is left empty publishes
|
|
213
|
+
nothing. A member no longer in the directory is known by its RDN alone:
|
|
214
|
+
`uid=jdoe,…` gives `{ "username": "jdoe" }` above. A group of 1,000 members
|
|
215
|
+
takes 1,000 reads, one after the other, off the path of the write's
|
|
216
|
+
response. A member that cannot be read, for a reason other than being gone,
|
|
217
|
+
drops that target, logged as an error: a member the service account may not
|
|
218
|
+
read silences every target of the group that lists its members. Every message carries
|
|
186
219
|
a random AMQP `messageId`.
|
|
187
220
|
|
|
188
221
|
A source can read any attribute of the entry: `$userPassword` would put the
|