ldap-rest 0.14.0 → 0.15.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.
@@ -12,8 +12,14 @@ branch per organization. It serves the groups of an organization under
12
12
  display name attribute, an optional color and its creation date.
13
13
  - A group's members belong to its organization: adding a user of another
14
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.
15
+ writes the group (REST, SCIM, another plugin), and so is moving a group
16
+ with its members, tombstones included, straight into another
17
+ organization's group branch: that refusal names none of them. The
18
+ placeholder (`--group-dummy-user`) is the one exception. Not checked:
19
+ moving a group out of the organizations' branches (to `ou=archive,…`, for
20
+ instance) or deeper than one level under another organization, which
21
+ takes its members along, and moving a user to another organization, whose
22
+ groups in the one it left keep it as a member.
17
23
  - An empty group holds the placeholder, because `groupOfNames` needs a member.
18
24
  The placeholder is never listed.
19
25
  - A tombstone (see [tombstone](tombstone.md)) keeps its memberships until it
@@ -33,7 +39,8 @@ branch per organization. It serves the groups of an organization under
33
39
 
34
40
  Every route answers 404 `ORGANIZATION_NOT_FOUND` when the organization entry
35
41
  is missing, and 410 `ORGANIZATION_DELETED` when it is deleted. Errors are
36
- `{ "error": "...", "code": "..." }`.
42
+ `{ "error": "...", "code": "..." }`. A request an authorization plugin refuses
43
+ answers 403 `REFUSED`.
37
44
 
38
45
  A group reads:
39
46
 
@@ -55,8 +62,8 @@ A group reads:
55
62
  display name reads its `cn`.
56
63
 
