Skip to content

Commit

Permalink
add grant_database_role resource
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-swinkler committed Dec 21, 2023
1 parent b19fe8b commit 6d62617
Show file tree
Hide file tree
Showing 11 changed files with 549 additions and 8 deletions.
2 changes: 2 additions & 0 deletions examples/resources/snowflake_grant_database_role/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# format is database_role_name (string) | object_type (ROLE|DATABASE ROLE|SHARE) | grantee_name (string)
terraform import "\"ABC\".\"test_db_role\"|ROLE|\"test_parent_role\""
45 changes: 45 additions & 0 deletions examples/resources/snowflake_grant_database_role/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
##################################
### grant database role to account role
##################################

resource "snowflake_database_role" "database_role" {
database = var.database
name = var.database_role_name
}

resource "snowflake_role" "parent_role" {
name = var.parent_role_name
}

resource "snowflake_grant_database_role" "g" {
database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\""
parent_role_name = snowflake_role.parent_role.name
}

##################################
### grant database role to database role
##################################

resource "snowflake_database_role" "database_role" {
database = var.database
name = var.database_role_name
}

resource "snowflake_database_role" "parent_database_role" {
database = var.database
name = var.parent_database_role_name
}

resource "snowflake_grant_database_role" "g" {
database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\""
parent_database_role_name = "\"${var.database}\".\"${snowflake_database_role.parent_database_role.name}\""
}

##################################
### grant database role to share
##################################

resource "snowflake_grant_database_role" "g" {
database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\""
share_name = snowflake_share.share.name
}
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ func getResources() map[string]*schema.Resource {
"snowflake_failover_group": resources.FailoverGroup(),
"snowflake_file_format": resources.FileFormat(),
"snowflake_function": resources.Function(),
"snowflake_grant_database_role": resources.GrantDatabaseRole(),
"snowflake_grant_privileges_to_role": resources.GrantPrivilegesToRole(),
"snowflake_managed_account": resources.ManagedAccount(),
"snowflake_masking_policy": resources.MaskingPolicy(),
Expand Down
228 changes: 228 additions & 0 deletions pkg/resources/grant_database_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package resources

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

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

var grantDatabaseRoleSchema = map[string]*schema.Schema{
"database_role_name": {
Type: schema.TypeString,
Required: true,
Description: "The fully qualified name of the database role which will be granted to share or parent role.",
ForceNew: true,
ValidateDiagFunc: IsValidIdentifier[sdk.DatabaseObjectIdentifier](),
},
"parent_role_name": {
Type: schema.TypeString,
Optional: true,
Description: "The fully qualified name of the parent account role which will create a parent-child relationship between the roles.",
ForceNew: true,
ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](),
ExactlyOneOf: []string{
"parent_role_name",
"parent_database_role_name",
"share_name",
},
},
"parent_database_role_name": {
Type: schema.TypeString,
Optional: true,
Description: "The fully qualified name of the parent database role which will create a parent-child relationship between the roles. If the identifier is not fully qualified (in the form of <db_name>.≤database_role_name>), the command looks for the database role in the current database for the session.",
ForceNew: true,
ValidateDiagFunc: IsValidIdentifier[sdk.DatabaseObjectIdentifier](),
ExactlyOneOf: []string{
"parent_role_name",
"parent_database_role_name",
"share_name",
},
},
"share_name": {
Type: schema.TypeString,
Optional: true,
Description: "The fully qualified name of the share on which privileges will be granted.",
ForceNew: true,
ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](),
ExactlyOneOf: []string{
"parent_role_name",
"parent_database_role_name",
"share_name",
},
},
}

// DynamicTable returns a pointer to the resource representing a dynamic table.
func GrantDatabaseRole() *schema.Resource {
return &schema.Resource{
Create: CreateGrantDatabaseRole,
Read: ReadGrantDatabaseRole,
Delete: DeleteGrantDatabaseRole,
Schema: grantDatabaseRoleSchema,
Importer: &schema.ResourceImporter{
StateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(d.Id(), helpers.IDDelimiter)
if len(parts) != 3 {
return nil, fmt.Errorf("invalid ID specified: %v, expected <database_role_name>|<object_type>|<target_identifier>", d.Id())
}
if err := d.Set("database_role_name", parts[0]); err != nil {
return nil, err
}
switch parts[1] {
case "ROLE":
if err := d.Set("parent_role_name", parts[2]); err != nil {
return nil, err
}
case "DATABASE ROLE":
if err := d.Set("parent_database_role_name", parts[2]); err != nil {
return nil, err
}
case "SHARE":
if err := d.Set("share_name", parts[2]); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("invalid object type specified: %v, expected ROLE, DATABASE ROLE, or SHARE", parts[1])
}

return []*schema.ResourceData{d}, nil
},
},
}
}

