Skip to content

Commit

Permalink
feat(route53): support for scoping down domain names in IHostedZone.g…
Browse files Browse the repository at this point in the history
…rantDelegation()

Adds a backwards compatible parameter to `IHostedZone.grantDelegation()`
in order to restrict the `NS` records with `UPSERT`/`DELETE` access.
  • Loading branch information
marcogrcr committed Nov 21, 2023
1 parent 25ee8ef commit 83a8b85
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ParentStack extends cdk.Stack {
roleName: delegationRoleName,
assumedBy: new iam.AccountPrincipal(crossAccount),
});
parentZone.grantDelegation(crossAccountRole);
parentZone.grantDelegation(crossAccountRole, route53.DelegationGrantNames.ofEquals(subZoneName));
}
}

Expand Down
15 changes: 15 additions & 0 deletions packages/aws-cdk-lib/aws-route53/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,21 @@ new route53.CrossAccountZoneDelegationRecord(this, 'delegate', {
});
```

You can optionally restrict the domain names that can be delegated with the IAM role. This allows you to follow the
minimum privilege principle:

```ts
const parentZone = new route53.PublicHostedZone(this, 'HostedZone', {
zoneName: 'someexample.com',
});

declare const betaCrossAccountRole: iam.Role;
parentZone.grantDelegation(betaCrossAccountRole, route53.DelegationGrantNames.ofEquals('beta.someexample.com'));

