packages/constructs/L3/ai/datascience-team-l3-construct/lib/datascience-team-l3-construct.ts (982 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { AthenaWorkgroupL3Construct, AthenaWorkgroupL3ConstructProps } from '@aws-mdaa/athena-workgroup-l3-construct'; import { AccessPolicyProps, BucketDefinition, DataLakeL3ConstructProps, InventoryDefinition, S3DatalakeBucketL3Construct, } from '@aws-mdaa/datalake-l3-construct'; import { MdaaManagedPolicy } from '@aws-mdaa/iam-constructs'; import { MdaaRoleRef } from '@aws-mdaa/iam-role-helper'; import { MdaaL3Construct, MdaaL3ConstructProps } from '@aws-mdaa/l3-construct'; import { MdaaLambdaRole } from '@aws-mdaa/lambda-constructs'; import { DomainProps, SagemakerStudioDomainL3Construct, SagemakerStudioDomainL3ConstructProps, } from '@aws-mdaa/sm-studio-domain-l3-construct'; import { Effect, IRole, ManagedPolicy, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam'; import { IKey } from 'aws-cdk-lib/aws-kms'; import { IBucket } from 'aws-cdk-lib/aws-s3'; import { MdaaNagSuppressions } from '@aws-mdaa/construct'; //NOSONAR import { Construct } from 'constructs'; export const CENTRAL_SM_REPO_ACCT = '341280168497'; export interface DataScienceTeamProps { /** * List of admin roles which will be provided access to team resources (like KMS/Bucket) */ readonly dataAdminRoles: MdaaRoleRef[]; /** * List of admin roles which will be provided access to team resources (like KMS/Bucket) */ readonly teamUserRoles?: MdaaRoleRef[]; /** * Reference to role which will be used as execution role on all team SageMaker resources. * The role must have assume role trust with sagemaker.amazonaws.com. Additional managed polies * may be added to the role to grant it access to team resources and relevant services. */ readonly teamExecutionRole: MdaaRoleRef; /** * List of inventory configurations to be applied to the bucket */ readonly inventories?: { [key: string]: InventoryDefinition }; /** * If defined, a studio domain will be created for the team */ readonly studioDomainConfig?: DomainProps; /** * If specified, policy names will be created using this prefix instead of using the naming module. * This is useful when policy names need to be portable across accounts (such as for integration with SSO permission sets) */ readonly verbatimPolicyNamePrefix?: string; } export interface DataScienceTeamL3ConstructProps extends MdaaL3ConstructProps { readonly team: DataScienceTeamProps; } //This stack creates all of the resources required for a Data Science Team //to use SageMaker Studio on top of a Data Lake export class DataScienceTeamL3Construct extends MdaaL3Construct { protected readonly props: DataScienceTeamL3ConstructProps; constructor(scope: Construct, id: string, props: DataScienceTeamL3ConstructProps) { super(scope, id, props); this.props = props; const teamExecutionRoleResolved = props.roleHelper.resolveRoleRefWithRefId( this.props.team.teamExecutionRole, 'team-execution-role', ); const teamExecutionRole = Role.fromRoleArn(this, 'team-execution-role', teamExecutionRoleResolved.arn()); const teamAssetsDeploymentRole = this.createAssetDeploymentRole(); const minilakeL3Construct = this.createMiniLakeL3Construct(teamExecutionRole, teamAssetsDeploymentRole); const teamKmsKey = minilakeL3Construct.kmsKey; const teamBucket = minilakeL3Construct.buckets['projects']; if (props.team.studioDomainConfig) { const domain = this.createTeamStudioDomain( props.team.studioDomainConfig, teamExecutionRole, teamKmsKey, teamBucket, teamAssetsDeploymentRole, ); if (teamBucket.policy) domain.node.addDependency(teamBucket.policy); } this.createAthenaWorkgroup(teamExecutionRole, teamKmsKey, teamBucket); const resolvedMutableTeamUserRoles = this.props.roleHelper .resolveRoleRefsWithOrdinals(this.props.team.teamUserRoles || [], 'TeamUser') .filter(x => !x.immutable()) .map(x => Role.fromRoleArn(this, x.refId(), x.arn())); const teamPolicy = this.createTeamPolicy(teamExecutionRole, teamBucket); teamPolicy.attachToRole(teamExecutionRole); resolvedMutableTeamUserRoles.forEach(x => teamPolicy.attachToRole(x)); const sagemakerReadPolicy = this.createSageMakerReadPolicy(); sagemakerReadPolicy.attachToRole(teamExecutionRole); resolvedMutableTeamUserRoles.forEach(x => sagemakerReadPolicy.attachToRole(x)); const sagemakerWritePolicies = this.createSageMakerWritePolicies( teamBucket, minilakeL3Construct.kmsKey, teamExecutionRole, ); sagemakerWritePolicies.forEach(pol => pol.attachToRole(teamExecutionRole)); resolvedMutableTeamUserRoles.forEach(role => sagemakerWritePolicies.forEach(pol => pol.attachToRole(role))); const sagemakerGuardrailManagedPolicy = this.createSageMakerGuardrailPolicy(minilakeL3Construct.kmsKey); sagemakerGuardrailManagedPolicy.attachToRole(teamExecutionRole); resolvedMutableTeamUserRoles.forEach(x => sagemakerGuardrailManagedPolicy.attachToRole(x)); return this; } private createAssetDeploymentRole(): Role { return new MdaaLambdaRole(this.scope, `asset-deployment-role`, { roleName: 'deployment', naming: this.props.naming, logGroupNames: [`*CustomCDK*`], }); } private createAthenaWorkgroup(teamExecutionRole: IRole, teamKmsKey: IKey, teamBucket: IBucket) { const workgroupL3ConstructProps: AthenaWorkgroupL3ConstructProps = { ...(this.props as MdaaL3ConstructProps), ...{ naming: this.props.naming.withModuleName(this.props.naming.props.moduleName + '-athena'), dataAdminRoles: this.props.team.dataAdminRoles, athenaUserRoles: [ ...[ { arn: teamExecutionRole.roleArn, }, ], ...(this.props.team.teamUserRoles || []), ], workgroupBucketName: teamBucket.bucketName, workgroupKmsKeyArn: teamKmsKey.keyArn, verbatimPolicyNamePrefix: this.props.team.verbatimPolicyNamePrefix ? this.props.team.verbatimPolicyNamePrefix + '-athena' : undefined, }, }; return new AthenaWorkgroupL3Construct(this, 'athena', workgroupL3ConstructProps); } private createTeamStudioDomain( studioDomainConfig: DomainProps, teamExecutionRole: IRole, teamKmsKey: IKey, teamBucket: IBucket, teamAssetsDeploymentRole: IRole, ): SagemakerStudioDomainL3Construct { const overrideDomainProps: DomainProps = { ...studioDomainConfig, defaultExecutionRole: { refId: 'ex-role', arn: teamExecutionRole.roleArn, name: teamExecutionRole.roleName, }, dataAdminRoles: this.props.team.dataAdminRoles, kmsKeyArn: teamKmsKey.keyArn, domainBucket: { domainBucketName: teamBucket.bucketName, assetDeploymentRole: { refId: 'deployment-role', arn: teamAssetsDeploymentRole.roleArn, name: teamAssetsDeploymentRole.roleName, }, }, assetPrefix: 'sagemaker-lifecycle-assets/studio', }; const studioDomainL3ConstructProps: SagemakerStudioDomainL3ConstructProps = { ...(this.props as MdaaL3ConstructProps), ...{ domain: overrideDomainProps }, }; return new SagemakerStudioDomainL3Construct(this, 'studio', studioDomainL3ConstructProps); } private createMiniLakeL3Construct(teamExecutionRole: IRole, teamAssetsDeploymentRole: Role) { const teamExecutionRoleRef = { arn: teamExecutionRole.roleArn, }; const teamAssetsDeploymentRoleRef = { arn: teamAssetsDeploymentRole.roleArn, // id: teamAssetsDeploymentRole.roleId }; const miniLakeAccessPolicies: AccessPolicyProps[] = [ { name: 'DataAdminRootAccess', s3Prefix: '/', readWriteSuperRoleRefs: this.props.team.dataAdminRoles, }, { name: 'TeamSageMakerAccess', s3Prefix: '/sagemaker/', readWriteRoleRefs: [teamExecutionRoleRef, ...(this.props.team.teamUserRoles || [])], }, { name: 'TeamLifecycleAssetsAccess', s3Prefix: '/sagemaker-lifecycle-assets/', readRoleRefs: [teamExecutionRoleRef, ...(this.props.team.teamUserRoles || [])], readWriteRoleRefs: [teamAssetsDeploymentRoleRef], }, { name: 'TeamProjectsAccess', s3Prefix: '/projects/', readWriteRoleRefs: [teamExecutionRoleRef, ...(this.props.team.teamUserRoles || [])], }, { name: 'TeamAthenaResultsAccess', s3Prefix: '/athena-results', readWriteRoleRefs: [teamExecutionRoleRef, ...(this.props.team.teamUserRoles || [])], }, ]; const minilakeBucketProps: BucketDefinition = { bucketZone: 'projects', inventories: this.props.team.inventories, accessPolicies: miniLakeAccessPolicies, }; const minilakeL3ConstructProps: DataLakeL3ConstructProps = { ...(this.props as MdaaL3ConstructProps), ...{ naming: this.props.naming.withModuleName(this.props.naming.props.moduleName + '-minilake'), buckets: [minilakeBucketProps], }, }; return new S3DatalakeBucketL3Construct(this, 'minilake', minilakeL3ConstructProps); } private createTeamPolicy(teamExecutionRole: IRole, teamBucket: IBucket): ManagedPolicy { const teamManagedPolicy = new MdaaManagedPolicy(this, 'team-managed-pol', { managedPolicyName: this.props.team.verbatimPolicyNamePrefix ? this.props.team.verbatimPolicyNamePrefix : undefined, verbatimPolicyName: this.props.team.verbatimPolicyNamePrefix != undefined, naming: this.props.naming, }); //Allow reading of team execution role const teamRoleStatement = new PolicyStatement({ sid: 'TeamRole', effect: Effect.ALLOW, resources: [teamExecutionRole.roleArn], actions: ['iam:GetRole'], }); teamManagedPolicy.addStatements(teamRoleStatement); //Allow smooth interactions with team bucket via Console const teamBucketConsoleStatement = new PolicyStatement({ sid: 'TeamBucketGet', effect: Effect.ALLOW, resources: [teamBucket.bucketArn], actions: [ 's3:GetBucketVersioning', 's3:GetBucketTagging', 's3:GetEncryptionConfiguration', 's3:GetIntelligentTieringConfiguration', 's3:GetBucketPolicy', ], }); teamManagedPolicy.addStatements(teamBucketConsoleStatement); //Allow reading team SSM params const ssmBasePath = this.props.naming.ssmPath('placeholder', false).replace('/placeholder', ''); const teamSSMStatement = new PolicyStatement({ sid: 'TeamSSM', effect: Effect.ALLOW, resources: [`arn:${this.partition}:ssm:${this.region}:${this.account}:parameter${ssmBasePath}/*`], actions: ['ssm:GetParameter', 'ssm:GetParameterHistory', 'ssm:GetParameters', 'ssm:GetParametersByPath'], }); teamManagedPolicy.addStatements(teamSSMStatement); MdaaNagSuppressions.addCodeResourceSuppressions( teamManagedPolicy, [ { id: 'AwsSolutions-IAM5', reason: 'SSM permissions scoped to team SSM params by path prefix', }, ], true, ); return teamManagedPolicy; } private createSageMakerReadPolicy(): ManagedPolicy { const sagemakerReadonlyManagedPolicy = new MdaaManagedPolicy(this, 'read-managed-pol', { managedPolicyName: this.props.team.verbatimPolicyNamePrefix ? this.props.team.verbatimPolicyNamePrefix + '-' + 'sm-read' : 'sm-read', verbatimPolicyName: this.props.team.verbatimPolicyNamePrefix != undefined, naming: this.props.naming, }); const sagemakerSearchStatement = new PolicyStatement({ sid: 'SageMakerSearch', effect: Effect.ALLOW, resources: ['*'], actions: ['sagemaker:Search'], }); sagemakerReadonlyManagedPolicy.addStatements(sagemakerSearchStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerReadonlyManagedPolicy, [ { id: 'AwsSolutions-IAM5', reason: 'SageMaker Search does not take a resource. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: ['Resource::*'], }, ], true, ); // Allow SageMaker readonly permissions const sagemakerListStatement = new PolicyStatement({ sid: 'SageMakerList', effect: Effect.ALLOW, resources: ['*'], actions: ['sagemaker:List*'], }); sagemakerReadonlyManagedPolicy.addStatements(sagemakerListStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerReadonlyManagedPolicy, [ { id: 'AwsSolutions-IAM5', reason: 'SageMaker List does not take a resource. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: ['Resource::*', 'Action::sagemaker:List*'], }, ], true, ); // Allow SageMaker readonly permissions const sagemakerDescribeGetStatement = new PolicyStatement({ sid: 'SageMakerDescribeGet', effect: Effect.ALLOW, resources: ['*'], actions: ['sagemaker:Describe*', 'sagemaker:BatchDescribe*', 'sagemaker:Get*', 'sagemaker:BatchGet*'], }); sagemakerReadonlyManagedPolicy.addStatements(sagemakerDescribeGetStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerReadonlyManagedPolicy, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for Describe and Get not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [ 'Resource::*', 'Action::sagemaker:Describe*', 'Action::sagemaker:Get*', 'Action::sagemaker:BatchDescribe*', 'Action::sagemaker:BatchGet*', ], }, ], true, ); //Allow reading of log streams for SageMaker const cloudwatchStatement = new PolicyStatement({ sid: 'CloudWatchSageMaker', effect: Effect.ALLOW, resources: [`arn:${this.partition}:logs:${this.region}:${this.account}:log-group:*sagemaker*`], actions: ['logs:GetLogEvents', 'logs:DescribeLogGroups', 'logs:DescribeLogStreams', 'logs:FilterLogEvents'], }); sagemakerReadonlyManagedPolicy.addStatements(cloudwatchStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerReadonlyManagedPolicy, [ { id: 'AwsSolutions-IAM5', reason: 'Log Group and Stream names not known at deployment time.', appliesTo: [`Resource::arn:${this.partition}:logs:${this.region}:${this.account}:log-group:*sagemaker*`], }, ], true, ); return sagemakerReadonlyManagedPolicy; } private createSageMakerWritePolicies(teamBucket: IBucket, teamKey: IKey, teamExecutionRole: IRole): ManagedPolicy[] { //We use two write policies in order to avoid policy length limits within IAM const sagemakerWriteManagedPolicy1 = new MdaaManagedPolicy(this, 'ex-write-managed-pol', { managedPolicyName: this.props.team.verbatimPolicyNamePrefix ? this.props.team.verbatimPolicyNamePrefix + '-' + 'sm-write' : 'sm-write', verbatimPolicyName: this.props.team.verbatimPolicyNamePrefix != undefined, naming: this.props.naming, }); //We use two write policies in order to avoid policy length limits within IAM //New statements should be added const sagemakerWriteManagedPolicy2 = new MdaaManagedPolicy(this, 'sm-write-managed-pol2', { managedPolicyName: this.props.team.verbatimPolicyNamePrefix ? this.props.team.verbatimPolicyNamePrefix + '-' + 'sm-write-2' : 'sm-write-2', verbatimPolicyName: this.props.team.verbatimPolicyNamePrefix != undefined, naming: this.props.naming, }); //Allow passing of team execution role to sagemaker jobs, etc const teamRoleStatement = new PolicyStatement({ sid: 'TeamRole', effect: Effect.ALLOW, resources: [teamExecutionRole.roleArn], actions: ['iam:PassRole'], }); sagemakerWriteManagedPolicy1.addStatements(teamRoleStatement); //Allow SageMaker permissions required to be used as execution role for Jobs const sagemakerJobStatement = new PolicyStatement({ sid: 'CreateandManageJobs', effect: Effect.ALLOW, resources: [ `arn:${this.partition}:sagemaker:${this.region}:${this.account}:*job/*`, `arn:${this.partition}:sagemaker:${this.region}:${this.account}:*job-definition/*`, ], actions: [ 'sagemaker:Create*Job', 'sagemaker:Create*JobDefinition', 'sagemaker:Delete*JobDefinition', 'sagemaker:Update*Job', 'sagemaker:Stop*Job', ], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerJobStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for Jobs not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [ `Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:*job/*`, `Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:*job-definition/*`, ], }, ], true, ); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Actions scoped for job management permissions, taking into account policy length limits. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [ `Action::sagemaker:Create*Job`, `Action::sagemaker:Create*JobDefinition`, `Action::sagemaker:Delete*JobDefinition`, `Action::sagemaker:Delete*Job`, `Action::sagemaker:Update*Job`, `Action::sagemaker:Stop*Job`, ], }, ], true, ); //Allow SageMaker permissions required to be used as execution role for Model Monitoring Schedules const sagemakerModelMonitoringStatement = new PolicyStatement({ sid: 'CreateandManageModelMonitoring', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:monitoring-schedule/*`], actions: [ 'sagemaker:CreateMonitoringSchedule', 'sagemaker:UpdateMonitoringSchedule', 'sagemaker:DeleteMonitoringSchedule', ], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerModelMonitoringStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for monitoring schedules not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:monitoring-schedule/*`], }, ], true, ); //Allow SageMaker permissions required to create and manage model cards const sagemakerModelCardStatement = new PolicyStatement({ sid: 'CreateandManageModelCards', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:model-card/*`], actions: ['sagemaker:CreateModelCard', 'sagemaker:DeleteModelCard', 'sagemaker:UpdateModelCard'], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerModelCardStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for model cards not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:model-card/*`], }, ], true, ); //Allow SageMaker permissions required to be used as execution role for Pipelines const sagemakerPipelineStatement = new PolicyStatement({ sid: 'CreateandManagePipelines', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:pipeline/*`], actions: [ 'sagemaker:CreatePipeline', 'sagemaker:DeletePipeline', 'sagemaker:RetryPipelineExecution', 'sagemaker:StartPipelineExecution', 'sagemaker:StopPipelineExecution', 'sagemaker:SendPipelineExecutionStepSuccess', 'sagemaker:SendPipelineExecutionStepFailure', 'sagemaker:UpdatePipeline', 'sagemaker:UpdatePipelineExecution', ], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerPipelineStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for Pipelines not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:pipeline/*`], }, ], true, ); //Allow SageMaker permissions required to create and manage models const sagemakerModelStatement = new PolicyStatement({ sid: 'CreateAndManageModels', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:model/*`], actions: ['sagemaker:CreateModel', 'sagemaker:DeleteModel'], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerModelStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for Models not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:model/*`], }, ], true, ); //Allow SageMaker permissions required to create and manage models const sagemakerModelPackageStatement = new PolicyStatement({ sid: 'CreateAndManageModelPackages', effect: Effect.ALLOW, resources: [ `arn:${this.partition}:sagemaker:${this.region}:${this.account}:model-package/*`, `arn:${this.partition}:sagemaker:${this.region}:${this.account}:model-package-group/*`, ], actions: [ 'sagemaker:CreateModelPackage', 'sagemaker:DeleteModelPackage', 'sagemaker:UpdateModelPackage', 'sagemaker:BatchDescribeModelPackage', 'sagemaker:CreateModelPackageGroup', 'sagemaker:DeleteModelPackageGroup', 'sagemaker:DeleteModelPackageGroupPolicy', 'sagemaker:PutModelPackageGroupPolicy', ], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerModelPackageStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for model packages not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [ `Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:model-package/*`, `Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:model-package-group/*`, ], }, ], true, ); //Allow SageMaker permissions required to create and manage projects const sagemakerProjectStatement = new PolicyStatement({ sid: 'CreateAndManageProjects', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:project/*`], actions: ['sagemaker:CreateProject', 'sagemaker:DeleteProject', 'sagemaker:UpdateProject'], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerProjectStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for Projects not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:project/*`], }, ], true, ); //Allow usage of Team KMS key for creating notebook instances const sagemakerKmsStatement = new PolicyStatement({ sid: 'SageMakerKmsAccess', effect: Effect.ALLOW, resources: [teamKey.keyArn], actions: ['kms:CreateGrant'], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerKmsStatement); //Allow SageMaker permissions required to create and manage endpoints const sagemakerEndpointStatement = new PolicyStatement({ sid: 'CreateAndManageEndpoints', effect: Effect.ALLOW, resources: [ `arn:${this.partition}:sagemaker:${this.region}:${this.account}:endpoint/*`, `arn:${this.partition}:sagemaker:${this.region}:${this.account}:endpoint-config/*`, ], actions: [ 'sagemaker:CreateEndpoint', 'sagemaker:DeleteEndpoint', 'sagemaker:UpdateEndpoint', 'sagemaker:UpdateEndpointWeightsAndCapacities', 'sagemaker:CreateEndpointConfig', 'sagemaker:DeleteEndpointConfig', 'sagemaker:InvokeEndpoint', 'sagemaker:InvokeEndpointAsync', ], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerEndpointStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for endpoints are not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [ `Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:endpoint/*`, `Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:endpoint-config/*`, ], }, ], true, ); //Allow SageMaker permissions required to create and manage trials const sagemakerTrialStatement = new PolicyStatement({ sid: 'CreateAndManageTrials', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:experiment*`], actions: [ 'sagemaker:CreateTrial', 'sagemaker:CreateTrialComponent', 'sagemaker:AssociateTrialComponent', 'sagemaker:DisassociateTrialComponent', 'sagemaker:DeleteTrial', 'sagemaker:DeleteTrialComponent', 'sagemaker:UpdateTrial', 'sagemaker:UpdateTrialComponent', ], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerTrialStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for experiments are not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:experiment*`], }, ], true, ); //Allow SageMaker permissions required to create and manage notebooks const sagemakerNotebookStatement = new PolicyStatement({ sid: 'CreateAndManageNotebooks', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:notebook-instance/*`], actions: [ 'sagemaker:CreateNotebookInstance', 'sagemaker:UpdateNotebookInstance', 'sagemaker:DeleteNotebookInstance', 'sagemaker:StartNotebookInstance', 'sagemaker:StopNotebookInstance', 'sagemaker:CreatePresignedNotebookInstanceUrl', ], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerNotebookStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for Notebook Instances not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:notebook-instance/*`], }, ], true, ); //Allow SageMaker permissions required to create and manage notebooks const sagemakerNotebookLifecycleStatement = new PolicyStatement({ sid: 'CreateAndManageNotebookLifecycles', effect: Effect.ALLOW, resources: [ `arn:${this.partition}:sagemaker:${this.region}:${this.account}:notebook-instance-lifecycle-config/*`, ], actions: [ 'sagemaker:CreateNotebookInstanceLifecycleConfig', 'sagemaker:UpdateNotebookInstanceLifecycleConfig', 'sagemaker:DeleteNotebookInstanceLifecycleConfig', ], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerNotebookLifecycleStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for Notebook Lifecycle Configs not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [ `Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:notebook-instance-lifecycle-config/*`, ], }, ], true, ); // Allow access to put records to feature groups const sagemakerPutRecordFeatureGroupStatement = new PolicyStatement({ sid: 'PutRecordFeatureGroups', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:feature-group/*`], actions: ['sagemaker:PutRecord', 'sagemaker:DeleteRecord'], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerPutRecordFeatureGroupStatement); //Allow SageMaker permissions required to create online feature-groups //Must be encrypted with team key const sagemakerCreateOnlineFeatureGroupStatement = new PolicyStatement({ sid: 'CreateOnlineFeatureGroups', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:feature-group/*`], actions: ['sagemaker:CreateFeatureGroup'], conditions: { StringEquals: { 'sagemaker:FeatureGroupOnlineStoreKmsKey': teamKey.keyArn, }, }, }); sagemakerWriteManagedPolicy1.addStatements(sagemakerCreateOnlineFeatureGroupStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for FeatureGroups not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:feature-group/*`], }, ], true, ); //Allow SageMaker permissions required to create offline feature-groups //Offline storage location must be team bucket, and must be encrypted with team key const sagemakerCreateOfflineFeatureGroupStatement = new PolicyStatement({ sid: 'CreateOfflineFeatureGroups', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:feature-group/*`], actions: ['sagemaker:CreateFeatureGroup'], conditions: { StringEquals: { 'sagemaker:FeatureGroupOfflineStoreKmsKey': teamKey.keyArn, }, StringLike: { 'sagemaker:FeatureGroupOfflineStoreS3Uri': teamBucket.arnForObjects('*'), }, }, }); sagemakerWriteManagedPolicy1.addStatements(sagemakerCreateOfflineFeatureGroupStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for FeatureGroups not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:feature-group/*`], }, ], true, ); //Allow SageMaker permissions required to manage feature-groups const sagemakerManageFeatureGroupStatement = new PolicyStatement({ sid: 'ManageFeatureGroups', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:feature-group/*`], actions: ['sagemaker:DeleteFeatureGroup', 'sagemaker:UpdateFeatureGroup', 'sagemaker:UpdateFeatureMetadata'], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerManageFeatureGroupStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for FeatureGroups not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:feature-group/*`], }, ], true, ); //Allow SageMaker permissions required to create and manage Experiments const sagemakerExperimentStatement = new PolicyStatement({ sid: 'CreateAndManageExperiments', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:experiment/*`], actions: ['sagemaker:CreateExperiment', 'sagemaker:DeleteExperiment', 'sagemaker:UpdateExperiment'], }); sagemakerWriteManagedPolicy1.addStatements(sagemakerExperimentStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy1, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for Experiments not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:experiment/*`], }, ], true, ); //Allow SageMaker permissions required to add and remove tags (important for SageMaker Clarify, among others) const sagemakerAddDeleteTagsStatement = new PolicyStatement({ sid: 'AddDeleteTagsSageMaker', effect: Effect.ALLOW, resources: [`arn:${this.partition}:sagemaker:${this.region}:${this.account}:*`], actions: ['sagemaker:AddTags', 'sagemaker:DeleteTags'], }); sagemakerWriteManagedPolicy2.addStatements(sagemakerAddDeleteTagsStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy2, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for tagged SageMaker resources are not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonsagemaker.html', appliesTo: [`Resource::arn:${this.partition}:sagemaker:${this.region}:${this.account}:*`], }, ], true, ); //Allow EC2 permissions required to be used as execution role for VPC Bound Jobs/Pipelines const sagemakerEc2Statement = new PolicyStatement({ sid: 'CreateEC2NetworkInterfaces', effect: Effect.ALLOW, resources: [`*`], actions: [ 'ec2:CreateNetworkInterface', 'ec2:CreateNetworkInterfacePermission', 'ec2:CreateVpcEndpoint', 'ec2:DeleteNetworkInterface', 'ec2:DeleteNetworkInterfacePermission', 'ec2:DescribeNetworkInterfaces', 'ec2:DescribeSecurityGroups', 'ec2:DescribeSubnets', 'ec2:DescribeVpcs', 'ec2:DescribeDhcpOptions', 'ec2:DescribeVpcEndpoints', ], }); sagemakerWriteManagedPolicy2.addStatements(sagemakerEc2Statement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy2, [ { id: 'AwsSolutions-IAM5', reason: 'Network Interface ID not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonec2.html#amazonec2-network-interface', appliesTo: ['Resource::*'], }, ], true, ); //Allow SageMaker ECR permissions to pull SageMaker images from the central SageMaker repository const sagemakerEcrStatement = new PolicyStatement({ sid: 'SageMakerECRReadonly', effect: Effect.ALLOW, resources: [`arn:${this.partition}:ecr:${this.region}:${CENTRAL_SM_REPO_ACCT}:repository/sagemaker*`], actions: ['ecr:ListImages', 'ecr:DescribeImages', 'ecr:DescribeRepositories', 'ecr:GetDownloadUrlForLayer'], }); sagemakerWriteManagedPolicy2.addStatements(sagemakerEcrStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy2, [ { id: 'AwsSolutions-IAM5', reason: 'Resource names for SageMaker ECR not known at deployment time. https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonelasticcontainerregistry.html', appliesTo: [ `Resource::arn:${this.partition}:ecr:${this.region}:${CENTRAL_SM_REPO_ACCT}:repository/sagemaker*`, ], }, ], true, ); //Allow creation of log groups and log streams for SageMaker const cloudwatchStatement = new PolicyStatement({ sid: 'CloudWatchSageMaker', effect: Effect.ALLOW, resources: [`arn:${this.partition}:logs:${this.region}:${this.account}:log-group:*sagemaker*`], actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], }); sagemakerWriteManagedPolicy2.addStatements(cloudwatchStatement); MdaaNagSuppressions.addCodeResourceSuppressions( sagemakerWriteManagedPolicy2, [ { id: 'AwsSolutions-IAM5', reason: 'Log Group and Stream names not known at deployment time.', appliesTo: [`Resource::arn:${this.partition}:logs:${this.region}:${this.account}:log-group:*sagemaker*`], }, ], true, ); sagemakerWriteManagedPolicy1.checkPolicyLength(true); sagemakerWriteManagedPolicy2.checkPolicyLength(true); return [sagemakerWriteManagedPolicy1, sagemakerWriteManagedPolicy2]; } private createSageMakerGuardrailPolicy(teamKey: IKey): ManagedPolicy { const sagemakerGuardrailManagedPolicy = new MdaaManagedPolicy(this, 'sm-guardrail-managed-pol', { managedPolicyName: this.props.team.verbatimPolicyNamePrefix ? this.props.team.verbatimPolicyNamePrefix + '-' + 'sm-guardrail' : 'sm-guardrail', verbatimPolicyName: this.props.team.verbatimPolicyNamePrefix != undefined, naming: this.props.naming, }); //Enforces use of Team KMS key for SageMaker Volumes const sagemakerForceVolumeKmsKeyStatement = new PolicyStatement({ sid: 'forceVolumeKmsKey', effect: Effect.DENY, resources: ['*'], actions: [ 'sagemaker:CreateEndpointConfig', 'sagemaker:CreateMonitoringSchedule', 'sagemaker:UpdateMonitoringSchedule', 'sagemaker:CreateNotebookInstance', 'sagemaker:Create*Job*', ], conditions: { StringNotEquals: { 'sagemaker:VolumeKmsKey': teamKey.keyArn, }, }, }); sagemakerGuardrailManagedPolicy.addStatements(sagemakerForceVolumeKmsKeyStatement); //Enforces use of Team KMS key for SageMaker Outputs const sagemakerForceOutputKmsKeyStatement = new PolicyStatement({ sid: 'forceOutputKmsKey', effect: Effect.DENY, resources: ['*'], actions: ['sagemaker:CreateMonitoringSchedule', 'sagemaker:UpdateMonitoringSchedule', 'sagemaker:Create*Job*'], conditions: { StringNotEquals: { 'sagemaker:OutputKmsKey': teamKey.keyArn, }, }, }); sagemakerGuardrailManagedPolicy.addStatements(sagemakerForceOutputKmsKeyStatement); const sagemakerForceIntercontainerEncryptionNonNullStatement = new PolicyStatement({ sid: 'forceIntercontainerEncryptionNonNull', effect: Effect.DENY, resources: ['*'], actions: ['sagemaker:CreateMonitoringSchedule', 'sagemaker:UpdateMonitoringSchedule', 'sagemaker:Create*Job*'], conditions: { Null: { 'sagemaker:InterContainerTrafficEncryption': 'true', }, }, }); sagemakerGuardrailManagedPolicy.addStatements(sagemakerForceIntercontainerEncryptionNonNullStatement); const sagemakerForceIntercontainerEncryptionTrueStatement = new PolicyStatement({ sid: 'forceIntercontainerEncryptionTrue', effect: Effect.DENY, resources: ['*'], actions: ['sagemaker:CreateMonitoringSchedule', 'sagemaker:UpdateMonitoringSchedule', 'sagemaker:Create*Job*'], conditions: { Bool: { 'sagemaker:InterContainerTrafficEncryption': 'false', }, }, }); sagemakerGuardrailManagedPolicy.addStatements(sagemakerForceIntercontainerEncryptionTrueStatement); const sagemakerForceJobVpc = new PolicyStatement({ sid: 'forceJobNotebookVpc', effect: Effect.DENY, resources: ['*'], actions: [ 'sagemaker:Create*Job*', 'sagemaker:CreateNotebookInstance', 'sagemaker:CreateMonitoringSchedule', 'sagemaker:UpdateMonitoringSchedule', 'sagemaker:CreateModel', ], conditions: { Null: { 'sagemaker:VpcSubnets': 'true', }, }, }); sagemakerGuardrailManagedPolicy.addStatements(sagemakerForceJobVpc); const sagemakerForceSecurityGroupIds = new PolicyStatement({ sid: 'forceJobNotebookSecurityGroups', effect: Effect.DENY, resources: ['*'], actions: [ 'sagemaker:Create*Job*', 'sagemaker:CreateNotebookInstance', 'sagemaker:CreateMonitoringSchedule', 'sagemaker:UpdateMonitoringSchedule', 'sagemaker:CreateModel', ], conditions: { Null: { 'sagemaker:VpcSecurityGroupIds': 'true', }, }, }); sagemakerGuardrailManagedPolicy.addStatements(sagemakerForceSecurityGroupIds); const sagemakerForceNotebookNonDirectNonNull = new PolicyStatement({ sid: 'forceNotebookNonPublicNonNull', effect: Effect.DENY, resources: ['*'], actions: ['sagemaker:CreateNotebookInstance'], conditions: { Null: { 'sagemaker:DirectInternetAccess': 'true', }, }, }); sagemakerGuardrailManagedPolicy.addStatements(sagemakerForceNotebookNonDirectNonNull); const sagemakerForceNotebookNonDirectDisabled = new PolicyStatement({ sid: 'forceNotebookNonPublicDisabled', effect: Effect.DENY, resources: ['*'], actions: ['sagemaker:CreateNotebookInstance'], conditions: { StringNotEquals: { 'sagemaker:DirectInternetAccess': 'Disabled', }, }, }); sagemakerGuardrailManagedPolicy.addStatements(sagemakerForceNotebookNonDirectDisabled); return sagemakerGuardrailManagedPolicy; } }