Skip to content

Commit 61e876b

Browse files
authored
feat(s3): throw ValidationError instead of untyped errors (#33031)
### Issue `aws-s3` for #32569 ### Description of changes Added an `UnscopedValidationError` for situations where now scope is available. This is to be used sparsely as it's less useful for users. ### Describe any new or updated permissions being added n/a ### Description of how you validated changes Existing tests. Exemptions granted as this is basically a refactor of existing code. ### 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 fa6f604 commit 61e876b

File tree

6 files changed

+95
-42
lines changed

6 files changed

+95
-42
lines changed

packages/aws-cdk-lib/.eslintrc.js

+9
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,13 @@ baseConfig.rules['import/no-extraneous-dependencies'] = [
1313
}
1414
];
1515

16+
17+
// no-throw-default-error
18+
const modules = ['aws-s3'];
19+
baseConfig.overrides.push({
20+
files: modules.map(m => `./${m}/lib/**`),
21+
rules: { "@cdklabs/no-throw-default-error": ['error'] },
22+
});
23+
24+
1625
module.exports = baseConfig;

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

+28-27
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
Tokenization,
2727
Annotations,
2828
} from '../../core';
29+
import { UnscopedValidationError, ValidationError } from '../../core/lib/errors';
2930
import { CfnReference } from '../../core/lib/private/cfn-reference';
3031
import { AutoDeleteObjectsProvider } from '../../custom-resource-handlers/dist/aws-s3/auto-delete-objects-provider.generated';
3132
import * as cxapi from '../../cx-api';
@@ -869,7 +870,7 @@ export abstract class BucketBase extends Resource implements IBucket {
869870
*/
870871
public grantPublicAccess(keyPrefix = '*', ...allowedActions: string[]) {
871872
if (this.disallowPublicAccess) {
872-
throw new Error("Cannot grant public access when 'blockPublicPolicy' is enabled");
873+
throw new ValidationError("Cannot grant public access when 'blockPublicPolicy' is enabled", this);
873874
}
874875

875876
allowedActions = allowedActions.length > 0 ? allowedActions : ['s3:GetObject'];
@@ -982,7 +983,7 @@ export abstract class BucketBase extends Resource implements IBucket {
982983
})).statementAdded);
983984
if (accessControlTransition) {
984985
if (!account) {
985-
throw new Error('account must be specified to override ownership access control transition');
986+
throw new ValidationError('account must be specified to override ownership access control transition', this);
986987
}
987988
results.push(this.addToResourcePolicy(new iam.PolicyStatement({
988989
actions: ['s3:ObjectOwnerOverrideToBucketOwner'],
@@ -1987,7 +1988,7 @@ export class Bucket extends BucketBase {
19871988

19881989
const bucketName = parseBucketName(scope, attrs);
19891990
if (!bucketName) {
1990-
throw new Error('Bucket name is required');
1991+
throw new ValidationError('Bucket name is required', scope);
19911992
}
19921993
Bucket.validateBucketName(bucketName, true);
19931994

@@ -2151,7 +2152,7 @@ export class Bucket extends BucketBase {
21512152
}
21522153

21532154
if (errors.length > 0) {
2154-
throw new Error(`Invalid S3 bucket name (value: ${bucketName})${EOL}${errors.join(EOL)}`);
2155+
throw new UnscopedValidationError(`Invalid S3 bucket name (value: ${bucketName})${EOL}${errors.join(EOL)}`);
21552156
}
21562157
}
21572158

@@ -2247,7 +2248,7 @@ export class Bucket extends BucketBase {
22472248
this.enforceSSLStatement();
22482249
this.minimumTLSVersionStatement(props.minimumTLSVersion);
22492250
} else if (props.minimumTLSVersion) {
2250-
throw new Error('\'enforceSSL\' must be enabled for \'minimumTLSVersion\' to be applied');
2251+
throw new ValidationError('\'enforceSSL\' must be enabled for \'minimumTLSVersion\' to be applied', this);
22512252
}
22522253

22532254
if (props.serverAccessLogsBucket instanceof Bucket) {
@@ -2281,15 +2282,15 @@ export class Bucket extends BucketBase {
22812282

22822283
if (props.publicReadAccess) {
22832284
if (props.blockPublicAccess === undefined) {
2284-
throw new Error('Cannot use \'publicReadAccess\' property on a bucket without allowing bucket-level public access through \'blockPublicAccess\' property.');
2285+
throw new ValidationError('Cannot use \'publicReadAccess\' property on a bucket without allowing bucket-level public access through \'blockPublicAccess\' property.', this);
22852286
}
22862287

22872288
this.grantPublicAccess();
22882289
}
22892290

22902291
if (props.autoDeleteObjects) {
22912292
if (props.removalPolicy !== RemovalPolicy.DESTROY) {
2292-
throw new Error('Cannot use \'autoDeleteObjects\' property on a bucket without setting removal policy to \'DESTROY\'.');
2293+
throw new ValidationError('Cannot use \'autoDeleteObjects\' property on a bucket without setting removal policy to \'DESTROY\'.', this);
22932294
}
22942295

22952296
this.enableAutoDeleteObjects();
@@ -2409,12 +2410,12 @@ export class Bucket extends BucketBase {
24092410

24102411
// if encryption key is set, encryption must be set to KMS or DSSE.
24112412
if (encryptionType !== BucketEncryption.DSSE && encryptionType !== BucketEncryption.KMS && props.encryptionKey) {
2412-
throw new Error(`encryptionKey is specified, so 'encryption' must be set to KMS or DSSE (value: ${encryptionType})`);
2413+
throw new ValidationError(`encryptionKey is specified, so 'encryption' must be set to KMS or DSSE (value: ${encryptionType})`, this);
24132414
}
24142415

24152416
// if bucketKeyEnabled is set, encryption can not be BucketEncryption.UNENCRYPTED
24162417
if (props.bucketKeyEnabled && encryptionType === BucketEncryption.UNENCRYPTED) {
2417-
throw new Error(`bucketKeyEnabled is specified, so 'encryption' must be set to KMS, DSSE or S3 (value: ${encryptionType})`);
2418+
throw new ValidationError(`bucketKeyEnabled is specified, so 'encryption' must be set to KMS, DSSE or S3 (value: ${encryptionType})`, this);
24182419
}
24192420

24202421
if (encryptionType === BucketEncryption.UNENCRYPTED) {
@@ -2497,7 +2498,7 @@ export class Bucket extends BucketBase {
24972498
return { bucketEncryption };
24982499
}
24992500

2500-
throw new Error(`Unexpected 'encryptionType': ${encryptionType}`);
2501+
throw new ValidationError(`Unexpected 'encryptionType': ${encryptionType}`, this);
25012502
}
25022503

25032504
/**
@@ -2521,7 +2522,7 @@ export class Bucket extends BucketBase {
25212522
if ((rule.expiredObjectDeleteMarker)
25222523
&& (rule.expiration || rule.expirationDate || self.parseTagFilters(rule.tagFilters))) {
25232524
// ExpiredObjectDeleteMarker cannot be specified with ExpirationInDays, ExpirationDate, or TagFilters.
2524-
throw new Error('ExpiredObjectDeleteMarker cannot be specified with expiration, ExpirationDate, or TagFilters.');
2525+
throw new ValidationError('ExpiredObjectDeleteMarker cannot be specified with expiration, ExpirationDate, or TagFilters.', self);
25252526
}
25262527

25272528
if (
@@ -2534,7 +2535,7 @@ export class Bucket extends BucketBase {
25342535
rule.noncurrentVersionTransitions === undefined &&
25352536
rule.transitions === undefined
25362537
) {
2537-
throw new Error('All rules for `lifecycleRules` must have at least one of the following properties: `abortIncompleteMultipartUploadAfter`, `expiration`, `expirationDate`, `expiredObjectDeleteMarker`, `noncurrentVersionExpiration`, `noncurrentVersionsToRetain`, `noncurrentVersionTransitions`, or `transitions`');
2538+
throw new ValidationError('All rules for `lifecycleRules` must have at least one of the following properties: `abortIncompleteMultipartUploadAfter`, `expiration`, `expirationDate`, `expiredObjectDeleteMarker`, `noncurrentVersionExpiration`, `noncurrentVersionsToRetain`, `noncurrentVersionTransitions`, or `transitions`', self);
25382539
}
25392540

25402541
const x: CfnBucket.RuleProperty = {
@@ -2580,7 +2581,7 @@ export class Bucket extends BucketBase {
25802581
props.encryption &&
25812582
[BucketEncryption.KMS_MANAGED, BucketEncryption.DSSE_MANAGED].includes(props.encryption)
25822583
) {
2583-
throw new Error('Default bucket encryption with KMS managed or DSSE managed key is not supported for Server Access Logging target buckets');
2584+
throw new ValidationError('Default bucket encryption with KMS managed or DSSE managed key is not supported for Server Access Logging target buckets', this);
25842585
}
25852586

25862587
// When there is an encryption key exists for the server access logs bucket, grant permission to the S3 logging SP.
@@ -2657,7 +2658,7 @@ export class Bucket extends BucketBase {
26572658
}
26582659

26592660
if (accessControlRequiresObjectOwnership && this.objectOwnership === ObjectOwnership.BUCKET_OWNER_ENFORCED) {
2660-
throw new Error (`objectOwnership must be set to "${ObjectOwnership.OBJECT_WRITER}" when accessControl is "${this.accessControl}"`);
2661+
throw new ValidationError (`objectOwnership must be set to "${ObjectOwnership.OBJECT_WRITER}" when accessControl is "${this.accessControl}"`, this);
26612662
}
26622663

26632664
return {
@@ -2703,7 +2704,7 @@ export class Bucket extends BucketBase {
27032704
return undefined;
27042705
}
27052706
if (objectLockEnabled === false && objectLockDefaultRetention) {
2706-
throw new Error('Object Lock must be enabled to configure default retention settings');
2707+
throw new ValidationError('Object Lock must be enabled to configure default retention settings', this);
27072708
}
27082709

27092710
return {
@@ -2723,16 +2724,16 @@ export class Bucket extends BucketBase {
27232724
}
27242725

27252726
if (props.websiteErrorDocument && !props.websiteIndexDocument) {
2726-
throw new Error('"websiteIndexDocument" is required if "websiteErrorDocument" is set');
2727+
throw new ValidationError('"websiteIndexDocument" is required if "websiteErrorDocument" is set', this);
27272728
}
27282729

27292730
if (props.websiteRedirect && (props.websiteErrorDocument || props.websiteIndexDocument || props.websiteRoutingRules)) {
2730-
throw new Error('"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used');
2731+
throw new ValidationError('"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used', this);
27312732
}
27322733

27332734
const routingRules = props.websiteRoutingRules ? props.websiteRoutingRules.map<CfnBucket.RoutingRuleProperty>((rule) => {
27342735
if (rule.condition && rule.condition.httpErrorCodeReturnedEquals == null && rule.condition.keyPrefixEquals == null) {
2735-
throw new Error('The condition property cannot be an empty object');
2736+
throw new ValidationError('The condition property cannot be an empty object', this);
27362737
}
27372738

27382739
return {
@@ -2762,17 +2763,17 @@ export class Bucket extends BucketBase {
27622763
}
27632764

27642765
if (!props.versioned) {
2765-
throw new Error('Replication rules require versioning to be enabled on the bucket');
2766+
throw new ValidationError('Replication rules require versioning to be enabled on the bucket', this);
27662767
}
27672768
if (props.replicationRules.length > 1 && props.replicationRules.some(rule => rule.priority === undefined)) {
2768-
throw new Error('\'priority\' must be specified for all replication rules when there are multiple rules');
2769+
throw new ValidationError('\'priority\' must be specified for all replication rules when there are multiple rules', this);
27692770
}
27702771
props.replicationRules.forEach(rule => {
27712772
if (rule.replicationTimeControl && !rule.metrics) {
2772-
throw new Error('\'replicationTimeControlMetrics\' must be enabled when \'replicationTimeControl\' is enabled.');
2773+
throw new ValidationError('\'replicationTimeControlMetrics\' must be enabled when \'replicationTimeControl\' is enabled.', this);
27732774
}
27742775
if (rule.deleteMarkerReplication && rule.filter?.tags) {
2775-
throw new Error('tag filter cannot be specified when \'deleteMarkerReplication\' is enabled.');
2776+
throw new ValidationError('tag filter cannot be specified when \'deleteMarkerReplication\' is enabled.', this);
27762777
}
27772778
});
27782779

@@ -2846,7 +2847,7 @@ export class Bucket extends BucketBase {
28462847
if (isCrossAccount) {
28472848
Annotations.of(this).addInfo('For Cross-account S3 replication, ensure to set up permissions on source bucket using method addReplicationPolicy() ');
28482849
} else if (rule.accessControlTransition) {
2849-
throw new Error('accessControlTranslation is only supported for cross-account replication');
2850+
throw new ValidationError('accessControlTranslation is only supported for cross-account replication', this);
28502851
}
28512852

28522853
return {
@@ -2921,7 +2922,7 @@ export class Bucket extends BucketBase {
29212922
conditions: conditions,
29222923
}));
29232924
} else if (this.accessControl && this.accessControl !== BucketAccessControl.LOG_DELIVERY_WRITE) {
2924-
throw new Error("Cannot enable log delivery to this bucket because the bucket's ACL has been set and can't be changed");
2925+
throw new ValidationError("Cannot enable log delivery to this bucket because the bucket's ACL has been set and can't be changed", this);
29252926
} else {
29262927
this.accessControl = BucketAccessControl.LOG_DELIVERY_WRITE;
29272928
}
@@ -2937,7 +2938,7 @@ export class Bucket extends BucketBase {
29372938
const format = inventory.format ?? InventoryFormat.CSV;
29382939
const frequency = inventory.frequency ?? InventoryFrequency.WEEKLY;
29392940
if (inventory.inventoryId !== undefined && (inventory.inventoryId.length > 64 || inventoryIdValidationRegex.test(inventory.inventoryId))) {
2940-
throw new Error(`inventoryId should not exceed 64 characters and should not contain special characters except . and -, got ${inventory.inventoryId}`);
2941+
throw new ValidationError(`inventoryId should not exceed 64 characters and should not contain special characters except . and -, got ${inventory.inventoryId}`, this);
29412942
}
29422943
const id = inventory.inventoryId ?? `${this.node.id}Inventory${index}`.replace(inventoryIdValidationRegex, '').slice(-64);
29432944

@@ -3523,10 +3524,10 @@ export class ObjectLockRetention {
35233524
private constructor(mode: ObjectLockMode, duration: Duration) {
35243525
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-managing.html#object-lock-managing-retention-limits
35253526
if (duration.toDays() > 365 * 100) {
3526-
throw new Error('Object Lock retention duration must be less than 100 years');
3527+
throw new UnscopedValidationError('Object Lock retention duration must be less than 100 years');
35273528
}
35283529
if (duration.toDays() < 1) {
3529-
throw new Error('Object Lock retention duration must be at least 1 day');
3530+
throw new UnscopedValidationError('Object Lock retention duration must be at least 1 day');
35303531
}
35313532

35323533
this.mode = mode;

packages/aws-cdk-lib/aws-s3/lib/notifications-resource/notifications-resource.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Construct, IConstruct } from 'constructs';
22
import { NotificationsResourceHandler } from './notifications-resource-handler';
33
import * as iam from '../../../aws-iam';
44
import * as cdk from '../../../core';
5+
import { ValidationError } from '../../../core/lib/errors';
56
import * as cxapi from '../../../cx-api';
67
import { Bucket, IBucket, EventType, NotificationKeyFilter } from '../bucket';
78
import { BucketNotificationDestinationType, IBucketNotificationDestination } from '../destination';
@@ -71,7 +72,7 @@ export class BucketNotifications extends Construct {
7172
const targetProps = target.bind(this, this.bucket);
7273
const commonConfig: CommonConfiguration = {
7374
Events: [event],
74-
Filter: renderFilters(filters),
75+
Filter: renderFilters(filters, this),
7576
};
7677

7778
// if the target specifies any dependencies, add them to the custom resource.
@@ -96,7 +97,7 @@ export class BucketNotifications extends Construct {
9697
break;
9798

9899
default:
99-
throw new Error('Unsupported notification target type:' + BucketNotificationDestinationType[targetProps.type]);
100+
throw new ValidationError('Unsupported notification target type:' + BucketNotificationDestinationType[targetProps.type], this);
100101
}
101102
}
102103

@@ -171,7 +172,7 @@ export class BucketNotifications extends Construct {
171172
}
172173
}
173174

174-
function renderFilters(filters?: NotificationKeyFilter[]): Filter | undefined {
175+
function renderFilters(filters: NotificationKeyFilter[], scope: BucketNotifications): Filter | undefined {
175176
if (!filters || filters.length === 0) {
176177
return undefined;
177178
}
@@ -182,20 +183,20 @@ function renderFilters(filters?: NotificationKeyFilter[]): Filter | undefined {
182183

183184
for (const rule of filters) {
184185
if (!rule.suffix && !rule.prefix) {
185-
throw new Error('NotificationKeyFilter must specify `prefix` and/or `suffix`');
186+
throw new ValidationError('NotificationKeyFilter must specify `prefix` and/or `suffix`', scope);
186187
}
187188

188189
if (rule.suffix) {
189190
if (hasSuffix) {
190-
throw new Error('Cannot specify more than one suffix rule in a filter.');
191+
throw new ValidationError('Cannot specify more than one suffix rule in a filter.', scope);
191192
}
192193
renderedRules.push({ Name: 'suffix', Value: rule.suffix });
193194
hasSuffix = true;
194195
}
195196

196197
if (rule.prefix) {
197198
if (hasPrefix) {
198-
throw new Error('Cannot specify more than one prefix rule in a filter.');
199+
throw new ValidationError('Cannot specify more than one prefix rule in a filter.', scope);
199200
}
200201
renderedRules.push({ Name: 'prefix', Value: rule.prefix });
201202
hasPrefix = true;

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IConstruct } from 'constructs';
22
import { BucketAttributes } from './bucket';
33
import * as cdk from '../../core';
4+
import { ValidationError } from '../../core/lib/errors';
45

56
export function parseBucketArn(construct: IConstruct, props: BucketAttributes): string {
67

@@ -20,7 +21,7 @@ export function parseBucketArn(construct: IConstruct, props: BucketAttributes):
2021
});
2122
}
2223

23-
throw new Error('Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed');
24+
throw new ValidationError('Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed', construct);
2425
}
2526

2627
export function parseBucketName(construct: IConstruct, props: BucketAttributes): string | undefined {

0 commit comments

Comments
 (0)