Skip to content

fix: error handling in guardian handlers for forbidden depreated feature #1086

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 31 additions & 19 deletions src/tools/auth0/handlers/guardianFactorProviders.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import DefaultHandler from './default';
import constants from '../../constants';
import { Asset, Assets } from '../../../types';
import { isForbiddenFeatureError } from '../../utils';

const mappings = Object.entries(constants.GUARDIAN_FACTOR_PROVIDERS).reduce(
(accum: { name: string; provider: string }[], [name, providers]) => {
Expand Down Expand Up @@ -35,29 +36,40 @@ export default class GuardianFactorProvidersHandler extends DefaultHandler {
});
}

async getType(): Promise<Asset[]> {
async getType(): Promise<Asset[] | null> {
if (this.existing) return this.existing;

const data = await Promise.all(
mappings.map(async (m) => {
let provider;
// TODO: This is quite a change, needs to be validated for sure.
if (m.name === 'phone' && m.provider === 'twilio') {
provider = await this.client.guardian.getPhoneFactorProviderTwilio();
} else if (m.name === 'sms' && m.provider === 'twilio') {
provider = await this.client.guardian.getSmsFactorProviderTwilio();
} else if (m.name === 'push-notification' && m.provider === 'apns') {
provider = await this.client.guardian.getPushNotificationProviderAPNS();
} else if (m.name === 'push-notification' && m.provider === 'sns') {
provider = await this.client.guardian.getPushNotificationProviderSNS();
}
try {
const data = await Promise.all(
mappings.map(async (m) => {
let provider;
// TODO: This is quite a change, needs to be validated for sure.
if (m.name === 'phone' && m.provider === 'twilio') {
provider = await this.client.guardian.getPhoneFactorProviderTwilio();
} else if (m.name === 'sms' && m.provider === 'twilio') {
provider = await this.client.guardian.getSmsFactorProviderTwilio();
} else if (m.name === 'push-notification' && m.provider === 'apns') {
provider = await this.client.guardian.getPushNotificationProviderAPNS();
} else if (m.name === 'push-notification' && m.provider === 'sns') {
provider = await this.client.guardian.getPushNotificationProviderSNS();
}

return { ...m, ...provider.data };
})
);
return { ...m, ...provider.data };
})
);

// Filter out empty, should have more then 2 keys (name, provider)
return data.filter((d) => Object.keys(d).length > 2);
} catch (err) {
if (err.statusCode === 404 || err.statusCode === 501) {
return null;
}
if (isForbiddenFeatureError(err, this.type)) {
return null;
}

// Filter out empty, should have more then 2 keys (name, provider)
return data.filter((d) => Object.keys(d).length > 2);
throw err;
}
}

async processChanges(assets: Assets): Promise<void> {
Expand Down
39 changes: 24 additions & 15 deletions src/tools/auth0/handlers/guardianFactorTemplates.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { TemplateMessages } from 'auth0';
import DefaultHandler from './default';
import constants from '../../constants';
import { Assets, Asset } from '../../../types';
import { TemplateMessages } from 'auth0';
import { isForbiddenFeatureError } from '../../utils';

export const schema = {
type: 'array',
Expand All @@ -25,25 +26,33 @@ export default class GuardianFactorTemplatesHandler extends DefaultHandler {
});
}

async getType(): Promise<Asset[]> {
async getType(): Promise<Asset[] | null> {
if (this.existing) return this.existing;
try {
const data = await Promise.all(
constants.GUARDIAN_FACTOR_TEMPLATES.map(async (name) => {
if (name === 'sms') {
const { data: templates } = await this.client.guardian.getSmsFactorTemplates();
return { name, ...templates };
}

const data = await Promise.all(
constants.GUARDIAN_FACTOR_TEMPLATES.map(async (name) => {
// TODO: This is quite a change, needs to be validated for sure.
if (name === 'sms') {
const { data: templates } = await this.client.guardian.getSmsFactorTemplates();
return { name, ...templates };
// TODO: GUARDIAN_FACTOR_TEMPLATES only contains 'sms'. Is that expected? We also have 'phone'.
} else {
const { data: templates } = await this.client.guardian.getPhoneFactorTemplates();
return { name, ...templates };
}
})
);
})
);

// Filter out empty, should have more then 1 keys (name)
return data.filter((d) => Object.keys(d).length > 1);
} catch (err) {
if (err.statusCode === 404 || err.statusCode === 501) {
return null;
}
if (isForbiddenFeatureError(err, this.type)) {
return null;
}

// Filter out empty, should have more then 1 keys (name)
return data.filter((d) => Object.keys(d).length > 1);
throw err;
}
}

async processChanges(assets: Assets): Promise<void> {
Expand Down
22 changes: 17 additions & 5 deletions src/tools/auth0/handlers/guardianFactors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Factor, FactorNameEnum } from 'auth0';
import DefaultHandler from './default';
import constants from '../../constants';
import { Asset, Assets } from '../../../types';
import { Factor, FactorNameEnum } from 'auth0';
import { isForbiddenFeatureError } from '../../utils';

export const schema = {
type: 'array',
Expand All @@ -25,11 +26,22 @@ export default class GuardianFactorsHandler extends DefaultHandler {
});
}

async getType(): Promise<Asset[]> {
async getType(): Promise<Asset[] | null> {
if (this.existing) return this.existing;
const { data } = await this.client.guardian.getFactors();
this.existing = data;
return this.existing;
try {
const { data } = await this.client.guardian.getFactors();
this.existing = data;
return this.existing;
} catch (err) {
if (err.statusCode === 404 || err.statusCode === 501) {
return null;
}
if (isForbiddenFeatureError(err, this.type)) {
return null;
}

throw err;
}
}

async processChanges(assets: Assets): Promise<void> {
Expand Down
18 changes: 11 additions & 7 deletions src/tools/auth0/handlers/guardianPhoneFactorMessageTypes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { GetMessageTypes200Response } from 'auth0';
import DefaultHandler from './default';
import constants from '../../constants';
import { Asset, Assets } from '../../../types';
import { GetMessageTypes200Response } from 'auth0';
import { isForbiddenFeatureError } from '../../utils';

export const schema = {
type: 'object',
Expand Down Expand Up @@ -45,26 +46,29 @@ export default class GuardianPhoneMessageTypesHandler extends DefaultHandler {
});
}

async getType(): Promise<Asset[] | {}> {
async getType(): Promise<Asset | null> {
// in case client version does not support the operation
if (
!this.client.guardian ||
typeof this.client.guardian.getPhoneFactorMessageTypes !== 'function'
) {
return {};
return null;
}

if (this.existing) return this.existing;

try {
const { data } = await this.client.guardian.getPhoneFactorMessageTypes();
this.existing = data;
} catch (e) {
if (isFeatureUnavailableError(e)) {
} catch (err) {
if (isFeatureUnavailableError(err)) {
// Gracefully skip processing this configuration value.
return {};
return null;
}
if (isForbiddenFeatureError(err, this.type)) {
return null;
}
throw e;
throw err;
}

return this.existing;
Expand Down
18 changes: 11 additions & 7 deletions src/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { GetPhoneProviders200Response } from 'auth0';
import DefaultHandler from './default';
import constants from '../../constants';
import { Asset, Assets } from '../../../types';
import { GetPhoneProviders200Response } from 'auth0';
import { isForbiddenFeatureError } from '../../utils';

export const schema = {
type: 'object',
Expand Down Expand Up @@ -42,26 +43,29 @@ export default class GuardianPhoneSelectedProviderHandler extends DefaultHandler
});
}

async getType() {
async getType(): Promise<Asset | null> {
// in case client version does not support the operation
if (
!this.client.guardian ||
typeof this.client.guardian.getPhoneFactorSelectedProvider !== 'function'
) {
return {};
return null;
}

if (this.existing) return this.existing;

try {
const { data } = await this.client.guardian.getPhoneFactorSelectedProvider();
this.existing = data;
} catch (e) {
if (isFeatureUnavailableError(e)) {
} catch (err) {
if (isFeatureUnavailableError(err)) {
// Gracefully skip processing this configuration value.
return {};
return null;
}
if (isForbiddenFeatureError(err, this.type)) {
return null;
}
throw e;
throw err;
}

return this.existing;
Expand Down
8 changes: 8 additions & 0 deletions src/tools/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,11 @@ export const isDeprecatedError = (err: { message: string; statusCode: number }):
if (!err) return false;
return !!(err.statusCode === 403 || err.message?.includes('deprecated feature'));
};

export const isForbiddenFeatureError = (err, type): boolean => {
if (err.statusCode === 403) {
log.warn(`${err.message};${err.errorCode ?? ''} - Skipping ${type}`);
return true;
}
return false;
};
22 changes: 22 additions & 0 deletions test/tools/auth0/handlers/guardianFactorProviders.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,28 @@ describe('#guardianFactorProviders handler', () => {
});

describe('#guardianFactorProviders process', () => {
it('should handle forbidden error', async () => {
const throwForbidden = () => {
const error = new Error('Forbidden resource access');
error.statusCode = 403;
throw error;
};

const auth0 = {
guardian: {
getPhoneFactorProviderTwilio: throwForbidden,
getSmsFactorProviderTwilio: throwForbidden,
getPushNotificationProviderAPNS: throwForbidden,
getPushNotificationProviderSNS: throwForbidden,
},
pool,
};

const handler = new guardianFactorProvidersTests.default({ client: auth0, config });
const data = await handler.getType();
expect(data).to.equal(null);
});

it('should get guardianFactorProviders', async () => {
const auth0 = {
guardian: {
Expand Down
17 changes: 17 additions & 0 deletions test/tools/auth0/handlers/guardianFactorTemplates.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ describe('#guardianFactorTemplates handler', () => {
});

describe('#guardianFactorTemplates process', () => {
it('should handle forbidden error', async () => {
const auth0 = {
guardian: {
getSmsFactorTemplates: () => {
const error = new Error('Forbidden resource access');
error.statusCode = 403;
throw error;
},
},
pool,
};

const handler = new guardianFactorTemplatesTests.default({ client: auth0, config });
const data = await handler.getType();
expect(data).to.equal(null);
});

it('should get guardianFactorTemplates', async () => {
const auth0 = {
guardian: {
Expand Down
17 changes: 17 additions & 0 deletions test/tools/auth0/handlers/guardianFactors.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ describe('#guardianFactors handler', () => {
});

describe('#guardianFactors process', () => {
it('should handle forbidden error', async () => {
const auth0 = {
guardian: {
getFactors: () => {
const error = new Error('Forbidden resource access');
error.statusCode = 403;
throw error;
},
},
pool,
};

const handler = new guardianFactorsTests.default({ client: auth0, config });
const data = await handler.getType();
expect(data).to.equal(null);
});

it('should get guardianFactors', async () => {
const factors = [
{ name: 'sms', enabled: true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('#guardianPhoneFactorMessageTypes handler', () => {

const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 });
const data = await handler.getType();
expect(data).to.deep.equal({});
expect(data).to.deep.equal(null);
});

it('should support when endpoint does not exist (older installations)', async () => {
Expand Down Expand Up @@ -42,7 +42,7 @@ describe('#guardianPhoneFactorMessageTypes handler', () => {

const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 });
const data = await handler.getType();
expect(data).to.deep.equal({});
expect(data).to.deep.equal(null);
});

it('should support when endpoint is disabled for tenant', async () => {
Expand Down Expand Up @@ -73,7 +73,7 @@ describe('#guardianPhoneFactorMessageTypes handler', () => {

const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 });
const data = await handler.getType();
expect(data).to.deep.equal({});
expect(data).to.deep.equal(null);
});

it('should get guardian phone factor message types', async () => {
Expand Down Expand Up @@ -140,7 +140,7 @@ describe('#guardianPhoneFactorMessageTypes handler', () => {
const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 });
const stageFn = Object.getPrototypeOf(handler).processChanges;

await stageFn.apply(handler, [{ guardianPhoneFactorMessageTypes: {} }]);
await stageFn.apply(handler, [{ guardianPhoneFactorMessageTypes: null }]);
});
});
});
Loading