This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
seahaven-slack-bot/lib/constructs/workorder-sync.ts
Adam Moussa 46f568f6ea
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
2026-06-09 12:33:06 -04:00

95 lines
3.7 KiB
TypeScript

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
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 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';
import * as path from 'path';
export interface WorkorderSyncProps {
region: string;
kbDocsBucket: s3.Bucket;
knowledgeBaseId: string;
dataSourceId: string;
}
export class WorkorderSyncConstruct extends Construct {
public readonly syncLambda: lambdaNodejs.NodejsFunction;
constructor(scope: Construct, id: string, props: WorkorderSyncProps) {
super(scope, id);
// Import existing DynamoDB tables by name (owned by workorder-ingest stack)
const workOrdersTable = dynamodb.Table.fromTableName(
this, 'WorkOrdersTable', 'WorkOrders',
);
const commentsTable = dynamodb.Table.fromTableName(
this, 'WorkOrderCommentsTable', 'WorkOrderComments',
);
// Lambda — scans DynamoDB work orders + comments, uploads markdown to S3, triggers KB ingestion
this.syncLambda = new lambdaNodejs.NodejsFunction(this, 'SyncLambda', {
functionName: 'seahaven-workorder-sync',
entry: path.join(__dirname, '../../lambda/workorder-sync/index.ts'),
runtime: lambda.Runtime.NODEJS_22_X,
architecture: lambda.Architecture.ARM_64,
logRetention: logs.RetentionDays.TWO_MONTHS,
memorySize: 512,
timeout: cdk.Duration.minutes(5),
environment: {
WORK_ORDERS_TABLE: workOrdersTable.tableName,
COMMENTS_TABLE: commentsTable.tableName,
KB_BUCKET_NAME: props.kbDocsBucket.bucketName,
KNOWLEDGE_BASE_ID: props.knowledgeBaseId,
DATA_SOURCE_ID: props.dataSourceId,
REGION: props.region,
},
});
// Grant read access to both DynamoDB tables
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);
// Allow Lambda to start a Bedrock KB ingestion job
this.syncLambda.addToRolePolicy(
new iam.PolicyStatement({
actions: ['bedrock:StartIngestionJob'],
resources: [
`arn:aws:bedrock:${props.region}:*:knowledge-base/${props.knowledgeBaseId}`,
],
}),
);
// EventBridge rule — fires daily at 02:00 UTC
const dailyRule = new events.Rule(this, 'DailySyncRule', {
ruleName: 'seahaven-workorder-daily-sync',
description: 'Daily Work Orders → KB sync at 02:00 UTC',
schedule: events.Schedule.cron({ minute: '0', hour: '2' }),
});
dailyRule.addTarget(new targets.LambdaFunction(this.syncLambda));
}
}