From a201af5e95f7bca80ba6e2457e357b04044e2c63 Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Sat, 4 Jan 2020 18:40:47 -0600 Subject: [PATCH 01/10] feat(apigateway): add lambda request authorizer construct This creates a common LambdaAuthorizer base class so that the token and request authorizers can share common functionality. --- .gitignore | 1 + packages/@aws-cdk/aws-apigateway/README.md | 45 ++- .../aws-apigateway/lib/authorizers/lambda.ts | 148 +++++--- ....request-authorizer-iam-role.expected.json | 320 ++++++++++++++++++ .../integ.request-authorizer-iam-role.ts | 47 +++ .../integ.request-authorizer.expected.json | 311 +++++++++++++++++ .../integ.request-authorizer.handler/index.ts | 23 ++ .../authorizers/integ.request-authorizer.ts | 40 +++ .../test/authorizers/test.lambda.ts | 120 +++++++ 9 files changed, 1010 insertions(+), 45 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.handler/index.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.ts diff --git a/.gitignore b/.gitignore index 1fc6393de0acd..17ba70d7ac568 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ pack/ .tools/ coverage/ .nyc_output +.nycrc .LAST_BUILD *.sw[a-z] *~ diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index e190d03b8ee2e..8f4322e74ed16 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..bcd0168e9a822 --- /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": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + } + ] + } + ] + } + ] + ] + } + }, + "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": { + "Name": "RequestAuthorizerIAMRoleIntegMyAuthorizerB6939B6E", + "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" + ] + ] + } + } + }, + "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": { + "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA": { + "Type": "String", + "Description": "S3 bucket for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + }, + "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64": { + "Type": "String", + "Description": "S3 key for asset version \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + }, + "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2ArtifactHash720B9536": { + "Type": "String", + "Description": "Artifact hash for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + } + }, + "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..e23eb908e1043 --- /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": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + } + ] + } + ] + } + ] + ] + } + }, + "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": { + "Name": "RequestAuthorizerIntegMyAuthorizer5D9D41C5", + "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" + ] + ] + } + } + }, + "MyAuthorizerFunctionRequestAuthorizerIntegMyAuthorizer5D9D41C5PermissionsCB8B246E": { + "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": { + "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA": { + "Type": "String", + "Description": "S3 bucket for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + }, + "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64": { + "Type": "String", + "Description": "S3 key for asset version \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + }, + "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2ArtifactHash720B9536": { + "Type": "String", + "Description": "Artifact hash for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + } + }, + "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(); } }; From 6eae6ca81c953e6bf6ca7c5480c07ceac45f854a Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Wed, 12 Feb 2020 14:51:20 -0600 Subject: [PATCH 02/10] fixup! feat(apigateway): add lambda request authorizer construct --- packages/@aws-cdk/aws-apigateway/README.md | 4 ++-- .../@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts | 9 ++++----- packages/@aws-cdk/aws-apigateway/package.json | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 8f4322e74ed16..603e1ee9aabcc 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -382,7 +382,7 @@ const authFn = new lambda.Function(this, 'booksAuthorizerLambda', { }); const auth = new apigateway.TokenAuthorizer(this, 'booksAuthorizer', { - function: authFn + handler: authFn }); books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { @@ -420,7 +420,7 @@ const authFn = new lambda.Function(this, 'booksAuthorizerLambda', { }); const auth = new apigateway.RequestAuthorizer(this, 'booksAuthorizer', { - function: authFn, + handler: authFn, }); books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index f68f6f8a04d9c..c1baaae405385 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -53,7 +53,6 @@ abstract class LambdaAuthorizer 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; @@ -68,7 +67,7 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer { const restApiId = Lazy.stringValue({ produce: () => this.restApiId }); - this.authorizerId = this.getResource(props).ref; + this.authorizerId = this.createResource(props).ref; this.authorizerArn = Stack.of(this).formatArn({ service: 'execute-api', @@ -105,7 +104,7 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer { this.restApiId = restApi.restApiId; } - protected abstract getResource(props: T): CfnAuthorizer; + protected abstract createResource(props: T): CfnAuthorizer; } /** @@ -142,7 +141,7 @@ export class TokenAuthorizer extends LambdaAuthorizer { super(scope, id, props); } - protected getResource(props: TokenAuthorizerProps): CfnAuthorizer { + protected createResource(props: TokenAuthorizerProps): CfnAuthorizer { const restApiId = Lazy.stringValue({ produce: () => this.restApiId }); return new CfnAuthorizer(this, 'Resource', { @@ -189,7 +188,7 @@ export class RequestAuthorizer extends LambdaAuthorizer { } } - protected getResource(props: RequestAuthorizerProps): CfnAuthorizer { + protected createResource(props: RequestAuthorizerProps): CfnAuthorizer { const restApiId = Lazy.stringValue({ produce: () => this.restApiId }); return new CfnAuthorizer(this, 'Resource', { diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 3ab5ac3205411..b5e8452e9fab4 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -292,8 +292,8 @@ "props-default-doc:@aws-cdk/aws-apigateway.MethodOptions.requestModels", "props-default-doc:@aws-cdk/aws-apigateway.MethodOptions.requestValidator", "docs-public-apis:@aws-cdk/aws-apigateway.ResourceBase.url", - "attribute-tag:@aws-cdk/aws-apigateway.TokenAuthorizer.authorizerArn" + "attribute-tag:@aws-cdk/aws-apigateway.LambdaAuthorizer.authorizerArn" ] }, "stability": "stable" -} \ No newline at end of file +} From 38834fddab4a7efc3c7ae31fa21e036429cd6c6e Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Wed, 12 Feb 2020 16:29:59 -0600 Subject: [PATCH 03/10] fixup! feat(apigateway): add lambda request authorizer construct --- packages/@aws-cdk/aws-apigateway/README.md | 6 +++--- .../aws-apigateway/lib/authorizers/lambda.ts | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 603e1ee9aabcc..4471f18aec291 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -400,9 +400,9 @@ 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. +an authorizer, API Gateway calls the Lambda authorizer, which takes specified parts of the request, known as identity sources, +as input and returns an IAM policy as output. A request-based Lambda authorizer (also called a request authorizer) receives +the identity sources in a series of values pulled from the request, from the headers, stage variables, query strings, and the context. 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 diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index c1baaae405385..4d05906813081 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -162,9 +162,15 @@ export class TokenAuthorizer extends LambdaAuthorizer { */ 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. + * An array of request header mapping expressions for identities. Supported parameter types are + * Header, Query String, Stage Variable, and Context. For instance, extracting an authorization + * token from a header would use the identity source `method.request.header.Authorizer`. + * + * Note: API Gateway uses the specified identity sources as the request authorizer caching key. When caching is + * enabled, API Gateway calls the authorizer's Lambda function only after successfully verifying that all the + * specified identity sources are present at runtime. If a specified identify source is missing, null, or empty, + * API Gateway returns a 401 Unauthorized response without calling the authorizer Lambda function. + * * @see https://docs.aws.amazon.com/apigateway/api-reference/link-relation/authorizer-create/#identitySource * @default no identity sources */ From 7259c708e1d65ca03e1dd9dd3de7ae47b58ddc82 Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Wed, 12 Feb 2020 19:36:44 -0600 Subject: [PATCH 04/10] fixup! feat(apigateway): add lambda request authorizer construct --- .../aws-apigateway/lib/authorizers/lambda.ts | 8 +- ...t-authorizer-identity-source.expected.json | 311 ++++++++++++++++++ .../index.ts | 23 ++ ...nteg.request-authorizer-identity-source.ts | 41 +++ .../integ.request-authorizer.handler/index.ts | 9 +- 5 files changed, 383 insertions(+), 9 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.expected.json create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.handler/index.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.ts diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index 4d05906813081..4057d8bcb2f3c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -149,8 +149,8 @@ export class TokenAuthorizer extends LambdaAuthorizer { 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(), + authorizerCredentials: props.assumeRole?.roleArn, + authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds() ?? 0, identitySource: props.identitySource || 'method.request.header.Authorization', identityValidationExpression: props.validationRegex, }); @@ -202,8 +202,8 @@ export class RequestAuthorizer extends LambdaAuthorizer { 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(), + authorizerCredentials: props.assumeRole?.roleArn, + authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds() ?? 0, identitySource: props.identitySource?.join(','), }); } diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.expected.json new file mode 100644 index 0000000000000..e23eb908e1043 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.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": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + } + ] + } + ] + } + ] + ] + } + }, + "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": { + "Name": "RequestAuthorizerIntegMyAuthorizer5D9D41C5", + "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" + ] + ] + } + } + }, + "MyAuthorizerFunctionRequestAuthorizerIntegMyAuthorizer5D9D41C5PermissionsCB8B246E": { + "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": { + "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA": { + "Type": "String", + "Description": "S3 bucket for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + }, + "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64": { + "Type": "String", + "Description": "S3 key for asset version \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + }, + "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2ArtifactHash720B9536": { + "Type": "String", + "Description": "Artifact hash for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + } + }, + "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-identity-source.handler/index.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.handler/index.ts new file mode 100644 index 0000000000000..e5e4e46974168 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.handler/index.ts @@ -0,0 +1,23 @@ +// tslint:disable:no-console + +export const handler = async (event: any, _context: any = {}): Promise => { + const httpMethod: string = event.requestContext.httpMethod; + console.log(`event.requestContext.httpMethod = ${httpMethod}`); + if (httpMethod === 'GET') { + return { + principalId: 'user', + policyDocument: { + Version: "2012-10-17", + Statement: [ + { + Action: "execute-api:Invoke", + Effect: 'Allow', + Resource: event.methodArn + } + ] + } + }; + } else { + throw new Error('Unauthorized'); + } +}; diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.ts new file mode 100644 index 0000000000000..ec5da348dbecf --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.ts @@ -0,0 +1,41 @@ +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-identity-source.handler')) +}); + +const restapi = new RestApi(stack, 'MyRestApi'); + +const authorizer = new RequestAuthorizer(stack, 'MyAuthorizer', { + handler: authorizerFn, + identitySource: ['method.request.header.Authorization', 'context.httpMethod'], +}); + +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/integ.request-authorizer.handler/index.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.handler/index.ts index 09f45034be73e..facd74cbb88ac 100644 --- 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 @@ -1,9 +1,8 @@ // 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') { + const arn = event.methodArn; + if (arn) { return { principalId: 'user', policyDocument: { @@ -11,8 +10,8 @@ export const handler = async (event: any, _context: any = {}): Promise => { Statement: [ { Action: "execute-api:Invoke", - Effect: authToken, - Resource: event.methodArn + Effect: 'Allow', + Resource: arn, } ] } From d4872bfdd38246a195f30abfd87499ed42446ddd Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Wed, 12 Feb 2020 20:33:29 -0600 Subject: [PATCH 05/10] fixup! fixup! feat(apigateway): add lambda request authorizer construct --- packages/@aws-cdk/aws-apigateway/package.json | 4 +- ....request-authorizer-iam-role.expected.json | 25 +++-- ...t-authorizer-identity-source.expected.json | 104 +++++++++--------- .../integ.request-authorizer.expected.json | 103 ++++++++--------- ...eg.token-authorizer-iam-role.expected.json | 5 +- .../integ.token-authorizer.expected.json | 85 +++++++------- 6 files changed, 167 insertions(+), 159 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index b5e8452e9fab4..3193d1102444a 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -292,7 +292,9 @@ "props-default-doc:@aws-cdk/aws-apigateway.MethodOptions.requestModels", "props-default-doc:@aws-cdk/aws-apigateway.MethodOptions.requestValidator", "docs-public-apis:@aws-cdk/aws-apigateway.ResourceBase.url", - "attribute-tag:@aws-cdk/aws-apigateway.LambdaAuthorizer.authorizerArn" + "attribute-tag:@aws-cdk/aws-apigateway.LambdaAuthorizer.authorizerArn", + "attribute-tag:@aws-cdk/aws-apigateway.RequestAuthorizer.authorizerArn", + "attribute-tag:@aws-cdk/aws-apigateway.TokenAuthorizer.authorizerArn" ] }, "stability": "stable" 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 index bcd0168e9a822..45774746bd0e6 100644 --- 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 @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA" + "Ref": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3BucketD3A1982E" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + "Ref": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + "Ref": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640" } ] } @@ -105,7 +105,6 @@ "MyAuthorizer6575980E": { "Type": "AWS::ApiGateway::Authorizer", "Properties": { - "Name": "RequestAuthorizerIAMRoleIntegMyAuthorizerB6939B6E", "RestApiId": { "Ref": "MyRestApi2D1F47A9" }, @@ -116,6 +115,7 @@ "Arn" ] }, + "AuthorizerResultTtlInSeconds": 0, "AuthorizerUri": { "Fn::Join": [ "", @@ -134,7 +134,8 @@ "/invocations" ] ] - } + }, + "Name": "RequestAuthorizerIAMRoleIntegMyAuthorizerB6939B6E" } }, "MyAuthorizerauthorizerInvokePolicy0F88B8E1": { @@ -276,17 +277,17 @@ } }, "Parameters": { - "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA": { + "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3BucketD3A1982E": { "Type": "String", - "Description": "S3 bucket for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + "Description": "S3 bucket for asset \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" }, - "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64": { + "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640": { "Type": "String", - "Description": "S3 key for asset version \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + "Description": "S3 key for asset version \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" }, - "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2ArtifactHash720B9536": { + "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dArtifactHashB5F2CA57": { "Type": "String", - "Description": "Artifact hash for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + "Description": "Artifact hash for asset \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" } }, "Outputs": { @@ -317,4 +318,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.expected.json index e23eb908e1043..10d27e8797037 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA" + "Ref": "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38S3Bucket9552A729" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + "Ref": "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38S3VersionKey00605ABB" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + "Ref": "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38S3VersionKey00605ABB" } ] } @@ -85,6 +85,46 @@ "MyAuthorizerFunctionServiceRole8A34C19E" ] }, + "MyAuthorizerFunctionRequestAuthorizerIntegMyAuthorizer5D9D41C5PermissionsCB8B246E": { + "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" + } + ] + ] + } + } + }, "MyRestApi2D1F47A9": { "Type": "AWS::ApiGateway::RestApi", "Properties": { @@ -199,11 +239,11 @@ "MyAuthorizer6575980E": { "Type": "AWS::ApiGateway::Authorizer", "Properties": { - "Name": "RequestAuthorizerIntegMyAuthorizer5D9D41C5", "RestApiId": { "Ref": "MyRestApi2D1F47A9" }, "Type": "REQUEST", + "AuthorizerResultTtlInSeconds": 0, "AuthorizerUri": { "Fn::Join": [ "", @@ -222,62 +262,24 @@ "/invocations" ] ] - } - } - }, - "MyAuthorizerFunctionRequestAuthorizerIntegMyAuthorizer5D9D41C5PermissionsCB8B246E": { - "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" - } - ] - ] - } + "IdentitySource": "method.request.header.Authorization,context.httpMethod", + "Name": "RequestAuthorizerIntegMyAuthorizer5D9D41C5" } } }, "Parameters": { - "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA": { + "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38S3Bucket9552A729": { "Type": "String", - "Description": "S3 bucket for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + "Description": "S3 bucket for asset \"5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38\"" }, - "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64": { + "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38S3VersionKey00605ABB": { "Type": "String", - "Description": "S3 key for asset version \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + "Description": "S3 key for asset version \"5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38\"" }, - "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2ArtifactHash720B9536": { + "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38ArtifactHash0448E5F4": { "Type": "String", - "Description": "Artifact hash for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + "Description": "Artifact hash for asset \"5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38\"" } }, "Outputs": { @@ -308,4 +310,4 @@ } } } -} +} \ No newline at end of file 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 index e23eb908e1043..be809f1068b5d 100644 --- 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 @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA" + "Ref": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3BucketD3A1982E" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + "Ref": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64" + "Ref": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640" } ] } @@ -85,6 +85,46 @@ "MyAuthorizerFunctionServiceRole8A34C19E" ] }, + "MyAuthorizerFunctionRequestAuthorizerIntegMyAuthorizer5D9D41C5PermissionsCB8B246E": { + "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" + } + ] + ] + } + } + }, "MyRestApi2D1F47A9": { "Type": "AWS::ApiGateway::RestApi", "Properties": { @@ -199,11 +239,11 @@ "MyAuthorizer6575980E": { "Type": "AWS::ApiGateway::Authorizer", "Properties": { - "Name": "RequestAuthorizerIntegMyAuthorizer5D9D41C5", "RestApiId": { "Ref": "MyRestApi2D1F47A9" }, "Type": "REQUEST", + "AuthorizerResultTtlInSeconds": 0, "AuthorizerUri": { "Fn::Join": [ "", @@ -222,62 +262,23 @@ "/invocations" ] ] - } - } - }, - "MyAuthorizerFunctionRequestAuthorizerIntegMyAuthorizer5D9D41C5PermissionsCB8B246E": { - "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" - } - ] - ] - } + "Name": "RequestAuthorizerIntegMyAuthorizer5D9D41C5" } } }, "Parameters": { - "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3Bucket83BA7EBA": { + "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3BucketD3A1982E": { "Type": "String", - "Description": "S3 bucket for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + "Description": "S3 bucket for asset \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" }, - "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2S3VersionKey42A4CA64": { + "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640": { "Type": "String", - "Description": "S3 key for asset version \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + "Description": "S3 key for asset version \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" }, - "AssetParameters453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2ArtifactHash720B9536": { + "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dArtifactHashB5F2CA57": { "Type": "String", - "Description": "Artifact hash for asset \"453d46b77465388a67f88c982f471ac19fe598a8f69714cce7b78eda373ddec2\"" + "Description": "Artifact hash for asset \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" } }, "Outputs": { @@ -308,4 +309,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json index d2c26cd1de4ba..e952976cf653d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json @@ -105,7 +105,6 @@ "MyAuthorizer6575980E": { "Type": "AWS::ApiGateway::Authorizer", "Properties": { - "Name": "TokenAuthorizerIAMRoleIntegMyAuthorizer1DFDE3B5", "RestApiId": { "Ref": "MyRestApi2D1F47A9" }, @@ -116,6 +115,7 @@ "Arn" ] }, + "AuthorizerResultTtlInSeconds": 0, "AuthorizerUri": { "Fn::Join": [ "", @@ -135,7 +135,8 @@ ] ] }, - "IdentitySource": "method.request.header.Authorization" + "IdentitySource": "method.request.header.Authorization", + "Name": "TokenAuthorizerIAMRoleIntegMyAuthorizer1DFDE3B5" } }, "MyAuthorizerauthorizerInvokePolicy0F88B8E1": { diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json index 60c98adf6e1d2..f29ddb2159ce1 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json @@ -85,6 +85,46 @@ "MyAuthorizerFunctionServiceRole8A34C19E" ] }, + "MyAuthorizerFunctionTokenAuthorizerIntegMyAuthorizer793B1D5FPermissions7557AE26": { + "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" + } + ] + ] + } + } + }, "MyRestApi2D1F47A9": { "Type": "AWS::ApiGateway::RestApi", "Properties": { @@ -199,11 +239,11 @@ "MyAuthorizer6575980E": { "Type": "AWS::ApiGateway::Authorizer", "Properties": { - "Name": "TokenAuthorizerIntegMyAuthorizer793B1D5F", "RestApiId": { "Ref": "MyRestApi2D1F47A9" }, "Type": "TOKEN", + "AuthorizerResultTtlInSeconds": 0, "AuthorizerUri": { "Fn::Join": [ "", @@ -223,47 +263,8 @@ ] ] }, - "IdentitySource": "method.request.header.Authorization" - } - }, - "MyAuthorizerFunctionTokenAuthorizerIntegMyAuthorizer793B1D5FPermissions7557AE26": { - "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" - } - ] - ] - } + "IdentitySource": "method.request.header.Authorization", + "Name": "TokenAuthorizerIntegMyAuthorizer793B1D5F" } } }, From 8792b5a51fe2d5e3d6519f913ea422d23f9ee5ae Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Wed, 12 Feb 2020 21:41:53 -0600 Subject: [PATCH 06/10] fixup! feat(apigateway): add lambda request authorizer construct --- packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts | 2 +- .../aws-apigateway/test/integ.restapi.books.expected.json | 2 +- .../aws-apigateway/test/integ.restapi.multistack.expected.json | 2 +- .../aws-apigateway/test/integ.restapi.multiuse.expected.json | 2 +- .../test/integ.usage-plan.multikey.expected.json | 3 +-- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index 4057d8bcb2f3c..855db4231d8d9 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -190,7 +190,7 @@ export class RequestAuthorizer extends LambdaAuthorizer { 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.`); + throw new Error(`At least one Identity Source is required for a REQUEST-based Lambda authorizer if caching is enabled.`); } } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index 55bbd73b8a143..b4a0de0c99ce1 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -880,4 +880,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json index 1cdcd9201dd7e..89fc935988bf7 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json @@ -319,4 +319,4 @@ } } } -] +] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json index 3ada6b4b13276..c90afb57e13c3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json @@ -523,4 +523,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json index 669c3c14f0c20..8e761f40e2a26 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json @@ -1,8 +1,7 @@ { "Resources": { "myusageplan4B391740": { - "Type": "AWS::ApiGateway::UsagePlan", - "Properties": {} + "Type": "AWS::ApiGateway::UsagePlan" }, "myusageplanUsagePlanKeyResource095B4EA9": { "Type": "AWS::ApiGateway::UsagePlanKey", From 9fc3c582b7e5674a7215b11eecee0caa7cbd4a39 Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Mon, 17 Feb 2020 20:38:03 -0600 Subject: [PATCH 07/10] fixup! feat(apigateway): add lambda request authorizer construct --- packages/@aws-cdk/aws-apigateway/README.md | 1 + .../lib/authorizers/identity-source.ts | 76 +++++ .../aws-apigateway/lib/authorizers/index.ts | 3 +- .../aws-apigateway/lib/authorizers/lambda.ts | 118 ++++--- packages/@aws-cdk/aws-apigateway/package.json | 4 + ....request-authorizer-iam-role.expected.json | 321 ------------------ .../integ.request-authorizer-iam-role.ts | 47 --- ...t-authorizer-identity-source.expected.json | 313 ----------------- .../index.ts | 23 -- ...nteg.request-authorizer-identity-source.ts | 41 --- .../integ.request-authorizer.expected.json | 21 +- .../integ.request-authorizer.handler/index.ts | 11 +- .../authorizers/integ.request-authorizer.ts | 6 +- ...eg.token-authorizer-iam-role.expected.json | 20 +- .../integ.token-authorizer.expected.json | 20 +- .../integ.token-authorizer.handler/index.ts | 2 +- .../test/authorizers/test.identity-source.ts | 33 ++ .../test/authorizers/test.lambda.ts | 11 +- 18 files changed, 239 insertions(+), 832 deletions(-) create mode 100644 packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts delete mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.expected.json delete mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.ts delete mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.expected.json delete mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.handler/index.ts delete mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.ts create mode 100644 packages/@aws-cdk/aws-apigateway/test/authorizers/test.identity-source.ts diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 4471f18aec291..b7537d82c63fb 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -421,6 +421,7 @@ const authFn = new lambda.Function(this, 'booksAuthorizerLambda', { const auth = new apigateway.RequestAuthorizer(this, 'booksAuthorizer', { handler: authFn, + identitySources: [IdentitySource.header('Authorization')] }); books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts new file mode 100644 index 0000000000000..c36e54fa6326e --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts @@ -0,0 +1,76 @@ +import { Token } from '@aws-cdk/core'; + +/** + * Represents an identity source. + * + * The source can be specified either as a literal value (e.g: `Auth`) which + * cannot be blank, or as an unresolved string token. + */ +export class IdentitySource { + /** + * @param headerName the name of the header the `IdentitySource` will represent. + * @returns a new `IdentitySource` representing `headerName` from the request. + */ + public static header(headerName: string): IdentitySource { + return new IdentitySource(headerName, SourceType.Header); + } + + /** + * @param queryString the name of the query string the `IdentitySource` will represent. + * @returns a new `IdentitySource` representing `queryString` from the request. + */ + public static queryString(queryString: string): IdentitySource { + return new IdentitySource(queryString, SourceType.QueryString); + } + + /** + * @param stageVariable the name of the stage variable the `IdentitySource` will represent. + * @returns a new `IdentitySource` representing `stageVariable` from the API Gateway. + */ + public static stageVariable(stageVariable: string): IdentitySource { + return new IdentitySource(stageVariable, SourceType.StageVariable); + } + + /** + * @param context the name of the context variable the `IdentitySource` will represent. + * @returns a new `IdentitySource` representing `context` from the request context. + */ + public static context(context: string): IdentitySource { + return new IdentitySource(context, SourceType.Context); + } + + private readonly source: string; + private readonly type: SourceType; + + private constructor(source: string, type: SourceType) { + if (!Token.isUnresolved(source) && source === '') { + throw new Error(`IdentitySources cannot be empty. Received: ${source}`); + } + + this.source = source; + this.type = type; + } + + /** + * Returns a string representation of this `IdentitySource` that is also a Token that cannot be successfully resolved. This + * protects users against inadvertently stringifying an `IdentitySource` object, when they should have called one of the + * `to*` methods instead. + */ + public toString() { + return `${this.type.toString()}.${this.source}`; + } +} + +class SourceType { + public static readonly Header = new SourceType('method.request.header'); + public static readonly QueryString = new SourceType('method.request.querystring'); + public static readonly StageVariable = new SourceType('stageVariables'); + public static readonly Context = new SourceType('context'); + + private constructor(public readonly label: string) { + } + + public toString() { + return this.label; + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts index 338a17e47cf8c..57289c931f760 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts @@ -1 +1,2 @@ -export * from './lambda'; \ No newline at end of file +export * from './lambda'; +export * from './identity-source'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index 855db4231d8d9..ed36a604cecd3 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -4,6 +4,7 @@ import { Construct, Duration, Lazy, Stack } from '@aws-cdk/core'; import { CfnAuthorizer } from '../apigateway.generated'; import { Authorizer, IAuthorizer } from '../authorizer'; import { RestApi } from '../restapi'; +import { IdentitySource } from './identity-source'; /** * Base properties for all lambda authorizers @@ -49,47 +50,34 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer { * The id of the authorizer. * @attribute */ - public readonly authorizerId: string; + public abstract readonly authorizerId: string; /** * The ARN of the authorizer to be used in permission policies, such as IAM and resource-based grants. */ - public readonly authorizerArn: string; + public abstract readonly authorizerArn: string; + + /** + * The Lambda function handler that this authorizer uses. + */ + protected readonly handler: lambda.IFunction; + + /** + * The IAM role that the API Gateway service assumes while invoking the Lambda function. + */ + protected readonly role?: iam.IRole; protected restApiId?: string; protected constructor(scope: Construct, id: string, props: LambdaAuthorizerProps) { super(scope, id); + this.handler = props.handler; + this.role = props.assumeRole; + if (props.resultsCacheTtl && props.resultsCacheTtl.toSeconds() > 3600) { throw new Error(`Lambda authorizer property 'resultsCacheTtl' must not be greater than 3600 seconds (1 hour)`); } - - const restApiId = Lazy.stringValue({ produce: () => this.restApiId }); - - this.authorizerId = this.createResource(props).ref; - - this.authorizerArn = Stack.of(this).formatArn({ - service: 'execute-api', - resource: restApiId, - resourceName: `authorizers/${this.authorizerId}` - }); - - if (!props.assumeRole) { - props.handler.addPermission(`${this.node.uniqueId}:Permissions`, { - principal: new iam.ServicePrincipal('apigateway.amazonaws.com'), - sourceArn: this.authorizerArn - }); - } else if (props.assumeRole instanceof iam.Role) { // i.e., not imported - props.assumeRole.attachInlinePolicy(new iam.Policy(this, 'authorizerInvokePolicy', { - statements: [ - new iam.PolicyStatement({ - resources: [ props.handler.functionArn ], - actions: [ 'lambda:InvokeFunction' ], - }) - ] - })); - } } /** @@ -104,7 +92,26 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer { this.restApiId = restApi.restApiId; } - protected abstract createResource(props: T): CfnAuthorizer; + /** + * Sets up the permissions necessary for the API Gateway service to invoke the Lambda function. + */ + protected setupPermissions() { + if (!this.role) { + this.handler.addPermission(`${this.node.uniqueId}:Permissions`, { + principal: new iam.ServicePrincipal('apigateway.amazonaws.com'), + sourceArn: this.authorizerArn + }); + } else if (this.role instanceof iam.Role) { // i.e. not imported + this.role.attachInlinePolicy(new iam.Policy(this, 'authorizerInvokePolicy', { + statements: [ + new iam.PolicyStatement({ + resources: [ this.handler.functionArn ], + actions: [ 'lambda:InvokeFunction' ], + }) + ] + })); + } + } } /** @@ -123,7 +130,7 @@ export interface TokenAuthorizerProps extends LambdaAuthorizerProps { * 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' + * @default `IdentitySource.header('Authorization')` */ readonly identitySource?: string; } @@ -137,34 +144,44 @@ export interface TokenAuthorizerProps extends LambdaAuthorizerProps { */ export class TokenAuthorizer extends LambdaAuthorizer { + public readonly authorizerId: string; + + public readonly authorizerArn: string; + constructor(scope: Construct, id: string, props: TokenAuthorizerProps) { super(scope, id, props); - } - protected createResource(props: TokenAuthorizerProps): CfnAuthorizer { const restApiId = Lazy.stringValue({ produce: () => this.restApiId }); - - return new CfnAuthorizer(this, 'Resource', { + 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?.roleArn, - authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds() ?? 0, + authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds() ?? Duration.minutes(5).toSeconds(), identitySource: props.identitySource || 'method.request.header.Authorization', identityValidationExpression: props.validationRegex, }); + + this.authorizerId = resource.ref; + this.authorizerArn = Stack.of(this).formatArn({ + service: 'execute-api', + resource: restApiId, + resourceName: `authorizers/${this.authorizerId}` + }); + + this.setupPermissions(); } } /** - * Properties for RequestAuthorizerProps + * Properties for RequestAuthorizer */ export interface RequestAuthorizerProps extends LambdaAuthorizerProps { /** * An array of request header mapping expressions for identities. Supported parameter types are * Header, Query String, Stage Variable, and Context. For instance, extracting an authorization - * token from a header would use the identity source `method.request.header.Authorizer`. + * token from a header would use the identity source `IdentitySource.header('Authorizer')`. * * Note: API Gateway uses the specified identity sources as the request authorizer caching key. When caching is * enabled, API Gateway calls the authorizer's Lambda function only after successfully verifying that all the @@ -172,9 +189,8 @@ export interface RequestAuthorizerProps extends LambdaAuthorizerProps { * API Gateway returns a 401 Unauthorized response without calling the authorizer Lambda function. * * @see https://docs.aws.amazon.com/apigateway/api-reference/link-relation/authorizer-create/#identitySource - * @default no identity sources */ - readonly identitySource?: string[]; + readonly identitySources: IdentitySource[]; } /** @@ -186,25 +202,35 @@ export interface RequestAuthorizerProps extends LambdaAuthorizerProps { */ export class RequestAuthorizer extends LambdaAuthorizer { + public readonly authorizerId: string; + + public readonly authorizerArn: string; + constructor(scope: Construct, id: string, props: RequestAuthorizerProps) { super(scope, id, props); - if (props.resultsCacheTtl && props.identitySource?.length === 0) { + if (props.resultsCacheTtl?.toSeconds() !== 0 && props.identitySources.length === 0) { throw new Error(`At least one Identity Source is required for a REQUEST-based Lambda authorizer if caching is enabled.`); } - } - protected createResource(props: RequestAuthorizerProps): CfnAuthorizer { const restApiId = Lazy.stringValue({ produce: () => this.restApiId }); - - return new CfnAuthorizer(this, 'Resource', { + const resource = 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?.roleArn, - authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds() ?? 0, - identitySource: props.identitySource?.join(','), + authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds() ?? Duration.minutes(5).toSeconds(), + identitySource: props.identitySources.map(is => is.toString()).join(','), + }); + + this.authorizerId = resource.ref; + this.authorizerArn = Stack.of(this).formatArn({ + service: 'execute-api', + resource: restApiId, + resourceName: `authorizers/${this.authorizerId}` }); + + this.setupPermissions(); } } diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 3193d1102444a..24c6e50bd9e03 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -191,6 +191,10 @@ "docs-public-apis:@aws-cdk/aws-apigateway.DomainNameOptions", "docs-public-apis:@aws-cdk/aws-apigateway.DomainNameProps", "docs-public-apis:@aws-cdk/aws-apigateway.HttpIntegrationProps", + "docs-public-apis:@aws-cdk/aws-apigateway.IdentitySource.header", + "docs-public-apis:@aws-cdk/aws-apigateway.IdentitySource.queryString", + "docs-public-apis:@aws-cdk/aws-apigateway.IdentitySource.stageVariable", + "docs-public-apis:@aws-cdk/aws-apigateway.IdentitySource.context", "docs-public-apis:@aws-cdk/aws-apigateway.IDomainName", "docs-public-apis:@aws-cdk/aws-apigateway.IModel", "docs-public-apis:@aws-cdk/aws-apigateway.IRequestValidator", 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 deleted file mode 100644 index 45774746bd0e6..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.expected.json +++ /dev/null @@ -1,321 +0,0 @@ -{ - "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": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3BucketD3A1982E" - }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640" - } - ] - } - ] - } - ] - ] - } - }, - "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" - ] - }, - "AuthorizerResultTtlInSeconds": 0, - "AuthorizerUri": { - "Fn::Join": [ - "", - [ - "arn:aws:apigateway:", - { - "Ref": "AWS::Region" - }, - ":lambda:path/2015-03-31/functions/", - { - "Fn::GetAtt": [ - "MyAuthorizerFunction70F1223E", - "Arn" - ] - }, - "/invocations" - ] - ] - }, - "Name": "RequestAuthorizerIAMRoleIntegMyAuthorizerB6939B6E" - } - }, - "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": { - "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3BucketD3A1982E": { - "Type": "String", - "Description": "S3 bucket for asset \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" - }, - "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640": { - "Type": "String", - "Description": "S3 key for asset version \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" - }, - "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dArtifactHashB5F2CA57": { - "Type": "String", - "Description": "Artifact hash for asset \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" - } - }, - "Outputs": { - "MyRestApiEndpoint4C55E4CB": { - "Value": { - "Fn::Join": [ - "", - [ - "https://", - { - "Ref": "MyRestApi2D1F47A9" - }, - ".execute-api.", - { - "Ref": "AWS::Region" - }, - ".", - { - "Ref": "AWS::URLSuffix" - }, - "/", - { - "Ref": "MyRestApiDeploymentStageprodC33B8E5F" - }, - "/" - ] - ] - } - } - } -} \ No newline at end of file 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 deleted file mode 100644 index eba34bc4b6268..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-iam-role.ts +++ /dev/null @@ -1,47 +0,0 @@ -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-identity-source.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.expected.json deleted file mode 100644 index 10d27e8797037..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.expected.json +++ /dev/null @@ -1,313 +0,0 @@ -{ - "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": "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38S3Bucket9552A729" - }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38S3VersionKey00605ABB" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38S3VersionKey00605ABB" - } - ] - } - ] - } - ] - ] - } - }, - "Handler": "index.handler", - "Role": { - "Fn::GetAtt": [ - "MyAuthorizerFunctionServiceRole8A34C19E", - "Arn" - ] - }, - "Runtime": "nodejs10.x" - }, - "DependsOn": [ - "MyAuthorizerFunctionServiceRole8A34C19E" - ] - }, - "MyAuthorizerFunctionRequestAuthorizerIntegMyAuthorizer5D9D41C5PermissionsCB8B246E": { - "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" - } - ] - ] - } - } - }, - "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", - "AuthorizerResultTtlInSeconds": 0, - "AuthorizerUri": { - "Fn::Join": [ - "", - [ - "arn:aws:apigateway:", - { - "Ref": "AWS::Region" - }, - ":lambda:path/2015-03-31/functions/", - { - "Fn::GetAtt": [ - "MyAuthorizerFunction70F1223E", - "Arn" - ] - }, - "/invocations" - ] - ] - }, - "IdentitySource": "method.request.header.Authorization,context.httpMethod", - "Name": "RequestAuthorizerIntegMyAuthorizer5D9D41C5" - } - } - }, - "Parameters": { - "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38S3Bucket9552A729": { - "Type": "String", - "Description": "S3 bucket for asset \"5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38\"" - }, - "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38S3VersionKey00605ABB": { - "Type": "String", - "Description": "S3 key for asset version \"5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38\"" - }, - "AssetParameters5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38ArtifactHash0448E5F4": { - "Type": "String", - "Description": "Artifact hash for asset \"5a543cea1a35367655ad84081b066979a2ecfe53165fd939ed3aa67807814c38\"" - } - }, - "Outputs": { - "MyRestApiEndpoint4C55E4CB": { - "Value": { - "Fn::Join": [ - "", - [ - "https://", - { - "Ref": "MyRestApi2D1F47A9" - }, - ".execute-api.", - { - "Ref": "AWS::Region" - }, - ".", - { - "Ref": "AWS::URLSuffix" - }, - "/", - { - "Ref": "MyRestApiDeploymentStageprodC33B8E5F" - }, - "/" - ] - ] - } - } - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.handler/index.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.handler/index.ts deleted file mode 100644 index e5e4e46974168..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.handler/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -// tslint:disable:no-console - -export const handler = async (event: any, _context: any = {}): Promise => { - const httpMethod: string = event.requestContext.httpMethod; - console.log(`event.requestContext.httpMethod = ${httpMethod}`); - if (httpMethod === 'GET') { - return { - principalId: 'user', - policyDocument: { - Version: "2012-10-17", - Statement: [ - { - Action: "execute-api:Invoke", - Effect: 'Allow', - Resource: event.methodArn - } - ] - } - }; - } else { - throw new Error('Unauthorized'); - } -}; diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.ts deleted file mode 100644 index ec5da348dbecf..0000000000000 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer-identity-source.ts +++ /dev/null @@ -1,41 +0,0 @@ -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-identity-source.handler')) -}); - -const restapi = new RestApi(stack, 'MyRestApi'); - -const authorizer = new RequestAuthorizer(stack, 'MyAuthorizer', { - handler: authorizerFn, - identitySource: ['method.request.header.Authorization', 'context.httpMethod'], -}); - -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/integ.request-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json index be809f1068b5d..286d326a779a5 100644 --- 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 @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3BucketD3A1982E" + "Ref": "AssetParameters1aae6dca4c69aacfac5abba2d62571e892f6748d8d14b11a197f80159ab832f4S3BucketC6C6A669" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640" + "Ref": "AssetParameters1aae6dca4c69aacfac5abba2d62571e892f6748d8d14b11a197f80159ab832f4S3VersionKeyBB6680D1" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640" + "Ref": "AssetParameters1aae6dca4c69aacfac5abba2d62571e892f6748d8d14b11a197f80159ab832f4S3VersionKeyBB6680D1" } ] } @@ -243,7 +243,7 @@ "Ref": "MyRestApi2D1F47A9" }, "Type": "REQUEST", - "AuthorizerResultTtlInSeconds": 0, + "AuthorizerResultTtlInSeconds": 300, "AuthorizerUri": { "Fn::Join": [ "", @@ -263,22 +263,23 @@ ] ] }, + "IdentitySource": "method.request.header.Authorization,method.request.querystring.allow", "Name": "RequestAuthorizerIntegMyAuthorizer5D9D41C5" } } }, "Parameters": { - "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3BucketD3A1982E": { + "AssetParameters1aae6dca4c69aacfac5abba2d62571e892f6748d8d14b11a197f80159ab832f4S3BucketC6C6A669": { "Type": "String", - "Description": "S3 bucket for asset \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" + "Description": "S3 bucket for asset \"1aae6dca4c69aacfac5abba2d62571e892f6748d8d14b11a197f80159ab832f4\"" }, - "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dS3VersionKey13BDD640": { + "AssetParameters1aae6dca4c69aacfac5abba2d62571e892f6748d8d14b11a197f80159ab832f4S3VersionKeyBB6680D1": { "Type": "String", - "Description": "S3 key for asset version \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" + "Description": "S3 key for asset version \"1aae6dca4c69aacfac5abba2d62571e892f6748d8d14b11a197f80159ab832f4\"" }, - "AssetParameters2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404dArtifactHashB5F2CA57": { + "AssetParameters1aae6dca4c69aacfac5abba2d62571e892f6748d8d14b11a197f80159ab832f4ArtifactHashA6CE14CB": { "Type": "String", - "Description": "Artifact hash for asset \"2ac36271139d2830e25eccfc0ead9624c5a16d74320c596cbd53df0ed302404d\"" + "Description": "Artifact hash for asset \"1aae6dca4c69aacfac5abba2d62571e892f6748d8d14b11a197f80159ab832f4\"" } }, "Outputs": { 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 index facd74cbb88ac..1f015b531e5aa 100644 --- 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 @@ -1,8 +1,11 @@ // tslint:disable:no-console export const handler = async (event: any, _context: any = {}): Promise => { - const arn = event.methodArn; - if (arn) { + const authToken: string = event.headers.Authorization; + const authQueryString: string = event.queryStringParameters.allow; + console.log(`event.headers.Authorization = ${authToken}`); + console.log(`event.queryStringParameters.allow = ${authQueryString}`); + if ((authToken === 'allow' || authToken === 'deny') && authQueryString === 'yes') { return { principalId: 'user', policyDocument: { @@ -10,8 +13,8 @@ export const handler = async (event: any, _context: any = {}): Promise => { Statement: [ { Action: "execute-api:Invoke", - Effect: 'Allow', - Resource: arn, + Effect: authToken, + Resource: event.methodArn } ] } 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 index 29bec4e47f957..b08cfb2e684bd 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.ts @@ -3,11 +3,12 @@ import { App, Stack } from '@aws-cdk/core'; import * as path from 'path'; import { MockIntegration, PassthroughBehavior, RestApi } from '../../lib'; import {RequestAuthorizer} from '../../lib/authorizers'; +import {IdentitySource} from '../../lib/authorizers/identity-source'; // 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 +// `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: deny' ?allow=yes` should return 403 +// `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: allow' ?allow=yes` should return 200 const app = new App(); const stack = new Stack(app, 'RequestAuthorizerInteg'); @@ -22,6 +23,7 @@ const restapi = new RestApi(stack, 'MyRestApi'); const authorizer = new RequestAuthorizer(stack, 'MyAuthorizer', { handler: authorizerFn, + identitySources: [IdentitySource.header('Authorization'), IdentitySource.queryString('allow')], }); restapi.root.addMethod('ANY', new MockIntegration({ diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json index e952976cf653d..c353f68edf133 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4" + "Ref": "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddS3Bucket7D65DBAC" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E" + "Ref": "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddS3VersionKey97EAAAFF" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E" + "Ref": "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddS3VersionKey97EAAAFF" } ] } @@ -115,7 +115,7 @@ "Arn" ] }, - "AuthorizerResultTtlInSeconds": 0, + "AuthorizerResultTtlInSeconds": 300, "AuthorizerUri": { "Fn::Join": [ "", @@ -278,17 +278,17 @@ } }, "Parameters": { - "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4": { + "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddS3Bucket7D65DBAC": { "Type": "String", - "Description": "S3 bucket for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + "Description": "S3 bucket for asset \"bb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744dd\"" }, - "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E": { + "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddS3VersionKey97EAAAFF": { "Type": "String", - "Description": "S3 key for asset version \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + "Description": "S3 key for asset version \"bb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744dd\"" }, - "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aArtifactHash1A0BBA4E": { + "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddArtifactHashCA306BF0": { "Type": "String", - "Description": "Artifact hash for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + "Description": "Artifact hash for asset \"bb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744dd\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json index f29ddb2159ce1..04b666e6e58f3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4" + "Ref": "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddS3Bucket7D65DBAC" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E" + "Ref": "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddS3VersionKey97EAAAFF" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E" + "Ref": "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddS3VersionKey97EAAAFF" } ] } @@ -243,7 +243,7 @@ "Ref": "MyRestApi2D1F47A9" }, "Type": "TOKEN", - "AuthorizerResultTtlInSeconds": 0, + "AuthorizerResultTtlInSeconds": 300, "AuthorizerUri": { "Fn::Join": [ "", @@ -269,17 +269,17 @@ } }, "Parameters": { - "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3Bucket115F9EA4": { + "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddS3Bucket7D65DBAC": { "Type": "String", - "Description": "S3 bucket for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + "Description": "S3 bucket for asset \"bb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744dd\"" }, - "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aS3VersionKey1039487E": { + "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddS3VersionKey97EAAAFF": { "Type": "String", - "Description": "S3 key for asset version \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + "Description": "S3 key for asset version \"bb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744dd\"" }, - "AssetParameters6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993aArtifactHash1A0BBA4E": { + "AssetParametersbb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744ddArtifactHashCA306BF0": { "Type": "String", - "Description": "Artifact hash for asset \"6695c4a3dad80ddeef2797a1729306b7c136d67ce21d2187fdbc7bbad009993a\"" + "Description": "Artifact hash for asset \"bb857fcbb64420f49c6bead2b96595eacd5fb9f9d8ea0301a3617a0091c744dd\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.handler/index.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.handler/index.ts index f1ad21bc7b09a..86292e971d30c 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.handler/index.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.handler/index.ts @@ -20,4 +20,4 @@ export const handler = async (event: any, _context: any = {}): Promise => { } else { throw new Error('Unauthorized'); } -}; \ No newline at end of file +}; diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/test.identity-source.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.identity-source.ts new file mode 100644 index 0000000000000..65c589fd7d53a --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.identity-source.ts @@ -0,0 +1,33 @@ +import * as nodeunit from 'nodeunit'; +import { IdentitySource } from '../../lib'; + +export = nodeunit.testCase({ + 'blank amount'(test: nodeunit.Test) { + test.throws(() => IdentitySource.context(''), /empty/); + test.done(); + }, + + 'IdentitySource header'(test: nodeunit.Test) { + const identitySource = IdentitySource.header('Authorization'); + test.equal(identitySource.toString(), 'method.request.header.Authorization'); + test.done(); + }, + + 'IdentitySource queryString'(test: nodeunit.Test) { + const identitySource = IdentitySource.queryString('param'); + test.equal(identitySource.toString(), 'method.request.querystring.param'); + test.done(); + }, + + 'IdentitySource stageVariable'(test: nodeunit.Test) { + const identitySource = IdentitySource.stageVariable('var1'); + test.equal(identitySource.toString(), 'stageVariables.var1'); + test.done(); + }, + + 'IdentitySource context'(test: nodeunit.Test) { + const identitySource = IdentitySource.context('httpMethod'); + test.equal(identitySource.toString(), 'context.httpMethod'); + test.done(); + }, +}); 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 c1a1a6b04006d..96428eb675dc2 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts @@ -5,6 +5,7 @@ import { Duration, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import { AuthorizationType, RestApi, TokenAuthorizer } from '../../lib'; import {RequestAuthorizer} from '../../lib/authorizers'; +import {IdentitySource} from '../../lib/authorizers/identity-source'; export = { 'default token authorizer'(test: Test) { @@ -52,7 +53,9 @@ export = { }); const auth = new RequestAuthorizer(stack, 'myauthorizer', { - handler: func + handler: func, + resultsCacheTtl: Duration.seconds(0), + identitySources: [], }); const restApi = new RestApi(stack, 'myrestapi'); @@ -122,7 +125,7 @@ export = { const auth = new RequestAuthorizer(stack, 'myauthorizer', { handler: func, - identitySource: ['method.request.header.whoami'], + identitySources: [IdentitySource.header('whoami')], authorizerName: 'myauthorizer', resultsCacheTtl: Duration.minutes(1), }); @@ -212,7 +215,9 @@ export = { const auth = new RequestAuthorizer(stack, 'myauthorizer', { handler: func, - assumeRole: role + assumeRole: role, + resultsCacheTtl: Duration.seconds(0), + identitySources: [] }); const restApi = new RestApi(stack, 'myrestapi'); From 7dcdd41fe1e1ec376b82e45d25712cf24328a8b7 Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Wed, 19 Feb 2020 21:58:35 -0600 Subject: [PATCH 08/10] fixup! feat(apigateway): add lambda request authorizer construct --- .../aws-apigateway/lib/authorizers/lambda.ts | 8 +++---- .../integ.request-authorizer.expected.json | 1 - ...eg.token-authorizer-iam-role.expected.json | 1 - .../integ.token-authorizer.expected.json | 1 - .../test/authorizers/test.lambda.ts | 22 ++++++++++++++++--- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index ed36a604cecd3..d984ca6d320fc 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -75,7 +75,7 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer { this.handler = props.handler; this.role = props.assumeRole; - if (props.resultsCacheTtl && props.resultsCacheTtl.toSeconds() > 3600) { + if (props.resultsCacheTtl && props.resultsCacheTtl?.toSeconds() > 3600) { throw new Error(`Lambda authorizer property 'resultsCacheTtl' must not be greater than 3600 seconds (1 hour)`); } } @@ -158,7 +158,7 @@ export class TokenAuthorizer extends LambdaAuthorizer { type: 'TOKEN', authorizerUri: `arn:aws:apigateway:${Stack.of(this).region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`, authorizerCredentials: props.assumeRole?.roleArn, - authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds() ?? Duration.minutes(5).toSeconds(), + authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds(), identitySource: props.identitySource || 'method.request.header.Authorization', identityValidationExpression: props.validationRegex, }); @@ -209,7 +209,7 @@ export class RequestAuthorizer extends LambdaAuthorizer { constructor(scope: Construct, id: string, props: RequestAuthorizerProps) { super(scope, id, props); - if (props.resultsCacheTtl?.toSeconds() !== 0 && props.identitySources.length === 0) { + if ((props.resultsCacheTtl === undefined || props.resultsCacheTtl.toSeconds() !== 0) && props.identitySources.length === 0) { throw new Error(`At least one Identity Source is required for a REQUEST-based Lambda authorizer if caching is enabled.`); } @@ -220,7 +220,7 @@ export class RequestAuthorizer extends LambdaAuthorizer { type: 'REQUEST', authorizerUri: `arn:aws:apigateway:${Stack.of(this).region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`, authorizerCredentials: props.assumeRole?.roleArn, - authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds() ?? Duration.minutes(5).toSeconds(), + authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds(), identitySource: props.identitySources.map(is => is.toString()).join(','), }); 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 index 286d326a779a5..692af2d9f0847 100644 --- 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 @@ -243,7 +243,6 @@ "Ref": "MyRestApi2D1F47A9" }, "Type": "REQUEST", - "AuthorizerResultTtlInSeconds": 300, "AuthorizerUri": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json index c353f68edf133..2d30983eaf02f 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json @@ -115,7 +115,6 @@ "Arn" ] }, - "AuthorizerResultTtlInSeconds": 300, "AuthorizerUri": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json index 04b666e6e58f3..8107871d15f64 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json @@ -243,7 +243,6 @@ "Ref": "MyRestApi2D1F47A9" }, "Type": "TOKEN", - "AuthorizerResultTtlInSeconds": 300, "AuthorizerUri": { "Fn::Join": [ "", 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 96428eb675dc2..92a476a7ed2e5 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts @@ -3,9 +3,7 @@ import * as iam from '@aws-cdk/aws-iam'; 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'; -import {IdentitySource} from '../../lib/authorizers/identity-source'; +import { AuthorizationType, IdentitySource, RequestAuthorizer, RestApi, TokenAuthorizer } from '../../lib'; export = { 'default token authorizer'(test: Test) { @@ -79,6 +77,24 @@ export = { test.done(); }, + 'invalid request authorizer config'(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, + }); + + test.throws(() => new RequestAuthorizer(stack, 'myauthorizer', { + handler: func, + resultsCacheTtl: Duration.seconds(1), + identitySources: [], + }), Error, 'At least one Identity Source is required for a REQUEST-based Lambda authorizer if caching is enabled.'); + + test.done(); + }, + 'token authorizer with all parameters specified'(test: Test) { const stack = new Stack(); From af65fd656ab9984a6880596ddc4abe4fc75cbf74 Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Thu, 20 Feb 2020 12:42:54 -0600 Subject: [PATCH 09/10] fixup! feat(apigateway): add lambda request authorizer construct --- .../lib/authorizers/identity-source.ts | 39 +++---------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts index c36e54fa6326e..f2606bcca40ec 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts @@ -12,7 +12,7 @@ export class IdentitySource { * @returns a new `IdentitySource` representing `headerName` from the request. */ public static header(headerName: string): IdentitySource { - return new IdentitySource(headerName, SourceType.Header); + return IdentitySource.toString(headerName, 'method.request.header'); } /** @@ -20,7 +20,7 @@ export class IdentitySource { * @returns a new `IdentitySource` representing `queryString` from the request. */ public static queryString(queryString: string): IdentitySource { - return new IdentitySource(queryString, SourceType.QueryString); + return IdentitySource.toString(queryString, 'method.request.querystring'); } /** @@ -28,7 +28,7 @@ export class IdentitySource { * @returns a new `IdentitySource` representing `stageVariable` from the API Gateway. */ public static stageVariable(stageVariable: string): IdentitySource { - return new IdentitySource(stageVariable, SourceType.StageVariable); + return IdentitySource.toString(stageVariable, 'stageVariables'); } /** @@ -36,41 +36,14 @@ export class IdentitySource { * @returns a new `IdentitySource` representing `context` from the request context. */ public static context(context: string): IdentitySource { - return new IdentitySource(context, SourceType.Context); + return IdentitySource.toString(context, 'context'); } - private readonly source: string; - private readonly type: SourceType; - - private constructor(source: string, type: SourceType) { + private static toString(source: string, type: string) { if (!Token.isUnresolved(source) && source === '') { throw new Error(`IdentitySources cannot be empty. Received: ${source}`); } - this.source = source; - this.type = type; - } - - /** - * Returns a string representation of this `IdentitySource` that is also a Token that cannot be successfully resolved. This - * protects users against inadvertently stringifying an `IdentitySource` object, when they should have called one of the - * `to*` methods instead. - */ - public toString() { - return `${this.type.toString()}.${this.source}`; - } -} - -class SourceType { - public static readonly Header = new SourceType('method.request.header'); - public static readonly QueryString = new SourceType('method.request.querystring'); - public static readonly StageVariable = new SourceType('stageVariables'); - public static readonly Context = new SourceType('context'); - - private constructor(public readonly label: string) { - } - - public toString() { - return this.label; + return `${type}.${source}`; } } From ddbf5656a8a28cfcce1d2a0724df80d45874b5b2 Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Fri, 21 Feb 2020 14:23:16 -0500 Subject: [PATCH 10/10] fixup! feat(apigateway): add lambda request authorizer construct --- .../lib/authorizers/identity-source.ts | 30 +++++++++++-------- packages/@aws-cdk/aws-apigateway/package.json | 4 --- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts index f2606bcca40ec..d4ecc739da38d 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/identity-source.ts @@ -1,5 +1,3 @@ -import { Token } from '@aws-cdk/core'; - /** * Represents an identity source. * @@ -8,40 +6,48 @@ import { Token } from '@aws-cdk/core'; */ export class IdentitySource { /** + * Provides a properly formatted header identity source. * @param headerName the name of the header the `IdentitySource` will represent. - * @returns a new `IdentitySource` representing `headerName` from the request. + * + * @returns a header identity source. */ - public static header(headerName: string): IdentitySource { + public static header(headerName: string): string { return IdentitySource.toString(headerName, 'method.request.header'); } /** + * Provides a properly formatted query string identity source. * @param queryString the name of the query string the `IdentitySource` will represent. - * @returns a new `IdentitySource` representing `queryString` from the request. + * + * @returns a query string identity source. */ - public static queryString(queryString: string): IdentitySource { + public static queryString(queryString: string): string { return IdentitySource.toString(queryString, 'method.request.querystring'); } /** + * Provides a properly formatted API Gateway stage variable identity source. * @param stageVariable the name of the stage variable the `IdentitySource` will represent. - * @returns a new `IdentitySource` representing `stageVariable` from the API Gateway. + * + * @returns an API Gateway stage variable identity source. */ - public static stageVariable(stageVariable: string): IdentitySource { + public static stageVariable(stageVariable: string): string { return IdentitySource.toString(stageVariable, 'stageVariables'); } /** + * Provides a properly formatted request context identity source. * @param context the name of the context variable the `IdentitySource` will represent. - * @returns a new `IdentitySource` representing `context` from the request context. + * + * @returns a request context identity source. */ - public static context(context: string): IdentitySource { + public static context(context: string): string { return IdentitySource.toString(context, 'context'); } private static toString(source: string, type: string) { - if (!Token.isUnresolved(source) && source === '') { - throw new Error(`IdentitySources cannot be empty. Received: ${source}`); + if (!source.trim()) { + throw new Error(`IdentitySources must be a non-empty string.`); } return `${type}.${source}`; diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 24c6e50bd9e03..3193d1102444a 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -191,10 +191,6 @@ "docs-public-apis:@aws-cdk/aws-apigateway.DomainNameOptions", "docs-public-apis:@aws-cdk/aws-apigateway.DomainNameProps", "docs-public-apis:@aws-cdk/aws-apigateway.HttpIntegrationProps", - "docs-public-apis:@aws-cdk/aws-apigateway.IdentitySource.header", - "docs-public-apis:@aws-cdk/aws-apigateway.IdentitySource.queryString", - "docs-public-apis:@aws-cdk/aws-apigateway.IdentitySource.stageVariable", - "docs-public-apis:@aws-cdk/aws-apigateway.IdentitySource.context", "docs-public-apis:@aws-cdk/aws-apigateway.IDomainName", "docs-public-apis:@aws-cdk/aws-apigateway.IModel", "docs-public-apis:@aws-cdk/aws-apigateway.IRequestValidator",