ldap-rest 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/abstract/plugin.js +1 -1
  3. package/dist/bin/index.js +1 -1
  4. package/dist/config/args.js +1 -1
  5. package/dist/config/schema.js +1 -0
  6. package/dist/lib/expressFormatedResponses.js +1 -1
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/lib/utils.js +1 -1
  9. package/dist/plugins/auth/authzScope.js +1 -0
  10. package/dist/plugins/auth/llng.js +1 -1
  11. package/dist/plugins/ldap/accountLifecycle.js +1 -0
  12. package/dist/plugins/ldap/bulkImport.js +1 -1
  13. package/dist/plugins/ldap/departmentSync.js +1 -1
  14. package/dist/plugins/ldap/enterpriseRules.js +1 -0
  15. package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
  16. package/dist/plugins/ldap/flatGeneric.js +1 -1
  17. package/dist/plugins/ldap/groups.js +1 -1
  18. package/dist/plugins/ldap/organizations.js +1 -1
  19. package/dist/plugins/scim/bulk.js +1 -1
  20. package/dist/plugins/scim/errors.js +1 -1
  21. package/dist/plugins/scim/filter.js +1 -1
  22. package/dist/plugins/scim/groups.js +1 -1
  23. package/dist/plugins/scim/list.js +1 -1
  24. package/dist/plugins/scim/mapping.js +1 -1
  25. package/dist/plugins/scim/patch.js +1 -1
  26. package/dist/plugins/scim/projection.js +1 -0
  27. package/dist/plugins/scim/scim.js +1 -1
  28. package/dist/plugins/scim/users.js +1 -1
  29. package/dist/plugins/twake/calendar.js +1 -0
  30. package/dist/plugins/twake/calendarResources.js +1 -1
  31. package/dist/plugins/twake/james.js +1 -1
  32. package/dist/src/abstract/ldapFlat.d.ts +159 -1
  33. package/dist/src/abstract/plugin.d.ts +13 -0
  34. package/dist/src/config/args.d.ts +5 -0
  35. package/dist/src/config/schema.d.ts +382 -12
  36. package/dist/src/lib/expressFormatedResponses.d.ts +14 -0
  37. package/dist/src/lib/ldapActions.d.ts +59 -6
  38. package/dist/src/lib/utils.d.ts +65 -0
  39. package/dist/src/plugins/auth/authzScope.d.ts +53 -0
  40. package/dist/src/plugins/auth/llng.d.ts +30 -1
  41. package/dist/src/plugins/ldap/accountLifecycle.d.ts +56 -0
  42. package/dist/src/plugins/ldap/departmentSync.d.ts +43 -0
  43. package/dist/src/plugins/ldap/enterpriseRules.d.ts +436 -0
  44. package/dist/src/plugins/ldap/flatGeneric.d.ts +29 -0
  45. package/dist/src/plugins/ldap/groups.d.ts +22 -0
  46. package/dist/src/plugins/ldap/organizations.d.ts +25 -4
  47. package/dist/src/plugins/scim/errors.d.ts +17 -4
  48. package/dist/src/plugins/scim/filter.d.ts +11 -1
  49. package/dist/src/plugins/scim/groups.d.ts +2 -0
  50. package/dist/src/plugins/scim/list.d.ts +3 -5
  51. package/dist/src/plugins/scim/mapping.d.ts +90 -9
  52. package/dist/src/plugins/scim/patch.d.ts +16 -1
  53. package/dist/src/plugins/scim/projection.d.ts +15 -0
  54. package/dist/src/plugins/scim/scim.d.ts +5 -0
  55. package/dist/src/plugins/scim/users.d.ts +21 -0
  56. package/dist/src/plugins/twake/calendar.d.ts +113 -0
  57. package/dist/src/plugins/twake/calendarResources.d.ts +9 -58
  58. package/docs/README.md +2 -0
  59. package/docs/client-development/api/openapi.md +1 -1
  60. package/docs/client-development/browser/libraries.md +1 -1
  61. package/docs/plugin-development/README.md +22 -0
  62. package/docs/plugin-development/dependencies.md +10 -7
  63. package/docs/presentations/en.md +1 -1
  64. package/docs/presentations/fr.md +1 -1
  65. package/docs/usage/README.md +2 -0
  66. package/docs/usage/configuration.md +35 -15
  67. package/docs/usage/directory-audit.md +105 -0
  68. package/docs/usage/plugins/README.md +6 -6
  69. package/docs/usage/plugins/auth/README.md +6 -5
  70. package/docs/usage/plugins/auth/authz-scope.md +103 -0
  71. package/docs/usage/plugins/auth/llng.md +17 -0
  72. package/docs/usage/plugins/integrations/README.md +5 -5
  73. package/docs/usage/plugins/integrations/{calendar-resources.md → calendar.md} +70 -13
  74. package/docs/usage/plugins/integrations/scim.md +147 -38
  75. package/docs/usage/plugins/ldap/README.md +13 -11
  76. package/docs/usage/plugins/ldap/account-lifecycle.md +129 -0
  77. package/docs/usage/plugins/ldap/bulk-import.md +8 -1
  78. package/docs/usage/plugins/ldap/enterprise-rules.md +167 -0
  79. package/docs/usage/plugins/ldap/flat-generic.md +172 -0
  80. package/docs/usage/plugins/ldap/groups.md +10 -4
  81. package/docs/usage/plugins/ldap/organizations.md +59 -33
  82. package/docs/usage/upgrading.md +444 -0
  83. package/package.json +54 -5
  84. package/static/schemas/example/README.md +36 -0
  85. package/static/schemas/example/groups.json +94 -0
  86. package/static/schemas/example/nomenclature/domains.json +49 -0
  87. package/static/schemas/example/organizations.json +106 -0
  88. package/static/schemas/example/positions.json +35 -0
  89. package/static/schemas/example/users.json +246 -0
  90. package/static/schemas/twake/groups.json +81 -15
  91. package/static/schemas/twake/nomenclature/twakeAccountStatus.json +26 -0
  92. package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +22 -0
  93. package/static/schemas/twake/nomenclature/twakeDomain.json +60 -0
  94. package/static/schemas/twake/nomenclature/twakeListType.json +22 -0
  95. package/static/schemas/twake/nomenclature/twakeMailboxType.json +22 -0
  96. package/static/schemas/twake/nomenclature/twakeTitle.json +42 -0
  97. package/static/schemas/twake/organizations.json +92 -12
  98. package/static/schemas/twake/positions.json +22 -4
  99. package/static/schemas/twake/users.json +269 -32
