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: http api - iam authorizer #14853

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [JWT Authorizers](#jwt-authorizers)
- [User Pool Authorizer](#user-pool-authorizer)
- [Lambda Authorizers](#lambda-authorizers)
- [IAM Authorizers](#iam-authorizers)

## Introduction

Expand Down Expand Up @@ -192,3 +193,25 @@ api.addRoutes({
authorizer,
});
```

## IAM Authorizers
Copy link
Contributor

Choose a reason for hiding this comment

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

Hi @iRoachie -

This does not feel like the right approach to solving IAM authorizers. Just having an HttpIamAuthorizer class to set the AuthorizationType and then leaving it up to the user to correctly configure the IAM policy feels sub-optimal.

I would suggest going with an alternate approach. Here's my proposal written in the form of what the user experience will be -

const api = new HttpApi(stack, 'HttpApi');
const routes: Route[] = api.addRoutes({ ... });
routes[0].grantInvoke(principal, { // principal is of type iam.IPrincipal
  httpMethod: 'GET'
});

This should, under the hood, automatically set the AuthorizationType to AWS_IAM and attach the relevant IAM permissions to the principal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was going based on usage and what was requested in #10534.

As far as I understand the use case for IAM Authorization is to set policies on individual users to grant access to all/some routes.

I see where you are going with your suggestion however I think this can be an add-on to what is currently in this PR. If we only change the authorizationType when grantInvoke is called on a route then we automatically rule out scenarios where user permissions are managed outside of cdk/cloudformation (which is pretty common for physical users).

So just to be clear they're two things here:

  1. Setting route to use IAM Authorization
  2. Allowing an easy way to assign the relevant policies to a user/principal so that it can invoke that route.


IAM Authorizers allow and restrict clients from accessing HTTP APIs by using IAM Policies. Unlike the other authorizers, the IAM Authorizer doesn't create a new resource in your stack, but configures the route(s) to use IAM authorization.

Clients are actual users defined in the IAM console with generated AWS Credentials. When enabled for a route, clients must use [Signature Version 4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) to sign their requests with AWS credentials.

Using this authorizer requires assigning the relevant policies for each client. Here are some [examples](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html).

```ts
const authorizer = new HttpIamAuthorizer();

const api = new HttpApi(stack, 'HttpApi');

api.addRoutes({
integration: new HttpProxyIntegration({
url: 'https://get-books-proxy.myproxy.internal',
}),
path: '/books',
authorizer,
});
```
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/iam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { HttpRouteAuthorizerConfig, IHttpRouteAuthorizer } from '@aws-cdk/aws-apigatewayv2';

/**
* Authorize Http Api routes with IAM using sigv4 to sign request
*/
export class HttpIamAuthorizer implements IHttpRouteAuthorizer {
public bind(): HttpRouteAuthorizerConfig {
return {
authorizationType: 'AWS_IAM',
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import '@aws-cdk/assert-internal/jest';
import { ABSENT } from '@aws-cdk/assert-internal';
import { HttpApi, IHttpRouteIntegration, HttpRouteIntegrationBindOptions, PayloadFormatVersion, HttpIntegrationType } from '@aws-cdk/aws-apigatewayv2';
import { Stack } from '@aws-cdk/core';
import { HttpIamAuthorizer } from '../../lib/http/iam';

describe('HttpIamAuthorizer', () => {
test('default', () => {
// GIVEN
const stack = new Stack();
const api = new HttpApi(stack, 'HttpApi');

const authorizer = new HttpIamAuthorizer();

// WHEN
api.addRoutes({
integration: new DummyRouteIntegration(),
path: '/books',
authorizer,
});

// THEN
expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', {
AuthorizationType: 'AWS_IAM',
AuthorizerId: ABSENT,
});
});

test('default integration', () => {
// GIVEN
const stack = new Stack();
const authorizer = new HttpIamAuthorizer();

// WHEN
new HttpApi(stack, 'HttpApi', {
defaultAuthorizer: authorizer,
defaultIntegration: new DummyRouteIntegration(),
});

// THEN
expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', {
AuthorizationType: 'AWS_IAM',
AuthorizerId: ABSENT,
});
});
});


class DummyRouteIntegration implements IHttpRouteIntegration {
public bind(_: HttpRouteIntegrationBindOptions) {
return {
payloadFormatVersion: PayloadFormatVersion.VERSION_2_0,
type: HttpIntegrationType.HTTP_PROXY,
uri: 'some-uri',
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
{
"Resources": {
"MyHttpApi8AEAAC21": {
"Type": "AWS::ApiGatewayV2::Api",
"Properties": {
"Name": "MyHttpApi",
"ProtocolType": "HTTP"
}
},
"MyHttpApiDefaultStageDCB9BC49": {
"Type": "AWS::ApiGatewayV2::Stage",
"Properties": {
"ApiId": {
"Ref": "MyHttpApi8AEAAC21"
},
"StageName": "$default",
"AutoDeploy": true
}
},
"MyHttpApiGETIAMAuthorizerIntegMyHttpApiGET271B2CE5PermissionE3A1E0E1": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"lambda8B5974B5",
"Arn"
]
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":execute-api:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":",
{
"Ref": "MyHttpApi8AEAAC21"
},
"/*/*/"
]
]
}
}
},
"MyHttpApiGETHttpIntegration6f095b8469365f72e33fa33d9711b140516EBE31": {
"Type": "AWS::ApiGatewayV2::Integration",
"Properties": {
"ApiId": {
"Ref": "MyHttpApi8AEAAC21"
},
"IntegrationType": "AWS_PROXY",
"IntegrationUri": {
"Fn::GetAtt": [
"lambda8B5974B5",
"Arn"
]
},
"PayloadFormatVersion": "2.0"
}
},
"MyHttpApiGETE0EFC6F8": {
"Type": "AWS::ApiGatewayV2::Route",
"Properties": {
"ApiId": {
"Ref": "MyHttpApi8AEAAC21"
},
"RouteKey": "GET /",
"AuthorizationType": "AWS_IAM",
"Target": {
"Fn::Join": [
"",
[
"integrations/",
{
"Ref": "MyHttpApiGETHttpIntegration6f095b8469365f72e33fa33d9711b140516EBE31"
}
]
]
}
}
},
"lambdaServiceRole494E4CA6": {
"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"
]
]
}
]
}
},
"lambda8B5974B5": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "AssetParameters1fd1c15cb7d5e2e36a11745fd10b4b7c3ca8eb30642b41954630413d2b913cdaS3Bucket2E6D85D3"
},
"S3Key": {
"Fn::Join": [
"",
[
{
"Fn::Select": [
0,
{
"Fn::Split": [
"||",
{
"Ref": "AssetParameters1fd1c15cb7d5e2e36a11745fd10b4b7c3ca8eb30642b41954630413d2b913cdaS3VersionKey22B8E7C6"
}
]
}
]
},
{
"Fn::Select": [
1,
{
"Fn::Split": [
"||",
{
"Ref": "AssetParameters1fd1c15cb7d5e2e36a11745fd10b4b7c3ca8eb30642b41954630413d2b913cdaS3VersionKey22B8E7C6"
}
]
}
]
}
]
]
}
},
"Role": {
"Fn::GetAtt": [
"lambdaServiceRole494E4CA6",
"Arn"
]
},
"Handler": "index.handler",
"Runtime": "nodejs12.x"
},
"DependsOn": [
"lambdaServiceRole494E4CA6"
]
},
"testuser14267055": {
"Type": "AWS::IAM::User",
"Properties": {
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/AmazonAPIGatewayInvokeFullAccess"
]
]
}
]
}
},
"accesskey": {
"Type": "AWS::IAM::AccessKey",
"Properties": {
"UserName": {
"Ref": "testuser14267055"
}
}
}
},
"Parameters": {
"AssetParameters1fd1c15cb7d5e2e36a11745fd10b4b7c3ca8eb30642b41954630413d2b913cdaS3Bucket2E6D85D3": {
"Type": "String",
"Description": "S3 bucket for asset \"1fd1c15cb7d5e2e36a11745fd10b4b7c3ca8eb30642b41954630413d2b913cda\""
},
"AssetParameters1fd1c15cb7d5e2e36a11745fd10b4b7c3ca8eb30642b41954630413d2b913cdaS3VersionKey22B8E7C6": {
"Type": "String",
"Description": "S3 key for asset version \"1fd1c15cb7d5e2e36a11745fd10b4b7c3ca8eb30642b41954630413d2b913cda\""
},
"AssetParameters1fd1c15cb7d5e2e36a11745fd10b4b7c3ca8eb30642b41954630413d2b913cdaArtifactHash82A279EA": {
"Type": "String",
"Description": "Artifact hash for asset \"1fd1c15cb7d5e2e36a11745fd10b4b7c3ca8eb30642b41954630413d2b913cda\""
}
},
"Outputs": {
"apiurl": {
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Ref": "MyHttpApi8AEAAC21"
},
".execute-api.",
{
"Ref": "AWS::Region"
},
".",
{
"Ref": "AWS::URLSuffix"
},
"/"
]
]
}
},
"accesskey": {
"Value": {
"Ref": "accesskey"
}
},
"secretaccesskey": {
"Value": {
"Fn::GetAtt": [
"accesskey",
"SecretAccessKey"
]
}
}
}
}
Loading