From 36d0a638a2991e6833556a2cbe4076ea2bf22f86 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 28 Jul 2026 14:19:15 -0300 Subject: [PATCH 01/13] fix(vendors): return completion documents in portal detail --- .../VendorPortalDocumentTests.cs | 137 +++++++++++++++++- .../VendorDocumentDataService.cs | 10 ++ .../Interfaces/IVendorDocumentDataService.cs | 1 + .../DTOs/VendorPortalServiceDTOs.cs | 18 +++ .../Implementation/VendorPortalService.cs | 21 ++- 5 files changed, 183 insertions(+), 4 deletions(-) diff --git a/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs b/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs index 33ef432..3e20aa3 100644 --- a/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs +++ b/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs @@ -35,6 +35,11 @@ public sealed class VendorPortalDocumentTests : IDisposable } private VendorPortalController NewController(ApplicationDbContext context) + { + return NewController(NewService(context)); + } + + private VendorPortalService NewService(ApplicationDbContext context) { var vendorData = new VendorDataService(context); var tokenService = new VendorPortalTokenService( @@ -45,11 +50,18 @@ public sealed class VendorPortalDocumentTests : IDisposable storageEnvironment.SetupGet(item => item.ContentRootPath).Returns(_contentRoot); var storage = new VendorDocumentStorageAdapter(storageEnvironment.Object); - var service = new VendorPortalService( + var upliftData = new Mock(); + upliftData.Setup(u => u.GetForVendorDispatchAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + var commentData = new Mock(); + commentData.Setup(c => c.GetVendorViewableForDispatchAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + + return new VendorPortalService( tokenService, new DispatchDataService(context), - Mock.Of(), - Mock.Of(), + upliftData.Object, + commentData.Object, Mock.Of(), Mock.Of(), documentData, @@ -57,7 +69,10 @@ public sealed class VendorPortalDocumentTests : IDisposable Microsoft.Extensions.Options.Options.Create(new FrontendOptions()), Microsoft.Extensions.Options.Options.Create(new ApprovalsOptions()), Microsoft.Extensions.Options.Options.Create(new VendorDocumentsOptions())); + } + private static VendorPortalController NewController(VendorPortalService service) + { var controller = new VendorPortalController(service, Mock.Of>()); controller.ControllerContext = new ControllerContext { @@ -292,6 +307,122 @@ public sealed class VendorPortalDocumentTests : IDisposable context.VendorCompletionDocuments.Should().HaveCount(1); } + [Fact] + public async Task GetDispatchDetail_ReturnsUploadedDocumentWithQuarantineAndReplacementMetadata() + { + using var context = NewContext(); + var (_, dispatch) = await SeedDispatch(context); + var service = NewService(context); + var pdf = "%PDF-1.4\nvendor completion"u8.ToArray(); + + await NewController(service).UploadCompletionDocument( + dispatch.Id, + FormFile(pdf, "completion-v1.pdf", "application/pdf")); + + var session = await service.ResolveSessionAsync(Token, CancellationToken.None); + var firstDetail = await service.GetDispatchDetailAsync(session!, dispatch.Id, CancellationToken.None); + + firstDetail.Should().NotBeNull(); + var firstDoc = firstDetail!.Documents.Should().ContainSingle().Subject; + firstDoc.OriginalFileName.Should().Be("completion-v1.pdf"); + firstDoc.ContentType.Should().Be("application/pdf"); + firstDoc.SizeBytes.Should().Be(pdf.Length); + firstDoc.ScanStatus.Should().Be("Pending"); + firstDoc.ReviewStatus.Should().Be("Processing"); + firstDoc.Version.Should().Be(1); + firstDoc.ReplacesDocumentId.Should().BeNull(); + firstDoc.CanDownload.Should().BeFalse(); + firstDoc.CreatedDate.Should().NotBeNull(); + + var first = await context.VendorCompletionDocuments.SingleAsync(); + await NewController(service).UploadCompletionDocument( + dispatch.Id, + FormFile(pdf, "completion-v2.pdf", "application/pdf"), + first.Id); + + var secondDetail = await service.GetDispatchDetailAsync(session!, dispatch.Id, CancellationToken.None); + var ordered = secondDetail!.Documents.OrderByDescending(d => d.Version).ToList(); + ordered.Should().HaveCount(2); + ordered[0].Version.Should().Be(2); + ordered[0].ReplacesDocumentId.Should().Be(first.Id); + ordered[0].OriginalFileName.Should().Be("completion-v2.pdf"); + ordered[1].Version.Should().Be(1); + ordered[1].ReplacesDocumentId.Should().BeNull(); + } + + [Fact] + public async Task GetDispatchDetail_EnablesDownloadOnlyWhenScanPasses() + { + using var context = NewContext(); + var (_, dispatch) = await SeedDispatch(context); + var service = NewService(context); + var pdf = "%PDF-1.4\nvendor completion"u8.ToArray(); + + await NewController(service).UploadCompletionDocument( + dispatch.Id, + FormFile(pdf, "completion.pdf", "application/pdf")); + + var document = await context.VendorCompletionDocuments.SingleAsync(); + document.ScanStatus = "Passed"; + document.ScannedAt = DateTime.UtcNow; + await context.SaveChangesAsync(); + + var session = await service.ResolveSessionAsync(Token, CancellationToken.None); + var detail = await service.GetDispatchDetailAsync(session!, dispatch.Id, CancellationToken.None); + + var doc = detail!.Documents.Should().ContainSingle().Subject; + doc.ScanStatus.Should().Be("Passed"); + doc.CanDownload.Should().BeTrue(); + doc.ScannedAt.Should().NotBeNull(); + } + + [Theory] + [InlineData("Verified")] + [InlineData("Cancelled")] + public async Task GetDispatchDetail_DisablesDownloadForLockedDispatches(string status) + { + using var context = NewContext(); + var (_, dispatch) = await SeedDispatch(context); + var service = NewService(context); + var pdf = "%PDF-1.4\nvendor completion"u8.ToArray(); + + await NewController(service).UploadCompletionDocument( + dispatch.Id, + FormFile(pdf, "completion.pdf", "application/pdf")); + + var document = await context.VendorCompletionDocuments.SingleAsync(); + document.ScanStatus = "Passed"; + dispatch.Status = status; + await context.SaveChangesAsync(); + + var session = await service.ResolveSessionAsync(Token, CancellationToken.None); + var detail = await service.GetDispatchDetailAsync(session!, dispatch.Id, CancellationToken.None); + + detail!.Documents.Should().ContainSingle().Which.CanDownload.Should().BeFalse(); + } + + [Fact] + public async Task GetDispatchDetail_ExcludesSoftDeletedDocuments() + { + using var context = NewContext(); + var (_, dispatch) = await SeedDispatch(context); + var service = NewService(context); + var pdf = "%PDF-1.4\nvendor completion"u8.ToArray(); + + await NewController(service).UploadCompletionDocument( + dispatch.Id, + FormFile(pdf, "completion.pdf", "application/pdf")); + + var document = await context.VendorCompletionDocuments.SingleAsync(); + document.IsDeleted = true; + await context.SaveChangesAsync(); + + var session = await service.ResolveSessionAsync(Token, CancellationToken.None); + var detail = await service.GetDispatchDetailAsync(session!, dispatch.Id, CancellationToken.None); + + detail!.Documents.Should().BeEmpty(); + } + public void Dispose() { if (Directory.Exists(_contentRoot)) diff --git a/SeaHaven.DataServices/Implementation/VendorDocumentDataService.cs b/SeaHaven.DataServices/Implementation/VendorDocumentDataService.cs index 2ee5192..05aa4e9 100644 --- a/SeaHaven.DataServices/Implementation/VendorDocumentDataService.cs +++ b/SeaHaven.DataServices/Implementation/VendorDocumentDataService.cs @@ -30,6 +30,16 @@ namespace SeaHaven.DataServices.Implementation && (document.IsDeleted == null || document.IsDeleted == false), cancellationToken); } + public Task> ListForVendorDispatchAsync(int dispatchId, int vendorId, CancellationToken cancellationToken) + { + return _context.VendorCompletionDocuments + .Where(document => document.DispatchId == dispatchId + && document.VendorId == vendorId + && (document.IsDeleted == null || document.IsDeleted == false)) + .OrderByDescending(document => document.Version) + .ToListAsync(cancellationToken); + } + public async Task AddAsync(VendorCompletionDocument document, CancellationToken cancellationToken) { await _context.VendorCompletionDocuments.AddAsync(document, cancellationToken); diff --git a/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs b/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs index 5849b51..d6c76a6 100644 --- a/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs +++ b/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs @@ -6,6 +6,7 @@ namespace SeaHaven.DataServices.Interfaces { Task GetLatestForDispatchAsync(int dispatchId, CancellationToken cancellationToken); Task GetForVendorDispatchAsync(int documentId, int dispatchId, int vendorId, CancellationToken cancellationToken); + Task> ListForVendorDispatchAsync(int dispatchId, int vendorId, CancellationToken cancellationToken); Task AddAsync(VendorCompletionDocument document, CancellationToken cancellationToken); Task SaveChangesAsync(CancellationToken cancellationToken); } diff --git a/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs b/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs index 27cc8e7..0fe7040 100644 --- a/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs +++ b/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs @@ -109,6 +109,24 @@ namespace SeaHaven.Services.DTOs public IEnumerable Signoffs { get; set; } = Enumerable.Empty(); public IEnumerable Comments { get; set; } = Enumerable.Empty(); public IEnumerable UpliftRequests { get; set; } = Enumerable.Empty(); + public IEnumerable Documents { get; set; } = Enumerable.Empty(); + } + + public class VendorPortalDocumentDTO + { + public int Id { get; set; } + public string? OriginalFileName { get; set; } + public string? ContentType { get; set; } + public long SizeBytes { get; set; } + public string? ScanStatus { get; set; } + public string? ReviewStatus { get; set; } + public string? RejectionReason { get; set; } + public int Version { get; set; } + public int? ReplacesDocumentId { get; set; } + public DateTime? CreatedDate { get; set; } + public DateTime? ScannedAt { get; set; } + public DateTime? ReviewedAt { get; set; } + public bool CanDownload { get; set; } } public class AcceptDispatchResultDTO diff --git a/SeaHaven.Services/Implementation/VendorPortalService.cs b/SeaHaven.Services/Implementation/VendorPortalService.cs index b3b2362..3606639 100644 --- a/SeaHaven.Services/Implementation/VendorPortalService.cs +++ b/SeaHaven.Services/Implementation/VendorPortalService.cs @@ -90,6 +90,7 @@ namespace SeaHaven.Services.Implementation var signoffs = await _dispatchData.GetPortalSignoffsAsync(id, cancellationToken); var rawUplifts = await _upliftData.GetForVendorDispatchAsync(id, cancellationToken); var rawComments = await _commentData.GetVendorViewableForDispatchAsync(id, cancellationToken); + var rawDocuments = await _documentData.ListForVendorDispatchAsync(id, session.Id, cancellationToken); var upliftRequests = rawUplifts.Select(u => new PortalUpliftDTO { @@ -192,7 +193,25 @@ namespace SeaHaven.Services.Implementation SignedAt = s.SignedAt }), Comments = comments, - UpliftRequests = upliftRequests + UpliftRequests = upliftRequests, + Documents = rawDocuments.Select(document => new VendorPortalDocumentDTO + { + Id = document.Id, + OriginalFileName = document.OriginalFileName, + ContentType = document.ContentType, + SizeBytes = document.SizeBytes, + ScanStatus = document.ScanStatus, + ReviewStatus = document.ReviewStatus, + RejectionReason = document.RejectionReason, + Version = document.Version, + ReplacesDocumentId = document.ReplacesDocumentId, + CreatedDate = document.CreatedDate, + ScannedAt = document.ScannedAt, + ReviewedAt = document.ReviewedAt, + CanDownload = string.Equals(document.ScanStatus, "Passed", StringComparison.OrdinalIgnoreCase) + && dispatch.Status != "Verified" + && dispatch.Status != "Cancelled" + }) }; } From 148a0750f9fe271b204ef536a08e4a4b3fa60623 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 28 Jul 2026 17:19:42 -0300 Subject: [PATCH 02/13] feat(vendor-portal): expose dispatch payment details --- .../VendorPortalDocumentTests.cs | 51 +++++++++++++++++++ .../DTOs/VendorPortalServiceDTOs.cs | 3 ++ .../Implementation/VendorPortalService.cs | 3 ++ 3 files changed, 57 insertions(+) diff --git a/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs b/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs index 3e20aa3..9ca6a7e 100644 --- a/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs +++ b/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs @@ -423,6 +423,57 @@ public sealed class VendorPortalDocumentTests : IDisposable detail!.Documents.Should().BeEmpty(); } + [Fact] + public async Task GetDispatchDetail_ReturnsInvoicePaymentFieldsOnlyForOwningVendor() + { + using var context = NewContext(); + var (_, dispatch) = await SeedDispatch(context); + dispatch.InvoiceNumber = "INV-2048"; + dispatch.InvoiceStatus = "Confirmed"; + dispatch.PaymentStatus = "Scheduled"; + await context.SaveChangesAsync(); + + var service = NewService(context); + var ownerSession = await service.ResolveSessionAsync(Token, CancellationToken.None); + var ownerDetail = await service.GetDispatchDetailAsync(ownerSession!, dispatch.Id, CancellationToken.None); + + ownerDetail.Should().NotBeNull(); + ownerDetail!.InvoiceNumber.Should().Be("INV-2048"); + ownerDetail.InvoiceStatus.Should().Be("Confirmed"); + ownerDetail.PaymentStatus.Should().Be("Scheduled"); + + var ownerResult = await NewController(service).GetDispatch(dispatch.Id); + var ownerOk = ownerResult.Should().BeOfType().Subject; + var ownerResponse = ownerOk.Value.Should().BeOfType().Subject; + var ownerData = ((object)ownerResponse.Data) + .Should().BeOfType().Subject; + ownerData.InvoiceNumber.Should().Be("INV-2048"); + ownerData.InvoiceStatus.Should().Be("Confirmed"); + ownerData.PaymentStatus.Should().Be("Scheduled"); + + var otherVendor = new Vendor { CompanyName = "Other Vendor", IsActive = true }; + context.Vendors.Add(otherVendor); + await context.SaveChangesAsync(); + const string otherToken = "other-vendor-token"; + context.VendorAccessTokens.Add(new VendorAccessToken + { + VendorId = otherVendor.Id, + Token = otherToken, + IssuedAt = DateTime.UtcNow, + ExpiresAt = DateTime.UtcNow.AddDays(1) + }); + await context.SaveChangesAsync(); + + var otherSession = await service.ResolveSessionAsync(otherToken, CancellationToken.None); + var otherDetail = await service.GetDispatchDetailAsync(otherSession!, dispatch.Id, CancellationToken.None); + otherDetail.Should().BeNull(); + + var otherController = NewController(service); + otherController.Request.Headers["X-Vendor-Token"] = otherToken; + var otherResult = await otherController.GetDispatch(dispatch.Id); + otherResult.Should().BeOfType(); + } + public void Dispose() { if (Directory.Exists(_contentRoot)) diff --git a/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs b/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs index 0fe7040..3a4b5e7 100644 --- a/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs +++ b/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs @@ -97,6 +97,9 @@ namespace SeaHaven.Services.DTOs public string? DispatchNumber { get; set; } public string? PONumber { get; set; } public string? Status { get; set; } + public string? InvoiceNumber { get; set; } + public string? InvoiceStatus { get; set; } + public string? PaymentStatus { get; set; } public decimal? NTEAmount { get; set; } public string? Description { get; set; } public DateTime? ScheduledDate { get; set; } diff --git a/SeaHaven.Services/Implementation/VendorPortalService.cs b/SeaHaven.Services/Implementation/VendorPortalService.cs index 3606639..d0514c6 100644 --- a/SeaHaven.Services/Implementation/VendorPortalService.cs +++ b/SeaHaven.Services/Implementation/VendorPortalService.cs @@ -150,6 +150,9 @@ namespace SeaHaven.Services.Implementation DispatchNumber = dispatch.DispatchNumber, PONumber = dispatch.PONumber, Status = dispatch.Status, + InvoiceNumber = dispatch.InvoiceNumber, + InvoiceStatus = dispatch.InvoiceStatus, + PaymentStatus = dispatch.PaymentStatus, NTEAmount = dispatch.NTEAmount, Description = dispatch.Description, ScheduledDate = dispatch.ScheduledDate, From 061a084fdaf817a3e59031a9fb62c4f61ba0ba1d Mon Sep 17 00:00:00 2001 From: brandizzi Date: Wed, 29 Jul 2026 08:01:01 -0300 Subject: [PATCH 03/13] fix(cdk): grant AWS-confirmed EB ACL read --- infra/cdk/README.md | 40 ++++++++++++++++++++--------------- infra/cdk/deploy-dev-stack.ts | 13 +++++++++++- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 93273f3..37446c4 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -45,10 +45,17 @@ The role grants only: ownership-safe bucket checks), plus `s3:CreateBucket` and `s3:PutBucketOwnershipControls` on the same bucket-level ARN. Under the `shoc-backend/` object prefix only: - `s3:PutObject`, `s3:GetObject`, `s3:GetObjectAcl`, and - `s3:GetObjectVersion`, which the pinned official deployment action and - Elastic Beanstalk require to validate the `CreateApplicationVersion` source - bundle after upload. + `s3:PutObject`, `s3:GetObject`, and `s3:GetObjectVersion`, which the pinned + official deployment action requires to validate the + `CreateApplicationVersion` source bundle after upload. +- `s3:GetObjectAcl` on objects under the service-wide + `arn:aws:s3:::elasticbeanstalk-*/*` namespace. AWS Support case + `178526484500047` confirmed that `UpdateEnvironment` uses the initiating + role to inspect objects in AWS-owned Elastic Beanstalk buckets, not only the + account-owned source-bundle bucket. The wildcard is limited to one read-only + ACL action and the Elastic Beanstalk bucket namespace; it grants no object + content read, write, delete, bucket-management, IAM, or `PassRole` + capability. `s3:CreateBucket` is part of the pinned `aws-actions/aws-elastic-beanstalk-deploy@cfad3e5e...` (v1.0.6) IAM @@ -112,15 +119,11 @@ The role grants only: successful administrator deployment. AWS supports resource-level constraints for all three mutations, so replacement ASGs remain covered without granting access to another environment. -- `s3:GetObjectAcl` under the existing - `elasticbeanstalk-us-east-1-396287094661/shoc-backend/*` object prefix. - Elastic Beanstalk emitted this read during the same attempt while validating - the uploaded application bundle. It grants no bucket-wide or cross-prefix - object access. It grants **no** IAM mutation or `PassRole`, **no** RDS / Secrets Manager access, no EC2 mutation, and **no** administrator policy. The only non-EB/S3 mutations are the three deployment-process Auto Scaling calls, restricted to -this environment's ASG name pattern. There are no wildcard mutation surfaces. +this environment's ASG name pattern. There are no wildcard mutation surfaces; +the only service-wide object grant is read-only ACL metadata. ## Prerequisites @@ -160,13 +163,16 @@ All commands run from `infra/cdk/`. The workflow's `AWS_DEPLOY_ROLE_ARN` repository secret (environment `dev`) must hold the ARN output by this stack (`GithubDeployRoleArn`). -The OIDC deployment is currently fail-closed, not repaired. Elastic Beanstalk -still reports a generic `s3:GetObjectAcl` denial after the account-owned source -prefix and every exact cross-account object referenced by the sanitized live -stack were tested independently. The runtime-prefix, platform-assets, and -launch-control hypotheses were disproved and are intentionally absent from the -policy. Do not broaden `GetObjectAcl` without an exact principal/action/resource -record from AWS Support or the AWS-owned bucket's diagnostic owner. +The previous OIDC deployment remained fail-closed after Elastic Beanstalk +reported a generic `s3:GetObjectAcl` denial outside the account-owned source +prefix. AWS Support case `178526484500047` subsequently confirmed that +`UpdateEnvironment` checks objects in AWS-owned Elastic Beanstalk buckets +using the initiating role and requires the `elasticbeanstalk-*/*` resource +namespace. This policy adds only the denied ACL-read action on that namespace; +it intentionally does not copy the managed +`AdministratorAccess-AWSElasticBeanstalk` policy's broad `s3:Get*`, +`s3:Put*`, or `s3:Delete*` grants. Any later denial must be evaluated and +granted independently. The pinned deployment action can return success after Elastic Beanstalk emits a fatal deployment event. The following workflow step therefore verifies that diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index 3575452..36addaf 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -142,11 +142,22 @@ export class DeployDevStack extends cdk.Stack { deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, - actions: ['s3:GetObject', 's3:GetObjectAcl', 's3:GetObjectVersion', 's3:PutObject'], + actions: ['s3:GetObject', 's3:GetObjectVersion', 's3:PutObject'], resources: [`${bucketArn}/${APPLICATION_NAME}/*`], }), ); + deployRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:GetObjectAcl'], + // UpdateEnvironment also checks objects in AWS-owned Elastic Beanstalk + // buckets. AWS Support case 178526484500047 confirmed that the caller's + // identity policy must cover the service-wide bucket namespace. + resources: ['arn:aws:s3:::elasticbeanstalk-*/*'], + }), + ); + new cdk.CfnOutput(this, 'GithubDeployRoleArn', { value: deployRole.roleArn, description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.', From b02787087eb1aacc48329a6a65f3bd671d049027 Mon Sep 17 00:00:00 2001 From: brandizzi Date: Wed, 29 Jul 2026 08:56:19 -0300 Subject: [PATCH 04/13] fix(cdk): allow EB runtime bundle copy --- infra/cdk/README.md | 7 +++++++ infra/cdk/deploy-dev-stack.ts | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 37446c4..4a61dc1 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -48,6 +48,13 @@ The role grants only: `s3:PutObject`, `s3:GetObject`, and `s3:GetObjectVersion`, which the pinned official deployment action requires to validate the `CreateApplicationVersion` source bundle after upload. +- `s3:PutObject` on only + `elasticbeanstalk-us-east-1-396287094661/resources/environments/e-hehnrqjjrt/_runtime/_versions/shoc-backend/*`. + Elastic Beanstalk copies each uploaded source bundle into this + environment-specific runtime prefix during `UpdateEnvironment`. Run + `30448885838` exposed the exact source and destination after the earlier ACL + denial was resolved. The grant does not cover another environment, another + application, bucket ACLs, object ACLs, tags, retention, deletion, or reads. - `s3:GetObjectAcl` on objects under the service-wide `arn:aws:s3:::elasticbeanstalk-*/*` namespace. AWS Support case `178526484500047` confirmed that `UpdateEnvironment` uses the initiating diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index 36addaf..3047287 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -18,6 +18,9 @@ export class DeployDevStack extends cdk.Stack { const applicationArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:application/${APPLICATION_NAME}`; const environmentArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:environment/${APPLICATION_NAME}/${ENVIRONMENT_NAME}`; const bucketArn = `arn:aws:s3:::${BUCKET_NAME}`; + const runtimeVersionArn = + `${bucketArn}/resources/environments/${ENVIRONMENT_ID}` + + `/_runtime/_versions/${APPLICATION_NAME}/*`; const oidcProviderArn = `arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com`; const deployRole = new iam.Role(this, 'GithubDeployRole', { @@ -147,6 +150,16 @@ export class DeployDevStack extends cdk.Stack { }), ); + deployRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:PutObject'], + // UpdateEnvironment copies the uploaded source bundle into this + // environment-specific runtime version prefix before deployment. + resources: [runtimeVersionArn], + }), + ); + deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, From 42d877295108645ea051b779b61f2e2e219fbd6e Mon Sep 17 00:00:00 2001 From: brandizzi Date: Wed, 29 Jul 2026 09:05:44 -0300 Subject: [PATCH 05/13] fix(cdk): allow EB runtime cleanup --- infra/cdk/README.md | 12 +++++++----- infra/cdk/deploy-dev-stack.ts | 5 +++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 4a61dc1..1102e63 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -48,13 +48,15 @@ The role grants only: `s3:PutObject`, `s3:GetObject`, and `s3:GetObjectVersion`, which the pinned official deployment action requires to validate the `CreateApplicationVersion` source bundle after upload. -- `s3:PutObject` on only +- `s3:PutObject` and `s3:DeleteObject` on only `elasticbeanstalk-us-east-1-396287094661/resources/environments/e-hehnrqjjrt/_runtime/_versions/shoc-backend/*`. Elastic Beanstalk copies each uploaded source bundle into this - environment-specific runtime prefix during `UpdateEnvironment`. Run - `30448885838` exposed the exact source and destination after the earlier ACL - denial was resolved. The grant does not cover another environment, another - application, bucket ACLs, object ACLs, tags, retention, deletion, or reads. + environment-specific runtime prefix during `UpdateEnvironment` and removes + that temporary copy after the version is registered. Attempts 1 and 2 of run + `30448885838` exposed the exact source, destination, and cleanup denial after + the earlier ACL denial was resolved. The grant does not cover another + environment, another application, source bundles, object versions, bucket + ACLs, object ACLs, tags, retention, or reads. - `s3:GetObjectAcl` on objects under the service-wide `arn:aws:s3:::elasticbeanstalk-*/*` namespace. AWS Support case `178526484500047` confirmed that `UpdateEnvironment` uses the initiating diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index 3047287..ee5d6e4 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -153,9 +153,10 @@ export class DeployDevStack extends cdk.Stack { deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, - actions: ['s3:PutObject'], + actions: ['s3:DeleteObject', 's3:PutObject'], // UpdateEnvironment copies the uploaded source bundle into this - // environment-specific runtime version prefix before deployment. + // environment-specific runtime prefix and removes that temporary copy + // after the version is registered. resources: [runtimeVersionArn], }), ); From 424bf20f54092864737412bba780053e1b86bdcc Mon Sep 17 00:00:00 2001 From: brandizzi Date: Wed, 29 Jul 2026 09:21:28 -0300 Subject: [PATCH 06/13] fix(cdk): allow EB runtime verification --- infra/cdk/README.md | 14 ++++++++------ infra/cdk/deploy-dev-stack.ts | 6 +++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 1102e63..e84694b 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -48,15 +48,17 @@ The role grants only: `s3:PutObject`, `s3:GetObject`, and `s3:GetObjectVersion`, which the pinned official deployment action requires to validate the `CreateApplicationVersion` source bundle after upload. -- `s3:PutObject` and `s3:DeleteObject` on only +- `s3:PutObject`, `s3:GetObject`, and `s3:DeleteObject` on only `elasticbeanstalk-us-east-1-396287094661/resources/environments/e-hehnrqjjrt/_runtime/_versions/shoc-backend/*`. Elastic Beanstalk copies each uploaded source bundle into this - environment-specific runtime prefix during `UpdateEnvironment` and removes - that temporary copy after the version is registered. Attempts 1 and 2 of run - `30448885838` exposed the exact source, destination, and cleanup denial after - the earlier ACL denial was resolved. The grant does not cover another + environment-specific runtime prefix during `UpdateEnvironment`, verifies it + with `HeadObject` (authorized by `s3:GetObject`), and removes the temporary + copy after the version is registered. Attempts 1 through 4 of run + `30448885838` exposed the exact source, destination, cleanup, and verification + operations after the earlier ACL denial was resolved. CloudTrail recorded + the exact `s3:GetObject` denial on attempt 4. The grant does not cover another environment, another application, source bundles, object versions, bucket - ACLs, object ACLs, tags, retention, or reads. + ACLs, object ACLs, tags, or retention. - `s3:GetObjectAcl` on objects under the service-wide `arn:aws:s3:::elasticbeanstalk-*/*` namespace. AWS Support case `178526484500047` confirmed that `UpdateEnvironment` uses the initiating diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index ee5d6e4..88c6009 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -153,10 +153,10 @@ export class DeployDevStack extends cdk.Stack { deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, - actions: ['s3:DeleteObject', 's3:PutObject'], + actions: ['s3:DeleteObject', 's3:GetObject', 's3:PutObject'], // UpdateEnvironment copies the uploaded source bundle into this - // environment-specific runtime prefix and removes that temporary copy - // after the version is registered. + // environment-specific runtime prefix, verifies the temporary copy, + // and removes it after the version is registered. resources: [runtimeVersionArn], }), ); From 9f54399e63e044c49e0dc9ac5cf0689760531d70 Mon Sep 17 00:00:00 2001 From: brandizzi Date: Wed, 29 Jul 2026 09:26:12 -0300 Subject: [PATCH 07/13] fix(cdk): allow EB runtime version ACL read --- infra/cdk/README.md | 10 ++++++---- infra/cdk/deploy-dev-stack.ts | 7 ++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index e84694b..7588086 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -48,7 +48,8 @@ The role grants only: `s3:PutObject`, `s3:GetObject`, and `s3:GetObjectVersion`, which the pinned official deployment action requires to validate the `CreateApplicationVersion` source bundle after upload. -- `s3:PutObject`, `s3:GetObject`, and `s3:DeleteObject` on only +- `s3:PutObject`, `s3:GetObject`, `s3:GetObjectVersionAcl`, and + `s3:DeleteObject` on only `elasticbeanstalk-us-east-1-396287094661/resources/environments/e-hehnrqjjrt/_runtime/_versions/shoc-backend/*`. Elastic Beanstalk copies each uploaded source bundle into this environment-specific runtime prefix during `UpdateEnvironment`, verifies it @@ -56,9 +57,10 @@ The role grants only: copy after the version is registered. Attempts 1 through 4 of run `30448885838` exposed the exact source, destination, cleanup, and verification operations after the earlier ACL denial was resolved. CloudTrail recorded - the exact `s3:GetObject` denial on attempt 4. The grant does not cover another - environment, another application, source bundles, object versions, bucket - ACLs, object ACLs, tags, or retention. + the exact `s3:GetObject` denial on attempt 4; attempt 6 then exposed the + version-specific ACL read performed on the copied object. The grant does not + cover another environment, another application, source bundles, object + content versions, ACL mutation, tags, or retention. - `s3:GetObjectAcl` on objects under the service-wide `arn:aws:s3:::elasticbeanstalk-*/*` namespace. AWS Support case `178526484500047` confirmed that `UpdateEnvironment` uses the initiating diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index 88c6009..97350e7 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -153,7 +153,12 @@ export class DeployDevStack extends cdk.Stack { deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, - actions: ['s3:DeleteObject', 's3:GetObject', 's3:PutObject'], + actions: [ + 's3:DeleteObject', + 's3:GetObject', + 's3:GetObjectVersionAcl', + 's3:PutObject', + ], // UpdateEnvironment copies the uploaded source bundle into this // environment-specific runtime prefix, verifies the temporary copy, // and removes it after the version is registered. From 156b7bbed6cbdab228f7ac7b31895ddb34e600f0 Mon Sep 17 00:00:00 2001 From: brandizzi Date: Wed, 29 Jul 2026 09:30:48 -0300 Subject: [PATCH 08/13] fix(cdk): allow EB runtime version ACL write --- infra/cdk/README.md | 11 ++++++----- infra/cdk/deploy-dev-stack.ts | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 7588086..5036031 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -48,8 +48,8 @@ The role grants only: `s3:PutObject`, `s3:GetObject`, and `s3:GetObjectVersion`, which the pinned official deployment action requires to validate the `CreateApplicationVersion` source bundle after upload. -- `s3:PutObject`, `s3:GetObject`, `s3:GetObjectVersionAcl`, and - `s3:DeleteObject` on only +- `s3:PutObject`, `s3:GetObject`, `s3:GetObjectVersionAcl`, + `s3:PutObjectVersionAcl`, and `s3:DeleteObject` on only `elasticbeanstalk-us-east-1-396287094661/resources/environments/e-hehnrqjjrt/_runtime/_versions/shoc-backend/*`. Elastic Beanstalk copies each uploaded source bundle into this environment-specific runtime prefix during `UpdateEnvironment`, verifies it @@ -58,9 +58,10 @@ The role grants only: `30448885838` exposed the exact source, destination, cleanup, and verification operations after the earlier ACL denial was resolved. CloudTrail recorded the exact `s3:GetObject` denial on attempt 4; attempt 6 then exposed the - version-specific ACL read performed on the copied object. The grant does not - cover another environment, another application, source bundles, object - content versions, ACL mutation, tags, or retention. + version-specific ACL read performed on the copied object; attempt 7 exposed + the matching version-ACL write. The grant does not cover another + environment, another application, source bundles, object content versions, + non-version ACL mutation, tags, or retention. - `s3:GetObjectAcl` on objects under the service-wide `arn:aws:s3:::elasticbeanstalk-*/*` namespace. AWS Support case `178526484500047` confirmed that `UpdateEnvironment` uses the initiating diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index 97350e7..df96f05 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -158,10 +158,11 @@ export class DeployDevStack extends cdk.Stack { 's3:GetObject', 's3:GetObjectVersionAcl', 's3:PutObject', + 's3:PutObjectVersionAcl', ], // UpdateEnvironment copies the uploaded source bundle into this // environment-specific runtime prefix, verifies the temporary copy, - // and removes it after the version is registered. + // preserves its version ACL, and removes it after registration. resources: [runtimeVersionArn], }), ); From 3f607304643084bd82bfdbacbdc36eece5d8a978 Mon Sep 17 00:00:00 2001 From: brandizzi Date: Wed, 29 Jul 2026 09:45:45 -0300 Subject: [PATCH 09/13] fix(cdk): allow EB embedded extension write --- infra/cdk/README.md | 7 +++++++ infra/cdk/deploy-dev-stack.ts | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 5036031..492db5b 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -62,6 +62,13 @@ The role grants only: the matching version-ACL write. The grant does not cover another environment, another application, source bundles, object content versions, non-version ACL mutation, tags, or retention. +- `s3:PutObject` on only + `elasticbeanstalk-us-east-1-396287094661/resources/_runtime/_embedded_extensions/shoc-backend/*`. + After the runtime bundle copy and version-ACL operations succeeded, attempt 8 + of run `30448885838` showed Elastic Beanstalk materializing the application's + embedded-extension manifest at this application-specific prefix. CloudTrail + recorded the exact denied action and object ARN. The grant does not include + reads, deletes, ACL mutation, another application, or another bucket. - `s3:GetObjectAcl` on objects under the service-wide `arn:aws:s3:::elasticbeanstalk-*/*` namespace. AWS Support case `178526484500047` confirmed that `UpdateEnvironment` uses the initiating diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index df96f05..433d3b2 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -21,6 +21,9 @@ export class DeployDevStack extends cdk.Stack { const runtimeVersionArn = `${bucketArn}/resources/environments/${ENVIRONMENT_ID}` + `/_runtime/_versions/${APPLICATION_NAME}/*`; + const embeddedExtensionArn = + `${bucketArn}/resources/_runtime/_embedded_extensions/` + + `${APPLICATION_NAME}/*`; const oidcProviderArn = `arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com`; const deployRole = new iam.Role(this, 'GithubDeployRole', { @@ -167,6 +170,16 @@ export class DeployDevStack extends cdk.Stack { }), ); + deployRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:PutObject'], + // UpdateEnvironment materializes the application's embedded-extension + // manifest under this application-specific runtime prefix. + resources: [embeddedExtensionArn], + }), + ); + deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, From 5b719a660e67a87e4052794903789e291416b640 Mon Sep 17 00:00:00 2001 From: brandizzi Date: Wed, 29 Jul 2026 09:55:58 -0300 Subject: [PATCH 10/13] fix(cdk): allow EB environment extension write --- infra/cdk/README.md | 14 +++++++++----- infra/cdk/deploy-dev-stack.ts | 7 +++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 492db5b..0b9da58 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -62,13 +62,17 @@ The role grants only: the matching version-ACL write. The grant does not cover another environment, another application, source bundles, object content versions, non-version ACL mutation, tags, or retention. -- `s3:PutObject` on only - `elasticbeanstalk-us-east-1-396287094661/resources/_runtime/_embedded_extensions/shoc-backend/*`. +- `s3:PutObject` on only the two embedded-extension prefixes + `elasticbeanstalk-us-east-1-396287094661/resources/_runtime/_embedded_extensions/shoc-backend/*` + and + `elasticbeanstalk-us-east-1-396287094661/resources/environments/e-hehnrqjjrt/_runtime/_embedded_extensions/shoc-backend/*`. After the runtime bundle copy and version-ACL operations succeeded, attempt 8 of run `30448885838` showed Elastic Beanstalk materializing the application's - embedded-extension manifest at this application-specific prefix. CloudTrail - recorded the exact denied action and object ARN. The grant does not include - reads, deletes, ACL mutation, another application, or another bucket. + embedded-extension manifest at the application-specific shared prefix. + Attempt 9 then showed the matching write into the exact dev-environment + prefix. CloudTrail recorded both denied actions and object ARNs. The grant + does not include reads, deletes, ACL mutation, another application, + another environment, or another bucket. - `s3:GetObjectAcl` on objects under the service-wide `arn:aws:s3:::elasticbeanstalk-*/*` namespace. AWS Support case `178526484500047` confirmed that `UpdateEnvironment` uses the initiating diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index 433d3b2..78c1fdf 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -24,6 +24,9 @@ export class DeployDevStack extends cdk.Stack { const embeddedExtensionArn = `${bucketArn}/resources/_runtime/_embedded_extensions/` + `${APPLICATION_NAME}/*`; + const environmentEmbeddedExtensionArn = + `${bucketArn}/resources/environments/${ENVIRONMENT_ID}` + + `/_runtime/_embedded_extensions/${APPLICATION_NAME}/*`; const oidcProviderArn = `arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com`; const deployRole = new iam.Role(this, 'GithubDeployRole', { @@ -175,8 +178,8 @@ export class DeployDevStack extends cdk.Stack { effect: iam.Effect.ALLOW, actions: ['s3:PutObject'], // UpdateEnvironment materializes the application's embedded-extension - // manifest under this application-specific runtime prefix. - resources: [embeddedExtensionArn], + // manifest under the shared and environment-specific runtime prefixes. + resources: [embeddedExtensionArn, environmentEmbeddedExtensionArn], }), ); From c2f2c411d8927455a6d990dbaf5b50e4ff3daab3 Mon Sep 17 00:00:00 2001 From: brandizzi Date: Wed, 29 Jul 2026 10:33:31 -0300 Subject: [PATCH 11/13] fix(cdk): allow EB extension verification --- infra/cdk/README.md | 4 ++++ infra/cdk/deploy-dev-stack.ts | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 0b9da58..c790ad8 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -73,6 +73,10 @@ The role grants only: prefix. CloudTrail recorded both denied actions and object ARNs. The grant does not include reads, deletes, ACL mutation, another application, another environment, or another bucket. +- `s3:GetObject` on only the environment-specific embedded-extension prefix + above. Attempt 10 showed that Elastic Beanstalk verifies the materialized + environment copy with `HeadObject`, which S3 authorizes through + `s3:GetObject`. The shared embedded-extension prefix remains write-only. - `s3:GetObjectAcl` on objects under the service-wide `arn:aws:s3:::elasticbeanstalk-*/*` namespace. AWS Support case `178526484500047` confirmed that `UpdateEnvironment` uses the initiating diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index 78c1fdf..e0f3853 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -183,6 +183,15 @@ export class DeployDevStack extends cdk.Stack { }), ); + deployRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:GetObject'], + // Elastic Beanstalk verifies the environment copy with HeadObject. + resources: [environmentEmbeddedExtensionArn], + }), + ); + deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, From b8db4a7e61e838c239752a8d506feadeea2a9ece Mon Sep 17 00:00:00 2001 From: brandizzi Date: Wed, 29 Jul 2026 11:02:30 -0300 Subject: [PATCH 12/13] fix(cdk): allow EB bucket policy read --- infra/cdk/README.md | 3 ++- infra/cdk/deploy-dev-stack.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index c790ad8..9485c85 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -42,7 +42,8 @@ The role grants only: - `elasticbeanstalk:UpdateEnvironment` on environment `shoc-backend-dev` only. - `s3:ListBucket` and `s3:GetBucketLocation` on `elasticbeanstalk-us-east-1-396287094661` (the official action's - ownership-safe bucket checks), plus `s3:CreateBucket` and + ownership-safe bucket checks), `s3:GetBucketPolicy` for the policy inspection + observed in attempt 11 of run `30448885838`, plus `s3:CreateBucket` and `s3:PutBucketOwnershipControls` on the same bucket-level ARN. Under the `shoc-backend/` object prefix only: `s3:PutObject`, `s3:GetObject`, and `s3:GetObjectVersion`, which the pinned diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index e0f3853..d501786 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -143,6 +143,7 @@ export class DeployDevStack extends cdk.Stack { 's3:CreateBucket', 's3:PutBucketOwnershipControls', 's3:GetBucketLocation', + 's3:GetBucketPolicy', ], resources: [bucketArn], }), From 76cdc70c18045dbb151ea483ad35e2b5f684518c Mon Sep 17 00:00:00 2001 From: brandizzi Date: Wed, 29 Jul 2026 11:07:53 -0300 Subject: [PATCH 13/13] fix(cdk): allow EB runtime manifest updates --- infra/cdk/README.md | 6 ++++++ infra/cdk/deploy-dev-stack.ts | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 9485c85..e32d030 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -78,6 +78,12 @@ The role grants only: above. Attempt 10 showed that Elastic Beanstalk verifies the materialized environment copy with `HeadObject`, which S3 authorizes through `s3:GetObject`. The shared embedded-extension prefix remains write-only. +- `s3:GetObject` and `s3:PutObject` on only + `elasticbeanstalk-us-east-1-396287094661/resources/environments/e-hehnrqjjrt/_runtime/versions/*`. + Attempt 12 showed Elastic Beanstalk reading the previous environment version + manifest and writing its replacement under this exact dev-environment + runtime prefix. The grant excludes deletes, ACL mutation, other environments, + and application bundle content. - `s3:GetObjectAcl` on objects under the service-wide `arn:aws:s3:::elasticbeanstalk-*/*` namespace. AWS Support case `178526484500047` confirmed that `UpdateEnvironment` uses the initiating diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index d501786..0cdef0c 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -27,6 +27,9 @@ export class DeployDevStack extends cdk.Stack { const environmentEmbeddedExtensionArn = `${bucketArn}/resources/environments/${ENVIRONMENT_ID}` + `/_runtime/_embedded_extensions/${APPLICATION_NAME}/*`; + const runtimeManifestArn = + `${bucketArn}/resources/environments/${ENVIRONMENT_ID}` + + '/_runtime/versions/*'; const oidcProviderArn = `arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com`; const deployRole = new iam.Role(this, 'GithubDeployRole', { @@ -193,6 +196,16 @@ export class DeployDevStack extends cdk.Stack { }), ); + deployRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:GetObject', 's3:PutObject'], + // UpdateEnvironment reads the prior environment version manifest and + // writes its replacement under this environment-only runtime prefix. + resources: [runtimeManifestArn], + }), + ); + deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW,