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
This commit is contained in:
Adam Moussa 2026-06-09 12:33:06 -04:00 • committed by GitHub
parent 9a158dcd52
commit 46f568f6ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 44 additions and 0 deletions

View file

@ -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',

View file

@ -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);

View file

@ -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);