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.
- package/bin/sync-james.mjs +262 -86
- package/dist/config/args.js +1 -1
- package/dist/plugins/twake/calendarResources.js +1 -1
- package/dist/plugins/twake/clouderyProvision.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/src/config/args.d.ts +2 -0
- package/dist/src/plugins/twake/calendarResources.d.ts +25 -2
- package/dist/src/plugins/twake/clouderyProvision.d.ts +1 -0
- package/package.json +1 -1
package/bin/sync-james.mjs
CHANGED
|
@@ -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
|
|
21
|
-
|
|
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
|
|
25
|
-
--dry-run, -n
|
|
26
|
-
--
|
|
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
|
-
|
|
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
|
|
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
|
|
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: `(
|
|
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
|
|
96
|
-
dm.logger.warn(`Skipping ${dn}: invalid mail
|
|
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
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
176
|
-
|
|
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
|
-
|
|
363
|
+
syncJames().catch(err => {
|
|
188
364
|
console.error('Fatal error:', err);
|
|
189
365
|
process.exit(1);
|
|
190
366
|
});
|
package/dist/config/args.js
CHANGED
|
@@ -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
|
|
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
|