-
Notifications
You must be signed in to change notification settings - Fork 175
feat: add deploy-cli support for Rate Limit Policies (EA) #1395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
8650df4
47fa940
79e2a33
aaeda7a
42d206e
2f80ba7
07200ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import path from 'path'; | ||
| import fs from 'fs-extra'; | ||
| import { constants } from '../../../tools'; | ||
| import { getFiles, existsMustBeDir, dumpJSON, loadJSON, sanitize } from '../../../utils'; | ||
| import { DirectoryHandler } from '.'; | ||
| import DirectoryContext from '..'; | ||
| import { ParsedAsset } from '../../../types'; | ||
| import { RateLimitPolicy } from '../../../tools/auth0/handlers/rateLimitPolicies'; | ||
|
|
||
| type ParsedRateLimitPolicies = ParsedAsset<'rateLimitPolicies', RateLimitPolicy[]>; | ||
|
|
||
| function parse(context: DirectoryContext): ParsedRateLimitPolicies { | ||
| const rateLimitPoliciesDirectory = path.join( | ||
| context.filePath, | ||
| constants.RATE_LIMIT_POLICIES_DIRECTORY | ||
| ); | ||
| if (!existsMustBeDir(rateLimitPoliciesDirectory)) return { rateLimitPolicies: null }; // Skip | ||
|
|
||
| const foundFiles = getFiles(rateLimitPoliciesDirectory, ['.json']); | ||
|
|
||
| const rateLimitPolicies = foundFiles | ||
| .map((f) => | ||
| loadJSON(f, { | ||
| mappings: context.mappings, | ||
| disableKeywordReplacement: context.disableKeywordReplacement, | ||
| }) | ||
| ) | ||
| .filter((p) => Object.keys(p).length > 0); | ||
|
|
||
| return { rateLimitPolicies }; | ||
| } | ||
|
|
||
| async function dump(context: DirectoryContext): Promise<void> { | ||
| const { rateLimitPolicies } = context.assets; | ||
|
|
||
| if (!rateLimitPolicies) return; // Skip, nothing to dump | ||
|
|
||
| const rateLimitPoliciesDirectory = path.join( | ||
| context.filePath, | ||
| constants.RATE_LIMIT_POLICIES_DIRECTORY | ||
| ); | ||
| fs.ensureDirSync(rateLimitPoliciesDirectory); | ||
|
|
||
| const removeKeysFromOutput = ['id', 'created_at', 'updated_at']; | ||
|
|
||
| rateLimitPolicies.forEach((policy) => { | ||
| const policyToWrite = { ...policy }; | ||
| removeKeysFromOutput.forEach((key) => { | ||
| delete policyToWrite[key]; | ||
| }); | ||
|
|
||
| const fileName = sanitize(policy.consumer_selector); | ||
| const filePath = path.join(rateLimitPoliciesDirectory, `${fileName}.json`); | ||
| dumpJSON(filePath, policyToWrite); | ||
| }); | ||
| } | ||
|
|
||
| const rateLimitPoliciesHandler: DirectoryHandler<ParsedRateLimitPolicies> = { | ||
| parse, | ||
| dump, | ||
| }; | ||
|
|
||
| export default rateLimitPoliciesHandler; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { YAMLHandler } from '.'; | ||
| import YAMLContext from '..'; | ||
| import { ParsedAsset } from '../../../types'; | ||
| import { RateLimitPolicy } from '../../../tools/auth0/handlers/rateLimitPolicies'; | ||
|
|
||
| type ParsedRateLimitPolicies = ParsedAsset<'rateLimitPolicies', RateLimitPolicy[]>; | ||
|
|
||
| async function parse(context: YAMLContext): Promise<ParsedRateLimitPolicies> { | ||
| const { rateLimitPolicies } = context.assets; | ||
|
|
||
| if (!rateLimitPolicies) return { rateLimitPolicies: null }; | ||
|
|
||
| return { rateLimitPolicies }; | ||
| } | ||
|
|
||
| async function dump(context: YAMLContext): Promise<ParsedRateLimitPolicies> { | ||
| const { rateLimitPolicies } = context.assets; | ||
|
|
||
| if (!rateLimitPolicies) return { rateLimitPolicies: null }; | ||
|
|
||
| const removeKeysFromOutput = ['id', 'created_at', 'updated_at']; | ||
|
|
||
| const cleaned = rateLimitPolicies.map((policy) => { | ||
| const policyToWrite = { ...policy }; | ||
| removeKeysFromOutput.forEach((key) => { | ||
| delete policyToWrite[key]; | ||
| }); | ||
| return policyToWrite; | ||
| }); | ||
|
|
||
| return { rateLimitPolicies: cleaned }; | ||
| } | ||
|
|
||
| const rateLimitPoliciesHandler: YAMLHandler<ParsedRateLimitPolicies> = { | ||
| parse, | ||
| dump, | ||
| }; | ||
|
|
||
| export default rateLimitPoliciesHandler; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| import DefaultAPIHandler from './default'; | ||
| import { Asset, Assets, CalculatedChanges } from '../../../types'; | ||
| import { paginate } from '../client'; | ||
| import log from '../../../logger'; | ||
|
|
||
| // Types will align with Management.RateLimitPolicy once node-auth0 PR #1348 is merged | ||
| export type RateLimitPolicyConfiguration = | ||
| | { action: 'allow' } | ||
| | { action: 'block' | 'log'; limit: number } | ||
| | { action: 'redirect'; limit: number; redirect_uri: string }; | ||
|
|
||
| export type RateLimitPolicy = { | ||
| id?: string; | ||
| resource: string; | ||
| consumer: string; | ||
| consumer_selector: string; | ||
| configuration: RateLimitPolicyConfiguration; | ||
| created_at?: string; | ||
| updated_at?: string; | ||
| }; | ||
|
|
||
| export const schema = { | ||
| type: 'array', | ||
| items: { | ||
| type: 'object', | ||
| properties: { | ||
| resource: { | ||
| type: 'string', | ||
| enum: ['oauth_authentication_api'], | ||
| }, | ||
| consumer: { | ||
| type: 'string', | ||
| enum: ['client'], | ||
| }, | ||
| consumer_selector: { | ||
| type: 'string', | ||
| }, | ||
| configuration: { | ||
| type: 'object', | ||
| oneOf: [ | ||
| { | ||
| required: ['action'], | ||
| properties: { | ||
| action: { type: 'string', enum: ['allow'] }, | ||
| }, | ||
| additionalProperties: false, | ||
| }, | ||
| { | ||
| required: ['action', 'limit'], | ||
| properties: { | ||
| action: { type: 'string', enum: ['block', 'log'] }, | ||
| limit: { type: 'number' }, | ||
| }, | ||
| additionalProperties: false, | ||
| }, | ||
| { | ||
| required: ['action', 'limit', 'redirect_uri'], | ||
| properties: { | ||
| action: { type: 'string', enum: ['redirect'] }, | ||
| limit: { type: 'number' }, | ||
| redirect_uri: { type: 'string' }, | ||
| }, | ||
| additionalProperties: false, | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| required: ['resource', 'consumer', 'consumer_selector', 'configuration'], | ||
| additionalProperties: false, | ||
| }, | ||
| }; | ||
|
|
||
| export default class RateLimitPoliciesHandler extends DefaultAPIHandler { | ||
| existing: RateLimitPolicy[] | null; | ||
|
|
||
| constructor(config: DefaultAPIHandler) { | ||
| super({ | ||
| ...config, | ||
| type: 'rateLimitPolicies', | ||
| id: 'id', | ||
| identifiers: ['id', 'consumer_selector'], | ||
| stripCreateFields: ['id', 'created_at', 'updated_at'], | ||
| stripUpdateFields: [ | ||
| 'id', | ||
| 'resource', | ||
| 'consumer', | ||
| 'consumer_selector', | ||
| 'created_at', | ||
| 'updated_at', | ||
| ], | ||
| }); | ||
| } | ||
|
|
||
| objString(policy: RateLimitPolicy): string { | ||
| return super.objString({ | ||
| consumer_selector: policy.consumer_selector, | ||
| resource: policy.resource, | ||
| }); | ||
| } | ||
|
|
||
| async getType(): Promise<Asset | null> { | ||
| if (this.existing) return this.existing; | ||
|
|
||
| try { | ||
| const rateLimitPolicies = await paginate<RateLimitPolicy>( | ||
| this.client.rateLimitPolicies.list, | ||
| { checkpoint: true } | ||
| ); | ||
| this.existing = rateLimitPolicies; | ||
| return this.existing; | ||
| } catch (err) { | ||
| if (err.statusCode === 404 || err.statusCode === 501) { | ||
| return null; | ||
| } | ||
| if (err.statusCode === 403) { | ||
| log.debug( | ||
| 'Rate Limit Policies feature is not enabled for this tenant. Please contact Auth0 support to enable this feature.' | ||
| ); | ||
| return null; | ||
| } | ||
| throw err; | ||
| } | ||
| } | ||
|
|
||
| async processChanges(assets: Assets): Promise<void> { | ||
| const { rateLimitPolicies } = assets; | ||
|
|
||
| if (!rateLimitPolicies) return; | ||
|
|
||
| const { del, update, create } = await this.calcChanges(assets); | ||
|
|
||
| log.debug( | ||
| `Start processChanges for rateLimitPolicies [delete:${del.length}] [update:${update.length}], [create:${create.length}]` | ||
| ); | ||
|
|
||
| const changes = [{ del }, { create }, { update }]; | ||
|
|
||
| await Promise.all( | ||
| changes.map(async (change) => { | ||
| switch (true) { | ||
| case change.del && change.del.length > 0: | ||
| await this.deleteRateLimitPolicies(change.del || []); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| break; | ||
| case change.create && change.create.length > 0: | ||
| await this.createRateLimitPolicies(change.create); | ||
| break; | ||
| case change.update && change.update.length > 0: | ||
| if (change.update) await this.updateRateLimitPolicies(change.update); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as |
||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| async createRateLimitPolicy(policy: RateLimitPolicy): Promise<RateLimitPolicy> { | ||
| const created = await this.client.rateLimitPolicies.create(policy as any); | ||
| return created as RateLimitPolicy; | ||
| } | ||
|
|
||
| async createRateLimitPolicies(creates: CalculatedChanges['create']): Promise<void> { | ||
| await this.client.pool | ||
| .addEachTask({ | ||
| data: creates || [], | ||
| generator: (item: RateLimitPolicy) => | ||
| this.createRateLimitPolicy(item) | ||
| .then((data) => { | ||
| this.didCreate(data); | ||
| this.created += 1; | ||
| }) | ||
| .catch((err) => { | ||
| throw new Error(`Problem creating ${this.type} ${this.objString(item)}\n${err}`); | ||
| }), | ||
| }) | ||
| .promise(); | ||
| } | ||
|
|
||
| async updateRateLimitPolicy(policy: RateLimitPolicy): Promise<void> { | ||
| const { id, configuration } = policy; | ||
|
|
||
| if (!id) { | ||
| throw new Error(`Missing id for ${this.type} ${this.objString(policy)}`); | ||
| } | ||
|
|
||
| await this.client.rateLimitPolicies.update(id, { configuration }); | ||
| } | ||
|
|
||
| async updateRateLimitPolicies(updates: CalculatedChanges['update']): Promise<void> { | ||
| await this.client.pool | ||
| .addEachTask({ | ||
| data: updates || [], | ||
| generator: (item: RateLimitPolicy) => | ||
| this.updateRateLimitPolicy(item) | ||
| .then(() => { | ||
| this.didUpdate(item); | ||
| this.updated += 1; | ||
| }) | ||
| .catch((err) => { | ||
| throw new Error(`Problem updating ${this.type} ${this.objString(item)}\n${err}`); | ||
| }), | ||
| }) | ||
| .promise(); | ||
| } | ||
|
|
||
| async deleteRateLimitPolicy(policy: RateLimitPolicy): Promise<void> { | ||
| if (!policy.id) { | ||
| throw new Error(`Missing id for ${this.type} ${this.objString(policy)}`); | ||
| } | ||
| await this.client.rateLimitPolicies.delete(policy.id); | ||
| } | ||
|
|
||
| async deleteRateLimitPolicies(data: Asset[]): Promise<void> { | ||
| if ( | ||
| this.config('AUTH0_ALLOW_DELETE') === 'true' || | ||
| this.config('AUTH0_ALLOW_DELETE') === true | ||
| ) { | ||
| await this.client.pool | ||
| .addEachTask({ | ||
| data: data || [], | ||
| generator: (item: RateLimitPolicy) => | ||
| this.deleteRateLimitPolicy(item) | ||
| .then(() => { | ||
| this.didDelete(item); | ||
| this.deleted += 1; | ||
| }) | ||
| .catch((err) => { | ||
| throw new Error(`Problem deleting ${this.type} ${this.objString(item)}\n${err}`); | ||
| }), | ||
| }) | ||
| .promise(); | ||
| } else { | ||
| 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${data | ||
| .map((i) => this.objString(i as RateLimitPolicy)) | ||
| .join('\n')}` | ||
| ); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a rate limit policy is deleted from the tenant, its .json file remains in rate-limit-policies/ after the next export. On the following deploy, that orphaned file causes the deleted policy to be re-created. connections.ts (lines 134–142) tracks expected filenames and call fs.removeSync() on any files not in that set. This handler needs the same pattern.
We had fixed this for connections here PR #1389