in app/lib/medical-transcription-analysis-stack.ts [33:490]
constructor(scope: cdk.Construct, id: string, props: MTAStackProps) {
super(scope, id, props);
this.resourceName = (name: any) => `${id}-${name}`.toLowerCase();
this.uuid = uuid.generate();
const corsRule = {
allowedOrigins: ['*'],
allowedMethods: [
s3.HttpMethods.HEAD,
s3.HttpMethods.GET,
s3.HttpMethods.PUT,
s3.HttpMethods.POST,
s3.HttpMethods.DELETE,
],
maxAge: 3000,
exposedHeaders: ['ETag'],
allowedHeaders: ['*'],
};
//S3 Bucket for Transcribe, Comprehend, and Audio
const storageS3Bucket = new s3.Bucket(this, this.resourceName('storageS3Bucket'), {
websiteIndexDocument: 'index.html',
cors: [corsRule],
// blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, change back
encryption: BucketEncryption.S3_MANAGED,
removalPolicy: RemovalPolicy.DESTROY,
});
// ### Client ###
const webAppS3Bucket = new s3.Bucket(this, this.resourceName('webAppS3Bucket'), {
websiteIndexDocument: 'index.html',
cors: [corsRule],
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: BucketEncryption.S3_MANAGED,
removalPolicy: RemovalPolicy.DESTROY,
});
const oai = new OriginAccessIdentity(this, 'mta-oai', {
comment: 'Origin Access Identity for Medical Transcription Analysis web stack bucket cloudfront distribution',
});
const distribution = new CloudFrontWebDistribution(this, 'mta-cfront', {
originConfigs: [
{
behaviors: [{ isDefaultBehavior: true }],
s3OriginSource: {
s3BucketSource: webAppS3Bucket,
originAccessIdentity: oai,
},
},
],
errorConfigurations: [
{
errorCode: 404,
responseCode: 200,
errorCachingMinTtl: 5,
responsePagePath: '/index.html',
},
],
priceClass: PriceClass.PRICE_CLASS_100,
httpVersion: HttpVersion.HTTP2,
enableIpV6: true,
defaultRootObject: 'index.html',
});
const cloudfrontPolicyStatement = new iam.PolicyStatement({
actions: ['s3:GetBucket*', 's3:GetObject*', 's3:List*'],
resources: [webAppS3Bucket.bucketArn, `${webAppS3Bucket.bucketArn}/*`],
principals: [new CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId)],
});
webAppS3Bucket.addToResourcePolicy(cloudfrontPolicyStatement);
const cloudfrontStorageBucketPolicyStatement = new iam.PolicyStatement({
actions: ['s3:GetBucket*', 's3:GetObject*', 's3:List*', 's3:PutObject'],
resources: [storageS3Bucket.bucketArn, `${storageS3Bucket.bucketArn}/*`],
principals: [new CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId)],
});
webAppS3Bucket.addToResourcePolicy(cloudfrontPolicyStatement);
storageS3Bucket.addToResourcePolicy(cloudfrontStorageBucketPolicyStatement);
// ####### Cognito User Authentication #######
const mtaUserPool = new UserPool(this, 'mta-user-pool', {
userPoolName: 'mta-user-pool',
autoVerify: { email: true },
passwordPolicy: {
minLength: 8,
requireUppercase: true,
requireDigits: true,
requireSymbols: true,
},
userInvitation: {
emailSubject: 'Your MTA login',
emailBody: `<p>You are invited to try the Medical Transcription Analysis Solution. Your credentials are:</p> \
<p> \
Username: <strong>{username}</strong><br /> \
Password: <strong>{####}</strong> \
</p> \
<p> \
Please sign in with the user name and your temporary password provided above at: <br /> \
https://${distribution.domainName} \
</p>`,
},
});
new cdk.CfnOutput(this, 'MTAUserPoolId', { value: mtaUserPool.userPoolId });
// Depends upon all other parts of the stack having been created.
const mtaUserPoolUser = new CfnUserPoolUser(this, 'mta-user-pool-user', {
desiredDeliveryMediums: ['EMAIL'],
forceAliasCreation: false,
userPoolId: mtaUserPool.userPoolId,
userAttributes: [
{
name: 'email',
value: props.email,
},
],
username: props.email.replace(/@/, '.'),
});
const mtaUserPoolClient = new UserPoolClient(this, 'mta-user-pool-client', {
userPoolClientName: 'mta_app',
userPool: mtaUserPool,
});
const mtaIdentityPool = new CfnIdentityPool(this, 'mta-identity-pool', {
identityPoolName: 'mtaUserIdentityPool',
allowUnauthenticatedIdentities: true,
cognitoIdentityProviders: [
{
clientId: mtaUserPoolClient.userPoolClientId,
providerName: mtaUserPool.userPoolProviderName,
serverSideTokenCheck: false,
},
],
});
const cognitoPolicy = new iam.Policy(this, 'mta-cognito-policy', {
statements: [
new iam.PolicyStatement({
actions: ['cognito-identity:GetId'],
resources: ['*'],
effect: iam.Effect.ALLOW,
}),
new iam.PolicyStatement({
actions: ['transcribe:*', 'comprehendmedical:*'],
resources: ['*'],
effect: iam.Effect.ALLOW,
}),
new iam.PolicyStatement({
actions: ['s3:GetObject*', 's3:List*', 's3:PutObject'],
resources: [storageS3Bucket.bucketArn, `${storageS3Bucket.bucketArn}/*`],
effect: iam.Effect.ALLOW,
}),
],
});
const cognitoPolicyResource = cognitoPolicy.node.findChild('Resource') as iam.CfnPolicy;
cognitoPolicyResource.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: 'W11',
reason: 'The resources in the policy are created/managed by this solution.',
},
],
},
};
const mtaCognitoAuthenticatedRole = new iam.Role(this, 'mta-cognito-authenticated-role', {
assumedBy: new iam.FederatedPrincipal(
'cognito-identity.amazonaws.com',
{
StringEquals: {
'cognito-identity.amazonaws.com:aud': mtaIdentityPool.ref,
},
'ForAnyValue:StringLike': {
'cognito-identity.amazonaws.com:amr': 'authenticated',
},
},
'sts:AssumeRoleWithWebIdentity',
),
path: '/',
});
cognitoPolicy.attachToRole(mtaCognitoAuthenticatedRole);
const mtaIdentityPoolRoleAttachment = new CfnIdentityPoolRoleAttachment(this, 'mta-identity-role-pool-attachment', {
identityPoolId: mtaIdentityPool.ref,
roles: {
authenticated: mtaCognitoAuthenticatedRole.roleArn,
},
});
const yarnBotoLoc = lambda.Code.fromAsset('lambda/boto3');
const boto3Layer = new lambda.LayerVersion(this, this.resourceName('Boto3'), {
code: yarnBotoLoc,
compatibleRuntimes: [lambda.Runtime.PYTHON_3_8],
license: 'Apache-2.0',
});
const transcriberRole = new iam.Role(this, this.resourceName('TranscriberRole'), {
assumedBy: new iam.ServicePrincipal('iam.amazonaws.com'),
});
transcriberRole.assumeRolePolicy?.addStatements(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['sts:AssumeRole'],
principals: [new iam.AccountRootPrincipal()],
}),
);
transcriberRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: ['*'],
actions: [
'transcribe:StartStreamTranscriptionWebSocket',
'transcribe:StartMedicalStreamTranscription',
'comprehendmedical:InferICD10CM',
'comprehendmedical:InferRxNorm',
'comprehendmedical:DetectEntitiesV2',
],
}),
);
// Dynamodb
const TableSessions = new ddb.Table(this, 'TableSessions', {
tableName: 'Sessions',
removalPolicy: RemovalPolicy.DESTROY,
partitionKey: { name: 'PatientId', type: ddb.AttributeType.STRING },
sortKey: { name: 'SessionId', type: ddb.AttributeType.STRING },
serverSideEncryption: true,
});
TableSessions.addGlobalSecondaryIndex({
indexName: 'hcpIndex',
partitionKey: { name: 'HealthCareProfessionalId', type: ddb.AttributeType.STRING },
sortKey: { name: 'SessionId', type: ddb.AttributeType.STRING },
});
const TablePatients = new ddb.Table(this, 'TablePatients', {
tableName: 'Patients',
removalPolicy: RemovalPolicy.DESTROY,
partitionKey: { name: 'PatientId', type: ddb.AttributeType.STRING },
serverSideEncryption: true,
});
const TableHealthCareProfessionals = new ddb.Table(this, 'TableHealthCareProfessionals', {
tableName: 'HealthCareProfessionals',
removalPolicy: RemovalPolicy.DESTROY,
partitionKey: { name: 'HealthCareProfessionalId', type: ddb.AttributeType.STRING },
serverSideEncryption: true,
});
// Lambda
/* MTAApiProcessor */
const onEventAthenaLambda = new lambda.Function(this, this.resourceName('MTAOnEventAthenaLambda'), {
runtime: lambda.Runtime.PYTHON_3_8,
code: lambda.Code.asset('lambda/custom_resource_athena/'),
handler: 'lambda_function.lambda_handler',
timeout: cdk.Duration.seconds(60),
environment: {
BUCKET_NAME: storageS3Bucket.bucketName,
},
});
onEventAthenaLambda.addLayers(boto3Layer);
onEventAthenaLambda.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'athena:StartQueryExecution',
'athena:CreateNamedQuery',
'athena:DeleteNamedQuery',
'athena:GetQueryResults',
'athena:CreateWorkGroup',
'athena:DeleteWorkGroup',
],
resources: ['*'],
}),
);
onEventAthenaLambda.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:PutObject', 's3:GetObject', 's3:AbortMultipartUpload'],
resources: ['*'],
}),
);
onEventAthenaLambda.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['glue:*'],
resources: ['*'],
}),
);
const apiProcessor = new lambda.Function(this, this.resourceName('MTAApiProcessor'), {
runtime: lambda.Runtime.PYTHON_3_8,
code: lambda.Code.asset('lambda'),
handler: 'lambda_function.lambda_handler',
timeout: cdk.Duration.seconds(60),
environment: {
TRANSCRIBE_ACCESS_ROLEARN: transcriberRole.roleArn,
BUCKET_NAME: storageS3Bucket.bucketName,
},
});
TableHealthCareProfessionals.grantReadWriteData(apiProcessor);
TablePatients.grantReadWriteData(apiProcessor);
TableSessions.grantReadWriteData(apiProcessor);
storageS3Bucket.grantReadWrite(apiProcessor);
storageS3Bucket.grantReadWrite(onEventAthenaLambda);
apiProcessor.addToRolePolicy(
new iam.PolicyStatement({
actions: ['sts:AssumeRole'],
effect: iam.Effect.ALLOW,
resources: [transcriberRole.roleArn],
}),
);
apiProcessor.addToRolePolicy(
new iam.PolicyStatement({
actions: ['translate:TranslateText'],
effect: iam.Effect.ALLOW,
resources: ['*'], // * permsissions needs to be provided for Translate APIs : https://docs.aws.amazon.com/translate/latest/dg/translate-api-permissions-ref.html
}),
);
apiProcessor.addLayers(boto3Layer);
const api = new apigateway.LambdaRestApi(this, this.resourceName('MTADemoAPI'), {
handler: apiProcessor,
proxy: false,
deployOptions: {
loggingLevel: apigateway.MethodLoggingLevel.INFO,
dataTraceEnabled: false,
},
});
const reqValidator = new apigateway.RequestValidator(this, this.resourceName('apigwResourceValidator'), {
restApi: api,
validateRequestBody: true,
validateRequestParameters: true,
});
//one authorizer
const authorizer = new apigateway.CfnAuthorizer(this, 'Authorizer', {
identitySource: 'method.request.header.Authorization',
name: 'Authorization',
type: 'COGNITO_USER_POOLS',
providerArns: [mtaUserPool.userPoolArn],
restApiId: api.restApiId,
});
function addCorsOptionsAndMethods(apiResource: apigateway.IResource | apigateway.Resource, methods: string[] | []) {
const options = apiResource.addMethod(
'OPTIONS',
new apigateway.MockIntegration({
integrationResponses: [
{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers':
"'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
'method.response.header.Access-Control-Allow-Origin': "'*'",
'method.response.header.Access-Control-Allow-Credentials': "'false'",
'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,GET,PUT,POST,DELETE'",
},
},
],
passthroughBehavior: apigateway.PassthroughBehavior.NEVER,
requestTemplates: {
'application/json': '{"statusCode": 200}',
},
}),
{
methodResponses: [
{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': true,
'method.response.header.Access-Control-Allow-Methods': true,
'method.response.header.Access-Control-Allow-Credentials': true,
'method.response.header.Access-Control-Allow-Origin': true,
},
},
],
requestValidator: reqValidator,
},
);
methods.forEach((method) => {
apiResource.addMethod(method, undefined, {
authorizationType: apigateway.AuthorizationType.COGNITO,
authorizer: {
authorizerId: `${authorizer.ref}`,
},
});
});
}
addCorsOptionsAndMethods(api.root, []);
const getCredentials = api.root.addResource('getCredentials');
addCorsOptionsAndMethods(getCredentials, ['GET', 'POST']);
const createSessionResource = api.root.addResource('createSession');
addCorsOptionsAndMethods(createSessionResource, ['POST']);
const listSessionsResource = api.root.addResource('listSessions');
addCorsOptionsAndMethods(listSessionsResource, ['GET']);
const listPatientsResource = api.root.addResource('listPatients');
addCorsOptionsAndMethods(listPatientsResource, ['GET']);
const createPatientResource = api.root.addResource('createPatient');
addCorsOptionsAndMethods(createPatientResource, ['POST']);
const listHealthCareProfessionalsResource = api.root.addResource('listHealthCareProfessionals');
addCorsOptionsAndMethods(listHealthCareProfessionalsResource, ['GET']);
const createHealthCareProfessionalResource = api.root.addResource('createHealthCareProfessional');
addCorsOptionsAndMethods(createHealthCareProfessionalResource, ['POST']);
const getTranscriptionComprehendResource = api.root.addResource('getTranscriptionComprehend');
addCorsOptionsAndMethods(getTranscriptionComprehendResource, ['GET']);
const getTranscriptionTranslationResource = api.root.addResource('getTranscriptionTranslation');
addCorsOptionsAndMethods(getTranscriptionTranslationResource, ['GET']);
cognitoPolicy.addStatements(
new iam.PolicyStatement({
actions: ['execute-api:Invoke'],
resources: [api.arnForExecuteApi()],
effect: iam.Effect.ALLOW,
}),
);
// Custom Resource
const athenaProvider = new cr.Provider(this, this.resourceName('athenaProvider'), {
onEventHandler: onEventAthenaLambda,
});
const athenaCustomResource = new CustomResource(this, this.resourceName('athenaCustomResource'), {
serviceToken: athenaProvider.serviceToken,
});
}