declare const prodCrossAccountRole: iam:Role;
parentZone.grantDelegation(prodCrossAccountRole, route53.DelegationGrantNames.ofEquals('prod.someexample.com'));
```

### Add Trailing Dot to Domain Names

In order to continue managing existing domain names with trailing dots using CDK, you can set `addTrailingDot: false` to prevent the Construct from adding a dot at the end of the domain name.
Expand Down
45 changes: 45 additions & 0 deletions packages/aws-cdk-lib/aws-route53/lib/delegation-grant-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Constraints the delegation grant to a set of domain names using the IAM
* `route53:ChangeResourceRecordSetsNormalizedRecordNames` context key.
*/
export abstract class DelegationGrantNames {
/**
* Match the domain names using the IAM `equals` operator.
*
* @param names The allowed names to match using the IAM `equals` operator.
*/
public static ofEquals(...names: string[]): DelegationGrantNames {
return new (class extends DelegationGrantNames {
public _equals() {
return names;
}
})();
}

/**
* Match the domain names using the IAM `like` operator.
*
* @param names The allowed names to match using the IAM `like` operator.
*/
public static ofLike(...names: string[]): DelegationGrantNames {
return new (class extends DelegationGrantNames {
public _like() {
return names;
}
})();
}

/**
* @internal
*/
public _equals(): string[] | null {
return null;
}

/**
* @internal
*/
public _like(): string[] | null {
return null;
}
}
6 changes: 5 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,3 +1,4 @@
import { DelegationGrantNames } from './delegation-grant-names';
import * as iam from '../../aws-iam';
import { IResource } from '../../core';

Expand Down Expand Up @@ -36,8 +37,11 @@ export interface IHostedZone extends IResource {

/**
* Grant permissions to add delegation records to this zone
*
* @param grantee grantee to receive the permissions
* @param names specify to restrict the delegation to a specific set of names
*/
grantDelegation(grantee: iam.IGrantable): iam.Grant;
grantDelegation(grantee: iam.IGrantable, names?: DelegationGrantNames): iam.Grant;
}

/**
Expand Down
25 changes: 13 additions & 12 deletions packages/aws-cdk-lib/aws-route53/lib/hosted-zone.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Construct } from 'constructs';
import { DelegationGrantNames } from './delegation-grant-names';
import { HostedZoneProviderProps } from './hosted-zone-provider';
import { HostedZoneAttributes, IHostedZone, PublicHostedZoneAttributes } from './hosted-zone-ref';
import { CaaAmazonRecord, ZoneDelegationRecord } from './record-set';
Expand Down Expand Up @@ -84,8 +85,8 @@ export class HostedZone extends Resource implements IHostedZone {
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
}
public grantDelegation(grantee: iam.IGrantable): iam.Grant {
return makeGrantDelegation(grantee, this.hostedZoneArn);
public grantDelegation(grantee: iam.IGrantable, names?: DelegationGrantNames): iam.Grant {
return makeGrantDelegation(grantee, this.hostedZoneArn, names);
}
}

Expand All @@ -108,8 +109,8 @@ export class HostedZone extends Resource implements IHostedZone {
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
}
public grantDelegation(grantee: iam.IGrantable): iam.Grant {
return makeGrantDelegation(grantee, this.hostedZoneArn);
public grantDelegation(grantee: iam.IGrantable, names?: DelegationGrantNames): iam.Grant {
return makeGrantDelegation(grantee, this.hostedZoneArn, names);
}
}

Expand Down Expand Up @@ -199,8 +200,8 @@ export class HostedZone extends Resource implements IHostedZone {
this.vpcs.push({ vpcId: vpc.vpcId, vpcRegion: vpc.env.region ?? Stack.of(vpc).region });
}

public grantDelegation(grantee: iam.IGrantable): iam.Grant {
return makeGrantDelegation(grantee, this.hostedZoneArn);
public grantDelegation(grantee: iam.IGrantable, names?: DelegationGrantNames): iam.Grant {
return makeGrantDelegation(grantee, this.hostedZoneArn, names);
}
}

Expand Down Expand Up @@ -274,8 +275,8 @@ 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);
public grantDelegation(grantee: iam.IGrantable, names?: DelegationGrantNames): iam.Grant {
return makeGrantDelegation(grantee, this.hostedZoneArn, names);
}
}
return new Import(scope, id);
Expand All @@ -297,8 +298,8 @@ 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);
public grantDelegation(grantee: iam.IGrantable, names?: DelegationGrantNames): iam.Grant {
return makeGrantDelegation(grantee, this.hostedZoneArn, names);
}
}
return new Import(scope, id);
Expand Down Expand Up @@ -435,8 +436,8 @@ export class PrivateHostedZone extends HostedZone implements IPrivateHostedZone
public get hostedZoneArn(): string {
return makeHostedZoneArn(this, this.hostedZoneId);
}
public grantDelegation(grantee: iam.IGrantable): iam.Grant {
return makeGrantDelegation(grantee, this.hostedZoneArn);
public grantDelegation(grantee: iam.IGrantable, names?: DelegationGrantNames): iam.Grant {
return makeGrantDelegation(grantee, this.hostedZoneArn, names);
}
}
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,4 +1,5 @@
export * from './alias-record-target';
export * from './delegation-grant-names';
export * from './hosted-zone';
export * from './hosted-zone-provider';
export * from './hosted-zone-ref';
Expand Down
11 changes: 10 additions & 1 deletion packages/aws-cdk-lib/aws-route53/lib/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Construct } from 'constructs';
import { DelegationGrantNames } from './delegation-grant-names';
import { IHostedZone } from './hosted-zone-ref';
import * as iam from '../../aws-iam';
import { Stack } from '../../core';
Expand Down Expand Up @@ -71,7 +72,7 @@ export function makeHostedZoneArn(construct: Construct, hostedZoneId: string): s
});
}

export function makeGrantDelegation(grantee: iam.IGrantable, hostedZoneArn: string): iam.Grant {
export function makeGrantDelegation(grantee: iam.IGrantable, hostedZoneArn: string, names?: DelegationGrantNames): iam.Grant {
const g1 = iam.Grant.addToPrincipal({
grantee,
actions: ['route53:ChangeResourceRecordSets'],
Expand All @@ -80,7 +81,15 @@ export function makeGrantDelegation(grantee: iam.IGrantable, hostedZoneArn: stri
'ForAllValues:StringEquals': {
'route53:ChangeResourceRecordSetsRecordTypes': ['NS'],
'route53:ChangeResourceRecordSetsActions': ['UPSERT', 'DELETE'],
...(names?._equals() ? {
'route53:ChangeResourceRecordSetsNormalizedRecordNames': names._equals(),
} : {}),
},
...(names?._like() ? {
'ForAllValues:StringLike': {
'route53:ChangeResourceRecordSetsNormalizedRecordNames': names._like(),
},
} : {}),
},
});
const g2 = iam.Grant.addToPrincipal({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DelegationGrantNames } from '../lib/delegation-grant-names';

describe('delegation-grant-names', () => {
const NAMES = ['name-1', 'name-2'];

test('ofEquals() creates instance whose _equals() is not null', () => {
// WHEN
const actual = DelegationGrantNames.ofEquals(...NAMES);

// THEN
expect(actual._equals()).toBe(NAMES);
expect(actual._like()).toBeNull();
});

test('ofLike() creates instance whose _like() is not null', () => {
// WHEN
const actual = DelegationGrantNames.ofLike(...NAMES);

// THEN
expect(actual._equals()).toBeNull();
expect(actual._like()).toBe(NAMES);
});
});
63 changes: 63 additions & 0 deletions packages/aws-cdk-lib/aws-route53/test/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as iam from '../../aws-iam';
import * as cdk from '../../core';
import { HostedZone } from '../lib';
import { DelegationGrantNames } from '../lib/delegation-grant-names';
import * as util from '../lib/util';

describe('util', () => {
Expand Down Expand Up @@ -67,4 +69,65 @@ describe('util', () => {
// THEN
expect(qualified).toEqual('test.domain.com.');
});

test('grant delegation without names returns ChangeResourceRecordSets statement with only two condition keys', () => {
// GIVEN
const stack = new cdk.Stack();
const grantee = new iam.User(stack, 'Grantee');

// WHEN
const actual = util.makeGrantDelegation(grantee, 'hosted-zone');

// WHEN
const statement = actual.principalStatements.find(x => x.actions.includes('route53:ChangeResourceRecordSets'));
expect(statement).not.toBeUndefined();
expect(statement?.conditions).toEqual({
'ForAllValues:StringEquals': {
'route53:ChangeResourceRecordSetsRecordTypes': ['NS'],
'route53:ChangeResourceRecordSetsActions': ['UPSERT', 'DELETE'],
},
});
});

test('grant delegation with equals names returns ChangeResourceRecordSets statement with normalized record names condition', () => {
// GIVEN
const stack = new cdk.Stack();
const grantee = new iam.User(stack, 'Grantee');

// WHEN
const actual = util.makeGrantDelegation(grantee, 'hosted-zone', DelegationGrantNames.ofEquals('name-1', 'name-2'));

// WHEN
const statement = actual.principalStatements.find(x => x.actions.includes('route53:ChangeResourceRecordSets'));
expect(statement).not.toBeUndefined();
expect(statement?.conditions).toEqual({
'ForAllValues:StringEquals': {
'route53:ChangeResourceRecordSetsRecordTypes': ['NS'],
'route53:ChangeResourceRecordSetsActions': ['UPSERT', 'DELETE'],
'route53:ChangeResourceRecordSetsNormalizedRecordNames': ['name-1', 'name-2'],
},
});
});

test('grant delegation with like names returns ChangeResourceRecordSets statement with normalized record names condition', () => {
// GIVEN
const stack = new cdk.Stack();
const grantee = new iam.User(stack, 'Grantee');

// WHEN
const actual = util.makeGrantDelegation(grantee, 'hosted-zone', DelegationGrantNames.ofLike(['name-1', 'name-2']));

// WHEN
const statement = actual.principalStatements.find(x => x.actions.includes('route53:ChangeResourceRecordSets'));
expect(statement).not.toBeUndefined();
expect(statement?.conditions).toEqual({
'ForAllValues:StringEquals': {
'route53:ChangeResourceRecordSetsRecordTypes': ['NS'],
'route53:ChangeResourceRecordSetsActions': ['UPSERT', 'DELETE'],
},
'ForAllValues:StringLike': {
'route53:ChangeResourceRecordSetsNormalizedRecordNames': ['name-1', 'name-2'],
},
});
});
});

0 comments on commit 83a8b85

Please sign in to comment.