Skip to content

Commit

Permalink
Handling hooks and rules for upcoming deprecation (#838)
Browse files Browse the repository at this point in the history
* Handling upcoming deprecations/removals of hooks and rules

* Adding hooks tests

* Update src/tools/auth0/handlers/hooks.ts

Co-authored-by: Sergiu Ghitea <[email protected]>

* Update src/tools/auth0/handlers/hooks.ts

Co-authored-by: Sergiu Ghitea <[email protected]>

* Error message changes

* Update src/tools/auth0/handlers/rules.ts

* Being more specific about targeting error message

---------

Co-authored-by: Will Vedder <[email protected]>
Co-authored-by: Sergiu Ghitea <[email protected]>
  • Loading branch information
3 people authored Aug 11, 2023
1 parent e63ef5b commit 88c5030
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 52 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions docs/configuring-the-deploy-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand All @@ -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

Expand Down
6 changes: 3 additions & 3 deletions docs/excluding-from-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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": "<YOUR_AUTH0_CLIENT_ID>",
"AUTH0_INCLUDED_ONLY": ["actions", "hooks", "rules"]
"AUTH0_INCLUDED_ONLY": ["actions", "clients", "connections"]
}
```

Expand Down
42 changes: 32 additions & 10 deletions src/tools/auth0/handlers/hooks.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -96,6 +98,9 @@ export default class HooksHandler extends DefaultHandler {

async processSecrets(hooks): Promise<void> {
const allHooks = await this.getType(true);

if (allHooks === null) return;

const changes: CalculatedChanges = {
create: [],
update: [],
Expand Down Expand Up @@ -155,7 +160,7 @@ export default class HooksHandler extends DefaultHandler {
}

//@ts-ignore because hooks use a special reload argument
async getType(reload: boolean): Promise<Asset[]> {
async getType(reload: boolean): Promise<Asset[] | null> {
if (this.existing && !reload) {
return this.existing;
}
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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;
}
}
}
99 changes: 66 additions & 33 deletions src/tools/auth0/handlers/rules.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -60,10 +60,17 @@ export default class RulesHandler extends DefaultHandler {
});
}

async getType(): Promise<Asset[]> {
if (this.existing) return this.existing;
this.existing = await this.client.rules.getAll({ paginate: true, include_totals: true });
return this.existing;
async getType(): Promise<Asset[] | null> {
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 {
Expand All @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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) => [
Expand Down Expand Up @@ -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;
}
}
}
15 changes: 13 additions & 2 deletions src/tools/auth0/handlers/rulesConfigs.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -26,8 +28,13 @@ export default class RulesConfigsHandler extends DefaultHandler {
});
}

async getType(): Promise<Asset[]> {
return this.client.rulesConfigs.getAll();
async getType(): Promise<Asset[] | null> {
try {
return this.client.rulesConfigs.getAll();
} catch (err) {
if (isDeprecatedError(err)) return null;
throw err;
}
}

objString(item): string {
Expand All @@ -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: [],
Expand Down
5 changes: 5 additions & 0 deletions src/tools/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,8 @@ export const detectInsufficientScopeError = async <T>(
export function sleep(ms: number): Promise<void> {
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'));
};
60 changes: 59 additions & 1 deletion test/tools/auth0/handlers/hooks.tests.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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;
});
});
});
Loading

0 comments on commit 88c5030

Please sign in to comment.