Skip to content

Commit

Permalink
feat(apigatewayv2): domain endpoint type, security policy and endpoin…
Browse files Browse the repository at this point in the history
…t migration (#17518)

Updating the L2 construct for `AWS::ApiGatewayV2::DomainName` resource to add support for DomainNameConfigurations.
DomainNameConfigurations is a list of configurations for an API's domain name (CFN equivalent - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-domainname-domainnameconfiguration.html) which includes endpoint type, security policy, ownership certificate.

Changes include:
- Code update to support the properties mentioned above
- Unit test changes for existing tests to account for the updated `domainNameConfigurations` Lazy evaluation. 
- New unit tests for mutual tls with ownership certificate, and domain name migration that were not present before.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
SmritiVashisth committed Nov 22, 2021
1 parent e09ceaa commit 261b331
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 13 deletions.
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ const api = new apigwv2.HttpApi(this, 'HttpProxyProdApi', {
});
```

To migrate a domain endpoint from one type to another, you can add a new endpoint configuration via `addEndpoint()`
and then configure DNS records to route traffic to the new endpoint. After that, you can remove the previous endpoint configuration.
Learn more at [Migrating a custom domain name](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-regional-api-custom-domain-migrate.html)

To associate a specific `Stage` to a custom domain mapping -

```ts
Expand Down
114 changes: 102 additions & 12 deletions packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
import { ICertificate } from '@aws-cdk/aws-certificatemanager';
import { IBucket } from '@aws-cdk/aws-s3';
import { IResource, Resource, Token } from '@aws-cdk/core';
import { IResource, Lazy, Resource, Token } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnDomainName, CfnDomainNameProps } from '../apigatewayv2.generated';

/**
* The minimum version of the SSL protocol that you want API Gateway to use for HTTPS connections.
*/
export enum SecurityPolicy {
/** Cipher suite TLS 1.0 */
TLS_1_0 = 'TLS_1_0',

/** Cipher suite TLS 1.2 */
TLS_1_2 = 'TLS_1_2',
}

/**
* Endpoint type for a domain name.
*/
export enum EndpointType {
/**
* For an edge-optimized custom domain name.
*/
EDGE = 'EDGE',
/**
* For a regional custom domain name.
*/
REGIONAL = 'REGIONAL',
}

/**
* Represents an APIGatewayV2 DomainName
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-domainname.html
Expand Down Expand Up @@ -51,20 +76,54 @@ export interface DomainNameAttributes {
/**
* properties used for creating the DomainName
*/
export interface DomainNameProps {
export interface DomainNameProps extends EndpointOptions {
/**
* The custom domain name
*/
readonly domainName: string;

/**
* The mutual TLS authentication configuration for a custom domain name.
* @default - mTLS is not configured.
*/
readonly mtls?: MTLSConfig;
}

/**
* properties for creating a domain name endpoint
*/
export interface EndpointOptions {
/**
* The ACM certificate for this domain name
* The ACM certificate for this domain name.
* Certificate can be both ACM issued or imported.
*/
readonly certificate: ICertificate;

/**
* The mutual TLS authentication configuration for a custom domain name.
* @default - mTLS is not configured.
* The user-friendly name of the certificate that will be used by the endpoint for this domain name.
* @default - No friendly certificate name
*/
readonly certificateName?: string;

/**
* The type of endpoint for this DomainName.
* @default EndpointType.REGIONAL
*/
readonly endpointType?: EndpointType;

/**
* The Transport Layer Security (TLS) version + cipher suite for this domain name.
* @default SecurityPolicy.TLS_1_2
*/
readonly securityPolicy?: SecurityPolicy;

/**
* A public certificate issued by ACM to validate that you own a custom domain. This parameter is required
* only when you configure mutual TLS authentication and you specify an ACM imported or private CA certificate
* for `certificate`. The ownership certificate validates that you have permissions to use the domain name.
* @default - only required when configuring mTLS
*/
readonly mtls?: MTLSConfig
readonly ownershipCertificate?: ICertificate;
}

/**
Expand Down Expand Up @@ -107,6 +166,7 @@ export class DomainName extends Resource implements IDomainName {
public readonly name: string;
public readonly regionalDomainName: string;
public readonly regionalHostedZoneId: string;
private readonly domainNameConfigurations: CfnDomainName.DomainNameConfigurationProperty[] = [];

constructor(scope: Construct, id: string, props: DomainNameProps) {
super(scope, id);
Expand All @@ -115,21 +175,25 @@ export class DomainName extends Resource implements IDomainName {
throw new Error('empty string for domainName not allowed');
}

// validation for ownership certificate
if (props.ownershipCertificate && !props.mtls) {
throw new Error('ownership certificate can only be used with mtls domains');
}

const mtlsConfig = this.configureMTLS(props.mtls);
const domainNameProps: CfnDomainNameProps = {
domainName: props.domainName,
domainNameConfigurations: [
{
certificateArn: props.certificate.certificateArn,
endpointType: 'REGIONAL',
},
],
domainNameConfigurations: Lazy.any({ produce: () => this.domainNameConfigurations }),
mutualTlsAuthentication: mtlsConfig,
};
const resource = new CfnDomainName(this, 'Resource', domainNameProps);
this.name = resource.ref;
this.regionalDomainName = Token.asString(resource.getAtt('RegionalDomainName'));
this.regionalHostedZoneId = Token.asString(resource.getAtt('RegionalHostedZoneId'));

if (props.certificate) {
this.addEndpoint(props);
}
}

private configureMTLS(mtlsConfig?: MTLSConfig): CfnDomainName.MutualTlsAuthenticationProperty | undefined {
Expand All @@ -139,4 +203,30 @@ export class DomainName extends Resource implements IDomainName {
truststoreVersion: mtlsConfig.version,
};
}

/**
* Adds an endpoint to a domain name.
* @param options domain name endpoint properties to be set
*/
public addEndpoint(options: EndpointOptions) : void {
const domainNameConfig: CfnDomainName.DomainNameConfigurationProperty = {
certificateArn: options.certificate.certificateArn,
certificateName: options.certificateName,
endpointType: options.endpointType ? options.endpointType?.toString() : 'REGIONAL',
ownershipVerificationCertificateArn: options.ownershipCertificate?.certificateArn,
securityPolicy: options.securityPolicy?.toString(),
};

this.validateEndpointType(domainNameConfig.endpointType);
this.domainNameConfigurations.push(domainNameConfig);
}

// validates that the new domain name configuration has a unique endpoint
private validateEndpointType(endpointType: string | undefined) : void {
for (let config of this.domainNameConfigurations) {
if (endpointType && endpointType == config.endpointType) {
throw new Error(`an endpoint with type ${endpointType} already exists`);
}
}
}
}
109 changes: 108 additions & 1 deletion packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { Template } from '@aws-cdk/assertions';
import { Certificate } from '@aws-cdk/aws-certificatemanager';
import { Bucket } from '@aws-cdk/aws-s3';
import { Stack } from '@aws-cdk/core';
import { DomainName, HttpApi } from '../../lib';
import { DomainName, EndpointType, HttpApi, SecurityPolicy } from '../../lib';