57
64
  - `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
65
+ or more; an empty one lists everything) matches the display name and the
66
+ description; `sortBy` is `displayName`, `description` or `createdAt`. Answers
60
67
  `{ organizationId, groups, pagination: { page, limit, total, totalPages } }`.
61
68
  - `POST /groups` with `{ name, description?, color? }`: `name` is at most 256
62
69
  characters, not blank, without control characters, and unique in the
@@ -195,7 +195,9 @@ Payload and `when` values are sources:
195
195
  - anything else: the value itself
196
196
 
197
197
  `$attr` and `$previous.attr` carry the first value of a multi-valued
198
- attribute. A source with no value leaves its field out.
198
+ attribute. A source with no value leaves its field out. Every attribute a
199
+ source names is requested by name when the entry is read, so an operational
200
+ one such as `entryUUID` works anywhere in a payload or a `when`.
199
201
 
200
202
  A payload field can also list members, as an object with one key:
201
203
 
@@ -0,0 +1,221 @@
1
+ # Organization Spaces
2
+
3
+ `core/twake/spaces` serves the spaces of an organization under
4
+ `/api/v1/organizations/:id/spaces`. A space gathers users and groups of the
5
+ organization, each with a role in it: `viewer`, `editor` or `admin`.
6
+
7
+ ## Behaviour
8
+
9
+ - A space lives in the organization's space branch, given by a DN pattern
10
+ where `{org}` stands for the organization id. Its `cn` is a generated UUID,
11
+ its name is in the display name attribute.
12
+ - A space holds DNs in three attributes, one per role. Each holds users of
13
+ the organization's user branch and groups of its group branch, the
14
+ branches [groups](groups.md) serves.
15
+ - A user or a group holds one role in a space. A user's role in a space is
16
+ the strongest of their own role and the roles of the linked groups they
17
+ belong to.
18
+ - The routes keep at least one admin among a space's users: a request that
19
+ would demote or remove the last one is refused with 409 `LAST_ADMIN`. A
20
+ linked group does not count as an admin. A tombstoned or erased admin is
21
+ not a request, and can leave a space with none.
22
+ - A space holds users and groups of its organization only: any other value is
23
+ refused with a 400, whichever API writes the space, or moves it straight
24
+ into another organization's space branch; a move refused names none of
25
+ them. Not checked: moving a space out of the organizations' branches (to
26
+ `ou=archive,…`, for instance) or deeper than one level under another
27
+ organization, which takes its holders along, and moving a user or group
28
+ to another organization, whose spaces in the one it left keep it. The
29
+ one role and the admin are kept by the routes below only; a user or group
30
+ a direct write, or two requests at once, left under two roles reads with
31
+ the strongest, and a role change or removal takes it out of all of them.
32
+ - A tombstone (see [tombstone](tombstone.md)) keeps its roles until it is
33
+ erased, and is hidden from the spaces read through these routes, whatever
34
+ the caller may read of the user branch: it never counts as an admin. It
35
+ cannot be added.
36
+ - An erased user or group leaves its spaces through the directory: enable the
37
+ `refint` overlay on the three role attributes.
38
+ - When a user who is erased or becomes a tombstone was the last admin among
39
+ a space's users, its editors become admins, or its viewers when it has no
40
+ editor. A space left with no user of its own is deleted, whatever groups
41
+ it links. Both happen shortly after the deletion, and publish their
42
+ events. Erasing a tombstone checks its spaces again, for a hand-over that
43
+ failed or was never made. The hand-over relies on refint: without it an erased admin's DN
44
+ stays in the space and still counts as an admin, so erasing two admins
45
+ one after the other hands nothing over. A write of the space made at the
46
+ same moment is retried twice; past that the hand-over is logged and left
47
+ undone.
48
+ - With `--twake-space-user-role-attribute`, each user entry holds one
49
+ `<space id>:<role>` value per space they are in, their resolved role. It
50
+ follows every write of a space, every change of a linked group's members,
51
+ and the deletion of a linked group, whichever API makes them, shortly after
52
+ the write. A change only tells which users to look at: each one's values
53
+ are then rewritten from the directory as it is, so changes made close
54
+ together end the same whatever order they are followed in. Values not
55
+ ending in `:viewer`, `:editor` or `:admin` are left alone. A tombstone keeps
56
+ the values it held when it was deleted.
57
+ - With `--rabbitmq-url`, every write of a space and every change of a
58
+ member's role is published as an event, see below.
59
+ - With either of the two, the plugin refuses to start when core/ldap/trash
60
+ watches the group or space branches: a group or space it moves away is
61
+ never followed.
62
+
63
+ ## Events
64
+
65
+ Each event goes to the `--twake-space-exchange` topic exchange with the
66
+ routing key `twake.space.<event>`. Like the user role attribute, events follow
67
+ every write, whichever API makes it, shortly after it.
68
+
69
+ ```json
70
+ {
71
+ "organizationId": "acme",
72
+ "id": "3b9e2c71-5d4a-4f0e-9c8b-1a2d6e7f8091",
73
+ "members": [
74
+ {
75
+ "uuid": "6f1c0a52-8e3b-4d7f-a9c2-5b0e1d4f7a83",
76
+ "username": "jdoe",
77
+ "email": "jdoe@acme.example.org",
78
+ "firstName": "John",
79
+ "lastName": "Doe",
80
+ "role": "editor"
81
+ }
82
+ ],
83
+ "actor": "jsmith",
84
+ "timestamp": "2026-10-06T09:12:44.512Z"
85
+ }
86
+ ```
87
+
88
+ Every event carries `organizationId`, `id` (the space), `actor` (who made
89
+ the write, when known) and `timestamp`. A member is described as above, its
90
+ `uuid` the user's `entryUUID`; a group is `{ id, name, role }`.
91
+
92
+ Writes made close together blur the actor: the member events of two writes
93
+ of one space or group followed together may go out under the first one's.
94
+ With `--twake-space-user-role-attribute`, the follow of a write may find a
95
+ role another write moved in another space, not followed yet: it publishes
96
+ that event with no actor, as the later follow finds nothing left to
97
+ announce.
98
+
99
+ - `created`: `name`, `members` (every user in the space with their resolved
100
+ role, linked groups' members included) and `groups`.
101
+ - `updated`: the changed `name`.
102
+ - `deleted`.
103
+ - `member.added`, `member.role.changed`, `member.removed`: `members`, the one
104
+ user whose resolved role changed, whatever the cause: a member write, a
105
+ group linked, unlinked or given another role, a user joining or leaving a
106
+ linked group. `member.removed` gives the role the user held.
107
+ - `group.linked`, `group.role.changed`, `group.unlinked`: `groups`, the one
108
+ group, with its name when the event is published. A deleted group is
109
+ unlinked from each of its spaces once, whether refint or a later write
110
+ takes its DN out of them; one unlinked, then deleted before the unlink is
111
+ followed, is unlinked too, under its id for a name. Renaming a linked group
112
+ publishes nothing.
113
+
114
+ A user who is deleted or becomes a tombstone publishes no member event.
115
+
116
+ The server does not start when the broker cannot be reached. An event the
117
+ broker cannot take once the server runs is logged with `result: "error"` and
118
+ not sent again.
119
+
120
+ Member events compare each moved user's roles before and after a change:
121
+
122
+ - With `--twake-space-user-role-attribute`, the roles before are the values
123
+ on the user's entry, so changes made close together announce each role
124
+ once, in whatever order they are followed.
125
+ - Without it, the roles before are worked out from the directory with the
126
+ change undone. Two changes followed late may announce a role twice, or
127
+ not at all. Consumers that need an exact copy resync from the routes.
128
+
129
+ ## Routes
130
+
131
+ Every route answers 404 `ORGANIZATION_NOT_FOUND` when the organization entry
132
+ is missing, and 410 `ORGANIZATION_DELETED` when it is deleted. Errors are
133
+ `{ "error": "...", "code": "..." }`. A request an authorization plugin refuses
134
+ answers 403 `REFUSED`. A write answers `{ "success": true }` unless stated
135
+ otherwise.
136
+
137
+ A space reads, strongest role first:
138
+
139
+ ```json
140
+ {
141
+ "id": "3b9e2c71-5d4a-4f0e-9c8b-1a2d6e7f8091",
142
+ "name": "Design Sprint",
143
+ "organizationId": "acme",
144
+ "members": [
145
+ { "username": "jsmith", "role": "admin" },
146
+ { "username": "jdoe", "role": "editor" }
147
+ ],
148
+ "groups": [{ "id": "c2a8e1f0-7b3d-4e9a-8f61-2d5b9c0e4a17", "role": "viewer" }]
149
+ }
150
+ ```
151
+
152
+ `username` is the RDN value of a user, `id` the `cn` of a group.
153
+
154
+ - `GET /spaces?page&limit&search&sortBy&sortOrder&user`: `search` (2
155
+ characters or more) matches the name; `sortBy` is `name`. With `user`, the
156
+ spaces that user is in, directly or through a linked group, each with the
157
+ user's `role` (404 `USER_NOT_FOUND` for an unknown user). Answers
158
+ `{ organizationId, spaces, pagination: { page, limit, total, totalPages } }`.
159
+ - `POST /spaces` with `{ name, members: [{ username, role }], groups?: [{ id, role }] }`:
160
+ `name` follows the [groups](groups.md) rule, `members` holds at least one
161
+ admin (400 `ADMIN_REQUIRED`), every user and group must exist in the
162
+ organization (404 `USER_NOT_FOUND`, `GROUP_NOT_FOUND`). Answers 201 with the
163
+ space.
164
+ - `GET /spaces/:spaceId`: the space, or 404 `SPACE_NOT_FOUND`.
165
+ - `PATCH /spaces/:spaceId` with `{ name }`: renames the space; any other
166
+ field is a 400.
167
+ - `DELETE /spaces/:spaceId`.
168
+ - `GET /spaces/:spaceId/members?page&limit&search&sortBy&sortOrder`: the
169
+ members' public profiles, as [groups](groups.md) answers them, each with
170
+ its `role`; `sortBy` is `uid` (default), `displayName`, `mail`, `jobTitle`
171
+ or `role`.
172
+ - `POST /spaces/:spaceId/members` with `{ usernames: [...], role }`: every
173
+ user must exist in the organization (404 `USER_NOT_FOUND`, nothing added),
174
+ at most `--twake-group-max-page-limit` per request, as for every list of
175
+ users or groups below.
176
+ A member already holding that role is left as is; one holding another role
177
+ answers 409 `MEMBER_EXISTS`, nothing added.
178
+ - `PATCH /spaces/:spaceId/members/:userId` with `{ role }`, and
179
+ `DELETE /spaces/:spaceId/members/:userId`: 404 `MEMBER_NOT_FOUND` for a
180
+ user who is not a member.
181
+ - `GET /spaces/:spaceId/groups`: `{ organizationId, id, groups }`, each group
182
+ `{ id, name, role }`, by name.
183
+ - `POST /spaces/:spaceId/groups` with `{ groupIds: [...], role }`: every group
184
+ must exist in the organization (404 `GROUP_NOT_FOUND`, nothing linked). A
185
+ group already linked with another role answers 409 `GROUP_ALREADY_LINKED`.
186
+ - `PATCH /spaces/:spaceId/groups/:groupId` with `{ role }`, and
187
+ `DELETE /spaces/:spaceId/groups/:groupId`: 404 `GROUP_NOT_FOUND` for a group
188
+ that is not linked.
189
+
190
+ ## Configuration
191
+
192
+ ```bash
193
+ --plugin core/twake/spaces \
194
+ --twake-space-base 'ou=spaces,ou={org},dc=example,dc=com'
195
+ ```
196
+
197
+ The plugin loads [groups](groups.md), and reads its options for the
198
+ organization's users, groups and organization entry.
199
+
200
+ - `--twake-space-base`: required, with `{org}`.
201
+ - `--twake-space-class` (default `top,twakeSpace`): the classes of a space
202
+ created.
203
+ - `--twake-space-display-name-attribute` (default `twakeDisplayName`).
204
+ - `--twake-space-admin-attribute` (default `twakeSpaceAdmin`),
205
+ `--twake-space-editor-attribute` (default `twakeSpaceEditor`),
206
+ `--twake-space-viewer-attribute` (default `twakeSpaceViewer`): the role
207
+ attributes, holding DNs.
208
+ - `--twake-space-user-role-attribute` (no default): the multi-valued user
209
+ attribute holding the user's role in each space, such as `twakeSpaceRole`.
210
+ Unset, no user entry is written.
211
+ - `--twake-space-exchange` (default `space`): the topic exchange of the
212
+ events, published only when `--rabbitmq-url` is set.
213
+
214
+ ## Dependencies
215
+
216
+ ```
217
+ core/twake/spaces
218
+ ├─ requires: core/twake/groups
219
+ ├─ requires: core/ldap/onChange
220
+ └─ requires: core/rabbitmq (with RabbitMQ)
221
+ ```
@@ -17,7 +17,7 @@ The companion browser library is [`ldap-browser`](../../../client-development/br
17
17
 
18
18
  ### Environment Variables
19
19
 
20
- - `DM_LDAP_RAW_BASE`: comma-separated subtrees to expose (default: `--ldap-base`)
20
+ - `DM_LDAP_RAW_BASE`: subtrees to expose, separated by `;` (default: `--ldap-base`)
21
21
  - `DM_LDAP_RAW_HIDDEN_ATTRIBUTES`: comma-separated attributes never returned, on top of the credential ones (default: none)
22
22
  - `DM_LDAP_RAW_SHOW_SECRETS`: serve credential attributes instead of hiding them (default: `false`)
23
23
  - `DM_LDAP_RAW_MAX_RESULTS`: maximum entries returned by a search or a children listing (default: `200`)
@@ -4,6 +4,103 @@ 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.15.0
8
+
9
+ ### A DN option read from the environment splits on `;` only
10
+
11
+ **Who is affected:** a deployment that sets `DM_LDAP_RAW_BASE`,
12
+ `DM_JAMES_MAILING_LIST_BRANCHES`, `DM_TWAKE_TOMBSTONE_DN`,
13
+ `DM_TWAKE_TOMBSTONE_GROUP_BASES` or `DM_TWAKE_INSTANCE_DN`, or gives
14
+ `--ldap-raw-bases` or `--james-mailing-list-branches`.
15
+
16
+ Their values are no longer cut on spaces or commas: a DN such as
17
+ `DM_LDAP_RAW_BASE="ou=My Unit,dc=example,dc=com"` stays whole. Separate
18
+ several DNs with `;`, as already documented.
19
+
20
+ ### HMAC and TOTP entries are no longer split on spaces
21
+
22
+ **Who is affected:** a deployment that sets `DM_AUTH_HMAC` or `DM_AUTH_TOTP`,
23
+ or gives `--auth-hmacs` or `--auth-totps`.
24
+
25
+ Separate entries with `,`, `;` or newlines. A name may now hold spaces, as in
26
+ `id:secret:Registration Service`. A list separated by spaces is read as one
27
+ entry: an HMAC list gives one service, the others cannot authenticate (their
28
+ requests get 401, and a warning is logged), and a TOTP list stops the server.
29
+
30
+ `core/auth/hmac` and `core/auth/totp` also stop the server on an entry that
31
+ cannot work, instead of skipping it with a warning: an HMAC entry with an
32
+ empty id, secret or name or fewer than 3 fields, a TOTP entry with an empty
33
+ secret or name, more than 3 fields, a secret that is not Base32 or digits that
34
+ are not an integer from 6 to 10. A TOTP entry such as `secret:name:abc` used
35
+ to register a user whose code was the string `NaN`.
36
+
37
+ ### The plural command-line form of an array option splits on `;` first
38
+
39
+ **Who is affected:** a deployment that gives a plural form such as
40
+ `--auth-tokens`, `--ldap-urls` or `--plugins`.
41
+
42
+ A value containing `;` is now split on `;` and not on `,`, as the environment
43
+ variable does: `--auth-tokens 'a;b;c'` was one token and is now three, and
44
+ `--auth-tokens 'a;b,c'` was `a;b` and `c` and is now `a` and `b,c`. A value
45
+ without `;` is split as before.
46
+
47
+ `--authz-dynamic-bypass` and `--twake-space-class` now split on commas in their
48
+ singular form, as the other options holding identifiers do.
49
+
50
+ ### An empty environment variable or a number that is not one
51
+
52
+ **Who is affected:** a deployment that sets an array option, a number or a JSON
53
+ option to an empty value, or a number option to something else than an integer.
54
+
55
+ - An array variable that is empty, only holds whitespace or only holds
56
+ separators (`DM_GROUP_CLASSES=`, `DM_GROUP_CLASSES=,`) keeps the default
57
+ instead of giving an empty list, which was a silent misconfiguration (a
58
+ server without object class, for instance). `DM_LDAP_OPERATIONAL_ATTRIBUTES=`
59
+ used to give `[]` and now gives the default; an option with a non-empty
60
+ default cannot be given an empty list from the environment.
61
+ - `DM_LDAP_URL=` is the exception: its default, `ldap://localhost`, is a guess,
62
+ not a fallback, so an empty value still stops the server. Set the variable
63
+ or leave it out.
64
+ - An empty number or JSON value (`DM_PORT=`) keeps its default instead of
65
+ giving `NaN` or stopping the server.
66
+ - A number option takes an integer only: `DM_PORT=abc`, `DM_PORT=1e3`,
67
+ `DM_PORT=8080abc`, `--port ''` or `--port --log-level debug` stop the server
68
+ instead of giving `NaN` or a truncated value.
69
+
70
+ Booleans and strings are unchanged.
71
+
72
+ ### An array option on the command line replaces its default
73
+
74
+ **Who is affected:** a deployment that gives, on the command line, an array
75
+ option that has a default: `--user-class`, `--group-class`,
76
+ `--ldap-organization-class`, `--external-branch-class`,
77
+ `--scim-user-object-class`, `--scim-group-object-class`,
78
+ `--applicative-account-attribute`, `--ldap-operational-attribute` or
79
+ `--ldap-url`, in their singular or plural form.
80
+
81
+ Their values used to be added to the default, and now replace it. Repeat the
82
+ default values you want to keep:
83
+
84
+ ```bash
85
+ # Until now: top, twakeAccount, twakeWhitePages, twakeExtra
86
+ ldap-rest --user-class twakeExtra
87
+
88
+ # Now
89
+ ldap-rest --user-class top,twakeAccount,twakeWhitePages,twakeExtra
90
+ ```
91
+
92
+ Without its default classes, an entry is created without its structural
93
+ class. Without the default operational attributes, `appAccountsConsistency`
94
+ recreates an applicative account with `userPassword` and the attributes the
95
+ directory generates. The defaults are listed in
96
+ [the configuration reference](configuration.md).
97
+
98
+ `--ldap-url ldap://ldap.example.com` no longer keeps `ldap://localhost` next
99
+ to it.
100
+
101
+ An environment variable already replaced the default, and values given on the
102
+ command line are still added to it.
103
+
7
104
  ## To 0.14.0
8
105
 
9
106
  ### `--twake-instance-organization-account` is removed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"
@@ -288,6 +288,10 @@
288
288
  "types": "./dist/src/plugins/twake/lifecycleEvents.d.ts",
289
289
  "import": "./dist/plugins/twake/lifecycleEvents.js"
290
290
  },
291
+ "./plugin-twake-spaces": {
292
+ "types": "./dist/src/plugins/twake/spaces.d.ts",
293
+ "import": "./dist/plugins/twake/spaces.js"
294
+ },
291
295
  "./plugin-twake-tombstone": {
292
296
  "types": "./dist/src/plugins/twake/tombstone.d.ts",
293
297
  "import": "./dist/plugins/twake/tombstone.js"
@@ -733,6 +737,7 @@
733
737
  "fast-xml-parser": ">=5.3.6",
734
738
  "glob": ">=11.0.4",
735
739
  "minimatch": "^10.2.1",
740
+ "postcss-selector-parser": "^7.1.6",
736
741
  "serialize-javascript": ">=7.0.3"
737
742
  },
738
743
  "dependencies": {