Skip to content

Commit

Permalink
feat: create snowflake_role_ownership_grant resource (#917)
Browse files Browse the repository at this point in the history
  • Loading branch information
aidanmelen committed Mar 21, 2022
1 parent 96152d7 commit 17de20f
Show file tree
Hide file tree
Showing 10 changed files with 441 additions and 0 deletions.
28 changes: 28 additions & 0 deletions docs/resources/role_ownership_grant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "snowflake_role_ownership_grant Resource - terraform-provider-snowflake"
subcategory: ""
description: |-
---

# snowflake_role_ownership_grant (Resource)





<!-- schema generated by tfplugindocs -->
## Schema

### Required

- **on_role_name** (String) The name of the role ownership is granted on.
- **to_role_name** (String) The name of the role to grant ownership. Please ensure that the role that terraform is using is granted access.

### Optional

- **current_grants** (String) Specifies whether to remove or transfer all existing outbound privileges on the object when ownership is transferred to a new role.
- **id** (String) The ID of this resource.


Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import snowflake_role_ownership_grant.example rolename
24 changes: 24 additions & 0 deletions examples/resources/snowflake_role_ownership_grants/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
resource "snowflake_role" "role" {
name = "rking_test_role"
comment = "for testing"
}

resource "snowflake_role" "other_role" {
name = "rking_test_role2"
}

# ensure the Terraform user inherits ownership privileges for the rking_test_role role
# otherwise Terraform will fail to destroy the rking_test_role2 role due to insufficient privileges
resource "snowflake_role_grants" "grants" {
role_name = snowflake_role.role.name

roles = [
"ACCOUNTADMIN",
]
}

resource "snowflake_role_ownership_grant" "grant" {
on_role_name = snowflake_role.role.name
to_role_name = snowflake_role.other_role.name
current_grants = "COPY"
}
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ func getResources() map[string]*schema.Resource {
"snowflake_resource_monitor": resources.ResourceMonitor(),
"snowflake_role": resources.Role(),
"snowflake_role_grants": resources.RoleGrants(),
"snowflake_role_ownership_grant": resources.RoleOwnershipGrant(),
"snowflake_row_access_policy": resources.RowAccessPolicy(),
"snowflake_saml_integration": resources.SAMLIntegration(),
"snowflake_schema": resources.Schema(),
Expand Down
8 changes: 8 additions & 0 deletions pkg/resources/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,14 @@ func roleGrants(t *testing.T, id string, params map[string]interface{}) *schema.
return d
}

func roleOwnershipGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
r := require.New(t)
d := schema.TestResourceDataRaw(t, resources.RoleOwnershipGrant().Schema, params)
r.NotNil(d)
d.SetId(id)
return d
}

