Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1c53bb0
feat: add grant method for the role is used replication
hassaku63 Apr 13, 2025
32bb56c
test: grant replication permission for custom iam role
hassaku63 Apr 13, 2025
ce97233
test: replace grant procedure for custom iam role
hassaku63 Apr 13, 2025
86fe648
docs: how to use grantReplicationPermission
hassaku63 Apr 13, 2025
901cbbc
fix: add JSDocs for prop
hassaku63 Apr 13, 2025
4f0ae47
fix: linter and remove old comments
hassaku63 Apr 13, 2025
6e1d9dc
docs: fix example variable name to reflect right intent
hassaku63 Apr 14, 2025
2e65d3d
docs: describe clearly when the grant replication permission method s…
hassaku63 Apr 14, 2025
231ff71
fix: JSII3008
hassaku63 Apr 14, 2025
57721e6
fix: remove debug statement
hassaku63 Apr 15, 2025
a3c715a
fix: add jsdoc
hassaku63 Apr 15, 2025
40b6e2b
test: add test case for invalid args
hassaku63 Apr 16, 2025
9aa8929
Merge branch 'main' into feat/grant-permission-for-replication-role
hassaku63 Apr 27, 2025
2837964
refactor: use private grant method
hassaku63 Apr 27, 2025
b970f0a
refactor: return iam.Grant
hassaku63 Apr 27, 2025
ad18d1d
fix: fit interface of grantReplicationPermission method
hassaku63 Apr 27, 2025
d2c434e
fix: lint error
hassaku63 Apr 27, 2025
3fedd55
Merge branch 'main' into feat/grant-permission-for-replication-role
hassaku63 May 3, 2025
bdbfd76
Merge branch 'main' into feat/grant-permission-for-replication-role
hassaku63 May 24, 2025
419d893
Update packages/aws-cdk-lib/aws-s3/lib/bucket.ts
hassaku63 May 24, 2025
a23abfa
test: improve error message
hassaku63 May 24, 2025
4c7c9d7
docs: improve JSDoc for clearly
hassaku63 May 24, 2025
83aa959
test: remove overriding logical id
hassaku63 May 24, 2025
30990b4
test: remove overriding logical id
hassaku63 May 24, 2025
a0df50e
Merge branch 'main' into feat/grant-permission-for-replication-role
shikha372 May 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -304,25 +304,25 @@
}
},
{
"Action": "kms:Decrypt",
"Action": [
"kms:Encrypt",
"kms:GenerateDataKey*",
"kms:ReEncrypt*"
],
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"SourceKmsKeyFE472F1C",
"DestinationKmsKey0D94AA3C",
"Arn"
]
}
},
{
"Action": [
"kms:Encrypt",
"kms:GenerateDataKey*",
"kms:ReEncrypt*"
],
"Action": "kms:Decrypt",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"DestinationKmsKey0D94AA3C",
"SourceKmsKeyFE472F1C",
"Arn"
]
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,12 @@ class TestStack extends Stack {
],
});

this.replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['s3:GetReplicationConfiguration', 's3:ListBucket'],
resources: [this.sourceBucket.bucketArn],
effect: iam.Effect.ALLOW,
}));
this.replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['s3:GetObjectVersionForReplication', 's3:GetObjectVersionAcl', 's3:GetObjectVersionTagging'],
resources: [this.sourceBucket.arnForObjects('*')],
effect: iam.Effect.ALLOW,
}));
this.replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['s3:ReplicateObject', 's3:ReplicateDelete', 's3:ReplicateTags', 's3:ObjectOwnerOverrideToBucketOwner'],
resources: [this.destinationBucket.arnForObjects('*')],
effect: iam.Effect.ALLOW,
}));
sourceKmsKey.grantDecrypt(this.replicationRole);
destinationKmsKey.grantEncrypt(this.replicationRole);
this.sourceBucket.grantReplicationPermission(this.replicationRole, {
sourceDecryptionKey: sourceKmsKey,
destinations: [
{ encryptionKey: destinationKmsKey, bucket: this.destinationBucket },
],
});
}
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 19 additions & 2 deletions packages/aws-cdk-lib/aws-s3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -941,11 +941,14 @@ To replicate objects to a destination bucket, you can specify the `replicationRu
declare const destinationBucket1: s3.IBucket;
declare const destinationBucket2: s3.IBucket;
declare const replicationRole: iam.IRole;
declare const kmsKey: kms.IKey;
declare const encryptionKey: kms.IKey;
declare const destinationEncryptionKey: kms.IKey;

const sourceBucket = new s3.Bucket(this, 'SourceBucket', {
// Versioning must be enabled on both the source and destination bucket
versioned: true,
// Optional. Specify the KMS key to use for encrypts objects in the source bucket.
encryptionKey,
// Optional. If not specified, a new role will be created.
replicationRole,
replicationRules: [
Expand All @@ -970,7 +973,7 @@ const sourceBucket = new s3.Bucket(this, 'SourceBucket', {
// If set, metrics will be output to indicate whether replication by S3 RTC took longer than the configured time.
metrics: s3.ReplicationTimeValue.FIFTEEN_MINUTES,
// The kms key to use for the destination bucket.
kmsKey,
kmsKey: destinationEncryptionKey,
// The storage class to use for the destination bucket.
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
// Whether to replicate objects with SSE-KMS encryption.
Expand All @@ -997,6 +1000,20 @@ const sourceBucket = new s3.Bucket(this, 'SourceBucket', {
},
],
});

// Grant permissions to the replication role.
// This method is not required if you choose to use an auto-generated replication role or manually grant permissions.
sourceBucket.grantReplicationPermission(replicationRole, {
// Optional. Specify the KMS key to use for decrypting objects in the source bucket.
sourceDecryptionKey: encryptionKey,
destinations: [
{ bucket: destinationBucket1 },
{ bucket: destinationBucket2, encryptionKey: destinationEncryptionKey },
],
// The 'encryptionKey' property within the 'destinations' array is optional.
// If not specified for a destination bucket, this method assumes that
// given destination bucket is not encrypted.
});
```

