Skip to content

Commit

Permalink
feat: Grant ownership follow up (#2628)
Browse files Browse the repository at this point in the history
A follow-up for #2604. 

Done in this pr:
- Add setId("") in Read (when ownership is not found on the target
object) and forcefully grant ownership in Create (this was already
present, but added test cases for it).
- Edge cases
- Granting `ON PIPE` and `ON ALL PIPES` is handled (pipes are paused
before and resumed after ownership transfer)

Full list of things that still need to be done:
- Deprecation messages
- More documentation (explain how grant_ownership resource handles edge
cases) and examples that would show simple usage, edge cases, cases
where the resource may cause trouble
- Referring to
#2604 (comment),
test different cases where the Delete operation may struggle with
- Test outside of Terraform interactions to see how it behaves in
different situations
- A test where used role is not privileged enough to transfer ownership
- Also cases within Terraform to see how grant_ownership will act with
other grant resources within certain configurations
- Edge cases
  - Granting `ON TASK`
  - Use `VIEW` when granting on `MATERIALIZED VIEW`
  - Granting `ON EXTERNAL TABLES`

## References
[GRANT
OWNERSHIP](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership)

## Mentioned in
A list of issues requesting this resource: #2549 #2199 #2084 #1942 #1875
  • Loading branch information
sfc-gh-jcieslak authored Apr 3, 2024
1 parent f0018c6 commit d467e5b
Show file tree
Hide file tree
Showing 20 changed files with 1,379 additions and 6 deletions.
64 changes: 64 additions & 0 deletions docs/resources/grant_ownership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
page_title: "snowflake_grant_ownership Resource - terraform-provider-snowflake"
subcategory: ""
description: |-
---

# snowflake_grant_ownership (Resource)





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

### Required

- `on` (Block List, Min: 1, Max: 1) Configures which object(s) should transfer their ownership to the specified role. (see [below for nested schema](#nestedblock--on))

### Optional

- `account_role_name` (String) The fully qualified name of the account role to which privileges will be granted.
- `database_role_name` (String) The fully qualified name of the database role to which privileges will be granted.
- `outbound_privileges` (String) Specifies whether to remove or transfer all existing outbound privileges on the object when ownership is transferred to a new role. Available options are: REVOKE for removing existing privileges and COPY to transfer them with ownership. For more information head over to [Snowflake documentation](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership#optional-parameters).

### Read-Only

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

<a id="nestedblock--on"></a>
### Nested Schema for `on`

Optional:

- `all` (Block List, Max: 1) Configures the privilege to be granted on all objects in either a database or schema. (see [below for nested schema](#nestedblock--on--all))
- `future` (Block List, Max: 1) Configures the privilege to be granted on all objects in either a database or schema. (see [below for nested schema](#nestedblock--on--future))
- `object_name` (String) Specifies the identifier for the object on which you are transferring ownership.
- `object_type` (String) Specifies the type of object on which you are transferring ownership. Available values are: AGGREGATION POLICY | ALERT | AUTHENTICATION POLICY | COMPUTE POOL | DATABASE | DATABASE ROLE | DYNAMIC TABLE | EVENT TABLE | EXTERNAL TABLE | EXTERNAL VOLUME | FAILOVER GROUP | FILE FORMAT | FUNCTION | HYBRID TABLE | ICEBERG TABLE | IMAGE REPOSITORY | INTEGRATION | MATERIALIZED VIEW | NETWORK POLICY | NETWORK RULE | PACKAGES POLICY | PIPE | PROCEDURE | MASKING POLICY | PASSWORD POLICY | PROJECTION POLICY | REPLICATION GROUP | ROLE | ROW ACCESS POLICY | SCHEMA | SESSION POLICY | SECRET | SEQUENCE | STAGE | STREAM | TABLE | TAG | TASK | USER | VIEW | WAREHOUSE

<a id="nestedblock--on--all"></a>
### Nested Schema for `on.all`

Required:

- `object_type_plural` (String) Specifies the type of object in plural form on which you are transferring ownership. Available values are: AGGREGATION POLICIES | ALERTS | AUTHENTICATION POLICIES | COMPUTE POOLS | DATABASES | DATABASE ROLES | DYNAMIC TABLES | EVENT TABLES | EXTERNAL TABLES | EXTERNAL VOLUMES | FAILOVER GROUPS | FILE FORMATS | FUNCTIONS | HYBRID TABLES | ICEBERG TABLES | IMAGE REPOSITORIES | INTEGRATIONS | MATERIALIZED VIEWS | NETWORK POLICIES | NETWORK RULES | PACKAGES POLICIES | PIPES | PROCEDURES | MASKING POLICIES | PASSWORD POLICIES | PROJECTION POLICIES | REPLICATION GROUPS | ROLES | ROW ACCESS POLICIES | SCHEMAS | SESSION POLICIES | SECRETS | SEQUENCES | STAGES | STREAMS | TABLES | TAGS | TASKS | USERS | VIEWS | WAREHOUSES. For more information head over to [Snowflake documentation](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership#required-parameters).

Optional:

- `in_database` (String) The fully qualified name of the database.
- `in_schema` (String) The fully qualified name of the schema.


<a id="nestedblock--on--future"></a>
### Nested Schema for `on.future`

Required:

- `object_type_plural` (String) Specifies the type of object in plural form on which you are transferring ownership. Available values are: AGGREGATION POLICIES | ALERTS | AUTHENTICATION POLICIES | COMPUTE POOLS | DATABASES | DATABASE ROLES | DYNAMIC TABLES | EVENT TABLES | EXTERNAL TABLES | EXTERNAL VOLUMES | FAILOVER GROUPS | FILE FORMATS | FUNCTIONS | HYBRID TABLES | ICEBERG TABLES | IMAGE REPOSITORIES | INTEGRATIONS | MATERIALIZED VIEWS | NETWORK POLICIES | NETWORK RULES | PACKAGES POLICIES | PIPES | PROCEDURES | MASKING POLICIES | PASSWORD POLICIES | PROJECTION POLICIES | REPLICATION GROUPS | ROLES | ROW ACCESS POLICIES | SCHEMAS | SESSION POLICIES | SECRETS | SEQUENCES | STAGES | STREAMS | TABLES | TAGS | TASKS | USERS | VIEWS | WAREHOUSES. For more information head over to [Snowflake documentation](https://docs.snowflake.com/en/sql-reference/sql/grant-ownership#required-parameters).

Optional:

- `in_database` (String) The fully qualified name of the database.
- `in_schema` (String) The fully qualified name of the schema.
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ func getResources() map[string]*schema.Resource {
"snowflake_function": resources.Function(),
"snowflake_grant_account_role": resources.GrantAccountRole(),
"snowflake_grant_database_role": resources.GrantDatabaseRole(),
"snowflake_grant_ownership": resources.GrantOwnership(),
"snowflake_grant_privileges_to_role": resources.GrantPrivilegesToRole(),
"snowflake_grant_privileges_to_account_role": resources.GrantPrivilegesToAccountRole(),
"snowflake_grant_privileges_to_database_role": resources.GrantPrivilegesToDatabaseRole(),
Expand Down
8 changes: 5 additions & 3 deletions pkg/resources/grant_ownership.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,11 +383,13 @@ func ReadGrantOwnership(ctx context.Context, d *schema.ResourceData, meta any) d
}

if !ownershipFound {
d.SetId("")

return diag.Diagnostics{
diag.Diagnostic{
Severity: diag.Error,
Summary: "Couldn't find OWNERSHIP privilege on target object",
Detail: fmt.Sprintf("Id: %s", d.Id()),
Severity: diag.Warning,
Summary: "Couldn't find OWNERSHIP privilege on the target object. Marking resource as removed.",
Detail: fmt.Sprintf("Id: %s", id.String()),
},
}
}
Expand Down
246 changes: 246 additions & 0 deletions pkg/resources/grant_ownership_acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"strings"
"testing"

"github.com/stretchr/testify/assert"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider"
"github.com/hashicorp/terraform-plugin-testing/terraform"

Expand Down Expand Up @@ -577,6 +579,250 @@ func TestAcc_GrantOwnership_InvalidConfiguration_MultipleTargets(t *testing.T) {
})
}

func TestAcc_GrantOwnership_MoveOwnershipOutsideTerraform(t *testing.T) {
databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
databaseFullyQualifiedName := sdk.NewAccountObjectIdentifier(databaseName).FullyQualifiedName()

accountRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
accountRoleFullyQualifiedName := sdk.NewAccountObjectIdentifier(accountRoleName).FullyQualifiedName()

otherAccountRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))

configVariables := config.Variables{
"account_role_name": config.StringVariable(accountRoleName),
"other_account_role_name": config.StringVariable(otherAccountRoleName),
"database_name": config.StringVariable(databaseName),
}
resourceName := "snowflake_grant_ownership.test"

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() { acc.TestAccPreCheck(t) },
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
Steps: []resource.TestStep{
{
ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantOwnership/MoveResourceOwnershipOutsideTerraform"),
ConfigVariables: configVariables,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "account_role_name", accountRoleName),
resource.TestCheckResourceAttr(resourceName, "on.0.object_type", "DATABASE"),
resource.TestCheckResourceAttr(resourceName, "on.0.object_name", databaseName),
resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("ToAccountRole|%s||OnObject|DATABASE|%s", accountRoleFullyQualifiedName, databaseFullyQualifiedName)),
),
},
{
PreConfig: func() {
moveResourceOwnershipToAccountRole(t, sdk.ObjectTypeDatabase, sdk.NewAccountObjectIdentifier(databaseName), sdk.NewAccountObjectIdentifier(otherAccountRoleName))
},
ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantOwnership/MoveResourceOwnershipOutsideTerraform"),
ConfigVariables: configVariables,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "account_role_name", accountRoleName),
resource.TestCheckResourceAttr(resourceName, "on.0.object_type", "DATABASE"),
resource.TestCheckResourceAttr(resourceName, "on.0.object_name", databaseName),
resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("ToAccountRole|%s||OnObject|DATABASE|%s", accountRoleFullyQualifiedName, databaseFullyQualifiedName)),
checkResourceOwnershipIsGranted(&sdk.ShowGrantOptions{
On: &sdk.ShowGrantsOn{
Object: &sdk.Object{
ObjectType: sdk.ObjectTypeDatabase,
Name: sdk.NewAccountObjectIdentifierFromFullyQualifiedName(databaseFullyQualifiedName),
},
},
}, sdk.ObjectTypeDatabase, accountRoleName, databaseName),
),
},
},
})
}

func TestAcc_GrantOwnership_ForceOwnershipTransferOnCreate(t *testing.T) {
databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
databaseFullyQualifiedName := sdk.NewAccountObjectIdentifier(databaseName).FullyQualifiedName()

accountRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
newDatabaseOwningAccountRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))

configVariables := config.Variables{
"account_role_name": config.StringVariable(newDatabaseOwningAccountRoleName),
"database_name": config.StringVariable(databaseName),
}
resourceName := "snowflake_grant_ownership.test"

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() { acc.TestAccPreCheck(t) },
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
Steps: []resource.TestStep{
{
PreConfig: func() {
createAccountRoleOutsideTerraform(t, accountRoleName)
registerAccountRoleCleanup(t, accountRoleName)
createAccountRoleOutsideTerraform(t, newDatabaseOwningAccountRoleName)
registerAccountRoleCleanup(t, newDatabaseOwningAccountRoleName)
t.Cleanup(createDatabaseWithRoleAsOwner(t, accountRoleName, databaseName))
},
ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantOwnership/ForceOwnershipTransferOnCreate"),
ConfigVariables: configVariables,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "account_role_name", newDatabaseOwningAccountRoleName),
resource.TestCheckResourceAttr(resourceName, "on.0.object_type", "DATABASE"),
resource.TestCheckResourceAttr(resourceName, "on.0.object_name", databaseName),
resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("ToAccountRole|\"%s\"||OnObject|DATABASE|%s", newDatabaseOwningAccountRoleName, databaseFullyQualifiedName)),
),
},
},
})
}