const domainName = 'example.com';
const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate';
const certArn2 = 'arn:aws:acm:us-east-1:111111111111:certificate2';
const ownershipCertArn = 'arn:aws:acm:us-east-1:111111111111:ownershipcertificate';

describe('DomainName', () => {
test('create domain name correctly', () => {
Expand Down Expand Up @@ -231,4 +233,109 @@ describe('DomainName', () => {
},
});
});

test('domain with mutual tls configuration and ownership cert', () => {
// GIVEN
const stack = new Stack();
const bucket = Bucket.fromBucketName(stack, 'testBucket', 'example-bucket');

// WHEN
new DomainName(stack, 'DomainName', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert2', certArn2),
ownershipCertificate: Certificate.fromCertificateArn(stack, 'ownershipCert', ownershipCertArn),
endpointType: EndpointType.REGIONAL,
securityPolicy: SecurityPolicy.TLS_1_2,
mtls: {
bucket,
key: 'someca.pem',
version: 'version',
},
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::DomainName', {
DomainName: 'example.com',
DomainNameConfigurations: [
{
CertificateArn: 'arn:aws:acm:us-east-1:111111111111:certificate2',
EndpointType: 'REGIONAL',
SecurityPolicy: 'TLS_1_2',
OwnershipVerificationCertificateArn: 'arn:aws:acm:us-east-1:111111111111:ownershipcertificate',
},
],
MutualTlsAuthentication: {
TruststoreUri: 's3://example-bucket/someca.pem',
TruststoreVersion: 'version',
},
});
});

test('throws when ownerhsip cert is used for non-mtls domain', () => {
// GIVEN
const stack = new Stack();

// WHEN
const t = () => {
new DomainName(stack, 'DomainName', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert2', certArn2),
ownershipCertificate: Certificate.fromCertificateArn(stack, 'ownershipCert', ownershipCertArn),
});
};

// THEN
expect(t).toThrow(/ownership certificate can only be used with mtls domains/);
});

test('add new configuration to a domain name for migration', () => {
// GIVEN
const stack = new Stack();

// WHEN
const dn = new DomainName(stack, 'DomainName', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert', certArn),
endpointType: EndpointType.REGIONAL,
});
dn.addEndpoint({
certificate: Certificate.fromCertificateArn(stack, 'cert2', certArn2),
endpointType: EndpointType.EDGE,
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::DomainName', {
DomainName: 'example.com',
DomainNameConfigurations: [
{
CertificateArn: 'arn:aws:acm:us-east-1:111111111111:certificate',
EndpointType: 'REGIONAL',
},
{
CertificateArn: 'arn:aws:acm:us-east-1:111111111111:certificate2',
EndpointType: 'EDGE',
},
],
});
});

test('throws when endpoint types for two domain name configurations are the same', () => {
// GIVEN
const stack = new Stack();

// WHEN
const t = () => {
const dn = new DomainName(stack, 'DomainName', {
domainName,
certificate: Certificate.fromCertificateArn(stack, 'cert', certArn),
endpointType: EndpointType.REGIONAL,
});
dn.addEndpoint({
certificate: Certificate.fromCertificateArn(stack, 'cert2', certArn2),
});
};

// THEN
expect(t).toThrow(/an endpoint with type REGIONAL already exists/);
});
});

0 comments on commit 261b331

Please sign in to comment.