### Cross Account Replication
Expand Down
139 changes: 111 additions & 28 deletions packages/aws-cdk-lib/aws-s3/lib/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,18 @@ export interface IBucket extends IResource {
*/
grantReadWrite(identity: iam.IGrantable, objectsKeyPattern?: any): iam.Grant;

/**
* Allows permissions for replication operation to bucket replication role.
*
* If an encryption key is used, permission to use the key for
* encrypt/decrypt will also be granted.
*
* @param identity The principal
* @param props The properties of the replication source and destination buckets.
* @returns The `iam.Grant` object, which represents the grant of permissions.
*/
grantReplicationPermission(identity: iam.IGrantable, props: GrantReplicationPermissionProps): iam.Grant;

/**
* Allows unrestricted access to objects from this bucket.
*
Expand Down Expand Up @@ -496,6 +508,45 @@ export interface BucketAttributes {
readonly notificationsHandlerRole?: iam.IRole;
}

/**
* The properties for the destination bucket for granting replication permission.
*/
export interface GrantReplicationPermissionDestinationProps {
/**
* The destination bucket
*/
readonly bucket: IBucket;

/**
* The KMS key to use for encryption if a destination bucket needs to be encrypted with a customer-managed KMS key.
*
* @default - no KMS key is used for replication.
*/
readonly encryptionKey?: kms.IKey;
}

/**
* The properties for the destination bucket for granting replication permission.
*/
export interface GrantReplicationPermissionProps {
/**
* The KMS key used to decrypt objects in the source bucket for replication.
* **Required if** the source bucket is encrypted with a customer-managed KMS key.
*
* @default - it's assumed the source bucket is not encrypted with a customer-managed KMS key.
*/
readonly sourceDecryptionKey?: kms.IKey;

/**
* The destination buckets for replication.
* Specify the KMS key to use for encryption if a destination bucket needs to be encrypted with a customer-managed KMS key.
* Required one or more destination buckets.
*
* @default - empty array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc for destinations property says default tag - empty array, but the implementation throws an error if the array is empty. I would suggest removing the default tag since there is no default value, and clarify that at least one destination is required

Copy link
Contributor Author

@hassaku63 hassaku63 May 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ozelalisen Thanks. The points you suggested are absolutely right. How about adopting the following policy?

  /**
   * The destination buckets for replication.
   * Specify the KMS key to use for encryption if a destination bucket needs to be encrypted with a customer-managed KMS key.
   *
   * One or more destination buckets are required if replication configuration is enabled (i.e., `replicationRole` is specified).
   *
   * @default - empty array (valid only if the `replicationRole` property is NOT specified)
   */
  readonly destinations: GrantReplicationPermissionDestinationProps[];

I aimed to clearly convey the behavior of this property by distinguishing between the default value itself and the conditions under which that default is valid or invalid.

Copy link
Member

@ozelalisen ozelalisen May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for addressing the issue, that looks good to me!

*/
readonly destinations: GrantReplicationPermissionDestinationProps[];
}

/**
* Represents an S3 Bucket.
*
Expand Down Expand Up @@ -845,6 +896,60 @@ export abstract class BucketBase extends Resource implements IBucket {
this.arnForObjects(objectsKeyPattern));
}

/**
* Grant replication permission to a principal.
* This method allows the principal to perform replication operations on this bucket.
*
* Note that when calling this function for source or destination buckets that support KMS encryption,
* you need to specify the KMS key for encryption and the KMS key for decryption, respectively.
*
* @param identity The principal to grant replication permission to.
* @param props The properties of the replication source and destination buckets.
*/
public grantReplicationPermission(identity: iam.IGrantable, props: GrantReplicationPermissionProps): iam.Grant {
if (props.destinations.length === 0) {
throw new ValidationError('destinations must be specified', this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "destinations must be specified" could be more descriptive. Maybe something like: At least one destination bucket must be specified in the destinations array

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ozelalisen Thanks for your review. I will revise the error message as you suggested.

}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add unit test for throwing this error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@badmintoncryer Thank you for your review, yes I have added a unit test that you noted.


// add permissions to the role
// @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/setting-repl-config-perm-overview.html
let result = this.grant(identity, ['s3:GetReplicationConfiguration', 's3:ListBucket'], [], Lazy.string({ produce: () => this.bucketArn }));

const g1 = this.grant(
identity,
['s3:GetObjectVersionForReplication', 's3:GetObjectVersionAcl', 's3:GetObjectVersionTagging'],
[],
Lazy.string({ produce: () => this.arnForObjects('*') }),
);
result = result.combine(g1);

const destinationBuckets = props.destinations.map(destination => destination.bucket);
if (destinationBuckets.length > 0) {
const g2 = iam.Grant.addToPrincipalOrResource({
grantee: identity,
actions: ['s3:ReplicateObject', 's3:ReplicateDelete', 's3:ReplicateTags', 's3:ObjectOwnerOverrideToBucketOwner'],
resourceArns: destinationBuckets.map(bucket => Lazy.string({ produce: () => bucket.arnForObjects('*') })),
resource: this,
});
result = result.combine(g2);
}

props.destinations.forEach(destination => {
const g = destination.encryptionKey?.grantEncrypt(identity);
if (g !== undefined) {
result = result.combine(g);
}
});

// If KMS key encryption is enabled on the source bucket, configure the decrypt permissions.
const g3 = this.encryptionKey?.grantDecrypt(identity);
if (g3 !== undefined) {
result = result.combine(g3);
}

return result;
}

/**
* Allows unrestricted access to objects from this bucket.
*
Expand Down Expand Up @@ -2811,42 +2916,20 @@ export class Bucket extends BucketBase {
}
});

const destinationBuckets = props.replicationRules.map(rule => rule.destination);
const kmsKeys = props.replicationRules.map(rule => rule.kmsKey).filter(kmsKey => kmsKey !== undefined) as kms.IKey[];

let replicationRole: iam.IRole;
if (!props.replicationRole) {
replicationRole = new iam.Role(this, 'ReplicationRole', {
assumedBy: new iam.ServicePrincipal('s3.amazonaws.com'),
roleName: FeatureFlags.of(this).isEnabled(cxapi.SET_UNIQUE_REPLICATION_ROLE_NAME) ? PhysicalName.GENERATE_IF_NEEDED : 'CDKReplicationRole',
});

// add permissions to the role
// @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/setting-repl-config-perm-overview.html
replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['s3:GetReplicationConfiguration', 's3:ListBucket'],
resources: [Lazy.string({ produce: () => this.bucketArn })],
effect: iam.Effect.ALLOW,
}));
replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['s3:GetObjectVersionForReplication', 's3:GetObjectVersionAcl', 's3:GetObjectVersionTagging'],
resources: [Lazy.string({ produce: () => this.arnForObjects('*') })],
effect: iam.Effect.ALLOW,
}));
if (destinationBuckets.length > 0) {
replicationRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['s3:ReplicateObject', 's3:ReplicateDelete', 's3:ReplicateTags', 's3:ObjectOwnerOverrideToBucketOwner'],
resources: destinationBuckets.map(bucket => bucket.arnForObjects('*')),
effect: iam.Effect.ALLOW,
}));
}

kmsKeys.forEach(kmsKey => {
kmsKey.grantEncrypt(replicationRole);
this.grantReplicationPermission(replicationRole, {
sourceDecryptionKey: props.encryptionKey,
destinations: props.replicationRules.map(rule => ({
encryptionKey: rule.kmsKey,
bucket: rule.destination,
})),
});

// If KMS key encryption is enabled on the source bucket, configure the decrypt permissions.
this.encryptionKey?.grantDecrypt(replicationRole);
} else {
replicationRole = props.replicationRole;
}
Expand Down
Loading
Loading