Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete Operation Secret stabilization for Managed Passwords enabled Clusters #184

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion aws-redshift-cluster/.rpdk-config
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@
"codegen_template_path": "guided_aws",
"protocolVersion": "2.0.0"
},
"executableEntrypoint": "software.amazon.redshift.cluster.HandlerWrapperExecutable"
"logProcessorEnabled": "true",
"executableEntrypoint": "software.amazon.redshift.cluster.HandlerWrapperExecutable",
"contractSettings": {},
"canarySettings": {}
}
22 changes: 19 additions & 3 deletions aws-redshift-cluster/aws-redshift-cluster.json
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@
"permissions": [
"iam:PassRole",
"iam:CreateServiceLinkedRole",
"kms:*",
"redshift:DescribeClusters",
"redshift:CreateCluster",
"redshift:RestoreFromClusterSnapshot",
Expand Down Expand Up @@ -443,7 +444,11 @@
"ec2:DescribeAvailabilityZones",
"ec2:DescribeNetworkAcls",
"ec2:DescribeRouteTables",
"cloudwatch:PutMetricData"
"cloudwatch:PutMetricData",
"secretsmanager:CreateSecret",
"secretsmanager:TagResource",
"secretsmanager:RotateSecret",
"secretsmanager:DescribeSecret"
],
"timeoutInMinutes": 2160
},
Expand All @@ -460,6 +465,7 @@
"update": {
"permissions": [
"iam:PassRole",
"kms:*",
"redshift:DescribeClusters",
"redshift:ModifyCluster",
"redshift:ModifyClusterIamRoles",
Expand All @@ -485,15 +491,25 @@
"redshift:PutResourcePolicy",
"redshift:GetResourcePolicy",
"redshift:DeleteResourcePolicy",
"cloudwatch:PutMetricData"
"cloudwatch:PutMetricData",
"secretsmanager:CreateSecret",
"secretsmanager:TagResource",
"secretsmanager:RotateSecret",
"secretsmanager:DescribeSecret",
"secretsmanager:UpdateSecret",
"secretsmanager:DeleteSecret"
],
"timeoutInMinutes": 2160
},
"delete": {
"permissions": [
"redshift:DescribeTags",
"redshift:DescribeClusters",
"redshift:DeleteCluster"
"redshift:DeleteCluster",
"redshift:DeleteResourcePolicy",
"kms:RetireGrant",
"secretsmanager:DescribeSecret",
"secretsmanager:DeleteSecret"
],
"timeoutInMinutes": 2160
},
Expand Down
5 changes: 3 additions & 2 deletions aws-redshift-cluster/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,9 +447,9 @@ _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormati

#### SnapshotCopyRetentionPeriod

The number of days to retain automated snapshots in the destination region after they are copied from the source region.
The number of days to retain automated snapshots in the destination region after they are copied from the source region.

Default is 7.
Default is 7.

Constraints: Must be at least 1 and no more than 35.

Expand Down Expand Up @@ -701,3 +701,4 @@ The Amazon Resource Name (ARN) of the cluster namespace.
#### MasterPasswordSecretArn

The Amazon Resource Name (ARN) for the cluster's admin user credentials secret.

1 change: 1 addition & 0 deletions aws-redshift-cluster/docs/endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ To declare this entity in your AWS CloudFormation template, use the following sy
</pre>

## Properties

1 change: 1 addition & 0 deletions aws-redshift-cluster/docs/loggingproperties.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ _Required_: No
_Type_: String

_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)

1 change: 1 addition & 0 deletions aws-redshift-cluster/docs/tag.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ _Minimum Length_: <code>1</code>
_Maximum Length_: <code>255</code>

_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)

6 changes: 6 additions & 0 deletions aws-redshift-cluster/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
<artifactId>redshift</artifactId>
<version>2.21.44</version>
</dependency>
<!-- https://mvnrepository.com/artifact/software.amazon.awssdk/secretsmanager -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>secretsmanager</artifactId>
<version>2.22.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/software.amazon.cloudformation/aws-cloudformation-rpdk-java-plugin -->
<dependency>
<groupId>software.amazon.cloudformation</groupId>
Expand Down
8 changes: 8 additions & 0 deletions aws-redshift-cluster/resource-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Resources:
- "ec2:ModifyVpcEndpoint"
- "iam:CreateServiceLinkedRole"
- "iam:PassRole"
- "kms:*"
- "kms:RetireGrant"
- "redshift:CreateCluster"
- "redshift:CreateTags"
- "redshift:DeleteCluster"
Expand Down Expand Up @@ -81,6 +83,12 @@ Resources:
- "redshift:RestoreFromClusterSnapshot"
- "redshift:ResumeCluster"
- "redshift:RotateEncryptionKey"
- "secretsmanager:CreateSecret"
- "secretsmanager:DeleteSecret"
- "secretsmanager:DescribeSecret"
- "secretsmanager:RotateSecret"
- "secretsmanager:TagResource"
- "secretsmanager:UpdateSecret"
Resource: "*"
Outputs:
ExecutionRoleArn:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
import software.amazon.cloudformation.proxy.delay.Constant;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretRequest;
import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretResponse;
import com.amazonaws.util.StringUtils;

