Skip to content

Commit

Permalink
fix(appsync): strongly type expires prop in apiKeyConfig (#9122)
Browse files Browse the repository at this point in the history
**[ISSUE]**
`apiKeyConfig` has prop `expires` that has unclear documentation/not strongly typed and is prone to user errors. 

**[APPROACH]**
Force `expires` to take `Expiration` class from `core` and will be able to output api key configurations easily through `Expiration` static functions: `after(...)`, `fromString(...)`, ` atDate(...)`, `atTimeStamp(...)`.

Fixes #8698 

BREAKING CHANGE:  force `apiKeyConfig` require a Expiration class instead of string
- **appsync**: Parameter `apiKeyConfig` takes `Expiration` class instead of `string`

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
BryanPan342 authored Sep 9, 2020
1 parent b895599 commit 287f808
Show file tree
Hide file tree
Showing 6 changed files with 371 additions and 13 deletions.
21 changes: 8 additions & 13 deletions packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IUserPool } from '@aws-cdk/aws-cognito';
import { ManagedPolicy, Role, ServicePrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam';
import { CfnResource, Construct, Duration, IResolvable, Stack } from '@aws-cdk/core';
import { CfnResource, Construct, Duration, Expiration, IResolvable, Stack } from '@aws-cdk/core';
import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated';
import { IGraphqlApi, GraphqlApiBase } from './graphqlapi-base';
import { Schema } from './schema';
Expand Down Expand Up @@ -111,12 +111,13 @@ export interface ApiKeyConfig {
readonly description?: string;

/**
* The time from creation time after which the API key expires, using RFC3339 representation.
* The time from creation time after which the API key expires.
* It must be a minimum of 1 day and a maximum of 365 days from date of creation.
* Rounded down to the nearest hour.
* @default - 7 days from creation time
*
* @default - 7 days rounded down to nearest hour
*/
readonly expires?: string;
readonly expires?: Expiration;
}

/**
Expand Down Expand Up @@ -556,16 +557,10 @@ export class GraphqlApi extends GraphqlApiBase {
}

private createAPIKey(config?: ApiKeyConfig) {
let expires: number | undefined;
if (config?.expires) {
expires = new Date(config.expires).valueOf();
const days = (d: number) =>
Date.now() + Duration.days(d).toMilliseconds();
if (expires < days(1) || expires > days(365)) {
throw Error('API key expiration must be between 1 and 365 days.');
}
expires = Math.round(expires / 1000);
if (config?.expires?.isBefore(Duration.days(1)) || config?.expires?.isAfter(Duration.days(365))) {
throw Error('API key expiration must be between 1 and 365 days.');
}
const expires = config?.expires ? config?.expires.toEpoch() : undefined;
return new CfnApiKey(this, `${config?.name || 'Default'}ApiKey`, {
expires,
description: config?.description,
Expand Down
65 changes: 65 additions & 0 deletions packages/@aws-cdk/aws-appsync/test/appsync-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,71 @@ describe('AppSync API Key Authorization', () => {
});
});

test('apiKeyConfig creates default with valid expiration date', () => {
const expirationDate: number = cdk.Expiration.after(cdk.Duration.days(10)).toEpoch();

// WHEN
new appsync.GraphqlApi(stack, 'API', {
name: 'apiKeyUnitTest',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.auth.graphql')),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
apiKeyConfig: {
expires: cdk.Expiration.after(cdk.Duration.days(10)),
},
},
},
});
// THEN
expect(stack).toHaveResourceLike('AWS::AppSync::ApiKey', {
ApiId: { 'Fn::GetAtt': ['API62EA1CFF', 'ApiId'] },
Expires: expirationDate,
});
});

test('apiKeyConfig fails if expire argument less than a day', () => {
// WHEN
const when = () => {
new appsync.GraphqlApi(stack, 'API', {
name: 'apiKeyUnitTest',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.auth.graphql')),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
apiKeyConfig: {
expires: cdk.Expiration.after(cdk.Duration.hours(1)),
},
},
},
});
};

// THEN
expect(when).toThrowError('API key expiration must be between 1 and 365 days.');
});

test('apiKeyConfig fails if expire argument greater than 365 day', () => {
// WHEN
const when = () => {
new appsync.GraphqlApi(stack, 'API', {
name: 'apiKeyUnitTest',
schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.auth.graphql')),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
apiKeyConfig: {
expires: cdk.Expiration.after(cdk.Duration.days(366)),
},
},
},
});
};

// THEN
expect(when).toThrowError('API key expiration must be between 1 and 365 days.');
});

test('appsync creates configured api key with additionalAuthorizationModes (not as first element)', () => {
// WHEN
new appsync.GraphqlApi(stack, 'api', {
Expand Down
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-appsync/test/appsync.auth.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
type test {
id: Int!
version: String!
}

type Query {
getTests: [ test! ]
}

type Mutation {
addTest(version: String!): test!
}
186 changes: 186 additions & 0 deletions packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
{
"Resources": {
"ApiF70053CD": {
"Type": "AWS::AppSync::GraphQLApi",
"Properties": {
"AuthenticationType": "API_KEY",
"Name": "Integ_Test_APIKey"
}
},
"ApiSchema510EECD7": {
"Type": "AWS::AppSync::GraphQLSchema",
"Properties": {
"ApiId": {
"Fn::GetAtt": [
"ApiF70053CD",
"ApiId"
]
},
"Definition": "type test {\n id: Int!\n version: String!\n}\n\ntype Query {\n getTests: [ test! ]\n}\n\ntype Mutation {\n addTest(version: String!): test!\n}"
}
},
"ApiDefaultApiKeyF991C37B": {
"Type": "AWS::AppSync::ApiKey",
"Properties": {
"ApiId": {
"Fn::GetAtt": [
"ApiF70053CD",
"ApiId"
]
},
"Expires": 1626566400
},
"DependsOn": [
"ApiSchema510EECD7"
]
},
"ApitestDataSourceServiceRoleACBC3F3D": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "appsync.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
}
},
"ApitestDataSourceServiceRoleDefaultPolicy897CD912": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"dynamodb:BatchGetItem",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:Query",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:BatchWriteItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem"
],
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"TestTable5769773A",
"Arn"
]
},
{
"Ref": "AWS::NoValue"
}
]
}
],
"Version": "2012-10-17"
},
"PolicyName": "ApitestDataSourceServiceRoleDefaultPolicy897CD912",
"Roles": [
{
"Ref": "ApitestDataSourceServiceRoleACBC3F3D"
}
]
}
},
"ApitestDataSource96AE54D5": {
"Type": "AWS::AppSync::DataSource",
"Properties": {
"ApiId": {
"Fn::GetAtt": [
"ApiF70053CD",
"ApiId"
]
},
"Name": "testDataSource",
"Type": "AMAZON_DYNAMODB",
"DynamoDBConfig": {
"AwsRegion": {
"Ref": "AWS::Region"
},
"TableName": {
"Ref": "TestTable5769773A"
}
},
"ServiceRoleArn": {
"Fn::GetAtt": [
"ApitestDataSourceServiceRoleACBC3F3D",
"Arn"
]
}
}
},
"ApitestDataSourceQuerygetTestsResolverA3BBB672": {
"Type": "AWS::AppSync::Resolver",
"Properties": {
"ApiId": {
"Fn::GetAtt": [
"ApiF70053CD",
"ApiId"
]
},
"FieldName": "getTests",
"TypeName": "Query",
"DataSourceName": "testDataSource",
"Kind": "UNIT",
"RequestMappingTemplate": "{\"version\" : \"2017-02-28\", \"operation\" : \"Scan\"}",
"ResponseMappingTemplate": "$util.toJson($ctx.result.items)"
},
"DependsOn": [
"ApiSchema510EECD7",
"ApitestDataSource96AE54D5"
]
},
"ApitestDataSourceMutationaddTestResolver36203D6B": {
"Type": "AWS::AppSync::Resolver",
"Properties": {
"ApiId": {
"Fn::GetAtt": [
"ApiF70053CD",
"ApiId"
]
},
"FieldName": "addTest",
"TypeName": "Mutation",
"DataSourceName": "testDataSource",
"Kind": "UNIT",
"RequestMappingTemplate": "\n #set($input = $ctx.args.test)\n \n {\n \"version\": \"2017-02-28\",\n \"operation\": \"PutItem\",\n \"key\" : {\n \"id\" : $util.dynamodb.toDynamoDBJson($util.autoId())\n },\n \"attributeValues\": $util.dynamodb.toMapValuesJson($input)\n }",
"ResponseMappingTemplate": "$util.toJson($ctx.result)"
},
"DependsOn": [
"ApiSchema510EECD7",
"ApitestDataSource96AE54D5"
]
},
"TestTable5769773A": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"KeySchema": [
{
"AttributeName": "id",
"KeyType": "HASH"
}
],
"AttributeDefinitions": [
{
"AttributeName": "id",
"AttributeType": "S"
}
],
"BillingMode": "PAY_PER_REQUEST"
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
}
}
}
63 changes: 63 additions & 0 deletions packages/@aws-cdk/aws-appsync/test/integ.auth-apikey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { join } from 'path';
import { AttributeType, BillingMode, Table } from '@aws-cdk/aws-dynamodb';
import { App, RemovalPolicy, Stack, Expiration } from '@aws-cdk/core';
import { AuthorizationType, GraphqlApi, MappingTemplate, PrimaryKey, Schema, Values } from '../lib';

/*
* Creates an Appsync GraphQL API with API_KEY authorization.
* Testing for API_KEY Authorization.
*
* Stack verification steps:
* Deploy stack, get api-key and endpoint.
* Check if authorization occurs with empty get.
*
* -- bash verify.integ.auth-apikey.sh --start -- deploy stack --
* -- aws appsync list-graphql-apis -- obtain api id && endpoint --
* -- aws appsync list-api-keys --api-id [API ID] -- obtain api key --
* -- bash verify.integ.auth-apikey.sh --check [APIKEY] [ENDPOINT] -- check if fails/success --
* -- bash verify.integ.auth-apikey.sh --clean -- clean dependencies/stack --
*/

const app = new App();
const stack = new Stack(app, 'aws-appsync-integ');

const api = new GraphqlApi(stack, 'Api', {
name: 'Integ_Test_APIKey',
schema: Schema.fromAsset(join(__dirname, 'appsync.auth.graphql')),
authorizationConfig: {
defaultAuthorization: {
authorizationType: AuthorizationType.API_KEY,
apiKeyConfig: {
// Generate a timestamp that's 365 days ahead, use atTimestamp so integ test doesn't fail
expires: Expiration.atTimestamp(1626566400000),
},
},
},
});

const testTable = new Table(stack, 'TestTable', {
billingMode: BillingMode.PAY_PER_REQUEST,
partitionKey: {
name: 'id',
type: AttributeType.STRING,
},
removalPolicy: RemovalPolicy.DESTROY,
});

const testDS = api.addDynamoDbDataSource('testDataSource', testTable);

testDS.createResolver({
typeName: 'Query',
fieldName: 'getTests',
requestMappingTemplate: MappingTemplate.dynamoDbScanTable(),
responseMappingTemplate: MappingTemplate.dynamoDbResultList(),
});

testDS.createResolver({
typeName: 'Mutation',
fieldName: 'addTest',
requestMappingTemplate: MappingTemplate.dynamoDbPutItem(PrimaryKey.partition('id').auto(), Values.projecting('test')),
responseMappingTemplate: MappingTemplate.dynamoDbResultItem(),
});

app.synth();
Loading

0 comments on commit 287f808

Please sign in to comment.