-
Notifications
You must be signed in to change notification settings - Fork 89
chore: migrate pg model dynamic auth e2e test in gen2 cdk #2920
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
Changes from 30 commits
5ade104
90019a0
ae910c3
123a480
7f5730f
5fd253a
153cfaa
73b25d6
1f18fd7
1a03e65
8536347
3f72891
b5a22de
69ac8d2
6e68ea0
691f1f5
04c21ef
a409c75
17c7e60
61e2404
d6645da
be1759e
65ecb07
9d377ed
fab3911
d30adf8
268467c
311a0a3
e98c99c
f4d541c
2bbcd68
51e2f97
5f16873
c9cbece
de91277
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,310 @@ | ||
| #!/usr/bin/env node | ||
| import 'source-map-support/register'; | ||
| import { App, Stack, CfnOutput, Duration, RemovalPolicy } from 'aws-cdk-lib'; | ||
| // @ts-ignore | ||
| import { | ||
| AmplifyGraphqlApi, | ||
| AmplifyGraphqlDefinition, | ||
| AuthorizationModes, | ||
| IAmplifyGraphqlDefinition, | ||
| SqlModelDataSourceDbConnectionConfig, | ||
| ModelDataSourceStrategySqlDbType, | ||
| PartialTranslationBehavior, | ||
| } from '@aws-amplify/graphql-api-construct'; | ||
| import { AccountPrincipal, Effect, PolicyDocument, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam'; | ||
| import { CfnUserPoolGroup, UserPool, UserPoolClient, UserPoolTriggers } from 'aws-cdk-lib/aws-cognito'; | ||
| import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda'; | ||
|
|
||
| // #region Utilities | ||
|
|
||
| enum AUTH_MODE { | ||
| API_KEY = 'API_KEY', | ||
| AWS_IAM = 'AWS_IAM', | ||
| AMAZON_COGNITO_USER_POOLS = 'AMAZON_COGNITO_USER_POOLS', | ||
| OPENID_CONNECT = 'OPENID_CONNECT', | ||
| AWS_LAMBDA = 'AWS_LAMBDA', | ||
| } | ||
|
|
||
| enum SCHEMA { | ||
| DEFAULT = /* GraphQL */ ` | ||
| type Todo @model @refersTo(name: "todos") { | ||
| id: ID! @primaryKey | ||
| description: String! | ||
| } | ||
| `, | ||
| } | ||
|
|
||
| interface DBDetails { | ||
| dbConfig: { | ||
| endpoint: string; | ||
| port: number; | ||
| dbName: string; | ||
| dbType: ModelDataSourceStrategySqlDbType; | ||
| strategyName: string; | ||
| vpcConfig: { | ||
| vpcId: string; | ||
| securityGroupIds: string[]; | ||
| subnetAvailabilityZones: [ | ||
| { | ||
| subnetId: string; | ||
| availabilityZone: string; | ||
| }, | ||
| ]; | ||
| }; | ||
| }; | ||
| dbConnectionConfig: SqlModelDataSourceDbConnectionConfig; | ||
| } | ||
|
|
||
| interface StackConfig { | ||
| /** | ||
| * The AppSync GraphQL schema, provided as string for AmplifyGraphqlApi Construct definition. | ||
| */ | ||
| schema: string; | ||
|
|
||
| /** | ||
| * The AuthorizationMode type for AmplifyGraphqlApi Construct. | ||
| */ | ||
| authMode?: AUTH_MODE; | ||
|
|
||
| /** | ||
| * If true, disable Cognito User Pool/Auth resources creation and only use API Key auth in sandbox mode. | ||
| */ | ||
| useSandbox?: boolean; | ||
|
|
||
| /** | ||
| * Cognito User Pool groups to create when provisioning the User Pool. | ||
| * | ||
| * **NOTE** | ||
| * Provide at least two group names for setup and testing purposes. | ||
| */ | ||
| userGroups?: string[]; | ||
|
|
||
| /** | ||
| * The OIDC options/config when using OIDC AuthorizationMode for AmplifyGraphqlApi Construct. | ||
| * | ||
| * @property {Record<string, string>} [triggers] - UserPoolTriggers for Cognito User Pool when provisioning the User Pool as OIDC provider. | ||
| * - key: trigger name e.g. 'preTokenGeneration' | ||
| * - value: the lambda function code inlined as a string | ||
| * | ||
| * **NOTE** | ||
| * - Only applicable when AuthorizationMode is set to OIDC. | ||
| * - Currently only supports Cognito User Pools as the simulated OIDC provider for E2E test. | ||
| * - Currently only supports JavaScript as the lambda function code, with Node.js runtime version 18.x. | ||
| * - Inline code needs to export the handler function as `handler` as `index.handler` would be used as the handler path. | ||
| */ | ||
| oidcOptions?: { | ||
| triggers: Record<string, string>; | ||
| }; | ||
| } | ||
|
|
||
| const createApiDefinition = (): IAmplifyGraphqlDefinition => { | ||
| const schema = stackConfig.schema ?? SCHEMA.DEFAULT; | ||
|
|
||
| return AmplifyGraphqlDefinition.fromString(schema, { | ||
| name: dbDetails.dbConfig.strategyName, | ||
| dbType: dbDetails.dbConfig.dbType, | ||
| vpcConfiguration: { | ||
| vpcId: dbDetails.dbConfig.vpcConfig.vpcId, | ||
| securityGroupIds: dbDetails.dbConfig.vpcConfig.securityGroupIds, | ||
| subnetAvailabilityZoneConfig: dbDetails.dbConfig.vpcConfig.subnetAvailabilityZones, | ||
| }, | ||
| dbConnectionConfig: { | ||
| ...dbDetails.dbConnectionConfig, | ||
| }, | ||
| sqlLambdaProvisionedConcurrencyConfig: { | ||
| provisionedConcurrentExecutions: 2, | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| const createAuthorizationModes = (): AuthorizationModes => { | ||
| const auth = stackConfig.authMode ?? AUTH_MODE.API_KEY; | ||
| let authorizationModes: AuthorizationModes; | ||
|
|
||
| switch (auth) { | ||
| case AUTH_MODE.API_KEY: { | ||
| authorizationModes = { | ||
| defaultAuthorizationMode: 'API_KEY', | ||
| apiKeyConfig: { expires: Duration.days(7) }, | ||
| }; | ||
|
|
||
| break; | ||
| } | ||
| case AUTH_MODE.AMAZON_COGNITO_USER_POOLS: { | ||
| const { userPool, userPoolClient } = createUserPool(dbDetails.dbConfig.dbName); | ||
|
|
||
| authorizationModes = { | ||
| defaultAuthorizationMode: 'AMAZON_COGNITO_USER_POOLS', | ||
| userPoolConfig: { | ||
| userPool, | ||
| }, | ||
| apiKeyConfig: { expires: Duration.days(2) }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need the API key to be valid for 2 days? Especially since we teardown these apps post test run, I would recommend setting it to 1 hr to begin with.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Line 75 of file Setting expiration to hours would cause CDK deployment to fail with error message:
We could adjust the construct time conversion if needed to give test a more tight time boundary.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That might be the right way to go forward here. |
||
| }; | ||
|
|
||
| break; | ||
| } | ||
| case AUTH_MODE.OPENID_CONNECT: { | ||
| const { userPool, userPoolClient } = createUserPool(dbDetails.dbConfig.dbName, stackConfig.oidcOptions?.triggers); | ||
|
|
||
| authorizationModes = { | ||
| defaultAuthorizationMode: 'OPENID_CONNECT', | ||
| oidcConfig: { | ||
| oidcProviderName: 'awscognitouserpool', | ||
| oidcIssuerUrl: `https://cognito-idp.${stack.region}.amazonaws.com/${userPool.userPoolId}`, | ||
| clientId: userPoolClient.userPoolClientId, | ||
| tokenExpiryFromAuth: Duration.hours(1), | ||
| tokenExpiryFromIssue: Duration.hours(1), | ||
| }, | ||
| }; | ||
|
|
||
| break; | ||
| } | ||
| default: { | ||
| throw new Error(`Unsupported auth mode: ${stackConfig.authMode}`); | ||
| } | ||
| } | ||
|
|
||
| return authorizationModes; | ||
| }; | ||
|
|
||
| const createTranslationBehavior = (): PartialTranslationBehavior => { | ||
| const translationBehavior: PartialTranslationBehavior = { | ||
| sandboxModeEnabled: stackConfig.useSandbox ?? false, | ||
| }; | ||
| return translationBehavior; | ||
| }; | ||
|
|
||
| const createUserPool = (prefix: string, triggers?: Record<string, string>): { userPool: UserPool; userPoolClient: UserPoolClient } => { | ||
| const userPool = new UserPool(stack, `${prefix}UserPool`, { | ||
| signInAliases: { | ||
| username: true, | ||
| email: false, | ||
| }, | ||
| selfSignUpEnabled: true, | ||
| autoVerify: { email: true }, | ||
| standardAttributes: { | ||
| email: { | ||
| required: true, | ||
| mutable: false, | ||
| }, | ||
| }, | ||
| lambdaTriggers: triggers ? createUserPoolTriggers(triggers) : {}, | ||
| }); | ||
| userPool.applyRemovalPolicy(RemovalPolicy.DESTROY); | ||
|
|
||
| stackConfig.userGroups?.forEach((group) => { | ||
| new CfnUserPoolGroup(userPool, `Group${group}`, { | ||
| userPoolId: userPool.userPoolId, | ||
| groupName: group, | ||
| }); | ||
| }); | ||
|
|
||
| const userPoolClient = userPool.addClient(`${prefix}UserPoolClient`, { | ||
| authFlows: { | ||
| userPassword: true, | ||
| userSrp: true, | ||
| }, | ||
| }); | ||
|
|
||
| new CfnOutput(stack, 'userPoolId', { value: userPool.userPoolId }); | ||
| new CfnOutput(stack, 'webClientId', { value: userPoolClient.userPoolClientId }); | ||
|
|
||
| return { userPool, userPoolClient }; | ||
| }; | ||
|
|
||
| const createUserPoolTriggers = (triggers: Record<string, string>): UserPoolTriggers => { | ||
| const userPoolTriggers: UserPoolTriggers = {}; | ||
|
|
||
| Object.keys(triggers).forEach((triggerName) => { | ||
| userPoolTriggers[triggerName] = createLambdaFunction(triggerName, triggers[triggerName]); | ||
| }); | ||
|
|
||
| return userPoolTriggers; | ||
| }; | ||
|
|
||
| const createLambdaFunction = (name: string, code: string): Function => { | ||
| return new Function(stack, `${name}Lambda`, { | ||
| runtime: Runtime.NODEJS_18_X, | ||
| handler: 'index.handler', | ||
| code: Code.fromInline(code), | ||
| }); | ||
| }; | ||
|
|
||
| const createBasicRole = () => { | ||
| const basicRole = new Role(stack, 'BasicRole', { | ||
| assumedBy: new AccountPrincipal(stack.account), | ||
| path: '/', | ||
| inlinePolicies: { | ||
| root: new PolicyDocument({ | ||
| statements: [ | ||
| new PolicyStatement({ | ||
| actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], | ||
| resources: ['arn:aws:logs:*:*:*'], | ||
| effect: Effect.ALLOW, | ||
| }), | ||
| ], | ||
| }), | ||
| }, | ||
| }); | ||
| basicRole.applyRemovalPolicy(RemovalPolicy.DESTROY); | ||
|
|
||
| basicRole.addToPolicy( | ||
| new PolicyStatement({ | ||
| actions: ['appsync:GraphQL'], | ||
| resources: [`${api.resources.graphqlApi.arn}/*`], | ||
| effect: Effect.ALLOW, | ||
| }), | ||
| ); | ||
|
|
||
| new CfnOutput(stack, 'BasicRoleArn', { value: basicRole.roleArn }); | ||
| }; | ||
|
|
||
| const createAdditionalCfnOutputs = () => { | ||
| switch (stackConfig.authMode) { | ||
| case AUTH_MODE.API_KEY: { | ||
| const { | ||
| resources: { functions }, | ||
| } = api; | ||
| const sqlLambda = functions[`SQLFunction${dbDetails.dbConfig.strategyName}`]; | ||
|
|
||
| new CfnOutput(stack, 'SQLFunctionName', { value: sqlLambda.functionName }); | ||
| } | ||
| default: { | ||
| new CfnOutput(stack, 'GraphQLApiId', { value: api.resources.graphqlApi.apiId }); | ||
| new CfnOutput(stack, 'GraphQLApiArn', { value: api.resources.graphqlApi.arn }); | ||
| new CfnOutput(stack, 'region', { value: stack.region }); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| // #endregion Utilities | ||
|
|
||
| // #region CDK App | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | ||
| const dbDetails: DBDetails = require('../db-details.json'); | ||
| const stackConfig: StackConfig = require('../stack-config.json'); | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | ||
| const packageJson = require('../package.json'); | ||
|
|
||
| const app = new App(); | ||
| const stack = new Stack(app, packageJson.name.replace(/_/g, '-'), { | ||
| env: { region: process.env.CLI_REGION || 'us-west-2' }, | ||
| }); | ||
|
|
||
| const definition = createApiDefinition(); | ||
| const authorizationModes = createAuthorizationModes(); | ||
| const translationBehavior = createTranslationBehavior(); | ||
|
|
||
| const api = new AmplifyGraphqlApi(stack, `SqlBoundApi`, { | ||
| apiName: `${dbDetails.dbConfig.dbType}${Date.now()}`, | ||
| definition, | ||
| authorizationModes, | ||
| translationBehavior, | ||
| }); | ||
|
|
||
| createBasicRole(); | ||
| createAdditionalCfnOutputs(); | ||
|
|
||
| // #endregion CDK App | ||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would we represent this as an enum vs a string?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both options are viable here. My thought originally was that there could be multiple existing schemas available in the stack which could be used without providing schema through
stack-configbut more like an option. But that could be an overdesign here, as almost every test's schema is different, and it's impossible to store all those schemas in the CDK file.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think maybe a similar question is why enum versus an object with a string field
default?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have to re-define these Auth modes and set a default for schema? Instead we could just fail if either of them is not defined in the test using it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could keep the Auth mode to sync with the stack config interface that is using the
AUTH_TYPEfrom AppSync, while schema default could be discarded to avoid overdesign, since schema is already required when defining the stack config interface.