import java.time.Duration;
import java.util.HashSet;
Expand Down Expand Up @@ -73,6 +77,7 @@ public final ProgressEvent<ResourceModel, CallbackContext> handleRequest(
request,
callbackContext != null ? callbackContext : new CallbackContext(),
proxy.newProxy(ClientBuilder::getClient),
proxy.newProxy(ClientBuilder::secretsManagerClient),
logger
);
}
Expand All @@ -82,9 +87,48 @@ protected abstract ProgressEvent<ResourceModel, CallbackContext> handleRequest(
final ResourceHandlerRequest<ResourceModel> request,
final CallbackContext callbackContext,
final ProxyClient<RedshiftClient> proxyClient,
final ProxyClient<SecretsManagerClient> secretsManagerProxyClient,
final Logger logger);


protected boolean isClusterSecretDeleted (final ProxyClient<SecretsManagerClient> secretsManagerProxyClient, CallbackContext context) {
String clusterSecretArn = context.getMasterPasswordSecretArn();

// For namespaces that aren't opted in to Redshift Managed Passwords, AdminPasswordSecretArn is null
if (StringUtils.isNullOrEmpty(clusterSecretArn)) {
return true;
}

DescribeSecretRequest describeSecretRequest = DescribeSecretRequest.builder().secretId(clusterSecretArn).build();
try {
secretsManagerProxyClient.injectCredentialsAndInvokeV2(describeSecretRequest, secretsManagerProxyClient.client()::describeSecret);
} catch (final software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException e) {
return true;
}
return false;
}

protected String getClusterSecretArn(final ProxyClient<RedshiftClient> proxyClient, final String clusterIdentifier) {
String clusterSecretArn = null;
DescribeClustersResponse describeClustersResponse = null;

DescribeClustersRequest awsRequest = DescribeClustersRequest.builder().clusterIdentifier(clusterIdentifier).build();

try {
describeClustersResponse = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::describeClusters);
Cluster cluster = describeClustersResponse.clusters()
.stream()
.findAny()
.orElse(Cluster.builder().build());

clusterSecretArn = cluster.masterPasswordSecretArn();

} catch (final ClusterNotFoundException e) {
// do nothing here, we will handle this in .handleError part of the handler instead
}
return clusterSecretArn;
}

protected boolean isClusterActive (final ProxyClient<RedshiftClient> proxyClient, ResourceModel model, CallbackContext cxt) {
DescribeClustersRequest awsRequest =
DescribeClustersRequest.builder().clusterIdentifier(model.getClusterIdentifier()).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@lombok.EqualsAndHashCode(callSuper = true)
public class CallbackContext extends StdCallbackContext {
String namespaceArn = null;
String masterPasswordSecretArn = null;
LoggingProperties loggingProperties;
boolean callBackForReboot = false;
boolean callBackForDelete = false;
Expand All @@ -21,10 +22,19 @@ public class CallbackContext extends StdCallbackContext {
boolean callbackAfterClusterRestore = false;
boolean callbackAfterAfterClusterParameterGroupNameModify = false;


public void setNamespaceArn(String namespaceArn) {this.namespaceArn = namespaceArn; }

public String getNamespaceArn() { return namespaceArn; }

public String getMasterPasswordSecretArn() {
return masterPasswordSecretArn;
}

public void setMasterPasswordSecretArn(String masterPasswordSecretArn) {
this.masterPasswordSecretArn = masterPasswordSecretArn;
}

public void setLoggingProperties(LoggingProperties loggingProperties) {
this.loggingProperties = loggingProperties;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package software.amazon.redshift.cluster;

import software.amazon.awssdk.services.redshift.RedshiftClient;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.cloudformation.LambdaWrapper;

public class ClientBuilder {
Expand All @@ -9,4 +10,10 @@ static RedshiftClient getClient() {
.httpClient(LambdaWrapper.HTTP_CLIENT)
.build();
}

public static SecretsManagerClient secretsManagerClient() {
return SecretsManagerClient.builder()
.httpClient(LambdaWrapper.HTTP_CLIENT)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import software.amazon.awssdk.services.redshift.model.TagLimitExceededException;
import software.amazon.awssdk.services.redshift.model.UnauthorizedOperationException;
import software.amazon.awssdk.services.redshift.model.UnsupportedOperationException;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException;
import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
Expand Down Expand Up @@ -78,6 +79,7 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
final ResourceHandlerRequest<ResourceModel> request,
final CallbackContext callbackContext,
final ProxyClient<RedshiftClient> proxyClient,
final ProxyClient<SecretsManagerClient> secretsManagerProxyClient,
final Logger logger) {

this.logger = logger;
Expand Down Expand Up @@ -162,7 +164,7 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
}
return progress;
})
.then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger));
.then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, secretsManagerProxyClient, logger));
}

