ldap-rest 1.3.0 → 2.0.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/README.md +8 -1
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/plugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/options.js +1 -1
- package/dist/config/schema.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/lib/errors.js +1 -1
- package/dist/lib/ldap/callerView.js +1 -0
- package/dist/lib/ldap/pool.js +1 -0
- package/dist/lib/ldap/schema.js +1 -0
- package/dist/lib/ldap/searchCache.js +1 -0
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/parseConfig.js +1 -1
- package/dist/lib/services/index.js +1 -0
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/ldap/accountLifecycle.js +1 -1
- package/dist/plugins/ldap/enterpriseRules.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/passwordPolicy.js +1 -1
- package/dist/plugins/ldap/raw.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/storage/file.js +1 -1
- 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 +13 -0
- package/dist/src/abstract/plugin.d.ts +21 -0
- package/dist/src/bin/index.d.ts +38 -4
- package/dist/src/config/options.d.ts +16 -1
- package/dist/src/lib/errors.d.ts +2 -1
- package/dist/src/lib/ldap/callerView.d.ts +45 -0
- package/dist/src/lib/ldap/pool.d.ts +108 -0
- package/dist/src/lib/ldap/schema.d.ts +157 -0
- package/dist/src/lib/ldap/searchCache.d.ts +112 -0
- package/dist/src/lib/ldapActions.d.ts +25 -262
- package/dist/src/lib/parseConfig.d.ts +26 -2
- package/dist/src/lib/services/index.d.ts +56 -0
- package/dist/src/lib/utils.d.ts +72 -3
- package/dist/src/plugins/ldap/enterpriseRules.d.ts +0 -14
- package/dist/src/plugins/ldap/passwordPolicy.d.ts +3 -2
- package/dist/src/plugins/scim/mapping.d.ts +16 -3
- package/dist/src/plugins/scim/types.d.ts +2 -0
- package/dist/src/plugins/twake/instances.d.ts +31 -0
- package/dist/src/plugins/twake/lifecycleAttributes.d.ts +13 -2
- package/dist/src/plugins/twake/tombstone.d.ts +9 -2
- package/docs/README.md +25 -2
- package/docs/architecture/README.md +180 -0
- package/docs/architecture/adr/0001-plugin-load-order.md +50 -0
- package/docs/architecture/adr/0002-authentication-dispatch.md +43 -0
- package/docs/architecture/adr/0003-authorization-composition.md +59 -0
- package/docs/architecture/adr/0004-fail-closed-request-context.md +45 -0
- package/docs/architecture/adr/0005-hook-contract.md +54 -0
- package/docs/architecture/adr/0006-search-cache.md +49 -0
- package/docs/architecture/adr/0007-trash-and-tombstone.md +69 -0
- package/docs/architecture/adr/0008-withconfig-views.md +58 -0
- package/docs/client-development/api/rest-api.md +4 -0
- package/docs/development/contracts.md +119 -0
- package/docs/development/modularization-plan.md +197 -0
- package/docs/plugin-development/README.md +8 -1
- package/docs/plugin-development/dependencies.md +13 -2
- package/docs/plugin-development/hooks.md +4 -2
- package/docs/plugin-development/options.md +40 -13
- package/docs/plugin-development/services.md +95 -0
- package/docs/usage/operations.md +12 -0
- package/docs/usage/plugins/integrations/instances.md +11 -3
- package/docs/usage/plugins/integrations/lifecycle-events.md +4 -1
- package/docs/usage/plugins/integrations/scim.md +20 -18
- package/docs/usage/plugins/integrations/tombstone.md +10 -6
- package/docs/usage/plugins/ldap/flat-generic.md +8 -1
- package/docs/usage/plugins/ldap/groups.md +4 -1
- package/docs/usage/plugins/ldap/password-policy.md +7 -0
- package/docs/usage/upgrading.md +59 -0
- package/package.json +21 -12
- package/static/browser/ldap-tree-viewer/LdapTreeViewer.js.map +1 -1
- package/static/browser/ldap-tree-viewer.esm.js.map +1 -1
- package/static/browser/ldap-tree-viewer.js.map +1 -1
- package/static/browser/ldap-tree-viewer.min.js.map +1 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Modularization plan
|
|
2
|
+
|
|
3
|
+
How the remaining architecture work is sequenced: [#397] and the five
|
|
4
|
+
`code-quality` issues that depend on it or prepare it. The plan is a roadmap,
|
|
5
|
+
not a contract; update it as the work lands.
|
|
6
|
+
|
|
7
|
+
## Open issues
|
|
8
|
+
|
|
9
|
+
| Issue | Subject | Role | Size |
|
|
10
|
+
| ------ | ----------------------------------------------------------------------------------- | ----------------------------- | ------ |
|
|
11
|
+
| [#397] | Split the repository into `@ldap-rest/*` modules | Final step | Major |
|
|
12
|
+
| [#593] | Finish the plugin service migration: entity registry, call sites, family boundaries | Prerequisite of #397 | Large |
|
|
13
|
+
| [#594] | Finish the `ldapActions` split: operation pipeline and façade | Prerequisite of #397 | Medium |
|
|
14
|
+
| [#595] | Snapshot the plugin API types and gate changes | Safety net, start before #397 | Medium |
|
|
15
|
+
| [#549] | Architecture pages, decision records, comment check | Independent, start early | Medium |
|
|
16
|
+
| [#603] | Enable the type-checked ESLint rules the test block leaves out | Independent, quick win | Small |
|
|
17
|
+
|
|
18
|
+
[#397] is still in the `2.0.0` milestone, which is released. Retarget it to a
|
|
19
|
+
2.x epic or to 3.0.0 once the compatibility decision below is made. [#593] and
|
|
20
|
+
[#594] are labelled `Minor`/`backlog` but are the real prerequisites of
|
|
21
|
+
[#397]; promote them to the same milestone.
|
|
22
|
+
|
|
23
|
+
## What blocks #397 today
|
|
24
|
+
|
|
25
|
+
The core still imports plugin code:
|
|
26
|
+
|
|
27
|
+
- `src/config/args.ts` imports the option sets of every plugin family.
|
|
28
|
+
- `src/bin/index.ts` imports `src/plugins/priority.json`.
|
|
29
|
+
- `src/hooks.ts` imports `ChangesToNotify` from
|
|
30
|
+
`src/plugins/ldap/onChange.ts` and the SCIM types from
|
|
31
|
+
`src/plugins/scim/types.ts`.
|
|
32
|
+
- `src/abstract/ldapFlat.ts` reads `server.loadedPlugins` and uses
|
|
33
|
+
`requirePlugin`.
|
|
34
|
+
- `src/lib/ldapActions.ts` is 1,375 lines; `src/lib/ldap/operations.ts` does
|
|
35
|
+
not exist.
|
|
36
|
+
- The single package generates 66 `./plugin-*` exports from
|
|
37
|
+
`rollup.config.mjs`, with no npm workspaces.
|
|
38
|
+
|
|
39
|
+
Between plugin families the coupling is smaller: only the Twake plugins import
|
|
40
|
+
`ldap`/`scim` (15 imports), plus `TwakeGroups extends LdapGroups`.
|
|
41
|
+
|
|
42
|
+
## Target architecture
|
|
43
|
+
|
|
44
|
+
An npm workspaces monorepo with an umbrella package that preserves the public
|
|
45
|
+
API:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
packages/
|
|
49
|
+
core/ @ldap-rest/core
|
|
50
|
+
plugin-auth/ @ldap-rest/plugin-auth
|
|
51
|
+
plugin-ldap/ @ldap-rest/plugin-ldap
|
|
52
|
+
plugin-scim/ @ldap-rest/plugin-scim
|
|
53
|
+
plugin-storage/ @ldap-rest/plugin-storage
|
|
54
|
+
plugin-twake/ @ldap-rest/plugin-twake
|
|
55
|
+
plugin-misc/ @ldap-rest/plugin-misc
|
|
56
|
+
browser/ @ldap-rest/browser
|
|
57
|
+
schemas/ @ldap-rest/schemas
|
|
58
|
+
ldap-rest/ umbrella package (CLI and re-exports)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Rules:
|
|
62
|
+
|
|
63
|
+
- `core` depends on no family; a family depends on no other family. Cross-family
|
|
64
|
+
needs go through services (`src/lib/services`) or hooks.
|
|
65
|
+
- `TwakeGroups extends LdapGroups` is either the one documented exception or
|
|
66
|
+
the shared base moves to `core`.
|
|
67
|
+
- `ldap-rest` stays the entry package: it depends on every package, keeps the
|
|
68
|
+
CLI and the `core/…` namespace, and re-exports the former sub-paths
|
|
69
|
+
(`ldap-rest/plugin-auth-authzlinid1`, …). The split is therefore not breaking
|
|
70
|
+
for users.
|
|
71
|
+
- The core parser accepts an injected `OptionSet`; the umbrella composes the
|
|
72
|
+
option sets of every family. `Config` may stay a type-only aggregate in
|
|
73
|
+
`core`, but runtime option imports leave the core.
|
|
74
|
+
- Each package builds on its own with a shared configuration; the umbrella
|
|
75
|
+
re-exports. The Twake-specific `bin/` scripts move to their package or to a
|
|
76
|
+
`@ldap-rest/cli` package.
|
|
77
|
+
|
|
78
|
+
A package per plugin (`@ldap-rest/authz-linid1`, …) is possible, but start with
|
|
79
|
+
families and extract optional plugins individually later. Otherwise the first
|
|
80
|
+
day costs 66 packages and 66 release cycles.
|
|
81
|
+
|
|
82
|
+
## Phases
|
|
83
|
+
|
|
84
|
+
### Phase 0 — Guardrails and quick wins
|
|
85
|
+
|
|
86
|
+
1. [#603]: enable the type-checked rules on `test/**/*.ts`, fix the 31
|
|
87
|
+
`no-floating-promises` and 24 `await-thenable` reports, add the block to CI.
|
|
88
|
+
2. [#549], part 1: `docs/architecture/README.md` (module map, life of a
|
|
89
|
+
request, life of a write) and the decision records for the decisions already
|
|
90
|
+
in place. Add `scripts/check-comments.sh` with a baseline that may only
|
|
91
|
+
shrink. The GitHub ruleset change can land separately.
|
|
92
|
+
3. [#595], part 1: `@microsoft/api-extractor` on the five main entry points
|
|
93
|
+
(`ldap-rest`, `./plugin`, `./hooks`, `./ldapactions`,
|
|
94
|
+
`./expressformatedresponses`), reports under `etc/api/`, wired into the
|
|
95
|
+
`contracts` job. The 66 `./plugin-*` entries wait for phase 3.
|
|
96
|
+
|
|
97
|
+
Outcome: CI catches regressions, the current architecture is documented, and
|
|
98
|
+
the public types have a baseline.
|
|
99
|
+
|
|
100
|
+
### Phase 1 — Decouple the core
|
|
101
|
+
|
|
102
|
+
1. [#593]:
|
|
103
|
+
- typed entity registry replacing the six hand-written loops;
|
|
104
|
+
- `GroupsApi`, `OrganizationsApi`, `TrashApi`, `PasswordPolicyApi`,
|
|
105
|
+
`TwakeGroupsApi`, `AuthenticatorsInfo` service interfaces;
|
|
106
|
+
- replace the 68 `loadedPlugins` reads and the 17 `requirePlugin` uses;
|
|
107
|
+
- move `ChangesToNotify` out of `plugins/ldap/onChange.ts` and the SCIM
|
|
108
|
+
types out of `plugins/scim/` into `src/lib/`;
|
|
109
|
+
- ESLint: forbid `loadedPlugins` under `src/plugins/**` and imports between
|
|
110
|
+
plugin families.
|
|
111
|
+
2. [#594]: extract the operation pipeline into `src/lib/ldap/operations.ts`,
|
|
112
|
+
bring `ldapActions.ts` under 400 lines, add the file-size test, move the
|
|
113
|
+
ESLint exemption, add the type test on `ldap-rest/ldapactions`.
|
|
114
|
+
3. Options refactor (needs its own issue): split `src/config/args.ts` into core
|
|
115
|
+
options and per-family option sets; let the parser/`DM` accept an
|
|
116
|
+
`OptionSet`; move `priority.json` into the core or make it a parameter.
|
|
117
|
+
|
|
118
|
+
Outcome: `src/hooks.ts` imports no plugin, the core reads no `loadedPlugins`,
|
|
119
|
+
families do not import each other, and `ldapActions` is a stable façade.
|
|
120
|
+
|
|
121
|
+
### Phase 2 — Workspaces and extraction
|
|
122
|
+
|
|
123
|
+
1. Set up npm workspaces, `packages/`, a shared `tsconfig` and per-package
|
|
124
|
+
builds.
|
|
125
|
+
2. Extract `@ldap-rest/core` first, with the `ldap-rest` umbrella depending on
|
|
126
|
+
it and re-exporting the existing entry points.
|
|
127
|
+
3. Extract the families in dependency order: storage → auth → ldap → scim →
|
|
128
|
+
twake → misc.
|
|
129
|
+
4. Extract `browser` and `schemas`.
|
|
130
|
+
5. After each step: `npm run test:dev`, `npm run check`,
|
|
131
|
+
`npm run check:openapi`, `npm run check:options`, and the `contracts` job.
|
|
132
|
+
6. Update the Dockerfile, the `bin/` scripts, the examples and the docs.
|
|
133
|
+
|
|
134
|
+
Outcome: a modular repository, with `ldap-rest` still installable and
|
|
135
|
+
compatible.
|
|
136
|
+
|
|
137
|
+
### Phase 3 — Contracts and finalization
|
|
138
|
+
|
|
139
|
+
1. [#595], part 2: one api-extractor report per sub-path, including the
|
|
140
|
+
`./plugin-*` entries, and non-additive diffs refused by `contracts`.
|
|
141
|
+
2. [#549], part 2: the modularization decision record, a shrinking comment
|
|
142
|
+
baseline, and the `require_code_owner_review` ruleset.
|
|
143
|
+
3. Publish the scoped packages and document the deprecation policy for the
|
|
144
|
+
former sub-paths.
|
|
145
|
+
4. Close [#397].
|
|
146
|
+
|
|
147
|
+
## Branch strategy
|
|
148
|
+
|
|
149
|
+
Phase 0 lands on `master`: it is non-breaking, and #603 fixes test bugs that
|
|
150
|
+
matter to the default branch too. Merge `master` into `v3.0` before phase 1,
|
|
151
|
+
so the 3.0 work starts from a green, guarded base. Phases 1 to 3 land on
|
|
152
|
+
`v3.0`.
|
|
153
|
+
|
|
154
|
+
## Order
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
#603 ───────────────┐
|
|
158
|
+
#549 (part 1) ──────┼──> Phase 0
|
|
159
|
+
#595 (part 1) ──────┘
|
|
160
|
+
│
|
|
161
|
+
#593 ────────────────┼──> Phase 1 ──> #397 (Phase 2) ──> #595/#549 (part 2)
|
|
162
|
+
#594 ────────────────┘
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
[#603] and [#549] part 1 can start in parallel. [#595] part 1 is a short
|
|
166
|
+
spike. [#593] and [#594] can then run in parallel, but both touch the core:
|
|
167
|
+
freeze new cross-family imports and new `loadedPlugins` reads from the start of
|
|
168
|
+
phase 1.
|
|
169
|
+
|
|
170
|
+
## Decisions to make
|
|
171
|
+
|
|
172
|
+
1. **Granularity**: packages per family (recommended) or one package per plugin
|
|
173
|
+
from the start?
|
|
174
|
+
2. **Versioning**: a non-breaking internal split in 2.x with the umbrella
|
|
175
|
+
(recommended), or 3.0.0 with the former sub-paths removed?
|
|
176
|
+
3. **`TwakeGroups extends LdapGroups`**: documented exception, or move the
|
|
177
|
+
shared base into `core`?
|
|
178
|
+
4. **`Config` type**: type-only aggregate in `core` (recommended to start), or
|
|
179
|
+
split per package with declaration merging?
|
|
180
|
+
5. **Build**: Rollup per package (consistent with today) or `tsc` with
|
|
181
|
+
generated exports?
|
|
182
|
+
|
|
183
|
+
## First actions
|
|
184
|
+
|
|
185
|
+
1. Retarget [#397] and add the package map and the dependency rule to it.
|
|
186
|
+
2. Open sub-issues for [#593] and [#594]; both are too large for one pull
|
|
187
|
+
request.
|
|
188
|
+
3. Start [#603] and [#549] part 1 in parallel.
|
|
189
|
+
4. Spike [#595] on the five main entry points.
|
|
190
|
+
5. Then [#593]/[#594], then the package extraction.
|
|
191
|
+
|
|
192
|
+
[#397]: https://github.com/linagora/ldap-rest/issues/397
|
|
193
|
+
[#549]: https://github.com/linagora/ldap-rest/issues/549
|
|
194
|
+
[#593]: https://github.com/linagora/ldap-rest/issues/593
|
|
195
|
+
[#594]: https://github.com/linagora/ldap-rest/issues/594
|
|
196
|
+
[#595]: https://github.com/linagora/ldap-rest/issues/595
|
|
197
|
+
[#603]: https://github.com/linagora/ldap-rest/issues/603
|
|
@@ -7,6 +7,7 @@ This guide will help you create powerful, production-ready plugins for LDAP-Rest
|
|
|
7
7
|
- [Why Create a Plugin](#why-create-a-plugin)
|
|
8
8
|
- [Plugin Structure](#plugin-structure)
|
|
9
9
|
- [Creating Your Plugin](#creating-your-plugin)
|
|
10
|
+
- [Plugin services](./services.md)
|
|
10
11
|
- [Custom Configuration Options](#custom-configuration-options)
|
|
11
12
|
- [Plugin options](./options.md)
|
|
12
13
|
- [Loading Your Plugin](#loading-your-plugin)
|
|
@@ -54,6 +55,8 @@ export default class MyPlugin extends DmPlugin {
|
|
|
54
55
|
- **roles** (optional): Array categorizing plugin purpose
|
|
55
56
|
- **dependencies** (optional): Required plugins that must load first
|
|
56
57
|
- **hooks** (optional): Callbacks for LDAP operations
|
|
58
|
+
- **provides** (optional): Typed services this plugin offers, see
|
|
59
|
+
[plugin services](services.md)
|
|
57
60
|
- **api()** (optional): Method to register REST endpoints
|
|
58
61
|
|
|
59
62
|
### Available from Base Class
|
|
@@ -65,6 +68,8 @@ export default class MyPlugin extends DmPlugin {
|
|
|
65
68
|
- **this.logger**: Winston logger instance
|
|
66
69
|
- **this.hookBus**: The server's hooks: chain, notify and wait on them, see
|
|
67
70
|
[hooks](hooks.md#the-bus)
|
|
71
|
+
- **this.service(token)**: The instance another plugin provides under a
|
|
72
|
+
service token, or `null`, see [plugin services](services.md)
|
|
68
73
|
- **this.fetch**: `fetch` timed into the
|
|
69
74
|
[outbound metrics](../usage/operations.md#metrics) under the plugin's
|
|
70
75
|
name; the lint refuses a bare `fetch` or `node-fetch` under `src`, and a
|
|
@@ -452,7 +457,8 @@ const results = await this.server.ldap.search(
|
|
|
452
457
|
'ou=users,dc=example,dc=com'
|
|
453
458
|
);
|
|
454
459
|
|
|
455
|
-
// Search with pagination (returns AsyncGenerator
|
|
460
|
+
// Search with pagination (returns AsyncGenerator; the connection is taken
|
|
461
|
+
// when the first page is read)
|
|
456
462
|
const pagedResults = await this.server.ldap.search(
|
|
457
463
|
{
|
|
458
464
|
paged: true,
|
|
@@ -1320,6 +1326,7 @@ See [Tests and checks](../../CONTRIBUTING.md#tests-and-checks).
|
|
|
1320
1326
|
|
|
1321
1327
|
- [Contributing Guide](../../CONTRIBUTING.md) - Git workflow and PR guidelines
|
|
1322
1328
|
- [Hooks Reference](./hooks.md) - Complete list of available hooks
|
|
1329
|
+
- [Plugin services](./services.md) - Typed interfaces between plugins
|
|
1323
1330
|
- [Core Plugin Examples](../../src/plugins/) - Browse existing plugins
|
|
1324
1331
|
- [Test Examples](../../test/plugins/) - See how core plugins are tested
|
|
1325
1332
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Plugin Dependency Graph
|
|
2
2
|
|
|
3
|
-
This document describes the dependencies between LDAP-Rest plugins. There are
|
|
3
|
+
This document describes the dependencies between LDAP-Rest plugins. There are three types of dependencies:
|
|
4
4
|
|
|
5
5
|
1. **Explicit Dependencies**: Defined in the `dependencies` property of a plugin class
|
|
6
|
-
2. **
|
|
6
|
+
2. **Service Dependencies**: A plugin reaches a typed interface another one provides, see [plugin services](services.md)
|
|
7
|
+
3. **Hook Dependencies**: Implicit dependencies through hooks (one plugin provides a hook, another consumes it)
|
|
7
8
|
|
|
8
9
|
## Dependency Types
|
|
9
10
|
|
|
@@ -24,6 +25,16 @@ overrides — a dependency also given to `--plugin`, or an entry listed twice
|
|
|
24
25
|
is loaded once: the second request waits for the first. Only an entry with
|
|
25
26
|
overrides builds a second instance, and it needs a name of its own.
|
|
26
27
|
|
|
28
|
+
### Service Dependencies
|
|
29
|
+
|
|
30
|
+
A plugin provides a typed interface under a token, and a consumer reaches it
|
|
31
|
+
with `this.service(token)`. The token replaces the registry name as the way to
|
|
32
|
+
find another plugin's code; `dependencies` still decides what loads first. A
|
|
33
|
+
consumer whose provider is absent gets `null` and a single warning.
|
|
34
|
+
|
|
35
|
+
See [plugin services](services.md) for the token, the provided interface and
|
|
36
|
+
the registration rules.
|
|
37
|
+
|
|
27
38
|
### Hook Dependencies
|
|
28
39
|
|
|
29
40
|
**Hook Providers**: Plugins that implement hooks (via `hooks: Hooks = { ... }`)
|
|
@@ -24,7 +24,8 @@ subscriber answers:
|
|
|
24
24
|
A chained hook is handed its arguments as one value, a tuple when it has
|
|
25
25
|
several. A notified hook is handed them one by one: its subscriber is
|
|
26
26
|
`(dn, entry, context) => …`, never `([dn, entry], context) => …` — that form
|
|
27
|
-
still compiles
|
|
27
|
+
still compiles without annotation, since its parameter takes the declared
|
|
28
|
+
`string` and a string can be destructured, but then reads the DN's first
|
|
28
29
|
character as the DN.
|
|
29
30
|
|
|
30
31
|
- **Chained.** The value goes from one subscriber to the next, each getting
|
|
@@ -60,7 +61,7 @@ await server.hookBus.settled();
|
|
|
60
61
|
- **hello**: raised by the [demo plugin](../../src/plugins/demo/helloworld.ts) for `/hello`, answered by the demo plugins
|
|
61
62
|
- **ldapsearchopts**: chained before any search, with its options
|
|
62
63
|
- **ldapsearchrequest**: chained before any search, with `[base, options, request]`; an authorization plugin refuses by throwing
|
|
63
|
-
- **ldapsearchresult**: chained after any search, with its result, before it is cached
|
|
64
|
+
- **ldapsearchresult**: chained after any search, with its result, before it is cached; for a paged search, when the first page is read
|
|
64
65
|
- **ldapsearchfilter**: chained on the result of a search after the cache, once per page, with `[result, request, options]`, to drop what the caller may not see
|
|
65
66
|
- **ldapaddrequest**: chained before any add, with `[dn, entry, request]`
|
|
66
67
|
- **ldapadddone**: notified once an add has written the entry, with the DN, the entry and the context, one by one; not awaited
|
|
@@ -118,6 +119,7 @@ await server.hookBus.settled();
|
|
|
118
119
|
- **scimgroupdeletedone**: notified once a SCIM group is deleted, with its id
|
|
119
120
|
- **scimbulkdone**: notified once a SCIM bulk request is answered
|
|
120
121
|
- **twakedeletionreplay**: notified with the DN, the entry and the context, one by one when a delete asks of an entry that is already a tombstone
|
|
122
|
+
|
|
121
123
|
<!-- /hooks -->
|
|
122
124
|
|
|
123
125
|
## Flat entities
|
|
@@ -42,17 +42,43 @@ export type ExampleOptions = OptionsOf<typeof exampleOptions>;
|
|
|
42
42
|
The key is the `Config` name: `example_timeout` is `--example-timeout`,
|
|
43
43
|
`DM_EXAMPLE_TIMEOUT` when the descriptor names that variable.
|
|
44
44
|
|
|
45
|
+
### Deprecating an option
|
|
46
|
+
|
|
47
|
+
Rename an option by giving the old name as an alias: the parser still reads
|
|
48
|
+
it, and the server warns once at startup. Keep the alias for two minor
|
|
49
|
+
releases, as [CONTRIBUTING.md](../../CONTRIBUTING.md#deprecating-a-published-contract)
|
|
50
|
+
requires.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
export const exampleOptions = defineOptions({
|
|
54
|
+
example_url: opt.string({
|
|
55
|
+
env: 'DM_EXAMPLE_URL',
|
|
56
|
+
aliases: ['example-uri'],
|
|
57
|
+
envAliases: ['DM_EXAMPLE_URI'],
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A CLI alias takes one value per occurrence, as the singular form does, and an
|
|
63
|
+
array option's plural form has no alias: renaming it is a breaking change. An
|
|
64
|
+
environment alias is read when `env` is unset, or empty for a type where an
|
|
65
|
+
empty value means nothing, and only for a non-array option when its canonical
|
|
66
|
+
CLI name is absent; an array option combines the environment value with the
|
|
67
|
+
CLI one, so its alias is still read.
|
|
68
|
+
|
|
45
69
|
Each declaration takes:
|
|
46
70
|
|
|
47
|
-
| Field
|
|
48
|
-
|
|
|
49
|
-
| `env`
|
|
50
|
-
| `default`
|
|
51
|
-
| `required`
|
|
52
|
-
| `plural`
|
|
53
|
-
| `kind`
|
|
54
|
-
| `min/max`
|
|
55
|
-
| `choices`
|
|
71
|
+
| Field | Meaning |
|
|
72
|
+
| ------------ | --------------------------------------------------------------------- |
|
|
73
|
+
| `env` | Environment variable read when the CLI does not set the option |
|
|
74
|
+
| `default` | Value kept when neither the CLI nor the environment sets the option |
|
|
75
|
+
| `required` | Refuse an empty value instead of reading it as unset |
|
|
76
|
+
| `plural` | Array only: CLI name of the plural form, without the leading `--` |
|
|
77
|
+
| `kind` | Array only: how a value splits, see `ArrayKind` in `parseConfig.ts` |
|
|
78
|
+
| `min/max` | Number only: bounds checked when the option is read |
|
|
79
|
+
| `choices` | String only: the values the option accepts |
|
|
80
|
+
| `aliases` | Former CLI names still accepted, without the leading `--` |
|
|
81
|
+
| `envAliases` | Former environment variables still accepted, read when `env` is unset |
|
|
56
82
|
|
|
57
83
|
## Reading options
|
|
58
84
|
|
|
@@ -103,7 +129,8 @@ moved. A migrated plugin no longer reads them: it uses `options()`.
|
|
|
103
129
|
|
|
104
130
|
Plugins outside the repository cannot add to `args.ts` yet, but they can
|
|
105
131
|
declare options the same way and name them in `optionDefinitions`: the startup
|
|
106
|
-
report subtracts those keys, and `options()` reads a declared
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
132
|
+
report subtracts those keys and their aliases, and `options()` reads a declared
|
|
133
|
+
`env` variable or a deprecated CLI alias the parser did not carry, warning at
|
|
134
|
+
startup for the alias as it does for a core option. An option without a
|
|
135
|
+
declaration still reaches `this.config` from an unknown `--key value` pair, and
|
|
136
|
+
the startup report names it.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Plugin services
|
|
2
|
+
|
|
3
|
+
A plugin sometimes needs what another plugin holds: the group operations, the
|
|
4
|
+
entity schemas, the trash. A service is a typed interface a plugin provides
|
|
5
|
+
under a token, and another plugin reaches with `this.service(token)`.
|
|
6
|
+
|
|
7
|
+
The token carries the interface. The compiler checks that an instance bound
|
|
8
|
+
with `with()` answers it, and gives a consumer the type it declared. Whether a
|
|
9
|
+
provider exists is a runtime question: a plugin that stops declaring the token
|
|
10
|
+
still compiles, and its consumers get `null` and a warning. The plugin
|
|
11
|
+
registry name stays what it is for: loading a plugin and naming it in the
|
|
12
|
+
logs.
|
|
13
|
+
|
|
14
|
+
## Declaring a service
|
|
15
|
+
|
|
16
|
+
Put the token and its interface in a module both sides import — a plugin
|
|
17
|
+
family's own service in its own module. A service shared between families is
|
|
18
|
+
declared in `src/lib/services/` and re-exported from `ldap-rest/plugin`, one
|
|
19
|
+
of the package's entry points; a module under `src/lib/` has no export path of
|
|
20
|
+
its own.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { serviceToken } from 'ldap-rest/plugin';
|
|
24
|
+
|
|
25
|
+
export interface GroupsApi {
|
|
26
|
+
listGroups(): Promise<string[]>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const GroupsService = serviceToken<GroupsApi>('groups');
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Declare a token once. The object is the registry key; calling `serviceToken`
|
|
33
|
+
again under the same name declares another service, not the same one. The name
|
|
34
|
+
only identifies it in the logs.
|
|
35
|
+
|
|
36
|
+
## Providing a service
|
|
37
|
+
|
|
38
|
+
Bind the plugin instance to each token it provides:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import DmPlugin from 'ldap-rest/plugin';
|
|
42
|
+
|
|
43
|
+
export default class LdapGroups extends DmPlugin {
|
|
44
|
+
name = 'ldapGroups';
|
|
45
|
+
provides = [GroupsService.with(this)];
|
|
46
|
+
|
|
47
|
+
async listGroups(): Promise<string[]> {
|
|
48
|
+
/* … */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`with(this)` is checked by the compiler: an instance that does not answer the
|
|
54
|
+
interface does not compile. A plugin can provide several services. A service
|
|
55
|
+
is registered once its plugin's registration completes — after `api()` and
|
|
56
|
+
the hooks — so a consumer loaded afterwards finds it, and a provider does not
|
|
57
|
+
find its own service there. A plugin needing another one declares it in
|
|
58
|
+
[`dependencies`](dependencies.md#explicit-dependencies) so that it loads
|
|
59
|
+
first.
|
|
60
|
+
|
|
61
|
+
The first provider to register wins. A second one offering the same token is
|
|
62
|
+
ignored and named in a warning: which one answered would otherwise depend on
|
|
63
|
+
load order. Loading the same module twice under two names gives two instances
|
|
64
|
+
but one module-level token, so the second instance cannot provide it either.
|
|
65
|
+
|
|
66
|
+
When a host registers a plugin after startup, its `assertComposition` runs
|
|
67
|
+
before the service is registered, so a provider does not find its own service
|
|
68
|
+
there; the configuration's plugins are all registered before their
|
|
69
|
+
`assertComposition` runs.
|
|
70
|
+
|
|
71
|
+
## Consuming a service
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const groups = this.service(GroupsService);
|
|
75
|
+
if (!groups) {
|
|
76
|
+
// Nothing provides it: the plugin skips the feature it would serve
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await groups.listGroups();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`service()` answers the provided instance, or `null` when nothing provides the
|
|
83
|
+
token. It logs one warning per plugin and service rather than one per call. A
|
|
84
|
+
plugin that cannot work without a service declares the provider in
|
|
85
|
+
`dependencies`, checks the answer in `assertComposition`, and throws naming the
|
|
86
|
+
service — that stops the server at startup instead of leaving a feature
|
|
87
|
+
silently absent. A constructor runs before the dependencies load, so it cannot
|
|
88
|
+
be the place for that check.
|
|
89
|
+
|
|
90
|
+
The registry belongs to the server and is shared by the views `withConfig`
|
|
91
|
+
builds for plugins loaded with overrides. It has no removal: a plugin
|
|
92
|
+
registers once and keeps its services for the life of the server.
|
|
93
|
+
|
|
94
|
+
`this.requirePlugin(name)` remains for a plugin whose interface is not yet
|
|
95
|
+
declared as a service; new code reaches another plugin through a token.
|
package/docs/usage/operations.md
CHANGED
|
@@ -53,6 +53,18 @@ The image runs as its unprivileged `node` user: with `docker run`, a
|
|
|
53
53
|
`DM_PORT` below 1024 stops the server at startup, where the default 8081
|
|
54
54
|
changes nothing.
|
|
55
55
|
|
|
56
|
+
The image is built in two stages: a builder produces the package and installs
|
|
57
|
+
its production dependencies, the runtime stage copies them and carries no
|
|
58
|
+
compiler nor package manager cache. The runtime stage upgrades the Alpine
|
|
59
|
+
packages of its base image with `apk upgrade --no-cache`, so a security fix
|
|
60
|
+
published after the base image was built still reaches the image: two builds
|
|
61
|
+
from the same digest may differ in those packages. Its base image is pinned by
|
|
62
|
+
digest in `scripts/buildDockerfile.ts`, so the image built is the one reviewed
|
|
63
|
+
even if the tag moves; the digest follows the tag it names, taken from the
|
|
64
|
+
[Docker Hub tag page](https://hub.docker.com/_/node/tags?name=25-alpine) and
|
|
65
|
+
updated by hand with it. Dependabot ignores the image: it would open a change
|
|
66
|
+
to the generated `Dockerfile`, which the drift check refuses.
|
|
67
|
+
|
|
56
68
|
### What the shipped plugins report
|
|
57
69
|
|
|
58
70
|
| Plugin | `degraded` when |
|
|
@@ -19,7 +19,13 @@ gets no instance.
|
|
|
19
19
|
An entry holding `--twake-instance-skip-value` in
|
|
20
20
|
`--twake-instance-skip-attribute`, a technical account for instance, gets
|
|
21
21
|
none, and neither does a tombstone
|
|
22
|
-
(`--twake-lifecycle-deleted-attribute`) put back in place.
|
|
22
|
+
(`--twake-lifecycle-deleted-attribute`) put back in place. Each expression
|
|
23
|
+
is tried against the canonical DN first, then against the DN as sent: an
|
|
24
|
+
account created under a branch written with other spacing or case is still
|
|
25
|
+
one, and an expression written with the directory's own spelling (`\2C`, a
|
|
26
|
+
doubled space) still matches. An expression naming a value that holds a
|
|
27
|
+
comma writes it `\2C` or `\,`: a value comma never stands in for an RDN
|
|
28
|
+
separator in the form matched.
|
|
23
29
|
- The address (`--twake-instance-fqdn-attribute`) and the mark
|
|
24
30
|
(`--twake-instance-sent-attribute`) are this plugin's to write: an add that
|
|
25
31
|
carries them has them removed, and an address written by a modify is taken
|
|
@@ -80,8 +86,10 @@ before it takes its members' `user.created`. Then `organization.created` is
|
|
|
80
86
|
published on the b2b exchange, and the entry is marked. The message carries
|
|
81
87
|
the organization's id, address, domain, and its name as `organization`, read
|
|
82
88
|
from `--twake-instance-organization-name-attribute`, which
|
|
83
|
-
`ensureOrganization` fills from `name` when the entry has none.
|
|
84
|
-
|
|
89
|
+
`ensureOrganization` fills from `name` when the entry has none. An id captured
|
|
90
|
+
from a `--twake-instance-dn` group is read from the organization entry when
|
|
91
|
+
one exists, so the case a caller spells the branch in is not sent. The mark,
|
|
92
|
+
not the address, says the organization is done.
|
|
85
93
|
|
|
86
94
|
With the Cloudery, a member's `user.created` waits for its organization's: a
|
|
87
95
|
member under an organization entry that is not marked yet gets its address
|
|
@@ -110,7 +110,10 @@ operational one such as `pwdAccountLockedTime` is seen on both sides.
|
|
|
110
110
|
means entries are never tombstones, and only a removal publishes `deleted`.
|
|
111
111
|
- `--twake-lifecycle-deleted-at-attribute`,
|
|
112
112
|
`--twake-lifecycle-deleted-at-format` (`iso8601`, the default, or
|
|
113
|
-
`generalizedTime`): the deletion date. It is
|
|
113
|
+
`generalizedTime`): the deletion date. It is read whether it is written as
|
|
114
|
+
a GeneralizedTime or as an ISO 8601 date, and always published as ISO 8601;
|
|
115
|
+
a value of neither shape is left out. A GeneralizedTime needs its zone; an
|
|
116
|
+
ISO 8601 date and time without one is read in the server's time zone.
|
|
114
117
|
- `--twake-lifecycle-member-attribute` (default `member`): the attribute
|
|
115
118
|
`memberAdded`, `memberRemoved` and member lists read.
|
|
116
119
|
- `--twake-lifecycle-rules`: a JSON file, or the JSON itself.
|
|
@@ -223,16 +223,17 @@ and `--scim-group-mapping`.
|
|
|
223
223
|
|
|
224
224
|
Each element of `entries` is a `MappingEntry` with the following optional fields:
|
|
225
225
|
|
|
226
|
-
| Field | Purpose
|
|
227
|
-
| --------------- |
|
|
228
|
-
| `scim` | **Required.** Top-level SCIM attribute name (`userName`, `displayName`, `emails`, …).
|
|
229
|
-
| `ldap` | Simple 1:1 mapping to an LDAP attribute.
|
|
230
|
-
| `ldapPrimary` | For multi-valued SCIM attributes (`emails`, `phoneNumbers`, …), the LDAP attribute that stores the _primary_ value.
|
|
231
|
-
| `ldapSecondary` | LDAP attribute that stores the _non-primary_ values of the same SCIM attribute (array).
|
|
232
|
-
| `sub` | Object that maps SCIM sub-attributes to LDAP attributes. Used for complex SCIM values like `name.familyName` → `sn`.
|
|
233
|
-
| `
|
|
234
|
-
| `
|
|
235
|
-
| `
|
|
226
|
+
| Field | Purpose |
|
|
227
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
228
|
+
| `scim` | **Required.** Top-level SCIM attribute name (`userName`, `displayName`, `emails`, …). |
|
|
229
|
+
| `ldap` | Simple 1:1 mapping to an LDAP attribute. |
|
|
230
|
+
| `ldapPrimary` | For multi-valued SCIM attributes (`emails`, `phoneNumbers`, …), the LDAP attribute that stores the _primary_ value. |
|
|
231
|
+
| `ldapSecondary` | LDAP attribute that stores the _non-primary_ values of the same SCIM attribute (array). |
|
|
232
|
+
| `sub` | Object that maps SCIM sub-attributes to LDAP attributes. Used for complex SCIM values like `name.familyName` → `sn`. |
|
|
233
|
+
| `subDn` | Sub-attribute names in `sub` whose value is a DN (for example `["value"]` for `manager`); they are held to RFC 4514 on write. |
|
|
234
|
+
| `multi` | `"array"` or `"single"` (default) — hints array serialization for `ldap`. |
|
|
235
|
+
| `readOnly` | `true` to skip this attribute on write (create/update ignore it). |
|
|
236
|
+
| `operational` | `true` for LDAP operational attributes (e.g. `entryUUID`) — loaded on read only. |
|
|
236
237
|
|
|
237
238
|
### Merging
|
|
238
239
|
|
|
@@ -269,7 +270,8 @@ override only what's different and inherit the rest.
|
|
|
269
270
|
},
|
|
270
271
|
{
|
|
271
272
|
"scim": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager",
|
|
272
|
-
"sub": { "value": "manager" }
|
|
273
|
+
"sub": { "value": "manager" },
|
|
274
|
+
"subDn": ["value"]
|
|
273
275
|
}
|
|
274
276
|
]
|
|
275
277
|
}
|
|
@@ -317,7 +319,11 @@ of them; a complex attribute takes an object whose sub-attributes are scalars
|
|
|
317
319
|
multi-valued complex attribute such as `emails` takes a list of objects whose
|
|
318
320
|
`value` is a scalar. Anything else is answered
|
|
319
321
|
`400 invalidValue`, as is a PATCH operation that is not an object or whose
|
|
320
|
-
`op` is not a string.
|
|
322
|
+
`op` is not a string. A reference the mapping declares a DN (`subDn`) is
|
|
323
|
+
refused with `400 invalidValue` when its value holds an escape RFC 4514 does
|
|
324
|
+
not define, as the flat routes hold a client-written DN. The directory
|
|
325
|
+
refuses such a value itself on an attribute of DN syntax; this names the
|
|
326
|
+
escape in the SCIM answer and applies wherever the mapping declares one.
|
|
321
327
|
|
|
322
328
|
A PATCH `add`, or a `remove` naming values, changes only the values it names:
|
|
323
329
|
values the caller's read did not show, such as group members an
|
|
@@ -715,18 +721,14 @@ Response shape (truncated):
|
|
|
715
721
|
"id",
|
|
716
722
|
"active"
|
|
717
723
|
],
|
|
718
|
-
"mapping": [
|
|
719
|
-
/* same shape as default-mapping.json */
|
|
720
|
-
]
|
|
724
|
+
"mapping": [/* same shape as default-mapping.json */]
|
|
721
725
|
},
|
|
722
726
|
{
|
|
723
727
|
"id": "Group",
|
|
724
728
|
"endpoint": "/scim/v2/Groups",
|
|
725
729
|
"schema": "urn:ietf:params:scim:schemas:core:2.0:Group",
|
|
726
730
|
"rdnAttribute": "cn",
|
|
727
|
-
"mapping": [
|
|
728
|
-
/* ... */
|
|
729
|
-
]
|
|
731
|
+
"mapping": [/* ... */]
|
|
730
732
|
}
|
|
731
733
|
],
|
|
732
734
|
"idStrategy": "rdn",
|
|
@@ -10,10 +10,13 @@ is old enough, or right away when forced.
|
|
|
10
10
|
|
|
11
11
|
## Behaviour
|
|
12
12
|
|
|
13
|
-
- A delete of an entry whose DN matches `--twake-tombstone-dn`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
- A delete of an entry whose DN matches `--twake-tombstone-dn` — under any
|
|
14
|
+
spelling the directory folds, so spaces, case and escapes do not matter —
|
|
15
|
+
writes, in one modify: the deleted attribute, the deletion date (now), the
|
|
16
|
+
reason, the lock, and removes `--twake-tombstone-clear-attributes`. Other
|
|
17
|
+
entries are deleted as usual. A multi-valued RDN may not match: an
|
|
18
|
+
expression anchored on `uid=` misses a DN written `cn=…+uid=…`, and that
|
|
19
|
+
delete removes the entry for good.
|
|
17
20
|
- A delete of an existing tombstone writes nothing and answers as a delete.
|
|
18
21
|
It fires the `twakedeletionreplay` hook with the tombstone, so
|
|
19
22
|
[lifecycle events](lifecycle-events.md) publishes the deletion again, with
|
|
@@ -62,8 +65,9 @@ Content-Type: application/json
|
|
|
62
65
|
|
|
63
66
|
- `200`: erased.
|
|
64
67
|
- `404`: no tombstone at this DN.
|
|
65
|
-
- `409`: the deletion is younger than
|
|
66
|
-
`
|
|
68
|
+
- `409`: `force` is not true, and either the deletion is younger than
|
|
69
|
+
`--twake-tombstone-erase-min-age` or its date cannot be read as
|
|
70
|
+
[lifecycle events](lifecycle-events.md) reads it.
|
|
67
71
|
|
|
68
72
|
Other plugins call `erase(dn, { force, req })`.
|
|
69
73
|
|
|
@@ -453,7 +453,14 @@ Content-Type: application/json
|
|
|
453
453
|
|
|
454
454
|
The identifier may also be the whole DN of the entry, in the entity's
|
|
455
455
|
branch: `"uid": "uid=Smith\\,John,ou=users,dc=example,dc=com"` creates the
|
|
456
|
-
entry `Smith,John`, whose DN spells the name with its escapes.
|
|
456
|
+
entry `Smith,John`, whose DN spells the name with its escapes. The DN must
|
|
457
|
+
use the escapes RFC 4514 defines; an escape it does not define (`\q`) is
|
|
458
|
+
refused with a `400`. Send the bare value to have the server escape it.
|
|
459
|
+
|
|
460
|
+
The name written is the value the RDN gives under the folding the directory
|
|
461
|
+
applies (Unicode NFKC, and its insignificant spaces): `uid=\ zoë` and
|
|
462
|
+
`uid=zoë` name one entry, written `uid=zoë`, and a name sent decomposed is
|
|
463
|
+
stored composed.
|
|
457
464
|
|
|
458
465
|
### Modify Entry
|
|
459
466
|
|