diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 1c1802f039153..d2594c95fa9b2 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -2,7 +2,10 @@ import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { Aws, CfnCondition, CfnCustomResource, Construct, CustomResource, Fn, IResource, Lazy, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; +import { + Aws, CfnCondition, CfnCustomResource, Construct, CustomResource, Fn, + IResource, Lazy, RemovalPolicy, Resource, Stack, Token, +} from '@aws-cdk/core'; import { CfnTable, CfnTableProps } from './dynamodb.generated'; import * as perms from './perms'; import { ReplicaProvider } from './replica-provider'; @@ -931,7 +934,7 @@ export class Table extends TableBase { this.tableSortKey = props.sortKey; } - if (props.replicationRegions) { + if (props.replicationRegions && props.replicationRegions.length > 0) { this.createReplicaTables(props.replicationRegions); } } @@ -1245,9 +1248,12 @@ export class Table extends TableBase { // Documentation at https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/V2gt_IAM.html // is currently incorrect. AWS Support recommends `dynamodb:*` in both source and destination regions + const onEventHandlerPolicy = new SourceTableAttachedPolicy(this, provider.onEventHandler.role!); + const isCompleteHandlerPolicy = new SourceTableAttachedPolicy(this, provider.isCompleteHandler.role!); + // Permissions in the source region - this.grant(provider.onEventHandler, 'dynamodb:*'); - this.grant(provider.isCompleteHandler, 'dynamodb:DescribeTable'); + this.grant(onEventHandlerPolicy, 'dynamodb:*'); + this.grant(isCompleteHandlerPolicy, 'dynamodb:DescribeTable'); let previousRegion; for (const region of new Set(regions)) { // Remove duplicates @@ -1261,6 +1267,10 @@ export class Table extends TableBase { Region: region, }, }); + currentRegion.node.addDependency( + onEventHandlerPolicy.policy, + isCompleteHandlerPolicy.policy, + ); // Deploy time check to prevent from creating a replica in the region // where this stack is deployed. Only needed for environment agnostic @@ -1292,7 +1302,7 @@ export class Table extends TableBase { // Permissions in the destination regions (outside of the loop to // minimize statements in the policy) - provider.onEventHandler.addToRolePolicy(new iam.PolicyStatement({ + onEventHandlerPolicy.grantPrincipal.addToPolicy(new iam.PolicyStatement({ actions: ['dynamodb:*'], resources: this.regionalArns, })); @@ -1428,3 +1438,48 @@ interface ScalableAttributePair { scalableReadAttribute?: ScalableTableAttribute; scalableWriteAttribute?: ScalableTableAttribute; } + +/** + * An inline policy that is logically bound to the source table of a DynamoDB Global Tables + * "cluster". This is here to ensure permissions are removed as part of (and not before) the + * CleanUp phase of a stack update, when a replica is removed (or the entire "cluster" gets + * replaced). + * + * If statements are added directly to the handler roles (as opposed to in a separate inline + * policy resource), new permissions are in effect before clean up happens, and so replicas that + * need to be dropped can no longer be due to lack of permissions. + */ +class SourceTableAttachedPolicy extends Construct implements iam.IGrantable { + public readonly grantPrincipal: iam.IPrincipal; + public readonly policy: iam.IPolicy; + + public constructor(sourceTable: Table, role: iam.IRole) { + super(sourceTable, `SourceTableAttachedPolicy-${role.node.uniqueId}`); + + const policy = new iam.Policy(this, 'Resource', { roles: [role] }); + this.policy = policy; + this.grantPrincipal = new SourceTableAttachedPrincipal(role, policy); + } +} + +/** + * An `IPrincipal` entity that can be used as the target of `grant` calls, used by the + * `SourceTableAttachedPolicy` class so it can act as an `IGrantable`. + */ +class SourceTableAttachedPrincipal extends iam.PrincipalBase { + public constructor(private readonly role: iam.IRole, private readonly policy: iam.Policy) { + super(); + } + + public get policyFragment(): iam.PrincipalPolicyFragment { + return this.role.policyFragment; + } + + public addToPrincipalPolicy(statement: iam.PolicyStatement): iam.AddToPrincipalPolicyResult { + this.policy.addStatements(statement); + return { + policyDependable: this.policy, + statementAdded: true, + }; + } +} diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json index dc4b5ce676ce6..9057e8c7ae31b 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json @@ -41,6 +41,140 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:*", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": "dynamodb:*", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:eu-west-2:", + { + "Ref": "AWS::AccountId" + }, + ":table/", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:eu-central-1:", + { + "Ref": "AWS::AccountId" + }, + ":table/", + { + "Ref": "TableCD117FA1" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA", + "Roles": [ + { + "Fn::GetAtt": [ + "awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D", + "Outputs.cdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole3E8625F3Ref" + ] + } + ] + } + }, + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:DescribeTable", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "leSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", + "Roles": [ + { + "Fn::GetAtt": [ + "awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D", + "Outputs.cdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole2F936EC4Ref" + ] + } + ] + } + }, "TableReplicaeuwest290D3CD3A": { "Type": "Custom::DynamoDBReplica", "Properties": { @@ -55,6 +189,10 @@ }, "Region": "eu-west-2" }, + "DependsOn": [ + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA" + ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, @@ -73,7 +211,9 @@ "Region": "eu-central-1" }, "DependsOn": [ - "TableReplicaeuwest290D3CD3A" + "TableReplicaeuwest290D3CD3A", + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA" ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -91,7 +231,7 @@ }, "/", { - "Ref": "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510S3BucketCE06C497" + "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3Bucket5148F39F" }, "/", { @@ -101,7 +241,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510S3VersionKey6B6B0A66" + "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3" } ] } @@ -114,7 +254,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510S3VersionKey6B6B0A66" + "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3" } ] } @@ -124,15 +264,6 @@ ] }, "Parameters": { - "referencetocdkdynamodbglobal20191121TableB640876BArn": { - "Fn::GetAtt": [ - "TableCD117FA1", - "Arn" - ] - }, - "referencetocdkdynamodbglobal20191121TableB640876BRef": { - "Ref": "TableCD117FA1" - }, "referencetocdkdynamodbglobal20191121AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3BucketE0999323Ref": { "Ref": "AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3BucketBDDEC9DD" }, @@ -174,17 +305,17 @@ "Type": "String", "Description": "Artifact hash for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" }, - "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510S3BucketCE06C497": { + "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3Bucket5148F39F": { "Type": "String", - "Description": "S3 bucket for asset \"1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510\"" + "Description": "S3 bucket for asset \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" }, - "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510S3VersionKey6B6B0A66": { + "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3": { "Type": "String", - "Description": "S3 key for asset version \"1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510\"" + "Description": "S3 key for asset version \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" }, - "AssetParameters1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510ArtifactHashAB28BC52": { + "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947ArtifactHashBF6B619B": { "Type": "String", - "Description": "Artifact hash for asset \"1e7110d85a2e13b58c2a0fb09f018c144489abfafc62bf10f8ab3561a9cb8510\"" + "Description": "Artifact hash for asset \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" } } } \ No newline at end of file