in packages/constructs/L3/ai/gaia-l3-construct/lib/chatbot-api/websocket-api.ts [45:306]
constructor(scope: Construct, id: string, props: WebSocketApiProps) {
super(scope, id, props);
this.props = props;
// Create the main Message Topic acting as a message bus
const messagesTopic = new MdaaSnsTopic(this, 'WeSocketMessagesTopic', {
masterKey: props.encryptionKey,
naming: props.naming,
createParams: true,
createOutputs: false,
topicName: 'WeSocketMessagesTopic',
});
const connectionsTable = new MdaaDDBTable(this, 'ConnectionsTable', {
encryptionKey: props.encryptionKey,
naming: props.naming,
tableName: props.naming.resourceName('Connections'),
createParams: true,
createOutputs: false,
partitionKey: {
name: 'connectionId',
type: dynamodb.AttributeType.STRING,
},
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
connectionsTable.addGlobalSecondaryIndex({
indexName: 'byUser',
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING },
});
const vpcNetworkInterfacePolicy = new PolicyStatement({
effect: Effect.ALLOW,
actions: ['ec2:CreateNetworkInterface', 'ec2:DescribeNetworkInterfaces', 'ec2:DeleteNetworkInterface'],
resources: ['*'],
});
const connectionHandlerFunctionRole = new MdaaLambdaRole(this, 'WebSocketConnectionHandlerFunctionRole', {
naming: this.props.naming,
roleName: 'WebSocketConnectionHandlerFunctionRole',
logGroupNames: [this.props.naming.resourceName('websocket-connection-handler')],
createParams: false,
createOutputs: false,
});
connectionHandlerFunctionRole.addToPolicy(vpcNetworkInterfacePolicy);
const connectionHandlerFunction = this.createConnectionHandlerFunction(
connectionHandlerFunctionRole,
props.shared.appSecurityGroup,
connectionsTable,
);
props.encryptionKey.grantEncryptDecrypt(connectionHandlerFunction);
connectionsTable.grantReadWriteData(connectionHandlerFunctionRole);
const authorizerFunctionRole = new MdaaLambdaRole(this, 'WebSocketAuthorizerFunctionRole', {
naming: this.props.naming,
roleName: 'WebSocketAuthorizerFunctionRole',
logGroupNames: [this.props.naming.resourceName('websocket-authorizer')],
createParams: false,
createOutputs: false,
});
authorizerFunctionRole.addToPolicy(vpcNetworkInterfacePolicy);
const authorizerFunction = this.createAuthorizerFunction(props.shared.appSecurityGroup, authorizerFunctionRole);
const webSocketApi = new apigwv2.WebSocketApi(this, 'WebSocketApi', {
connectRouteOptions: {
authorizer: new WebSocketLambdaAuthorizer('Authorizer', authorizerFunction, {
identitySource: ['route.request.querystring.token'],
}),
integration: new WebSocketLambdaIntegration('ConnectIntegration', connectionHandlerFunction),
},
disconnectRouteOptions: {
integration: new WebSocketLambdaIntegration('DisconnectIntegration', connectionHandlerFunction),
},
});
if (this.props.config?.api?.socketApiDomainName === undefined) {
new ssm.StringParameter(this, 'SocketApiIdSSMParam', {
parameterName: this.props.naming.ssmPath('socket/api/id'),
stringValue: webSocketApi.apiId,
});
}
const stage = new apigwv2.WebSocketStage(this, 'WebSocketApiStage', {
webSocketApi,
stageName: 'socket',
autoDeploy: true,
});
const apiCustomDomainConfigs = props.config.api;
if (apiCustomDomainConfigs !== undefined) {
this.applyCustomDomain(apiCustomDomainConfigs, webSocketApi, stage);
}
const apiAccessLogGroupProps: MdaaLogGroupProps = {
logGroupName: 'genai-admin-websocket-api',
encryptionKey: this.props.encryptionKey,
// WAF log group destination names must start with aws-waf-logs-
// https://docs.aws.amazon.com/waf/latest/developerguide/logging-cw-logs.html
logGroupNamePathPrefix: 'genai-admin-websocket-api-access-logs-',
retention: RetentionDays.INFINITE,
naming: this.props.naming,
createParams: false,
createOutputs: false,
};
const apiAccessLogGroup = new MdaaLogGroup(this, 'WebSocketApiLogGroup', apiAccessLogGroupProps);
const cfnStage = stage.node.defaultChild as unknown as apigwv2.CfnStage;
cfnStage.accessLogSettings = {
destinationArn: apiAccessLogGroup.logGroupArn,
format: JSON.stringify({
requestId: '$context.requestId',
ip: '$context.identity.sourceIp',
caller: '$context.identity.caller',
user: '$context.identity.user',
connectionId: '$context.connectionId',
}),
};
const incomingMessageHandlerFunctionRole = new MdaaLambdaRole(this, 'WebSocketIncomingMessageHandlerFunctionRole', {
naming: props.naming,
roleName: 'WebSocketIncomingMessageHandlerFunctionRole',
logGroupNames: [props.naming.resourceName('websocket-incoming-message-handler')],
createParams: false,
createOutputs: false,
});
incomingMessageHandlerFunctionRole.addToPolicy(vpcNetworkInterfacePolicy);
const incomingMessageHandlerFunction = this.createIncomingMessageHandlerFunction(
incomingMessageHandlerFunctionRole,
props.shared.appSecurityGroup,
messagesTopic,
stage,
);
props.encryptionKey.grantEncryptDecrypt(incomingMessageHandlerFunctionRole);
messagesTopic.grantPublish(incomingMessageHandlerFunctionRole);
incomingMessageHandlerFunctionRole.addToPolicy(
new iam.PolicyStatement({
actions: ['events:PutEvents'],
resources: [`arn:${cdk.Aws.PARTITION}:events:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:event-bus/default`],
}),
);
incomingMessageHandlerFunctionRole.addToPolicy(vpcNetworkInterfacePolicy);
webSocketApi.addRoute('$default', {
integration: new WebSocketLambdaIntegration('DefaultIntegration', incomingMessageHandlerFunction),
});
const outgoingMessageHandlerFunctionRole = new MdaaLambdaRole(this, 'WebSocketOutgoingMessageFunctionRole', {
naming: props.naming,
roleName: 'WebSocketOutgoingMessageHandlerFunctionRole',
logGroupNames: [props.naming.resourceName('websocket-outgoing-message-handler')],
createParams: false,
createOutputs: false,
});
props.encryptionKey.grantEncryptDecrypt(outgoingMessageHandlerFunctionRole);
outgoingMessageHandlerFunctionRole.addToPolicy(vpcNetworkInterfacePolicy);
const outgoingMessageHandlerFunction = this.createOutgoingMessageHandlerFunction(
outgoingMessageHandlerFunctionRole,
connectionsTable,
stage,
);
connectionsTable.grantReadData(outgoingMessageHandlerFunctionRole);
outgoingMessageHandlerFunctionRole.addToPolicy(
new iam.PolicyStatement({
actions: ['execute-api:ManageConnections'],
resources: [
`arn:${cdk.Aws.PARTITION}:execute-api:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:${webSocketApi.apiId}/${stage.stageName}/*/*`,
],
}),
);
const deadLetterQueue = new MdaaSqsDeadLetterQueue(this, 'WebSocketOutgoingMessagesDLQ', {
encryptionMasterKey: props.encryptionKey,
naming: props.naming,
queueName: 'WebSocketOutgoingMessagesDLQ',
createParams: false,
createOutputs: false,
});
const queue = new MdaaSqsQueue(this, 'WebSocketOutgoingMessagesQueue', {
encryptionMasterKey: props.encryptionKey,
naming: props.naming,
createParams: false,
createOutputs: false,
queueName: 'WebSocketOutgoingMessagesQueue',
deadLetterQueue: {
queue: deadLetterQueue,
maxReceiveCount: 3,
},
});
// grant eventbridge permissions to send messages to the queue
queue.addToResourcePolicy(
new iam.PolicyStatement({
actions: ['sqs:SendMessage'],
resources: [queue.queueArn],
principals: [new iam.ServicePrincipal('events.amazonaws.com'), new iam.ServicePrincipal('sqs.amazonaws.com')],
}),
);
outgoingMessageHandlerFunction.addEventSource(new lambdaEventSources.SqsEventSource(queue));
// Route all outgoing messages to the websocket interface queue
messagesTopic.addSubscription(
new subscriptions.SqsSubscription(queue, {
filterPolicyWithMessageBody: {
direction: sns.FilterOrPolicy.filter(
sns.SubscriptionFilter.stringFilter({
allowlist: [Direction.OUT],
}),
),
},
}),
);
MdaaNagSuppressions.addCodeResourceSuppressions(
incomingMessageHandlerFunctionRole,
[
{
id: 'AwsSolutions-IAM5',
reason:
'X-Ray actions only support wildcard and execute api manage connections restricted to stack api gateway',
},
{ id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Inline policy managed by MDAA framework.' },
{ id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Inline policy managed by MDAA framework.' },
{ id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Inline policy managed by MDAA framework.' },
],
true,
);
MdaaNagSuppressions.addCodeResourceSuppressions(
outgoingMessageHandlerFunctionRole,
[
{
id: 'AwsSolutions-IAM5',
reason:
'X-Ray actions only support wildcard and execute api manage connections restricted to stack api gateway, and AWSLambdaBasicExecutionRole restrictive enough',
},
{ id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Inline policy managed by MDAA framework.' },
{ id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Inline policy managed by MDAA framework.' },
{ id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Inline policy managed by MDAA framework.' },
],
true,
);
MdaaNagSuppressions.addCodeResourceSuppressions(
webSocketApi,
[{ id: 'AwsSolutions-APIG4', reason: 'API guarded with Cognito Auth and an Authorizer lambda' }],
true,
);
this.api = webSocketApi;
this.messagesTopic = messagesTopic;
}