ldap-rest 0.0.1 → 0.1.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.
Files changed (62) hide show
  1. package/README.md +38 -0
  2. package/bin/sync-app-accounts.mjs +276 -0
  3. package/dist/abstract/ldapFlat.js +1 -1
  4. package/dist/abstract/plugin.js +1 -1
  5. package/dist/bin/index.js +1 -1
  6. package/dist/config/args.js +1 -1
  7. package/dist/lib/ldapActions.js +1 -1
  8. package/dist/lib/utils.js +1 -1
  9. package/dist/logger/winston.js +1 -1
  10. package/dist/plugins/auth/hmac.js +1 -0
  11. package/dist/plugins/auth/totp.js +1 -0
  12. package/dist/plugins/demo/helloworld.js +1 -1
  13. package/dist/plugins/ldap/groups.js +1 -1
  14. package/dist/plugins/ldap/onChange.js +1 -1
  15. package/dist/plugins/static.js +1 -1
  16. package/dist/plugins/twake/appAccountsApi.js +1 -0
  17. package/dist/plugins/twake/appAccountsConsistency.js +1 -0
  18. package/dist/plugins/twake/james.js +1 -1
  19. package/dist/plugins/weblogs.js +1 -1
  20. package/dist/src/abstract/ldapFlat.d.ts +0 -5
  21. package/dist/src/abstract/plugin.d.ts +1 -0
  22. package/dist/src/bin/index.d.ts +1 -0
  23. package/dist/src/browser/shared/index.d.ts +1 -0
  24. package/dist/src/browser/shared/utils/hmac.d.ts +73 -0
  25. package/dist/src/browser/shared/utils/totp.d.ts +64 -0
  26. package/dist/src/config/args.d.ts +9 -1
  27. package/dist/src/hooks.d.ts +2 -2
  28. package/dist/src/lib/ldapActions.d.ts +3 -7
  29. package/dist/src/lib/utils.d.ts +17 -0
  30. package/dist/src/logger/winston.d.ts +5 -0
  31. package/dist/src/plugins/auth/hmac.d.ts +24 -0
  32. package/dist/src/plugins/auth/totp.d.ts +28 -0
  33. package/dist/src/plugins/ldap/onChange.d.ts +1 -0
  34. package/dist/src/plugins/static.d.ts +4 -0
  35. package/dist/src/plugins/twake/appAccountsApi.d.ts +52 -0
  36. package/dist/src/plugins/twake/appAccountsConsistency.d.ts +58 -0
  37. package/docs/DEVELOPER_GUIDE.md +1 -0
  38. package/docs/PLUGIN_DEPENDENCIES.md +19 -0
  39. package/docs/README.md +76 -30
  40. package/docs/authentication.md +604 -12
  41. package/docs/plugins/DEVELOPMENT.md +162 -76
  42. package/docs/testing-with-embedded-ldap.md +347 -0
  43. package/docs/twakeAppAccountsApi.md +485 -0
  44. package/docs/weblogs.md +37 -8
  45. package/examples/web/README.md +175 -0
  46. package/examples/web/ldap-tree-viewer.html +16 -2
  47. package/examples/web/totp-client.html +541 -0
  48. package/package.json +132 -87
  49. package/static/browser/shared/index.js +1 -0
  50. package/static/browser/shared/index.js.map +1 -1
  51. package/static/browser/shared/utils/hmac.js +202 -0
  52. package/static/browser/shared/utils/hmac.js.map +1 -0
  53. package/static/browser/shared/utils/totp.js +182 -0
  54. package/static/browser/shared/utils/totp.js.map +1 -0
  55. package/static/browser/shared.esm.js +181 -1
  56. package/static/browser/shared.esm.js.map +1 -1
  57. package/static/browser/shared.js +184 -0
  58. package/static/browser/shared.js.map +1 -1
  59. package/static/browser/shared.min.js +1 -1
  60. package/static/browser/shared.min.js.map +1 -1
  61. package/static/schemas/obm/organizations.json +24 -0
  62. package/static/schemas/obm/users.json +192 -0
package/README.md CHANGED
@@ -8,6 +8,15 @@ and extensible REST API.
8
8
 
9
9
  Core plugins also provide plugins that ensure LDAP data consistency.
10
10
 
11
+ ## Key Features
12
+
13
+ - 🔒 **Robust Error Handling** - Server stays online even when plugins encounter errors
14
+ - 🔌 **Plugin Architecture** - Extensible through a powerful plugin system
15
+ - 🔐 **Flexible Authentication** - Support for multiple authentication methods
16
+ - 📊 **REST API** - Complete LDAP operations through REST endpoints
17
+ - 🎯 **Event Hooks** - Intercept and customize LDAP operations
18
+ - 🌐 **Browser Libraries** - Ready-to-use JavaScript components
19
+
11
20
  ## How it works
12
21
 
13
22
  All configuration is done via command-line arguments and/or environment variables.
@@ -17,10 +26,33 @@ Example:
17
26
  npx ldap-rest --ldap-base 'dc=example,dc=com' \
18
27
  --ldap-dn 'cn=admin,dc=example,dc=com' --ldap-pwd admin \
19
28
  --ldap-url ldap://localhost \
29
+ --log-level notice \
20
30
  --plugin core/ldap/groups --ldap-group-base 'ou=groups,dc=example,dc=com' \
21
31
  --plugin core/ldap/externalUsersInGroups
