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(apigateway): rate limited API key #6509

Merged
merged 9 commits into from
Mar 11, 2020
30 changes: 30 additions & 0 deletions packages/@aws-cdk/aws-apigateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,36 @@ plan.addApiStage({
});
```

In scenarios where you need to create a single api key and configure rate limiting for it, you can use `RateLimitedApiKey`.
This construct lets you specify rate limiting properties which should be applied only to the api key being created.
The API key created has the specified rate limits, such as quota and throttles, applied.

The following example shows how to use a rate limited api key :
```ts
const hello = new lambda.Function(this, 'hello', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'hello.handler',
code: lambda.Code.fromAsset('lambda')
});

const api = new apigateway.RestApi(this, 'hello-api', { });
const integration = new apigateway.LambdaIntegration(hello);

const v1 = api.root.addResource('v1');
const echo = v1.addResource('echo');
const echoMethod = echo.addMethod('GET', integration, { apiKeyRequired: true });

const key = new apigateway.RateLimitedApiKey(this, 'rate-limited-api-key', {
customerId: 'hello-customer',
resources: [api],
quota: {
limit: 10000,
period: apigateway.Period.MONTH
}
});

```

### Working with models

When you work with Lambda integrations that are not Proxy integrations, you
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apigateway/lib/api-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface IApiKey extends IResourceBase {
*/
export interface ApiKeyProps extends ResourceOptions {
/**
* [disable-awslint:ref-via-interface]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this needed? We generally don't encourage any new linter exclusions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The build was failing after I made RateLimitedApiKeyProps extend ApiKeyProps :

error: [awslint:ref-via-interface:@aws-cdk/aws-apigateway.RateLimitedApiKeyProps.resources] API should use interface and not the concrete class (@aws-cdk/aws-apigateway.RestApi). If this is intentional, add "[disable-awslint:ref-via-interface]" to element's jsdoc

So I followed the suggestion and added [disable-awslint:ref-via-interface] to the jsdoc of resources property in ApiKeyProps

Copy link
Contributor

Choose a reason for hiding this comment

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

I see it now.

I believe this is occurring because this object is using RestApi instead of IRestApi as the type for resources.
Can we try switching it to IRestApi and see if all tests still pass?

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 tried doing that, but it resulted in compilation errors as renderStageKeys method expects resources to be of type RestApi and changing it to IRestApi is not possible as it doesn't have deploymentStage property needed by this method
https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-apigateway/lib/api-key.ts#L90-L101

* A list of resources this api key is associated with.
* @default none
*/
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apigateway/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './stage';
export * from './integrations';
export * from './lambda-api';
export * from './api-key';
export * from './rate-limited-api-key';
export * from './usage-plan';
export * from './vpc-link';
export * from './methodresponse';
Expand Down
54 changes: 54 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/rate-limited-api-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Construct, Resource } from '@aws-cdk/core';
import { ApiKey, ApiKeyProps, IApiKey } from './api-key';
import { QuotaSettings, ThrottleSettings, UsagePlan, UsagePlanPerApiStage } from './usage-plan';

/**
* RateLimitedApiKey properties.
*/
export interface RateLimitedApiKeyProps extends ApiKeyProps {
Copy link
Contributor

Choose a reason for hiding this comment

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

Missed something from last time. We usually don't extend one Props class from another Props class.

The following changes are needed here

  • Rename ApiKeyProps to ApiKeyOptions
  • Create a new empty ApiKeyProps class -
    export interface ApiKeyProps extends ApiKeyOptions {
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if I understand.

Is the suggestion to :

  1. Rename ApiKeyProps defined in ApiKey, to ApiKeyOptions
  2. Create an empty interface (ApiKeyProps) in RateLimitedApiKey extending ApiKeyOptions
  3. Use it like
export interface RateLimitedApiKeyProps extends ApiKeyProps {

If that's the case, I'm confused how it will help/is better

Copy link
Contributor

Choose a reason for hiding this comment

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

You're correct. I think I was looking for an optimization/future-proofing whose value is unclear yet. Ignore this.

/**
* API Stages to be associated with the RateLimitedApiKey.
* @default none
*/
readonly apiStages?: UsagePlanPerApiStage[];

/**
* Number of requests clients can make in a given time period.
* @default none
*/
readonly quota?: QuotaSettings;

/**
* Overall throttle settings for the API.
* @default none
*/
readonly throttle?: ThrottleSettings;
}

/**
* An API Gateway ApiKey, for which a rate limiting configuration can be specified.
*
* @resource AWS::ApiGateway::ApiKey
*/
export class RateLimitedApiKey extends Resource implements IApiKey {
public readonly keyId: string;

constructor(scope: Construct, id: string, props: RateLimitedApiKeyProps = { }) {
super(scope, id, {
physicalName: props.apiKeyName,
});

const resource = new ApiKey(this, 'Resource', props);

if (props.apiStages || props.quota || props.throttle) {
new UsagePlan(this, 'UsagePlanResource', {
apiKey: resource,
apiStages: props.apiStages,
quota: props.quota,
throttle: props.throttle
});
}

this.keyId = resource.keyId;
}
}
108 changes: 108 additions & 0 deletions packages/@aws-cdk/aws-apigateway/test/test.rate-limited-api-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { expect, haveResource, ResourcePart } from '@aws-cdk/assert';
import * as cdk from '@aws-cdk/core';
import { Test } from "nodeunit";
import * as apigateway from '../lib';

const API_KEY_RESOURCE_TYPE = 'AWS::ApiGateway::ApiKey';
const USAGE_PLAN_RESOURCE_TYPE = 'AWS::ApiGateway::UsagePlan';
const USAGE_PLAN_KEY_RESOURCE_TYPE = 'AWS::ApiGateway::UsagePlanKey';

export = {
'default setup'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'my-api', { cloudWatchRole: false, deploy: false });
api.root.addMethod('GET'); // Need at least one method on the api

// WHEN
new apigateway.RateLimitedApiKey(stack, 'my-api-key');

// THEN
// should have an api key with no props defined.
expect(stack).to(haveResource(API_KEY_RESOURCE_TYPE, undefined, ResourcePart.CompleteDefinition));
// should not have a usage plan.
expect(stack).notTo(haveResource(USAGE_PLAN_RESOURCE_TYPE));
// should not have a usage plan key.
expect(stack).notTo(haveResource(USAGE_PLAN_KEY_RESOURCE_TYPE));

test.done();
},

'only api key is created when rate limiting properties are not provided'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } });
api.root.addMethod('GET'); // api must have atleast one method.

// WHEN
new apigateway.RateLimitedApiKey(stack, 'test-api-key', {
customerId: 'test-customer',
resources: [api]
});

// THEN
expect(stack).to(haveResource('AWS::ApiGateway::ApiKey', {
CustomerId: 'test-customer',
StageKeys: [
{
RestApiId: { Ref: "testapiD6451F70" },
StageName: { Ref: "testapiDeploymentStagetest5869DF71" }
}
]
}));
// should not have a usage plan.
expect(stack).notTo(haveResource(USAGE_PLAN_RESOURCE_TYPE));
// should not have a usage plan key.
expect(stack).notTo(haveResource(USAGE_PLAN_KEY_RESOURCE_TYPE));

test.done();
},

'api key and usage plan are created and linked when rate limiting properties are provided'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } });
api.root.addMethod('GET'); // api must have atleast one method.

// WHEN
new apigateway.RateLimitedApiKey(stack, 'test-api-key', {
customerId: 'test-customer',
resources: [api],
quota: {
limit: 10000,
period: apigateway.Period.MONTH
}
});

// THEN
// should have an api key
expect(stack).to(haveResource('AWS::ApiGateway::ApiKey', {
CustomerId: 'test-customer',
StageKeys: [
{
RestApiId: { Ref: "testapiD6451F70" },
StageName: { Ref: "testapiDeploymentStagetest5869DF71" }
}
]
}));
// should have a usage plan with specified quota.
expect(stack).to(haveResource(USAGE_PLAN_RESOURCE_TYPE, {
Quota: {
Limit: 10000,
Period: 'MONTH'
}
}, ResourcePart.Properties));
// should have a usage plan key linking the api key and usage plan
expect(stack).to(haveResource(USAGE_PLAN_KEY_RESOURCE_TYPE, {
KeyId: {
Ref: 'testapikey998028B6'
},
KeyType: 'API_KEY',
UsagePlanId: {
Ref: 'testapikeyUsagePlanResource66DB63D6'
}
}, ResourcePart.Properties));

test.done();
}
};