packages/constructs/L3/governance/datazone-l3-construct/lib/datazone-l3-construct.ts (424 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { MdaaParamAndOutput } from '@aws-mdaa/construct'; //NOSONAR //NOSONAR import { MdaaCustomResource, MdaaCustomResourceProps } from '@aws-mdaa/custom-constructs'; import { MdaaManagedPolicy, MdaaRole } from '@aws-mdaa/iam-constructs'; import { MdaaResolvableRole, MdaaRoleRef } from '@aws-mdaa/iam-role-helper'; import { DECRYPT_ACTIONS, ENCRYPT_ACTIONS, MdaaKmsKey } from '@aws-mdaa/kms-constructs'; import { MdaaL3Construct, MdaaL3ConstructProps } from '@aws-mdaa/l3-construct'; import { MdaaBoto3LayerVersion } from '@aws-mdaa/lambda-constructs'; import { Duration } from 'aws-cdk-lib'; import { CfnDomain, CfnDomainProps, CfnUserProfile, CfnUserProfileProps } from 'aws-cdk-lib/aws-datazone'; import { Conditions, Effect, IRole, PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; import { IKey } from 'aws-cdk-lib/aws-kms'; import { Code, Runtime } from 'aws-cdk-lib/aws-lambda'; import { CfnResourceShare, CfnResourceShareProps } from 'aws-cdk-lib/aws-ram'; import { ParameterTier, StringParameter } from 'aws-cdk-lib/aws-ssm'; import { MdaaNagSuppressions } from '@aws-mdaa/construct'; //NOSONAR import { Construct } from 'constructs'; export interface AdminUser { readonly role: MdaaRoleRef; readonly userType: 'IAM_ROLE' | 'SSO_USER'; } export interface NamedAdminUsers { /** @jsii ignore */ readonly [name: string]: AdminUser; } export interface AsscociatedAccount { readonly account: string; readonly region?: string; readonly cdkRoleArn?: string; readonly adminUsers?: NamedAdminUsers; } export interface NamedAssociatedAccounts { /** @jsii ignore */ [name: string]: AsscociatedAccount; } export interface NamedBaseDomainsProps { /** @jsii ignore */ readonly [name: string]: BaseDomainProps; } export interface BaseDomainProps { readonly dataAdminRole: MdaaRoleRef; readonly additionalAdminUsers?: NamedAdminUsers; readonly description?: string; readonly userAssignment: 'MANUAL' | 'AUTOMATIC'; readonly associatedAccounts?: NamedAssociatedAccounts; } export interface DomainProps extends BaseDomainProps { readonly domainVersion?: 'V1' | 'V2'; readonly singleSignOnType: 'DISABLED' | 'IAM_IDC'; } export interface NamedDomainsProps { /** @jsii ignore */ readonly [name: string]: DomainProps; } export interface DataZoneL3ConstructProps extends MdaaL3ConstructProps { readonly domains?: NamedDomainsProps; } const DEFAULT_SSO_TYPE = 'DISABLED'; const DEFAULT_USER_ASSIGNMENT = 'MANUAL'; export class DataZoneL3Construct extends MdaaL3Construct { protected readonly props: DataZoneL3ConstructProps; private static CUSTOM_ENV_BLUEPRINT_ID = 'dqsdikgj5tspu2'; constructor(scope: Construct, id: string, props: DataZoneL3ConstructProps) { super(scope, id, props); this.props = props; Object.entries(this.props.domains || {}).forEach(entry => { const domainName = entry[0]; const domainProps = entry[1]; this.createDomain(domainName, domainProps); }); } private createDomain(domainName: string, domainProps: DomainProps) { const dataAdminRole = this.props.roleHelper.resolveRoleRefWithRefId(domainProps.dataAdminRole, 'admin'); const kmsKey = this.createDomainKmsKey(domainName, domainProps, dataAdminRole); const kmsKeyArnParam = new MdaaParamAndOutput(this, { resourceType: 'kms-cmk', resourceId: domainName, name: 'arn', value: kmsKey.keyArn, ...this.props, tier: ParameterTier.ADVANCED, }); // Resolve Execution Role const executionRole = this.createExecutionRole(`${domainName}-execution-role`, kmsKey, domainProps.domainVersion); const singleSignOn: CfnDomain.SingleSignOnProperty = { type: domainProps.singleSignOnType ?? DEFAULT_SSO_TYPE, userAssignment: domainProps.userAssignment ?? DEFAULT_USER_ASSIGNMENT, }; const cfnDomainProps: CfnDomainProps = { domainExecutionRole: executionRole.roleArn, name: this.props.naming.resourceName(domainName), kmsKeyIdentifier: kmsKey.keyArn, description: domainProps.description, singleSignOn: singleSignOn, domainVersion: domainProps.domainVersion, serviceRole: domainProps.domainVersion == 'V2' ? this.createServiceRole('service', kmsKey).roleArn : undefined, }; // Create domain const domain = new CfnDomain(this, `${domainName}-domain`, cfnDomainProps); const adminUserProfileProps: CfnUserProfileProps = { domainIdentifier: domain.attrId, userIdentifier: dataAdminRole.arn(), userType: 'IAM_ROLE', status: 'ACTIVATED', }; const adminUserProfile = new CfnUserProfile(this, `${domainName}-admin-user-profile`, adminUserProfileProps); Object.entries(domainProps.additionalAdminUsers || {}).forEach(adminUser => { const resolvedUserRole = this.props.roleHelper.resolveRoleRefWithRefId(adminUser[1].role, adminUser[0]); const accountUserProfileProps: CfnUserProfileProps = { domainIdentifier: domain.attrId, userIdentifier: adminUser[1].userType == 'IAM_ROLE' ? resolvedUserRole.arn() : resolvedUserRole.name(), userType: adminUser[1].userType, status: 'ACTIVATED', }; new CfnUserProfile(this, `${domainName}-admin-user-${adminUser[0]}`, accountUserProfileProps); }); new MdaaParamAndOutput(this, { resourceType: 'admin-user', resourceId: domainName, name: 'id', value: adminUserProfile.attrId, ...this.props, tier: ParameterTier.ADVANCED, }); const customEnvBlueprintConfig = this.createCustomBlueprintConfig( this, domain.attrId, [this.region], kmsKey.keyArn, ); new MdaaParamAndOutput(this, { resourceType: 'custom-datalake-env-blueprint', resourceId: domainName, name: 'id', value: customEnvBlueprintConfig.getAttString('id'), ...this.props, tier: ParameterTier.ADVANCED, }); const domainIdParam = new MdaaParamAndOutput(this, { resourceType: 'domain', resourceId: domainName, name: 'id', value: domain.attrId, ...this.props, tier: ParameterTier.ADVANCED, }); const domainArnParam = new MdaaParamAndOutput(this, { resourceType: 'domain', resourceId: domainName, name: 'arn', value: domain.attrArn, tier: ParameterTier.ADVANCED, ...this.props, }); const configParam = this.createDomainConfigParam( domainName, domain, adminUserProfile, customEnvBlueprintConfig.getAttString('id'), domain.domainVersion ?? 'V1', ); if (domainProps.associatedAccounts) { const ramShareProps: CfnResourceShareProps = { name: `DataZone-${this.props.naming.resourceName('domain-config-ssm')}-${domain.attrId}`, resourceArns: [domain.attrArn], principals: Object.entries(domainProps.associatedAccounts).map(x => x[1].account), permissionArns: ['arn:aws:ram::aws:permission/AWSRAMDefaultPermissionAmazonDataZoneDomain'], }; new CfnResourceShare(this, `domain-ram-share`, ramShareProps); if (configParam.param) { const ramShareProps: CfnResourceShareProps = { name: this.props.naming.resourceName('domain-config-ssm'), resourceArns: [configParam.param.parameterArn], principals: Object.entries(domainProps.associatedAccounts).map(x => x[1].account), }; new CfnResourceShare(this, `domain-config-ram-share`, ramShareProps); } if (domainArnParam.param) { const ramShareProps: CfnResourceShareProps = { name: this.props.naming.resourceName('domain-arn-ssm'), resourceArns: [domainArnParam.param.parameterArn], principals: Object.entries(domainProps.associatedAccounts).map(x => x[1].account), }; new CfnResourceShare(this, `domain-arn-ram-share`, ramShareProps); } if (domainIdParam.param && kmsKeyArnParam.param) { const domainIdShareProps: CfnResourceShareProps = { name: this.props.naming.resourceName('domain-id-ssm'), resourceArns: [domainIdParam.param.parameterArn], principals: Object.entries(domainProps.associatedAccounts).map(x => x[1].account), }; new CfnResourceShare(this, `domain-id-ram-share`, domainIdShareProps); const domainKeyArnShareProps: CfnResourceShareProps = { name: this.props.naming.resourceName('domain-key-arn-ssm'), resourceArns: [kmsKeyArnParam.param.parameterArn], principals: Object.entries(domainProps.associatedAccounts).map(x => x[1].account), }; new CfnResourceShare(this, `domain-key-arn-ram-share`, domainKeyArnShareProps); Object.entries(domainProps.associatedAccounts).forEach(associatedAccount => { const accountCdkUserProfileProps: CfnUserProfileProps = { domainIdentifier: domain.attrId, userIdentifier: associatedAccount[1].cdkRoleArn ?? `arn:${this.partition}:iam::${associatedAccount[1].account}:role/cdk-hnb659fds-cfn-exec-role-${associatedAccount[1].account}-${this.region}`, userType: 'IAM_ROLE', status: 'ACTIVATED', }; new CfnUserProfile( this, `${domainName}-${associatedAccount[1].account}-cdk-user-profile`, accountCdkUserProfileProps, ); if (this.props.crossAccountStacks) { const crossAccountStack = this.props.crossAccountStacks[associatedAccount[1].account]; if (crossAccountStack) { const domainIdSsmParamArn = `arn:${this.partition}:ssm:${associatedAccount[1].region ?? this.region}:${ this.account }:parameter${domainIdParam.paramName}`; const kmsKeyArnParamArn = `arn:${this.partition}:ssm:${associatedAccount[1].region ?? this.region}:${ this.account }:parameter${kmsKeyArnParam.paramName}`; this.createCustomBlueprintConfig( crossAccountStack, StringParameter.fromStringParameterArn( crossAccountStack, `domain-id-import-${associatedAccount}`, domainIdSsmParamArn, ).stringValue, [associatedAccount[1].region || this.region], StringParameter.fromStringParameterArn( crossAccountStack, `domain-key-arn-id-import-${associatedAccount}`, kmsKeyArnParamArn, ).stringValue, ); } else { console.warn( `Cross account stack not defined for associated account ${associatedAccount[0]}/${associatedAccount[1].account} on domain ${domainName}`, ); } } }); } } } private createDomainConfigParam( domainName: string, domain: CfnDomain, adminUserProfile: CfnUserProfile, customEnvBlueprintConfigId: string, domainVersion: string, ): MdaaParamAndOutput { return new MdaaParamAndOutput(this, { resourceType: 'domain', resourceId: domainName, name: 'config', tier: ParameterTier.ADVANCED, value: JSON.stringify({ domainId: domain.attrId, domainArn: domain.attrArn, adminUserProfileId: adminUserProfile.attrId, datalakeEnvBlueprintId: customEnvBlueprintConfigId, domainVersion: domainVersion, }), ...this.props, }); } private createDomainKmsKey(domainName: string, domainProps: DomainProps, dataAdminRole: MdaaResolvableRole): IKey { // Create KMS Key const kmsKey = new MdaaKmsKey(this, `${domainName}-cmk`, { naming: this.props.naming, alias: domainName, keyAdminRoleIds: [dataAdminRole.id()], }); const keyAccessAccounts = [ ...Object.entries(domainProps.associatedAccounts || {}).map(x => x[1].account), this.account, ]; keyAccessAccounts.forEach(account => { //Add a statement that allows anyone in the account to use the key as long as it is via Datazone const accountKeyUsagePolicyStatement = new PolicyStatement({ effect: Effect.ALLOW, // Use of * mirrors what is done in the CDK methods for adding policy helpers. resources: ['*'], actions: [...DECRYPT_ACTIONS, ...ENCRYPT_ACTIONS, 'kms:DescribeKey', 'kms:CreateGrant'], }); accountKeyUsagePolicyStatement.addAnyPrincipal(); accountKeyUsagePolicyStatement.addCondition('StringEquals', { 'kms:CallerAccount': account, 'kms:ViaService': `datazone.${this.region}.amazonaws.com`, }); kmsKey.addToResourcePolicy(accountKeyUsagePolicyStatement); }); return kmsKey; } /** * Creates an Execution Role for a DataZone Domain * @param roleName name to use for the role * @param kmsArn KMS key ARN created for the domain * @returns a Role */ private createServiceRole(roleName: string, kmsKey: IKey): IRole { const serviceRoleConditions: Conditions = { StringEquals: { 'aws:SourceAccount': this.account, }, }; const serviceRole = new MdaaRole(this, roleName, { naming: this.props.naming, roleName: roleName, assumedBy: new ServicePrincipal('datazone.amazonaws.com').withConditions(serviceRoleConditions), // managedPolicies: [ // MdaaManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonDataZoneDomainExecutionRolePolicy'), // ], }); serviceRole.addToPolicy( new PolicyStatement({ effect: Effect.ALLOW, actions: ['kms:Decrypt', 'kms:GenerateDataKey'], resources: [kmsKey.keyArn], }), ); MdaaNagSuppressions.addCodeResourceSuppressions( serviceRole, [ { id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Permission to use Key for DataZone. No other role requires this.', }, { id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Permission to use Key for DataZone. No other role requires this.', }, { id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Permission to use Key for DataZone. No other role requires this.', }, ], true, ); return serviceRole; } /** * Creates an Execution Role for a DataZone Domain * @param roleName name to use for the role * @param kmsArn KMS key ARN created for the domain * @returns a Role */ private createExecutionRole(roleName: string, kmsKey: IKey, domainVersion?: string): IRole { const executionRoleCondition: Conditions = { StringEquals: { 'aws:SourceAccount': this.account, }, 'ForAllValues:StringLike': { 'aws:TagKeys': 'datazone*', }, }; const executionRole = new MdaaRole(this, roleName, { naming: this.props.naming, roleName: roleName, assumedBy: new ServicePrincipal('datazone.amazonaws.com').withConditions(executionRoleCondition), managedPolicies: [ domainVersion == 'V2' ? MdaaManagedPolicy.fromAwsManagedPolicyName('service-role/SageMakerStudioDomainExecutionRolePolicy') : MdaaManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonDataZoneDomainExecutionRolePolicy'), ], }); executionRole.assumeRolePolicy?.addStatements( new PolicyStatement({ actions: ['sts:TagSession'], principals: [new ServicePrincipal('datazone.amazonaws.com').withConditions(executionRoleCondition)], }), ); executionRole.addToPolicy( new PolicyStatement({ effect: Effect.ALLOW, actions: ['kms:Decrypt', 'kms:GenerateDataKey'], resources: [kmsKey.keyArn], }), ); MdaaNagSuppressions.addCodeResourceSuppressions( executionRole, [ { id: 'AwsSolutions-IAM4', reason: 'Permissions are related DataZone and only one permission is given to RAM to get share associations.', }, { id: 'NIST.800.53.R5-IAMNoInlinePolicy', reason: 'Permission to use Key for DataZone. No other role requires this.', }, { id: 'HIPAA.Security-IAMNoInlinePolicy', reason: 'Permission to use Key for DataZone. No other role requires this.', }, { id: 'PCI.DSS.321-IAMNoInlinePolicy', reason: 'Permission to use Key for DataZone. No other role requires this.', }, ], true, ); return executionRole; } private createCustomBlueprintConfig( scope: Construct, domainId: string, regions: string[], domainKmsKeyArn: string, ): MdaaCustomResource { const envBlueprintConfigsStatements = [ new PolicyStatement({ resources: ['*'], actions: ['datazone:PutEnvironmentBlueprintConfiguration'], }), new PolicyStatement({ resources: [domainKmsKeyArn], actions: [...DECRYPT_ACTIONS, ...ENCRYPT_ACTIONS], }), ]; const crProps: MdaaCustomResourceProps = { resourceType: 'EnvBlueprintConfig', code: Code.fromAsset(`${__dirname}/../src/lambda/blueprint_configuration`), runtime: Runtime.PYTHON_3_13, handler: 'blueprint_configuration.lambda_handler', handlerRolePolicyStatements: envBlueprintConfigsStatements, handlerPolicySuppressions: [ { id: 'AwsSolutions-IAM5', reason: 'PutEnvironmentBlueprintConfiguration does not take a resource: https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazondatazone.html ', }, ], handlerProps: { domainIdentifier: domainId, environmentBlueprintIdentifier: DataZoneL3Construct.CUSTOM_ENV_BLUEPRINT_ID, enabledRegions: regions, }, naming: this.props.naming, pascalCaseProperties: false, handlerLayers: [new MdaaBoto3LayerVersion(scope, 'boto3-layer', { naming: this.props.naming })], handlerTimeout: Duration.seconds(120), environment: { LOG_LEVEL: 'INFO', }, }; return new MdaaCustomResource(scope, 'env-blueprint-config-cr', crProps); } }