in source/constructs/lib/api-stack.ts [43:445]
constructor(scope: Construct, id: string, props: ApiProps) {
super(scope, id);
// Can define custom bucket to hold the plugin url. Default to aws-gcr-solutions
const templateBucket = process.env.TEMPLATE_OUTPUT_BUCKET || 'aws-gcr-solutions'
let s3PluginVersion = 'v1.0.0'
let ecrPluginVersion = 'v1.0.0'
let suffix = '-plugin'
if (templateBucket === 'aws-gcr-solutions') {
s3PluginVersion = 'v2.0.2'
ecrPluginVersion = 'v1.0.1'
suffix = ''
}
const PLUGIN_TEMPLATE_S3EC2 = `https://${templateBucket}.s3.amazonaws.com/data-transfer-hub-s3${suffix}/${s3PluginVersion}/DataTransferS3Stack-ec2.template`;
const PLUGIN_TEMPLATE_ECR = `https://${templateBucket}.s3.amazonaws.com/data-transfer-hub-ecr${suffix}/${ecrPluginVersion}/DataTransferECRStack.template`;
// This Lambda is to create the AppSync Service Linked Role
const appSyncServiceLinkRoleFn = new lambda.Function(this, 'AppSyncServiceLinkRoleFn', {
runtime: lambda.Runtime.PYTHON_3_8,
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda/custom-resource')),
handler: 'crete_service_linked_role.lambda_handler',
timeout: Duration.seconds(60),
memorySize: 128,
description: 'Data Transfer Hub - Service Linked Role Create Handler'
});
// Grant IAM Policy to the appSyncServiceLinkRoleFn lambda
const serviceLikedRolePolicy = new iam.Policy(this, 'serviceLikedRolePolicy', {
statements: [
new iam.PolicyStatement({
actions: [
'iam:GetRole',
'iam:CreateServiceLinkedRole'
],
resources: [
'*'
]
}),
]
});
appSyncServiceLinkRoleFn.role?.attachInlinePolicy(serviceLikedRolePolicy)
addCfnNagSuppressRules(serviceLikedRolePolicy.node.defaultChild as iam.CfnPolicy, [
{
id: 'W12',
reason: 'This policy needs to be able to have access to all resources'
}
])
const appSyncServiceLinkRoleFnProvider = new cr.Provider(this, 'appSyncServiceLinkRoleFnProvider', {
onEventHandler: appSyncServiceLinkRoleFn,
});
appSyncServiceLinkRoleFnProvider.node.addDependency(appSyncServiceLinkRoleFn)
const appSyncServiceLinkRoleFnTrigger = new CustomResource(this, 'appSyncServiceLinkRoleFnTrigger', {
serviceToken: appSyncServiceLinkRoleFnProvider.serviceToken,
});
appSyncServiceLinkRoleFnTrigger.node.addDependency(appSyncServiceLinkRoleFnProvider)
// Create the Progress DynamoDB Table
this.taskTable = new ddb.Table(this, 'TaskTable', {
billingMode: ddb.BillingMode.PAY_PER_REQUEST,
partitionKey: {
name: 'id',
type: ddb.AttributeType.STRING
},
removalPolicy: RemovalPolicy.DESTROY,
encryption: TableEncryption.DEFAULT,
pointInTimeRecovery: true,
})
this.taskTable.addGlobalSecondaryIndex({
indexName: 'byStackId',
partitionKey: {
name: 'stackId',
type: ddb.AttributeType.STRING
},
projectionType: ddb.ProjectionType.INCLUDE,
nonKeyAttributes: ['id', 'status', 'stackStatus']
})
const cfnTable = this.taskTable.node.defaultChild as ddb.CfnTable
addCfnNagSuppressRules(cfnTable, [
{
id: 'W74',
reason: 'This table is set to use DEFAULT encryption, the key is owned by DDB.'
},
])
const lambdaLayer = new lambda.LayerVersion(this, 'Layer', {
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda/layer/api/'), {
bundling: {
image: lambda.Runtime.NODEJS_14_X.bundlingImage,
command: [
'bash', '-c', [
`cd /asset-output/`,
`mkdir nodejs`,
`cp /asset-input/nodejs/package.json /asset-output/nodejs/`,
`cd /asset-output/nodejs/`,
`npm install`
].join(' && ')
],
user: 'root'
}
}),
compatibleRuntimes: [lambda.Runtime.NODEJS_14_X],
description: 'Data Transfer Hub - Lambda Layer'
})
const stateMachine = new cfnSate.CloudFormationStateMachine(this, 'CfnWorkflow', {
taskTableName: this.taskTable.tableName,
taskTableArn: this.taskTable.tableArn,
lambdaLayer: lambdaLayer
})
if (props.authType === AuthType.OPENID) {
// Open Id Auth Config
this.authDefaultConfig = {
authorizationType: appsync.AuthorizationType.OIDC,
openIdConnectConfig: {
oidcProvider: props.oidcProvider?.valueAsString
}
}
} else {
const poolSmsRole = new iam.Role(this, 'UserPoolSmsRole', {
assumedBy: new iam.ServicePrincipal('cognito-idp.amazonaws.com'),
});
const poolSmsPolicy = new iam.Policy(this, 'PoolSmsPolicy', {
// policyName: `${cdk.Aws.STACK_NAME}CustomResourcePolicy`,
statements: [
new iam.PolicyStatement({
actions: [
'sns:Publish',
],
resources: [
'*'
]
}),
]
});
poolSmsRole.attachInlinePolicy(
poolSmsPolicy
)
const cfnPoolSmsPolicy = poolSmsPolicy.node.defaultChild as iam.CfnPolicy
addCfnNagSuppressRules(cfnPoolSmsPolicy, [
{
id: 'W12',
reason: 'User Pool SMS notification requires to publish to any resources'
}
])
// Create Cognito User Pool
this.userPool = new cognito.UserPool(this, 'UserPool', {
selfSignUpEnabled: false,
signInCaseSensitive: false,
signInAliases: {
email: true,
username: false,
phone: true
},
smsRole: poolSmsRole,
removalPolicy: RemovalPolicy.DESTROY,
})
this.userPool.node.addDependency(poolSmsRole, poolSmsPolicy)
const cfnUserPool = this.userPool.node.defaultChild as cognito.CfnUserPool
cfnUserPool.overrideLogicalId('DataTransferHubUserPool')
cfnUserPool.userPoolAddOns = {
advancedSecurityMode: 'ENFORCED'
}
// Create User Pool Client
this.userPoolApiClient = new cognito.UserPoolClient(this, 'UserPoolClient', {
userPool: this.userPool,
userPoolClientName: 'DTHPortal',
preventUserExistenceErrors: true
})
// Create an Admin User
// TODO: The user can be created, however, the state is FORCE_PASSWORD_CHANGE, the customer still cannot use the account yet.
// https://stackoverflow.com/questions/40287012/how-to-change-user-status-force-change-password
// resolution: create a custom lambda to set user password
new cognito.CfnUserPoolUser(this, 'AdminUser', {
userPoolId: this.userPool.userPoolId,
username: props?.usernameParameter?.valueAsString,
userAttributes: [
{
name: 'email',
value: props?.usernameParameter?.valueAsString
}
]
})
this.userPoolDomain = new cognito.UserPoolDomain(this, 'UserPoolDomain', {
userPool: this.userPool,
cognitoDomain: {
domainPrefix: `dth-portal-${Stack.of(this).account}`
}
})
// const userPoolDomainOutput = new cdk.CfnOutput(this, 'UserPoolDomainOutput', {
// exportName: 'UserPoolDomain',
// value: `https://${userPoolDomain.domainName}.auth.${this.region}.amazoncognito.com`,
// description: 'Cognito Hosted UI domain name'
// })
this.authDefaultConfig = {
authorizationType: appsync.AuthorizationType.USER_POOL,
userPoolConfig: {
userPool: this.userPool,
appIdClientRegex: this.userPoolApiClient.userPoolClientId,
defaultAction: appsync.UserPoolDefaultAction.ALLOW
}
}
}
// AWSAppSyncPushToCloudWatchLogs managed policy is not available in China regions.
// Create the policy manually
const apiLogRole = new iam.Role(this, 'ApiLogRole', {
assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'),
});
const apiLogPolicy = new iam.Policy(this, 'ApiLogPolicy', {
statements: [
new iam.PolicyStatement({
actions: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents',
],
resources: [
'*'
]
}),
]
});
apiLogRole.attachInlinePolicy(apiLogPolicy)
const cfnApiLogRoley = apiLogPolicy.node.defaultChild as iam.CfnPolicy
addCfnNagSuppressRules(cfnApiLogRoley, [
{
id: 'W12',
reason: 'The managed policy AWSAppSyncPushToCloudWatchLogs needs to use any resources'
}
])
// Create the GraphQL API Endpoint, enable Cognito User Pool Auth and IAM Auth.
this.api = new appsync.GraphqlApi(this, 'ApiEndpoint', {
name: 'DataTransferHubAPI',
schema: appsync.Schema.fromAsset(path.join(__dirname, '../../schema/schema.graphql')),
authorizationConfig: {
defaultAuthorization: this.authDefaultConfig,
additionalAuthorizationModes: [
{
authorizationType: appsync.AuthorizationType.IAM
}
]
},
logConfig: {
fieldLogLevel: appsync.FieldLogLevel.ERROR,
role: apiLogRole,
},
xrayEnabled: true
})
this.api.node.addDependency(appSyncServiceLinkRoleFnTrigger);
const taskDS = this.api.addDynamoDbDataSource('TaskTableDS', this.taskTable)
taskDS.createResolver({
typeName: 'Query',
fieldName: 'listTasks',
requestMappingTemplate: appsync.MappingTemplate.fromFile(path.join(__dirname, '../../schema/vtl/DynamoDBScanTable.vtl')),
responseMappingTemplate: appsync.MappingTemplate.fromFile(path.join(__dirname, '../../schema/vtl/DynamoDBScanTableResult.vtl'))
})
taskDS.createResolver({
typeName: 'Query',
fieldName: 'getTask',
requestMappingTemplate: appsync.MappingTemplate.dynamoDbGetItem('id', 'id'),
responseMappingTemplate: appsync.MappingTemplate.fromFile(path.join(__dirname, '../../schema/vtl/GetTaskResult.vtl'))
})
// Create Lambda Data Source
const isDryRun = this.node.tryGetContext('DRY_RUN')
const taskHandlerFn = new lambda.Function(this, 'TaskHandlerFn', {
code: lambda.AssetCode.fromAsset(path.join(__dirname, '../lambda/'), {
exclude: ['cdk/*', 'layer/*']
}),
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'api/api-task.handler',
timeout: Duration.seconds(10),
memorySize: 512,
environment: {
STATE_MACHINE_ARN: stateMachine.stateMachineArn,
TASK_TABLE: this.taskTable.tableName,
PLUGIN_TEMPLATE_S3EC2: PLUGIN_TEMPLATE_S3EC2,
PLUGIN_TEMPLATE_ECR: PLUGIN_TEMPLATE_ECR,
DRY_RUN: isDryRun ? 'True' : 'False'
},
layers: [lambdaLayer]
})
const cfnTaskHandlerFn = taskHandlerFn.node.defaultChild as lambda.CfnFunction
addCfnNagSuppressRules(cfnTaskHandlerFn, [
{
id: 'W58',
reason: 'Lambda function already has permission to write CloudWatch Logs'
}
])
this.taskTable.grantReadWriteData(taskHandlerFn)
taskHandlerFn.addToRolePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [`${stateMachine.stateMachineArn}`],
actions: [
'states:StartExecution'
]
}))
const lambdaDS = this.api.addLambdaDataSource('TaskLambdaDS', taskHandlerFn, {
description: 'Lambda Resolver Datasource'
});
lambdaDS.createResolver({
typeName: 'Mutation',
fieldName: 'createTask',
requestMappingTemplate: appsync.MappingTemplate.fromFile(path.join(__dirname, '../../schema/vtl/CreateTask.vtl')),
responseMappingTemplate: appsync.MappingTemplate.lambdaResult()
})
lambdaDS.createResolver({
typeName: 'Mutation',
fieldName: 'stopTask',
requestMappingTemplate: appsync.MappingTemplate.lambdaRequest(),
responseMappingTemplate: appsync.MappingTemplate.lambdaResult()
})
// lambdaDS.createResolver({
// typeName: 'Mutation',
// fieldName: 'updateTaskProgress',
// requestMappingTemplate: appsync.MappingTemplate.lambdaRequest(),
// responseMappingTemplate: appsync.MappingTemplate.lambdaResult()
// })
// Create Lambda Data Source
const secretManagerHandlerFn = new lambda.Function(this, 'SecretManagerHandlerFn', {
code: lambda.AssetCode.fromAsset(path.join(__dirname, '../lambda/'), {
exclude: ['cdk/*', 'layer/*']
}),
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'api/api-sm-param.handler',
timeout: Duration.seconds(60),
memorySize: 128,
})
const cfnSecretManagerHandlerFn = secretManagerHandlerFn.node.defaultChild as lambda.CfnFunction
addCfnNagSuppressRules(cfnSecretManagerHandlerFn, [
{
id: 'W58',
reason: 'Lambda function already has permission to write CloudWatch Logs'
}
])
const secretManagerReadOnlyPolicy = new iam.Policy(this, 'secretManagerReadOnlyPolicy')
secretManagerReadOnlyPolicy.addStatements(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: ['*'],
actions: [
"secretsmanager:ListSecrets",
]
}))
const cfnSecretManagerReadOnlyPolicy = secretManagerReadOnlyPolicy.node.defaultChild as iam.CfnPolicy
addCfnNagSuppressRules(cfnSecretManagerReadOnlyPolicy, [
{
id: 'W12',
reason: 'Need to be able to list all secrets in Secrets Manager'
},
])
secretManagerHandlerFn.role?.attachInlinePolicy(secretManagerReadOnlyPolicy)
const secretManagerLambdaDS = this.api.addLambdaDataSource('secretManagerLambdaDS', secretManagerHandlerFn, {
description: 'Lambda Resolver Datasource for Secret Manager'
});
secretManagerLambdaDS.createResolver({
typeName: 'Query',
fieldName: 'listSecrets',
requestMappingTemplate: appsync.MappingTemplate.lambdaRequest(),
responseMappingTemplate: appsync.MappingTemplate.lambdaResult()
})
}