emailengine-app 2.67.3 → 2.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yml +3 -0
- package/CHANGELOG.md +27 -0
- package/data/google-crawlers.json +1 -1
- package/lib/account.js +24 -1
- package/lib/api-routes/account-routes.js +12 -2
- package/lib/email-client/base-client.js +26 -20
- package/lib/email-client/gmail-client.js +14 -12
- package/lib/oauth/external-account-config.js +132 -0
- package/lib/oauth/external-account-signer.js +256 -0
- package/lib/oauth/gmail.js +113 -14
- package/lib/oauth/verify-app.js +397 -0
- package/lib/oauth2-apps.js +51 -6
- package/lib/routes-ui.js +153 -1
- package/lib/schemas.js +80 -2
- package/lib/settings.js +1 -0
- package/lib/tools.js +10 -10
- package/package.json +22 -22
- package/sbom.json +1 -1
- package/static/js/ace/ace.js +1 -1
- package/static/js/ace/ext-searchbox.js +1 -1
- package/static/js/ace/mode-handlebars.js +1 -1
- package/static/js/ace/mode-html.js +1 -1
- package/static/js/ace/mode-javascript.js +1 -1
- package/static/js/ace/mode-markdown.js +1 -1
- package/static/js/ace/worker-html.js +1 -1
- package/static/js/ace/worker-javascript.js +1 -1
- package/static/js/ace/worker-json.js +1 -1
- package/static/licenses.html +226 -66
- package/translations/messages.pot +13 -13
- package/views/config/oauth/app.hbs +224 -0
- package/views/config/oauth/edit.hbs +69 -0
- package/views/config/oauth/new.hbs +69 -0
- package/views/partials/oauth_form.hbs +99 -32
- package/workers/api.js +91 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
msgid ""
|
|
2
2
|
msgstr ""
|
|
3
3
|
"Content-Type: text/plain; charset=ascii\n"
|
|
4
|
-
"POT-Creation-Date: 2026-04-
|
|
4
|
+
"POT-Creation-Date: 2026-04-29 10:14+0000\n"
|
|
5
5
|
|
|
6
6
|
#: views/error.hbs:4
|
|
7
7
|
#: workers/api.js:7103
|
|
@@ -27,10 +27,6 @@ msgid_plural "%d days"
|
|
|
27
27
|
msgstr[0] ""
|
|
28
28
|
msgstr[1] ""
|
|
29
29
|
|
|
30
|
-
#: views/redirect.hbs:1
|
|
31
|
-
msgid "Click <a href=\"%s\">here</a> to continue…"
|
|
32
|
-
msgstr ""
|
|
33
|
-
|
|
34
30
|
#: views/oauth-scope-error.hbs:2
|
|
35
31
|
msgid "Insufficient Permissions"
|
|
36
32
|
msgstr ""
|
|
@@ -55,6 +51,10 @@ msgstr ""
|
|
|
55
51
|
msgid "Try Again"
|
|
56
52
|
msgstr ""
|
|
57
53
|
|
|
54
|
+
#: views/redirect.hbs:1
|
|
55
|
+
msgid "Click <a href=\"%s\">here</a> to continue…"
|
|
56
|
+
msgstr ""
|
|
57
|
+
|
|
58
58
|
#: views/unsubscribe.hbs:3
|
|
59
59
|
#: views/unsubscribe.hbs:62
|
|
60
60
|
#: views/unsubscribe.hbs:85
|
|
@@ -104,14 +104,6 @@ msgstr ""
|
|
|
104
104
|
msgid "Enter your email address"
|
|
105
105
|
msgstr ""
|
|
106
106
|
|
|
107
|
-
#: views/accounts/register/index.hbs:2
|
|
108
|
-
msgid "Choose your email account provider"
|
|
109
|
-
msgstr ""
|
|
110
|
-
|
|
111
|
-
#: views/accounts/register/index.hbs:15
|
|
112
|
-
msgid "Standard IMAP"
|
|
113
|
-
msgstr ""
|
|
114
|
-
|
|
115
107
|
#: views/accounts/register/imap.hbs:11
|
|
116
108
|
msgid "Your name"
|
|
117
109
|
msgstr ""
|
|
@@ -134,6 +126,14 @@ msgstr ""
|
|
|
134
126
|
msgid "Continue"
|
|
135
127
|
msgstr ""
|
|
136
128
|
|
|
129
|
+
#: views/accounts/register/index.hbs:2
|
|
130
|
+
msgid "Choose your email account provider"
|
|
131
|
+
msgstr ""
|
|
132
|
+
|
|
133
|
+
#: views/accounts/register/index.hbs:15
|
|
134
|
+
msgid "Standard IMAP"
|
|
135
|
+
msgstr ""
|
|
136
|
+
|
|
137
137
|
#: views/accounts/register/imap-server.hbs:19
|
|
138
138
|
msgid "IMAP"
|
|
139
139
|
msgstr ""
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
<i class="fas fa-user-edit fa-fw"></i> Edit app
|
|
34
34
|
</a>
|
|
35
35
|
|
|
36
|
+
<button type="button" class="btn btn-light" data-toggle="modal" data-target="#verifySetupModal"
|
|
37
|
+
title="Run live checks against the provider" id="verify-btn" data-placement="top">
|
|
38
|
+
<i class="fas fa-clipboard-check fa-fw"></i> Verify setup
|
|
39
|
+
</button>
|
|
40
|
+
|
|
36
41
|
<button type="button" class="btn btn-light" data-toggle="modal" data-target="#deleteModal"
|
|
37
42
|
title="Delete this application" id="delete-btn" data-placement="top">
|
|
38
43
|
<i class="fas fa-trash-alt fa-fw"></i> Delete app
|
|
@@ -40,6 +45,16 @@
|
|
|
40
45
|
|
|
41
46
|
</div>
|
|
42
47
|
|
|
48
|
+
{{#if canAddServiceAccount}}
|
|
49
|
+
<div class="btn-group ml-auto mb-1" role="group" aria-label="Account actions">
|
|
50
|
+
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addServiceAccountModal"
|
|
51
|
+
title="Register an email account that authenticates through this service application"
|
|
52
|
+
id="add-service-account-btn" data-placement="top">
|
|
53
|
+
<i class="fas fa-user-plus fa-fw"></i> Add account
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
{{/if}}
|
|
57
|
+
|
|
43
58
|
</div>
|
|
44
59
|
|
|
45
60
|
{{#if app.description}}
|
|
@@ -164,6 +179,22 @@
|
|
|
164
179
|
<dd class="col-sm-9 text-monospace"><small>*****</small></dd>
|
|
165
180
|
{{/if}}
|
|
166
181
|
|
|
182
|
+
{{#if appShowAuthMethod}}
|
|
183
|
+
<dt class="col-sm-3">Authentication method</dt>
|
|
184
|
+
<dd class="col-sm-9">
|
|
185
|
+
{{#if authMethodIsExternalAccount}}
|
|
186
|
+
Workload Identity Federation
|
|
187
|
+
{{else}}
|
|
188
|
+
Service account key
|
|
189
|
+
{{/if}}
|
|
190
|
+
</dd>
|
|
191
|
+
{{/if}}
|
|
192
|
+
|
|
193
|
+
{{#if app.externalAccount}}
|
|
194
|
+
<dt class="col-sm-3">External account config</dt>
|
|
195
|
+
<dd class="col-sm-9 text-monospace"><small>***** (set)</small></dd>
|
|
196
|
+
{{/if}}
|
|
197
|
+
|
|
167
198
|
{{#if app.redirectUrl}}
|
|
168
199
|
<dt class="col-sm-3">Redirect URL</dt>
|
|
169
200
|
<dd class="col-sm-9 text-monospace"><small>{{app.redirectUrl}}</small></dd>
|
|
@@ -337,6 +368,199 @@
|
|
|
337
368
|
</div>
|
|
338
369
|
</div>
|
|
339
370
|
|
|
371
|
+
<div class="modal fade" id="verifySetupModal" tabindex="-1" aria-labelledby="verifySetupModalLabel" aria-hidden="true">
|
|
372
|
+
<div class="modal-dialog modal-lg">
|
|
373
|
+
<div class="modal-content">
|
|
374
|
+
<div class="modal-header">
|
|
375
|
+
<h5 class="modal-title" id="verifySetupModalLabel">Verify OAuth2 setup</h5>
|
|
376
|
+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
377
|
+
<span aria-hidden="true">×</span>
|
|
378
|
+
</button>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="modal-body">
|
|
381
|
+
<input type="hidden" id="verify-crumb" value="{{crumb}}" />
|
|
382
|
+
<p><small class="text-muted">Runs the provider authentication chain step by step and reports what passes
|
|
383
|
+
or fails.{{#if appShowAuthMethod}} Enter a mailbox address in your Workspace domain to also verify
|
|
384
|
+
domain-wide delegation and live mailbox access.{{/if}}</small></p>
|
|
385
|
+
|
|
386
|
+
<div class="form-group">
|
|
387
|
+
<div class="input-group">
|
|
388
|
+
<input type="email" class="form-control" id="verify-account"
|
|
389
|
+
placeholder="mailbox@example.com (optional)" autocomplete="off" />
|
|
390
|
+
<div class="input-group-append">
|
|
391
|
+
<button type="button" class="btn btn-primary" id="verify-run-btn">
|
|
392
|
+
<i class="fas fa-play fa-fw"></i> Run test
|
|
393
|
+
</button>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
<small class="form-text text-muted">Leave empty to check configuration and signing only.</small>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
<div id="verify-pending" class="d-none">
|
|
400
|
+
<p><i class="fas fa-spinner fa-spin fa-sm"></i> Running checks, please wait…</p>
|
|
401
|
+
</div>
|
|
402
|
+
<div id="verify-error" class="d-none">
|
|
403
|
+
<small class="alert alert-danger text-monospace error-message" style="display: block;"></small>
|
|
404
|
+
</div>
|
|
405
|
+
<div id="verify-verdict" class="d-none mb-3"></div>
|
|
406
|
+
<ul id="verify-steps" class="list-group"></ul>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="modal-footer">
|
|
409
|
+
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
{{#if canAddServiceAccount}}
|
|
416
|
+
<div class="modal fade" id="addServiceAccountModal" tabindex="-1" aria-labelledby="addServiceAccountLabel"
|
|
417
|
+
aria-hidden="true">
|
|
418
|
+
<div class="modal-dialog">
|
|
419
|
+
<div class="modal-content">
|
|
420
|
+
<div class="modal-header">
|
|
421
|
+
<h5 class="modal-title" id="addServiceAccountLabel">Add an account</h5>
|
|
422
|
+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
423
|
+
<span aria-hidden="true">×</span>
|
|
424
|
+
</button>
|
|
425
|
+
</div>
|
|
426
|
+
<form method="post" action="/admin/config/oauth/app/{{app.id}}/add-account">
|
|
427
|
+
<input type="hidden" name="crumb" value="{{crumb}}">
|
|
428
|
+
<div class="modal-body">
|
|
429
|
+
<p><small class="text-muted">This service application authenticates without an interactive consent
|
|
430
|
+
screen, so the account is registered directly. The mailbox must be reachable with the
|
|
431
|
+
application's service credentials.</small></p>
|
|
432
|
+
<div class="form-group">
|
|
433
|
+
<label for="service-account-name" class="col-form-label">Full name:</label>
|
|
434
|
+
<input type="text" class="form-control" id="service-account-name" name="name"
|
|
435
|
+
placeholder="e.g., John Smith">
|
|
436
|
+
</div>
|
|
437
|
+
<div class="form-group">
|
|
438
|
+
<label for="service-account-email" class="col-form-label">Email address:</label>
|
|
439
|
+
<input type="email" class="form-control" id="service-account-email" name="email"
|
|
440
|
+
placeholder="e.g., user@example.com" required>
|
|
441
|
+
</div>
|
|
442
|
+
<div class="form-group">
|
|
443
|
+
<label for="service-account-id" class="col-form-label">Account identifier (optional):</label>
|
|
444
|
+
<input type="text" class="form-control" id="service-account-id" name="account"
|
|
445
|
+
placeholder="e.g., account_123">
|
|
446
|
+
<small class="form-text text-muted">Leave blank to auto-generate. Existing accounts with this ID
|
|
447
|
+
will be updated.</small>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
<div class="modal-footer">
|
|
451
|
+
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
|
452
|
+
<button type="submit" class="btn btn-primary btn-icon-split">
|
|
453
|
+
<span class="icon text-white-50">
|
|
454
|
+
<i class="fas fa-user-plus"></i>
|
|
455
|
+
</span>
|
|
456
|
+
<span class="text">Add account</span>
|
|
457
|
+
</button>
|
|
458
|
+
</div>
|
|
459
|
+
</form>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<script>
|
|
465
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
466
|
+
$('#addServiceAccountModal').on('shown.bs.modal', () => {
|
|
467
|
+
document.getElementById('service-account-name').focus();
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
</script>
|
|
471
|
+
{{/if}}
|
|
472
|
+
|
|
473
|
+
<script>
|
|
474
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
475
|
+
const appId = "{{app.id}}";
|
|
476
|
+
const stepsElm = document.getElementById('verify-steps');
|
|
477
|
+
const pendingElm = document.getElementById('verify-pending');
|
|
478
|
+
const errorElm = document.getElementById('verify-error');
|
|
479
|
+
const verdictElm = document.getElementById('verify-verdict');
|
|
480
|
+
const runBtn = document.getElementById('verify-run-btn');
|
|
481
|
+
const accountInput = document.getElementById('verify-account');
|
|
482
|
+
|
|
483
|
+
const ICONS = {
|
|
484
|
+
ok: '<i class="fas fa-check-circle text-success fa-fw"></i>',
|
|
485
|
+
fail: '<i class="fas fa-times-circle text-danger fa-fw"></i>',
|
|
486
|
+
skip: '<i class="fas fa-minus-circle text-muted fa-fw"></i>'
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const esc = str => {
|
|
490
|
+
let div = document.createElement('div');
|
|
491
|
+
div.textContent = str == null ? '' : String(str);
|
|
492
|
+
return div.innerHTML;
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const renderResult = data => {
|
|
496
|
+
stepsElm.innerHTML = '';
|
|
497
|
+
(data.steps || []).forEach(step => {
|
|
498
|
+
let li = document.createElement('li');
|
|
499
|
+
li.className = 'list-group-item';
|
|
500
|
+
let html = `${ICONS[step.status] || ''} <strong>${esc(step.label)}</strong>`;
|
|
501
|
+
if (step.message) {
|
|
502
|
+
html += `<div class="small text-muted ml-4">${esc(step.message)}</div>`;
|
|
503
|
+
}
|
|
504
|
+
if (step.hint) {
|
|
505
|
+
html += `<div class="small text-info ml-4"><i class="fas fa-lightbulb fa-fw"></i> ${esc(step.hint)}</div>`;
|
|
506
|
+
}
|
|
507
|
+
li.innerHTML = html;
|
|
508
|
+
stepsElm.appendChild(li);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
let failed = (data.steps || []).some(s => s.status === 'fail');
|
|
512
|
+
verdictElm.className = 'mb-3 alert ' + (failed ? 'alert-danger' : 'alert-success');
|
|
513
|
+
verdictElm.textContent = failed
|
|
514
|
+
? 'Setup has problems - review the failing step(s) below.'
|
|
515
|
+
: 'Setup verified - all checks passed.';
|
|
516
|
+
verdictElm.classList.remove('d-none');
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const runVerify = async account => {
|
|
520
|
+
errorElm.classList.add('d-none');
|
|
521
|
+
verdictElm.classList.add('d-none');
|
|
522
|
+
stepsElm.innerHTML = '';
|
|
523
|
+
pendingElm.classList.remove('d-none');
|
|
524
|
+
runBtn.disabled = true;
|
|
525
|
+
try {
|
|
526
|
+
let res = await fetch(`/admin/config/oauth/verify/${encodeURIComponent(appId)}`, {
|
|
527
|
+
method: 'post',
|
|
528
|
+
headers: { 'content-type': 'application/json' },
|
|
529
|
+
body: JSON.stringify({
|
|
530
|
+
crumb: document.getElementById('verify-crumb').value,
|
|
531
|
+
account: account || '',
|
|
532
|
+
testConnection: true
|
|
533
|
+
})
|
|
534
|
+
});
|
|
535
|
+
let data = await res.json();
|
|
536
|
+
if (!res.ok) {
|
|
537
|
+
throw new Error(data.error || `HTTP ${res.status}`);
|
|
538
|
+
}
|
|
539
|
+
renderResult(data);
|
|
540
|
+
} catch (err) {
|
|
541
|
+
errorElm.querySelector('.error-message').textContent = err.message;
|
|
542
|
+
errorElm.classList.remove('d-none');
|
|
543
|
+
} finally {
|
|
544
|
+
pendingElm.classList.add('d-none');
|
|
545
|
+
runBtn.disabled = false;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
$('#verifySetupModal').on('shown.bs.modal', () => {
|
|
550
|
+
// Auto-run the no-mailbox checks (config + signing chain) on open.
|
|
551
|
+
runVerify(accountInput.value.trim());
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
runBtn.addEventListener('click', () => runVerify(accountInput.value.trim()));
|
|
555
|
+
accountInput.addEventListener('keydown', e => {
|
|
556
|
+
if (e.key === 'Enter') {
|
|
557
|
+
e.preventDefault();
|
|
558
|
+
runVerify(accountInput.value.trim());
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
</script>
|
|
563
|
+
|
|
340
564
|
<script>
|
|
341
565
|
document.addEventListener('DOMContentLoaded', () => {
|
|
342
566
|
// not set up by default as this element has a different data-toggle
|
|
@@ -89,6 +89,75 @@
|
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
let externalAccountFileElm = document.getElementById('externalAccountFile');
|
|
93
|
+
if (externalAccountFileElm) {
|
|
94
|
+
externalAccountFileElm.addEventListener('click', e => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
browseFileContents('text').then(jsonStr => {
|
|
97
|
+
if (!jsonStr) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let data;
|
|
102
|
+
try {
|
|
103
|
+
data = JSON.parse(jsonStr);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return showToast('Selected file is not JSON formatted', 'alert-triangle');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (data.type !== 'external_account') {
|
|
109
|
+
if (data.type === 'service_account') {
|
|
110
|
+
return showToast('This is a service account key file, not a Workload Identity Federation config. Switch to the "Service account key" tab to load it.', 'alert-triangle');
|
|
111
|
+
}
|
|
112
|
+
return showToast(`Selected file is not an external_account credential config (type: ${data.type || 'missing'})`, 'alert-triangle');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let impersonationUrl = data.service_account_impersonation_url || '';
|
|
116
|
+
let emailMatch = impersonationUrl.match(/\/serviceAccounts\/([^/]+):generateAccessToken$/);
|
|
117
|
+
if (emailMatch) {
|
|
118
|
+
let derivedEmail = decodeURIComponent(emailMatch[1]);
|
|
119
|
+
let emailField = document.getElementById('serviceClientEmail');
|
|
120
|
+
if (emailField && !emailField.value) {
|
|
121
|
+
emailField.value = derivedEmail;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
document.getElementById('externalAccount').value = JSON.stringify(data, null, 2);
|
|
126
|
+
return showToast('Loaded external account configuration from file', 'check-circle');
|
|
127
|
+
}).catch(err => {
|
|
128
|
+
console.error(err);
|
|
129
|
+
return showToast('Failed to load external account file', 'alert-triangle');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let authMethodTabs = document.querySelectorAll('.auth-method-tab');
|
|
135
|
+
let authMethodInput = document.getElementById('authMethod');
|
|
136
|
+
if (authMethodTabs.length && authMethodInput) {
|
|
137
|
+
let updateSections = selected => {
|
|
138
|
+
document.querySelectorAll('.auth-method-section').forEach(section => {
|
|
139
|
+
let active = section.classList.contains('auth-method-section-' + selected);
|
|
140
|
+
section.classList.toggle('d-none', !active);
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
authMethodTabs.forEach(tab => {
|
|
144
|
+
tab.addEventListener('click', e => {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
if (tab.classList.contains('disabled') || tab.classList.contains('active')) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
let selected = tab.getAttribute('data-auth-method');
|
|
150
|
+
authMethodTabs.forEach(other => {
|
|
151
|
+
let isActive = other === tab;
|
|
152
|
+
other.classList.toggle('active', isActive);
|
|
153
|
+
other.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
154
|
+
});
|
|
155
|
+
authMethodInput.value = selected;
|
|
156
|
+
updateSections(selected);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
92
161
|
});
|
|
93
162
|
|
|
94
163
|
</script>
|
|
@@ -87,6 +87,75 @@
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
let externalAccountFileElm = document.getElementById('externalAccountFile');
|
|
91
|
+
if (externalAccountFileElm) {
|
|
92
|
+
externalAccountFileElm.addEventListener('click', e => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
browseFileContents('text').then(jsonStr => {
|
|
95
|
+
if (!jsonStr) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let data;
|
|
100
|
+
try {
|
|
101
|
+
data = JSON.parse(jsonStr);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return showToast('Selected file is not JSON formatted', 'alert-triangle');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (data.type !== 'external_account') {
|
|
107
|
+
if (data.type === 'service_account') {
|
|
108
|
+
return showToast('This is a service account key file, not a Workload Identity Federation config. Switch to the "Service account key" tab to load it.', 'alert-triangle');
|
|
109
|
+
}
|
|
110
|
+
return showToast(`Selected file is not an external_account credential config (type: ${data.type || 'missing'})`, 'alert-triangle');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let impersonationUrl = data.service_account_impersonation_url || '';
|
|
114
|
+
let emailMatch = impersonationUrl.match(/\/serviceAccounts\/([^/]+):generateAccessToken$/);
|
|
115
|
+
if (emailMatch) {
|
|
116
|
+
let derivedEmail = decodeURIComponent(emailMatch[1]);
|
|
117
|
+
let emailField = document.getElementById('serviceClientEmail');
|
|
118
|
+
if (emailField && !emailField.value) {
|
|
119
|
+
emailField.value = derivedEmail;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
document.getElementById('externalAccount').value = JSON.stringify(data, null, 2);
|
|
124
|
+
return showToast('Loaded external account configuration from file', 'check-circle');
|
|
125
|
+
}).catch(err => {
|
|
126
|
+
console.error(err);
|
|
127
|
+
return showToast('Failed to load external account file', 'alert-triangle');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let authMethodTabs = document.querySelectorAll('.auth-method-tab');
|
|
133
|
+
let authMethodInput = document.getElementById('authMethod');
|
|
134
|
+
if (authMethodTabs.length && authMethodInput) {
|
|
135
|
+
let updateSections = selected => {
|
|
136
|
+
document.querySelectorAll('.auth-method-section').forEach(section => {
|
|
137
|
+
let active = section.classList.contains('auth-method-section-' + selected);
|
|
138
|
+
section.classList.toggle('d-none', !active);
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
authMethodTabs.forEach(tab => {
|
|
142
|
+
tab.addEventListener('click', e => {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
if (tab.classList.contains('disabled') || tab.classList.contains('active')) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
let selected = tab.getAttribute('data-auth-method');
|
|
148
|
+
authMethodTabs.forEach(other => {
|
|
149
|
+
let isActive = other === tab;
|
|
150
|
+
other.classList.toggle('active', isActive);
|
|
151
|
+
other.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
152
|
+
});
|
|
153
|
+
authMethodInput.value = selected;
|
|
154
|
+
updateSections(selected);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
90
159
|
});
|
|
91
160
|
|
|
92
161
|
</script>
|
|
@@ -25,18 +25,22 @@
|
|
|
25
25
|
<div class="card-body">
|
|
26
26
|
<div class="row no-gutters align-items-center">
|
|
27
27
|
<div class="col mr-2">
|
|
28
|
-
<strong>Service accounts</strong>
|
|
29
|
-
|
|
30
|
-
delegation enabled in
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
<strong>Service accounts</strong> let EmailEngine access any mailbox in your Google Workspace
|
|
29
|
+
organization without an interactive user login. The service account must have domain-wide
|
|
30
|
+
delegation enabled in Workspace. Use this for automated integrations where you cannot perform
|
|
31
|
+
interactive logins.
|
|
32
|
+
<p class="mt-2 mb-0">
|
|
33
|
+
Two authentication methods are supported: a downloaded <strong>service account key</strong>
|
|
34
|
+
(classic) or <strong>Workload Identity Federation</strong> (keyless, for GKE, EKS, AKS, or
|
|
35
|
+
any OIDC-emitting workload). Pick one below.
|
|
36
|
+
</p>
|
|
37
|
+
<p class="mt-2 mb-0">
|
|
38
|
+
Accounts using service access can only be added via the
|
|
39
|
+
<a href="/admin/swagger#/Account/postV1Account">REST API</a>, not through the hosted
|
|
40
|
+
authentication form.{{#if providerData.tutorialUrl}} Read the
|
|
41
|
+
<a href="{{providerData.tutorialUrl}}" target="_blank" rel="noopener noreferrer"
|
|
42
|
+
referrerpolicy="no-referrer">setup guide</a> for both methods.{{/if}}
|
|
43
|
+
</p>
|
|
40
44
|
</div>
|
|
41
45
|
<div class="col-auto">
|
|
42
46
|
<i class="fas fa-info-circle fa-2x text-gray-300"></i>
|
|
@@ -184,14 +188,58 @@
|
|
|
184
188
|
{{#if activeGmailService}}
|
|
185
189
|
|
|
186
190
|
<div class="form-group">
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
<label class="d-block">Authentication method</label>
|
|
192
|
+
<input type="hidden" name="authMethod" id="authMethod"
|
|
193
|
+
value="{{#if authMethodIsExternalAccount}}externalAccount{{else}}serviceKey{{/if}}" />
|
|
194
|
+
{{#if authMethodLocked}}
|
|
195
|
+
<p class="mb-1">
|
|
196
|
+
<strong>{{#if authMethodIsExternalAccount}}Workload Identity Federation (keyless){{else}}Service account key{{/if}}</strong>
|
|
197
|
+
</p>
|
|
198
|
+
<small class="form-text text-muted">The authentication method is fixed once the application has been created and
|
|
199
|
+
cannot be changed.</small>
|
|
200
|
+
{{else}}
|
|
201
|
+
<ul class="nav nav-tabs" id="auth-method-tabs" role="tablist">
|
|
202
|
+
<li class="nav-item" role="presentation">
|
|
203
|
+
<a class="nav-link auth-method-tab {{#if authMethodIsServiceKey}}active{{/if}}"
|
|
204
|
+
id="auth-method-tab-serviceKey" data-auth-method="serviceKey" href="#" role="tab"
|
|
205
|
+
aria-selected="{{#if authMethodIsServiceKey}}true{{else}}false{{/if}}">Service account key</a>
|
|
206
|
+
</li>
|
|
207
|
+
<li class="nav-item" role="presentation">
|
|
208
|
+
<a class="nav-link auth-method-tab {{#if authMethodIsExternalAccount}}active{{/if}}"
|
|
209
|
+
id="auth-method-tab-externalAccount" data-auth-method="externalAccount" href="#" role="tab"
|
|
210
|
+
aria-selected="{{#if authMethodIsExternalAccount}}true{{else}}false{{/if}}">Workload Identity Federation (keyless)</a>
|
|
211
|
+
</li>
|
|
212
|
+
</ul>
|
|
213
|
+
<small class="form-text text-muted">"Service account key" expects a downloaded JSON key file.
|
|
214
|
+
"Workload Identity Federation" authenticates without a long-lived key, using a credential issued
|
|
215
|
+
by your runtime environment.</small>
|
|
216
|
+
{{/if}}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div class="auth-method-section auth-method-section-serviceKey {{#unless authMethodIsServiceKey}}d-none{{/unless}}">
|
|
220
|
+
<div class="form-group">
|
|
221
|
+
<button type="button" class="btn btn-info btn-icon-split" id="serviceFile">
|
|
222
|
+
<span class="icon text-white-50">
|
|
223
|
+
<i class="fas fa-cloud-upload-alt"></i>
|
|
224
|
+
</span>
|
|
225
|
+
<span class="text">Load configuration from the service key file…</span>
|
|
226
|
+
</button>
|
|
227
|
+
<small class="form-text text-muted">Select the JSON file you received after generating a new service key to
|
|
228
|
+
retrieve the service values.</small>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div class="auth-method-section auth-method-section-externalAccount {{#unless authMethodIsExternalAccount}}d-none{{/unless}}">
|
|
233
|
+
<div class="form-group">
|
|
234
|
+
<button type="button" class="btn btn-info btn-icon-split" id="externalAccountFile">
|
|
235
|
+
<span class="icon text-white-50">
|
|
236
|
+
<i class="fas fa-cloud-upload-alt"></i>
|
|
237
|
+
</span>
|
|
238
|
+
<span class="text">Load configuration from the external-account JSON file…</span>
|
|
239
|
+
</button>
|
|
240
|
+
<small class="form-text text-muted">Select the credential configuration file produced by
|
|
241
|
+
<code>gcloud iam workload-identity-pools create-cred-config</code>.</small>
|
|
242
|
+
</div>
|
|
195
243
|
</div>
|
|
196
244
|
|
|
197
245
|
<div class="form-group">
|
|
@@ -235,18 +283,37 @@
|
|
|
235
283
|
<small class="form-text text-muted">OAuth2 Service Client ID</small>
|
|
236
284
|
</div>
|
|
237
285
|
|
|
238
|
-
<div class="
|
|
286
|
+
<div class="auth-method-section auth-method-section-serviceKey {{#unless authMethodIsServiceKey}}d-none{{/unless}}">
|
|
287
|
+
<div class="form-group">
|
|
239
288
|
|
|
240
|
-
|
|
289
|
+
<label for="serviceKey">Secret service key</label>
|
|
241
290
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
291
|
+
<textarea class="form-control droptxt autoselect {{#if errors.serviceKey}}is-invalid{{/if}}" id="serviceKey"
|
|
292
|
+
name="serviceKey" rows="8" data-enable-grammarly="false" spellcheck="false" {{#if
|
|
293
|
+
hasServiceKey}}placeholder="Service key is set but not shown…" {{else}}
|
|
294
|
+
placeholder="Starts with "-----BEGIN PRIVATE KEY-----"…" {{/if}}>{{values.serviceKey}}</textarea>
|
|
295
|
+
{{#if errors.serviceKey}}
|
|
296
|
+
<span class="invalid-feedback">{{errors.serviceKey}}</span>
|
|
297
|
+
{{/if}}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div class="auth-method-section auth-method-section-externalAccount {{#unless authMethodIsExternalAccount}}d-none{{/unless}}">
|
|
302
|
+
<div class="form-group">
|
|
303
|
+
|
|
304
|
+
<label for="externalAccount">External account configuration</label>
|
|
305
|
+
|
|
306
|
+
<textarea class="form-control droptxt autoselect text-monospace {{#if errors.externalAccount}}is-invalid{{/if}}"
|
|
307
|
+
id="externalAccount" name="externalAccount" rows="8" data-enable-grammarly="false" spellcheck="false" {{#if
|
|
308
|
+
hasExternalAccount}}placeholder="External account configuration is set but not shown…" {{else}}
|
|
309
|
+
placeholder="{"type":"external_account","audience":"...","credential_source":{"file":"/var/run/secrets/..."}}" {{/if}}>{{values.externalAccount}}</textarea>
|
|
310
|
+
{{#if errors.externalAccount}}
|
|
311
|
+
<span class="invalid-feedback">{{errors.externalAccount}}</span>
|
|
312
|
+
{{/if}}
|
|
313
|
+
<small class="form-text text-muted">Paste the JSON produced by
|
|
314
|
+
<code>gcloud iam workload-identity-pools create-cred-config</code>. Only the
|
|
315
|
+
<code>file</code> and <code>url</code> credential sources are supported.</small>
|
|
316
|
+
</div>
|
|
250
317
|
</div>
|
|
251
318
|
|
|
252
319
|
{{else if activeOutlookService}}
|
|
@@ -811,7 +878,7 @@
|
|
|
811
878
|
|
|
812
879
|
<div id="select-pubsub-app" class="card-footer {{#unless baseScopesApi}}d-none{{/unless}}">
|
|
813
880
|
|
|
814
|
-
<p>Microsoft
|
|
881
|
+
<p>Microsoft Graph delivers change notifications to EmailEngine at these two endpoints:</p>
|
|
815
882
|
|
|
816
883
|
<pre><code>{{mainServiceUrl}}/oauth/msg/lifecycle
|
|
817
884
|
{{mainServiceUrl}}/oauth/msg/notification</code></pre>
|
|
@@ -825,7 +892,7 @@
|
|
|
825
892
|
<p>
|
|
826
893
|
If your EmailEngine instance cannot be exposed directly, configure a public
|
|
827
894
|
proxy domain in <span class="text-muted code-link"><a href="/admin/swagger#/Settings/postV1Settings"
|
|
828
|
-
target="_blank" rel="noopener noreferrer">notificationBaseUrl</a></span>. Microsoft
|
|
895
|
+
target="_blank" rel="noopener noreferrer">notificationBaseUrl</a></span>. Microsoft Graph will then
|
|
829
896
|
post to <code>https://<proxy-domain>/oauth/msg/...</code> instead of
|
|
830
897
|
<code>{{mainServiceUrl}}</code>.
|
|
831
898
|
</p>
|