Skip to content

Commit

Permalink
[feature] account and account object grants (#154)
Browse files Browse the repository at this point in the history
Introduces `GRANT`s for:

- `ACCOUNT`
- `INTEGRATION`
- `RESOURCE MONITOR`
  • Loading branch information
zpencerq committed Mar 26, 2020
1 parent 4346dee commit 445f352
Show file tree
Hide file tree
Showing 13 changed files with 712 additions and 19 deletions.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ You can see a number of examples [here](examples).

<!-- START -->

### snowflake_account_grant

**Note**: The snowflake_account_grant resource creates exclusive attachments of grants.
Across the entire Snowflake account, all of the accounts to which a single grant is attached must be declared
by a single snowflake_account_grant resource. This means that even any snowflake_account that have the attached
grant via any other mechanism (including other Terraform resources) will have that attached grant revoked by this resource.
These resources do not enforce exclusive attachment of a grant, it is the user's responsibility to enforce this.

#### properties

| NAME | TYPE | DESCRIPTION | OPTIONAL | REQUIRED | COMPUTED | DEFAULT |
|-----------|--------|---------------------------------------|----------|-----------|----------|---------|
| privilege | string | The privilege to grant on the schema. | true | false | false | "USAGE" |
| roles | set | Grants privilege to these roles. | true | false | false | <nil> |

### snowflake_database

#### properties
Expand Down Expand Up @@ -97,6 +112,22 @@ These resources do not enforce exclusive attachment of a grant, it is the user's
| roles | set | Grants privilege to these roles. | true | false | false | <nil> |
| shares | set | Grants privilege to these shares. | true | false | false | <nil> |

### snowflake_integration_grant

**Note**: The snowflake_integration_grant resource creates exclusive attachments of grants.
Across the entire Snowflake account, all of the integrations to which a single grant is attached must be declared
by a single snowflake_integration_grant resource. This means that even any snowflake_integration that have the attached
grant via any other mechanism (including other Terraform resources) will have that attached grant revoked by this resource.
These resources do not enforce exclusive attachment of a grant, it is the user's responsibility to enforce this.

#### properties

| NAME | TYPE | DESCRIPTION | OPTIONAL | REQUIRED | COMPUTED | DEFAULT |
|------------------|--------|------------------------------------------------------------------|----------|-----------|----------|---------|
| integration_name | string | Identifier for the integration; must be unique for your account. | false | true | false | <nil> |
| privilege | string | The privilege to grant on the integration. | true | false | false | "USAGE" |
| roles | set | Grants privilege to these roles. | true | false | false | <nil> |

### snowflake_managed_account

#### properties
Expand Down Expand Up @@ -144,6 +175,22 @@ These resources do not enforce exclusive attachment of a grant, it is the user's
| suspend_immediate_triggers | set | A list of percentage thresholds at which to immediately suspend all warehouses. | true | false | false | <nil> |
| suspend_triggers | set | A list of percentage thresholds at which to suspend all warehouses. | true | false | false | <nil> |

### snowflake_resource_monitor_grant

**Note**: The snowflake_resource_monitor_grant resource creates exclusive attachments of grants.
Across the entire Snowflake account, all of the resource_monitors to which a single grant is attached must be declared
by a single snowflake_resource_monitor_grant resource. This means that even any snowflake_resource_monitor that have the attached
grant via any other mechanism (including other Terraform resources) will have that attached grant revoked by this resource.
These resources do not enforce exclusive attachment of a grant, it is the user's responsibility to enforce this.

#### properties

| NAME | TYPE | DESCRIPTION | OPTIONAL | REQUIRED | COMPUTED | DEFAULT |
|--------------|--------|-----------------------------------------------------------------------|----------|-----------|----------|-----------|
| monitor_name | string | Identifier for the resource monitor; must be unique for your account. | false | true | false | <nil> |
| privilege | string | The privilege to grant on the resource monitor. | true | false | false | "MONITOR" |
| roles | set | Grants privilege to these roles. | true | false | false | <nil> |

### snowflake_role

#### properties
Expand Down
41 changes: 22 additions & 19 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,25 +60,28 @@ func Provider() *schema.Provider {
},
},
ResourcesMap: map[string]*schema.Resource{
"snowflake_database": resources.Database(),
"snowflake_database_grant": resources.DatabaseGrant(),
"snowflake_managed_account": resources.ManagedAccount(),
"snowflake_pipe": resources.Pipe(),
"snowflake_resource_monitor": resources.ResourceMonitor(),
"snowflake_role": resources.Role(),
"snowflake_role_grants": resources.RoleGrants(),
"snowflake_schema": resources.Schema(),
"snowflake_schema_grant": resources.SchemaGrant(),
"snowflake_share": resources.Share(),
"snowflake_stage": resources.Stage(),
"snowflake_stage_grant": resources.StageGrant(),
"snowflake_storage_integration": resources.StorageIntegration(),
"snowflake_user": resources.User(),
"snowflake_view": resources.View(),
"snowflake_view_grant": resources.ViewGrant(),
"snowflake_table_grant": resources.TableGrant(),
"snowflake_warehouse": resources.Warehouse(),
"snowflake_warehouse_grant": resources.WarehouseGrant(),
"snowflake_account_grant": resources.AccountGrant(),
"snowflake_database": resources.Database(),
"snowflake_database_grant": resources.DatabaseGrant(),
"snowflake_integration_grant": resources.IntegrationGrant(),
"snowflake_managed_account": resources.ManagedAccount(),
"snowflake_pipe": resources.Pipe(),
"snowflake_resource_monitor": resources.ResourceMonitor(),
"snowflake_resource_monitor_grant": resources.ResourceMonitorGrant(),
"snowflake_role": resources.Role(),
"snowflake_role_grants": resources.RoleGrants(),
"snowflake_schema": resources.Schema(),
"snowflake_schema_grant": resources.SchemaGrant(),
"snowflake_share": resources.Share(),
"snowflake_stage": resources.Stage(),
"snowflake_stage_grant": resources.StageGrant(),
"snowflake_storage_integration": resources.StorageIntegration(),
"snowflake_user": resources.User(),
"snowflake_view": resources.View(),
"snowflake_view_grant": resources.ViewGrant(),
"snowflake_table_grant": resources.TableGrant(),
"snowflake_warehouse": resources.Warehouse(),
"snowflake_warehouse_grant": resources.WarehouseGrant(),
},
DataSourcesMap: map[string]*schema.Resource{},
ConfigureFunc: ConfigureProvider,
Expand Down
94 changes: 94 additions & 0 deletions pkg/resources/account_grant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package resources

