Skip to content

Commit

Permalink
feat(rds): ClientPasswordAuthType property on DatabaseProxy (#28540)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
lpizzinidev authored Jan 3, 2024
1 parent 508825b commit 669e6ff
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@
"Auth": [
{
"AuthScheme": "SECRETS",
"ClientPasswordAuthType": "POSTGRES_SCRAM_SHA_256",
"IAMAuth": "DISABLED",
"SecretArn": {
"Ref": "dbInstanceSecretAttachment88CFBDAE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
21 changes: 21 additions & 0 deletions packages/aws-cdk-lib/aws-rds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.DBAccounts.html> 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.
Expand Down
48 changes: 48 additions & 0 deletions packages/aws-cdk-lib/aws-rds/lib/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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}`);
}
}
}

/**
Expand Down
135 changes: 135 additions & 0 deletions packages/aws-cdk-lib/aws-rds/test/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down

0 comments on commit 669e6ff

Please sign in to comment.