Skip to content

Commit 5891172

Browse files
authored
feat(s3): add BucketGrants (#36102)
Create a new class, `BucketGrants`, that has all the grants methods, and delegate the `grant*()` methods in `BucketBase` to it. **Note**: `grantPublicAccess()` first validates that it is allowed to proceed by checking the `disallowPublicAccess` protected property. To move this logic over to a new class, it was necessary to make `disallowPublicAccess` public. Since it was only set during instantiation and was protected, it was effectively immutable from the outside. But we can't make it immutable because this would be considered a backwards incompatible change. On the other hand, there is not harm. Users can disallow public access at any moment now, instead of just at creation of the object. For `CfnBucket`, granting public access will always be allowed. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 93a76e4 commit 5891172

File tree

3 files changed

+295
-114
lines changed

3 files changed

+295
-114
lines changed
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import { GrantReplicationPermissionProps } from './bucket';
2+
import * as perms from './perms';
3+
import { IBucketRef } from './s3.generated';
4+
import { AnyPrincipal, Grant, IEncryptedResource, IGrantable, IResourceWithPolicyV2 } from '../../aws-iam';
5+
import * as iam from '../../aws-iam/lib/grant';
6+
import { FeatureFlags, Lazy, ValidationError } from '../../core';
7+
import * as cxapi from '../../cx-api/index';
8+
9+
/**
10+
* A bucket in which public access can be allowed or disallowed.
11+
*/
12+
interface IPubliclyAccessibleBucket extends IBucketRef {
13+
/**
14+
* Whether public access is disallowed for this bucket
15+
*/
16+
readonly disallowPublicAccess?: boolean;
17+
}
18+
19+
/**
20+
* Collection of grant methods for a Bucket
21+
*/
22+
export class BucketGrants {
23+
/**
24+
* Creates grants for an IBucketRef
25+
*
26+
* @internal
27+
*/
28+
public static _fromBucket(bucket: IBucketRef): BucketGrants {
29+
return new BucketGrants(
30+
bucket,
31+
iam.GrantableResources.isEncryptedResource(bucket) ? bucket : undefined,
32+
iam.GrantableResources.isResourceWithPolicy(bucket) ? bucket : undefined,
33+
);
34+
}
35+
36+
private constructor(
37+
private readonly bucket: IPubliclyAccessibleBucket,
38+
private readonly encryptedResource?: IEncryptedResource,
39+
private readonly policyResource?: IResourceWithPolicyV2) {
40+
}
41+
42+
/**
43+
* Grant read permissions for this bucket and it's contents to an IAM
44+
* principal (Role/Group/User).
45+
*
46+
* If encryption is used, permission to use the key to decrypt the contents
47+
* of the bucket will also be granted to the same principal.
48+
*
49+
* @param identity The principal
50+
* @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*'). Parameter type is `any` but `string` should be passed in.
51+
*/
52+
public read(identity: IGrantable, objectsKeyPattern: any = '*') {
53+
return this.grant(identity, perms.BUCKET_READ_ACTIONS, perms.KEY_READ_ACTIONS,
54+
this.bucket.bucketRef.bucketArn,
55+
this.arnForObjects(objectsKeyPattern));
56+
}
57+
58+
/**
59+
* Grant write permissions for this bucket and it's contents to an IAM
60+
* principal (Role/Group/User).
61+
*
62+
* If encryption is used, permission to use the key to decrypt the contents
63+
* of the bucket will also be granted to the same principal.
64+
*
65+
* @param identity The principal
66+
* @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*'). Parameter type is `any` but `string` should be passed in.
67+
*/
68+
public write(identity: IGrantable, objectsKeyPattern: any = '*', allowedActionPatterns: string[] = []) {
69+
const grantedWriteActions = allowedActionPatterns.length > 0 ? allowedActionPatterns : this.writeActions;
70+
return this.grant(identity, grantedWriteActions, perms.KEY_WRITE_ACTIONS,
71+
this.bucket.bucketRef.bucketArn,
72+
this.arnForObjects(objectsKeyPattern));
73+
}
74+
75+
/**
76+
* Grants s3:DeleteObject* permission to an IAM principal for objects
77+
* in this bucket.
78+
*
79+
* @param grantee The principal
80+
* @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*'). Parameter type is `any` but `string` should be passed in.
81+
*/
82+
public delete(grantee: IGrantable, objectsKeyPattern: any = '*'): Grant {
83+
return this.grant(grantee, perms.BUCKET_DELETE_ACTIONS, [], this.arnForObjects(objectsKeyPattern));
84+
}
85+
86+
/**
87+
* Allows unrestricted access to objects from this bucket.
88+
*
89+
* IMPORTANT: This permission allows anyone to perform actions on S3 objects
90+
* in this bucket, which is useful for when you configure your bucket as a
91+
* website and want everyone to be able to read objects in the bucket without
92+
* needing to authenticate.
93+
*
94+
* Without arguments, this method will grant read ("s3:GetObject") access to
95+
* all objects ("*") in the bucket.
96+
*
97+
* The method returns the `iam.Grant` object, which can then be modified
98+
* as needed. For example, you can add a condition that will restrict access only
99+
* to an IPv4 range like this:
100+
*
101+
* const grant = bucket.grantPublicAccess();
102+
* grant.resourceStatement!.addCondition(‘IpAddress’, { “aws:SourceIp”: “54.240.143.0/24” });
103+
*
104+
* Note that if this `IBucket` refers to an existing bucket, possibly not
105+
* managed by CloudFormation, this method will have no effect, since it's
106+
* impossible to modify the policy of an existing bucket.
107+
*
108+
* @param keyPrefix the prefix of S3 object keys (e.g. `home/*`). Default is "*".
109+
* @param allowedActions the set of S3 actions to allow. Default is "s3:GetObject".
110+
*/
111+
public publicAccess(keyPrefix = '*', ...allowedActions: string[]) {
112+
if (this.bucket.disallowPublicAccess) {
113+
throw new ValidationError("Cannot grant public access when 'blockPublicPolicy' is enabled", this.bucket);
114+
}
115+
116+
allowedActions = allowedActions.length > 0 ? allowedActions : ['s3:GetObject'];
117+
118+
return this.grant(new AnyPrincipal(), allowedActions, [], this.arnForObjects(keyPrefix));
119+
}
120+
121+
/**
122+
* Grants s3:PutObject* and s3:Abort* permissions for this bucket to an IAM principal.
123+
*
124+
* If encryption is used, permission to use the key to encrypt the contents
125+
* of written files will also be granted to the same principal.
126+
* @param identity The principal
127+
* @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*'). Parameter type is `any` but `string` should be passed in.
128+
*/
129+
public put(identity: IGrantable, objectsKeyPattern: any = '*') {
130+
return this.grant(identity, this.putActions, perms.KEY_WRITE_ACTIONS, this.arnForObjects(objectsKeyPattern));
131+
}
132+
133+
/**
134+
* Grants s3:PutObjectAcl and s3:PutObjectVersionAcl permissions for this bucket to an IAM principal.
135+
*
136+
* If encryption is used, permission to use the key to encrypt the contents
137+
* of written files will also be granted to the same principal.
138+
* @param identity The principal
139+
* @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*'). Parameter type is `any` but `string` should be passed in.
140+
*/
141+
public putAcl(identity: IGrantable, objectsKeyPattern: string = '*') {
142+
return this.grant(identity, perms.BUCKET_PUT_ACL_ACTIONS, [], this.arnForObjects(objectsKeyPattern));
143+
}
144+
145+
/**
146+
* Grant read and write permissions for this bucket and it's contents to an IAM
147+
* principal (Role/Group/User).
148+
*
149+
* If encryption is used, permission to use the key to decrypt the contents
150+
* of the bucket will also be granted to the same principal.
151+
*
152+
* @param identity The principal
153+
* @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*'). Parameter type is `any` but `string` should be passed in.
154+
*/
155+
public readWrite(identity: IGrantable, objectsKeyPattern: any = '*') {
156+
const bucketActions = perms.BUCKET_READ_ACTIONS.concat(this.writeActions);
157+
// we need unique permissions because some permissions are common between read and write key actions
158+
const keyActions = [...new Set([...perms.KEY_READ_ACTIONS, ...perms.KEY_WRITE_ACTIONS])];
159+
160+
return this.grant(identity,
161+
bucketActions,
162+
keyActions,
163+
this.bucket.bucketRef.bucketArn,
164+
this.arnForObjects(objectsKeyPattern));
165+
}
166+
167+
private get putActions(): string[] {
168+
return FeatureFlags.of(this.bucket).isEnabled(cxapi.S3_GRANT_WRITE_WITHOUT_ACL)
169+
? perms.BUCKET_PUT_ACTIONS
170+
: perms.LEGACY_BUCKET_PUT_ACTIONS;
171+
}
172+
173+
private get writeActions(): string[] {
174+
return [
175+
...perms.BUCKET_DELETE_ACTIONS,
176+
...this.putActions,
177+
];
178+
}
179+
180+
/**
181+
* Grant replication permission to a principal.
182+
* This method allows the principal to perform replication operations on this bucket.
183+
*
184+
* Note that when calling this function for source or destination buckets that support KMS encryption,
185+
* you need to specify the KMS key for encryption and the KMS key for decryption, respectively.
186+
*
187+
* @param identity The principal to grant replication permission to.
188+
* @param props The properties of the replication source and destination buckets.
189+
*/
190+
public replicationPermission(identity: IGrantable, props: GrantReplicationPermissionProps): iam.Grant {
191+
if (props.destinations.length === 0) {
192+
throw new ValidationError('At least one destination bucket must be specified in the destinations array', this.bucket);
193+
}
194+
195+
// add permissions to the role
196+
// @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/setting-repl-config-perm-overview.html
197+
let result = this.grant(identity, ['s3:GetReplicationConfiguration', 's3:ListBucket'], [], Lazy.string({ produce: () => this.bucket.bucketRef.bucketArn }));
198+
199+
const g1 = this.grant(
200+
identity,
201+
['s3:GetObjectVersionForReplication', 's3:GetObjectVersionAcl', 's3:GetObjectVersionTagging'],
202+
[],
203+
Lazy.string({ produce: () => this.arnForObjects('*') }),
204+
);
205+
result = result.combine(g1);
206+
207+
const destinationBuckets = props.destinations.map(destination => destination.bucket);
208+
if (destinationBuckets.length > 0) {
209+
const bucketActions = ['s3:ReplicateObject', 's3:ReplicateDelete', 's3:ReplicateTags', 's3:ObjectOwnerOverrideToBucketOwner'];
210+
const g2 = (this.policyResource ? Grant.addToPrincipalOrResource({
211+
actions: bucketActions,
212+
grantee: identity,
213+
resourceArns: destinationBuckets.map(bucket => Lazy.string({ produce: () => bucket.arnForObjects('*') })),
214+
resource: this.policyResource,
215+
}) : Grant.addToPrincipal({
216+
actions: bucketActions,
217+
grantee: identity,
218+
resourceArns: destinationBuckets.map(bucket => Lazy.string({ produce: () => bucket.arnForObjects('*') })),
219+
}));
220+
221+
result = result.combine(g2);
222+
}
223+
224+
props.destinations.forEach(destination => {
225+
const g = destination.encryptionKey?.grantEncrypt(identity);
226+
if (g !== undefined) {
227+
result = result.combine(g);
228+
}
229+
});
230+
231+
// If KMS key encryption is enabled on the source bucket, configure the decrypt permissions.
232+
const grantOnKeyResult = this.encryptedResource?.grantOnKey(identity, 'kms:Decrypt');
233+
if (grantOnKeyResult?.grant) {
234+
result = result.combine(grantOnKeyResult.grant);
235+
}
236+
237+
return result;
238+
}
239+
240+
private grant(
241+
grantee: IGrantable,
242+
bucketActions: string[],
243+
keyActions: string[],
244+
resourceArn: string, ...otherResourceArns: string[]) {
245+
const resources = [resourceArn, ...otherResourceArns];
246+
247+
const result = (this.policyResource ? Grant.addToPrincipalOrResource({
248+
actions: bucketActions,
249+
grantee: grantee,
250+
resourceArns: resources,
251+
resource: this.policyResource,
252+
}) : Grant.addToPrincipal({
253+
actions: bucketActions,
254+
grantee: grantee,
255+
resourceArns: resources,
256+
}));
257+
258+
if (keyActions.length > 0) {
259+
this.encryptedResource?.grantOnKey(grantee, ...keyActions);
260+
}
261+
262+
return result;
263+
}
264+
265+
private arnForObjects(keyPattern: string): string {
266+
return `${this.bucket.bucketRef.bucketArn}/${keyPattern}`;
267+
}
268+
}

0 commit comments

Comments
 (0)