import (
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"

"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
)

var validAccountPrivileges = newPrivilegeSet(
privilegeCreateRole,
privilegeCreateUser,
privilegeCreateWarehouse,
privilegeCreateDatabase,
privilegeCreateIntegration,
privilegeManageGrants,
privilegeMonitorUsage,
)

var accountGrantSchema = map[string]*schema.Schema{
"privilege": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The privilege to grant on the schema.",
Default: "USAGE",
ValidateFunc: validation.StringInSlice(validAccountPrivileges.toList(), true),
ForceNew: true,
},
"roles": &schema.Schema{
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Description: "Grants privilege to these roles.",
ForceNew: true,
},
}

// ViewGrant returns a pointer to the resource representing a view grant
func AccountGrant() *schema.Resource {
return &schema.Resource{
Create: CreateAccountGrant,
Read: ReadAccountGrant,
Delete: DeleteAccountGrant,

Schema: accountGrantSchema,
}
}

// CreateAccountGrant implements schema.CreateFunc
func CreateAccountGrant(data *schema.ResourceData, meta interface{}) error {
priv := data.Get("privilege").(string)

builder := snowflake.AccountGrant()

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

grantID := &grantID{
ResourceName: "ACCOUNT",
Privilege: priv,
}
dataIDInput, err := grantID.String()
if err != nil {
return err
}
data.SetId(dataIDInput)

return ReadAccountGrant(data, meta)
}

// ReadAccountGrant implements schema.ReadFunc
func ReadAccountGrant(data *schema.ResourceData, meta interface{}) error {
grantID, err := grantIDFromString(data.Id())
if err != nil {
return err
}
err = data.Set("privilege", grantID.Privilege)
if err != nil {
return err
}

builder := snowflake.AccountGrant()

return readGenericGrant(data, meta, builder, false, validAccountPrivileges)
}

// DeleteAccountGrant implements schema.DeleteFunc
func DeleteAccountGrant(data *schema.ResourceData, meta interface{}) error {
builder := snowflake.AccountGrant()

return deleteGenericGrant(data, meta, builder)
}
70 changes: 70 additions & 0 deletions pkg/resources/account_grant_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package resources_test

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

sqlmock "github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"

"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/provider"
"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/resources"
. "github.com/chanzuckerberg/terraform-provider-snowflake/pkg/testhelpers"
)

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

func TestAccountGrantCreate(t *testing.T) {
a := assert.New(t)

in := map[string]interface{}{
"privilege": "CREATE DATABASE",
"roles": []interface{}{"test-role-1", "test-role-2"},
}
d := schema.TestResourceDataRaw(t, resources.AccountGrant().Schema, in)
a.NotNil(d)

WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
mock.ExpectExec(`^GRANT CREATE DATABASE ON ACCOUNT TO ROLE "test-role-1"$`).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(`^GRANT CREATE DATABASE ON ACCOUNT TO ROLE "test-role-2"$`).WillReturnResult(sqlmock.NewResult(1, 1))
expectReadAccountGrant(mock)
err := resources.CreateAccountGrant(d, db)
a.NoError(err)
})
}

func TestAccountGrantRead(t *testing.T) {
a := assert.New(t)

d := accountGrant(t, "ACCOUNT|||MANAGE GRANTS", map[string]interface{}{
"privilege": "MANAGE GRANTS",
"roles": []interface{}{"test-role-1", "test-role-2"},
})

a.NotNil(d)

WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
expectReadAccountGrant(mock)
err := resources.ReadAccountGrant(d, db)
a.NoError(err)
})
}

func expectReadAccountGrant(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), "MANAGE GRANTS", "ACCOUNT", "", "ROLE", "test-role-1", false, "bob",
).AddRow(
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), "MANAGE GRANTS", "ACCOUNT", "", "ROLE", "test-role-2", false, "bob",
)
mock.ExpectQuery(`^SHOW GRANTS ON ACCOUNT$`).WillReturnRows(rows)
}
7 changes: 7 additions & 0 deletions pkg/resources/grant_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,13 @@ func readGenericCurrentGrants(db *sql.DB, builder snowflake.GrantBuilder) ([]*gr
if err != nil {
return nil, err
}
if currentGrant.GrantedBy == "" {
// If GrantedBy is empty string, terraform can't
// manage the grant because the grant is a default
// grant seeded by Snowflake.
continue
}

grant := &grant{
CreatedOn: currentGrant.CreatedOn,
Privilege: currentGrant.Privilege,
Expand Down
24 changes: 24 additions & 0 deletions pkg/resources/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,30 @@ func databaseGrant(t *testing.T, id string, params map[string]interface{}) *sche
return d
}

func resourceMonitorGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
a := assert.New(t)
d := schema.TestResourceDataRaw(t, resources.ResourceMonitorGrant().Schema, params)
a.NotNil(d)
d.SetId(id)
return d
}

func integrationGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
a := assert.New(t)
d := schema.TestResourceDataRaw(t, resources.IntegrationGrant().Schema, params)
a.NotNil(d)
d.SetId(id)
return d
}

func accountGrant(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
a := assert.New(t)
d := schema.TestResourceDataRaw(t, resources.AccountGrant().Schema, params)
a.NotNil(d)
d.SetId(id)
return d
}

func fixture(name string) (string, error) {
b, err := ioutil.ReadFile(filepath.Join("testdata", name))
return string(b), err
Expand Down
Loading

0 comments on commit 445f352

Please sign in to comment.