ldap-rest 0.12.0 → 0.13.1
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/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/lib/ldapActions.d.ts +6 -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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import type { DM } from '../../bin';
|
|
3
|
+
import type { Schema } from '../../config/schema';
|
|
4
|
+
import type { Hooks } from '../../hooks';
|
|
5
|
+
import type { AttributeValue } from '../../lib/ldapActions';
|
|
6
|
+
import LdapGroups from '../ldap/groups';
|
|
7
|
+
import { type LifecycleAttributes } from './lifecycleAttributes';
|
|
8
|
+
/** The values of an attribute, whatever shape the directory gave them. */
|
|
9
|
+
export declare function values(value: AttributeValue | undefined): string[];
|
|
10
|
+
export default class TwakeGroups extends LdapGroups {
|
|
11
|
+
name: string;
|
|
12
|
+
protected readonly groupBase: string;
|
|
13
|
+
protected readonly userBase: string;
|
|
14
|
+
protected readonly groupPattern: RegExp;
|
|
15
|
+
protected readonly attrs: LifecycleAttributes;
|
|
16
|
+
private readonly orgPattern;
|
|
17
|
+
private readonly displayName;
|
|
18
|
+
private readonly color;
|
|
19
|
+
private readonly createdAt;
|
|
20
|
+
private readonly maxPage;
|
|
21
|
+
private readonly memberFields;
|
|
22
|
+
constructor(server: DM);
|
|
23
|
+
/**
|
|
24
|
+
* Declare the group attributes under their configured names, whatever case
|
|
25
|
+
* the schema spells them in, or add them when it does not: the schema does
|
|
26
|
+
* not have to follow a renamed attribute.
|
|
27
|
+
*/
|
|
28
|
+
protected adaptSchema(schema: Schema): Schema;
|
|
29
|
+
/**
|
|
30
|
+
* No `ldapdeletedone`: an erased member leaves its groups through the
|
|
31
|
+
* directory's referential integrity, whatever made the delete. The cleanup
|
|
32
|
+
* inherited from core/ldap/groups writes through LDAP-Rest, so every group
|
|
33
|
+
* would announce a member removed on an erase, which publishes nothing.
|
|
34
|
+
*/
|
|
35
|
+
hooks: Hooks;
|
|
36
|
+
/**
|
|
37
|
+
* The id a `scimgroupcreate` hook assigns names the entry only when the
|
|
38
|
+
* SCIM group mapping leaves the RDN attribute alone.
|
|
39
|
+
*/
|
|
40
|
+
afterLoad(): void;
|
|
41
|
+
/** The organization a group DN belongs to, if it is a group at all. */
|
|
42
|
+
organizationOf(dn: string): string | undefined;
|
|
43
|
+
groupBaseOf(org: string): string;
|
|
44
|
+
userBaseOf(org: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* A group's members belong to its organization, as a provider is trusted
|
|
47
|
+
* within its own organization only. The placeholder belongs to none.
|
|
48
|
+
*/
|
|
49
|
+
private checkMembers;
|
|
50
|
+
/**
|
|
51
|
+
* A tombstone keeps its memberships until it is erased, and is hidden from
|
|
52
|
+
* them, so every API reads the same member list.
|
|
53
|
+
*/
|
|
54
|
+
private hideTombstones;
|
|
55
|
+
private tombstonesOf;
|
|
56
|
+
/**
|
|
57
|
+
* Only the organization routes: the flat `/v1/ldap/groups` of
|
|
58
|
+
* core/ldap/groups would read one base that holds no group here.
|
|
59
|
+
*/
|
|
60
|
+
api(app: Express): void;
|
|
61
|
+
private groupDn;
|
|
62
|
+
private get userAttribute();
|
|
63
|
+
/** Every route answers 404 for a missing organization, 410 for a deleted one. */
|
|
64
|
+
private checkOrganization;
|
|
65
|
+
/** Page, search and sort of a list route, or a refusal of them. */
|
|
66
|
+
private page;
|
|
67
|
+
private sorted;
|
|
68
|
+
/** A group as the routes answer it: members named by their RDN value. */
|
|
69
|
+
private groupOf;
|
|
70
|
+
private groups;
|
|
71
|
+
private readGroup;
|
|
72
|
+
/** The group of that display name in the organization, if any. */
|
|
73
|
+
private named;
|
|
74
|
+
/** A write to a group that is not there answers 404. */
|
|
75
|
+
private onGroup;
|
|
76
|
+
private listRoute;
|
|
77
|
+
private createRoute;
|
|
78
|
+
private updateRoute;
|
|
79
|
+
private membersRoute;
|
|
80
|
+
/**
|
|
81
|
+
* Users of the organization by name, read in pages of the size limit. A
|
|
82
|
+
* tombstone is no user: it cannot be added, as it would join hidden.
|
|
83
|
+
*/
|
|
84
|
+
private users;
|
|
85
|
+
private profiles;
|
|
86
|
+
private addMembersRoute;
|
|
87
|
+
private dropPlaceholder;
|
|
88
|
+
private removeMemberRoute;
|
|
89
|
+
}
|
|
@@ -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
|
|