Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions aws-cdk.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"jest.runMode": {
"type": "on-demand"
},
"eslint.useFlatConfig": true,
"eslint.format.enable": true,
"jest.virtualFolders": [
{
"name": "aws-cdk-lib",
Expand Down
127 changes: 127 additions & 0 deletions packages/aws-cdk-lib/aws-s3/lib/bucket-reflection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { IConstruct } from 'constructs';
import type { CfnBucket, CfnBucketPolicy } from './s3.generated';
import type { CfnKey } from '../../aws-kms';
import { Fn, Reference, Tokenization, UnscopedValidationError } from '../../core';
import { findClosestRelatedResource, findL1FromRef, memoizedGetter, resolvedEquals, resolvedExists, resolvedGet } from '../../core/lib/helpers-internal';
import type { IBucketRef } from '../../interfaces/generated/aws-s3-interfaces.generated';

/**
* Provides read-only reflection on the configuration of an S3 Bucket's
* underlying CfnBucket resource.
*
* Use `BucketReflection.of()` to obtain an instance from any Bucket reference.
* All getters read directly from the L1 resource, providing a single source of
* truth regardless of whether the bucket was created as an L2, imported, or
* constructed directly as a CfnBucket.
*/
export class BucketReflection {
/**
* Creates a BucketReflection for the given bucket reference.
*
* Resolves the underlying CfnBucket from the construct tree.
*
* @param bucketRef - The bucket reference to reflect on.
* @throws If the underlying CfnBucket resource cannot be found.
*/
static of(bucketRef: IBucketRef) {
return new BucketReflection(bucketRef);
}

/**
* The CfnBucket L1 resource that is reflected on.
*
* @throws If the underlying CfnBucket resource cannot be found.
*/
public get bucket(): CfnBucket {
if (!this._bucket) {
throw new UnscopedValidationError('CannotFindUnderlyingResource', `Unable to find underlying resource for ${this.ref.node.path}. Please pass the resource construct directly.`);
}
return this._bucket;
}

private readonly ref: IBucketRef;
private readonly _bucket: CfnBucket | undefined;

private constructor(ref: IBucketRef) {
this.ref = ref;
this._bucket = findL1FromRef<IBucketRef, CfnBucket>(
ref,
'AWS::S3::Bucket',
(cfn, bucketRef) => bucketRef.bucketRef == cfn.bucketRef,
);
}

/**
* The domain name of the static website endpoint.
*
* Derived from the bucket's website URL attribute.
*/
@memoizedGetter
get bucketWebsiteDomainName(): string {
return Fn.select(2, Fn.split('/', this.bucket.attrWebsiteUrl));
}

/**
* Whether the bucket is configured for static website hosting.
*/
get isWebsite(): boolean | undefined {
return resolvedExists(this._bucket, 'websiteConfiguration');
}

/**
* Whether the bucket's public access block configuration blocks public bucket policies.
*/
get disallowPublicAccess(): boolean | undefined {
return resolvedEquals(this._bucket, 'publicAccessBlockConfiguration.blockPublicPolicy', true);
}

/**
* The bucket policy associated with this bucket, if any.
*
* Searches the construct tree for a CfnBucketPolicy that references this bucket.
*/
get policy(): CfnBucketPolicy | undefined {
const resolved = this._bucket || this.ref;

return findClosestRelatedResource<IBucketRef | CfnBucket, CfnBucketPolicy>(
resolved,
'AWS::S3::BucketPolicy',
(bucket, policy) => {
const reversed = Tokenization.reverse(policy.bucket) || policy.bucket;
if (Reference.isReference(reversed)) {
return bucket === reversed.target;
}

const possibleRefs = new Set<any>([
(bucket as CfnBucket).ref,
bucket.bucketRef?.bucketName,
bucket.bucketRef?.bucketArn,
].filter(Boolean));

return possibleRefs.has(reversed) || String(reversed).includes(bucket.node.id);
},
);
}

/**
* The KMS key used for server-side encryption, if any.
*
* Searches the construct tree for a CfnKey referenced by the bucket's
* encryption configuration.
*/
get encryptionKey(): CfnKey | undefined {
if (!this._bucket) {
return undefined;
}
const kmsMasterKeyId = resolvedGet(this._bucket, 'bucketEncryption.serverSideEncryptionConfiguration.0.serverSideEncryptionByDefault.kmsMasterKeyId', undefined);
if (!kmsMasterKeyId) {
return undefined;
}

return findClosestRelatedResource<IConstruct, CfnKey>(
this._bucket,
'AWS::KMS::Key',
(_, key) => key.ref === kmsMasterKeyId || key.attrKeyId === kmsMasterKeyId || key.attrArn === kmsMasterKeyId,
);
}
}
43 changes: 33 additions & 10 deletions packages/aws-cdk-lib/aws-s3/lib/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { EOL } from 'os';
import type { Construct, IConstruct } from 'constructs';
import { BucketGrants } from './bucket-grants';
import { BucketPolicy } from './bucket-policy';
import { BucketReflection } from './bucket-reflection';
import type { IBucketNotificationDestination } from './destination';
import { BucketAutoDeleteObjects } from './mixins';
import { BucketNotifications } from './notifications-resource';
Expand Down Expand Up @@ -2200,20 +2201,34 @@ export class Bucket extends BucketBase {
public readonly bucketDualStackDomainName = cfnBucket.attrDualStackDomainName;
public readonly bucketRegionalDomainName = cfnBucket.attrRegionalDomainName;
public readonly bucketWebsiteUrl = cfnBucket.attrWebsiteUrl;
public readonly bucketWebsiteDomainName = Fn.select(2, Fn.split('/', cfnBucket.attrWebsiteUrl));

public readonly encryptionKey = encryptionKey;
public readonly isWebsite = cfnBucket.websiteConfiguration !== undefined;
public policy = undefined;
public replicationRoleArn = undefined;
protected autoCreatePolicy = true;
public disallowPublicAccess = cfnBucket.publicAccessBlockConfiguration &&
(cfnBucket.publicAccessBlockConfiguration as any).blockPublicPolicy;

private readonly reflection: BucketReflection;

constructor() {
super(cfnBucket, id);

this.node.defaultChild = cfnBucket;
this.reflection = BucketReflection.of(this);
}

public get bucketWebsiteDomainName() {
return this.reflection.bucketWebsiteDomainName;
}

public get isWebsite(): boolean | undefined {
return this.reflection.isWebsite;
}

public get disallowPublicAccess(): boolean | undefined {
return this.reflection.disallowPublicAccess;
}
public set disallowPublicAccess(_value: boolean | undefined) {
// Ignored — value is derived from the CfnBucket resource via reflection
}
}();
}
Expand Down Expand Up @@ -2314,17 +2329,26 @@ export class Bucket extends BucketBase {
}
public readonly bucketDomainName: string;
public readonly bucketWebsiteUrl: string;
public readonly bucketWebsiteDomainName: string;
public get bucketWebsiteDomainName(): string {
return this.reflection.bucketWebsiteDomainName;
}
public readonly bucketDualStackDomainName: string;
public readonly bucketRegionalDomainName: string;

public readonly encryptionKey?: kms.IKey;
public readonly isWebsite?: boolean;
public get isWebsite(): boolean | undefined {
return this.reflection.isWebsite;
}
public policy?: BucketPolicy;

public replicationRoleArn?: string;
protected autoCreatePolicy = true;
public disallowPublicAccess?: boolean;
public get disallowPublicAccess(): boolean | undefined {
return this.reflection.disallowPublicAccess || undefined;
}
public set disallowPublicAccess(_value: boolean | undefined) {
// Ignored — value is derived from the CfnBucket resource via reflection
}
private accessControl?: BucketAccessControl;
private readonly lifecycleRules: LifecycleRule[] = [];
private readonly transitionDefaultMinimumObjectSize?: TransitionDefaultMinimumObjectSize;
Expand All @@ -2333,6 +2357,7 @@ export class Bucket extends BucketBase {
private readonly cors: CorsRule[] = [];
private readonly inventories: Inventory[] = [];
private readonly _resource: CfnBucket;
private readonly reflection: BucketReflection;

constructor(scope: Construct, id: string, props: BucketProps = {}) {
super(scope, id, {
Expand All @@ -2355,7 +2380,6 @@ export class Bucket extends BucketBase {
}

const websiteConfiguration = this.renderWebsiteConfiguration(props);
this.isWebsite = (websiteConfiguration !== undefined);

const objectLockConfiguration = this.parseObjectLockConfig(props);
const replicationConfiguration = this.renderReplicationConfiguration(props);
Expand Down Expand Up @@ -2383,18 +2407,17 @@ export class Bucket extends BucketBase {
abacStatus: props.abacStatus !== undefined ? (props.abacStatus ? 'Enabled' : 'Disabled') : undefined,
});
this._resource = resource;
this.reflection = BucketReflection.of(this);

resource.applyRemovalPolicy(props.removalPolicy);

this.eventBridgeEnabled = props.eventBridgeEnabled;

this.bucketDomainName = resource.attrDomainName;
this.bucketWebsiteUrl = resource.attrWebsiteUrl;
this.bucketWebsiteDomainName = Fn.select(2, 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
Expand Down
6 changes: 3 additions & 3 deletions packages/aws-cdk-lib/aws-s3/lib/private/default-traits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import { DefaultEncryptedResourceFactories, DefaultPolicyFactories } from '../..
import { KeyGrants } from '../../../aws-kms';
import type { CfnResource, ResourceEnvironment } from '../../../core';
import { ValidationError } from '../../../core';
import { BucketReflection } from '../bucket-reflection';
import { BucketPolicyStatements } from '../mixins/bucket-policy';
import { CfnBucket, CfnBucketPolicy } from '../s3.generated';
import { tryFindBucketPolicyForBucket, tryFindKmsKeyforBucket } from './reflections';

/**
* Factory to create an encrypted resource for a Bucket.
Expand All @@ -34,7 +34,7 @@ class EncryptedCfnBucket implements IEncryptedResource {
}

public grantOnKey(grantee: IGrantable, ...actions: string[]): GrantOnKeyResult {
const key = tryFindKmsKeyforBucket(this.bucket);
const key = BucketReflection.of(this.bucket).encryptionKey;
return {
grant: key ? KeyGrants.fromKey(key).actions(grantee, ...actions) : undefined,
};
Expand All @@ -60,7 +60,7 @@ class CfnBucketWithPolicy implements IResourceWithPolicyV2 {

public addToResourcePolicy(statement: PolicyStatement): AddToResourcePolicyResult {
if (!this.policy) {
this.policy = tryFindBucketPolicyForBucket(this.bucket) ?? new CfnBucketPolicy(this.bucket, 'S3BucketPolicy', {
this.policy = BucketReflection.of(this.bucket)?.policy ?? new CfnBucketPolicy(this.bucket, 'S3BucketPolicy', {
bucket: this.bucket.ref,
policyDocument: { Statement: [] },
});
Expand Down
68 changes: 0 additions & 68 deletions packages/aws-cdk-lib/aws-s3/lib/private/reflections.ts

This file was deleted.

Loading
Loading