Skip to content
Merged
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
9 changes: 4 additions & 5 deletions packages/rest-api-construct/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@
import * as apiGateway from 'aws-cdk-lib/aws-apigateway';
import { Construct } from 'constructs';
import { FunctionResources } from '@aws-amplify/backend/types/platform';
import { IUserPool } from 'aws-cdk-lib/aws-cognito';
import { ResourceProvider } from '@aws-amplify/backend/types/platform';

// @public (undocumented)
export type AuthorizerConfig = {
type: 'none';
} | {
type: 'userPool';
} | {
type: 'userPool';
groups: string[];
};

// @public (undocumented)
Expand All @@ -28,9 +26,9 @@ export type MethodsProps = {
authorizer?: AuthorizerConfig;
};

// @public
// @public (undocumented)
export class RestApiConstruct extends Construct {
constructor(scope: Construct, id: string, props: RestApiConstructProps);
constructor(scope: Construct, id: string, props: RestApiConstructProps, userPool?: IUserPool);
// (undocumented)
readonly api: apiGateway.RestApi;
}
Expand All @@ -39,6 +37,7 @@ export class RestApiConstruct extends Construct {
export type RestApiConstructProps = {
apiName: string;
apiProps: RestApiPathConfig[];
defaultAuthorizer?: AuthorizerConfig;
};

// @public (undocumented)
Expand Down
81 changes: 81 additions & 0 deletions packages/rest-api-construct/src/construct.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import assert from 'node:assert';
import { RestApiPathConfig } from './types.js';
import { handler } from './test-assets/handler.js';
import { Context } from 'aws-lambda';
import { Template } from 'aws-cdk-lib/assertions';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as apiGateway from 'aws-cdk-lib/aws-apigateway';

const setupExampleLambda = (stack: Stack) => {
const factory = defineFunction({
Expand Down Expand Up @@ -61,6 +64,84 @@ const constructApiWithPath = (path: string, n: number = 1) => {
});
};

void describe('RestApiConstruct User Pool Handling', () => {
void it('attaches a Cognito authorizer when a user pool is provided', () => {
const app = new App();
const stack = new Stack(app);

// Create a test Cognito user pool
const userPool = new cognito.UserPool(stack, 'TestUserPool', {
userPoolName: 'test-pool',
});

// Setup a Lambda to attach to the API
const lambdaResource = setupExampleLambda(stack);

// Create the API with a GET method using user pool authorization
new RestApiConstruct(
stack,
'RestApiWithAuth',
{
apiName: 'RestApiWithAuth',
apiProps: [
{
path: '/test',
lambdaEntry: lambdaResource,
methods: [
{ method: 'GET', authorizer: { type: 'userPool' } },
{ method: 'POST', authorizer: { type: 'userPool' } },
],
},
],
},
userPool,
);

const template = Template.fromStack(stack); // <-- use Template here

// Check that the GET method has the proper Cognito user pool auth
template.hasResourceProperties('AWS::ApiGateway::Method', {
AuthorizationType: apiGateway.AuthorizationType.COGNITO, // this is COGNITO_USER_POOLS
HttpMethod: 'GET',
});

// Check that the POST method also has the proper Cognito user pool auth
template.hasResourceProperties('AWS::ApiGateway::Method', {
AuthorizationType: apiGateway.AuthorizationType.COGNITO,
HttpMethod: 'POST',
});

// Optionally, check that authorizer is correctly attached
template.hasResourceProperties('AWS::ApiGateway::Method', {
AuthorizerId: { Ref: 'RestApiWithAuthDefaultUserPoolAuth0006E5FA' }, // CDK auto-generated ID
});
});

void it('does not create an authorizer if no user pool is passed', () => {
const stack = new Stack(new App());
const lambdaResource = setupExampleLambda(stack);

new RestApiConstruct(stack, 'RestApiWithoutAuth', {
apiName: 'RestApiWithoutAuth',
apiProps: [
{
path: '/test',
lambdaEntry: lambdaResource,
methods: [{ method: 'GET', authorizer: { type: 'none' } }],
},
],
});

const template = Template.fromStack(stack); // <-- use Template here

// GET method should be open (no auth)
template.hasResourceProperties('AWS::ApiGateway::Method', {
AuthorizationType: apiGateway.AuthorizationType.NONE,
HttpMethod: 'GET',
});
});
});

void describe('RestApiConstruct Lambda Handling', () => {
void it('integrates the result of defineFunction into the api', () => {
const app = new App();
Expand Down
80 changes: 71 additions & 9 deletions packages/rest-api-construct/src/construct.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,105 @@
import { Construct } from 'constructs';
import * as apiGateway from 'aws-cdk-lib/aws-apigateway';
import { RestApiConstructProps } from './types.js';
import { IUserPool } from 'aws-cdk-lib/aws-cognito';
import { MethodsProps, RestApiConstructProps } from './types.js';
import { validateRestApiPaths } from './validate_paths.js';

/**
* Rest API construct for Amplify Backend
*
*/
export class RestApiConstruct extends Construct {
public readonly api: apiGateway.RestApi;
private readonly userPoolAuthorizer?: apiGateway.CognitoUserPoolsAuthorizer;

/**
* Create a new RestApiConstruct
* Creates a new REST API in API Gateway with optional Cognito User Pool authorization.
*
* If a user pool is provided, all routes default to using it for authentication unless overridden.
* @param scope - The scope in which this construct is defined.
* @param id - The unique identifier for this construct.
* @param props - Configuration options for the REST API, including name and route definitions.
* @param userPool - Optional Cognito User Pool to use as the default authorizer.
*/
constructor(scope: Construct, id: string, props: RestApiConstructProps) {
constructor(
scope: Construct,
id: string,
props: RestApiConstructProps,
userPool?: IUserPool,
) {
super(scope, id);

//check that the paths are valid before creating the API
const paths: string[] = [];
props.apiProps.forEach((value) => paths.push(value.path));
validateRestApiPaths(paths);

// Create a new API Gateway REST API with the specified name
// Create API
this.api = new apiGateway.RestApi(this, 'RestApi', {
restApiName: props.apiName,
});

// Iterate over each path configuration
// If userPool exists, create default authorizer
if (userPool) {
this.userPoolAuthorizer = new apiGateway.CognitoUserPoolsAuthorizer(
this,
'DefaultUserPoolAuth',
{
cognitoUserPools: [userPool],
},
);
}

for (const pathConfig of props.apiProps) {
const { path, methods, lambdaEntry } = pathConfig;
// Add resource and methods for this route
const resource = this.addNestedResource(this.api.root, path);

for (const method of methods) {
resource.addMethod(
method.method,
new apiGateway.LambdaIntegration(lambdaEntry.resources.lambda),
const integration = new apiGateway.LambdaIntegration(
lambdaEntry.resources.lambda,
);

resource.addMethod(method.method, integration, {
authorizer: this.getAuthorizerForMethod(method),
authorizationType: this.getAuthorizationType(method),
});
}
}
}

/**
*
* If the method specifies a user pool authorizer, it returns the default user pool authorizer.
* If no authorizer is specified but a user pool exists, it returns the default user pool authorizer.
* Otherwise, it returns undefined.
*/

private getAuthorizerForMethod(method: MethodsProps) {
if (method.authorizer?.type === 'userPool' && this.userPoolAuthorizer) {
return this.userPoolAuthorizer;
}
if (!method.authorizer && this.userPoolAuthorizer) {
return this.userPoolAuthorizer;
}
return undefined;
}

/**
* Determines the authorization type based on the method's authorizer configuration.
* If a user pool authorizer is set or if no authorizer is specified but a user pool exists, it returns COGNITO.
* Otherwise, it returns NONE.
*/

private getAuthorizationType(method: MethodsProps) {
if (
method.authorizer?.type === 'userPool' ||
(!method.authorizer && this.userPoolAuthorizer)
) {
return apiGateway.AuthorizationType.COGNITO;
}
return apiGateway.AuthorizationType.NONE;
}

/**
* Adds nested resources to the API based on the provided path.
*/
Expand Down
6 changes: 4 additions & 2 deletions packages/rest-api-construct/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import {
export type RestApiConstructProps = {
apiName: string;
apiProps: RestApiPathConfig[];
defaultAuthorizer?: AuthorizerConfig;
};

export type AuthorizerConfig =
| { type: 'none' } // public
| { type: 'userPool' } // signed-in users
| { type: 'userPool'; groups: string[] }; // signed-in + group restriction
| { type: 'userPool' }; // signed-in users
// TODO: Group Validation
// | { type: 'userPool', groups: string[] } // signed-in users in a group

export type MethodsProps = {
method: HttpMethod;
Expand Down