@@ -1,7 +1,3 @@
1
- /**
2
- * LDAP low-level library
3
- * @author Xavier Guimard <xguimard@linagora.com>
4
- */
5
1
  import type { Request } from 'express';
6
2
  import type { ClientOptions, SearchResult, SearchOptions } from 'ldapts';
7
3
  import type winston from 'winston';
@@ -12,6 +8,27 @@ export type AttributeValue = Buffer | Buffer[] | string[] | string;
12
8
  export type AttributesList = Record<string, AttributeValue>;
13
9
  export type LdapList = Record<string, AttributesList>;
14
10
  export type { SearchOptions, SearchResult };
11
+ /**
12
+ * The directory as seen by one HTTP request.
13
+ *
14
+ * `ldapActions`'s own methods take the request as a trailing optional
15
+ * argument, and the authorization plugins hook `ldap*request` and skip every
16
+ * check when it is missing — so omitting it is an authorization bypass that
17
+ * fails silently rather than loudly. Three shipped in the SCIM plugin alone.
18
+ *
19
+ * These methods take no request parameter: it is captured once by
20
+ * `ldapActions.forRequest()`, and forgetting it stops being expressible.
21
+ * Request handlers should bind once and use this; the unbound methods remain
22
+ * for work that genuinely belongs to no request, such as startup and cron
23
+ * tasks, where passing nothing is the deliberate answer.
24
+ */
25
+ export interface RequestBoundLdap {
26
+ search(options: SearchOptions, base?: string): Promise<SearchResult | AsyncGenerator<SearchResult>>;
27
+ add(dn: string, entry: AttributesList): Promise<boolean>;
28
+ modify(dn: string, changes: ModifyRequest): Promise<boolean>;
29
+ rename(dn: string, newRdn: string): Promise<boolean>;
30
+ delete(dn: string | string[]): Promise<boolean>;
31
+ }
15
32
  export interface ModifyRequest {
16
33
  add?: AttributesList;
17
34
  replace?: AttributesList;
@@ -26,6 +43,22 @@ declare class ldapActions {
26
43
  parent: DM;
27
44
  logger: winston.Logger;
28
45
  private searchCache;
46
+ private cacheEnabled;
47
+ /**
48
+ * How many writes this instance has made since it was built.
49
+ *
50
+ * A read can be overtaken by a write: the write lands and `invalidateCache`
51
+ * finds nothing to drop, because the read has not stored its answer yet —
52
+ * and the read then stores what it read *before* the write, an entry
53
+ * nothing will drop until its TTL runs out. `search()` remembers the count
54
+ * it started at and stores only if no write happened in between, so a read
55
+ * a write overtook costs a cache entry, never a stale answer.
56
+ *
57
+ * The count is per instance and per write, not per DN: a write to another
58
+ * branch keeps a concurrent read out of the cache too. That is hit rate
59
+ * given up, not freshness.
60
+ */
61
+ private cacheGeneration;
29
62
  queryLimit: ReturnType<typeof pLimit>;
30
63
  private connectionPool;
31
64
  private poolSize;
@@ -62,13 +95,33 @@ declare class ldapActions {
62
95
  private getAttributeSignature;
63
96
  /**
64
97
  * Generate cache key for LDAP search
98
+ *
99
+ * Every option that changes the answer belongs in here, or a search that
100
+ * asked for more is served what a search that asked for less got: the
101
+ * requested attributes above all, but also the ones that change the shape
102
+ * of what comes back. `timeLimit` is deliberately absent — it bounds how
103
+ * long the server may spend, not what it returns.
65
104
  */
66
105
  private getCacheKey;
67
106
  /**
68
- * Invalidate cache entries for a specific DN
69
- * Called after modifications to ensure cache consistency
107
+ * Drop every cached read of `dn`, and of everything below it.
108
+ *
109
+ * Called after each write, so the next read of what just changed goes to
110
+ * the directory. Two things this does that a `key.startsWith(dn)` test did
111
+ * not: it compares DNs the way the directory does (see {@link foldDn}), so
112
+ * a write spelling a DN differently from the read that cached it still
113
+ * drops it; and it drops the subtree, because `rename()` and `move()`
114
+ * re-parent a whole branch in one operation — renaming an organization
115
+ * changes the DN of every entry under it — leaving the cached reads of
116
+ * those children filed under DNs that no longer exist. A leaf has nothing
117
+ * below it and the extra test costs nothing.
70
118
  */
71
119
  invalidateCache(dn: string): void;
120
+ /**
121
+ * Bind every directory operation to one request, so the authorization
122
+ * hooks always see it. See {@link RequestBoundLdap}.
123
+ */
124
+ forRequest(req: Request): RequestBoundLdap;
72
125
  search(options: SearchOptions, base?: string, req?: Request): Promise<SearchResult | AsyncGenerator<SearchResult>>;
73
126
  /**
74
127
  * Wrap paginated search to ensure connection is released when done
@@ -41,6 +41,30 @@ export declare const transformSchemas: (schemas: string | Buffer, config: Config
41
41
  * ```
42
42
  */
43
43
  export declare function escapeLdapFilter(value: string): string;
44
+ /**
45
+ * Build the LDAP filter of a list search: a substring match on one attribute,
46
+ * or on any of several.
47
+ *
48
+ * The value is escaped, so nothing a client types can reach the filter as
49
+ * syntax; the attribute names cannot be escaped — they *are* syntax — so they
50
+ * are checked against what an LDAP attribute name may contain and refused
51
+ * otherwise.
52
+ *
53
+ * Every list route builds the same filter, and a client asking for a mail
54
+ * address expects the same answer whichever entity it is asking about.
55
+ *
56
+ * @param match - The substring to look for
57
+ * @param attribute - One attribute name, or several separated by commas
58
+ * @returns The LDAP filter
59
+ * @throws BadRequestError when an attribute name is not one
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * substringSearchFilter('ali', 'cn,mail')
64
+ * // => '(|(cn=*ali*)(mail=*ali*))'
65
+ * ```
66
+ */
67
+ export declare function substringSearchFilter(match: string, attribute: string): string;
44
68
  /**
45
69
  * Escape special characters in LDAP DN attribute values according to RFC 4514
46
70
  * Prevents LDAP injection attacks by escaping characters that have special meaning in DNs
@@ -145,6 +169,22 @@ export declare function getParentDn(dn: string): string;
145
169
  * ```
146
170
  */
147
171
  export declare function getRdn(dn: string): string;
172
+ /**
173
+ * Extract the *value* of the first RDN of a DN, with its escapes removed
174
+ *
175
+ * Where {@link getRdn} returns `ou=Test Org`, this returns `Test Org`: the
176
+ * name the entry is known by, which is what a path or a label is built from.
177
+ *
178
+ * @param dn - The DN to read
179
+ * @returns The value, or an empty string when the DN has no RDN
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * rdnValue('ou=Test\\, Org,ou=organization,dc=example,dc=com')
184
+ * // => 'Test, Org'
185
+ * ```
186
+ */
187
+ export declare function rdnValue(dn: string): string;
148
188
  /**
149
189
  * Check if a DN is a child of another DN
150
190
  *
@@ -184,6 +224,31 @@ export declare function isChildOf(dn: string, parentDn: string): boolean;
184
224
  * ```
185
225
  */
186
226
  export declare function normalizeDn(dn: string): string;
227
+ /**
228
+ * Whether a member DN is the placeholder a `groupOfNames` holds so it stays
229
+ * valid with no real member (`--group-dummy-user`).
230
+ *
231
+ * Compared as DNs rather than as text: the directory answers with its own
232
+ * spelling, so the configured `uid=fakeUser,ou=users,…` has to match the
233
+ * `uid=fakeuser, ou=users,…` a group may actually hold. A textual comparison
234
+ * counted such a member as a real one — enough to make an empty group refuse
235
+ * deletion, and to show the placeholder to SCIM clients.
236
+ *
237
+ * Every reader of the member list asks here, so the four of them cannot drift
238
+ * apart again.
239
+ *
240
+ * @param member - The member DN to test
241
+ * @param dummy - The configured placeholder DN, if any
242
+ * @returns true when `member` is that placeholder; false when none is
243
+ * configured, as there is then no placeholder to hide
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * isDummyMemberDn('uid=fakeuser, ou=users,dc=e,dc=c', 'uid=fakeUser,ou=users,dc=e,dc=c')
248
+ * // => true
249
+ * ```
250
+ */
251
+ export declare function isDummyMemberDn(member: unknown, dummy: string | undefined): boolean;
187
252
  /**
188
253
  * Check whether a DN is a branch base itself or sits anywhere below it.
189
254
  *
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @module plugins/auth/authzScope
3
+ * @author Xavier Guimard <xguimard@linagora.com>
4
+ *
5
+ * Tells a client what the signed-in administrator may actually do, before it
6
+ * offers them a button that will fail.
7
+ *
8
+ * In a local-administration model the scope *is* the interface: a manager
9
+ * administers a few branches of the tree and nothing else, and an application
10
+ * that cannot name those branches leaves them guessing. This endpoint answers
11
+ * both halves — which branches, and which entities can be created in them —
12
+ * by asking the authorization plugin in force, whichever one that is.
13
+ * @group Plugins
14
+ */
15
+ import type { Express } from 'express';
16
+ import DmPlugin, { type Role } from '../../abstract/plugin';
17
+ export default class AuthzScope extends DmPlugin {
18
+ name: string;
19
+ roles: Role[];
20
+ /**
21
+ * Register `GET {api_prefix}/v1/authz/scope`.
22
+ *
23
+ * @param app Express application
24
+ */
25
+ api(app: Express): void;
26
+ /**
27
+ * The authorization plugin in force, if any.
28
+ *
29
+ * @returns the plugin, or undefined when the server is unrestricted
30
+ */
31
+ private authz;
32
+ /**
33
+ * Entities a client can create, as declared by the loaded plugins.
34
+ *
35
+ * @returns entity name and branch, one per creatable entity
36
+ */
37
+ private entities;
38
+ /**
39
+ * Answer the scope request.
40
+ *
41
+ * @param req Express request, carrying the authenticated user
42
+ * @param res Express response
43
+ */
44
+ private scope;
45
+ /**
46
+ * Read the display name and path of a branch, so a client can show the
47
+ * scope in the words the directory uses rather than as a raw DN.
48
+ *
49
+ * @param dn branch to describe
50
+ * @returns name and path when the entry carries them
51
+ */
52
+ private branchLabel;
53
+ }
@@ -1,8 +1,37 @@
1
- import type { Response } from 'express';
1
+ /**
2
+ * @module plugins/auth/llng
3
+ * @group Plugins
4
+ * @author Xavier Guimard <xguimard@linagora.com>
5
+ *
6
+ * Lemonldap::NG authentication plugin
7
+ * This plugin enables authentication and authorization using Lemonldap::NG.
8
+ */
9
+ import type { Express, Response } from 'express';
2
10
  import AuthBase, { DmRequest } from '../../lib/auth/base';
3
11
  import type { Role } from '../../abstract/plugin';
12
+ type LlngHandler = typeof import('lemonldap-ng-handler');
4
13
  export default class AuthLLNG extends AuthBase {
5
14
  name: string;
6
15
  roles: Role[];
16
+ private handler?;
17
+ /**
18
+ * Load the optional `lemonldap-ng-handler` dependency before the server
19
+ * starts serving requests, rather than importing it statically.
20
+ *
21
+ * A static import would throw the moment this file is loaded — as soon as
22
+ * this plugin is configured — with Node's own opaque "Cannot find
23
+ * package" error. Loading it here instead means a server that never
24
+ * configures this plugin never touches the package at all, and one that
25
+ * does configure it gets a clear failure naming both the plugin and the
26
+ * missing dependency, at startup, instead of on the first request an auth
27
+ * plugin that cannot run must not pretend to succeed.
28
+ */
29
+ api(app: Express): Promise<void>;
30
+ /**
31
+ * Load the handler. Split out so tests can override it to simulate the
32
+ * dependency being absent, without actually uninstalling it.
33
+ */
34
+ protected loadHandler(): Promise<LlngHandler>;
7
35
  authMethod(req: DmRequest, res: Response, next: () => void): void;
8
36
  }
37
+ export {};
@@ -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
  */