constructor()

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;
  }