import os
import string
import secrets
from aws_cdk import (
Aspects as Aspects,
Duration as duration,
CfnOutput as output,
custom_resources as cr,
aws_stepfunctions as _aws_stepfunctions,
aws_stepfunctions_tasks as _aws_stepfunctions_tasks,
aws_lambda as _lambda,
aws_apigateway as apigateway,
aws_cognito as cognito,
aws_s3 as s3,
aws_s3_deployment as s3_deploy,
aws_s3_assets as s3_assets,
aws_ec2 as ec2,
aws_opensearchservice as opensearch,
aws_events as events,
aws_events_targets as targets,
aws_cloudfront as cloudfront,
aws_cloudfront_origins as origins,
aws_logs as logs,
aws_iam as iam,
App, Duration, Stack
)
from cdk_nag import AwsSolutionsChecks, NagSuppressions
class JobPollerStack(Stack):
def __init__(self, app: App, id: str, **kwargs) -> None:
super().__init__(app, id, **kwargs)
# region = "us-east-1"
index_name = "adms-index"
# voice_id = "Vitoria"
os_instance_type="m6g.large.search"
ebs_volume_size=100
data_nodes=2
region = os.environ["REGION"]
email = os.environ["EMAIL"]
# index_name = os.environ["INDEX_NAME"]
voice_id = os.environ["VOICE_ID"]
# os_instance_type=os.environ["OS_INSTANCE_TYPE"]
# ebs_volume_size=int(os.environ["EBS_VOLUME_SIZE"])
# data_nodes=int(os.environ["DATA_NODES"])
alphabet = string.ascii_letters
password = ''.join(secrets.choice(alphabet) for i in range(10)) # for a 10-character password
# S3 Buckets
cors_rule = s3.CorsRule(
allowed_methods=[s3.HttpMethods.GET, s3.HttpMethods.POST, s3.HttpMethods.HEAD, s3.HttpMethods.PUT, s3.HttpMethods.DELETE],
allowed_origins=["*"],
allowed_headers=["*"],
exposed_headers=["ETag"]
)
bucket_access_logs = s3.Bucket(self, "access_logs",
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
# object_ownership=s3.ObjectOwnership.BUCKET_OWNER_ENFORCED,
encryption=s3.BucketEncryption.S3_MANAGED,
enforce_ssl=True
)
bucket_front = s3.Bucket(self, "bucket-front",
website_index_document="login.html",
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
object_ownership=s3.ObjectOwnership.BUCKET_OWNER_ENFORCED,
server_access_logs_bucket=bucket_access_logs,
server_access_logs_prefix="bucket_front/",
encryption=s3.BucketEncryption.S3_MANAGED,
enforce_ssl=True
)
bucket_raw = s3.Bucket(self, "bucket-raw",
cors=([cors_rule]),
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
event_bridge_enabled=True,
object_ownership=s3.ObjectOwnership.BUCKET_OWNER_PREFERRED,
server_access_logs_bucket=bucket_access_logs,
server_access_logs_prefix="bucket_raw/",
encryption=s3.BucketEncryption.S3_MANAGED,
enforce_ssl=True
)
# VPC
vpc = ec2.Vpc(self, "adms-vpc")
vpc.add_flow_log("FlowLog")
# VPC Subnets
selection = vpc.select_subnets(
subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT
)
for subnet in selection.subnets:
pass
# Security Group
security_group=ec2.SecurityGroup(self, "adms-sg",vpc=vpc)
security_group.add_ingress_rule(security_group,ec2.Port.all_traffic(),"adms rule",False)
iam.CfnServiceLinkedRole(self, "OpensearchSLR",
aws_service_name="opensearchservice.amazonaws.com"
)
# Domain Access Policy
domainStatement = iam.PolicyStatement(
actions=[
"es:ESHttpGet",
"es:ESHttpHead",
"es:ESHttpPut",
"es:ESHttpDelete",
"es:ESHttpPost",
"es:ESHttpPatch"
],
resources= [
"arn:aws:es:"+self.region+":"+self.account+":domain/ADMSDomain/*"
],
conditions= {
"IpAddress": {
"aws:SourceIp": [vpc.DEFAULT_CIDR_RANGE]
}
}
)
# Opensearch
domain = opensearch.Domain(self, "ADMSDomain",
version=opensearch.EngineVersion.OPENSEARCH_1_3,
ebs=opensearch.EbsOptions(
volume_size=ebs_volume_size,
volume_type=ec2.EbsDeviceVolumeType.GP3
),
capacity=opensearch.CapacityConfig(
data_nodes=data_nodes,
data_node_instance_type=os_instance_type
),
logging=opensearch.LoggingOptions(
slow_search_log_enabled=True,
app_log_enabled=True,
slow_index_log_enabled=True
),
zone_awareness=opensearch.ZoneAwarenessConfig(
availability_zone_count=data_nodes
),
node_to_node_encryption=True,
vpc=vpc,
vpc_subnets=[ec2.SubnetSelection(
subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS
)],
security_groups=[security_group],
encryption_at_rest=opensearch.EncryptionAtRestOptions(
enabled=True
),
access_policies=[domainStatement]
)
# Lambda Functions
format_textract = _lambda.Function(self, 'format_textract',
handler='formatTextract.lambda_handler',
runtime=_lambda.Runtime.PYTHON_3_9,
timeout= duration.seconds(300),
code=_lambda.Code.from_asset('../../lambda',exclude=['searchApi.py','indexDoc.py','indexMedia.py']))
format_textract.add_environment("S3_BUCKET_OUTPUT", bucket_front.bucket_name)
format_textract.add_environment("VOICE_ID", voice_id)
format_textract.add_environment("REGION", region)
format_textract.add_environment("OPENSEARCH_ENDPOINT", domain.domain_endpoint)
format_textract.add_environment("INDEX_NAME", index_name)
index_doc = _lambda.Function(self, 'index_doc',
handler='indexDoc.lambda_handler',
runtime=_lambda.Runtime.PYTHON_3_9,
timeout= duration.seconds(300),
vpc=vpc,
vpc_subnets=ec2.SubnetSelection(
subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS
),
security_groups=([security_group]),
code=_lambda.Code.from_asset('../../lambda',exclude=['searchApi.py','formatTextract.py','indexMedia.py']))
index_doc.add_environment("S3_BUCKET_OUTPUT", bucket_front.bucket_name)
index_doc.add_environment("VOICE_ID", voice_id)
index_doc.add_environment("REGION", region)
index_doc.add_environment("OPENSEARCH_ENDPOINT", domain.domain_endpoint)
index_doc.add_environment("INDEX_NAME", index_name)
index_media = _lambda.Function(self, 'index_media',
handler='indexMedia.lambda_handler',
runtime=_lambda.Runtime.PYTHON_3_9,
timeout= duration.seconds(300),
vpc=vpc,
vpc_subnets=ec2.SubnetSelection(
subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS
),
security_groups=([security_group]),
code=_lambda.Code.from_asset('../../lambda',exclude=['searchApi.py','formatTextract.py','indexDoc.py']))
index_media.add_environment("S3_BUCKET_OUTPUT", bucket_front.bucket_name)
index_media.add_environment("VOICE_ID", voice_id)
index_media.add_environment("REGION", region)
index_media.add_environment("OPENSEARCH_ENDPOINT", domain.domain_endpoint)
index_media.add_environment("INDEX_NAME", index_name)
search_api = _lambda.Function(self, 'search_api',
handler='searchApi.lambda_handler',
runtime=_lambda.Runtime.PYTHON_3_9,
timeout= duration.seconds(300),
vpc=vpc,
vpc_subnets=ec2.SubnetSelection(
subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS
),
security_groups=([security_group]),
code=_lambda.Code.from_asset('../../lambda',exclude=['formatTextract.py','indexDoc.py','indexMedia.py']))
search_api.add_environment("S3_BUCKET_OUTPUT", bucket_front.bucket_name)
search_api.add_environment("VOICE_ID", voice_id)
search_api.add_environment("REGION", region)
search_api.add_environment("OPENSEARCH_ENDPOINT", domain.domain_endpoint)
search_api.add_environment("INDEX_NAME", index_name)
# Cloud Front
origin_access_identity = cloudfront.OriginAccessIdentity(self, "ADMSOriginAccessIdentity",
comment="ADMS"
)
adms_dist = cloudfront.CloudFrontWebDistribution(self, "admsWebDistribution",
origin_configs=[cloudfront.SourceConfiguration(
s3_origin_source=cloudfront.S3OriginConfig(
s3_bucket_source=bucket_front,
origin_access_identity=origin_access_identity
),
behaviors=[cloudfront.Behavior(is_default_behavior=True)]
)],
logging_config = cloudfront.LoggingConfiguration(
bucket=bucket_access_logs,
include_cookies=False,
prefix="cloudfront"
),
viewer_certificate=cloudfront.ViewerCertificate.from_iam_certificate("certificateId",
security_policy=cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, # default
)
)
output(self, "WebSiteDistributionOut", value="https://"+adms_dist.distribution_domain_name+"/login.html")
# Cognito UserPool
pool = cognito.UserPool(self, "Pool",
auto_verify=cognito.AutoVerifiedAttrs(email=True),
user_invitation=cognito.UserInvitationConfig(
email_subject="Invite to join ADMS!",
# email_body="You are invited to try the Accessibility Document Media Searcher. Your credentials are: Username: {username} Password: " + password +"Please wait until the deployent has completed before accessing the website. Please sign in with the user name and your password provided above at: https://{adms_dist.distribution_domain_name}/login.html ID:{####}",
email_body="
You are invited to try the Accessibility Document Media Searcher. Your credentials are:
\
\
Username: {username}
\
Password: "+password+" \
\
\
Please wait until the deployent has completed for ADMS stack before accessing the website \
\
\
Please sign in with the user name and your password provided above at:
\
https://"+ adms_dist.distribution_domain_name+ "/login.html \
RequestID: {####}\
",
),
password_policy=cognito.PasswordPolicy(
min_length=8,
require_lowercase=True,
require_uppercase=True,
require_digits=True,
require_symbols=True,
temp_password_validity=Duration.days(7)
)
)
poolClient = pool.add_client("app-client",
o_auth=cognito.OAuthSettings(
flows=cognito.OAuthFlows(
authorization_code_grant=True,
implicit_code_grant=True
),
scopes=[cognito.OAuthScope.OPENID,cognito.OAuthScope.EMAIL,cognito.OAuthScope.COGNITO_ADMIN],
callback_urls=["https://"+adms_dist.distribution_domain_name+"/index.html"],
logout_urls=["https://"+adms_dist.distribution_domain_name+"/login.html"]
)
)
# Cognito Identity Pool
admsIdentityPool = cognito.CfnIdentityPool(self, "ADMSIdentityPool",
cognito_identity_providers=pool.identity_providers,
# allow_unauthenticated_identities=True
allow_unauthenticated_identities=False
)
# Cognito Identity Pool Role
identitypoolAuthAssumeRolePolicyDoc = iam.PolicyDocument(
statements=[
iam.PolicyStatement(
actions=[
"mobileanalytics:PutEvents",
"cognito-sync:*",
"cognito-identity:*"
],
resources= [
"*"
]
),
iam.PolicyStatement(
actions=[
"S3:*"
],
resources= [
"arn:aws:s3:::"+bucket_front.bucket_name+"/*",
"arn:aws:s3:::"+bucket_raw.bucket_name+"/*",
"arn:aws:s3:::"+bucket_front.bucket_name,
"arn:aws:s3:::"+bucket_raw.bucket_name
]
)
]
)
identitypoolAuthRole = iam.Role(self, "IdentityPoolAuthRole",
assumed_by=iam.FederatedPrincipal("cognito-identity.amazonaws.com",{
"StringEquals": {
"cognito-identity.amazonaws.com:aud": admsIdentityPool.ref
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated"
}
}, "sts:AssumeRoleWithWebIdentity")
)
identitypoolAuthRole.attach_inline_policy(iam.Policy(self,"authPolicy",document=identitypoolAuthAssumeRolePolicyDoc))
identitypoolUnauthRole = iam.Role(self, "IdentityPoolUnauthRole",
assumed_by=iam.FederatedPrincipal("cognito-identity.amazonaws.com",{
"StringEquals": {
"cognito-identity.amazonaws.com:aud": admsIdentityPool.ref
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "unauthenticated"
}
}, "sts:AssumeRoleWithWebIdentity")
)
identitypoolUnauthRole.attach_inline_policy(iam.Policy(self,"unauthPolicy",document=identitypoolAuthAssumeRolePolicyDoc))
# Cognito Identity Pool Role Attachment
cfn_identity_pool_role_attachment = cognito.CfnIdentityPoolRoleAttachment(self,
"MyCfnIdentityPoolRoleAttachment",
identity_pool_id=admsIdentityPool.ref,
roles={
"authenticated":identitypoolAuthRole.role_arn,
"unauthenticated":identitypoolUnauthRole.role_arn,
}
)
# API Gateway
prd_log_group = logs.LogGroup(self, "adms-search-api-LogGroup")
api = apigateway.RestApi(self, "search-api",
description="ADMS Lambda search-api",
rest_api_name="adms-search-api",
deploy_options=apigateway.StageOptions(
logging_level=apigateway.MethodLoggingLevel.INFO,
data_trace_enabled=True,
access_log_destination=apigateway.LogGroupLogDestination(prd_log_group),
access_log_format=apigateway.AccessLogFormat.json_with_standard_fields(
caller=False,
http_method=True,
ip=True,
protocol=True,
request_time=True,
resource_path=True,
response_length=True,
status=True,
user=True
)
)
)
# API Gateway Authorizer
apiAuth = apigateway.CognitoUserPoolsAuthorizer(self, "apiAuth",
cognito_user_pools=[pool]
)
# API Gateway Method
resource = api.root.add_resource("search")
api.root.add_method("GET", apigateway.LambdaIntegration(
search_api,
proxy=True,
integration_responses=[apigateway.IntegrationResponse(
# Successful response from the Lambda function, no filter defined
# - the selectionPattern filter only tests the error message
# We will set the response status code to 200
status_code="200",
response_parameters={
# We can map response parameters
# - Destination parameters (the key) are the response parameters (used in mappings)
# - Source parameters (the value) are the integration response parameters or expressions
"method.response.header.Content-Type": "'application/json'",
"method.response.header.Access-Control-Allow-Origin": "'*'",
"method.response.header.Access-Control-Allow-Credentials": "'true'"
}
)]
),
request_parameters={"method.request.querystring.q":True},
authorizer=apiAuth,
authorization_type=apigateway.AuthorizationType.COGNITO,
authorization_scopes=["email", "aws.cognito.signin.user.admin"],
method_responses=[apigateway.MethodResponse(
status_code="200",
# response_models={
# "application/json": apigateway.Model.EMPTY_MODEL
# },
response_parameters={
"method.response.header.Content-Type": True,
"method.response.header.Access-Control-Allow-Origin": True,
"method.response.header.Access-Control-Allow-Credentials": True
}
),apigateway.MethodResponse(
status_code="400",
response_parameters={
"method.response.header.Content-Type": True,
"method.response.header.Access-Control-Allow-Origin": True,
"method.response.header.Access-Control-Allow-Credentials": True
}
# response_models={
# "application/json": apigateway.Model.EMPTY_MODEL
# }
)
]
)
resource.add_cors_preflight(
allow_origins=["*"],
allow_methods=["GET", "PUT"]
)
# IAM Policy Document
adms_policyDoc = iam.PolicyDocument(
statements=[
iam.PolicyStatement(
actions=[
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:CreateLogDelivery",
"logs:GetLogDelivery",
"logs:UpdateLogDelivery",
"logs:DeleteLogDelivery",
"logs:ListLogDeliveries",
"logs:PutLogEvents",
"logs:PutResourcePolicy",
"logs:DescribeResourcePolicies",
"logs:DescribeLogGroups"
],
resources=["*"]
),
iam.PolicyStatement(
actions=[
"s3:*",
"s3-object-lambda:*"
],
resources=[
"arn:aws:s3:::"+bucket_front.bucket_name+"",
"arn:aws:s3:::"+bucket_raw.bucket_name+"",
"arn:aws:s3:::"+bucket_front.bucket_name+"/*",
"arn:aws:s3:::"+bucket_raw.bucket_name+"/*"
]
),
iam.PolicyStatement(
actions=[
"polly:*"
],
resources=[
"*"
]
),
iam.PolicyStatement(
actions=[
"textract:*"
],
resources=[
"*"
]
),
iam.PolicyStatement(
actions=[
"transcribe:*"
],
resources=[
"*"
]
),
iam.PolicyStatement(
actions=[
"es:ESHttpGet",
"es:ESHttpHead",
"es:ESHttpPut",
"es:ESHttpDelete",
"es:ESHttpPost",
"es:ESHttpPatch"
],
resources=[
"*"
]
),
iam.PolicyStatement(
actions=[
"xray:PutTraceSegments",
"xray:PutTelemetryRecords"
],
resources=[
"*"
]
),
iam.PolicyStatement(
actions=[
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface"
],
resources=[
"*"
]
),
iam.PolicyStatement(
actions=[
"lambda:InvokeFunction"
],
resources=[
index_doc.function_arn+"*",
index_media.function_arn+"*",
search_api.function_arn+"*",
format_textract.function_arn+"*"
]
)
]
)
# IAM Policy
adms_policy = iam.Policy(self,"adms-policy",document=adms_policyDoc)
# IAM Policy Attachment
format_textract.role.attach_inline_policy(iam.Policy(self,"format_textract_lambda-policy",document=adms_policyDoc))
index_doc.role.attach_inline_policy(iam.Policy(self,"index_doc_lambda-policy",document=adms_policyDoc))
index_media.role.attach_inline_policy(iam.Policy(self,"index_media_lambda-policy",document=adms_policyDoc))
search_api.role.attach_inline_policy(iam.Policy(self,"search_api_lambda-policy",document=adms_policyDoc))
# State Machine Role
state_machine_role = iam.Role(self, "StateMachineRole",
assumed_by=iam.ServicePrincipal("states.amazonaws.com")
)
state_machine_role.attach_inline_policy(adms_policy)
# Load step function template file
sfn_def = open("../../step-function/StateMachineTemplate.json", "r")
log_group = logs.LogGroup(self, "admsStateMachineLogGroup")
# Step Functions
cfn_state_machine = _aws_stepfunctions.CfnStateMachine(self, "admsStateMachine",
role_arn=state_machine_role.role_arn,
definition_string=sfn_def.read(),
definition_substitutions={
"OUTPUT_BUCKET":bucket_front.bucket_name,
"LAMBDA_INDEX_MEDIA_ARN":index_media.function_arn,
"LAMBDA_FORMAT_TEXTRACT_ARN":format_textract.function_arn,
"LAMBDA_INDEX_DOC_ARN":index_doc.function_arn
},
state_machine_name="admsStateMachine",
state_machine_type="STANDARD",
logging_configuration=_aws_stepfunctions.CfnStateMachine.LoggingConfigurationProperty(
destinations=[_aws_stepfunctions.CfnStateMachine.LogDestinationProperty(
cloud_watch_logs_log_group=_aws_stepfunctions.CfnStateMachine.CloudWatchLogsLogGroupProperty(
log_group_arn=log_group.log_group_arn
)
)],
include_execution_data=False,
level="ALL"
),
tracing_configuration=_aws_stepfunctions.CfnStateMachine.TracingConfigurationProperty(
enabled=True
)
)
# Event Role
event_role = iam.Role(self, "EventRole",
assumed_by=iam.ServicePrincipal("events.amazonaws.com")
)
# Event Role Policy
event_role_policy_doc = iam.PolicyDocument(
statements=[
iam.PolicyStatement(
actions=["states:StartExecution"],
resources= [cfn_state_machine.attr_arn]
)]
)
event_role.attach_inline_policy(iam.Policy(self,"InvokeADMSStateMachine",document=event_role_policy_doc))
# Event Rule
rule = events.CfnRule(self, "adms-rule",
description="adms-rule",
event_pattern={
"source": ["aws.s3"],
"detail-type": ["Object Created"],
"detail": {
"bucket": {
"name": [bucket_raw.bucket_name]
},
"object": {
"key": [{
"prefix": "input/"
}]
}
}
},
name="adms-rule",
role_arn=event_role.role_arn,
targets=[events.CfnRule.TargetProperty(
arn=cfn_state_machine.attr_arn,
id="adms",
role_arn=event_role.role_arn)
]
)
# S3 Config Map File
json_template={
"cognito": {
"userPoolId":pool.user_pool_id,
"clientId":poolClient.user_pool_client_id,
"identityPoolId":admsIdentityPool.ref
},
"apigatewayendpoint": api.url,
"bucket": {
"region": region,
"name": bucket_raw.bucket_name,
}
}
config_file = s3_deploy.Source.data("scripts/config.js","window._config = "+ str(json_template)+";")
# S3 Deployment
deployment = s3_deploy.BucketDeployment(self, "DeployWebsite",
sources=[
s3_deploy.Source.asset(os.path.join("front", '../../../sample-site')),
config_file
],
destination_bucket=bucket_front,
distribution=adms_dist
)
# Cognito UserPool User
cfn_user_pool_user = cognito.CfnUserPoolUser(self, "MyCfnUserPoolUser",
user_pool_id=pool.user_pool_id,
desired_delivery_mediums=["EMAIL"],
force_alias_creation=False,
username="adms-user",
user_attributes=[cognito.CfnUserPoolUser.AttributeTypeProperty(
name="email",
value=email
),
cognito.CfnUserPoolUser.AttributeTypeProperty(
name="email_verified",
value="true"
)]
)
aws_reset_passwd = cr.AwsCustomResource(self, "aws-reset-cognito-user-passwd",
on_create=cr.AwsSdkCall(
service="CognitoIdentityServiceProvider",
action="adminSetUserPassword",
parameters={
"UserPoolId": pool.user_pool_id,
"Username": "adms-user",
"Password": password,
"Permanent": True
},
physical_resource_id=cr.PhysicalResourceId.of("adms-user")
),
policy=cr.AwsCustomResourcePolicy.from_sdk_calls(
resources=cr.AwsCustomResourcePolicy.ANY_RESOURCE
)
)
Aspects.of(self).add(AwsSolutionsChecks())
NagSuppressions.add_stack_suppressions(
self, [{"id": "AwsSolutions-S1", "reason": "The S3 Bucket has server access logs disabled. (Central bucket server access log left disable)"}]
)
NagSuppressions.add_stack_suppressions(
self, [{"id": "AwsSolutions-OS4", "reason": "Not implemented."}]
)
NagSuppressions.add_stack_suppressions(
self, [{"id": "AwsSolutions-COG3", "reason": "Not implemented due advanced security extra costs."}]
)
NagSuppressions.add_stack_suppressions(
self, [{"id": "AwsSolutions-APIG4", "reason": "Added API authorization via CDK add_resource."}]
)
NagSuppressions.add_stack_suppressions(
self, [{"id": "AwsSolutions-COG4", "reason": "Added in apiAuth = apigateway.CognitoUserPoolsAuthorizer."}]
)
NagSuppressions.add_stack_suppressions(
self, [{"id": "AwsSolutions-L1", "reason": "CDK uses an older version of Node for custom resource provider."}]
)
NagSuppressions.add_stack_suppressions(
self, [{"id": "AwsSolutions-APIG2", "reason": "Not implemented."}]
)
NagSuppressions.add_stack_suppressions(
self, [{"id": "AwsSolutions-OS3", "reason": "Fix not available in L2 Construct when using VPC-bound cluster.'"}]
)
NagSuppressions.add_stack_suppressions(
self, [{"id": "AwsSolutions-IAM4", "reason": "Using AWS managed policies."}]
)
NagSuppressions.add_stack_suppressions(
self, [{"id": "AwsSolutions-IAM5", "reason": "recursive objects (S3, transcribe, polly) and wildcards"}]
)