UNPKG

@aws-cdk/aws-s3

Version:

The CDK Construct Library for AWS::S3

1,195 lines (1,194 loc) 224 kB
"use strict"; var _a, _b, _c, _d; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReplaceKey = exports.BucketAccessControl = exports.EventType = exports.BucketEncryption = exports.Bucket = exports.ObjectOwnership = exports.InventoryObjectVersion = exports.InventoryFrequency = exports.InventoryFormat = exports.RedirectProtocol = exports.HttpMethods = exports.BlockPublicAccess = exports.BucketBase = void 0; const jsiiDeprecationWarnings = require("../.warnings.jsii.js"); const JSII_RTTI_SYMBOL_1 = Symbol.for("jsii.rtti"); const os_1 = require("os"); const path = require("path"); const events = require("@aws-cdk/aws-events"); const iam = require("@aws-cdk/aws-iam"); const kms = require("@aws-cdk/aws-kms"); const core_1 = require("@aws-cdk/core"); const cxapi = require("@aws-cdk/cx-api"); const bucket_policy_1 = require("./bucket-policy"); const notifications_resource_1 = require("./notifications-resource"); const perms = require("./perms"); const s3_generated_1 = require("./s3.generated"); const util_1 = require("./util"); const AUTO_DELETE_OBJECTS_RESOURCE_TYPE = 'Custom::S3AutoDeleteObjects'; const AUTO_DELETE_OBJECTS_TAG = 'aws-cdk:auto-delete-objects'; /** * Represents an S3 Bucket. * * Buckets can be either defined within this stack: * * new Bucket(this, 'MyBucket', { props }); * * Or imported from an existing bucket: * * Bucket.import(this, 'MyImportedBucket', { bucketArn: ... }); * * You can also export a bucket and import it into another stack: * * const ref = myBucket.export(); * Bucket.import(this, 'MyImportedBucket', ref); * */ class BucketBase extends core_1.Resource { constructor(scope, id, props = {}) { super(scope, id, props); } /** * Define a CloudWatch event that triggers when something happens to this repository * * Requires that there exists at least one CloudTrail Trail in your account * that captures the event. This method will not create the Trail. * * @param id The id of the rule * @param options Options for adding the rule */ onCloudTrailEvent(id, options = {}) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_OnCloudTrailBucketEventOptions(options); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.onCloudTrailEvent); } throw error; } const rule = new events.Rule(this, id, options); rule.addTarget(options.target); rule.addEventPattern({ source: ['aws.s3'], detailType: ['AWS API Call via CloudTrail'], detail: { resources: { ARN: options.paths?.map(p => this.arnForObjects(p)) ?? [this.bucketArn], }, }, }); return rule; } /** * Defines an AWS CloudWatch event that triggers when an object is uploaded * to the specified paths (keys) in this bucket using the PutObject API call. * * Note that some tools like `aws s3 cp` will automatically use either * PutObject or the multipart upload API depending on the file size, * so using `onCloudTrailWriteObject` may be preferable. * * Requires that there exists at least one CloudTrail Trail in your account * that captures the event. This method will not create the Trail. * * @param id The id of the rule * @param options Options for adding the rule */ onCloudTrailPutObject(id, options = {}) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_OnCloudTrailBucketEventOptions(options); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.onCloudTrailPutObject); } throw error; } const rule = this.onCloudTrailEvent(id, options); rule.addEventPattern({ detail: { eventName: ['PutObject'], }, }); return rule; } /** * Defines an AWS CloudWatch event that triggers when an object at the * specified paths (keys) in this bucket are written to. This includes * the events PutObject, CopyObject, and CompleteMultipartUpload. * * Note that some tools like `aws s3 cp` will automatically use either * PutObject or the multipart upload API depending on the file size, * so using this method may be preferable to `onCloudTrailPutObject`. * * Requires that there exists at least one CloudTrail Trail in your account * that captures the event. This method will not create the Trail. * * @param id The id of the rule * @param options Options for adding the rule */ onCloudTrailWriteObject(id, options = {}) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_OnCloudTrailBucketEventOptions(options); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.onCloudTrailWriteObject); } throw error; } const rule = this.onCloudTrailEvent(id, options); rule.addEventPattern({ detail: { eventName: [ 'CompleteMultipartUpload', 'CopyObject', 'PutObject', ], requestParameters: { bucketName: [this.bucketName], key: options.paths, }, }, }); return rule; } /** * Adds a statement to the resource policy for a principal (i.e. * account/role/service) to perform actions on this bucket and/or its * contents. Use `bucketArn` and `arnForObjects(keys)` to obtain ARNs for * this bucket or objects. * * Note that the policy statement may or may not be added to the policy. * For example, when an `IBucket` is created from an existing bucket, * it's not possible to tell whether the bucket already has a policy * attached, let alone to re-use that policy to add more statements to it. * So it's safest to do nothing in these cases. * * @param permission the policy statement to be added to the bucket's * policy. * @returns metadata about the execution of this method. If the policy * was not added, the value of `statementAdded` will be `false`. You * should always check this value to make sure that the operation was * actually carried out. Otherwise, synthesis and deploy will terminate * silently, which may be confusing. */ addToResourcePolicy(permission) { if (!this.policy && this.autoCreatePolicy) { this.policy = new bucket_policy_1.BucketPolicy(this, 'Policy', { bucket: this }); } if (this.policy) { this.policy.document.addStatements(permission); return { statementAdded: true, policyDependable: this.policy }; } return { statementAdded: false }; } validate() { const errors = super.validate(); errors.push(...this.policy?.document.validateForResourcePolicy() || []); return errors; } /** * The https URL of an S3 object. Specify `regional: false` at the options * for non-regional URLs. For example: * * - `https://s3.us-west-1.amazonaws.com/onlybucket` * - `https://s3.us-west-1.amazonaws.com/bucket/key` * - `https://s3.cn-north-1.amazonaws.com.cn/china-bucket/mykey` * * @param key The S3 key of the object. If not specified, the URL of the * bucket is returned. * @returns an ObjectS3Url token */ urlForObject(key) { const stack = core_1.Stack.of(this); const prefix = `https://s3.${this.env.region}.${stack.urlSuffix}/`; if (typeof key !== 'string') { return this.urlJoin(prefix, this.bucketName); } return this.urlJoin(prefix, this.bucketName, key); } /** * The https Transfer Acceleration URL of an S3 object. Specify `dualStack: true` at the options * for dual-stack endpoint (connect to the bucket over IPv6). For example: * * - `https://bucket.s3-accelerate.amazonaws.com` * - `https://bucket.s3-accelerate.amazonaws.com/key` * * @param key The S3 key of the object. If not specified, the URL of the * bucket is returned. * @param options Options for generating URL. * @returns an TransferAccelerationUrl token */ transferAccelerationUrlForObject(key, options) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_TransferAccelerationUrlOptions(options); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.transferAccelerationUrlForObject); } throw error; } const dualStack = options?.dualStack ? '.dualstack' : ''; const prefix = `https://${this.bucketName}.s3-accelerate${dualStack}.amazonaws.com/`; if (typeof key !== 'string') { return this.urlJoin(prefix); } return this.urlJoin(prefix, key); } /** * The virtual hosted-style URL of an S3 object. Specify `regional: false` at * the options for non-regional URL. For example: * * - `https://only-bucket.s3.us-west-1.amazonaws.com` * - `https://bucket.s3.us-west-1.amazonaws.com/key` * - `https://bucket.s3.amazonaws.com/key` * - `https://china-bucket.s3.cn-north-1.amazonaws.com.cn/mykey` * * @param key The S3 key of the object. If not specified, the URL of the * bucket is returned. * @param options Options for generating URL. * @returns an ObjectS3Url token */ virtualHostedUrlForObject(key, options) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_VirtualHostedStyleUrlOptions(options); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.virtualHostedUrlForObject); } throw error; } const domainName = options?.regional ?? true ? this.bucketRegionalDomainName : this.bucketDomainName; const prefix = `https://${domainName}`; if (typeof key !== 'string') { return prefix; } return this.urlJoin(prefix, key); } /** * The S3 URL of an S3 object. For example: * * - `s3://onlybucket` * - `s3://bucket/key` * * @param key The S3 key of the object. If not specified, the S3 URL of the * bucket is returned. * @returns an ObjectS3Url token */ s3UrlForObject(key) { const prefix = 's3://'; if (typeof key !== 'string') { return this.urlJoin(prefix, this.bucketName); } return this.urlJoin(prefix, this.bucketName, key); } /** * Returns an ARN that represents all objects within the bucket that match * the key pattern specified. To represent all keys, specify ``"*"``. * * If you need to specify a keyPattern with multiple components, concatenate them into a single string, e.g.: * * arnForObjects(`home/${team}/${user}/*`) * */ arnForObjects(keyPattern) { return `${this.bucketArn}/${keyPattern}`; } /** * Grant read permissions for this bucket and it's contents to an IAM * principal (Role/Group/User). * * If encryption is used, permission to use the key to decrypt the contents * of the bucket will also be granted to the same principal. * * @param identity The principal * @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*') */ grantRead(identity, objectsKeyPattern = '*') { return this.grant(identity, perms.BUCKET_READ_ACTIONS, perms.KEY_READ_ACTIONS, this.bucketArn, this.arnForObjects(objectsKeyPattern)); } grantWrite(identity, objectsKeyPattern = '*') { return this.grant(identity, this.writeActions, perms.KEY_WRITE_ACTIONS, this.bucketArn, this.arnForObjects(objectsKeyPattern)); } /** * Grants s3:PutObject* and s3:Abort* permissions for this bucket to an IAM principal. * * If encryption is used, permission to use the key to encrypt the contents * of written files will also be granted to the same principal. * @param identity The principal * @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*') */ grantPut(identity, objectsKeyPattern = '*') { return this.grant(identity, this.putActions, perms.KEY_WRITE_ACTIONS, this.arnForObjects(objectsKeyPattern)); } grantPutAcl(identity, objectsKeyPattern = '*') { return this.grant(identity, perms.BUCKET_PUT_ACL_ACTIONS, [], this.arnForObjects(objectsKeyPattern)); } /** * Grants s3:DeleteObject* permission to an IAM principal for objects * in this bucket. * * @param identity The principal * @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*') */ grantDelete(identity, objectsKeyPattern = '*') { return this.grant(identity, perms.BUCKET_DELETE_ACTIONS, [], this.arnForObjects(objectsKeyPattern)); } grantReadWrite(identity, objectsKeyPattern = '*') { const bucketActions = perms.BUCKET_READ_ACTIONS.concat(this.writeActions); // we need unique permissions because some permissions are common between read and write key actions const keyActions = [...new Set([...perms.KEY_READ_ACTIONS, ...perms.KEY_WRITE_ACTIONS])]; return this.grant(identity, bucketActions, keyActions, this.bucketArn, this.arnForObjects(objectsKeyPattern)); } /** * Allows unrestricted access to objects from this bucket. * * IMPORTANT: This permission allows anyone to perform actions on S3 objects * in this bucket, which is useful for when you configure your bucket as a * website and want everyone to be able to read objects in the bucket without * needing to authenticate. * * Without arguments, this method will grant read ("s3:GetObject") access to * all objects ("*") in the bucket. * * The method returns the `iam.Grant` object, which can then be modified * as needed. For example, you can add a condition that will restrict access only * to an IPv4 range like this: * * const grant = bucket.grantPublicAccess(); * grant.resourceStatement!.addCondition(‘IpAddress’, { “aws:SourceIp”: “54.240.143.0/24” }); * * Note that if this `IBucket` refers to an existing bucket, possibly not * managed by CloudFormation, this method will have no effect, since it's * impossible to modify the policy of an existing bucket. * * @param keyPrefix the prefix of S3 object keys (e.g. `home/*`). Default is "*". * @param allowedActions the set of S3 actions to allow. Default is "s3:GetObject". */ grantPublicAccess(keyPrefix = '*', ...allowedActions) { if (this.disallowPublicAccess) { throw new Error("Cannot grant public access when 'blockPublicPolicy' is enabled"); } allowedActions = allowedActions.length > 0 ? allowedActions : ['s3:GetObject']; return iam.Grant.addToPrincipalOrResource({ actions: allowedActions, resourceArns: [this.arnForObjects(keyPrefix)], grantee: new iam.AnyPrincipal(), resource: this, }); } /** * Adds a bucket notification event destination. * @param event The event to trigger the notification * @param dest The notification destination (Lambda, SNS Topic or SQS Queue) * * @param filters S3 object key filter rules to determine which objects * trigger this event. Each filter must include a `prefix` and/or `suffix` * that will be matched against the s3 object key. Refer to the S3 Developer Guide * for details about allowed filter rules. * * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#notification-how-to-filtering * * @example * * declare const myLambda: lambda.Function; * const bucket = new s3.Bucket(this, 'MyBucket'); * bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(myLambda), {prefix: 'home/myusername/*'}); * * @see * https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html */ addEventNotification(event, dest, ...filters) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_EventType(event); jsiiDeprecationWarnings._aws_cdk_aws_s3_IBucketNotificationDestination(dest); jsiiDeprecationWarnings._aws_cdk_aws_s3_NotificationKeyFilter(filters); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.addEventNotification); } throw error; } this.withNotifications(notifications => notifications.addNotification(event, dest, ...filters)); } withNotifications(cb) { if (!this.notifications) { this.notifications = new notifications_resource_1.BucketNotifications(this, 'Notifications', { bucket: this, handlerRole: this.notificationsHandlerRole, }); } cb(this.notifications); } /** * Subscribes a destination to receive notifications when an object is * created in the bucket. This is identical to calling * `onEvent(EventType.OBJECT_CREATED)`. * * @param dest The notification destination (see onEvent) * @param filters Filters (see onEvent) */ addObjectCreatedNotification(dest, ...filters) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_IBucketNotificationDestination(dest); jsiiDeprecationWarnings._aws_cdk_aws_s3_NotificationKeyFilter(filters); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.addObjectCreatedNotification); } throw error; } return this.addEventNotification(EventType.OBJECT_CREATED, dest, ...filters); } /** * Subscribes a destination to receive notifications when an object is * removed from the bucket. This is identical to calling * `onEvent(EventType.OBJECT_REMOVED)`. * * @param dest The notification destination (see onEvent) * @param filters Filters (see onEvent) */ addObjectRemovedNotification(dest, ...filters) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_IBucketNotificationDestination(dest); jsiiDeprecationWarnings._aws_cdk_aws_s3_NotificationKeyFilter(filters); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.addObjectRemovedNotification); } throw error; } return this.addEventNotification(EventType.OBJECT_REMOVED, dest, ...filters); } enableEventBridgeNotification() { this.withNotifications(notifications => notifications.enableEventBridgeNotification()); } get writeActions() { return [ ...perms.BUCKET_DELETE_ACTIONS, ...this.putActions, ]; } get putActions() { return core_1.FeatureFlags.of(this).isEnabled(cxapi.S3_GRANT_WRITE_WITHOUT_ACL) ? perms.BUCKET_PUT_ACTIONS : perms.LEGACY_BUCKET_PUT_ACTIONS; } urlJoin(...components) { return components.reduce((result, component) => { if (result.endsWith('/')) { result = result.slice(0, -1); } if (component.startsWith('/')) { component = component.slice(1); } return `${result}/${component}`; }); } grant(grantee, bucketActions, keyActions, resourceArn, ...otherResourceArns) { const resources = [resourceArn, ...otherResourceArns]; const ret = iam.Grant.addToPrincipalOrResource({ grantee, actions: bucketActions, resourceArns: resources, resource: this, }); if (this.encryptionKey && keyActions && keyActions.length !== 0) { this.encryptionKey.grant(grantee, ...keyActions); } return ret; } } exports.BucketBase = BucketBase; _a = JSII_RTTI_SYMBOL_1; BucketBase[_a] = { fqn: "@aws-cdk/aws-s3.BucketBase", version: "1.204.0" }; class BlockPublicAccess { constructor(options) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_BlockPublicAccessOptions(options); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, BlockPublicAccess); } throw error; } this.blockPublicAcls = options.blockPublicAcls; this.blockPublicPolicy = options.blockPublicPolicy; this.ignorePublicAcls = options.ignorePublicAcls; this.restrictPublicBuckets = options.restrictPublicBuckets; } } exports.BlockPublicAccess = BlockPublicAccess; _b = JSII_RTTI_SYMBOL_1; BlockPublicAccess[_b] = { fqn: "@aws-cdk/aws-s3.BlockPublicAccess", version: "1.204.0" }; BlockPublicAccess.BLOCK_ALL = new BlockPublicAccess({ blockPublicAcls: true, blockPublicPolicy: true, ignorePublicAcls: true, restrictPublicBuckets: true, }); BlockPublicAccess.BLOCK_ACLS = new BlockPublicAccess({ blockPublicAcls: true, ignorePublicAcls: true, }); /** * All http request methods */ var HttpMethods; (function (HttpMethods) { /** * The GET method requests a representation of the specified resource. */ HttpMethods["GET"] = "GET"; /** * The PUT method replaces all current representations of the target resource with the request payload. */ HttpMethods["PUT"] = "PUT"; /** * The HEAD method asks for a response identical to that of a GET request, but without the response body. */ HttpMethods["HEAD"] = "HEAD"; /** * The POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server. */ HttpMethods["POST"] = "POST"; /** * The DELETE method deletes the specified resource. */ HttpMethods["DELETE"] = "DELETE"; })(HttpMethods = exports.HttpMethods || (exports.HttpMethods = {})); /** * All http request methods */ var RedirectProtocol; (function (RedirectProtocol) { RedirectProtocol["HTTP"] = "http"; RedirectProtocol["HTTPS"] = "https"; })(RedirectProtocol = exports.RedirectProtocol || (exports.RedirectProtocol = {})); /** * All supported inventory list formats. */ var InventoryFormat; (function (InventoryFormat) { /** * Generate the inventory list as CSV. */ InventoryFormat["CSV"] = "CSV"; /** * Generate the inventory list as Parquet. */ InventoryFormat["PARQUET"] = "Parquet"; /** * Generate the inventory list as ORC. */ InventoryFormat["ORC"] = "ORC"; })(InventoryFormat = exports.InventoryFormat || (exports.InventoryFormat = {})); /** * All supported inventory frequencies. */ var InventoryFrequency; (function (InventoryFrequency) { /** * A report is generated every day. */ InventoryFrequency["DAILY"] = "Daily"; /** * A report is generated every Sunday (UTC timezone) after the initial report. */ InventoryFrequency["WEEKLY"] = "Weekly"; })(InventoryFrequency = exports.InventoryFrequency || (exports.InventoryFrequency = {})); /** * Inventory version support. */ var InventoryObjectVersion; (function (InventoryObjectVersion) { /** * Includes all versions of each object in the report. */ InventoryObjectVersion["ALL"] = "All"; /** * Includes only the current version of each object in the report. */ InventoryObjectVersion["CURRENT"] = "Current"; })(InventoryObjectVersion = exports.InventoryObjectVersion || (exports.InventoryObjectVersion = {})); /** * The ObjectOwnership of the bucket. * * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/about-object-ownership.html * */ var ObjectOwnership; (function (ObjectOwnership) { /** * ACLs are disabled, and the bucket owner automatically owns * and has full control over every object in the bucket. * ACLs no longer affect permissions to data in the S3 bucket. * The bucket uses policies to define access control. */ ObjectOwnership["BUCKET_OWNER_ENFORCED"] = "BucketOwnerEnforced"; /** * Objects uploaded to the bucket change ownership to the bucket owner . */ ObjectOwnership["BUCKET_OWNER_PREFERRED"] = "BucketOwnerPreferred"; /** * The uploading account will own the object. */ ObjectOwnership["OBJECT_WRITER"] = "ObjectWriter"; })(ObjectOwnership = exports.ObjectOwnership || (exports.ObjectOwnership = {})); /** * An S3 bucket with associated policy objects * * This bucket does not yet have all features that exposed by the underlying * BucketResource. */ class Bucket extends BucketBase { constructor(scope, id, props = {}) { super(scope, id, { physicalName: props.bucketName, }); this.autoCreatePolicy = true; this.lifecycleRules = []; this.metrics = []; this.cors = []; this.inventories = []; try { jsiiDeprecationWarnings._aws_cdk_aws_s3_BucketProps(props); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, Bucket); } throw error; } this.notificationsHandlerRole = props.notificationsHandlerRole; const { bucketEncryption, encryptionKey } = this.parseEncryption(props); Bucket.validateBucketName(this.physicalName); const websiteConfiguration = this.renderWebsiteConfiguration(props); this.isWebsite = (websiteConfiguration !== undefined); const resource = new s3_generated_1.CfnBucket(this, 'Resource', { bucketName: this.physicalName, bucketEncryption, versioningConfiguration: props.versioned ? { status: 'Enabled' } : undefined, lifecycleConfiguration: core_1.Lazy.any({ produce: () => this.parseLifecycleConfiguration() }), websiteConfiguration, publicAccessBlockConfiguration: props.blockPublicAccess, metricsConfigurations: core_1.Lazy.any({ produce: () => this.parseMetricConfiguration() }), corsConfiguration: core_1.Lazy.any({ produce: () => this.parseCorsConfiguration() }), accessControl: core_1.Lazy.string({ produce: () => this.accessControl }), loggingConfiguration: this.parseServerAccessLogs(props), inventoryConfigurations: core_1.Lazy.any({ produce: () => this.parseInventoryConfiguration() }), ownershipControls: this.parseOwnershipControls(props), accelerateConfiguration: props.transferAcceleration ? { accelerationStatus: 'Enabled' } : undefined, intelligentTieringConfigurations: this.parseTieringConfig(props), }); this._resource = resource; resource.applyRemovalPolicy(props.removalPolicy); this.versioned = props.versioned; this.encryptionKey = encryptionKey; this.eventBridgeEnabled = props.eventBridgeEnabled; this.bucketName = this.getResourceNameAttribute(resource.ref); this.bucketArn = this.getResourceArnAttribute(resource.attrArn, { region: '', account: '', service: 's3', resource: this.physicalName, }); this.bucketDomainName = resource.attrDomainName; this.bucketWebsiteUrl = resource.attrWebsiteUrl; this.bucketWebsiteDomainName = core_1.Fn.select(2, core_1.Fn.split('/', this.bucketWebsiteUrl)); this.bucketDualStackDomainName = resource.attrDualStackDomainName; this.bucketRegionalDomainName = resource.attrRegionalDomainName; this.disallowPublicAccess = props.blockPublicAccess && props.blockPublicAccess.blockPublicPolicy; this.accessControl = props.accessControl; // Enforce AWS Foundational Security Best Practice if (props.enforceSSL) { this.enforceSSLStatement(); } if (props.serverAccessLogsBucket instanceof Bucket) { props.serverAccessLogsBucket.allowLogDelivery(); } for (const inventory of props.inventories ?? []) { this.addInventory(inventory); } // Add all bucket metric configurations rules (props.metrics || []).forEach(this.addMetric.bind(this)); // Add all cors configuration rules (props.cors || []).forEach(this.addCorsRule.bind(this)); // Add all lifecycle rules (props.lifecycleRules || []).forEach(this.addLifecycleRule.bind(this)); if (props.publicReadAccess) { this.grantPublicAccess(); } if (props.autoDeleteObjects) { if (props.removalPolicy !== core_1.RemovalPolicy.DESTROY) { throw new Error('Cannot use \'autoDeleteObjects\' property on a bucket without setting removal policy to \'DESTROY\'.'); } this.enableAutoDeleteObjects(); } if (this.eventBridgeEnabled) { this.enableEventBridgeNotification(); } } static fromBucketArn(scope, id, bucketArn) { return Bucket.fromBucketAttributes(scope, id, { bucketArn }); } static fromBucketName(scope, id, bucketName) { return Bucket.fromBucketAttributes(scope, id, { bucketName }); } /** * Creates a Bucket construct that represents an external bucket. * * @param scope The parent creating construct (usually `this`). * @param id The construct's name. * @param attrs A `BucketAttributes` object. Can be obtained from a call to * `bucket.export()` or manually created. */ static fromBucketAttributes(scope, id, attrs) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_BucketAttributes(attrs); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.fromBucketAttributes); } throw error; } const stack = core_1.Stack.of(scope); const region = attrs.region ?? stack.region; const urlSuffix = stack.urlSuffix; const bucketName = util_1.parseBucketName(scope, attrs); if (!bucketName) { throw new Error('Bucket name is required'); } Bucket.validateBucketName(bucketName); const newUrlFormat = attrs.bucketWebsiteNewUrlFormat === undefined ? false : attrs.bucketWebsiteNewUrlFormat; const websiteDomain = newUrlFormat ? `${bucketName}.s3-website.${region}.${urlSuffix}` : `${bucketName}.s3-website-${region}.${urlSuffix}`; class Import extends BucketBase { constructor() { super(...arguments); this.bucketName = bucketName; this.bucketArn = util_1.parseBucketArn(scope, attrs); this.bucketDomainName = attrs.bucketDomainName || `${bucketName}.s3.${urlSuffix}`; this.bucketWebsiteUrl = attrs.bucketWebsiteUrl || `http://${websiteDomain}`; this.bucketWebsiteDomainName = attrs.bucketWebsiteUrl ? core_1.Fn.select(2, core_1.Fn.split('/', attrs.bucketWebsiteUrl)) : websiteDomain; this.bucketRegionalDomainName = attrs.bucketRegionalDomainName || `${bucketName}.s3.${region}.${urlSuffix}`; this.bucketDualStackDomainName = attrs.bucketDualStackDomainName || `${bucketName}.s3.dualstack.${region}.${urlSuffix}`; this.bucketWebsiteNewUrlFormat = newUrlFormat; this.encryptionKey = attrs.encryptionKey; this.isWebsite = attrs.isWebsite ?? false; this.policy = undefined; this.autoCreatePolicy = false; this.disallowPublicAccess = false; this.notificationsHandlerRole = attrs.notificationsHandlerRole; } /** * Exports this bucket from the stack. */ export() { return attrs; } } return new Import(scope, id, { account: attrs.account, region: attrs.region, }); } /** * Thrown an exception if the given bucket name is not valid. * * @param physicalName name of the bucket. */ static validateBucketName(physicalName) { const bucketName = physicalName; if (!bucketName || core_1.Token.isUnresolved(bucketName)) { // the name is a late-bound value, not a defined string, // so skip validation return; } const errors = []; // Rules codified from https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html if (bucketName.length < 3 || bucketName.length > 63) { errors.push('Bucket name must be at least 3 and no more than 63 characters'); } const charsetMatch = bucketName.match(/[^a-z0-9.-]/); if (charsetMatch) { errors.push('Bucket name must only contain lowercase characters and the symbols, period (.) and dash (-) ' + `(offset: ${charsetMatch.index})`); } if (!/[a-z0-9]/.test(bucketName.charAt(0))) { errors.push('Bucket name must start and end with a lowercase character or number ' + '(offset: 0)'); } if (!/[a-z0-9]/.test(bucketName.charAt(bucketName.length - 1))) { errors.push('Bucket name must start and end with a lowercase character or number ' + `(offset: ${bucketName.length - 1})`); } const consecSymbolMatch = bucketName.match(/\.-|-\.|\.\./); if (consecSymbolMatch) { errors.push('Bucket name must not have dash next to period, or period next to dash, or consecutive periods ' + `(offset: ${consecSymbolMatch.index})`); } if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(bucketName)) { errors.push('Bucket name must not resemble an IP address'); } if (errors.length > 0) { throw new Error(`Invalid S3 bucket name (value: ${bucketName})${os_1.EOL}${errors.join(os_1.EOL)}`); } } /** * Add a lifecycle rule to the bucket * * @param rule The rule to add */ addLifecycleRule(rule) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_LifecycleRule(rule); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.addLifecycleRule); } throw error; } if ((rule.noncurrentVersionExpiration !== undefined || (rule.noncurrentVersionTransitions && rule.noncurrentVersionTransitions.length > 0)) && !this.versioned) { throw new Error("Cannot use 'noncurrent' rules on a nonversioned bucket"); } this.lifecycleRules.push(rule); } /** * Adds a metrics configuration for the CloudWatch request metrics from the bucket. * * @param metric The metric configuration to add */ addMetric(metric) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_BucketMetrics(metric); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.addMetric); } throw error; } this.metrics.push(metric); } /** * Adds a cross-origin access configuration for objects in an Amazon S3 bucket * * @param rule The CORS configuration rule to add */ addCorsRule(rule) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_CorsRule(rule); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.addCorsRule); } throw error; } this.cors.push(rule); } /** * Add an inventory configuration. * * @param inventory configuration to add */ addInventory(inventory) { try { jsiiDeprecationWarnings._aws_cdk_aws_s3_Inventory(inventory); } catch (error) { if (process.env.JSII_DEBUG !== "1" && error.name === "DeprecationError") { Error.captureStackTrace(error, this.addInventory); } throw error; } this.inventories.push(inventory); } /** * Adds an iam statement to enforce SSL requests only. */ enforceSSLStatement() { const statement = new iam.PolicyStatement({ actions: ['s3:*'], conditions: { Bool: { 'aws:SecureTransport': 'false' }, }, effect: iam.Effect.DENY, resources: [ this.bucketArn, this.arnForObjects('*'), ], principals: [new iam.AnyPrincipal()], }); this.addToResourcePolicy(statement); } /** * Set up key properties and return the Bucket encryption property from the * user's configuration. */ parseEncryption(props) { // default based on whether encryptionKey is specified let encryptionType = props.encryption; if (encryptionType === undefined) { encryptionType = props.encryptionKey ? BucketEncryption.KMS : BucketEncryption.UNENCRYPTED; } // if encryption key is set, encryption must be set to KMS. if (encryptionType !== BucketEncryption.KMS && props.encryptionKey) { throw new Error(`encryptionKey is specified, so 'encryption' must be set to KMS (value: ${encryptionType})`); } // if bucketKeyEnabled is set, encryption must be set to KMS. if (props.bucketKeyEnabled && encryptionType !== BucketEncryption.KMS) { throw new Error(`bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: ${encryptionType})`); } if (encryptionType === BucketEncryption.UNENCRYPTED) { return { bucketEncryption: undefined, encryptionKey: undefined }; } if (encryptionType === BucketEncryption.KMS) { const encryptionKey = props.encryptionKey || new kms.Key(this, 'Key', { description: `Created by ${this.node.path}`, }); const bucketEncryption = { serverSideEncryptionConfiguration: [ { bucketKeyEnabled: props.bucketKeyEnabled, serverSideEncryptionByDefault: { sseAlgorithm: 'aws:kms', kmsMasterKeyId: encryptionKey.keyArn, }, }, ], }; return { encryptionKey, bucketEncryption }; } if (encryptionType === BucketEncryption.S3_MANAGED) { const bucketEncryption = { serverSideEncryptionConfiguration: [ { serverSideEncryptionByDefault: { sseAlgorithm: 'AES256' } }, ], }; return { bucketEncryption }; } if (encryptionType === BucketEncryption.KMS_MANAGED) { const bucketEncryption = { serverSideEncryptionConfiguration: [ { serverSideEncryptionByDefault: { sseAlgorithm: 'aws:kms' } }, ], }; return { bucketEncryption }; } throw new Error(`Unexpected 'encryptionType': ${encryptionType}`); } /** * Parse the lifecycle configuration out of the bucket props * @param props Par */ parseLifecycleConfiguration() { if (!this.lifecycleRules || this.lifecycleRules.length === 0) { return undefined; } const self = this; return { rules: this.lifecycleRules.map(parseLifecycleRule) }; function parseLifecycleRule(rule) { const enabled = rule.enabled ?? true; const x = { // eslint-disable-next-line max-len abortIncompleteMultipartUpload: rule.abortIncompleteMultipartUploadAfter !== undefined ? { daysAfterInitiation: rule.abortIncompleteMultipartUploadAfter.toDays() } : undefined, expirationDate: rule.expirationDate, expirationInDays: rule.expiration?.toDays(), id: rule.id, noncurrentVersionExpiration: rule.noncurrentVersionExpiration && { noncurrentDays: rule.noncurrentVersionExpiration.toDays(), newerNoncurrentVersions: rule.noncurrentVersionsToRetain, }, noncurrentVersionTransitions: mapOrUndefined(rule.noncurrentVersionTransitions, t => ({ storageClass: t.storageClass.value, transitionInDays: t.transitionAfter.toDays(), newerNoncurrentVersions: t.noncurrentVersionsToRetain, })), prefix: rule.prefix, status: enabled ? 'Enabled' : 'Disabled', transitions: mapOrUndefined(rule.transitions, t => ({ storageClass: t.storageClass.value, transitionDate: t.transitionDate, transitionInDays: t.transitionAfter && t.transitionAfter.toDays(), })), expiredObjectDeleteMarker: rule.expiredObjectDeleteMarker, tagFilters: self.parseTagFilters(rule.tagFilters), objectSizeLessThan: rule.objectSizeLessThan, objectSizeGreaterThan: rule.objectSizeGreaterThan, }; return x; } } parseServerAccessLogs(props) { if (!props.serverAccessLogsBucket && !props.serverAccessLogsPrefix) { return undefined; } return { destinationBucketName: props.serverAccessLogsBucket?.bucketName, logFilePrefix: props.serverAccessLogsPrefix, }; } parseMetricConfiguration() { if (!this.metrics || this.metrics.length === 0) { return undefined; } const self = this; return this.metrics.map(parseMetric); function parseMetric(metric) { return { id: metric.id, prefix: metric.prefix, tagFilters: self.parseTagFilters(metric.tagFilters), }; } } parseCorsConfiguration() { if (!this.cors || this.cors.length === 0) { return undefined; } return { corsRules: this.cors.map(parseCors) }; function parseCors(rule) { return { id: rule.id, maxAge: rule.maxAge, allowedHeaders: rule.allowedHeaders, allowedMethods: rule.allowedMethods, allowedOrigins: rule.allowedOrigins, exposedHeaders: rule.exposedHeaders, }; } } parseTagFilters(tagFilters) { if (!tagFilters || tagFilters.length === 0) { return undefined; } return Object.keys(tagFilters).map(tag => ({ key: tag, value: tagFilters[tag], })); } parseOwnershipControls({ objectOwnership }) { if (!objectOwnership) { return undefined; } return { rules: [{ objectOwnership, }], }; } parseTieringConfig({ intelligentTieringConfigurations }) { if (!intelligentTieringConfigurations) { return undefined; } return intelligentTieringConfigurations.map(config => { const tierings = []; if (config.archiveAccessTierTime) { tierings.push({ accessTier: 'ARCHIVE_ACCESS', days: config.archiveAccessTierTime.toDays({ integral: true }), }); } if (config.deepArchiveAccessTierTime) { tierings.push({ accessTier: 'DEEP_ARCHIVE_ACCESS', days: config.deepArchiveAccessTierTime.toDays({ integral: true }), }); } return { id: config.name, prefix: config.prefix, status: 'Enabled', tagFilters: config.tags, tierings: tierings, }; }); } renderWebsiteConfiguration(props) { if (!props.websiteErrorDocument && !props.websiteIndexDocument && !props.websiteRedirect && !props.websiteRoutingRules) { return undefined; } if (props.websiteErrorDocument && !props.websiteIndexDocument) { throw new Error('"websiteIndexDocument" is required if "websiteErrorDocument" is set'); } if (props.websiteRedirect && (props.websiteErrorDocument || props.websiteIndexDocument || props.websiteRoutingRules)) { throw new Error('"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used'); } const routingRules = props.websiteRoutingRules ? props.websiteRoutingRules.map((rule) => { if (rule.condition && !rule.condition.httpErrorCodeReturnedEquals && !rule.condition.keyPrefixEquals) { throw new Error('The condition property cannot be an empty object'); } return { redirectRule: { hostName: rule.hostName, httpRedirectCode: rule.httpRedirectCode, protocol: rule.protocol, replaceKeyWith: rule.replaceKey && rule.replaceKey.withKey, replaceKeyPrefixWith: rule.replaceKey && rule.replaceKey.prefixWithKey, }, routingRuleCondition: rule.condition, }; }) : undefined; return { indexDocument: props.websiteIndexDocument, errorDocument: props.websiteErrorDocument, redirectAllRequestsTo: props.websiteRedirect, routingRules, }; } /** * Allows the LogDelivery group to write, fails if ACL was set differently. * * @see * https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl */ allowLogDelivery() { if (this.accessControl && this.accessControl !== BucketAccessControl.LOG_DELIVERY_WRITE) { throw new Error("Cannot enable log delivery to this bucket because the bucket's ACL has been set and can't be changed"); } this.accessControl = BucketAccessControl.LOG_DELIVERY_WRITE; } parseInventoryConfiguration() { if (!this.inventories || this.inventories.length === 0) { return undefined; } return this.inventories.map((inventory, index) => { const format = inventory.format ?? InventoryFormat.CSV; const frequency = inventory.frequency ?? InventoryFrequency.WEEKLY; const id = inventory.inventoryId ?? `${this.node.id}Inventory${index}`; if (inventory.destination.bucket instanceof Bucket) { inventory.destination.bucket.addToResourcePolicy(new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['s3:PutObject'], resources: [ inventory.destination.bucket.bucketArn, inventory.destination.bucket.arnForObjects(`${inventory.destination.prefix ?? ''}*`), ], principals: [new iam.ServicePrincipal('s3.amazonaws.com')], conditions: { ArnLike: { 'aws:SourceArn': this.bucketArn, }, }, })); } return { id, destination: { bucketArn: inventory.destination.bucket.bucketArn, bucketAccountId: inventory.destination.bucketOwner, prefix: inventory.destination.prefix, format, }, enabled: inventory.enabled ?? true, includedObjectVersions: inventory.includeObjectVersions ?? InventoryObjectVersion.ALL, scheduleFrequency: frequency, optionalFields: inventory.optionalFields, prefix: inventory.objectsPrefix,