ldap-rest 0.4.3 → 0.4.5

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.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Sync utility to verify and fix consistency between LDAP and James
4
- * Ensures James quotas match LDAP mailQuota values
4
+ * Ensures James quotas match LDAP mailQuota values and that James aliases
5
+ * match the LDAP alias attribute (catch-up in case the event-based sync failed)
5
6
  * @author Generated with Claude Code
6
7
  */
7
8
 
@@ -12,18 +13,27 @@ import { DM } from '../dist/bin/index.js';
12
13
  const args = process.argv.slice(2);
13
14
  const quiet = args.includes('--quiet') || args.includes('-q');
14
15
  const dryRun = args.includes('--dry-run') || args.includes('-n');
16
+ const noAliasDelete = args.includes('--no-alias-delete');
15
17
 
16
18
  if (args.includes('--help') || args.includes('-h')) {
17
19
  console.log(`
18
20
  Usage: sync-james [options]
19
21
 
20
- Synchronizes James quotas with LDAP mailQuota values.
21
- LDAP is considered the source of truth.
22
+ Synchronizes James with LDAP. LDAP is considered the source of truth.
23
+
24
+ For every user it reconciles:
25
+ - the mailbox quota (James quota <- LDAP mailQuota)
26
+ - the mail aliases (James aliases <- LDAP alias attribute)
27
+
28
+ The alias reconciliation is a catch-up mechanism: it repairs aliases that
29
+ were not propagated to James (e.g. when the event-based sync failed).
22
30
 
23
31
  Options:
24
- --quiet, -q Only show summary and errors
25
- --dry-run, -n Show what would be changed without making changes
26
- --help, -h Show this help message
32
+ --quiet, -q Only show summary and errors
33
+ --dry-run, -n Show what would be changed without making changes
34
+ --no-alias-delete Do not remove James aliases that are absent from LDAP
35
+ (only add missing ones)
36
+ --help, -h Show this help message
27
37
 
28
38
  Environment variables:
29
39
  DM_JAMES_WEBADMIN_URL James WebAdmin URL (required)
@@ -31,16 +41,45 @@ Environment variables:
31
41
  DM_LDAP_BASE LDAP search base
32
42
  DM_MAIL_ATTRIBUTE Mail attribute name (default: mail)
33
43
  DM_QUOTA_ATTRIBUTE Quota attribute name (default: mailQuota)
44
+ DM_ALIAS_ATTRIBUTE Alias attribute name (default: mailAlternateAddress)
34
45
  `);
35
46
  process.exit(0);
36
47
  }
37
48
 
