Skip to content

Commit

Permalink
Cloudformation support for Secrets Manager Integration (#26)
Browse files Browse the repository at this point in the history
The changes provide Cloudformation support for Secrets Manager integration with Redshift Serverless. With this new feature, customers can opt in to store their namespace's admin credentials in a service linked secret in Secrets Manager. It allows us to create/modify Redshift Serverless namespaces with Secrets Manager support using the Cloudformation template. The changes in this request allow us to use create-namespace and update-namespace APIs for Redshift Serverless namespaces when opting in to this feature.

We are adding a new boolean parameter "ManageAdminPassword" to allow customers to opt in to this feature and another parameter "AdminPasswordSecretKmsKeyId" allows customers to specify the key ID of the KMS key in the customer account which will be used to encrypt the namespace's secret. These parameters can be used while setting CreateNamespaceRequest and UpdateNamespaceRequest. The response of these requests will return the "AdminPasswordSecretArn" when the namespace is opted in to this feature.
  • Loading branch information
nidhimanthale authored Dec 21, 2023
1 parent ef6358f commit 0dc06cd
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@
},
"CreationDate": {
"type": "string"
},
"AdminPasswordSecretArn": {
"type": "string"
},
"AdminPasswordSecretKmsKeyId": {
"type": "string"
}
},
"additionalProperties": false
Expand Down Expand Up @@ -96,8 +102,12 @@
}
},
"properties": {
"AdminPasswordSecretKmsKeyId": {
"description": "The ID of the AWS Key Management Service (KMS) key used to encrypt and store the namespace's admin credentials secret. You can only use this parameter if manageAdminPassword is true.",
"type": "string"
},
"AdminUserPassword": {
"description": "The password associated with the admin user for the namespace that is being created. Password must be at least 8 characters in length, should be any printable ASCII character. Must contain at least one lowercase letter, one uppercase letter and one decimal digit.",
"description": "The password associated with the admin user for the namespace that is being created. Password must be at least 8 characters in length, should be any printable ASCII character. Must contain at least one lowercase letter, one uppercase letter and one decimal digit. You can't use adminUserPassword if manageAdminPassword is true.",
"type": "string",
"maxLength": 64,
"minLength": 8,
Expand Down Expand Up @@ -142,6 +152,10 @@
"maxItems": 16,
"minItems": 0
},
"ManageAdminPassword": {
"description": "If true, Amazon Redshift uses AWS Secrets Manager to manage the namespace's admin credentials. You can't use adminUserPassword if manageAdminPassword is true. If manageAdminPassword is false or not set, Amazon Redshift uses adminUserPassword for the admin user account's password.",
"type": "boolean"
},
"Namespace": {
"$ref": "#/definitions/Namespace"
},
Expand Down Expand Up @@ -195,15 +209,18 @@
"/properties/Namespace/IamRoles",
"/properties/Namespace/LogExports",
"/properties/Namespace/Status",
"/properties/Namespace/CreationDate"
"/properties/Namespace/CreationDate",
"/properties/Namespace/AdminPasswordSecretArn",
"/properties/Namespace/AdminPasswordSecretKmsKeyId"
],
"writeOnlyProperties": [
"/properties/AdminUserPassword",
"/properties/FinalSnapshotName",
"/properties/FinalSnapshotRetentionPeriod",
"/properties/Tags",
"/properties/Tags/*/Key",
"/properties/Tags/*/Value"
"/properties/Tags/*/Value",
"/properties/ManageAdminPassword"
],
"createOnlyProperties": [
"/properties/NamespaceName",
Expand Down
34 changes: 33 additions & 1 deletion aws-redshiftserverless-namespace/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ To declare this entity in your AWS CloudFormation template, use the following sy
{
"Type" : "AWS::RedshiftServerless::Namespace",
"Properties" : {
"<a href="#adminpasswordsecretkmskeyid" title="AdminPasswordSecretKmsKeyId">AdminPasswordSecretKmsKeyId</a>" : <i>String</i>,
"<a href="#adminuserpassword" title="AdminUserPassword">AdminUserPassword</a>" : <i>String</i>,
"<a href="#adminusername" title="AdminUsername">AdminUsername</a>" : <i>String</i>,
"<a href="#dbname" title="DbName">DbName</a>" : <i>String</i>,
"<a href="#defaultiamrolearn" title="DefaultIamRoleArn">DefaultIamRoleArn</a>" : <i>String</i>,
"<a href="#iamroles" title="IamRoles">IamRoles</a>" : <i>[ String, ... ]</i>,
"<a href="#kmskeyid" title="KmsKeyId">KmsKeyId</a>" : <i>String</i>,
"<a href="#logexports" title="LogExports">LogExports</a>" : <i>[ String, ... ]</i>,
"<a href="#manageadminpassword" title="ManageAdminPassword">ManageAdminPassword</a>" : <i>Boolean</i>,
"<a href="#namespacename" title="NamespaceName">NamespaceName</a>" : <i>String</i>,
"<a href="#tags" title="Tags">Tags</a>" : <i>[ <a href="tag.md">Tag</a>, ... ]</i>,
"<a href="#finalsnapshotname" title="FinalSnapshotName">FinalSnapshotName</a>" : <i>String</i>,
Expand All @@ -33,6 +35,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy
<pre>
Type: AWS::RedshiftServerless::Namespace
Properties:
<a href="#adminpasswordsecretkmskeyid" title="AdminPasswordSecretKmsKeyId">AdminPasswordSecretKmsKeyId</a>: <i>String</i>
<a href="#adminuserpassword" title="AdminUserPassword">AdminUserPassword</a>: <i>String</i>
<a href="#adminusername" title="AdminUsername">AdminUsername</a>: <i>String</i>
<a href="#dbname" title="DbName">DbName</a>: <i>String</i>
Expand All @@ -42,6 +45,7 @@ Properties:
<a href="#kmskeyid" title="KmsKeyId">KmsKeyId</a>: <i>String</i>
<a href="#logexports" title="LogExports">LogExports</a>: <i>
- String</i>
<a href="#manageadminpassword" title="ManageAdminPassword">ManageAdminPassword</a>: <i>Boolean</i>
<a href="#namespacename" title="NamespaceName">NamespaceName</a>: <i>String</i>
<a href="#tags" title="Tags">Tags</a>: <i>
- <a href="tag.md">Tag</a></i>
Expand All @@ -52,9 +56,19 @@ Properties:

## Properties

#### AdminPasswordSecretKmsKeyId

The ID of the AWS Key Management Service (KMS) key used to encrypt and store the namespace's admin credentials secret. You can only use this parameter if manageAdminPassword is true.

_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)

#### AdminUserPassword

The password associated with the admin user for the namespace that is being created. Password must be at least 8 characters in length, should be any printable ASCII character. Must contain at least one lowercase letter, one uppercase letter and one decimal digit.
The password associated with the admin user for the namespace that is being created. Password must be at least 8 characters in length, should be any printable ASCII character. Must contain at least one lowercase letter, one uppercase letter and one decimal digit. You can't use adminUserPassword if manageAdminPassword is true.

_Required_: No

Expand Down Expand Up @@ -134,6 +148,16 @@ _Type_: List of String

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

#### ManageAdminPassword

If true, Amazon Redshift uses AWS Secrets Manager to manage the namespace's admin credentials. You can't use adminUserPassword if manageAdminPassword is true. If manageAdminPassword is false or not set, Amazon Redshift uses adminUserPassword for the admin user account's password.

_Required_: No

_Type_: Boolean

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

#### NamespaceName

A unique identifier for the namespace. You use this identifier to refer to the namespace for any subsequent namespace operations such as deleting or modifying. All alphabetical characters must be lower case. Namespace name should be unique for all namespaces within an AWS account.
Expand Down Expand Up @@ -253,3 +277,11 @@ Returns the <code>Status</code> value.
#### CreationDate

Returns the <code>CreationDate</code> value.

#### AdminPasswordSecretArn

Returns the <code>AdminPasswordSecretArn</code> value.

#### AdminPasswordSecretKmsKeyId

Returns the <code>AdminPasswordSecretKmsKeyId</code> value.
2 changes: 1 addition & 1 deletion aws-redshiftserverless-namespace/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>redshiftserverless</artifactId>
<version>2.18.31</version>
<version>2.21.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/software.amazon.awssdk/redshift -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package software.amazon.redshiftserverless.namespace;

import com.amazonaws.util.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.awssdk.awscore.AwsRequest;
import software.amazon.awssdk.services.redshift.model.DeleteResourcePolicyRequest;
import software.amazon.awssdk.services.redshift.model.GetResourcePolicyRequest;
import software.amazon.awssdk.services.redshift.model.PutResourcePolicyRequest;
import software.amazon.awssdk.services.redshift.model.GetResourcePolicyResponse;
import software.amazon.awssdk.services.redshiftserverless.model.CreateNamespaceRequest;
import software.amazon.awssdk.services.redshiftserverless.model.DeleteNamespaceRequest;
import software.amazon.awssdk.services.redshiftserverless.model.GetNamespaceRequest;
Expand Down Expand Up @@ -49,6 +49,8 @@ static CreateNamespaceRequest translateToCreateRequest(final ResourceModel model
.iamRoles(model.getIamRoles())
.logExportsWithStrings(model.getLogExports())
.tags(translateTagsToSdk(model.getTags()))
.manageAdminPassword(model.getManageAdminPassword())
.adminPasswordSecretKmsKeyId(model.getAdminPasswordSecretKmsKeyId())
.build();
}

Expand Down Expand Up @@ -88,6 +90,8 @@ static ResourceModel translateFromReadResponse(final GetNamespaceResponse awsRes
.logExports(awsResponse.namespace().logExportsAsStrings())
.namespaceName(awsResponse.namespace().namespaceName())
.namespace(translateToModelNamespace(awsResponse.namespace()))
.manageAdminPassword(StringUtils.isNullOrEmpty(awsResponse.namespace().adminPasswordSecretArn()) ? null : true)
.adminPasswordSecretKmsKeyId(awsResponse.namespace().adminPasswordSecretKmsKeyId())
.build();
}

Expand Down Expand Up @@ -121,6 +125,8 @@ static UpdateNamespaceRequest translateToUpdateRequest(final ResourceModel model
//TODO: we only support updating db-name after GA
// .dbName(model.getDbName())
.defaultIamRoleArn(model.getDefaultIamRoleArn())
.manageAdminPassword(model.getManageAdminPassword())
.adminPasswordSecretKmsKeyId(model.getAdminPasswordSecretKmsKeyId())
.build();
}

Expand Down Expand Up @@ -194,6 +200,8 @@ private static Namespace translateToModelNamespace(
.logExports(namespace.logExportsAsStrings())
.status(namespace.statusAsString())
.creationDate(namespace.creationDate() == null ? null : namespace.creationDate().toString())
.adminPasswordSecretArn(namespace.adminPasswordSecretArn())
.adminPasswordSecretKmsKeyId(namespace.adminPasswordSecretKmsKeyId())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package software.amazon.redshiftserverless.namespace;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.services.cloudwatch.model.InvalidParameterValueException;
import software.amazon.awssdk.services.redshift.RedshiftClient;
import software.amazon.awssdk.services.redshift.model.*;
import software.amazon.awssdk.services.redshift.model.PutResourcePolicyRequest;
import software.amazon.awssdk.services.redshift.model.PutResourcePolicyResponse;
import software.amazon.awssdk.services.redshift.model.UnsupportedOperationException;
import software.amazon.awssdk.services.redshift.model.DeleteResourcePolicyRequest;
import software.amazon.awssdk.services.redshift.model.DeleteResourcePolicyResponse;
import software.amazon.awssdk.services.redshiftserverless.RedshiftServerlessClient;
import software.amazon.awssdk.services.redshiftserverless.model.GetNamespaceRequest;
import software.amazon.awssdk.services.redshiftserverless.model.GetNamespaceResponse;
Expand Down Expand Up @@ -54,6 +51,8 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
.defaultIamRoleArn(StringUtils.equals(prevModel.getDefaultIamRoleArn(), currentModel.getDefaultIamRoleArn()) ? null : currentModel.getDefaultIamRoleArn())
.iamRoles(compareListParamsEqualOrNot(prevModel.getIamRoles(), currentModel.getIamRoles()) ? null : currentModel.getIamRoles())
.logExports(compareListParamsEqualOrNot(prevModel.getLogExports(), currentModel.getLogExports()) ? null : currentModel.getLogExports())
.manageAdminPassword((prevModel.getManageAdminPassword() == currentModel.getManageAdminPassword()) ? null : currentModel.getManageAdminPassword())
.adminPasswordSecretKmsKeyId(StringUtils.equals(prevModel.getAdminPasswordSecretKmsKeyId(), currentModel.getAdminPasswordSecretKmsKeyId()) ? null : currentModel.getAdminPasswordSecretKmsKeyId())
.build();

// To update the adminUserPassword or adminUserName, we need to specify both username and password in update request
Expand All @@ -72,6 +71,14 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
.build();
}

// To update the adminPasswordSecretKmsKeyId, we need to specify both manageAdminPassword and adminPasswordSecretKmsKeyId in update request
if (!StringUtils.equals(prevModel.getAdminPasswordSecretKmsKeyId(), currentModel.getAdminPasswordSecretKmsKeyId())) {
tempUpdateRequestModel = tempUpdateRequestModel.toBuilder()
.manageAdminPassword(currentModel.getManageAdminPassword())
.adminPasswordSecretKmsKeyId(currentModel.getAdminPasswordSecretKmsKeyId())
.build();
}

final ResourceModel updateRequestModel = tempUpdateRequestModel;
return ProgressEvent.progress(currentModel, callbackContext)
.then(progress ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@ public class AbstractTestBase {
private static final String STATUS;
private static final Instant CREATION_DATE;
private static final software.amazon.awssdk.services.redshiftserverless.model.Namespace NAMESPACE;
private static final software.amazon.awssdk.services.redshiftserverless.model.Namespace NAMESPACE_WITH_MANAGED_ADMIN_PASSWORD;
private static final String FINAL_SNAPSHOT_NAME;
private static final int FINAL_SNAPSHOT_RETENTION_PERIOD;
protected static final String AWS_REGION = "us-east-1";
protected static final String NAMESPACE_RESOURCE_POLICY_DOCUMENT;
protected static final String NAMESPACE_RESOURCE_POLICY_DOCUMENT_EMPTY;
protected static final ResourcePolicy NAMESPACE_RESOURCE_POLICY;
protected static final ResourcePolicy NAMESPACE_RESOURCE_POLICY_EMPTY;
protected static final String SECRET_ARN;
protected static final String SECRET_KMS_KEY_ID;

static {
MOCK_CREDENTIALS = new Credentials("accessKey", "secretKey", "token");
Expand All @@ -71,6 +74,8 @@ public class AbstractTestBase {
FINAL_SNAPSHOT_RETENTION_PERIOD = 1;
NAMESPACE_RESOURCE_POLICY_DOCUMENT = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Resource\": \"*\",\"Action\":\"test:test\"}]}";
NAMESPACE_RESOURCE_POLICY_DOCUMENT_EMPTY = "{}";
SECRET_ARN = "DummyClusterSecretArn";
SECRET_KMS_KEY_ID = "DummySecretKmsKeyId";

NAMESPACE = software.amazon.awssdk.services.redshiftserverless.model.Namespace.builder()
.namespaceName(NAMESPACE_NAME)
Expand All @@ -86,6 +91,11 @@ public class AbstractTestBase {
.creationDate(CREATION_DATE)
.build();

NAMESPACE_WITH_MANAGED_ADMIN_PASSWORD = NAMESPACE.toBuilder()
.adminPasswordSecretArn(SECRET_ARN)
.adminPasswordSecretKmsKeyId(SECRET_KMS_KEY_ID)
.build();

NAMESPACE_RESOURCE_POLICY = ResourcePolicy.builder()
.resourceArn(NAMESPACE.namespaceArn())
.policy(NAMESPACE_RESOURCE_POLICY_DOCUMENT)
Expand Down Expand Up @@ -153,7 +163,15 @@ public static ResourceModel getCreateRequestResourceModel() {
.build();
}

public static ResourceModel getCreateResponseResourceMode() {
public static ResourceModel getCreateRequestResourceModelWithManagedAdminPassword() {
return getCreateRequestResourceModel().toBuilder()
.adminUserPassword(null)
.manageAdminPassword(true)
.adminPasswordSecretKmsKeyId(SECRET_KMS_KEY_ID)
.build();
}

public static ResourceModel getCreateResponseResourceModel() {
return ResourceModel.builder()
.adminUsername(ADMIN_USERNAME)
.dbName(DB_NAME)
Expand All @@ -166,6 +184,15 @@ public static ResourceModel getCreateResponseResourceMode() {
.build();
}

public static ResourceModel getCreateResponseResourceModelWithManagedAdminPassword() {
return getCreateResponseResourceModel().toBuilder()
.adminPasswordSecretKmsKeyId(SECRET_KMS_KEY_ID)
.manageAdminPassword(true)
.namespace(translateToModelNamespace(NAMESPACE_WITH_MANAGED_ADMIN_PASSWORD))
.build();
}


private static Namespace translateToModelNamespace(
software.amazon.awssdk.services.redshiftserverless.model.Namespace namespace) {
return Namespace.builder()
Expand All @@ -180,6 +207,8 @@ private static Namespace translateToModelNamespace(
.logExports(namespace.logExportsAsStrings())
.status(namespace.statusAsString())
.creationDate(namespace.creationDate().toString())
.adminPasswordSecretArn(namespace.adminPasswordSecretArn())
.adminPasswordSecretKmsKeyId(namespace.adminPasswordSecretKmsKeyId())
.build();
}

Expand All @@ -189,12 +218,24 @@ public static CreateNamespaceResponse getCreateResponseSdk() {
.build();
}

public static CreateNamespaceResponse getCreateResponseSdkForManagedAdminPasswords() {
return CreateNamespaceResponse.builder()
.namespace(NAMESPACE_WITH_MANAGED_ADMIN_PASSWORD)
.build();
}

public static GetNamespaceResponse getNamespaceResponseSdk() {
return GetNamespaceResponse.builder()
.namespace(NAMESPACE)
.build();
}

public static GetNamespaceResponse getNamespaceResponseSdkForManagedAdminPasswords() {
return GetNamespaceResponse.builder()
.namespace(NAMESPACE_WITH_MANAGED_ADMIN_PASSWORD)
.build();
}

public static ResourceModel getDeleteRequestResourceModel() {
return ResourceModel.builder()
.namespaceName(NAMESPACE_NAME)
Expand Down Expand Up @@ -255,8 +296,20 @@ public static ResourceModel getUpdateRequestResourceModel() {
.build();
}

public static ResourceModel getUpdateRequestResourceModelWithManagedAdminPassword() {
return getUpdateRequestResourceModel().toBuilder()
.adminUserPassword(null)
.manageAdminPassword(true)
.adminPasswordSecretKmsKeyId(SECRET_KMS_KEY_ID)
.build();
}

public static ResourceModel getUpdateResponseResourceModel() {
return getCreateResponseResourceMode();
return getCreateResponseResourceModel();
}

public static ResourceModel getUpdateResponseResourceModelWithManagedAdminPassword() {
return getCreateResponseResourceModelWithManagedAdminPassword();
}

public static UpdateNamespaceResponse getUpdateResponseSdk() {
Expand All @@ -265,6 +318,12 @@ public static UpdateNamespaceResponse getUpdateResponseSdk() {
.build();
}

public static UpdateNamespaceResponse getUpdateResponseSdkForManagedAdminPasswords() {
return UpdateNamespaceResponse.builder()
.namespace(NAMESPACE_WITH_MANAGED_ADMIN_PASSWORD)
.build();
}

public static PutResourcePolicyResponse putResourcePolicyResponseSdk() {
return PutResourcePolicyResponse.builder()
.resourcePolicy(NAMESPACE_RESOURCE_POLICY)
Expand Down
Loading

0 comments on commit 0dc06cd

Please sign in to comment.