From c233ff0202734c8bf449398ff5312aaec1eb1e20 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Wed, 26 Mar 2025 15:26:27 -0400 Subject: [PATCH 01/13] Add API Gateway to the stack --- backend/src/iac/backend-stack.ts | 294 +++++++++++++++++++++++-------- 1 file changed, 223 insertions(+), 71 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index ae522ffe..b0acd15c 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -2,8 +2,11 @@ import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as logs from 'aws-cdk-lib/aws-logs'; -import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as cognito from 'aws-cdk-lib/aws-cognito'; +import * as apigateway from 'aws-cdk-lib/aws-apigateway'; +import * as servicediscovery from 'aws-cdk-lib/aws-servicediscovery'; +import * as iam from 'aws-cdk-lib/aws-iam'; + import { Construct } from 'constructs'; import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; import { RemovalPolicy } from 'aws-cdk-lib'; @@ -12,8 +15,6 @@ interface BackendStackProps extends cdk.StackProps { environment: string; cognitoClientId: string; cognitoUserPoolId: string; - domainName?: string; // Optional domain name for certificate - hostedZoneId?: string; // Optional hosted zone ID for domain } export class BackendStack extends cdk.Stack { @@ -23,26 +24,34 @@ export class BackendStack extends cdk.Stack { const isProd = props.environment === 'production'; const appName = 'AIMedicalReport'; - // Look up existing VPC or create a new one - const vpc: ec2.IVpc = new ec2.Vpc(this, `${appName}VPC`, { + // VPC + const vpc = new ec2.Vpc(this, `${appName}VPC`, { vpcName: `${appName}VPC-${props.environment}`, maxAzs: 2, + natGateways: isProd ? 2 : 1, }); + // ECS Cluster const cluster = new ecs.Cluster(this, `${appName}Cluster`, { vpc, clusterName: `${appName}Cluster-${props.environment}`, containerInsights: true, + enableFargateCapacityProviders: true, + }); + + // CloudMap Namespace for service discovery + const namespace = cluster.addDefaultCloudMapNamespace({ + name: `${appName.toLowerCase()}.local`, }); - // Create Log Group for container + // Log Group const logGroup = new logs.LogGroup(this, `${appName}LogGroup`, { logGroupName: `/ecs/${appName}-${props.environment}`, retention: isProd ? logs.RetentionDays.ONE_MONTH : logs.RetentionDays.ONE_WEEK, removalPolicy: isProd ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY, }); - // Create DynamoDB table for reports + // DynamoDB table for reports const reportsTable = new Table(this, `${appName}ReportsTable-${props.environment}`, { tableName: `${appName}ReportsTable${props.environment}`, partitionKey: { @@ -57,7 +66,7 @@ export class BackendStack extends cdk.Stack { removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY, }); - // Add a GSI for querying by date (most recent first) + // Add GSI for querying by date reportsTable.addGlobalSecondaryIndex({ indexName: 'userIdDateIndex', partitionKey: { @@ -70,20 +79,50 @@ export class BackendStack extends cdk.Stack { }, }); - // Look up existing Cognito User Pool - const userPoolId = - props.cognitoUserPoolId || - cognito.UserPool.fromUserPoolId(this, `${appName}UserPool`, 'us-east-1_PszlvSmWc').userPoolId; + // Cognito User Pool + const userPool = cognito.UserPool.fromUserPoolId( + this, + `${appName}UserPool`, + props.cognitoUserPoolId || 'us-east-1_PszlvSmWc', + ); - // Create a Cognito domain if it doesn't exist + // Cognito domain const userPoolDomain = cognito.UserPoolDomain.fromDomainName( this, `${appName}ExistingDomain-${props.environment}`, - 'us-east-1pszlvsmwc', // The domain prefix without the .auth.region.amazoncognito.com part + 'us-east-1pszlvsmwc', + ); + + // User Pool Client + const userPoolClient = cognito.UserPoolClient.fromUserPoolClientId( + this, + `${appName}UserPoolClient-${props.environment}`, + props.cognitoClientId, + ); + + // Security Group for Fargate service + const serviceSecurityGroup = new ec2.SecurityGroup( + this, + `${appName}ServiceSG-${props.environment}`, + { + vpc, + allowAllOutbound: true, + description: 'Security group for Fargate service', + }, ); - // Replace the userPoolClient reference with a direct reference to the client ID - const userPoolClientId = props.cognitoClientId; + // Add inbound rules to allow traffic from API Gateway + serviceSecurityGroup.addIngressRule( + ec2.Peer.ipv4(vpc.vpcCidrBlock), + ec2.Port.tcp(3000), + 'Allow inbound HTTP traffic from within VPC', + ); + + serviceSecurityGroup.addIngressRule( + ec2.Peer.ipv4(vpc.vpcCidrBlock), + ec2.Port.tcp(3443), + 'Allow inbound HTTPS traffic from within VPC', + ); // Task Definition const taskDefinition = new ecs.FargateTaskDefinition( @@ -95,6 +134,23 @@ export class BackendStack extends cdk.Stack { }, ); + // Grant DynamoDB permissions to task + reportsTable.grantReadWriteData(taskDefinition.taskRole); + + // Create a secrets manager for the SSL certificate and key + const certificateSecret = new cdk.aws_secretsmanager.Secret(this, `${appName}CertSecret-${props.environment}`, { + secretName: `${appName}/ssl-cert-${props.environment}`, + description: 'SSL certificate and private key for HTTPS', + generateSecretString: { + secretStringTemplate: JSON.stringify({ + // You'll need to populate these values after deployment + certificate: '-----BEGIN CERTIFICATE-----\nYour certificate here\n-----END CERTIFICATE-----', + privateKey: '-----BEGIN PRIVATE KEY-----\nYour private key here\n-----END PRIVATE KEY-----' + }), + generateStringKey: 'dummy' // This key won't be used but is required + }, + }); + // Container const container = taskDefinition.addContainer(`${appName}Container-${props.environment}`, { image: ecs.ContainerImage.fromAsset('../backend/', { @@ -107,85 +163,68 @@ export class BackendStack extends cdk.Stack { // Basic environment variables NODE_ENV: props.environment, PORT: '3000', + HTTPS_PORT: '3443', // Add HTTPS port + ENABLE_HTTPS: 'true', // Enable HTTPS // AWS related AWS_REGION: this.region, - AWS_COGNITO_USER_POOL_ID: userPoolId, - AWS_COGNITO_CLIENT_ID: userPoolClientId, + AWS_COGNITO_USER_POOL_ID: userPool.userPoolId, + AWS_COGNITO_CLIENT_ID: userPoolClient.userPoolClientId, DYNAMODB_REPORTS_TABLE: reportsTable.tableName, // Perplexity related PERPLEXITY_API_KEY_SECRET_NAME: `medical-reports-explainer/${props.environment}/perplexity-api-key`, PERPLEXITY_MODEL: 'sonar', PERPLEXITY_MAX_TOKENS: '2048', + + // SSL Certificate secret + SSL_CERT_SECRET_NAME: certificateSecret.secretName, }, logging: ecs.LogDrivers.awsLogs({ streamPrefix: appName, logGroup, }), + healthCheck: { + command: ['CMD-SHELL', 'curl -f -k https://localhost:3443/api/health || exit 1'], + interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(5), + retries: 3, + startPeriod: cdk.Duration.seconds(60), + }, }); + // Grant the task role access to read the SSL certificate secret + certificateSecret.grantRead(taskDefinition.taskRole); + container.addPortMappings({ containerPort: 3000, + name: 'http-api', protocol: ecs.Protocol.TCP, }); - // 1. Create ALB - const alb = new elbv2.ApplicationLoadBalancer(this, `${appName}ALB-${props.environment}`, { - vpc, - internetFacing: true, - loadBalancerName: `${appName}-${props.environment}`, - }); - - // 2. Create ALB Target Group - const targetGroup = new elbv2.ApplicationTargetGroup( - this, - `${appName}TargetGroup-${props.environment}`, - { - vpc, - port: 3000, - protocol: elbv2.ApplicationProtocol.HTTP, - targetType: elbv2.TargetType.IP, - healthCheck: { - path: '/api/health', - interval: cdk.Duration.seconds(30), - timeout: cdk.Duration.seconds(5), - }, - }, - ); - - // 3. HTTP 80 Listener - const httpListener = alb.addListener(`${appName}HttpListener-${props.environment}`, { - port: 80, - protocol: elbv2.ApplicationProtocol.HTTP, - defaultAction: elbv2.ListenerAction.forward([targetGroup]), + container.addPortMappings({ + containerPort: 3443, + name: 'https-api', + protocol: ecs.Protocol.TCP, }); - // 4. Create a security group for the Fargate service - const serviceSecurityGroup = new ec2.SecurityGroup( - this, - `${appName}ServiceSG-${props.environment}`, - { - vpc, - allowAllOutbound: true, - }, - ); - - // 5. Create the Fargate service WITHOUT registering it with the target group yet + // Create Fargate Service with CloudMap service discovery const fargateService = new ecs.FargateService(this, `${appName}Service-${props.environment}`, { cluster, taskDefinition, desiredCount: isProd ? 2 : 1, - assignPublicIp: false, securityGroups: [serviceSecurityGroup], + assignPublicIp: false, // Using private subnets with NAT gateway + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + cloudMapOptions: { + name: `${appName.toLowerCase()}-service`, + dnsRecordType: servicediscovery.DnsRecordType.A, + dnsTtl: cdk.Duration.seconds(30), + container: container, + containerPort: 3000, + }, }); - // 6. Add explicit dependency to ensure the listener exists before the service - fargateService.node.addDependency(httpListener); - - // 7. Now register the service with the target group - targetGroup.addTarget(fargateService); - // Add autoscaling for production if (isProd) { const scaling = fargateService.autoScaleTaskCount({ @@ -200,22 +239,135 @@ export class BackendStack extends cdk.Stack { }); } - // Add output for the table name + // Create VPC Link for API Gateway (using HTTP API VPC Link) + const vpcLink = new apigateway.VpcLink(this, `${appName}VpcLink-${props.environment}`, { + vpc, + description: `VPC Link for ${appName} ${props.environment}`, + vpcLinkName: `${appName}VpcLink-${props.environment}`, + }); + + // Create API Gateway + const api = new apigateway.RestApi(this, `${appName}Api-${props.environment}`, { + restApiName: `${appName}-${props.environment}`, + description: `API for ${appName} ${props.environment}`, + deployOptions: { + stageName: props.environment, + loggingLevel: apigateway.MethodLoggingLevel.INFO, + dataTraceEnabled: true, + }, + defaultCorsPreflightOptions: { + allowOrigins: apigateway.Cors.ALL_ORIGINS, + allowMethods: apigateway.Cors.ALL_METHODS, + allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'], + }, + }); + + // Create Cognito Authorizer + const authorizer = new apigateway.CognitoUserPoolsAuthorizer( + this, + `${appName}Authorizer-${props.environment}`, + { + cognitoUserPools: [userPool], + authorizerName: `${appName}Authorizer-${props.environment}`, + identitySource: 'method.request.header.Authorization', + }, + ); + + // Get the service URL from CloudMap (now using HTTPS) + const serviceUrl = `https://${appName.toLowerCase()}-service.${appName.toLowerCase()}.local:3443`; + + // Create proxy resource with Cognito authorization + const proxyResource = api.root.addResource('{proxy+}'); + + // Integration with Fargate service via VPC Link + const integration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'ANY', + options: { + connectionType: apigateway.ConnectionType.VPC_LINK, + vpcLink: vpcLink, + requestParameters: { + 'integration.request.path.proxy': 'method.request.path.proxy', + }, + // Skip TLS verification for self-signed certificates in internal traffic + tlsConfig: { + insecureSkipVerification: true, + }, + }, + uri: `${serviceUrl}/{proxy}`, + }); + + proxyResource.addMethod('ANY', integration, { + authorizer: authorizer, + authorizationType: apigateway.AuthorizationType.COGNITO, + requestParameters: { + 'method.request.path.proxy': true, + }, + }); + + // Add health check endpoint without authorization + const healthResource = api.root.addResource('health'); + healthResource.addMethod( + 'GET', + new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'GET', + options: { + connectionType: apigateway.ConnectionType.VPC_LINK, + vpcLink: vpcLink, + // Skip TLS verification for self-signed certificates in internal traffic + tlsConfig: { + insecureSkipVerification: true, + }, + }, + uri: `${serviceUrl}/api/health`, + }), + ); + + // Add execution role policy to allow API Gateway to access VPC resources + const executionRole = new iam.Role(this, `${appName}APIGatewayVPCRole-${props.environment}`, { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs'), + iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonVPCCrossAccountNetworkInterfaceOperations'), + ], + }); + + // Attach the execution role to the API + const apiResource = api.node.findChild('Default') as apigateway.CfnRestApi; + apiResource.addPropertyOverride('Policy', { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'apigateway.amazonaws.com', + }, + Action: 'sts:AssumeRole', + Resource: executionRole.roleArn, + }, + ], + }); + + // Outputs new cdk.CfnOutput(this, 'ReportsTableName', { value: reportsTable.tableName, description: 'DynamoDB Reports Table Name', }); - // Add output for Cognito domain new cdk.CfnOutput(this, 'CognitoDomain', { value: `https://${userPoolDomain.domainName}.auth.${this.region}.amazoncognito.com`, description: 'Cognito Domain URL', }); - // Outputs - new cdk.CfnOutput(this, 'LoadBalancerDNS', { - value: alb.loadBalancerDnsName, - description: 'Load Balancer DNS Name', + new cdk.CfnOutput(this, 'ApiGatewayUrl', { + value: api.url, + description: 'API Gateway URL', + }); + + new cdk.CfnOutput(this, 'ServiceDiscoveryUrl', { + value: serviceUrl, + description: 'Service Discovery URL', }); } } From 0bb9dfc1040a69cb873659fdbaff94fadf219962 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Wed, 26 Mar 2025 15:26:40 -0400 Subject: [PATCH 02/13] Add API Gateway to the stack --- backend/src/iac/backend-stack.ts | 38 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index b0acd15c..433785d6 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -40,7 +40,7 @@ export class BackendStack extends cdk.Stack { }); // CloudMap Namespace for service discovery - const namespace = cluster.addDefaultCloudMapNamespace({ + cluster.addDefaultCloudMapNamespace({ name: `${appName.toLowerCase()}.local`, }); @@ -138,18 +138,24 @@ export class BackendStack extends cdk.Stack { reportsTable.grantReadWriteData(taskDefinition.taskRole); // Create a secrets manager for the SSL certificate and key - const certificateSecret = new cdk.aws_secretsmanager.Secret(this, `${appName}CertSecret-${props.environment}`, { - secretName: `${appName}/ssl-cert-${props.environment}`, - description: 'SSL certificate and private key for HTTPS', - generateSecretString: { - secretStringTemplate: JSON.stringify({ - // You'll need to populate these values after deployment - certificate: '-----BEGIN CERTIFICATE-----\nYour certificate here\n-----END CERTIFICATE-----', - privateKey: '-----BEGIN PRIVATE KEY-----\nYour private key here\n-----END PRIVATE KEY-----' - }), - generateStringKey: 'dummy' // This key won't be used but is required + const certificateSecret = new cdk.aws_secretsmanager.Secret( + this, + `${appName}CertSecret-${props.environment}`, + { + secretName: `${appName}/ssl-cert-${props.environment}`, + description: 'SSL certificate and private key for HTTPS', + generateSecretString: { + secretStringTemplate: JSON.stringify({ + // You'll need to populate these values after deployment + certificate: + '-----BEGIN CERTIFICATE-----\nYour certificate here\n-----END CERTIFICATE-----', + privateKey: + '-----BEGIN PRIVATE KEY-----\nYour private key here\n-----END PRIVATE KEY-----', + }), + generateStringKey: 'dummy', // This key won't be used but is required + }, }, - }); + ); // Container const container = taskDefinition.addContainer(`${appName}Container-${props.environment}`, { @@ -328,8 +334,12 @@ export class BackendStack extends cdk.Stack { const executionRole = new iam.Role(this, `${appName}APIGatewayVPCRole-${props.environment}`, { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs'), - iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonVPCCrossAccountNetworkInterfaceOperations'), + iam.ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AmazonAPIGatewayPushToCloudWatchLogs', + ), + iam.ManagedPolicy.fromAwsManagedPolicyName( + 'AmazonVPCCrossAccountNetworkInterfaceOperations', + ), ], }); From 5c1d6095b5f759e61e1828e0cc14e2387494f376 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Wed, 26 Mar 2025 15:48:13 -0400 Subject: [PATCH 03/13] Add API Gateway with ALB --- backend/package-lock.json | 10 +++--- backend/package.json | 18 +++++------ backend/src/iac/backend-stack.ts | 53 ++++++++++++++++++++++---------- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 431b674c..853ab10c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -54,7 +54,7 @@ "@typescript-eslint/parser": "^7.9.0", "@vitest/coverage-c8": "^0.33.0", "aws-cdk": "2.139.0", - "aws-cdk-lib": "^2.184.1", + "aws-cdk-lib": "^2.185.0", "dotenv-cli": "^8.0.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.0.0", @@ -72,7 +72,7 @@ "vitest": "^0.33.0" }, "peerDependencies": { - "aws-cdk-lib": "2.184.1" + "aws-cdk-lib": "^2.185.0" } }, "node_modules/@ampproject/remapping": { @@ -4677,9 +4677,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.184.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.184.1.tgz", - "integrity": "sha512-No9g0SGadiDz0IEUIeJg4wSV/jFCGcouW2zUOTjV8OU4gTMoGiqC8BYSv7E6ucUtW6rmSFVK+pbc8XOFZOo1cg==", + "version": "2.185.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.185.0.tgz", + "integrity": "sha512-RNcQeNnInumDF1hq3gAf+/A6jhvYDof5a7418gEs/y6359gTYZpTCQkgItC50iV3MmkgerrBAdOE7CDEtQNDWw==", "bundleDependencies": [ "@balena/dockerignore", "case", diff --git a/backend/package.json b/backend/package.json index 7d20ce1f..03c865e8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,14 +28,15 @@ }, "dependencies": { "@aws-sdk/client-dynamodb": "^3.758.0", - "@aws-sdk/util-dynamodb": "^3.758.0", "@aws-sdk/client-secrets-manager": "^3.758.0", + "@aws-sdk/util-dynamodb": "^3.758.0", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", - "@nestjs/platform-express": "^10.0.0", "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.1.13", "@types/jest": "^29.5.12", "axios": "^1.8.1", "class-transformer": "^0.5.1", @@ -47,15 +48,14 @@ "helmet": "^7.0.0", "jsonwebtoken": "^9.0.2", "jwk-to-pem": "^2.0.5", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "source-map-support": "^0.5.21", - "web-vitals": "^2.1.4", - "aws-cdk-lib": "2.184.1", - "@nestjs/swagger": "^7.1.13", "swagger-ui-express": "^5.0.0", - "passport": "^0.7.0", - "passport-jwt": "^4.0.1" + "web-vitals": "^2.1.4", + "aws-cdk-lib": "^2.185.0" }, "devDependencies": { "@aws-cdk/assert": "^2.68.0", @@ -73,7 +73,7 @@ "@typescript-eslint/parser": "^7.9.0", "@vitest/coverage-c8": "^0.33.0", "aws-cdk": "2.139.0", - "aws-cdk-lib": "^2.184.1", + "aws-cdk-lib": "^2.185.0", "dotenv-cli": "^8.0.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.0.0", @@ -91,6 +91,6 @@ "vitest": "^0.33.0" }, "peerDependencies": { - "aws-cdk-lib": "2.184.1" + "aws-cdk-lib": "^2.185.0" } } diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 433785d6..3be31ecc 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -6,6 +6,7 @@ import * as cognito from 'aws-cdk-lib/aws-cognito'; import * as apigateway from 'aws-cdk-lib/aws-apigateway'; import * as servicediscovery from 'aws-cdk-lib/aws-servicediscovery'; import * as iam from 'aws-cdk-lib/aws-iam'; +import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import { Construct } from 'constructs'; import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; @@ -245,11 +246,39 @@ export class BackendStack extends cdk.Stack { }); } - // Create VPC Link for API Gateway (using HTTP API VPC Link) - const vpcLink = new apigateway.VpcLink(this, `${appName}VpcLink-${props.environment}`, { + // Create a Network Load Balancer for the Fargate service + const nlb = new elbv2.NetworkLoadBalancer(this, `${appName}NLB-${props.environment}`, { vpc, + internetFacing: false, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + }); + + // Add a listener to the NLB + const listener = nlb.addListener(`${appName}Listener-${props.environment}`, { + port: 80, + protocol: elbv2.Protocol.TCP, + }); + + // Add the Fargate service as a target to the listener + listener.addTargets(`${appName}TargetGroup-${props.environment}`, { + targets: [fargateService], + port: 3000, + protocol: elbv2.Protocol.TCP, + healthCheck: { + enabled: true, + protocol: elbv2.Protocol.HTTP, + path: '/api/health', + interval: cdk.Duration.seconds(30), + healthyThresholdCount: 2, + unhealthyThresholdCount: 2, + timeout: cdk.Duration.seconds(5), + }, + }); + + // Create VPC Link for API Gateway using the NLB + const vpcLink = new apigateway.VpcLink(this, `${appName}VpcLink-${props.environment}`, { + targets: [nlb], description: `VPC Link for ${appName} ${props.environment}`, - vpcLinkName: `${appName}VpcLink-${props.environment}`, }); // Create API Gateway @@ -279,8 +308,8 @@ export class BackendStack extends cdk.Stack { }, ); - // Get the service URL from CloudMap (now using HTTPS) - const serviceUrl = `https://${appName.toLowerCase()}-service.${appName.toLowerCase()}.local:3443`; + // Use the NLB DNS name for the service URL + const serviceUrl = `http://${nlb.loadBalancerDnsName}`; // Create proxy resource with Cognito authorization const proxyResource = api.root.addResource('{proxy+}'); @@ -295,10 +324,6 @@ export class BackendStack extends cdk.Stack { requestParameters: { 'integration.request.path.proxy': 'method.request.path.proxy', }, - // Skip TLS verification for self-signed certificates in internal traffic - tlsConfig: { - insecureSkipVerification: true, - }, }, uri: `${serviceUrl}/{proxy}`, }); @@ -321,10 +346,6 @@ export class BackendStack extends cdk.Stack { options: { connectionType: apigateway.ConnectionType.VPC_LINK, vpcLink: vpcLink, - // Skip TLS verification for self-signed certificates in internal traffic - tlsConfig: { - insecureSkipVerification: true, - }, }, uri: `${serviceUrl}/api/health`, }), @@ -375,9 +396,9 @@ export class BackendStack extends cdk.Stack { description: 'API Gateway URL', }); - new cdk.CfnOutput(this, 'ServiceDiscoveryUrl', { - value: serviceUrl, - description: 'Service Discovery URL', + new cdk.CfnOutput(this, 'NetworkLoadBalancerDns', { + value: nlb.loadBalancerDnsName, + description: 'Network Load Balancer DNS Name', }); } } From df333f673d8f49e77d3e885377cc7412dd01e26b Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Wed, 26 Mar 2025 15:50:57 -0400 Subject: [PATCH 04/13] Fixing CDK errors --- backend/src/iac/backend-stack.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 3be31ecc..d7b99987 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -364,22 +364,22 @@ export class BackendStack extends cdk.Stack { ], }); - // Attach the execution role to the API - const apiResource = api.node.findChild('Default') as apigateway.CfnRestApi; - apiResource.addPropertyOverride('Policy', { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Principal: { - Service: 'apigateway.amazonaws.com', - }, - Action: 'sts:AssumeRole', - Resource: executionRole.roleArn, - }, + // Replace the problematic code with a proper way to set the API Gateway policy + const apiPolicy = new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + principals: [new iam.ServicePrincipal('apigateway.amazonaws.com')], + actions: ['sts:AssumeRole'], + resources: [executionRole.roleArn], + }), ], }); + // Apply the policy to the API Gateway using CfnRestApi + const cfnApi = api.node.defaultChild as apigateway.CfnRestApi; + cfnApi.policy = apiPolicy.toJSON(); + // Outputs new cdk.CfnOutput(this, 'ReportsTableName', { value: reportsTable.tableName, From cdf57b65df8d2ebf226aa364dd315de226c4e280 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Wed, 26 Mar 2025 20:52:52 -0400 Subject: [PATCH 05/13] Remove healthcheck --- backend/src/iac/backend-stack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index d7b99987..5f03de43 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -191,13 +191,13 @@ export class BackendStack extends cdk.Stack { streamPrefix: appName, logGroup, }), - healthCheck: { + /*healthCheck: { command: ['CMD-SHELL', 'curl -f -k https://localhost:3443/api/health || exit 1'], interval: cdk.Duration.seconds(30), timeout: cdk.Duration.seconds(5), retries: 3, startPeriod: cdk.Duration.seconds(60), - }, + },*/ }); // Grant the task role access to read the SSL certificate secret From 54dd10ea34a2acc948eb838293213dc2ef618cc4 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Thu, 27 Mar 2025 10:48:16 -0400 Subject: [PATCH 06/13] Add API Resource Policy --- backend/src/iac/backend-stack.ts | 43 ++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 5f03de43..a4721a7a 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -352,7 +352,7 @@ export class BackendStack extends cdk.Stack { ); // Add execution role policy to allow API Gateway to access VPC resources - const executionRole = new iam.Role(this, `${appName}APIGatewayVPCRole-${props.environment}`, { + new iam.Role(this, `${appName}APIGatewayVPCRole-${props.environment}`, { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( @@ -364,21 +364,50 @@ export class BackendStack extends cdk.Stack { ], }); - // Replace the problematic code with a proper way to set the API Gateway policy - const apiPolicy = new iam.PolicyDocument({ + const apiResourcePolicy = new iam.PolicyDocument({ statements: [ + // Allow all users to access the health endpoint in all stages new iam.PolicyStatement({ effect: iam.Effect.ALLOW, - principals: [new iam.ServicePrincipal('apigateway.amazonaws.com')], - actions: ['sts:AssumeRole'], - resources: [executionRole.roleArn], + principals: [new iam.AnyPrincipal()], + actions: ['execute-api:Invoke'], + resources: [ + `arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/GET/health`, + ], + }), + + // Allow only authenticated Cognito users to access all other endpoints + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + principals: [new iam.AnyPrincipal()], + actions: ['execute-api:Invoke'], + resources: [`arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/*`], + conditions: { + StringEquals: { + 'aws:PrincipalTag/cognito-identity.amazonaws.com:sub': + '${cognito-identity.amazonaws.com:sub}', + }, + }, + }), + + // Deny all non-HTTPS requests + new iam.PolicyStatement({ + effect: iam.Effect.DENY, + principals: [new iam.AnyPrincipal()], + actions: ['execute-api:Invoke'], + resources: [`arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/*`], + conditions: { + Bool: { + 'aws:SecureTransport': 'false', + }, + }, }), ], }); // Apply the policy to the API Gateway using CfnRestApi const cfnApi = api.node.defaultChild as apigateway.CfnRestApi; - cfnApi.policy = apiPolicy.toJSON(); + cfnApi.policy = apiResourcePolicy.toJSON(); // Outputs new cdk.CfnOutput(this, 'ReportsTableName', { From 5994615a276cf701e8755c0424f72267c8bc494e Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Mon, 31 Mar 2025 10:20:03 -0400 Subject: [PATCH 07/13] Remove healthcheck --- backend/src/iac/backend-stack.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index a4721a7a..0ead8bdc 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -191,13 +191,6 @@ export class BackendStack extends cdk.Stack { streamPrefix: appName, logGroup, }), - /*healthCheck: { - command: ['CMD-SHELL', 'curl -f -k https://localhost:3443/api/health || exit 1'], - interval: cdk.Duration.seconds(30), - timeout: cdk.Duration.seconds(5), - retries: 3, - startPeriod: cdk.Duration.seconds(60), - },*/ }); // Grant the task role access to read the SSL certificate secret From 3de8742d1c78ce71f43d493c480a6bcb7ff58806 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Mon, 31 Mar 2025 10:26:10 -0400 Subject: [PATCH 08/13] Address SonarQube warnings --- backend/src/iac/backend-stack.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 0ead8bdc..19c00605 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -342,6 +342,9 @@ export class BackendStack extends cdk.Stack { }, uri: `${serviceUrl}/api/health`, }), + { + authorizationType: apigateway.AuthorizationType.NONE, + } ); // Add execution role policy to allow API Gateway to access VPC resources @@ -360,6 +363,8 @@ export class BackendStack extends cdk.Stack { const apiResourcePolicy = new iam.PolicyDocument({ statements: [ // Allow all users to access the health endpoint in all stages + // Security note: This is intentionally public as it's a non-sensitive health check endpoint + // that doesn't expose any protected data or functionality new iam.PolicyStatement({ effect: iam.Effect.ALLOW, principals: [new iam.AnyPrincipal()], @@ -372,6 +377,8 @@ export class BackendStack extends cdk.Stack { // Allow only authenticated Cognito users to access all other endpoints new iam.PolicyStatement({ effect: iam.Effect.ALLOW, + // Using AnyPrincipal here but access is restricted by the Cognito condition below + // This is not truly public access as only authenticated users can meet the condition principals: [new iam.AnyPrincipal()], actions: ['execute-api:Invoke'], resources: [`arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/*`], From af1b8d288ba15f492cbf346e8cbd877b10cbd8f1 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Mon, 31 Mar 2025 10:27:06 -0400 Subject: [PATCH 09/13] Address SonarQube warnings --- backend/src/iac/backend-stack.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 19c00605..cbc2be60 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -247,6 +247,9 @@ export class BackendStack extends cdk.Stack { }); // Add a listener to the NLB + // Security note: This NLB is internal-only within a private subnet and not internet-facing. + // External traffic is secured via API Gateway's HTTPS endpoints, so unencrypted internal + // communication within the VPC's private network boundary is acceptable here. const listener = nlb.addListener(`${appName}Listener-${props.environment}`, { port: 80, protocol: elbv2.Protocol.TCP, @@ -344,7 +347,7 @@ export class BackendStack extends cdk.Stack { }), { authorizationType: apigateway.AuthorizationType.NONE, - } + }, ); // Add execution role policy to allow API Gateway to access VPC resources From afac2e2300c152983837ff97c80b1822c890d50c Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Mon, 31 Mar 2025 10:36:55 -0400 Subject: [PATCH 10/13] Remove public health endpoint --- backend/src/iac/backend-stack.ts | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index cbc2be60..41b6cf3a 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -332,24 +332,6 @@ export class BackendStack extends cdk.Stack { }, }); - // Add health check endpoint without authorization - const healthResource = api.root.addResource('health'); - healthResource.addMethod( - 'GET', - new apigateway.Integration({ - type: apigateway.IntegrationType.HTTP_PROXY, - integrationHttpMethod: 'GET', - options: { - connectionType: apigateway.ConnectionType.VPC_LINK, - vpcLink: vpcLink, - }, - uri: `${serviceUrl}/api/health`, - }), - { - authorizationType: apigateway.AuthorizationType.NONE, - }, - ); - // Add execution role policy to allow API Gateway to access VPC resources new iam.Role(this, `${appName}APIGatewayVPCRole-${props.environment}`, { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), @@ -365,18 +347,6 @@ export class BackendStack extends cdk.Stack { const apiResourcePolicy = new iam.PolicyDocument({ statements: [ - // Allow all users to access the health endpoint in all stages - // Security note: This is intentionally public as it's a non-sensitive health check endpoint - // that doesn't expose any protected data or functionality - new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - principals: [new iam.AnyPrincipal()], - actions: ['execute-api:Invoke'], - resources: [ - `arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/GET/health`, - ], - }), - // Allow only authenticated Cognito users to access all other endpoints new iam.PolicyStatement({ effect: iam.Effect.ALLOW, From a5817c87920ce81b7b6a7a2fdd5eb5a4dfd348f8 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 1 Apr 2025 11:02:05 -0400 Subject: [PATCH 11/13] Re-create the API Gateway --- backend/src/iac/backend-stack.ts | 187 ++++++++++++++++++++----------- 1 file changed, 122 insertions(+), 65 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 41b6cf3a..c253c91e 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -277,8 +277,9 @@ export class BackendStack extends cdk.Stack { description: `VPC Link for ${appName} ${props.environment}`, }); - // Create API Gateway - const api = new apigateway.RestApi(this, `${appName}Api-${props.environment}`, { + // Create API Gateway first without any resources or methods + const apiLogicalId = `${appName}-api-${props.environment}`; + const api = new apigateway.RestApi(this, apiLogicalId, { restApiName: `${appName}-${props.environment}`, description: `API for ${appName} ${props.environment}`, deployOptions: { @@ -286,11 +287,7 @@ export class BackendStack extends cdk.Stack { loggingLevel: apigateway.MethodLoggingLevel.INFO, dataTraceEnabled: true, }, - defaultCorsPreflightOptions: { - allowOrigins: apigateway.Cors.ALL_ORIGINS, - allowMethods: apigateway.Cors.ALL_METHODS, - allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'], - }, + // Important: Do NOT use defaultCorsPreflightOptions here }); // Create Cognito Authorizer @@ -301,87 +298,147 @@ export class BackendStack extends cdk.Stack { cognitoUserPools: [userPool], authorizerName: `${appName}Authorizer-${props.environment}`, identitySource: 'method.request.header.Authorization', - }, + } ); - // Use the NLB DNS name for the service URL + // Define the service URL using the NLB DNS const serviceUrl = `http://${nlb.loadBalancerDnsName}`; - // Create proxy resource with Cognito authorization - const proxyResource = api.root.addResource('{proxy+}'); + // Create the 'api' resource + const apiResource = api.root.addResource('api'); + + // Create the 'reports' resource under 'api' + const reportsResource = apiResource.addResource('reports'); + + // Create the 'latest' resource under 'reports' + const latestResource = reportsResource.addResource('latest'); + + // Create the ':id' resource under 'reports' with a path parameter + const reportIdResource = reportsResource.addResource('{id}'); + + // Create the 'status' resource under ':id' + const reportStatusResource = reportIdResource.addResource('status'); + + // Define integration options once for reuse + const integrationOptions = { + connectionType: apigateway.ConnectionType.VPC_LINK, + vpcLink: vpcLink, + }; + + // Create integrations for each endpoint + const getReportsIntegration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'GET', + uri: `${serviceUrl}/api/reports`, + options: integrationOptions, + }); + + const getLatestReportIntegration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'GET', + uri: `${serviceUrl}/api/reports/latest`, + options: integrationOptions, + }); + + const getReportByIdIntegration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'GET', + uri: `${serviceUrl}/api/reports/{id}`, + options: { + ...integrationOptions, + requestParameters: { + 'integration.request.path.id': 'method.request.path.id', + }, + }, + }); - // Integration with Fargate service via VPC Link - const integration = new apigateway.Integration({ + const patchReportStatusIntegration = new apigateway.Integration({ type: apigateway.IntegrationType.HTTP_PROXY, - integrationHttpMethod: 'ANY', + integrationHttpMethod: 'PATCH', + uri: `${serviceUrl}/api/reports/{id}/status`, options: { - connectionType: apigateway.ConnectionType.VPC_LINK, - vpcLink: vpcLink, + ...integrationOptions, requestParameters: { - 'integration.request.path.proxy': 'method.request.path.proxy', + 'integration.request.path.id': 'method.request.path.id', }, }, - uri: `${serviceUrl}/{proxy}`, }); - proxyResource.addMethod('ANY', integration, { + // Define method options with authorization + const methodOptions = { authorizer: authorizer, authorizationType: apigateway.AuthorizationType.COGNITO, + }; + + // Add methods to the resources + reportsResource.addMethod('GET', getReportsIntegration, methodOptions); + latestResource.addMethod('GET', getLatestReportIntegration, methodOptions); + + // For path parameter methods, add the request parameter configuration + reportIdResource.addMethod('GET', getReportByIdIntegration, { + ...methodOptions, requestParameters: { - 'method.request.path.proxy': true, + 'method.request.path.id': true, }, }); - // Add execution role policy to allow API Gateway to access VPC resources - new iam.Role(this, `${appName}APIGatewayVPCRole-${props.environment}`, { - assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), - managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName( - 'service-role/AmazonAPIGatewayPushToCloudWatchLogs', - ), - iam.ManagedPolicy.fromAwsManagedPolicyName( - 'AmazonVPCCrossAccountNetworkInterfaceOperations', - ), - ], + reportStatusResource.addMethod('PATCH', patchReportStatusIntegration, { + ...methodOptions, + requestParameters: { + 'method.request.path.id': true, + }, }); - const apiResourcePolicy = new iam.PolicyDocument({ - statements: [ - // Allow only authenticated Cognito users to access all other endpoints - new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - // Using AnyPrincipal here but access is restricted by the Cognito condition below - // This is not truly public access as only authenticated users can meet the condition - principals: [new iam.AnyPrincipal()], - actions: ['execute-api:Invoke'], - resources: [`arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/*`], - conditions: { - StringEquals: { - 'aws:PrincipalTag/cognito-identity.amazonaws.com:sub': - '${cognito-identity.amazonaws.com:sub}', - }, - }, - }), - - // Deny all non-HTTPS requests - new iam.PolicyStatement({ - effect: iam.Effect.DENY, - principals: [new iam.AnyPrincipal()], - actions: ['execute-api:Invoke'], - resources: [`arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/*`], - conditions: { - Bool: { - 'aws:SecureTransport': 'false', - }, - }, - }), + // Add CORS to each resource separately - after methods have been created + const corsOptions = { + allowOrigins: ['*'], + allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'], + maxAge: cdk.Duration.seconds(300), + }; + + // Add CORS to all resources + api.root.addCorsPreflight(corsOptions); + apiResource.addCorsPreflight(corsOptions); + reportsResource.addCorsPreflight(corsOptions); + latestResource.addCorsPreflight(corsOptions); + reportIdResource.addCorsPreflight(corsOptions); + reportStatusResource.addCorsPreflight(corsOptions); + + // Apply resource policy separately after resources and methods are created + // const apiResourcePolicy = new iam.PolicyDocument({ + // statements: [ + // // Allow authenticated Cognito users + // new iam.PolicyStatement({ + // effect: iam.Effect.ALLOW, + // principals: [new iam.AnyPrincipal()], + // actions: ['execute-api:Invoke'], + // resources: [`arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/*`], + // }), + // // Deny non-HTTPS requests + // new iam.PolicyStatement({ + // effect: iam.Effect.DENY, + // principals: [new iam.AnyPrincipal()], + // actions: ['execute-api:Invoke'], + // resources: [`arn:aws:execute-api:${this.region}:${this.account}:${api.restApiId}/*/*`], + // conditions: { + // Bool: { + // 'aws:SecureTransport': 'false', + // }, + // }, + // }), + // ], + // }); + + // Create API Gateway execution role with required permissions + new iam.Role(this, `${appName}APIGatewayRole-${props.environment}`, { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs'), + iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonVPCCrossAccountNetworkInterfaceOperations'), ], }); - // Apply the policy to the API Gateway using CfnRestApi - const cfnApi = api.node.defaultChild as apigateway.CfnRestApi; - cfnApi.policy = apiResourcePolicy.toJSON(); - // Outputs new cdk.CfnOutput(this, 'ReportsTableName', { value: reportsTable.tableName, From d84116842929c6e252a115acb1ff166b691865e2 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 1 Apr 2025 11:02:52 -0400 Subject: [PATCH 12/13] Re-create the API Gateway --- backend/src/iac/backend-stack.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index c253c91e..b5fb3645 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -298,7 +298,7 @@ export class BackendStack extends cdk.Stack { cognitoUserPools: [userPool], authorizerName: `${appName}Authorizer-${props.environment}`, identitySource: 'method.request.header.Authorization', - } + }, ); // Define the service URL using the NLB DNS @@ -434,8 +434,12 @@ export class BackendStack extends cdk.Stack { new iam.Role(this, `${appName}APIGatewayRole-${props.environment}`, { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs'), - iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonVPCCrossAccountNetworkInterfaceOperations'), + iam.ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AmazonAPIGatewayPushToCloudWatchLogs', + ), + iam.ManagedPolicy.fromAwsManagedPolicyName( + 'AmazonVPCCrossAccountNetworkInterfaceOperations', + ), ], }); From 2eedfa34f127ea83a251968e4590151dea337338 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Tue, 1 Apr 2025 13:14:36 -0400 Subject: [PATCH 13/13] Add resource policy after the deployment is done --- backend/README.md | 90 ++++++++ backend/package-lock.json | 295 ++++++++++++++++++++++++++- backend/package.json | 5 +- backend/src/iac/deploy.sh | 8 + backend/src/iac/update-api-policy.js | 115 +++++++++++ 5 files changed, 506 insertions(+), 7 deletions(-) create mode 100644 backend/README.md create mode 100755 backend/src/iac/deploy.sh create mode 100755 backend/src/iac/update-api-policy.js diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 00000000..99f2ed43 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,90 @@ +# API Gateway Resource Policy Update Script + +This script updates your API Gateway resource policy after your CDK deployment to allow only Cognito authenticated users and deny non-HTTPS requests. Using a separate script avoids the circular dependency issues that can occur when adding policies in CDK. + +## Setup + +1. Save the script to a file named `update-api-policy.js` in your project. + +2. Install the AWS SDK if you haven't already: + ```bash + npm install aws-sdk + ``` + +3. Make the script executable: + ```bash + chmod +x update-api-policy.js + ``` + +## Configuration + +Update the following variables in the script to match your environment: + +- `STACK_NAME`: The name of your CloudFormation stack (e.g., 'ai-team-medical-reports-stack-development') +- `REGION`: Your AWS region (e.g., 'us-east-1') +- `API_NAME`: The name of your API Gateway (e.g., 'AIMedicalReport-development') + +## Usage + +You can run the script after each successful CDK deployment: + +```bash +# Run CDK deployment first +cdk deploy ai-team-medical-reports-stack-development + +# Then run the policy update script +./update-api-policy.js +``` + +Alternatively, you can set the Cognito User Pool ID as an environment variable: + +```bash +COGNITO_USER_POOL_ID=us-east-1_yourPoolId ./update-api-policy.js +``` + +## Automation + +To automatically run this after each deployment, you can create a simple shell script: + +```bash +#!/bin/bash +# deploy-and-update.sh + +# Deploy with CDK +cdk deploy ai-team-medical-reports-stack-development + +# If deployment was successful, update the API policy +if [ $? -eq 0 ]; then + echo "CDK deployment successful, updating API policy..." + ./update-api-policy.js +else + echo "CDK deployment failed, skipping API policy update." + exit 1 +fi +``` + +Make it executable: +```bash +chmod +x deploy-and-update.sh +``` + +## Troubleshooting + +If you encounter any issues: + +1. **Authentication errors**: Make sure your AWS credentials are configured correctly with the necessary permissions. + +2. **API not found**: Verify the API_NAME matches exactly what's in your AWS Console. + +3. **Stack not found**: Check that the STACK_NAME is correct. + +4. **Cognito User Pool ID not found**: You can set it manually with the COGNITO_USER_POOL_ID environment variable. + +## Security Considerations + +This script sets a resource policy that: + +1. Allows only authenticated Cognito users to access your API +2. Denies any non-HTTPS requests to your API + +If you need more complex permissions, you can modify the policy object in the script. diff --git a/backend/package-lock.json b/backend/package-lock.json index 853ab10c..f81914ec 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -20,6 +20,8 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.1.13", "@types/jest": "^29.5.12", + "aws-cdk-lib": "^2.185.0", + "aws-sdk": "^2.1692.0", "axios": "^1.8.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -4660,6 +4662,21 @@ "node": ">= 4.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/aws-cdk": { "version": "2.139.0", "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.139.0.tgz", @@ -5076,6 +5093,63 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/axios": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", @@ -5224,7 +5298,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5525,7 +5598,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -6213,7 +6285,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -7304,6 +7375,21 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", @@ -7724,7 +7810,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -7979,6 +8064,22 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -8006,6 +8107,18 @@ "dev": true, "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -8052,6 +8165,24 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8095,6 +8226,24 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -8108,6 +8257,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -9044,6 +9208,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10291,6 +10464,15 @@ "node": ">=4" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -10468,6 +10650,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10845,12 +11036,35 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -10993,7 +11207,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -12135,6 +12348,35 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12568,6 +12810,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -12650,6 +12913,28 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index 03c865e8..61bf11af 100644 --- a/backend/package.json +++ b/backend/package.json @@ -38,6 +38,8 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.1.13", "@types/jest": "^29.5.12", + "aws-cdk-lib": "^2.185.0", + "aws-sdk": "^2.1692.0", "axios": "^1.8.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -54,8 +56,7 @@ "rxjs": "^7.8.1", "source-map-support": "^0.5.21", "swagger-ui-express": "^5.0.0", - "web-vitals": "^2.1.4", - "aws-cdk-lib": "^2.185.0" + "web-vitals": "^2.1.4" }, "devDependencies": { "@aws-cdk/assert": "^2.68.0", diff --git a/backend/src/iac/deploy.sh b/backend/src/iac/deploy.sh new file mode 100755 index 00000000..458f6401 --- /dev/null +++ b/backend/src/iac/deploy.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Run CDK deployment +cdk deploy ai-team-medical-reports-stack-development + +# If successful, update the API policy +if [ $? -eq 0 ]; then + ./update-api-policy.js +fi diff --git a/backend/src/iac/update-api-policy.js b/backend/src/iac/update-api-policy.js new file mode 100755 index 00000000..35f59502 --- /dev/null +++ b/backend/src/iac/update-api-policy.js @@ -0,0 +1,115 @@ +#!/usr/bin/env node +const { APIGateway, CloudFormation } = require('aws-sdk'); +const fs = require('fs'); +const path = require('path'); + +// Configuration - update these values +const STACK_NAME = 'ai-team-medical-reports-stack-development'; +const REGION = 'us-east-1'; // Update with your region +const API_NAME = 'AIMedicalReport-development'; // Your API Gateway name + +async function main() { + try { + console.log('Starting API Gateway policy update script...'); + + // Initialize AWS clients + const cloudformation = new CloudFormation({ region: REGION }); + const apigateway = new APIGateway({ region: REGION }); + + // Get the stack outputs to find resources + console.log(`Getting information from CloudFormation stack: ${STACK_NAME}`); + const stackResponse = await cloudformation.describeStacks({ + StackName: STACK_NAME + }).promise(); + + if (!stackResponse.Stacks || stackResponse.Stacks.length === 0) { + throw new Error(`Stack ${STACK_NAME} not found`); + } + + // Find the API Gateway and Cognito User Pool ID from stack outputs + const outputs = stackResponse.Stacks[0].Outputs || []; + let cognitoUserPoolId = null; + + for (const output of outputs) { + if (output.OutputKey === 'UserPoolId') { + cognitoUserPoolId = output.OutputValue; + console.log(`Found Cognito User Pool ID: ${cognitoUserPoolId}`); + } + } + + // If Cognito ID wasn't found in outputs, prompt for it + if (!cognitoUserPoolId) { + cognitoUserPoolId = process.env.COGNITO_USER_POOL_ID; + if (!cognitoUserPoolId) { + console.log('Cognito User Pool ID not found in stack outputs.'); + console.log('Please set the COGNITO_USER_POOL_ID environment variable and try again.'); + process.exit(1); + } + } + + // Find the API Gateway + console.log('Listing API Gateways...'); + const apis = await apigateway.getRestApis().promise(); + + const api = apis.items.find(api => api.name === API_NAME); + if (!api) { + throw new Error(`API Gateway with name ${API_NAME} not found`); + } + + console.log(`Found API Gateway: ${api.name} (${api.id})`); + + // Create the resource policy + const policy = { + Version: '2012-10-17', + Statement: [ + // Allow authenticated Cognito users + { + Effect: 'Allow', + Principal: '*', + Action: 'execute-api:Invoke', + Resource: `arn:aws:execute-api:${REGION}:*:${api.id}/*/*`, + Condition: { + StringEquals: { + 'cognito-identity.amazonaws.com:aud': cognitoUserPoolId + } + } + }, + // Deny non-HTTPS requests + { + Effect: 'Deny', + Principal: '*', + Action: 'execute-api:Invoke', + Resource: `arn:aws:execute-api:${REGION}:*:${api.id}/*/*`, + Condition: { + Bool: { + 'aws:SecureTransport': 'false' + } + } + } + ] + }; + + console.log('Updating API Gateway policy...'); + console.log(JSON.stringify(policy, null, 2)); + + // Update the API with the new policy + await apigateway.updateRestApi({ + restApiId: api.id, + patchOperations: [ + { + op: 'replace', + path: '/policy', + value: JSON.stringify(policy) + } + ] + }).promise(); + + console.log('API Gateway policy updated successfully!'); + + } catch (error) { + console.error('Error updating API policy:', error); + process.exit(1); + } +} + +main();