Skip to content

Commit

Permalink
fix(rds): clusters created from snapshots generate incorrect passwords (
Browse files Browse the repository at this point in the history
aws#20504)

Deprecate `credentials` and explain how it is broken.

Replace it with `snapshotCredentials` that offer the expected behavior.

Fixes aws#20434
Closes aws#20473 


----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)?
	* [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
jogold authored Jun 2, 2022
1 parent ade7af5 commit 4a87d39
Show file tree
Hide file tree
Showing 18 changed files with 6,075 additions and 5 deletions.
54 changes: 51 additions & 3 deletions packages/@aws-cdk/aws-rds/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import * as cxapi from '@aws-cdk/cx-api';
import { Construct } from 'constructs';
import { IClusterEngine } from './cluster-engine';
import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref';
import { DatabaseSecret } from './database-secret';
import { Endpoint } from './endpoint';
import { IParameterGroup, ParameterGroup } from './parameter-group';
import { applyDefaultRotationOptions, defaultDeletionProtection, renderCredentials, setupS3ImportExport, helperRemovalPolicy, renderUnless } from './private/util';
import { BackupProps, Credentials, InstanceProps, PerformanceInsightRetention, RotationSingleUserOptions, RotationMultiUserOptions } from './props';
import { BackupProps, Credentials, InstanceProps, PerformanceInsightRetention, RotationSingleUserOptions, RotationMultiUserOptions, SnapshotCredentials } from './props';
import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy';
import { CfnDBCluster, CfnDBClusterProps, CfnDBInstance } from './rds.generated';
import { ISubnetGroup, SubnetGroup } from './subnet-group';
Expand Down Expand Up @@ -661,9 +662,27 @@ export interface DatabaseClusterFromSnapshotProps extends DatabaseClusterBasePro
/**
* Credentials for the administrative user
*
* Note - using this prop only works with `Credentials.fromPassword()` with the
* username of the snapshot, `Credentials.fromUsername()` with the username and
* password of the snapshot or `Credentials.fromSecret()` with a secret containing
* the username and password of the snapshot.
*
* @default - A username of 'admin' (or 'postgres' for PostgreSQL) and SecretsManager-generated password
* that **will not be applied** to the cluster, use `snapshotCredentials` for the correct behavior.
*
* @deprecated use `snapshotCredentials` which allows to generate a new password
*/
readonly credentials?: Credentials;

/**
* Master user credentials.
*
* Note - It is not possible to change the master username for a snapshot;
* however, it is possible to provide (or generate) a new password.
*
* @default - The existing username and password from the snapshot will be used.
*/
readonly snapshotCredentials?: SnapshotCredentials;
}

