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
27 changes: 27 additions & 0 deletions packages/aws-cdk-lib/aws-route53/lib/hosted-zone-grants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { GrantDelegationOptions, INamedHostedZoneRef } from './hosted-zone-ref';
import { makeGrantDelegation } from './util';
import { IGrantable } from '../../aws-iam';
import { Grant } from '../../aws-iam/lib/grant';

/**
* Collection of grant methods for a INamedHostedZoneRef
*/
export class HostedZoneGrants {
/**
* Creates grants for INamedHostedZoneRef
*
*/
public static fromHostedZone(hostedZone: INamedHostedZoneRef): HostedZoneGrants {
return new HostedZoneGrants(hostedZone);
}

private constructor(private readonly hostedZone: INamedHostedZoneRef) {
}

/**
* Grant permissions to add delegation records to this zone
*/
public delegation(grantee: IGrantable, delegationOptions?: GrantDelegationOptions): Grant {
return makeGrantDelegation(grantee, this.hostedZone, delegationOptions);
}
}
7 changes: 6 additions & 1 deletion packages/aws-cdk-lib/aws-route53/lib/hosted-zone-ref.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import * as iam from '../../aws-iam';
import { IResource } from '../../core';
import { IHostedZoneRef } from '../../interfaces/generated/aws-route53-interfaces.generated';

export interface INamedHostedZoneRef extends IHostedZoneRef {
readonly name: string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not zoneName: string which already exists?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that CfnHostedZone automatically implements this interface.

}

