Skip to content

Commit

Permalink
feat: snowflake_user_ownership_grant resource (#969)
Browse files Browse the repository at this point in the history
  • Loading branch information
aidanmelen authored Apr 11, 2022
1 parent 1d1084d commit 6f3f09d
Show file tree
Hide file tree
Showing 13 changed files with 455 additions and 9 deletions.
28 changes: 28 additions & 0 deletions docs/resources/user_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_user_ownership_grant Resource - terraform-provider-snowflake"
subcategory: ""
description: |-
---

# snowflake_user_ownership_grant (Resource)





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

### Required

- **on_user_name** (String) The name of the user 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.


4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/stretchr/testify v1.7.0
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70
golang.org/x/tools v0.1.9
golang.org/x/tools v0.1.10
)

require (
Expand Down Expand Up @@ -111,7 +111,7 @@ require (
github.com/vmihailenco/tagparser v0.1.2 // indirect
github.com/zclconf/go-cty v1.10.0 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/mod v0.5.1 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -642,8 +642,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -861,8 +861,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ func getResources() map[string]*schema.Resource {
"snowflake_tag": resources.Tag(),
"snowflake_task": resources.Task(),
"snowflake_user": resources.User(),
"snowflake_user_ownership_grant": resources.UserOwnershipGrant(),
"snowflake_user_public_keys": resources.UserPublicKeys(),
"snowflake_view": resources.View(),
"snowflake_warehouse": resources.Warehouse(),
Expand Down
2 changes: 1 addition & 1 deletion pkg/resources/grant_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func grantIDFromString(stringID string) (*grantID, error) {
} else if len(lines[0]) == 5 && lines[0][4] == "true" {
grantOption = true
}

schemaName := ""
objectName := ""
privilege := ""
Expand Down
2 changes: 0 additions & 2 deletions pkg/resources/grant_helpers_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ func TestGrantIDFromString(t *testing.T) {
_, err = grantIDFromString(id)
r.Equal(fmt.Errorf("1 to 6 fields allowed in ID"), err)


// 0 lines
id = ""
_, err = grantIDFromString(id)
Expand Down Expand Up @@ -169,4 +168,3 @@ func TestGrantIDFromStringRoleGrant(t *testing.T) {
r.Equal([]string{"role3", "role4"}, grant.Roles)
r.Equal(false, grant.GrantOption)
}

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 userOwnershipGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
r := require.New(t)
d := schema.TestResourceDataRaw(t, resources.UserOwnershipGrant().Schema, params)
r.NotNil(d)
d.SetId(id)
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)
Expand Down
File renamed without changes.
152 changes: 152 additions & 0 deletions pkg/resources/user_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 userOwnershipGrantSchema = map[string]*schema.Schema{
"on_user_name": {
Type: schema.TypeString,
Elem: &schema.Schema{Type: schema.TypeString},
Required: true,
Description: "The name of the user 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 UserOwnershipGrant() *schema.Resource {
return &schema.Resource{
Create: CreateUserOwnershipGrant,
Read: ReadUserOwnershipGrant,
Delete: DeleteUserOwnershipGrant,
Update: UpdateUserOwnershipGrant,
Schema: userOwnershipGrantSchema,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
}
}

func CreateUserOwnershipGrant(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
user := d.Get("on_user_name").(string)
role := d.Get("to_role_name").(string)
currentGrants := d.Get("current_grants").(string)

g := snowflake.UserOwnershipGrant(user, currentGrants)
err := snowflake.Exec(db, g.Role(role).Grant())
if err != nil {
return err
}

d.SetId(fmt.Sprintf(`%s|%s|%s`, user, role, currentGrants))

return ReadUserOwnershipGrant(d, meta)
}

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

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

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

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

grant.Name.String = strings.TrimPrefix(grant.Name.String, `"`)
grant.Name.String = strings.TrimSuffix(grant.Name.String, `"`)
err = d.Set("on_user_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 UpdateUserOwnershipGrant(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
user := d.Get("on_user_name").(string)
role := d.Get("to_role_name").(string)
currentGrants := d.Get("current_grants").(string)

d.SetId(fmt.Sprintf(`%s|%s|%s`, user, role, currentGrants))

g := snowflake.UserOwnershipGrant(user, currentGrants)
err := snowflake.Exec(db, g.Role(role).Grant())
if err != nil {
return err
}

return ReadUserOwnershipGrant(d, meta)
}

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

g := snowflake.UserOwnershipGrant(user, 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/user_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 TestAccUserOwnershipGrant_defaults(t *testing.T) {
user := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)
role := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)

resource.ParallelTest(t, resource.TestCase{
Providers: providers(),
Steps: []resource.TestStep{
{
Config: userOwnershipGrantConfig(user, role),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("snowflake_user_ownership_grant.grant", "on_user_name", user),
resource.TestCheckResourceAttr("snowflake_user_ownership_grant.grant", "to_role_name", role),
resource.TestCheckResourceAttr("snowflake_user_ownership_grant.grant", "current_grants", "COPY"),
),
},
},
})
}

func userOwnershipGrantConfig(user, role string) string {
return fmt.Sprintf(`
resource "snowflake_user" "user" {
name = "%v"
}
resource "snowflake_role" "role" {
name = "%v"
}
resource "snowflake_role_grants" "grants" {
role_name = snowflake_role.role.name
roles = [
"ACCOUNTADMIN",
]
}
resource "snowflake_user_ownership_grant" "grant" {
on_user_name = snowflake_user.user.name
to_role_name = snowflake_role.role.name
current_grants = "COPY"
}
`, user, role)
}
Loading

0 comments on commit 6f3f09d

Please sign in to comment.