func TestAcc_GrantOwnership_OnPipe(t *testing.T) {
accountRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
stageName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
tableName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
pipeName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
accountRoleFullyQualifiedName := sdk.NewAccountObjectIdentifier(accountRoleName).FullyQualifiedName()
pipeFullyQualifiedName := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, pipeName).FullyQualifiedName()

configVariables := config.Variables{
"account_role_name": config.StringVariable(accountRoleName),
"database": config.StringVariable(acc.TestDatabaseName),
"schema": config.StringVariable(acc.TestSchemaName),
"stage": config.StringVariable(stageName),
"table": config.StringVariable(tableName),
"pipe": config.StringVariable(pipeName),
}
resourceName := "snowflake_grant_ownership.test"

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() { acc.TestAccPreCheck(t) },
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
Steps: []resource.TestStep{
{
ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantOwnership/OnPipe"),
ConfigVariables: configVariables,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "account_role_name", accountRoleName),
resource.TestCheckResourceAttr(resourceName, "on.0.object_type", sdk.ObjectTypePipe.String()),
resource.TestCheckResourceAttr(resourceName, "on.0.object_name", pipeFullyQualifiedName),
resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("ToAccountRole|%s||OnObject|PIPE|%s", accountRoleFullyQualifiedName, pipeFullyQualifiedName)),
checkResourceOwnershipIsGranted(&sdk.ShowGrantOptions{
On: &sdk.ShowGrantsOn{
Object: &sdk.Object{
ObjectType: sdk.ObjectTypePipe,
Name: sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(pipeFullyQualifiedName),
},
},
// TODO(SNOW-999049): Fix this identifier
}, sdk.ObjectTypePipe, accountRoleName, fmt.Sprintf("%s\".\"%s\".%s", acc.TestDatabaseName, acc.TestSchemaName, pipeName)),
),
},
},
})
}