/**
* Imported or created hosted zone
*/
export interface IHostedZone extends IResource {
export interface IHostedZone extends IResource, INamedHostedZoneRef {
/**
* ID of this hosted zone, such as "Z23ABC4XYZL05B"
*
Expand Down
73 changes: 64 additions & 9 deletions packages/aws-cdk-lib/aws-route53/lib/hosted-zone.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Construct } from 'constructs';
import { HostedZoneGrants } from './hosted-zone-grants';
import { HostedZoneProviderProps } from './hosted-zone-provider';
import { GrantDelegationOptions, HostedZoneAttributes, IHostedZone, PublicHostedZoneAttributes, PrivateHostedZoneAttributes } from './hosted-zone-ref';
import { IKeySigningKey, KeySigningKey } from './key-signing-key';
import { CaaAmazonRecord, ZoneDelegationRecord } from './record-set';
import { CfnHostedZone, CfnDNSSEC, CfnKeySigningKey } from './route53.generated';
import { makeGrantDelegation, makeHostedZoneArn, validateZoneName } from './util';
import { CfnHostedZone, CfnDNSSEC, CfnKeySigningKey, HostedZoneReference } from './route53.generated';
import { makeHostedZoneArn, validateZoneName } from './util';
import * as ec2 from '../../aws-ec2';
import * as iam from '../../aws-iam';
import * as kms from '../../aws-kms';
Expand Down Expand Up @@ -98,6 +99,13 @@ export class HostedZone extends Resource implements IHostedZone {
return makeHostedZoneArn(this, this.hostedZoneId);
}

/**
* FQDN of this hosted zone
*/
public get name(): string {
return this.zoneName;
}

/**
* Import a Route 53 hosted zone defined either outside the CDK, or in a different CDK stack
*
Expand All @@ -111,14 +119,20 @@ export class HostedZone extends Resource implements IHostedZone {
public static fromHostedZoneId(scope: Construct, id: string, hostedZoneId: string): IHostedZone {
class Import extends Resource implements IHostedZone {
public readonly hostedZoneId = hostedZoneId;
public get name(): string { return this.zoneName; }
public get zoneName(): string {
throw new ValidationError('Cannot reference `zoneName` when using `HostedZone.fromHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`. If this is the case, use `fromHostedZoneAttributes()` or `fromLookup()` instead.', this);
}
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
}
public grantDelegation(grantee: iam.IGrantable, options?: GrantDelegationOptions): iam.Grant {
return makeGrantDelegation(grantee, this, options);
return HostedZoneGrants.fromHostedZone(this).delegation(grantee, options);
}
public get hostedZoneRef(): HostedZoneReference {
return {
hostedZoneId: this.hostedZoneId,
};
}
}

Expand All @@ -138,11 +152,17 @@ export class HostedZone extends Resource implements IHostedZone {
class Import extends Resource implements IHostedZone {
public readonly hostedZoneId = attrs.hostedZoneId;
public readonly zoneName = attrs.zoneName;
public readonly name = attrs.zoneName;
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
}
public grantDelegation(grantee: iam.IGrantable, options?: GrantDelegationOptions): iam.Grant {
return makeGrantDelegation(grantee, this, options);
return HostedZoneGrants.fromHostedZone(this).delegation(grantee, options);
}
public get hostedZoneRef(): HostedZoneReference {
return {
hostedZoneId: this.hostedZoneId,
};
}
}

Expand Down Expand Up @@ -204,6 +224,11 @@ export class HostedZone extends Resource implements IHostedZone {
*/
private keySigningKey?: IKeySigningKey;

/**
* Grants helper for this hosted zone
*/
public readonly grants = HostedZoneGrants.fromHostedZone(this);

constructor(scope: Construct, id: string, props: HostedZoneProps) {
super(scope, id);
// Enhanced CDK Analytics Telemetry
Expand All @@ -230,6 +255,12 @@ export class HostedZone extends Resource implements IHostedZone {
}
}

public get hostedZoneRef(): HostedZoneReference {
return {
hostedZoneId: this.hostedZoneId,
};
}

/**
* Add another VPC to this private hosted zone.
*
Expand All @@ -242,7 +273,7 @@ export class HostedZone extends Resource implements IHostedZone {

@MethodMetadata()
public grantDelegation(grantee: iam.IGrantable, options?: GrantDelegationOptions): iam.Grant {
return makeGrantDelegation(grantee, this, options);
return this.grants.delegation(grantee, options);
}

/**
Expand Down Expand Up @@ -341,12 +372,18 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone {
public static fromPublicHostedZoneId(scope: Construct, id: string, publicHostedZoneId: string): IPublicHostedZone {
class Import extends Resource implements IPublicHostedZone {
public readonly hostedZoneId = publicHostedZoneId;
public get name(): string { return this.zoneName; }
public get zoneName(): string { throw new ValidationError('Cannot reference `zoneName` when using `PublicHostedZone.fromPublicHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`. If this is the case, use `fromPublicHostedZoneAttributes()` instead', this); }
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
}
public grantDelegation(grantee: iam.IGrantable, options?: GrantDelegationOptions): iam.Grant {
return makeGrantDelegation(grantee, this, options);
return HostedZoneGrants.fromHostedZone(this).delegation(grantee, options);
}
public get hostedZoneRef(): HostedZoneReference {
return {
hostedZoneId: this.hostedZoneId,
};
}
}
return new Import(scope, id);
Expand All @@ -365,11 +402,17 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone {
class Import extends Resource implements IPublicHostedZone {
public readonly hostedZoneId = attrs.hostedZoneId;
public readonly zoneName = attrs.zoneName;
public readonly name = attrs.zoneName;
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
}
public grantDelegation(grantee: iam.IGrantable, options?: GrantDelegationOptions): iam.Grant {
return makeGrantDelegation(grantee, this, options);
return HostedZoneGrants.fromHostedZone(this).delegation(grantee, options);
}
public get hostedZoneRef(): HostedZoneReference {
return {
hostedZoneId: this.zoneName,
};
}
}
return new Import(scope, id);
Expand Down Expand Up @@ -509,12 +552,18 @@ export class PrivateHostedZone extends HostedZone implements IPrivateHostedZone
public static fromPrivateHostedZoneId(scope: Construct, id: string, privateHostedZoneId: string): IPrivateHostedZone {
class Import extends Resource implements IPrivateHostedZone {
public readonly hostedZoneId = privateHostedZoneId;
public get name(): string { return this.zoneName; }
public get zoneName(): string { throw new ValidationError('Cannot reference `zoneName` when using `PrivateHostedZone.fromPrivateHostedZoneId()`. A construct consuming this hosted zone may be trying to reference its `zoneName`', this); }
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
}
public grantDelegation(grantee: iam.IGrantable, options?: GrantDelegationOptions): iam.Grant {
return makeGrantDelegation(grantee, this, options);
return HostedZoneGrants.fromHostedZone(this).delegation(grantee, options);
}
public get hostedZoneRef(): HostedZoneReference {
return {
hostedZoneId: this.hostedZoneId,
};
}
}
return new Import(scope, id);
Expand All @@ -533,11 +582,17 @@ export class PrivateHostedZone extends HostedZone implements IPrivateHostedZone
class Import extends Resource implements IPrivateHostedZone {
public readonly hostedZoneId = attrs.hostedZoneId;
public readonly zoneName = attrs.zoneName;
public get name(): string { return this.zoneName; }
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
}
public grantDelegation(grantee: iam.IGrantable, options?: GrantDelegationOptions): iam.Grant {
return makeGrantDelegation(grantee, this, options);
return HostedZoneGrants.fromHostedZone(this).delegation(grantee, options);
}
public get hostedZoneRef(): HostedZoneReference {
return {
hostedZoneId: this.hostedZoneId,
};
}
}
return new Import(scope, id);
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/aws-route53/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './alias-record-target';
export * from './hosted-zone';
export * from './hosted-zone-grants';
export * from './hosted-zone-provider';
export * from './hosted-zone-ref';
export * from './key-signing-key';
Expand Down
8 changes: 4 additions & 4 deletions packages/aws-cdk-lib/aws-route53/lib/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Construct } from 'constructs';
import { GrantDelegationOptions, IHostedZone } from './hosted-zone-ref';
import { GrantDelegationOptions, IHostedZone, INamedHostedZoneRef } from './hosted-zone-ref';
import * as iam from '../../aws-iam';
import { Stack, Token, UnscopedValidationError } from '../../core';

Expand Down Expand Up @@ -137,15 +137,15 @@ function validateDelegatedZoneName(parentZoneName: string, delegatedZoneName: st
}
}

export function makeGrantDelegation(grantee: iam.IGrantable, hostedZone: IHostedZone, delegationOptions?: GrantDelegationOptions): iam.Grant {
export function makeGrantDelegation(grantee: iam.IGrantable, hostedZone: INamedHostedZoneRef, delegationOptions?: GrantDelegationOptions): iam.Grant {
const delegatedZoneNames = delegationOptions?.delegatedZoneNames?.map(delegatedZoneName => {
validateDelegatedZoneName(hostedZone.zoneName, delegatedZoneName);
validateDelegatedZoneName(hostedZone.name, delegatedZoneName);
return octalEncodeDelegatedZoneName(delegatedZoneName);
});
const g1 = iam.Grant.addToPrincipal({
grantee,
actions: ['route53:ChangeResourceRecordSets'],
resourceArns: [hostedZone.hostedZoneArn],
resourceArns: [makeHostedZoneArn(hostedZone, hostedZone.hostedZoneRef.hostedZoneId)],
conditions: {
'ForAllValues:StringEquals': {
'route53:ChangeResourceRecordSetsRecordTypes': ['NS'],
Expand Down
60 changes: 59 additions & 1 deletion packages/aws-cdk-lib/aws-route53/test/hosted-zone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import * as ec2 from '../../aws-ec2';
import * as iam from '../../aws-iam';
import * as kms from '../../aws-kms';
import * as cdk from '../../core';
import { HostedZone, PrivateHostedZone, PublicHostedZone, ZoneSigningOptions } from '../lib';
import { CfnHostedZone, HostedZone, PrivateHostedZone, PublicHostedZone } from '../lib';
import { HostedZoneGrants } from '../lib/hosted-zone-grants';

describe('hosted zone', () => {
describe('Hosted Zone', () => {
Expand Down Expand Up @@ -289,6 +290,63 @@ test('grantDelegation', () => {
});
});

test('grantDelegation on L1s', () => {
// 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 = new CfnHostedZone(stack, 'Zone', {
name: 'banana.com',
});

// WHEN
HostedZoneGrants.fromHostedZone(zone).delegation(role);

// THEN
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: [
{
Action: 'route53:ChangeResourceRecordSets',
Effect: 'Allow',
Resource: {
'Fn::Join': [
'',
[
'arn:',
{
Ref: 'AWS::Partition',
},
':route53:::hostedzone/',
{
Ref: 'Zone',
},
],
],
},
Condition: {
'ForAllValues:StringEquals': {
'route53:ChangeResourceRecordSetsRecordTypes': ['NS'],
'route53:ChangeResourceRecordSetsActions': ['UPSERT', 'DELETE'],
},
},
},
{
Action: 'route53:ListHostedZonesByName',
Effect: 'Allow',
Resource: '*',
},
],
},
});
});

test('grantDelegation on imported zones', () => {
// GIVEN
const stack = new cdk.Stack(undefined, 'TestStack', {
Expand Down
Loading