From 02200240cb1ebd64472d5e20d0c8b896959f3476 Mon Sep 17 00:00:00 2001 From: Wojciech Inglot Date: Wed, 25 Aug 2021 08:50:27 +0200 Subject: [PATCH 1/2] Grant for database objects --- redshift/helpers.go | 23 +- redshift/provider.go | 1 + redshift/resource_redshift_grant.go | 365 +++++++++++++++++++++++ redshift/resource_redshift_grant_test.go | 48 +++ 4 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 redshift/resource_redshift_grant.go create mode 100644 redshift/resource_redshift_grant_test.go diff --git a/redshift/helpers.go b/redshift/helpers.go index 82df96b..f7a2e80 100644 --- a/redshift/helpers.go +++ b/redshift/helpers.go @@ -170,8 +170,16 @@ func validatePrivileges(privileges []string, objectType string) bool { default: return false } + case "DATABASE": + switch strings.ToUpper(p) { + case "CREATE", "TEMPORARY", "TEMP": + continue + default: + return false + } + default: + return false } - } return true @@ -182,3 +190,16 @@ func appendIfTrue(condition bool, item string, list *[]string) { *list = append(*list, item) } } + +func setToPgIdentList(identifiers *schema.Set, prefix string) string { + quoted := make([]string, identifiers.Len()) + for i, identifier := range identifiers.List() { + if prefix == "" { + quoted[i] = pq.QuoteIdentifier(identifier.(string)) + } else { + quoted[i] = fmt.Sprintf("%s.%s", pq.QuoteIdentifier(prefix), pq.QuoteIdentifier(identifier.(string))) + } + } + + return strings.Join(quoted, ",") +} diff --git a/redshift/provider.go b/redshift/provider.go index 53d94d9..652ce06 100644 --- a/redshift/provider.go +++ b/redshift/provider.go @@ -121,6 +121,7 @@ func Provider() *schema.Provider { "redshift_schema": redshiftSchema(), "redshift_privilege": redshiftPrivilege(), "redshift_default_privileges": redshiftDefaultPrivileges(), + "redshift_grant": redshiftGrant(), "redshift_database": redshiftDatabase(), "redshift_datashare": redshiftDatashare(), "redshift_datashare_privilege": redshiftDatasharePrivilege(), diff --git a/redshift/resource_redshift_grant.go b/redshift/resource_redshift_grant.go new file mode 100644 index 0000000..726f1a6 --- /dev/null +++ b/redshift/resource_redshift_grant.go @@ -0,0 +1,365 @@ +package redshift + +import ( + "database/sql" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/lib/pq" +) + +const ( + grantGroupAttr = "group" + grantSchemaAttr = "schema" + grantObjectTypeAttr = "object_type" + grantObjectsAttr = "objects" + grantPrivilegesAttr = "privileges" +) + +var grantAllowedObjectTypes = []string{ + "table", + "schema", + "database", +} + +var grantObjectTypesCodes = map[string]string{ + "table": "r", +} + +func redshiftGrant() *schema.Resource { + return &schema.Resource{ + Description: ` +Defines access privileges for user group. Privileges include access options such as being able to read data in tables and views, write data, create tables, and drop tables. Use this command to give specific privileges for a table, database, schema, function, procedure, language, or column. +`, + Read: RedshiftResourceFunc(resourceRedshiftGrantRead), + Create: RedshiftResourceFunc( + RedshiftResourceRetryOnPQErrors(resourceRedshiftGrantCreate), + ), + Delete: RedshiftResourceFunc( + RedshiftResourceRetryOnPQErrors(resourceRedshiftGrantDelete), + ), + + // Since we revoke all when creating, we can use create as update + Update: RedshiftResourceFunc( + RedshiftResourceRetryOnPQErrors(resourceRedshiftGrantCreate), + ), + + Schema: map[string]*schema.Schema{ + grantGroupAttr: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the group to grant privileges on.", + }, + grantSchemaAttr: { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The database schema to grant privileges on for this group.", + }, + grantObjectTypeAttr: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(grantAllowedObjectTypes, false), + Description: "The Redshift object type to grant privileges on (one of: " + strings.Join(grantAllowedObjectTypes, ", ") + ").", + }, + grantObjectsAttr: { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + StateFunc: func(val interface{}) string { + return strings.ToLower(val.(string)) + }, + }, + Set: schema.HashString, + Description: "The objects upon which to grant the privileges. An empty list (the default) means to grant permissions on all objects of the specified type. Only has effect if `object_type` is set to `table`.", + }, + grantPrivilegesAttr: { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + StateFunc: func(val interface{}) string { + return strings.ToLower(val.(string)) + }, + }, + Set: schema.HashString, + Description: "The list of privileges to apply as default privileges. See [GRANT command documentation](https://docs.aws.amazon.com/redshift/latest/dg/r_GRANT.html) to see what privileges are available to which object type. An empty list could be provided to revoke all privileges for this group", + }, + }, + } +} + +func resourceRedshiftGrantCreate(db *DBConnection, d *schema.ResourceData) error { + objectType := d.Get(grantObjectTypeAttr).(string) + schemaName := d.Get(grantSchemaAttr).(string) + objects := d.Get(grantObjectsAttr).(*schema.Set).List() + + privileges := []string{} + for _, p := range d.Get(grantPrivilegesAttr).(*schema.Set).List() { + privileges = append(privileges, p.(string)) + } + + // validate parameters + if objectType == "table" && schemaName == "" { + return fmt.Errorf("parameter `%s` is required for objects of type table", grantSchemaAttr) + } + + if (objectType == "database" || objectType == "schema") && len(objects) > 0 { + return fmt.Errorf("cannot specify `%s` when `%s` is `database` or `schema`", grantObjectsAttr, grantObjectTypeAttr) + } + + if !validatePrivileges(privileges, objectType) { + return fmt.Errorf("Invalid privileges list %v for object of type %s", privileges, objectType) + } + + tx, err := startTransaction(db.client, "") + if err != nil { + return err + } + defer deferredRollback(tx) + + if err := revokeGroupGrants(tx, db.client.databaseName, d); err != nil { + return err + } + + if err := createGroupGrants(tx, db.client.databaseName, d); err != nil { + return err + } + + if err = tx.Commit(); err != nil { + return fmt.Errorf("could not commit transaction: %w", err) + } + + d.SetId(generateGrantID(d)) + + return resourceRedshiftGrantReadImpl(db, d) +} + +func resourceRedshiftGrantDelete(db *DBConnection, d *schema.ResourceData) error { + tx, err := startTransaction(db.client, "") + if err != nil { + return err + } + defer deferredRollback(tx) + + if err := revokeGroupGrants(tx, db.client.databaseName, d); err != nil { + return err + } + + if err = tx.Commit(); err != nil { + return fmt.Errorf("could not commit transaction: %w", err) + } + + return nil +} + +func resourceRedshiftGrantRead(db *DBConnection, d *schema.ResourceData) error { + return resourceRedshiftGrantReadImpl(db, d) +} + +func resourceRedshiftGrantReadImpl(db *DBConnection, d *schema.ResourceData) error { + objectType := d.Get(grantObjectTypeAttr).(string) + + switch objectType { + case "database": + return readGroupDatabaseGrants(db, d) + default: + return fmt.Errorf("Unsupported %s %s", grantObjectTypeAttr, objectType) + } +} + +func readGroupDatabaseGrants(db *DBConnection, d *schema.ResourceData) error { + groupName := d.Get(grantGroupAttr).(string) + var databaseCreate, databaseTemp bool + + query := ` + SELECT + decode(charindex('C',split_part(split_part(array_to_string(db.datacl, '|'),gr.groname,2 ) ,'/',1)), 0,0,1) as create, + decode(charindex('T',split_part(split_part(array_to_string(db.datacl, '|'),gr.groname,2 ) ,'/',1)), 0,0,1) as temporary + FROM pg_database db, pg_group gr + WHERE + db.datname=$1 + AND gr.groname=$2 +` + + if err := db.QueryRow(query, db.client.databaseName, groupName).Scan(&databaseCreate, &databaseTemp); err != nil { + return err + } + + privileges := []string{} + appendIfTrue(databaseCreate, "create", &privileges) + appendIfTrue(databaseTemp, "temporary", &privileges) + + log.Printf("[DEBUG] Collected database '%s' privileges for group %s: %v", db.client.databaseName, groupName, privileges) + + d.Set(grantPrivilegesAttr, privileges) + + return nil +} + +func readGroupGrantsForTables(tx *sql.Tx, groupName, schemaName string, tablesNames []string) ([]string, error) { + var tables, tableSelect, tableUpdate, tableInsert, tableDelete, tableReferences int + query := ` + SELECT + nvl(count(cl.relname), 0) tables, + nvl(sum(decode(charindex('r',split_part(split_part(array_to_string(relacl, '|'),pu.groname,2 ) ,'/',1)), 0,0,1)), 0) as select, + nvl(sum(decode(charindex('w',split_part(split_part(array_to_string(relacl, '|'),pu.groname,2 ) ,'/',1)), 0,0,1)), 0) as update, + nvl(sum(decode(charindex('a',split_part(split_part(array_to_string(relacl, '|'),pu.groname,2 ) ,'/',1)), 0,0,1)), 0) as insert, + nvl(sum(decode(charindex('d',split_part(split_part(array_to_string(relacl, '|'),pu.groname,2 ) ,'/',1)), 0,0,1)), 0) as delete, + nvl(sum(decode(charindex('x',split_part(split_part(array_to_string(relacl, '|'),pu.groname,2 ) ,'/',1)), 0,0,1)), 0) as references + FROM pg_class cl + JOIN pg_group gr ON array_to_string(cl.relacl, '|') LIKE '%group '||gr.groname||'=%' + JOIN pg_namespace nsp ON nsp.oid = cl.relnamespace + WHERE + cl.relkind = 'r' + AND gr.groname=$1 + AND nsp.nspname=$2 +` + + var err error = nil + if len(tablesNames) > 0 { + query = fmt.Sprintf("%s AND cl.relname = ANY($3)", query) + err = tx.QueryRow(query, groupName, schemaName, pq.Array(tablesNames)).Scan(tables, tableSelect, tableUpdate, tableInsert, tableDelete, tableReferences) + } else { + err = tx.QueryRow(query, groupName, schemaName).Scan(tableSelect, tableUpdate, tableInsert, tableDelete, tableReferences) + } + + if err != nil { + return []string{}, fmt.Errorf("failed to collect group privileges: %w", err) + } + + privileges := []string{} + expectedPrivileges := len(tablesNames) + appendIfTrue(tableSelect == expectedPrivileges, "select", &privileges) + appendIfTrue(tableUpdate == expectedPrivileges, "update", &privileges) + appendIfTrue(tableInsert == expectedPrivileges, "insert", &privileges) + appendIfTrue(tableDelete == expectedPrivileges, "delete", &privileges) + appendIfTrue(tableReferences == expectedPrivileges, "references", &privileges) + + log.Printf("[DEBUG] Collected privileges for group %s: %v\n", groupName, privileges) + + return privileges, nil +} + +func revokeGroupGrants(tx *sql.Tx, databaseName string, d *schema.ResourceData) error { + query := createGroupRevokeQuery(d, databaseName) + _, err := tx.Exec(query) + return err +} + +func createGroupGrants(tx *sql.Tx, databaseName string, d *schema.ResourceData) error { + if d.Get(grantPrivilegesAttr).(*schema.Set).Len() == 0 { + log.Printf("[DEBUG] no privileges to grant for group %s", d.Get(grantGroupAttr).(string)) + return nil + } + + query := createGroupGrantQuery(d, databaseName) + _, err := tx.Exec(query) + return err +} + +func createGroupRevokeQuery(d *schema.ResourceData, databaseName string) string { + var query string + + switch strings.ToUpper(d.Get(grantObjectTypeAttr).(string)) { + case "DATABASE": + query = fmt.Sprintf( + "REVOKE ALL PRIVILEGES ON DATABASE %s FROM GROUP %s", + pq.QuoteIdentifier(databaseName), + pq.QuoteIdentifier(d.Get(grantGroupAttr).(string)), + ) + case "SCHEMA": + query = fmt.Sprintf( + "REVOKE ALL PRIVILEGES ON SCHEMA %s FROM GROUP %s", + pq.QuoteIdentifier(d.Get(grantSchemaAttr).(string)), + pq.QuoteIdentifier(d.Get(grantGroupAttr).(string)), + ) + case "TABLE": + objects := d.Get(grantObjectsAttr).(*schema.Set) + if objects.Len() > 0 { + query = fmt.Sprintf( + "REVOKE ALL PRIVILEGES ON %s %s FROM GROUP %s", + strings.ToUpper(d.Get(grantObjectTypeAttr).(string)), + setToPgIdentList(objects, d.Get(grantSchemaAttr).(string)), + pq.QuoteIdentifier(d.Get(grantGroupAttr).(string)), + ) + } else { + query = fmt.Sprintf( + "REVOKE ALL PRIVILEGES ON ALL %sS IN SCHEMA %s FROM GROUP %s", + strings.ToUpper(d.Get(grantObjectTypeAttr).(string)), + pq.QuoteIdentifier(d.Get(grantSchemaAttr).(string)), + pq.QuoteIdentifier(d.Get(grantGroupAttr).(string)), + ) + } + } + + return query +} + +func createGroupGrantQuery(d *schema.ResourceData, databaseName string) string { + var query string + privileges := []string{} + for _, p := range d.Get(grantPrivilegesAttr).(*schema.Set).List() { + privileges = append(privileges, p.(string)) + } + + switch strings.ToUpper(d.Get(grantObjectTypeAttr).(string)) { + case "DATABASE": + query = fmt.Sprintf( + "GRANT %s ON DATABASE %s TO GROUP %s", + strings.Join(privileges, ","), + pq.QuoteIdentifier(databaseName), + pq.QuoteIdentifier(d.Get(grantGroupAttr).(string)), + ) + case "SCHEMA": + query = fmt.Sprintf( + "GRANT %s ON SCHEMA %s TO GROUP %s", + strings.Join(privileges, ","), + pq.QuoteIdentifier(d.Get(grantSchemaAttr).(string)), + pq.QuoteIdentifier(d.Get(grantGroupAttr).(string)), + ) + case "TABLE": + objects := d.Get(grantObjectsAttr).(*schema.Set) + if objects.Len() > 0 { + query = fmt.Sprintf( + "GRANT %s ON %s %s TO GROUP %s", + strings.Join(privileges, ","), + strings.ToUpper(d.Get(grantObjectTypeAttr).(string)), + setToPgIdentList(objects, d.Get(grantSchemaAttr).(string)), + pq.QuoteIdentifier(d.Get(grantGroupAttr).(string)), + ) + } else { + query = fmt.Sprintf( + "GRANT %s ON ALL %sS IN SCHEMA %s TO GROUP %s", + strings.Join(privileges, ","), + strings.ToUpper(d.Get(grantObjectTypeAttr).(string)), + pq.QuoteIdentifier(d.Get(grantSchemaAttr).(string)), + pq.QuoteIdentifier(d.Get(grantGroupAttr).(string)), + ) + } + } + + return query +} + +func generateGrantID(d *schema.ResourceData) string { + groupName := d.Get(defaultPrivilegesGroupAttr).(string) + objectType := d.Get(defaultPrivilegesObjectTypeAttr).(string) + parts := []string{groupName, objectType} + + if objectType != "database" { + parts = append(parts, d.Get(grantSchemaAttr).(string)) + } + + for _, object := range d.Get(grantObjectsAttr).(*schema.Set).List() { + parts = append(parts, object.(string)) + } + + return strings.Join(parts, "_") +} diff --git a/redshift/resource_redshift_grant_test.go b/redshift/resource_redshift_grant_test.go new file mode 100644 index 0000000..9410fc5 --- /dev/null +++ b/redshift/resource_redshift_grant_test.go @@ -0,0 +1,48 @@ +package redshift + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccRedshiftGrant_BasicDatabase(t *testing.T) { + groupName := strings.ReplaceAll(acctest.RandomWithPrefix("tf_acc_group_basic"), "-", "_") + //dbName := os.Getenv("REDSHIFT_DATABASE") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: func(s *terraform.State) error { return nil }, + Steps: []resource.TestStep{ + { + Config: testAccRedshiftGrantConfig_BasicDatabase(groupName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("redshift_grant.grant", "id", fmt.Sprintf("%s_database", groupName)), + resource.TestCheckResourceAttr("redshift_grant.grant", "group", groupName), + resource.TestCheckResourceAttr("redshift_grant.grant", "object_type", "database"), + resource.TestCheckResourceAttr("redshift_grant.grant", "privileges.#", "2"), + resource.TestCheckTypeSetElemAttr("redshift_grant.grant", "privileges.*", "create"), + resource.TestCheckTypeSetElemAttr("redshift_grant.grant", "privileges.*", "temporary"), + ), + }, + }, + }) +} + +func testAccRedshiftGrantConfig_BasicDatabase(groupName string) string { + return fmt.Sprintf(` +resource "redshift_group" "group" { + name = %[1]q +} + +resource "redshift_grant" "grant" { + group = redshift_group.group.name + object_type = "database" + privileges = ["create", "temporary"] +}`, groupName) +} From 643389ae9325f8d3eba8a5fdc49ae71ed7bf65fa Mon Sep 17 00:00:00 2001 From: Wojciech Inglot Date: Thu, 26 Aug 2021 16:37:14 +0200 Subject: [PATCH 2/2] Grant/revoke for tables --- redshift/helpers.go | 2 +- redshift/resource_redshift_grant.go | 115 +++++++++++++++++------ redshift/resource_redshift_grant_test.go | 95 ++++++++++++++++++- 3 files changed, 179 insertions(+), 33 deletions(-) diff --git a/redshift/helpers.go b/redshift/helpers.go index f7a2e80..5f71a32 100644 --- a/redshift/helpers.go +++ b/redshift/helpers.go @@ -172,7 +172,7 @@ func validatePrivileges(privileges []string, objectType string) bool { } case "DATABASE": switch strings.ToUpper(p) { - case "CREATE", "TEMPORARY", "TEMP": + case "CREATE", "TEMPORARY": continue default: return false diff --git a/redshift/resource_redshift_grant.go b/redshift/resource_redshift_grant.go index 726f1a6..87997e1 100644 --- a/redshift/resource_redshift_grant.go +++ b/redshift/resource_redshift_grant.go @@ -169,6 +169,10 @@ func resourceRedshiftGrantReadImpl(db *DBConnection, d *schema.ResourceData) err switch objectType { case "database": return readGroupDatabaseGrants(db, d) + case "schema": + return readGroupSchemaGrants(db, d) + case "table": + return readGroupTableGrants(db, d) default: return fmt.Errorf("Unsupported %s %s", grantObjectTypeAttr, objectType) } @@ -203,48 +207,99 @@ func readGroupDatabaseGrants(db *DBConnection, d *schema.ResourceData) error { return nil } -func readGroupGrantsForTables(tx *sql.Tx, groupName, schemaName string, tablesNames []string) ([]string, error) { - var tables, tableSelect, tableUpdate, tableInsert, tableDelete, tableReferences int +func readGroupSchemaGrants(db *DBConnection, d *schema.ResourceData) error { + groupName := d.Get(grantGroupAttr).(string) + schemaName := d.Get(grantSchemaAttr).(string) + + var schemaCreate, schemaUsage bool + query := ` SELECT - nvl(count(cl.relname), 0) tables, - nvl(sum(decode(charindex('r',split_part(split_part(array_to_string(relacl, '|'),pu.groname,2 ) ,'/',1)), 0,0,1)), 0) as select, - nvl(sum(decode(charindex('w',split_part(split_part(array_to_string(relacl, '|'),pu.groname,2 ) ,'/',1)), 0,0,1)), 0) as update, - nvl(sum(decode(charindex('a',split_part(split_part(array_to_string(relacl, '|'),pu.groname,2 ) ,'/',1)), 0,0,1)), 0) as insert, - nvl(sum(decode(charindex('d',split_part(split_part(array_to_string(relacl, '|'),pu.groname,2 ) ,'/',1)), 0,0,1)), 0) as delete, - nvl(sum(decode(charindex('x',split_part(split_part(array_to_string(relacl, '|'),pu.groname,2 ) ,'/',1)), 0,0,1)), 0) as references - FROM pg_class cl - JOIN pg_group gr ON array_to_string(cl.relacl, '|') LIKE '%group '||gr.groname||'=%' - JOIN pg_namespace nsp ON nsp.oid = cl.relnamespace + decode(charindex('C',split_part(split_part(array_to_string(ns.nspacl, '|'),gr.groname,2 ) ,'/',1)), 0,0,1) as create, + decode(charindex('U',split_part(split_part(array_to_string(ns.nspacl, '|'),gr.groname,2 ) ,'/',1)), 0,0,1) as usage + FROM pg_namespace ns, pg_group gr WHERE - cl.relkind = 'r' - AND gr.groname=$1 - AND nsp.nspname=$2 + ns.nspname=$1 + AND gr.groname=$2 ` - var err error = nil - if len(tablesNames) > 0 { - query = fmt.Sprintf("%s AND cl.relname = ANY($3)", query) - err = tx.QueryRow(query, groupName, schemaName, pq.Array(tablesNames)).Scan(tables, tableSelect, tableUpdate, tableInsert, tableDelete, tableReferences) - } else { - err = tx.QueryRow(query, groupName, schemaName).Scan(tableSelect, tableUpdate, tableInsert, tableDelete, tableReferences) + if err := db.QueryRow(query, schemaName, groupName).Scan(&schemaCreate, &schemaUsage); err != nil { + return err } + privileges := []string{} + appendIfTrue(schemaCreate, "create", &privileges) + appendIfTrue(schemaUsage, "usage", &privileges) + + log.Printf("[DEBUG] Collected schema '%s' privileges for group %s: %v", schemaName, groupName, privileges) + + d.Set(grantPrivilegesAttr, privileges) + + return nil +} + +func readGroupTableGrants(db *DBConnection, d *schema.ResourceData) error { + query := ` + SELECT + relname, + decode(charindex('r',split_part(split_part(array_to_string(relacl, '|'),gr.groname,2 ) ,'/',1)), 0,0,1) as select, + decode(charindex('w',split_part(split_part(array_to_string(relacl, '|'),gr.groname,2 ) ,'/',1)), 0,0,1) as update, + decode(charindex('a',split_part(split_part(array_to_string(relacl, '|'),gr.groname,2 ) ,'/',1)), 0,0,1) as insert, + decode(charindex('d',split_part(split_part(array_to_string(relacl, '|'),gr.groname,2 ) ,'/',1)), 0,0,1) as delete, + decode(charindex('x',split_part(split_part(array_to_string(relacl, '|'),gr.groname,2 ) ,'/',1)), 0,0,1) as references + FROM pg_group gr, pg_class cl + JOIN pg_namespace nsp ON nsp.oid = cl.relnamespace + WHERE + cl.relkind = $1 + AND gr.groname=$2 + AND nsp.nspname=$3 +` + + groupName := d.Get(grantGroupAttr).(string) + schemaName := d.Get(grantSchemaAttr).(string) + objects := d.Get(grantObjectsAttr).(*schema.Set) + + rows, err := db.Query(query, grantObjectTypesCodes["table"], groupName, schemaName) if err != nil { - return []string{}, fmt.Errorf("failed to collect group privileges: %w", err) + return err } - privileges := []string{} - expectedPrivileges := len(tablesNames) - appendIfTrue(tableSelect == expectedPrivileges, "select", &privileges) - appendIfTrue(tableUpdate == expectedPrivileges, "update", &privileges) - appendIfTrue(tableInsert == expectedPrivileges, "insert", &privileges) - appendIfTrue(tableDelete == expectedPrivileges, "delete", &privileges) - appendIfTrue(tableReferences == expectedPrivileges, "references", &privileges) + for rows.Next() { + var objName string + var tableSelect, tableUpdate, tableInsert, tableDelete, tableReferences bool + + if err := rows.Scan(&objName, &tableSelect, &tableUpdate, &tableInsert, &tableDelete, &tableReferences); err != nil { + return err + } + + if objects.Len() > 0 && !objects.Contains(objName) { + continue + } + + privilegesSet := schema.NewSet(schema.HashString, nil) + if tableSelect { + privilegesSet.Add("select") + } + if tableUpdate { + privilegesSet.Add("update") + } + if tableInsert { + privilegesSet.Add("insert") + } + if tableDelete { + privilegesSet.Add("delete") + } + if tableReferences { + privilegesSet.Add("references") + } - log.Printf("[DEBUG] Collected privileges for group %s: %v\n", groupName, privileges) + if !privilegesSet.Equal(d.Get(grantPrivilegesAttr).(*schema.Set)) { + d.Set(grantPrivilegesAttr, privilegesSet) + break + } + } - return privileges, nil + return nil } func revokeGroupGrants(tx *sql.Tx, databaseName string, d *schema.ResourceData) error { diff --git a/redshift/resource_redshift_grant_test.go b/redshift/resource_redshift_grant_test.go index 9410fc5..ed28172 100644 --- a/redshift/resource_redshift_grant_test.go +++ b/redshift/resource_redshift_grant_test.go @@ -12,8 +12,6 @@ import ( func TestAccRedshiftGrant_BasicDatabase(t *testing.T) { groupName := strings.ReplaceAll(acctest.RandomWithPrefix("tf_acc_group_basic"), "-", "_") - //dbName := os.Getenv("REDSHIFT_DATABASE") - resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -46,3 +44,96 @@ resource "redshift_grant" "grant" { privileges = ["create", "temporary"] }`, groupName) } + +func TestAccRedshiftGrant_BasicSchema(t *testing.T) { + userName := strings.ReplaceAll(acctest.RandomWithPrefix("tf_acc_user_basic"), "-", "_") + groupName := strings.ReplaceAll(acctest.RandomWithPrefix("tf_acc_group_basic"), "-", "_") + schemaName := strings.ReplaceAll(acctest.RandomWithPrefix("tf_acc_schema_basic"), "-", "_") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: func(s *terraform.State) error { return nil }, + Steps: []resource.TestStep{ + { + Config: testAccRedshiftGrantConfig_BasicSchema(userName, groupName, schemaName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("redshift_grant.grant", "id", fmt.Sprintf("%s_schema_%s", groupName, schemaName)), + resource.TestCheckResourceAttr("redshift_grant.grant", "group", groupName), + resource.TestCheckResourceAttr("redshift_grant.grant", "object_type", "schema"), + resource.TestCheckResourceAttr("redshift_grant.grant", "privileges.#", "2"), + resource.TestCheckTypeSetElemAttr("redshift_grant.grant", "privileges.*", "create"), + resource.TestCheckTypeSetElemAttr("redshift_grant.grant", "privileges.*", "usage"), + ), + }, + }, + }) +} + +func testAccRedshiftGrantConfig_BasicSchema(userName, groupName, schemaName string) string { + return fmt.Sprintf(` +resource "redshift_user" "user" { + name = %[1]q +} + +resource "redshift_group" "group" { + name = %[2]q +} + +resource "redshift_schema" "schema" { + name = %[3]q + + owner = redshift_user.user.name +} + +resource "redshift_grant" "grant" { + group = redshift_group.group.name + schema = redshift_schema.schema.name + + object_type = "schema" + privileges = ["create", "usage"] +} +`, userName, groupName, schemaName) +} + +func TestAccRedshiftGrant_BasicTable(t *testing.T) { + groupName := strings.ReplaceAll(acctest.RandomWithPrefix("tf_acc_group_basic"), "-", "_") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: func(s *terraform.State) error { return nil }, + Steps: []resource.TestStep{ + { + Config: testAccRedshiftGrantConfig_BasicTable(groupName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("redshift_grant.grant", "id", fmt.Sprintf("%s_table_pg_catalog_pg_user_info", groupName)), + resource.TestCheckResourceAttr("redshift_grant.grant", "group", groupName), + resource.TestCheckResourceAttr("redshift_grant.grant", "schema", "pg_catalog"), + resource.TestCheckResourceAttr("redshift_grant.grant", "object_type", "table"), + resource.TestCheckResourceAttr("redshift_grant.grant", "objects.#", "1"), + resource.TestCheckTypeSetElemAttr("redshift_grant.grant", "objects.*", "pg_user_info"), + resource.TestCheckResourceAttr("redshift_grant.grant", "privileges.#", "1"), + resource.TestCheckTypeSetElemAttr("redshift_grant.grant", "privileges.*", "select"), + ), + }, + }, + }) +} + +func testAccRedshiftGrantConfig_BasicTable(groupName string) string { + return fmt.Sprintf(` +resource "redshift_group" "group" { + name = %[1]q +} + +resource "redshift_grant" "grant" { + group = redshift_group.group.name + schema = "pg_catalog" + + object_type = "table" + objects = ["pg_user_info"] + privileges = ["select"] +} +`, groupName) +}