From f21970e0d55d13611b10be4f8108c9f6bc0d1a9d Mon Sep 17 00:00:00 2001 From: Patrick Malouin Date: Mon, 6 Jul 2020 12:12:00 -0400 Subject: [PATCH 1/2] fix(guardian): support for older installations --- .../guardianPhoneFactorMessageTypes.js | 32 ++++++- .../guardianPhoneFactorSelectedProvider.js | 32 ++++++- src/auth0/handlers/guardianPolicies.js | 26 +++--- .../guardianPhoneFactorMessageTypes.tests.js | 84 ++++++++++++++++++- ...ardianPhoneFactorSelectedProvider.tests.js | 84 ++++++++++++++++++- .../auth0/handlers/guardianPolicies.tests.js | 14 +++- tests/auth0/validator.tests.js | 26 +++--- 7 files changed, 259 insertions(+), 39 deletions(-) diff --git a/src/auth0/handlers/guardianPhoneFactorMessageTypes.js b/src/auth0/handlers/guardianPhoneFactorMessageTypes.js index 33c4b05..5458e44 100644 --- a/src/auth0/handlers/guardianPhoneFactorMessageTypes.js +++ b/src/auth0/handlers/guardianPhoneFactorMessageTypes.js @@ -12,10 +12,24 @@ export const schema = { } } }, - required: [ 'message_types' ], additionalProperties: false }; +const isFeatureUnavailableError = (err) => { + if (err.statusCode === 404) { + // Older Management API version where the endpoint is not available. + return true; + } + if (err.statusCode === 403 + && err.originalError + && err.originalError.response + && err.originalError.response.body + && err.originalError.response.body.errorCode === 'voice_mfa_not_allowed') { + // Recent Management API version, but with feature explicitly disabled. + return true; + } + return false; +}; export default class GuardianPhoneMessageTypesHandler extends DefaultHandler { constructor(options) { @@ -28,11 +42,21 @@ export default class GuardianPhoneMessageTypesHandler extends DefaultHandler { async getType() { // in case client version does not support the operation if (!this.client.guardian || typeof this.client.guardian.getPhoneFactorMessageTypes !== 'function') { - return null; + return {}; } if (this.existing) return this.existing; - this.existing = await this.client.guardian.getPhoneFactorMessageTypes(); + + try { + this.existing = await this.client.guardian.getPhoneFactorMessageTypes(); + } catch (e) { + if (isFeatureUnavailableError(e)) { + // Gracefully skip processing this configuration value. + return {}; + } + throw e; + } + return this.existing; } @@ -41,7 +65,7 @@ export default class GuardianPhoneMessageTypesHandler extends DefaultHandler { const { guardianPhoneFactorMessageTypes } = assets; // Do nothing if not set - if (!guardianPhoneFactorMessageTypes) return; + if (!guardianPhoneFactorMessageTypes || !guardianPhoneFactorMessageTypes.message_types) return; const params = {}; const data = guardianPhoneFactorMessageTypes; diff --git a/src/auth0/handlers/guardianPhoneFactorSelectedProvider.js b/src/auth0/handlers/guardianPhoneFactorSelectedProvider.js index a627c97..2955293 100644 --- a/src/auth0/handlers/guardianPhoneFactorSelectedProvider.js +++ b/src/auth0/handlers/guardianPhoneFactorSelectedProvider.js @@ -9,10 +9,24 @@ export const schema = { enum: constants.GUARDIAN_PHONE_PROVIDERS } }, - required: [ 'provider' ], additionalProperties: false }; +const isFeatureUnavailableError = (err) => { + if (err.statusCode === 404) { + // Older Management API version where the endpoint is not available. + return true; + } + if (err.statusCode === 403 + && err.originalError + && err.originalError.response + && err.originalError.response.body + && err.originalError.response.body.errorCode === 'hooks_not_allowed') { + // Recent Management API version, but with feature explicitly disabled. + return true; + } + return false; +}; export default class GuardianPhoneSelectedProviderHandler extends DefaultHandler { constructor(options) { @@ -25,11 +39,21 @@ export default class GuardianPhoneSelectedProviderHandler extends DefaultHandler async getType() { // in case client version does not support the operation if (!this.client.guardian || typeof this.client.guardian.getPhoneFactorSelectedProvider !== 'function') { - return null; + return {}; } if (this.existing) return this.existing; - this.existing = await this.client.guardian.getPhoneFactorSelectedProvider(); + + try { + this.existing = await this.client.guardian.getPhoneFactorSelectedProvider(); + } catch (e) { + if (isFeatureUnavailableError(e)) { + // Gracefully skip processing this configuration value. + return {}; + } + throw e; + } + return this.existing; } @@ -38,7 +62,7 @@ export default class GuardianPhoneSelectedProviderHandler extends DefaultHandler const { guardianPhoneFactorSelectedProvider } = assets; // Do nothing if not set - if (!guardianPhoneFactorSelectedProvider) return; + if (!guardianPhoneFactorSelectedProvider || !guardianPhoneFactorSelectedProvider.provider) return; const params = {}; const data = guardianPhoneFactorSelectedProvider; diff --git a/src/auth0/handlers/guardianPolicies.js b/src/auth0/handlers/guardianPolicies.js index 4b332a9..6e02c30 100644 --- a/src/auth0/handlers/guardianPolicies.js +++ b/src/auth0/handlers/guardianPolicies.js @@ -2,16 +2,19 @@ import DefaultHandler from './default'; import constants from '../../constants'; export const schema = { - type: 'array', - items: { - type: 'string', - enum: constants.GUARDIAN_POLICIES + type: 'object', + properties: { + policies: { + type: 'array', + items: { + type: 'string', + enum: constants.GUARDIAN_POLICIES + } + } }, - minLength: 0, - maxLength: 1 + additionalProperties: false }; - export default class GuardianPoliciesHandler extends DefaultHandler { constructor(options) { super({ @@ -23,11 +26,12 @@ export default class GuardianPoliciesHandler extends DefaultHandler { async getType() { // in case client version does not support the operation if (!this.client.guardian || typeof this.client.guardian.getPolicies !== 'function') { - return null; + return {}; } if (this.existing) return this.existing; - this.existing = await this.client.guardian.getPolicies(); + const policies = await this.client.guardian.getPolicies(); + this.existing = { policies }; return this.existing; } @@ -36,10 +40,10 @@ export default class GuardianPoliciesHandler extends DefaultHandler { const { guardianPolicies } = assets; // Do nothing if not set - if (!guardianPolicies) return; + if (!guardianPolicies || !guardianPolicies.policies) return; const params = {}; - const data = guardianPolicies; + const data = guardianPolicies.policies; await this.client.guardian.updatePolicies(params, data); this.updated += 1; this.didUpdate(guardianPolicies); diff --git a/tests/auth0/handlers/guardianPhoneFactorMessageTypes.tests.js b/tests/auth0/handlers/guardianPhoneFactorMessageTypes.tests.js index c592c92..d099b93 100644 --- a/tests/auth0/handlers/guardianPhoneFactorMessageTypes.tests.js +++ b/tests/auth0/handlers/guardianPhoneFactorMessageTypes.tests.js @@ -12,7 +12,68 @@ describe('#guardianPhoneFactorMessageTypes handler', () => { const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 }); const data = await handler.getType(); - expect(data).to.deep.equal(null); + expect(data).to.deep.equal({}); + }); + + it('should support when endpoint does not exist (older installations)', async () => { + const auth0 = { + guardian: { + getPhoneFactorMessageTypes: () => { + const err = new Error('Not Found'); + err.name = 'Not Found'; + err.statusCode = 404; + err.requestInfo = { + method: 'get', + url: 'https://example.auth0.com/api/v2/guardian/factors/phone/message-types' + }; + err.originalError = new Error('Not Found'); + err.originalError.status = 404; + err.originalError.response = { + body: { + statusCode: 404, + error: 'Not Found', + message: 'Not Found' + } + }; + return Promise.reject(err); + } + } + }; + + const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal({}); + }); + + it('should support when endpoint is disabled for tenant', async () => { + const auth0 = { + guardian: { + getPhoneFactorMessageTypes: () => { + const err = new Error('This endpoint is disabled for your tenant.'); + err.name = 'Forbidden'; + err.statusCode = 403; + err.requestInfo = { + method: 'get', + url: 'https://example.auth0.com/api/v2/guardian/factors/phone/message-types' + }; + err.originalError = new Error('Forbidden'); + err.originalError.status = 403; + err.originalError.response = { + body: { + statusCode: 403, + error: 'Forbidden', + message: 'This endpoint is disabled for your tenant.', + errorCode: 'voice_mfa_not_allowed' + } + }; + return Promise.reject(err); + } + } + }; + + const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal({}); }); it('should get guardian phone factor message types', async () => { @@ -26,6 +87,25 @@ describe('#guardianPhoneFactorMessageTypes handler', () => { const data = await handler.getType(); expect(data).to.deep.equal({ message_types: [ 'sms', 'voice' ] }); }); + + it('should throw an error for all other failed requests', async () => { + const auth0 = { + guardian: { + getPhoneFactorMessageTypes: () => { + const error = new Error('Bad request'); + error.statusCode = 500; + throw error; + } + } + }; + + const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 }); + try { + await handler.getType(); + } catch (error) { + expect(error).to.be.an.instanceOf(Error); + } + }); }); describe('#processChanges', () => { @@ -61,7 +141,7 @@ describe('#guardianPhoneFactorMessageTypes handler', () => { const stageFn = Object.getPrototypeOf(handler).processChanges; await stageFn.apply(handler, [ - { guardianPhoneFactorMessageTypes: null } + { guardianPhoneFactorMessageTypes: {} } ]); }); }); diff --git a/tests/auth0/handlers/guardianPhoneFactorSelectedProvider.tests.js b/tests/auth0/handlers/guardianPhoneFactorSelectedProvider.tests.js index d843951..da20dfb 100644 --- a/tests/auth0/handlers/guardianPhoneFactorSelectedProvider.tests.js +++ b/tests/auth0/handlers/guardianPhoneFactorSelectedProvider.tests.js @@ -12,7 +12,68 @@ describe('#guardianPhoneFactorSelectedProvider handler', () => { const handler = new guardianPhoneFactorSelectedProvider.default({ client: auth0 }); const data = await handler.getType(); - expect(data).to.deep.equal(null); + expect(data).to.deep.equal({}); + }); + + it('should support when endpoint does not exist (older installations)', async () => { + const auth0 = { + guardian: { + getPhoneFactorSelectedProvider: () => { + const err = new Error('Not Found'); + err.name = 'Not Found'; + err.statusCode = 404; + err.requestInfo = { + method: 'get', + url: 'https://example.auth0.com/api/v2/guardian/factors/sms/selected-provider' + }; + err.originalError = new Error('Not Found'); + err.originalError.status = 404; + err.originalError.response = { + body: { + statusCode: 404, + error: 'Not Found', + message: 'Not Found' + } + }; + return Promise.reject(err); + } + } + }; + + const handler = new guardianPhoneFactorSelectedProvider.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal({}); + }); + + it('should support when endpoint is disabled for tenant', async () => { + const auth0 = { + guardian: { + getPhoneFactorSelectedProvider: () => { + const err = new Error('This endpoint is disabled for your tenant.'); + err.name = 'Forbidden'; + err.statusCode = 403; + err.requestInfo = { + method: 'get', + url: 'https://example.auth0.com/api/v2/guardian/factors/sms/selected-provider' + }; + err.originalError = new Error('Forbidden'); + err.originalError.status = 403; + err.originalError.response = { + body: { + statusCode: 403, + error: 'Forbidden', + message: 'This endpoint is disabled for your tenant.', + errorCode: 'hooks_not_allowed' + } + }; + return Promise.reject(err); + } + } + }; + + const handler = new guardianPhoneFactorSelectedProvider.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal({}); }); it('should get guardian phone factor selected provider', async () => { @@ -26,6 +87,25 @@ describe('#guardianPhoneFactorSelectedProvider handler', () => { const data = await handler.getType(); expect(data).to.deep.equal({ provider: 'twilio' }); }); + + it('should throw an error for all other failed requests', async () => { + const auth0 = { + guardian: { + getPhoneFactorSelectedProvider: () => { + const error = new Error('Bad request'); + error.statusCode = 500; + throw error; + } + } + }; + + const handler = new guardianPhoneFactorSelectedProvider.default({ client: auth0 }); + try { + await handler.getType(); + } catch (error) { + expect(error).to.be.an.instanceOf(Error); + } + }); }); describe('#processChanges', () => { @@ -61,7 +141,7 @@ describe('#guardianPhoneFactorSelectedProvider handler', () => { const stageFn = Object.getPrototypeOf(handler).processChanges; await stageFn.apply(handler, [ - { guardianPhoneFactorSelectedProvider: null } + { guardianPhoneFactorSelectedProvider: {} } ]); }); }); diff --git a/tests/auth0/handlers/guardianPolicies.tests.js b/tests/auth0/handlers/guardianPolicies.tests.js index 3f8a518..a1f5aa9 100644 --- a/tests/auth0/handlers/guardianPolicies.tests.js +++ b/tests/auth0/handlers/guardianPolicies.tests.js @@ -12,7 +12,7 @@ describe('#guardianPolicies handler', () => { const handler = new guardianPolicies.default({ client: auth0 }); const data = await handler.getType(); - expect(data).to.deep.equal(null); + expect(data).to.deep.equal({}); }); it('should get guardian policies', async () => { @@ -24,7 +24,9 @@ describe('#guardianPolicies handler', () => { const handler = new guardianPolicies.default({ client: auth0 }); const data = await handler.getType(); - expect(data).to.deep.equal([ 'all-applications' ]); + expect(data).to.deep.equal({ + policies: [ 'all-applications' ] + }); }); }); @@ -44,7 +46,11 @@ describe('#guardianPolicies handler', () => { const stageFn = Object.getPrototypeOf(handler).processChanges; await stageFn.apply(handler, [ - { guardianPolicies: [ 'all-applications' ] } + { + guardianPolicies: { + policies: [ 'all-applications' ] + } + } ]); }); @@ -62,7 +68,7 @@ describe('#guardianPolicies handler', () => { const stageFn = Object.getPrototypeOf(handler).processChanges; await stageFn.apply(handler, [ - { guardianPolicies: null } + { guardianPolicies: {} } ]); }); }); diff --git a/tests/auth0/validator.tests.js b/tests/auth0/validator.tests.js index 06014a0..d0fd764 100644 --- a/tests/auth0/validator.tests.js +++ b/tests/auth0/validator.tests.js @@ -361,33 +361,35 @@ describe('#schema validation tests', () => { describe('#guardianPolicies validate', () => { it('should fail validation if guardianPolicies is not an array of strings', (done) => { - const data = [ { - anything: 'anything' - } ]; - - const auth0 = new Auth0({}, { guardianPolicies: data }, {}); + const data = { + policies: 'all-applications' + }; - auth0.validate().then(failedCb(done), passedCb(done, 'should be string')); + checkTypeError('policies', 'array', { guardianPolicies: data }, done); }); it('should pass validation', (done) => { - const data = [ 'all-applications' ]; + const data = { + policies: [ 'all-applications' ] + }; checkPassed({ guardianPolicies: data }, done); }); it('should allow empty array', (done) => { - const data = []; + const data = { + policies: [] + }; checkPassed({ guardianPolicies: data }, done); }); }); describe('#guardianPhoneFactorSelectedProvider validate', () => { - it('should fail validation if no "provider" provided', (done) => { + it('should pass validation if no "provider" provided', (done) => { const data = {}; - checkRequired('provider', { guardianPhoneFactorSelectedProvider: data }, done); + checkPassed({ guardianPhoneFactorSelectedProvider: data }, done); }); it('should pass validation', (done) => { @@ -398,10 +400,10 @@ describe('#schema validation tests', () => { }); describe('#guardianPhoneFactorMessageTypes validate', () => { - it('should fail validation if no "message_types" provided', (done) => { + it('should pass validation if no "message_types" provided', (done) => { const data = {}; - checkRequired('message_types', { guardianPhoneFactorMessageTypes: data }, done); + checkPassed({ guardianPhoneFactorMessageTypes: data }, done); }); it('should pass validation', (done) => { From c8aafe14cb2458ef565b2657070a84cc50c7f3b4 Mon Sep 17 00:00:00 2001 From: Patrick Malouin Date: Mon, 6 Jul 2020 12:12:49 -0400 Subject: [PATCH 2/2] chore: bump to 4.1.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 796d4cf..ed8cd9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "auth0-source-control-extension-tools", - "version": "4.1.0", + "version": "4.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e90af7c..c195539 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "auth0-source-control-extension-tools", - "version": "4.1.0", + "version": "4.1.1", "description": "Supporting tools for the Source Control extensions", "main": "lib/index.js", "scripts": {