Skip to content

Commit

Permalink
feat: Create a snowflake_user_grant resource. (#1193)
Browse files Browse the repository at this point in the history
* feat: snowflake_user_grant resource

* fix user_grant_acceptance_test

* fix acceptance test test and docs

Co-authored-by: hleb.lizunkou <Hleb.Lizunkou@itechart-group.com>
  • Loading branch information
hleb-lizunkou1 and Hleb-Lizunkou committed Sep 20, 2022
1 parent ea01e66 commit 37500ac
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 0 deletions.
53 changes: 53 additions & 0 deletions docs/resources/user_grant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "snowflake_user_grant Resource - terraform-provider-snowflake"
subcategory: ""
description: |-
---

# snowflake_user_grant (Resource)



## Example Usage

```terraform
resource snowflake_user_grant grant {
user_name = "user"
privilege = "MONITOR"
roles = [
"role1",
]
with_grant_option = false
}
```

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

### Required

- `privilege` (String) The privilege to grant on the user.
- `user_name` (String) The name of the user on which to grant privileges.

### Optional

- `enable_multiple_grants` (Boolean) When this is set to true, multiple grants of the same type can be created. This will cause Terraform to not revoke grants applied to roles and objects outside Terraform.
- `roles` (Set of String) Grants privilege to these roles.
- `with_grant_option` (Boolean) When this is set to true, allows the recipient role to grant the privileges to other roles.

### Read-Only

- `id` (String) The ID of this resource.

## Import

Import is supported using the following syntax:

```shell
# format is user name | | | privilege | true/false for with_grant_option
terraform import snowflake_user_grant.example 'userName|||MONITOR|true'
```
2 changes: 2 additions & 0 deletions examples/resources/snowflake_user_grant/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# format is user name | | | privilege | true/false for with_grant_option
terraform import snowflake_user_grant.example 'userName|||MONITOR|true'
10 changes: 10 additions & 0 deletions examples/resources/snowflake_user_grant/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
resource snowflake_user_grant grant {
user_name = "user"
privilege = "MONITOR"

roles = [
"role1",
]

with_grant_option = false
}
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ func GetGrantResources() resources.TerraformGrantResources {
"snowflake_task_grant": resources.TaskGrant(),
"snowflake_view_grant": resources.ViewGrant(),
"snowflake_warehouse_grant": resources.WarehouseGrant(),
"snowflake_user_grant": resources.UserGrant(),
}
return grants
}
Expand Down
178 changes: 178 additions & 0 deletions pkg/resources/user_grant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package resources

import (
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

var validUserPrivileges = NewPrivilegeSet(
privilegeMonitor,
privilegeOwnership,
)
var userGrantSchema = map[string]*schema.Schema{
"user_name": {
Type: schema.TypeString,
Required: true,
Description: "The name of the user on which to grant privileges.",
ForceNew: true,
},
"privilege": {
Type: schema.TypeString,
Required: true,
Description: "The privilege to grant on the user.",
ForceNew: true,
ValidateFunc: validation.StringInSlice(validUserPrivileges.ToList(), true),
},
"roles": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Description: "Grants privilege to these roles.",
},
"with_grant_option": {
Type: schema.TypeBool,
Optional: true,
Description: "When this is set to true, allows the recipient role to grant the privileges to other roles.",
Default: false,
ForceNew: true,
},
"enable_multiple_grants": {
Type: schema.TypeBool,
Optional: true,
Description: "When this is set to true, multiple grants of the same type can be created. This will cause Terraform to not revoke grants applied to roles and objects outside Terraform.",
Default: false,
},
}

// UserGrant returns a pointer to the resource representing a user grant
func UserGrant() *TerraformGrantResource {
return &TerraformGrantResource{
Resource: &schema.Resource{
Create: CreateUserGrant,
Read: ReadUserGrant,
Delete: DeleteUserGrant,
Update: UpdateUserGrant,

Schema: userGrantSchema,
// FIXME - tests for this don't currently work
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
},
ValidPrivs: validUserPrivileges,
}
}

// CreateUserGrant implements schema.CreateFunc
func CreateUserGrant(d *schema.ResourceData, meta interface{}) error {
w := d.Get("user_name").(string)
priv := d.Get("privilege").(string)
grantOption := d.Get("with_grant_option").(bool)
builder := snowflake.UserGrant(w)
roles := expandStringList(d.Get("roles").(*schema.Set).List())

err := createGenericGrant(d, meta, builder)
if err != nil {
return err
}

grant := &grantID{
ResourceName: w,
Privilege: priv,
GrantOption: grantOption,
Roles: roles,
}
dataIDInput, err := grant.String()
if err != nil {
return err
}
d.SetId(dataIDInput)

return ReadUserGrant(d, meta)
}

// ReadUserGrant implements schema.ReadFunc
func ReadUserGrant(d *schema.ResourceData, meta interface{}) error {
grantID, err := grantIDFromString(d.Id())
if err != nil {
return err
}
w := grantID.ResourceName
priv := grantID.Privilege

err = d.Set("user_name", w)
if err != nil {
return err
}
err = d.Set("privilege", priv)
if err != nil {
return err
}
err = d.Set("with_grant_option", grantID.GrantOption)
if err != nil {
return err
}

builder := snowflake.UserGrant(w)

return readGenericGrant(d, meta, userGrantSchema, builder, false, validUserPrivileges)
}

// DeleteUserGrant implements schema.DeleteFunc
func DeleteUserGrant(d *schema.ResourceData, meta interface{}) error {
grantID, err := grantIDFromString(d.Id())
if err != nil {
return err
}
w := grantID.ResourceName

builder := snowflake.UserGrant(w)

return deleteGenericGrant(d, meta, builder)
}

// UpdateUserGrant implements schema.UpdateFunc
func UpdateUserGrant(d *schema.ResourceData, meta interface{}) error {
// for now the only thing we can update is roles. if nothing changed,
// nothing to update and we're done.
if !d.HasChanges("roles") {
return nil
}

rolesToAdd, rolesToRevoke := changeDiff(d, "roles")

grantID, err := grantIDFromString(d.Id())
if err != nil {
return err
}

// create the builder
builder := snowflake.UserGrant(grantID.ResourceName)

// first revoke
if err := deleteGenericGrantRolesAndShares(
meta,
builder,
grantID.Privilege,
rolesToRevoke,
nil,
); err != nil {
return err
}

// then add
if err := createGenericGrantRolesAndShares(
meta,
builder,
grantID.Privilege,
grantID.GrantOption,
rolesToAdd,
nil,
); err != nil {
return err
}

// Done, refresh state
return ReadUserGrant(d, meta)
}
61 changes: 61 additions & 0 deletions pkg/resources/user_grant_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package resources_test

import (
"fmt"
"os"
"strings"
"testing"

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

func TestAcc_UserGrant(t *testing.T) {
if _, ok := os.LookupEnv("SKIP_USER_GRANT_TESTS"); ok {
t.Skip("Skipping TestAccUserGrant")
}
wName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
roleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))

resource.ParallelTest(t, resource.TestCase{
Providers: providers(),
CheckDestroy: nil,
Steps: []resource.TestStep{
{
Config: userGrantConfig(wName, roleName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("snowflake_user_grant.test", "user_name", wName),
resource.TestCheckResourceAttr("snowflake_user_grant.test", "privilege", "MONITOR"),
),
},
// IMPORT
{
ResourceName: "snowflake_user_grant.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"enable_multiple_grants", // feature flag attribute not defined in Snowflake, can't be imported
},
},
},
})
}

func userGrantConfig(n, role string) string {
return fmt.Sprintf(`
resource "snowflake_user" "test" {
name = "%v"
}
resource "snowflake_role" "test" {
name = "%v"
}
resource "snowflake_user_grant" "test" {
user_name = snowflake_user.test.name
roles = [snowflake_role.test.name]
privilege = "MONITOR"
}
`, n, role)
}
51 changes: 51 additions & 0 deletions pkg/resources/user_grant_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package resources_test

import (
"database/sql"
"testing"
"time"

sqlmock "github.com/DATA-DOG/go-sqlmock"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources"
. "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/testhelpers"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/stretchr/testify/require"
)

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

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

in := map[string]interface{}{
"user_name": "test-user",
"privilege": "MONITOR",
"roles": []interface{}{"test-role-1", "test-role-2"},
}
d := schema.TestResourceDataRaw(t, resources.UserGrant().Resource.Schema, in)
r.NotNil(d)

WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
mock.ExpectExec(`^GRANT MONITOR ON USER "test-user" TO ROLE "test-role-1"$`).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(`^GRANT MONITOR ON USER "test-user" TO ROLE "test-role-2"$`).WillReturnResult(sqlmock.NewResult(1, 1))
expectReadUserGrant(mock)
err := resources.CreateUserGrant(d, db)
r.NoError(err)
})
}

func expectReadUserGrant(mock sqlmock.Sqlmock) {
rows := sqlmock.NewRows([]string{
"created_on", "privilege", "granted_on", "name", "granted_to", "grantee_name", "grant_option", "granted_by",
}).AddRow(
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), "MONITOR", "USER", "test-user", "ROLE", "test-role-1", false, "bob",
).AddRow(
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), "MONITOR", "USER", "test-user", "ROLE", "test-role-2", false, "bob",
)
mock.ExpectQuery(`^SHOW GRANTS ON USER "test-user"$`).WillReturnRows(rows)
}
10 changes: 10 additions & 0 deletions pkg/snowflake/grant.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
taskType grantType = "TASK"
rowAccessPolicyType grantType = "ROW ACCESS POLICY"
tagType grantType = "TAG"
userGrantType grantType = "USER"
)

type GrantExecutable interface {
Expand Down Expand Up @@ -203,6 +204,15 @@ func WarehouseGrant(w string) GrantBuilder {
}
}

// UserGrant returns a pointer to a CurrentGrantBuilder for a user
func UserGrant(w string) GrantBuilder {
return &CurrentGrantBuilder{
name: w,
qualifiedName: fmt.Sprintf(`"%v"`, w),
grantType: userGrantType,
}
}

// ExternalTableGrant returns a pointer to a CurrentGrantBuilder for an external table
func ExternalTableGrant(db, schema, externalTable string) GrantBuilder {
return &CurrentGrantBuilder{
Expand Down

0 comments on commit 37500ac

Please sign in to comment.