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

fix(apigateway): cannot remove first api key from usage plan #13817

Merged
merged 3 commits into from
Apr 6, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 38 additions & 27 deletions packages/@aws-cdk/aws-apigateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ running on AWS Lambda, or any web application.
- [Breaking up Methods and Resources across Stacks](#breaking-up-methods-and-resources-across-stacks)
- [AWS Lambda-backed APIs](#aws-lambda-backed-apis)
- [Integration Targets](#integration-targets)
- [API Keys](#api-keys)
- [Usage Plan & API Keys](#usage-plan--api-keys)
- [Working with models](#working-with-models)
- [Default Integration and Method Options](#default-integration-and-method-options)
- [Proxy Routes](#proxy-routes)
Expand Down Expand Up @@ -168,34 +168,36 @@ const getMessageIntegration = new apigateway.AwsIntegration({
});
```

## API Keys
## Usage Plan & API Keys

The following example shows how to use an API Key with a usage plan:
A usage plan specifies who can access one or more deployed API stages and methods, and the rate at which they can be
accessed. The plan uses API keys to identify API clients and meters access to the associated API stages for each key.
Usage plans also allow configuring throttling limits and quota limits that are enforced on individual client API keys.

```ts
const hello = new lambda.Function(this, 'hello', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'hello.handler',
code: lambda.Code.fromAsset('lambda')
});
The following example shows how to create and asscociate a usage plan and an API key:

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

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

const plan = api.addUsagePlan('UsagePlan', {
name: 'Easy',
apiKey: key,
throttle: {
rateLimit: 10,
burstLimit: 2
}
});

const key = api.addApiKey('ApiKey');
plan.addApiKey(key);
```

To associate a plan to a given RestAPI stage:

```ts
plan.addApiStage({
stage: api.deploymentStage,
throttle: [
Expand Down Expand Up @@ -233,26 +235,36 @@ following code provides read permission to an API key.
importedKey.grantRead(lambda);
```

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.
### ⚠️ Multiple API Keys

The following example shows how to use a rate limited api key :
It is possible to specify multiple API keys for a given Usage Plan, by calling `usagePlan.addApiKey()`.

When using multiple API keys, a past bug of the CDK prevents API key associations to a Usage Plan to be deleted.
If the CDK app had the [feature flag] - `@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId` - enabled when the API
keys were created, then the app will not be affected by this bug.

If this is not the case, you will need to ensure that the CloudFormation [logical ids] of the API keys that are not
being deleted remain unchanged.
Make note of the logical ids of these API keys before removing any, and set it as part of the `addApiKey()` method:

```ts
const hello = new lambda.Function(this, 'hello', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'hello.handler',
code: lambda.Code.fromAsset('lambda')
usageplan.addApiKey(apiKey, {
overrideLogicalId: '...',
});
```

const api = new apigateway.RestApi(this, 'hello-api', { });
const integration = new apigateway.LambdaIntegration(hello);
[feature flag]: https://docs.aws.amazon.com/cdk/latest/guide/featureflags.html
[logical ids]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html

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

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 key = new apigateway.RateLimitedApiKey(this, 'rate-limited-api-key', {
customerId: 'hello-customer',
resources: [api],
Expand All @@ -261,7 +273,6 @@ const key = new apigateway.RateLimitedApiKey(this, 'rate-limited-api-key', {
period: apigateway.Period.MONTH
}
});

```

## Working with models
Expand Down
34 changes: 28 additions & 6 deletions packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Lazy, Names, Resource, Token } from '@aws-cdk/core';
import { FeatureFlags, Lazy, Names, Resource, Token } from '@aws-cdk/core';
import { APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID } from '@aws-cdk/cx-api';
import { Construct } from 'constructs';
import { IApiKey } from './api-key';
import { CfnUsagePlan, CfnUsagePlanKey } from './apigateway.generated';
Expand Down Expand Up @@ -139,10 +140,22 @@ export interface UsagePlanProps {
/**
* ApiKey to be associated with the usage plan.
* @default none
* @deprecated use `addApiKey()`
*/
readonly apiKey?: IApiKey;
}

/**
* Options to the UsagePlan.addApiKey() method
*/
export interface AddApiKeyOptions {
/**
* Override the CloudFormation logical id of the AWS::ApiGateway::UsagePlanKey resource
* @default - autogenerated by the CDK
*/
readonly overrideLogicalId?: string;
}

export class UsagePlan extends Resource {
/**
* @attribute
Expand Down Expand Up @@ -176,19 +189,28 @@ export class UsagePlan extends Resource {
/**
* Adds an ApiKey.
*
* @param apiKey
* @param apiKey the api key to associate with this usage plan
* @param options options that control the behaviour of this method
*/
public addApiKey(apiKey: IApiKey): void {
public addApiKey(apiKey: IApiKey, options?: AddApiKeyOptions): void {
let id: string;
const prefix = 'UsagePlanKeyResource';

// Postfixing apikey id only from the 2nd child, to keep physicalIds of UsagePlanKey for existing CDK apps unmodified.
const id = this.node.tryFindChild(prefix) ? `${prefix}:${Names.nodeUniqueId(apiKey.node)}` : prefix;
if (FeatureFlags.of(this).isEnabled(APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID)) {
id = `${prefix}:${Names.nodeUniqueId(apiKey.node)}`;
} else {
// Postfixing apikey id only from the 2nd child, to keep physicalIds of UsagePlanKey for existing CDK apps unmodified.
id = this.node.tryFindChild(prefix) ? `${prefix}:${Names.nodeUniqueId(apiKey.node)}` : prefix;
}

new CfnUsagePlanKey(this, id, {
const resource = new CfnUsagePlanKey(this, id, {
keyId: apiKey.keyId,
keyType: UsagePlanKeyType.API_KEY,
usagePlanId: this.usagePlanId,
});
if (options?.overrideLogicalId) {
resource.overrideLogicalId(options?.overrideLogicalId);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@
"UsagePlanName": "Basic"
}
},
"myapiUsagePlanUsagePlanKeyResource050D133F": {
"myapiUsagePlanUsagePlanKeyResourcetestapigatewayrestapimyapiApiKeyC43601CB600D112D": {
"Type": "AWS::ApiGateway::UsagePlanKey",
"Properties": {
"KeyId": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"myusageplan4B391740": {
"Type": "AWS::ApiGateway::UsagePlan"
},
"myusageplanUsagePlanKeyResource095B4EA9": {
"myusageplanUsagePlanKeyResourcetestapigatewayusageplanmultikeymyapikey1DDABC389A2809A73": {
"Type": "AWS::ApiGateway::UsagePlanKey",
"Properties": {
"KeyId": {
Expand Down
148 changes: 101 additions & 47 deletions packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import '@aws-cdk/assert/jest';
import { ResourcePart } from '@aws-cdk/assert';
import * as cdk from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag';
import * as apigateway from '../lib';

const RESOURCE_TYPE = 'AWS::ApiGateway::UsagePlan';
Expand Down Expand Up @@ -149,60 +151,112 @@ describe('usage plan', () => {
}, ResourcePart.Properties);
});

test('UsagePlanKey', () => {
// GIVEN
const stack = new cdk.Stack();
const usagePlan: apigateway.UsagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan', {
name: 'Basic',
describe('UsagePlanKey', () => {

test('default', () => {
// GIVEN
const stack = new cdk.Stack();
const usagePlan: apigateway.UsagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan', {
name: 'Basic',
});
const apiKey: apigateway.ApiKey = new apigateway.ApiKey(stack, 'my-api-key');

// WHEN
usagePlan.addApiKey(apiKey);

// THEN
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
KeyId: {
Ref: 'myapikey1B052F70',
},
KeyType: 'API_KEY',
UsagePlanId: {
Ref: 'myusageplan23AA1E32',
},
}, ResourcePart.Properties);
});
const apiKey: apigateway.ApiKey = new apigateway.ApiKey(stack, 'my-api-key');

// WHEN
usagePlan.addApiKey(apiKey);
test('multiple keys', () => {
// GIVEN
const stack = new cdk.Stack();
const usagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan');
const apiKey1 = new apigateway.ApiKey(stack, 'my-api-key-1', {
apiKeyName: 'my-api-key-1',
});
const apiKey2 = new apigateway.ApiKey(stack, 'my-api-key-2', {
apiKeyName: 'my-api-key-2',
});

// THEN
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
KeyId: {
Ref: 'myapikey1B052F70',
},
KeyType: 'API_KEY',
UsagePlanId: {
Ref: 'myusageplan23AA1E32',
},
}, ResourcePart.Properties);
});
// WHEN
usagePlan.addApiKey(apiKey1);
usagePlan.addApiKey(apiKey2);

test('UsagePlan can have multiple keys', () => {
// GIVEN
const stack = new cdk.Stack();
const usagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan');
const apiKey1 = new apigateway.ApiKey(stack, 'my-api-key-1', {
apiKeyName: 'my-api-key-1',
// THEN
expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', {
Name: 'my-api-key-1',
}, ResourcePart.Properties);
expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', {
Name: 'my-api-key-2',
}, ResourcePart.Properties);
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
KeyId: {
Ref: 'myapikey11F723FC7',
},
}, ResourcePart.Properties);
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
KeyId: {
Ref: 'myapikey2ABDEF012',
},
}, ResourcePart.Properties);
});
const apiKey2 = new apigateway.ApiKey(stack, 'my-api-key-2', {
apiKeyName: 'my-api-key-2',

test('overrideLogicalId', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app);
const usagePlan: apigateway.UsagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan', { name: 'Basic' });
const apiKey: apigateway.ApiKey = new apigateway.ApiKey(stack, 'my-api-key');

// WHEN
usagePlan.addApiKey(apiKey, { overrideLogicalId: 'mylogicalid' });

// THEN
const template = app.synth().getStackByName(stack.stackName).template;
const logicalIds = Object.entries(template.Resources)
.filter(([_, v]) => (v as any).Type === 'AWS::ApiGateway::UsagePlanKey')
.map(([k, _]) => k);
expect(logicalIds).toEqual(['mylogicalid']);
});

// WHEN
usagePlan.addApiKey(apiKey1);
usagePlan.addApiKey(apiKey2);
describe('future flag: @aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId', () => {
const flags = { [cxapi.APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID]: true };

// THEN
expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', {
Name: 'my-api-key-1',
}, ResourcePart.Properties);
expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', {
Name: 'my-api-key-2',
}, ResourcePart.Properties);
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
KeyId: {
Ref: 'myapikey11F723FC7',
},
}, ResourcePart.Properties);
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
KeyId: {
Ref: 'myapikey2ABDEF012',
},
}, ResourcePart.Properties);
testFutureBehavior('UsagePlanKeys have unique logical ids', flags, cdk.App, (app) => {
// GIVEN
const stack = new cdk.Stack(app, 'my-stack');
const usagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan');
const apiKey1 = new apigateway.ApiKey(stack, 'my-api-key-1', {
apiKeyName: 'my-api-key-1',
});
const apiKey2 = new apigateway.ApiKey(stack, 'my-api-key-2', {
apiKeyName: 'my-api-key-2',
});

// WHEN
usagePlan.addApiKey(apiKey1);
usagePlan.addApiKey(apiKey2);

// THEN
const template = app.synth().getStackByName(stack.stackName).template;
const logicalIds = Object.entries(template.Resources)
.filter(([_, v]) => (v as any).Type === 'AWS::ApiGateway::UsagePlanKey')
.map(([k, _]) => k);

expect(logicalIds).toEqual([
'myusageplanUsagePlanKeyResourcemystackmyapikey1EE9AA1B359121274',
'myusageplanUsagePlanKeyResourcemystackmyapikey2B4E8EB1456DC88E9',
]);
});
});
});
});
Loading