Skip to content

Commit bfa6490

Browse files
authored
feat(s3): add grantReplicationPermission for IAM Role permissions (#34138)
### Issue # (if applicable) Closes #34119 ### Reason for this change This change introduces a new method, grantReplicationPermission, to the aws-cdk-lib.aws_s3.Bucket construct. The purpose of this addition is to provide a more convenient and programmatic way for AWS CDK users to grant the necessary IAM permissions to a user-provided IAM Role that will be used for S3 bucket replication. ### Description of changes This pull request includes the following code changes: - Added a new public method `grantReplicationPermission` to the Bucket class. - The implementation of this method programmatically attaches the necessary IAM permissions for S3 bucket replication to the provided identity. This change refactors the `renderReplicationConfiguration` method by extracting the IAM permission granting functionality into a dedicated `grantReplicationPermission` method. - unit and integ test - The README was updated to show that users can now grant replication rights to custom IAM roles. ### Describe any new or updated permissions being added No new IAM permissions are being added at the CDK level. The permissions granted by the `grantReplicationPermission` method are the same as those already handled internally by the existing replication configuration logic. This change simply exposes that functionality through a dedicated method. ### Description of how you validated changes - Added unit tests to verify the functionality of the `grantReplicationPermission` method, ensuring that the correct IAM policies are attached to the provided role. Notably, the unit tests specifically cover scenarios where an explicit `replicationRole` is provided. - Existing integration tests were run to confirm that no regressions were introduced by this change. In addition, the existing test scenario `integ.bucket-replication-use-custom-role.ts` was refactored to use the new `grantReplicationPermission` method instead of manually attaching the required permissions to the IAM role, and its behavior was verified to remain equivalent. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent d004216 commit bfa6490

File tree

11 files changed

+340
-64
lines changed

11 files changed

+340
-64
lines changed

packages/@aws-cdk-testing/framework-integ/test/aws-dynamodb/test/integ.import-source.js.snapshot/asset.530055f7515b3f0a47900f5df37e729ba40ca977b2d07b952bdefa2b8f883f42.bundle/index.js

Lines changed: 2 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-s3/test/integ.bucket-replication-use-custom-role.js.snapshot/BucketReplicationTestStack.assets.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-s3/test/integ.bucket-replication-use-custom-role.js.snapshot/BucketReplicationTestStack.template.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -304,25 +304,25 @@
304304
}
305305
},
306306
{
307-
"Action": "kms:Decrypt",
307+
"Action": [
308+
"kms:Encrypt",
309+
"kms:GenerateDataKey*",
310+
"kms:ReEncrypt*"
311+
],
308312
"Effect": "Allow",
309313
"Resource": {
310314
"Fn::GetAtt": [
311-
"SourceKmsKeyFE472F1C",
315+
"DestinationKmsKey0D94AA3C",
312316
"Arn"
313317
]
314318
}
315319
},
316320
{
317-
"Action": [
318-
"kms:Encrypt",
319-
"kms:GenerateDataKey*",
320-
"kms:ReEncrypt*"
321-
],
321+
"Action": "kms:Decrypt",
322322
"Effect": "Allow",
323323
"Resource": {
324324
"Fn::GetAtt": [
325-
"DestinationKmsKey0D94AA3C",
325+
"SourceKmsKeyFE472F1C",
326326
"Arn"
327327
]
328328
}

packages/@aws-cdk-testing/framework-integ/test/aws-s3/test/integ.bucket-replication-use-custom-role.js.snapshot/ReplicationIntegDefaultTestDeployAssert2C07A074.assets.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-s3/test/integ.bucket-replication-use-custom-role.js.snapshot/ReplicationIntegDefaultTestDeployAssert2C07A074.template.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-s3/test/integ.bucket-replication-use-custom-role.js.snapshot/manifest.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-s3/test/integ.bucket-replication-use-custom-role.js.snapshot/tree.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk-testing/framework-integ/test/aws-s3/test/integ.bucket-replication-use-custom-role.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,12 @@ class TestStack extends Stack {
4747
],
4848
});
4949

