diff --git a/package-lock.json b/package-lock.json index 0f5f25a..796d4cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "auth0-source-control-extension-tools", - "version": "4.0.7", + "version": "4.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 69d3078..e90af7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "auth0-source-control-extension-tools", - "version": "4.0.7", + "version": "4.1.0", "description": "Supporting tools for the Source Control extensions", "main": "lib/index.js", "scripts": { diff --git a/src/auth0/handlers/guardianPhoneFactorMessageTypes.js b/src/auth0/handlers/guardianPhoneFactorMessageTypes.js new file mode 100644 index 0000000..33c4b05 --- /dev/null +++ b/src/auth0/handlers/guardianPhoneFactorMessageTypes.js @@ -0,0 +1,52 @@ +import DefaultHandler from './default'; +import constants from '../../constants'; + +export const schema = { + type: 'object', + properties: { + message_types: { + type: 'array', + items: { + type: 'string', + enum: constants.GUARDIAN_PHONE_MESSAGE_TYPES + } + } + }, + required: [ 'message_types' ], + additionalProperties: false +}; + + +export default class GuardianPhoneMessageTypesHandler extends DefaultHandler { + constructor(options) { + super({ + ...options, + type: 'guardianPhoneFactorMessageTypes' + }); + } + + async getType() { + // in case client version does not support the operation + if (!this.client.guardian || typeof this.client.guardian.getPhoneFactorMessageTypes !== 'function') { + return null; + } + + if (this.existing) return this.existing; + this.existing = await this.client.guardian.getPhoneFactorMessageTypes(); + return this.existing; + } + + async processChanges(assets) { + // No API to delete or create guardianPhoneFactorMessageTypes, we can only update. + const { guardianPhoneFactorMessageTypes } = assets; + + // Do nothing if not set + if (!guardianPhoneFactorMessageTypes) return; + + const params = {}; + const data = guardianPhoneFactorMessageTypes; + await this.client.guardian.updatePhoneFactorMessageTypes(params, data); + this.updated += 1; + this.didUpdate(guardianPhoneFactorMessageTypes); + } +} diff --git a/src/auth0/handlers/guardianPhoneFactorSelectedProvider.js b/src/auth0/handlers/guardianPhoneFactorSelectedProvider.js new file mode 100644 index 0000000..a627c97 --- /dev/null +++ b/src/auth0/handlers/guardianPhoneFactorSelectedProvider.js @@ -0,0 +1,49 @@ +import DefaultHandler from './default'; +import constants from '../../constants'; + +export const schema = { + type: 'object', + properties: { + provider: { + type: 'string', + enum: constants.GUARDIAN_PHONE_PROVIDERS + } + }, + required: [ 'provider' ], + additionalProperties: false +}; + + +export default class GuardianPhoneSelectedProviderHandler extends DefaultHandler { + constructor(options) { + super({ + ...options, + type: 'guardianPhoneFactorSelectedProvider' + }); + } + + async getType() { + // in case client version does not support the operation + if (!this.client.guardian || typeof this.client.guardian.getPhoneFactorSelectedProvider !== 'function') { + return null; + } + + if (this.existing) return this.existing; + this.existing = await this.client.guardian.getPhoneFactorSelectedProvider(); + return this.existing; + } + + async processChanges(assets) { + // No API to delete or create guardianPhoneFactorSelectedProvider, we can only update. + const { guardianPhoneFactorSelectedProvider } = assets; + + // Do nothing if not set + if (!guardianPhoneFactorSelectedProvider) return; + + const params = {}; + const data = guardianPhoneFactorSelectedProvider; + await this.client.guardian.updatePhoneFactorSelectedProvider(params, data); + this.updated += 1; + this.didUpdate(guardianPhoneFactorSelectedProvider); + } +} diff --git a/src/auth0/handlers/guardianPolicies.js b/src/auth0/handlers/guardianPolicies.js new file mode 100644 index 0000000..4b332a9 --- /dev/null +++ b/src/auth0/handlers/guardianPolicies.js @@ -0,0 +1,47 @@ +import DefaultHandler from './default'; +import constants from '../../constants'; + +export const schema = { + type: 'array', + items: { + type: 'string', + enum: constants.GUARDIAN_POLICIES + }, + minLength: 0, + maxLength: 1 +}; + + +export default class GuardianPoliciesHandler extends DefaultHandler { + constructor(options) { + super({ + ...options, + type: 'guardianPolicies' + }); + } + + async getType() { + // in case client version does not support the operation + if (!this.client.guardian || typeof this.client.guardian.getPolicies !== 'function') { + return null; + } + + if (this.existing) return this.existing; + this.existing = await this.client.guardian.getPolicies(); + return this.existing; + } + + async processChanges(assets) { + // No API to delete or create guardianPolicies, we can only update. + const { guardianPolicies } = assets; + + // Do nothing if not set + if (!guardianPolicies) return; + + const params = {}; + const data = guardianPolicies; + await this.client.guardian.updatePolicies(params, data); + this.updated += 1; + this.didUpdate(guardianPolicies); + } +} diff --git a/src/auth0/handlers/index.js b/src/auth0/handlers/index.js index d3a5ede..4c053be 100644 --- a/src/auth0/handlers/index.js +++ b/src/auth0/handlers/index.js @@ -13,6 +13,9 @@ import * as clientGrants from './clientGrants'; import * as guardianFactors from './guardianFactors'; import * as guardianFactorProviders from './guardianFactorProviders'; import * as guardianFactorTemplates from './guardianFactorTemplates'; +import * as guardianPolicies from './guardianPolicies'; +import * as guardianPhoneFactorSelectedProvider from './guardianPhoneFactorSelectedProvider'; +import * as guardianPhoneFactorMessageTypes from './guardianPhoneFactorMessageTypes'; import * as roles from './roles'; import * as branding from './branding'; import * as prompts from './prompts'; @@ -33,6 +36,9 @@ export { guardianFactors, guardianFactorProviders, guardianFactorTemplates, + guardianPolicies, + guardianPhoneFactorSelectedProvider, + guardianPhoneFactorMessageTypes, roles, branding, prompts diff --git a/src/constants.js b/src/constants.js index f87bfc2..b7de000 100644 --- a/src/constants.js +++ b/src/constants.js @@ -110,6 +110,19 @@ constants.GUARDIAN_FACTORS = [ 'email', 'duo' ]; +constants.GUARDIAN_POLICIES = [ + 'all-applications', + 'confidence-score' +]; +constants.GUARDIAN_PHONE_PROVIDERS = [ + 'auth0', + 'twilio', + 'phone-message-hook' +]; +constants.GUARDIAN_PHONE_MESSAGE_TYPES = [ + 'sms', + 'voice' +]; constants.GUARDIAN_FACTOR_TEMPLATES = [ 'sms' diff --git a/tests/auth0/handlers/guardianPhoneFactorMessageTypes.tests.js b/tests/auth0/handlers/guardianPhoneFactorMessageTypes.tests.js new file mode 100644 index 0000000..c592c92 --- /dev/null +++ b/tests/auth0/handlers/guardianPhoneFactorMessageTypes.tests.js @@ -0,0 +1,68 @@ +const { expect } = require('chai'); +const guardianPhoneFactorMessageTypes = require('../../../src/auth0/handlers/guardianPhoneFactorMessageTypes'); + +describe('#guardianPhoneFactorMessageTypes handler', () => { + describe('#getType', () => { + it('should support older version of auth0 client', async () => { + const auth0 = { + guardian: { + // omitting getPhoneFactorMessageTypes() + } + }; + + const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal(null); + }); + + it('should get guardian phone factor message types', async () => { + const auth0 = { + guardian: { + getPhoneFactorMessageTypes: () => ({ message_types: [ 'sms', 'voice' ] }) + } + }; + + const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal({ message_types: [ 'sms', 'voice' ] }); + }); + }); + + describe('#processChanges', () => { + it('should update guardian phone factor message types', async () => { + const auth0 = { + guardian: { + updatePhoneFactorMessageTypes: (params, data) => { + expect(data).to.eql({ message_types: [ 'sms', 'voice' ] }); + return Promise.resolve(data); + } + } + }; + + const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ + { guardianPhoneFactorMessageTypes: { message_types: [ 'sms', 'voice' ] } } + ]); + }); + + it('should skip processing if assets are empty', async () => { + const auth0 = { + guardian: { + updatePhoneFactorMessageTypes: () => { + const err = new Error('updatePhoneFactorMessageTypes() should not have been called'); + return Promise.reject(err); + } + } + }; + + const handler = new guardianPhoneFactorMessageTypes.default({ client: auth0 }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ + { guardianPhoneFactorMessageTypes: null } + ]); + }); + }); +}); diff --git a/tests/auth0/handlers/guardianPhoneFactorSelectedProvider.tests.js b/tests/auth0/handlers/guardianPhoneFactorSelectedProvider.tests.js new file mode 100644 index 0000000..d843951 --- /dev/null +++ b/tests/auth0/handlers/guardianPhoneFactorSelectedProvider.tests.js @@ -0,0 +1,68 @@ +const { expect } = require('chai'); +const guardianPhoneFactorSelectedProvider = require('../../../src/auth0/handlers/guardianPhoneFactorSelectedProvider'); + +describe('#guardianPhoneFactorSelectedProvider handler', () => { + describe('#getType', () => { + it('should support older version of auth0 client', async () => { + const auth0 = { + guardian: { + // omitting getPhoneFactorSelectedProvider() + } + }; + + const handler = new guardianPhoneFactorSelectedProvider.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal(null); + }); + + it('should get guardian phone factor selected provider', async () => { + const auth0 = { + guardian: { + getPhoneFactorSelectedProvider: () => ({ provider: 'twilio' }) + } + }; + + const handler = new guardianPhoneFactorSelectedProvider.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal({ provider: 'twilio' }); + }); + }); + + describe('#processChanges', () => { + it('should update guardian phone factor selected provider', async () => { + const auth0 = { + guardian: { + updatePhoneFactorSelectedProvider: (params, data) => { + expect(data).to.eql({ provider: 'twilio' }); + return Promise.resolve(data); + } + } + }; + + const handler = new guardianPhoneFactorSelectedProvider.default({ client: auth0 }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ + { guardianPhoneFactorSelectedProvider: { provider: 'twilio' } } + ]); + }); + + it('should skip processing if assets are empty', async () => { + const auth0 = { + guardian: { + updatePhoneFactorSelectedProvider: () => { + const err = new Error('updatePhoneFactorSelectedProvider() should not have been called'); + return Promise.reject(err); + } + } + }; + + const handler = new guardianPhoneFactorSelectedProvider.default({ client: auth0 }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ + { guardianPhoneFactorSelectedProvider: null } + ]); + }); + }); +}); diff --git a/tests/auth0/handlers/guardianPolicies.tests.js b/tests/auth0/handlers/guardianPolicies.tests.js new file mode 100644 index 0000000..3f8a518 --- /dev/null +++ b/tests/auth0/handlers/guardianPolicies.tests.js @@ -0,0 +1,69 @@ +const { expect } = require('chai'); +const guardianPolicies = require('../../../src/auth0/handlers/guardianPolicies'); + +describe('#guardianPolicies handler', () => { + describe('#getType', () => { + it('should support older version of auth0 client', async () => { + const auth0 = { + guardian: { + // omitting getPolicies() + } + }; + + const handler = new guardianPolicies.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal(null); + }); + + it('should get guardian policies', async () => { + const auth0 = { + guardian: { + getPolicies: () => ([ 'all-applications' ]) + } + }; + + const handler = new guardianPolicies.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal([ 'all-applications' ]); + }); + }); + + describe('#processChanges', () => { + it('should update guardian policies settings', async () => { + const auth0 = { + guardian: { + updatePolicies: (params, data) => { + expect(data).to.be.an('array'); + expect(data[0]).to.equal('all-applications'); + return Promise.resolve(data); + } + } + }; + + const handler = new guardianPolicies.default({ client: auth0 }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ + { guardianPolicies: [ 'all-applications' ] } + ]); + }); + + it('should skip processing if assets are empty', async () => { + const auth0 = { + guardian: { + updatePolicies: () => { + const err = new Error('updatePolicies() should not have been called'); + return Promise.reject(err); + } + } + }; + + const handler = new guardianPolicies.default({ client: auth0 }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ + { guardianPolicies: null } + ]); + }); + }); +}); diff --git a/tests/auth0/validator.tests.js b/tests/auth0/validator.tests.js index d625ff7..06014a0 100644 --- a/tests/auth0/validator.tests.js +++ b/tests/auth0/validator.tests.js @@ -359,6 +359,58 @@ 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 }, {}); + + auth0.validate().then(failedCb(done), passedCb(done, 'should be string')); + }); + + it('should pass validation', (done) => { + const data = [ 'all-applications' ]; + + checkPassed({ guardianPolicies: data }, done); + }); + + it('should allow empty array', (done) => { + const data = []; + + checkPassed({ guardianPolicies: data }, done); + }); + }); + + describe('#guardianPhoneFactorSelectedProvider validate', () => { + it('should fail validation if no "provider" provided', (done) => { + const data = {}; + + checkRequired('provider', { guardianPhoneFactorSelectedProvider: data }, done); + }); + + it('should pass validation', (done) => { + const data = { provider: 'twilio' }; + + checkPassed({ guardianPhoneFactorSelectedProvider: data }, done); + }); + }); + + describe('#guardianPhoneFactorMessageTypes validate', () => { + it('should fail validation if no "message_types" provided', (done) => { + const data = {}; + + checkRequired('message_types', { guardianPhoneFactorMessageTypes: data }, done); + }); + + it('should pass validation', (done) => { + const data = { message_types: [ 'sms', 'voice' ] }; + + checkPassed({ guardianPhoneFactorMessageTypes: data }, done); + }); + }); + describe('#pages validate', () => { it('should fail validation if no "name" provided', (done) => { const data = [ {