func apiIntegration(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
r := require.New(t)
d := schema.TestResourceDataRaw(t, resources.APIIntegration().Schema, params)
Expand Down
152 changes: 152 additions & 0 deletions pkg/resources/role_ownership_grant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package resources

import (
"database/sql"
"fmt"
"log"
"strings"

"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

var roleOwnershipGrantSchema = map[string]*schema.Schema{
"on_role_name": {
Type: schema.TypeString,
Elem: &schema.Schema{Type: schema.TypeString},
Required: true,
Description: "The name of the role ownership is granted on.",
ValidateFunc: func(val interface{}, key string) ([]string, []error) {
return snowflake.ValidateIdentifier(val)
},
},
"to_role_name": {
Type: schema.TypeString,
Elem: &schema.Schema{Type: schema.TypeString},
Required: true,
Description: "The name of the role to grant ownership. Please ensure that the role that terraform is using is granted access.",
ValidateFunc: func(val interface{}, key string) ([]string, []error) {
return snowflake.ValidateIdentifier(val)
},
},
"current_grants": {
Type: schema.TypeString,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Description: "Specifies whether to remove or transfer all existing outbound privileges on the object when ownership is transferred to a new role.",
Default: "COPY",
ValidateFunc: validation.StringInSlice([]string{
"COPY",
"REVOKE",
}, true),
},
}

func RoleOwnershipGrant() *schema.Resource {
return &schema.Resource{
Create: CreateRoleOwnershipGrant,
Read: ReadRoleOwnershipGrant,
Delete: DeleteRoleOwnershipGrant,
Update: UpdateRoleOwnershipGrant,
Schema: roleOwnershipGrantSchema,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
}
}

func CreateRoleOwnershipGrant(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
onRoleName := d.Get("on_role_name").(string)
toRoleName := d.Get("to_role_name").(string)
currentGrants := d.Get("current_grants").(string)

g := snowflake.RoleOwnershipGrant(onRoleName, currentGrants)
err := snowflake.Exec(db, g.Role(toRoleName).Grant())
if err != nil {
return err
}

d.SetId(fmt.Sprintf(`%s|%s|%s`, onRoleName, toRoleName, currentGrants))

return ReadRoleOwnershipGrant(d, meta)
}

func ReadRoleOwnershipGrant(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
log.Println(d.Id())
onRoleName := strings.Split(d.Id(), "|")[0]
currentGrants := strings.Split(d.Id(), "|")[2]

stmt := fmt.Sprintf("SHOW ROLES LIKE '%s'", onRoleName)
row := snowflake.QueryRow(db, stmt)

grant, err := snowflake.ScanRoleOwnershipGrant(row)
if err == sql.ErrNoRows {
// If not found, mark resource to be removed from statefile during apply or refresh
log.Printf("[DEBUG] role (%s) not found", d.Id())
d.SetId("")
return nil
}
if err != nil {
return err
}

if onRoleName != grant.Name.String {
return fmt.Errorf("no role found like '%s'", onRoleName)
}

grant.Name.String = strings.TrimPrefix(grant.Name.String, `"`)
grant.Name.String = strings.TrimSuffix(grant.Name.String, `"`)
err = d.Set("on_role_name", grant.Name.String)
if err != nil {
return err
}

grant.Owner.String = strings.TrimPrefix(grant.Owner.String, `"`)
grant.Owner.String = strings.TrimSuffix(grant.Owner.String, `"`)
err = d.Set("to_role_name", grant.Owner.String)
if err != nil {
return err
}

err = d.Set("current_grants", currentGrants)
if err != nil {
return err
}

return nil
}

func UpdateRoleOwnershipGrant(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
onRoleName := d.Get("on_role_name").(string)
toRoleName := d.Get("to_role_name").(string)
currentGrants := d.Get("current_grants").(string)

d.SetId(fmt.Sprintf(`%s|%s|%s`, onRoleName, toRoleName, currentGrants))

g := snowflake.RoleOwnershipGrant(onRoleName, currentGrants)
err := snowflake.Exec(db, g.Role(toRoleName).Grant())
if err != nil {
return err
}

return ReadRoleOwnershipGrant(d, meta)
}

func DeleteRoleOwnershipGrant(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
onRoleName := d.Get("on_role_name").(string)
currentGrants := d.Get("current_grants").(string)

g := snowflake.RoleOwnershipGrant(onRoleName, currentGrants)
err := snowflake.Exec(db, g.Role("ACCOUNTADMIN").Revoke())
if err != nil {
return err
}

d.SetId("")
return nil
}
57 changes: 57 additions & 0 deletions pkg/resources/role_ownership_grant_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package resources_test

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccRoleOwnershipGrant_defaults(t *testing.T) {
onRoleName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
toRoleName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)

resource.ParallelTest(t, resource.TestCase{
Providers: providers(),
Steps: []resource.TestStep{
{
Config: roleOwnershipGrantConfig(onRoleName, toRoleName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("snowflake_role_ownership_grant.grant", "on_role_name", onRoleName),
resource.TestCheckResourceAttr("snowflake_role_ownership_grant.grant", "to_role_name", toRoleName),
resource.TestCheckResourceAttr("snowflake_role_ownership_grant.grant", "current_grants", "COPY"),
),
},
},
})
}

func roleOwnershipGrantConfig(onRoleName, toRoleName string) string {
return fmt.Sprintf(`
resource "snowflake_role" "role" {
name = "%v"
}
resource "snowflake_role" "other_role" {
name = "%v"
}
resource "snowflake_role_grants" "grants" {
role_name = snowflake_role.role.name
roles = [
"ACCOUNTADMIN",
]
}
resource "snowflake_role_ownership_grant" "grant" {
on_role_name = snowflake_role.role.name
to_role_name = snowflake_role.other_role.name
current_grants = "COPY"
}
`, onRoleName, toRoleName)
}
84 changes: 84 additions & 0 deletions pkg/resources/role_ownership_grants_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package resources_test

import (
"database/sql"
"testing"

sqlmock "github.com/DATA-DOG/go-sqlmock"
"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/provider"
"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/resources"
. "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/testhelpers"
"github.com/stretchr/testify/require"
)

func TestRoleOwnershipGrant(t *testing.T) {
r := require.New(t)
err := resources.RoleOwnershipGrant().InternalValidate(provider.Provider().Schema, true)
r.NoError(err)
}

func TestRoleOwnershipGrantCreate(t *testing.T) {
r := require.New(t)

d := roleOwnershipGrant(t, "good_name", map[string]interface{}{
"on_role_name": "good_name",
"to_role_name": "other_good_name",
"current_grants": "COPY",
})

WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
mock.ExpectExec(`GRANT OWNERSHIP ON ROLE "good_name" TO ROLE "other_good_name" COPY CURRENT GRANTS`).WillReturnResult(sqlmock.NewResult(1, 1))
expectReadRoleOwnershipGrant(mock)
err := resources.CreateRoleOwnershipGrant(d, db)
r.NoError(err)
})
}

func TestRoleOwnershipGrantRead(t *testing.T) {
r := require.New(t)

d := roleOwnershipGrant(t, "good_name|other_good_name|COPY", map[string]interface{}{
"on_role_name": "good_name",
"to_role_name": "other_good_name",
"current_grants": "COPY",
})

WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
expectReadRoleOwnershipGrant(mock)
err := resources.ReadRoleOwnershipGrant(d, db)
r.NoError(err)
})
}

func expectReadRoleOwnershipGrant(mock sqlmock.Sqlmock) {
rows := sqlmock.NewRows([]string{
"created_on",
"name",
"is_default",
"is_current",
"is_inherited",
"assigned_to_users",
"granted_to_roles",
"granted_roles",
"owner",
"comment",
}).AddRow("_", "good_name", "", "", "", "", "", "", "other_good_name", "")
mock.ExpectQuery(`SHOW ROLES LIKE 'good_name'`).WillReturnRows(rows)
}

func TestRoleOwnershipGrantDelete(t *testing.T) {
r := require.New(t)

d := roleOwnershipGrant(t, "good_name|other_good_name|COPY", map[string]interface{}{
"on_role_name": "good_name",
"to_role_name": "other_good_name",
"current_grants": "COPY",
})

WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {

mock.ExpectExec(`GRANT OWNERSHIP ON ROLE "good_name" TO ROLE "ACCOUNTADMIN" COPY CURRENT GRANTS`).WillReturnResult(sqlmock.NewResult(1, 1))
err := resources.DeleteRoleOwnershipGrant(d, db)
r.NoError(err)
})
}

0 comments on commit 17de20f

Please sign in to comment.