private RestoreFromClusterSnapshotResponse restoreFromClusterSnapshot(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import software.amazon.awssdk.services.redshift.model.InvalidClusterStateException;
import software.amazon.awssdk.services.redshift.model.InvalidRetentionPeriodException;
import software.amazon.awssdk.services.redshift.model.RedshiftException;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
import software.amazon.cloudformation.exceptions.CfnNotFoundException;
Expand All @@ -32,30 +33,40 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
final ResourceHandlerRequest<ResourceModel> request,
final CallbackContext callbackContext,
final ProxyClient<RedshiftClient> proxyClient,
final ProxyClient<SecretsManagerClient> secretsManagerProxyClient,
final Logger logger) {

this.logger = logger;

final ResourceModel model = request.getDesiredResourceState();

return ProgressEvent.progress(model, callbackContext)
.then(progress -> {
if (!callbackContext.getCallBackForDelete()) {
callbackContext.setCallBackForDelete(true);
logger.log ("In Delete, Initiate a CallBack Delay of "+CALLBACK_DELAY_SECONDS+" seconds");
progress = ProgressEvent.defaultInProgressHandler(callbackContext, CALLBACK_DELAY_SECONDS, model);
}
return progress;
})
.then(progress ->
proxy.initiate("AWS-Redshift-Cluster::Delete", proxyClient, model, callbackContext)
.translateToServiceRequest((_model) -> Translator.translateToDeleteRequest(_model, request.getSnapshotRequested()))
.makeServiceCall(this::deleteResource)
.stabilize((_request, _response, _client, _model, _context) -> isClusterActiveAfterDelete(_client, _model, _context))
.done((response) -> {
logger.log(String.format("%s %s deleted.",ResourceModel.TYPE_NAME, model.getClusterIdentifier()));
return ProgressEvent.defaultSuccessHandler(null);
}));
.then(progress -> {
if (!callbackContext.getCallBackForDelete()) {
callbackContext.setCallBackForDelete(true);
logger.log ("In Delete, Initiate a CallBack Delay of "+CALLBACK_DELAY_SECONDS+" seconds");
progress = ProgressEvent.defaultInProgressHandler(callbackContext, CALLBACK_DELAY_SECONDS, model);
}
return progress;
})
.then(progress -> {
// Set the secret ARN in the callback context. We will use this in the stabilize operation of this handler
if (callbackContext.getMasterPasswordSecretArn() == null) {
String masterPasswordSecretArn = getClusterSecretArn(proxyClient, model.getClusterIdentifier());
callbackContext.setMasterPasswordSecretArn(masterPasswordSecretArn);
}
progress = proxy.initiate("AWS-Redshift-Cluster::Delete", proxyClient, model, callbackContext)
.translateToServiceRequest((_model) -> Translator.translateToDeleteRequest(_model, request.getSnapshotRequested()))
.makeServiceCall(this::deleteResource)
.stabilize((_request, _response, _client, _model, _context) -> isClusterActiveAfterDelete(_client, _model, _context) &&
isClusterSecretDeleted(secretsManagerProxyClient, _context))
.done((response) -> {
logger.log(String.format("%s %s deleted.", ResourceModel.TYPE_NAME, model.getClusterIdentifier()));
return ProgressEvent.defaultSuccessHandler(null);
});

return progress;
});

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import software.amazon.awssdk.services.redshift.model.ResourceNotFoundException;
import software.amazon.awssdk.services.redshift.model.ResourcePolicy;
import software.amazon.awssdk.services.redshift.model.UnsupportedOperationException;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
import software.amazon.cloudformation.exceptions.CfnNotFoundException;
Expand All @@ -30,6 +31,7 @@
import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;


public class ReadHandler extends BaseHandlerStd {
private Logger logger;
private final String DESCRIBE_LOGGING_ERROR = "not authorized to perform: redshift:DescribeLoggingStatus";
Expand All @@ -45,6 +47,7 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
final ResourceHandlerRequest<ResourceModel> request,
final CallbackContext callbackContext,
final ProxyClient<RedshiftClient> proxyClient,
final ProxyClient<SecretsManagerClient> secretsManagerProxyClient,
final Logger logger) {

this.logger = logger;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
import software.amazon.awssdk.services.redshift.model.UnknownSnapshotCopyRegionException;
import software.amazon.awssdk.services.redshift.model.UnsupportedOperationException;
import software.amazon.awssdk.services.redshift.model.UnsupportedOptionException;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
import software.amazon.cloudformation.exceptions.CfnNotFoundException;
Expand Down Expand Up @@ -174,6 +175,7 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
final ResourceHandlerRequest<ResourceModel> request,
final CallbackContext callbackContext,
final ProxyClient<RedshiftClient> proxyClient,
final ProxyClient<SecretsManagerClient> secretsManagerProxyClient,
final Logger logger) {

this.logger = logger;
Expand Down Expand Up @@ -508,7 +510,7 @@ && isCrossRegionCopyEnabled(proxyClient, model)) {
}
return progress;
})
.then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger));
.then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, secretsManagerProxyClient, logger));
}

private DescribeClustersResponse describeCluster (
Expand Down
Loading