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`