diff --git a/.gitignore b/.gitignore index 1be7eae0b8a63..4a2cbd724bb8f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ cdk.out/ # Yarn error log yarn-error.log + +.nycrc diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index fb8b7df94acd4..edf2adc2b02ce 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -370,8 +370,8 @@ API Gateway interacts with the authorizer Lambda function handler by passing inp The event object that the handler is called with contains the `authorizationToken` and the `methodArn` from the request to the API Gateway endpoint. The handler is expected to return the `principalId` (i.e. the client identifier) and a `policyDocument` stating what the client is authorizer to perform. -See https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html for a detailed specification on -inputs and outputs of the lambda handler. +See [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html) for a detailed specification on +inputs and outputs of the Lambda handler. The following code attaches a token-based Lambda authorizer to the 'GET' Method of the Book resource: @@ -397,6 +397,44 @@ Authorizers can also be passed via the `defaultMethodOptions` property within th explicitly overridden, the specified defaults will be applied across all `Method`s across the `RestApi` or across all `Resource`s, depending on where the defaults were specified. +#### Lambda-based request authorizer + +This module provides support for request-based Lambda authorizers. When a client makes a request to an API's methods configured with such +an authorizer, API Gateway calls the Lambda authorizer, which takes the caller's identity as input and returns an IAM policy as output. +A request-based Lambda authorizer (also called a request authorizer) receives the caller's identity in a series of values pulled from +the request, from the headers, query strings, etc. + +API Gateway interacts with the authorizer Lambda function handler by passing input and expecting the output in a specific format. +The event object that the handler is called with contains the body of the request and the `methodArn` from the request to the +API Gateway endpoint. The handler is expected to return the `principalId` (i.e. the client identifier) and a `policyDocument` stating +what the client is authorizer to perform. +See [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html) for a detailed specification on +inputs and outputs of the Lambda handler. + +The following code attaches a request-based Lambda authorizer to the 'GET' Method of the Book resource: + +```ts +const authFn = new lambda.Function(this, 'booksAuthorizerLambda', { + // ... + // ... +}); + +const auth = new apigateway.RequestAuthorizer(this, 'booksAuthorizer', { + function: authFn, +}); + +books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { + authorizer: auth +}); +``` + +By default, the `RequestAuthorizer` does not pass any kind of information from the request. This can, +however, be modified by changing the `identitySource` property, and is required when specifying a value for caching. + +Authorizers can also be passed via the `defaultMethodOptions` property within the `RestApi` construct or the `Method` construct. Unless +explicitly overridden, the specified defaults will be applied across all `Method`s across the `RestApi` or across all `Resource`s, +depending on where the defaults were specified. + ### Deployments By default, the `RestApi` construct will automatically create an API Gateway @@ -539,7 +577,8 @@ running at one origin, access to selected resources from a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) from its own. -You can add the CORS [preflight](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests) OPTIONS HTTP method to any API resource via the `defaultCorsPreflightOptions` option or by calling the `addCorsPreflight` on a specific resource. +You can add the CORS [preflight](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests) OPTIONS +HTTP method to any API resource via the `defaultCorsPreflightOptions` option or by calling the `addCorsPreflight` on a specific resource. The following example will enable CORS for all methods and all origins on all resources of the API: diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index 5175407981186..f68f6f8a04d9c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -6,10 +6,9 @@ import { Authorizer, IAuthorizer } from '../authorizer'; import { RestApi } from '../restapi'; /** - * Properties for TokenAuthorizer + * Base properties for all lambda authorizers */ -export interface TokenAuthorizerProps { - +export interface LambdaAuthorizerProps { /** * An optional human friendly name for the authorizer. Note that, this is not the primary identifier of the authorizer. * @@ -27,14 +26,6 @@ export interface TokenAuthorizerProps { */ readonly handler: lambda.IFunction; - /** - * The request header mapping expression for the bearer token. This is typically passed as part of the header, in which case - * this should be `method.request.header.Authorizer` where Authorizer is the header containing the bearer token. - * @see https://docs.aws.amazon.com/apigateway/api-reference/link-relation/authorizer-create/#identitySource - * @default 'method.request.header.Authorization' - */ - readonly identitySource?: string; - /** * How long APIGateway should cache the results. Max 1 hour. * Disable caching by setting this to 0. @@ -43,14 +34,6 @@ export interface TokenAuthorizerProps { */ readonly resultsCacheTtl?: Duration; - /** - * An optional regex to be matched against the authorization token. When matched the authorizer lambda is invoked, - * otherwise a 401 Unauthorized is returned to the client. - * - * @default - no regex filter will be applied. - */ - readonly validationRegex?: string; - /** * An optional IAM role for APIGateway to assume before calling the Lambda-based authorizer. The IAM role must be * assumable by 'apigateway.amazonaws.com'. @@ -60,14 +43,7 @@ export interface TokenAuthorizerProps { readonly assumeRole?: iam.IRole; } -/** - * Token based lambda authorizer that recognizes the caller's identity as a bearer token, - * such as a JSON Web Token (JWT) or an OAuth token. - * Based on the token, authorization is performed by a lambda function. - * - * @resource AWS::ApiGateway::Authorizer - */ -export class TokenAuthorizer extends Authorizer implements IAuthorizer { +abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer { /** * The id of the authorizer. @@ -77,12 +53,13 @@ export class TokenAuthorizer extends Authorizer implements IAuthorizer { /** * The ARN of the authorizer to be used in permission policies, such as IAM and resource-based grants. + * @attribute */ public readonly authorizerArn: string; - private restApiId?: string; + protected restApiId?: string; - constructor(scope: Construct, id: string, props: TokenAuthorizerProps) { + protected constructor(scope: Construct, id: string, props: LambdaAuthorizerProps) { super(scope, id); if (props.resultsCacheTtl && props.resultsCacheTtl.toSeconds() > 3600) { @@ -91,18 +68,7 @@ export class TokenAuthorizer extends Authorizer implements IAuthorizer { const restApiId = Lazy.stringValue({ produce: () => this.restApiId }); - const resource = new CfnAuthorizer(this, 'Resource', { - name: props.authorizerName ?? this.node.uniqueId, - restApiId, - type: 'TOKEN', - authorizerUri: `arn:aws:apigateway:${Stack.of(this).region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`, - authorizerCredentials: props.assumeRole ? props.assumeRole.roleArn : undefined, - authorizerResultTtlInSeconds: props.resultsCacheTtl && props.resultsCacheTtl.toSeconds(), - identitySource: props.identitySource || 'method.request.header.Authorization', - identityValidationExpression: props.validationRegex, - }); - - this.authorizerId = resource.ref; + this.authorizerId = this.getResource(props).ref; this.authorizerArn = Stack.of(this).formatArn({ service: 'execute-api', @@ -138,4 +104,102 @@ export class TokenAuthorizer extends Authorizer implements IAuthorizer { this.restApiId = restApi.restApiId; } -} \ No newline at end of file + + protected abstract getResource(props: T): CfnAuthorizer; +} + +/** + * Properties for TokenAuthorizer + */ +export interface TokenAuthorizerProps extends LambdaAuthorizerProps { + /** + * An optional regex to be matched against the authorization token. When matched the authorizer lambda is invoked, + * otherwise a 401 Unauthorized is returned to the client. + * + * @default - no regex filter will be applied. + */ + readonly validationRegex?: string; + + /** + * The request header mapping expression for the bearer token. This is typically passed as part of the header, in which case + * this should be `method.request.header.Authorizer` where Authorizer is the header containing the bearer token. + * @see https://docs.aws.amazon.com/apigateway/api-reference/link-relation/authorizer-create/#identitySource + * @default 'method.request.header.Authorization' + */ + readonly identitySource?: string; +} + +/** + * Token based lambda authorizer that recognizes the caller's identity as a bearer token, + * such as a JSON Web Token (JWT) or an OAuth token. + * Based on the token, authorization is performed by a lambda function. + * + * @resource AWS::ApiGateway::Authorizer + */ +export class TokenAuthorizer extends LambdaAuthorizer { + + constructor(scope: Construct, id: string, props: TokenAuthorizerProps) { + super(scope, id, props); + } + + protected getResource(props: TokenAuthorizerProps): CfnAuthorizer { + const restApiId = Lazy.stringValue({ produce: () => this.restApiId }); + + return new CfnAuthorizer(this, 'Resource', { + name: props.authorizerName ?? this.node.uniqueId, + restApiId, + type: 'TOKEN', + authorizerUri: `arn:aws:apigateway:${Stack.of(this).region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`, + authorizerCredentials: props.assumeRole ? props.assumeRole.roleArn : undefined, + authorizerResultTtlInSeconds: props.resultsCacheTtl && props.resultsCacheTtl.toSeconds(), + identitySource: props.identitySource || 'method.request.header.Authorization', + identityValidationExpression: props.validationRegex, + }); + } +} + +/** + * Properties for RequestAuthorizerProps + */ +export interface RequestAuthorizerProps extends LambdaAuthorizerProps { + /** + * An array of request header mapping expressions for identities. This is typically passed as part of the header, + * in which case this should be `method.request.header.Authorizer` where Authorizer is the header containing the + * bearer token. + * @see https://docs.aws.amazon.com/apigateway/api-reference/link-relation/authorizer-create/#identitySource + * @default no identity sources + */ + readonly identitySource?: string[]; +} + +/** + * Request-based lambda authorizer that recognizes the caller's identity via request parameters, + * such as headers, paths, query strings, stage variables, or context variables. + * Based on the request, authorization is performed by a lambda function. + * + * @resource AWS::ApiGateway::Authorizer + */ +export class RequestAuthorizer extends LambdaAuthorizer { + + constructor(scope: Construct, id: string, props: RequestAuthorizerProps) { + super(scope, id, props); + + if (props.resultsCacheTtl && props.identitySource?.length === 0) { + throw new Error(`At least one Identity Source is required for a REQUEST-based Lambda authorizer.`); + } + } + + protected getResource(props: RequestAuthorizerProps): CfnAuthorizer { + const restApiId = Lazy.stringValue({ produce: () => this.restApiId }); + + return new CfnAuthorizer(this, 'Resource', { + name: props.authorizerName ?? this.node.uniqueId, + restApiId, + type: 'REQUEST', + authorizerUri: `arn:aws:apigateway:${Stack.of(this).region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`, + authorizerCredentials: props.assumeRole ? props.assumeRole.roleArn : undefined, + authorizerResultTtlInSeconds: props.resultsCacheTtl && props.resultsCacheTtl.toSeconds(), + identitySource: props.identitySource?.join(','), + }); + } +} diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.expected.json new file mode 100644 index 0000000000000..b9474e57cfa3e --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.expected.json @@ -0,0 +1,320 @@ +{ + "Resources": { + "MyAuthorizerFunctionServiceRole8A34C19E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyAuthorizerFunction70F1223E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthorizerFunctionServiceRole8A34C19E", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "MyAuthorizerFunctionServiceRole8A34C19E" + ] + }, + "authorizerRole06E70703": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyAuthorizer6575980E": { + "Type": "AWS::ApiGateway::Authorizer", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Type": "REQUEST", + "AuthorizerCredentials": { + "Fn::GetAtt": [ + "authorizerRole06E70703", + "Arn" + ] + }, + "AuthorizerUri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "MyAuthorizerFunction70F1223E", + "Arn" + ] + }, + "/invocations" + ] + ] + }, + "IdentitySource": "Header" + } + }, + "MyAuthorizerauthorizerInvokePolicy0F88B8E1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyAuthorizerFunction70F1223E", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyAuthorizerauthorizerInvokePolicy0F88B8E1", + "Roles": [ + { + "Ref": "authorizerRole06E70703" + } + ] + } + }, + "MyRestApi2D1F47A9": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "MyRestApi" + } + }, + "MyRestApiDeploymentB555B5828fad37a0e56bbac79ae37ae990881dca": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyRestApiANY05143F93" + ] + }, + "MyRestApiDeploymentStageprodC33B8E5F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "DeploymentId": { + "Ref": "MyRestApiDeploymentB555B5828fad37a0e56bbac79ae37ae990881dca" + }, + "StageName": "prod" + } + }, + "MyRestApiCloudWatchRoleD4042E8E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "MyRestApiAccount2FB6DB7A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "MyRestApiCloudWatchRoleD4042E8E", + "Arn" + ] + } + }, + "DependsOn": [ + "MyRestApi2D1F47A9" + ] + }, + "MyRestApiANY05143F93": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "MyRestApi2D1F47A9", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Ref": "MyAuthorizer6575980E" + }, + "Integration": { + "IntegrationResponses": [ + { + "StatusCode": "200" + } + ], + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "application/json": "{ \"statusCode\": 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "StatusCode": "200" + } + ] + } + } + }, + "Parameters": { + "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4": { + "Type": "String", + "Description": "S3 bucket for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + }, + "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E": { + "Type": "String", + "Description": "S3 key for asset version \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + }, + "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aArtifactHash1A0BBA4E": { + "Type": "String", + "Description": "Artifact hash for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + } + }, + "Outputs": { + "MyRestApiEndpoint4C55E4CB": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "MyRestApi2D1F47A9" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyRestApiDeploymentStageprodC33B8E5F" + }, + "/" + ] + ] + } + } + } +} diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.ts new file mode 100644 index 0000000000000..eba34bc4b6268 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.ts @@ -0,0 +1,47 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { App, Stack } from '@aws-cdk/core'; +import * as path from 'path'; +import { AuthorizationType, MockIntegration, PassthroughBehavior, RestApi } from '../../lib'; +import {RequestAuthorizer} from '../../lib/authorizers'; + +// Against the RestApi endpoint from the stack output, run +// `curl -s -o /dev/null -w "%{http_code}" ` should return 401 +// `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: deny' ` should return 403 +// `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: allow' ` should return 200 + +const app = new App(); +const stack = new Stack(app, 'RequestAuthorizerIAMRoleInteg'); + +const authorizerFn = new lambda.Function(stack, 'MyAuthorizerFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.AssetCode.fromAsset(path.join(__dirname, 'integ.request-authorizer.handler')) +}); + +const role = new iam.Role(stack, 'authorizerRole', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com') +}); + +const authorizer = new RequestAuthorizer(stack, 'MyAuthorizer', { + handler: authorizerFn, + assumeRole: role, +}); + +const restapi = new RestApi(stack, 'MyRestApi'); + +restapi.root.addMethod('ANY', new MockIntegration({ + integrationResponses: [ + { statusCode: '200' } + ], + passthroughBehavior: PassthroughBehavior.NEVER, + requestTemplates: { + 'application/json': '{ "statusCode": 200 }', + }, +}), { + methodResponses: [ + { statusCode: '200' } + ], + authorizer, + authorizationType: AuthorizationType.CUSTOM +}); diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json new file mode 100644 index 0000000000000..c5f4205805210 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json @@ -0,0 +1,311 @@ +{ + "Resources": { + "MyAuthorizerFunctionServiceRole8A34C19E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyAuthorizerFunction70F1223E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyAuthorizerFunctionServiceRole8A34C19E", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "MyAuthorizerFunctionServiceRole8A34C19E" + ] + }, + "MyRestApi2D1F47A9": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "MyRestApi" + } + }, + "MyRestApiDeploymentB555B5828fad37a0e56bbac79ae37ae990881dca": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyRestApiANY05143F93" + ] + }, + "MyRestApiDeploymentStageprodC33B8E5F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "DeploymentId": { + "Ref": "MyRestApiDeploymentB555B5828fad37a0e56bbac79ae37ae990881dca" + }, + "StageName": "prod" + } + }, + "MyRestApiCloudWatchRoleD4042E8E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "MyRestApiAccount2FB6DB7A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "MyRestApiCloudWatchRoleD4042E8E", + "Arn" + ] + } + }, + "DependsOn": [ + "MyRestApi2D1F47A9" + ] + }, + "MyRestApiANY05143F93": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "MyRestApi2D1F47A9", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "AuthorizationType": "CUSTOM", + "AuthorizerId": { + "Ref": "MyAuthorizer6575980E" + }, + "Integration": { + "IntegrationResponses": [ + { + "StatusCode": "200" + } + ], + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "application/json": "{ \"statusCode\": 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "StatusCode": "200" + } + ] + } + }, + "MyAuthorizer6575980E": { + "Type": "AWS::ApiGateway::Authorizer", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Type": "REQUEST", + "AuthorizerUri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "MyAuthorizerFunction70F1223E", + "Arn" + ] + }, + "/invocations" + ] + ] + }, + "IdentitySource": "Header" + } + }, + "MyAuthorizerFunctionRequestAuthorizerIntegMyAuthorizer793B1D5FPermissions7557AE26": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthorizerFunction70F1223E", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyRestApi2D1F47A9" + }, + "/authorizers/", + { + "Ref": "MyAuthorizer6575980E" + } + ] + ] + } + } + } + }, + "Parameters": { + "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4": { + "Type": "String", + "Description": "S3 bucket for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + }, + "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E": { + "Type": "String", + "Description": "S3 key for asset version \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + }, + "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aArtifactHash1A0BBA4E": { + "Type": "String", + "Description": "Artifact hash for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + } + }, + "Outputs": { + "MyRestApiEndpoint4C55E4CB": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "MyRestApi2D1F47A9" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyRestApiDeploymentStageprodC33B8E5F" + }, + "/" + ] + ] + } + } + } +} diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.handler/index.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.handler/index.ts new file mode 100644 index 0000000000000..09f45034be73e --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.handler/index.ts @@ -0,0 +1,23 @@ +// tslint:disable:no-console + +export const handler = async (event: any, _context: any = {}): Promise => { + const authToken: string = event.headers.HeaderAuth1; + console.log(`event.authorizationToken = ${authToken}`); + if (authToken === 'allow' || authToken === 'deny') { + return { + principalId: 'user', + policyDocument: { + Version: "2012-10-17", + Statement: [ + { + Action: "execute-api:Invoke", + Effect: authToken, + Resource: event.methodArn + } + ] + } + }; + } else { + throw new Error('Unauthorized'); + } +}; diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.ts new file mode 100644 index 0000000000000..29bec4e47f957 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.ts @@ -0,0 +1,40 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import { App, Stack } from '@aws-cdk/core'; +import * as path from 'path'; +import { MockIntegration, PassthroughBehavior, RestApi } from '../../lib'; +import {RequestAuthorizer} from '../../lib/authorizers'; + +// Against the RestApi endpoint from the stack output, run +// `curl -s -o /dev/null -w "%{http_code}" ` should return 401 +// `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: deny' ` should return 403 +// `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: allow' ` should return 200 + +const app = new App(); +const stack = new Stack(app, 'RequestAuthorizerInteg'); + +const authorizerFn = new lambda.Function(stack, 'MyAuthorizerFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.AssetCode.fromAsset(path.join(__dirname, 'integ.request-authorizer.handler')) +}); + +const restapi = new RestApi(stack, 'MyRestApi'); + +const authorizer = new RequestAuthorizer(stack, 'MyAuthorizer', { + handler: authorizerFn, +}); + +restapi.root.addMethod('ANY', new MockIntegration({ + integrationResponses: [ + { statusCode: '200' } + ], + passthroughBehavior: PassthroughBehavior.NEVER, + requestTemplates: { + 'application/json': '{ "statusCode": 200 }', + }, +}), { + methodResponses: [ + { statusCode: '200' } + ], + authorizer +}); diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts index f58edf2657ced..c1a1a6b04006d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts @@ -4,6 +4,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import { Duration, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import { AuthorizationType, RestApi, TokenAuthorizer } from '../../lib'; +import {RequestAuthorizer} from '../../lib/authorizers'; export = { 'default token authorizer'(test: Test) { @@ -41,6 +42,40 @@ export = { test.done(); }, + 'default request authorizer'(test: Test) { + const stack = new Stack(); + + const func = new lambda.Function(stack, 'myfunction', { + handler: 'handler', + code: lambda.Code.fromInline('foo'), + runtime: lambda.Runtime.NODEJS_12_X, + }); + + const auth = new RequestAuthorizer(stack, 'myauthorizer', { + handler: func + }); + + const restApi = new RestApi(stack, 'myrestapi'); + restApi.root.addMethod('ANY', undefined, { + authorizer: auth, + authorizationType: AuthorizationType.CUSTOM + }); + + expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', { + Type: 'REQUEST', + RestApiId: stack.resolve(restApi.restApiId), + })); + + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + Principal: 'apigateway.amazonaws.com', + })); + + test.ok(auth.authorizerArn.endsWith(`/authorizers/${auth.authorizerId}`), 'Malformed authorizer ARN'); + + test.done(); + }, + 'token authorizer with all parameters specified'(test: Test) { const stack = new Stack(); @@ -76,6 +111,39 @@ export = { test.done(); }, + 'request authorizer with all parameters specified'(test: Test) { + const stack = new Stack(); + + const func = new lambda.Function(stack, 'myfunction', { + handler: 'handler', + code: lambda.Code.fromInline('foo'), + runtime: lambda.Runtime.NODEJS_12_X, + }); + + const auth = new RequestAuthorizer(stack, 'myauthorizer', { + handler: func, + identitySource: ['method.request.header.whoami'], + authorizerName: 'myauthorizer', + resultsCacheTtl: Duration.minutes(1), + }); + + const restApi = new RestApi(stack, 'myrestapi'); + restApi.root.addMethod('ANY', undefined, { + authorizer: auth, + authorizationType: AuthorizationType.CUSTOM + }); + + expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', { + Type: 'REQUEST', + RestApiId: stack.resolve(restApi.restApiId), + IdentitySource: 'method.request.header.whoami', + Name: 'myauthorizer', + AuthorizerResultTtlInSeconds: 60 + })); + + test.done(); + }, + 'token authorizer with assume role'(test: Test) { const stack = new Stack(); @@ -125,6 +193,58 @@ export = { expect(stack).notTo(haveResource('AWS::Lambda::Permission')); + test.done(); + }, + + 'request authorizer with assume role'(test: Test) { + const stack = new Stack(); + + const func = new lambda.Function(stack, 'myfunction', { + handler: 'handler', + code: lambda.Code.fromInline('foo'), + runtime: lambda.Runtime.NODEJS_12_X, + }); + + const role = new iam.Role(stack, 'authorizerassumerole', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + roleName: 'authorizerassumerole' + }); + + const auth = new RequestAuthorizer(stack, 'myauthorizer', { + handler: func, + assumeRole: role + }); + + const restApi = new RestApi(stack, 'myrestapi'); + restApi.root.addMethod('ANY', undefined, { + authorizer: auth, + authorizationType: AuthorizationType.CUSTOM + }); + + expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', { + Type: 'REQUEST', + RestApiId: stack.resolve(restApi.restApiId), + })); + + expect(stack).to(haveResource('AWS::IAM::Role')); + + expect(stack).to(haveResource('AWS::IAM::Policy', { + Roles: [ + stack.resolve(role.roleName) + ], + PolicyDocument: { + Statement: [ + { + Resource: stack.resolve(func.functionArn), + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + } + ], + } + }, ResourcePart.Properties, true)); + + expect(stack).notTo(haveResource('AWS::Lambda::Permission')); + test.done(); } };