50-
this.replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
51-
actions: ['s3:GetReplicationConfiguration', 's3:ListBucket'],
52-
resources: [this.sourceBucket.bucketArn],
53-
effect: iam.Effect.ALLOW,
54-
}));
55-
this.replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
56-
actions: ['s3:GetObjectVersionForReplication', 's3:GetObjectVersionAcl', 's3:GetObjectVersionTagging'],
57-
resources: [this.sourceBucket.arnForObjects('*')],
58-
effect: iam.Effect.ALLOW,
59-
}));
60-
this.replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
61-
actions: ['s3:ReplicateObject', 's3:ReplicateDelete', 's3:ReplicateTags', 's3:ObjectOwnerOverrideToBucketOwner'],
62-
resources: [this.destinationBucket.arnForObjects('*')],
63-
effect: iam.Effect.ALLOW,
64-
}));
65-
sourceKmsKey.grantDecrypt(this.replicationRole);
66-
destinationKmsKey.grantEncrypt(this.replicationRole);
50+
this.sourceBucket.grantReplicationPermission(this.replicationRole, {
51+
sourceDecryptionKey: sourceKmsKey,
52+
destinations: [
53+
{ encryptionKey: destinationKmsKey, bucket: this.destinationBucket },
54+
],
55+
});
6756
}
6857
}
6958

packages/aws-cdk-lib/aws-s3/README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -941,11 +941,14 @@ To replicate objects to a destination bucket, you can specify the `replicationRu
941941
declare const destinationBucket1: s3.IBucket;
942942
declare const destinationBucket2: s3.IBucket;
943943
declare const replicationRole: iam.IRole;
944-
declare const kmsKey: kms.IKey;
944+
declare const encryptionKey: kms.IKey;
945+
declare const destinationEncryptionKey: kms.IKey;
945946

946947
const sourceBucket = new s3.Bucket(this, 'SourceBucket', {
947948
// Versioning must be enabled on both the source and destination bucket
948949
versioned: true,
950+
// Optional. Specify the KMS key to use for encrypts objects in the source bucket.
951+
encryptionKey,
949952
// Optional. If not specified, a new role will be created.
950953
replicationRole,
951954
replicationRules: [
@@ -970,7 +973,7 @@ const sourceBucket = new s3.Bucket(this, 'SourceBucket', {
970973
// If set, metrics will be output to indicate whether replication by S3 RTC took longer than the configured time.
971974
metrics: s3.ReplicationTimeValue.FIFTEEN_MINUTES,
972975
// The kms key to use for the destination bucket.
973-
kmsKey,
976+
kmsKey: destinationEncryptionKey,
974977
// The storage class to use for the destination bucket.
975978
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
976979
// Whether to replicate objects with SSE-KMS encryption.
@@ -997,6 +1000,20 @@ const sourceBucket = new s3.Bucket(this, 'SourceBucket', {
9971000
},
9981001
],
9991002
});
1003+
1004+
// Grant permissions to the replication role.
1005+
// This method is not required if you choose to use an auto-generated replication role or manually grant permissions.
1006+
sourceBucket.grantReplicationPermission(replicationRole, {
1007+
// Optional. Specify the KMS key to use for decrypting objects in the source bucket.
1008+
sourceDecryptionKey: encryptionKey,
1009+
destinations: [
1010+
{ bucket: destinationBucket1 },
1011+
{ bucket: destinationBucket2, encryptionKey: destinationEncryptionKey },
1012+
],
1013+
// The 'encryptionKey' property within the 'destinations' array is optional.
1014+
// If not specified for a destination bucket, this method assumes that
1015+
// given destination bucket is not encrypted.
1016+
});
10001017
```
10011018

10021019
### Cross Account Replication

packages/aws-cdk-lib/aws-s3/lib/bucket.ts

