packages/constructs/L3/ai/sm-studio-domain-l3-construct/lib/sm-studio-domain-l3-construct.ts (477 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { MdaaSecurityGroup, MdaaSecurityGroupProps, MdaaSecurityGroupRuleProps } from '@aws-mdaa/ec2-constructs'; import { IMdaaManagedPolicy, MdaaRole } from '@aws-mdaa/iam-constructs'; import { MdaaRoleRef } from '@aws-mdaa/iam-role-helper'; import { DECRYPT_ACTIONS, ENCRYPT_ACTIONS, IMdaaKmsKey, MdaaKmsKey } from '@aws-mdaa/kms-constructs'; import { MdaaL3Construct, MdaaL3ConstructProps } from '@aws-mdaa/l3-construct'; import { MdaaLambdaRole } from '@aws-mdaa/lambda-constructs'; import { RestrictBucketToRoles, RestrictObjectPrefixToRoles } from '@aws-mdaa/s3-bucketpolicy-helper'; import { MdaaBucket } from '@aws-mdaa/s3-constructs'; import { LifecycleConfigAppType, MdaaStudioDomain, MdaaStudioLifecycleConfig, MdaaStudioLifecycleConfigProps, } from '@aws-mdaa/sagemaker-constructs'; import { AssetDeploymentProps, LifeCycleConfigHelper, LifecycleScriptProps } from '@aws-mdaa/sm-shared'; import { CfnTag } from 'aws-cdk-lib'; import { ISecurityGroup, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2'; import { Effect, IRole, ManagedPolicy, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; import { IKey, Key } from 'aws-cdk-lib/aws-kms'; import { Bucket, IBucket } from 'aws-cdk-lib/aws-s3'; import { CfnDomain, CfnUserProfile } from 'aws-cdk-lib/aws-sagemaker'; import { MdaaNagSuppressions } from '@aws-mdaa/construct'; //NOSONAR import { Construct } from 'constructs'; export type AuthMode = 'SSO' | 'IAM'; export interface DomainUserSettings { /** * The Jupyter server's app settings. * * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-domain-usersettings.html#cfn-sagemaker-domain-usersettings-jupyterserverappsettings */ readonly jupyterServerAppSettings?: CfnDomain.JupyterServerAppSettingsProperty; /** * The kernel gateway app settings. * * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-domain-usersettings.html#cfn-sagemaker-domain-usersettings-kernelgatewayappsettings */ readonly kernelGatewayAppSettings?: CfnDomain.KernelGatewayAppSettingsProperty; /** * A collection of settings that configure the `RSessionGateway` app. * * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-domain-usersettings.html#cfn-sagemaker-domain-usersettings-rsessionappsettings */ readonly rSessionAppSettings?: CfnDomain.RSessionAppSettingsProperty; /** * A collection of settings that configure user interaction with the `RStudioServerPro` app. * * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-domain-usersettings.html#cfn-sagemaker-domain-usersettings-rstudioserverproappsettings */ readonly rStudioServerProAppSettings?: CfnDomain.RStudioServerProAppSettingsProperty; /** * The security groups for the Amazon Virtual Private Cloud (VPC) that Studio uses for communication. * * Optional when the `CreateDomain.AppNetworkAccessType` parameter is set to `PublicInternetOnly` . * * Required when the `CreateDomain.AppNetworkAccessType` parameter is set to `VpcOnly` , unless specified as part of the `DefaultUserSettings` for the domain. * * Amazon SageMaker adds a security group to allow NFS traffic from SageMaker Studio. Therefore, the number of security groups that you can specify is one less than the maximum number shown. * * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-domain-usersettings.html#cfn-sagemaker-domain-usersettings-securitygroups */ readonly securityGroups?: string[]; /** * Specifies options for sharing SageMaker Studio notebooks. * * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-domain-usersettings.html#cfn-sagemaker-domain-usersettings-sharingsettings */ readonly sharingSettings?: CfnDomain.SharingSettingsProperty; readonly studioWebPortal?: 'ENABLED' | 'DISABLED'; } export interface DomainProps { /** * Ids of roles which will be provided administrator access to Studio resources */ readonly dataAdminRoles?: MdaaRoleRef[]; /** * The AuthMode for the domain. Must be either 'SSO' or 'IAM' */ readonly authMode: AuthMode; /** * The ID of the VPC to which all Studio user apps will be bound */ readonly vpcId: string; /** * The IDs of the subnets to which all Studio user apps will be bound */ readonly subnetIds: string[]; /** * Default user settings for user apps. */ readonly defaultUserSettings?: DomainUserSettings; /** * Id of an existing security group. If specified, will be used instead of creating * a security group */ readonly securityGroupId?: string; /** * Security group ingress rules. */ readonly securityGroupIngress?: MdaaSecurityGroupRuleProps; /** * Security group Egress rules. */ readonly securityGroupEgress?: MdaaSecurityGroupRuleProps; /** * If defined, will be set as the default execution role for the domain. * If undefined, a default execution role will be created with minimal permissions required * to launch Studio Apps. */ readonly defaultExecutionRole?: MdaaRoleRef; /** * If specified, this will be used as the domain bucket. * If not specified, a new bucket will be created. */ readonly domainBucket?: DomainBucketProps; /** * If defined, will be set as the studio KMS Key (for EFS) */ readonly kmsKeyArn?: string; /** * List of Studio user profiles which will be created. The key/name * of the user profile should be specified as follows: * If the Domain is in SSO mode, this should map to an SSO User ID. * If in IAM mode, this should map to Session Name portion of the aws:userid variable. */ readonly userProfiles?: NamedUserProfileProps; /** * S3 Prefix where shared notebooks will be stored. * If not specified, defaults to "sharing/" */ readonly notebookSharingPrefix?: string; /** * S3 Prefix where lifecycle assets will be stored. * If not specified, default to "lifecycle-assets/" */ readonly assetPrefix?: string; /** * Lifecycle configs to be created and bound to domain applications */ readonly lifecycleConfigs?: StudioLifecycleConfigProps; /** * Memory to be allocated to the Lifecycle asset deployment Lambda. * May need to be increased for very large asset deployments. */ readonly assetDeploymentMemoryLimitMB?: number; } export interface StudioLifecycleConfigProps { /** * Lifecycle config scripts */ readonly jupyter?: LifecycleScriptProps; /** * Lifecycle config scripts */ readonly kernel?: LifecycleScriptProps; } export interface NamedUserProfileProps { /** @jsii ignore */ [name: string]: UserProfileProps; } export interface UserProfileProps { /** * Required if the domain is in IAM AuthMode. This is the role * from which the user will launch the user profile in Studio. * The role's id will be combined with the userid * to grant the user access to launch the user profile. */ readonly userRole?: MdaaRoleRef; } export interface DomainBucketProps { /** * If specified, will be used as the bucket for the domain, * where notebooks will be shared, and lifecycle assets will be uploaded. * Otherwise a new bucket will be created. */ readonly domainBucketName: string; /** * If defined, this role will be used to deploy lifecycle assets. * Should be assumable by lambda, and have write access * to the domain bucket under the assetPrefix. * Must be specified if an existing domainBucketName is also specified. * Otherwise, a new role will be created with access to the generated * domain bucket. */ readonly assetDeploymentRole: MdaaRoleRef; } export interface SagemakerStudioDomainL3ConstructProps extends MdaaL3ConstructProps { readonly domain: DomainProps; } //This stack creates and manages a SageMaker Studio Domain export class SagemakerStudioDomainL3Construct extends MdaaL3Construct { protected readonly props: SagemakerStudioDomainL3ConstructProps; public readonly kmsKey: IKey; public readonly securityGroup: ISecurityGroup; public readonly domain: MdaaStudioDomain; constructor(scope: Construct, id: string, props: SagemakerStudioDomainL3ConstructProps) { super(scope, id, props); this.props = props; const resolvableDefaultExecutionRole = props.domain.defaultExecutionRole ? this.props.roleHelper.resolveRoleRefWithRefId(props.domain.defaultExecutionRole, 'ex-role') : undefined; const defaultExecutionRole = resolvableDefaultExecutionRole ? MdaaRole.fromRoleArn(this, 'ex-role', resolvableDefaultExecutionRole.arn()) : this.createDefaultExecutionRole(); const resolvableDeploymentRole = props.domain.domainBucket ? this.props.roleHelper.resolveRoleRefWithRefId( props.domain.domainBucket.assetDeploymentRole, `asset-deployment-role`, ) : undefined; const assetDeploymentRole = resolvableDeploymentRole ? MdaaRole.fromRoleArn(this, `asset-deployment-role`, resolvableDeploymentRole.arn()) : this.createAssetDeploymentRole(); this.kmsKey = props.domain.kmsKeyArn ? Key.fromKeyArn(this, 'kmsKey', props.domain.kmsKeyArn) : this.createDomainEfsKmsKey(defaultExecutionRole, assetDeploymentRole); const notebookSharingPrefix = props.domain.notebookSharingPrefix || 'sharing/'; const assetPrefix = props.domain.assetPrefix || 'lifecycle-assets'; const domainBucket: IBucket = props.domain.domainBucket ? Bucket.fromBucketName(this, 'existing-domain-bucket', props.domain.domainBucket.domainBucketName) : this.createDomainBucket( this.kmsKey, defaultExecutionRole, notebookSharingPrefix, assetPrefix, assetDeploymentRole, ); this.securityGroup = props.domain.securityGroupId ? SecurityGroup.fromSecurityGroupId(this, `domain-sg`, props.domain.securityGroupId) : this.createDomainSecurityGroup( this.props.domain.vpcId, this.props.domain.subnetIds, this.props.domain.securityGroupIngress, this.props.domain.securityGroupEgress, ); this.domain = this.createDomain( defaultExecutionRole, this.securityGroup, this.kmsKey, domainBucket, assetPrefix, assetDeploymentRole, props.domain.assetDeploymentMemoryLimitMB, ); this.createBasicExecutionPolicy(this.domain.attrDomainId, defaultExecutionRole, this.kmsKey); this.createSageMakerStudioUserProfiles( defaultExecutionRole, this.kmsKey, domainBucket, this.domain.attrDomainId, notebookSharingPrefix, ); } private createAssetDeploymentRole(): IRole { return new MdaaLambdaRole(this.scope, `asset-deployment-role`, { roleName: 'deployment', naming: this.props.naming, logGroupNames: [`*CustomCDK*`], }); } private createStudioLifecycleConfig( lifecycleConfig: LifecycleScriptProps, lifecycleType: LifecycleConfigAppType, domainBucket: IBucket, assetPrefix: string, assetDeploymentRole: IRole, assetDeploymentMemoryLimitMB?: number, ): MdaaStudioLifecycleConfig { const assetDeployment: AssetDeploymentProps = { scope: this, assetBucket: domainBucket, assetPrefix: assetPrefix, assetDeploymentRole: assetDeploymentRole, memoryLimitMB: assetDeploymentMemoryLimitMB, }; const studioLifecycleConfigProps: MdaaStudioLifecycleConfigProps = { lifecycleConfigName: lifecycleType, lifecycleConfigContent: LifeCycleConfigHelper.createLifecycleConfigContents( lifecycleConfig, lifecycleType, assetDeployment, ), lifecycleConfigAppType: lifecycleType, naming: this.props.naming, }; return new MdaaStudioLifecycleConfig(this, `lifecycle-config-${lifecycleType}`, studioLifecycleConfigProps); } private createDomain( defaultExecutionRole: IRole, securityGroup: ISecurityGroup, kmsKey: IKey, domainBucket: IBucket, assetPrefix: string, assetDeploymentRole: IRole, assetDeploymentMemoryLimitMB?: number, ): MdaaStudioDomain { const jupyterLifecycleConfig = this.props.domain.lifecycleConfigs?.jupyter ? this.createStudioLifecycleConfig( this.props.domain.lifecycleConfigs.jupyter, 'JupyterServer', domainBucket, assetPrefix, assetDeploymentRole, assetDeploymentMemoryLimitMB, ) : undefined; const kernelLifecycleConfig = this.props.domain.lifecycleConfigs?.kernel ? this.createStudioLifecycleConfig( this.props.domain.lifecycleConfigs.kernel, 'KernelGateway', domainBucket, assetPrefix, assetDeploymentRole, assetDeploymentMemoryLimitMB, ) : undefined; if (jupyterLifecycleConfig && kernelLifecycleConfig) { kernelLifecycleConfig.node.addDependency(jupyterLifecycleConfig); } const jupyterServerAppSettings = jupyterLifecycleConfig ? { defaultResourceSpec: { lifecycleConfigArn: jupyterLifecycleConfig.arn, }, lifecycleConfigArns: [jupyterLifecycleConfig.arn], } : undefined; const kernelGatewayAppSettings = kernelLifecycleConfig ? { defaultResourceSpec: { lifecycleConfigArn: kernelLifecycleConfig.arn, }, lifecycleConfigArns: [kernelLifecycleConfig.arn], } : undefined; const defaultUserSettingsWithLifecycle: CfnDomain.UserSettingsProperty = { executionRole: defaultExecutionRole.roleArn, jupyterServerAppSettings: jupyterServerAppSettings, kernelGatewayAppSettings: kernelGatewayAppSettings, }; // nosemgrep const _ = require('lodash'); function customizer(objValue: unknown[], srcValue: unknown): void | unknown[] { if (_.isArray(objValue)) { return objValue.concat(srcValue); } } const defaultUserSettings: CfnDomain.UserSettingsProperty = _.mergeWith( this.props.domain.defaultUserSettings, defaultUserSettingsWithLifecycle, customizer, ); const domainProps = { authMode: this.props.domain.authMode, vpcId: this.props.domain.vpcId, subnetIds: this.props.domain.subnetIds, kmsKeyId: kmsKey.keyId, defaultUserSettings: defaultUserSettings, naming: this.props.naming, securityGroupId: securityGroup.securityGroupId, executionRole: defaultExecutionRole, }; const domain = new MdaaStudioDomain(this, 'domain', domainProps); if (jupyterLifecycleConfig) domain.node.addDependency(jupyterLifecycleConfig); if (kernelLifecycleConfig) domain.node.addDependency(kernelLifecycleConfig); return domain; } private createDefaultExecutionRole(): Role { // Create a default ExecutionRole. This role will be used by users logging into SageMaker Studio // in order to initialize their basic environment. This role has no access to data. const defaultExecutionRole = new MdaaRole(this, 'default-execution-role', { assumedBy: new ServicePrincipal('sagemaker.amazonaws.com'), roleName: 'default-execution-role', naming: this.props.naming, }); defaultExecutionRole.assumeRolePolicy?.addStatements( new PolicyStatement({ actions: ['sts:SetSourceIdentity'], principals: [new ServicePrincipal('sagemaker.amazonaws.com')], effect: Effect.ALLOW, }), ); return defaultExecutionRole; } private createBasicExecutionPolicy(domainId: string, executionRole: IRole, kmsKey: IKey): IMdaaManagedPolicy { const basicExecutionPolicy = new ManagedPolicy(this, 'basic-execution-policy', { managedPolicyName: this.props.naming.resourceName('basic-execution'), roles: [executionRole], }); const kmsUsageStatement = new PolicyStatement({ effect: Effect.ALLOW, resources: [kmsKey.keyArn], actions: [ ...DECRYPT_ACTIONS, ...ENCRYPT_ACTIONS, 'kms:GenerateDataKeyWithoutPlaintext', 'kms:CreateGrant', 'kms:DescribeKey', 'kms:ListAliases', ], }); basicExecutionPolicy.addStatements(kmsUsageStatement); //Allow ExecutionRole creation of SageMaker Studio apps and spaces const studioAppStatement = new PolicyStatement({ effect: Effect.ALLOW, resources: [ `arn:${this.partition}:sagemaker:${this.region}:${this.account}:app/${domainId}/*`, `arn:${this.partition}:sagemaker:${this.region}:${this.account}:space/${domainId}/*`, ], actions: [ 'sagemaker:CreateApp', 'sagemaker:DeleteApp', 'sagemaker:DescribeApp', 'sagemaker:CreateSpace', 'sagemaker:UpdateSpace', 'sagemaker:DeleteSpace', 'sagemaker:DescribeSpace', ], }); basicExecutionPolicy.addStatements(studioAppStatement); //Allow ExecutionRole to Describe the SageMaker Domain const studioDescribeDomainStatement = new PolicyStatement({ effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:domain/${domainId}`], actions: ['sagemaker:DescribeDomain'], }); basicExecutionPolicy.addStatements(studioDescribeDomainStatement); //Allow ExecutionRole list SageMaker Studio Lifecycle Configs const studioLifecycleListStatement = new PolicyStatement({ effect: Effect.ALLOW, resources: [`*`], actions: ['sagemaker:ListStudioLifecycleConfigs'], }); basicExecutionPolicy.addStatements(studioLifecycleListStatement); //Allow ExecutionRole describe SageMaker Studio Lifecycle Configs const studioLifecycleDescStatement = new PolicyStatement({ effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:studio-lifecycle-config/*`], actions: ['sagemaker:DescribeStudioLifecycleConfig'], }); basicExecutionPolicy.addStatements(studioLifecycleDescStatement); //Allow ExecutionRole to write studio cloudwatch logs const studioCloudwatchGroupStatement = new PolicyStatement({ effect: Effect.ALLOW, actions: ['logs:CreateLogGroup', 'logs:DescribeLogGroups', 'logs:DescribeLogStreams'], resources: [`arn:${this.partition}:logs:${this.region}:${this.account}:log-group:/aws/sagemaker/studio`], }); basicExecutionPolicy.addStatements(studioCloudwatchGroupStatement); //Allow ExecutionRole to write studio cloudwatch logs const studioCloudwatchStreamStatement = new PolicyStatement({ effect: Effect.ALLOW, actions: ['logs:CreateLogStream', 'logs:PutLogEvents'], resources: [ `arn:${this.partition}:logs:${this.region}:${this.account}:log-group:/aws/sagemaker/studio:log-stream:*`, ], }); basicExecutionPolicy.addStatements(studioCloudwatchStreamStatement); MdaaNagSuppressions.addCodeResourceSuppressions(basicExecutionPolicy, [ { id: 'AwsSolutions-IAM5', reason: 'User Profile and App names not known at deployment time. ListStudioLifecycleConfigs does not take resource. LogStream names not know at deployment time.', }, ]); return basicExecutionPolicy; } private createDomainEfsKmsKey(executionRole: IRole, deploymentRole: IRole): IKey { const efsKmsKey = new MdaaKmsKey(this, 'efs-key', { alias: 'efs', naming: this.props.naming, }); const keyUsageStatement = new PolicyStatement({ actions: [...ENCRYPT_ACTIONS, ...DECRYPT_ACTIONS], principals: [executionRole, deploymentRole], effect: Effect.ALLOW, }); efsKmsKey.addToResourcePolicy(keyUsageStatement); return efsKmsKey; } private createDomainSecurityGroup( vpcId: string, subnetIds: string[], securityGroupIngress?: MdaaSecurityGroupRuleProps, securityGroupEgress?: MdaaSecurityGroupRuleProps, ): SecurityGroup { // Import vpc const vpc = Vpc.fromVpcAttributes(this.scope, `vpc`, { vpcId: vpcId, availabilityZones: ['dummy'], privateSubnetIds: subnetIds, }); const customEgress: boolean = (securityGroupEgress?.ipv4 && securityGroupEgress?.ipv4.length > 0) || (securityGroupEgress?.prefixList && securityGroupEgress?.prefixList.length > 0) || (securityGroupEgress?.sg && securityGroupEgress?.sg.length > 0) || false; const securityGroupProps: MdaaSecurityGroupProps = { securityGroupName: this.props.naming.resourceName(), vpc: vpc, allowAllOutbound: !customEgress, naming: this.props.naming, ingressRules: securityGroupIngress, egressRules: securityGroupEgress, addSelfReferenceRule: true, // Required for intercontainer traffic and EFS }; // Create security group return new MdaaSecurityGroup(this.scope, `security-group`, securityGroupProps); } private createSageMakerStudioUserProfiles( executionRole: IRole, kmsKey: IKey, domainBucket: IBucket, studioDomainId: string, notebookSharingPrefix: string, ) { Object.entries(this.props.domain.userProfiles || {}).forEach(userProfileEntry => { const userid = userProfileEntry[0]; const userProfileProps = userProfileEntry[1]; const profileTags: CfnTag[] = []; if (this.props.domain.authMode == 'IAM') { if (!userProfileProps.userRole) { throw new Error("'userRole' must be defined on user profile when domain is in IAM authMode"); } else { const resolvedRole = this.props.roleHelper.resolveRoleRefWithRefId(userProfileProps.userRole, userid); const tag = { key: 'userid', value: `${resolvedRole.id()}:${userid}`, }; profileTags.push(tag); } } new CfnUserProfile(this, `user-profile-${userid}`, { domainId: studioDomainId, userProfileName: userid.replace(/\W/g, '-'), singleSignOnUserValue: this.props.domain.authMode == 'SSO' ? userid : undefined, singleSignOnUserIdentifier: this.props.domain.authMode == 'SSO' ? 'UserName' : undefined, userSettings: { executionRole: executionRole.roleArn, sharingSettings: { notebookOutputOption: 'Allowed', s3KmsKeyId: kmsKey.keyId, s3OutputPath: domainBucket.s3UrlForObject(notebookSharingPrefix), }, }, tags: profileTags, }); }); } private createDomainBucket( domainKmsKey: IMdaaKmsKey, defaultExecutionRole: IRole, notebookSharingPrefix: string, assetPrefix: string, assetDeploymentRole: IRole, ): MdaaBucket { if (!this.props.domain.dataAdminRoles) { throw new Error('dataAdminRoles must be defined if creating a Notebook Sharing Bucket'); } const dataAdminRoleIds = this.props.roleHelper .resolveRoleRefsWithOrdinals(this.props.domain.dataAdminRoles, 'DataAdmin') .map(x => x.id()); const domainBucket = new MdaaBucket(this.scope, `Bucketsharing`, { encryptionKey: domainKmsKey, naming: this.props.naming, }); MdaaNagSuppressions.addCodeResourceSuppressions( domainBucket, [ { id: 'NIST.800.53.R5-S3BucketReplicationEnabled', reason: 'MDAA does not use bucket replication.' }, { id: 'HIPAA.Security-S3BucketReplicationEnabled', reason: 'MDAA does not use bucket replication.' }, { id: 'PCI.DSS.321-S3BucketReplicationEnabled', reason: 'MDAA does not use bucket replication.' }, ], true, ); //Allow data admins to manage the bucket const rootPolicy = new RestrictObjectPrefixToRoles({ s3Bucket: domainBucket, s3Prefix: '/', readWriteSuperRoleIds: dataAdminRoleIds, }); rootPolicy.statements().forEach(statement => domainBucket.addToResourcePolicy(statement)); //Allow athena users to use the bucket const notebooksPolicy = new RestrictObjectPrefixToRoles({ s3Bucket: domainBucket, s3Prefix: notebookSharingPrefix, readWritePrincipals: [defaultExecutionRole], }); notebooksPolicy.statements().forEach(statement => domainBucket.addToResourcePolicy(statement)); //Allow athena users to use the bucket const assetsPolicy = new RestrictObjectPrefixToRoles({ s3Bucket: domainBucket, s3Prefix: assetPrefix, readWritePrincipals: [assetDeploymentRole], readPrincipals: [defaultExecutionRole], }); assetsPolicy.statements().forEach(statement => domainBucket.addToResourcePolicy(statement)); //Default Deny Policy //Any role not specified in config is explicitely denied access to the bucket const bucketRestrictPolicy = new RestrictBucketToRoles({ s3Bucket: domainBucket, roleExcludeIds: [...dataAdminRoleIds], principalExcludes: [defaultExecutionRole.roleArn, assetDeploymentRole.roleArn], }); domainBucket.addToResourcePolicy(bucketRestrictPolicy.denyStatement); domainBucket.addToResourcePolicy(bucketRestrictPolicy.allowStatement); const allowBucketListingStatement = new PolicyStatement({ effect: Effect.ALLOW, principals: [assetDeploymentRole, defaultExecutionRole], actions: ['s3:List*', 's3:GetBucket*'], resources: [domainBucket.bucketArn], }); domainBucket.addToResourcePolicy(allowBucketListingStatement); return domainBucket; } }