From 717289f71fd4a08f44d4f20f6e16dc4dded77803 Mon Sep 17 00:00:00 2001 From: Artur Sawicki Date: Thu, 21 Sep 2023 16:57:52 +0200 Subject: [PATCH] feat: Add grant privileges to database role to SDK (#2023) * Extract grants validations and fix them * Extract grants impl * Adjust grants to interface * Use helper methods for grants * Add grant privileges to database role (WIP) * Add validations to grant privileges to database role (WIP) * Add implementation to grant privileges to database role (WIP) * Use common assertions in grants tests * Add unit tests for grant privileges to database role (WIP) * Test granting privileges to database role * Search for privilege in tests * Revoke grant from database role * Assert database privilege * Add validation tests to grants for database role * Refactor validation tests for grants * Add validation tests for revoking database role privileges * Fix linter comment * Fix test * Fix after review --- pkg/sdk/grants.go | 443 +++++------------------ pkg/sdk/grants_impl.go | 80 +++++ pkg/sdk/grants_integration_test.go | 208 ++++++++++- pkg/sdk/grants_test.go | 558 +++++++++++++++++++++-------- pkg/sdk/grants_validations.go | 279 +++++++++++++++ pkg/sdk/helper_test.go | 23 ++ 6 files changed, 1097 insertions(+), 494 deletions(-) create mode 100644 pkg/sdk/grants_impl.go create mode 100644 pkg/sdk/grants_validations.go diff --git a/pkg/sdk/grants.go b/pkg/sdk/grants.go index 8739d5b162..7710e315bb 100644 --- a/pkg/sdk/grants.go +++ b/pkg/sdk/grants.go @@ -2,91 +2,23 @@ package sdk import ( "context" - "fmt" "strings" "time" ) -var ( - _ validatable = new(GrantPrivilegesToAccountRoleOptions) - _ validatable = new(RevokePrivilegesFromAccountRoleOptions) - _ validatable = new(grantPrivilegeToShareOptions) - _ validatable = new(revokePrivilegeFromShareOptions) - _ validatable = new(ShowGrantOptions) -) +var _ convertibleRow[Grant] = new(grantRow) type Grants interface { GrantPrivilegesToAccountRole(ctx context.Context, privileges *AccountRoleGrantPrivileges, on *AccountRoleGrantOn, role AccountObjectIdentifier, opts *GrantPrivilegesToAccountRoleOptions) error RevokePrivilegesFromAccountRole(ctx context.Context, privileges *AccountRoleGrantPrivileges, on *AccountRoleGrantOn, role AccountObjectIdentifier, opts *RevokePrivilegesFromAccountRoleOptions) error - // todo: GrantPrivilegesToDatabaseRole and RevokePrivilegesFromDatabaseRole + GrantPrivilegesToDatabaseRole(ctx context.Context, privileges *DatabaseRoleGrantPrivileges, on *DatabaseRoleGrantOn, role DatabaseObjectIdentifier, opts *GrantPrivilegesToDatabaseRoleOptions) error + RevokePrivilegesFromDatabaseRole(ctx context.Context, privileges *DatabaseRoleGrantPrivileges, on *DatabaseRoleGrantOn, role DatabaseObjectIdentifier, opts *RevokePrivilegesFromDatabaseRoleOptions) error GrantPrivilegeToShare(ctx context.Context, privilege ObjectPrivilege, on *GrantPrivilegeToShareOn, to AccountObjectIdentifier) error RevokePrivilegeFromShare(ctx context.Context, privilege ObjectPrivilege, on *RevokePrivilegeFromShareOn, from AccountObjectIdentifier) error - Show(ctx context.Context, opts *ShowGrantOptions) ([]*Grant, error) -} - -var _ Grants = (*grants)(nil) - -type grants struct { - client *Client -} - -type Grant struct { - CreatedOn time.Time - Privilege string - GrantedOn ObjectType - GrantOn ObjectType - Name ObjectIdentifier - GrantedTo ObjectType - GranteeName AccountObjectIdentifier - GrantOption bool - GrantedBy AccountObjectIdentifier -} - -func (v *Grant) ID() ObjectIdentifier { - return v.Name -} - -type grantRow struct { - CreatedOn time.Time `db:"created_on"` - Privilege string `db:"privilege"` - GrantedOn string `db:"granted_on"` - GrantOn string `db:"grant_on"` - Name string `db:"name"` - GrantedTo string `db:"granted_to"` - GranteeName string `db:"grantee_name"` - GrantOption bool `db:"grant_option"` - GrantedBy string `db:"granted_by"` -} - -func (row *grantRow) toGrant() (*Grant, error) { - grantedTo := ObjectType(strings.ReplaceAll(row.GrantedTo, "_", " ")) - granteeName := NewAccountObjectIdentifier(row.GranteeName) - if grantedTo == ObjectTypeShare { - parts := strings.Split(row.GranteeName, ".") - name := strings.Join(parts[1:], ".") - granteeName = NewAccountObjectIdentifier(name) - } - grant := &Grant{ - CreatedOn: row.CreatedOn, - Privilege: row.Privilege, - GrantedTo: grantedTo, - Name: NewAccountObjectIdentifier(strings.Trim(row.Name, "\"")), - GranteeName: granteeName, - GrantOption: row.GrantOption, - GrantedBy: NewAccountObjectIdentifier(row.GrantedBy), - } - - // true for current grants - if row.GrantedOn != "" { - grant.GrantedOn = ObjectType(strings.ReplaceAll(row.GrantedOn, "_", " ")) - } - // true for future grants - if row.GrantOn != "" { - grant.GrantOn = ObjectType(strings.ReplaceAll(row.GrantOn, "_", " ")) - } - return grant, nil + Show(ctx context.Context, opts *ShowGrantOptions) ([]Grant, error) } +// GrantPrivilegesToAccountRoleOptions is based on https://docs.snowflake.com/en/sql-reference/sql/grant-privilege#syntax. type GrantPrivilegesToAccountRoleOptions struct { grant bool `ddl:"static" sql:"GRANT"` privileges *AccountRoleGrantPrivileges `ddl:"-"` @@ -95,22 +27,6 @@ type GrantPrivilegesToAccountRoleOptions struct { WithGrantOption *bool `ddl:"keyword" sql:"WITH GRANT OPTION"` } -func (opts *GrantPrivilegesToAccountRoleOptions) validate() error { - if !valueSet(opts.privileges) { - return fmt.Errorf("privileges must be set") - } - if err := opts.privileges.validate(); err != nil { - return err - } - if !valueSet(opts.on) { - return fmt.Errorf("on must be set") - } - if err := opts.on.validate(); err != nil { - return err - } - return nil -} - type AccountRoleGrantPrivileges struct { GlobalPrivileges []GlobalPrivilege `ddl:"-"` AccountObjectPrivileges []AccountObjectPrivilege `ddl:"-"` @@ -119,13 +35,6 @@ type AccountRoleGrantPrivileges struct { AllPrivileges *bool `ddl:"keyword" sql:"ALL PRIVILEGES"` } -func (v *AccountRoleGrantPrivileges) validate() error { - if !exactlyOneValueSet(v.AllPrivileges, v.GlobalPrivileges, v.AccountObjectPrivileges, v.SchemaPrivileges, v.SchemaObjectPrivileges) { - return fmt.Errorf("exactly one of AllPrivileges, GlobalPrivileges, AccountObjectPrivileges, SchemaPrivileges, or SchemaObjectPrivileges must be set") - } - return nil -} - type AccountRoleGrantOn struct { Account *bool `ddl:"keyword" sql:"ACCOUNT"` AccountObject *GrantOnAccountObject `ddl:"-"` @@ -133,28 +42,6 @@ type AccountRoleGrantOn struct { SchemaObject *GrantOnSchemaObject `ddl:"-"` } -func (v *AccountRoleGrantOn) validate() error { - if !exactlyOneValueSet(v.Account, v.AccountObject, v.Schema, v.SchemaObject) { - return fmt.Errorf("exactly one of Account, AccountObject, Schema, or SchemaObject must be set") - } - if valueSet(v.AccountObject) { - if err := v.AccountObject.validate(); err != nil { - return err - } - } - if valueSet(v.Schema) { - if err := v.Schema.validate(); err != nil { - return err - } - } - if valueSet(v.SchemaObject) { - if err := v.SchemaObject.validate(); err != nil { - return err - } - } - return nil -} - type GrantOnAccountObject struct { User *AccountObjectIdentifier `ddl:"identifier" sql:"USER"` ResourceMonitor *AccountObjectIdentifier `ddl:"identifier" sql:"RESOURCE MONITOR"` @@ -165,73 +52,25 @@ type GrantOnAccountObject struct { ReplicationGroup *AccountObjectIdentifier `ddl:"identifier" sql:"REPLICATION GROUP"` } -func (v *GrantOnAccountObject) validate() error { - if !exactlyOneValueSet(v.User, v.ResourceMonitor, v.Warehouse, v.Database, v.Integration, v.FailoverGroup, v.ReplicationGroup) { - return fmt.Errorf("exactly one of User, ResourceMonitor, Warehouse, Database, Integration, FailoverGroup, or ReplicationGroup must be set") - } - return nil -} - type GrantOnSchema struct { Schema *DatabaseObjectIdentifier `ddl:"identifier" sql:"SCHEMA"` AllSchemasInDatabase *AccountObjectIdentifier `ddl:"identifier" sql:"ALL SCHEMAS IN DATABASE"` FutureSchemasInDatabase *AccountObjectIdentifier `ddl:"identifier" sql:"FUTURE SCHEMAS IN DATABASE"` } -func (v *GrantOnSchema) validate() error { - if !exactlyOneValueSet(v.Schema, v.AllSchemasInDatabase, v.FutureSchemasInDatabase) { - return fmt.Errorf("exactly one of Schema, AllSchemasInDatabase, or FutureSchemasInDatabase must be set") - } - return nil -} - type GrantOnSchemaObject struct { SchemaObject *Object `ddl:"-"` All *GrantOnSchemaObjectIn `ddl:"keyword" sql:"ALL"` Future *GrantOnSchemaObjectIn `ddl:"keyword" sql:"FUTURE"` } -func (v *GrantOnSchemaObject) validate() error { - if !exactlyOneValueSet(v.SchemaObject, v.All, v.Future) { - return fmt.Errorf("exactly one of Object, AllIn or Future must be set") - } - return nil -} - type GrantOnSchemaObjectIn struct { PluralObjectType PluralObjectType `ddl:"keyword" sql:"ALL"` InDatabase *AccountObjectIdentifier `ddl:"identifier" sql:"IN DATABASE"` InSchema *DatabaseObjectIdentifier `ddl:"identifier" sql:"IN SCHEMA"` } -func (v *GrantOnSchemaObjectIn) validate() error { - if !exactlyOneValueSet(v.PluralObjectType, v.InDatabase, v.InSchema) { - return fmt.Errorf("exactly one of PluralObjectType, InDatabase, or InSchema must be set") - } - return nil -} - -func (v *grants) GrantPrivilegesToAccountRole(ctx context.Context, privileges *AccountRoleGrantPrivileges, on *AccountRoleGrantOn, role AccountObjectIdentifier, opts *GrantPrivilegesToAccountRoleOptions) error { - if opts == nil { - opts = &GrantPrivilegesToAccountRoleOptions{} - } - opts.privileges = privileges - opts.on = on - opts.accountRole = role - if err := opts.validate(); err != nil { - return err - } - sql, err := structToSQL(opts) - if err != nil { - return err - } - _, err = v.client.exec(ctx, sql) - if err != nil { - return err - } - return nil -} - +// RevokePrivilegesFromAccountRoleOptions is based on https://docs.snowflake.com/en/sql-reference/sql/revoke-privilege#syntax. type RevokePrivilegesFromAccountRoleOptions struct { revoke bool `ddl:"static" sql:"REVOKE"` GrantOptionFor *bool `ddl:"keyword" sql:"GRANT OPTION FOR"` @@ -242,47 +81,36 @@ type RevokePrivilegesFromAccountRoleOptions struct { Cascade *bool `ddl:"keyword" sql:"CASCADE"` } -func (opts *RevokePrivilegesFromAccountRoleOptions) validate() error { - if !valueSet(opts.privileges) { - return fmt.Errorf("privileges must be set") - } - if err := opts.privileges.validate(); err != nil { - return err - } - if !valueSet(opts.on) { - return fmt.Errorf("on must be set") - } - if err := opts.on.validate(); err != nil { - return err - } - if !validObjectidentifier(opts.accountRole) { - return ErrInvalidObjectIdentifier - } - if everyValueSet(opts.Restrict, opts.Cascade) { - return fmt.Errorf("either Restrict or Cascade can be set, or neither but not both") - } - return nil +// GrantPrivilegesToDatabaseRoleOptions is based on https://docs.snowflake.com/en/sql-reference/sql/grant-privilege#syntax. +type GrantPrivilegesToDatabaseRoleOptions struct { + grant bool `ddl:"static" sql:"GRANT"` + privileges *DatabaseRoleGrantPrivileges `ddl:"-"` + on *DatabaseRoleGrantOn `ddl:"keyword" sql:"ON"` + databaseRole DatabaseObjectIdentifier `ddl:"identifier" sql:"TO DATABASE ROLE"` + WithGrantOption *bool `ddl:"keyword" sql:"WITH GRANT OPTION"` } -func (v *grants) RevokePrivilegesFromAccountRole(ctx context.Context, privileges *AccountRoleGrantPrivileges, on *AccountRoleGrantOn, role AccountObjectIdentifier, opts *RevokePrivilegesFromAccountRoleOptions) error { - if opts == nil { - opts = &RevokePrivilegesFromAccountRoleOptions{} - } - opts.privileges = privileges - opts.on = on - opts.accountRole = role - if err := opts.validate(); err != nil { - return err - } - sql, err := structToSQL(opts) - if err != nil { - return err - } - _, err = v.client.exec(ctx, sql) - if err != nil { - return err - } - return nil +type DatabaseRoleGrantPrivileges struct { + DatabasePrivileges []AccountObjectPrivilege `ddl:"-"` + SchemaPrivileges []SchemaPrivilege `ddl:"-"` + SchemaObjectPrivileges []SchemaObjectPrivilege `ddl:"-"` +} + +type DatabaseRoleGrantOn struct { + Database *AccountObjectIdentifier `ddl:"identifier" sql:"DATABASE"` + Schema *GrantOnSchema `ddl:"-"` + SchemaObject *GrantOnSchemaObject `ddl:"-"` +} + +// RevokePrivilegesFromDatabaseRoleOptions is based on https://docs.snowflake.com/en/sql-reference/sql/revoke-privilege#syntax. +type RevokePrivilegesFromDatabaseRoleOptions struct { + revoke bool `ddl:"static" sql:"REVOKE"` + GrantOptionFor *bool `ddl:"keyword" sql:"GRANT OPTION FOR"` + privileges *DatabaseRoleGrantPrivileges `ddl:"-"` + on *DatabaseRoleGrantOn `ddl:"keyword" sql:"ON"` + databaseRole DatabaseObjectIdentifier `ddl:"identifier" sql:"FROM DATABASE ROLE"` + Restrict *bool `ddl:"keyword" sql:"RESTRICT"` + Cascade *bool `ddl:"keyword" sql:"CASCADE"` } type grantPrivilegeToShareOptions struct { @@ -292,19 +120,6 @@ type grantPrivilegeToShareOptions struct { to AccountObjectIdentifier `ddl:"identifier" sql:"TO SHARE"` } -func (opts *grantPrivilegeToShareOptions) validate() error { - if !validObjectidentifier(opts.to) { - return ErrInvalidObjectIdentifier - } - if !valueSet(opts.On) || opts.privilege == "" { - return fmt.Errorf("on and privilege are required") - } - if !exactlyOneValueSet(opts.On.Database, opts.On.Schema, opts.On.Function, opts.On.Table, opts.On.View) { - return fmt.Errorf("only one of database, schema, function, table, or view can be set") - } - return nil -} - type GrantPrivilegeToShareOn struct { Database AccountObjectIdentifier `ddl:"identifier" sql:"DATABASE"` Schema DatabaseObjectIdentifier `ddl:"identifier" sql:"SCHEMA"` @@ -313,47 +128,11 @@ type GrantPrivilegeToShareOn struct { View SchemaObjectIdentifier `ddl:"identifier" sql:"VIEW"` } -func (v *GrantPrivilegeToShareOn) validate() error { - if !exactlyOneValueSet(v.Database, v.Schema, v.Function, v.Table, v.View) { - return fmt.Errorf("only one of database, schema, function, table, or view can be set") - } - if valueSet(v.Table) { - if err := v.Table.validate(); err != nil { - return err - } - } - return nil -} - type OnTable struct { Name SchemaObjectIdentifier `ddl:"identifier" sql:"TABLE"` AllInSchema DatabaseObjectIdentifier `ddl:"identifier" sql:"ALL TABLES IN SCHEMA"` } -func (v *OnTable) validate() error { - if !exactlyOneValueSet(v.Name, v.AllInSchema) { - return fmt.Errorf("only one of name or allInSchema can be set") - } - return nil -} - -func (v *grants) GrantPrivilegeToShare(ctx context.Context, privilege ObjectPrivilege, on *GrantPrivilegeToShareOn, to AccountObjectIdentifier) error { - opts := &grantPrivilegeToShareOptions{ - privilege: privilege, - On: on, - to: to, - } - if err := opts.validate(); err != nil { - return err - } - sql, err := structToSQL(opts) - if err != nil { - return err - } - _, err = v.client.exec(ctx, sql) - return err -} - type revokePrivilegeFromShareOptions struct { revoke bool `ddl:"static" sql:"REVOKE"` privilege ObjectPrivilege `ddl:"keyword"` @@ -361,24 +140,6 @@ type revokePrivilegeFromShareOptions struct { from AccountObjectIdentifier `ddl:"identifier" sql:"FROM SHARE"` } -func (opts *revokePrivilegeFromShareOptions) validate() error { - if !validObjectidentifier(opts.from) { - return ErrInvalidObjectIdentifier - } - if !valueSet(opts.On) || opts.privilege == "" { - return fmt.Errorf("on and privilege are required") - } - if !exactlyOneValueSet(opts.On.Database, opts.On.Schema, opts.On.Table, opts.On.View) { - return fmt.Errorf("only one of database, schema, function, table, or view can be set") - } - - if err := opts.On.validate(); err != nil { - return err - } - - return nil -} - type RevokePrivilegeFromShareOn struct { Database AccountObjectIdentifier `ddl:"identifier" sql:"DATABASE"` Schema DatabaseObjectIdentifier `ddl:"identifier" sql:"SCHEMA"` @@ -386,48 +147,11 @@ type RevokePrivilegeFromShareOn struct { View *OnView `ddl:"-"` } -func (v *RevokePrivilegeFromShareOn) validate() error { - if !exactlyOneValueSet(v.Database, v.Schema, v.Table, v.View) { - return fmt.Errorf("only one of database, schema, table, or view can be set") - } - if valueSet(v.Table) { - return v.Table.validate() - } - if valueSet(v.View) { - return v.View.validate() - } - return nil -} - type OnView struct { Name SchemaObjectIdentifier `ddl:"identifier" sql:"VIEW"` AllInSchema DatabaseObjectIdentifier `ddl:"identifier" sql:"ALL VIEWS IN SCHEMA"` } -func (v *OnView) validate() error { - if !exactlyOneValueSet(v.Name, v.AllInSchema) { - return fmt.Errorf("only one of name or allInSchema can be set") - } - return nil -} - -func (v *grants) RevokePrivilegeFromShare(ctx context.Context, privilege ObjectPrivilege, on *RevokePrivilegeFromShareOn, id AccountObjectIdentifier) error { - opts := &revokePrivilegeFromShareOptions{ - privilege: privilege, - On: on, - from: id, - } - if err := opts.validate(); err != nil { - return err - } - sql, err := structToSQL(opts) - if err != nil { - return err - } - _, err = v.client.exec(ctx, sql) - return err -} - type ShowGrantOptions struct { show bool `ddl:"static" sql:"SHOW"` Future *bool `ddl:"keyword" sql:"FUTURE"` @@ -438,16 +162,6 @@ type ShowGrantOptions struct { In *ShowGrantsIn `ddl:"keyword" sql:"IN"` } -func (opts *ShowGrantOptions) validate() error { - if everyValueNil(opts.On, opts.To, opts.Of, opts.In) { - return fmt.Errorf("at least one of on, to, of, or in is required") - } - if !exactlyOneValueSet(opts.On, opts.To, opts.Of, opts.In) { - return fmt.Errorf("only one of on, to, of, or in can be set") - } - return nil -} - type ShowGrantsIn struct { Schema *DatabaseObjectIdentifier `ddl:"identifier" sql:"SCHEMA"` Database *AccountObjectIdentifier `ddl:"identifier" sql:"DATABASE"` @@ -459,9 +173,10 @@ type ShowGrantsOn struct { } type ShowGrantsTo struct { - Role AccountObjectIdentifier `ddl:"identifier" sql:"ROLE"` - User AccountObjectIdentifier `ddl:"identifier" sql:"USER"` - Share AccountObjectIdentifier `ddl:"identifier" sql:"SHARE"` + Role AccountObjectIdentifier `ddl:"identifier" sql:"ROLE"` + User AccountObjectIdentifier `ddl:"identifier" sql:"USER"` + Share AccountObjectIdentifier `ddl:"identifier" sql:"SHARE"` + DatabaseRole DatabaseObjectIdentifier `ddl:"identifier" sql:"DATABASE ROLE"` } type ShowGrantsOf struct { @@ -469,29 +184,69 @@ type ShowGrantsOf struct { Share AccountObjectIdentifier `ddl:"identifier" sql:"SHARE"` } -func (v *grants) Show(ctx context.Context, opts *ShowGrantOptions) ([]*Grant, error) { - if opts == nil { - opts = &ShowGrantOptions{} - } - if err := opts.validate(); err != nil { - return nil, err +type grantRow struct { + CreatedOn time.Time `db:"created_on"` + Privilege string `db:"privilege"` + GrantedOn string `db:"granted_on"` + GrantOn string `db:"grant_on"` + Name string `db:"name"` + GrantedTo string `db:"granted_to"` + GrantTo string `db:"grant_to"` + GranteeName string `db:"grantee_name"` + GrantOption bool `db:"grant_option"` + GrantedBy string `db:"granted_by"` +} + +type Grant struct { + CreatedOn time.Time + Privilege string + GrantedOn ObjectType + GrantOn ObjectType + Name ObjectIdentifier + GrantedTo ObjectType + GrantTo ObjectType + GranteeName AccountObjectIdentifier + GrantOption bool + GrantedBy AccountObjectIdentifier +} + +func (v *Grant) ID() ObjectIdentifier { + return v.Name +} + +func (row grantRow) convert() *Grant { + grantedTo := ObjectType(strings.ReplaceAll(row.GrantedTo, "_", " ")) + grantTo := ObjectType(strings.ReplaceAll(row.GrantTo, "_", " ")) + var granteeName AccountObjectIdentifier + if grantedTo == ObjectTypeShare { + parts := strings.Split(row.GranteeName, ".") + name := strings.Join(parts[1:], ".") + granteeName = NewAccountObjectIdentifier(name) + } else { + granteeName = NewAccountObjectIdentifier(row.GranteeName) } - sql, err := structToSQL(opts) - if err != nil { - return nil, err + + var grantedOn ObjectType + // true for current grants + if row.GrantedOn != "" { + grantedOn = ObjectType(strings.ReplaceAll(row.GrantedOn, "_", " ")) } - var rows []grantRow - err = v.client.query(ctx, &rows, sql) - if err != nil { - return nil, err + var grantOn ObjectType + // true for future grants + if row.GrantOn != "" { + grantOn = ObjectType(strings.ReplaceAll(row.GrantOn, "_", " ")) } - grants := make([]*Grant, 0, len(rows)) - for _, row := range rows { - grant, err := row.toGrant() - if err != nil { - return nil, err - } - grants = append(grants, grant) + + return &Grant{ + CreatedOn: row.CreatedOn, + Privilege: row.Privilege, + GrantedOn: grantedOn, + GrantOn: grantOn, + GrantedTo: grantedTo, + GrantTo: grantTo, + Name: NewAccountObjectIdentifier(strings.Trim(row.Name, "\"")), + GranteeName: granteeName, + GrantOption: row.GrantOption, + GrantedBy: NewAccountObjectIdentifier(row.GrantedBy), } - return grants, nil } diff --git a/pkg/sdk/grants_impl.go b/pkg/sdk/grants_impl.go new file mode 100644 index 0000000000..3d03a79c3f --- /dev/null +++ b/pkg/sdk/grants_impl.go @@ -0,0 +1,80 @@ +package sdk + +import "context" + +var _ Grants = (*grants)(nil) + +type grants struct { + client *Client +} + +func (v *grants) GrantPrivilegesToAccountRole(ctx context.Context, privileges *AccountRoleGrantPrivileges, on *AccountRoleGrantOn, role AccountObjectIdentifier, opts *GrantPrivilegesToAccountRoleOptions) error { + if opts == nil { + opts = &GrantPrivilegesToAccountRoleOptions{} + } + opts.privileges = privileges + opts.on = on + opts.accountRole = role + return validateAndExec(v.client, ctx, opts) +} + +func (v *grants) RevokePrivilegesFromAccountRole(ctx context.Context, privileges *AccountRoleGrantPrivileges, on *AccountRoleGrantOn, role AccountObjectIdentifier, opts *RevokePrivilegesFromAccountRoleOptions) error { + if opts == nil { + opts = &RevokePrivilegesFromAccountRoleOptions{} + } + opts.privileges = privileges + opts.on = on + opts.accountRole = role + return validateAndExec(v.client, ctx, opts) +} + +func (v *grants) GrantPrivilegesToDatabaseRole(ctx context.Context, privileges *DatabaseRoleGrantPrivileges, on *DatabaseRoleGrantOn, role DatabaseObjectIdentifier, opts *GrantPrivilegesToDatabaseRoleOptions) error { + if opts == nil { + opts = &GrantPrivilegesToDatabaseRoleOptions{} + } + opts.privileges = privileges + opts.on = on + opts.databaseRole = role + return validateAndExec(v.client, ctx, opts) +} + +func (v *grants) RevokePrivilegesFromDatabaseRole(ctx context.Context, privileges *DatabaseRoleGrantPrivileges, on *DatabaseRoleGrantOn, role DatabaseObjectIdentifier, opts *RevokePrivilegesFromDatabaseRoleOptions) error { + if opts == nil { + opts = &RevokePrivilegesFromDatabaseRoleOptions{} + } + opts.privileges = privileges + opts.on = on + opts.databaseRole = role + return validateAndExec(v.client, ctx, opts) +} + +func (v *grants) GrantPrivilegeToShare(ctx context.Context, privilege ObjectPrivilege, on *GrantPrivilegeToShareOn, to AccountObjectIdentifier) error { + opts := &grantPrivilegeToShareOptions{ + privilege: privilege, + On: on, + to: to, + } + return validateAndExec(v.client, ctx, opts) +} + +func (v *grants) RevokePrivilegeFromShare(ctx context.Context, privilege ObjectPrivilege, on *RevokePrivilegeFromShareOn, id AccountObjectIdentifier) error { + opts := &revokePrivilegeFromShareOptions{ + privilege: privilege, + On: on, + from: id, + } + return validateAndExec(v.client, ctx, opts) +} + +func (v *grants) Show(ctx context.Context, opts *ShowGrantOptions) ([]Grant, error) { + if opts == nil { + opts = &ShowGrantOptions{} + } + + dbRows, err := validateAndQuery[grantRow](v.client, ctx, opts) + if err != nil { + return nil, err + } + resultList := convertRows[grantRow, Grant](dbRows) + return resultList, nil +} diff --git a/pkg/sdk/grants_integration_test.go b/pkg/sdk/grants_integration_test.go index e011ebc042..90b74837fb 100644 --- a/pkg/sdk/grants_integration_test.go +++ b/pkg/sdk/grants_integration_test.go @@ -216,6 +216,208 @@ func TestInt_GrantAndRevokePrivilegesToAccountRole(t *testing.T) { }) } +func TestInt_GrantAndRevokePrivilegesToDatabaseRole(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + database, databaseCleanup := createDatabase(t, client) + t.Cleanup(databaseCleanup) + + t.Run("on database", func(t *testing.T) { + databaseRole, _ := createDatabaseRole(t, client, database) + databaseRoleId := NewDatabaseObjectIdentifier(database.Name, databaseRole.Name) + + privileges := &DatabaseRoleGrantPrivileges{ + DatabasePrivileges: []AccountObjectPrivilege{AccountObjectPrivilegeCreateSchema}, + } + on := &DatabaseRoleGrantOn{ + Database: Pointer(database.ID()), + } + + err := client.Grants.GrantPrivilegesToDatabaseRole(ctx, privileges, on, databaseRoleId, nil) + require.NoError(t, err) + + returnedGrants, err := client.Grants.Show(ctx, &ShowGrantOptions{ + To: &ShowGrantsTo{ + DatabaseRole: databaseRoleId, + }, + }) + require.NoError(t, err) + // Expecting two grants because database rol has usage on database by default + require.Equal(t, 2, len(returnedGrants)) + + usagePrivilege, err := findOne[Grant](returnedGrants, func(g Grant) bool { return g.Privilege == AccountObjectPrivilegeUsage.String() }) + require.NoError(t, err) + assert.Equal(t, ObjectTypeDatabaseRole, usagePrivilege.GrantedTo) + + createSchemaPrivilege, err := findOne[Grant](returnedGrants, func(g Grant) bool { return g.Privilege == AccountObjectPrivilegeCreateSchema.String() }) + require.NoError(t, err) + assert.Equal(t, ObjectTypeDatabase, createSchemaPrivilege.GrantedOn) + assert.Equal(t, ObjectTypeDatabaseRole, createSchemaPrivilege.GrantedTo) + + // now revoke and verify that the new grant is gone + err = client.Grants.RevokePrivilegesFromDatabaseRole(ctx, privileges, on, databaseRoleId, nil) + require.NoError(t, err) + + returnedGrants, err = client.Grants.Show(ctx, &ShowGrantOptions{ + To: &ShowGrantsTo{ + DatabaseRole: databaseRoleId, + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(returnedGrants)) + assert.Equal(t, AccountObjectPrivilegeUsage.String(), returnedGrants[0].Privilege) + }) + + t.Run("on schema", func(t *testing.T) { + databaseRole, _ := createDatabaseRole(t, client, database) + databaseRoleId := NewDatabaseObjectIdentifier(database.Name, databaseRole.Name) + schema, _ := createSchema(t, client, database) + + privileges := &DatabaseRoleGrantPrivileges{ + SchemaPrivileges: []SchemaPrivilege{SchemaPrivilegeCreateAlert}, + } + on := &DatabaseRoleGrantOn{ + Schema: &GrantOnSchema{ + Schema: Pointer(schema.ID()), + }, + } + + err := client.Grants.GrantPrivilegesToDatabaseRole(ctx, privileges, on, databaseRoleId, nil) + require.NoError(t, err) + + returnedGrants, err := client.Grants.Show(ctx, &ShowGrantOptions{ + To: &ShowGrantsTo{ + DatabaseRole: databaseRoleId, + }, + }) + require.NoError(t, err) + // Expecting two grants because database rol has usage on database by default + require.Equal(t, 2, len(returnedGrants)) + + usagePrivilege, err := findOne[Grant](returnedGrants, func(g Grant) bool { return g.Privilege == AccountObjectPrivilegeUsage.String() }) + require.NoError(t, err) + assert.Equal(t, ObjectTypeDatabaseRole, usagePrivilege.GrantedTo) + + createAlertPrivilege, err := findOne[Grant](returnedGrants, func(g Grant) bool { return g.Privilege == SchemaPrivilegeCreateAlert.String() }) + require.NoError(t, err) + assert.Equal(t, ObjectTypeSchema, createAlertPrivilege.GrantedOn) + assert.Equal(t, ObjectTypeDatabaseRole, createAlertPrivilege.GrantedTo) + + // now revoke and verify that the new grant is gone + err = client.Grants.RevokePrivilegesFromDatabaseRole(ctx, privileges, on, databaseRoleId, nil) + require.NoError(t, err) + + returnedGrants, err = client.Grants.Show(ctx, &ShowGrantOptions{ + To: &ShowGrantsTo{ + DatabaseRole: databaseRoleId, + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(returnedGrants)) + assert.Equal(t, AccountObjectPrivilegeUsage.String(), returnedGrants[0].Privilege) + }) + + t.Run("on schema object", func(t *testing.T) { + databaseRole, _ := createDatabaseRole(t, client, database) + databaseRoleId := NewDatabaseObjectIdentifier(database.Name, databaseRole.Name) + schema, _ := createSchema(t, client, database) + table, _ := createTable(t, client, database, schema) + + privileges := &DatabaseRoleGrantPrivileges{ + SchemaObjectPrivileges: []SchemaObjectPrivilege{SchemaObjectPrivilegeSelect}, + } + on := &DatabaseRoleGrantOn{ + SchemaObject: &GrantOnSchemaObject{ + All: &GrantOnSchemaObjectIn{ + PluralObjectType: PluralObjectTypeTables, + InSchema: Pointer(schema.ID()), + }, + }, + } + + err := client.Grants.GrantPrivilegesToDatabaseRole(ctx, privileges, on, databaseRoleId, nil) + require.NoError(t, err) + + returnedGrants, err := client.Grants.Show(ctx, &ShowGrantOptions{ + To: &ShowGrantsTo{ + DatabaseRole: databaseRoleId, + }, + }) + require.NoError(t, err) + // Expecting two grants because database rol has usage on database by default + require.Equal(t, 2, len(returnedGrants)) + + usagePrivilege, err := findOne[Grant](returnedGrants, func(g Grant) bool { return g.Privilege == AccountObjectPrivilegeUsage.String() }) + require.NoError(t, err) + assert.Equal(t, ObjectTypeDatabaseRole, usagePrivilege.GrantedTo) + + selectPrivilege, err := findOne[Grant](returnedGrants, func(g Grant) bool { return g.Privilege == SchemaObjectPrivilegeSelect.String() }) + require.NoError(t, err) + assert.Equal(t, ObjectTypeTable, selectPrivilege.GrantedOn) + assert.Equal(t, ObjectTypeDatabaseRole, selectPrivilege.GrantedTo) + assert.Equal(t, table.ID().FullyQualifiedName(), selectPrivilege.Name.FullyQualifiedName()) + + // now revoke and verify that the new grant is gone + err = client.Grants.RevokePrivilegesFromDatabaseRole(ctx, privileges, on, databaseRoleId, nil) + require.NoError(t, err) + + returnedGrants, err = client.Grants.Show(ctx, &ShowGrantOptions{ + To: &ShowGrantsTo{ + DatabaseRole: databaseRoleId, + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(returnedGrants)) + assert.Equal(t, AccountObjectPrivilegeUsage.String(), returnedGrants[0].Privilege) + }) + + t.Run("on future schema object", func(t *testing.T) { + databaseRole, _ := createDatabaseRole(t, client, database) + databaseRoleId := NewDatabaseObjectIdentifier(database.Name, databaseRole.Name) + + privileges := &DatabaseRoleGrantPrivileges{ + SchemaObjectPrivileges: []SchemaObjectPrivilege{SchemaObjectPrivilegeSelect}, + } + on := &DatabaseRoleGrantOn{ + SchemaObject: &GrantOnSchemaObject{ + Future: &GrantOnSchemaObjectIn{ + PluralObjectType: PluralObjectTypeExternalTables, + InDatabase: Pointer(database.ID()), + }, + }, + } + err := client.Grants.GrantPrivilegesToDatabaseRole(ctx, privileges, on, databaseRoleId, nil) + require.NoError(t, err) + + returnedGrants, err := client.Grants.Show(ctx, &ShowGrantOptions{ + Future: Bool(true), + To: &ShowGrantsTo{ + DatabaseRole: databaseRoleId, + }, + }) + require.NoError(t, err) + require.Equal(t, 1, len(returnedGrants)) + + assert.Equal(t, SchemaObjectPrivilegeSelect.String(), returnedGrants[0].Privilege) + assert.Equal(t, ObjectTypeExternalTable, returnedGrants[0].GrantOn) + assert.Equal(t, ObjectTypeDatabaseRole, returnedGrants[0].GrantTo) + + // now revoke and verify that the new grant is gone + err = client.Grants.RevokePrivilegesFromDatabaseRole(ctx, privileges, on, databaseRoleId, nil) + require.NoError(t, err) + + returnedGrants, err = client.Grants.Show(ctx, &ShowGrantOptions{ + Future: Bool(true), + To: &ShowGrantsTo{ + DatabaseRole: databaseRoleId, + }, + }) + require.NoError(t, err) + assert.Equal(t, 0, len(returnedGrants)) + }) +} + func TestInt_GrantPrivilegeToShare(t *testing.T) { client := testClient(t) ctx := context.Background() @@ -243,9 +445,9 @@ func TestInt_GrantPrivilegeToShare(t *testing.T) { require.NoError(t, err) assert.LessOrEqual(t, 2, len(grants)) var shareGrant *Grant - for _, grant := range grants { + for i, grant := range grants { if grant.GranteeName.Name() == shareTest.ID().Name() { - shareGrant = grant + shareGrant = &grants[i] break } } @@ -303,7 +505,7 @@ func TestInt_ShowGrants(t *testing.T) { }) t.Run("without options", func(t *testing.T) { _, err := client.Grants.Show(ctx, nil) - require.Error(t, err) + require.NoError(t, err) }) t.Run("with options", func(t *testing.T) { grants, err := client.Grants.Show(ctx, &ShowGrantOptions{ diff --git a/pkg/sdk/grants_test.go b/pkg/sdk/grants_test.go index 23e57bfb04..1c600e708b 100644 --- a/pkg/sdk/grants_test.go +++ b/pkg/sdk/grants_test.go @@ -1,11 +1,9 @@ package sdk import ( + "errors" "fmt" "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestGrantPrivilegesToAccountRole(t *testing.T) { @@ -20,10 +18,7 @@ func TestGrantPrivilegesToAccountRole(t *testing.T) { accountRole: NewAccountObjectIdentifier("role1"), WithGrantOption: Bool(true), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `GRANT MONITOR USAGE, APPLY TAG ON ACCOUNT TO ROLE "role1" WITH GRANT OPTION` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `GRANT MONITOR USAGE, APPLY TAG ON ACCOUNT TO ROLE "role1" WITH GRANT OPTION`) }) t.Run("on account object", func(t *testing.T) { @@ -38,10 +33,7 @@ func TestGrantPrivilegesToAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `GRANT ALL PRIVILEGES ON DATABASE "db1" TO ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `GRANT ALL PRIVILEGES ON DATABASE "db1" TO ROLE "role1"`) }) t.Run("on schema", func(t *testing.T) { opts := &GrantPrivilegesToAccountRoleOptions{ @@ -55,10 +47,7 @@ func TestGrantPrivilegesToAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `GRANT CREATE ALERT ON SCHEMA "db1"."schema1" TO ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `GRANT CREATE ALERT ON SCHEMA "db1"."schema1" TO ROLE "role1"`) }) t.Run("on all schemas in database", func(t *testing.T) { @@ -73,10 +62,7 @@ func TestGrantPrivilegesToAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `GRANT CREATE ALERT ON ALL SCHEMAS IN DATABASE "db1" TO ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `GRANT CREATE ALERT ON ALL SCHEMAS IN DATABASE "db1" TO ROLE "role1"`) }) t.Run("on all future schemas in database", func(t *testing.T) { @@ -91,10 +77,7 @@ func TestGrantPrivilegesToAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `GRANT CREATE ALERT ON FUTURE SCHEMAS IN DATABASE "db1" TO ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `GRANT CREATE ALERT ON FUTURE SCHEMAS IN DATABASE "db1" TO ROLE "role1"`) }) t.Run("on schema object", func(t *testing.T) { @@ -112,10 +95,7 @@ func TestGrantPrivilegesToAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `GRANT APPLY ON TABLE "db1"."schema1"."table1" TO ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `GRANT APPLY ON TABLE "db1"."schema1"."table1" TO ROLE "role1"`) }) t.Run("on future schema object in database", func(t *testing.T) { @@ -133,10 +113,7 @@ func TestGrantPrivilegesToAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `GRANT APPLY ON FUTURE TABLES IN DATABASE "db1" TO ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `GRANT APPLY ON FUTURE TABLES IN DATABASE "db1" TO ROLE "role1"`) }) t.Run("on future schema object in schema", func(t *testing.T) { @@ -154,10 +131,7 @@ func TestGrantPrivilegesToAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `GRANT APPLY ON FUTURE TABLES IN SCHEMA "db1"."schema1" TO ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `GRANT APPLY ON FUTURE TABLES IN SCHEMA "db1"."schema1" TO ROLE "role1"`) }) } @@ -172,10 +146,7 @@ func TestRevokePrivilegesFromAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `REVOKE MONITOR USAGE, APPLY TAG ON ACCOUNT FROM ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `REVOKE MONITOR USAGE, APPLY TAG ON ACCOUNT FROM ROLE "role1"`) }) t.Run("on account object", func(t *testing.T) { @@ -190,10 +161,7 @@ func TestRevokePrivilegesFromAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `REVOKE ALL PRIVILEGES ON DATABASE "db1" FROM ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `REVOKE ALL PRIVILEGES ON DATABASE "db1" FROM ROLE "role1"`) }) t.Run("on account object", func(t *testing.T) { @@ -208,10 +176,7 @@ func TestRevokePrivilegesFromAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `REVOKE CREATE DATABASE ROLE, MODIFY ON DATABASE "db1" FROM ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `REVOKE CREATE DATABASE ROLE, MODIFY ON DATABASE "db1" FROM ROLE "role1"`) }) t.Run("on schema", func(t *testing.T) { opts := &RevokePrivilegesFromAccountRoleOptions{ @@ -225,10 +190,7 @@ func TestRevokePrivilegesFromAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `REVOKE CREATE ALERT, ADD SEARCH OPTIMIZATION ON SCHEMA "db1"."schema1" FROM ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `REVOKE CREATE ALERT, ADD SEARCH OPTIMIZATION ON SCHEMA "db1"."schema1" FROM ROLE "role1"`) }) t.Run("on all schemas in database + restrict", func(t *testing.T) { @@ -244,10 +206,7 @@ func TestRevokePrivilegesFromAccountRole(t *testing.T) { accountRole: NewAccountObjectIdentifier("role1"), Restrict: Bool(true), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `REVOKE CREATE ALERT, ADD SEARCH OPTIMIZATION ON ALL SCHEMAS IN DATABASE "db1" FROM ROLE "role1" RESTRICT` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `REVOKE CREATE ALERT, ADD SEARCH OPTIMIZATION ON ALL SCHEMAS IN DATABASE "db1" FROM ROLE "role1" RESTRICT`) }) t.Run("on all future schemas in database + cascade", func(t *testing.T) { @@ -263,10 +222,7 @@ func TestRevokePrivilegesFromAccountRole(t *testing.T) { accountRole: NewAccountObjectIdentifier("role1"), Cascade: Bool(true), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `REVOKE CREATE ALERT, ADD SEARCH OPTIMIZATION ON FUTURE SCHEMAS IN DATABASE "db1" FROM ROLE "role1" CASCADE` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `REVOKE CREATE ALERT, ADD SEARCH OPTIMIZATION ON FUTURE SCHEMAS IN DATABASE "db1" FROM ROLE "role1" CASCADE`) }) t.Run("on schema object", func(t *testing.T) { @@ -284,10 +240,7 @@ func TestRevokePrivilegesFromAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `REVOKE SELECT, UPDATE ON TABLE "db1"."schema1"."table1" FROM ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `REVOKE SELECT, UPDATE ON TABLE "db1"."schema1"."table1" FROM ROLE "role1"`) }) t.Run("on future schema object in database", func(t *testing.T) { @@ -305,10 +258,7 @@ func TestRevokePrivilegesFromAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `REVOKE SELECT, UPDATE ON FUTURE TABLES IN DATABASE "db1" FROM ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `REVOKE SELECT, UPDATE ON FUTURE TABLES IN DATABASE "db1" FROM ROLE "role1"`) }) t.Run("on future schema object in schema", func(t *testing.T) { @@ -326,10 +276,381 @@ func TestRevokePrivilegesFromAccountRole(t *testing.T) { }, accountRole: NewAccountObjectIdentifier("role1"), } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := `REVOKE SELECT, UPDATE ON FUTURE TABLES IN SCHEMA "db1"."schema1" FROM ROLE "role1"` - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, `REVOKE SELECT, UPDATE ON FUTURE TABLES IN SCHEMA "db1"."schema1" FROM ROLE "role1"`) + }) +} + +func TestGrants_GrantPrivilegesToDatabaseRole(t *testing.T) { + dbId := NewAccountObjectIdentifier("db1") + + defaultGrantsForDb := func() *GrantPrivilegesToDatabaseRoleOptions { + return &GrantPrivilegesToDatabaseRoleOptions{ + privileges: &DatabaseRoleGrantPrivileges{ + DatabasePrivileges: []AccountObjectPrivilege{AccountObjectPrivilegeCreateSchema}, + }, + on: &DatabaseRoleGrantOn{ + Database: &dbId, + }, + databaseRole: NewDatabaseObjectIdentifier("db1", "role1"), + } + } + + defaultGrantsForSchema := func() *GrantPrivilegesToDatabaseRoleOptions { + return &GrantPrivilegesToDatabaseRoleOptions{ + privileges: &DatabaseRoleGrantPrivileges{ + SchemaPrivileges: []SchemaPrivilege{SchemaPrivilegeCreateAlert}, + }, + on: &DatabaseRoleGrantOn{ + Schema: &GrantOnSchema{ + Schema: Pointer(NewDatabaseObjectIdentifier("db1", "schema1")), + }, + }, + databaseRole: NewDatabaseObjectIdentifier("db1", "role1"), + } + } + + defaultGrantsForSchemaObject := func() *GrantPrivilegesToDatabaseRoleOptions { + return &GrantPrivilegesToDatabaseRoleOptions{ + privileges: &DatabaseRoleGrantPrivileges{ + SchemaObjectPrivileges: []SchemaObjectPrivilege{SchemaObjectPrivilegeApply}, + }, + on: &DatabaseRoleGrantOn{ + SchemaObject: &GrantOnSchemaObject{ + SchemaObject: &Object{ + ObjectType: ObjectTypeTable, + Name: NewSchemaObjectIdentifier("db1", "schema1", "table1"), + }, + }, + }, + databaseRole: NewDatabaseObjectIdentifier("db1", "role1"), + } + } + + t.Run("validation: no privileges set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.privileges = nil + assertOptsInvalid(t, opts, fmt.Errorf("privileges must be set")) + }) + + t.Run("validation: no privileges set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.privileges = &DatabaseRoleGrantPrivileges{} + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of DatabasePrivileges, SchemaPrivileges, or SchemaObjectPrivileges must be set")) + }) + + t.Run("validation: too many privileges set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.privileges = &DatabaseRoleGrantPrivileges{ + DatabasePrivileges: []AccountObjectPrivilege{AccountObjectPrivilegeCreateSchema}, + SchemaPrivileges: []SchemaPrivilege{SchemaPrivilegeCreateAlert}, + } + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of DatabasePrivileges, SchemaPrivileges, or SchemaObjectPrivileges must be set")) + }) + + t.Run("validation: no on set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.on = nil + assertOptsInvalid(t, opts, fmt.Errorf("on must be set")) + }) + + t.Run("validation: no on set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.on = &DatabaseRoleGrantOn{} + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of Database, Schema, or SchemaObject must be set")) + }) + + t.Run("validation: too many ons set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.on = &DatabaseRoleGrantOn{ + Database: &dbId, + Schema: &GrantOnSchema{ + Schema: Pointer(NewDatabaseObjectIdentifier("db1", "schema1")), + }, + } + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of Database, Schema, or SchemaObject must be set")) + }) + + t.Run("validation: grant on schema", func(t *testing.T) { + opts := defaultGrantsForSchema() + opts.on.Schema = &GrantOnSchema{} + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of Schema, AllSchemasInDatabase, or FutureSchemasInDatabase must be set")) + }) + + t.Run("validation: grant on schema object", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + opts.on.SchemaObject = &GrantOnSchemaObject{} + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of Object, AllIn or Future must be set")) + }) + + t.Run("validation: grant on schema object - all", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + opts.on = &DatabaseRoleGrantOn{ + SchemaObject: &GrantOnSchemaObject{ + All: &GrantOnSchemaObjectIn{ + PluralObjectType: PluralObjectTypeTables, + }, + }, + } + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of InDatabase, or InSchema must be set")) + }) + + t.Run("validation: grant on schema object - future", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + opts.on = &DatabaseRoleGrantOn{ + SchemaObject: &GrantOnSchemaObject{ + Future: &GrantOnSchemaObjectIn{ + PluralObjectType: PluralObjectTypeTables, + }, + }, + } + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of InDatabase, or InSchema must be set")) + }) + + t.Run("validation: unsupported database privilege", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.privileges.DatabasePrivileges = []AccountObjectPrivilege{AccountObjectPrivilegeCreateDatabaseRole} + assertOptsInvalid(t, opts, fmt.Errorf("privilege CREATE DATABASE ROLE is not allowed")) + }) + + t.Run("on database", func(t *testing.T) { + opts := defaultGrantsForDb() + assertOptsValidAndSQLEquals(t, opts, `GRANT CREATE SCHEMA ON DATABASE "db1" TO DATABASE ROLE "db1"."role1"`) + }) + + t.Run("on schema", func(t *testing.T) { + opts := defaultGrantsForSchema() + assertOptsValidAndSQLEquals(t, opts, `GRANT CREATE ALERT ON SCHEMA "db1"."schema1" TO DATABASE ROLE "db1"."role1"`) + }) + + t.Run("on all schemas in database", func(t *testing.T) { + opts := defaultGrantsForSchema() + opts.on.Schema = &GrantOnSchema{ + AllSchemasInDatabase: Pointer(NewAccountObjectIdentifier("db1")), + } + assertOptsValidAndSQLEquals(t, opts, `GRANT CREATE ALERT ON ALL SCHEMAS IN DATABASE "db1" TO DATABASE ROLE "db1"."role1"`) + }) + + t.Run("on all future schemas in database", func(t *testing.T) { + opts := defaultGrantsForSchema() + opts.on.Schema = &GrantOnSchema{ + FutureSchemasInDatabase: Pointer(NewAccountObjectIdentifier("db1")), + } + assertOptsValidAndSQLEquals(t, opts, `GRANT CREATE ALERT ON FUTURE SCHEMAS IN DATABASE "db1" TO DATABASE ROLE "db1"."role1"`) + }) + + t.Run("on schema object", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + assertOptsValidAndSQLEquals(t, opts, `GRANT APPLY ON TABLE "db1"."schema1"."table1" TO DATABASE ROLE "db1"."role1"`) + }) + + t.Run("on future schema object in database", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + opts.on.SchemaObject = &GrantOnSchemaObject{ + Future: &GrantOnSchemaObjectIn{ + PluralObjectType: PluralObjectTypeTables, + InDatabase: Pointer(NewAccountObjectIdentifier("db1")), + }, + } + assertOptsValidAndSQLEquals(t, opts, `GRANT APPLY ON FUTURE TABLES IN DATABASE "db1" TO DATABASE ROLE "db1"."role1"`) + }) + + t.Run("on future schema object in schema", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + opts.on.SchemaObject = &GrantOnSchemaObject{ + Future: &GrantOnSchemaObjectIn{ + PluralObjectType: PluralObjectTypeTables, + InSchema: Pointer(NewDatabaseObjectIdentifier("db1", "schema1")), + }, + } + assertOptsValidAndSQLEquals(t, opts, `GRANT APPLY ON FUTURE TABLES IN SCHEMA "db1"."schema1" TO DATABASE ROLE "db1"."role1"`) + }) +} + +func TestGrants_RevokePrivilegesFromDatabaseRoleRole(t *testing.T) { + dbId := NewAccountObjectIdentifier("db1") + + defaultGrantsForDb := func() *RevokePrivilegesFromDatabaseRoleOptions { + return &RevokePrivilegesFromDatabaseRoleOptions{ + privileges: &DatabaseRoleGrantPrivileges{ + DatabasePrivileges: []AccountObjectPrivilege{AccountObjectPrivilegeCreateSchema}, + }, + on: &DatabaseRoleGrantOn{ + Database: &dbId, + }, + databaseRole: NewDatabaseObjectIdentifier("db1", "role1"), + } + } + + defaultGrantsForSchema := func() *RevokePrivilegesFromDatabaseRoleOptions { + return &RevokePrivilegesFromDatabaseRoleOptions{ + privileges: &DatabaseRoleGrantPrivileges{ + SchemaPrivileges: []SchemaPrivilege{SchemaPrivilegeCreateAlert, SchemaPrivilegeAddSearchOptimization}, + }, + on: &DatabaseRoleGrantOn{ + Schema: &GrantOnSchema{ + Schema: Pointer(NewDatabaseObjectIdentifier("db1", "schema1")), + }, + }, + databaseRole: NewDatabaseObjectIdentifier("db1", "role1"), + } + } + + defaultGrantsForSchemaObject := func() *RevokePrivilegesFromDatabaseRoleOptions { + return &RevokePrivilegesFromDatabaseRoleOptions{ + privileges: &DatabaseRoleGrantPrivileges{ + SchemaObjectPrivileges: []SchemaObjectPrivilege{SchemaObjectPrivilegeSelect, SchemaObjectPrivilegeUpdate}, + }, + on: &DatabaseRoleGrantOn{ + SchemaObject: &GrantOnSchemaObject{ + SchemaObject: &Object{ + ObjectType: ObjectTypeTable, + Name: NewSchemaObjectIdentifier("db1", "schema1", "table1"), + }, + }, + }, + databaseRole: NewDatabaseObjectIdentifier("db1", "role1"), + } + } + + t.Run("validation: no privileges set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.privileges = nil + assertOptsInvalid(t, opts, fmt.Errorf("privileges must be set")) + }) + + t.Run("validation: no privileges set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.privileges = &DatabaseRoleGrantPrivileges{} + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of DatabasePrivileges, SchemaPrivileges, or SchemaObjectPrivileges must be set")) + }) + + t.Run("validation: too many privileges set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.privileges = &DatabaseRoleGrantPrivileges{ + DatabasePrivileges: []AccountObjectPrivilege{AccountObjectPrivilegeCreateSchema}, + SchemaPrivileges: []SchemaPrivilege{SchemaPrivilegeCreateAlert}, + } + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of DatabasePrivileges, SchemaPrivileges, or SchemaObjectPrivileges must be set")) + }) + + t.Run("validation: no on set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.on = nil + assertOptsInvalid(t, opts, fmt.Errorf("on must be set")) + }) + + t.Run("validation: no on set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.on = &DatabaseRoleGrantOn{} + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of Database, Schema, or SchemaObject must be set")) + }) + + t.Run("validation: too many ons set", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.on = &DatabaseRoleGrantOn{ + Database: &dbId, + Schema: &GrantOnSchema{ + Schema: Pointer(NewDatabaseObjectIdentifier("db1", "schema1")), + }, + } + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of Database, Schema, or SchemaObject must be set")) + }) + + t.Run("validation: grant on schema", func(t *testing.T) { + opts := defaultGrantsForSchema() + opts.on.Schema = &GrantOnSchema{} + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of Schema, AllSchemasInDatabase, or FutureSchemasInDatabase must be set")) + }) + + t.Run("validation: grant on schema object", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + opts.on.SchemaObject = &GrantOnSchemaObject{} + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of Object, AllIn or Future must be set")) + }) + + t.Run("validation: grant on schema object - all", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + opts.on = &DatabaseRoleGrantOn{ + SchemaObject: &GrantOnSchemaObject{ + All: &GrantOnSchemaObjectIn{ + PluralObjectType: PluralObjectTypeTables, + }, + }, + } + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of InDatabase, or InSchema must be set")) + }) + + t.Run("validation: grant on schema object - future", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + opts.on = &DatabaseRoleGrantOn{ + SchemaObject: &GrantOnSchemaObject{ + Future: &GrantOnSchemaObjectIn{ + PluralObjectType: PluralObjectTypeTables, + }, + }, + } + assertOptsInvalid(t, opts, fmt.Errorf("exactly one of InDatabase, or InSchema must be set")) + }) + + t.Run("validation: unsupported database privilege", func(t *testing.T) { + opts := defaultGrantsForDb() + opts.privileges.DatabasePrivileges = []AccountObjectPrivilege{AccountObjectPrivilegeCreateDatabaseRole} + assertOptsInvalid(t, opts, errors.New("privilege CREATE DATABASE ROLE is not allowed")) + }) + + t.Run("on database", func(t *testing.T) { + opts := defaultGrantsForDb() + assertOptsValidAndSQLEquals(t, opts, `REVOKE CREATE SCHEMA ON DATABASE "db1" FROM DATABASE ROLE "db1"."role1"`) + }) + + t.Run("on schema", func(t *testing.T) { + opts := defaultGrantsForSchema() + assertOptsValidAndSQLEquals(t, opts, `REVOKE CREATE ALERT, ADD SEARCH OPTIMIZATION ON SCHEMA "db1"."schema1" FROM DATABASE ROLE "db1"."role1"`) + }) + + t.Run("on all schemas in database + restrict", func(t *testing.T) { + opts := defaultGrantsForSchema() + opts.on.Schema = &GrantOnSchema{ + AllSchemasInDatabase: Pointer(NewAccountObjectIdentifier("db1")), + } + opts.Restrict = Bool(true) + assertOptsValidAndSQLEquals(t, opts, `REVOKE CREATE ALERT, ADD SEARCH OPTIMIZATION ON ALL SCHEMAS IN DATABASE "db1" FROM DATABASE ROLE "db1"."role1" RESTRICT`) + }) + + t.Run("on all future schemas in database + cascade", func(t *testing.T) { + opts := defaultGrantsForSchema() + opts.on.Schema = &GrantOnSchema{ + FutureSchemasInDatabase: Pointer(NewAccountObjectIdentifier("db1")), + } + opts.Cascade = Bool(true) + assertOptsValidAndSQLEquals(t, opts, `REVOKE CREATE ALERT, ADD SEARCH OPTIMIZATION ON FUTURE SCHEMAS IN DATABASE "db1" FROM DATABASE ROLE "db1"."role1" CASCADE`) + }) + + t.Run("on schema object", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + assertOptsValidAndSQLEquals(t, opts, `REVOKE SELECT, UPDATE ON TABLE "db1"."schema1"."table1" FROM DATABASE ROLE "db1"."role1"`) + }) + + t.Run("on future schema object in database", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + opts.on.SchemaObject = &GrantOnSchemaObject{ + Future: &GrantOnSchemaObjectIn{ + PluralObjectType: PluralObjectTypeTables, + InDatabase: Pointer(NewAccountObjectIdentifier("db1")), + }, + } + assertOptsValidAndSQLEquals(t, opts, `REVOKE SELECT, UPDATE ON FUTURE TABLES IN DATABASE "db1" FROM DATABASE ROLE "db1"."role1"`) + }) + + t.Run("on future schema object in schema", func(t *testing.T) { + opts := defaultGrantsForSchemaObject() + opts.on.SchemaObject = &GrantOnSchemaObject{ + Future: &GrantOnSchemaObjectIn{ + PluralObjectType: PluralObjectTypeTables, + InSchema: Pointer(NewDatabaseObjectIdentifier("db1", "schema1")), + }, + } + assertOptsValidAndSQLEquals(t, opts, `REVOKE SELECT, UPDATE ON FUTURE TABLES IN SCHEMA "db1"."schema1" FROM DATABASE ROLE "db1"."role1"`) }) } @@ -344,10 +665,7 @@ func TestGrantPrivilegeToShare(t *testing.T) { }, to: id, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("GRANT USAGE ON DATABASE %s TO SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "GRANT USAGE ON DATABASE %s TO SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) }) t.Run("on schema", func(t *testing.T) { @@ -359,10 +677,7 @@ func TestGrantPrivilegeToShare(t *testing.T) { }, to: id, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("GRANT USAGE ON SCHEMA %s TO SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "GRANT USAGE ON SCHEMA %s TO SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) }) t.Run("on table", func(t *testing.T) { @@ -376,10 +691,7 @@ func TestGrantPrivilegeToShare(t *testing.T) { }, to: id, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("GRANT USAGE ON TABLE %s TO SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "GRANT USAGE ON TABLE %s TO SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) }) t.Run("on all tables", func(t *testing.T) { @@ -393,10 +705,7 @@ func TestGrantPrivilegeToShare(t *testing.T) { }, to: id, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("GRANT USAGE ON ALL TABLES IN SCHEMA %s TO SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "GRANT USAGE ON ALL TABLES IN SCHEMA %s TO SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) }) t.Run("on view", func(t *testing.T) { @@ -408,10 +717,7 @@ func TestGrantPrivilegeToShare(t *testing.T) { }, to: id, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("GRANT USAGE ON VIEW %s TO SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "GRANT USAGE ON VIEW %s TO SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) }) } @@ -426,10 +732,7 @@ func TestRevokePrivilegeFromShare(t *testing.T) { }, from: id, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("REVOKE USAGE ON DATABASE %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "REVOKE USAGE ON DATABASE %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) }) t.Run("on schema", func(t *testing.T) { @@ -441,10 +744,7 @@ func TestRevokePrivilegeFromShare(t *testing.T) { }, from: id, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("REVOKE USAGE ON SCHEMA %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "REVOKE USAGE ON SCHEMA %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) }) t.Run("on table", func(t *testing.T) { @@ -458,10 +758,7 @@ func TestRevokePrivilegeFromShare(t *testing.T) { }, from: id, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("REVOKE USAGE ON TABLE %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "REVOKE USAGE ON TABLE %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) }) t.Run("on all tables", func(t *testing.T) { @@ -475,10 +772,7 @@ func TestRevokePrivilegeFromShare(t *testing.T) { }, from: id, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("REVOKE USAGE ON ALL TABLES IN SCHEMA %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "REVOKE USAGE ON ALL TABLES IN SCHEMA %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) }) t.Run("on view", func(t *testing.T) { @@ -492,10 +786,7 @@ func TestRevokePrivilegeFromShare(t *testing.T) { }, from: id, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("REVOKE USAGE ON VIEW %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "REVOKE USAGE ON VIEW %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) }) t.Run("on all views", func(t *testing.T) { @@ -509,20 +800,14 @@ func TestRevokePrivilegeFromShare(t *testing.T) { }, from: id, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("REVOKE USAGE ON ALL VIEWS IN SCHEMA %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "REVOKE USAGE ON ALL VIEWS IN SCHEMA %s FROM SHARE %s", otherID.FullyQualifiedName(), id.FullyQualifiedName()) }) } func TestGrantShow(t *testing.T) { t.Run("no options", func(t *testing.T) { opts := &ShowGrantOptions{} - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := "SHOW GRANTS" - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "SHOW GRANTS") }) t.Run("on account", func(t *testing.T) { @@ -531,10 +816,7 @@ func TestGrantShow(t *testing.T) { Account: Bool(true), }, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := "SHOW GRANTS ON ACCOUNT" - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "SHOW GRANTS ON ACCOUNT") }) t.Run("on database", func(t *testing.T) { @@ -547,10 +829,7 @@ func TestGrantShow(t *testing.T) { }, }, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("SHOW GRANTS ON DATABASE %s", dbID.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "SHOW GRANTS ON DATABASE %s", dbID.FullyQualifiedName()) }) t.Run("to role", func(t *testing.T) { @@ -560,10 +839,7 @@ func TestGrantShow(t *testing.T) { Role: roleID, }, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("SHOW GRANTS TO ROLE %s", roleID.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "SHOW GRANTS TO ROLE %s", roleID.FullyQualifiedName()) }) t.Run("to user", func(t *testing.T) { @@ -573,10 +849,7 @@ func TestGrantShow(t *testing.T) { User: userID, }, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("SHOW GRANTS TO USER %s", userID.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "SHOW GRANTS TO USER %s", userID.FullyQualifiedName()) }) t.Run("to share", func(t *testing.T) { @@ -586,10 +859,7 @@ func TestGrantShow(t *testing.T) { Share: shareID, }, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("SHOW GRANTS TO SHARE %s", shareID.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "SHOW GRANTS TO SHARE %s", shareID.FullyQualifiedName()) }) t.Run("of role", func(t *testing.T) { @@ -599,10 +869,7 @@ func TestGrantShow(t *testing.T) { Role: roleID, }, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("SHOW GRANTS OF ROLE %s", roleID.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "SHOW GRANTS OF ROLE %s", roleID.FullyQualifiedName()) }) t.Run("of share", func(t *testing.T) { @@ -612,9 +879,6 @@ func TestGrantShow(t *testing.T) { Share: shareID, }, } - actual, err := structToSQL(opts) - require.NoError(t, err) - expected := fmt.Sprintf("SHOW GRANTS OF SHARE %s", shareID.FullyQualifiedName()) - assert.Equal(t, expected, actual) + assertOptsValidAndSQLEquals(t, opts, "SHOW GRANTS OF SHARE %s", shareID.FullyQualifiedName()) }) } diff --git a/pkg/sdk/grants_validations.go b/pkg/sdk/grants_validations.go new file mode 100644 index 0000000000..0e83919bb6 --- /dev/null +++ b/pkg/sdk/grants_validations.go @@ -0,0 +1,279 @@ +package sdk + +import ( + "fmt" + + // TODO: change to slices with go 1.21 + "golang.org/x/exp/slices" +) + +var ( + _ validatable = new(GrantPrivilegesToAccountRoleOptions) + _ validatable = new(RevokePrivilegesFromAccountRoleOptions) + _ validatable = new(GrantPrivilegesToDatabaseRoleOptions) + _ validatable = new(RevokePrivilegesFromDatabaseRoleOptions) + _ validatable = new(grantPrivilegeToShareOptions) + _ validatable = new(revokePrivilegeFromShareOptions) + _ validatable = new(ShowGrantOptions) +) + +func (opts *GrantPrivilegesToAccountRoleOptions) validate() error { + if !valueSet(opts.privileges) { + return fmt.Errorf("privileges must be set") + } + if err := opts.privileges.validate(); err != nil { + return err + } + if !valueSet(opts.on) { + return fmt.Errorf("on must be set") + } + if err := opts.on.validate(); err != nil { + return err + } + return nil +} + +func (v *AccountRoleGrantPrivileges) validate() error { + if !exactlyOneValueSet(v.AllPrivileges, v.GlobalPrivileges, v.AccountObjectPrivileges, v.SchemaPrivileges, v.SchemaObjectPrivileges) { + return fmt.Errorf("exactly one of AllPrivileges, GlobalPrivileges, AccountObjectPrivileges, SchemaPrivileges, or SchemaObjectPrivileges must be set") + } + return nil +} + +func (v *AccountRoleGrantOn) validate() error { + if !exactlyOneValueSet(v.Account, v.AccountObject, v.Schema, v.SchemaObject) { + return fmt.Errorf("exactly one of Account, AccountObject, Schema, or SchemaObject must be set") + } + if valueSet(v.AccountObject) { + if err := v.AccountObject.validate(); err != nil { + return err + } + } + if valueSet(v.Schema) { + if err := v.Schema.validate(); err != nil { + return err + } + } + if valueSet(v.SchemaObject) { + if err := v.SchemaObject.validate(); err != nil { + return err + } + } + return nil +} + +func (v *GrantOnAccountObject) validate() error { + if !exactlyOneValueSet(v.User, v.ResourceMonitor, v.Warehouse, v.Database, v.Integration, v.FailoverGroup, v.ReplicationGroup) { + return fmt.Errorf("exactly one of User, ResourceMonitor, Warehouse, Database, Integration, FailoverGroup, or ReplicationGroup must be set") + } + return nil +} + +func (v *GrantOnSchema) validate() error { + if !exactlyOneValueSet(v.Schema, v.AllSchemasInDatabase, v.FutureSchemasInDatabase) { + return fmt.Errorf("exactly one of Schema, AllSchemasInDatabase, or FutureSchemasInDatabase must be set") + } + return nil +} + +func (v *GrantOnSchemaObject) validate() error { + if !exactlyOneValueSet(v.SchemaObject, v.All, v.Future) { + return fmt.Errorf("exactly one of Object, AllIn or Future must be set") + } + if valueSet(v.All) { + if err := v.All.validate(); err != nil { + return err + } + } + if valueSet(v.Future) { + if err := v.Future.validate(); err != nil { + return err + } + } + return nil +} + +func (v *GrantOnSchemaObjectIn) validate() error { + if !exactlyOneValueSet(v.InDatabase, v.InSchema) { + return fmt.Errorf("exactly one of InDatabase, or InSchema must be set") + } + return nil +} + +func (opts *RevokePrivilegesFromAccountRoleOptions) validate() error { + if !valueSet(opts.privileges) { + return fmt.Errorf("privileges must be set") + } + if err := opts.privileges.validate(); err != nil { + return err + } + if !valueSet(opts.on) { + return fmt.Errorf("on must be set") + } + if err := opts.on.validate(); err != nil { + return err + } + if !validObjectidentifier(opts.accountRole) { + return ErrInvalidObjectIdentifier + } + if everyValueSet(opts.Restrict, opts.Cascade) { + return fmt.Errorf("either Restrict or Cascade can be set, or neither but not both") + } + return nil +} + +func (opts *GrantPrivilegesToDatabaseRoleOptions) validate() error { + if !valueSet(opts.privileges) { + return fmt.Errorf("privileges must be set") + } + if err := opts.privileges.validate(); err != nil { + return err + } + if !valueSet(opts.on) { + return fmt.Errorf("on must be set") + } + if err := opts.on.validate(); err != nil { + return err + } + return nil +} + +func (v *DatabaseRoleGrantPrivileges) validate() error { + if !exactlyOneValueSet(v.DatabasePrivileges, v.SchemaPrivileges, v.SchemaObjectPrivileges) { + return fmt.Errorf("exactly one of DatabasePrivileges, SchemaPrivileges, or SchemaObjectPrivileges must be set") + } + if valueSet(v.DatabasePrivileges) { + allowedPrivileges := []AccountObjectPrivilege{ + AccountObjectPrivilegeCreateSchema, + AccountObjectPrivilegeModify, + AccountObjectPrivilegeMonitor, + AccountObjectPrivilegeUsage, + } + for _, p := range v.DatabasePrivileges { + if !slices.Contains(allowedPrivileges, p) { + return fmt.Errorf("privilege %s is not allowed", p.String()) + } + } + } + return nil +} + +func (v *DatabaseRoleGrantOn) validate() error { + if !exactlyOneValueSet(v.Database, v.Schema, v.SchemaObject) { + return fmt.Errorf("exactly one of Database, Schema, or SchemaObject must be set") + } + if valueSet(v.Schema) { + if err := v.Schema.validate(); err != nil { + return err + } + } + if valueSet(v.SchemaObject) { + if err := v.SchemaObject.validate(); err != nil { + return err + } + } + return nil +} + +func (opts *RevokePrivilegesFromDatabaseRoleOptions) validate() error { + if !valueSet(opts.privileges) { + return fmt.Errorf("privileges must be set") + } + if err := opts.privileges.validate(); err != nil { + return err + } + if !valueSet(opts.on) { + return fmt.Errorf("on must be set") + } + if err := opts.on.validate(); err != nil { + return err + } + if !validObjectidentifier(opts.databaseRole) { + return ErrInvalidObjectIdentifier + } + if everyValueSet(opts.Restrict, opts.Cascade) { + return fmt.Errorf("either Restrict or Cascade can be set, or neither but not both") + } + return nil +} + +func (opts *grantPrivilegeToShareOptions) validate() error { + if !validObjectidentifier(opts.to) { + return ErrInvalidObjectIdentifier + } + if !valueSet(opts.On) || opts.privilege == "" { + return fmt.Errorf("on and privilege are required") + } + if err := opts.On.validate(); err != nil { + return err + } + return nil +} + +func (v *GrantPrivilegeToShareOn) validate() error { + if !exactlyOneValueSet(v.Database, v.Schema, v.Function, v.Table, v.View) { + return fmt.Errorf("only one of database, schema, function, table, or view can be set") + } + if valueSet(v.Table) { + if err := v.Table.validate(); err != nil { + return err + } + } + return nil +} + +func (v *OnTable) validate() error { + if !exactlyOneValueSet(v.Name, v.AllInSchema) { + return fmt.Errorf("only one of name or allInSchema can be set") + } + return nil +} + +func (opts *revokePrivilegeFromShareOptions) validate() error { + if !validObjectidentifier(opts.from) { + return ErrInvalidObjectIdentifier + } + if !valueSet(opts.On) || opts.privilege == "" { + return fmt.Errorf("on and privilege are required") + } + if !exactlyOneValueSet(opts.On.Database, opts.On.Schema, opts.On.Table, opts.On.View) { + return fmt.Errorf("only one of database, schema, function, table, or view can be set") + } + + if err := opts.On.validate(); err != nil { + return err + } + + return nil +} + +func (v *RevokePrivilegeFromShareOn) validate() error { + if !exactlyOneValueSet(v.Database, v.Schema, v.Table, v.View) { + return fmt.Errorf("only one of database, schema, table, or view can be set") + } + if valueSet(v.Table) { + return v.Table.validate() + } + if valueSet(v.View) { + return v.View.validate() + } + return nil +} + +func (v *OnView) validate() error { + if !exactlyOneValueSet(v.Name, v.AllInSchema) { + return fmt.Errorf("only one of name or allInSchema can be set") + } + return nil +} + +// TODO: add validations for ShowGrantsOn, ShowGrantsTo, ShowGrantsOf and ShowGrantsIn +func (opts *ShowGrantOptions) validate() error { + if everyValueNil(opts.On, opts.To, opts.Of, opts.In) { + return nil + } + if !exactlyOneValueSet(opts.On, opts.To, opts.Of, opts.In) { + return fmt.Errorf("only one of [on, to, of, in] can be set") + } + return nil +} diff --git a/pkg/sdk/helper_test.go b/pkg/sdk/helper_test.go index 640d967dc8..14415f1b17 100644 --- a/pkg/sdk/helper_test.go +++ b/pkg/sdk/helper_test.go @@ -504,6 +504,29 @@ func createRole(t *testing.T, client *Client) (*Role, func()) { } } +func createDatabaseRole(t *testing.T, client *Client, database *Database) (*DatabaseRole, func()) { + t.Helper() + name := randomString(t) + id := NewDatabaseObjectIdentifier(database.Name, name) + ctx := context.Background() + + err := client.DatabaseRoles.Create(ctx, NewCreateDatabaseRoleRequest(id)) + require.NoError(t, err) + + databaseRole, err := client.DatabaseRoles.ShowByID(ctx, id) + require.NoError(t, err) + + return databaseRole, cleanupDatabaseRoleProvider(t, ctx, client, id) +} + +func cleanupDatabaseRoleProvider(t *testing.T, ctx context.Context, client *Client, id DatabaseObjectIdentifier) func() { + t.Helper() + return func() { + err := client.DatabaseRoles.Drop(ctx, NewDropDatabaseRoleRequest(id)) + require.NoError(t, err) + } +} + func createMaskingPolicy(t *testing.T, client *Client, database *Database, schema *Schema) (*MaskingPolicy, func()) { t.Helper() signature := []TableColumnSignature{