22
32
  ```
23
33
 
34
+ ### LDAP Failover
35
+
36
+ Multiple LDAP servers are supported for high availability:
37
+
38
+ ```shell
39
+ --ldap-url ldap://ldap1.example.com,ldap://ldap2.example.com,ldap://ldap3.example.com
40
+ ```
41
+
42
+ The system automatically tries each URL in order and fails over if a connection fails.
43
+
44
+ ### Log Levels
45
+
46
+ LDAP-Rest uses syslog-style log levels:
47
+
48
+ - `error` - Only errors
49
+ - `warn` - Warnings and errors
50
+ - `notice` - Web access logs (recommended for production)
51
+ - `info` - General info + web logs
52
+ - `debug` - All debug output
53
+
54
+ Use `--log-level notice` for production to see web access logs without general info messages.
55
+
24
56
  ## Command-line options and environment variables
25
57
 
26
58
  [ToDo]
@@ -32,6 +64,7 @@ See also [Typescript declarations](./src/config/args.ts).
32
64
  ### For Application Developers
33
65
 
34
66
  - **[Developer Guide](./docs/DEVELOPER_GUIDE.md)** - Complete guide for building web applications using LDAP-Rest APIs and browser libraries
67
+ - **[Browser Examples](./examples/web/)** - Interactive demos of browser libraries (TOTP client, LDAP tree viewer, etc.)
35
68
 
36
69
  ### For Plugin Developers
37
70
 
@@ -39,6 +72,11 @@ See also [Typescript declarations](./src/config/args.ts).
39
72
  - [Plugins](./src/plugins/README.md)
40
73
  - [Hooks](./HOOKS.md)
41
74
 
75
+ ### Authentication
76
+
77
+ - **[Authentication Guide](./docs/authentication.md)** - Token, TOTP, LemonLDAP::NG, OpenID Connect
78
+ - **[TOTP Client Demo](./examples/web/totp-client.html)** - Interactive TOTP generator and API client
79
+
42
80
  ## Copyright and license
43
81
 
44
82
  [![Powered by LINAGORA](./docs/linagora.png)](https://linagora.com)
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sync utility to verify and fix consistency between users and applicative accounts
4
+ * Ensures principal accounts exist for all users with mail
5
+ * Removes orphaned applicative accounts
6
+ * @author Generated with Claude Code
7
+ */
8
+
9
+ import { DM } from '../dist/bin/index.js';
10
+
11
+ // Parse command line arguments
12
+ const args = process.argv.slice(2);
13
+ const quiet = args.includes('--quiet') || args.includes('-q');
14
+ const dryRun = args.includes('--dry-run') || args.includes('-n');
15
+
16
+ if (args.includes('--help') || args.includes('-h')) {
17
+ console.log(`
18
+ Usage: sync-app-accounts [options]
19
+
20
+ Synchronizes applicative accounts with LDAP users.
21
+ Ensures consistency between ou=users and ou=applicative branches.
22
+
23
+ Operations performed:
24
+ 1. Create missing principal accounts (uid=mail) for users with mail
25
+ 2. Delete orphaned applicative accounts (user_cXXXXXXXX without parent user)
26
+ 3. Delete orphaned principal accounts (uid=mail without matching user)
27
+
28
+ Options:
29
+ --quiet, -q Only show summary and errors
30
+ --dry-run, -n Show what would be changed without making changes
31
+ --help, -h Show this help message
32
+
33
+ Environment variables:
34
+ DM_LDAP_BASE LDAP search base (required)
35
+ DM_APPLICATIVE_ACCOUNT_BASE Applicative accounts base (required)
36
+ DM_MAIL_ATTRIBUTE Mail attribute name (default: mail)
37
+ `);
38
+ process.exit(0);
39
+ }
40
+
41
+ async function syncAppAccounts() {
42
+ const dm = new DM();
43
+ await dm.ready;
44
+
45
+ const mailAttr = dm.config.mail_attribute || 'mail';
46
+ const applicativeBase = dm.config.applicative_account_base;
47
+ const userBase = dm.config.ldap_base;
48
+
49
+ if (!applicativeBase) {
50
+ dm.logger.error('DM_APPLICATIVE_ACCOUNT_BASE is not configured');
51
+ process.exit(1);
52
+ }
53
+
54
+ if (!userBase) {
55
+ dm.logger.error('DM_LDAP_BASE is not configured');
56
+ process.exit(1);
57
+ }
58
+
59
+ if (!quiet) {
60
+ dm.logger.info('Starting applicative accounts synchronization...');
61
+ dm.logger.info(`User base: ${userBase}`);
62
+ dm.logger.info(`Applicative base: ${applicativeBase}`);
63
+ if (dryRun) {
64
+ dm.logger.info('DRY RUN MODE: No changes will be made');
65
+ }
66
+ dm.logger.info('');
67
+ }
68
+
69
+ let principalCreated = 0;
70
+ let appAccountsDeleted = 0;
71
+ let principalDeleted = 0;
72
+ let errors = 0;
73
+
74
+ try {
75
+ // Step 1: Check users and create missing principal accounts
76
+ if (!quiet) {
77
+ dm.logger.info(
78
+ 'Step 1: Checking users and creating missing principal accounts...'
79
+ );
80
+ }
81
+
82
+ const usersResult = await dm.ldap.search(
83
+ {
84
+ paged: false,
85
+ filter: `(${mailAttr}=*)`,
86
+ attributes: [mailAttr, 'uid', 'cn', 'sn', 'givenName', 'displayName'],
87
+ },
88
+ userBase
89
+ );
90
+
91
+ const users = usersResult.searchEntries || [];
92
+ if (!quiet) {
93
+ dm.logger.info(`Found ${users.length} users with mail attribute`);
94
+ }
95
+
96
+ for (const user of users) {
97
+ const mail = Array.isArray(user[mailAttr])
98
+ ? String(user[mailAttr][0])
99
+ : String(user[mailAttr]);
100
+
101
+ if (!mail) continue;
102
+
103
+ try {
104
+ // Check if principal account exists
105
+ const principalDn = `uid=${mail},${applicativeBase}`;
106
+ const principalResult = await dm.ldap.search(
107
+ {
108
+ scope: 'base',
109
+ paged: false,
110
+ },
111
+ principalDn
112
+ );
113
+
114
+ if (
115
+ !principalResult.searchEntries ||
116
+ principalResult.searchEntries.length === 0
117
+ ) {
118
+ dm.logger.warn(`Missing principal account for ${mail}`);
119
+
120
+ if (dryRun) {
121
+ dm.logger.info(` Would create: ${principalDn}`);
122
+ principalCreated++;
123
+ } else {
124
+ // Create principal account
125
+ const attrs = {
126
+ objectClass: ['inetOrgPerson'],
127
+ uid: mail,
128
+ };
129
+
130
+ // Copy attributes
131
+ const attrsToCopy = [
132
+ 'cn',
133
+ 'sn',
134
+ 'givenName',
135
+ mailAttr,
136
+ 'displayName',
137
+ ];
138
+ for (const attr of attrsToCopy) {
139
+ if (user[attr]) {
140
+ const value = user[attr];
141
+ if (Array.isArray(value)) {
142
+ attrs[attr] = value.map(v =>
143
+ Buffer.isBuffer(v) ? v.toString() : String(v)
144
+ );
145
+ } else {
146
+ attrs[attr] = Buffer.isBuffer(value)
147
+ ? value.toString()
148
+ : String(value);
149
+ }
150
+ }
151
+ }
152
+
153
+ await dm.ldap.add(principalDn, attrs);
154
+ dm.logger.info(` Created: ${principalDn}`);
155
+ principalCreated++;
156
+ }
157
+ }
158
+ } catch (err) {
159
+ dm.logger.error(`Error processing user ${mail}: ${err.message}`);
160
+ errors++;
161
+ }
162
+ }
163
+
164
+ // Step 2: Check applicative accounts and delete orphans
165
+ if (!quiet) {
166
+ dm.logger.info('\nStep 2: Checking applicative accounts for orphans...');
167
+ }
168
+
169
+ const appAccountsResult = await dm.ldap.search(
170
+ {
171
+ paged: false,
172
+ filter: '(uid=*)',
173
+ },
174
+ applicativeBase
175
+ );
176
+
177
+ const appAccounts = appAccountsResult.searchEntries || [];
178
+ if (!quiet) {
179
+ dm.logger.info(`Found ${appAccounts.length} entries in applicative base`);
180
+ }
181
+
182
+ // Build user map for quick lookup
183
+ const userMap = new Map();
184
+ const mailMap = new Map();
185
+ for (const user of users) {
186
+ const uid = Array.isArray(user.uid)
187
+ ? String(user.uid[0])
188
+ : String(user.uid);
189
+ const mail = Array.isArray(user[mailAttr])
190
+ ? String(user[mailAttr][0])
191
+ : String(user[mailAttr]);
192
+ userMap.set(uid, true);
193
+ if (mail) mailMap.set(mail, true);
194
+ }
195
+
196
+ for (const account of appAccounts) {
197
+ const uid = Array.isArray(account.uid)
198
+ ? String(account.uid[0])
199
+ : String(account.uid);
200
+ const dn = account.dn;
201
+
202
+ try {
203
+ // Check if it's an applicative account (format: username_cXXXXXXXX)
204
+ if (uid.includes('_c')) {
205
+ const username = uid.split('_')[0];
206
+
207
+ if (!userMap.has(username)) {
208
+ dm.logger.warn(
209
+ `Orphaned applicative account: ${uid} (user ${username} not found)`
210
+ );
211
+
212
+ if (dryRun) {
213
+ dm.logger.info(` Would delete: ${dn}`);
214
+ appAccountsDeleted++;
215
+ } else {
216
+ await dm.ldap.delete(dn);
217
+ dm.logger.info(` Deleted: ${dn}`);
218
+ appAccountsDeleted++;
219
+ }
220
+ }
221
+ } else {
222
+ // Principal account (uid=mail) - check if mail exists in users
223
+ const mail = uid;
224
+
225
+ if (!mailMap.has(mail)) {
226
+ dm.logger.warn(
227
+ `Orphaned principal account: ${mail} (no user with this mail)`
228
+ );
229
+
230
+ if (dryRun) {
231
+ dm.logger.info(` Would delete: ${dn}`);
232
+ principalDeleted++;
233
+ } else {
234
+ await dm.ldap.delete(dn);
235
+ dm.logger.info(` Deleted: ${dn}`);
236
+ principalDeleted++;
237
+ }
238
+ }
239
+ }
240
+ } catch (err) {
241
+ dm.logger.error(`Error processing account ${uid}: ${err.message}`);
242
+ errors++;
243
+ }
244
+ }
245
+
246
+ // Summary
247
+ dm.logger.info('\n' + '='.repeat(60));
248
+ dm.logger.info('Synchronization summary:');
249
+ dm.logger.info(
250
+ ` Principal accounts ${dryRun ? 'to create' : 'created'}: ${principalCreated}`
251
+ );
252
+ dm.logger.info(
253
+ ` Applicative accounts ${dryRun ? 'to delete' : 'deleted'}: ${appAccountsDeleted}`
254
+ );
255
+ dm.logger.info(
256
+ ` Principal accounts ${dryRun ? 'to delete' : 'deleted'}: ${principalDeleted}`
257
+ );
258
+ dm.logger.info(` Errors: ${errors}`);
259
+ dm.logger.info('='.repeat(60));
260
+
261
+ if (errors > 0) {
262
+ process.exit(1);
263
+ }
264
+ } catch (err) {
265
+ dm.logger.error('Error during synchronization:', err);
266
+ process.exit(1);
267
+ } finally {
268
+ await dm.ldap.unbind();
269
+ }
270
+ }
271
+
272
+ // Run the sync
273
+ syncAppAccounts().catch(err => {
274
+ console.error('Fatal error:', err);
275
+ process.exit(1);
276
+ });
@@ -1 +1 @@
1
- import t from"fs";import{wantJson as e,serverError as i,notFound as r,jsonBody as s,created as a,tryMethod as n}from"../lib/expressFormatedResponses.js";import{transformSchemas as o,launchHooksChained as h,launchHooks as c}from"../lib/utils.js";import d from"./plugin.js";class LdapFlat extends d{constructor(e,i){if(super(e),this.ldap=e.ldap,this.base=i.base,this.mainAttribute=i.mainAttribute,this.objectClass=i.objectClass,this.defaultAttributes=i.defaultAttributes||{},this.singularName=i.singularName,this.pluralName=i.pluralName,this.hookPrefix=i.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(o(e,this.config)),this.logger.info(`${this.singularName} schema loaded from ${i.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${i.schemaPath}: ${t}`)}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,async(t,r)=>{if(e(t,r))try{const e={};t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(e.filter=`(${t.query.attribute}=*${t.query.match}*)`),t.query.attributes&&"string"==typeof t.query.attributes&&(e.attributes=t.query.attributes.split(","));const i=await this.listEntries(e);r.json(i)}catch(t){return i(r,t)}}),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiGet(t,e)),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,async(t,e)=>this.apiAdd(t,e)),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiDelete(t,e)),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiModify(t,e)),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,async(t,e)=>this.apiMove(t,e))}async apiGet(t,s){if(!e(t,s))return;const a=decodeURIComponent(t.params.id);try{const t=/,/.test(a)?a:`${this.mainAttribute}=${a},${this.base}`,e=await this.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)return r(s,`${this.singularName} not found`);s.json(e.searchEntries[0])}catch(t){return t.code&&32===t.code?r(s,`${this.singularName} not found`):i(s,t)}}async apiAdd(t,e){const r=s(t,e,this.mainAttribute);if(!r)return;const n=r[this.mainAttribute],o={...r};delete o[this.mainAttribute],delete o.dn;try{await this.addEntry(n,o,t);const i=await this.searchEntriesByName(n,!1);return a(e,i[n])}catch(t){return i(e,t)}}async apiDelete(t,i){if(!e(t,i))return;const r=decodeURIComponent(t.params.id);await n(i,this.deleteEntry.bind(this),r)}async apiModify(t,e){const i=s(t,e);if(!i)return;const r=decodeURIComponent(t.params.id);await n(e,this.modifyEntry.bind(this),r,i)}async apiMove(t,r){if(!e(t,r))return;const a=s(t,r,"targetOrgDn");if(!a)return;const n=decodeURIComponent(t.params.id),{targetOrgDn:o}=a;if(o&&"string"==typeof o)try{const e=await this.moveEntry(n,o,t);r.json({success:!0,...e})}catch(t){return i(r,t)}else r.status(400).json({error:"Missing or invalid targetOrgDn in request body"})}async addEntry(t,e={},i){let r;if(new RegExp(`^${this.mainAttribute}=`).test(t)){r=t;const e=`,${this.base}`;if(!r.endsWith(e))throw new Error(`DN must be in the flat branch "${this.base}". Provided DN "${r}" does not end with "${e}"`);t=t.replace(new RegExp(`^${this.mainAttribute}=([^,]+).*`),"$1")}else r=`${this.mainAttribute}=${t},${this.base}`;await this.validateNewEntry(r,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let s,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[r,a]=await h(this.registeredHooks[`${this.hookPrefix}add`],[r,a]),this.logger.debug("Adding LDAP entry:",{dn:r,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{s=await this.ldap.add(r,a,i)}catch(t){throw this.logger.error("LDAP add failed:",{dn:r,entry:JSON.stringify(a,null,2),error:t}),new Error(`Failed to add ${this.singularName} ${r}: ${t}`)}return c(this.registeredHooks[`${this.hookPrefix}adddone`],[r,a]),s}async modifyEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`;const r=this.opNumber();if([i,e]=await h(this.registeredHooks[`${this.hookPrefix}modify`],[i,e,r]),e.add&&e.add[this.mainAttribute])throw new Error(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new Error(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new Error(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new Error(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(i,e);const s=await this.ldap.modify(i,e);return c(this.registeredHooks[`${this.hookPrefix}modifydone`],[i,e,r]),s}async renameEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`,r=/,/.test(e)?e:`${this.mainAttribute}=${e},${this.base}`;[i,r]=await h(this.registeredHooks[`${this.hookPrefix}rename`],[i,r]);const s=await this.ldap.rename(i,r);return c(this.registeredHooks[`${this.hookPrefix}renamedone`],[i,r]),s}async deleteEntry(t){let e=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`;e=await h(this.registeredHooks[`${this.hookPrefix}delete`],e);const i=await this.ldap.delete(e);return c(this.registeredHooks[`${this.hookPrefix}deletedone`],e),i}async moveEntry(t,e,i){const r=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`,s=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[s]||!this.schema?.attributes[a])throw new Error(`Schema for ${this.singularName} does not support move operation (missing ${s} or ${a})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[s]},r,i);if(!n.searchEntries||0===n.searchEntries.length)throw new Error(`Entry ${r} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await h(this.registeredHooks[`${this.hookPrefix}move`],[r,e,i]);const c={replace:{[s]:e,[a]:o}};return await this.modifyEntry(t,c),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const r=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!r.searchEntries||0===r.searchEntries.length)throw new Error(`Organization ${t} not found`);const s=r.searchEntries[0];if(s[i]){const t=s[i];return Array.isArray(t)?String(t[0]):String(t)}const a=s.ou||s.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e}){const i={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(i.attributes=e);const r=await this.ldap.search(i,this.base),s={};for await(const t of r)t.searchEntries.forEach(t=>{if(t[this.mainAttribute]){const e=t[this.mainAttribute];let i;i=Array.isArray(e)?"string"==typeof e[0]?e[0]:String(e[0]):"string"==typeof e?e:String(e),s[i]=t}});return s}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const r=e?`(${this.mainAttribute}=*${t}*)`:`(${this.mainAttribute}=${t})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new Error(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const r=JSON.stringify(e.default);if(r!==JSON.stringify(i))throw new Error(`Attribute "${t}" is fixed and cannot be modified. Expected: ${r}`)}if(!await this._validateOneChange(t,i))throw new Error(`Invalid value for attribute "${t}"`);if(e.required&&!i)throw new Error(`Attribute "${t}" is required`)}for(const[t,i]of Object.entries(this.schema.attributes))if(i.required&&!e[t])throw new Error(`Attribute "${t}" is required`);return!0}async validateChanges(t,e){if(!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new Error(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new Error(`Invalid value for attribute "${t}"`);if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new Error(`Invalid value for attribute "${t}"`);if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new Error(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if("pointer"===i.type){if("string"!=typeof e)throw new Error(`Field ${t} must be a string (DN pointer)`);const r=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>new RegExp(`,?${t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}$`,"i").test(r)))throw new Error(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.search({paged:!1,scope:"base"},r);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new Error(`Field ${t} points to non-existent DN: ${r}`)}catch(e){throw new Error(`Field ${t} points to invalid or non-existent DN: ${r}`)}}if(i.test){const t=new RegExp(i.test);return Array.isArray(e)?e.every(e=>t.test(e)):t.test(e)}return!0}escapeDnValue(t){if(!t)return t;return t.split(",").map(t=>{const e=t.trim(),i=e.indexOf("=");if(-1===i)return e;const r=e.substring(0,i);let s=e.substring(i+1);return s=s.replace(/\\/g,"\\\\").replace(/ /g,"\\ ").replace(/#/g,"\\#").replace(/\+/g,"\\+").replace(/,/g,"\\,").replace(/;/g,"\\;").replace(/</g,"\\<").replace(/>/g,"\\>"),`${r}=${s}`}).join(",")}}export{LdapFlat as default};
1
+ import t from"fs";import{wantJson as e,serverError as i,notFound as r,jsonBody as s,created as a,tryMethod as n}from"../lib/expressFormatedResponses.js";import{transformSchemas as o,launchHooksChained as h,launchHooks as d}from"../lib/utils.js";import c from"./plugin.js";class LdapFlat extends c{constructor(e,i){if(super(e),this.ldap=e.ldap,this.base=i.base,this.mainAttribute=i.mainAttribute,this.objectClass=i.objectClass,this.defaultAttributes=i.defaultAttributes||{},this.singularName=i.singularName,this.pluralName=i.pluralName,this.hookPrefix=i.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(o(e,this.config)),this.logger.info(`${this.singularName} schema loaded from ${i.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${i.schemaPath}: ${t}`)}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,async(t,r)=>{if(e(t,r))try{const e={};t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(e.filter=`(${t.query.attribute}=*${t.query.match}*)`),t.query.attributes&&"string"==typeof t.query.attributes&&(e.attributes=t.query.attributes.split(","));const i=await this.listEntries(e);r.json(i)}catch(t){return i(r,t)}}),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiGet(t,e)),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,async(t,e)=>this.apiAdd(t,e)),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiDelete(t,e)),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiModify(t,e)),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,async(t,e)=>this.apiMove(t,e))}async apiGet(t,s){if(!e(t,s))return;const a=decodeURIComponent(t.params.id);try{const t=/,/.test(a)?a:`${this.mainAttribute}=${a},${this.base}`,e=await this.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)return r(s,`${this.singularName} not found`);s.json(e.searchEntries[0])}catch(t){return t.code&&32===t.code?r(s,`${this.singularName} not found`):i(s,t)}}async apiAdd(t,e){const r=s(t,e,this.mainAttribute);if(!r)return;const n=r[this.mainAttribute],o={...r};delete o[this.mainAttribute],delete o.dn;try{await this.addEntry(n,o,t);const i=await this.searchEntriesByName(n,!1);return a(e,i[n])}catch(t){return i(e,t)}}async apiDelete(t,i){if(!e(t,i))return;const r=decodeURIComponent(t.params.id);await n(i,this.deleteEntry.bind(this),r)}async apiModify(t,e){const i=s(t,e);if(!i)return;const r=decodeURIComponent(t.params.id);await n(e,this.modifyEntry.bind(this),r,i)}async apiMove(t,r){if(!e(t,r))return;const a=s(t,r,"targetOrgDn");if(!a)return;const n=decodeURIComponent(t.params.id),{targetOrgDn:o}=a;if(o&&"string"==typeof o)try{const e=await this.moveEntry(n,o,t);r.json({success:!0,...e})}catch(t){return i(r,t)}else r.status(400).json({error:"Missing or invalid targetOrgDn in request body"})}async addEntry(t,e={},i){let r;if(new RegExp(`^${this.mainAttribute}=`).test(t)){r=t;const e=`,${this.base}`;if(!r.endsWith(e))throw new Error(`DN must be in the flat branch "${this.base}". Provided DN "${r}" does not end with "${e}"`);t=t.replace(new RegExp(`^${this.mainAttribute}=([^,]+).*`),"$1")}else r=`${this.mainAttribute}=${t},${this.base}`;await this.validateNewEntry(r,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let s,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[r,a]=await h(this.registeredHooks[`${this.hookPrefix}add`],[r,a]),this.logger.debug("Adding LDAP entry:",{dn:r,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{s=await this.ldap.add(r,a,i)}catch(t){throw this.logger.error("LDAP add failed:",{dn:r,entry:JSON.stringify(a,null,2),error:t}),new Error(`Failed to add ${this.singularName} ${r}: ${t}`)}return d(this.registeredHooks[`${this.hookPrefix}adddone`],[r,a]),s}async modifyEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`;const r=this.opNumber();if([i,e]=await h(this.registeredHooks[`${this.hookPrefix}modify`],[i,e,r]),e.add&&e.add[this.mainAttribute])throw new Error(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new Error(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new Error(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new Error(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(i,e);const s=await this.ldap.modify(i,e);return d(this.registeredHooks[`${this.hookPrefix}modifydone`],[i,e,r]),s}async renameEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`,r=/,/.test(e)?e:`${this.mainAttribute}=${e},${this.base}`;[i,r]=await h(this.registeredHooks[`${this.hookPrefix}rename`],[i,r]);const s=await this.ldap.rename(i,r);return d(this.registeredHooks[`${this.hookPrefix}renamedone`],[i,r]),s}async deleteEntry(t){let e=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`;e=await h(this.registeredHooks[`${this.hookPrefix}delete`],e);const i=await this.ldap.delete(e);return d(this.registeredHooks[`${this.hookPrefix}deletedone`],e),i}async moveEntry(t,e,i){const r=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`,s=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[s]||!this.schema?.attributes[a])throw new Error(`Schema for ${this.singularName} does not support move operation (missing ${s} or ${a})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[s]},r,i);if(!n.searchEntries||0===n.searchEntries.length)throw new Error(`Entry ${r} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await h(this.registeredHooks[`${this.hookPrefix}move`],[r,e,i]);const d={replace:{[s]:e,[a]:o}};return await this.modifyEntry(t,d),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const r=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!r.searchEntries||0===r.searchEntries.length)throw new Error(`Organization ${t} not found`);const s=r.searchEntries[0];if(s[i]){const t=s[i];return Array.isArray(t)?String(t[0]):String(t)}const a=s.ou||s.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e}){const i={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(i.attributes=e);const r=await this.ldap.search(i,this.base),s={};for await(const t of r)t.searchEntries.forEach(t=>{if(t[this.mainAttribute]){const e=t[this.mainAttribute];let i;i=Array.isArray(e)?"string"==typeof e[0]?e[0]:String(e[0]):"string"==typeof e?e:String(e),s[i]=t}});return s}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const r=e?`(${this.mainAttribute}=*${t}*)`:`(${this.mainAttribute}=${t})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new Error(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const r=JSON.stringify(e.default);if(r!==JSON.stringify(i))throw new Error(`Attribute "${t}" is fixed and cannot be modified. Expected: ${r}`)}if(!await this._validateOneChange(t,i))throw new Error(`Invalid value for attribute "${t}"`);if(e.required&&!i)throw new Error(`Attribute "${t}" is required`)}for(const[t,i]of Object.entries(this.schema.attributes))if(i.required&&!e[t])throw new Error(`Attribute "${t}" is required`);return!0}async validateChanges(t,e){if(!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new Error(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new Error(`Invalid value for attribute "${t}"`);if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new Error(`Invalid value for attribute "${t}"`);if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new Error(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if("pointer"===i.type){if("string"!=typeof e)throw new Error(`Field ${t} must be a string (DN pointer)`);const r=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>new RegExp(`,?${t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}$`,"i").test(r)))throw new Error(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.search({paged:!1,scope:"base"},r);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new Error(`Field ${t} points to non-existent DN: ${r}`)}catch(e){throw new Error(`Field ${t} points to invalid or non-existent DN: ${r}`)}}if(i.test){const t=new RegExp(i.test);return Array.isArray(e)?e.every(e=>t.test(e)):t.test(e)}return!0}}export{LdapFlat as default};
@@ -1 +1 @@
1
- class DmPlugin{constructor(e){this.registeredHooks={},this.server=e,this.config=e.config,this.logger=e.logger,this.registeredHooks=e.hooks}opNumber(){return this.server.operationSequence++}}export{DmPlugin as default};
1
+ export{asyncHandler}from"../lib/utils.js";class DmPlugin{constructor(e){this.registeredHooks={},this.server=e,this.config=e.config,this.logger=e.logger,this.registeredHooks=e.hooks}opNumber(){return this.server.operationSequence++}}export{DmPlugin as default};
package/dist/bin/index.js CHANGED
@@ -1 +1 @@
1
- import{fileURLToPath as i}from"url";import{join as e,dirname as s}from"path";import o from"express";import r from"body-parser";import{parseConfig as t}from"../lib/parseConfig.js";import n from"../config/args.js";import g from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as a}from"../lib/expressFormatedResponses.js";import h from"../plugins/priority.json.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this.config=t(n),this.app=o(),this.app.use(r.json()),this.app.use(r.urlencoded({extended:!0})),this.logger=l(this.config),this.ldap=new g(this),a(this.logger),this.operationSequence=0;const i=[];if(this.config.plugin){const e=this.config.plugin.find(i=>i.includes("configApi"));let s=this.config.plugin.filter(i=>!i.includes("configApi"));for(const e of h)s.includes(e)&&(s=s.filter(i=>i!==e),i.push(this.loadPlugin(e)));for(const e of s)i.push(this.loadPlugin(e));e&&i.push(Promise.all(i).then(()=>this.loadPlugin(e)))}this.ready=new Promise((e,s)=>{i.length>0?Promise.all(i).then(()=>e()).catch(i=>s(new Error("Error loading plugins: "+i))):e()})}run(){return new Promise((i,e)=>{this.server=this.app.listen(this.config.port,s=>{s?(this.logger.error(`Error starting server: ${s}`),e(s)):(this.logger.debug(`Server started on port ${this.config.port}`),i())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(o){let r,t;if(/:/.test(o)){let i=o.substring(o.indexOf(":")+1);if(o=o.substring(0,o.indexOf(":")),/:/.test(i)){r=i.substring(0,i.indexOf(":")),r||(r=void 0),i=i.substring(i.indexOf(":")+1);try{t=JSON.parse(i),"object"!=typeof t?(this.logger.error(`Overrides for plugin ${o} are not valid: ${i}`),t=void 0):this.logger.debug(`Overrides for plugin ${r||o}: ${i}`)}catch(e){this.logger.error(`Failed to parse overrides for plugin ${o}: ${e}, using ${i}`),t=void 0}}else r=i,r||(r=void 0)}else r=void 0;return this.logger.debug(`Loading plugin ${o}`),o.startsWith("core/")&&(o=o.replace("core/",e(s(i(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((i,e)=>{import(o).then(async s=>{let n;if(s&&s.default&&(s=s.default),t){const i={...this.config,...t};n=new s({...this,config:i})}else n=new s(this);if(!n)return e(new Error(`Unable to load ${o}`));i(await this.registerPlugin(o,n,r)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(i=>e(new Error(`Failed to load plugin ${o}: ${i}`)))})}async registerPlugin(i,e,s){if(e.name||(e.name=i),s&&(e.name=s),this.loadedPlugins[e.name])return this.logger.info(`Plugin ${i} already loaded as ${e.name}`),!1;if(this.logger.debug(`Registering plugin ${i} as ${e.name}`),e.dependencies)for(const i in e.dependencies)this.loadedPlugins[i]||(this.logger.debug(`Plugin ${e.name} depends on ${i}, loading it first`),await this.loadPlugin(e.dependencies[i]));if(e.api&&(this.logger.debug(`Plugin ${e.name} has API, registering it`),await e.api(this.app)),e.hooks)for(const i in e.hooks){this.logger.debug(`Plugin ${e.name} has hook ${i}, registering it`);const s=e.hooks[i];if(this.hooks[i]||(this.hooks[i]=[]),!s||"function"!=typeof s)throw new Error(`Plugin ${e.name}: hook ${i} is invalid`);this.hooks[i].push(s)}return this.loadedPlugins[e.name]=e,!0}}export{DM};
1
+ import{fileURLToPath as e}from"url";import{join as i,dirname as r}from"path";import s from"express";import o from"body-parser";import{parseConfig as t}from"../lib/parseConfig.js";import n from"../config/args.js";import g from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as a}from"../lib/expressFormatedResponses.js";import h from"../plugins/priority.json.js";export{asyncHandler,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas}from"../lib/utils.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this.config=t(n),this.app=s(),this.app.use(o.json()),this.app.use(o.urlencoded({extended:!0})),this.logger=l(this.config),this.ldap=new g(this),a(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const i=this.config.plugin.find(e=>e.includes("configApi"));let r=this.config.plugin.filter(e=>!e.includes("configApi"));for(const i of h)r.includes(i)&&(r=r.filter(e=>e!==i),e.push(this.loadPlugin(i)));for(const i of r)e.push(this.loadPlugin(i));i&&e.push(Promise.all(e).then(()=>this.loadPlugin(i)))}this.ready=new Promise((i,r)=>{e.length>0?Promise.all(e).then(()=>i()).catch(e=>r(new Error("Error loading plugins: "+e))):i()})}run(){return this.app.use((e,i,r,s)=>{this.logger.error(`Unhandled error in request ${i.method} ${i.path}: ${e.message}`,{stack:e.stack,url:i.url,method:i.method}),r.headersSent||r.status(500).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})}),process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,i)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:i})}),new Promise((e,i)=>{this.server=this.app.listen(this.config.port,r=>{r?(this.logger.error(`Error starting server: ${r}`),i(r)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(s){let o,t;if(/:/.test(s)){let e=s.substring(s.indexOf(":")+1);if(s=s.substring(0,s.indexOf(":")),/:/.test(e)){o=e.substring(0,e.indexOf(":")),o||(o=void 0),e=e.substring(e.indexOf(":")+1);try{t=JSON.parse(e),"object"!=typeof t?(this.logger.error(`Overrides for plugin ${s} are not valid: ${e}`),t=void 0):this.logger.debug(`Overrides for plugin ${o||s}: ${e}`)}catch(i){this.logger.error(`Failed to parse overrides for plugin ${s}: ${i}, using ${e}`),t=void 0}}else o=e,o||(o=void 0)}else o=void 0;return this.logger.debug(`Loading plugin ${s}`),s.startsWith("core/")&&(s=s.replace("core/",i(r(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((e,i)=>{import(s).then(async r=>{let n;if(r&&r.default&&(r=r.default),t){const e={...this.config,...t};n=new r({...this,config:e})}else n=new r(this);if(!n)return i(new Error(`Unable to load ${s}`));e(await this.registerPlugin(s,n,o)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(e=>i(new Error(`Failed to load plugin ${s}: ${e}`)))})}async registerPlugin(e,i,r){if(i.name||(i.name=e),r&&(i.name=r),this.loadedPlugins[i.name])return this.logger.info(`Plugin ${e} already loaded as ${i.name}`),!1;if(this.logger.debug(`Registering plugin ${e} as ${i.name}`),i.dependencies)for(const e in i.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${i.name} depends on ${e}, loading it first`),await this.loadPlugin(i.dependencies[e]));if(i.api&&(this.logger.debug(`Plugin ${i.name} has API, registering it`),await i.api(this.app)),i.hooks)for(const e in i.hooks){this.logger.debug(`Plugin ${i.name} has hook ${e}, registering it`);const r=i.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!r||"function"!=typeof r)throw new Error(`Plugin ${i.name}: hook ${e} is invalid`);this.hooks[e].push(r)}return this.loadedPlugins[i.name]=i,!0}}export{DM};
@@ -1 +1 @@
1
- import{fileURLToPath as a}from"url";import{join as _,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","info"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL","ldap://localhost"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(e(a(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(e(a(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",_(e(a(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"]];export{t as default};
1
+ import{fileURLToPath as a}from"url";import{join as _,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(e(a(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(e(a(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",_(e(a(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"]];export{t as default};
@@ -1 +1 @@
1
- import{Client as e,Attribute as t,Change as n}from"ldapts";import{LRUCache as i}from"lru-cache";import o from"p-limit";import{launchHooksChained as s,launchHooks as r}from"./utils.js";const a={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};class ldapActions{constructor(e){this.connectionPool=[],this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=o(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const n="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=1e3*(this.config.ldap_cache_ttl||300);if(this.searchCache=new i({max:n,ttl:s,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(`LDAP search cache initialized: max=${n}, ttl=${s/1e3}s`),!e.config.ldap_url)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");e.config.ldap_base?this.base=e.config.ldap_base:(this.base=e.config.ldap_dn.split(",",2)[1],this.logger.warn(`LDAP base is not defined, using "${this.base}"`)),this.options={url:e.config.ldap_url,timeout:0,connectTimeout:0,strictDN:!1},e.config.ldap_url.startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const t=new e(this.options);try{await t.bind(this.dn,this.pwd)}catch(e){throw this.logger.error("LDAP bind error:",e),new Error("LDAP bind error")}if(!t)throw new Error("LDAP connection error");return t}cleanupExpiredConnections(){const e=Date.now(),t=[];for(let n=this.connectionPool.length-1;n>=0;n--){const i=this.connectionPool[n];!i.inUse&&e-i.createdAt>this.connectionTtl&&(t.push(i),this.connectionPool.splice(n,1))}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}async acquireConnection(){this.cleanupExpiredConnections();const e=this.connectionPool.find(e=>!e.inUse);if(e)return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e;if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise(e=>{const t=setInterval(()=>{this.cleanupExpiredConnections();const n=this.connectionPool.find(e=>!e.inUse);n&&(clearInterval(t),n.inUse=!0,e(n))},50)})}releaseConnection(e){e.inUse=!1,this.logger.debug("Released LDAP connection back to pool")}async connect(){return await this.createConnection()}getCacheKey(e,t){const n=t.attributes?[...t.attributes].sort().join(","):"*",i="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return`${e}:${t.scope||"sub"}:${i}:${n}`}invalidateCache(e){for(const t of this.searchCache.keys())t.startsWith(e)&&this.searchCache.delete(t)}async search(e,t=this.base,n){let i={...a,...e};if(i=await s(this.parent.hooks.ldapsearchopts,i),[t,i]=await s(this.parent.hooks.ldapsearchrequest,[t,i,n]),!i.paged&&"base"===i.scope){const e=this.getCacheKey(t,i),n=this.searchCache.get(e);if(n)return this.logger.debug(`LDAP search cache hit: ${e}`),n}const o=await this.acquireConnection();try{let e=i.paged?o.client.searchPaginated(t,i):o.client.search(t,i);if(e=await s(this.parent.hooks.ldapsearchresult,e),!i.paged&&"base"===i.scope&&e instanceof Promise){const n=await e,o=this.getCacheKey(t,i);return this.searchCache.set(o,n),this.logger.debug(`LDAP search cached: ${o}`),n}return i.paged?this.wrapPaginatedSearch(e,o):e}finally{i.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,n,i){e=this.setDn(e),n.objectClass&&0!==n.objectClass.length||!this.config.user_class||(n.objectClass=this.config.user_class);const o={};for(const[e,t]of Object.entries(n))Buffer.isBuffer(t)?o[e]=t.toString():Array.isArray(t)&&t.length>0&&Buffer.isBuffer(t[0])?o[e]=t.map(e=>e.toString()):o[e]=t;[e,n]=await s(this.parent.hooks.ldapaddrequest,[e,o,i]);const a=[];for(const[e,n]of Object.entries(o)){const i=Array.isArray(n)?n:[n];a.push(new t({type:e,values:i}))}const c=await this.acquireConnection();try{return await c.client.add(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapadddone,[e,n]),!0}catch(e){throw new Error(`LDAP add error: ${e}`)}finally{this.releaseConnection(c)}}async modify(e,i,o){e=this.setDn(e);const a=[],c=this.opNumber();if([e,i]=await s(this.parent.hooks.ldapmodifyrequest,[e,i,c,o]),i.add)for(const[e,o]of Object.entries(i.add))a.push(new n({operation:"add",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(i.replace)for(const[e,o]of Object.entries(i.replace))a.push(new n({operation:"replace",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(i.delete)if(Array.isArray(i.delete))for(const e of i.delete)e&&a.push(new n({operation:"delete",modification:new t({type:e,values:[]})}));else for(const[e,o]of Object.entries(i.delete)){const i=new n({operation:"delete",modification:new t(o?{type:e,values:Array.isArray(o)?o:[o]}:{type:e})});a.push(i)}if(0===a.length)return this.logger.error("No changes to apply"),r(this.parent.hooks.ldapmodifydone,[e,{},c]),!1;{const t=await this.acquireConnection();try{return await t.client.modify(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapmodifydone,[e,i,c]),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(a)}`),new Error(`LDAP modify error: ${t}`)}finally{this.releaseConnection(t)}}}async rename(e,t,n){e=this.setDn(e),t=this.setDn(t),[e,t]=await s(this.parent.hooks.ldaprenamerequest,[e,t,n]);const i=await this.acquireConnection();try{return await i.client.modifyDN(e,t),r(this.parent.hooks.ldaprenamedone,[e,t]),!0}catch(e){throw new Error(`LDAP rename error: ${e}`)}finally{this.releaseConnection(i)}}async move(e,t){e=this.setDn(e),t=this.setDn(t);const n=await this.acquireConnection();try{return await n.client.modifyDN(e,t),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw new Error(`LDAP move error: ${e}`)}finally{this.releaseConnection(n)}}async delete(e){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),e=await s(this.parent?.hooks.ldapdeleterequest,e);const t=await this.acquireConnection();try{for(const n of e){try{await t.client.del(n),this.invalidateCache(n)}catch(e){throw new Error(`LDAP delete error: ${e}`)}r(this.parent.hooks.ldapdeletedone,n)}return!0}finally{this.releaseConnection(t)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${e},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
1
+ import{Client as e,Attribute as t,Change as i}from"ldapts";import{LRUCache as n}from"lru-cache";import o from"p-limit";import{launchHooksChained as s,launchHooks as r}from"./utils.js";const a={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};class ldapActions{constructor(e){this.connectionPool=[],this.currentUrlIndex=0,this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=o(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=1e3*(this.config.ldap_cache_ttl||300);if(this.searchCache=new n({max:i,ttl:s,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(`LDAP search cache initialized: max=${i}, ttl=${s/1e3}s`),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");e.config.ldap_base?this.base=e.config.ldap_base:(this.base=e.config.ldap_dn.split(",",2)[1],this.logger.warn(`LDAP base is not defined, using "${this.base}"`)),this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const t=[];for(let i=0;i<this.ldapUrls.length;i++){const n=(this.currentUrlIndex+i)%this.ldapUrls.length,o=this.ldapUrls[n];try{this.logger.debug(`Attempting connection to ${o}`);const t={...this.options,url:o,tlsOptions:o.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new e(t);return await i.bind(this.dn,this.pwd),n!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${o}`),this.currentUrlIndex=n),i}catch(e){this.logger.warn(`Failed to connect to ${o}: ${String(e)}`),t.push(e)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${t.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){const e=Date.now(),t=[];for(let i=this.connectionPool.length-1;i>=0;i--){const n=this.connectionPool[i];!n.inUse&&e-n.createdAt>this.connectionTtl&&(t.push(n),this.connectionPool.splice(i,1))}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}async acquireConnection(){this.cleanupExpiredConnections();const e=this.connectionPool.find(e=>!e.inUse);if(e)return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e;if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise(e=>{const t=setInterval(()=>{this.cleanupExpiredConnections();const i=this.connectionPool.find(e=>!e.inUse);i&&(clearInterval(t),i.inUse=!0,e(i))},50)})}releaseConnection(e){e.inUse=!1,this.logger.debug("Released LDAP connection back to pool")}getCacheKey(e,t){const i=t.attributes?[...t.attributes].sort().join(","):"*",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return`${e}:${t.scope||"sub"}:${n}:${i}`}invalidateCache(e){for(const t of this.searchCache.keys())t.startsWith(e)&&this.searchCache.delete(t)}async search(e,t=this.base,i){let n={...a,...e};if(n=await s(this.parent.hooks.ldapsearchopts,n),[t,n]=await s(this.parent.hooks.ldapsearchrequest,[t,n,i]),!n.paged&&"base"===n.scope){const e=this.getCacheKey(t,n),i=this.searchCache.get(e);if(i)return this.logger.debug(`LDAP search cache hit: ${e}`),i}const o=await this.acquireConnection();try{let e=n.paged?o.client.searchPaginated(t,n):o.client.search(t,n);if(e=await s(this.parent.hooks.ldapsearchresult,e),!n.paged&&"base"===n.scope&&e instanceof Promise){const i=await e,o=this.getCacheKey(t,n);return this.searchCache.set(o,i),this.logger.debug(`LDAP search cached: ${o}`),i}return n.paged?this.wrapPaginatedSearch(e,o):e}finally{n.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,i,n){e=this.setDn(e),i.objectClass&&0!==i.objectClass.length||!this.config.user_class||(i.objectClass=this.config.user_class);const o={};for(const[e,t]of Object.entries(i))Buffer.isBuffer(t)?o[e]=t.toString():Array.isArray(t)&&t.length>0&&Buffer.isBuffer(t[0])?o[e]=t.map(e=>e.toString()):o[e]=t;[e,i]=await s(this.parent.hooks.ldapaddrequest,[e,o,n]);const a=[];for(const[e,i]of Object.entries(o)){const n=Array.isArray(i)?i:[i];a.push(new t({type:e,values:n}))}const c=await this.acquireConnection();try{return await c.client.add(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapadddone,[e,i]),!0}catch(e){throw new Error(`LDAP add error: ${e}`)}finally{this.releaseConnection(c)}}async modify(e,n,o){e=this.setDn(e);const a=[],c=this.opNumber();if([e,n]=await s(this.parent.hooks.ldapmodifyrequest,[e,n,c,o]),n.add)for(const[e,o]of Object.entries(n.add))a.push(new i({operation:"add",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(n.replace)for(const[e,o]of Object.entries(n.replace))a.push(new i({operation:"replace",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(n.delete)if(Array.isArray(n.delete))for(const e of n.delete)e&&a.push(new i({operation:"delete",modification:new t({type:e,values:[]})}));else for(const[e,o]of Object.entries(n.delete)){const n=new i({operation:"delete",modification:new t(o?{type:e,values:Array.isArray(o)?o:[o]}:{type:e})});a.push(n)}if(0===a.length)return this.logger.error("No changes to apply"),r(this.parent.hooks.ldapmodifydone,[e,{},c]),!1;{const t=await this.acquireConnection();try{return await t.client.modify(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapmodifydone,[e,n,c]),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(a)}`),new Error(`LDAP modify error: ${t}`)}finally{this.releaseConnection(t)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t),[e,t]=await s(this.parent.hooks.ldaprenamerequest,[e,t,i]);const n=await this.acquireConnection();try{return await n.client.modifyDN(e,t),r(this.parent.hooks.ldaprenamedone,[e,t]),!0}catch(e){throw new Error(`LDAP rename error: ${e}`)}finally{this.releaseConnection(n)}}async move(e,t){e=this.setDn(e),t=this.setDn(t);const i=await this.acquireConnection();try{return await i.client.modifyDN(e,t),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw new Error(`LDAP move error: ${e}`)}finally{this.releaseConnection(i)}}async delete(e){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),e=await s(this.parent?.hooks.ldapdeleterequest,e);const t=await this.acquireConnection();try{for(const i of e){try{await t.client.del(i),this.invalidateCache(i)}catch(e){throw new Error(`LDAP delete error: ${e}`)}r(this.parent.hooks.ldapdeletedone,i)}return!0}finally{this.releaseConnection(t)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${e},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
package/dist/lib/utils.js CHANGED
@@ -1 +1 @@
1
- import{getLogger as t}from"./expressFormatedResponses.js";const r=t(),launchHooks=async(t,...e)=>{if(t)for(const n of t)if(n)try{await n(...e)}catch(t){r.error("Hook error",t)}},launchHooksChained=async(t,r)=>{if(t)for(const e of t)e&&(r=await e(r));return r},transformSchemas=(t,r)=>t.toString().replace(/__(\S+)__/g,(t,e)=>{if(!e||"string"!=typeof e)return t;const n=e.trim().toLowerCase();return r[n]?"object"!=typeof r[n]?r[n]:JSON.stringify(r[n]):t});function parseDn(t){const r=[];let e="",n=!1;for(let o=0;o<t.length;o++){const s=t[o];n?(e+=s,n=!1):"\\"===s?(e+=s,n=!0):","===s?(r.push(e.trim()),e=""):e+=s}return e&&r.push(e.trim()),r}function getParentDn(t){const r=parseDn(t);return r.length<=1?t:r.slice(1).join(",")}export{getParentDn,launchHooks,launchHooksChained,parseDn,transformSchemas};
1
+ import{getLogger as t}from"./expressFormatedResponses.js";const e=t(),launchHooks=async(t,...n)=>{if(t)for(const r of t)if(r)try{await r(...n)}catch(t){e.error("Hook error",t)}},launchHooksChained=async(t,e)=>{if(t)for(const n of t)n&&(e=await n(e));return e},transformSchemas=(t,e)=>t.toString().replace(/__(\S+)__/g,(t,n)=>{if(!n||"string"!=typeof n)return t;const r=n.trim().toLowerCase();return e[r]?"object"!=typeof e[r]?e[r]:JSON.stringify(e[r]):t});function parseDn(t){const e=[];let n="",r=!1;for(let o=0;o<t.length;o++){const s=t[o];r?(n+=s,r=!1):"\\"===s?(n+=s,r=!0):","===s?(e.push(n.trim()),n=""):n+=s}return n&&e.push(n.trim()),e}function getParentDn(t){const e=parseDn(t);return e.length<=1?t:e.slice(1).join(",")}function getRdn(t){return parseDn(t)[0]||""}function isChildOf(t,e){const n=t.toLowerCase(),r=e.toLowerCase();if(!n.endsWith(r))return!1;if(n.length===r.length)return!1;return n.substring(0,n.length-r.length).endsWith(",")}const asyncHandler=t=>(e,n,r)=>{Promise.resolve(t(e,n,r)).catch(r)};export{asyncHandler,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas};
@@ -1 +1 @@
1
- import e from"winston";const buildLogger=r=>e.createLogger({level:r.log_level,format:e.format.combine(e.format.timestamp(),e.format.printf(({timestamp:e,level:r,message:o})=>`${e} [${r}]: ${o}`)),transports:["console"===r.logger?new e.transports.Console({stderrLevels:["error","warn"],format:e.format.json()}):new e.transports.File({filename:"error.log"})]});export{buildLogger};
1
+ import e from"winston";const r={levels:{error:0,warn:1,notice:2,info:3,debug:4}},buildLogger=o=>e.createLogger({levels:r.levels,level:o.log_level,format:e.format.combine(e.format.timestamp(),e.format.printf(({timestamp:e,level:r,message:o})=>`${e} [${r}]: ${o}`)),transports:["console"===o.logger?new e.transports.Console({stderrLevels:["error","warn"],format:e.format.json()}):new e.transports.File({filename:"error.log"})]});export{buildLogger};
@@ -0,0 +1 @@
1
+ import{createHmac as e,createHash as t,timingSafeEqual as i}from"crypto";import{unauthorized as r}from"../../lib/expressFormatedResponses.js";import s from"../../lib/auth/base.js";class AuthHmac extends s{constructor(...e){super(...e),this.name="authHmac",this.roles=["auth"],this.services=new Map;const t=this.config.auth_hmac;this.timeWindow=this.config.auth_hmac_window??12e4,t&&Array.isArray(t)&&t.forEach((e,t)=>{const i=e.split(":");if(i.length>=3){const e=i[0].trim(),r=i[1].trim(),s=i.slice(2).join(":").trim();if(!e||!r||!s)return void this.logger.warn(`Invalid HMAC config at index ${t}: missing id, secret, or name`);r.length<32&&this.logger.warn(`HMAC secret for service "${e}" is too short (minimum 32 characters recommended)`),this.services.set(e,{id:e,secret:r,name:s})}else this.logger.warn(`Invalid HMAC config format at index ${t}: expected "service-id:secret:name"`)}),0===this.services.size?this.logger.warn("No valid HMAC services configured"):(this.logger.info(`HMAC authentication initialized with ${this.services.size} service(s): ${Array.from(this.services.keys()).join(", ")}`),this.logger.info(`Time window for replay protection: ${this.timeWindow}ms`))}authMethod(e,t,i){const s=e.headers.authorization;if(!s||!s.startsWith("HMAC-SHA256 "))return this.logger.warn("Missing or invalid Authorization header (expected HMAC-SHA256)"),r(t);const n=s.substring(12).split(":");if(3!==n.length)return this.logger.warn("Invalid HMAC authorization format (expected service-id:timestamp:signature)"),r(t);const[o,a,c]=n,h=this.services.get(o);if(!h)return this.logger.warn(`Unknown service ID: ${o}`),r(t);const g=parseInt(a,10);if(isNaN(g)||g<=0)return this.logger.warn(`Invalid timestamp: ${a}`),r(t);const u=Date.now(),m=Math.abs(u-g);if(m>this.timeWindow)return this.logger.warn(`Timestamp outside allowed window: ${m}ms (max: ${this.timeWindow}ms) for service ${o}`),r(t);const d=this.calculateBodyHash(e),l=`${e.method.toUpperCase()}|${e.originalUrl||e.url}|${g}|${d}`,f=this.calculateHmac(h.secret,l);if(!this.secureCompare(c,f))return this.logger.warn(`Invalid signature for service ${o} (${h.name}). Expected: ${f.substring(0,8)}..., Got: ${c.substring(0,8)}...`),this.logger.debug(`Signing string: ${l}`),r(t);this.logger.debug(`HMAC authentication successful for service: ${o} (${h.name})`),e.user=h.name,i()}calculateHmac(t,i){const r=e("sha256",t);return r.update(i),r.digest("hex")}calculateBodyHash(e){const i=e.method.toUpperCase();if("GET"===i||"DELETE"===i||"HEAD"===i)return"";if(e.body){const i="string"==typeof e.body?e.body:JSON.stringify(e.body),r=t("sha256");return r.update(i),r.digest("hex")}return""}secureCompare(e,t){if(e.length!==t.length)return!1;try{const r=Buffer.from(e,"utf8"),s=Buffer.from(t,"utf8");return i(r,s)}catch{return!1}}}export{AuthHmac as default};
@@ -0,0 +1 @@
1
+ import{createHmac as t}from"crypto";import{unauthorized as e}from"../../lib/expressFormatedResponses.js";import s from"../../lib/auth/base.js";class AuthTotp extends s{constructor(...t){super(...t),this.name="authTotp",this.roles=["auth"],this.totpUsers=[];const e=this.config.auth_totp;this.window=this.config.auth_totp_window??1,this.step=this.config.auth_totp_step??30,e&&Array.isArray(e)&&e.forEach((t,e)=>{const s=t.split(":");if(s.length>=2){const t=s[0].trim(),r=s[1].trim(),i=s[2]?parseInt(s[2].trim(),10):6;if(!this.isValidBase32(t))return void this.logger.warn(`Invalid Base32 secret for TOTP user "${r}" (index ${e})`);if(i<6||i>10)return void this.logger.warn(`Invalid digits count for TOTP user "${r}": ${i} (must be 6-10)`);this.totpUsers.push({name:r,secret:t,digits:i})}else this.logger.warn(`Invalid TOTP config format at index ${e}: expected "secret:name[:digits]"`)}),0===this.totpUsers.length?this.logger.warn("No valid TOTP users configured"):this.logger.info(`TOTP authentication initialized with ${this.totpUsers.length} user(s)`)}authMethod(t,s,r){const i=t.headers.authorization;if(!i||!/^Bearer .+/.test(i))return this.logger.warn("Missing or invalid Authorization header"),e(s);const o=i.split(" ")[1];for(const e of this.totpUsers)if(this.verifyTotp(e.secret,o,e.digits))return this.logger.debug(`TOTP authentication successful for user: ${e.name}`),t.user=e.name,r();return this.logger.warn(`Unauthorized TOTP token: ${o}`),e(s)}verifyTotp(t,e,s){const r=Math.floor(Date.now()/1e3);for(let i=-this.window;i<=this.window;i++){const o=Math.floor(r/this.step)+i;if(this.generateTotp(t,o,s)===e)return!0}return!1}generateTotp(e,s,r){const i=this.base32Decode(e),o=Buffer.alloc(8);o.writeBigUInt64BE(BigInt(s));const n=t("sha1",i);n.update(o);const a=n.digest(),h=15&a[a.length-1];return(((127&a[h])<<24|(255&a[h+1])<<16|(255&a[h+2])<<8|255&a[h+3])%Math.pow(10,r)).toString().padStart(r,"0")}base32Decode(t){let e=t.toUpperCase();for(;e.endsWith("=");)e=e.slice(0,-1);let s=0,r=0;const i=[];for(let t=0;t<e.length;t++){const o="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(e[t]);if(-1===o)throw new Error(`Invalid Base32 character: ${e[t]}`);r=r<<5|o,s+=5,s>=8&&(i.push(r>>>s-8&255),s-=8)}return Buffer.from(i)}isValidBase32(t){return/^[A-Z2-7]+=*$/.test(t.toUpperCase())}}export{AuthTotp as default};
@@ -1 +1 @@
1
- import o from"../../abstract/plugin.js";class HelloWorld extends o{constructor(){super(...arguments),this.name="hello",this.roles=["demo"]}api(o){o.get(`${this.config.api_prefix}/hello`,async(o,s)=>{const e={message:"Hello",hookResults:[]};if(this.server.hooks&&this.server.hooks.hello)for(const o of this.server.hooks.hello)e.hookResults.push(await o());s.json(e)})}}export{HelloWorld as default};
1
+ import o from"../../abstract/plugin.js";import{asyncHandler as s}from"../../lib/utils.js";class HelloWorld extends o{constructor(){super(...arguments),this.name="hello",this.roles=["demo"]}api(o){o.get(`${this.config.api_prefix}/hello`,s(async(o,s)=>{const e={message:"Hello",hookResults:[]};if(this.server.hooks&&this.server.hooks.hello)for(const o of this.server.hooks.hello)e.hookResults.push(await o());s.json(e)}))}}export{HelloWorld as default};
@@ -1 +1 @@
1
- import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,badRequest as i,ok as s,serverError as a,jsonBody as o,tryMethod as n,notFound as d}from"../../lib/expressFormatedResponses.js";import{transformSchemas as c,launchHooksChained as h,launchHooks as p}from"../../lib/utils.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async e=>{let t=e;return Array.isArray(t)||(t=[t]),this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)}),e}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(c(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,async(e,t)=>{if(r(e,t))try{const r={};if(e.query.match&&("string"!=typeof e.query.match||!/^[\w*=()&|]+$/.test(e.query.match)))return i(t,"Invalid match query");e.query.match&&(r.filter=/=/.test(e.query.match)?`${e.query.match}`:`(${this.cn}=${e.query.match})`),e.query.attributes&&"string"==typeof e.query.attributes&&(r.attributes=e.query.attributes.split(","));const a=await this.listGroups(r);return s(t,a)}catch(e){return a(t,e)}}),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiGet(e,t)),e.post(`${this.config.api_prefix}/v1/ldap/groups`,async(e,t)=>this.apiAdd(e,t,this.cn)),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiDelete(e,t)),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiModify(e,t)),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)return i(t,"cn is required");const s=o(e,t,"member");s&&await n(t,this.addMember.bind(this),r,s.member)}),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!s||!a)return i(t,"cn and member are required");await n(t,this.deleteMember.bind(this),s,a)}),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");const a=o(e,t,"targetOrgDn");a&&await n(t,this.moveGroup.bind(this),s,a.targetOrgDn,e)}),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");const a=o(e,t,"newCn");a&&await n(t,this.renameGroup.bind(this),s,a.newCn)})}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${i},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)return d(t,"Group not found");t.json(r.searchEntries[0])}catch(e){return e.code&&32===e.code?d(t,"Group not found"):a(t,e)}}async apiAdd(e,t,...r){const i=o(e,t,...r);if(!i)return;const s=i[this.cn],a=i.member?i.member:[],d=Object.fromEntries(Object.entries(i).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await n(t,this.addGroup.bind(this),s,a,d)}async apiDelete(e,t){if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");await n(t,this.deleteGroup.bind(this),s)}async apiModify(e,t){const r=o(e,t);if(!r)return;const s=this.fixDn(decodeURIComponent(e.params.cn));if(!s)return i(t);const a=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await n(t,this.modifyGroup.bind(this),s,a)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=([^,]+).*`),"$1")):i=`${this.cn}=${e},${this.base}`,await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});let s,a={objectClass:this.config.group_class,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&a.member.push(...t),[i,a]=await h(this.registeredHooks.ldapgroupadd,[i,a]);try{s=await this.ldap.add(i,a)}catch(e){throw new Error(`Failed to add group ${i}: ${e}`)}return p(this.registeredHooks.ldapgroupadddone,[i,a]),s}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;const i=this.opNumber();if([r,t]=await h(this.registeredHooks.ldapgroupmodify,[r,t,i]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(r,t);const s=await this.ldap.modify(r,t);return p(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${t},${this.base}`;[r,i]=await h(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return p(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;t=await h(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return p(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await h(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;if([e,t]=await h(this.registeredHooks.ldapgroupdeletemember,[e,t]),t===this.config.group_dummy_user)throw new Error("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${e}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r))))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${e},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let d;try{d=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===d.searchEntries.length)throw new Error(`Target organization ${t} not found`);const c=d.searchEntries[0][s];if(!c)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:c}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)e.searchEntries.map(e=>{const t=e[this.cn];t&&(i[t]=e),Array.isArray(i[t].member)||(i[t].member=[i[t].member]),i[t].member=i[t].member.filter(e=>e!==this.config.group_dummy_user)});return i=await h(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=t?`(${this.cn}=*${e}*)`:`(${this.cn}=${e})`;return await this.listGroups({filter:i,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${e},${this.base}`)}async validateMembers(e,t){if([e,t]=await h(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await h(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new Error(`Invalid value for field ${e}`);for(const[e,r]of Object.entries(this.schema.attributes))if(r.required&&null==t[e])throw new Error(`Missing required field ${e}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await h(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new Error(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new Error(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new Error(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test){"string"==typeof r.items.test&&(r.items.test=new RegExp(r.items.test));for(let i of t){if(typeof i!==r.items.type)throw new Error(`Field ${e} must be of type ${r.items.type}`);if("string"!=typeof i&&(i=i.toString()),r.items.test&&!r.items.test.test(i))throw new Error(`Field ${e} has invalid value ${i}`)}}}else if("pointer"===r.type){if("string"!=typeof t)throw new Error(`Field ${e} must be a string (DN pointer)`);const i=t;if(r.branch&&r.branch.length>0){if(!r.branch.some(e=>new RegExp(`,?${e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}$`,"i").test(i)))throw new Error(`Field ${e} must point to a DN within allowed branches: ${r.branch.join(", ")}`)}try{const t=await this.ldap.search({paged:!1,scope:"base"},i);if(!t||!t.searchEntries||0===t.searchEntries.length)throw new Error(`Field ${e} points to non-existent DN: ${i}`)}catch(t){throw new Error(`Field ${e} points to invalid or non-existent DN: ${i}: ${t}`)}if(r.test&&("string"==typeof r.test&&(r.test=new RegExp(r.test)),r.test&&!r.test.test(i)))throw new Error(`Field ${e} has invalid value ${i}`)}else{if(typeof t!==r.type)return!1;if("string"!=typeof t&&(t=t.toString()),r.test&&("string"==typeof r.test&&(r.test=new RegExp(r.test)),r.test&&!r.test.test(t)))throw new Error(`Field ${e} has invalid value ${t}`)}return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
1
+ import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,badRequest as i,ok as s,serverError as a,jsonBody as o,tryMethod as n,notFound as d}from"../../lib/expressFormatedResponses.js";import{transformSchemas as c,launchHooksChained as h,launchHooks as p}from"../../lib/utils.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async e=>{let t=e;return Array.isArray(t)||(t=[t]),this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)}),e}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(c(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,async(e,t)=>{if(r(e,t))try{const r={};if(e.query.match&&("string"!=typeof e.query.match||!/^[\w*=()&|]+$/.test(e.query.match)))return i(t,"Invalid match query");e.query.match&&(r.filter=/=/.test(e.query.match)?`${e.query.match}`:`(${this.cn}=${e.query.match})`),e.query.attributes&&"string"==typeof e.query.attributes&&(r.attributes=e.query.attributes.split(","));const a=await this.listGroups(r);return s(t,a)}catch(e){return a(t,e)}}),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiGet(e,t)),e.post(`${this.config.api_prefix}/v1/ldap/groups`,async(e,t)=>this.apiAdd(e,t,this.cn)),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiDelete(e,t)),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiModify(e,t)),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)return i(t,"cn is required");const s=o(e,t,"member");s&&await n(t,this.addMember.bind(this),r,s.member)}),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!s||!a)return i(t,"cn and member are required");await n(t,this.deleteMember.bind(this),s,a)}),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");const a=o(e,t,"targetOrgDn");a&&await n(t,this.moveGroup.bind(this),s,a.targetOrgDn,e)}),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");const a=o(e,t,"newCn");a&&await n(t,this.renameGroup.bind(this),s,a.newCn)})}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${i},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)return d(t,"Group not found");t.json(r.searchEntries[0])}catch(e){return e.code&&32===e.code?d(t,"Group not found"):a(t,e)}}async apiAdd(e,t,...r){const i=o(e,t,...r);if(!i)return;const s=i[this.cn],a=i.member?i.member:[],d=Object.fromEntries(Object.entries(i).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await n(t,this.addGroup.bind(this),s,a,d)}async apiDelete(e,t){if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");await n(t,this.deleteGroup.bind(this),s)}async apiModify(e,t){const r=o(e,t);if(!r)return;const s=this.fixDn(decodeURIComponent(e.params.cn));if(!s)return i(t);const a=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await n(t,this.modifyGroup.bind(this),s,a)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=([^,]+).*`),"$1")):i=`${this.cn}=${e},${this.base}`,await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});const s=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||s.includes("twakeGroup")||s.includes("twakeStaticGroup")||s.push("twakeGroup");let a,o={objectClass:s,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&o.member.push(...t),[i,o]=await h(this.registeredHooks.ldapgroupadd,[i,o]);try{a=await this.ldap.add(i,o)}catch(e){throw new Error(`Failed to add group ${i}: ${e}`)}return p(this.registeredHooks.ldapgroupadddone,[i,o]),a}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;const i=this.opNumber();if([r,t]=await h(this.registeredHooks.ldapgroupmodify,[r,t,i]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(r,t);const s=await this.ldap.modify(r,t);return p(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${t},${this.base}`;[r,i]=await h(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return p(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;t=await h(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return p(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await h(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;if([e,t]=await h(this.registeredHooks.ldapgroupdeletemember,[e,t]),t===this.config.group_dummy_user)throw new Error("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${e}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r))))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${e},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let d;try{d=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===d.searchEntries.length)throw new Error(`Target organization ${t} not found`);const c=d.searchEntries[0][s];if(!c)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:c}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)e.searchEntries.map(e=>{const t=e[this.cn];t&&(i[t]=e),Array.isArray(i[t].member)||(i[t].member=[i[t].member]),i[t].member=i[t].member.filter(e=>e!==this.config.group_dummy_user)});return i=await h(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=t?`(${this.cn}=*${e}*)`:`(${this.cn}=${e})`;return await this.listGroups({filter:i,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${e},${this.base}`)}async validateMembers(e,t){if([e,t]=await h(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await h(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new Error(`Invalid value for field ${e}`);for(const[e,r]of Object.entries(this.schema.attributes))if(r.required&&null==t[e])throw new Error(`Missing required field ${e}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await h(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new Error(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new Error(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new Error(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test){"string"==typeof r.items.test&&(r.items.test=new RegExp(r.items.test));for(let i of t){if(typeof i!==r.items.type)throw new Error(`Field ${e} must be of type ${r.items.type}`);if("string"!=typeof i&&(i=i.toString()),r.items.test&&!r.items.test.test(i))throw new Error(`Field ${e} has invalid value ${i}`)}}}else if("pointer"===r.type){if("string"!=typeof t)throw new Error(`Field ${e} must be a string (DN pointer)`);const i=t;if(r.branch&&r.branch.length>0){if(!r.branch.some(e=>new RegExp(`,?${e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}$`,"i").test(i)))throw new Error(`Field ${e} must point to a DN within allowed branches: ${r.branch.join(", ")}`)}try{const t=await this.ldap.search({paged:!1,scope:"base"},i);if(!t||!t.searchEntries||0===t.searchEntries.length)throw new Error(`Field ${e} points to non-existent DN: ${i}`)}catch(t){throw new Error(`Field ${e} points to invalid or non-existent DN: ${i}: ${t}`)}if(r.test&&("string"==typeof r.test&&(r.test=new RegExp(r.test)),r.test&&!r.test.test(i)))throw new Error(`Field ${e} has invalid value ${i}`)}else{if(typeof t!==r.type)return!1;if("string"!=typeof t&&(t=t.toString()),r.test&&("string"==typeof r.test&&(r.test=new RegExp(r.test)),r.test&&!r.test.test(t)))throw new Error(`Field ${e} has invalid value ${t}`)}return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};