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(appsync): extend authorization configuration #6260

Merged
merged 4 commits into from
Feb 25, 2020
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
13 changes: 10 additions & 3 deletions packages/@aws-cdk/aws-appsync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,16 @@ export class ApiStack extends Stack {
logConfig: {
fieldLogLevel: FieldLogLevel.ALL,
},
userPoolConfig: {
userPool,
defaultAction: UserPoolDefaultAction.ALLOW,
authorizationConfig: {
defaultAuthorization: {
userPool,
defaultAction: UserPoolDefaultAction.ALLOW,
},
additionalAuthorizationModes: [
{
apiKeyDesc: 'My API Key',
},
],
},
schemaDefinitionFile: './schema.graphql',
});
Expand Down
131 changes: 110 additions & 21 deletions packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ 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";

/**
* Marker interface for the different authorization modes.
*/
export interface AuthMode { }

/**
* enum with all possible values for Cognito user-pool default actions
Expand All @@ -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
Expand All @@ -43,6 +48,51 @@ 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 extends AuthMode {
/**
* 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;
}

/**
* Configuration of the API authorization modes.
*/
export interface AuthorizationConfig {
/**
* Optional authorization configuration
*
* @default - API Key authorization
*/
readonly defaultAuthorization?: AuthMode;

/**
* Additional authorization modes
*
* @default - No other modes
*/
readonly additionalAuthorizationModes?: [AuthMode]
}

/**
* log-level for fields in AppSync
*/
Expand Down Expand Up @@ -90,11 +140,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
Expand Down Expand Up @@ -145,7 +195,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);
Expand All @@ -156,22 +205,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,
Expand All @@ -186,6 +222,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;
Expand Down Expand Up @@ -230,6 +270,55 @@ 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;
}

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 } {
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' };
}
}

/**
Expand Down
9 changes: 8 additions & 1 deletion packages/@aws-cdk/aws-appsync/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
95 changes: 93 additions & 2 deletions packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
20 changes: 19 additions & 1 deletion packages/@aws-cdk/aws-appsync/test/integ.graphql.ts
Original file line number Diff line number Diff line change
@@ -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', {
Expand Down