// CreateGrantDatabaseRole implements schema.CreateFunc.
func CreateGrantDatabaseRole(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
client := sdk.NewClientFromDB(db)
ctx := context.Background()
databaseRoleName := d.Get("database_role_name").(string)
databaseRoleIdentifier := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(databaseRoleName)
// format of snowflakeResourceID is <database_role_identifier>|<object type>|<parent_role_name>
var snowflakeResourceID string
if parentRoleName, ok := d.GetOk("parent_role_name"); ok && parentRoleName.(string) != "" {
parentRoleIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(parentRoleName.(string))
snowflakeResourceID = helpers.EncodeSnowflakeID(databaseRoleIdentifier.FullyQualifiedName(), sdk.ObjectTypeRole.String(), parentRoleIdentifier.FullyQualifiedName())
req := sdk.NewGrantDatabaseRoleRequest(databaseRoleIdentifier).WithAccountRole(parentRoleIdentifier)
if err := client.DatabaseRoles.Grant(ctx, req); err != nil {
return err
}
} else if parentDatabaseRoleName, ok := d.GetOk("parent_database_role_name"); ok && parentDatabaseRoleName.(string) != "" {
parentRoleIdentifier := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(parentDatabaseRoleName.(string))
snowflakeResourceID = helpers.EncodeSnowflakeID(databaseRoleIdentifier.FullyQualifiedName(), sdk.ObjectTypeDatabaseRole.String(), parentRoleIdentifier.FullyQualifiedName())
req := sdk.NewGrantDatabaseRoleRequest(databaseRoleIdentifier).WithDatabaseRole(parentRoleIdentifier)
if err := client.DatabaseRoles.Grant(ctx, req); err != nil {
return err
}
} else if shareName, ok := d.GetOk("share_name"); ok && shareName.(string) != "" {
shareIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(shareName.(string))
snowflakeResourceID = helpers.EncodeSnowflakeID(databaseRoleIdentifier.FullyQualifiedName(), sdk.ObjectTypeShare.String(), shareIdentifier.FullyQualifiedName())
req := sdk.NewGrantDatabaseRoleToShareRequest(databaseRoleIdentifier, shareIdentifier)
if err := client.DatabaseRoles.GrantToShare(ctx, req); err != nil {
return err
}
}
d.SetId(snowflakeResourceID)
return ReadGrantDatabaseRole(d, meta)
}

// ReadGrantDatabaseRole implements schema.ReadFunc.
func ReadGrantDatabaseRole(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
client := sdk.NewClientFromDB(db)
parts := strings.Split(d.Id(), helpers.IDDelimiter)
databaseRoleName := parts[0]
databaseRoleIdentifier := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(databaseRoleName)
objectType := parts[1]
targetIdentifier := parts[2]
ctx := context.Background()
grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{
Of: &sdk.ShowGrantsOf{
DatabaseRole: databaseRoleIdentifier,
},
})
if err != nil {
log.Printf("[DEBUG] database role (%s) not found", databaseRoleIdentifier.FullyQualifiedName())
d.SetId("")
return nil
}

var found bool
for _, grant := range grants {
if grant.GrantedTo == sdk.ObjectType(objectType) {
if grant.GrantedTo == sdk.ObjectTypeRole || grant.GrantedTo == sdk.ObjectTypeShare {
if grant.GranteeName.FullyQualifiedName() == targetIdentifier {
found = true
break
}
} else {
/*
note that grantee_name is not saved as a valid identifier in the
SHOW GRANTS OF DATABASE ROLE <database_role_name> command
for example, "ABC"."test_parent_role" is saved as ABC."test_parent_role"
or "ABC"."test_parent_role" is saved as ABC.test_parent_role
and our internal mapper thereby fails to parse it correctly, returning "ABC."test_parent_role"
so this funny string replacement is needed to make it work
*/
s := grant.GranteeName.FullyQualifiedName()
if !strings.Contains(s, "\"") {
parts := strings.Split(s, ".")
s = sdk.NewDatabaseObjectIdentifier(parts[0], parts[1]).FullyQualifiedName()
} else {
parts := strings.Split(s, "\".\"")
if len(parts) < 2 {
parts = strings.Split(s, "\".")
if len(parts) < 2 {
parts = strings.Split(s, ".\"")
}
}
s = sdk.NewDatabaseObjectIdentifier(parts[0], parts[1]).FullyQualifiedName()
}
if s == targetIdentifier {
found = true
break
}
}
}
}
if !found {
log.Printf("[DEBUG] database role grant (%s) not found", d.Id())
d.SetId("")
}

return nil
}

// DeleteGrantDatabaseRole implements schema.DeleteFunc.
func DeleteGrantDatabaseRole(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
client := sdk.NewClientFromDB(db)

parts := strings.Split(d.Id(), "|")
id := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(parts[0])
objectType := parts[1]
granteeName := parts[2]
ctx := context.Background()
switch objectType {
case "ROLE":
if err := client.DatabaseRoles.Revoke(ctx, sdk.NewRevokeDatabaseRoleRequest(id).WithAccountRole(sdk.NewAccountObjectIdentifierFromFullyQualifiedName(granteeName))); err != nil {
return err
}
case "DATABASE ROLE":
if err := client.DatabaseRoles.Revoke(ctx, sdk.NewRevokeDatabaseRoleRequest(id).WithDatabaseRole(sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(granteeName))); err != nil {
return err
}
case "SHARE":
if err := client.DatabaseRoles.RevokeFromShare(ctx, sdk.NewRevokeDatabaseRoleFromShareRequest(id, sdk.NewAccountObjectIdentifierFromFullyQualifiedName(granteeName))); err != nil {
return err
}
}
d.SetId("")
return nil
}
Loading

0 comments on commit 6d62617

Please sign in to comment.