Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apigatewayv2): websocket api: grant manage connections #16872

Merged
merged 13 commits into from
Nov 9, 2021
20 changes: 20 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Higher level constructs for Websocket APIs | ![Experimental](https://img.shields
- [VPC Link](#vpc-link)
- [Private Integration](#private-integration)
- [WebSocket API](#websocket-api)
- [Manage Connections Permission](#manage-connections-permission)

## Introduction

Expand Down Expand Up @@ -336,3 +337,22 @@ webSocketApi.addRoute('sendmessage', {
}),
});
```

### Manage Connections Permission

Grant permission to use API Gateway Management API of a WebSocket API by calling the `grantManageConnections` API.
tmokmss marked this conversation as resolved.
Show resolved Hide resolved
You can use Management API to send a callback message to a connected client, get connection information, or disconnect the client. Learn more at [Use @connections commands in your backend service](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-connections.html).

```ts
const lambda = new lambda.Function(this, 'lambda', { /* ... */ });

const webSocketApi = new WebSocketApi(stack, 'mywsapi');
const stage = new WebSocketStage(stack, 'mystage', {
webSocketApi,
stageName: 'dev',
});
// per stage permission
stage.grantManageConnections(lambda);
// for all the stages permission
webSocketApi.grantManageConnections(lambda);
```
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Grant, IGrantable } from '@aws-cdk/aws-iam';
import { Stack } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnApi } from '../apigatewayv2.generated';
import { IApi } from '../common/api';
Expand Down Expand Up @@ -127,4 +129,23 @@ export class WebSocketApi extends ApiBase implements IWebSocketApi {
...options,
});
}

/**
* Grant access to the API Gateway management API for this WebSocket API to an IAM
* principal (Role/Group/User).
*
* @param identity The principal
*/
public grantManageConnections(identity: IGrantable): Grant {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding this method to IWebsocketApi. Then, this feature will be available to imported APIs as well.

This would mean creating a new base class and moving this implementation to it.

class WebsocketApiBase extends ApiBase {
  // ...
  public grantManageConnections(...) {
    // ...
  }
}

The same applies to Stage.

This is strictly not necessary for this PR if you prefer to not do this now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I prefer to do this in another PR. I think we can create a new issue to support importing a webSocketApi and refactor them in a corresponding PR.

const arn = Stack.of(this).formatArn({
service: 'execute-api',
resource: this.apiId,
});

return Grant.addToPrincipal({
grantee: identity,
actions: ['execute-api:ManageConnections'],
resourceArns: [`${arn}/*/POST/@connections/*`],
});
}
}
20 changes: 20 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Grant, IGrantable } from '@aws-cdk/aws-iam';
import { Stack } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnStage } from '../apigatewayv2.generated';
Expand Down Expand Up @@ -114,4 +115,23 @@ export class WebSocketStage extends StageBase implements IWebSocketStage {
const urlPath = this.stageName;
return `https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`;
}

/**
* Grant access to the API Gateway management API for this WebSocket API Stage to an IAM
* principal (Role/Group/User).
*
* @param identity The principal
*/
public grantManagementApiAccess(identity: IGrantable): Grant {
const arn = Stack.of(this.api).formatArn({
service: 'execute-api',
resource: this.api.apiId,
});

return Grant.addToPrincipal({
grantee: identity,
actions: ['execute-api:ManageConnections'],
resourceArns: [`${arn}/${this.stageName}/POST/@connections/*`],
});
}
}
46 changes: 45 additions & 1 deletion packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Template } from '@aws-cdk/assertions';
import { Match, Template } from '@aws-cdk/assertions';
import { User } from '@aws-cdk/aws-iam';
import { Stack } from '@aws-cdk/core';
import {
IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType,
Expand Down Expand Up @@ -80,6 +81,49 @@ describe('WebSocketApi', () => {
RouteKey: '$default',
});
});

describe('grantManageConnections', () => {
test('adds an IAM policy to the principal', () => {
// GIVEN
const stack = new Stack();
const api = new WebSocketApi(stack, 'api');
const principal = new User(stack, 'user');

// WHEN
api.grantManageConnections(principal);

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: Match.arrayWith([{
Action: 'execute-api:ManageConnections',
Effect: 'Allow',
Resource: {
'Fn::Join': ['', [
'arn:',
{
Ref: 'AWS::Partition',
},
':execute-api:',
{
Ref: 'AWS::Region',
},
':',
{
Ref: 'AWS::AccountId',
},
':',
{
Ref: 'apiC8550315',
},
'/*/POST/@connections/*',
]],
},
}]),
},
});
});
});
});

class DummyIntegration implements IWebSocketRouteIntegration {
Expand Down
50 changes: 49 additions & 1 deletion packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Template } from '@aws-cdk/assertions';
import { Match, Template } from '@aws-cdk/assertions';
import { User } from '@aws-cdk/aws-iam';
import { Stack } from '@aws-cdk/core';
import { WebSocketApi, WebSocketStage } from '../../lib';

Expand Down Expand Up @@ -59,4 +60,51 @@ describe('WebSocketStage', () => {
expect(defaultStage.callbackUrl.endsWith('/dev')).toBe(true);
expect(defaultStage.callbackUrl.startsWith('https://')).toBe(true);
});

describe('grantManageConnections', () => {
test('adds an IAM policy to the principal', () => {
// GIVEN
const stack = new Stack();
const api = new WebSocketApi(stack, 'Api');
const defaultStage = new WebSocketStage(stack, 'Stage', {
webSocketApi: api,
stageName: 'dev',
});
const principal = new User(stack, 'User');

// WHEN
defaultStage.grantManagementApiAccess(principal);

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: Match.arrayWith([{
Action: 'execute-api:ManageConnections',
Effect: 'Allow',
Resource: {
'Fn::Join': ['', [
'arn:',
{
Ref: 'AWS::Partition',
},
':execute-api:',
{
Ref: 'AWS::Region',
},
':',
{
Ref: 'AWS::AccountId',
},
':',
{
Ref: 'ApiF70053CD',
},
`/${defaultStage.stageName}/POST/@connections/*`,
]],
},
}]),
},
});
});
});
});