diff --git a/packages/amplify-graphql-api-construct/.jsii b/packages/amplify-graphql-api-construct/.jsii index 27e1c38605..ad4308e809 100644 --- a/packages/amplify-graphql-api-construct/.jsii +++ b/packages/amplify-graphql-api-construct/.jsii @@ -4490,7 +4490,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 292 + "line": 299 }, "name": "addDynamoDbDataSource", "parameters": [ @@ -4539,7 +4539,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 304 + "line": 311 }, "name": "addElasticsearchDataSource", "parameters": [ @@ -4586,7 +4586,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 314 + "line": 321 }, "name": "addEventBridgeDataSource", "parameters": [ @@ -4633,7 +4633,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 396 + "line": 403 }, "name": "addFunction", "parameters": [ @@ -4668,7 +4668,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 325 + "line": 332 }, "name": "addHttpDataSource", "parameters": [ @@ -4716,7 +4716,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 336 + "line": 343 }, "name": "addLambdaDataSource", "parameters": [ @@ -4764,7 +4764,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 347 + "line": 354 }, "name": "addNoneDataSource", "parameters": [ @@ -4803,7 +4803,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 358 + "line": 365 }, "name": "addOpenSearchDataSource", "parameters": [ @@ -4851,7 +4851,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 371 + "line": 378 }, "name": "addRdsDataSource", "parameters": [ @@ -4918,7 +4918,7 @@ }, "locationInModule": { "filename": "src/amplify-graphql-api.ts", - "line": 387 + "line": 394 }, "name": "addResolver", "parameters": [ @@ -8959,5 +8959,5 @@ } }, "version": "1.15.1", - "fingerprint": "ONMG7fvcOLL6e3aEIqPma1mGRBz9zrwcR4rugtL8yQ0=" + "fingerprint": "L/GeKg7k8fmB+9I3VgG3cmEWsNBlijYsctatQrwnsuU=" } \ No newline at end of file diff --git a/packages/amplify-graphql-api-construct/src/__tests__/__functional__/authorizer.ts b/packages/amplify-graphql-api-construct/src/__tests__/__functional__/authorizer.ts new file mode 100644 index 0000000000..7420b06283 --- /dev/null +++ b/packages/amplify-graphql-api-construct/src/__tests__/__functional__/authorizer.ts @@ -0,0 +1,3 @@ +export const handler = async ({ authorizationToken }: { authorizationToken: string }): Promise<{ isAuthorized: boolean }> => ({ + isAuthorized: authorizationToken === 'letmein', +}); diff --git a/packages/amplify-graphql-api-construct/src/__tests__/__functional__/metadata.test.ts b/packages/amplify-graphql-api-construct/src/__tests__/__functional__/metadata.test.ts new file mode 100644 index 0000000000..2a947171b6 --- /dev/null +++ b/packages/amplify-graphql-api-construct/src/__tests__/__functional__/metadata.test.ts @@ -0,0 +1,477 @@ +import * as path from 'path'; +import { Duration, Stack } from 'aws-cdk-lib'; +import { ArnPrincipal, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { CfnIdentityPool, UserPool } from 'aws-cdk-lib/aws-cognito'; +import { Template } from 'aws-cdk-lib/assertions'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { mockSqlDataSourceStrategy } from '@aws-amplify/graphql-transformer-test-utils'; +import { AmplifyGraphqlApi } from '../../amplify-graphql-api'; +import { AmplifyGraphqlDefinition } from '../../amplify-graphql-definition'; + +describe('metrics metadata', () => { + describe('dataSources', () => { + test('default dynamodb', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo @model @auth(rules: [{ allow: public }]) { + description: String! + } + `), + authorizationModes: { + apiKeyConfig: { expires: Duration.days(7) }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "api_key", + "customOperations": "", + "dataSources": "dynamodb", + } + `); + }); + + test('amplify managed dynamodb', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString( + /* GraphQL */ ` + type Todo @model @auth(rules: [{ allow: public }]) { + description: String! + } + `, + { + dbType: 'DYNAMODB', + provisionStrategy: 'AMPLIFY_TABLE', + }, + ), + authorizationModes: { + apiKeyConfig: { expires: Duration.days(7) }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "api_key", + "customOperations": "", + "dataSources": "dynamodb", + } + `); + }); + + test('mysql', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString( + /* GraphQL */ ` + type Todo @model @auth(rules: [{ allow: public }]) { + id: ID! @primaryKey + description: String! + } + `, + mockSqlDataSourceStrategy({ dbType: 'MYSQL' }), + ), + authorizationModes: { + apiKeyConfig: { expires: Duration.days(7) }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "api_key", + "customOperations": "", + "dataSources": "mysql", + } + `); + }); + + test('postgres', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString( + /* GraphQL */ ` + type Todo @model @auth(rules: [{ allow: public }]) { + id: ID! @primaryKey + description: String! + } + `, + mockSqlDataSourceStrategy({ dbType: 'POSTGRES' }), + ), + authorizationModes: { + apiKeyConfig: { expires: Duration.days(7) }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "api_key", + "customOperations": "", + "dataSources": "postgres", + } + `); + }); + + test('dynamodb, mysql, and postgres', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.combine([ + AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type TodoDynamoDB @model @auth(rules: [{ allow: public }]) { + description: String! + } + `), + AmplifyGraphqlDefinition.fromString( + /* GraphQL */ ` + type TodoMySQL @model @auth(rules: [{ allow: public }]) { + id: ID! @primaryKey + description: String! + } + `, + mockSqlDataSourceStrategy({ dbType: 'MYSQL' }), + ), + AmplifyGraphqlDefinition.fromString( + /* GraphQL */ ` + type TodoPostgres @model @auth(rules: [{ allow: public }]) { + id: ID! @primaryKey + description: String! + } + `, + mockSqlDataSourceStrategy({ dbType: 'POSTGRES' }), + ), + ]), + authorizationModes: { + apiKeyConfig: { expires: Duration.days(7) }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "api_key", + "customOperations": "", + "dataSources": "dynamodb,mysql,postgres", + } + `); + }); + }); + + describe('authorizationModes', () => { + test('AWS_IAM', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo @model @auth(rules: [{ provider: iam, allow: public }, { provider: iam, allow: private }]) { + description: String! + } + `), + authorizationModes: { + iamConfig: { + identityPoolId: 'abc', + unauthenticatedUserRole: new Role(stack, 'testUnauthRole', { + assumedBy: new ArnPrincipal('aws:iam::1234:root'), + }), + authenticatedUserRole: new Role(stack, 'testAuthRole', { + assumedBy: new ArnPrincipal('aws:iam::1234:root'), + }), + }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "aws_iam", + "customOperations": "", + "dataSources": "dynamodb", + } + `); + }); + + test('AMAZON_COGNITO_USER_POOLS', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo @model @auth(rules: [{ allow: owner }]) { + description: String! + } + `), + authorizationModes: { + userPoolConfig: { + userPool: new UserPool(stack, 'testUserPool'), + }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "amazon_cognito_user_pools", + "customOperations": "", + "dataSources": "dynamodb", + } + `); + }); + + test('API_KEY', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo @model @auth(rules: [{ allow: public }]) { + description: String! + } + `), + authorizationModes: { + apiKeyConfig: { expires: Duration.days(7) }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "api_key", + "customOperations": "", + "dataSources": "dynamodb", + } + `); + }); + + test('AWS_LAMBDA', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo @model @auth(rules: [{ provider: function, allow: custom }]) { + description: String! + } + `), + authorizationModes: { + lambdaConfig: { + function: new NodejsFunction(stack, 'TestAuthorizer', { entry: path.join(__dirname, 'authorizer.ts') }), + ttl: Duration.days(7), + }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "aws_lambda", + "customOperations": "", + "dataSources": "dynamodb", + } + `); + }); + + test('OPENID_CONNECT', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo @model @auth(rules: [{ provider: oidc, allow: owner }]) { + description: String! + } + `), + authorizationModes: { + oidcConfig: { + oidcProviderName: 'testProvider', + oidcIssuerUrl: 'https://test.client/', + clientId: 'testClient', + tokenExpiryFromAuth: Duration.minutes(5), + tokenExpiryFromIssue: Duration.minutes(5), + }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "openid_connect", + "customOperations": "", + "dataSources": "dynamodb", + } + `); + }); + + test('AMAZON_COGNITO_IDENTITY_POOLS', () => { + const stack = new Stack(); + const identityPool = new CfnIdentityPool(stack, 'TestIdentityPool', { allowUnauthenticatedIdentities: true }); + const appsync = new ServicePrincipal('appsync.amazonaws.com'); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo @model @auth(rules: [{ provider: iam, allow: public }, { provider: iam, allow: private }]) { + description: String! + } + `), + authorizationModes: { + identityPoolConfig: { + identityPoolId: identityPool.logicalId, + authenticatedUserRole: new Role(stack, 'AuthRole', { assumedBy: appsync }), + unauthenticatedUserRole: new Role(stack, 'UnauthRole', { assumedBy: appsync }), + }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "amazon_cognito_identity_pools", + "customOperations": "", + "dataSources": "dynamodb", + } + `); + }); + + test('multiple', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo + @model + @auth( + rules: [ + { allow: public } + { provider: iam, allow: public } + { provider: iam, allow: private } + { provider: function, allow: custom } + ] + ) { + description: String! + } + `), + authorizationModes: { + defaultAuthorizationMode: 'API_KEY', + // assert output authorizationModes is sorted correctly + // aws_lambda should be last in list + lambdaConfig: { + function: new NodejsFunction(stack, 'TestAuthorizer', { entry: path.join(__dirname, 'authorizer.ts') }), + ttl: Duration.days(7), + }, + apiKeyConfig: { expires: Duration.days(7) }, + iamConfig: { + identityPoolId: 'abc', + unauthenticatedUserRole: new Role(stack, 'testUnauthRole', { + assumedBy: new ArnPrincipal('aws:iam::1234:root'), + }), + authenticatedUserRole: new Role(stack, 'testAuthRole', { + assumedBy: new ArnPrincipal('aws:iam::1234:root'), + }), + }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "api_key,aws_iam,aws_lambda", + "customOperations": "", + "dataSources": "dynamodb", + } + `); + }); + }); + + describe('customOperations', () => { + test('queries', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo @model @auth(rules: [{ allow: public }]) { + description: String! + } + + type Query { + getCustomTodos: [Todo] + } + `), + authorizationModes: { + apiKeyConfig: { expires: Duration.days(7) }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "api_key", + "customOperations": "queries", + "dataSources": "dynamodb", + } + `); + }); + + test('mutations', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo @model @auth(rules: [{ allow: public }]) { + description: String! + } + + type Mutation { + addCustomTodo: Todo + } + `), + authorizationModes: { + apiKeyConfig: { expires: Duration.days(7) }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "api_key", + "customOperations": "mutations", + "dataSources": "dynamodb", + } + `); + }); + + test('subscriptions', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo @model @auth(rules: [{ allow: public }]) { + description: String! + } + + type Subscription { + onCreateCustomTodo: Todo + } + `), + authorizationModes: { + apiKeyConfig: { expires: Duration.days(7) }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "api_key", + "customOperations": "subscriptions", + "dataSources": "dynamodb", + } + `); + }); + + test('mutations, queries, and subscriptions', () => { + const stack = new Stack(); + new AmplifyGraphqlApi(stack, 'TestApi', { + definition: AmplifyGraphqlDefinition.fromString(/* GraphQL */ ` + type Todo @model @auth(rules: [{ allow: public }]) { + description: String! + } + + type Mutation { + addCustomTodo: Todo + } + + type Query { + getCustomTodos: [Todo] + } + + type Subscription { + onCreateCustomTodo: Todo + } + `), + authorizationModes: { + apiKeyConfig: { expires: Duration.days(7) }, + }, + }); + const template = Template.fromStack(stack); + expect(JSON.parse(template.toJSON().Description).metadata).toMatchInlineSnapshot(` + Object { + "authorizationModes": "api_key", + "customOperations": "queries,mutations,subscriptions", + "dataSources": "dynamodb", + } + `); + }); + }); +}); diff --git a/packages/amplify-graphql-api-construct/src/amplify-graphql-api.ts b/packages/amplify-graphql-api-construct/src/amplify-graphql-api.ts index 09405a4b39..2d77a15c9a 100644 --- a/packages/amplify-graphql-api-construct/src/amplify-graphql-api.ts +++ b/packages/amplify-graphql-api-construct/src/amplify-graphql-api.ts @@ -35,7 +35,6 @@ import type { IBackendOutputStorageStrategy, AddFunctionProps, DataStoreConfiguration, - IAmplifyGraphqlDefinition, } from './types'; import { convertAuthorizationModesToTransformerAuthConfig, @@ -50,6 +49,7 @@ import { } from './internal'; import { getStackForScope, walkAndProcessNodes } from './internal/construct-tree'; import { getDataSourceStrategiesProvider } from './internal/data-source-config'; +import { getMetadataDataSources, getMetadataAuthorizationModes, getMetadataCustomOperations } from './internal/metadata'; /** * L3 Construct which invokes the Amplify Transformer Pattern over an input Graphql Schema. @@ -168,11 +168,18 @@ export class AmplifyGraphqlApi extends Construct { this.dataStoreConfiguration = dataStoreConfiguration || conflictResolution; - const dataSources = getMetadataDataSources(definition); + const attributionMetadata = { + dataSources: getMetadataDataSources(definition), + authorizationModes: getMetadataAuthorizationModes(authorizationModes), + customOperations: getMetadataCustomOperations(definition), + }; - new AttributionMetadataStorage().storeAttributionMetadata(Stack.of(scope), this.stackType, path.join(__dirname, '..', 'package.json'), { - dataSources, - }); + new AttributionMetadataStorage().storeAttributionMetadata( + Stack.of(scope), + this.stackType, + path.join(__dirname, '..', 'package.json'), + attributionMetadata, + ); validateAuthorizationModes(authorizationModes); const { authConfig, authSynthParameters } = convertAuthorizationModesToTransformerAuthConfig(authorizationModes); @@ -420,10 +427,3 @@ const validateNoOtherAmplifyGraphqlApiInStack = (scope: Construct): void => { throw new Error('Only one AmplifyGraphqlApi is expected in a stack. Place the AmplifyGraphqlApis in separate nested stacks.'); } }; - -const getMetadataDataSources = (definition: IAmplifyGraphqlDefinition): string => { - const dataSourceDbTypes = Object.values(definition.dataSourceStrategies).map((strategy) => strategy.dbType.toLocaleLowerCase()); - const customSqlDbTypes = (definition.customSqlDataSourceStrategies ?? []).map((strategy) => strategy.strategy.dbType.toLocaleLowerCase()); - const dataSources = [...new Set([...dataSourceDbTypes, ...customSqlDbTypes])].sort(); - return dataSources.join(','); -}; diff --git a/packages/amplify-graphql-api-construct/src/internal/metadata.ts b/packages/amplify-graphql-api-construct/src/internal/metadata.ts new file mode 100644 index 0000000000..7b3b72dfab --- /dev/null +++ b/packages/amplify-graphql-api-construct/src/internal/metadata.ts @@ -0,0 +1,40 @@ +import { AuthorizationModes, IAmplifyGraphqlDefinition } from '../types'; + +export const getMetadataDataSources = (definition: IAmplifyGraphqlDefinition): string => { + const dataSourceDbTypes = Object.values(definition.dataSourceStrategies).map((strategy) => strategy.dbType.toLocaleLowerCase()); + const customSqlDbTypes = (definition.customSqlDataSourceStrategies ?? []).map((strategy) => strategy.strategy.dbType.toLocaleLowerCase()); + const dataSources = [...new Set([...dataSourceDbTypes, ...customSqlDbTypes])].sort(); + return dataSources.join(','); +}; + +export const getMetadataAuthorizationModes = (authorizationModes: AuthorizationModes): string => { + const configKeyToAuthMode: Record = { + iamConfig: 'aws_iam', + oidcConfig: 'openid_connect', + identityPoolConfig: 'amazon_cognito_identity_pools', + userPoolConfig: 'amazon_cognito_user_pools', + apiKeyConfig: 'api_key', + lambdaConfig: 'aws_lambda', + }; + const authModes = Object.keys(authorizationModes) + .map((mode) => configKeyToAuthMode[mode]) + // remove values not found in mapping + .filter((mode) => !!mode) + .sort(); + return authModes.join(','); +}; + +export const getMetadataCustomOperations = (definition: IAmplifyGraphqlDefinition): string => { + const customOperations: string[] = []; + if (definition.schema.includes('type Query')) { + customOperations.push('queries'); + } + if (definition.schema.includes('type Mutation')) { + customOperations.push('mutations'); + } + if (definition.schema.includes('type Subscription')) { + customOperations.push('subscriptions'); + } + + return customOperations.join(','); +};