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(appsync): add dependency between apikey and schema #9737

Merged
merged 13 commits into from
Aug 19, 2020
115 changes: 39 additions & 76 deletions packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,21 +397,21 @@ export class GraphQLApi extends GraphqlApiBase {

/**
* the configured API key, if present
*
* @default - no api key
*/
public get apiKey(): string | undefined {
return this._apiKey;
}
public readonly apiKey?: string;

private schemaMode: SchemaDefinition;
private api: CfnGraphQLApi;
private _apiKey?: string;
private _apiKey?: CfnApiKey;

constructor(scope: Construct, id: string, props: GraphQLApiProps) {
super(scope, id);

this.validateAuthorizationProps(props);
const defaultAuthorizationType =
props.authorizationConfig?.defaultAuthorization?.authorizationType ||
props.authorizationConfig?.defaultAuthorization?.authorizationType ??
BryanPan342 marked this conversation as resolved.
Show resolved Hide resolved
AuthorizationType.API_KEY;

let apiLogsRole;
Expand Down Expand Up @@ -462,22 +462,24 @@ export class GraphQLApi extends GraphqlApiBase {
this.graphQlUrl = this.api.attrGraphQlUrl;
this.name = this.api.name;
this.schemaMode = props.schemaDefinition;
this.schema = this.defineSchema(props.schemaDefinitionFile);

if (
defaultAuthorizationType === AuthorizationType.API_KEY ||
if (defaultAuthorizationType === AuthorizationType.API_KEY ||
props.authorizationConfig?.additionalAuthorizationModes?.some(
(authMode) => authMode.authorizationType === AuthorizationType.API_KEY
)
(authMode) => authMode.authorizationType === AuthorizationType.API_KEY)
) {
const apiKeyConfig: ApiKeyConfig = props.authorizationConfig
?.defaultAuthorization?.apiKeyConfig || {
name: 'DefaultAPIKey',
description: 'Default API Key created by CDK',
};
// create a variable for apiKeyConfig if one has been specified by the user
// first check is for default authorization
// second check is for additional authorization modes
const apiKeyConfig = props.authorizationConfig?.defaultAuthorization?.apiKeyConfig ??
props.authorizationConfig?.additionalAuthorizationModes?.
find((mode: AuthorizationMode) => {
return mode.authorizationType === AuthorizationType.API_KEY && mode.apiKeyConfig;
})?.apiKeyConfig;
this._apiKey = this.createAPIKey(apiKeyConfig);
this._apiKey.addDependsOn(this.schema);
this.apiKey = this._apiKey.attrApiKey;
}

this.schema = this.defineSchema(props.schemaDefinitionFile);
}

/**
Expand Down Expand Up @@ -531,65 +533,27 @@ export class GraphQLApi extends GraphqlApiBase {
}

private validateAuthorizationProps(props: GraphQLApiProps) {
const defaultAuthorizationType =
props.authorizationConfig?.defaultAuthorization?.authorizationType ||
AuthorizationType.API_KEY;
const defaultMode = props.authorizationConfig?.defaultAuthorization ?? {
authorizationType: AuthorizationType.API_KEY,
};
const additionalModes = props.authorizationConfig?.additionalAuthorizationModes ?? [];
const allModes = [defaultMode, ...additionalModes];

if (
defaultAuthorizationType === AuthorizationType.OIDC &&
!props.authorizationConfig?.defaultAuthorization?.openIdConnectConfig
) {
throw new Error('Missing default OIDC Configuration');
}
allModes.map((mode) => {
if(mode.authorizationType === AuthorizationType.OIDC && !mode.openIdConnectConfig){
throw new Error('Missing default OIDC Configuration');
}
if(mode.authorizationType === AuthorizationType.USER_POOL && !mode.userPoolConfig){
throw new Error('Missing default OIDC Configuration');
}
});

if (
defaultAuthorizationType === AuthorizationType.USER_POOL &&
!props.authorizationConfig?.defaultAuthorization?.userPoolConfig
) {
throw new Error('Missing default User Pool Configuration');
if(allModes.filter((mode) => mode.authorizationType === AuthorizationType.API_KEY).length > 1){
throw new Error('You can\'t duplicate API_KEY configuration. See https://docs.aws.amazon.com/appsync/latest/devguide/security.html');
}

if (props.authorizationConfig?.additionalAuthorizationModes) {
props.authorizationConfig.additionalAuthorizationModes.forEach(
(authorizationMode) => {
if (
authorizationMode.authorizationType === AuthorizationType.API_KEY &&
defaultAuthorizationType === AuthorizationType.API_KEY
) {
throw new Error(
"You can't duplicate API_KEY in additional authorization config. See https://docs.aws.amazon.com/appsync/latest/devguide/security.html",
);
}

if (
authorizationMode.authorizationType === AuthorizationType.IAM &&
defaultAuthorizationType === AuthorizationType.IAM
) {
throw new Error(
"You can't duplicate IAM in additional authorization config. See https://docs.aws.amazon.com/appsync/latest/devguide/security.html",
);
}

if (
authorizationMode.authorizationType === AuthorizationType.OIDC &&
!authorizationMode.openIdConnectConfig
) {
throw new Error(
'Missing OIDC Configuration inside an additional authorization mode',
);
}

if (
authorizationMode.authorizationType ===
AuthorizationType.USER_POOL &&
!authorizationMode.userPoolConfig
) {
throw new Error(
'Missing User Pool Configuration inside an additional authorization mode',
);
}
},
);
if(allModes.filter((mode) => mode.authorizationType === AuthorizationType.IAM).length > 1){
throw new Error('You can\'t duplicate IAM configuration. See https://docs.aws.amazon.com/appsync/latest/devguide/security.html');
}
}

Expand Down Expand Up @@ -625,9 +589,9 @@ export class GraphQLApi extends GraphqlApiBase {
};
}

private createAPIKey(config: ApiKeyConfig) {
private createAPIKey(config?: ApiKeyConfig) {
let expires: number | undefined;
if (config.expires) {
if (config?.expires) {
expires = new Date(config.expires).valueOf();
const days = (d: number) =>
Date.now() + Duration.days(d).toMilliseconds();
Expand All @@ -636,12 +600,11 @@ export class GraphQLApi extends GraphqlApiBase {
}
expires = Math.round(expires / 1000);
}
const key = new CfnApiKey(this, `${config.name || 'DefaultAPIKey'}ApiKey`, {
return new CfnApiKey(this, `${config?.name || 'Default'}ApiKey`, {
expires,
description: config.description || 'Default API Key created by CDK',
description: config?.description,
apiId: this.apiId,
});
return key.attrApiKey;
}

private formatAdditionalAuthorizationModes(
Expand Down
121 changes: 121 additions & 0 deletions packages/@aws-cdk/aws-appsync/test/appsync-apikey.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '@aws-cdk/assert/jest';
import * as path from 'path';
import * as cognito from '@aws-cdk/aws-cognito';
import * as cdk from '@aws-cdk/core';
import * as appsync from '../lib';

Expand Down Expand Up @@ -82,4 +83,124 @@ describe('AppSync Authorization Config', () => {
// THEN
expect(stack).not.toHaveResource('AWS::AppSync::ApiKey');
});

test('appsync creates configured api key with additionalAuthorizationModes', () => {
// GIVEN
const stack = new cdk.Stack();

// WHEN
new appsync.GraphQLApi(stack, 'api', {
name: 'api',
schemaDefinition: appsync.SchemaDefinition.FILE,
schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.IAM,
},
additionalAuthorizationModes: [{
authorizationType: appsync.AuthorizationType.API_KEY,
apiKeyConfig: {
description: 'Custom Description',
},
}],
},
});

// THEN
expect(stack).toHaveResourceLike('AWS::AppSync::ApiKey', {
Description: 'Custom Description',
});
});

test('appsync creates configured api key with additionalAuthorizationModes (not as first element)', () => {
// GIVEN
const stack = new cdk.Stack();
const userPool = new cognito.UserPool(stack, 'myPool');

// WHEN
new appsync.GraphQLApi(stack, 'api', {
name: 'api',
schemaDefinition: appsync.SchemaDefinition.FILE,
schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.IAM,
},
additionalAuthorizationModes: [
{
authorizationType: appsync.AuthorizationType.USER_POOL,
userPoolConfig: {
userPool,
},
},
{
authorizationType: appsync.AuthorizationType.API_KEY,
apiKeyConfig: {
description: 'Custom Description',
},
},
],
},
});

// THEN
expect(stack).toHaveResourceLike('AWS::AppSync::ApiKey', {
Description: 'Custom Description',
});
});

test('appsync fails when multiple API_KEY auth modes', () => {
// GIVEN
const stack = new cdk.Stack();

// WHEN
const when = () => {
new appsync.GraphQLApi(stack, 'api', {
name: 'api',
schemaDefinition: appsync.SchemaDefinition.FILE,
schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
},
additionalAuthorizationModes: [{
authorizationType: appsync.AuthorizationType.API_KEY,
}],
},
});
};

// THEN
expect(when).toThrowError('You can\'t duplicate API_KEY configuration. See https://docs.aws.amazon.com/appsync/latest/devguide/security.html');
});

test('appsync fails when multiple API_KEY auth modes in additionalXxx', () => {
// GIVEN
const stack = new cdk.Stack();

// WHEN
const when = () => {
new appsync.GraphQLApi(stack, 'api', {
name: 'api',
schemaDefinition: appsync.SchemaDefinition.FILE,
schemaDefinitionFile: path.join(__dirname, 'appsync.test.graphql'),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.IAM,
},
additionalAuthorizationModes: [
{
authorizationType: appsync.AuthorizationType.API_KEY,
},
{
authorizationType: appsync.AuthorizationType.API_KEY,
},
],
},
});
};

// THEN
expect(when).toThrowError('You can\'t duplicate API_KEY configuration. See https://docs.aws.amazon.com/appsync/latest/devguide/security.html');
});
});
18 changes: 10 additions & 8 deletions packages/@aws-cdk/aws-appsync/test/integ.api-import.expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,31 @@
"Name": "baseApi"
}
},
"baseApiDefaultAPIKeyApiKey4804ACE5": {
"Type": "AWS::AppSync::ApiKey",
"baseApiSchemaB12C7BB0": {
"Type": "AWS::AppSync::GraphQLSchema",
"Properties": {
"ApiId": {
"Fn::GetAtt": [
"baseApiCDA4D43A",
"ApiId"
]
},
"Description": "Default API Key created by CDK"
"Definition": "type test {\n version: String!\n}\n\ntype Query {\n getTests: [ test! ]!\n}\n\ntype Mutation {\n addTest(version: String!): test\n}\n"
}
},
"baseApiSchemaB12C7BB0": {
"Type": "AWS::AppSync::GraphQLSchema",
"baseApiDefaultApiKeyA3A0A16A": {
"Type": "AWS::AppSync::ApiKey",
"Properties": {
"ApiId": {
"Fn::GetAtt": [
"baseApiCDA4D43A",
"ApiId"
]
},
"Definition": "type test {\n version: String!\n}\n\ntype Query {\n getTests: [ test! ]!\n}\n\ntype Mutation {\n addTest(version: String!): test\n}\n"
}
}
},
"DependsOn": [
"baseApiSchemaB12C7BB0"
]
}
},
"Outputs": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,31 @@
"Name": "api"
}
},
"codefirstapiDefaultAPIKeyApiKeyB73DF94B": {
"Type": "AWS::AppSync::ApiKey",
"codefirstapiSchema148B6CDE": {
"Type": "AWS::AppSync::GraphQLSchema",
"Properties": {
"ApiId": {
"Fn::GetAtt": [
"codefirstapi1A3CC7D2",
"ApiId"
]
},
"Description": "Default API Key created by CDK"
"Definition": "type Planet {\n name: String\n diameter: Int\n rotationPeriod: Int\n orbitalPeriod: Int\n gravity: String\n population: [String]\n climates: [String]\n terrains: [String]\n surfaceWater: Float\n created: String\n edited: String\n id: ID!\n}\ntype Species {\n name: String\n classification: String\n designation: String\n averageHeight: Float\n averageLifespan: Int\n eyeColors: [String]\n hairColors: [String]\n skinColors: [String]\n language: String\n homeworld: Planet\n created: String\n edited: String\n id: ID!\n}\n\ntype Query {\n getPlanets: [Planet]\n}\n"
}
},
"codefirstapiSchema148B6CDE": {
"Type": "AWS::AppSync::GraphQLSchema",
"codefirstapiDefaultApiKey89863A80": {
"Type": "AWS::AppSync::ApiKey",
"Properties": {
"ApiId": {
"Fn::GetAtt": [
"codefirstapi1A3CC7D2",
"ApiId"
]
},
"Definition": "type Planet {\n name: String\n diameter: Int\n rotationPeriod: Int\n orbitalPeriod: Int\n gravity: String\n population: [String]\n climates: [String]\n terrains: [String]\n surfaceWater: Float\n created: String\n edited: String\n id: ID!\n}\ntype Species {\n name: String\n classification: String\n designation: String\n averageHeight: Float\n averageLifespan: Int\n eyeColors: [String]\n hairColors: [String]\n skinColors: [String]\n language: String\n homeworld: Planet\n created: String\n edited: String\n id: ID!\n}\n\ntype Query {\n getPlanets: [Planet]\n}\n"
}
}
},
"DependsOn": [
"codefirstapiSchema148B6CDE"
]
},
"codefirstapiplanetsServiceRole2F4AA8E7": {
"Type": "AWS::IAM::Role",
Expand Down
Loading