From a07113d0612604698c1f393f2f0840714657860f Mon Sep 17 00:00:00 2001 From: FahdShalhoub Date: Sun, 16 Aug 2026 20:04:04 +0300 Subject: [PATCH] FIX: Permissions To s3 And MediaConvert] --- cms/main.go | 9 +- infrastructure/main.go | 202 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 196 insertions(+), 15 deletions(-) diff --git a/cms/main.go b/cms/main.go index ea5c708..cb996d3 100644 --- a/cms/main.go +++ b/cms/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "os" "thamanyah/cms/v2/internal/handlers" "thamanyah/cms/v2/internal/services" @@ -21,7 +22,7 @@ func main() { concreteS3Client := &services.S3Concrete{ S3Client: s3.NewFromConfig(config), - Bucket: "", + Bucket: os.Getenv("S3_BUCKET"), } concreteS3Client.AssertSuccessfulConnection(context.Background()) @@ -30,9 +31,9 @@ func main() { services.MediaConvertClient = &services.MediaConvertConcrete{ MediaConvertClient: mediaconvert.NewFromConfig(config), - Role: "test", - InputBucket: "", - OutputBucket: "", + Role: os.Getenv("MEDIACONVERT_ROLE_ARN"), + InputBucket: os.Getenv("MEDIACONVERT_INPUT_BUCKET"), + OutputBucket: os.Getenv("MEDIACONVERT_OUTPUT_BUCKET"), } mux := http.NewServeMux() diff --git a/infrastructure/main.go b/infrastructure/main.go index 2781660..2fb5fe4 100644 --- a/infrastructure/main.go +++ b/infrastructure/main.go @@ -80,17 +80,29 @@ func newServiceDatabase(ctx *pulumi.Context, provider *postgresql.Provider, serv return password, nil } +// envVar is a name/value pair for a container's extra environment variables, +// where the value is only known after other resources are provisioned. +type envVar struct { + Name string + Value pulumi.StringOutput +} + // deployFargateService gives a service its own ECR repo, log group, task // definition, ECS service, and its own ALB — so it gets its own DNS name // rather than sharing one load balancer with the other service on a // different port. The service's DB password is stored in Secrets Manager and // injected into the container as a secret rather than a plaintext env var. +// taskRole is optional (nil means no TaskRoleArn is set, i.e. the container +// gets no AWS identity of its own beyond execRole's pull/logs permissions); +// extraEnv is appended to the container's environment on top of the DB_* vars. func deployFargateService( ctx *pulumi.Context, name string, containerPort int, cluster *ecs.Cluster, execRole *iam.Role, + taskRole *iam.Role, + extraEnv []envVar, vpcID string, subnetIDs []string, albSecurityGroup *ec2.SecurityGroup, @@ -194,7 +206,12 @@ func deployFargateService( return nil, nil, nil, err } - containerDefinitions := pulumi.All(repo.RepositoryUrl, logGroup.Name, secret.Arn, dbAddress, dbPort).ApplyT( + baseArgs := []any{repo.RepositoryUrl, logGroup.Name, secret.Arn, dbAddress, dbPort} + for _, ev := range extraEnv { + baseArgs = append(baseArgs, ev.Value) + } + + containerDefinitions := pulumi.All(baseArgs...).ApplyT( func(args []any) (string, error) { image := args[0].(string) + ":latest" logGroupName := args[1].(string) @@ -202,6 +219,16 @@ func deployFargateService( dbHost := args[3].(string) dbPort := args[4].(int) + environment := []map[string]string{ + {"name": "DB_HOST", "value": dbHost}, + {"name": "DB_PORT", "value": fmt.Sprintf("%d", dbPort)}, + {"name": "DB_NAME", "value": name}, + {"name": "DB_USER", "value": name}, + } + for i, ev := range extraEnv { + environment = append(environment, map[string]string{"name": ev.Name, "value": args[5+i].(string)}) + } + def := []map[string]any{ { "name": name, @@ -209,12 +236,7 @@ func deployFargateService( "portMappings": []map[string]any{ {"containerPort": containerPort, "protocol": "tcp"}, }, - "environment": []map[string]string{ - {"name": "DB_HOST", "value": dbHost}, - {"name": "DB_PORT", "value": fmt.Sprintf("%d", dbPort)}, - {"name": "DB_NAME", "value": name}, - {"name": "DB_USER", "value": name}, - }, + "environment": environment, "secrets": []map[string]string{ {"name": "DB_PASSWORD", "valueFrom": secretArn}, }, @@ -233,7 +255,7 @@ func deployFargateService( }, ).(pulumi.StringOutput) - taskDefinition, err := ecs.NewTaskDefinition(ctx, name+"-task", &ecs.TaskDefinitionArgs{ + taskDefinitionArgs := &ecs.TaskDefinitionArgs{ Family: pulumi.String(name), Cpu: pulumi.String("256"), Memory: pulumi.String("512"), @@ -241,7 +263,12 @@ func deployFargateService( RequiresCompatibilities: pulumi.StringArray{pulumi.String("FARGATE")}, ExecutionRoleArn: execRole.Arn, ContainerDefinitions: containerDefinitions, - }) + } + if taskRole != nil { + taskDefinitionArgs.TaskRoleArn = taskRole.Arn + } + + taskDefinition, err := ecs.NewTaskDefinition(ctx, name+"-task", taskDefinitionArgs) if err != nil { return nil, nil, nil, err } @@ -579,15 +606,168 @@ func main() { return err } + // Private bucket for raw video uploads (pre-transcode). Kept separate + // from encoded-bucket, which is fronted by CloudFront/OAC for public + // delivery of finished renditions — raw source video must not be + // reachable through that CDN. + rawUploadsBucket, err := s3.NewBucket(ctx, "raw-uploads-bucket", nil) + if err != nil { + return err + } + + _, err = s3.NewBucketPublicAccessBlock(ctx, "raw-uploads-bucket-public-access-block", &s3.BucketPublicAccessBlockArgs{ + Bucket: rawUploadsBucket.ID(), + BlockPublicAcls: pulumi.Bool(true), + BlockPublicPolicy: pulumi.Bool(true), + IgnorePublicAcls: pulumi.Bool(true), + RestrictPublicBuckets: pulumi.Bool(true), + }) + if err != nil { + return err + } + + // Role MediaConvert itself assumes (passed as CreateJobInput.Role by + // the cms service) to read the raw upload and write the encoded + // output — distinct from the ECS task role below. + mediaConvertAssumePolicy, err := json.Marshal(map[string]any{ + "Version": "2012-10-17", + "Statement": []map[string]any{ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Principal": map[string]string{"Service": "mediaconvert.amazonaws.com"}, + }, + }, + }) + if err != nil { + return err + } + + mediaConvertRole, err := iam.NewRole(ctx, "mediaconvert-service-role", &iam.RoleArgs{ + AssumeRolePolicy: pulumi.String(mediaConvertAssumePolicy), + }) + if err != nil { + return err + } + + mediaConvertS3Policy := pulumi.All(rawUploadsBucket.Arn, bucket.Arn).ApplyT( + func(args []any) (string, error) { + rawUploadsArn := args[0].(string) + encodedBucketArn := args[1].(string) + + doc := map[string]any{ + "Version": "2012-10-17", + "Statement": []map[string]any{ + { + "Sid": "ReadRawUploads", + "Effect": "Allow", + "Action": "s3:GetObject", + "Resource": rawUploadsArn + "/*", + }, + { + "Sid": "WriteEncodedOutput", + "Effect": "Allow", + "Action": "s3:PutObject", + "Resource": encodedBucketArn + "/*", + }, + }, + } + b, err := json.Marshal(doc) + return string(b), err + }, + ).(pulumi.StringOutput) + + _, err = iam.NewRolePolicy(ctx, "mediaconvert-s3-access", &iam.RolePolicyArgs{ + Role: mediaConvertRole.ID(), + Policy: mediaConvertS3Policy, + }) + if err != nil { + return err + } + + // cms's own ECS task role (distinct from the shared execRole, which + // is only for the ECS agent's pull/logs/secrets access): lets the + // running container call S3 and MediaConvert directly. discovery + // gets none of this since it never touches either API. + cmsTaskRole, err := iam.NewRole(ctx, "cms-task-role", &iam.RoleArgs{ + AssumeRolePolicy: pulumi.String(execRoleAssumePolicy), + }) + if err != nil { + return err + } + + cmsTaskPolicy := pulumi.All(rawUploadsBucket.Arn, mediaConvertRole.Arn).ApplyT( + func(args []any) (string, error) { + rawUploadsArn := args[0].(string) + mediaConvertRoleArn := args[1].(string) + + doc := map[string]any{ + "Version": "2012-10-17", + "Statement": []map[string]any{ + { + "Sid": "ListRawUploadsBucket", + "Effect": "Allow", + "Action": "s3:ListBucket", + "Resource": rawUploadsArn, + }, + { + "Sid": "RawUploadsObjectAccess", + "Effect": "Allow", + "Action": []string{ + "s3:PutObject", + "s3:GetObject", + }, + "Resource": rawUploadsArn + "/*", + }, + { + "Sid": "CreateTranscodeJob", + "Effect": "Allow", + "Action": "mediaconvert:CreateJob", + "Resource": "*", + }, + { + "Sid": "PassMediaConvertRole", + "Effect": "Allow", + "Action": "iam:PassRole", + "Resource": mediaConvertRoleArn, + "Condition": map[string]any{ + "StringEquals": map[string]string{ + "iam:PassedToService": "mediaconvert.amazonaws.com", + }, + }, + }, + }, + } + b, err := json.Marshal(doc) + return string(b), err + }, + ).(pulumi.StringOutput) + + _, err = iam.NewRolePolicy(ctx, "cms-task-role-policy", &iam.RolePolicyArgs{ + Role: cmsTaskRole.ID(), + Policy: cmsTaskPolicy, + }) + if err != nil { + return err + } + + cmsExtraEnv := []envVar{ + {Name: "AWS_REGION", Value: pulumi.String(awsRegion).ToStringOutput()}, + {Name: "S3_BUCKET", Value: rawUploadsBucket.ID().ToStringOutput()}, + {Name: "MEDIACONVERT_INPUT_BUCKET", Value: rawUploadsBucket.ID().ToStringOutput()}, + {Name: "MEDIACONVERT_OUTPUT_BUCKET", Value: bucket.ID().ToStringOutput()}, + {Name: "MEDIACONVERT_ROLE_ARN", Value: mediaConvertRole.Arn}, + } + cmsRepo, cmsService, cmsAlb, err := deployFargateService(ctx, "cms", 8081, - cluster, execRole, vpc.Id, subnets.Ids, albSecurityGroup, serviceSecurityGroup, + cluster, execRole, cmsTaskRole, cmsExtraEnv, vpc.Id, subnets.Ids, albSecurityGroup, serviceSecurityGroup, db.Address, db.Port, cmsPassword) if err != nil { return err } discoveryRepo, discoveryService, discoveryAlb, err := deployFargateService(ctx, "discovery", 8080, - cluster, execRole, vpc.Id, subnets.Ids, albSecurityGroup, serviceSecurityGroup, + cluster, execRole, nil, nil, vpc.Id, subnets.Ids, albSecurityGroup, serviceSecurityGroup, db.Address, db.Port, discoveryPassword) if err != nil { return err