From 226745ccd8df3834fb6951d79e92e23ab87e876f Mon Sep 17 00:00:00 2001
From: ricardorey10 <29745418+ricardorey10@users.noreply.github.com>
Date: Sat, 10 Aug 2024 22:49:27 -0600
Subject: [PATCH] ENG-14270: redshift iam (#559)
* ENG-14270: add redshiftSettings and new AWS IAM authN flag
* Add documentation
* Rename authenticate_as_iam_user to authenticate_as_iam_role
* Improves description
* Improves description
---------
Co-authored-by: Wilson de Carvalho <796900+wcmjunior@users.noreply.github.com>
---
cyral/internal/repository/constants.go | 5 ++
cyral/internal/repository/model.go | 65 +++++++++++++++---
cyral/internal/repository/resource.go | 25 +++++++
cyral/internal/repository/resource_test.go | 66 ++++++++++++++++++-
.../internal/repository/useraccount/model.go | 9 ++-
.../repository/useraccount/resource.go | 6 ++
.../repository/useraccount/resource_test.go | 14 +++-
docs/resources/repository.md | 11 ++++
docs/resources/repository_user_account.md | 4 ++
9 files changed, 189 insertions(+), 16 deletions(-)
diff --git a/cyral/internal/repository/constants.go b/cyral/internal/repository/constants.go
index 851bd085..9a863592 100644
--- a/cyral/internal/repository/constants.go
+++ b/cyral/internal/repository/constants.go
@@ -26,6 +26,11 @@ const (
RepoMongoDBServerTypeKey = "server_type"
RepoMongoDBSRVRecordName = "srv_record_name"
RepoMongoDBFlavorKey = "flavor"
+ // Redshift settings keys
+ RepoRedshiftSettingsKey = "redshift_settings"
+ RepoRedshiftClusterIdentifier = "cluster_identifier"
+ RepoRedshiftWorkgroupName = "workgroup_name"
+ RepoRedshiftAWSRegion = "aws_region"
)
const (
diff --git a/cyral/internal/repository/model.go b/cyral/internal/repository/model.go
index 494bec6b..01fdf2f7 100644
--- a/cyral/internal/repository/model.go
+++ b/cyral/internal/repository/model.go
@@ -11,15 +11,16 @@ type Labels []string
type RepoNodes []*RepoNode
type RepoInfo struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Type string `json:"type"`
- Host string `json:"repoHost"`
- Port uint32 `json:"repoPort"`
- ConnParams *ConnParams `json:"connParams"`
- Labels Labels `json:"labels"`
- RepoNodes RepoNodes `json:"repoNodes,omitempty"`
- MongoDBSettings *MongoDBSettings `json:"mongoDbSettings,omitempty"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Host string `json:"repoHost"`
+ Port uint32 `json:"repoPort"`
+ ConnParams *ConnParams `json:"connParams"`
+ Labels Labels `json:"labels"`
+ RepoNodes RepoNodes `json:"repoNodes,omitempty"`
+ MongoDBSettings *MongoDBSettings `json:"mongoDbSettings,omitempty"`
+ RedshiftSettings *RedshiftSettings `json:"redshiftSettings,omitempty"`
}
type ConnParams struct {
@@ -38,6 +39,12 @@ type MongoDBSettings struct {
Flavor string `json:"flavor,omitempty"`
}
+type RedshiftSettings struct {
+ ClusterIdentifier string `json:"clusterIdentifier,omitempty"`
+ WorkgroupName string `json:"workgroupName,omitempty"`
+ AWSRegion string `json:"awsRegion,omitempty"`
+}
+
type RepoNode struct {
Name string `json:"name"`
Host string `json:"host"`
@@ -60,6 +67,7 @@ func (res *RepoInfo) WriteToSchema(d *schema.ResourceData) error {
d.Set(RepoConnDrainingKey, res.ConnParams.AsInterface())
d.Set(RepoNodesKey, res.RepoNodes.AsInterface())
d.Set(RepoMongoDBSettingsKey, res.MongoDBSettings.AsInterface())
+ d.Set(RepoRedshiftSettingsKey, res.RedshiftSettings.AsInterface())
return nil
}
@@ -77,7 +85,18 @@ func (r *RepoInfo) ReadFromSchema(d *schema.ResourceData) error {
return fmt.Errorf("'%s' block is only allowed when '%s=%s'", RepoMongoDBSettingsKey, utils.TypeKey, MongoDB)
}
m, err := mongoDBSettingsFromInterface(mongoDBSettings)
+ if err != nil {
+ return err
+ }
r.MongoDBSettings = m
+
+ var redshiftSettings = d.Get(RepoRedshiftSettingsKey).(*schema.Set).List()
+ if r.Type != Redshift && len(redshiftSettings) > 0 {
+ return fmt.Errorf("'%s' block is only allowed when '%s=%s'", RepoRedshiftSettingsKey, utils.TypeKey, Redshift)
+ }
+ redshift, err := redshiftSettingsFromInterface(redshiftSettings)
+ r.RedshiftSettings = redshift
+
return err
}
@@ -156,6 +175,18 @@ func repoNodesFromInterface(i []interface{}) RepoNodes {
return repoNodes
}
+func (r *RedshiftSettings) AsInterface() []interface{} {
+ if r == nil {
+ return nil
+ }
+
+ return []interface{}{map[string]interface{}{
+ RepoRedshiftClusterIdentifier: r.ClusterIdentifier,
+ RepoRedshiftWorkgroupName: r.WorkgroupName,
+ RepoRedshiftAWSRegion: r.AWSRegion,
+ }}
+}
+
func (m *MongoDBSettings) AsInterface() []interface{} {
if m == nil {
return nil
@@ -169,6 +200,22 @@ func (m *MongoDBSettings) AsInterface() []interface{} {
}}
}
+func redshiftSettingsFromInterface(i []interface{}) (*RedshiftSettings, error) {
+ if len(i) == 0 {
+ return nil, nil
+ }
+
+ var clusterIdentifier = i[0].(map[string]interface{})[RepoRedshiftClusterIdentifier].(string)
+ var workgroupName = i[0].(map[string]interface{})[RepoRedshiftWorkgroupName].(string)
+ var awsRegion = i[0].(map[string]interface{})[RepoRedshiftAWSRegion].(string)
+
+ return &RedshiftSettings{
+ ClusterIdentifier: clusterIdentifier,
+ WorkgroupName: workgroupName,
+ AWSRegion: awsRegion,
+ }, nil
+}
+
func mongoDBSettingsFromInterface(i []interface{}) (*MongoDBSettings, error) {
if len(i) == 0 {
return nil, nil
diff --git a/cyral/internal/repository/resource.go b/cyral/internal/repository/resource.go
index 37a3ac4c..c7cba459 100644
--- a/cyral/internal/repository/resource.go
+++ b/cyral/internal/repository/resource.go
@@ -171,6 +171,31 @@ func resourceSchema() *schema.Resource {
},
},
},
+ RepoRedshiftSettingsKey: {
+ Description: "Parameters related to Redshift repositories.",
+ Type: schema.TypeSet,
+ Optional: true,
+ MaxItems: 1,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ RepoRedshiftClusterIdentifier: {
+ Description: "Name of the provisioned cluster.",
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ RepoRedshiftWorkgroupName: {
+ Description: "Workgroup name for serverless cluster.",
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ RepoRedshiftAWSRegion: {
+ Description: "Code of the AWS region where the Redshift instance is deployed.",
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ },
+ },
+ },
},
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
diff --git a/cyral/internal/repository/resource_test.go b/cyral/internal/repository/resource_test.go
index db71fb0a..c046cadc 100644
--- a/cyral/internal/repository/resource_test.go
+++ b/cyral/internal/repository/resource_test.go
@@ -140,6 +140,21 @@ var (
Flavor: "documentdb",
},
}
+
+ withRedshiftSettings = repository.RepoInfo{
+ Name: utils.AccTestName(utils.RepositoryResourceName, "repo-with-redshift-settings"),
+ Type: "redshift",
+ RepoNodes: repository.RepoNodes{
+ {
+ Host: "redshift.local",
+ Port: 3333,
+ },
+ },
+ RedshiftSettings: &repository.RedshiftSettings{
+ ClusterIdentifier: "myCluster",
+ AWSRegion: "us-east-1",
+ },
+ }
)
func TestAccRepositoryResource(t *testing.T) {
@@ -153,11 +168,13 @@ func TestAccRepositoryResource(t *testing.T) {
connDrainingConfig, "conn_draining_test")
allDynamic := setupRepositoryTest(
allRepoNodesAreDynamic, "all_repo_nodes_are_dynamic")
+ redshift := setupRepositoryTest(
+ withRedshiftSettings, "with_redshift_settings")
multiNode := setupRepositoryTest(
mixedMultipleNodesConfig, "multi_node_test")
- // Should use name of the last resource created.
+ // Must use name of the last resource created.
importTest := resource.TestStep{
ImportState: true,
ImportStateVerify: true,
@@ -171,6 +188,7 @@ func TestAccRepositoryResource(t *testing.T) {
update,
connDrainingEmpty,
connDraining,
+ redshift,
allDynamic,
multiNode,
importTest,
@@ -256,6 +274,23 @@ func repoCheckFuctions(repo repository.RepoInfo, resName string) resource.TestCh
}...)
}
+ if repo.RedshiftSettings != nil {
+ checkFuncs = append(checkFuncs, []resource.TestCheckFunc{
+ resource.TestCheckResourceAttr(resourceFullName,
+ "redshift_settings.0.cluster_identifier",
+ repo.RedshiftSettings.ClusterIdentifier,
+ ),
+ resource.TestCheckResourceAttr(resourceFullName,
+ "redshift_settings.0.workgroup_name",
+ repo.RedshiftSettings.WorkgroupName,
+ ),
+ resource.TestCheckResourceAttr(resourceFullName,
+ "redshift_settings.0.aws_region",
+ repo.RedshiftSettings.AWSRegion,
+ ),
+ }...)
+ }
+
return resource.ComposeTestCheckFunc(checkFuncs...)
}
@@ -307,6 +342,35 @@ func repoAsConfig(repo repository.RepoInfo, resName string) string {
)
}
+ if repo.RedshiftSettings != nil {
+ clusterIdentifier := "null"
+ workgroupName := "null"
+ awsRegion := "null"
+
+ if repo.RedshiftSettings.ClusterIdentifier != "" {
+ clusterIdentifier = fmt.Sprintf(`"%s"`, repo.RedshiftSettings.ClusterIdentifier)
+ }
+
+ if repo.RedshiftSettings.WorkgroupName != "" {
+ workgroupName = fmt.Sprintf(`"%s"`, repo.RedshiftSettings.WorkgroupName)
+ }
+
+ if repo.RedshiftSettings.AWSRegion != "" {
+ awsRegion = fmt.Sprintf(`"%s"`, repo.RedshiftSettings.AWSRegion)
+ }
+
+ config += fmt.Sprintf(`
+ redshift_settings {
+ cluster_identifier = %s
+ workgroup_name = %s
+ aws_region = %s
+ }`,
+ clusterIdentifier,
+ workgroupName,
+ awsRegion,
+ )
+ }
+
for _, node := range repo.RepoNodes {
name, host := "null", "null"
if node.Name != "" {
diff --git a/cyral/internal/repository/useraccount/model.go b/cyral/internal/repository/useraccount/model.go
index 5b58c991..1d6cfdb4 100644
--- a/cyral/internal/repository/useraccount/model.go
+++ b/cyral/internal/repository/useraccount/model.go
@@ -20,7 +20,8 @@ type AuthScheme struct {
}
type AuthSchemeAWSIAM struct {
- RoleARN string `json:"roleARN,omitempty"`
+ RoleARN string `json:"roleARN,omitempty"`
+ AuthenticateAsIAMRole bool `json:"authenticateAsIAMRole,omitempty"`
}
type AuthSchemeAWSSecretsManager struct {
@@ -118,7 +119,8 @@ func (resource *UserAccountResource) WriteToSchema(d *schema.ResourceData) error
map[string]interface{}{
"aws_iam": []interface{}{
map[string]interface{}{
- "role_arn": resource.AuthScheme.AWSIAM.RoleARN,
+ "role_arn": resource.AuthScheme.AWSIAM.RoleARN,
+ "authenticate_as_iam_role": resource.AuthScheme.AWSIAM.AuthenticateAsIAMRole,
},
},
},
@@ -259,7 +261,8 @@ func (userAccount *UserAccountResource) ReadFromSchema(d *schema.ResourceData) e
case "aws_iam":
userAccount.AuthScheme = &AuthScheme{
AWSIAM: &AuthSchemeAWSIAM{
- RoleARN: m["role_arn"].(string),
+ RoleARN: m["role_arn"].(string),
+ AuthenticateAsIAMRole: m["authenticate_as_iam_role"].(bool),
},
}
case "aws_secrets_manager":
diff --git a/cyral/internal/repository/useraccount/resource.go b/cyral/internal/repository/useraccount/resource.go
index 67bfcf2a..eae869f2 100644
--- a/cyral/internal/repository/useraccount/resource.go
+++ b/cyral/internal/repository/useraccount/resource.go
@@ -212,6 +212,12 @@ func resourceSchema() *schema.Resource {
Type: schema.TypeString,
Required: true,
},
+ "authenticate_as_iam_role": {
+ Description: "Indicates whether to access as an AWS IAM role (`true`)" +
+ "or a native database user (`false`). Defaults to `false`.",
+ Type: schema.TypeBool,
+ Optional: true,
+ },
},
},
},
diff --git a/cyral/internal/repository/useraccount/resource_test.go b/cyral/internal/repository/useraccount/resource_test.go
index 22f1b77e..8aeb329c 100644
--- a/cyral/internal/repository/useraccount/resource_test.go
+++ b/cyral/internal/repository/useraccount/resource_test.go
@@ -97,7 +97,8 @@ func TestAccRepositoryUserAccountResource(t *testing.T) {
Name: "aws-iam-useracc",
AuthScheme: &useraccount.AuthScheme{
AWSIAM: &useraccount.AuthSchemeAWSIAM{
- RoleARN: "role-arn-1",
+ RoleARN: "role-arn-1",
+ AuthenticateAsIAMRole: true,
},
},
}
@@ -286,7 +287,11 @@ func setupRepositoryUserAccountCheck(resName string, userAccount useraccount.Use
checkFuncs = append(checkFuncs,
resource.TestCheckResourceAttr(resFullName,
authSchemeScope+"aws_iam.0.role_arn",
- authScheme.AWSIAM.RoleARN))
+ authScheme.AWSIAM.RoleARN),
+ resource.TestCheckResourceAttr(resFullName,
+ authSchemeScope+"aws_iam.0.authenticate_as_iam_role",
+ strconv.FormatBool(authScheme.AWSIAM.AuthenticateAsIAMRole)),
+ )
case authScheme.AWSSecretsManager != nil:
checkFuncs = append(checkFuncs,
resource.TestCheckResourceAttr(resFullName,
@@ -348,7 +353,10 @@ func setupRepositoryUserAccountConfig(resName string, userAccount useraccount.Us
authSchemeStr = fmt.Sprintf(`
aws_iam {
role_arn = "%s"
- }`, authScheme.AWSIAM.RoleARN)
+ authenticate_as_iam_role = %t
+ }`,
+ authScheme.AWSIAM.RoleARN,
+ authScheme.AWSIAM.AuthenticateAsIAMRole)
case authScheme.AWSSecretsManager != nil:
authSchemeStr = fmt.Sprintf(`
aws_secrets_manager {
diff --git a/docs/resources/repository.md b/docs/resources/repository.md
index e03a63dd..ccd151cc 100644
--- a/docs/resources/repository.md
+++ b/docs/resources/repository.md
@@ -112,6 +112,7 @@ resource "cyral_repository" "multi_node_mongo_repo" {
- `connection_draining` (Block Set, Max: 1) Parameters related to connection draining. (see [below for nested schema](#nestedblock--connection_draining))
- `labels` (List of String) Labels enable you to categorize your repository.
- `mongodb_settings` (Block Set, Max: 1) Parameters related to MongoDB repositories. (see [below for nested schema](#nestedblock--mongodb_settings))
+- `redshift_settings` (Block Set, Max: 1) Parameters related to Redshift repositories. (see [below for nested schema](#nestedblock--redshift_settings))
### Read-Only
@@ -175,3 +176,13 @@ Optional:
- `replica_set_name` (String) Name of the replica set, if applicable.
- `srv_record_name` (String) Name of a DNS SRV record which contains cluster topology details. If specified, then all `repo_node` blocks must be declared dynamic (see [`dynamic`](#dynamic)). Only supported for `server_type="sharded"` or `server_type="replicaset".
+
+
+
+### Nested Schema for `redshift_settings`
+
+Optional:
+
+- `aws_region` (String) Code of the AWS region where the Redshift instance is deployed.
+- `cluster_identifier` (String) Name of the provisioned cluster.
+- `workgroup_name` (String) Workgroup name for serverless cluster.
diff --git a/docs/resources/repository_user_account.md b/docs/resources/repository_user_account.md
index 7363df5b..2cc5ca71 100644
--- a/docs/resources/repository_user_account.md
+++ b/docs/resources/repository_user_account.md
@@ -159,6 +159,10 @@ Required:
- `role_arn` (String) The AWS IAM roleARN to gain access to the database.
+Optional:
+
+- `authenticate_as_iam_role` (Boolean) Indicates whether to access as an AWS IAM role (`true`)or a native database user (`false`). Defaults to `false`.
+
### Nested Schema for `auth_scheme.aws_secrets_manager`