Skip to content

Commit

Permalink
feat(oauth): Allow to disable base OAuth2 scopes like SMTP.Send
Browse files Browse the repository at this point in the history
  • Loading branch information
andris9 committed Oct 24, 2023
1 parent 676d3d6 commit ef89d83
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 14 deletions.
57 changes: 44 additions & 13 deletions lib/oauth2-apps.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,19 @@ function oauth2ProviderData(provider) {
return providerData;
}

function formatExtraScopes(extraScopes, baseScopes, defaultScopesList) {
function formatExtraScopes(extraScopes, baseScopes, defaultScopesList, skipScopes) {
let defaultScopes;

skipScopes = [].concat(skipScopes || []);

if (Array.isArray(defaultScopesList)) {
defaultScopes = defaultScopesList;
} else {
defaultScopes = (baseScopes && defaultScopesList[baseScopes]) || defaultScopesList.imap;
}

let extras = [];
if (!extraScopes) {
if (!extraScopes && !skipScopes.length) {
return defaultScopes;
}

Expand All @@ -122,11 +124,27 @@ function formatExtraScopes(extraScopes, baseScopes, defaultScopesList) {
}
extras.push(extraScope);
}

let result = [];

if (extras.length) {
return extras.concat(defaultScopes);
result = extras.concat(defaultScopes);
}

result = defaultScopes;

if (skipScopes.length) {
result = result.filter(scope => {
for (let skipScope of skipScopes) {
if (scope === skipScope || scope === `https://outlook.office.com/${skipScope}`) {
return false;
}
}
return true;
});
}

return defaultScopes;
return result;
}

class OAuth2AppsHandler {
Expand Down Expand Up @@ -242,6 +260,12 @@ class OAuth2AppsHandler {
}
extraScopes = extraScopes.filter(entry => entry);

let skipScopes = await settings.get(`${id}SkipScopes`);
if (!Array.isArray(skipScopes)) {
skipScopes = (skipScopes || '').toString().split(/[\s,]+/);
}
skipScopes = skipScopes.filter(entry => entry);

switch (id) {
case 'gmail': {
let appData = {
Expand All @@ -253,6 +277,7 @@ class OAuth2AppsHandler {
clientSecret: await settings.get(`${id}ClientSecret`),
redirectUrl: await settings.get(`${id}RedirectUrl`),
extraScopes,
skipScopes,

name: 'Gmail',
description: 'Legacy OAuth2 app',
Expand All @@ -272,6 +297,7 @@ class OAuth2AppsHandler {
serviceClient: await settings.get(`${id}Client`),
serviceKey: await settings.get(`${id}Key`),
extraScopes,
skipScopes,

name: 'Gmail service',
description: 'Legacy OAuth2 app',
Expand All @@ -296,6 +322,7 @@ class OAuth2AppsHandler {
clientSecret: await settings.get(`${id}ClientSecret`),
redirectUrl: await settings.get(`${id}RedirectUrl`),
extraScopes,
skipScopes,

name: 'Outlook',
description: 'Legacy OAuth2 app',
Expand All @@ -317,6 +344,7 @@ class OAuth2AppsHandler {
clientSecret: await settings.get(`${id}ClientSecret`),
redirectUrl: await settings.get(`${id}RedirectUrl`),
extraScopes,
skipScopes,

name: 'Mail.ru',
description: 'Legacy OAuth2 app',
Expand All @@ -340,21 +368,24 @@ class OAuth2AppsHandler {

switch (id) {
case 'gmail':
for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'extraScopes']) {
for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'extraScopes', 'skipScopes']) {
if (typeof updates[key] !== 'undefined') {
data[`${id}${key.replace(/^./, c => c.toUpperCase())}`] = updates[key];
}
}
break;

case 'gmailService':
for (let key of ['serviceClient', 'serviceKey', 'extraScopes']) {
for (let key of ['serviceClient', 'serviceKey', 'extraScopes', 'skipScopes']) {
if (typeof updates[key] !== 'undefined') {
let dataKey;
switch (key) {
case 'extraScopes':
dataKey = 'gmailServiceExtraScopes';
break;
case 'skipScopes':
dataKey = 'gmailServiceSkipScopes';
break;
default:
dataKey = `gmail${key.replace(/^./, c => c.toUpperCase())}`;
}
Expand All @@ -364,7 +395,7 @@ class OAuth2AppsHandler {
break;

case 'outlook':
for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'authority', 'extraScopes']) {
for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'authority', 'extraScopes', 'skipScopes']) {
if (typeof updates[key] !== 'undefined') {
data[`${id}${key.replace(/^./, c => c.toUpperCase())}`] = updates[key];
}
Expand All @@ -373,7 +404,7 @@ class OAuth2AppsHandler {
break;

case 'mailRu':
for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'extraScopes']) {
for (let key of ['clientId', 'clientSecret', 'redirectUrl', 'extraScopes', 'skipScopes']) {
if (typeof updates[key] !== 'undefined') {
data[`${id}${key.replace(/^./, c => c.toUpperCase())}`] = updates[key];
}
Expand All @@ -398,7 +429,7 @@ class OAuth2AppsHandler {

async delLegacyApp(id) {
let pipeline = redis.multi();
for (let key of ['Enabled', 'RedirectUrl', 'Client', 'ClientId', 'ClientSecret', 'Authority', 'ExtraScopes', 'Key', 'AuthFlag']) {
for (let key of ['Enabled', 'RedirectUrl', 'Client', 'ClientId', 'ClientSecret', 'Authority', 'ExtraScopes', 'SkipScopes', 'Key', 'AuthFlag']) {
pipeline = pipeline.hdel(`${REDIS_PREFIX}settings`, `${id}${key}`);
}
await pipeline.exec();
Expand Down Expand Up @@ -604,7 +635,7 @@ class OAuth2AppsHandler {
let clientId = appData.clientId;
let clientSecret = appData.clientSecret ? await decrypt(appData.clientSecret) : null;
let redirectUrl = appData.redirectUrl;
let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, GMAIL_SCOPES);
let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, GMAIL_SCOPES, appData.skipScopes);

if (!clientId || !clientSecret || !redirectUrl) {
let error = Boom.boomify(new Error('OAuth2 credentials not set up for Gmail'), { statusCode: 400 });
Expand Down Expand Up @@ -638,7 +669,7 @@ class OAuth2AppsHandler {
case 'gmailService': {
let serviceClient = appData.serviceClient;
let serviceKey = appData.serviceKey ? await decrypt(appData.serviceKey) : null;
let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, GMAIL_SCOPES);
let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, GMAIL_SCOPES, appData.skipScopes);

if (!serviceClient || !serviceKey) {
let error = Boom.boomify(new Error('OAuth2 credentials not set up for Gmail'), { statusCode: 400 });
Expand Down Expand Up @@ -673,7 +704,7 @@ class OAuth2AppsHandler {
let clientId = appData.clientId;
let clientSecret = appData.clientSecret ? await decrypt(appData.clientSecret) : null;
let redirectUrl = appData.redirectUrl;
let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, OUTLOOK_SCOPES);
let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, OUTLOOK_SCOPES, appData.skipScopes);

if (!clientId || !clientSecret || !authority || !redirectUrl) {
let error = Boom.boomify(new Error('OAuth2 credentials not set up for Outlook'), { statusCode: 400 });
Expand Down Expand Up @@ -709,7 +740,7 @@ class OAuth2AppsHandler {
let clientId = appData.clientId;
let clientSecret = appData.clientSecret ? await decrypt(appData.clientSecret) : null;
let redirectUrl = appData.redirectUrl;
let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, MAIL_RU_SCOPES);
let scopes = formatExtraScopes(appData.extraScopes, appData.baseScopes, MAIL_RU_SCOPES, appData.skipScopes);

if (!clientId || !clientSecret || !redirectUrl) {
let error = Boom.boomify(new Error('OAuth2 credentials not set up for Mail.ru'), { statusCode: 400 });
Expand Down
26 changes: 25 additions & 1 deletion lib/routes-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,12 @@ const oauthUpdateSchema = {
.max(10 * 1024)
.description('OAuth2 Extra Scopes'),

skipScopes: Joi.string()
.allow('')
.trim()
.max(10 * 1024)
.description('OAuth2 scopes to skip from the base set'),

serviceClient: Joi.string()
.trim()
.allow('')
Expand Down Expand Up @@ -2151,6 +2157,11 @@ return true;`

let providerData = oauth2ProviderData(app.provider);

let disabledScopes = {};
if (app.skipScopes?.includes('SMTP.Send') || app.skipScopes?.includes('https://outlook.office.com/SMTP.Send')) {
disabledScopes.SMTP_Send = true;
}

return h.view(
'config/oauth/app',
{
Expand All @@ -2161,6 +2172,8 @@ return true;`

app,

disabledScopes,

providerData
},
{
Expand Down Expand Up @@ -2298,6 +2311,11 @@ return true;`
.map(scope => scope.trim())
.filter(scope => scope);

appData.skipScopes = appData.skipScopes
.split(/\s+/)
.map(scope => scope.trim())
.filter(scope => scope);

let oauth2App = await oauth2Apps.create(appData);
if (!oauth2App || !oauth2App.id) {
throw new Error('Unexpected result');
Expand Down Expand Up @@ -2424,7 +2442,8 @@ return true;`
let values = Object.assign({}, appData, {
clientSecret: '',
serviceKey: '',
extraScopes: [].concat(appData.extraScopes || []).join('\n')
extraScopes: [].concat(appData.extraScopes || []).join('\n'),
skipScopes: [].concat(appData.skipScopes || []).join('\n')
});

return h.view(
Expand Down Expand Up @@ -2489,6 +2508,11 @@ return true;`
.map(scope => scope.trim())
.filter(scope => scope);

updates.skipScopes = updates.skipScopes
.split(/\s+/)
.map(scope => scope.trim())
.filter(scope => scope);

let oauth2App = await oauth2Apps.update(appData.id, updates);
if (!oauth2App || !oauth2App.id) {
throw new Error('Unexpected result');
Expand Down
12 changes: 12 additions & 0 deletions lib/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,18 @@ const oauthCreateSchema = {
})
.description('OAuth2 Extra Scopes'),

skipScopes: Joi.any()
.alter({
web: () =>
Joi.string()
.allow('')
.trim()
.example('SMTP.Send')
.max(10 * 1024),
api: () => Joi.array().items(Joi.string().trim().max(255).example('SMTP.Send'))
})
.description('OAuth2 scopes to skip from the base set'),

serviceClient: Joi.string()
.trim()
.allow('')
Expand Down
15 changes: 15 additions & 0 deletions views/config/oauth/app.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@
<div>Your OAuth2 project <strong>must</strong> have the following scopes enabled:</div>
<ul>
<li><code>"IMAP.AccessAsUser.All"</code>

{{#unless disabledScopes.SMTP_Send}}
<li><code>"SMTP.Send"</code>
{{/unless}}

<li><code>"offline_access"</code>
</ul>
<div>
Expand Down Expand Up @@ -191,6 +195,17 @@
</dd>
{{/if}}

{{#if app.skipScopes}}
<dt class="col-sm-3">Disabled OAuth2 scopes</dt>
<dd class="col-sm-9">
<ul>
{{#each app.skipScopes}}
<li class="text-monospace"><small>{{ this }}</small></li>
{{/each}}
</ul>
</dd>
{{/if}}

</dl>

</div>
Expand Down
27 changes: 27 additions & 0 deletions views/partials/oauth_form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,33 @@
<small class="form-text text-muted">Keep one scope per line</small>
</div>

</div>
</div>
</div>

<div class="card mb-4">
<a href="#skipScopes" class="d-block card-header py-3 {{#unless values.skipScopes}} collapsed{{/unless}}"
data-toggle="collapse" role="button" aria-expanded="true" aria-controls="setupScopes">
<h6 class="m-0 font-weight-bold text-primary">Disabled scopes</h6>
</a>
<div class="collapse {{#if values.skipScopes}} show{{/if}}" id="skipScopes">
<div class="card-body">

<p>Here you can list OAuth2 scopes that EmailEngine should not use. This is mainly needed
when you want to disable some base scopes like <code>SMTP.Send</code> in Outlook.</p>

<div class="form-group">
<label for="skipScopes">List of disabled OAuth2 scopes</label>

<textarea class="form-control text-monospace {{#if errors.skipScopes}}is-invalid{{/if}}" id="skipScopes"
name="skipScopes" rows="5" spellcheck="false" data-enable-grammarly="false"
placeholder="Scope identifiers like &quot;SMTP.Send&quot;&mldr;">{{values.skipScopes}}</textarea>
{{#if errors.skipScopes}}
<span class="invalid-feedback">{{errors.skipScopes}}</span>
{{/if}}
<small class="form-text text-muted">Keep one scope per line</small>
</div>

</div>
</div>
</div>
2 changes: 2 additions & 0 deletions workers/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -6210,6 +6210,8 @@ When making API calls remember that requests against the same account are queued

extraScopes: Joi.array().items(Joi.string().trim().max(255).example('User.Read')).description('OAuth2 Extra Scopes'),

skipScopes: Joi.array().items(Joi.string().trim().max(255).example('SMTP.Send')).description('OAuth2 scopes to skip from the base set'),

serviceClient: Joi.string()
.trim()
.allow('', null, false)
Expand Down

0 comments on commit ef89d83

Please sign in to comment.