Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/context/directory/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import userAttributeProfiles from './userAttributeProfiles';
import connectionProfiles from './connectionProfiles';
import tokenExchangeProfiles from './tokenExchangeProfiles';
import supplementalSignals from './supplementalSignals';
import rateLimitPolicies from './rateLimitPolicies';
import eventStreams from './eventStreams';

import DirectoryContext from '..';
Expand Down Expand Up @@ -93,6 +94,7 @@ const directoryHandlers: {
connectionProfiles,
tokenExchangeProfiles,
supplementalSignals,
rateLimitPolicies,
eventStreams,
};

Expand Down
63 changes: 63 additions & 0 deletions src/context/directory/handlers/rateLimitPolicies.ts
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);
Comment on lines +42 to +54

Copy link
Copy Markdown
Contributor

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

});
}

const rateLimitPoliciesHandler: DirectoryHandler<ParsedRateLimitPolicies> = {
parse,
dump,
};

export default rateLimitPoliciesHandler;
2 changes: 2 additions & 0 deletions src/context/yaml/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import userAttributeProfiles from './userAttributeProfiles';
import connectionProfiles from './connectionProfiles';
import tokenExchangeProfiles from './tokenExchangeProfiles';
import supplementalSignals from './supplementalSignals';
import rateLimitPolicies from './rateLimitPolicies';
import eventStreams from './eventStreams';

import YAMLContext from '..';
Expand Down Expand Up @@ -91,6 +92,7 @@ const yamlHandlers: { [key in AssetTypes]: YAMLHandler<{ [key: string]: unknown
connectionProfiles,
tokenExchangeProfiles,
supplementalSignals,
rateLimitPolicies,
eventStreams,
};

Expand Down
39 changes: 39 additions & 0 deletions src/context/yaml/handlers/rateLimitPolicies.ts
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;
2 changes: 2 additions & 0 deletions src/tools/auth0/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import * as userAttributeProfiles from './userAttributeProfiles';
import * as connectionProfiles from './connectionProfiles';
import * as tokenExchangeProfiles from './tokenExchangeProfiles';
import * as supplementalSignals from './supplementalSignals';
import * as rateLimitPolicies from './rateLimitPolicies';
import * as eventStreams from './eventStreams';

import { AssetTypes } from '../../../types';
Expand Down Expand Up @@ -87,6 +88,7 @@ const auth0ApiHandlers: { [key in AssetTypes]: any } = {
connectionProfiles,
tokenExchangeProfiles,
supplementalSignals,
rateLimitPolicies,
eventStreams,
};

Expand Down
242 changes: 242 additions & 0 deletions src/tools/auth0/handlers/rateLimitPolicies.ts
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 || []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change.del || [] seems like dead code since case already proved change.del is non-empty

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as change.del above (case satisfied only if change.update is truthy)

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')}`
);
}
}
}
Loading