func TestAcc_GrantOwnership_OnAllPipes(t *testing.T) {
accountRoleName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
stageName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
tableName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
pipeName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
secondPipeName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
accountRoleFullyQualifiedName := sdk.NewAccountObjectIdentifier(accountRoleName).FullyQualifiedName()
schemaFullyQualifiedName := sdk.NewDatabaseObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName).FullyQualifiedName()

configVariables := config.Variables{
"account_role_name": config.StringVariable(accountRoleName),
"database": config.StringVariable(acc.TestDatabaseName),
"schema": config.StringVariable(acc.TestSchemaName),
"stage": config.StringVariable(stageName),
"table": config.StringVariable(tableName),
"pipe": config.StringVariable(pipeName),
"second_pipe": config.StringVariable(secondPipeName),
}
resourceName := "snowflake_grant_ownership.test"

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() { acc.TestAccPreCheck(t) },
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
Steps: []resource.TestStep{
{
ConfigDirectory: acc.ConfigurationDirectory("TestAcc_GrantOwnership/OnAllPipes"),
ConfigVariables: configVariables,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "account_role_name", accountRoleName),
resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("ToAccountRole|%s||OnAll|PIPES|InSchema|%s", accountRoleFullyQualifiedName, schemaFullyQualifiedName)),
checkResourceOwnershipIsGranted(&sdk.ShowGrantOptions{
To: &sdk.ShowGrantsTo{
Role: sdk.NewAccountObjectIdentifier(accountRoleName),
},
// TODO(SNOW-999049): Fix this identifier
}, sdk.ObjectTypePipe, accountRoleName, fmt.Sprintf("%s\".\"%s\".%s", acc.TestDatabaseName, acc.TestSchemaName, pipeName), fmt.Sprintf("%s\".\"%s\".%s", acc.TestDatabaseName, acc.TestSchemaName, secondPipeName)),
),
},
},
})
}

func createDatabaseWithRoleAsOwner(t *testing.T, roleName string, databaseName string) func() {
t.Helper()
client, err := sdk.NewDefaultClient()
assert.NoError(t, err)

ctx := context.Background()
databaseId := sdk.NewAccountObjectIdentifier(databaseName)
assert.NoError(t, client.Databases.Create(ctx, databaseId, &sdk.CreateDatabaseOptions{}))

err = client.Grants.GrantOwnership(
ctx,
sdk.OwnershipGrantOn{
Object: &sdk.Object{
ObjectType: sdk.ObjectTypeDatabase,
Name: databaseId,
},
},
sdk.OwnershipGrantTo{
AccountRoleName: sdk.Pointer(sdk.NewAccountObjectIdentifier(roleName)),
},
new(sdk.GrantOwnershipOptions),
)
assert.NoError(t, err)

return func() {
assert.NoError(t, client.Databases.Drop(ctx, databaseId, &sdk.DropDatabaseOptions{}))
}
}

func moveResourceOwnershipToAccountRole(t *testing.T, objectType sdk.ObjectType, objectName sdk.ObjectIdentifier, accountRoleName sdk.AccountObjectIdentifier) {
t.Helper()

client, err := sdk.NewDefaultClient()
assert.NoError(t, err)

ctx := context.Background()
err = client.Grants.GrantOwnership(
ctx,
sdk.OwnershipGrantOn{
Object: &sdk.Object{
ObjectType: objectType,
Name: objectName,
},
},
sdk.OwnershipGrantTo{
AccountRoleName: &accountRoleName,
},
new(sdk.GrantOwnershipOptions),
)
assert.NoError(t, err)
}

func checkResourceOwnershipIsGranted(opts *sdk.ShowGrantOptions, grantOn sdk.ObjectType, roleName string, objectNames ...string) func(s *terraform.State) error {
return func(s *terraform.State) error {
client := acc.TestAccProvider.Meta().(*provider.Context).Client
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "snowflake_grant_ownership" "test" {
account_role_name = var.account_role_name
on {
object_type = "DATABASE"
object_name = var.database_name
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
variable "account_role_name" {
type = string
}

variable "database_name" {
type = string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
resource "snowflake_role" "test" {
name = var.account_role_name
}

resource "snowflake_role" "other_role" {
name = var.other_account_role_name
}

resource "snowflake_database" "test" {
name = var.database_name
}

resource "snowflake_grant_ownership" "test" {
account_role_name = snowflake_role.test.name
on {
object_type = "DATABASE"
object_name = snowflake_database.test.name
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
variable "account_role_name" {
type = string
}

variable "other_account_role_name" {
type = string
}

variable "database_name" {
type = string
}
Loading

0 comments on commit d467e5b

Please sign in to comment.