/**
Expand All @@ -687,12 +706,34 @@ export class DatabaseClusterFromSnapshot extends DatabaseClusterNew {
constructor(scope: Construct, id: string, props: DatabaseClusterFromSnapshotProps) {
super(scope, id, props);

const credentials = renderCredentials(this, props.engine, props.credentials);
const secret = credentials.secret;
if (props.credentials && !props.credentials.password && !props.credentials.secret) {
Annotations.of(this).addWarning('Use `snapshotCredentials` to modify password of a cluster created from a snapshot.');
}
if (!props.credentials && !props.snapshotCredentials) {
Annotations.of(this).addWarning('Generated credentials will not be applied to cluster. Use `snapshotCredentials` instead. `addRotationSingleUser()` and `addRotationMultiUser()` cannot be used on tbis cluster.');
}
const deprecatedCredentials = renderCredentials(this, props.engine, props.credentials);

let credentials = props.snapshotCredentials;
let secret = credentials?.secret;
if (!secret && credentials?.generatePassword) {
if (!credentials.username) {
throw new Error('`snapshotCredentials` `username` must be specified when `generatePassword` is set to true');
}

secret = new DatabaseSecret(this, 'SnapshotSecret', {
username: credentials.username,
encryptionKey: credentials.encryptionKey,
excludeCharacters: credentials.excludeCharacters,
replaceOnPasswordCriteriaChanges: credentials.replaceOnPasswordCriteriaChanges,
replicaRegions: credentials.replicaRegions,
});
}

const cluster = new CfnDBCluster(this, 'Resource', {
...this.newCfnProps,
snapshotIdentifier: props.snapshotIdentifier,
masterUserPassword: secret?.secretValueFromJson('password')?.unsafeUnwrap() ?? credentials?.password?.unsafeUnwrap(), // Safe usage
});

this.clusterIdentifier = cluster.ref;
Expand All @@ -701,6 +742,13 @@ export class DatabaseClusterFromSnapshot extends DatabaseClusterNew {
this.secret = secret.attach(this);
}

if (deprecatedCredentials.secret) {
const deprecatedSecret = deprecatedCredentials.secret.attach(this);
if (!this.secret) {
this.secret = deprecatedSecret;
}
}

// create a number token that represents the port of the cluster
const portAttribute = Token.asNumber(cluster.attrEndpointPort);
this.clusterEndpoint = new Endpoint(cluster.attrEndpointAddress, portAttribute);
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-rds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@aws-cdk/aws-events-targets": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
"@aws-cdk/cdk-build-tools": "0.0.0",
"@aws-cdk/custom-resources": "0.0.0",
"@aws-cdk/integ-runner": "0.0.0",
"@aws-cdk/cfn2ts": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { IsCompleteRequest, IsCompleteResponse, OnEventRequest, OnEventResponse } from '@aws-cdk/custom-resources/lib/provider-framework/types';
export declare function onEventHandler(event: OnEventRequest): Promise<OnEventResponse>;
export declare function isCompleteHandler(event: IsCompleteRequest): Promise<IsCompleteResponse>;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* eslint-disable no-console */
import type { IsCompleteRequest, IsCompleteResponse, OnEventRequest, OnEventResponse } from '@aws-cdk/custom-resources/lib/provider-framework/types';
import { RDS } from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies

export async function onEventHandler(event: OnEventRequest): Promise<OnEventResponse> {
console.log('Event: %j', event);

const rds = new RDS();

const physicalResourceId = `${event.ResourceProperties.DBClusterIdentifier}-${event.ResourceProperties.DBClusterIdentifier}`;

if (event.RequestType === 'Create' || event.RequestType === 'Update') {
const data = await rds.createDBClusterSnapshot({
DBClusterIdentifier: event.ResourceProperties.DBClusterIdentifier,
DBClusterSnapshotIdentifier: event.ResourceProperties.DBClusterSnapshotIdentifier,
}).promise();
return {
PhysicalResourceId: physicalResourceId,
Data: {
DBClusterSnapshotArn: data.DBClusterSnapshot?.DBClusterSnapshotArn,
},
};
}

if (event.RequestType === 'Delete') {
await rds.deleteDBClusterSnapshot({
DBClusterSnapshotIdentifier: event.ResourceProperties.DBClusterSnapshotIdentifier,
}).promise();
}

return {
PhysicalResourceId: `${event.ResourceProperties.DBClusterIdentifier}-${event.ResourceProperties.DBClusterIdentifier}`,
};
}

export async function isCompleteHandler(event: IsCompleteRequest): Promise<IsCompleteResponse> {
console.log('Event: %j', event);

const snapshotStatus = await tryGetClusterSnapshotStatus(event.ResourceProperties.DBClusterSnapshotIdentifier);

switch (event.RequestType) {
case 'Create':
case 'Update':
return { IsComplete: snapshotStatus === 'available' };
case 'Delete':
return { IsComplete: snapshotStatus === undefined };
}
}

async function tryGetClusterSnapshotStatus(identifier: string): Promise<string | undefined> {
try {
const rds = new RDS();
const data = await rds.describeDBClusterSnapshots({
DBClusterSnapshotIdentifier: identifier,
}).promise();
return data.DBClusterSnapshots?.[0].Status;
} catch (err) {
if (err.code === 'DBClusterSnapshotNotFoundFault') {
return undefined;
}
throw err;
}
}
Loading

0 comments on commit 4a87d39

Please sign in to comment.