diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/aws-cdk-route53-cross-account-integ.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/aws-cdk-route53-cross-account-integ.assets.json index 03a8cf2340c2f..53d04417642e3 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/aws-cdk-route53-cross-account-integ.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/aws-cdk-route53-cross-account-integ.assets.json @@ -14,7 +14,7 @@ } } }, - "3222f491727b0389ac87f972f2443b490ff3cee14d24c28f1527c3f085cab460": { + "52da24cb67101152630cedcc08830f183f595580f8a7f6fcef1e0aac216c7198": { "source": { "path": "aws-cdk-route53-cross-account-integ.template.json", "packaging": "file" @@ -22,7 +22,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "3222f491727b0389ac87f972f2443b490ff3cee14d24c28f1527c3f085cab460.json", + "objectKey": "52da24cb67101152630cedcc08830f183f595580f8a7f6fcef1e0aac216c7198.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/aws-cdk-route53-cross-account-integ.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/aws-cdk-route53-cross-account-integ.template.json index 0dc079b05fef5..1d8521c793e6e 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/aws-cdk-route53-cross-account-integ.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/aws-cdk-route53-cross-account-integ.template.json @@ -302,6 +302,86 @@ ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" + }, + "Role1ABCC5F0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "RoleDefaultPolicy5FFB7DAB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "route53:ChangeResourceRecordSets", + "Condition": { + "ForAllValues:StringEquals": { + "route53:ChangeResourceRecordSetsRecordTypes": [ + "NS" + ], + "route53:ChangeResourceRecordSetsActions": [ + "UPSERT", + "DELETE" + ] + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":route53:::hostedzone/imported-public-zone-id" + ] + ] + } + }, + { + "Action": "route53:ListHostedZonesByName", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RoleDefaultPolicy5FFB7DAB", + "Roles": [ + { + "Ref": "Role1ABCC5F0" + } + ] + } } }, "Parameters": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/manifest.json index 646cd1f7d8514..4ccccaed927c8 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/manifest.json @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/3222f491727b0389ac87f972f2443b490ff3cee14d24c28f1527c3f085cab460.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/52da24cb67101152630cedcc08830f183f595580f8a7f6fcef1e0aac216c7198.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -93,6 +93,18 @@ "data": "DelegationWithZoneNameCrossAccountZoneDelegationCustomResourceA1A1C94A" } ], + "/aws-cdk-route53-cross-account-integ/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Role1ABCC5F0" + } + ], + "/aws-cdk-route53-cross-account-integ/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "RoleDefaultPolicy5FFB7DAB" + } + ], "/aws-cdk-route53-cross-account-integ/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/tree.json index d74a2a27509b1..583d08a41ccc4 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.js.snapshot/tree.json @@ -427,6 +427,138 @@ "version": "0.0.0" } }, + "Role": { + "id": "Role", + "path": "aws-cdk-route53-cross-account-integ/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "aws-cdk-route53-cross-account-integ/Role/ImportRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-route53-cross-account-integ/Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-route53-cross-account-integ/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-route53-cross-account-integ/Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "route53:ChangeResourceRecordSets", + "Condition": { + "ForAllValues:StringEquals": { + "route53:ChangeResourceRecordSetsRecordTypes": [ + "NS" + ], + "route53:ChangeResourceRecordSetsActions": [ + "UPSERT", + "DELETE" + ] + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":route53:::hostedzone/imported-public-zone-id" + ] + ] + } + }, + { + "Action": "route53:ListHostedZonesByName", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "policyName": "RoleDefaultPolicy5FFB7DAB", + "roles": [ + { + "Ref": "Role1ABCC5F0" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "ImportedPublicZone": { + "id": "ImportedPublicZone", + "path": "aws-cdk-route53-cross-account-integ/ImportedPublicZone", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "aws-cdk-route53-cross-account-integ/BootstrapVersion", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.ts index 0e72c2df287d5..e8e5d3d154b1b 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-route53/test/integ.cross-account-zone-delegation.ts @@ -32,8 +32,16 @@ new CrossAccountZoneDelegationRecord(stack, 'DelegationWithZoneName', { delegationRole: parentZone.crossAccountZoneDelegationRole!, }); +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AccountRootPrincipal(), +}); + +const importedPublicZone = PublicHostedZone.fromPublicHostedZoneId(stack, 'ImportedPublicZone', 'imported-public-zone-id'); +importedPublicZone.grantDelegation(role); + new IntegTest(app, 'Route53CrossAccountInteg', { testCases: [stack], diffAssets: true, }); + app.synth(); diff --git a/packages/aws-cdk-lib/aws-ec2/README.md b/packages/aws-cdk-lib/aws-ec2/README.md index 709e363140811..9f909c613d08c 100644 --- a/packages/aws-cdk-lib/aws-ec2/README.md +++ b/packages/aws-cdk-lib/aws-ec2/README.md @@ -980,8 +980,8 @@ Endpoint services support private DNS, which makes it easier for clients to conn You can enable private DNS on an endpoint service like so: ```ts -import { HostedZone, VpcEndpointServiceDomainName } from 'aws-cdk-lib/aws-route53'; -declare const zone: HostedZone; +import { PublicHostedZone, VpcEndpointServiceDomainName } from 'aws-cdk-lib/aws-route53'; +declare const zone: PublicHostedZone; declare const vpces: ec2.VpcEndpointService; new VpcEndpointServiceDomainName(this, 'EndpointDomain', { diff --git a/packages/aws-cdk-lib/aws-route53/README.md b/packages/aws-cdk-lib/aws-route53/README.md index 3b5ee894c9494..af2036ca097fd 100644 --- a/packages/aws-cdk-lib/aws-route53/README.md +++ b/packages/aws-cdk-lib/aws-route53/README.md @@ -289,6 +289,20 @@ const zoneFromAttributes = route53.PublicHostedZone.fromPublicHostedZoneAttribut const zoneFromId = route53.PublicHostedZone.fromPublicHostedZoneId(this, 'MyZone', 'ZOJJZC49E0EPZ'); ``` +You can use `CrossAccountZoneDelegationRecord` on imported Public Hosted Zones with the `grantDelegation` method: + +```ts +const crossAccountRole = new iam.Role(this, 'CrossAccountRole', { + // The role name must be predictable + roleName: 'MyDelegationRole', + // The other account + assumedBy: new iam.AccountPrincipal('12345678901'), +}); + +const zoneFromId = route53.PublicHostedZone.fromPublicHostedZoneId(this, 'MyZone', 'ZOJJZC49E0EPZ'); +zoneFromId.grantDelegation(crossAccountRole); +``` + ## VPC Endpoint Service Private DNS When you create a VPC endpoint service, AWS generates endpoint-specific DNS hostnames that consumers use to communicate with the service. diff --git a/packages/aws-cdk-lib/aws-route53/lib/hosted-zone.ts b/packages/aws-cdk-lib/aws-route53/lib/hosted-zone.ts index efe94731622ae..cbb8ad6faf524 100644 --- a/packages/aws-cdk-lib/aws-route53/lib/hosted-zone.ts +++ b/packages/aws-cdk-lib/aws-route53/lib/hosted-zone.ts @@ -3,7 +3,7 @@ import { HostedZoneProviderProps } from './hosted-zone-provider'; import { HostedZoneAttributes, IHostedZone, PublicHostedZoneAttributes } from './hosted-zone-ref'; import { CaaAmazonRecord, ZoneDelegationRecord } from './record-set'; import { CfnHostedZone } from './route53.generated'; -import { makeHostedZoneArn, validateZoneName } from './util'; +import { makeGrantDelegation, makeHostedZoneArn, validateZoneName } from './util'; import * as ec2 from '../../aws-ec2'; import * as iam from '../../aws-iam'; import * as cxschema from '../../cloud-assembly-schema'; @@ -238,7 +238,12 @@ export interface PublicHostedZoneProps extends CommonHostedZoneProps { /** * Represents a Route 53 public hosted zone */ -export interface IPublicHostedZone extends IHostedZone { } +export interface IPublicHostedZone extends IHostedZone { + /** + * Grant permissions to add delegation records to this zone + */ + grantDelegation(grantee: iam.IGrantable): iam.Grant; +} /** * Create a Route53 public hosted zone. @@ -264,6 +269,9 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { public get hostedZoneArn(): string { return makeHostedZoneArn(this, this.hostedZoneId); } + public grantDelegation(grantee: iam.IGrantable): iam.Grant { + return makeGrantDelegation(grantee, this.hostedZoneArn); + }; } return new Import(scope, id); } @@ -284,6 +292,9 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { public get hostedZoneArn(): string { return makeHostedZoneArn(this, this.hostedZoneId); } + public grantDelegation(grantee: iam.IGrantable): iam.Grant { + return makeGrantDelegation(grantee, this.hostedZoneArn); + }; } return new Import(scope, id); } @@ -354,28 +365,8 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { }); } - /** - * Grant permissions to add delegation records to this zone - */ - public grantDelegation(grantee: iam.IGrantable) { - const g1 = iam.Grant.addToPrincipal({ - grantee, - actions: ['route53:ChangeResourceRecordSets'], - resourceArns: [this.hostedZoneArn], - conditions: { - 'ForAllValues:StringEquals': { - 'route53:ChangeResourceRecordSetsRecordTypes': ['NS'], - 'route53:ChangeResourceRecordSetsActions': ['UPSERT', 'DELETE'], - }, - }, - }); - const g2 = iam.Grant.addToPrincipal({ - grantee, - actions: ['route53:ListHostedZonesByName'], - resourceArns: ['*'], - }); - - return g1.combine(g2); + public grantDelegation(grantee: iam.IGrantable): iam.Grant { + return makeGrantDelegation(grantee, this.hostedZoneArn); } } diff --git a/packages/aws-cdk-lib/aws-route53/lib/util.ts b/packages/aws-cdk-lib/aws-route53/lib/util.ts index b6416e49a366f..6f8f832289a98 100644 --- a/packages/aws-cdk-lib/aws-route53/lib/util.ts +++ b/packages/aws-cdk-lib/aws-route53/lib/util.ts @@ -1,5 +1,6 @@ import { Construct } from 'constructs'; import { IHostedZone } from './hosted-zone-ref'; +import * as iam from '../../aws-iam'; import { Stack } from '../../core'; /** @@ -69,3 +70,24 @@ export function makeHostedZoneArn(construct: Construct, hostedZoneId: string): s resourceName: hostedZoneId, }); } + +export function makeGrantDelegation(grantee: iam.IGrantable, hostedZoneArn: string): iam.Grant { + const g1 = iam.Grant.addToPrincipal({ + grantee, + actions: ['route53:ChangeResourceRecordSets'], + resourceArns: [hostedZoneArn], + conditions: { + 'ForAllValues:StringEquals': { + 'route53:ChangeResourceRecordSetsRecordTypes': ['NS'], + 'route53:ChangeResourceRecordSetsActions': ['UPSERT', 'DELETE'], + }, + }, + }); + const g2 = iam.Grant.addToPrincipal({ + grantee, + actions: ['route53:ListHostedZonesByName'], + resourceArns: ['*'], + }); + + return g1.combine(g2); +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-route53/test/hosted-zone.test.ts b/packages/aws-cdk-lib/aws-route53/test/hosted-zone.test.ts index 4f37b586ee041..a5b2722a8adf5 100644 --- a/packages/aws-cdk-lib/aws-route53/test/hosted-zone.test.ts +++ b/packages/aws-cdk-lib/aws-route53/test/hosted-zone.test.ts @@ -288,6 +288,57 @@ test('grantDelegation', () => { }); }); +test('grantDelegation on imported public zones', () => { + // GIVEN + const stack = new cdk.Stack(undefined, 'TestStack', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AccountPrincipal('22222222222222'), + }); + + const zone = PublicHostedZone.fromPublicHostedZoneId(stack, 'Zone', 'hosted-id'); + + // WHEN + zone.grantDelegation(role); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'route53:ChangeResourceRecordSets', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':route53:::hostedzone/hosted-id', + ], + ], + }, + Condition: { + 'ForAllValues:StringEquals': { + 'route53:ChangeResourceRecordSetsRecordTypes': ['NS'], + 'route53:ChangeResourceRecordSetsActions': ['UPSERT', 'DELETE'], + }, + }, + }, + { + Action: 'route53:ListHostedZonesByName', + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }); +}); + describe('Hosted Zone with dot', () => { test('Hosted Zone constructs without trailing dot by default', () => { // GIVEN