diff --git a/README.md b/README.md index ad07129..d836360 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,5 @@ Shared logic for the Source Control Extensions and CLI: - - https://github.com/auth0-extensions/auth0-bitbucket-deploy - - https://github.com/auth0-extensions/auth0-visualstudio-deploy - - https://github.com/auth0-extensions/auth0-gitlab-deploy - - https://github.com/auth0-extensions/auth0-github-deploy + - https://github.com/auth0-extensions/auth0-deploy-extensions - https://github.com/auth0/auth0-deploy-cli diff --git a/package-lock.json b/package-lock.json index e245f2b..72d988e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "auth0-source-control-extension-tools", - "version": "3.0.9", + "version": "3.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b89ca84..1bc24d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "auth0-source-control-extension-tools", - "version": "3.1.0", + "version": "3.5.1", "description": "Supporting tools for the Source Control extensions", "main": "lib/index.js", "scripts": { @@ -8,7 +8,7 @@ "prepare": "npm run build", "release": "git tag $npm_package_version && git push --tags && npm publish", "lint:js": "eslint --ignore-path .gitignore --ignore-pattern webpack .", - "test": "npm run test:pre && cross-env NODE_ENV=test nyc mocha tests/mocha.js ./tests/**/*.tests.js ./tests/*.tests.js", + "test": "npm run test:pre && cross-env NODE_ENV=test nyc mocha tests/mocha.js './tests/**/*.tests.js'", "test:watch": "cross-env NODE_ENV=test mocha tests/mocha.js ./tests/**/*.tests.js ./tests/*.tests.js --watch", "test:pre": "npm run test:clean && npm run lint:js", "test:clean": "rimraf ./coverage && rimraf ./.nyc_output" diff --git a/src/auth0/handlers/branding.js b/src/auth0/handlers/branding.js new file mode 100644 index 0000000..ef20b93 --- /dev/null +++ b/src/auth0/handlers/branding.js @@ -0,0 +1,38 @@ +import DefaultHandler from './default'; + +export const schema = { type: 'object' }; + +export default class BrandingHandler extends DefaultHandler { + constructor(options) { + super({ + ...options, + type: 'branding' + }); + } + + async getType() { + // in case client version does not support branding + if (!this.client.branding || typeof this.client.branding.getSettings !== 'function') { + return {}; + } + + try { + return await this.client.branding.getSettings(); + } catch (err) { + if (err.statusCode === 404) return {}; + if (err.statusCode === 501) return {}; + throw err; + } + } + + async processChanges(assets) { + const { branding } = assets; + + // Do nothing if not set + if (!branding || !Object.keys(branding).length) return; + + await this.client.branding.updateSettings(branding); + this.updated += 1; + this.didUpdate(branding); + } +} diff --git a/src/auth0/handlers/clientGrants.js b/src/auth0/handlers/clientGrants.js index 82e5255..df384e4 100644 --- a/src/auth0/handlers/clientGrants.js +++ b/src/auth0/handlers/clientGrants.js @@ -13,7 +13,7 @@ export const schema = { uniqueItems: true } }, - require: [ 'name' ] + required: [ 'client_id', 'scope', 'audience' ] } }; @@ -48,26 +48,53 @@ export default class ClientHandler extends DefaultHandler { return this.existing; } - async calcChanges(assets) { + // Run after clients are updated so we can convert client_id names to id's + @order('60') + async processChanges(assets) { const { clientGrants } = assets; // Do nothing if not set - if (!clientGrants || !clientGrants.length) return {}; + if (!clientGrants) return; - // Convert enabled_clients by name to the id const clients = await this.client.clients.getAll({ paginate: true }); + const excludedClientsByNames = (assets.exclude && assets.exclude.clients) || []; + const excludedClients = excludedClientsByNames.map((clientName) => { + const found = clients.find(c => c.name === clientName); + return (found && found.client_id) || clientName; + }); + + // Convert clients by name to the id const formatted = assets.clientGrants.map((clientGrant) => { const grant = { ...clientGrant }; const found = clients.find(c => c.name === grant.client_id); if (found) grant.client_id = found.client_id; return grant; }); - return super.calcChanges({ ...assets, clientGrants: formatted }); - } - // Run after clients are updated so we can convert client_id names to id's - @order('60') - async processChanges(assets) { - await super.processChanges(assets); + // Always filter out the client we are using to access Auth0 Management API + const currentClient = this.config('AUTH0_CLIENT_ID'); + + const { + del, update, create, conflicts + } = await this.calcChanges({ ...assets, clientGrants: formatted }); + + const filterGrants = (list) => { + if (excludedClients.length) { + return list.filter(item => item.client_id !== currentClient && !excludedClients.includes(item.client_id)); + } + + return list.filter(item => item.client_id !== currentClient); + }; + + const changes = { + del: filterGrants(del), + update: filterGrants(update), + create: filterGrants(create), + conflicts: filterGrants(conflicts) + }; + + await super.processChanges(assets, { + ...changes + }); } } diff --git a/src/auth0/handlers/clients.js b/src/auth0/handlers/clients.js index 6905724..f8dd5cd 100644 --- a/src/auth0/handlers/clients.js +++ b/src/auth0/handlers/clients.js @@ -7,7 +7,7 @@ export const schema = { properties: { name: { type: 'string', minLength: 1, pattern: '[^<>]+' } }, - require: [ 'name' ] + required: [ 'name' ] } }; @@ -34,7 +34,9 @@ export default class ClientHandler extends DefaultHandler { const { clients } = assets; // Do nothing if not set - if (!clients || !clients.length) return; + if (!clients) return; + + const excludedClients = (assets.exclude && assets.exclude.clients) || []; const { del, update, create, conflicts @@ -43,11 +45,20 @@ export default class ClientHandler extends DefaultHandler { // Always filter out the client we are using to access Auth0 Management API // As it could cause problems if it gets deleted or updated etc const currentClient = this.config('AUTH0_CLIENT_ID'); + + const filterClients = (list) => { + if (excludedClients.length) { + return list.filter(item => item.client_id !== currentClient && !excludedClients.includes(item.name)); + } + + return list.filter(item => item.client_id !== currentClient); + }; + const changes = { - del: del.filter(c => c.client_id !== currentClient), - update: update.filter(c => c.client_id !== currentClient), - create: create.filter(c => c.client_id !== currentClient), - conflicts: conflicts.filter(c => c.client_id !== currentClient) + del: filterClients(del), + update: filterClients(update), + create: filterClients(create), + conflicts: filterClients(conflicts) }; await super.processChanges(assets, { diff --git a/src/auth0/handlers/connections.js b/src/auth0/handlers/connections.js index 5215bad..8b4b8f4 100644 --- a/src/auth0/handlers/connections.js +++ b/src/auth0/handlers/connections.js @@ -1,4 +1,5 @@ import DefaultHandler, { order } from './default'; +import { filterExcluded } from '../../utils'; export const schema = { type: 'array', @@ -7,13 +8,13 @@ export const schema = { properties: { name: { type: 'string' }, strategy: { type: 'string' }, - options: { type: 'object' } + options: { type: 'object' }, + enabled_clients: { type: 'array', items: { type: 'string' } }, + realms: { type: 'array', items: { type: 'string' } }, + metadata: { type: 'object' } }, - enabled_clients: { type: 'array', items: { type: 'string' } }, - realms: { type: 'array', items: { type: 'string' } }, - metadata: { type: 'object' } - }, - require: [ 'name', 'strategy' ] + required: [ 'name', 'strategy' ] + } }; @@ -43,7 +44,7 @@ export default class ConnectionsHandler extends DefaultHandler { const { connections } = assets; // Do nothing if not set - if (!connections || !connections.length) return {}; + if (!connections) return {}; // Convert enabled_clients by name to the id const clients = await this.client.clients.getAll({ paginate: true }); @@ -68,8 +69,12 @@ export default class ConnectionsHandler extends DefaultHandler { const { connections } = assets; // Do nothing if not set - if (!connections || !connections.length) return; + if (!connections) return; + + const excludedConnections = (assets.exclude && assets.exclude.connections) || []; + + const changes = await this.calcChanges(assets); - await super.processChanges(assets); + await super.processChanges(assets, filterExcluded(changes, excludedConnections)); } } diff --git a/src/auth0/handlers/databases.js b/src/auth0/handlers/databases.js index 220b5aa..65da739 100644 --- a/src/auth0/handlers/databases.js +++ b/src/auth0/handlers/databases.js @@ -1,5 +1,6 @@ import DefaultHandler, { order } from './default'; import constants from '../../constants'; +import { filterExcluded } from '../../utils'; export const schema = { type: 'array', @@ -15,13 +16,12 @@ export const schema = { type: 'object', properties: { ...constants.DATABASE_SCRIPTS.reduce((o, script) => ({ ...o, [script]: { type: 'string' } }), {}) - }, - require: [ 'login', 'get_user' ] + } } } } }, - require: [ 'name', 'import_mode' ] + required: [ 'name' ] } }; @@ -41,6 +41,15 @@ export default class DatabaseHandler extends DefaultHandler { getClientFN(fn) { // Override this as a database is actually a connection but we are treating them as a different object + // If we going to update database, we need to get current options first + if (fn === this.functions.update) { + return (params, payload) => this.client.connections.get(params) + .then((connection) => { + payload.options = Object.assign({}, connection.options, payload.options); + return this.client.connections.update(params, payload); + }); + } + return Reflect.get(this.client.connections, fn, this.client.connections); } @@ -55,20 +64,24 @@ export default class DatabaseHandler extends DefaultHandler { const { databases } = assets; // Do nothing if not set - if (!databases || !databases.length) return {}; + if (!databases) return {}; // Convert enabled_clients by name to the id const clients = await this.client.clients.getAll({ paginate: true }); - const formatted = databases.map(db => ({ - ...db, - enabled_clients: [ - ...(db.enabled_clients || []).map((name) => { - const found = clients.find(c => c.name === name); - if (found) return found.client_id; - return name; - }) - ] - })); + const formatted = databases.map((db) => { + if (db.enabled_clients) { + return { + ...db, + enabled_clients: db.enabled_clients.map((name) => { + const found = clients.find(c => c.name === name); + if (found) return found.client_id; + return name; + }) + }; + } + + return db; + }); return super.calcChanges({ ...assets, databases: formatted }); } @@ -79,8 +92,12 @@ export default class DatabaseHandler extends DefaultHandler { const { databases } = assets; // Do nothing if not set - if (!databases || !databases.length) return; + if (!databases) return; + + const excludedConnections = (assets.exclude && assets.exclude.databases) || []; + + const changes = await this.calcChanges(assets); - await super.processChanges(assets); + await super.processChanges(assets, filterExcluded(changes, excludedConnections)); } } diff --git a/src/auth0/handlers/default.js b/src/auth0/handlers/default.js index b29a348..0a04211 100644 --- a/src/auth0/handlers/default.js +++ b/src/auth0/handlers/default.js @@ -71,10 +71,10 @@ export default class DefaultHandler { } async calcChanges(assets) { - const typeAssets = assets[this.type] || []; + const typeAssets = assets[this.type]; // Do nothing if not set - if (!typeAssets.length) return {}; + if (!typeAssets) return {}; const existing = await this.getType(); @@ -122,7 +122,9 @@ export default class DefaultHandler { // Process Deleted if (del.length > 0) { - const shouldDelete = this.config('AUTH0_ALLOW_DELETE') === 'true' || this.config('AUTH0_ALLOW_DELETE') === true; + const allowDelete = this.config('AUTH0_ALLOW_DELETE') === 'true' || this.config('AUTH0_ALLOW_DELETE') === true; + const byExtension = this.config('EXTENSION_SECRET') && (this.type === 'rules' || this.type === 'resourceServers'); + const shouldDelete = allowDelete || byExtension; if (!shouldDelete) { log.warn(`Detected the following ${this.type} should be deleted. Doing so may be destructive.\nYou can enable deletes by setting 'AUTH0_ALLOW_DELETE' to true in the config \n${changes.del.map(i => this.objString(i)).join('\n')} diff --git a/src/auth0/handlers/emailProvider.js b/src/auth0/handlers/emailProvider.js index 69a66ce..80c45a6 100644 --- a/src/auth0/handlers/emailProvider.js +++ b/src/auth0/handlers/emailProvider.js @@ -2,6 +2,9 @@ import DefaultHandler from './default'; export const schema = { type: 'object' }; +// The Management API requires the fields to be specified +const defaultFields = [ 'name', 'enabled', 'credentials', 'settings', 'default_from_address' ]; + export default class EmailProviderHandler extends DefaultHandler { constructor(options) { super({ @@ -12,13 +15,17 @@ export default class EmailProviderHandler extends DefaultHandler { async getType() { try { - return await this.client.emailProvider.get(); + return await this.client.emailProvider.get({ include_fields: true, fields: defaultFields }); } catch (err) { if (err.statusCode === 404) return {}; throw err; } } + objString(provider) { + return super.objString({ name: provider.name, enabled: provider.enabled }); + } + async processChanges(assets) { const { emailProvider } = assets; diff --git a/src/auth0/handlers/emailTemplates.js b/src/auth0/handlers/emailTemplates.js index e78545d..2dbd484 100644 --- a/src/auth0/handlers/emailTemplates.js +++ b/src/auth0/handlers/emailTemplates.js @@ -13,7 +13,7 @@ export const schema = { template: { type: 'string', enum: supportedTemplates }, body: { type: 'string', default: '' } }, - require: [ 'template', 'body' ] + required: [ 'template' ] } }; diff --git a/src/auth0/handlers/guardianFactorProviders.js b/src/auth0/handlers/guardianFactorProviders.js index 4a50e57..91e94e1 100644 --- a/src/auth0/handlers/guardianFactorProviders.js +++ b/src/auth0/handlers/guardianFactorProviders.js @@ -17,7 +17,7 @@ export const schema = { name: { type: 'string', enum: constants.GUARDIAN_FACTORS }, provider: { type: 'string', enum: mappings.map(p => p.provider) } }, - require: [ 'name', 'provider' ] + required: [ 'name', 'provider' ] } }; diff --git a/src/auth0/handlers/guardianFactorTemplates.js b/src/auth0/handlers/guardianFactorTemplates.js index 46fe1eb..ccacf76 100644 --- a/src/auth0/handlers/guardianFactorTemplates.js +++ b/src/auth0/handlers/guardianFactorTemplates.js @@ -9,7 +9,7 @@ export const schema = { properties: { name: { type: 'string', enum: constants.GUARDIAN_FACTOR_TEMPLATES } }, - require: [ 'name' ] + required: [ 'name' ] } }; diff --git a/src/auth0/handlers/guardianFactors.js b/src/auth0/handlers/guardianFactors.js index 080ce11..34e0f6e 100644 --- a/src/auth0/handlers/guardianFactors.js +++ b/src/auth0/handlers/guardianFactors.js @@ -8,7 +8,7 @@ export const schema = { properties: { name: { type: 'string', enum: constants.GUARDIAN_FACTORS } }, - require: [ 'name' ] + required: [ 'name' ] } }; diff --git a/src/auth0/handlers/index.js b/src/auth0/handlers/index.js index df4bf78..5308717 100644 --- a/src/auth0/handlers/index.js +++ b/src/auth0/handlers/index.js @@ -12,6 +12,9 @@ import * as clientGrants from './clientGrants'; import * as guardianFactors from './guardianFactors'; import * as guardianFactorProviders from './guardianFactorProviders'; import * as guardianFactorTemplates from './guardianFactorTemplates'; +import * as roles from './roles'; +import * as branding from './branding'; +import * as prompts from './prompts'; export { rules, @@ -27,5 +30,8 @@ export { clientGrants, guardianFactors, guardianFactorProviders, - guardianFactorTemplates + guardianFactorTemplates, + roles, + branding, + prompts }; diff --git a/src/auth0/handlers/pages.js b/src/auth0/handlers/pages.js index f665505..44a99e3 100644 --- a/src/auth0/handlers/pages.js +++ b/src/auth0/handlers/pages.js @@ -20,7 +20,7 @@ export const schema = { html: { type: 'string', default: '' }, enabled: { type: 'boolean' } }, - require: [ 'html', 'name' ] + required: [ 'name' ] } }; @@ -115,7 +115,7 @@ export default class PageHandler extends DefaultHandler { const { pages } = assets; // Do nothing if not set - if (!pages || !pages.length) return; + if (!pages) return; // Login page is handled via the global client const loginPage = pages.find(p => p.name === 'login'); diff --git a/src/auth0/handlers/prompts.js b/src/auth0/handlers/prompts.js new file mode 100644 index 0000000..31a4726 --- /dev/null +++ b/src/auth0/handlers/prompts.js @@ -0,0 +1,39 @@ +import DefaultHandler from './default'; + +export const schema = { type: 'object' }; + +export default class PromptsHandler extends DefaultHandler { + constructor(options) { + super({ + ...options, + type: 'prompts' + }); + } + + async getType() { + // in case client version does not support branding + if (!this.client.prompts || typeof this.client.prompts.getSettings !== 'function') { + return {}; + } + + try { + return await this.client.prompts.getSettings(); + } catch (err) { + if (err.statusCode === 404) return {}; + if (err.statusCode === 501) return {}; + + throw err; + } + } + + async processChanges(assets) { + const { prompts } = assets; + + // Do nothing if not set + if (!prompts || !Object.keys(prompts).length) return; + + await this.client.prompts.updateSettings(prompts); + this.updated += 1; + this.didUpdate(prompts); + } +} diff --git a/src/auth0/handlers/resourceServers.js b/src/auth0/handlers/resourceServers.js index ac4a3ba..905e708 100644 --- a/src/auth0/handlers/resourceServers.js +++ b/src/auth0/handlers/resourceServers.js @@ -25,9 +25,11 @@ export const schema = { description: { type: 'string' } } } - } + }, + enforce_policies: { type: 'boolean' }, + token_dialect: { type: 'string' } }, - require: [ 'name', 'identifier' ] + required: [ 'name', 'identifier' ] } }; @@ -55,7 +57,7 @@ export default class ResourceServersHandler extends DefaultHandler { let { resourceServers } = assets; // Do nothing if not set - if (!resourceServers || !resourceServers.length) return {}; + if (!resourceServers) return {}; const excluded = (assets.exclude && assets.exclude.resourceServers) || []; @@ -65,14 +67,14 @@ export default class ResourceServersHandler extends DefaultHandler { resourceServers = resourceServers.filter(r => !excluded.includes(r.name)); existing = existing.filter(r => !excluded.includes(r.name)); - return calcChanges(resourceServers, existing, [ 'id', 'name' ]); + return calcChanges(resourceServers, existing, [ 'id', 'identifier' ]); } async validate(assets) { const { resourceServers } = assets; // Do nothing if not set - if (!resourceServers || !resourceServers.length) return; + if (!resourceServers) return; const mgmtAPIResource = resourceServers.find(r => r.name === constants.RESOURCE_SERVERS_MANAGEMENT_API_NAME); if (mgmtAPIResource) { diff --git a/src/auth0/handlers/roles.js b/src/auth0/handlers/roles.js new file mode 100644 index 0000000..2a006fb --- /dev/null +++ b/src/auth0/handlers/roles.js @@ -0,0 +1,135 @@ +import DefaultHandler, { order } from './default'; +import { calcChanges } from '../../utils'; +import log from '../../logger'; + +export const schema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + id: { type: 'string' }, + description: { type: 'string' }, + permissions: { + type: 'array', + items: { + type: 'object', + properties: { + permission_name: { type: 'string' }, + resource_server_identifier: { type: 'string' } + } + } + } + }, + required: [ 'name' ] + } +}; + +export default class RoleHandler extends DefaultHandler { + constructor(config) { + super({ + ...config, + type: 'roles', + id: 'id', + identifiers: [ 'name' ] + }); + } + + async createRoles(creates) { + await Promise.all(creates.map(async (roleData) => { + const data = { ...roleData }; + delete data.permissions; + const created = await this.client.roles.create(data); + if (typeof roleData.permissions !== 'undefined' && roleData.permissions.length > 0) await this.client.roles.permissions.create({ id: created.id }, { permissions: roleData.permissions }); + this.didCreate(created); + this.created += 1; + })); + } + + async deleteRoles(dels) { + if (this.config('AUTH0_ALLOW_DELETE') === 'true' || this.config('AUTH0_ALLOW_DELETE') === true) { + await Promise.all(dels.map(async (roleToDelete) => { + await this.client.roles.delete({ id: roleToDelete.id }); + this.didDelete(roleToDelete); + this.deleted += 1; + })); + } else { + log.warn(`Detected the following roles should be deleted. Doing so may be destructive.\nYou can enable deletes by setting 'AUTH0_ALLOW_DELETE' to true in the config + \n${dels.map(i => this.objString(i)).join('\n')}`); + } + } + + async updateRoles(updates, roles) { + await Promise.all(updates.map(async (updateRole) => { + const existingRole = await roles.find(roleDataForUpdate => roleDataForUpdate.name === updateRole.name); + const params = { id: updateRole.id }; + const newPermissions = updateRole.permissions; + delete updateRole.permissions; + delete updateRole.id; + await this.client.roles.update(params, updateRole); + if (typeof existingRole.permissions !== 'undefined' && existingRole.permissions.length > 0) { + await this.client.roles.permissions.delete(params, { permissions: existingRole.permissions }); + } + if (typeof newPermissions !== 'undefined' && newPermissions.length > 0) await this.client.roles.permissions.create(params, { permissions: newPermissions }); + this.didUpdate(params); + this.updated += 1; + })); + } + + async getType() { + if (this.existing) { + return this.existing; + } + + // in case client version does not support roles + if (!this.client.roles || typeof this.client.roles.getAll !== 'function') { + return {}; + } + + try { + const roles = await this.client.roles.getAll(); + for (let index = 0; index < roles.length; index++) { + const permissions = await this.client.roles.permissions.get({ id: roles[index].id }); + const strippedPerms = await Promise.all(permissions.map(async (permission) => { + delete permission.resource_server_name; + delete permission.description; + return permission; + })); + roles[index].permissions = strippedPerms; + } + this.existing = roles; + return this.existing; + } catch (err) { + if (err.statusCode === 404) return {}; + if (err.statusCode === 501) return {}; + throw err; + } + } + + @order('60') + async processChanges(assets) { + const { roles } = assets; + // Do nothing if not set + if (!roles) return; + // Gets roles from destination tenant + const existing = await this.getType(); + const changes = calcChanges(roles, existing, [ 'id', 'name' ]); + log.debug(`Start processChanges for roles [delete:${changes.del.length}] [update:${changes.update.length}], [create:${changes.create.length}]`); + const myChanges = [ { del: changes.del }, { create: changes.create }, { update: changes.update } ]; + await Promise.all(myChanges.map(async (change) => { + switch (true) { + case change.del && change.del.length > 0: + await this.deleteRoles(change.del); + break; + case change.create && change.create.length > 0: + await this.createRoles(changes.create); + break; + case change.update && change.update.length > 0: + await this.updateRoles(change.update, existing); + break; + default: + break; + } + })); + } +} diff --git a/src/auth0/handlers/rules.js b/src/auth0/handlers/rules.js index 8faff4d..c778f01 100644 --- a/src/auth0/handlers/rules.js +++ b/src/auth0/handlers/rules.js @@ -27,7 +27,7 @@ export const schema = { pattern: '^[^-\\s][a-zA-Z0-9-\\s]+[^-\\s]$' }, order: { - type: 'number', + type: [ 'number', 'null' ], description: 'The rule\'s order in relation to other rules. A rule with a lower order than another rule executes first.', default: null }, @@ -42,7 +42,8 @@ export const schema = { default: 'login_success', enum: [ 'login_success', 'login_failure', 'pre_authorize' ] } - } + }, + required: [ 'name' ] } }; @@ -113,7 +114,7 @@ export default class RulesHandler extends DefaultHandler { const { rules } = assets; // Do nothing if not set - if (!rules || !rules.length) return; + if (!rules) return; const excludedRules = (assets.exclude && assets.exclude.rules) || []; @@ -152,7 +153,7 @@ export default class RulesHandler extends DefaultHandler { const { rules } = assets; // Do nothing if not set - if (!rules || !rules.length) return; + if (!rules) return; // Figure out what needs to be updated vs created const changes = await this.calcChanges(assets); diff --git a/src/auth0/handlers/rulesConfigs.js b/src/auth0/handlers/rulesConfigs.js index 728ca41..6e09342 100644 --- a/src/auth0/handlers/rulesConfigs.js +++ b/src/auth0/handlers/rulesConfigs.js @@ -7,7 +7,8 @@ export const schema = { properties: { key: { type: 'string', pattern: '^[A-Za-z0-9_-]*$' }, value: { type: 'string' } - } + }, + required: [ 'key', 'value' ] }, additionalProperties: false }; diff --git a/src/constants.js b/src/constants.js index 85e1c9c..52cc0c6 100644 --- a/src/constants.js +++ b/src/constants.js @@ -132,5 +132,6 @@ constants.CONNECTIONS_CLIENT_NAME = 'connections'; constants.CONNECTIONS_ID_NAME = 'id'; constants.CONCURRENT_CALLS = 5; +constants.ROLES_DIRECTORY = 'roles'; export default constants; diff --git a/src/utils.js b/src/utils.js index f3bca47..086ab49 100644 --- a/src/utils.js +++ b/src/utils.js @@ -167,3 +167,23 @@ export function duplicateItems(arr, key) { }, {}); return Object.values(duplicates).filter(g => g.length > 1); } + + +export function filterExcluded(changes, exclude) { + const { + del, update, create, conflicts + } = changes; + + if (!exclude.length) { + return changes; + } + + const filter = list => list.filter(item => !exclude.includes(item.name)); + + return { + del: filter(del), + update: filter(update), + create: filter(create), + conflicts: filter(conflicts) + }; +} diff --git a/tests/auth0/handlers/branding.tests.js b/tests/auth0/handlers/branding.tests.js new file mode 100644 index 0000000..583a8ea --- /dev/null +++ b/tests/auth0/handlers/branding.tests.js @@ -0,0 +1,41 @@ +const { expect } = require('chai'); +const branding = require('../../../src/auth0/handlers/branding'); + +describe('#branding handler', () => { + describe('#branding process', () => { + it('should get branding', async () => { + const auth0 = { + branding: { + getSettings: () => ({ + logo_url: 'https://example.com/logo.png' + }) + } + }; + + const handler = new branding.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal({ + logo_url: 'https://example.com/logo.png' + }); + }); + + it('should update branding settings', async () => { + const auth0 = { + branding: { + updateSettings: (data) => { + expect(data).to.be.an('object'); + expect(data.logo_url).to.equal('https://example.com/logo.png'); + return Promise.resolve(data); + } + } + }; + + const handler = new branding.default({ client: auth0 }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ + { branding: { logo_url: 'https://example.com/logo.png' } } + ]); + }); + }); +}); diff --git a/tests/auth0/handlers/clientGrants.tests.js b/tests/auth0/handlers/clientGrants.tests.js index fe0bb9e..e86ccc4 100644 --- a/tests/auth0/handlers/clientGrants.tests.js +++ b/tests/auth0/handlers/clientGrants.tests.js @@ -12,7 +12,7 @@ const pool = { describe('#clientGrants handler', () => { const config = function(key) { - return this.data && this.data[key]; + return config.data && config.data[key]; }; config.data = { @@ -217,5 +217,153 @@ describe('#clientGrants handler', () => { await stageFn.apply(handler, [ { clientGrants: data } ]); }); + + it('should not delete nor create client grant for own client', async () => { + const auth0 = { + clientGrants: { + create: (params) => { + expect(params).to.be.an('undefined'); + + return Promise.resolve([]); + }, + update: (params) => { + expect(params).to.be.an('undefined'); + + return Promise.resolve([]); + }, + delete: (params) => { + expect(params).to.be.an('undefined'); + + return Promise.resolve([]); + }, + getAll: () => [ { id: 'id', client_id: 'client_id', audience: 'audience' } ] + }, + clients: { + getAll: () => [] + }, + pool + }; + + const handler = new clientGrants.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + const data = [ + { + name: 'someClientGrant', + client_id: 'client_id', + audience: 'audience' + } + ]; + + await stageFn.apply(handler, [ { clientGrants: data } ]); + }); + + it('should delete all client grants', async () => { + let removed = false; + const auth0 = { + clientGrants: { + create: () => Promise.resolve([]), + update: () => Promise.resolve([]), + delete: (params) => { + expect(params).to.be.an('object'); + expect(params.id).to.equal('cg1'); + removed = true; + return Promise.resolve([]); + }, + getAll: () => [ { id: 'cg1', client_id: 'client1', audience: 'audience1' } ] + }, + clients: { + getAll: () => [] + }, + pool + }; + + const handler = new clientGrants.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { clientGrants: [] } ]); + expect(removed).to.equal(true); + }); + + it('should not delete client grants if run by extensions', async () => { + config.data = { + EXTENSION_SECRET: 'some-secret' + }; + + const auth0 = { + clientGrants: { + create: () => Promise.resolve([]), + update: () => Promise.resolve([]), + delete: (params) => { + expect(params).to.be.an('undefined'); + + return Promise.resolve([]); + }, + getAll: () => [ { id: 'cg1', client_id: 'client1', audience: 'audience1' } ] + }, + clients: { + getAll: () => [] + }, + pool + }; + + const handler = new clientGrants.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { clientGrants: [] } ]); + }); + + it('should not touch client grants of excluded clients', async () => { + config.data = { + EXTENSION_SECRET: 'some-secret' + }; + + const auth0 = { + clientGrants: { + create: (params) => { + expect(params).to.be.an('undefined'); + + return Promise.resolve([]); + }, + update: (params) => { + expect(params).to.be.an('undefined'); + + return Promise.resolve([]); + }, + delete: (params) => { + expect(params).to.be.an('undefined'); + + return Promise.resolve([]); + }, + getAll: () => [ + { id: 'cg1', client_id: 'client1', audience: 'audience1' }, + { id: 'cg2', client_id: 'client2', audience: 'audience2' } + ] + }, + clients: { + getAll: () => [ + { name: 'client_delete', client_id: 'client1', audience: 'audience1' }, + { name: 'client_update', client_id: 'client2', audience: 'audience2' }, + { name: 'client_create', client_id: 'client3', audience: 'audience3' } + ] + }, + pool + }; + + const handler = new clientGrants.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + const assets = { + clientGrants: [ + { + name: 'newClientGrant', + client_id: 'client_create', + audience: 'audience3' + } + ], + exclude: { clients: [ 'client_delete', 'client_update', 'client_create' ] } + }; + + await stageFn.apply(handler, [ assets ]); + }); }); }); diff --git a/tests/auth0/handlers/clients.tests.js b/tests/auth0/handlers/clients.tests.js index 7fde289..268b17d 100644 --- a/tests/auth0/handlers/clients.tests.js +++ b/tests/auth0/handlers/clients.tests.js @@ -149,6 +149,30 @@ describe('#clients handler', () => { await stageFn.apply(handler, [ { clients: [ { name: 'someClient' } ] } ]); }); + it('should delete all clients', async () => { + let removed = false; + const auth0 = { + clients: { + create: () => Promise.resolve([]), + update: () => Promise.resolve([]), + delete: (params) => { + expect(params).to.be.an('object'); + expect(params.client_id).to.equal('client1'); + removed = true; + return Promise.resolve([]); + }, + getAll: () => [ { client_id: 'client1', name: 'existingClient' }, { client_id: 'client_id', name: 'deploy client' } ] + }, + pool + }; + + const handler = new clients.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { clients: [] } ]); + expect(removed).to.equal(true); + }); + it('should not remove client if it is not allowed by config', async () => { config.data.AUTH0_ALLOW_DELETE = false; const auth0 = { @@ -169,5 +193,77 @@ describe('#clients handler', () => { await stageFn.apply(handler, [ { clients: [ { name: 'newClient' } ] } ]); }); + + it('should not remove, update or create client if it is excluded', async () => { + config.data.AUTH0_ALLOW_DELETE = true; + const auth0 = { + clients: { + create: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + update: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + delete: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + getAll: () => [ + { client_id: 'client1', name: 'existingClient' }, + { client_id: 'client2', name: 'existingClient2' } + ] + }, + pool + }; + + const assets = { + clients: [ + { name: 'excludedClient' }, + { name: 'existingClient' } + ], + exclude: { + clients: [ + 'excludedClient', + 'existingClient', + 'existingClient2' + ] + } + }; + + const handler = new clients.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ assets ]); + }); + + it('should not remove clients if run by extension', async () => { + config.data = { + EXTENSION_SECRET: 'some-secret' + }; + + const auth0 = { + clients: { + create: () => Promise.resolve([]), + update: () => Promise.resolve([]), + delete: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + getAll: () => [ + { client_id: 'client1', name: 'existingClient' }, + { client_id: 'client2', name: 'existingClient2' } + ] + }, + pool + }; + + + const handler = new clients.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { clients: [] } ]); + }); }); }); diff --git a/tests/auth0/handlers/connections.tests.js b/tests/auth0/handlers/connections.tests.js index 6d35abd..6e804c5 100644 --- a/tests/auth0/handlers/connections.tests.js +++ b/tests/auth0/handlers/connections.tests.js @@ -179,6 +179,33 @@ describe('#connections handler', () => { await stageFn.apply(handler, [ { connections: data } ]); }); + it('should delete all connections', async () => { + let removed = false; + const auth0 = { + connections: { + create: () => Promise.resolve([]), + update: () => Promise.resolve([]), + delete: (params) => { + expect(params).to.be.an('object'); + expect(params.id).to.equal('con1'); + removed = true; + return Promise.resolve([]); + }, + getAll: () => [ { id: 'con1', name: 'existingConnection', strategy: 'custom' } ] + }, + clients: { + getAll: () => [] + }, + pool + }; + + const handler = new connections.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { connections: [] } ]); + expect(removed).to.equal(true); + }); + it('should not remove if it is not allowed by config', async () => { config.data.AUTH0_ALLOW_DELETE = false; const auth0 = { @@ -208,5 +235,73 @@ describe('#connections handler', () => { await stageFn.apply(handler, [ { connections: data } ]); }); + + it('should not remove connections if run by extension', async () => { + config.data = { + EXTENSION_SECRET: 'some-secret' + }; + const auth0 = { + connections: { + create: () => Promise.resolve(), + update: () => Promise.resolve([]), + delete: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + getAll: () => [ { id: 'con1', name: 'existingConnection', strategy: 'custom' } ] + }, + clients: { + getAll: () => [] + }, + pool + }; + + const handler = new connections.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { connections: [] } ]); + }); + + it('should not remove/create/update excluded connections', async () => { + config.data = { + EXTENSION_SECRET: false, + AUTH0_ALLOW_DELETE: true + }; + const auth0 = { + connections: { + create: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + update: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + delete: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + getAll: () => [ + { id: 'con1', name: 'existing1', strategy: 'custom' }, + { id: 'con2', name: 'existing2', strategy: 'custom' } + ] + }, + clients: { + getAll: () => [] + }, + pool + }; + + const handler = new connections.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + const assets = { + exclude: { + connections: [ 'existing1', 'existing2', 'existing3' ] + }, + connections: [ { name: 'existing3', strategy: 'custom' } ] + }; + + await stageFn.apply(handler, [ assets ]); + }); }); }); diff --git a/tests/auth0/handlers/databases.tests.js b/tests/auth0/handlers/databases.tests.js index abd0fc0..8781a21 100644 --- a/tests/auth0/handlers/databases.tests.js +++ b/tests/auth0/handlers/databases.tests.js @@ -98,6 +98,11 @@ describe('#databases handler', () => { it('should update database', async () => { const auth0 = { connections: { + get: (params) => { + expect(params).to.be.an('object'); + expect(params.id).to.equal('con1'); + return Promise.resolve({ options: { someOldOption: true } }); + }, create: (data) => { expect(data).to.be.an('undefined'); return Promise.resolve(data); @@ -107,7 +112,7 @@ describe('#databases handler', () => { expect(params.id).to.equal('con1'); expect(data).to.deep.equal({ enabled_clients: [ 'YwqVtt8W3pw5AuEz3B2Kse9l2Ruy7Tec' ], - options: { passwordPolicy: 'testPolicy' } + options: { passwordPolicy: 'testPolicy', someOldOption: true } }); return Promise.resolve({ ...params, ...data }); @@ -135,6 +140,49 @@ describe('#databases handler', () => { await stageFn.apply(handler, [ { databases: data } ]); }); + it('should update database without "enabled_clients" setting', async () => { + const auth0 = { + connections: { + get: (params) => { + expect(params).to.be.an('object'); + expect(params.id).to.equal('con1'); + return Promise.resolve({}); + }, + create: (data) => { + expect(data).to.be.an('undefined'); + return Promise.resolve(data); + }, + update: (params, data) => { + expect(params).to.be.an('object'); + expect(params.id).to.equal('con1'); + expect(data).to.deep.equal({ + options: { passwordPolicy: 'testPolicy' } + }); + + return Promise.resolve({ ...params, ...data }); + }, + delete: () => Promise.resolve([]), + getAll: () => [ { name: 'someDatabase', id: 'con1', strategy: 'auth0' } ] + }, + clients: { + getAll: () => [ { name: 'client1', client_id: 'YwqVtt8W3pw5AuEz3B2Kse9l2Ruy7Tec' } ] + }, + pool + }; + + const handler = new databases.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + const data = [ + { + name: 'someDatabase', + strategy: 'auth0', + options: { passwordPolicy: 'testPolicy' } + } + ]; + + await stageFn.apply(handler, [ { databases: data } ]); + }); + it('should delete database and create another one instead', async () => { const auth0 = { connections: { @@ -170,6 +218,33 @@ describe('#databases handler', () => { await stageFn.apply(handler, [ { databases: data } ]); }); + it('should delete all databases', async () => { + let removed = false; + const auth0 = { + connections: { + create: () => Promise.resolve([]), + update: () => Promise.resolve([]), + delete: (params) => { + expect(params).to.be.an('object'); + expect(params.id).to.equal('con1'); + removed = true; + return Promise.resolve([]); + }, + getAll: () => [ { id: 'con1', name: 'existingConnection', strategy: 'auth0' } ] + }, + clients: { + getAll: () => [] + }, + pool + }; + + const handler = new databases.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { databases: [] } ]); + expect(removed).to.equal(true); + }); + it('should not remove if it is not allowed by config', async () => { config.data.AUTH0_ALLOW_DELETE = false; const auth0 = { @@ -199,5 +274,73 @@ describe('#databases handler', () => { await stageFn.apply(handler, [ { databases: data } ]); }); + + it('should not remove databases if run by extension', async () => { + config.data = { + EXTENSION_SECRET: 'some-secret' + }; + const auth0 = { + connections: { + create: () => Promise.resolve(), + update: () => Promise.resolve([]), + delete: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + getAll: () => [ { id: 'con1', name: 'existingConnection', strategy: 'auth0' } ] + }, + clients: { + getAll: () => [] + }, + pool + }; + + const handler = new databases.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { databases: [] } ]); + }); + + it('should not remove/create/update excluded connections', async () => { + config.data = { + EXTENSION_SECRET: false, + AUTH0_ALLOW_DELETE: true + }; + const auth0 = { + connections: { + create: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + update: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + delete: (params) => { + expect(params).to.be.an('undefined'); + return Promise.resolve([]); + }, + getAll: () => [ + { id: 'con1', name: 'existing1', strategy: 'auth0' }, + { id: 'con2', name: 'existing2', strategy: 'auth0' } + ] + }, + clients: { + getAll: () => [] + }, + pool + }; + + const handler = new databases.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + const assets = { + exclude: { + databases: [ 'existing1', 'existing2', 'existing3' ] + }, + databases: [ { name: 'existing3', strategy: 'auth0' } ] + }; + + await stageFn.apply(handler, [ assets ]); + }); }); }); diff --git a/tests/auth0/handlers/prompts.tests.js b/tests/auth0/handlers/prompts.tests.js new file mode 100644 index 0000000..409f513 --- /dev/null +++ b/tests/auth0/handlers/prompts.tests.js @@ -0,0 +1,41 @@ +const { expect } = require('chai'); +const prompts = require('../../../src/auth0/handlers/prompts'); + +describe('#prompts handler', () => { + describe('#prompts process', () => { + it('should get prompts', async () => { + const auth0 = { + prompts: { + getSettings: () => ({ + universal_login_experience: 'new' + }) + } + }; + + const handler = new prompts.default({ client: auth0 }); + const data = await handler.getType(); + expect(data).to.deep.equal({ + universal_login_experience: 'new' + }); + }); + + it('should update prompts settings', async () => { + const auth0 = { + prompts: { + updateSettings: (data) => { + expect(data).to.be.an('object'); + expect(data.universal_login_experience).to.equal('new'); + return Promise.resolve(data); + } + } + }; + + const handler = new prompts.default({ client: auth0 }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ + { prompts: { universal_login_experience: 'new' } } + ]); + }); + }); +}); diff --git a/tests/auth0/handlers/resourceServers.tests.js b/tests/auth0/handlers/resourceServers.tests.js index 0e82aab..d1d4730 100644 --- a/tests/auth0/handlers/resourceServers.tests.js +++ b/tests/auth0/handlers/resourceServers.tests.js @@ -130,6 +130,33 @@ describe('#resourceServers handler', () => { await stageFn.apply(handler, [ { resourceServers: [ { name: 'someAPI', identifier: 'some-api', scope: 'new:scope' } ] } ]); }); + it('should create new resource server with same name but different identifier', async () => { + const auth0 = { + resourceServers: { + create: (data) => { + expect(data).to.be.an('object'); + expect(data.name).to.equal('someAPI'); + expect(data.scope).to.equal('new:scope'); + expect(data.identifier).to.equal('another-api'); + return Promise.resolve(data); + }, + update: (params, data) => { + expect(params).to.be('undefined'); + expect(data).to.be('undefined'); + return Promise.resolve(data); + }, + delete: () => Promise.resolve([]), + getAll: () => [ { id: 'rs1', identifier: 'some-api', name: 'someAPI' } ] + }, + pool + }; + + const handler = new resourceServers.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { resourceServers: [ { name: 'someAPI', identifier: 'another-api', scope: 'new:scope' } ] } ]); + }); + it('should remove resource server', async () => { const auth0 = { resourceServers: { @@ -151,6 +178,58 @@ describe('#resourceServers handler', () => { await stageFn.apply(handler, [ { resourceServers: [ {} ] } ]); }); + it('should remove all resource servers', async () => { + let removed = false; + const auth0 = { + resourceServers: { + create: () => Promise.resolve([]), + update: () => Promise.resolve([]), + delete: (data) => { + expect(data).to.be.an('object'); + expect(data.id).to.equal('rs1'); + removed = true; + return Promise.resolve(data); + }, + getAll: () => [ { id: 'rs1', identifier: 'some-api', name: 'someAPI' } ] + }, + pool + }; + + const handler = new resourceServers.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { resourceServers: [] } ]); + expect(removed).to.equal(true); + }); + + it('should remove resource servers is run by extension', async () => { + config.data = { + EXTENSION_SECRET: 'some-secret' + }; + + let removed = false; + const auth0 = { + resourceServers: { + create: () => Promise.resolve([]), + update: () => Promise.resolve([]), + delete: (data) => { + expect(data).to.be.an('object'); + expect(data.id).to.equal('rs1'); + removed = true; + return Promise.resolve(data); + }, + getAll: () => [ { id: 'rs1', identifier: 'some-api', name: 'someAPI' } ] + }, + pool + }; + + const handler = new resourceServers.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { resourceServers: [] } ]); + expect(removed).to.equal(true); + }); + it('should not touch excluded resource servers', async () => { const auth0 = { resourceServers: { diff --git a/tests/auth0/handlers/roles.tests.js b/tests/auth0/handlers/roles.tests.js new file mode 100644 index 0000000..43d44f6 --- /dev/null +++ b/tests/auth0/handlers/roles.tests.js @@ -0,0 +1,240 @@ +const { expect } = require('chai'); +const roles = require('../../../src/auth0/handlers/roles'); + +const pool = { + addEachTask: (data) => { + if (data.data && data.data.length) { + data.generator(data.data[0]); + } + return { promise: () => null }; + } +}; + +describe('#roles handler', () => { + const config = function(key) { + return config.data && config.data[key]; + }; + + config.data = { + AUTH0_CLIENT_ID: 'client_id', + AUTH0_ALLOW_DELETE: true + }; + + describe('#roles validate', () => { + it('should not allow same names', async () => { + const handler = new roles.default({ client: {}, config }); + const stageFn = Object.getPrototypeOf(handler).validate; + const data = [ + { + name: 'myRole' + }, + { + name: 'myRole' + } + ]; + + try { + await stageFn.apply(handler, [ { roles: data } ]); + } catch (err) { + expect(err).to.be.an('object'); + expect(err.message).to.include('Names must be unique'); + } + }); + + it('should pass validation', async () => { + const handler = new roles.default({ client: {}, config }); + const stageFn = Object.getPrototypeOf(handler).validate; + const data = [ + { + name: 'myRole' + } + ]; + + await stageFn.apply(handler, [ { roles: data } ]); + }); + }); + + describe('#roles process', () => { + it('should create role', async () => { + const auth0 = { + roles: { + create: (data) => { + expect(data).to.be.an('object'); + expect(data.name).to.equal('myRole'); + expect(data.description).to.equal('myDescription'); + return Promise.resolve(data); + }, + update: () => Promise.resolve([]), + delete: () => Promise.resolve([]), + getAll: () => [], + permissions: { + get: () => [ + { permission_name: 'Create:cal_entry', resource_server_identifier: 'organise' } + ], + create: (params, data) => { + expect(params).to.be.an('object'); + expect(params.id).to.equal('myRoleId'); + expect(data).to.be.an('object'); + expect(data.permissions).to.not.equal(null); + expect(data.permissions).to.be.an('Array'); + return Promise.resolve(data.permissions); + }, + update: Promise.resolve([]) + } + }, + pool + }; + const handler = new roles.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + await stageFn.apply(handler, [ + { + roles: [ + { + name: 'myRole', + id: 'myRoleId', + description: 'myDescription', + permissions: [] + } + ] + } + ]); + }); + + it('should get roles', async () => { + const auth0 = { + roles: { + getAll: () => [ + { name: 'myRole', id: 'myRoleId', description: 'myDescription' } + ], + permissions: { + get: () => [ + { permission_name: 'Create:cal_entry', resource_server_identifier: 'organise' } + ] + } + }, + pool + }; + + const handler = new roles.default({ client: auth0, config }); + const data = await handler.getType(); + expect(data).to.deep.equal([ + { + name: 'myRole', + id: 'myRoleId', + description: 'myDescription', + permissions: [ + { + permission_name: 'Create:cal_entry', resource_server_identifier: 'organise' + } + ] + } + ]); + }); + + it('should update role', async () => { + const auth0 = { + roles: { + create: (data) => { + expect(data).to.be.an('object'); + expect(data.length).to.equal(0); + return Promise.resolve(data); + }, + update: (params, data) => { + expect(params).to.be.an('object'); + expect(params.id).to.equal('myRoleId'); + expect(data).to.be.an('object'); + expect(data.name).to.equal('myRole'); + expect(data.description).to.equal('myDescription'); + + return Promise.resolve(data); + }, + delete: () => Promise.resolve([]), + getAll: () => [ + { + name: 'myRole', + id: 'myRoleId', + description: 'myDescription' + } + ], + permissions: { + get: () => [ + { permission_name: 'Create:cal_entry', resource_server_identifier: 'organise' } + ], + getAll: () => [ + { permission_name: 'Create:cal_entry', resource_server_identifier: 'organise' } + ], + create: (params, data) => { + expect(params).to.be.an('object'); + expect(params.id).to.equal('myRoleId'); + expect(data).to.be.an('object'); + expect(data.permissions).to.not.equal(null); + expect(data.permissions).to.be.an('Array'); + return Promise.resolve(data); + }, + delete: (params, data) => { + expect(params).to.be.an('object'); + expect(params.id).to.equal('myRoleId'); + expect(data.permissions).to.be.an('Array'); + return Promise.resolve(data.permissions); + } + } + + }, + pool + }; + + const handler = new roles.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ + { + roles: [ + { + name: 'myRole', + id: 'myRoleId', + description: 'myDescription', + permissions: [ + { + permission_name: 'Create:cal_entry', resource_server_identifier: 'organise' + } + ] + } + ] + } + ]); + }); + + it('should delete role', async () => { + const auth0 = { + roles: { + create: () => Promise.resolve([]), + update: () => Promise.resolve([]), + delete: (data) => { + expect(data).to.be.an('object'); + expect(data.id).to.equal('myRoleId'); + return Promise.resolve(data); + }, + getAll: () => [ + { + name: 'myRole', + id: 'myRoleId', + description: 'myDescription', + permissions: [ + { + permission_name: 'Create:cal_entry', resource_server_identifier: 'organise' + } + ] + } + ], + permissions: { + get: () => [] + } + }, + pool + }; + const handler = new roles.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + await stageFn.apply(handler, [ { roles: [ {} ] } ]); + }); + }); +}); diff --git a/tests/auth0/handlers/rules.tests.js b/tests/auth0/handlers/rules.tests.js index 4aba443..338509a 100644 --- a/tests/auth0/handlers/rules.tests.js +++ b/tests/auth0/handlers/rules.tests.js @@ -208,10 +208,65 @@ describe('#rules handler', () => { await stageFn.apply(handler, [ { rules: [ {} ] } ]); }); - it('should not touch excluded rules', async () => { + it('should remove all rules', async () => { + let removed = false; const auth0 = { rules: { create: () => Promise.resolve([]), + update: () => Promise.resolve([]), + delete: (data) => { + expect(data).to.be.an('object'); + expect(data.id).to.equal('rule1'); + removed = true; + return Promise.resolve(data); + }, + getAll: () => [ { id: 'rule1', name: 'existingRule', order: '10' } ] + }, + pool + }; + + const handler = new rules.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { rules: [] } ]); + expect(removed).to.equal(true); + }); + + it('should remove rules if run by extension', async () => { + config.data = { + EXTENSION_SECRET: 'some-secret' + }; + + let removed = false; + const auth0 = { + rules: { + create: () => Promise.resolve([]), + update: () => Promise.resolve([]), + delete: (data) => { + expect(data).to.be.an('object'); + expect(data.id).to.equal('rule1'); + removed = true; + return Promise.resolve(data); + }, + getAll: () => [ { id: 'rule1', name: 'existingRule', order: '10' } ] + }, + pool + }; + + const handler = new rules.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await stageFn.apply(handler, [ { rules: [] } ]); + expect(removed).to.equal(true); + }); + + it('should not touch excluded rules', async () => { + const auth0 = { + rules: { + create: (data) => { + expect(data).to.be.an('undefined'); + return Promise.resolve(data); + }, update: (data) => { expect(data).to.be.an('undefined'); return Promise.resolve(data); @@ -231,11 +286,12 @@ describe('#rules handler', () => { const handler = new rules.default({ client: auth0, config }); const stageFn = Object.getPrototypeOf(handler).processChanges; const data = { - rules: [ { name: 'Rule1', script: 'new-rule-one-script' } ], + rules: [ { name: 'Rule1', script: 'new-rule-one-script' }, { name: 'Rule3', script: 'new-rule-three-script' } ], exclude: { rules: [ 'Rule1', - 'Rule2' + 'Rule2', + 'Rule3' ] } }; diff --git a/tests/auth0/validator.tests.js b/tests/auth0/validator.tests.js new file mode 100644 index 0000000..7949834 --- /dev/null +++ b/tests/auth0/validator.tests.js @@ -0,0 +1,522 @@ +import { expect } from 'chai'; +import Auth0 from '../../src/auth0'; + +describe('#schema validation tests', () => { + const client = { + rules: { + getAll: () => [] + } + }; + + const failedCb = done => err => done(err || 'test failed'); + + const passedCb = (done, message) => (err) => { + if (err || message) expect(err.message).to.contain(message); + done(); + }; + + const checkPassed = (data, done) => { + const auth0 = new Auth0(client, data, {}); + + auth0.validate().then(passedCb(done), failedCb(done)); + }; + + const checkRequired = (field, data, done) => { + const auth0 = new Auth0({}, data, {}); + + auth0 + .validate() + .then( + failedCb(done), + passedCb(done, `should have required property '${field}'`) + ); + }; + + const checkEnum = (data, done) => { + const auth0 = new Auth0({}, data, {}); + + auth0 + .validate() + .then( + failedCb(done), + passedCb(done, 'should be equal to one of the allowed values') + ); + }; + + describe('#branding validate', () => { + it('should fail validation if branding is not an object', (done) => { + const data = [ { + anything: 'anything' + } ]; + + const auth0 = new Auth0({}, { branding: data }, {}); + + auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); + }); + + it('should pass validation', (done) => { + const data = { + anything: 'anything' + }; + + checkPassed({ branding: data }, done); + }); + }); + + describe('#clientGrants validate', () => { + it('should fail validation if no "client_id" provided', (done) => { + const data = [ { + name: 'name' + } ]; + + checkRequired('client_id', { clientGrants: data }, done); + }); + + it('should fail validation if no "scope" provided', (done) => { + const data = [ { + client_id: 'client_id', + audience: 'audience' + } ]; + + checkRequired('scope', { clientGrants: data }, done); + }); + + it('should fail validation if no "audience" provided', (done) => { + const data = [ { + client_id: 'client_id', + scope: [ 'scope' ] + } ]; + + checkRequired('audience', { clientGrants: data }, done); + }); + + it('should fail validation if bad "scope" provided', (done) => { + const data = [ { + client_id: 'client_id', + scope: 'scope', + audience: 'audience' + } ]; + + const auth0 = new Auth0({}, { clientGrants: data }, {}); + + auth0.validate().then(failedCb(done), passedCb(done, 'should be array')); + }); + + it('should pass validation', (done) => { + const data = [ { + client_id: 'client_id', + scope: [ 'scope' ], + audience: 'audience' + } ]; + + checkPassed({ clientGrants: data }, done); + }); + }); + + describe('#clients validate', () => { + it('should fail validation if no "name" provided', (done) => { + const data = [ { + id: 'id' + } ]; + + checkRequired('name', { clients: data }, done); + }); + + it('should fail validation if bad "name" provided', (done) => { + const data = [ { + name: '' + } ]; + + const auth0 = new Auth0({}, { clients: data }, {}); + + auth0 + .validate() + .then( + failedCb(done), + passedCb(done, 'should NOT be shorter than 1 characters') + ); + }); + + it('should pass validation', (done) => { + const data = [ { + name: 'name' + } ]; + + checkPassed({ clients: data }, done); + }); + }); + + describe('#connections validate', () => { + it('should fail validation if no "name" provided', (done) => { + const data = [ { + id: 'id' + } ]; + + checkRequired('name', { connections: data }, done); + }); + + it('should fail validation if no "strategy" provided', (done) => { + const data = [ { + name: 'name' + } ]; + + checkRequired('strategy', { connections: data }, done); + }); + + it('should pass validation', (done) => { + const data = [ { + name: 'name', + strategy: 'strategy' + } ]; + + checkPassed({ connections: data }, done); + }); + }); + + describe('#databases validate', () => { + it('should fail validation if no "name" provided', (done) => { + const data = [ { + id: 'id' + } ]; + + checkRequired('name', { databases: data }, done); + }); + + it('should fail validation if bad "strategy" provided', (done) => { + const data = [ { + name: 'name', + strategy: 'strategy' + } ]; + + checkEnum({ databases: data }, done); + }); + + it('should pass validation', (done) => { + const data = [ { + name: 'name', + options: {} + } ]; + + checkPassed({ databases: data }, done); + }); + }); + + describe('#emailProvider validate', () => { + it('should fail validation if emailProvider is not an object', (done) => { + const data = [ { + anything: 'anything' + } ]; + + const auth0 = new Auth0({}, { emailProvider: data }, {}); + + auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); + }); + + it('should pass validation', (done) => { + const data = { + anything: 'anything' + }; + + checkPassed({ emailProvider: data }, done); + }); + }); + + describe('#emailTemplates validate', () => { + it('should fail validation if no "template" provided', (done) => { + const data = [ { + anything: 'anything' + } ]; + + checkRequired('template', { emailTemplates: data }, done); + }); + + it('should fail validation if bad "template" provided', (done) => { + const data = [ { + template: 'template', + body: 'body' + } ]; + + checkEnum({ emailTemplates: data }, done); + }); + + it('should pass validation', (done) => { + const data = [ { + template: 'verify_email', + body: 'body' + } ]; + + checkPassed({ emailTemplates: data }, done); + }); + }); + + describe('#guardianFactorProviders validate', () => { + it('should fail validation if no "name" provided', (done) => { + const data = [ { + anything: 'anything' + } ]; + + checkRequired('name', { guardianFactorProviders: data }, done); + }); + + it('should fail validation if no "provider" provided', (done) => { + const data = [ { + name: 'sms' + } ]; + + checkRequired('provider', { guardianFactorProviders: data }, done); + }); + + it('should fail validation if bad "name" provided', (done) => { + const data = [ { + name: 'name', + provider: 'provider' + } ]; + + checkEnum({ guardianFactorProviders: data }, done); + }); + + it('should fail validation if bad "provider" provided', (done) => { + const data = [ { + name: 'sms', + provider: 'provider' + } ]; + + checkEnum({ guardianFactorProviders: data }, done); + }); + + it('should pass validation', (done) => { + const data = [ { + name: 'sms', + provider: 'twilio' + } ]; + + checkPassed({ guardianFactorProviders: data }, done); + }); + }); + + describe('#guardianFactors validate', () => { + it('should fail validation if no "name" provided', (done) => { + const data = [ { + anything: 'anything' + } ]; + + checkRequired('name', { guardianFactors: data }, done); + }); + + it('should fail validation if bad "name" provided', (done) => { + const data = [ { + name: 'name' + } ]; + + checkEnum({ guardianFactors: data }, done); + }); + + it('should pass validation', (done) => { + const data = [ { + name: 'sms' + } ]; + + checkPassed({ guardianFactors: data }, done); + }); + }); + + describe('#guardianFactorTemplates validate', () => { + it('should fail validation if no "name" provided', (done) => { + const data = [ { + anything: 'anything' + } ]; + + checkRequired('name', { guardianFactorTemplates: data }, done); + }); + + it('should fail validation if bad "name" provided', (done) => { + const data = [ { + name: 'name' + } ]; + + checkEnum({ guardianFactorTemplates: data }, done); + }); + + it('should pass validation', (done) => { + const data = [ { + name: 'sms' + } ]; + + checkPassed({ guardianFactorTemplates: data }, done); + }); + }); + + describe('#pages validate', () => { + it('should fail validation if no "name" provided', (done) => { + const data = [ { + anything: 'anything' + } ]; + + checkRequired('name', { pages: data }, done); + }); + + it('should fail validation if bad "name" provided', (done) => { + const data = [ { + name: 'name' + } ]; + + checkEnum({ pages: data }, done); + }); + + it('should pass validation', (done) => { + const data = [ { + name: 'login' + } ]; + + checkPassed({ pages: data }, done); + }); + }); + + describe('#prompts validate', () => { + it('should fail validation if prompts is not an object', (done) => { + const data = [ { + anything: 'anything' + } ]; + + const auth0 = new Auth0({}, { prompts: data }, {}); + + auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); + }); + + it('should pass validation', (done) => { + const data = { + anything: 'anything' + }; + + checkPassed({ prompts: data }, done); + }); + }); + + describe('#resourceServers validate', () => { + it('should fail validation if no "name" provided', (done) => { + const data = [ { + anything: 'anything' + } ]; + + checkRequired('name', { resourceServers: data }, done); + }); + + it('should fail validation if no "identifier" provided', (done) => { + const data = [ { + name: 'name' + } ]; + + checkRequired('identifier', { resourceServers: data }, done); + }); + + it('should pass validation', (done) => { + const data = [ { + name: 'name', + identifier: 'identifier' + } ]; + + checkPassed({ resourceServers: data }, done); + }); + }); + + describe('#rules validate', () => { + it('should fail validation if no "name" provided', (done) => { + const data = [ { + anything: 'anything' + } ]; + + checkRequired('name', { rules: data }, done); + }); + + it('should fail validation if bad "name" provided', (done) => { + const data = [ { + name: '-rule-' + } ]; + + const auth0 = new Auth0({}, { rules: data }, {}); + + auth0 + .validate() + .then(failedCb(done), passedCb(done, 'should match pattern')); + }); + + it('should fail validation if bad "stage" provided', (done) => { + const data = [ { + name: 'rule', + stage: 'stage' + } ]; + + checkEnum({ rules: data }, done); + }); + + it('should pass validation', (done) => { + const data = [ { + name: 'name', + order: 1, + stage: 'login_failure' + } ]; + + checkPassed({ rules: data }, done); + }); + }); + + describe('#rulesConfigs validate', () => { + it('should fail validation if no "key" provided', (done) => { + const data = [ { + anything: 'anything' + } ]; + + checkRequired('key', { rulesConfigs: data }, done); + }); + + it('should fail validation if no "value" provided', (done) => { + const data = [ { + key: 'key' + } ]; + + checkRequired('value', { rulesConfigs: data }, done); + }); + + it('should fail validation if bad "key" provided', (done) => { + const data = [ { + key: ':-?', + value: 'value' + } ]; + + const auth0 = new Auth0({}, { rulesConfigs: data }, {}); + + auth0 + .validate() + .then(failedCb(done), passedCb(done, 'should match pattern')); + }); + + it('should pass validation', (done) => { + const data = [ { + key: 'key', + value: 'value' + } ]; + + checkPassed({ rulesConfigs: data }, done); + }); + }); + + describe('#tenant validate', () => { + it('should fail validation if tenant is not an object', (done) => { + const data = [ { + anything: 'anything' + } ]; + + const auth0 = new Auth0({}, { tenant: data }, {}); + + auth0.validate().then(failedCb(done), passedCb(done, 'should be object')); + }); + + it('should pass validation', (done) => { + const data = { + anything: 'anything' + }; + + checkPassed({ tenant: data }, done); + }); + }); +}); diff --git a/tests/utils.tests.js b/tests/utils.tests.js index 0ac7e44..c9229db 100644 --- a/tests/utils.tests.js +++ b/tests/utils.tests.js @@ -163,4 +163,25 @@ describe('#utils calcChanges', () => { expect(update).to.have.length(1); expect(update).to.deep.include({ client_id: 'client1', audience: 'audience1', id: 'id3' }); }); + + it('should filter excluded items', () => { + const changes = { + del: [ { name: 'excluded_delete' }, { name: 'delete' } ], + create: [ { name: 'excluded_create' }, { name: 'create' } ], + update: [ { name: 'excluded_update' }, { name: 'update' } ], + conflicts: [ { name: 'excluded_conflicts' }, { name: 'conflicts' } ] + }; + + const exclude = [ 'excluded_create', 'excluded_update', 'excluded_delete', 'excluded_conflicts' ]; + + const result = utils.filterExcluded(changes, exclude); + + expect(Object.keys(result)).to.have.length(4); + expect(result).to.deep.equal({ + del: [ { name: 'delete' } ], + create: [ { name: 'create' } ], + update: [ { name: 'update' } ], + conflicts: [ { name: 'conflicts' } ] + }); + }); });