From ffe98638de6c9cfe40eabc22980501c8943d5534 Mon Sep 17 00:00:00 2001 From: Ike Nefcy Date: Fri, 31 Jan 2025 02:00:31 -0700 Subject: [PATCH] fix(apigatewayv2): incorrect arn function causing unwanted behavior (#33100) fixes: #33218 ### Reason for this change In websocket APIs in `aws-apigatewayv2`, the function arnForExecuteApi has essentially the same exact functionality as a REST API, which is not appropriate for Websockets which are fundamentally different. The way I found this issue was I used arnForExecuteApi to put the arn of the api into an IAM Role. The reason for this was because I was trying to use an IAM authorizer, which from a React standpoint meant signing iam credentials from my Cognito id pool using Amplify lib. When doing this I used arnForExecuteApi from CDK to write the policy, I did not include any arguments, just the default. The issue was that this was not working. I spent time diving deep on the issue in case it was the method in which I was signing the credentials, since I was not too familiar with this process. I also got the assistance of a Cloud Support Engineer from AWS to try and identify the problem. Shout-out Mike Sacks. The problem ended up being that that the resource policy was not correct. The policy that was generated by the function arnForExecuteApi was ``` "Resource": "arn:aws:execute-api:us-east-1:acc:apiId/*/*/*", ``` This is because the function itself has 3 values, stage, method and path, so when all are left in default states, this indicates `all` or `*`. So when adding each value at default you get `/*/*/*`, 3 x /*. This is an issue because Websocket arns are not structured like this, and as it turns out **iam prevents access if you have too many wild cards than applicable**. This means the reason for getting access denied was not because of my signed url, but because having 1 extra /* means that you no longer have permissions. Websocket arns are structured like this ``` arn:aws:execute-api:us-east-1:acc:apiId/*/$connect ``` In this example, * is the stage (this is what it shows on the console) and $connect is the `route`. You can add as many routes as you want, but the main ones by default are $connect, $disconnect and $default for no matching route. So if I want to grant an IAM role to have access to all routes and all stages, I would use this: ``` "Resource": "arn:aws:execute-api:us-east-1:acc:apiId/*/*", ``` Note 2 x /* instead of 3. Simply changing this by hand (deleting 2 characters) was enough to get the websocket to begin connecting via my signed url. ### Description of changes A re-write of the function for creating the arn. This is implemented as arnForExecuteApiV2, the original function has been changes to include the deprecated tag. This is to avoid making a breaking change since the new function only has 2 args and the original had 3. ```ts /** * Get the "execute-api" ARN. * * @default - The default behavior applies when no specific route, or stage is provided. * In this case, the ARN will cover all routes, and all stages of this API. * Specifically, if 'route' is not specified, it defaults to '*', representing all routes. * If 'stage' is not specified, it also defaults to '*', representing all stages. */ public arnForExecuteApiV2(route?: string, stage?: string): string { return Stack.of(this).formatArn({ service: 'execute-api', resource: this.apiId, arnFormat: ArnFormat.SLASH_RESOURCE_NAME, resourceName: `${stage ?? '*'}/${route ?? '*'}`, }); } ``` I removed "Method" and "Path" entirely since these are not even appropriate to use as terms for websockets. I used Route instead. ### Description of how you validated changes Updated Tests, there were 4 tests before: * get arnForExecuteApi * is now using route, and intentionally uses a route with no `$` to check that the `$` is being added correctly. * get arnForExecuteApi with default values * is now using route * get arnForExecuteApi with ANY method (removed) * doesn't make any sense here because "ANY" is not a term used with websockets, and method does not exist. Thus, removed this test * throws when call arnForExecuteApi method with specifing a string that does not start with / for the path argument. (removed) * This test is checking for a specific format for path, which is not needed since the route can be anything. Also path does not exist. This leaves 2 total tests now. Added a new integ function, `integ.api-grant-invoke.ts` and used --update-on-failed with my personal account to bootstrap new snapshots to match. For this test I included an iam role and 2 arns, one with default settings and one with `.arnForExecuteApi('connect', 'prod')` Intentionally left off the `$` to check that it's being added. All tests and integ are passing. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-cdk-aws-apigatewayv2.assets.json | 19 ++ .../aws-cdk-aws-apigatewayv2.template.json | 136 +++++++++ .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 125 +++++++++ .../tree.json | 261 ++++++++++++++++++ ...efaultTestDeployAssert230DE1C6.assets.json | 19 ++ ...aultTestDeployAssert230DE1C6.template.json | 36 +++ .../test/websocket/integ.api-grant-invoke.ts | 36 +++ .../aws-cdk-lib/aws-apigatewayv2/README.md | 2 +- .../aws-apigatewayv2/lib/websocket/api.ts | 24 +- .../test/websocket/api.test.ts | 38 +++ 12 files changed, 702 insertions(+), 7 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/aws-cdk-aws-apigatewayv2.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/aws-cdk-aws-apigatewayv2.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/websocketapiDefaultTestDeployAssert230DE1C6.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/websocketapiDefaultTestDeployAssert230DE1C6.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/aws-cdk-aws-apigatewayv2.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/aws-cdk-aws-apigatewayv2.assets.json new file mode 100644 index 0000000000000..14cacc6c03810 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/aws-cdk-aws-apigatewayv2.assets.json @@ -0,0 +1,19 @@ +{ + "version": "39.0.0", + "files": { + "b5aa9732267df14d00c2f0ac2e8311e9edf1a6f9039ad10a691a26072389c7bc": { + "source": { + "path": "aws-cdk-aws-apigatewayv2.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "b5aa9732267df14d00c2f0ac2e8311e9edf1a6f9039ad10a691a26072389c7bc.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/aws-cdk-aws-apigatewayv2.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/aws-cdk-aws-apigatewayv2.template.json new file mode 100644 index 0000000000000..c75dce8529bb4 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/aws-cdk-aws-apigatewayv2.template.json @@ -0,0 +1,136 @@ +{ + "Resources": { + "webocketapiD5DB5DB0": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "webocket-api", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + }, + "websocketstageA39DAC37": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "webocketapiD5DB5DB0" + }, + "StageName": "prod" + } + }, + "testiamrole05EFDD08": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "webocketapiD5DB5DB0" + }, + "/*/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "webocketapiD5DB5DB0" + }, + "/prod/connect" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "webSocketAccess" + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/cdk.out new file mode 100644 index 0000000000000..91e1a8b9901d5 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"39.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/integ.json new file mode 100644 index 0000000000000..99dced49b5c9e --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "39.0.0", + "testCases": { + "web-socket-api/DefaultTest": { + "stacks": [ + "aws-cdk-aws-apigatewayv2" + ], + "assertionStack": "web-socket-api/DefaultTest/DeployAssert", + "assertionStackName": "websocketapiDefaultTestDeployAssert230DE1C6" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/manifest.json new file mode 100644 index 0000000000000..892d7eb105705 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/manifest.json @@ -0,0 +1,125 @@ +{ + "version": "39.0.0", + "artifacts": { + "aws-cdk-aws-apigatewayv2.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-aws-apigatewayv2.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-aws-apigatewayv2": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-aws-apigatewayv2.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/b5aa9732267df14d00c2f0ac2e8311e9edf1a6f9039ad10a691a26072389c7bc.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-aws-apigatewayv2.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-aws-apigatewayv2.assets" + ], + "metadata": { + "/aws-cdk-aws-apigatewayv2/webocket-api/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "webocketapiD5DB5DB0" + } + ], + "/aws-cdk-aws-apigatewayv2/websocket-stage/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "websocketstageA39DAC37" + } + ], + "/aws-cdk-aws-apigatewayv2/test-iam-role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "testiamrole05EFDD08" + } + ], + "/aws-cdk-aws-apigatewayv2/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-aws-apigatewayv2/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-aws-apigatewayv2" + }, + "websocketapiDefaultTestDeployAssert230DE1C6.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "websocketapiDefaultTestDeployAssert230DE1C6.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "websocketapiDefaultTestDeployAssert230DE1C6": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "websocketapiDefaultTestDeployAssert230DE1C6.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "websocketapiDefaultTestDeployAssert230DE1C6.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "websocketapiDefaultTestDeployAssert230DE1C6.assets" + ], + "metadata": { + "/web-socket-api/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/web-socket-api/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "web-socket-api/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/tree.json new file mode 100644 index 0000000000000..1b718045f7aa2 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/tree.json @@ -0,0 +1,261 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-aws-apigatewayv2": { + "id": "aws-cdk-aws-apigatewayv2", + "path": "aws-cdk-aws-apigatewayv2", + "children": { + "webocket-api": { + "id": "webocket-api", + "path": "aws-cdk-aws-apigatewayv2/webocket-api", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-apigatewayv2/webocket-api/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Api", + "aws:cdk:cloudformation:props": { + "name": "webocket-api", + "protocolType": "WEBSOCKET", + "routeSelectionExpression": "$request.body.action" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnApi", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketApi", + "version": "0.0.0" + } + }, + "websocket-stage": { + "id": "websocket-stage", + "path": "aws-cdk-aws-apigatewayv2/websocket-stage", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-apigatewayv2/websocket-stage/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Stage", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "webocketapiD5DB5DB0" + }, + "stageName": "prod" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnStage", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketStage", + "version": "0.0.0" + } + }, + "test-iam-role": { + "id": "test-iam-role", + "path": "aws-cdk-aws-apigatewayv2/test-iam-role", + "children": { + "Importtest-iam-role": { + "id": "Importtest-iam-role", + "path": "aws-cdk-aws-apigatewayv2/test-iam-role/Importtest-iam-role", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-aws-apigatewayv2/test-iam-role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "policies": [ + { + "policyName": "webSocketAccess", + "policyDocument": { + "Statement": [ + { + "Action": "execute-api:Invoke", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "webocketapiD5DB5DB0" + }, + "/*/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "webocketapiD5DB5DB0" + }, + "/prod/connect" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-aws-apigatewayv2/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-aws-apigatewayv2/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "web-socket-api": { + "id": "web-socket-api", + "path": "web-socket-api", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "web-socket-api/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "web-socket-api/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "web-socket-api/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "web-socket-api/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "web-socket-api/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/websocketapiDefaultTestDeployAssert230DE1C6.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/websocketapiDefaultTestDeployAssert230DE1C6.assets.json new file mode 100644 index 0000000000000..6c0629e7c5c94 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/websocketapiDefaultTestDeployAssert230DE1C6.assets.json @@ -0,0 +1,19 @@ +{ + "version": "39.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "websocketapiDefaultTestDeployAssert230DE1C6.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/websocketapiDefaultTestDeployAssert230DE1C6.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/websocketapiDefaultTestDeployAssert230DE1C6.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.js.snapshot/websocketapiDefaultTestDeployAssert230DE1C6.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.ts new file mode 100644 index 0000000000000..2392b07b5bcef --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/websocket/integ.api-grant-invoke.ts @@ -0,0 +1,36 @@ +#!/usr/bin/env node +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import * as cdk from 'aws-cdk-lib'; +import * as apigw from 'aws-cdk-lib/aws-apigatewayv2'; +import * as iam from 'aws-cdk-lib/aws-iam'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-aws-apigatewayv2'); + +const websocketApi = new apigw.WebSocketApi(stack, 'webocket-api'); + +new apigw.WebSocketStage(stack, 'websocket-stage', { + stageName: 'prod', + webSocketApi: websocketApi, +}); + +new iam.Role(stack, 'test-iam-role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + inlinePolicies: { + webSocketAccess: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + actions: ['execute-api:Invoke'], + resources: [ + websocketApi.arnForExecuteApiV2(), + websocketApi.arnForExecuteApiV2('connect', 'prod'), + ], + }), + ], + }), + }, +}); + +new IntegTest(app, 'web-socket-api', { + testCases: [stack], +}); diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/README.md b/packages/aws-cdk-lib/aws-apigatewayv2/README.md index fdde2f476c216..90eae4cc7a05e 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2/README.md +++ b/packages/aws-cdk-lib/aws-apigatewayv2/README.md @@ -433,7 +433,7 @@ To generate an ARN for Execute API: ```ts const api = new apigwv2.WebSocketApi(this, 'mywsapi'); -const arn = api.arnForExecuteApi('GET', '/myApiPath', 'dev'); +const arn = api.arnForExecuteApiV2('$connect', 'dev'); ``` For a detailed explanation of this function, including usage and examples, please refer to the [Generating ARN for Execute API](#generating-arn-for-execute-api) section under HTTP API. diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/api.ts b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/api.ts index 5a07db39a2c22..7eb85ae30552b 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/api.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/api.ts @@ -191,13 +191,8 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { /** * Get the "execute-api" ARN. - * When 'ANY' is passed to the method, an ARN with the method set to '*' is obtained. * - * @default - The default behavior applies when no specific method, path, or stage is provided. - * In this case, the ARN will cover all methods, all resources, and all stages of this API. - * Specifically, if 'method' is not specified, it defaults to '*', representing all methods. - * If 'path' is not specified, it defaults to '/*', representing all paths. - * If 'stage' is not specified, it also defaults to '*', representing all stages. + * @deprecated Use `arnForExecuteApiV2()` instead. */ public arnForExecuteApi(method?: string, path?: string, stage?: string): string { if (path && !Token.isUnresolved(path) && !path.startsWith('/')) { @@ -215,4 +210,21 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi { resourceName: `${stage ?? '*'}/${method ?? '*'}${path ?? '/*'}`, }); } + + /** + * Get the "execute-api" ARN. + * + * @default - The default behavior applies when no specific route, or stage is provided. + * In this case, the ARN will cover all routes, and all stages of this API. + * Specifically, if 'route' is not specified, it defaults to '*', representing all routes. + * If 'stage' is not specified, it also defaults to '*', representing all stages. + */ + public arnForExecuteApiV2(route?: string, stage?: string): string { + return Stack.of(this).formatArn({ + service: 'execute-api', + resource: this.apiId, + arnFormat: ArnFormat.SLASH_RESOURCE_NAME, + resourceName: `${stage ?? '*'}/${route ?? '*'}`, + }); + } } diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/test/websocket/api.test.ts b/packages/aws-cdk-lib/aws-apigatewayv2/test/websocket/api.test.ts index 8abe6d44c1340..ef76d356da04a 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2/test/websocket/api.test.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2/test/websocket/api.test.ts @@ -213,6 +213,44 @@ describe('WebSocketApi', () => { .toThrow("Path must start with '/': path"); }); + test('get arnForExecuteApiV2', () => { + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api'); + + expect(stack.resolve(api.arnForExecuteApiV2('route', 'stage'))).toEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':execute-api:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':', + stack.resolve(api.apiId), + '/stage/route', + ]], + }); + }); + + test('get arnForExecuteApiV2 with default values', () => { + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api'); + + expect(stack.resolve(api.arnForExecuteApiV2())).toEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':execute-api:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':', + stack.resolve(api.apiId), + '/*/*', + ]], + }); + }); + describe('grantManageConnections', () => { test('adds an IAM policy to the principal', () => { // GIVEN