From 9fccccfa5422d29244319aac910a88b22705f556 Mon Sep 17 00:00:00 2001 From: Duarte Nunes Date: Wed, 12 Feb 2020 17:42:10 -0300 Subject: [PATCH 1/3] feat(appsync): allow configuring API key authorization mode By default, the AppSync L2 constructs use API key authorization, but it doesn't allow configuring the API key. Fix that by allowing a default authorization mode to be specified. Currently, the supported modes are Cognito user pools and API keys. When specifying API key authorization, allow configuring it. BREAKING CHANGE: Configuration the user pool authorization is now done through the authorizationConfig property. This allows us to specify a default authorization mode out of the supported ones, currently limited to Cognito user pools and API keys. Fixes #6246 Signed-off-by: Duarte Nunes --- packages/@aws-cdk/aws-appsync/README.md | 8 +- .../@aws-cdk/aws-appsync/lib/graphqlapi.ts | 110 ++++++++++++++---- 2 files changed, 95 insertions(+), 23 deletions(-) diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index 56e08034ebb38..2e6b7af245644 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -70,9 +70,11 @@ export class ApiStack extends Stack { logConfig: { fieldLogLevel: FieldLogLevel.ALL, }, - userPoolConfig: { - userPool, - defaultAction: UserPoolDefaultAction.ALLOW, + authorizationConfig: { + defaultAuthorization: { + userPool, + defaultAction: UserPoolDefaultAction.ALLOW, + }, }, schemaDefinitionFile: './schema.graphql', }); diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index 15de26772b6c9..220e9aa95b3d0 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -2,9 +2,9 @@ import { IUserPool } from "@aws-cdk/aws-cognito"; import { Table } from '@aws-cdk/aws-dynamodb'; import { IGrantable, IPrincipal, IRole, ManagedPolicy, Role, ServicePrincipal } from "@aws-cdk/aws-iam"; import { IFunction } from "@aws-cdk/aws-lambda"; -import { Construct, IResolvable } from "@aws-cdk/core"; +import { Construct, Duration, IResolvable } from "@aws-cdk/core"; import { readFileSync } from "fs"; -import { CfnDataSource, CfnGraphQLApi, CfnGraphQLSchema, CfnResolver } from "./appsync.generated"; +import { CfnApiKey, CfnDataSource, CfnGraphQLApi, CfnGraphQLSchema, CfnResolver } from "./appsync.generated"; /** * enum with all possible values for Cognito user-pool default actions @@ -43,6 +43,46 @@ export interface UserPoolConfig { readonly defaultAction?: UserPoolDefaultAction; } +function isUserPoolConfig(obj: unknown): obj is UserPoolConfig { + return (obj as UserPoolConfig).userPool !== undefined; +} + +/** + * Configuration for API Key authorization in AppSync + */ +export interface ApiKeyConfig { + /** + * Unique description of the API key + */ + readonly apiKeyDesc: string; + + /** + * The time from creation time after which the API key expires, using RFC3339 representation. + * 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 + */ + readonly expires?: string; +} + +function isApiKeyConfig(obj: unknown): obj is ApiKeyConfig { + return (obj as ApiKeyConfig).apiKeyDesc !== undefined; +} + +type AuthModes = UserPoolConfig | ApiKeyConfig; + +/** + * Marker interface for the different authorization modes. + */ +export interface AuthorizationConfig { + /** + * Optional authorization configuration + * + * @default - API Key authorization + */ + readonly defaultAuthorization?: AuthModes; +} + /** * log-level for fields in AppSync */ @@ -90,11 +130,11 @@ export interface GraphQLApiProps { readonly name: string; /** - * Optional user pool authorizer configuration + * Optional authorization configuration * - * @default - Do not use Cognito auth + * @default - API Key authorization */ - readonly userPoolConfig?: UserPoolConfig; + readonly authorizationConfig?: AuthorizationConfig; /** * Logging configuration for this api @@ -145,7 +185,6 @@ export class GraphQLApi extends Construct { public readonly schema: CfnGraphQLSchema; private api: CfnGraphQLApi; - private authenticationType: string; constructor(scope: Construct, id: string, props: GraphQLApiProps) { super(scope, id); @@ -156,22 +195,9 @@ export class GraphQLApi extends Construct { apiLogsRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSAppSyncPushToCloudWatchLogs')); } - if (props.userPoolConfig) { - this.authenticationType = 'AMAZON_COGNITO_USER_POOLS'; - } else { - this.authenticationType = 'API_KEY'; - } - this.api = new CfnGraphQLApi(this, 'Resource', { name: props.name, - authenticationType: this.authenticationType, - ...props.userPoolConfig && { - userPoolConfig: { - userPoolId: props.userPoolConfig.userPool.userPoolId, - awsRegion: props.userPoolConfig.userPool.stack.region, - defaultAction: props.userPoolConfig.defaultAction ? props.userPoolConfig.defaultAction.toString() : 'ALLOW', - }, - }, + authenticationType: 'API_KEY', ...props.logConfig && { logConfig: { cloudWatchLogsRoleArn: apiLogsRole ? apiLogsRole.roleArn : undefined, @@ -186,6 +212,10 @@ export class GraphQLApi extends Construct { this.graphQlUrl = this.api.attrGraphQlUrl; this.name = this.api.name; + if (props.authorizationConfig) { + this.setupAuth(props.authorizationConfig); + } + let definition; if (props.schemaDefinition) { definition = props.schemaDefinition; @@ -230,6 +260,46 @@ export class GraphQLApi extends Construct { }); } + private setupAuth(auth: AuthorizationConfig) { + if (isUserPoolConfig(auth.defaultAuthorization)) { + const { authenticationType, userPoolConfig } = this.userPoolDescFrom(auth.defaultAuthorization); + this.api.authenticationType = authenticationType; + this.api.userPoolConfig = userPoolConfig; + } else if (isApiKeyConfig(auth.defaultAuthorization)) { + this.api.authenticationType = this.apiKeyDesc(auth.defaultAuthorization).authenticationType; + } + } + + private userPoolDescFrom(upConfig: UserPoolConfig): { authenticationType: string; userPoolConfig: CfnGraphQLApi.UserPoolConfigProperty } { + return { + authenticationType: 'AMAZON_COGNITO_USER_POOLS', + userPoolConfig: { + appIdClientRegex: upConfig.appIdClientRegex, + userPoolId: upConfig.userPool.userPoolId, + awsRegion: upConfig.userPool.stack.region, + defaultAction: upConfig.defaultAction ? upConfig.defaultAction.toString() : 'ALLOW', + } + }; + } + + private apiKeyDesc(akConfig: ApiKeyConfig): { authenticationType: string } { + let expires: number | undefined; + if (akConfig.expires) { + expires = new Date(akConfig.expires).valueOf(); + const now = Date.now(); + const days = (d: number) => 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); + } + new CfnApiKey(this, `${akConfig.apiKeyDesc || ''}ApiKey`, { + expires, + description: akConfig.apiKeyDesc, + apiId: this.apiId, + }); + return { authenticationType: 'API_KEY' }; + } } /** From 303924b10385109a924b5ef7befa2069dab4d300 Mon Sep 17 00:00:00 2001 From: Duarte Nunes Date: Wed, 12 Feb 2020 20:01:09 -0300 Subject: [PATCH 2/3] feat(appsync): allow specifying additional authorization modes Currently the AppSync L2 constructs don't provide a way to configure additional authorization modes. Add the ability to specify additional authorization modes, currently limited to Cognito user pools and API keys. Fixes #6247 Signed-off-by: Duarte Nunes --- packages/@aws-cdk/aws-appsync/README.md | 5 +++ .../@aws-cdk/aws-appsync/lib/graphqlapi.ts | 31 +++++++++++++++---- packages/@aws-cdk/aws-appsync/package.json | 9 +++++- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index 2e6b7af245644..4dff64bd8ed60 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -75,6 +75,11 @@ export class ApiStack extends Stack { userPool, defaultAction: UserPoolDefaultAction.ALLOW, }, + additionalAuthorizationModes: [ + { + apiKeyDesc: 'My API Key', + }, + ], }, schemaDefinitionFile: './schema.graphql', }); diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index 220e9aa95b3d0..36a87436aa4d3 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -6,6 +6,11 @@ import { Construct, Duration, IResolvable } from "@aws-cdk/core"; import { readFileSync } from "fs"; import { CfnApiKey, CfnDataSource, CfnGraphQLApi, CfnGraphQLSchema, CfnResolver } from "./appsync.generated"; +/** + * Marker interface for the different authorization modes. + */ +export interface AuthMode { } + /** * enum with all possible values for Cognito user-pool default actions */ @@ -23,7 +28,7 @@ export enum UserPoolDefaultAction { /** * Configuration for Cognito user-pools in AppSync */ -export interface UserPoolConfig { +export interface UserPoolConfig extends AuthMode { /** * The Cognito user pool to use as identity source @@ -50,7 +55,7 @@ function isUserPoolConfig(obj: unknown): obj is UserPoolConfig { /** * Configuration for API Key authorization in AppSync */ -export interface ApiKeyConfig { +export interface ApiKeyConfig extends AuthMode { /** * Unique description of the API key */ @@ -69,10 +74,8 @@ function isApiKeyConfig(obj: unknown): obj is ApiKeyConfig { return (obj as ApiKeyConfig).apiKeyDesc !== undefined; } -type AuthModes = UserPoolConfig | ApiKeyConfig; - /** - * Marker interface for the different authorization modes. + * Configuration of the API authorization modes. */ export interface AuthorizationConfig { /** @@ -80,7 +83,14 @@ export interface AuthorizationConfig { * * @default - API Key authorization */ - readonly defaultAuthorization?: AuthModes; + readonly defaultAuthorization?: AuthMode; + + /** + * Additional authorization modes + * + * @default - No other modes + */ + readonly additionalAuthorizationModes?: [AuthMode] } /** @@ -268,6 +278,15 @@ export class GraphQLApi extends Construct { } else if (isApiKeyConfig(auth.defaultAuthorization)) { this.api.authenticationType = this.apiKeyDesc(auth.defaultAuthorization).authenticationType; } + + this.api.additionalAuthenticationProviders = []; + for (const mode of (auth.additionalAuthorizationModes || [])) { + if (isUserPoolConfig(mode)) { + this.api.additionalAuthenticationProviders.push(this.userPoolDescFrom(mode)); + } else if (isApiKeyConfig(mode)) { + this.api.additionalAuthenticationProviders.push(this.apiKeyDesc(mode)); + } + } } private userPoolDescFrom(upConfig: UserPoolConfig): { authenticationType: string; userPoolConfig: CfnGraphQLApi.UserPoolConfigProperty } { diff --git a/packages/@aws-cdk/aws-appsync/package.json b/packages/@aws-cdk/aws-appsync/package.json index 2153ca23fccb6..359bcd95e638d 100644 --- a/packages/@aws-cdk/aws-appsync/package.json +++ b/packages/@aws-cdk/aws-appsync/package.json @@ -103,5 +103,12 @@ "engines": { "node": ">= 10.3.0" }, + "awslint": { + "exclude": [ + "no-unused-type:@aws-cdk/aws-appsync.ApiKeyConfig", + "no-unused-type:@aws-cdk/aws-appsync.UserPoolConfig", + "no-unused-type:@aws-cdk/aws-appsync.UserPoolDefaultAction" + ] + }, "stability": "experimental" -} \ No newline at end of file +} From 78ed6b82c69ac623dece74dbfa959d2011d09f94 Mon Sep 17 00:00:00 2001 From: Duarte Nunes Date: Wed, 12 Feb 2020 23:35:08 -0300 Subject: [PATCH 3/3] test(integ.graphql): test cognito and api key authorization Test using cognito user pools as the default authorization mode and an api key as the additional mode. Signed-off-by: Duarte Nunes --- .../test/integ.graphql.expected.json | 95 ++++++++++++++++++- .../aws-appsync/test/integ.graphql.ts | 20 +++- 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json index 1fad1ba0f55a6..2face78179375 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json @@ -1,10 +1,101 @@ { "Resources": { + "PoolsmsRoleC3352CE6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "awsappsyncintegPool5D14B05B" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cognito-idp.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sns-publish" + } + ] + } + }, + "PoolD3F588B8": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "Hello {username}, Your verification code is {####}", + "EmailVerificationSubject": "Verify your new account", + "LambdaConfig": {}, + "SmsConfiguration": { + "ExternalId": "awsappsyncintegPool5D14B05B", + "SnsCallerArn": { + "Fn::GetAtt": [ + "PoolsmsRoleC3352CE6", + "Arn" + ] + } + }, + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolName": "myPool", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "Hello {username}, Your verification code is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + } + }, "ApiF70053CD": { "Type": "AWS::AppSync::GraphQLApi", "Properties": { - "AuthenticationType": "API_KEY", - "Name": "demoapi" + "AuthenticationType": "AMAZON_COGNITO_USER_POOLS", + "Name": "demoapi", + "AdditionalAuthenticationProviders": [ + { + "AuthenticationType": "API_KEY" + } + ], + "UserPoolConfig": { + "AwsRegion": { + "Ref": "AWS::Region" + }, + "DefaultAction": "ALLOW", + "UserPoolId": { + "Ref": "PoolD3F588B8" + } + } + } + }, + "ApiMyAPIKeyApiKeyACDEE2CC": { + "Type": "AWS::AppSync::ApiKey", + "Properties": { + "ApiId": { + "Fn::GetAtt": [ + "ApiF70053CD", + "ApiId" + ] + }, + "Description": "My API Key" } }, "ApiSchema510EECD7": { diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts index a3d1cee096029..a2efb337e3a28 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts @@ -1,14 +1,32 @@ +import { UserPool } from '@aws-cdk/aws-cognito'; import { AttributeType, BillingMode, Table } from '@aws-cdk/aws-dynamodb'; import { App, Stack } from '@aws-cdk/core'; import { join } from 'path'; -import { GraphQLApi, KeyCondition, MappingTemplate, PrimaryKey, Values } from '../lib'; +import { GraphQLApi, KeyCondition, MappingTemplate, PrimaryKey, UserPoolDefaultAction, Values } from '../lib'; const app = new App(); const stack = new Stack(app, 'aws-appsync-integ'); +const userPool = new UserPool(stack, 'Pool', { + userPoolName: 'myPool', +}); + const api = new GraphQLApi(stack, 'Api', { name: `demoapi`, schemaDefinitionFile: join(__dirname, 'schema.graphql'), + authorizationConfig: { + defaultAuthorization: { + userPool, + defaultAction: UserPoolDefaultAction.ALLOW, + }, + additionalAuthorizationModes: [ + { + apiKeyDesc: 'My API Key', + // Can't specify a date because it will inevitably be in the past. + // expires: '2019-02-05T12:00:00Z', + }, + ], + }, }); const customerTable = new Table(stack, 'CustomerTable', {