Skip to content
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

Update types #3

Merged
merged 1 commit into from
Feb 24, 2023
Merged
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
210 changes: 150 additions & 60 deletions auth-policy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* AuthPolicy receives a set of allowed and denied methods and generates a valid
* AWS policy for the API Gateway authorizer. The constructor receives the calling
Expand All @@ -23,23 +24,21 @@
*/

export class AuthPolicy {

/**
* A set of existing HTTP verbs supported by API Gateway. This property is here
* only to avoid spelling mistakes in the policy.
*
* @property HttpVerb
* @type {Object}
*/
public HttpVerb = {
static HttpVerb: Record<TAllowedVerbsKeys, TAllowedVerbsValues> = {
GET: "GET",
POST: "POST",
PUT: "PUT",
PATCH: "PATCH",
HEAD: "HEAD",
DELETE: "DELETE",
OPTIONS: "OPTIONS",
ALL: "*"
ALL: "*",
};

private readonly awsAccountId: string;
Expand All @@ -52,7 +51,11 @@ export class AuthPolicy {
private readonly region: string;
private readonly stage: string;

constructor(principal: string, awsAccountId: string, apiOptions?: ApiOptions) {
constructor(
principal: string,
awsAccountId: string,
apiOptions?: ApiOptions
) {
/**
* The AWS account id the policy will be generated for. This is used to create
* the method ARNs.
Expand Down Expand Up @@ -87,7 +90,7 @@ export class AuthPolicy {
* @type {RegExp}
* @default '^\/[/.a-zA-Z0-9-\*]+$'
*/
this.pathRegex = new RegExp("^[/.a-zA-Z0-9-\*]+$");
this.pathRegex = new RegExp("^[/.a-zA-Z0-9-*]+$");

// these are the internal lists of allowed and denied methods. These are lists
// of objects and each object has 2 properties: A resource ARN and a nullable
Expand Down Expand Up @@ -115,22 +118,22 @@ export class AuthPolicy {
}

/**
* Adds an allow "*" statement to the policy.
* Adds an Allow "*" statement to the policy.
*
* @method allowAllMethods
*/
public allowAllMethods = () => {
this.addMethod.call(this, "allow", "*", "*", null);
}
this.addMethod.call(this, "Allow", "*", "*", null);
};

/**
* Adds a deny "*" statement to the policy.
* Adds a Deny "*" statement to the policy.
*
* @method denyAllMethods
*/
public denyAllMethods = () => {
this.addMethod.call(this, "deny", "*", "*", null);
}
this.addMethod.call(this, "Deny", "*", "*", null);
};

/**
* Adds an API Gateway method (Http verb + Resource path) to the list of allowed
Expand All @@ -142,9 +145,9 @@ export class AuthPolicy {
* @param resource {string} The resource path. For example "/pets"
* @return {void}
*/
public allowMethod = (verb: string, resource: string) => {
this.addMethod.call(this, "allow", verb, resource, null);
}
public allowMethod = (verb: TAllowedVerbs, resource: string): void => {
this.addMethod.call(this, "Allow", verb, resource, null);
};

/**
* Adds an API Gateway method (Http verb + Resource path) to the list of denied
Expand All @@ -156,9 +159,9 @@ export class AuthPolicy {
* @param resource {string} The resource path. For example "/pets"
* @return {void}
*/
public denyMethod = (verb: string, resource: string) => {
this.addMethod.call(this, "deny", verb, resource, null);
}
public denyMethod = (verb: TAllowedVerbs, resource: string): void => {
this.addMethod.call(this, "Deny", verb, resource, null);
};

/**
* Adds an API Gateway method (Http verb + Resource path) to the list of allowed
Expand All @@ -172,9 +175,13 @@ export class AuthPolicy {
* @param conditions {Object} The conditions object in the format specified by the AWS docs
* @return {void}
*/
public allowMethodWithConditions = (verb: string, resource: string, conditions: any) => {
this.addMethod.call(this, "allow", verb, resource, conditions);
}
public allowMethodWithConditions = (
verb: TAllowedVerbs,
resource: string,
conditions: any
): void => {
this.addMethod.call(this, "Allow", verb, resource, conditions);
};

/**
* Adds an API Gateway method (Http verb + Resource path) to the list of denied
Expand All @@ -188,9 +195,13 @@ export class AuthPolicy {
* @param conditions {Object} The conditions object in the format specified by the AWS docs
* @return {void}
*/
public denyMethodWithConditions = (verb: string, resource: string, conditions: any) => {
this.addMethod.call(this, "deny", verb, resource, conditions);
}
public denyMethodWithConditions = (
verb: TAllowedVerbs,
resource: string,
conditions: any
): void => {
this.addMethod.call(this, "Deny", verb, resource, conditions);
};

/**
* Generates the policy document based on the internal lists of allowed and denied
Expand All @@ -201,25 +212,38 @@ export class AuthPolicy {
* @method build
* @return {Object} The policy object that can be serialized to JSON.
*/
public build = () => {
if ((!this.allowMethods || this.allowMethods.length === 0) &&
(!this.denyMethods || this.denyMethods.length === 0)) {
public build = (): IPolicy => {
if (
(!this.allowMethods || this.allowMethods.length === 0) &&
(!this.denyMethods || this.denyMethods.length === 0)
) {
throw new Error("No statements defined for the policy");
}

const policy: any = {};
policy.principalId = this.principalId;
const doc: any = {};
doc.Version = this.version;
doc.Statement = [];

doc.Statement = doc.Statement.concat(this.getStatementsForEffect.call(this, "Allow", this.allowMethods));
doc.Statement = doc.Statement.concat(this.getStatementsForEffect.call(this, "Deny", this.denyMethods));
const policy: IPolicy = {
principalId: this.principalId,
policyDocument: {
Version: this.version,
Statement: [],
},
};

const doc: IPolicyDocument = {
Version: this.version,
Statement: [],
};

doc.Statement = doc.Statement.concat(
this.getStatementsForEffect.call(this, "Allow", this.allowMethods)
);
doc.Statement = doc.Statement.concat(
this.getStatementsForEffect.call(this, "Deny", this.denyMethods)
);

policy.policyDocument = doc;

return policy;
}
};

/**
* Adds a method to the internal lists of allowed or denied methods. Each object in
Expand All @@ -234,39 +258,57 @@ export class AuthPolicy {
* @param conditions {Object} The conditions object in the format specified by the AWS docs.
* @return {void}
*/
private addMethod = (effect: string, verb: string, resource: string, conditions: any) => {
if (verb !== "*" && !this.HttpVerb.hasOwnProperty(verb)) {
throw new Error("Invalid HTTP verb " + verb + ". Allowed verbs in AuthPolicy.HttpVerb");
private addMethod = (
effect: TEffect,
verb: TAllowedVerbs,
resource: string,
conditions: any
): void => {
if (verb !== "*" && !AuthPolicy.HttpVerb[verb]) {
throw new Error(
"Invalid HTTP verb " + verb + ". Allowed verbs in AuthPolicy.HttpVerb"
);
}

if (!this.pathRegex.test(resource)) {
throw new Error("Invalid resource path: " + resource + ". Path should match " + this.pathRegex);
throw new Error(
"Invalid resource path: " +
resource +
". Path should match " +
this.pathRegex
);
}

let cleanedResource = resource;
if (resource.substring(0, 1) === "/") {
cleanedResource = resource.substring(1, resource.length);
}
const resourceArn = "arn:aws:execute-api:" +
this.region + ":" +
this.awsAccountId + ":" +
this.restApiId + "/" +
this.stage + "/" +
verb + "/" +
const resourceArn =
"arn:aws:execute-api:" +
this.region +
":" +
this.awsAccountId +
":" +
this.restApiId +
"/" +
this.stage +
"/" +
verb +
"/" +
cleanedResource;

if (effect.toLowerCase() === "allow") {
this.allowMethods.push({
resourceArn,
conditions
conditions,
});
} else if (effect.toLowerCase() === "deny") {
this.denyMethods.push({
resourceArn,
conditions
conditions,
});
}
}
};

/**
* Returns an empty statement object prepopulated with the correct action and the
Expand All @@ -277,15 +319,15 @@ export class AuthPolicy {
* @return {Object} An empty statement object with the Action, Effect, and Resource
* properties prepopulated.
*/
private getEmptyStatement = (effect: string) => {
effect = effect.substring(0, 1).toUpperCase() + effect.substring(1, effect.length).toLowerCase();
const statement: any = {};
statement.Action = "execute-api:Invoke";
statement.Effect = effect;
statement.Resource = [];
private getEmptyStatement = (effect: TEffect): IStatement => {
const statement: IStatement = {
Action: "execute-api:Invoke",
Effect: effect,
Resource: [],
};

return statement;
}
};

/**
* This function loops over an array of objects containing a resourceArn and
Expand All @@ -297,14 +339,20 @@ export class AuthPolicy {
* and the conditions for the policy
* @return {Array} an array of formatted statements for the policy.
*/
private getStatementsForEffect = (effect: string, methods: ApiMethod[]) => {
const statements = [];
private getStatementsForEffect = (
effect: TEffect,
methods: ApiMethod[]
): Array<any> => {
const statements: IStatement[] = [];

if (methods.length > 0) {
const statement = this.getEmptyStatement(effect);

for (const curMethod of methods) {
if (curMethod.conditions === null || curMethod.conditions.length === 0) {
if (
curMethod.conditions === null ||
curMethod.conditions.length === 0
) {
statement.Resource.push(curMethod.resourceArn);
} else {
const conditionalStatement = this.getEmptyStatement(effect);
Expand All @@ -320,7 +368,7 @@ export class AuthPolicy {
}

return statements;
}
};
}

interface ApiMethod {
Expand All @@ -333,3 +381,45 @@ export interface ApiOptions {
region?: string;
stage?: string;
}

export interface IStatement {
Action: string;
Effect: TEffect;
Resource: string[];
Condition?: any;
}

export interface IPolicy {
principalId: string;
policyDocument: IPolicyDocument;
context?: any;
}

export interface IPolicyDocument {
Version: string;
Statement: IStatement[];
}

type TAllowedVerbsKeys =
| "GET"
| "POST"
| "PUT"
| "PATCH"
| "HEAD"
| "DELETE"
| "OPTIONS"
| "ALL";

type TAllowedVerbsValues =
| "*"
| "GET"
| "POST"
| "PUT"
| "PATCH"
| "HEAD"
| "DELETE"
| "OPTIONS";

type TAllowedVerbs = TAllowedVerbsKeys | TAllowedVerbsValues;

type TEffect = "Allow" | "Deny";