diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/cache-policy.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/cache-policy.ts index 5ac829dbf98d6..31c4c6e699ad2 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/cache-policy.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/cache-policy.ts @@ -1,10 +1,9 @@ -import { Construct, Node } from 'constructs'; +import { Construct } from 'constructs'; import { CachePolicyReference, CfnCachePolicy, ICachePolicyRef } from './cloudfront.generated'; import { Duration, Names, Resource, - ResourceEnvironment, Stack, Token, UnscopedValidationError, @@ -12,6 +11,7 @@ import { withResolved, } from '../../core'; import { addConstructMetadata } from '../../core/lib/metadata-resource'; +import { DetachedConstruct } from '../../core/lib/private/detached-construct'; import { propertyInjectable } from '../../core/lib/prop-injectable'; /** @@ -148,19 +148,14 @@ export class CachePolicy extends Resource implements ICachePolicy { /** Use an existing managed cache policy. */ private static fromManagedCachePolicy(managedCachePolicyId: string): ICachePolicy { - return new class implements ICachePolicy { - public get node(): Node { - throw new UnscopedValidationError('The result of fromManagedCachePolicy can not be used in this API'); - } - - public get env(): ResourceEnvironment { - throw new UnscopedValidationError('The result of fromManagedCachePolicy can not be used in this API'); - } - + return new class extends DetachedConstruct implements ICachePolicy { public readonly cachePolicyId = managedCachePolicyId; public readonly cachePolicyRef = { cachePolicyId: managedCachePolicyId, }; + constructor() { + super('The result of fromManagedCachePolicy can not be used in this API'); + } }(); } diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-request-policy.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-request-policy.ts index 0c7e8bd276b4d..cfdb25e9a5f2b 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-request-policy.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-request-policy.ts @@ -1,7 +1,8 @@ -import { Construct, Node } from 'constructs'; +import { Construct } from 'constructs'; import { CfnOriginRequestPolicy, IOriginRequestPolicyRef, OriginRequestPolicyReference } from './cloudfront.generated'; -import { Names, Resource, ResourceEnvironment, Token, UnscopedValidationError, ValidationError } from '../../core'; +import { Names, Resource, Token, UnscopedValidationError, ValidationError } from '../../core'; import { addConstructMetadata } from '../../core/lib/metadata-resource'; +import { DetachedConstruct } from '../../core/lib/private/detached-construct'; import { propertyInjectable } from '../../core/lib/prop-injectable'; /** @@ -87,19 +88,14 @@ export class OriginRequestPolicy extends Resource implements IOriginRequestPolic /** Use an existing managed origin request policy. */ private static fromManagedOriginRequestPolicy(managedOriginRequestPolicyId: string): IOriginRequestPolicy { - return new class implements IOriginRequestPolicy { - public get node(): Node { - throw new UnscopedValidationError('The result of fromManagedOriginRequestPolicy can not be used in this API'); - } - - public get env(): ResourceEnvironment { - throw new UnscopedValidationError('The result of fromManagedOriginRequestPolicy can not be used in this API'); - } - + return new class extends DetachedConstruct implements IOriginRequestPolicy { public readonly originRequestPolicyId = managedOriginRequestPolicyId; public readonly originRequestPolicyRef = { originRequestPolicyId: managedOriginRequestPolicyId, }; + constructor() { + super('The result of fromManagedOriginRequestPolicy can not be used in this API'); + } }(); } diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/response-headers-policy.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/response-headers-policy.ts index 404bb0d6c1a29..7674bd898cbac 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/response-headers-policy.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/response-headers-policy.ts @@ -1,11 +1,12 @@ -import { Construct, Node } from 'constructs'; +import { Construct } from 'constructs'; import { CfnResponseHeadersPolicy, IResponseHeadersPolicyRef, ResponseHeadersPolicyReference, } from './cloudfront.generated'; -import { Duration, Names, Resource, ResourceEnvironment, Token, UnscopedValidationError, ValidationError, withResolved } from '../../core'; +import { Duration, Names, Resource, Token, ValidationError, withResolved } from '../../core'; import { addConstructMetadata } from '../../core/lib/metadata-resource'; +import { DetachedConstruct } from '../../core/lib/private/detached-construct'; import { propertyInjectable } from '../../core/lib/prop-injectable'; /** @@ -109,19 +110,14 @@ export class ResponseHeadersPolicy extends Resource implements IResponseHeadersP } private static fromManagedResponseHeadersPolicy(managedResponseHeadersPolicyId: string): IResponseHeadersPolicy { - return new class implements IResponseHeadersPolicy { - public get node(): Node { - throw new UnscopedValidationError('The result of fromManagedResponseHeadersPolicy can not be used in this API'); - } - - public get env(): ResourceEnvironment { - throw new UnscopedValidationError('The result of fromManagedResponseHeadersPolicy can not be used in this API'); - } - + return new class extends DetachedConstruct implements IResponseHeadersPolicy { public readonly responseHeadersPolicyId = managedResponseHeadersPolicyId; public readonly responseHeadersPolicyRef = { responseHeadersPolicyId: managedResponseHeadersPolicyId, }; + constructor() { + super('The result of fromManagedResponseHeadersPolicy can not be used in this API'); + } }; } diff --git a/packages/aws-cdk-lib/aws-codepipeline-actions/lib/elastic-beanstalk/deploy-action.ts b/packages/aws-cdk-lib/aws-codepipeline-actions/lib/elastic-beanstalk/deploy-action.ts index 952ef5a8dd68a..9c1776d4e8943 100644 --- a/packages/aws-cdk-lib/aws-codepipeline-actions/lib/elastic-beanstalk/deploy-action.ts +++ b/packages/aws-cdk-lib/aws-codepipeline-actions/lib/elastic-beanstalk/deploy-action.ts @@ -1,6 +1,7 @@ -import { Construct, Node } from 'constructs'; +import { Construct } from 'constructs'; import * as codepipeline from '../../../aws-codepipeline'; -import { Aws, ResourceEnvironment, UnscopedValidationError } from '../../../core'; +import { Aws } from '../../../core'; +import { DetachedConstruct } from '../../../core/lib/private/detached-construct'; import { Action } from '../action'; import { deployArtifactBounds } from '../common'; @@ -53,16 +54,13 @@ export class ElasticBeanstalkDeployAction extends Action { // it doesn't seem we can scope this down further for the codepipeline action. const policyArn = `arn:${Aws.PARTITION}:iam::aws:policy/AdministratorAccess-AWSElasticBeanstalk`; - options.role.addManagedPolicy({ - get node(): Node { - throw new UnscopedValidationError('This object can not be used in this API'); - }, - get env(): ResourceEnvironment { - throw new UnscopedValidationError('This object can not be used in this API'); - }, - managedPolicyArn: policyArn, - managedPolicyRef: { policyArn }, - }); + options.role.addManagedPolicy(new class extends DetachedConstruct { + managedPolicyArn = policyArn; + managedPolicyRef = { policyArn }; + constructor() { + super('This object can not be used in this API'); + } + }()); // the Action's Role needs to read from the Bucket to get artifacts options.bucket.grantRead(options.role); diff --git a/packages/aws-cdk-lib/aws-iam/lib/managed-policy.ts b/packages/aws-cdk-lib/aws-iam/lib/managed-policy.ts index d868b5877d44b..36cd7f67b97b5 100644 --- a/packages/aws-cdk-lib/aws-iam/lib/managed-policy.ts +++ b/packages/aws-cdk-lib/aws-iam/lib/managed-policy.ts @@ -1,4 +1,4 @@ -import { Construct, Node } from 'constructs'; +import { Construct } from 'constructs'; import { CfnManagedPolicy, IGroupRef, @@ -13,9 +13,10 @@ import { AddToPrincipalPolicyResult, ArnPrincipal, IGrantable, IPrincipal, Princ import { undefinedIfEmpty } from './private/util'; import { IRole } from './role'; import { IUser } from './user'; -import { Arn, ArnFormat, Aws, Resource, ResourceEnvironment, Stack, UnscopedValidationError, ValidationError, Lazy } from '../../core'; +import { Arn, ArnFormat, Aws, Resource, Stack, ValidationError, Lazy } from '../../core'; import { getCustomizeRolesConfig, PolicySynthesizer } from '../../core/lib/helpers-internal'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; +import { DetachedConstruct } from '../../core/lib/private/detached-construct'; import { propertyInjectable } from '../../core/lib/prop-injectable'; /** @@ -179,7 +180,7 @@ export class ManagedPolicy extends Resource implements IManagedPolicy, IGrantabl * prefix when constructing this object. */ public static fromAwsManagedPolicyName(managedPolicyName: string): IManagedPolicy { - class AwsManagedPolicy implements IManagedPolicy { + class AwsManagedPolicy extends DetachedConstruct implements IManagedPolicy { public readonly managedPolicyArn = Arn.format({ partition: Aws.PARTITION, service: 'iam', @@ -188,17 +189,14 @@ export class ManagedPolicy extends Resource implements IManagedPolicy, IGrantabl resource: 'policy', resourceName: managedPolicyName, }); + constructor() { + super('The result of fromAwsManagedPolicyName can not be used in this API'); + } public get managedPolicyRef(): ManagedPolicyReference { return { policyArn: this.managedPolicyArn, }; } - public get node(): Node { - throw new UnscopedValidationError('The result of fromAwsManagedPolicyName can not be used in this API'); - } - public get env(): ResourceEnvironment { - throw new UnscopedValidationError('The result of fromAwsManagedPolicyName can not be used in this API'); - } } return new AwsManagedPolicy(); } diff --git a/packages/aws-cdk-lib/core/lib/private/detached-construct.ts b/packages/aws-cdk-lib/core/lib/private/detached-construct.ts new file mode 100644 index 0000000000000..e9d22fd909564 --- /dev/null +++ b/packages/aws-cdk-lib/core/lib/private/detached-construct.ts @@ -0,0 +1,46 @@ +import { Construct, IConstruct } from 'constructs'; +import type { ResourceEnvironment } from '../environment'; +import { UnscopedValidationError } from '../errors'; + +const CONSTRUCT_SYM = Symbol.for('constructs.Construct'); + +/** + * Base class for detached constructs that throw UnscopedValidationError + * when accessing node, env, or with() methods. + * + * This is used by legacy APIs like ManagedPolicy.fromAwsManagedPolicyName() and + * CloudFront policy imports that return construct-like objects without requiring + * a scope parameter. These APIs predate modern CDK patterns and cannot be changed + * without breaking existing customer code. + * + * DO NOT USE for new code. New APIs should require a scope parameter. + * + * @internal + */ +export abstract class DetachedConstruct extends Construct implements IConstruct { + private readonly errorMessage: string; + + constructor(errorMessage: string) { + super(null as any, undefined as any); + + this.errorMessage = errorMessage; + + // Use Object.defineProperty to override 'node' property instead of a getter + // to avoid TS2611 error (property vs accessor conflict with base class) + Object.defineProperty(this, 'node', { + get() { throw new UnscopedValidationError(errorMessage); }, + }); + + // Despite extending Construct, DetachedConstruct doesn't work like one. + // So we try to not pretend that this is a construct as much as possible. + Object.defineProperty(this, CONSTRUCT_SYM, { + value: false, + enumerable: false, + writable: false, + }); + } + + public get env(): ResourceEnvironment { + throw new UnscopedValidationError(this.errorMessage); + } +} diff --git a/packages/aws-cdk-lib/core/test/private/detached-construct.test.ts b/packages/aws-cdk-lib/core/test/private/detached-construct.test.ts new file mode 100644 index 0000000000000..5db6616851fa1 --- /dev/null +++ b/packages/aws-cdk-lib/core/test/private/detached-construct.test.ts @@ -0,0 +1,31 @@ +import { Construct } from 'constructs'; +import { UnscopedValidationError } from '../../lib/errors'; +import { DetachedConstruct } from '../../lib/private/detached-construct'; + +class TestDetachedConstruct extends DetachedConstruct { + constructor(message: string) { + super(message); + } +} + +describe('DetachedConstruct', () => { + test('throws UnscopedValidationError when accessing node', () => { + const construct = new TestDetachedConstruct('test error message'); + + expect(() => construct.node).toThrow(UnscopedValidationError); + expect(() => construct.node).toThrow('test error message'); + }); + + test('throws UnscopedValidationError when accessing env', () => { + const construct = new TestDetachedConstruct('test error message'); + + expect(() => construct.env).toThrow(UnscopedValidationError); + expect(() => construct.env).toThrow('test error message'); + }); + + test('returns false for Construct.isConstruct', () => { + const construct = new TestDetachedConstruct('test error message'); + + expect(Construct.isConstruct(construct)).toBe(false); + }); +});