From 46f568f6ea76cf17ca335103935af49e66422ad4 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:33:06 -0400 Subject: [PATCH] feat(slack-bot): codify kms:Decrypt grants for CMK'd DynamoDB readers (INFRA-95 / M-3) (#51) wo-po-lookup, po-sync, and workorder-sync read WorkOrders, WorkOrderComments, purchase-orders and PaymentsDashboard, which are now SSE-encrypted with alias/seahaven-dynamodb. Tables are imported by name so grantReadData adds no KMS perms; grant kms:Decrypt explicitly via the CMK imported from SSM /seahaven/dynamodb/cmk-arn. Replaces the interim CLI inline policy (Sid Infra95DynamoDbCmkDecrypt) with IaC. INFRA-95 --- lib/constructs/bedrock-agent.ts | 15 +++++++++++++++ lib/constructs/po-sync.ts | 13 +++++++++++++ lib/constructs/workorder-sync.ts | 16 ++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/lib/constructs/bedrock-agent.ts b/lib/constructs/bedrock-agent.ts index 27d4607..76b18cc 100644 --- a/lib/constructs/bedrock-agent.ts +++ b/lib/constructs/bedrock-agent.ts @@ -5,6 +5,8 @@ import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as kms from 'aws-cdk-lib/aws-kms'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as bedrock from 'aws-cdk-lib/aws-bedrock'; @@ -123,6 +125,19 @@ export class BedrockAgentConstruct extends Construct { sitesTable.grantReadData(this.woPoLambda); paymentsTable.grantReadData(this.woPoLambda); + // WorkOrders, WorkOrderComments, purchase-orders and PaymentsDashboard are + // SSE-encrypted with the shared customer-managed CMK (INFRA-95 / M-3). These + // tables are imported by name, so grantReadData does not add KMS perms — this + // cross-stack reader needs kms:Decrypt explicitly or every read on those four + // tables fails with AccessDenied. (verified-sites is NOT CMK-encrypted; it is + // unaffected.) The CMK key policy delegates to IAM via kms:ViaService. + const dynamodbCmk = kms.Key.fromKeyArn( + this, + 'DynamoDbCmk', + ssm.StringParameter.valueForStringParameter(this, '/seahaven/dynamodb/cmk-arn'), + ); + dynamodbCmk.grantDecrypt(this.woPoLambda); + // ── Bedrock Agent execution role ────────────────────────────────────────── const agentRole = new iam.Role(this, 'AgentRole', { roleName: 'AmazonBedrockExecutionRoleForAgents_seahaven', diff --git a/lib/constructs/po-sync.ts b/lib/constructs/po-sync.ts index 40e2c80..95faf87 100644 --- a/lib/constructs/po-sync.ts +++ b/lib/constructs/po-sync.ts @@ -5,6 +5,8 @@ import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as kms from 'aws-cdk-lib/aws-kms'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; import * as events from 'aws-cdk-lib/aws-events'; import * as targets from 'aws-cdk-lib/aws-events-targets'; import * as iam from 'aws-cdk-lib/aws-iam'; @@ -49,6 +51,17 @@ export class PoSyncConstruct extends Construct { // Grant read access to the purchase-orders table poTable.grantReadData(this.syncLambda); + // purchase-orders is SSE-encrypted with the shared customer-managed CMK + // (INFRA-95 / M-3). The table is imported by name, so grantReadData does not + // add KMS perms — grant kms:Decrypt explicitly or scans fail with + // AccessDenied. (CMK key policy delegates to IAM via kms:ViaService.) + const dynamodbCmk = kms.Key.fromKeyArn( + this, + 'DynamoDbCmk', + ssm.StringParameter.valueForStringParameter(this, '/seahaven/dynamodb/cmk-arn'), + ); + dynamodbCmk.grantDecrypt(this.syncLambda); + // Grant read/write to the KB docs bucket (read to list+delete old, write new) props.kbDocsBucket.grantReadWrite(this.syncLambda); diff --git a/lib/constructs/workorder-sync.ts b/lib/constructs/workorder-sync.ts index 13cbdef..50bdb49 100644 --- a/lib/constructs/workorder-sync.ts +++ b/lib/constructs/workorder-sync.ts @@ -5,6 +5,8 @@ import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as kms from 'aws-cdk-lib/aws-kms'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; import * as events from 'aws-cdk-lib/aws-events'; import * as targets from 'aws-cdk-lib/aws-events-targets'; import * as iam from 'aws-cdk-lib/aws-iam'; @@ -54,6 +56,20 @@ export class WorkorderSyncConstruct extends Construct { workOrdersTable.grantReadData(this.syncLambda); commentsTable.grantReadData(this.syncLambda); + // WorkOrders + WorkOrderComments are SSE-encrypted with the shared + // customer-managed CMK (INFRA-95 / M-3). Because the tables are imported by + // name (fromTableName), CDK does not know about their encryption key, so + // grantReadData does NOT add the KMS permissions — they must be granted + // explicitly or every read fails with kms:Decrypt AccessDenied. The CMK key + // policy delegates to IAM via kms:ViaService, so this identity grant is what + // authorizes this cross-stack reader. + const dynamodbCmk = kms.Key.fromKeyArn( + this, + 'DynamoDbCmk', + ssm.StringParameter.valueForStringParameter(this, '/seahaven/dynamodb/cmk-arn'), + ); + dynamodbCmk.grantDecrypt(this.syncLambda); + // Grant read/write to the KB docs bucket (read to list+delete old, write new) props.kbDocsBucket.grantReadWrite(this.syncLambda);