38
- async function syncJamesQuotas() {
49
+ /**
50
+ * Normalize an email alias - handle AD format (smtp:alias@domain.com).
51
+ * Mirrors the behaviour of TwakePlugin.normalizeAlias in the main codebase.
52
+ * @param {string} alias
53
+ * @returns {string}
54
+ */
55
+ function normalizeAlias(alias) {
56
+ if (alias.toLowerCase().startsWith('smtp:')) {
57
+ return alias.substring(5);
58
+ }
59
+ return alias;
60
+ }
61
+
62
+ /**
63
+ * Extract normalized aliases from an LDAP attribute value.
64
+ * @param {string|string[]|Buffer|Buffer[]|undefined} value
65
+ * @returns {string[]}
66
+ */
67
+ function getAliases(value) {
68
+ if (!value) return [];
69
+ const aliases = Array.isArray(value) ? value : [value];
70
+ return aliases
71
+ .map(a => (Buffer.isBuffer(a) ? a.toString('utf-8') : String(a)))
72
+ .map(a => normalizeAlias(a))
73
+ .filter(a => a.length > 0);
74
+ }
75
+
76
+ async function syncJames() {
39
77
  const dm = new DM();
40
78
  await dm.ready;
41
79
 
42
80
  const mailAttr = dm.config.mail_attribute || 'mail';
43
81
  const quotaAttr = dm.config.quota_attribute || 'mailQuota';
82
+ const aliasAttr = dm.config.alias_attribute || 'mailAlternateAddress';
44
83
  const jamesUrl = dm.config.james_webadmin_url;
45
84
  const jamesToken = dm.config.james_webadmin_token;
46
85
 
@@ -49,8 +88,13 @@ async function syncJamesQuotas() {
49
88
  process.exit(1);
50
89
  }
51
90
 
91
+ const headers = {};
92
+ if (jamesToken) {
93
+ headers.Authorization = `Bearer ${jamesToken}`;
94
+ }
95
+
52
96
  if (!quiet) {
53
- dm.logger.info('Starting LDAP to James quota synchronization...');
97
+ dm.logger.info('Starting LDAP to James synchronization...');
54
98
  dm.logger.info(`LDAP base: ${dm.config.ldap_base}`);
55
99
  dm.logger.info(`James URL: ${jamesUrl}`);
56
100
  if (dryRun) {
@@ -59,18 +103,196 @@ async function syncJamesQuotas() {
59
103
  dm.logger.info('');
60
104
  }
61
105
 
106
+ const stats = {
107
+ checked: 0,
108
+ quotaSynced: 0,
109
+ aliasesAdded: 0,
110
+ aliasesDeleted: 0,
111
+ errors: 0,
112
+ };
113
+
114
+ /**
115
+ * Reconcile the mailbox quota for a single user.
116
+ */
117
+ async function syncQuota(dn, mail, ldapQuota) {
118
+ if (isNaN(ldapQuota)) return;
119
+
120
+ const url = `${jamesUrl}/quota/users/${mail}/size`;
121
+ const getRes = await fetch(url, { method: 'GET', headers });
122
+
123
+ if (!getRes.ok) {
124
+ if (getRes.status === 404) {
125
+ dm.logger.warn(`User ${mail} not found in James, skipping quota (DN: ${dn})`);
126
+ } else {
127
+ dm.logger.error(
128
+ `Error getting quota for ${mail}: ${getRes.status} ${getRes.statusText}`
129
+ );
130
+ stats.errors++;
131
+ }
132
+ return;
133
+ }
134
+
135
+ const jamesQuota = Number(await getRes.text());
136
+
137
+ if (jamesQuota === ldapQuota) {
138
+ if (!quiet) {
139
+ dm.logger.info(`${mail}: quota OK (${ldapQuota})`);
140
+ }
141
+ return;
142
+ }
143
+
144
+ dm.logger.warn(
145
+ `${mail}: quota mismatch - LDAP: ${ldapQuota}, James: ${jamesQuota}`
146
+ );
147
+ if (dryRun) {
148
+ dm.logger.info(` Would update James quota to ${ldapQuota}`);
149
+ stats.quotaSynced++;
150
+ return;
151
+ }
152
+
153
+ if (!quiet) {
154
+ dm.logger.info(` Updating James quota to ${ldapQuota}...`);
155
+ }
156
+ const putRes = await fetch(url, {
157
+ method: 'PUT',
158
+ headers,
159
+ body: ldapQuota.toString(),
160
+ });
161
+ if (putRes.ok) {
162
+ if (!quiet) {
163
+ dm.logger.info(` Updated successfully`);
164
+ }
165
+ stats.quotaSynced++;
166
+ } else {
167
+ dm.logger.error(` Failed to update quota: ${putRes.status} ${putRes.statusText}`);
168
+ stats.errors++;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Fetch the aliases currently declared in James for a destination mail.
174
+ * @returns {Promise<string[]|null>} list of source addresses, or null on error
175
+ */
176
+ async function fetchJamesAliases(mail) {
177
+ const res = await fetch(`${jamesUrl}/address/aliases/${mail}`, {
178
+ method: 'GET',
179
+ headers,
180
+ });
181
+
182
+ // James returns 404 when the destination has no alias at all
183
+ if (res.status === 404) {
184
+ return [];
185
+ }
186
+ if (!res.ok) {
187
+ dm.logger.error(
188
+ `Error getting aliases for ${mail}: ${res.status} ${res.statusText}`
189
+ );
190
+ return null;
191
+ }
192
+
193
+ const body = await res.json();
194
+ if (!Array.isArray(body)) {
195
+ return [];
196
+ }
197
+ // James returns [{ source: 'alias@domain' }, ...]
198
+ return body
199
+ .map(e => (e && typeof e.source === 'string' ? e.source : null))
200
+ .filter(Boolean);
201
+ }
202
+
203
+ /**
204
+ * Reconcile the aliases for a single user (catch-up).
205
+ * LDAP is the source of truth: missing aliases are added, extra aliases in
206
+ * James are removed (unless --no-alias-delete is set).
207
+ */
208
+ async function syncAliases(dn, mail, ldapAliasesRaw) {
209
+ const ldapAliases = getAliases(ldapAliasesRaw);
210
+
211
+ const jamesAliases = await fetchJamesAliases(mail);
212
+ if (jamesAliases === null) {
213
+ stats.errors++;
214
+ return;
215
+ }
216
+
217
+ // Case-insensitive comparison to avoid spurious add/delete churn
218
+ const ldapSet = new Set(ldapAliases.map(a => a.toLowerCase()));
219
+ const jamesSet = new Set(jamesAliases.map(a => a.toLowerCase()));
220
+
221
+ const toAdd = ldapAliases.filter(a => !jamesSet.has(a.toLowerCase()));
222
+ const toDelete = noAliasDelete
223
+ ? []
224
+ : jamesAliases.filter(a => !ldapSet.has(a.toLowerCase()));
225
+
226
+ if (toAdd.length === 0 && toDelete.length === 0) {
227
+ if (!quiet && (ldapAliases.length > 0 || jamesAliases.length > 0)) {
228
+ dm.logger.info(`${mail}: aliases OK (${ldapAliases.length})`);
229
+ }
230
+ return;
231
+ }
232
+
233
+ for (const alias of toAdd) {
234
+ dm.logger.warn(`${mail}: missing alias in James - ${alias}`);
235
+ if (dryRun) {
236
+ dm.logger.info(` Would add alias ${alias}`);
237
+ stats.aliasesAdded++;
238
+ continue;
239
+ }
240
+ const res = await fetch(
241
+ `${jamesUrl}/address/aliases/${mail}/sources/${alias}`,
242
+ { method: 'PUT', headers }
243
+ );
244
+ // 409 = alias already exists, treat as success (mirrors plugin behaviour)
245
+ if (res.ok || res.status === 409) {
246
+ if (!quiet) {
247
+ dm.logger.info(` Added alias ${alias}`);
248
+ }
249
+ stats.aliasesAdded++;
250
+ } else {
251
+ dm.logger.error(
252
+ ` Failed to add alias ${alias}: ${res.status} ${res.statusText}`
253
+ );
254
+ stats.errors++;
255
+ }
256
+ }
257
+
258
+ for (const alias of toDelete) {
259
+ dm.logger.warn(`${mail}: stale alias in James - ${alias}`);
260
+ if (dryRun) {
261
+ dm.logger.info(` Would delete alias ${alias}`);
262
+ stats.aliasesDeleted++;
263
+ continue;
264
+ }
265
+ const res = await fetch(
266
+ `${jamesUrl}/address/aliases/${mail}/sources/${alias}`,
267
+ { method: 'DELETE', headers }
268
+ );
269
+ if (res.ok) {
270
+ if (!quiet) {
271
+ dm.logger.info(` Deleted alias ${alias}`);
272
+ }
273
+ stats.aliasesDeleted++;
274
+ } else {
275
+ dm.logger.error(
276
+ ` Failed to delete alias ${alias}: ${res.status} ${res.statusText}`
277
+ );
278
+ stats.errors++;
279
+ }
280
+ }
281
+ }
282
+
62
283
  try {
63
- // Search for all users with mail and quota attributes (paginated for large directories)
284
+ // Search for every user that has a mail (paginated for large directories).
285
+ // We enumerate all mailboxes, not only those carrying a quota/alias, so
286
+ // that a user whose alias attribute was cleared still gets its stale James
287
+ // aliases removed (LDAP is the source of truth). Destinations whose mail is
288
+ // absent from LDAP are intentionally left alone: James refuses delivery for
289
+ // an alias pointing to a non-existent mailbox, so they are harmless.
64
290
  const resultGenerator = await dm.ldap.search({
65
291
  paged: true,
66
- filter: `(&(${mailAttr}=*)(${quotaAttr}=*))`,
67
- attributes: [mailAttr, quotaAttr, 'dn'],
292
+ filter: `(${mailAttr}=*)`,
293
+ attributes: [mailAttr, quotaAttr, aliasAttr, 'dn'],
68
294
  });
69
295
 
70
- let checked = 0;
71
- let synced = 0;
72
- let errors = 0;
73
-
74
296
  // Process results page by page
75
297
  for await (const result of resultGenerator) {
76
298
  if (!result.searchEntries || result.searchEntries.length === 0) {
@@ -88,92 +310,46 @@ async function syncJamesQuotas() {
88
310
  const mail = Array.isArray(entry[mailAttr])
89
311
  ? entry[mailAttr][0]
90
312
  : entry[mailAttr];
91
- const ldapQuota = Array.isArray(entry[quotaAttr])
92
- ? Number(entry[quotaAttr][0])
93
- : Number(entry[quotaAttr]);
94
313
 
95
- if (!mail || isNaN(ldapQuota)) {
96
- dm.logger.warn(`Skipping ${dn}: invalid mail or quota`);
314
+ if (!mail) {
315
+ dm.logger.warn(`Skipping ${dn}: invalid mail`);
97
316
  continue;
98
317
  }
99
318
 
100
- checked++;
319
+ stats.checked++;
101
320
 
102
321
  try {
103
- // Get James quota
104
- const getUrl = `${jamesUrl}/quota/users/${mail}/size`;
105
- const headers = {};
106
- if (jamesToken) {
107
- headers.Authorization = `Bearer ${jamesToken}`;
108
- }
109
-
110
- const getRes = await fetch(getUrl, { method: 'GET', headers });
111
-
112
- if (!getRes.ok) {
113
- if (getRes.status === 404) {
114
- dm.logger.warn(
115
- `User ${mail} not found in James, skipping (DN: ${dn})`
116
- );
117
- } else {
118
- dm.logger.error(
119
- `Error getting quota for ${mail}: ${getRes.status} ${getRes.statusText}`
120
- );
121
- errors++;
122
- }
123
- continue;
322
+ // Reconcile quota only when the user has a quota attribute
323
+ if (entry[quotaAttr] !== undefined) {
324
+ const ldapQuota = Array.isArray(entry[quotaAttr])
325
+ ? Number(entry[quotaAttr][0])
326
+ : Number(entry[quotaAttr]);
327
+ await syncQuota(dn, mail, ldapQuota);
124
328
  }
125
329
 
126
- const jamesQuota = Number(await getRes.text());
127
-
128
- if (jamesQuota === ldapQuota) {
129
- if (!quiet) {
130
- dm.logger.info(`${mail}: quota OK (${ldapQuota})`);
131
- }
132
- } else {
133
- dm.logger.warn(
134
- `${mail}: quota mismatch - LDAP: ${ldapQuota}, James: ${jamesQuota}`
135
- );
136
- if (dryRun) {
137
- dm.logger.info(` Would update James quota to ${ldapQuota}`);
138
- synced++;
139
- } else {
140
- if (!quiet) {
141
- dm.logger.info(` Updating James quota to ${ldapQuota}...`);
142
- }
143
-
144
- // Update James quota
145
- const putUrl = `${jamesUrl}/quota/users/${mail}/size`;
146
- const putRes = await fetch(putUrl, {
147
- method: 'PUT',
148
- headers,
149
- body: ldapQuota.toString(),
150
- });
151
-
152
- if (putRes.ok) {
153
- if (!quiet) {
154
- dm.logger.info(` Updated successfully`);
155
- }
156
- synced++;
157
- } else {
158
- dm.logger.error(
159
- ` Failed to update: ${putRes.status} ${putRes.statusText}`
160
- );
161
- errors++;
162
- }
163
- }
164
- }
330
+ // Reconcile aliases (catch-up). Always run so that stale aliases
331
+ // are removed even when the LDAP attribute has been cleared.
332
+ await syncAliases(dn, mail, entry[aliasAttr]);
165
333
  } catch (err) {
166
334
  dm.logger.error(`Error processing ${mail}: ${err.message}`);
167
- errors++;
335
+ stats.errors++;
168
336
  }
169
337
  }
170
338
  }
171
339
 
172
340
  dm.logger.info('\n' + '='.repeat(60));
173
341
  dm.logger.info('Synchronization summary:');
174
- dm.logger.info(` Users checked: ${checked}`);
175
- dm.logger.info(` Quotas ${dryRun ? 'needing sync' : 'synced'}: ${synced}`);
176
- dm.logger.info(` Errors: ${errors}`);
342
+ dm.logger.info(` Users checked: ${stats.checked}`);
343
+ dm.logger.info(
344
+ ` Quotas ${dryRun ? 'needing sync' : 'synced'}: ${stats.quotaSynced}`
345
+ );
346
+ dm.logger.info(
347
+ ` Aliases ${dryRun ? 'to add' : 'added'}: ${stats.aliasesAdded}`
348
+ );
349
+ dm.logger.info(
350
+ ` Aliases ${dryRun ? 'to delete' : 'deleted'}: ${stats.aliasesDeleted}`
351
+ );
352
+ dm.logger.info(` Errors: ${stats.errors}`);
177
353
  dm.logger.info('='.repeat(60));
178
354
  } catch (err) {
179
355
  dm.logger.error('Error during synchronization:', err);
@@ -184,7 +360,7 @@ async function syncJamesQuotas() {
184
360
  }
185
361
 
186
362
  // Run the sync
187
- syncJamesQuotas().catch(err => {
363
+ syncJames().catch(err => {
188
364
  console.error('Fatal error:', err);
189
365
  process.exit(1);
190
366
  });
@@ -1 +1 @@
1
- import{fileURLToPath as _}from"url";import{join as a,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",a(e(_(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"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--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",a(e(_(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",a(e(_(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"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--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",""],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--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-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--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-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--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"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
1
+ import{fileURLToPath as _}from"url";import{join as a,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",a(e(_(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"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--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",a(e(_(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",a(e(_(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"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--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",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--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-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--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-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--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"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
@@ -1 +1 @@
1
- import e from"node-fetch";import{TwakePlugin as r}from"../../abstract/twakePlugin.js";class CalendarResources extends r{constructor(e){super(e,"calendar_webadmin_url","calendar_webadmin_token","calendar_concurrency"),this.name="calendarResources",this.roles=["consistency"],this.hooks={ldapcalendarResourceadddone:async e=>{const[r,s]=e;if(!this.isResource(r,s))return;const t=this.buildResourceData(r,s);t?await this.callWebAdminApi("ldapcalendarResourceadddone",`${this.webadminUrl}/resources`,"POST",r,JSON.stringify(t),{resourceName:t.name}):this.logger.debug(`Skipping resource creation for ${r}: missing required fields`)},ldapcalendarResourcemodifydone:async e=>{const[r,s]=e,t=this.getResourceId(r);if(!t)return;const a={};s.replace?.cn&&(a.name=Array.isArray(s.replace.cn)?String(s.replace.cn[0]):String(s.replace.cn)),s.replace?.description&&(a.description=Array.isArray(s.replace.description)?String(s.replace.description[0]):String(s.replace.description)),0!==Object.keys(a).length?await this.callWebAdminApi("ldapcalendarResourcemodifydone",`${this.webadminUrl}/resources/${t}`,"PATCH",r,JSON.stringify(a),{resourceId:t,...a}):this.logger.debug(`No relevant changes for resource ${r}`)},ldapcalendarResourcedeletedone:async e=>{const r=this.getResourceId(e);r&&await this.callWebAdminApi("ldapcalendarResourcedeletedone",`${this.webadminUrl}/resources/${r}`,"DELETE",e,null,{resourceId:r})}},this.resourceBase=this.config.calendar_resource_base||"",this.resourceObjectClass=this.config.calendar_resource_objectclass||"",this.resourceCreator=this.config.calendar_resource_creator||"admin@example.com",this.resourceDomain=this.config.calendar_resource_domain||""}isResource(e,r){if(this.resourceBase&&!e.toLowerCase().includes(this.resourceBase.toLowerCase()))return!1;const s=r.objectClass;if(this.resourceObjectClass){return(Array.isArray(s)?s:[s]).some(e=>String(e).toLowerCase()===this.resourceObjectClass.toLowerCase())}return!0}buildResourceData(e,r){const s=r.cn,t=this.attributeToString(s);if(!t)return null;const a=this.attributeToString(r.description)||void 0,i=this.resourceCreator,o=this.resourceDomain||this.extractDomainFromDn(e),c=this.getResourceId(e)||t.toLowerCase().replace(/\s+/g,"-");return{name:t,description:a,creator:i,domain:o,id:c}}getResourceId(e){const r=e.match(/(?:cn|uid)=([^,]+)/i);return r?r[1]:null}async deleteUserData(r){const s={plugin:this.name,event:"deleteUserData",mail:r};try{const t=new URL(`${this.webadminUrl}/users/${r}`);t.searchParams.set("action","deleteData");const a=await this.requestLimit(()=>e(t.toString(),{method:"POST",headers:this.createHeaders()}));if(!a.ok)return this.logger.error({...s,http_status:a.status,http_status_text:a.statusText}),null;const i=await a.json();return this.logger.info({...s,http_status:a.status,taskId:i.taskId}),i}catch(e){return this.logger.error({...s,error:`${e}`}),null}}}export{CalendarResources as default};
1
+ import e from"node-fetch";import{TwakePlugin as t}from"../../abstract/twakePlugin.js";class CalendarResources extends t{constructor(e){super(e,"calendar_webadmin_url","calendar_webadmin_token","calendar_concurrency"),this.name="calendarResources",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={ldapcalendarResourceadddone:async e=>{const[t,s]=e;if(!this.isResource(t,s))return;const r=this.buildResourceData(t,s);r?await this.callWebAdminApi("ldapcalendarResourceadddone",`${this.webadminUrl}/resources`,"POST",t,JSON.stringify(r),{resourceName:r.name}):this.logger.debug(`Skipping resource creation for ${t}: missing required fields`)},ldapcalendarResourcemodifydone:async e=>{const[t,s]=e,r=this.getResourceId(t);if(!r)return;const a={};s.replace?.cn&&(a.name=Array.isArray(s.replace.cn)?String(s.replace.cn[0]):String(s.replace.cn)),s.replace?.description&&(a.description=Array.isArray(s.replace.description)?String(s.replace.description[0]):String(s.replace.description)),0!==Object.keys(a).length?await this.callWebAdminApi("ldapcalendarResourcemodifydone",`${this.webadminUrl}/resources/${r}`,"PATCH",t,JSON.stringify(a),{resourceId:r,...a}):this.logger.debug(`No relevant changes for resource ${t}`)},ldapcalendarResourcedeletedone:async e=>{const t=this.getResourceId(e);t&&await this.callWebAdminApi("ldapcalendarResourcedeletedone",`${this.webadminUrl}/resources/${t}`,"DELETE",e,null,{resourceId:t})},onLdapChange:async(e,t)=>{const s=t[this.mailAttr];if(s){const t=this.attributeToString(s[0]),r=this.attributeToString(s[1]);return void(t&&r&&t!==r&&await this.syncRegisteredUser("onLdapMailChange",e,t))}(t[this.firstnameAttr]||t[this.lastnameAttr])&&await this.syncRegisteredUser("onLdapDisplayNameChange",e)}},this.resourceBase=this.config.calendar_resource_base||"",this.resourceObjectClass=this.config.calendar_resource_objectclass||"",this.resourceCreator=this.config.calendar_resource_creator||"admin@example.com",this.resourceDomain=this.config.calendar_resource_domain||"",this.firstnameAttr=this.config.calendar_firstname_attribute||"givenName",this.lastnameAttr=this.config.calendar_lastname_attribute||"sn"}isResource(e,t){if(this.resourceBase&&!e.toLowerCase().includes(this.resourceBase.toLowerCase()))return!1;const s=t.objectClass;if(this.resourceObjectClass){return(Array.isArray(s)?s:[s]).some(e=>String(e).toLowerCase()===this.resourceObjectClass.toLowerCase())}return!0}buildResourceData(e,t){const s=t.cn,r=this.attributeToString(s);if(!r)return null;const a=this.attributeToString(t.description)||void 0,i=this.resourceCreator,n=this.resourceDomain||this.extractDomainFromDn(e),o=this.getResourceId(e)||r.toLowerCase().replace(/\s+/g,"-");return{name:r,description:a,creator:i,domain:n,id:o}}getResourceId(e){const t=e.match(/(?:cn|uid)=([^,]+)/i);return t?t[1]:null}async syncRegisteredUser(t,s,r){const a={plugin:this.name,event:t,result:"error",dn:s},i=await this.ldapGetAttributes(s,[this.mailAttr,this.firstnameAttr,this.lastnameAttr]);if(!i)return void this.logger.warn({...a,message:`Cannot sync registered user: entry not found for ${s}`});const n=this.attributeToString(i[this.mailAttr]);if(!n)return void this.logger.warn({...a,message:`Cannot sync registered user: no mail found for ${s}`});const o=this.attributeToString(i[this.firstnameAttr]),c=this.attributeToString(i[this.lastnameAttr]),d=r||n;try{const t=await this.requestLimit(()=>e(`${this.webadminUrl}/registeredUsers`,{method:"GET",headers:this.createHeaders()}));if(!t.ok)return void this.logger.error({...a,step:"list_registered_users",http_status:t.status,http_status_text:t.statusText});const s=(await t.json()).find(e=>e.email?.toLowerCase()===d.toLowerCase());if(!s)return void this.logger.warn({...a,step:"find_registered_user",searchEmail:d,message:"user not registered in Calendar"});const r=new URL(`${this.webadminUrl}/registeredUsers`);r.searchParams.set("id",s.id);const i={email:n};o&&(i.firstname=o),c&&(i.lastname=c);const u=await this.requestLimit(()=>e(r.toString(),{method:"PATCH",headers:this.createHeaders("application/json"),body:JSON.stringify(i)}));u.ok?this.logger.info({...a,result:"success",id:s.id,http_status:u.status,...i}):this.logger.error({...a,step:"patch_registered_user",id:s.id,http_status:u.status,http_status_text:u.statusText})}catch(e){this.logger.error({...a,error:`${e}`})}}async deleteUserData(t){const s={plugin:this.name,event:"deleteUserData",mail:t};try{const r=new URL(`${this.webadminUrl}/users/${t}`);r.searchParams.set("action","deleteData");const a=await this.requestLimit(()=>e(r.toString(),{method:"POST",headers:this.createHeaders()}));if(!a.ok)return this.logger.error({...s,http_status:a.status,http_status_text:a.statusText}),null;const i=await a.json();return this.logger.info({...s,http_status:a.status,taskId:i.taskId}),i}catch(e){return this.logger.error({...s,error:`${e}`}),null}}}export{CalendarResources as default};
@@ -1 +1 @@
1
- import{setTimeout as e}from"node:timers/promises";import t from"node-fetch";import r from"../../abstract/plugin.js";import{escapeDnValue as i,isChildOf as s}from"../../lib/utils.js";import{BaseResolver as n}from"../scim/baseResolver.js";const a=new Set(["admin","owner","member"]),o=1e3;class ClouderyProvision extends r{constructor(e){super(e),this.name="clouderyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.createStash=new Map,this.deleteStash=new Map,this.hooks={scimusercreate:e=>(this.captureCreate(e[0],e[1]),e),scimusercreatedone:async e=>{await this.provision(e)},scimuserdelete:async e=>(await this.captureDelete(e[0],e[1]),e),scimuserdeletedone:async e=>{await this.deprovision(e)}};const t=this.config;this.managerUrl=(t.cloudery_manager_url||"").replace(/\/$/,""),this.managerToken=t.cloudery_manager_token||"",this.offer=t.cloudery_offer||"b2b_twake_default",this.clouderyDomain=t.cloudery_domain||"",this.userBranch=t.cloudery_user_branch||"",this.orgIdHeader=(t.cloudery_org_id_header||"x-cloudery-org-id").toLowerCase(),this.orgRoleHeader=(t.cloudery_org_role_header||"x-cloudery-org-role").toLowerCase();const r=t.cloudery_default_org_role||"member",i=r.trim().toLowerCase();a.has(i)?this.defaultOrgRole=i:(this.defaultOrgRole="member",this.logger.warn(`${this.name}: cloudery_default_org_role "${r}" is not one of ${[...a].join(", ")} — using "member"`)),this.fqdnAttribute=t.cloudery_fqdn_attribute||"twakeWorkspaceUrl",this.orgIdAttribute=t.cloudery_org_id_attribute||"twakeOrganizationId",this.orgRoleAttribute=t.cloudery_org_role_attribute||"twakeOrganizationRole",this.phonesAttribute=t.cloudery_phones_attribute||"twakePhones",this.invitedAttribute=t.cloudery_invited_attribute||"twakeInvited",this.defaultLocale=t.cloudery_default_locale||"en",this.rdnAttribute=t.scim_user_rdn_attribute||"uid",this.authExchange=t.cozy_auth_exchange||"auth",this.b2bExchange=t.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=t.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=t.cozy_user_deleted_routing_key||"domain.user.deleted",this.pollIntervalMs=Number(t.cloudery_workflow_poll_interval_ms)||2e3,this.maxPollAttempts=Number(t.cloudery_workflow_max_attempts)||60,this.baseResolver=new n(this.config),this.managerUrl||this.logger.warn(`${this.name}: cloudery_manager_url is empty — instance provisioning will be skipped`),this.userBranch||this.logger.warn(`${this.name}: cloudery_user_branch is empty — no users will be provisioned`),this.logger.info({plugin:this.name,event:"init",managerUrl:this.managerUrl||void 0,userBranch:this.userBranch||void 0,orgIdHeader:this.orgIdHeader,orgRoleHeader:this.orgRoleHeader,defaultOrgRole:this.defaultOrgRole,fqdnAttribute:this.fqdnAttribute,orgIdAttribute:this.orgIdAttribute,orgRoleAttribute:this.orgRoleAttribute,phonesAttribute:this.phonesAttribute,invitedAttribute:this.invitedAttribute,pollIntervalMs:this.pollIntervalMs,maxPollAttempts:this.maxPollAttempts})}captureCreate(e,t){this.pruneStash();const r=this.extractId(e),i=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,result:"skipped",reason:"missing_configuration"});if(!r)return void this.logger.info({plugin:this.name,event:"captureCreate",base:i,result:"skipped",reason:"missing_user_name"});if(!this.isAtOrUnder(i,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,result:"skipped",reason:"outside_b2b_branch"});const s=this.readHeader(t,this.orgIdHeader);if(!s)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:`B2B branch create without ${this.orgIdHeader} — skipping provisioning`});const n=this.extractPrimaryEmail(e);if(!n)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:"B2B branch create without a primary email — skipping provisioning"});const a=n.trim(),o=this.resolveRole(t,r);this.createStash.set(a.toLowerCase(),{orgId:s.trim(),email:a,role:o,base:i,ts:Date.now()}),this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,orgId:s.trim(),email:a,role:o,result:"stashed"})}async provision(e){const t=this.extractId(e);if(this.logger.info({plugin:this.name,event:"provision",userName:t||void 0}),!t)return void this.logger.info({plugin:this.name,event:"provision",result:"skipped",reason:"missing_user_name"});const r=this.extractPrimaryEmail(e);if(!r)return void this.logger.info({plugin:this.name,event:"provision",userName:t,result:"skipped",reason:"missing_primary_email"});const s=r.trim().toLowerCase(),n=this.createStash.get(s);if(!n)return void this.logger.info({plugin:this.name,event:"provision",userName:t,email:r.trim(),result:"skipped",reason:"no_stashed_context"});this.createStash.delete(s),this.logger.info({plugin:this.name,event:"provision",userName:t,email:n.email,orgId:n.orgId,base:n.base,role:n.role,result:"context_loaded"});const a=n.email,o=emailDomain(a),l=t.replace(/\./g,"")+n.orgId;const u=this.extractPrimaryPhone(e),h=this.extractPhones(e),g=this.extractPublicName(e)||t,d={plugin:this.name,event:"provision",userName:t,orgId:n.orgId,slug:l},c=`${l}.${this.clouderyDomain}`.toLowerCase();let m,p=null;try{p=await this.findInstanceUuidByFqdn(c)}catch(e){this.logger.warn({...d,result:"existence_lookup_failed",fqdn:c,error:`${e}`})}if(p)this.logger.info({...d,result:"instance_exists",fqdn:c,uuid:p}),m=c;else{let t;try{this.logger.info({...d,result:"creating_instance",orgDomain:o,email:a,mobile:u||void 0}),t=await this.createInstance({email:a,publicName:g,locale:this.extractLocale(e),oidc:l,phone:u||"",slug:l,public_name:g,offer:this.offer,domain:this.clouderyDomain,skip_email_validation:!0,internal_email:a,org_domain:o,org_id:n.orgId})}catch(e){return void this.logger.error({...d,result:"error",error:`${e}`})}if(!(!t.workflow||await this.waitForWorkflow(t.workflow)))return void this.logger.error({...d,result:"workflow_failed",workflow:t.workflow});m=t.fqdn}const f=`${this.rdnAttribute}=${i(t)},${n.base}`,w={[this.fqdnAttribute]:m,[this.orgIdAttribute]:n.orgId,[this.orgRoleAttribute]:n.role,[this.invitedAttribute]:"TRUE",cn:t};h.length>0&&(w[this.phonesAttribute]=JSON.stringify(h));try{this.logger.info({...d,result:"writing_fqdn",dn:f,hasPhones:h.length>0}),await this.server.ldap.modify(f,{replace:w}),this.logger.info({...d,result:"written_fqdn",dn:f})}catch(e){this.logger.error({...d,event:"writeFqdn",dn:f,error:`${e}`})}await this.publishUserCreated({twakeId:t,orgDomain:o,orgId:n.orgId,fqdn:m,email:a,mobile:u}),this.logger.info({...d,result:"success",fqdn:m})}async captureDelete(e,t){const r=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"missing_configuration"});if(!this.isAtOrUnder(r,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"outside_b2b_branch"});const s=`${this.rdnAttribute}=${i(e)},${r}`;try{this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,result:"searching"});const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.fqdnAttribute,"mail"]},s),r=t.searchEntries?.[0],i=r?firstValue(r[this.fqdnAttribute]):void 0,n=i?i.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0]:void 0;if(!n)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,result:"skipped",reason:"missing_fqdn"});const a=r?firstValue(r.mail):void 0,o=a?emailDomain(a):"";this.deleteStash.set(e,{fqdn:n,domain:o}),this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,fqdn:n,domain:o,result:"stashed"})}catch(t){this.logger.warn({plugin:this.name,event:"captureDelete",id:e,error:`${t}`})}}async deprovision(e){this.logger.info({plugin:this.name,event:"deprovision",id:e});const t=this.deleteStash.get(e);if(!t)return void this.logger.info({plugin:this.name,event:"deprovision",id:e,result:"skipped",reason:"no_stashed_context"});this.deleteStash.delete(e);const r={plugin:this.name,event:"deprovision",id:e,fqdn:t.fqdn};let i=!1;try{this.logger.info({...r,result:"resolving_uuid"});const e=await this.findInstanceUuidByFqdn(t.fqdn);e?(this.logger.info({...r,result:"deleting_instance",uuid:e}),await this.deleteInstance(e),i=!0,this.logger.info({...r,result:"deleted",uuid:e})):this.logger.warn({...r,result:"not_found"})}catch(e){this.logger.error({...r,result:"error",error:`${e}`})}i&&await this.publishUserDeleted(t.fqdn,t.domain)}async createInstance(e){this.logger.info({plugin:this.name,event:"createInstance",url:`${this.managerUrl}/api/v1/instances`});const r=await t(`${this.managerUrl}/api/v1/instances`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.managerToken}`},body:JSON.stringify(e)});if(!r.ok){const e=(await r.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"createInstance",result:"error",http_status:r.status,http_status_text:r.statusText,body:e||void 0}),new Error(`Cloudery createInstance HTTP ${r.status}`)}const i=await r.json();return this.logger.info({plugin:this.name,event:"createInstance",result:"success",id:i.id,fqdn:i.fqdn,workflow:i.workflow}),i}async waitForWorkflow(r){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,maxPollAttempts:this.maxPollAttempts,pollIntervalMs:this.pollIntervalMs});for(let i=1;i<=this.maxPollAttempts;i++){try{const e=await t(`${this.managerUrl}/api/v1/workflows/${encodeURIComponent(r)}`,{headers:{Authorization:`Bearer ${this.managerToken}`}});if(e.ok){const t=await e.json();if(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,status:t.status}),"succeeded"===t.status)return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"success"}),!0;if("failed"===t.status||"error"===t.status)return this.logger.warn({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"failed",status:t.status}),!1}else{const t=(await e.text().catch(()=>"")).slice(0,o);this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"http_error",http_status:e.status,http_status_text:e.statusText,body:t||void 0})}}catch(e){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,error:`${e}`})}i<this.maxPollAttempts&&(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"retrying"}),await e(this.pollIntervalMs))}return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,result:"timeout",maxPollAttempts:this.maxPollAttempts}),!1}async findInstanceUuidByFqdn(e){this.logger.info({plugin:this.name,event:"findInstanceUuidByFqdn",fqdn:e});const r=new URL(`${this.managerUrl}/api/v2/instances`);r.searchParams.set("fqdn",e),r.searchParams.append("only","_id"),r.searchParams.set("limit","1");const i=await t(r.toString(),{headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok){const t=(await i.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"findInstanceUuidByFqdn",fqdn:e,result:"error",http_status:i.status,http_status_text:i.statusText,body:t||void 0}),new Error(`Cloudery searchInstances HTTP ${i.status}`)}const s=await i.json(),n=s.items&&s.items.length>0?s.items[0]._id:null;return this.logger.info({plugin:this.name,event:"findInstanceUuidByFqdn",fqdn:e,result:n?"found":"not_found",uuid:n||void 0}),n}async deleteInstance(e){const r=new URL(`${this.managerUrl}/api/v1/instances/${encodeURIComponent(e)}`);r.searchParams.set("user_request","true"),this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,url:r.toString()});const i=await t(r.toString(),{method:"DELETE",headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok){const t=(await i.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"deleteInstance",uuid:e,result:"error",http_status:i.status,http_status_text:i.statusText,body:t||void 0}),new Error(`Cloudery deleteInstance HTTP ${i.status}`)}this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,result:"success",http_status:i.status})}async publishUserCreated(e){const t=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,orgId:e.orgId,orgDomain:e.orgDomain,hasRabbitmq:Boolean(t)}),!t)return void this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,result:"skipped",reason:"missing_rabbitmq"});const r={twakeId:e.twakeId,domain:e.orgDomain,organizationDomain:e.orgDomain,workplaceFqdn:e.fqdn,organizationId:e.orgId};e.email&&(r.internalEmail=e.email),e.mobile&&(r.mobile=e.mobile);try{await t.publish(this.authExchange,this.userCreatedRoutingKey,r),this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,exchange:this.authExchange,routingKey:this.userCreatedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,error:`${t}`})}}async publishUserDeleted(e,t){const r=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,domain:t,hasRabbitmq:Boolean(r)}),r)try{await r.publish(this.b2bExchange,this.userDeletedRoutingKey,{workplaceFqdn:e,domain:t}),this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,exchange:this.b2bExchange,routingKey:this.userDeletedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,error:`${t}`})}else this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,result:"skipped",reason:"missing_rabbitmq"})}readHeader(e,t){if(!e)return;const r=e.get?e.get(t):void 0;if("string"==typeof r&&r.length>0)return r;const i=e.query?.[t];return"string"==typeof i&&i.length>0?i:void 0}resolveRole(e,t){const r=this.readHeader(e,this.orgRoleHeader)?.trim().toLowerCase();return r?a.has(r)?(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"accepted",role:r}),r):(this.logger.warn({plugin:this.name,event:"captureCreate",userName:t,message:`Unknown ${this.orgRoleHeader} "${r}" — falling back to ${this.defaultOrgRole}`}),this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole):(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole)}isAtOrUnder(e,t){return e.trim().toLowerCase()===t.trim().toLowerCase()||s(e,t)}pruneStash(){const e=Date.now()-3e5;for(const[t,r]of this.createStash)r.ts<e&&this.createStash.delete(t)}extractId(e){return"string"==typeof e.userName&&e.userName.length>0?e.userName:"string"==typeof e.id&&e.id.length>0?e.id:null}extractPrimaryEmail(e){const t=e.emails;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPrimaryPhone(e){const t=e.phoneNumbers;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPhones(e){const t=e.phoneNumbers;return t&&0!==t.length?t.filter(e=>"string"==typeof e.value&&e.value.trim().length>0).map(e=>({number:e.value.trim(),primary:Boolean(e.primary)})):[]}extractPublicName(e){if("string"==typeof e.displayName&&e.displayName.trim().length>0)return e.displayName.trim();const t=e.name?.formatted;if("string"==typeof t&&t.trim().length>0)return t.trim();const r=`${e.name?.givenName?.trim()??""} ${e.name?.familyName?.trim()??""}`.trim();return r.length>0?r:null}extractLocale(e){return"string"==typeof e.locale&&e.locale.length>0?e.locale:"string"==typeof e.preferredLanguage&&e.preferredLanguage.length>0?e.preferredLanguage:this.defaultLocale}}function emailDomain(e){const t=e.lastIndexOf("@");return t>=0?e.slice(t+1):""}function firstValue(e){if(Array.isArray(e)){const t=e[0];return"string"==typeof t?t:void 0}return"string"==typeof e?e:void 0}export{ClouderyProvision as default};
1
+ import{setTimeout as e}from"node:timers/promises";import t from"node-fetch";import r from"../../abstract/plugin.js";import{escapeDnValue as i,isChildOf as s}from"../../lib/utils.js";import{BaseResolver as n}from"../scim/baseResolver.js";const a=new Set(["admin","owner","member"]),o=1e3;class ClouderyProvision extends r{constructor(e){super(e),this.name="clouderyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.createStash=new Map,this.deleteStash=new Map,this.hooks={scimusercreate:e=>(this.normalizeUserName(e[0]),this.captureCreate(e[0],e[1]),e),scimusercreatedone:async e=>{await this.provision(e)},scimuserdelete:async e=>(e[0]=e[0].toLowerCase(),await this.captureDelete(e[0],e[1]),e),scimuserdeletedone:async e=>{await this.deprovision(e)}};const t=this.config;this.managerUrl=(t.cloudery_manager_url||"").replace(/\/$/,""),this.managerToken=t.cloudery_manager_token||"",this.offer=t.cloudery_offer||"b2b_twake_default",this.clouderyDomain=t.cloudery_domain||"",this.userBranch=t.cloudery_user_branch||"",this.orgIdHeader=(t.cloudery_org_id_header||"x-cloudery-org-id").toLowerCase(),this.orgRoleHeader=(t.cloudery_org_role_header||"x-cloudery-org-role").toLowerCase();const r=t.cloudery_default_org_role||"member",i=r.trim().toLowerCase();a.has(i)?this.defaultOrgRole=i:(this.defaultOrgRole="member",this.logger.warn(`${this.name}: cloudery_default_org_role "${r}" is not one of ${[...a].join(", ")} — using "member"`)),this.fqdnAttribute=t.cloudery_fqdn_attribute||"twakeWorkspaceUrl",this.orgIdAttribute=t.cloudery_org_id_attribute||"twakeOrganizationId",this.orgRoleAttribute=t.cloudery_org_role_attribute||"twakeOrganizationRole",this.phonesAttribute=t.cloudery_phones_attribute||"twakePhones",this.invitedAttribute=t.cloudery_invited_attribute||"twakeInvited",this.defaultLocale=t.cloudery_default_locale||"en",this.rdnAttribute=t.scim_user_rdn_attribute||"uid",this.authExchange=t.cozy_auth_exchange||"auth",this.b2bExchange=t.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=t.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=t.cozy_user_deleted_routing_key||"domain.user.deleted",this.pollIntervalMs=Number(t.cloudery_workflow_poll_interval_ms)||2e3,this.maxPollAttempts=Number(t.cloudery_workflow_max_attempts)||60,this.baseResolver=new n(this.config),this.managerUrl||this.logger.warn(`${this.name}: cloudery_manager_url is empty — instance provisioning will be skipped`),this.userBranch||this.logger.warn(`${this.name}: cloudery_user_branch is empty — no users will be provisioned`),this.logger.info({plugin:this.name,event:"init",managerUrl:this.managerUrl||void 0,userBranch:this.userBranch||void 0,orgIdHeader:this.orgIdHeader,orgRoleHeader:this.orgRoleHeader,defaultOrgRole:this.defaultOrgRole,fqdnAttribute:this.fqdnAttribute,orgIdAttribute:this.orgIdAttribute,orgRoleAttribute:this.orgRoleAttribute,phonesAttribute:this.phonesAttribute,invitedAttribute:this.invitedAttribute,pollIntervalMs:this.pollIntervalMs,maxPollAttempts:this.maxPollAttempts})}captureCreate(e,t){this.pruneStash();const r=this.extractId(e),i=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,result:"skipped",reason:"missing_configuration"});if(!r)return void this.logger.info({plugin:this.name,event:"captureCreate",base:i,result:"skipped",reason:"missing_user_name"});if(!this.isAtOrUnder(i,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,result:"skipped",reason:"outside_b2b_branch"});const s=this.readHeader(t,this.orgIdHeader);if(!s)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:`B2B branch create without ${this.orgIdHeader} — skipping provisioning`});const n=this.extractPrimaryEmail(e);if(!n)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:"B2B branch create without a primary email — skipping provisioning"});const a=n.trim(),o=this.resolveRole(t,r);this.createStash.set(a.toLowerCase(),{orgId:s.trim(),email:a,role:o,base:i,ts:Date.now()}),this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,orgId:s.trim(),email:a,role:o,result:"stashed"})}async provision(e){const t=this.extractId(e);if(this.logger.info({plugin:this.name,event:"provision",userName:t||void 0}),!t)return void this.logger.info({plugin:this.name,event:"provision",result:"skipped",reason:"missing_user_name"});const r=this.extractPrimaryEmail(e);if(!r)return void this.logger.info({plugin:this.name,event:"provision",userName:t,result:"skipped",reason:"missing_primary_email"});const s=r.trim().toLowerCase(),n=this.createStash.get(s);if(!n)return void this.logger.info({plugin:this.name,event:"provision",userName:t,email:r.trim(),result:"skipped",reason:"no_stashed_context"});this.createStash.delete(s),this.logger.info({plugin:this.name,event:"provision",userName:t,email:n.email,orgId:n.orgId,base:n.base,role:n.role,result:"context_loaded"});const a=n.email,o=emailDomain(a),l=t.replace(/\./g,"")+n.orgId;const u=this.extractPrimaryPhone(e),h=this.extractPhones(e),g=this.extractPublicName(e)||t,d={plugin:this.name,event:"provision",userName:t,orgId:n.orgId,slug:l},c=`${l}.${this.clouderyDomain}`.toLowerCase();let m,p=null;try{p=await this.findInstanceUuidByFqdn(c)}catch(e){this.logger.warn({...d,result:"existence_lookup_failed",fqdn:c,error:`${e}`})}if(p)this.logger.info({...d,result:"instance_exists",fqdn:c,uuid:p}),m=c;else{let t;try{this.logger.info({...d,result:"creating_instance",orgDomain:o,email:a,mobile:u||void 0}),t=await this.createInstance({email:a,publicName:g,locale:this.extractLocale(e),oidc:l,phone:u||"",slug:l,public_name:g,offer:this.offer,domain:this.clouderyDomain,skip_email_validation:!0,internal_email:a,org_domain:o,org_id:n.orgId})}catch(e){return void this.logger.error({...d,result:"error",error:`${e}`})}if(!(!t.workflow||await this.waitForWorkflow(t.workflow)))return void this.logger.error({...d,result:"workflow_failed",workflow:t.workflow});m=t.fqdn}const f=`${this.rdnAttribute}=${i(t)},${n.base}`,w={[this.fqdnAttribute]:m,[this.orgIdAttribute]:n.orgId,[this.orgRoleAttribute]:n.role,[this.invitedAttribute]:"TRUE",cn:t};h.length>0&&(w[this.phonesAttribute]=JSON.stringify(h));try{this.logger.info({...d,result:"writing_fqdn",dn:f,hasPhones:h.length>0}),await this.server.ldap.modify(f,{replace:w}),this.logger.info({...d,result:"written_fqdn",dn:f})}catch(e){this.logger.error({...d,event:"writeFqdn",dn:f,error:`${e}`})}await this.publishUserCreated({twakeId:t,orgDomain:o,orgId:n.orgId,fqdn:m,email:a,mobile:u}),this.logger.info({...d,result:"success",fqdn:m})}async captureDelete(e,t){const r=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"missing_configuration"});if(!this.isAtOrUnder(r,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"outside_b2b_branch"});const s=`${this.rdnAttribute}=${i(e)},${r}`;try{this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,result:"searching"});const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.fqdnAttribute,"mail"]},s),r=t.searchEntries?.[0],i=r?firstValue(r[this.fqdnAttribute]):void 0,n=i?i.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0]:void 0;if(!n)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,result:"skipped",reason:"missing_fqdn"});const a=r?firstValue(r.mail):void 0,o=a?emailDomain(a):"";this.deleteStash.set(e,{fqdn:n,domain:o}),this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,fqdn:n,domain:o,result:"stashed"})}catch(t){this.logger.warn({plugin:this.name,event:"captureDelete",id:e,error:`${t}`})}}async deprovision(e){this.logger.info({plugin:this.name,event:"deprovision",id:e});const t=this.deleteStash.get(e);if(!t)return void this.logger.info({plugin:this.name,event:"deprovision",id:e,result:"skipped",reason:"no_stashed_context"});this.deleteStash.delete(e);const r={plugin:this.name,event:"deprovision",id:e,fqdn:t.fqdn};let i=!1;try{this.logger.info({...r,result:"resolving_uuid"});const e=await this.findInstanceUuidByFqdn(t.fqdn);e?(this.logger.info({...r,result:"deleting_instance",uuid:e}),await this.deleteInstance(e),i=!0,this.logger.info({...r,result:"deleted",uuid:e})):this.logger.warn({...r,result:"not_found"})}catch(e){this.logger.error({...r,result:"error",error:`${e}`})}i&&await this.publishUserDeleted(t.fqdn,t.domain)}async createInstance(e){this.logger.info({plugin:this.name,event:"createInstance",url:`${this.managerUrl}/api/v1/instances`});const r=await t(`${this.managerUrl}/api/v1/instances`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.managerToken}`},body:JSON.stringify(e)});if(!r.ok){const e=(await r.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"createInstance",result:"error",http_status:r.status,http_status_text:r.statusText,body:e||void 0}),new Error(`Cloudery createInstance HTTP ${r.status}`)}const i=await r.json();return this.logger.info({plugin:this.name,event:"createInstance",result:"success",id:i.id,fqdn:i.fqdn,workflow:i.workflow}),i}async waitForWorkflow(r){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,maxPollAttempts:this.maxPollAttempts,pollIntervalMs:this.pollIntervalMs});for(let i=1;i<=this.maxPollAttempts;i++){try{const e=await t(`${this.managerUrl}/api/v1/workflows/${encodeURIComponent(r)}`,{headers:{Authorization:`Bearer ${this.managerToken}`}});if(e.ok){const t=await e.json();if(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,status:t.status}),"succeeded"===t.status)return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"success"}),!0;if("failed"===t.status||"error"===t.status)return this.logger.warn({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"failed",status:t.status}),!1}else{const t=(await e.text().catch(()=>"")).slice(0,o);this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"http_error",http_status:e.status,http_status_text:e.statusText,body:t||void 0})}}catch(e){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,error:`${e}`})}i<this.maxPollAttempts&&(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"retrying"}),await e(this.pollIntervalMs))}return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,result:"timeout",maxPollAttempts:this.maxPollAttempts}),!1}async findInstanceUuidByFqdn(e){this.logger.info({plugin:this.name,event:"findInstanceUuidByFqdn",fqdn:e});const r=new URL(`${this.managerUrl}/api/v2/instances`);r.searchParams.set("fqdn",e),r.searchParams.append("only","_id"),r.searchParams.set("limit","1");const i=await t(r.toString(),{headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok){const t=(await i.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"findInstanceUuidByFqdn",fqdn:e,result:"error",http_status:i.status,http_status_text:i.statusText,body:t||void 0}),new Error(`Cloudery searchInstances HTTP ${i.status}`)}const s=await i.json(),n=s.items&&s.items.length>0?s.items[0]._id:null;return this.logger.info({plugin:this.name,event:"findInstanceUuidByFqdn",fqdn:e,result:n?"found":"not_found",uuid:n||void 0}),n}async deleteInstance(e){const r=new URL(`${this.managerUrl}/api/v1/instances/${encodeURIComponent(e)}`);r.searchParams.set("user_request","true"),this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,url:r.toString()});const i=await t(r.toString(),{method:"DELETE",headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok){const t=(await i.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"deleteInstance",uuid:e,result:"error",http_status:i.status,http_status_text:i.statusText,body:t||void 0}),new Error(`Cloudery deleteInstance HTTP ${i.status}`)}this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,result:"success",http_status:i.status})}async publishUserCreated(e){const t=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,orgId:e.orgId,orgDomain:e.orgDomain,hasRabbitmq:Boolean(t)}),!t)return void this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,result:"skipped",reason:"missing_rabbitmq"});const r={twakeId:e.twakeId,domain:e.orgDomain,organizationDomain:e.orgDomain,workplaceFqdn:e.fqdn,organizationId:e.orgId};e.email&&(r.internalEmail=e.email),e.mobile&&(r.mobile=e.mobile);try{await t.publish(this.authExchange,this.userCreatedRoutingKey,r),this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,exchange:this.authExchange,routingKey:this.userCreatedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,error:`${t}`})}}async publishUserDeleted(e,t){const r=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,domain:t,hasRabbitmq:Boolean(r)}),r)try{await r.publish(this.b2bExchange,this.userDeletedRoutingKey,{workplaceFqdn:e,domain:t}),this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,exchange:this.b2bExchange,routingKey:this.userDeletedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,error:`${t}`})}else this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,result:"skipped",reason:"missing_rabbitmq"})}readHeader(e,t){if(!e)return;const r=e.get?e.get(t):void 0;if("string"==typeof r&&r.length>0)return r;const i=e.query?.[t];return"string"==typeof i&&i.length>0?i:void 0}resolveRole(e,t){const r=this.readHeader(e,this.orgRoleHeader)?.trim().toLowerCase();return r?a.has(r)?(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"accepted",role:r}),r):(this.logger.warn({plugin:this.name,event:"captureCreate",userName:t,message:`Unknown ${this.orgRoleHeader} "${r}" — falling back to ${this.defaultOrgRole}`}),this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole):(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole)}isAtOrUnder(e,t){return e.trim().toLowerCase()===t.trim().toLowerCase()||s(e,t)}pruneStash(){const e=Date.now()-3e5;for(const[t,r]of this.createStash)r.ts<e&&this.createStash.delete(t)}normalizeUserName(e){"string"==typeof e.userName&&(e.userName=e.userName.toLowerCase())}extractId(e){return"string"==typeof e.userName&&e.userName.length>0?e.userName:"string"==typeof e.id&&e.id.length>0?e.id:null}extractPrimaryEmail(e){const t=e.emails;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPrimaryPhone(e){const t=e.phoneNumbers;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPhones(e){const t=e.phoneNumbers;return t&&0!==t.length?t.filter(e=>"string"==typeof e.value&&e.value.trim().length>0).map(e=>({number:e.value.trim(),primary:Boolean(e.primary)})):[]}extractPublicName(e){if("string"==typeof e.displayName&&e.displayName.trim().length>0)return e.displayName.trim();const t=e.name?.formatted;if("string"==typeof t&&t.trim().length>0)return t.trim();const r=`${e.name?.givenName?.trim()??""} ${e.name?.familyName?.trim()??""}`.trim();return r.length>0?r:null}extractLocale(e){return"string"==typeof e.locale&&e.locale.length>0?e.locale:"string"==typeof e.preferredLanguage&&e.preferredLanguage.length>0?e.preferredLanguage:this.defaultLocale}}function emailDomain(e){const t=e.lastIndexOf("@");return t>=0?e.slice(t+1):""}function firstValue(e){if(Array.isArray(e)){const t=e[0];return"string"==typeof t?t:void 0}return"string"==typeof e?e:void 0}export{ClouderyProvision as default};
@@ -1 +1 @@
1
- import t from"node-fetch";import{TwakePlugin as e}from"../../abstract/twakePlugin.js";import{asyncHandler as i,escapeLdapFilter as a}from"../../lib/utils.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";class James extends e{constructor(t){super(t,"james_webadmin_url","james_webadmin_token","james_concurrency"),this.name="james",this.roles=["consistency","api"],this.dependencies={onLdapChange:"core/ldap/onChange",ldapGroups:"core/ldap/groups"},this.hooks={ldapadddone:async t=>{const[e,i]=t,a=i[this.mailAttr],s=i[this.quotaAttr],r=i[this.aliasAttr];if(!a)return;const n=Array.isArray(a)?String(a[0]):String(a);if(this.initDelay>0&&await new Promise(t=>setTimeout(t,this.initDelay)),s){const t=Array.isArray(s)?Number(s[0]):Number(s);!isNaN(t)&&t>0&&await this.callWebAdminApi("ldapadddone:quota",`${this.webadminUrl}/quota/users/${n}/size`,"PUT",e,t.toString(),{mail:n,quota:t})}if(r){const t=this.getAliases(r);await Promise.all(t.map(t=>this.callWebAdminApi("ldapadddone:alias",`${this.webadminUrl}/address/aliases/${n}/sources/${t}`,"PUT",e,null,{mail:n,alias:t})))}const l=this.getDisplayNameFromAttributes(i);l&&await this.updateJamesIdentity(e,n,l)},onLdapMailChange:async(t,e,i)=>{const a=e?Array.isArray(e)?String(e[0]):String(e):null,s=i?Array.isArray(i)?String(i[0]):String(i):null;if(a){await this.callWebAdminApi("onLdapMailChange",`${this.webadminUrl}/users/${a}/rename/${s}?action=rename`,"POST",t,null,{oldmail:a,newmail:s});try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.aliasAttr]},t);if(e.searchEntries&&e.searchEntries.length>0){const i=this.getAliases(e.searchEntries[0][this.aliasAttr]);i.length>0&&await Promise.all([...i.map(e=>this.callWebAdminApi("onLdapMailChange-delete",`${this.webadminUrl}/address/aliases/${a}/sources/${e}`,"DELETE",t,null,{oldmail:a,alias:e})),...i.map(e=>this.callWebAdminApi("onLdapMailChange-create",`${this.webadminUrl}/address/aliases/${s}/sources/${e}`,"PUT",t,null,{newmail:s,alias:e}))])}}catch(t){this.logger.debug("Could not fetch aliases for mail change:",t)}}else this.logger.debug(`Skipping mail rename for ${t}: oldmail is empty (mail attribute was added, not changed)`)},onLdapAliasChange:async(t,e,i,a)=>{const s=i.map(t=>this.normalizeAlias(t)),r=a.map(t=>this.normalizeAlias(t)),n=s.filter(t=>!r.includes(t)),l=r.filter(t=>!s.includes(t));await Promise.all([...n.map(i=>this.callWebAdminApi("onLdapAliasChange-delete",`${this.webadminUrl}/address/aliases/${e}/sources/${i}`,"DELETE",t,null,{mail:e,alias:i,action:"delete"})),...l.map(i=>this.callWebAdminApi("onLdapAliasChange-add",`${this.webadminUrl}/address/aliases/${e}/sources/${i}`,"PUT",t,null,{mail:e,alias:i,action:"add"}))])},onLdapQuotaChange:(t,e,i,a)=>this.callWebAdminApi("onLdapQuotaChange",`${this.webadminUrl}/quota/users/${e}/size`,"PUT",t,a.toString(),{oldQuota:i,newQuota:a}),onLdapForwardChange:async(t,e,i,a)=>{const s=this.extractMailDomain(e);if(!s)return void this.logger.error(`Cannot extract domain from mail ${e} for forward management`);const r=i.filter(t=>!a.includes(t)),n=a.filter(t=>!i.includes(t));await Promise.all([...r.map(i=>this.callWebAdminApi("onLdapForwardChange-delete",`${this.webadminUrl}/domains/${s}/forwards/${e}/${i}`,"DELETE",t,null,{mail:e,forward:i,domain:s,action:"delete"})),...n.map(i=>this.callWebAdminApi("onLdapForwardChange-add",`${this.webadminUrl}/domains/${s}/forwards/${e}/${i}`,"PUT",t,null,{mail:e,forward:i,domain:s,action:"add"}))])},onLdapChange:async(t,e)=>{this.config.delegation_attribute&&e[this.config.delegation_attribute]&&await this._handleDelegationChange(t,e)},onLdapDisplayNameChange:async t=>{const e=[this.mailAttr,this.displayNameAttr,"cn","givenName","sn"],i=await this.ldapGetAttributes(t,e);if(!i)return void this.logger.warn(`Cannot update James identity: entry not found for ${t}`);const a=this.attributeToString(i[this.mailAttr]);if(!a)return void this.logger.warn(`Cannot update James identity: no mail found for ${t}`);const s=this.getDisplayNameFromAttributes(i);if(s)return this.updateJamesIdentity(t,a,s);this.logger.warn(`Cannot update James identity: no display name found for ${t}`)},ldapgroupadddone:async t=>{const[e,i]=t,a=i.mail;if(!a)return void this.logger.debug(`Group ${e} has no mail attribute, skipping James sync`);const s=this.getMailboxType(i),r=Array.isArray(a)?String(a[0]):String(a);if("teamMailbox"===s){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&this.isInAllowedBranches(e,t))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:e,mailboxType:"teamMailbox",error:`Team mailbox cannot be in mailing list branches: ${t.join(", ")}`});await this.createTeamMailbox(e,i,r)}else if("mailingList"===s||null===s){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&!this.isInAllowedBranches(e,t))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:e,mailboxType:s||"mailingList (default)",error:`Mailing list must be in allowed branches: ${t.join(", ")}`});await this.createMailingList(e,i,r)}},ldapgroupmodifydone:async t=>{const[e,i]=t;if(i.replace?.[this.mailboxTypeAttr]||i.add?.[this.mailboxTypeAttr])return void await this.handleMailboxTypeTransition(e,i);const a=await this.getGroupMail(e);if(!a)return void this.logger.debug(`Group ${e} has no mail attribute, skipping James sync`);const s="teamMailbox"===await this.getGroupMailboxType(e),r=s?this.extractMailDomain(a):null,n=[];if(i.add?.member){const t=i.add.member,l=Array.isArray(t)?t:[t],o=await this.getMemberEmails(l);n.push(...o.map(t=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${t}`,"PUT",e,null,{domain:r,groupMail:a,memberMail:t,action:"add"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${t}`,"PUT",e,null,{groupMail:a,memberMail:t,action:"add"})))}if(this.isAttributesList(i.delete)){const t=i.delete.member;if(t){const i=Array.isArray(t)?t:[t],l=await this.getMemberEmails(i);n.push(...l.map(t=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${t}`,"DELETE",e,null,{domain:r,groupMail:a,memberMail:t,action:"delete"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${t}`,"DELETE",e,null,{groupMail:a,memberMail:t,action:"delete"})))}}await Promise.all(n)},ldapgroupdeletedone:async t=>{const e=await this.getGroupMail(t);if(!e)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);"teamMailbox"===await this.getGroupMailboxType(t)?await this.removeAllTeamMailboxMembers(t,e):await this.callWebAdminApi("ldapgroupdeletedone:mailingList",`${this.webadminUrl}/address/groups/${e}`,"DELETE",t,null,{groupMail:e})}},this.quotaAttr=this.config.quota_attribute||"mailQuotaSize",this.aliasAttr=this.config.alias_attribute||"mailAlternateAddress",this.mailboxTypeAttr=this.config.james_mailbox_type_attribute||"twakeMailboxType",this.initDelay="number"==typeof this.config.james_init_delay?this.config.james_init_delay:1e3}api(t){const e=this.config.api_prefix||"/api";t.get(`${e}/v1/users/:user/quota-usage`,i((t,e)=>this.getUserQuota(t,e))),this.logger.info(`James API registered at ${e}/v1/users/:user/quota-usage`)}async getUserQuota(e,i){if(!s(e,i))return;const r=e.params.user;try{const e=a(r),s=await this.server.ldap.search({scope:"sub",filter:`(uid=${e})`,paged:!1,attributes:[this.mailAttr]},this.config.ldap_base||""),n=s.searchEntries?.[0];if(!n)return void i.status(404).json({error:`User ${r} not found`});const l=n[this.mailAttr];if(!l)return void i.status(400).json({error:`User ${r} has no mail attribute`});const o=Array.isArray(l)?String(l[0]):String(l),m=`${this.webadminUrl}/quota/users/${o}`,d=await this.requestLimit(()=>t(m,{method:"GET",headers:this.createHeaders()}));if(!d.ok)return void(404===d.status?i.status(404).json({error:`Quota not found for user ${r}`}):i.status(502).json({error:`Failed to retrieve quota from James: ${d.statusText}`}));const h=await d.json();i.json(h)}catch(t){this.logger.error({plugin:this.name,event:"getUserQuota",username:r,error:t instanceof Error?t.message:String(t)}),i.status(500).json({error:"Internal server error while retrieving quota"})}}async handleMailboxTypeTransition(t,e){const i=await this.ldapGetAttributes(t,["mail","member",this.mailboxTypeAttr]);if(!i)return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,error:"Could not fetch group attributes"});const a=i.mail;if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping mailbox type transition`);const s=Array.isArray(a)?String(a[0]):String(a),r=e.replace?.[this.mailboxTypeAttr]||e.add?.[this.mailboxTypeAttr],n=Array.isArray(r)?String(r[0]):String(r),l=this.getMailboxType({[this.mailboxTypeAttr]:n}),o=this.getMailboxType(i);if(this.logger.info({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,mail:s,oldType:o||"none",newType:l||"none"}),o!==l)if("mailingList"===o?await this.deleteMailingList(t,s):"teamMailbox"===o&&await this.removeAllTeamMailboxMembers(t,s),"mailingList"===l){const e=this.config.james_mailing_list_branch||[];if(e.length>0&&!this.isInAllowedBranches(t,e))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,newType:l,error:`Mailing list must be in allowed branches: ${e.join(", ")}`});await this.createMailingList(t,i,s)}else if("teamMailbox"===l){const e=this.config.james_mailing_list_branch||[];if(e.length>0&&this.isInAllowedBranches(t,e))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,newType:l,error:`Team mailbox cannot be in mailing list branches: ${e.join(", ")}`});await this.createTeamMailbox(t,i,s)}}getDisplayNameFromAttributes(t){const e=this.attributeToString(t[this.displayNameAttr]);if(e)return e;const i=this.attributeToString(t.cn);if(i)return i;const a=this.attributeToString(t.givenName),s=this.attributeToString(t.sn);if(a||s){const t=[];return a&&t.push(a),s&&t.push(s),t.join(" ")}const r=this.attributeToString(t[this.mailAttr]);return r||null}async getMailFromDN(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}async getDisplayNameFromDN(t){const e=[this.displayNameAttr,"cn","givenName","sn",this.mailAttr],i=await this.ldapGetAttributes(t,e);return i?this.getDisplayNameFromAttributes(i):null}async generateSignature(t){const e=this.config.james_signature_template;if(!e)return null;const i=await this.ldapGetAttributes(t);if(!i)return null;let a=e;return a=a.replace(/\{(\w+)\}/g,(t,e)=>"string"==typeof e&&i&&"object"==typeof i&&Object.prototype.hasOwnProperty.call(i,e)&&this.attributeToString(i[e])||""),a}async updateJamesIdentity(e,i,a){const s={plugin:this.name,event:"onLdapDisplayNameChange",result:"error",dn:e,mail:i,displayName:a};try{const r=`${this.webadminUrl}/jmap/identities/${i}`,n=await this.requestLimit(()=>t(r,{method:"GET",headers:this.createHeaders()}));if(!n.ok)return void this.logger.error({...s,step:"get_identities",http_status:n.status,http_status_text:n.statusText});const l=await n.json(),o=l.find(t=>t.email===i)||l[0];if(!o)return void this.logger.warn({...s,step:"find_identity",message:"No identity found for user"});const m=await this.generateSignature(e),d=`${this.webadminUrl}/jmap/identities/${i}/${o.id}`,h={id:o.id,email:o.email,name:a};m&&(h.htmlSignature=m);const u=await this.requestLimit(()=>t(d,{method:"PUT",headers:this.createHeaders("application/json"),body:JSON.stringify(h)}));u.ok?this.logger.info({...s,result:"success",http_status:u.status}):this.logger.error({...s,step:"update_identity",http_status:u.status,http_status_text:u.statusText})}catch(t){this.logger.error({...s,error:`${t}`})}}async deleteUserData(e,i){const a={plugin:this.name,event:"deleteUserData",mail:e,fromStep:i};try{const s=new URL(`${this.webadminUrl}/users/${e}`);s.searchParams.set("action","deleteData"),i&&s.searchParams.set("fromStep",i);const r=await this.requestLimit(()=>t(s.toString(),{method:"POST",headers:this.createHeaders()}));if(!r.ok)return this.logger.error({...a,http_status:r.status,http_status_text:r.statusText}),null;const n=await r.json();return this.logger.info({...a,http_status:r.status,taskId:n.taskId}),n}catch(t){return this.logger.error({...a,error:`${t}`}),null}}isAttributesList(t){return void 0!==t&&"object"==typeof t&&!Array.isArray(t)}getMailboxType(t){const e=t[this.mailboxTypeAttr];if(!e)return null;const i=(Array.isArray(e)?String(e[0]):String(e)).match(/^cn=([^,]+)/);if(!i)return null;const a=i[1];return"group"===a||"mailingList"===a||"teamMailbox"===a?a:null}isInAllowedBranches(t,e){return!e||0===e.length||e.some(e=>t===e||t.endsWith(","+e))}async createMailingList(t,e,i){const a=e.member||[],s=Array.isArray(a)?a:[a];this.logger.debug(`Creating mailing list ${i} in James with ${s.length} members`);const r=await this.getMemberEmails(s);await Promise.all(r.map(e=>this.callWebAdminApi("ldapgroupadddone:mailingList",`${this.webadminUrl}/address/groups/${i}/${e}`,"PUT",t,null,{groupMail:i,memberMail:e})))}async createTeamMailbox(t,e,i){const a=this.extractMailDomain(i);if(!a)return void this.logger.error({plugin:this.name,event:"createTeamMailbox",dn:t,mail:i,error:"Cannot extract domain from mail address"});const s=e.member||[],r=Array.isArray(s)?s:[s];this.logger.debug(`Creating team mailbox ${i} in James with ${r.length} members`);const n=await this.getMemberEmails(r),l=[];l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:create",`${this.webadminUrl}/domains/${a}/team-mailboxes/${i}`,"PUT",t,null,{domain:a,teamMailbox:i}));for(const e of n)l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:addMember",`${this.webadminUrl}/domains/${a}/team-mailboxes/${i}/members/${e}`,"PUT",t,null,{domain:a,teamMailbox:i,memberMail:e}));await Promise.all(l)}async deleteTeamMailbox(t,e){const i=this.extractMailDomain(e);i?await this.callWebAdminApi("ldapgroupdeletedone:teamMailbox",`${this.webadminUrl}/domains/${i}/team-mailboxes/${e}`,"DELETE",t,null,{domain:i,teamMailbox:e}):this.logger.error({plugin:this.name,event:"deleteTeamMailbox",dn:t,mail:e,error:"Cannot extract domain from mail address"})}async removeAllTeamMailboxMembers(t,e){const i=this.extractMailDomain(e);if(i)try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:["member"]},t);if(a.searchEntries&&a.searchEntries.length>0){const s=a.searchEntries[0].member||[],r=Array.isArray(s)?s:[s],n=await this.getMemberEmails(r);await Promise.all(n.map(a=>this.callWebAdminApi("removeAllTeamMailboxMembers",`${this.webadminUrl}/domains/${i}/team-mailboxes/${e}/members/${a}`,"DELETE",t,null,{domain:i,teamMailbox:e,memberMail:a}))),this.logger.info({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:t,teamMailbox:e,membersRemoved:n.length,message:"All members removed from team mailbox (mailbox preserved)"})}}catch(t){const i=t instanceof Error?t.message:String(t);this.logger.error(`Failed to remove members from team mailbox ${e}: ${i}`)}else this.logger.error({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:t,mail:e,error:"Cannot extract domain from mail address"})}async deleteMailingList(t,e){await this.callWebAdminApi("deleteMailingList",`${this.webadminUrl}/address/groups/${e}`,"DELETE",t,null,{groupMail:e})}async getMemberEmails(t){const e=t.filter(t=>t!==this.config.group_dummy_user).map(t=>this.server.ldap.queryLimit(async()=>{const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}));return(await Promise.all(e)).filter(t=>null!==t)}async getGroupMail(t){const e=await this.ldapGetAttributes(t,["mail"]);return e?this.attributeToString(e.mail):null}async getGroupMailboxType(t){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.mailboxTypeAttr]},t);if(e.searchEntries&&e.searchEntries.length>0)return this.getMailboxType(e.searchEntries[0])}catch(e){const i=e instanceof Error?e.message:String(e);this.logger.debug(`Could not get mailbox type for group ${t}: ${i}`)}return null}async _handleDelegationChange(t,e){const i=await this.ldapGetAttributes(t,[this.mailAttr]);if(!i)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"Could not find user entry to get mail attribute"});const a=i[this.mailAttr];if(!a||"string"!=typeof a)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"User has no mail attribute, cannot manage delegation"});const s=this.config.delegation_attribute;if(!s)return;const[r,n]=e[s]||[],l=this._normalizeToArray(r),o=this._normalizeToArray(n),m=o.filter(t=>!l.includes(t)),d=l.filter(t=>!o.includes(t)),h=m.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),u=d.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),[g,c]=await Promise.all([Promise.all(h),Promise.all(u)]),p=[];for(const{dn:e,email:i}of g)i&&p.push(this.callWebAdminApi("onLdapChange:addDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${i}`,"PUT",t,null,{userMail:a,delegateEmail:i,delegateDN:e,action:"add"}));for(const{dn:e,email:i}of c)i&&p.push(this.callWebAdminApi("onLdapChange:removeDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${i}`,"DELETE",t,null,{userMail:a,delegateEmail:i,delegateDN:e,action:"remove"}));await Promise.all(p)}async _getDelegateEmail(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):(this.logger.warn({plugin:this.name,event:"getDelegateEmail",dn:t,message:"Could not resolve delegate DN to email"}),null)}_normalizeToArray(t){return t?Array.isArray(t)?t:[t]:[]}}export{James as default};
1
+ import t from"node-fetch";import{TwakePlugin as e}from"../../abstract/twakePlugin.js";import{asyncHandler as i,escapeLdapFilter as a}from"../../lib/utils.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";class James extends e{constructor(t){super(t,"james_webadmin_url","james_webadmin_token","james_concurrency"),this.name="james",this.roles=["consistency","api"],this.dependencies={onLdapChange:"core/ldap/onChange",ldapGroups:"core/ldap/groups"},this.hooks={ldapadddone:async t=>{const[e,i]=t,a=i[this.mailAttr],s=i[this.quotaAttr],r=i[this.aliasAttr];if(!a)return;const n=Array.isArray(a)?String(a[0]):String(a);if(this.initDelay>0&&await new Promise(t=>setTimeout(t,this.initDelay)),s){const t=Array.isArray(s)?Number(s[0]):Number(s);!isNaN(t)&&t>0&&await this.callWebAdminApi("ldapadddone:quota",`${this.webadminUrl}/quota/users/${n}/size`,"PUT",e,t.toString(),{mail:n,quota:t})}if(r){const t=this.getAliases(r);await Promise.all(t.map(t=>this.callWebAdminApi("ldapadddone:alias",`${this.webadminUrl}/address/aliases/${n}/sources/${t}`,"PUT",e,null,{mail:n,alias:t})))}const l=this.getDisplayNameFromAttributes(i);l&&await this.updateJamesIdentity(e,n,l)},onLdapMailChange:async(t,e,i)=>{const a=e?Array.isArray(e)?String(e[0]):String(e):null,s=i?Array.isArray(i)?String(i[0]):String(i):null;if(a)if(s){await this.callWebAdminApi("onLdapMailChange",`${this.webadminUrl}/users/${a}/rename/${s}?action=rename&force`,"POST",t,null,{oldmail:a,newmail:s});try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.aliasAttr]},t);if(e.searchEntries&&e.searchEntries.length>0){const i=this.getAliases(e.searchEntries[0][this.aliasAttr]);i.length>0&&await Promise.all([...i.map(e=>this.callWebAdminApi("onLdapMailChange-delete",`${this.webadminUrl}/address/aliases/${a}/sources/${e}`,"DELETE",t,null,{oldmail:a,alias:e})),...i.map(e=>this.callWebAdminApi("onLdapMailChange-create",`${this.webadminUrl}/address/aliases/${s}/sources/${e}`,"PUT",t,null,{newmail:s,alias:e}))])}}catch(t){this.logger.debug("Could not fetch aliases for mail change:",t)}}else this.logger.debug(`Skipping mail rename for ${t}: newmail is empty (mail attribute was deleted, not changed)`);else this.logger.debug(`Skipping mail rename for ${t}: oldmail is empty (mail attribute was added, not changed)`)},onLdapAliasChange:async(t,e,i,a)=>{const s=i.map(t=>this.normalizeAlias(t)),r=a.map(t=>this.normalizeAlias(t)),n=s.filter(t=>!r.includes(t)),l=r.filter(t=>!s.includes(t));await Promise.all([...n.map(i=>this.callWebAdminApi("onLdapAliasChange-delete",`${this.webadminUrl}/address/aliases/${e}/sources/${i}`,"DELETE",t,null,{mail:e,alias:i,action:"delete"})),...l.map(i=>this.callWebAdminApi("onLdapAliasChange-add",`${this.webadminUrl}/address/aliases/${e}/sources/${i}`,"PUT",t,null,{mail:e,alias:i,action:"add"}))])},onLdapQuotaChange:(t,e,i,a)=>this.callWebAdminApi("onLdapQuotaChange",`${this.webadminUrl}/quota/users/${e}/size`,"PUT",t,a.toString(),{oldQuota:i,newQuota:a}),onLdapForwardChange:async(t,e,i,a)=>{const s=this.extractMailDomain(e);if(!s)return void this.logger.error(`Cannot extract domain from mail ${e} for forward management`);const r=i.filter(t=>!a.includes(t)),n=a.filter(t=>!i.includes(t));await Promise.all([...r.map(i=>this.callWebAdminApi("onLdapForwardChange-delete",`${this.webadminUrl}/address/forwards/${e}/targets/${i}`,"DELETE",t,null,{mail:e,forward:i,domain:s,action:"delete"})),...n.map(i=>this.callWebAdminApi("onLdapForwardChange-add",`${this.webadminUrl}/address/forwards/${e}/targets/${i}`,"PUT",t,null,{mail:e,forward:i,domain:s,action:"add"}))])},onLdapChange:async(t,e)=>{this.config.delegation_attribute&&e[this.config.delegation_attribute]&&await this._handleDelegationChange(t,e)},onLdapDisplayNameChange:async t=>{const e=[this.mailAttr,this.displayNameAttr,"cn","givenName","sn"],i=await this.ldapGetAttributes(t,e);if(!i)return void this.logger.warn(`Cannot update James identity: entry not found for ${t}`);const a=this.attributeToString(i[this.mailAttr]);if(!a)return void this.logger.warn(`Cannot update James identity: no mail found for ${t}`);const s=this.getDisplayNameFromAttributes(i);if(s)return this.updateJamesIdentity(t,a,s);this.logger.warn(`Cannot update James identity: no display name found for ${t}`)},ldapgroupadddone:async t=>{const[e,i]=t,a=i.mail;if(!a)return void this.logger.debug(`Group ${e} has no mail attribute, skipping James sync`);const s=this.getMailboxType(i),r=Array.isArray(a)?String(a[0]):String(a);if("teamMailbox"===s){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&this.isInAllowedBranches(e,t))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:e,mailboxType:"teamMailbox",error:`Team mailbox cannot be in mailing list branches: ${t.join(", ")}`});await this.createTeamMailbox(e,i,r)}else if("mailingList"===s||null===s){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&!this.isInAllowedBranches(e,t))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:e,mailboxType:s||"mailingList (default)",error:`Mailing list must be in allowed branches: ${t.join(", ")}`});await this.createMailingList(e,i,r)}},ldapgroupmodifydone:async t=>{const[e,i]=t;if(i.replace?.[this.mailboxTypeAttr]||i.add?.[this.mailboxTypeAttr])return void await this.handleMailboxTypeTransition(e,i);const a=await this.getGroupMail(e);if(!a)return void this.logger.debug(`Group ${e} has no mail attribute, skipping James sync`);const s="teamMailbox"===await this.getGroupMailboxType(e),r=s?this.extractMailDomain(a):null,n=[];if(i.add?.member){const t=i.add.member,l=Array.isArray(t)?t:[t],o=await this.getMemberEmails(l);n.push(...o.map(t=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${t}`,"PUT",e,null,{domain:r,groupMail:a,memberMail:t,action:"add"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${t}`,"PUT",e,null,{groupMail:a,memberMail:t,action:"add"})))}if(this.isAttributesList(i.delete)){const t=i.delete.member;if(t){const i=Array.isArray(t)?t:[t],l=await this.getMemberEmails(i);n.push(...l.map(t=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${t}`,"DELETE",e,null,{domain:r,groupMail:a,memberMail:t,action:"delete"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${t}`,"DELETE",e,null,{groupMail:a,memberMail:t,action:"delete"})))}}await Promise.all(n)},ldapgroupdeletedone:async t=>{const e=await this.getGroupMail(t);if(!e)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);"teamMailbox"===await this.getGroupMailboxType(t)?await this.removeAllTeamMailboxMembers(t,e):await this.callWebAdminApi("ldapgroupdeletedone:mailingList",`${this.webadminUrl}/address/groups/${e}`,"DELETE",t,null,{groupMail:e})}},this.quotaAttr=this.config.quota_attribute||"mailQuotaSize",this.aliasAttr=this.config.alias_attribute||"mailAlternateAddress",this.mailboxTypeAttr=this.config.james_mailbox_type_attribute||"twakeMailboxType",this.initDelay="number"==typeof this.config.james_init_delay?this.config.james_init_delay:1e3}api(t){const e=this.config.api_prefix||"/api";t.get(`${e}/v1/users/:user/quota-usage`,i((t,e)=>this.getUserQuota(t,e))),this.logger.info(`James API registered at ${e}/v1/users/:user/quota-usage`)}async getUserQuota(e,i){if(!s(e,i))return;const r=e.params.user;try{const e=a(r),s=await this.server.ldap.search({scope:"sub",filter:`(uid=${e})`,paged:!1,attributes:[this.mailAttr]},this.config.ldap_base||""),n=s.searchEntries?.[0];if(!n)return void i.status(404).json({error:`User ${r} not found`});const l=n[this.mailAttr];if(!l)return void i.status(400).json({error:`User ${r} has no mail attribute`});const o=Array.isArray(l)?String(l[0]):String(l),m=`${this.webadminUrl}/quota/users/${o}`,d=await this.requestLimit(()=>t(m,{method:"GET",headers:this.createHeaders()}));if(!d.ok)return void(404===d.status?i.status(404).json({error:`Quota not found for user ${r}`}):i.status(502).json({error:`Failed to retrieve quota from James: ${d.statusText}`}));const h=await d.json();i.json(h)}catch(t){this.logger.error({plugin:this.name,event:"getUserQuota",username:r,error:t instanceof Error?t.message:String(t)}),i.status(500).json({error:"Internal server error while retrieving quota"})}}async handleMailboxTypeTransition(t,e){const i=await this.ldapGetAttributes(t,["mail","member",this.mailboxTypeAttr]);if(!i)return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,error:"Could not fetch group attributes"});const a=i.mail;if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping mailbox type transition`);const s=Array.isArray(a)?String(a[0]):String(a),r=e.replace?.[this.mailboxTypeAttr]||e.add?.[this.mailboxTypeAttr],n=Array.isArray(r)?String(r[0]):String(r),l=this.getMailboxType({[this.mailboxTypeAttr]:n}),o=this.getMailboxType(i);if(this.logger.info({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,mail:s,oldType:o||"none",newType:l||"none"}),o!==l)if("mailingList"===o?await this.deleteMailingList(t,s):"teamMailbox"===o&&await this.removeAllTeamMailboxMembers(t,s),"mailingList"===l){const e=this.config.james_mailing_list_branch||[];if(e.length>0&&!this.isInAllowedBranches(t,e))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,newType:l,error:`Mailing list must be in allowed branches: ${e.join(", ")}`});await this.createMailingList(t,i,s)}else if("teamMailbox"===l){const e=this.config.james_mailing_list_branch||[];if(e.length>0&&this.isInAllowedBranches(t,e))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:t,newType:l,error:`Team mailbox cannot be in mailing list branches: ${e.join(", ")}`});await this.createTeamMailbox(t,i,s)}}getDisplayNameFromAttributes(t){const e=this.attributeToString(t[this.displayNameAttr]);if(e)return e;const i=this.attributeToString(t.cn);if(i)return i;const a=this.attributeToString(t.givenName),s=this.attributeToString(t.sn);if(a||s){const t=[];return a&&t.push(a),s&&t.push(s),t.join(" ")}const r=this.attributeToString(t[this.mailAttr]);return r||null}async getMailFromDN(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}async getDisplayNameFromDN(t){const e=[this.displayNameAttr,"cn","givenName","sn",this.mailAttr],i=await this.ldapGetAttributes(t,e);return i?this.getDisplayNameFromAttributes(i):null}async generateSignature(t){const e=this.config.james_signature_template;if(!e)return null;const i=await this.ldapGetAttributes(t);if(!i)return null;let a=e;return a=a.replace(/\{(\w+)\}/g,(t,e)=>"string"==typeof e&&i&&"object"==typeof i&&Object.prototype.hasOwnProperty.call(i,e)&&this.attributeToString(i[e])||""),a}async updateJamesIdentity(e,i,a){const s={plugin:this.name,event:"onLdapDisplayNameChange",result:"error",dn:e,mail:i,displayName:a};try{const r=`${this.webadminUrl}/users/${i}/identities`,n=await this.requestLimit(()=>t(r,{method:"GET",headers:this.createHeaders()}));if(!n.ok)return void this.logger.error({...s,step:"get_identities",http_status:n.status,http_status_text:n.statusText});const l=await n.json(),o=l.find(t=>t.email===i)||l[0];if(!o)return void this.logger.warn({...s,step:"find_identity",message:"No identity found for user"});const m=await this.generateSignature(e),d=`${this.webadminUrl}/users/${i}/identities/${o.id}`,h={id:o.id,email:o.email,name:a};m&&(h.htmlSignature=m);const u=await this.requestLimit(()=>t(d,{method:"PUT",headers:this.createHeaders("application/json"),body:JSON.stringify(h)}));u.ok?this.logger.info({...s,result:"success",http_status:u.status}):this.logger.error({...s,step:"update_identity",http_status:u.status,http_status_text:u.statusText})}catch(t){this.logger.error({...s,error:`${t}`})}}async deleteUserData(e,i){const a={plugin:this.name,event:"deleteUserData",mail:e,fromStep:i};try{const s=new URL(`${this.webadminUrl}/users/${e}`);s.searchParams.set("action","deleteData"),i&&s.searchParams.set("fromStep",i);const r=await this.requestLimit(()=>t(s.toString(),{method:"POST",headers:this.createHeaders()}));if(!r.ok)return this.logger.error({...a,http_status:r.status,http_status_text:r.statusText}),null;const n=await r.json();return this.logger.info({...a,http_status:r.status,taskId:n.taskId}),n}catch(t){return this.logger.error({...a,error:`${t}`}),null}}isAttributesList(t){return void 0!==t&&"object"==typeof t&&!Array.isArray(t)}getMailboxType(t){const e=t[this.mailboxTypeAttr];if(!e)return null;const i=(Array.isArray(e)?String(e[0]):String(e)).match(/^cn=([^,]+)/);if(!i)return null;const a=i[1];return"group"===a||"mailingList"===a||"teamMailbox"===a?a:null}isInAllowedBranches(t,e){return!e||0===e.length||e.some(e=>t===e||t.endsWith(","+e))}async createMailingList(t,e,i){const a=e.member||[],s=Array.isArray(a)?a:[a];this.logger.debug(`Creating mailing list ${i} in James with ${s.length} members`);const r=await this.getMemberEmails(s);await Promise.all(r.map(e=>this.callWebAdminApi("ldapgroupadddone:mailingList",`${this.webadminUrl}/address/groups/${i}/${e}`,"PUT",t,null,{groupMail:i,memberMail:e})))}async createTeamMailbox(t,e,i){const a=this.extractMailDomain(i);if(!a)return void this.logger.error({plugin:this.name,event:"createTeamMailbox",dn:t,mail:i,error:"Cannot extract domain from mail address"});const s=e.member||[],r=Array.isArray(s)?s:[s];this.logger.debug(`Creating team mailbox ${i} in James with ${r.length} members`);const n=await this.getMemberEmails(r),l=[];l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:create",`${this.webadminUrl}/domains/${a}/team-mailboxes/${i}`,"PUT",t,null,{domain:a,teamMailbox:i}));for(const e of n)l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:addMember",`${this.webadminUrl}/domains/${a}/team-mailboxes/${i}/members/${e}`,"PUT",t,null,{domain:a,teamMailbox:i,memberMail:e}));await Promise.all(l)}async deleteTeamMailbox(t,e){const i=this.extractMailDomain(e);i?await this.callWebAdminApi("ldapgroupdeletedone:teamMailbox",`${this.webadminUrl}/domains/${i}/team-mailboxes/${e}`,"DELETE",t,null,{domain:i,teamMailbox:e}):this.logger.error({plugin:this.name,event:"deleteTeamMailbox",dn:t,mail:e,error:"Cannot extract domain from mail address"})}async removeAllTeamMailboxMembers(t,e){const i=this.extractMailDomain(e);if(i)try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:["member"]},t);if(a.searchEntries&&a.searchEntries.length>0){const s=a.searchEntries[0].member||[],r=Array.isArray(s)?s:[s],n=await this.getMemberEmails(r);await Promise.all(n.map(a=>this.callWebAdminApi("removeAllTeamMailboxMembers",`${this.webadminUrl}/domains/${i}/team-mailboxes/${e}/members/${a}`,"DELETE",t,null,{domain:i,teamMailbox:e,memberMail:a}))),this.logger.info({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:t,teamMailbox:e,membersRemoved:n.length,message:"All members removed from team mailbox (mailbox preserved)"})}}catch(t){const i=t instanceof Error?t.message:String(t);this.logger.error(`Failed to remove members from team mailbox ${e}: ${i}`)}else this.logger.error({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:t,mail:e,error:"Cannot extract domain from mail address"})}async deleteMailingList(t,e){await this.callWebAdminApi("deleteMailingList",`${this.webadminUrl}/address/groups/${e}`,"DELETE",t,null,{groupMail:e})}async getMemberEmails(t){const e=t.filter(t=>t!==this.config.group_dummy_user).map(t=>this.server.ldap.queryLimit(async()=>{const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}));return(await Promise.all(e)).filter(t=>null!==t)}async getGroupMail(t){const e=await this.ldapGetAttributes(t,["mail"]);return e?this.attributeToString(e.mail):null}async getGroupMailboxType(t){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.mailboxTypeAttr]},t);if(e.searchEntries&&e.searchEntries.length>0)return this.getMailboxType(e.searchEntries[0])}catch(e){const i=e instanceof Error?e.message:String(e);this.logger.debug(`Could not get mailbox type for group ${t}: ${i}`)}return null}async _handleDelegationChange(t,e){const i=await this.ldapGetAttributes(t,[this.mailAttr]);if(!i)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"Could not find user entry to get mail attribute"});const a=i[this.mailAttr];if(!a||"string"!=typeof a)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"User has no mail attribute, cannot manage delegation"});const s=this.config.delegation_attribute;if(!s)return;const[r,n]=e[s]||[],l=this._normalizeToArray(r),o=this._normalizeToArray(n),m=o.filter(t=>!l.includes(t)),d=l.filter(t=>!o.includes(t)),h=m.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),u=d.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),[g,c]=await Promise.all([Promise.all(h),Promise.all(u)]),p=[];for(const{dn:e,email:i}of g)i&&p.push(this.callWebAdminApi("onLdapChange:addDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${i}`,"PUT",t,null,{userMail:a,delegateEmail:i,delegateDN:e,action:"add"}));for(const{dn:e,email:i}of c)i&&p.push(this.callWebAdminApi("onLdapChange:removeDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${i}`,"DELETE",t,null,{userMail:a,delegateEmail:i,delegateDN:e,action:"remove"}));await Promise.all(p)}async _getDelegateEmail(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):(this.logger.warn({plugin:this.name,event:"getDelegateEmail",dn:t,message:"Could not resolve delegate DN to email"}),null)}_normalizeToArray(t){return t?Array.isArray(t)?t:[t]:[]}}export{James as default};
@@ -120,6 +120,8 @@ export interface Config {
120
120
  calendar_resource_objectclass?: string;
121
121
  calendar_resource_creator?: string;
122
122
  calendar_resource_domain?: string;
123
+ calendar_firstname_attribute?: string;
124
+ calendar_lastname_attribute?: string;
123
125
  cozy_admin_url?: string;
124
126
  cozy_admin_user?: string;
125
127
  cozy_admin_passphrase?: string;
@@ -2,18 +2,26 @@ import TwakePlugin from '../../abstract/twakePlugin';
2
2
  import { type Role } from '../../abstract/plugin';
3
3
  import { Hooks } from '../../hooks';
4
4
  /**
5
- * Plugin to sync LDAP resources with Twake Calendar
5
+ * Plugin to sync LDAP resources and users with Twake Calendar
6
6
  *
7
7
  * Monitors a LDAP branch for resources (meeting rooms, equipment, etc.)
8
- * and automatically creates/updates/deletes them in Twake Calendar via WebAdmin API
8
+ * and automatically creates/updates/deletes them in Twake Calendar via WebAdmin API.
9
+ *
10
+ * Also propagates user identity changes (email, first name, last name) to the
11
+ * Twake Calendar "registered users" via the WebAdmin API.
9
12
  */
10
13
  export default class CalendarResources extends TwakePlugin {
11
14
  name: string;
12
15
  roles: Role[];
16
+ dependencies: {
17
+ onLdapChange: string;
18
+ };
13
19
  private resourceBase;
14
20
  private resourceObjectClass;
15
21
  private resourceCreator;
16
22
  private resourceDomain;
23
+ private firstnameAttr;
24
+ private lastnameAttr;
17
25
  constructor(server: import('../../bin').DM);
18
26
  hooks: Hooks;
19
27
  /**
@@ -28,6 +36,21 @@ export default class CalendarResources extends TwakePlugin {
28
36
  * Extract resource ID from DN
29
37
  */
30
38
  private getResourceId;
39
+ /**
40
+ * Synchronize an LDAP user's identity (email, first and last name) to the
41
+ * Twake Calendar registered users via the WebAdmin API.
42
+ *
43
+ * Registered users are keyed by an internal id, and `GET /registeredUsers`
44
+ * exposes no filter, so we list all registered users, locate the entry by
45
+ * email, then `PATCH /registeredUsers?id={id}` with the LDAP values.
46
+ *
47
+ * @param event Hook name, used for logging
48
+ * @param dn LDAP DN of the user
49
+ * @param lookupEmail Email used to locate the existing registered user.
50
+ * Defaults to the user's current mail; pass the OLD mail when the email
51
+ * itself is changing (Calendar still holds the previous address).
52
+ */
53
+ syncRegisteredUser(event: string, dn: string, lookupEmail?: string): Promise<void>;
31
54
  /**
32
55
  * Delete user data from Twake Calendar WebAdmin API
33
56
  * Calls POST /users/{mail}?action=deleteData
@@ -67,6 +67,7 @@ export default class ClouderyProvision extends DmPlugin {
67
67
  private resolveRole;
68
68
  private isAtOrUnder;
69
69
  private pruneStash;
70
+ private normalizeUserName;
70
71
  private extractId;
71
72
  private extractPrimaryEmail;
72
73
  private extractPrimaryPhone;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"