From 88c5030884918f64e45d7870356e3d19acb6a8af Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Fri, 11 Aug 2023 08:13:01 -0400 Subject: [PATCH] Handling hooks and rules for upcoming deprecation (#838) * Handling upcoming deprecations/removals of hooks and rules * Adding hooks tests * Update src/tools/auth0/handlers/hooks.ts Co-authored-by: Sergiu Ghitea <28300158+sergiught@users.noreply.github.com> * Update src/tools/auth0/handlers/hooks.ts Co-authored-by: Sergiu Ghitea <28300158+sergiught@users.noreply.github.com> * Error message changes * Update src/tools/auth0/handlers/rules.ts * Being more specific about targeting error message --------- Co-authored-by: Will Vedder Co-authored-by: Sergiu Ghitea <28300158+sergiught@users.noreply.github.com> --- README.md | 2 +- docs/configuring-the-deploy-cli.md | 4 +- docs/excluding-from-management.md | 6 +- src/tools/auth0/handlers/hooks.ts | 42 +++++++--- src/tools/auth0/handlers/rules.ts | 99 ++++++++++++++++-------- src/tools/auth0/handlers/rulesConfigs.ts | 15 +++- src/tools/utils.ts | 5 ++ test/tools/auth0/handlers/hooks.tests.js | 60 +++++++++++++- test/tools/auth0/handlers/rules.tests.js | 42 ++++++++++ test/tools/utils.test.js | 27 +++++++ 10 files changed, 250 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index f79ce906b..bbc797e24 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The Auth0 Deploy CLI is a tool that helps you manage your Auth0 tenant configuration. It integrates into your development workflows as a standalone CLI or as a node module. -**Supported resource types:** actions, branding, client grants, clients (applications), connections, custom domains, email templates, emails, grants, guardian, hook secrets, hooks, log streams, migrations, organizations, pages, prompts, resource servers (APIs), roles, rules, rules configs, tenant settings, themes. +**Supported resource types:** actions, branding, client grants, clients (applications), connections, custom domains, email templates, emails, grants, guardian, hook secrets, log streams, migrations, organizations, pages, prompts, resource servers (APIs), roles, tenant settings, themes. 🎢 [Highlights](#highlights) • 📚 [Documentation](#documentation) • 🚀 [Getting Started](#getting-started) • 💬 [Feedback](#feedback) diff --git a/docs/configuring-the-deploy-cli.md b/docs/configuring-the-deploy-cli.md index f5940d2d4..1e5edcee0 100644 --- a/docs/configuring-the-deploy-cli.md +++ b/docs/configuring-the-deploy-cli.md @@ -80,7 +80,7 @@ Boolean. When enabled, will allow the tool to delete resources. Default: `false` ### `AUTH0_EXCLUDED` -Array of strings. Excludes entire resource types from being managed, bi-directionally. See also: [excluding resources from management](excluding-from-management.md). Possible values: `actions`, `attackProtection`, `branding`, `clientGrants`, `clients`, `connections`, `customDomains`, `databases`, `emailProvider`, `emailTemplates`, `guardianFactorProviders`, `guardianFactorTemplates`, `guardianFactors`, `guardianPhoneFactorMessageTypes`, `guardianPhoneFactorSelectedProvider`, `guardianPolicies`, `hooks`, `logStreams`, `migrations`, `organizations`, `pages`, `prompts`, `resourceServers`, `roles`, `rules`, `rulesConfigs`, `tenant`, `triggers`. +Array of strings. Excludes entire resource types from being managed, bi-directionally. See also: [excluding resources from management](excluding-from-management.md). Possible values: `actions`, `attackProtection`, `branding`, `clientGrants`, `clients`, `connections`, `customDomains`, `databases`, `emailProvider`, `emailTemplates`, `guardianFactorProviders`, `guardianFactorTemplates`, `guardianFactors`, `guardianPhoneFactorMessageTypes`, `guardianPhoneFactorSelectedProvider`, `guardianPolicies`, `logStreams`, `migrations`, `organizations`, `pages`, `prompts`, `resourceServers`, `roles`, `tenant`, `triggers`. Cannot be used simultaneously with `AUTH0_INCLUDED_ONLY`. @@ -94,7 +94,7 @@ Cannot be used simultaneously with `AUTH0_INCLUDED_ONLY`. ### `AUTH0_INCLUDED_ONLY` -Array of strings. Dictates which resource types to _only_ manage, bi-directionally. See also: [excluding resources from management](excluding-from-management.md). Possible values: `actions`, `attackProtection`, `branding`, `clientGrants`, `clients`, `connections`, `customDomains`, `databases`, `emailProvider`, `emailTemplates`, `guardianFactorProviders`, `guardianFactorTemplates`, `guardianFactors`, `guardianPhoneFactorMessageTypes`, `guardianPhoneFactorSelectedProvider`, `guardianPolicies`, `hooks`, `logStreams`, `migrations`, `organizations`, `pages`, `prompts`, `resourceServers`, `roles`, `rules`, `rulesConfigs`, `tenant`, `triggers` +Array of strings. Dictates which resource types to _only_ manage, bi-directionally. See also: [excluding resources from management](excluding-from-management.md). Possible values: `actions`, `attackProtection`, `branding`, `clientGrants`, `clients`, `connections`, `customDomains`, `databases`, `emailProvider`, `emailTemplates`, `guardianFactorProviders`, `guardianFactorTemplates`, `guardianFactors`, `guardianPhoneFactorMessageTypes`, `guardianPhoneFactorSelectedProvider`, `guardianPolicies`, `logStreams`, `migrations`, `organizations`, `pages`, `prompts`, `resourceServers`, `roles`, `tenant`, `triggers` #### Example diff --git a/docs/excluding-from-management.md b/docs/excluding-from-management.md index 49c683910..55ef9f36f 100644 --- a/docs/excluding-from-management.md +++ b/docs/excluding-from-management.md @@ -14,7 +14,7 @@ This type of exclusion is expressed by passing an array of resource names into e All supported resource values for exclusion: -`actions`, `attackProtection`, `branding`, `clientGrants`, `clients`, `connections`, `customDomains`, `databases`, `emailProvider`, `emailTemplates`, `guardianFactorProviders`, `guardianFactorTemplates`, `guardianFactors`, `guardianPhoneFactorMessageTypes`, `guardianPhoneFactorSelectedProvider`, `guardianPolicies`, `hooks`, `logStreams`, `migrations`, `organizations`, `pages`, `prompts`, `resourceServers`, `roles`, `rules`, `rulesConfigs`, `tenant`, `triggers` +`actions`, `attackProtection`, `branding`, `clientGrants`, `clients`, `connections`, `customDomains`, `databases`, `emailProvider`, `emailTemplates`, `guardianFactorProviders`, `guardianFactorTemplates`, `guardianFactors`, `guardianPhoneFactorMessageTypes`, `guardianPhoneFactorSelectedProvider`, `guardianPolicies`, `logStreams`, `migrations`, `organizations`, `pages`, `prompts`, `resourceServers`, `roles`, `tenant`, `triggers` ### Exclusion Example @@ -30,13 +30,13 @@ The following example excludes `clients`, `connections`, `databases` and `organi ### Inclusion Example -The following example dictates to _only_ manage `actions`, `hooks` and `rules` by the Deploy CLI. +The following example dictates to _only_ manage `actions`, `clients` and `connections` by the Deploy CLI. ```json { "AUTH0_DOMAIN": "example-site.us.auth0.com", "AUTH0_CLIENT_ID": "", - "AUTH0_INCLUDED_ONLY": ["actions", "hooks", "rules"] + "AUTH0_INCLUDED_ONLY": ["actions", "clients", "connections"] } ``` diff --git a/src/tools/auth0/handlers/hooks.ts b/src/tools/auth0/handlers/hooks.ts index 076c56ff6..dbdde96e7 100644 --- a/src/tools/auth0/handlers/hooks.ts +++ b/src/tools/auth0/handlers/hooks.ts @@ -1,6 +1,8 @@ import DefaultHandler from './default'; import constants from '../../constants'; import { Asset, Assets, CalculatedChanges } from '../../../types'; +import log from '../../../logger'; +import { isDeprecatedError } from '../../utils'; const ALLOWED_TRIGGER_IDS = [ 'credentials-exchange', @@ -96,6 +98,9 @@ export default class HooksHandler extends DefaultHandler { async processSecrets(hooks): Promise { const allHooks = await this.getType(true); + + if (allHooks === null) return; + const changes: CalculatedChanges = { create: [], update: [], @@ -155,7 +160,7 @@ export default class HooksHandler extends DefaultHandler { } //@ts-ignore because hooks use a special reload argument - async getType(reload: boolean): Promise { + async getType(reload: boolean): Promise { if (this.existing && !reload) { return this.existing; } @@ -186,6 +191,9 @@ export default class HooksHandler extends DefaultHandler { if (err.statusCode === 404 || err.statusCode === 501) { return []; } + if (isDeprecatedError(err)) { + return null; + } throw err; } } @@ -234,15 +242,29 @@ export default class HooksHandler extends DefaultHandler { // Do nothing if not set if (!hooks) return; - // Figure out what needs to be updated vs created - const changes = await this.calcChanges(assets); - await super.processChanges(assets, { - del: changes.del, - create: changes.create, - update: changes.update, - conflicts: changes.conflicts, - }); + log.warn( + 'Hooks are deprecated, migrate to using actions instead. See: https://auth0.com/docs/customize/actions/migrate/migrate-from-hooks-to-actions for more information.' + ); - await this.processSecrets(hooks); + try { + // Figure out what needs to be updated vs created + const changes = await this.calcChanges(assets); + await super.processChanges(assets, { + del: changes.del, + create: changes.create, + update: changes.update, + conflicts: changes.conflicts, + }); + + await this.processSecrets(hooks); + } catch (err) { + if (isDeprecatedError(err)) { + log.warn( + 'Failed to update hooks because functionality has been deprecated in favor of actions. See: https://auth0.com/docs/customize/actions/migrate/migrate-from-hooks-to-actions for more information.' + ); + return; + } + throw err; + } } } diff --git a/src/tools/auth0/handlers/rules.ts b/src/tools/auth0/handlers/rules.ts index a9c7f3558..b95d0dbf8 100644 --- a/src/tools/auth0/handlers/rules.ts +++ b/src/tools/auth0/handlers/rules.ts @@ -1,5 +1,5 @@ import ValidationError from '../../validationError'; -import { convertJsonToString, stripFields, duplicateItems } from '../../utils'; +import { convertJsonToString, stripFields, duplicateItems, isDeprecatedError } from '../../utils'; import DefaultHandler from './default'; import log from '../../../logger'; import { calculateChanges } from '../../calculateChanges'; @@ -60,10 +60,17 @@ export default class RulesHandler extends DefaultHandler { }); } - async getType(): Promise { - if (this.existing) return this.existing; - this.existing = await this.client.rules.getAll({ paginate: true, include_totals: true }); - return this.existing; + async getType(): Promise { + try { + if (this.existing) return this.existing; + this.existing = await this.client.rules.getAll({ paginate: true, include_totals: true }); + return this.existing; + } catch (err) { + if (isDeprecatedError(err)) { + return null; + } + throw err; + } } objString(rule): string { @@ -79,6 +86,15 @@ export default class RulesHandler extends DefaultHandler { const excludedRules = (assets.exclude && assets.exclude.rules) || []; let existing = await this.getType(); + if (existing === null) { + return { + del: [], + update: [], + conflicts: [], + create: [], + reOrder: [], + }; + } // Filter excluded rules if (!includeExcluded) { @@ -103,6 +119,7 @@ export default class RulesHandler extends DefaultHandler { //@ts-ignore because we know reOrder is Asset[] const reOrder: Asset[] = futureRules.reduce((accum: Asset[], r: Asset) => { + if (existing === null) return accum; const conflict = existing.find((f) => r.order === f.order && r.name !== f.name); if (conflict !== undefined) { nextOrderNo += 1; @@ -155,6 +172,8 @@ export default class RulesHandler extends DefaultHandler { // Detect Rules that are changing stage as it's not allowed. const existing = await this.getType(); + if (existing === null) return; + const stateChanged = futureRules .reduce( (changed: Asset[], rule) => [ @@ -182,33 +201,47 @@ export default class RulesHandler extends DefaultHandler { // Do nothing if not set if (!rules) return; - // Figure out what needs to be updated vs created - const changes = await this.calcChanges(assets); - - // Temporally re-order rules with conflicting ordering - await this.client.pool - .addEachTask({ - data: changes.reOrder, - generator: (rule) => - this.client.rules - .update({ id: rule.id }, stripFields(rule, this.stripUpdateFields)) - .then(() => { - const updated = { - name: rule.name, - stage: rule.stage, - order: rule.order, - id: rule.id, - }; - log.info(`Temporally re-order Rule ${convertJsonToString(updated)}`); - }), - }) - .promise(); - - await super.processChanges(assets, { - del: changes.del, - create: changes.create, - update: changes.update, - conflicts: changes.conflicts, - }); + log.warn( + 'Rules are deprecated, migrate to using actions instead. See: https://auth0.com/docs/customize/actions/migrate/migrate-from-rules-to-actions for more information.' + ); + + try { + // Figure out what needs to be updated vs created + const changes = await this.calcChanges(assets); + + // Temporally re-order rules with conflicting ordering + await this.client.pool + .addEachTask({ + data: changes.reOrder, + generator: (rule) => + this.client.rules + .update({ id: rule.id }, stripFields(rule, this.stripUpdateFields)) + .then(() => { + const updated = { + name: rule.name, + stage: rule.stage, + order: rule.order, + id: rule.id, + }; + log.info(`Temporally re-order Rule ${convertJsonToString(updated)}`); + }), + }) + .promise(); + + await super.processChanges(assets, { + del: changes.del, + create: changes.create, + update: changes.update, + conflicts: changes.conflicts, + }); + } catch (err) { + if (isDeprecatedError(err)) { + log.warn( + 'Failed to update rules because functionality has been deprecated in favor of actions. See: https://auth0.com/docs/customize/actions/migrate/migrate-from-rules-to-actions for more information.' + ); + return; + } + throw err; + } } } diff --git a/src/tools/auth0/handlers/rulesConfigs.ts b/src/tools/auth0/handlers/rulesConfigs.ts index e61be2f80..8e4301332 100644 --- a/src/tools/auth0/handlers/rulesConfigs.ts +++ b/src/tools/auth0/handlers/rulesConfigs.ts @@ -1,5 +1,7 @@ import { Assets, Asset, CalculatedChanges } from '../../../types'; import DefaultHandler from './default'; +import log from '../../../logger'; +import { isDeprecatedError } from '../../utils'; export const schema = { type: 'array', @@ -26,8 +28,13 @@ export default class RulesConfigsHandler extends DefaultHandler { }); } - async getType(): Promise { - return this.client.rulesConfigs.getAll(); + async getType(): Promise { + try { + return this.client.rulesConfigs.getAll(); + } catch (err) { + if (isDeprecatedError(err)) return null; + throw err; + } } objString(item): string { @@ -46,6 +53,10 @@ export default class RulesConfigsHandler extends DefaultHandler { conflicts: [], }; + log.warn( + 'Rules are deprecated, migrate to using actions instead. See: https://auth0.com/docs/customize/actions/migrate/migrate-from-rules-to-actions for more information.' + ); + // Intention is to not delete/cleanup old configRules, that needs to be handled manually. return { del: [], diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 3e6ea1c57..787b05c2d 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -270,3 +270,8 @@ export const detectInsufficientScopeError = async ( export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } + +export const isDeprecatedError = (err: { message: string; statusCode: number }): boolean => { + if (!err) return false; + return !!(err.statusCode === 403 || err.message?.includes('deprecated feature')); +}; diff --git a/test/tools/auth0/handlers/hooks.tests.js b/test/tools/auth0/handlers/hooks.tests.js index ebd040dc6..6b82c1d16 100644 --- a/test/tools/auth0/handlers/hooks.tests.js +++ b/test/tools/auth0/handlers/hooks.tests.js @@ -1,7 +1,13 @@ -const { expect } = require('chai'); +import chai, { expect } from 'chai'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; + const constants = require('../../../../src/tools/constants').default; const hooks = require('../../../../src/tools/auth0/handlers/hooks'); +chai.use(chaiAsPromised); +chai.use(sinonChai); + const pool = { addEachTask: (data) => { if (data.data && data.data.length) { @@ -233,6 +239,23 @@ describe('#hooks handler', () => { ); }); + it('should return if trying to get hooks when deprecated for tenant', async () => { + const auth0 = { + hooks: { + getAll: () => { + const error = new Error(); + error.statusCode = 403; + error.message = 'Insufficient privileges to use this deprecated feature'; + throw error; + }, + }, + }; + + const handler = new hooks.default({ client: auth0, config }); + const data = await handler.getType(); + expect(data).to.equal(null); + }); + it('should return an empty array for 501 status code', async () => { const auth0 = { hooks: { @@ -561,5 +584,40 @@ describe('#hooks handler', () => { await stageFn.apply(handler, [assets]); }); + + it('should not throw if attempted to update hooks when deprecated for tenant', async () => { + const auth0 = { + hooks: { + getAll: () => { + const error = new Error(); + error.statusCode = 403; + error.message = 'Insufficient privileges to use this deprecated feature'; + throw error; + }, + create: () => { + const error = new Error(); + error.statusCode = 403; + error.message = 'Insufficient privileges to use this deprecated feature'; + throw error; + }, + }, + pool, + }; + const data = { + hooks: [ + { + name: 'someHook', + code: 'new-code', + triggerId: 'credentials-exchange', + secrets: [], + }, + ], + }; + + const handler = new hooks.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await expect(stageFn.apply(handler, [data])).to.be.eventually.fulfilled; + }); }); }); diff --git a/test/tools/auth0/handlers/rules.tests.js b/test/tools/auth0/handlers/rules.tests.js index edecd0d48..d9e66a263 100644 --- a/test/tools/auth0/handlers/rules.tests.js +++ b/test/tools/auth0/handlers/rules.tests.js @@ -253,6 +253,23 @@ describe('#rules handler', () => { expect(data).to.deep.equal(rulesData); }); + it('should not throw if rules endpoint deprecated', async () => { + const auth0 = { + rules: { + getAll: () => { + const error = new Error(); + error.statusCode = 403; + error.message = 'Insufficient privileges to use this deprecated feature'; + throw error; + }, + }, + }; + + const handler = new rules.default({ client: auth0, config }); + const data = await handler.getType(); + expect(data).to.equal(null); + }); + it('should update rule', async () => { const auth0 = { rules: { @@ -389,5 +406,30 @@ describe('#rules handler', () => { await stageFn.apply(handler, [data]); }); + + it('should not throw if attempted to update rules when deprecated for tenant', async () => { + const auth0 = { + rules: { + getAll: () => { + const error = new Error(); + error.statusCode = 403; + error.message = 'Insufficient privileges to use this deprecated feature'; + throw error; + }, + }, + pool, + }; + const data = { + rules: [ + { name: 'Rule1', script: 'new-rule-one-script' }, + { name: 'Rule3', script: 'new-rule-three-script' }, + ], + }; + + const handler = new rules.default({ client: auth0, config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + await expect(stageFn.apply(handler, [data])).to.be.eventually.fulfilled; + }); }); }); diff --git a/test/tools/utils.test.js b/test/tools/utils.test.js index 4e0fa35ee..9af0b02ab 100644 --- a/test/tools/utils.test.js +++ b/test/tools/utils.test.js @@ -623,3 +623,30 @@ describe('#detectInsufficientScopeError', () => { expect(didThrow).to.equal(true); }); }); + +describe('#isDeprecatedError', () => { + it('should return true if deprecated error', async () => { + const error = { + message: 'Insufficient privileges to use this deprecated feature', + statusCode: 403, + }; + // eslint-disable-next-line no-unused-expressions + expect(utils.isDeprecatedError(error)).to.be.true; + }); + + it('should return false if not a deprecated error', async () => { + const error = { + message: 'Not found', + statusCode: 404, + }; + // eslint-disable-next-line no-unused-expressions + expect(utils.isDeprecatedError(error)).to.be.false; + }); + + it('should return false error is not error types', async () => { + // eslint-disable-next-line no-unused-expressions + expect(utils.isDeprecatedError(undefined)).to.be.false; + // eslint-disable-next-line no-unused-expressions + expect(utils.isDeprecatedError({})).to.be.false; + }); +});