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
17 changes: 6 additions & 11 deletions packages/aws-cdk-lib/aws-cloudfront/lib/cache-policy.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Construct, Node } from 'constructs';
import { Construct } from 'constructs';
import { CachePolicyReference, CfnCachePolicy, ICachePolicyRef } from './cloudfront.generated';
import {
Duration,
Names,
Resource,
ResourceEnvironment,
Stack,
Token,
UnscopedValidationError,
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';

/**
Expand Down Expand Up @@ -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');
}
}();
}

Expand Down
18 changes: 7 additions & 11 deletions packages/aws-cdk-lib/aws-cloudfront/lib/origin-request-policy.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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');
}
}();
}

Expand Down
18 changes: 7 additions & 11 deletions packages/aws-cdk-lib/aws-cloudfront/lib/response-headers-policy.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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');
}
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
Expand Down
16 changes: 7 additions & 9 deletions packages/aws-cdk-lib/aws-iam/lib/managed-policy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Construct, Node } from 'constructs';
import { Construct } from 'constructs';
import {
CfnManagedPolicy,
IGroupRef,
Expand All @@ -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';

/**
Expand Down Expand Up @@ -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',
Expand All @@ -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();
}
Expand Down
46 changes: 46 additions & 0 deletions packages/aws-cdk-lib/core/lib/private/detached-construct.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading