ldap-rest 0.7.0 → 0.8.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/abstract/plugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/auth/authzScope.js +1 -0
- package/dist/plugins/auth/llng.js +1 -1
- package/dist/plugins/ldap/accountLifecycle.js +1 -0
- package/dist/plugins/ldap/bulkImport.js +1 -1
- package/dist/plugins/ldap/departmentSync.js +1 -1
- package/dist/plugins/ldap/enterpriseRules.js +1 -0
- package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
- package/dist/plugins/ldap/flatGeneric.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/twake/calendar.js +1 -0
- package/dist/plugins/twake/calendarResources.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +159 -1
- package/dist/src/abstract/plugin.d.ts +13 -0
- package/dist/src/config/args.d.ts +2 -0
- package/dist/src/config/schema.d.ts +382 -12
- package/dist/src/lib/expressFormatedResponses.d.ts +14 -0
- package/dist/src/lib/ldapActions.d.ts +33 -6
- package/dist/src/lib/utils.d.ts +65 -0
- package/dist/src/plugins/auth/authzScope.d.ts +53 -0
- package/dist/src/plugins/auth/llng.d.ts +30 -1
- package/dist/src/plugins/ldap/accountLifecycle.d.ts +56 -0
- package/dist/src/plugins/ldap/departmentSync.d.ts +43 -0
- package/dist/src/plugins/ldap/enterpriseRules.d.ts +436 -0
- package/dist/src/plugins/ldap/flatGeneric.d.ts +29 -0
- package/dist/src/plugins/ldap/groups.d.ts +22 -0
- package/dist/src/plugins/ldap/organizations.d.ts +25 -4
- package/dist/src/plugins/twake/calendar.d.ts +113 -0
- package/dist/src/plugins/twake/calendarResources.d.ts +9 -58
- package/docs/README.md +1 -0
- package/docs/client-development/api/openapi.md +1 -1
- package/docs/client-development/browser/libraries.md +1 -1
- package/docs/plugin-development/dependencies.md +10 -7
- package/docs/presentations/en.md +1 -1
- package/docs/presentations/fr.md +1 -1
- package/docs/usage/README.md +1 -0
- package/docs/usage/configuration.md +35 -15
- package/docs/usage/directory-audit.md +105 -0
- package/docs/usage/plugins/README.md +6 -6
- package/docs/usage/plugins/auth/README.md +6 -5
- package/docs/usage/plugins/auth/authz-scope.md +103 -0
- package/docs/usage/plugins/auth/llng.md +17 -0
- package/docs/usage/plugins/integrations/README.md +5 -5
- package/docs/usage/plugins/integrations/{calendar-resources.md → calendar.md} +70 -13
- package/docs/usage/plugins/ldap/README.md +13 -11
- package/docs/usage/plugins/ldap/account-lifecycle.md +129 -0
- package/docs/usage/plugins/ldap/bulk-import.md +8 -1
- package/docs/usage/plugins/ldap/enterprise-rules.md +167 -0
- package/docs/usage/plugins/ldap/flat-generic.md +172 -0
- package/docs/usage/plugins/ldap/groups.md +10 -4
- package/docs/usage/plugins/ldap/organizations.md +59 -33
- package/docs/usage/upgrading.md +322 -0
- package/package.json +50 -5
- package/static/schemas/example/README.md +36 -0
- package/static/schemas/example/groups.json +94 -0
- package/static/schemas/example/nomenclature/domains.json +49 -0
- package/static/schemas/example/organizations.json +106 -0
- package/static/schemas/example/positions.json +35 -0
- package/static/schemas/example/users.json +246 -0
- package/static/schemas/twake/groups.json +81 -15
- package/static/schemas/twake/nomenclature/twakeAccountStatus.json +26 -0
- package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +22 -0
- package/static/schemas/twake/nomenclature/twakeDomain.json +60 -0
- package/static/schemas/twake/nomenclature/twakeListType.json +22 -0
- package/static/schemas/twake/nomenclature/twakeMailboxType.json +22 -0
- package/static/schemas/twake/nomenclature/twakeTitle.json +42 -0
- package/static/schemas/twake/organizations.json +92 -12
- package/static/schemas/twake/positions.json +22 -4
- package/static/schemas/twake/users.json +269 -32
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
3
|
+
/**
|
|
4
|
+
* Draw a password from {@link PASSWORD_ALPHABET} with a uniform distribution.
|
|
5
|
+
*
|
|
6
|
+
* Bytes whose value would wrap the alphabet are discarded rather than folded
|
|
7
|
+
* with a modulo, which would make the first characters of the alphabet more
|
|
8
|
+
* likely than the last.
|
|
9
|
+
*
|
|
10
|
+
* @param length number of characters
|
|
11
|
+
* @returns the generated password
|
|
12
|
+
*/
|
|
13
|
+
export declare function generatePassword(length?: number): string;
|
|
14
|
+
export default class LdapAccountLifecycle extends DmPlugin {
|
|
15
|
+
name: string;
|
|
16
|
+
roles: Role[];
|
|
17
|
+
dependencies: {
|
|
18
|
+
ldapFlatGeneric: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Register the lifecycle endpoints of every flat entity whose schema asks
|
|
22
|
+
* for them.
|
|
23
|
+
*
|
|
24
|
+
* @param app Express application
|
|
25
|
+
*/
|
|
26
|
+
api(app: Express): void;
|
|
27
|
+
/**
|
|
28
|
+
* Apply a named state to an account.
|
|
29
|
+
*
|
|
30
|
+
* @param entity flat entity holding the account
|
|
31
|
+
* @param attribute attribute carrying the state
|
|
32
|
+
* @param states state names declared by the schema
|
|
33
|
+
* @param req Express request
|
|
34
|
+
* @param res Express response
|
|
35
|
+
*/
|
|
36
|
+
private setStatus;
|
|
37
|
+
/**
|
|
38
|
+
* Replace an account's credential, and flag it for a change at next login
|
|
39
|
+
* when the schema knows how to.
|
|
40
|
+
*
|
|
41
|
+
* @param entity flat entity holding the account
|
|
42
|
+
* @param attribute attribute carrying the credential
|
|
43
|
+
* @param req Express request
|
|
44
|
+
* @param res Express response
|
|
45
|
+
*/
|
|
46
|
+
private resetPassword;
|
|
47
|
+
/**
|
|
48
|
+
* Apply a change through the entity's own modify path, turning a missing
|
|
49
|
+
* entry into a 404 rather than a directory error.
|
|
50
|
+
*
|
|
51
|
+
* @param entity flat entity holding the account
|
|
52
|
+
* @param id identifier or DN of the entry
|
|
53
|
+
* @param changes modification to apply
|
|
54
|
+
*/
|
|
55
|
+
private modify;
|
|
56
|
+
}
|
|
@@ -15,7 +15,50 @@ export default class LdapDepartmentSync extends DmPlugin {
|
|
|
15
15
|
private linkAttr;
|
|
16
16
|
private pathAttr;
|
|
17
17
|
constructor(server: DM);
|
|
18
|
+
/**
|
|
19
|
+
* The attribute holding an organization's path: the `organizationPath` role
|
|
20
|
+
* of the organization schema, then the configured name.
|
|
21
|
+
*
|
|
22
|
+
* Role first, as the enterprise rules read it: a deployment naming the
|
|
23
|
+
* attribute only through the role otherwise saw nothing recomputed, and
|
|
24
|
+
* nothing said so. Resolved on each call, the organization schema being
|
|
25
|
+
* read asynchronously.
|
|
26
|
+
*
|
|
27
|
+
* @returns the attribute name
|
|
28
|
+
*/
|
|
29
|
+
private organizationPathAttribute;
|
|
30
|
+
/**
|
|
31
|
+
* The link and path attributes of the entries attached to an organization:
|
|
32
|
+
* those every loaded entity schema declares through the `organizationLink`
|
|
33
|
+
* and `organizationPath` roles, then the configured names.
|
|
34
|
+
*
|
|
35
|
+
* @returns distinct pairs, the ones declared by roles first
|
|
36
|
+
*/
|
|
37
|
+
private linkedAttributes;
|
|
18
38
|
hooks: Hooks;
|
|
39
|
+
/**
|
|
40
|
+
* Recompute the path of a renamed or moved organization and of every
|
|
41
|
+
* organization below it.
|
|
42
|
+
*
|
|
43
|
+
* A path names an organization's ancestors, root first, its own name last,
|
|
44
|
+
* the top organization left out. Moving or renaming one therefore changes
|
|
45
|
+
* the path of its whole subtree. Only the entries *linked* to the tree used
|
|
46
|
+
* to be updated, from paths the tree itself still held: the organizations
|
|
47
|
+
* kept the path of their former parent, the linked users were rewritten to
|
|
48
|
+
* it, and nothing could correct it since the path is computed by the server.
|
|
49
|
+
*
|
|
50
|
+
* Only organizations already holding a path are written.
|
|
51
|
+
*
|
|
52
|
+
* @param orgDn DN of the organization after the rename
|
|
53
|
+
*/
|
|
54
|
+
private updateOrganizationPaths;
|
|
55
|
+
/**
|
|
56
|
+
* Read the path an organization holds.
|
|
57
|
+
*
|
|
58
|
+
* @param dn organization DN
|
|
59
|
+
* @returns its path, or null when it holds none or cannot be read
|
|
60
|
+
*/
|
|
61
|
+
private readPath;
|
|
19
62
|
/**
|
|
20
63
|
* Update resources that are directly linked to the renamed organization
|
|
21
64
|
*/
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/ldap/enterpriseRules
|
|
3
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
+
*
|
|
5
|
+
* Business rules an enterprise directory needs on top of declarative schema
|
|
6
|
+
* validation: uniqueness across a shared namespace, mail addresses confined to
|
|
7
|
+
* the domains an organization owns, computed organization paths, normalised
|
|
8
|
+
* quotas, and referential-integrity guards on deletion.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here knows a concrete attribute name, a domain, or a nomenclature
|
|
11
|
+
* value. Every rule is driven by the loaded entity schemas — their semantic
|
|
12
|
+
* `role`s and their per-attribute markers (`unique`, `mailDomainScope`,
|
|
13
|
+
* `normalize`, `referentialIntegrity`, `deleteGuard`) — so the same code
|
|
14
|
+
* serves a directory laid out differently without a line of change.
|
|
15
|
+
*
|
|
16
|
+
* ## Order of evaluation
|
|
17
|
+
*
|
|
18
|
+
* Checks run **before** transformations, and both run inside the same hook.
|
|
19
|
+
* A check therefore always sees the payload as the client sent it: uniqueness
|
|
20
|
+
* and domain rules validate the address that was submitted, never one the
|
|
21
|
+
* server has already rewritten, and an identifier derived from that address
|
|
22
|
+
* cannot influence the check that justified it.
|
|
23
|
+
*
|
|
24
|
+
* ## What "a reference" is, and the one case this cannot see
|
|
25
|
+
*
|
|
26
|
+
* Two rules — the delete guard and the rename cascade — have to find every
|
|
27
|
+
* entry naming another one by its DN. They look in two places, both declared
|
|
28
|
+
* by the schemas: every `pointer` (single, or under `items`) whose `branch`
|
|
29
|
+
* admits the target, and every attribute carrying the `members` or `owners`
|
|
30
|
+
* role. The second is not redundant: a group's member list cannot be a
|
|
31
|
+
* pointer, since pointer validation requires the target to exist and a group
|
|
32
|
+
* legitimately holds the DN of an external member that has none.
|
|
33
|
+
*
|
|
34
|
+
* A deployment whose schema stores a DN in an attribute that is neither of
|
|
35
|
+
* those — a plain `string` carrying no role — is therefore not covered: its
|
|
36
|
+
* value is left naming the old DN after a rename, and does not hold a
|
|
37
|
+
* deletion back. Declare such an attribute as a `pointer`, or give it the
|
|
38
|
+
* role it plays. No marker is invented here to cover the gap: a marker
|
|
39
|
+
* meaning "this happens to contain a DN" would be a third way of saying what
|
|
40
|
+
* `pointer` and the roles already say.
|
|
41
|
+
* @group Plugins
|
|
42
|
+
*/
|
|
43
|
+
import type { Request } from 'express';
|
|
44
|
+
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
45
|
+
import type { DM } from '../../bin';
|
|
46
|
+
import type { Hooks } from '../../hooks';
|
|
47
|
+
import type { AttributeValue } from '../../lib/ldapActions';
|
|
48
|
+
import type { Schema, SchemaAttribute } from '../../config/schema';
|
|
49
|
+
/**
|
|
50
|
+
* One place a DN is stored: which entry, in which attribute, and what that
|
|
51
|
+
* attribute currently holds there.
|
|
52
|
+
*/
|
|
53
|
+
export interface DnReference {
|
|
54
|
+
/** Entry holding the reference */
|
|
55
|
+
dn: string;
|
|
56
|
+
/** Attribute it sits in, in the schema's spelling */
|
|
57
|
+
attribute: string;
|
|
58
|
+
/**
|
|
59
|
+
* Every value the attribute holds in that entry, as the directory spells
|
|
60
|
+
* them. Kept whole rather than reduced to the matching one: a rewrite
|
|
61
|
+
* deletes the spelling the directory actually stores, not the one the
|
|
62
|
+
* caller typed, and it has to know whether the new DN is already there.
|
|
63
|
+
*/
|
|
64
|
+
values: string[];
|
|
65
|
+
/** True when the schema declares a list rather than a single value */
|
|
66
|
+
multiValued: boolean;
|
|
67
|
+
}
|
|
68
|
+
/** What a reference rewrite did, and what it could not do. */
|
|
69
|
+
export interface ReferenceRewriteReport {
|
|
70
|
+
/** References rewritten */
|
|
71
|
+
updated: number;
|
|
72
|
+
/**
|
|
73
|
+
* References left naming the old DN, counted per attribute. The referring
|
|
74
|
+
* DNs are deliberately absent: the caller owns the entry it renamed, not
|
|
75
|
+
* the entries pointing at it — the same care `checkUnique` and
|
|
76
|
+
* `guardReferences` take. They are in the log.
|
|
77
|
+
*/
|
|
78
|
+
failed: {
|
|
79
|
+
attribute: string;
|
|
80
|
+
count: number;
|
|
81
|
+
}[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read a human-readable size and return a number of bytes.
|
|
85
|
+
*
|
|
86
|
+
* `5GB`, `500 MB`, `2048` and `2048B` are all accepted; the multipliers are
|
|
87
|
+
* decimal (1 GB = 10⁹ bytes), which is what mail servers report.
|
|
88
|
+
*
|
|
89
|
+
* @param raw value as submitted
|
|
90
|
+
* @returns the size in bytes
|
|
91
|
+
* @throws BadRequestError when the value is not a size
|
|
92
|
+
*/
|
|
93
|
+
export declare function parseByteSize(raw: AttributeValue): number;
|
|
94
|
+
/**
|
|
95
|
+
* Tell whether the enterprise rules fill a generated attribute: a default the
|
|
96
|
+
* server owns, applied on creation, or the organization path, derived from the
|
|
97
|
+
* link or from the tree.
|
|
98
|
+
*
|
|
99
|
+
* A function of the schema alone, so that `audit:directory` asks the same
|
|
100
|
+
* question the server asks without loading the plugin.
|
|
101
|
+
*
|
|
102
|
+
* @param name attribute name
|
|
103
|
+
* @param attr its schema definition
|
|
104
|
+
* @param schema schema it belongs to
|
|
105
|
+
* @returns true when the rules supply a value
|
|
106
|
+
*/
|
|
107
|
+
export declare function fillsGeneratedAttribute(name: string, attr: SchemaAttribute, schema: Schema): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Parse a directory date: an LDAP generalized time or anything `Date`
|
|
110
|
+
* understands.
|
|
111
|
+
*
|
|
112
|
+
* A generalized time (RFC 4517 section 3.3.13) is `yyyyMMddHH`, then optional
|
|
113
|
+
* minutes and seconds, an optional fraction of the last unit given, and a zone:
|
|
114
|
+
* `Z` or an offset of hours with optional minutes (`+02`, `-0530`). `Date` reads
|
|
115
|
+
* none of the shorter forms, and not an offset of bare hours, so they are
|
|
116
|
+
* computed here rather than rewritten into an ISO string it might refuse.
|
|
117
|
+
*
|
|
118
|
+
* @param raw value as stored or submitted
|
|
119
|
+
* @returns the date, or null when it cannot be read
|
|
120
|
+
*/
|
|
121
|
+
export declare function parseDirectoryDate(raw: AttributeValue): Date | null;
|
|
122
|
+
/**
|
|
123
|
+
* Start of a day, in UTC.
|
|
124
|
+
*
|
|
125
|
+
* A directory stores generalized times in UTC, and a console that lets someone
|
|
126
|
+
* pick a day stores it as `yyyyMMdd000000Z`. Comparing that against the
|
|
127
|
+
* server's *local* midnight refused a date picked today on every server west
|
|
128
|
+
* of UTC, where local midnight comes hours after midnight UTC: the deployment's
|
|
129
|
+
* timezone decided whether the form worked. Both sides are in UTC now.
|
|
130
|
+
*
|
|
131
|
+
* @param now instant to take the day from, the current one by default
|
|
132
|
+
* @returns midnight UTC of that day, in milliseconds
|
|
133
|
+
*/
|
|
134
|
+
export declare function startOfUtcDay(now?: Date): number;
|
|
135
|
+
/**
|
|
136
|
+
* Tell whether a mail address belongs to one of a set of domains.
|
|
137
|
+
*
|
|
138
|
+
* The host is taken after the *last* `@`, so the comparison cannot be fooled
|
|
139
|
+
* by a prefix — the address format itself is the schema's business. A single
|
|
140
|
+
* `*` among the domains authorises every domain.
|
|
141
|
+
*
|
|
142
|
+
* @param mail address to test
|
|
143
|
+
* @param domains authorised domains; an empty list authorises everything
|
|
144
|
+
* @param allowSubdomains accept `list@lists.example.org` under `example.org`
|
|
145
|
+
* @returns true when the address is authorised
|
|
146
|
+
*/
|
|
147
|
+
export declare function mailInDomains(mail: string, domains: string[], allowSubdomains?: boolean): boolean;
|
|
148
|
+
export default class LdapEnterpriseRules extends DmPlugin {
|
|
149
|
+
name: string;
|
|
150
|
+
roles: Role[];
|
|
151
|
+
/** Placeholder member some directories keep to satisfy `groupOfNames` */
|
|
152
|
+
private dummyMember?;
|
|
153
|
+
/** Root of the organization tree */
|
|
154
|
+
private topOrganization?;
|
|
155
|
+
constructor(server: DM);
|
|
156
|
+
/**
|
|
157
|
+
* Tell whether this plugin fills a given generated attribute.
|
|
158
|
+
*
|
|
159
|
+
* `ldapFlat` exempts a required attribute from its check when the schema
|
|
160
|
+
* marks it generated, which is only sound if something will honour the
|
|
161
|
+
* bargain. It asks every loaded plugin, so the answer has to be about this
|
|
162
|
+
* attribute, not about a role a dozen plugins happen to carry.
|
|
163
|
+
*
|
|
164
|
+
* @param name attribute the entity is about to demand
|
|
165
|
+
* @param attr its schema definition
|
|
166
|
+
* @param schema schema it belongs to
|
|
167
|
+
* @returns true when a hook here will supply a value
|
|
168
|
+
*/
|
|
169
|
+
fillsGeneratedAttribute(name: string, attr: SchemaAttribute, schema: Schema): boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Entities the rules apply to, rebuilt on each call.
|
|
172
|
+
*
|
|
173
|
+
* The organization and group plugins read their schema asynchronously, so a
|
|
174
|
+
* snapshot taken in the constructor would miss them; the list is cheap to
|
|
175
|
+
* rebuild since it only collects references.
|
|
176
|
+
*
|
|
177
|
+
* @returns one binding per entity whose schema is loaded
|
|
178
|
+
*/
|
|
179
|
+
private bindings;
|
|
180
|
+
/**
|
|
181
|
+
* Find the entity a DN belongs to: the binding with the longest matching
|
|
182
|
+
* base, so a group branch nested inside the user branch still wins over it.
|
|
183
|
+
*
|
|
184
|
+
* @param dn entry being written
|
|
185
|
+
* @returns the binding, or undefined when the DN belongs to no known entity
|
|
186
|
+
*/
|
|
187
|
+
private resolveEntity;
|
|
188
|
+
hooks: Hooks;
|
|
189
|
+
/**
|
|
190
|
+
* Validate a payload against the schema markers that need the directory:
|
|
191
|
+
* uniqueness, mail domains and lifecycle dates.
|
|
192
|
+
*
|
|
193
|
+
* @param entity entity the entry belongs to
|
|
194
|
+
* @param dn entry being written; excluded from uniqueness searches
|
|
195
|
+
* @param values attributes as submitted
|
|
196
|
+
* @param current entry as currently stored, on a modification
|
|
197
|
+
*/
|
|
198
|
+
private runChecks;
|
|
199
|
+
/**
|
|
200
|
+
* Apply the value rewrites the server owns: normalisation, defaults for
|
|
201
|
+
* attributes it fills itself, and the organization path derived from the
|
|
202
|
+
* organization link.
|
|
203
|
+
*
|
|
204
|
+
* @param entity entity the entry belongs to
|
|
205
|
+
* @param values attributes as submitted; mutated in place
|
|
206
|
+
* @param creating true on a creation, where defaults apply
|
|
207
|
+
*/
|
|
208
|
+
private runTransformations;
|
|
209
|
+
/**
|
|
210
|
+
* Refuse a value already used elsewhere.
|
|
211
|
+
*
|
|
212
|
+
* Two things the rule this replaces got wrong are fixed here: values are
|
|
213
|
+
* compared by content, and the entry being updated is excluded from the
|
|
214
|
+
* search — so re-sending an unchanged name no longer conflicts with itself.
|
|
215
|
+
*
|
|
216
|
+
* @param entity entity the entry belongs to
|
|
217
|
+
* @param name attribute being checked
|
|
218
|
+
* @param attr its schema definition
|
|
219
|
+
* @param values values submitted for it
|
|
220
|
+
* @param selfDn DN to exclude, on a modification
|
|
221
|
+
* @throws ConflictError naming the entry that already holds the value
|
|
222
|
+
*/
|
|
223
|
+
private checkUnique;
|
|
224
|
+
/**
|
|
225
|
+
* Refuse a mail address outside the domains its organization may use.
|
|
226
|
+
*
|
|
227
|
+
* @param entity entity the entry belongs to
|
|
228
|
+
* @param attr schema definition of the mail attribute
|
|
229
|
+
* @param values addresses submitted
|
|
230
|
+
* @param submitted attributes as submitted, read for the organization link
|
|
231
|
+
* @param current entry as stored, when the link is not being changed
|
|
232
|
+
* @throws BadRequestError when no organization can be resolved
|
|
233
|
+
* @throws ConflictError when the address is outside the authorised domains
|
|
234
|
+
*/
|
|
235
|
+
private checkMailDomains;
|
|
236
|
+
/**
|
|
237
|
+
* Domains an entry may use, walking up from its organization to the root of
|
|
238
|
+
* the tree and collecting what each level declares. An organization that
|
|
239
|
+
* declares none inherits whatever its ancestors allow; when nobody declares
|
|
240
|
+
* anything, no restriction applies.
|
|
241
|
+
*
|
|
242
|
+
* @param entity entity the entry belongs to
|
|
243
|
+
* @param submitted attributes as submitted
|
|
244
|
+
* @param current entry as stored, when the link is not being changed
|
|
245
|
+
* @returns the authorised domain names
|
|
246
|
+
*/
|
|
247
|
+
private organizationDomains;
|
|
248
|
+
/**
|
|
249
|
+
* Attribute holding the mail domain of a domain entry.
|
|
250
|
+
*
|
|
251
|
+
* Role first, as its sibling `domainLink` is: a deployment naming the
|
|
252
|
+
* attribute otherwise and declaring the role is exactly what the marker is
|
|
253
|
+
* for, and reading only the option made the rule search an attribute its
|
|
254
|
+
* entries do not have — and accept every address, silently.
|
|
255
|
+
*
|
|
256
|
+
* @returns the attribute name
|
|
257
|
+
*/
|
|
258
|
+
private domainNameAttribute;
|
|
259
|
+
/** Every mail domain declared anywhere in the directory. */
|
|
260
|
+
private directoryDomains;
|
|
261
|
+
/**
|
|
262
|
+
* Refuse a lifecycle date that has already passed. The comparison is against
|
|
263
|
+
* the start of the current day *in UTC*, the timezone the values themselves
|
|
264
|
+
* are written in, so scheduling something for today is allowed — which is
|
|
265
|
+
* what "not earlier than today" means to the person filling the form,
|
|
266
|
+
* wherever the server happens to stand.
|
|
267
|
+
*
|
|
268
|
+
* @param name attribute being checked
|
|
269
|
+
* @param attr its schema definition
|
|
270
|
+
* @param values dates submitted
|
|
271
|
+
* @throws BadRequestError when a date is unreadable or in the past
|
|
272
|
+
*/
|
|
273
|
+
private checkNotInThePast;
|
|
274
|
+
/**
|
|
275
|
+
* Read the human-readable path of an organization.
|
|
276
|
+
*
|
|
277
|
+
* @param orgDn DN of the organization
|
|
278
|
+
* @returns the path, or undefined when the organization carries none
|
|
279
|
+
*/
|
|
280
|
+
private organizationPath;
|
|
281
|
+
/**
|
|
282
|
+
* Refuse a deletion that would leave the directory inconsistent: an entry
|
|
283
|
+
* other entries still point at, or a group that still has members.
|
|
284
|
+
*
|
|
285
|
+
* @param dn entry about to be deleted
|
|
286
|
+
* @throws ConflictError naming what still depends on the entry
|
|
287
|
+
*/
|
|
288
|
+
private guardDelete;
|
|
289
|
+
/**
|
|
290
|
+
* Refuse to delete an entry that a `referentialIntegrity: restrict` pointer
|
|
291
|
+
* still names. The pointers are read from the schemas themselves, so a new
|
|
292
|
+
* reference is protected by declaring it, not by editing this file.
|
|
293
|
+
*
|
|
294
|
+
* @param dn entry about to be deleted
|
|
295
|
+
* @throws ConflictError naming the first referencing entry
|
|
296
|
+
*/
|
|
297
|
+
private guardReferences;
|
|
298
|
+
/**
|
|
299
|
+
* Every attribute of every loaded entity that could hold this DN.
|
|
300
|
+
*
|
|
301
|
+
* This is the traversal the delete guard and the rename cascade share: it
|
|
302
|
+
* knows no attribute name, only what the schemas declare — a `pointer`
|
|
303
|
+
* (single or under `items`) whose `branch` admits the DN's parent, or an
|
|
304
|
+
* attribute carrying the `members` or `owners` role. The module header says
|
|
305
|
+
* why both are needed and what neither covers.
|
|
306
|
+
*
|
|
307
|
+
* The branch narrowing is what keeps the searches few: only the attributes
|
|
308
|
+
* that could legally name an entry of this branch are looked into.
|
|
309
|
+
*
|
|
310
|
+
* @param dn entry being deleted or renamed
|
|
311
|
+
* @returns one candidate per (entity, attribute) pair
|
|
312
|
+
*/
|
|
313
|
+
private referringAttributes;
|
|
314
|
+
/**
|
|
315
|
+
* Entries whose attribute holds a given DN.
|
|
316
|
+
*
|
|
317
|
+
* Run without a request, like every other lookup here: these rules answer
|
|
318
|
+
* about the directory as a whole. A rename that only rewrote the references
|
|
319
|
+
* its caller happens to be able to read would leave the rest silently
|
|
320
|
+
* wrong; the writes are authorized one by one instead.
|
|
321
|
+
*
|
|
322
|
+
* @param candidate entity and attribute to look into
|
|
323
|
+
* @param dn DN to look for
|
|
324
|
+
* @returns the matching entries, carrying that attribute
|
|
325
|
+
*/
|
|
326
|
+
private searchReferrers;
|
|
327
|
+
/**
|
|
328
|
+
* Every reference the directory holds to a DN.
|
|
329
|
+
*
|
|
330
|
+
* A reference is a stored value, so this answers about a DN whether or not
|
|
331
|
+
* an entry still carries it — which is what lets the rename ask again
|
|
332
|
+
* afterwards, and what lets a repair run find what an interrupted one left
|
|
333
|
+
* behind.
|
|
334
|
+
*
|
|
335
|
+
* @param dn DN to look for
|
|
336
|
+
* @returns one entry per (referring entry, attribute) pair
|
|
337
|
+
*/
|
|
338
|
+
findReferences(dn: string): Promise<DnReference[]>;
|
|
339
|
+
/**
|
|
340
|
+
* Identity of a reference, for comparing the two passes below.
|
|
341
|
+
*
|
|
342
|
+
* @param reference reference to name
|
|
343
|
+
* @returns a key equal for the same attribute of the same entry
|
|
344
|
+
*/
|
|
345
|
+
private static referenceKey;
|
|
346
|
+
/**
|
|
347
|
+
* Point every reference at the new DN.
|
|
348
|
+
*
|
|
349
|
+
* Each rewrite is attempted on its own and its failure collected rather
|
|
350
|
+
* than thrown: the rename itself has already been committed by the time
|
|
351
|
+
* this runs, so refusing the whole thing would report a directory state
|
|
352
|
+
* that does not exist. The caller answers 207 with this report.
|
|
353
|
+
*
|
|
354
|
+
* The write is a plain modify, never the group plugin's member API: that
|
|
355
|
+
* one fires member validation, which creates missing external members —
|
|
356
|
+
* work that has no business running while an existing DN is being respelled.
|
|
357
|
+
*
|
|
358
|
+
* **The directory may have done part of the work already.** OpenLDAP's
|
|
359
|
+
* `refint` overlay rewrites the attributes it is configured for — `member`,
|
|
360
|
+
* `owner`, `uniqueMember`, `manager`, `memberOf` in the stock configuration
|
|
361
|
+
* — the moment a `modifyDN` lands, and knows nothing of the rest. Which
|
|
362
|
+
* attributes, and whether the overlay is there at all, is a property of the
|
|
363
|
+
* deployment. So the references are looked for a second time, *after* the
|
|
364
|
+
* rename: what is still stale is what this has to write, and what is no
|
|
365
|
+
* longer there was written by the directory itself and counts as done.
|
|
366
|
+
* Re-adding a value the overlay had already added is refused with
|
|
367
|
+
* `attributeOrValueExists`, which would have been reported as a 207 on a
|
|
368
|
+
* directory that was in fact perfectly consistent.
|
|
369
|
+
*
|
|
370
|
+
* @param references what {@link findReferences} found, before the rename
|
|
371
|
+
* @param oldDn DN as it was
|
|
372
|
+
* @param newDn DN as it now is
|
|
373
|
+
* @param req request the rename came from, so each write is authorized
|
|
374
|
+
* @returns how many references now name the new DN, and what was left behind
|
|
375
|
+
*/
|
|
376
|
+
rewriteReferences(references: DnReference[], oldDn: string, newDn: string, req?: Request): Promise<ReferenceRewriteReport>;
|
|
377
|
+
/**
|
|
378
|
+
* Respell one reference, converging when the directory is respelling it at
|
|
379
|
+
* the same moment.
|
|
380
|
+
*
|
|
381
|
+
* `refint` does its fixups in a task of its own, *after* the `modifyDN` has
|
|
382
|
+
* answered, so a rewrite of one of its attributes is a race nobody can win
|
|
383
|
+
* by looking first: the overlay may land between the search and the modify.
|
|
384
|
+
* Both outcomes of that race say the same thing —
|
|
385
|
+
* `attributeOrValueExists` that the new DN is already in, `noSuchAttribute`
|
|
386
|
+
* that the old one is already out — so the entry is read again and the
|
|
387
|
+
* remaining half, if any, is applied. Anything else is a real failure.
|
|
388
|
+
*
|
|
389
|
+
* @param ldap directory to write through, bound to the request
|
|
390
|
+
* @param reference reference to rewrite
|
|
391
|
+
* @param stale canonical form of the old DN
|
|
392
|
+
* @param target canonical form of the new DN
|
|
393
|
+
* @param newDn new DN, as it is to be written
|
|
394
|
+
* @throws whatever the directory answered, when it is not a race
|
|
395
|
+
*/
|
|
396
|
+
private applyRewrite;
|
|
397
|
+
/**
|
|
398
|
+
* Read one attribute of one entry, past the cache.
|
|
399
|
+
*
|
|
400
|
+
* The cached copy may predate a write nobody here made — `refint` fixing
|
|
401
|
+
* its own attributes — and the cache is only invalidated by the writes
|
|
402
|
+
* this process performs.
|
|
403
|
+
*
|
|
404
|
+
* @param dn entry to read
|
|
405
|
+
* @param name attribute to read
|
|
406
|
+
* @returns its values, as the directory spells them
|
|
407
|
+
*/
|
|
408
|
+
private freshValues;
|
|
409
|
+
/**
|
|
410
|
+
* The modification that respells one reference.
|
|
411
|
+
*
|
|
412
|
+
* A list has the stale values deleted and the new one added, so the other
|
|
413
|
+
* members of a group are left alone — and nothing is added when the new DN
|
|
414
|
+
* is already there, which is what a repair run finds. A single-valued
|
|
415
|
+
* attribute is replaced instead: `ldapActions` emits an `add` before a
|
|
416
|
+
* `delete`, which a single-valued attribute refuses, and the replacement is
|
|
417
|
+
* built from the values read so that a directory holding more than the
|
|
418
|
+
* schema says loses none of them.
|
|
419
|
+
*
|
|
420
|
+
* @param reference reference to rewrite
|
|
421
|
+
* @param stale canonical form of the old DN
|
|
422
|
+
* @param target canonical form of the new DN
|
|
423
|
+
* @param newDn new DN, as it is to be written
|
|
424
|
+
* @returns the changes, or undefined when there is nothing to do
|
|
425
|
+
*/
|
|
426
|
+
private rewrite;
|
|
427
|
+
/**
|
|
428
|
+
* Read a single entry, without a request so the lookup is not narrowed by
|
|
429
|
+
* the caller's own permissions: these rules answer about the directory as a
|
|
430
|
+
* whole, not about what one manager may see.
|
|
431
|
+
*
|
|
432
|
+
* @param dn entry to read
|
|
433
|
+
* @returns the entry, or null when it does not exist
|
|
434
|
+
*/
|
|
435
|
+
private readEntry;
|
|
436
|
+
}
|
|
@@ -2,12 +2,41 @@ import type { Express } from 'express';
|
|
|
2
2
|
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
3
3
|
import LdapFlat from '../../abstract/ldapFlat';
|
|
4
4
|
import type { DM } from '../../bin';
|
|
5
|
+
import type { LocalizedText } from '../../config/schema';
|
|
6
|
+
/**
|
|
7
|
+
* Collections the other LDAP plugins serve under the same `/v1/ldap/` prefix,
|
|
8
|
+
* with the plugin that serves each: its registry name, and the module paths a
|
|
9
|
+
* configuration loads it by.
|
|
10
|
+
*
|
|
11
|
+
* They are listed rather than read from the plugins because those write their
|
|
12
|
+
* routes literally — `scripts/generate-openapi.ts` reads the route text, and a
|
|
13
|
+
* `${this.pluralName}` there would document `/v1/ldap/{resource}` instead of
|
|
14
|
+
* the real path. `test/plugins/ldap/flatGenericPluginClash.test.ts` checks
|
|
15
|
+
* this list against the routes those plugins really register, so it cannot
|
|
16
|
+
* drift away from them in silence.
|
|
17
|
+
*/
|
|
18
|
+
export declare const ldapPluginCollections: {
|
|
19
|
+
plugin: string;
|
|
20
|
+
modules: string[];
|
|
21
|
+
collection: string;
|
|
22
|
+
}[];
|
|
5
23
|
/**
|
|
6
24
|
* Concrete implementation of LdapFlat for generic instances
|
|
7
25
|
*/
|
|
8
26
|
declare class LdapFlatInstance extends LdapFlat {
|
|
9
27
|
name: string;
|
|
10
28
|
roles: Role[];
|
|
29
|
+
/** Names a client shows for the collection and for one entry */
|
|
30
|
+
label?: LocalizedText;
|
|
31
|
+
singularLabel?: LocalizedText;
|
|
32
|
+
/**
|
|
33
|
+
* What a client shows for an entry, keyed by its main attribute value. Also
|
|
34
|
+
* reachable nested in `schema.entity.valueLabels` (the whole schema file is
|
|
35
|
+
* exposed as `schema`); mirrored here, like `label`, so a client finds it
|
|
36
|
+
* at the top level of a `flatResources` entry rather than three levels
|
|
37
|
+
* down under a property `Schema` never declared.
|
|
38
|
+
*/
|
|
39
|
+
valueLabels?: Record<string, LocalizedText>;
|
|
11
40
|
dependencies: {
|
|
12
41
|
departmentSync: string;
|
|
13
42
|
};
|
|
@@ -94,6 +94,28 @@ export default class LdapGroups extends DmPlugin {
|
|
|
94
94
|
* API routes
|
|
95
95
|
*/
|
|
96
96
|
api(app: Express): void;
|
|
97
|
+
/**
|
|
98
|
+
* Whether a member is the placeholder `groupOfNames` needs to stay valid
|
|
99
|
+
* with no real member.
|
|
100
|
+
*
|
|
101
|
+
* Compared as DNs, not as text: the directory answers with its own spelling
|
|
102
|
+
* of a DN, and `uid=fakeUser,ou=users,…` configured must match the
|
|
103
|
+
* `uid=fakeuser, ou=users,…` it may hold.
|
|
104
|
+
*
|
|
105
|
+
* @param member DN of a member
|
|
106
|
+
* @returns true for the placeholder
|
|
107
|
+
*/
|
|
108
|
+
isDummyMember(member: string): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* An entry with the placeholder taken out of its members. It is there for
|
|
111
|
+
* the directory's schema, not for anyone reading the group: listed, it
|
|
112
|
+
* reads as a person, and a client editing the list would offer to remove
|
|
113
|
+
* the one member the server refuses to remove.
|
|
114
|
+
*
|
|
115
|
+
* @param entry group as read
|
|
116
|
+
* @returns the same entry, members filtered
|
|
117
|
+
*/
|
|
118
|
+
withoutDummyMember<T extends Record<string, unknown>>(entry: T): T;
|
|
97
119
|
apiGet(req: Request, res: Response): Promise<void>;
|
|
98
120
|
apiAdd(req: Request, res: Response, ...requiredFields: string[]): Promise<void>;
|
|
99
121
|
apiDelete(req: Request, res: Response): Promise<void>;
|
|
@@ -121,9 +121,14 @@ export default class LdapOrganizations extends DmPlugin {
|
|
|
121
121
|
checkDeptLink(entry: AttributesList): Promise<void>;
|
|
122
122
|
/**
|
|
123
123
|
* Check if the department path is valid
|
|
124
|
+
*
|
|
124
125
|
* @param entry LDAP entry to check
|
|
126
|
+
* @param dn DN the entry is written at, when the caller knows it. It is
|
|
127
|
+
* what says whether the entry is an organization and where it hangs
|
|
128
|
+
* from; a modify carries the changed attributes alone, neither `ou` nor
|
|
129
|
+
* `objectClass`, so without it the invariants below cannot be checked.
|
|
125
130
|
*/
|
|
126
|
-
checkDeptPath(entry: AttributesList): Promise<void>;
|
|
131
|
+
checkDeptPath(entry: AttributesList, dn?: string): Promise<void>;
|
|
127
132
|
isEmptyOrganization(dn: string): Promise<void>;
|
|
128
133
|
/**
|
|
129
134
|
* Check if entry is an organisation
|
|
@@ -136,9 +141,25 @@ export default class LdapOrganizations extends DmPlugin {
|
|
|
136
141
|
getOrganisationSubnodes(dn: string, req?: Request): Promise<AttributesList[]>;
|
|
137
142
|
addOrganization(dn: string, entry: AttributesList, req?: Request): Promise<boolean>;
|
|
138
143
|
modifyOrganization(dn: string, changes: ModifyRequest): Promise<boolean>;
|
|
139
|
-
validateNewOrganization(dn: string, entry: AttributesList): boolean
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
validateNewOrganization(dn: string, entry: AttributesList): Promise<boolean>;
|
|
145
|
+
/**
|
|
146
|
+
* Build the rejection message for a value that failed its schema `test`,
|
|
147
|
+
* quoting the `hint` when the schema carries one so the answer says what a
|
|
148
|
+
* valid value looks like.
|
|
149
|
+
*
|
|
150
|
+
* @param field attribute name
|
|
151
|
+
* @returns message for a `BadRequestError`
|
|
152
|
+
*/
|
|
153
|
+
invalidValueMessage(field: string): string;
|
|
154
|
+
validateChanges(dn: string, changes: ModifyRequest): Promise<boolean>;
|
|
155
|
+
_validateOneChange(field: string, value: AttributeValue | null): Promise<boolean>;
|
|
156
|
+
/**
|
|
157
|
+
* Tell whether a DN names an existing entry.
|
|
158
|
+
*
|
|
159
|
+
* @param dn DN to look up
|
|
160
|
+
* @returns true when the directory holds it
|
|
161
|
+
*/
|
|
162
|
+
private entryExists;
|
|
142
163
|
/**
|
|
143
164
|
* Move an organization to a different parent organization
|
|
144
165
|
* Uses LDAP modifyDN to change the DN hierarchy
|