Lines changed: 111 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,18 @@ export interface IBucket extends IResource {
266266
*/
267267
grantReadWrite(identity: iam.IGrantable, objectsKeyPattern?: any): iam.Grant;
268268

269+
/**
270+
* Allows permissions for replication operation to bucket replication role.
271+
*
272+
* If an encryption key is used, permission to use the key for
273+
* encrypt/decrypt will also be granted.
274+
*
275+
* @param identity The principal
276+
* @param props The properties of the replication source and destination buckets.
277+
* @returns The `iam.Grant` object, which represents the grant of permissions.
278+
*/
279+
grantReplicationPermission(identity: iam.IGrantable, props: GrantReplicationPermissionProps): iam.Grant;
280+
269281
/**
270282
* Allows unrestricted access to objects from this bucket.
271283
*
@@ -497,6 +509,45 @@ export interface BucketAttributes {
497509
readonly notificationsHandlerRole?: iam.IRole;
498510
}
499511

512+
/**
513+
* The properties for the destination bucket for granting replication permission.
514+
*/
515+
export interface GrantReplicationPermissionDestinationProps {
516+
/**
517+
* The destination bucket
518+
*/
519+
readonly bucket: IBucket;
520+
521+
/**
522+
* The KMS key to use for encryption if a destination bucket needs to be encrypted with a customer-managed KMS key.
523+
*
524+
* @default - no KMS key is used for replication.
525+
*/
526+
readonly encryptionKey?: kms.IKey;
527+
}
528+
529+
/**
530+
* The properties for the destination bucket for granting replication permission.
531+
*/
532+
export interface GrantReplicationPermissionProps {
533+
/**
534+
* The KMS key used to decrypt objects in the source bucket for replication.
535+
* **Required if** the source bucket is encrypted with a customer-managed KMS key.
536+
*
537+
* @default - it's assumed the source bucket is not encrypted with a customer-managed KMS key.
538+
*/
539+
readonly sourceDecryptionKey?: kms.IKey;
540+
541+
/**
542+
* The destination buckets for replication.
543+
* Specify the KMS key to use for encryption if a destination bucket needs to be encrypted with a customer-managed KMS key.
544+
* One or more destination buckets are required if replication configuration is enabled (i.e., `replicationRole` is specified).
545+
*
546+
* @default - empty array (valid only if the `replicationRole` property is NOT specified)
547+
*/
548+
readonly destinations: GrantReplicationPermissionDestinationProps[];
549+
}
550+
500551
/**
501552
* Represents an S3 Bucket.
502553
*
@@ -846,6 +897,60 @@ export abstract class BucketBase extends Resource implements IBucket {
846897
this.arnForObjects(objectsKeyPattern));
847898
}
848899

900+
/**
901+
* Grant replication permission to a principal.
902+
* This method allows the principal to perform replication operations on this bucket.
903+
*
904+
* Note that when calling this function for source or destination buckets that support KMS encryption,
905+
* you need to specify the KMS key for encryption and the KMS key for decryption, respectively.
906+
*
907+
* @param identity The principal to grant replication permission to.
908+
* @param props The properties of the replication source and destination buckets.
909+
*/
910+
public grantReplicationPermission(identity: iam.IGrantable, props: GrantReplicationPermissionProps): iam.Grant {
911+
if (props.destinations.length === 0) {
912+
throw new ValidationError('At least one destination bucket must be specified in the destinations array', this);
913+
}
914+
915+
// add permissions to the role
916+
// @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/setting-repl-config-perm-overview.html
917+
let result = this.grant(identity, ['s3:GetReplicationConfiguration', 's3:ListBucket'], [], Lazy.string({ produce: () => this.bucketArn }));
918+
919+
const g1 = this.grant(
920+
identity,
921+
['s3:GetObjectVersionForReplication', 's3:GetObjectVersionAcl', 's3:GetObjectVersionTagging'],
922+
[],
923+
Lazy.string({ produce: () => this.arnForObjects('*') }),
924+
);
925+
result = result.combine(g1);
926+
927+
const destinationBuckets = props.destinations.map(destination => destination.bucket);
928+
if (destinationBuckets.length > 0) {
929+
const g2 = iam.Grant.addToPrincipalOrResource({
930+
grantee: identity,
931+
actions: ['s3:ReplicateObject', 's3:ReplicateDelete', 's3:ReplicateTags', 's3:ObjectOwnerOverrideToBucketOwner'],
932+
resourceArns: destinationBuckets.map(bucket => Lazy.string({ produce: () => bucket.arnForObjects('*') })),
933+
resource: this,
934+
});
935+
result = result.combine(g2);
936+
}
937+
938+
props.destinations.forEach(destination => {
939+
const g = destination.encryptionKey?.grantEncrypt(identity);
940+
if (g !== undefined) {
941+
result = result.combine(g);
942+
}
943+
});
944+
945+
// If KMS key encryption is enabled on the source bucket, configure the decrypt permissions.
946+
const g3 = this.encryptionKey?.grantDecrypt(identity);
947+
if (g3 !== undefined) {
948+
result = result.combine(g3);
949+
}
950+
951+
return result;
952+
}
953+
849954
/**
850955
* Allows unrestricted access to objects from this bucket.
851956
*
@@ -2841,42 +2946,20 @@ export class Bucket extends BucketBase {
28412946
}
28422947
});
28432948

2844-
const destinationBuckets = props.replicationRules.map(rule => rule.destination);
2845-
const kmsKeys = props.replicationRules.map(rule => rule.kmsKey).filter(kmsKey => kmsKey !== undefined) as kms.IKey[];
2846-
28472949
let replicationRole: iam.IRole;
28482950
if (!props.replicationRole) {
28492951
replicationRole = new iam.Role(this, 'ReplicationRole', {
28502952
assumedBy: new iam.ServicePrincipal('s3.amazonaws.com'),
28512953
roleName: FeatureFlags.of(this).isEnabled(cxapi.SET_UNIQUE_REPLICATION_ROLE_NAME) ? PhysicalName.GENERATE_IF_NEEDED : 'CDKReplicationRole',
28522954
});
28532955

2854-
// add permissions to the role
2855-
// @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/setting-repl-config-perm-overview.html
2856-
replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
2857-
actions: ['s3:GetReplicationConfiguration', 's3:ListBucket'],
2858-
resources: [Lazy.string({ produce: () => this.bucketArn })],
2859-
effect: iam.Effect.ALLOW,
2860-
}));
2861-
replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
2862-
actions: ['s3:GetObjectVersionForReplication', 's3:GetObjectVersionAcl', 's3:GetObjectVersionTagging'],
2863-
resources: [Lazy.string({ produce: () => this.arnForObjects('*') })],
2864-
effect: iam.Effect.ALLOW,
2865-
}));
2866-
if (destinationBuckets.length > 0) {
2867-
replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
2868-
actions: ['s3:ReplicateObject', 's3:ReplicateDelete', 's3:ReplicateTags', 's3:ObjectOwnerOverrideToBucketOwner'],
2869-
resources: destinationBuckets.map(bucket => bucket.arnForObjects('*')),
2870-
effect: iam.Effect.ALLOW,
2871-
}));
2872-
}
2873-
2874-
kmsKeys.forEach(kmsKey => {
2875-
kmsKey.grantEncrypt(replicationRole);
2956+
this.grantReplicationPermission(replicationRole, {
2957+
sourceDecryptionKey: props.encryptionKey,
2958+
destinations: props.replicationRules.map(rule => ({
2959+
encryptionKey: rule.kmsKey,
2960+
bucket: rule.destination,
2961+
})),
28762962
});
2877-
2878-
// If KMS key encryption is enabled on the source bucket, configure the decrypt permissions.
2879-
this.encryptionKey?.grantDecrypt(replicationRole);
28802963
} else {
28812964
replicationRole = props.replicationRole;
28822965
}

0 commit comments

Comments
 (0)