def setup_ga4gh_passport(self): """Experimental setup for GA4GH Passport + htsget This add lambda function as authorizer hook into ApiGatewayv2 HttpApi. Lambda function implements GA4GH Passport Clearinghouse -- claims verification logic -- to decide whether to allow the said claim to pass access htsget endpoint or, deny otherwise. """ # --- Setup Authz lambda function that implement GA4GH Passport Clearinghouse component function_name = "htsget_passport_authz_lambda" lmbda_deps_file = "lambdas/requirements.txt" lmbda_deps_out = f"lambdas/.build/{function_name}" # Setup Python dependencies as Lambda layer if not os.path.exists(lmbda_deps_out): dkr_client = docker.from_env() dkr_image = dkr_client.images.pull(repository="lambci/lambda", tag="build-python3.8") cmd = f"pip install -r {lmbda_deps_file} -t {lmbda_deps_out}/python" dkr_client.containers.run(image=dkr_image.tags[0], command=cmd, auto_remove=True, volumes={ os.getcwd(): { 'bind': "/var/task", 'mode': "rw", }, }) authzr_func = lmbda.Function( self, "PassportAuthzLambda", function_name=function_name, handler="ppauthz.handler", runtime=lmbda.Runtime.PYTHON_3_8, code=lmbda.Code.from_asset("lambdas/ppauthz"), timeout=core.Duration.seconds(20), layers=[ lmbda.LayerVersion(self, "PassportAuthzLambdaDeps", code=lmbda.Code.from_asset(lmbda_deps_out)) ]) # --- Setup GA4GH Passport ApiGatewayv2 Authorizer authzr_uri = f"arn:aws:apigateway:{self.region}:lambda:path/2015-03-31/functions/" \ f"{authzr_func.function_arn}/invocations" authzr = apigwv2.CfnAuthorizer( self, "PassportAuthorizer", api_id=self.http_api.http_api_id, authorizer_type="REQUEST", authorizer_uri=authzr_uri, authorizer_result_ttl_in_seconds=300, authorizer_payload_format_version="2.0", identity_source=[ "$request.header.Authorization", ], enable_simple_responses=True, name="PassportAuthorizer", ) authzr_arn = f"arn:aws:execute-api:{self.region}:{self.account}:" \ f"{self.http_api.http_api_id}/authorizers/{authzr.ref}" core.CfnOutput(self, "PassportAuthorizerArn", value=authzr_arn) # Allow ApiGatewayv2 to invoke authz lambda function authzr_func.add_permission( "ApiGatewayInvokePermission", principal=iam.ServicePrincipal("apigateway.amazonaws.com"), action="lambda:InvokeFunction", source_arn=authzr_arn, ) # --- Add protected endpoint routes in ApiGatewayv2 HttpApi for secured data serving with htsget backend! # Add route to protected resources with GA4GH Passport resources = [ "/reads/giab.NA12878.NIST7086.2", "/reads/data/giab.NA12878.NIST7086.2", "/variants/giab.NA12878", "/variants/data/giab.NA12878", ] for idx, res in enumerate(resources, start=1): rt_protected_pp = apigwv2.HttpRoute( self, f"PassportProtectedRoute{idx}", http_api=self.http_api, route_key=apigwv2.HttpRouteKey.with_( path=f"{res}", method=apigwv2.HttpMethod.ANY), integration=self.apigwv2_alb_integration) rt_protected_pp_cfn: apigwv2.CfnRoute = rt_protected_pp.node.default_child rt_protected_pp_cfn.authorizer_id = authzr.ref rt_protected_pp_cfn.authorization_type = "CUSTOM"
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) table = dynamodb.Table(self, "Dynamo", partition_key=dynamodb.Attribute( name="pk", type=dynamodb.AttributeType.STRING), sort_key=dynamodb.Attribute( name="sk", type=dynamodb.AttributeType.STRING), removal_policy=core.RemovalPolicy.DESTROY) http_api = add_cors_http_api(self) website_bucket = add_website_bucket(self) user_pool, identity_pool, user_pool_client = add_cognito(self) authorizer = _apigw2.CfnAuthorizer( self, "MyAuthorizer", api_id=http_api.http_api_id, authorizer_type="JWT", jwt_configuration={ "audience": [user_pool_client.user_pool_client_id], "issuer": f"https://cognito-idp.{core.Aws.REGION}.amazonaws.com/{user_pool.user_pool_id}" }, identity_source=['$request.header.Authorization'], name="MyAuthorizer", ) lambdas = [{ "name": "create", "method": _apigw2.HttpMethod.POST, }, { "name": "list", "method": _apigw2.HttpMethod.GET, }, { "name": "read", "method": _apigw2.HttpMethod.GET, }, { "name": "delete", "method": _apigw2.HttpMethod.DELETE, }] for mylambda in lambdas: add_lambda(self, mylambda["name"], table, http_api, mylambda["method"], authorizer) core.CfnOutput(self, "APP_CLIENT_ID", value=user_pool_client.user_pool_client_id) core.CfnOutput(self, "USER_POOL_ID", value=user_pool.user_pool_id) core.CfnOutput(self, "PROVIDER", value=user_pool.user_pool_provider_name) core.CfnOutput(self, "GATEWAY_URL", value=http_api.url) core.CfnOutput(self, "BUCKET_URL", value=website_bucket.bucket_website_url) core.CfnOutput(self, "BUCKET_NAME", value=website_bucket.bucket_name) core.CfnOutput(self, "REGION", value=core.Aws.REGION) core.CfnOutput(self, "IDENTITY_POOL_ID", value=identity_pool.ref)
def __init__(self, scope: core.Construct, id_: str, props, **kwargs) -> None: super().__init__(scope, id_, **kwargs) namespace = props['namespace'] htsget_refserver_ecr_repo: ecr.Repository = props['ecr_repo'] htsget_refserver_image_tag = props['htsget_refserver_image_tag'] cors_allowed_origins = props['cors_allowed_origins'] # --- Query deployment env specific config from SSM Parameter Store cert_apse2_arn = ssm.StringParameter.from_string_parameter_name( self, "SSLCertAPSE2ARN", string_parameter_name="/htsget/acm/apse2_arn", ) cert_apse2 = acm.Certificate.from_certificate_arn( self, "SSLCertAPSE2", certificate_arn=cert_apse2_arn.string_value, ) hosted_zone_id = ssm.StringParameter.from_string_parameter_name( self, "HostedZoneID", string_parameter_name="hosted_zone_id") hosted_zone_name = ssm.StringParameter.from_string_parameter_name( self, "HostedZoneName", string_parameter_name="hosted_zone_name") domain_name = ssm.StringParameter.from_string_parameter_name( self, "DomainName", string_parameter_name="/htsget/domain", ) # --- Cognito parameters are from data portal terraform stack cog_user_pool_id = ssm.StringParameter.from_string_parameter_name( self, "CogUserPoolID", string_parameter_name="/data_portal/client/cog_user_pool_id", ) cog_app_client_id_stage = ssm.StringParameter.from_string_parameter_name( self, "CogAppClientIDStage", string_parameter_name="/data_portal/client/cog_app_client_id_stage", ) cog_app_client_id_local = ssm.StringParameter.from_string_parameter_name( self, "CogAppClientIDLocal", string_parameter_name="/data_portal/client/cog_app_client_id_local", ) # --- Query main VPC and setup Security Groups vpc = ec2.Vpc.from_lookup( self, "VPC", vpc_name="main-vpc", tags={ 'Stack': "networking", }, ) private_subnets = ec2.SubnetSelection( subnet_type=ec2.SubnetType.PRIVATE, availability_zones=["ap-southeast-2a"], ) sg_elb = ec2.SecurityGroup( self, "ELBSecurityGroup", vpc=vpc, description=f"Security Group for ELB in {namespace} stack", security_group_name=f"{namespace} ELB Security Group", allow_all_outbound=False, ) sg_elb.add_ingress_rule(peer=ec2.Peer.any_ipv4(), connection=ec2.Port.tcp(80), description="Allow http inbound within VPC") sg_ecs_service = ec2.SecurityGroup( self, "ECSServiceSecurityGroup", vpc=vpc, description=f"Security Group for ECS Service in {namespace} stack", security_group_name=f"{namespace} ECS Security Group", ) sg_ecs_service.add_ingress_rule( peer=sg_elb, connection=ec2.Port.tcp(3000), description="Allow traffic from Load balancer to ECS service") # --- Setup ECS Fargate cluster config_vol = ecs.Volume( name="config-vol", host=ecs.Host(), ) task_execution_role = iam.Role( self, "ecsTaskExecutionRole", assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com")) task_execution_role.add_to_policy( iam.PolicyStatement( actions=[ "s3:GetBucketLocation", "s3:GetObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts", "s3:GetObjectTagging", "s3:GetObjectVersionTagging", "logs:CreateLogStream", "logs:PutLogEvents", "ssm:GetParameterHistory", "ssm:GetParametersByPath", "ssm:GetParameters", "ssm:GetParameter", ], resources=["*"], )) task_execution_role.add_managed_policy( iam.ManagedPolicy.from_aws_managed_policy_name( 'service-role/AmazonECSTaskExecutionRolePolicy')) task = ecs.FargateTaskDefinition( self, f"{namespace}-task", cpu=512, memory_limit_mib=1024, volumes=[config_vol], task_role=task_execution_role, execution_role=task_execution_role, ) cmd_ssm = "ssm get-parameter --name '/htsget/refserver/config' --output text --query Parameter.Value" sidecar_container: ecs.ContainerDefinition = task.add_container( f"{namespace}-sidecar", image=ecs.ContainerImage.from_registry( "quay.io/victorskl/aws-cli:2.1.3"), essential=False, entry_point=[ "/bin/bash", "-c", f"aws {cmd_ssm} > config.json", ], logging=ecs.LogDriver.aws_logs(stream_prefix=f"{namespace}", ), ) sidecar_container.add_mount_points( ecs.MountPoint( container_path="/aws", read_only=False, source_volume=config_vol.name, )) main_container: ecs.ContainerDefinition = task.add_container( namespace, image=ecs.ContainerImage.from_ecr_repository( repository=htsget_refserver_ecr_repo, tag=htsget_refserver_image_tag, ), essential=True, command=[ "./htsget-refserver", "-config", "/usr/src/app/config/config.json" ], logging=ecs.LogDriver.aws_logs(stream_prefix=f"{namespace}", ), ) main_container.add_port_mappings( ecs.PortMapping( container_port=3000, protocol=ecs.Protocol.TCP, )) main_container.add_mount_points( ecs.MountPoint( container_path="/usr/src/app/config", read_only=True, source_volume=config_vol.name, )) main_container.add_container_dependencies( ecs.ContainerDependency( container=sidecar_container, condition=ecs.ContainerDependencyCondition.COMPLETE, )) cluster = ecs.Cluster(self, f"{namespace}-cluster", vpc=vpc) service = ecs.FargateService( self, f"{namespace}-service", platform_version=ecs.FargatePlatformVersion.VERSION1_4, task_definition=task, cluster=cluster, vpc_subnets=private_subnets, desired_count=1, security_groups=[ sg_ecs_service, ], ) # --- Setup Application Load Balancer in front of ECS cluster lb = elbv2.ApplicationLoadBalancer( self, f"{namespace}-lb", vpc=vpc, internet_facing=False, security_group=sg_elb, deletion_protection=True, ) http_listener = lb.add_listener( "HttpLBListener", port=80, ) health_check = elbv2.HealthCheck(interval=core.Duration.seconds(30), path="/reads/service-info", timeout=core.Duration.seconds(5)) http_listener.add_targets( "LBtoECS", port=3000, protocol=elbv2.ApplicationProtocol.HTTP, targets=[service], health_check=health_check, ) core.CfnOutput(self, "LoadBalancerDNS", value=lb.load_balancer_dns_name) # --- Setup APIGatewayv2 HttpApi using VpcLink private integration to ALB/ECS in private subnets vpc_link = apigwv2.VpcLink(self, f"{namespace}-VpcLink", vpc=vpc, security_groups=[ sg_ecs_service, sg_elb, ]) self.apigwv2_alb_integration = apigwv2i.HttpAlbIntegration( listener=http_listener, vpc_link=vpc_link, ) custom_domain = apigwv2.DomainName( self, "CustomDomain", certificate=cert_apse2, domain_name=domain_name.string_value, ) self.http_api = apigwv2.HttpApi( self, f"{namespace}-apigw", default_domain_mapping=apigwv2.DomainMappingOptions( domain_name=custom_domain), cors_preflight=apigwv2.CorsPreflightOptions( allow_origins=cors_allowed_origins, allow_headers=["*"], allow_methods=[ apigwv2.CorsHttpMethod.ANY, ], allow_credentials=True, )) core.CfnOutput(self, "ApiEndpoint", value=self.http_api.api_endpoint) # --- Setup DNS for the custom domain hosted_zone = route53.HostedZone.from_hosted_zone_attributes( self, "HostedZone", hosted_zone_id=hosted_zone_id.string_value, zone_name=hosted_zone_name.string_value, ) route53.ARecord( self, "ApiCustomDomainAlias", zone=hosted_zone, record_name="htsget", target=route53.RecordTarget.from_alias( route53t.ApiGatewayv2DomainProperties( regional_domain_name=custom_domain.regional_domain_name, regional_hosted_zone_id=custom_domain. regional_hosted_zone_id)), ) core.CfnOutput( self, "HtsgetEndpoint", value=custom_domain.name, ) cognito_authzr = apigwv2.CfnAuthorizer( self, "CognitoAuthorizer", api_id=self.http_api.http_api_id, authorizer_type="JWT", identity_source=[ "$request.header.Authorization", ], name="CognitoAuthorizer", jwt_configuration=apigwv2.CfnAuthorizer.JWTConfigurationProperty( audience=[ cog_app_client_id_stage.string_value, cog_app_client_id_local.string_value, ], issuer= f"https://cognito-idp.{self.region}.amazonaws.com/{cog_user_pool_id.string_value}" )) # Add catch all routes rt_catchall = apigwv2.HttpRoute( self, "CatchallRoute", http_api=self.http_api, route_key=apigwv2.HttpRouteKey.with_( path="/{proxy+}", method=apigwv2.HttpMethod.GET), integration=self.apigwv2_alb_integration) rt_catchall_cfn: apigwv2.CfnRoute = rt_catchall.node.default_child rt_catchall_cfn.authorizer_id = cognito_authzr.ref rt_catchall_cfn.authorization_type = "JWT" # Comment this to opt-out setting up experimental Passport + htsget self.setup_ga4gh_passport()