Skip to content
Draft
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 DirectoryContext from '..';
import { AssetTypes, Asset } from '../../../types';
Expand Down Expand Up @@ -92,6 +93,7 @@ const directoryHandlers: {
connectionProfiles,
tokenExchangeProfiles,
supplementalSignals,
rateLimitPolicies,
};

export default directoryHandlers;
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);
});
}

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 YAMLContext from '..';
import { AssetTypes } from '../../../types';
Expand Down Expand Up @@ -90,6 +91,7 @@ const yamlHandlers: { [key in AssetTypes]: YAMLHandler<{ [key: string]: unknown
connectionProfiles,
tokenExchangeProfiles,
supplementalSignals,
rateLimitPolicies,
};

export default yamlHandlers;
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 { AssetTypes } from '../../../types';
import APIHandler from './default';
Expand Down Expand Up @@ -86,6 +87,7 @@ const auth0ApiHandlers: { [key in AssetTypes]: any } = {
connectionProfiles,
tokenExchangeProfiles,
supplementalSignals,
rateLimitPolicies,
};

export default auth0ApiHandlers as {
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 || []);
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);
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