diff --git a/packages/aws-cdk-lib/aws-iam/test/merge-statements.test.ts b/packages/aws-cdk-lib/aws-iam/test/merge-statements.test.ts index 12796f768b741..45984b95ac5b3 100644 --- a/packages/aws-cdk-lib/aws-iam/test/merge-statements.test.ts +++ b/packages/aws-cdk-lib/aws-iam/test/merge-statements.test.ts @@ -1,4 +1,4 @@ -import { App, Stack } from '../../core'; +import { App, Lazy, Stack } from '../../core'; import * as iam from '../lib'; import { PolicyStatement } from '../lib'; @@ -490,6 +490,40 @@ test('lazily generated statements are merged correctly', () => { ]); }); +test('merge statements if resource is a lazy', () => { + const stack = new Stack(); + const user1 = new iam.User(stack, 'User1'); + const user2 = new iam.User(stack, 'User2'); + const resourceToken = Lazy.string({ + produce: () => 'a', + }); + + assertMerged([ + new iam.PolicyStatement({ + resources: [resourceToken], + actions: ['service:Action'], + principals: [user1], + }), + new iam.PolicyStatement({ + resources: [resourceToken], + actions: ['service:Action'], + principals: [user2], + }), + ], [ + { + Effect: 'Allow', + Resource: 'a', + Action: 'service:Action', + Principal: { + AWS: [ + { 'Fn::GetAtt': ['User1E278A736', 'Arn'] }, + { 'Fn::GetAtt': ['User21F1486D1', 'Arn'] }, + ], + }, + }, + ]); +}); + function assertNoMerge(statements: iam.PolicyStatement[]) { const app = new App(); const stack = new Stack(app, 'Stack'); diff --git a/packages/aws-cdk-lib/aws-secretsmanager/lib/secret.ts b/packages/aws-cdk-lib/aws-secretsmanager/lib/secret.ts index 9a5ab700556ec..6f214e3448cc7 100644 --- a/packages/aws-cdk-lib/aws-secretsmanager/lib/secret.ts +++ b/packages/aws-cdk-lib/aws-secretsmanager/lib/secret.ts @@ -4,7 +4,7 @@ import { RotationSchedule, RotationScheduleOptions } from './rotation-schedule'; import * as secretsmanager from './secretsmanager.generated'; import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; -import { ArnFormat, FeatureFlags, Fn, IResource, Lazy, RemovalPolicy, Resource, ResourceProps, SecretValue, Stack, Token, TokenComparison } from '../../core'; +import { ArnFormat, FeatureFlags, Fn, IResolveContext, IResource, Lazy, RemovalPolicy, Resource, ResourceProps, SecretValue, Stack, Token, TokenComparison } from '../../core'; import * as cxapi from '../../cx-api'; const SECRET_SYMBOL = Symbol.for('@aws-cdk/secretsmanager.Secret'); @@ -340,9 +340,22 @@ abstract class SecretBase extends Resource implements ISecret { protected abstract readonly autoCreatePolicy: boolean; private policy?: ResourcePolicy; + private _arnForPolicies: string; constructor(scope: Construct, id: string, props: ResourceProps = {}) { super(scope, id, props); + this._arnForPolicies = Lazy.uncachedString({ + produce: (context: IResolveContext) => { + const consumingStack = Stack.of(context.scope); + if (this.stack.account !== consumingStack.account || + (this.stack.region !== consumingStack.region && + !consumingStack._crossRegionReferences) || !this.secretFullArn) { + return `${this.secretArn}-??????`; + } else { + return this.secretFullArn; + } + }, + }); this.node.addValidation({ validate: () => this.policy?.document.validateForResourcePolicy() ?? [] }); } @@ -450,7 +463,7 @@ abstract class SecretBase extends Resource implements ISecret { * then we need to add a suffix to capture the full ARN's format. */ protected get arnForPolicies() { - return this.secretFullArn ? this.secretFullArn : `${this.secretArn}-??????`; + return this._arnForPolicies; } /** diff --git a/packages/aws-cdk-lib/aws-secretsmanager/test/secret.test.ts b/packages/aws-cdk-lib/aws-secretsmanager/test/secret.test.ts index 18849d76aff72..628649d7ee4be 100644 --- a/packages/aws-cdk-lib/aws-secretsmanager/test/secret.test.ts +++ b/packages/aws-cdk-lib/aws-secretsmanager/test/secret.test.ts @@ -1357,3 +1357,36 @@ describe('secretObjectValue', () => { }); }); }); + +test('cross-environment grant with direct object reference', () => { + // GIVEN + const producerStack = new cdk.Stack(app, 'ProducerStack', { env: { region: 'foo', account: '1111111111' } }); + const consumerStack = new cdk.Stack(app, 'ConsumerStack', { env: { region: 'bar', account: '1111111111' } }); + const secret = new secretsmanager.Secret(producerStack, 'Secret', { secretName: 'MySecret' }); + const role = new iam.Role(consumerStack, 'Role', { assumedBy: new iam.AccountRootPrincipal() }); + + // WHEN + secret.grantRead(role); + + // THEN + Template.fromStack(consumerStack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':secretsmanager:foo:1111111111:secret:MySecret-??????', + ]], + }, + }], + }, + }); + +});