From afb37883ab5c8c741f0cccbc89c39acb6803914b Mon Sep 17 00:00:00 2001 From: Luca Pizzini Date: Wed, 3 Jan 2024 17:13:48 +0100 Subject: [PATCH] feat(rds): ClientPasswordAuthType property on DatabaseProxy (#28540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for [`ClientPasswordAuthType`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbproxy-authformat.html#cfn-rds-dbproxy-authformat-clientpasswordauthtype) on `DatabaseProxy` construct. Closes #28415. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-cdk-rds-proxy.template.json | 1 + .../test/aws-rds/test/integ.proxy.ts | 1 + packages/aws-cdk-lib/aws-rds/README.md | 21 +++ packages/aws-cdk-lib/aws-rds/lib/proxy.ts | 48 +++++++ .../aws-cdk-lib/aws-rds/test/proxy.test.ts | 135 ++++++++++++++++++ 5 files changed, 206 insertions(+) diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-rds/test/integ.proxy.js.snapshot/aws-cdk-rds-proxy.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-rds/test/integ.proxy.js.snapshot/aws-cdk-rds-proxy.template.json index a7fff003e31a9..21aa37f0796d9 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-rds/test/integ.proxy.js.snapshot/aws-cdk-rds-proxy.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-rds/test/integ.proxy.js.snapshot/aws-cdk-rds-proxy.template.json @@ -601,6 +601,7 @@ "Auth": [ { "AuthScheme": "SECRETS", + "ClientPasswordAuthType": "POSTGRES_SCRAM_SHA_256", "IAMAuth": "DISABLED", "SecretArn": { "Ref": "dbInstanceSecretAttachment88CFBDAE" diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-rds/test/integ.proxy.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-rds/test/integ.proxy.ts index 8b8164175d258..f293338ee941e 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-rds/test/integ.proxy.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-rds/test/integ.proxy.ts @@ -27,6 +27,7 @@ new rds.DatabaseProxy(stack, 'dbProxy', { secrets: [dbInstance.secret!], proxyTarget: rds.ProxyTarget.fromInstance(dbInstance), vpc, + clientPasswordAuthType: rds.ClientPasswordAuthType.POSTGRES_SCRAM_SHA_256, }); const cluster = new rds.DatabaseCluster(stack, 'dbCluster', { diff --git a/packages/aws-cdk-lib/aws-rds/README.md b/packages/aws-cdk-lib/aws-rds/README.md index 781ba62dd53c7..24046fb698435 100644 --- a/packages/aws-cdk-lib/aws-rds/README.md +++ b/packages/aws-cdk-lib/aws-rds/README.md @@ -788,6 +788,27 @@ proxy.grantConnect(role, 'admin'); // Grant the role connection access to the DB **Note**: In addition to the setup above, a database user will need to be created to support IAM auth. See for setup instructions. +To specify the details of authentication used by a proxy to log in as a specific database +user use the `clientPasswordAuthType` property: + +```ts +declare const vpc: ec2.Vpc; +const cluster = new rds.DatabaseCluster(this, 'Database', { + engine: rds.DatabaseClusterEngine.auroraMysql({ + version: rds.AuroraMysqlEngineVersion.VER_3_03_0, + }), + writer: rds.ClusterInstance.provisioned('writer'), + vpc, +}); + +const proxy = new rds.DatabaseProxy(this, 'Proxy', { + proxyTarget: rds.ProxyTarget.fromCluster(cluster), + secrets: [cluster.secret!], + vpc, + clientPasswordAuthType: rds.ClientPasswordAuthType.MYSQL_NATIVE_PASSWORD, +}); +``` + ### Cluster The following example shows granting connection access for an IAM role to an Aurora Cluster. diff --git a/packages/aws-cdk-lib/aws-rds/lib/proxy.ts b/packages/aws-cdk-lib/aws-rds/lib/proxy.ts index c1cfae1925ca0..1e66307be4406 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/proxy.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/proxy.ts @@ -10,6 +10,28 @@ import * as secretsmanager from '../../aws-secretsmanager'; import * as cdk from '../../core'; import * as cxapi from '../../cx-api'; +/** + * Client password authentication type used by a proxy to log in as a specific database user. + */ +export enum ClientPasswordAuthType { + /** + * MySQL Native Password client authentication type. + */ + MYSQL_NATIVE_PASSWORD = 'MYSQL_NATIVE_PASSWORD', + /** + * SCRAM SHA 256 client authentication type. + */ + POSTGRES_SCRAM_SHA_256 = 'POSTGRES_SCRAM_SHA_256', + /** + * PostgreSQL MD5 client authentication type. + */ + POSTGRES_MD5 = 'POSTGRES_MD5', + /** + * SQL Server Authentication client authentication type. + */ + SQL_SERVER_AUTHENTICATION = 'SQL_SERVER_AUTHENTICATION', +} + /** * SessionPinningFilter * @@ -259,6 +281,13 @@ export interface DatabaseProxyOptions { * The VPC to associate with the new proxy. */ readonly vpc: ec2.IVpc; + + /** + * Specifies the details of authentication used by a proxy to log in as a specific database user. + * + * @default - CloudFormation defaults will apply given the specified database engine. + */ + readonly clientPasswordAuthType?: ClientPasswordAuthType; } /** @@ -445,10 +474,13 @@ export class DatabaseProxy extends DatabaseProxyBase } this.secrets = props.secrets; + this.validateClientPasswordAuthType(bindResult.engineFamily, props.clientPasswordAuthType); + this.resource = new CfnDBProxy(this, 'Resource', { auth: props.secrets.map(_ => { return { authScheme: 'SECRETS', + clientPasswordAuthType: props.clientPasswordAuthType, iamAuth: props.iamAuth ? 'REQUIRED' : 'DISABLED', secretArn: _.secretArn, }; @@ -529,6 +561,22 @@ export class DatabaseProxy extends DatabaseProxyBase } return super.grantConnect(grantee, dbUser); } + + private validateClientPasswordAuthType(engineFamily: string, clientPasswordAuthType?: ClientPasswordAuthType) { + if (!clientPasswordAuthType || cdk.Token.isUnresolved(clientPasswordAuthType)) return; + if (clientPasswordAuthType === ClientPasswordAuthType.MYSQL_NATIVE_PASSWORD && engineFamily !== 'MYSQL') { + throw new Error(`${ClientPasswordAuthType.MYSQL_NATIVE_PASSWORD} client password authentication type requires MYSQL engineFamily, got ${engineFamily}`); + } + if (clientPasswordAuthType === ClientPasswordAuthType.POSTGRES_SCRAM_SHA_256 && engineFamily !== 'POSTGRESQL') { + throw new Error(`${ClientPasswordAuthType.POSTGRES_SCRAM_SHA_256} client password authentication type requires POSTGRESQL engineFamily, got ${engineFamily}`); + } + if (clientPasswordAuthType === ClientPasswordAuthType.POSTGRES_MD5 && engineFamily !== 'POSTGRESQL') { + throw new Error(`${ClientPasswordAuthType.POSTGRES_MD5} client password authentication type requires POSTGRESQL engineFamily, got ${engineFamily}`); + } + if (clientPasswordAuthType === ClientPasswordAuthType.SQL_SERVER_AUTHENTICATION && engineFamily !== 'SQLSERVER') { + throw new Error(`${ClientPasswordAuthType.SQL_SERVER_AUTHENTICATION} client password authentication type requires SQLSERVER engineFamily, got ${engineFamily}`); + } + } } /** diff --git a/packages/aws-cdk-lib/aws-rds/test/proxy.test.ts b/packages/aws-cdk-lib/aws-rds/test/proxy.test.ts index 492e90c295d94..6546164210972 100644 --- a/packages/aws-cdk-lib/aws-rds/test/proxy.test.ts +++ b/packages/aws-cdk-lib/aws-rds/test/proxy.test.ts @@ -450,6 +450,141 @@ describe('proxy', () => { ], }); }); + + describe('clientPasswordAuthType', () => { + test('create a DB proxy with specified client password authentication type', () => { + // GIVEN + const instance = new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.mysql({ + version: rds.MysqlEngineVersion.VER_5_7, + }), + vpc, + }); + + // WHEN + new rds.DatabaseProxy(stack, 'Proxy', { + proxyTarget: rds.ProxyTarget.fromInstance(instance), + secrets: [instance.secret!], + vpc, + clientPasswordAuthType: rds.ClientPasswordAuthType.MYSQL_NATIVE_PASSWORD, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::RDS::DBProxy', { + Auth: [ + { + AuthScheme: 'SECRETS', + IAMAuth: 'DISABLED', + ClientPasswordAuthType: 'MYSQL_NATIVE_PASSWORD', + SecretArn: { + Ref: 'InstanceSecretAttachment83BEE581', + }, + }, + ], + DBProxyName: 'Proxy', + EngineFamily: 'MYSQL', + RequireTLS: true, + RoleArn: { + 'Fn::GetAtt': [ + 'ProxyIAMRole2FE8AB0F', + 'Arn', + ], + }, + VpcSubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + }); + }); + + test('MYSQL_NATIVE_PASSWORD clientPasswordAuthType requires MYSQL engine family', () => { + // GIVEN + const instance = new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.postgres({ + version: rds.PostgresEngineVersion.VER_11, + }), + vpc, + }); + + // WHEN + // THEN + expect(() => { + new rds.DatabaseProxy(stack, 'Proxy', { + proxyTarget: rds.ProxyTarget.fromInstance(instance), + secrets: [instance.secret!], + vpc, + clientPasswordAuthType: rds.ClientPasswordAuthType.MYSQL_NATIVE_PASSWORD, + }); + }).toThrow(/MYSQL_NATIVE_PASSWORD client password authentication type requires MYSQL engineFamily, got POSTGRESQL/); + }); + + test('POSTGRES_SCRAM_SHA_256 clientPasswordAuthType requires POSTGRESQL engine family', () => { + // GIVEN + const instance = new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.mysql({ + version: rds.MysqlEngineVersion.VER_5_7, + }), + vpc, + }); + + // WHEN + // THEN + expect(() => { + new rds.DatabaseProxy(stack, 'Proxy', { + proxyTarget: rds.ProxyTarget.fromInstance(instance), + secrets: [instance.secret!], + vpc, + clientPasswordAuthType: rds.ClientPasswordAuthType.POSTGRES_SCRAM_SHA_256, + }); + }).toThrow(/POSTGRES_SCRAM_SHA_256 client password authentication type requires POSTGRESQL engineFamily, got MYSQL/); + }); + + test('POSTGRES_MD5 clientPasswordAuthType requires POSTGRESQL engine family', () => { + // GIVEN + const instance = new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.mysql({ + version: rds.MysqlEngineVersion.VER_5_7, + }), + vpc, + }); + + // WHEN + // THEN + expect(() => { + new rds.DatabaseProxy(stack, 'Proxy', { + proxyTarget: rds.ProxyTarget.fromInstance(instance), + secrets: [instance.secret!], + vpc, + clientPasswordAuthType: rds.ClientPasswordAuthType.POSTGRES_MD5, + }); + }).toThrow(/POSTGRES_MD5 client password authentication type requires POSTGRESQL engineFamily, got MYSQL/); + }); + + test('SQL_SERVER_AUTHENTICATION clientPasswordAuthType requires SQLSERVER engine family', () => { + // GIVEN + const instance = new rds.DatabaseInstance(stack, 'Instance', { + engine: rds.DatabaseInstanceEngine.mysql({ + version: rds.MysqlEngineVersion.VER_5_7, + }), + vpc, + }); + + // WHEN + // THEN + expect(() => { + new rds.DatabaseProxy(stack, 'Proxy', { + proxyTarget: rds.ProxyTarget.fromInstance(instance), + secrets: [instance.secret!], + vpc, + clientPasswordAuthType: rds.ClientPasswordAuthType.SQL_SERVER_AUTHENTICATION, + }); + }).toThrow(/SQL_SERVER_AUTHENTICATION client password authentication type requires SQLSERVER engineFamily, got MYSQL/); + }); + }); }); describe('feature flag @aws-cdk/aws-rds:databaseProxyUniqueResourceName', () => {