ldap-rest 0.2.2 → 0.3.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 +1 -1
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/plugins/auth/authzDynamic.js +1 -0
- package/dist/plugins/auth/authzDynamicHash.js +1 -0
- package/dist/plugins/ldap/organization.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -0
- package/dist/plugins/scim/baseResolver.js +1 -0
- package/dist/plugins/scim/bulk.js +1 -0
- package/dist/plugins/scim/discovery.js +1 -0
- package/dist/plugins/scim/errors.js +1 -0
- package/dist/plugins/scim/filter.js +1 -0
- package/dist/plugins/scim/groups.js +1 -0
- package/dist/plugins/scim/mapping.js +1 -0
- package/dist/plugins/scim/patch.js +1 -0
- package/dist/plugins/scim/scim.js +1 -0
- package/dist/plugins/scim/types.js +1 -0
- package/dist/plugins/scim/users.js +1 -0
- package/dist/src/abstract/ldapFlat.d.ts +127 -0
- package/dist/src/config/args.d.ts +24 -0
- package/dist/src/plugins/auth/authzDynamic.d.ts +117 -0
- package/dist/src/plugins/auth/authzDynamicHash.d.ts +27 -0
- package/dist/src/plugins/configApi.d.ts +51 -0
- package/dist/src/plugins/demo/helloworld.d.ts +21 -0
- package/dist/src/plugins/ldap/bulkImport.d.ts +65 -0
- package/dist/src/plugins/ldap/groups.d.ts +68 -0
- package/dist/src/plugins/ldap/organization.d.ts +8 -65
- package/dist/src/plugins/ldap/organizations.d.ts +155 -0
- package/dist/src/plugins/ldap/passwordPolicy.d.ts +178 -0
- package/dist/src/plugins/scim/baseResolver.d.ts +23 -0
- package/dist/src/plugins/scim/bulk.d.ts +43 -0
- package/dist/src/plugins/scim/discovery.d.ts +27 -0
- package/dist/src/plugins/scim/errors.d.ts +37 -0
- package/dist/src/plugins/scim/filter.d.ts +13 -0
- package/dist/src/plugins/scim/groups.d.ts +61 -0
- package/dist/src/plugins/scim/mapping.d.ts +52 -0
- package/dist/src/plugins/scim/patch.d.ts +39 -0
- package/dist/src/plugins/scim/scim.d.ts +412 -0
- package/dist/src/plugins/scim/types.d.ts +222 -0
- package/dist/src/plugins/scim/users.d.ts +58 -0
- package/dist/src/plugins/static.d.ts +33 -0
- package/dist/src/plugins/twake/appAccountsApi.d.ts +63 -0
- package/dist/src/plugins/twake/james.d.ts +47 -0
- package/docs/plugin-development/README.md +2 -1
- package/docs/plugin-development/openapi.md +144 -0
- package/docs/usage/plugins/auth/authz-dynamic.md +183 -0
- package/docs/usage/plugins/integrations/scim.md +567 -0
- package/package.json +72 -1
- package/static/schemas/scim/Group.json +31 -0
- package/static/schemas/scim/User.json +147 -0
- package/static/schemas/scim/default-mapping.json +48 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/auth/authzDynamic
|
|
3
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
+
*
|
|
5
|
+
* LDAP-backed authentication + per-branch authorization plugin.
|
|
6
|
+
*
|
|
7
|
+
* Tokens are stored as entries under a dedicated LDAP branch (one entry per
|
|
8
|
+
* bearer token). The plugin caches them and enforces read / write / delete
|
|
9
|
+
* permissions on subsequent LDAP operations — so each client is scoped to
|
|
10
|
+
* its own subtree without touching the process configuration.
|
|
11
|
+
*
|
|
12
|
+
* Entry shape (conventions, overridable via CLI):
|
|
13
|
+
* dn: cn=<token-name>,<authz-dynamic-base>
|
|
14
|
+
* cn: <token-name>
|
|
15
|
+
* userPassword: {SSHA}… ← secret, hashed
|
|
16
|
+
* description: {"tenant":"acme", ← JSON config
|
|
17
|
+
* "bases":[
|
|
18
|
+
* {"dn":"ou=users,ou=acme,dc=example,dc=com",
|
|
19
|
+
* "read":true,"write":true,"delete":true}
|
|
20
|
+
* ]}
|
|
21
|
+
*
|
|
22
|
+
* The entry is treated as "token for tenant `acme`, permitted to read/write/
|
|
23
|
+
* delete under `ou=users,ou=acme,…`". Multiple bases per token are allowed.
|
|
24
|
+
* Sub-branch matching is used (a permission on `ou=acme` covers
|
|
25
|
+
* `ou=users,ou=acme`).
|
|
26
|
+
*/
|
|
27
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
28
|
+
import type { Express, Response } from 'express';
|
|
29
|
+
import { type Role } from '../../abstract/plugin';
|
|
30
|
+
import type { DM } from '../../bin';
|
|
31
|
+
import AuthBase, { type DmRequest } from '../../lib/auth/base';
|
|
32
|
+
import type { Hooks } from '../../hooks';
|
|
33
|
+
export interface BranchAcl {
|
|
34
|
+
dn: string;
|
|
35
|
+
read?: boolean;
|
|
36
|
+
write?: boolean;
|
|
37
|
+
delete?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface TokenConfig {
|
|
40
|
+
tenant?: string;
|
|
41
|
+
bases?: BranchAcl[];
|
|
42
|
+
}
|
|
43
|
+
export interface TokenEntry {
|
|
44
|
+
dn: string;
|
|
45
|
+
cn: string;
|
|
46
|
+
tenant: string;
|
|
47
|
+
hash: string;
|
|
48
|
+
bases: BranchAcl[];
|
|
49
|
+
}
|
|
50
|
+
/** Extends DmRequest with the resolved token entry for downstream hooks. */
|
|
51
|
+
export interface AuthzDynamicRequest extends DmRequest {
|
|
52
|
+
authzToken?: TokenEntry;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Per-request async context holding the authenticated token.
|
|
56
|
+
*
|
|
57
|
+
* Core plugins (ldapGroups, ldapFlat, etc.) do not always forward the Express
|
|
58
|
+
* `req` object down to `ldapActions.search()` / `.modify()`. Relying solely
|
|
59
|
+
* on `req` would therefore let un-tracked operations bypass authorization.
|
|
60
|
+
* Using AsyncLocalStorage gives us a request-scoped, ambient handle that the
|
|
61
|
+
* authz hooks can read regardless of whether the caller threaded `req`.
|
|
62
|
+
*/
|
|
63
|
+
export declare const authzContext: AsyncLocalStorage<TokenEntry>;
|
|
64
|
+
export default class AuthzDynamic extends AuthBase {
|
|
65
|
+
name: string;
|
|
66
|
+
roles: Role[];
|
|
67
|
+
private tokens;
|
|
68
|
+
private lastLoad;
|
|
69
|
+
private lastFailure;
|
|
70
|
+
private readonly cacheTtlMs;
|
|
71
|
+
private readonly failureBackoffMs;
|
|
72
|
+
private readonly base;
|
|
73
|
+
private readonly tokenAttr;
|
|
74
|
+
private readonly configAttr;
|
|
75
|
+
private readonly tenantAttr;
|
|
76
|
+
private readonly reloadEndpoint;
|
|
77
|
+
private loading?;
|
|
78
|
+
constructor(server: DM);
|
|
79
|
+
api(app: Express): void;
|
|
80
|
+
/**
|
|
81
|
+
* Load token entries from LDAP. Swallows errors so a transient LDAP hiccup
|
|
82
|
+
* does not poison the cache — the previous snapshot stays in use.
|
|
83
|
+
*/
|
|
84
|
+
reload(): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Ensure the cache is fresh (blocks if reloading).
|
|
87
|
+
*
|
|
88
|
+
* On a bootstrap or successful reload the cache is valid for
|
|
89
|
+
* `cacheTtlMs`. If the load fails, we still refuse to retry on every
|
|
90
|
+
* request — `failureBackoffMs` (default 1/4 of TTL, min 5 s) keeps the
|
|
91
|
+
* previous snapshot in use and shields LDAP from a thundering-herd
|
|
92
|
+
* during an outage.
|
|
93
|
+
*/
|
|
94
|
+
private ensureFresh;
|
|
95
|
+
authMethod(req: DmRequest, res: Response, next: () => void): void;
|
|
96
|
+
/**
|
|
97
|
+
* Authorization hooks. These run after the auth middleware has populated
|
|
98
|
+
* `req.authzToken`, so we can read the allowed bases directly from the
|
|
99
|
+
* request.
|
|
100
|
+
*/
|
|
101
|
+
hooks: Hooks;
|
|
102
|
+
/** Read the active token from the async context, or (as a fallback) from `req`. */
|
|
103
|
+
private activeToken;
|
|
104
|
+
/**
|
|
105
|
+
* Throw a ForbiddenError (403) if the token cannot perform `permission` on
|
|
106
|
+
* the branch containing `branchOrDn`. Any ACL entry that is `branchOrDn`
|
|
107
|
+
* itself or a parent of it (sub-branch match) qualifies.
|
|
108
|
+
*
|
|
109
|
+
* The client-facing message omits the target DN to avoid leaking tenant
|
|
110
|
+
* topology through authorization failures; the full context is still
|
|
111
|
+
* logged server-side at debug level for operators.
|
|
112
|
+
*/
|
|
113
|
+
private requireBranchPermission;
|
|
114
|
+
/** Expose current cache (primarily for the configApi auto-discovery). */
|
|
115
|
+
getConfigApiData(): Record<string, unknown>;
|
|
116
|
+
_tokens(): TokenEntry[];
|
|
117
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify a provided bearer token against a stored LDAP-style hash.
|
|
3
|
+
*
|
|
4
|
+
* The stored hash is typically the contents of `userPassword`:
|
|
5
|
+
* either `{SCHEME}<data>` or cleartext.
|
|
6
|
+
*/
|
|
7
|
+
export declare function verifyLdapPassword(provided: string, stored: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Convenience helpers for tests / tooling to generate an LDAP-style hash.
|
|
10
|
+
*
|
|
11
|
+
* These are NOT used by the runtime verification path and NOT intended for
|
|
12
|
+
* use as a general password-hashing function: the primary use case is
|
|
13
|
+
* seeding LDAP token fixtures in tests. Production `userPassword` values
|
|
14
|
+
* should be generated by `slappasswd` (or another operator tool) with a
|
|
15
|
+
* scheme of your choice. The RFC 3112 `{SSHA}` scheme uses a single SHA-1
|
|
16
|
+
* round — adequate for LDAP bearer-token interop, not adequate for
|
|
17
|
+
* user-chosen passwords. Prefer `sshaHash('sha512', …)` (`{SSHA512}`) when
|
|
18
|
+
* generating new fixtures.
|
|
19
|
+
*/
|
|
20
|
+
export type SshaAlgo = 'sha1' | 'sha256' | 'sha512';
|
|
21
|
+
export declare function sshaHash(algo: SshaAlgo, password: string, salt?: Buffer): string;
|
|
22
|
+
/**
|
|
23
|
+
* Legacy alias: generate an {SSHA} (salted SHA-1) hash.
|
|
24
|
+
* Retained so existing tests keep working. Prefer `sshaHash('sha512', ...)`
|
|
25
|
+
* for new fixtures.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ssha(password: string, salt?: Buffer): string;
|
|
@@ -10,6 +10,57 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { Express } from 'express';
|
|
12
12
|
import DmPlugin, { type Role } from '../abstract/plugin';
|
|
13
|
+
/**
|
|
14
|
+
* @openapi-component
|
|
15
|
+
* Config:
|
|
16
|
+
* type: object
|
|
17
|
+
* description: |
|
|
18
|
+
* Aggregated server configuration. The top-level fields are always
|
|
19
|
+
* present; `features` is a dynamic map whose keys are the names of
|
|
20
|
+
* loaded plugins that declare the `configurable` role. Each plugin
|
|
21
|
+
* contributes an arbitrary sub-object — consult the individual plugin
|
|
22
|
+
* documentation for its exact shape.
|
|
23
|
+
* additionalProperties: true
|
|
24
|
+
* properties:
|
|
25
|
+
* apiPrefix:
|
|
26
|
+
* type: string
|
|
27
|
+
* description: The API base path configured on this server.
|
|
28
|
+
* example: /api
|
|
29
|
+
* ldapBase:
|
|
30
|
+
* type: string
|
|
31
|
+
* description: The root LDAP suffix used by this server.
|
|
32
|
+
* example: dc=example,dc=com
|
|
33
|
+
* features:
|
|
34
|
+
* type: object
|
|
35
|
+
* description: |
|
|
36
|
+
* Per-plugin configuration blocks. Keys match the plugin's
|
|
37
|
+
* `name` field; values are plugin-specific objects.
|
|
38
|
+
* additionalProperties: true
|
|
39
|
+
* example:
|
|
40
|
+
* ldapGroups:
|
|
41
|
+
* enabled: true
|
|
42
|
+
* base: ou=groups,dc=example,dc=com
|
|
43
|
+
* mainAttribute: cn
|
|
44
|
+
* objectClass: [top, groupOfNames]
|
|
45
|
+
* static:
|
|
46
|
+
* enabled: true
|
|
47
|
+
* staticPath: /static
|
|
48
|
+
* appAccountsApi:
|
|
49
|
+
* enabled: true
|
|
50
|
+
* base: ou=app-accounts,dc=example,dc=com
|
|
51
|
+
* maxAccounts: 5
|
|
52
|
+
* example:
|
|
53
|
+
* apiPrefix: /api
|
|
54
|
+
* ldapBase: dc=example,dc=com
|
|
55
|
+
* features:
|
|
56
|
+
* ldapGroups:
|
|
57
|
+
* enabled: true
|
|
58
|
+
* base: ou=groups,dc=example,dc=com
|
|
59
|
+
* mainAttribute: cn
|
|
60
|
+
* static:
|
|
61
|
+
* enabled: true
|
|
62
|
+
* staticPath: /static
|
|
63
|
+
*/
|
|
13
64
|
export default class ConfigApi extends DmPlugin {
|
|
14
65
|
name: string;
|
|
15
66
|
roles: Role[];
|
|
@@ -5,6 +5,27 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Express } from 'express';
|
|
7
7
|
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
8
|
+
/**
|
|
9
|
+
* @openapi-component
|
|
10
|
+
* Hello:
|
|
11
|
+
* type: object
|
|
12
|
+
* description: Response returned by the hello-world demo endpoint.
|
|
13
|
+
* properties:
|
|
14
|
+
* message:
|
|
15
|
+
* type: string
|
|
16
|
+
* description: Static greeting string.
|
|
17
|
+
* example: Hello
|
|
18
|
+
* hookResults:
|
|
19
|
+
* type: array
|
|
20
|
+
* description: |
|
|
21
|
+
* Results collected from every function registered on the `hello`
|
|
22
|
+
* hook. Empty when no hook listeners are installed.
|
|
23
|
+
* items: {}
|
|
24
|
+
* example: []
|
|
25
|
+
* example:
|
|
26
|
+
* message: Hello
|
|
27
|
+
* hookResults: []
|
|
28
|
+
*/
|
|
8
29
|
export default class HelloWorld extends DmPlugin {
|
|
9
30
|
name: string;
|
|
10
31
|
roles: Role[];
|
|
@@ -6,6 +6,71 @@
|
|
|
6
6
|
import type { Express } from 'express';
|
|
7
7
|
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
8
8
|
import type { DM } from '../../bin';
|
|
9
|
+
/**
|
|
10
|
+
* Shared OpenAPI schemas surfaced by this plugin. Picked up by
|
|
11
|
+
* scripts/generate-openapi.ts and merged into `components.schemas`.
|
|
12
|
+
*
|
|
13
|
+
* @openapi-component
|
|
14
|
+
* BulkImportResult:
|
|
15
|
+
* type: object
|
|
16
|
+
* description: Summary returned after a bulk import operation.
|
|
17
|
+
* required: [success, total, created, updated, skipped, failed, errors, details]
|
|
18
|
+
* properties:
|
|
19
|
+
* success:
|
|
20
|
+
* type: boolean
|
|
21
|
+
* description: True when no fatal error aborted the import early.
|
|
22
|
+
* total:
|
|
23
|
+
* type: integer
|
|
24
|
+
* description: Total number of CSV data rows (excluding header).
|
|
25
|
+
* example: 42
|
|
26
|
+
* created:
|
|
27
|
+
* type: integer
|
|
28
|
+
* description: Entries successfully created (or counted in dry-run).
|
|
29
|
+
* example: 38
|
|
30
|
+
* updated:
|
|
31
|
+
* type: integer
|
|
32
|
+
* description: Existing entries updated.
|
|
33
|
+
* example: 2
|
|
34
|
+
* skipped:
|
|
35
|
+
* type: integer
|
|
36
|
+
* description: Existing entries skipped because `updateExisting` was false.
|
|
37
|
+
* example: 1
|
|
38
|
+
* failed:
|
|
39
|
+
* type: integer
|
|
40
|
+
* description: Rows that caused an error.
|
|
41
|
+
* example: 1
|
|
42
|
+
* errors:
|
|
43
|
+
* type: array
|
|
44
|
+
* items:
|
|
45
|
+
* type: object
|
|
46
|
+
* required: [line, error]
|
|
47
|
+
* properties:
|
|
48
|
+
* line:
|
|
49
|
+
* type: integer
|
|
50
|
+
* description: 1-based CSV line number (2 = first data row).
|
|
51
|
+
* identifier:
|
|
52
|
+
* type: string
|
|
53
|
+
* description: Value of the main attribute for the failing row (if known).
|
|
54
|
+
* error:
|
|
55
|
+
* type: string
|
|
56
|
+
* description: Error message.
|
|
57
|
+
* example:
|
|
58
|
+
* - line: 5
|
|
59
|
+
* identifier: bob
|
|
60
|
+
* error: 'Missing required attribute: mail'
|
|
61
|
+
* details:
|
|
62
|
+
* type: object
|
|
63
|
+
* required: [duration, linesProcessed]
|
|
64
|
+
* properties:
|
|
65
|
+
* duration:
|
|
66
|
+
* type: string
|
|
67
|
+
* description: Wall-clock time for the import (e.g. `1.2s`).
|
|
68
|
+
* example: 1.2s
|
|
69
|
+
* linesProcessed:
|
|
70
|
+
* type: integer
|
|
71
|
+
* description: Total data rows attempted.
|
|
72
|
+
* example: 42
|
|
73
|
+
*/
|
|
9
74
|
export default class LdapBulkImport extends DmPlugin {
|
|
10
75
|
name: string;
|
|
11
76
|
roles: Role[];
|
|
@@ -10,6 +10,74 @@ export interface postAdd {
|
|
|
10
10
|
[key: string]: AttributeValue | undefined;
|
|
11
11
|
}
|
|
12
12
|
export type postModify = ModifyRequest;
|
|
13
|
+
/**
|
|
14
|
+
* Shared OpenAPI schemas surfaced by this plugin. Picked up by
|
|
15
|
+
* scripts/generate-openapi.ts and merged into `components.schemas`.
|
|
16
|
+
*
|
|
17
|
+
* @openapi-component
|
|
18
|
+
* Group:
|
|
19
|
+
* type: object
|
|
20
|
+
* description: An LDAP group entry (groupOfNames / groupOfUniqueNames).
|
|
21
|
+
* required: [dn, cn]
|
|
22
|
+
* properties:
|
|
23
|
+
* dn:
|
|
24
|
+
* type: string
|
|
25
|
+
* description: Fully-qualified distinguished name of the entry.
|
|
26
|
+
* example: cn=admins,ou=groups,dc=example,dc=com
|
|
27
|
+
* cn:
|
|
28
|
+
* type: string
|
|
29
|
+
* description: Common name (RDN attribute) of the group.
|
|
30
|
+
* example: admins
|
|
31
|
+
* description:
|
|
32
|
+
* type: string
|
|
33
|
+
* example: Server administrators
|
|
34
|
+
* member:
|
|
35
|
+
* type: array
|
|
36
|
+
* items: { type: string }
|
|
37
|
+
* description: DNs of the group's members.
|
|
38
|
+
* example:
|
|
39
|
+
* - uid=alice,ou=users,dc=example,dc=com
|
|
40
|
+
* - uid=bob,ou=users,dc=example,dc=com
|
|
41
|
+
* GroupCreate:
|
|
42
|
+
* type: object
|
|
43
|
+
* required: [cn]
|
|
44
|
+
* properties:
|
|
45
|
+
* cn:
|
|
46
|
+
* type: string
|
|
47
|
+
* description: Common name of the new group.
|
|
48
|
+
* description:
|
|
49
|
+
* type: string
|
|
50
|
+
* member:
|
|
51
|
+
* type: array
|
|
52
|
+
* items: { type: string }
|
|
53
|
+
* description: Initial members (DNs).
|
|
54
|
+
* example:
|
|
55
|
+
* cn: admins
|
|
56
|
+
* description: Server administrators
|
|
57
|
+
* member:
|
|
58
|
+
* - uid=alice,ou=users,dc=example,dc=com
|
|
59
|
+
* GroupModify:
|
|
60
|
+
* type: object
|
|
61
|
+
* description: |
|
|
62
|
+
* Partial update. Each provided attribute is replaced wholesale. Use
|
|
63
|
+
* the dedicated `/members` endpoints to add/remove members one at a
|
|
64
|
+
* time without supplying the full list.
|
|
65
|
+
* properties:
|
|
66
|
+
* description: { type: string }
|
|
67
|
+
* member:
|
|
68
|
+
* type: array
|
|
69
|
+
* items: { type: string }
|
|
70
|
+
* example:
|
|
71
|
+
* description: Updated description
|
|
72
|
+
* Error:
|
|
73
|
+
* type: object
|
|
74
|
+
* properties:
|
|
75
|
+
* error: { type: string }
|
|
76
|
+
* code: { type: integer }
|
|
77
|
+
* example:
|
|
78
|
+
* error: Group not found
|
|
79
|
+
* code: 404
|
|
80
|
+
*/
|
|
13
81
|
export default class LdapGroups extends DmPlugin {
|
|
14
82
|
name: string;
|
|
15
83
|
roles: Role[];
|
|
@@ -1,65 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
roles: Role[];
|
|
10
|
-
pathAttr: string;
|
|
11
|
-
linkAttr: string;
|
|
12
|
-
schema?: Schema;
|
|
13
|
-
constructor(dm: DM);
|
|
14
|
-
/**
|
|
15
|
-
* API routes for LDAP organizations
|
|
16
|
-
* @param app Express application
|
|
17
|
-
*/
|
|
18
|
-
api(app: Express): void;
|
|
19
|
-
apiAdd(req: Request, res: Response): Promise<void>;
|
|
20
|
-
apiModify(req: Request, res: Response): Promise<void>;
|
|
21
|
-
apiDelete(req: Request, res: Response): Promise<void>;
|
|
22
|
-
apiMove(req: Request, res: Response): Promise<void>;
|
|
23
|
-
/**
|
|
24
|
-
* Consistency checks on any entry
|
|
25
|
-
*/
|
|
26
|
-
hooks: Hooks;
|
|
27
|
-
/**
|
|
28
|
-
* Check if the department link is valid
|
|
29
|
-
* @param entry LDAP entry to check
|
|
30
|
-
*/
|
|
31
|
-
checkDeptLink(entry: AttributesList): Promise<void>;
|
|
32
|
-
/**
|
|
33
|
-
* Check if the department path is valid
|
|
34
|
-
* @param entry LDAP entry to check
|
|
35
|
-
*/
|
|
36
|
-
checkDeptPath(entry: AttributesList): Promise<void>;
|
|
37
|
-
isEmptyOrganization(dn: string): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Check if entry is an organisation
|
|
40
|
-
* @param entry LDAP entry to check
|
|
41
|
-
* @returns True if entry is an organisation, false otherwise
|
|
42
|
-
*/
|
|
43
|
-
isOu(entry: AttributesList): boolean;
|
|
44
|
-
getOrganisationTop(req?: Request): Promise<AttributesList | AttributesList[]>;
|
|
45
|
-
getOrganisationByDn(dn: string, req?: Request): Promise<AttributesList>;
|
|
46
|
-
getOrganisationSubnodes(dn: string, req?: Request): Promise<AttributesList[]>;
|
|
47
|
-
addOrganization(dn: string, entry: AttributesList, req?: Request): Promise<boolean>;
|
|
48
|
-
modifyOrganization(dn: string, changes: ModifyRequest): Promise<boolean>;
|
|
49
|
-
validateNewOrganization(dn: string, entry: AttributesList): boolean;
|
|
50
|
-
validateChanges(dn: string, changes: ModifyRequest): boolean;
|
|
51
|
-
_validateOneChange(field: string, value: AttributeValue | null): boolean;
|
|
52
|
-
/**
|
|
53
|
-
* Move an organization to a different parent organization
|
|
54
|
-
* Uses LDAP modifyDN to change the DN hierarchy
|
|
55
|
-
*/
|
|
56
|
-
moveOrganization(dn: string, targetOrgDn: string, req?: Request): Promise<{
|
|
57
|
-
newDn: string;
|
|
58
|
-
}>;
|
|
59
|
-
deleteOrganization(dn: string): Promise<boolean>;
|
|
60
|
-
searchOrganisationSubnodes(dn: string, query: string): Promise<AttributesList[]>;
|
|
61
|
-
/**
|
|
62
|
-
* Provide configuration for config API
|
|
63
|
-
*/
|
|
64
|
-
getConfigApiData(): Record<string, unknown>;
|
|
65
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Use `core/ldap/organizations` instead. This file is a
|
|
3
|
+
* backwards-compatibility shim for setups that still load the plugin under
|
|
4
|
+
* the historical (singular) filename. It will be removed in a future major
|
|
5
|
+
* release.
|
|
6
|
+
*/
|
|
7
|
+
import LdapOrganizations from './organizations';
|
|
8
|
+
export default LdapOrganizations;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { Express, Request, Response } from 'express';
|
|
2
|
+
import DmPlugin, { type Role } from '../../abstract/plugin';
|
|
3
|
+
import { DM } from '../../bin';
|
|
4
|
+
import { Hooks } from '../../hooks';
|
|
5
|
+
import { AttributesList, AttributeValue, ModifyRequest } from '../../lib/ldapActions';
|
|
6
|
+
import type { Schema } from '../../config/schema';
|
|
7
|
+
/**
|
|
8
|
+
* Shared OpenAPI schemas surfaced by this plugin. Picked up by
|
|
9
|
+
* scripts/generate-openapi.ts and merged into `components.schemas`.
|
|
10
|
+
*
|
|
11
|
+
* @openapi-component
|
|
12
|
+
* Organization:
|
|
13
|
+
* type: object
|
|
14
|
+
* description: An LDAP organizational unit entry.
|
|
15
|
+
* required: [dn, ou]
|
|
16
|
+
* properties:
|
|
17
|
+
* dn:
|
|
18
|
+
* type: string
|
|
19
|
+
* description: Fully-qualified distinguished name of the entry.
|
|
20
|
+
* example: ou=Engineering,ou=organizations,dc=example,dc=com
|
|
21
|
+
* ou:
|
|
22
|
+
* type: string
|
|
23
|
+
* description: Organizational unit name (RDN attribute).
|
|
24
|
+
* example: Engineering
|
|
25
|
+
* o:
|
|
26
|
+
* type: string
|
|
27
|
+
* description: Organization display name.
|
|
28
|
+
* example: Engineering Department
|
|
29
|
+
* l:
|
|
30
|
+
* type: string
|
|
31
|
+
* description: Locality / city.
|
|
32
|
+
* example: Paris
|
|
33
|
+
* description:
|
|
34
|
+
* type: string
|
|
35
|
+
* example: Software engineering division
|
|
36
|
+
* organizationLink:
|
|
37
|
+
* type: string
|
|
38
|
+
* description: DN of the parent organization (link attribute).
|
|
39
|
+
* example: ou=organizations,dc=example,dc=com
|
|
40
|
+
* organizationPath:
|
|
41
|
+
* type: string
|
|
42
|
+
* description: |
|
|
43
|
+
* Human-readable slash-separated path to this node, used by the
|
|
44
|
+
* UI tree. Set automatically on creation and move.
|
|
45
|
+
* example: Engineering / Backend
|
|
46
|
+
* member:
|
|
47
|
+
* type: array
|
|
48
|
+
* items: { type: string }
|
|
49
|
+
* description: DNs of entries linked to this organization.
|
|
50
|
+
* example:
|
|
51
|
+
* - uid=alice,ou=users,dc=example,dc=com
|
|
52
|
+
* OrgSummary:
|
|
53
|
+
* type: object
|
|
54
|
+
* description: Lightweight organization reference (used in node lists).
|
|
55
|
+
* required: [dn, ou]
|
|
56
|
+
* properties:
|
|
57
|
+
* dn: { type: string, example: ou=HR,ou=organizations,dc=example,dc=com }
|
|
58
|
+
* ou: { type: string, example: HR }
|
|
59
|
+
* description: { type: string }
|
|
60
|
+
* objectClass:
|
|
61
|
+
* type: array
|
|
62
|
+
* items: { type: string }
|
|
63
|
+
* example: [top, organizationalUnit]
|
|
64
|
+
* OrganizationCreate:
|
|
65
|
+
* type: object
|
|
66
|
+
* required: [ou]
|
|
67
|
+
* properties:
|
|
68
|
+
* ou:
|
|
69
|
+
* type: string
|
|
70
|
+
* description: Name of the new organizational unit.
|
|
71
|
+
* example: Marketing
|
|
72
|
+
* parentDn:
|
|
73
|
+
* type: string
|
|
74
|
+
* description: |
|
|
75
|
+
* DN of the parent organizational unit. Defaults to the configured
|
|
76
|
+
* top organization when omitted.
|
|
77
|
+
* example: ou=organizations,dc=example,dc=com
|
|
78
|
+
* description: { type: string }
|
|
79
|
+
* l: { type: string }
|
|
80
|
+
* example:
|
|
81
|
+
* ou: Marketing
|
|
82
|
+
* parentDn: ou=organizations,dc=example,dc=com
|
|
83
|
+
* description: Marketing division
|
|
84
|
+
* OrganizationModify:
|
|
85
|
+
* type: object
|
|
86
|
+
* description: |
|
|
87
|
+
* Partial update. Any provided attribute is replaced wholesale.
|
|
88
|
+
* `ou` cannot be changed via this endpoint — use the move endpoint
|
|
89
|
+
* to relocate the node within the tree.
|
|
90
|
+
* properties:
|
|
91
|
+
* description: { type: string }
|
|
92
|
+
* l: { type: string }
|
|
93
|
+
* o: { type: string }
|
|
94
|
+
* example:
|
|
95
|
+
* description: Updated description for Marketing
|
|
96
|
+
*/
|
|
97
|
+
export default class LdapOrganizations extends DmPlugin {
|
|
98
|
+
name: string;
|
|
99
|
+
roles: Role[];
|
|
100
|
+
pathAttr: string;
|
|
101
|
+
linkAttr: string;
|
|
102
|
+
schema?: Schema;
|
|
103
|
+
constructor(dm: DM);
|
|
104
|
+
/**
|
|
105
|
+
* API routes for LDAP organizations
|
|
106
|
+
* @param app Express application
|
|
107
|
+
*/
|
|
108
|
+
api(app: Express): void;
|
|
109
|
+
apiAdd(req: Request, res: Response): Promise<void>;
|
|
110
|
+
apiModify(req: Request, res: Response): Promise<void>;
|
|
111
|
+
apiDelete(req: Request, res: Response): Promise<void>;
|
|
112
|
+
apiMove(req: Request, res: Response): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Consistency checks on any entry
|
|
115
|
+
*/
|
|
116
|
+
hooks: Hooks;
|
|
117
|
+
/**
|
|
118
|
+
* Check if the department link is valid
|
|
119
|
+
* @param entry LDAP entry to check
|
|
120
|
+
*/
|
|
121
|
+
checkDeptLink(entry: AttributesList): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Check if the department path is valid
|
|
124
|
+
* @param entry LDAP entry to check
|
|
125
|
+
*/
|
|
126
|
+
checkDeptPath(entry: AttributesList): Promise<void>;
|
|
127
|
+
isEmptyOrganization(dn: string): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Check if entry is an organisation
|
|
130
|
+
* @param entry LDAP entry to check
|
|
131
|
+
* @returns True if entry is an organisation, false otherwise
|
|
132
|
+
*/
|
|
133
|
+
isOu(entry: AttributesList): boolean;
|
|
134
|
+
getOrganisationTop(req?: Request): Promise<AttributesList | AttributesList[]>;
|
|
135
|
+
getOrganisationByDn(dn: string, req?: Request): Promise<AttributesList>;
|
|
136
|
+
getOrganisationSubnodes(dn: string, req?: Request): Promise<AttributesList[]>;
|
|
137
|
+
addOrganization(dn: string, entry: AttributesList, req?: Request): Promise<boolean>;
|
|
138
|
+
modifyOrganization(dn: string, changes: ModifyRequest): Promise<boolean>;
|
|
139
|
+
validateNewOrganization(dn: string, entry: AttributesList): boolean;
|
|
140
|
+
validateChanges(dn: string, changes: ModifyRequest): boolean;
|
|
141
|
+
_validateOneChange(field: string, value: AttributeValue | null): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Move an organization to a different parent organization
|
|
144
|
+
* Uses LDAP modifyDN to change the DN hierarchy
|
|
145
|
+
*/
|
|
146
|
+
moveOrganization(dn: string, targetOrgDn: string, req?: Request): Promise<{
|
|
147
|
+
newDn: string;
|
|
148
|
+
}>;
|
|
149
|
+
deleteOrganization(dn: string): Promise<boolean>;
|
|
150
|
+
searchOrganisationSubnodes(dn: string, query: string): Promise<AttributesList[]>;
|
|
151
|
+
/**
|
|
152
|
+
* Provide configuration for config API
|
|
153
|
+
*/
|
|
154
|
+
getConfigApiData(): Record<string, unknown>;
|
|
155
|
+
}
|