Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add option to use CMK and set the resources retention #573

Merged
merged 6 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ venv
lib/user-interface/react-app/public/aws-exports.json
out.tmp
bin/config.json
bin/config*.json

# Docs
docs/.vitepress/cache
Expand Down
23 changes: 21 additions & 2 deletions cli/magic-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ const embeddingModels = [
fs.readFileSync("./bin/config.json").toString("utf8")
);
options.prefix = config.prefix;
options.createCMKs = config.createCMKs;
options.retainOnDelete = config.retainOnDelete;
options.vpcId = config.vpc?.vpcId;
options.bedrockEnable = config.bedrock?.enabled;
options.bedrockRegion = config.bedrock?.region;
Expand Down Expand Up @@ -287,6 +289,22 @@ async function processCreateOptions(options: any): Promise<void> {
return !(this as any).state.answers.existingVpc;
},
},
{
type: "confirm",
name: "createCMKs",
message:
"Do you want to create KMS Customer Managed Keys (CMKs)? (It will be used to encrypt the data at rest.)",
initial: true,
hint: "It is recommended but enabling it on an existing environment will cause the re-creation of some of the resources (for example Aurora cluster, Open Search collection). To prevent data loss, it is recommended to use it on a new environment or at least enable retain on cleanup (needs to be deployed before enabling the use of CMK). For more information on Aurora migration, please refer to the documentation.",
},
{
type: "confirm",
name: "retainOnDelete",
message:
"Do you want to retain data stores on cleanup of the project (Logs, S3, Tables, Indexes, Cognito User pools)?",
initial: true,
hint: "It reduces the risk of deleting data. It will however not delete all the resources on cleanup (would require manual removal if relevant)",
},
{
type: "confirm",
name: "bedrockEnable",
Expand Down Expand Up @@ -718,7 +736,7 @@ async function processCreateOptions(options: any): Promise<void> {
{
type: "input",
name: "name",
message: "KnowledgeBase source name",
message: "Bedrock KnowledgeBase source name",
validate(v: string) {
return RegExp(/^\w[\w-_]*\w$/).test(v);
},
Expand Down Expand Up @@ -1102,10 +1120,11 @@ async function processCreateOptions(options: any): Promise<void> {
}

const randomSuffix = randomBytes(8).toString("hex");

// Create the config object
const config = {
prefix: answers.prefix,
createCMKs: answers.createCMKs,
retainOnDelete: answers.retainOnDelete,
vpc: answers.existingVpc
? {
vpcId: answers.vpcId.toLowerCase(),
Expand Down
4 changes: 2 additions & 2 deletions integtests/clients/cognito_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ def get_credentials(self, email: str) -> Credentials:

def get_password(self):
return "".join(
random.choices(
random.choices( # NOSONAR Only used for testing. Temporary password
string.ascii_uppercase, k=10
) # NOSONAR Only used for testing. Temporary password
)
+ random.choices(string.ascii_lowercase, k=10) # NOSONAR
+ random.choices(string.digits, k=5) # NOSONAR
+ random.choices(string.punctuation, k=3) # NOSONAR
Expand Down
4 changes: 2 additions & 2 deletions integtests/user_interface/react_app/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ def test_invalid_credentials(selenium_driver):
**{
"id_token": "",
"email": "invalid",
"password": "invalid",
"password": "invalid", # NOSONAR
"aws_access_key": "",
"aws_secret_key": "",
"aws_token": "",
}
) # NOSONAR
)
)
assert page.get_error() != None
5 changes: 4 additions & 1 deletion lib/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ export class Authentication extends Construct {
super(scope, id);

const userPool = new cognito.UserPool(this, "UserPool", {
removalPolicy: cdk.RemovalPolicy.DESTROY,
removalPolicy:
config.retainOnDelete === true
? cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE
: cdk.RemovalPolicy.DESTROY,
selfSignUpEnabled: false,
mfa: cognito.Mfa.OPTIONAL,
advancedSecurityMode: cognito.AdvancedSecurityMode.ENFORCED,
Expand Down
9 changes: 9 additions & 0 deletions lib/chatbot-api/appsync-ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import { ITopic } from "aws-cdk-lib/aws-sns";
import { UserPool } from "aws-cdk-lib/aws-cognito";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import * as path from "path";
import { IKey } from "aws-cdk-lib/aws-kms";

interface RealtimeResolversProps {
readonly queue: IQueue;
readonly topic: ITopic;
readonly topicKey: IKey;
readonly userPool: UserPool;
readonly shared: Shared;
readonly api: appsync.GraphqlApi;
Expand Down Expand Up @@ -78,6 +80,13 @@ export class RealtimeResolvers extends Construct {
outgoingMessageHandler.addEventSource(new SqsEventSource(props.queue));

props.topic.grantPublish(resolverFunction);
if (props.topicKey && resolverFunction.role) {
props.topicKey.grant(
resolverFunction.role,
"kms:GenerateDataKey",
"kms:Decrypt"
);
}

const functionDataSource = props.api.addLambdaDataSource(
"realtimeResolverFunction",
Expand Down
18 changes: 15 additions & 3 deletions lib/chatbot-api/chatbot-dynamodb-tables/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as kms from "aws-cdk-lib/aws-kms";

export interface ChatBotDynamoDBTablesProps {
readonly retainOnDelete?: boolean;
readonly kmsKey?: kms.Key;
}

export class ChatBotDynamoDBTables extends Construct {
public readonly sessionsTable: dynamodb.Table;
public readonly byUserIdIndex: string = "byUserId";

constructor(scope: Construct, id: string) {
constructor(scope: Construct, id: string, props: ChatBotDynamoDBTablesProps) {
super(scope, id);

const sessionsTable = new dynamodb.Table(this, "SessionsTable", {
Expand All @@ -19,8 +25,14 @@ export class ChatBotDynamoDBTables extends Construct {
type: dynamodb.AttributeType.STRING,
},
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
encryption: dynamodb.TableEncryption.AWS_MANAGED,
removalPolicy: cdk.RemovalPolicy.DESTROY,
encryption: props.kmsKey
? dynamodb.TableEncryption.CUSTOMER_MANAGED
: dynamodb.TableEncryption.AWS_MANAGED,
encryptionKey: props.kmsKey,
Copy link
Collaborator

@grinko grinko Sep 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if props.kmsKey does not exist? Same comment for other constructs like ChatBotS3Buckets, RealtimeResolvers, ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passes undefined. (property is ignored)

removalPolicy:
props.retainOnDelete === true
? cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE
: cdk.RemovalPolicy.DESTROY,
pointInTimeRecovery: true,
});

Expand Down
41 changes: 34 additions & 7 deletions lib/chatbot-api/chatbot-s3-buckets/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as kms from "aws-cdk-lib/aws-kms";
import { NagSuppressions } from "cdk-nag";

export interface ChatBotS3BucketsProps {
readonly retainOnDelete?: boolean;
readonly kmsKey?: kms.Key;
}

export class ChatBotS3Buckets extends Construct {
public readonly filesBucket: s3.Bucket;
public readonly userFeedbackBucket: s3.Bucket;

constructor(scope: Construct, id: string) {
constructor(scope: Construct, id: string, props: ChatBotS3BucketsProps) {
super(scope, id);

const logsBucket = new s3.Bucket(this, "LogsBucket", {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
removalPolicy:
props.retainOnDelete === true
? cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE
: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: props.retainOnDelete !== true,
enforceSSL: true,
encryption: s3.BucketEncryption.S3_MANAGED,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does logs bucket encrypted with S3 managed key and files bucket has options?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not supported.
https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-server-access-logging.html

"You can use default bucket encryption on the destination bucket only if you use server-side encryption with Amazon S3 managed keys (SSE-S3), which uses the 256-bit Advanced Encryption Standard (AES-256). Default server-side encryption with AWS Key Management Service (AWS KMS) keys (SSE-KMS) is not supported."

versioned: true,
});

const filesBucket = new s3.Bucket(this, "FilesBucket", {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
removalPolicy:
props.retainOnDelete === true
? cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE
: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: props.retainOnDelete !== true,
transferAcceleration: true,
enforceSSL: true,
serverAccessLogsBucket: logsBucket,
encryption: props.kmsKey
? s3.BucketEncryption.KMS
: s3.BucketEncryption.S3_MANAGED,
encryptionKey: props.kmsKey,
versioned: true,
cors: [
{
allowedHeaders: ["*"],
Expand All @@ -42,10 +61,18 @@ export class ChatBotS3Buckets extends Construct {

const userFeedbackBucket = new s3.Bucket(this, "UserFeedbackBucket", {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
removalPolicy:
props.retainOnDelete === true
? cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE
: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: props.retainOnDelete !== true,
enforceSSL: true,
serverAccessLogsBucket: logsBucket,
encryption: props.kmsKey
? s3.BucketEncryption.KMS
: s3.BucketEncryption.S3_MANAGED,
encryptionKey: props.kmsKey,
versioned: true,
});

this.filesBucket = filesBucket;
Expand Down
12 changes: 9 additions & 3 deletions lib/chatbot-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ export class ChatBotApi extends Construct {
constructor(scope: Construct, id: string, props: ChatBotApiProps) {
super(scope, id);

const chatTables = new ChatBotDynamoDBTables(this, "ChatDynamoDBTables");
const chatBuckets = new ChatBotS3Buckets(this, "ChatBuckets");
const chatTables = new ChatBotDynamoDBTables(this, "ChatDynamoDBTables", {
kmsKey: props.shared.kmsKey,
retainOnDelete: props.config.retainOnDelete,
});
const chatBuckets = new ChatBotS3Buckets(this, "ChatBuckets", {
kmsKey: props.shared.kmsKey,
retainOnDelete: props.config.retainOnDelete,
});

const loggingRole = new iam.Role(this, "apiLoggingRole", {
assumedBy: new iam.ServicePrincipal("appsync.amazonaws.com"),
Expand Down Expand Up @@ -80,7 +86,7 @@ export class ChatBotApi extends Construct {
},
logConfig: {
fieldLogLevel: appsync.FieldLogLevel.ALL,
retention: RetentionDays.ONE_WEEK,
retention: props.config.logRetention ?? RetentionDays.ONE_WEEK,
role: loggingRole,
},
xrayEnabled: true,
Expand Down
14 changes: 13 additions & 1 deletion lib/chatbot-api/websocket-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,24 @@ export class RealtimeGraphqlApiBackend extends Construct {
) {
super(scope, id);
// Create the main Message Topic acting as a message bus
const messagesTopic = new sns.Topic(this, "MessagesTopic");
const messagesTopic = new sns.Topic(this, "MessagesTopic", {
enforceSSL: true,
masterKey: props.shared.kmsKey,
});

const deadLetterQueue = new sqs.Queue(this, "OutgoingMessagesDLQ", {
encryption: props.shared.queueKmsKey
? sqs.QueueEncryption.KMS
: undefined,
encryptionMasterKey: props.shared.queueKmsKey,
enforceSSL: true,
});

const queue = new sqs.Queue(this, "OutgoingMessagesQueue", {
encryption: props.shared.queueKmsKey
? sqs.QueueEncryption.KMS
: undefined,
encryptionMasterKey: props.shared.queueKmsKey,
removalPolicy: cdk.RemovalPolicy.DESTROY,
enforceSSL: true,
deadLetterQueue: {
Expand All @@ -62,6 +73,7 @@ export class RealtimeGraphqlApiBackend extends Construct {
const resolvers = new RealtimeResolvers(this, "Resolvers", {
queue: queue,
topic: messagesTopic,
topicKey: props.shared.kmsKey,
userPool: props.userPool,
shared: props.shared,
api: props.api,
Expand Down
25 changes: 21 additions & 4 deletions lib/model-interfaces/idefics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { Construct } from "constructs";
import * as path from "path";
import { Shared } from "../../shared";
import { SystemConfig } from "../../shared/types";
import { RemovalPolicy } from "aws-cdk-lib";
import { NagSuppressions } from "cdk-nag";

interface IdeficsInterfaceProps {
Expand Down Expand Up @@ -78,6 +77,9 @@ export class IdeficsInterface extends Construct {
props.chatbotFilesBucket.grantRead(requestHandler);
props.sessionsTable.grantReadWriteData(requestHandler);
props.messagesTopic.grantPublish(requestHandler);
if (props.shared.kmsKey && requestHandler.role) {
props.shared.kmsKey.grantEncrypt(requestHandler.role);
}
props.shared.configParameter.grantRead(requestHandler);
requestHandler.addToRolePolicy(
new iam.PolicyStatement({
Expand All @@ -89,8 +91,17 @@ export class IdeficsInterface extends Construct {

const deadLetterQueue = new sqs.Queue(this, "DLQ", {
enforceSSL: true,
encryption: props.shared.queueKmsKey
? sqs.QueueEncryption.KMS
: undefined,
encryptionMasterKey: props.shared.queueKmsKey,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const queue = new sqs.Queue(this, "IdeficsIngestionQueue", {
encryption: props.shared.queueKmsKey
? sqs.QueueEncryption.KMS
: undefined,
encryptionMasterKey: props.shared.queueKmsKey,
removalPolicy: cdk.RemovalPolicy.DESTROY,
// https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#events-sqs-queueconfig
visibilityTimeout: cdk.Duration.minutes(lambdaDurationInMinutes * 6),
Expand Down Expand Up @@ -175,7 +186,11 @@ export class IdeficsInterface extends Construct {
this,
"ChatbotFilesPrivateApiAccessLogs",
{
removalPolicy: RemovalPolicy.DESTROY,
removalPolicy:
this.props.config.retainOnDelete === true
? cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE
: cdk.RemovalPolicy.DESTROY,
retention: this.props.config.logRetention,
}
);

Expand All @@ -200,7 +215,8 @@ export class IdeficsInterface extends Construct {
actions: ["execute-api:Invoke"],
effect: iam.Effect.ALLOW,
resources: ["execute-api:/*/*/*"],
principals: [new iam.AnyPrincipal()],
principals: [new iam.AnyPrincipal()], // NOSONAR
// Private integration with deny based on the VPCe
}),
new iam.PolicyStatement({
actions: ["execute-api:Invoke"],
Expand Down Expand Up @@ -263,10 +279,11 @@ export class IdeficsInterface extends Construct {
},
});

// prettier-ignore
api.root
.addResource("{folder}")
.addResource("{key}")
.addMethod("GET", s3Integration, {
.addMethod("GET", s3Integration, { // NOSONAR Private integration
methodResponses: [
{
statusCode: "200",
Expand Down
9 changes: 9 additions & 0 deletions lib/model-interfaces/langchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ export class LangChainInterface extends Construct {

props.sessionsTable.grantReadWriteData(requestHandler);
props.messagesTopic.grantPublish(requestHandler);
if (props.shared.kmsKey && requestHandler.role) {
props.shared.kmsKey.grantEncrypt(requestHandler.role);
}
props.shared.apiKeysSecret.grantRead(requestHandler);
props.shared.configParameter.grantRead(requestHandler);

Expand All @@ -233,10 +236,16 @@ export class LangChainInterface extends Construct {
);

const deadLetterQueue = new sqs.Queue(this, "DLQ", {
encryption: props.shared.kmsKey ? sqs.QueueEncryption.KMS : undefined,
encryptionMasterKey: props.shared.kmsKey,
enforceSSL: true,
});

const queue = new sqs.Queue(this, "LangChainIngestionQueue", {
encryption: props.shared.queueKmsKey
? sqs.QueueEncryption.KMS
: undefined,
encryptionMasterKey: props.shared.queueKmsKey,
removalPolicy: cdk.RemovalPolicy.DESTROY,
// https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#events-sqs-queueconfig
visibilityTimeout: cdk.Duration.minutes(15 * 6),
Expand Down
Loading
Loading