diff --git a/Makefile b/Makefile index 110e9ee284..b344bc6691 100644 --- a/Makefile +++ b/Makefile @@ -155,6 +155,12 @@ protobuild: @protoc-go-inject-tag -input=./internal/oplog/oplog_test/oplog_test.pb.go @protoc-go-inject-tag -input=./internal/iam/store/group_member.pb.go @protoc-go-inject-tag -input=./internal/iam/store/role.pb.go + @protoc-go-inject-tag -input=./internal/iam/store/role_global_individual_org_grant_scope.pb.go + @protoc-go-inject-tag -input=./internal/iam/store/role_global_individual_project_grant_scope.pb.go + @protoc-go-inject-tag -input=./internal/iam/store/role_org_individual_grant_scope.pb.go + @protoc-go-inject-tag -input=./internal/iam/store/role_global.pb.go + @protoc-go-inject-tag -input=./internal/iam/store/role_org.pb.go + @protoc-go-inject-tag -input=./internal/iam/store/role_project.pb.go @protoc-go-inject-tag -input=./internal/iam/store/principal_role.pb.go @protoc-go-inject-tag -input=./internal/iam/store/role_grant.pb.go @protoc-go-inject-tag -input=./internal/iam/store/role_grant_scope.pb.go diff --git a/api/scopes/option.gen.go b/api/scopes/option.gen.go index aef8586a37..96b591c074 100644 --- a/api/scopes/option.gen.go +++ b/api/scopes/option.gen.go @@ -132,6 +132,30 @@ func WithRecursive(recurse bool) Option { } } +func WithCreateAdminRole(inCreateAdminRole bool) Option { + return func(o *options) { + o.postMap["create_admin_role"] = inCreateAdminRole + } +} + +func DefaultCreateAdminRole() Option { + return func(o *options) { + o.postMap["create_admin_role"] = nil + } +} + +func WithCreateDefaultRole(inCreateDefaultRole bool) Option { + return func(o *options) { + o.postMap["create_default_role"] = inCreateDefaultRole + } +} + +func DefaultCreateDefaultRole() Option { + return func(o *options) { + o.postMap["create_default_role"] = nil + } +} + func WithDescription(inDescription string) Option { return func(o *options) { o.postMap["description"] = inDescription diff --git a/globals/globals.go b/globals/globals.go index 117d0377c1..97cf71c171 100644 --- a/globals/globals.go +++ b/globals/globals.go @@ -18,6 +18,7 @@ const ( GrantScopeThis = "this" GrantScopeChildren = "children" GrantScopeDescendants = "descendants" + GrantScopeIndividual = "individual" // CorrelationIdKey defines the http header and grpc metadata key used for specifying a // correlation id. When getting the correlationId (from the http header or grpc metadata) diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index 0f497ec2b3..6ca012d3c0 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -228,6 +228,18 @@ var inputStructs = []*structInfo{ FieldType: "bool", Query: true, }, + { + Name: "CreateAdminRole", + ProtoName: "create_admin_role", + FieldType: "bool", + Query: false, + }, + { + Name: "CreateDefaultRole", + ProtoName: "create_default_role", + FieldType: "bool", + Query: false, + }, }, versionEnabled: true, createResponseTypes: []string{CreateResponseType, ReadResponseType, UpdateResponseType, DeleteResponseType, ListResponseType}, diff --git a/internal/cmd/base/dev.go b/internal/cmd/base/dev.go index e8881d5d3e..3eb5d092df 100644 --- a/internal/cmd/base/dev.go +++ b/internal/cmd/base/dev.go @@ -160,10 +160,7 @@ func (b *Server) CreateDevDatabase(ctx context.Context, opt ...Option) error { return nil } - if _, _, err := b.CreateInitialScopes(ctx, WithIamOptions( - iam.WithSkipAdminRoleCreation(true), - iam.WithSkipDefaultRoleCreation(true), - )); err != nil { + if _, _, err := b.CreateInitialScopes(ctx); err != nil { return err } diff --git a/internal/cmd/commands/database/init.go b/internal/cmd/commands/database/init.go index 486b918cc6..7ac37eaaab 100644 --- a/internal/cmd/commands/database/init.go +++ b/internal/cmd/commands/database/init.go @@ -11,7 +11,9 @@ import ( "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/internal/cmd/config" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/version" "github.com/hashicorp/go-secure-stdlib/mlock" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/mitchellh/cli" @@ -44,6 +46,8 @@ type InitCommand struct { flagMigrationUrl string flagSkipInitialLoginRoleCreation bool flagSkipInitialAuthenticatedUserRoleCreation bool + flagCreateInitialLoginRole bool + flagCreateInitialAuthenticatedUserRole bool flagSkipAuthMethodCreation bool flagSkipScopesCreation bool flagSkipHostResourcesCreation bool @@ -395,7 +399,14 @@ func (c *InitCommand) Run(args []string) (retCode int) { return base.CommandSuccess } - orgScope, projScope, err := c.CreateInitialScopes(c.Context) + iamOpts := []iam.Option{} + if version.SupportsFeature(version.Binary, version.CreateDefaultAndAdminRoles) { + iamOpts = []iam.Option{ + iam.WithCreateAdminRole(c.flagCreateInitialAuthenticatedUserRole), + iam.WithCreateDefaultRole(c.flagCreateInitialLoginRole), + } + } + orgScope, projScope, err := c.CreateInitialScopes(c.Context, base.WithIamOptions(iamOpts...)) if err != nil { c.UI.Error(fmt.Errorf("Error creating initial scopes: %w", err).Error()) return base.CommandCliError diff --git a/internal/cmd/commands/scopescmd/funcs.go b/internal/cmd/commands/scopescmd/funcs.go index b33652e5cb..801c43cc42 100644 --- a/internal/cmd/commands/scopescmd/funcs.go +++ b/internal/cmd/commands/scopescmd/funcs.go @@ -11,12 +11,15 @@ import ( "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/scopes" "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/version" ) const ( flagPrimaryAuthMethodIdName = "primary-auth-method-id" flagSkipAdminRoleCreationName = "skip-admin-role-creation" flagSkipDefaultRoleCreationName = "skip-default-role-creation" + flagCreateAdminRoleName = "create-admin-role" + flagCreateDefaultRoleName = "create-default-role" flagStoragePolicyIdName = "storage-policy-id" ) @@ -28,17 +31,25 @@ func init() { } func extraActionsFlagsMapFuncImpl() map[string][]string { - return map[string][]string{ - "create": {flagSkipAdminRoleCreationName, flagSkipDefaultRoleCreationName}, + extraActionsFlagsMap := map[string][]string{ "update": {flagPrimaryAuthMethodIdName}, "attach-storage-policy": {"id", "version", flagStoragePolicyIdName}, "detach-storage-policy": {"id", "version"}, } + if version.SupportsFeature(version.Binary, version.CreateDefaultAndAdminRoles) { + extraActionsFlagsMap["create"] = append(extraActionsFlagsMap["create"], flagCreateAdminRoleName, flagCreateDefaultRoleName) + } + if version.SupportsFeature(version.Binary, version.SkipDefaultAndAdminRoleCreation) { + extraActionsFlagsMap["create"] = append(extraActionsFlagsMap["create"], flagSkipAdminRoleCreationName, flagSkipDefaultRoleCreationName) + } + return extraActionsFlagsMap } type extraCmdVars struct { flagSkipAdminRoleCreation bool flagSkipDefaultRoleCreation bool + flagCreateAdminRole bool + flagCreateDefaultRole bool flagPrimaryAuthMethodId string flagStoragePolicyId string } @@ -50,13 +61,13 @@ func extraFlagsFuncImpl(c *Command, set *base.FlagSets, f *base.FlagSet) { f.BoolVar(&base.BoolVar{ Name: flagSkipAdminRoleCreationName, Target: &c.flagSkipAdminRoleCreation, - Usage: "If set, a role granting the current user access to administer the newly-created scope will not automatically be created", + Usage: "Deprecated: If set, a role granting the current user access to administer the newly-created scope will not automatically be created", }) case flagSkipDefaultRoleCreationName: f.BoolVar(&base.BoolVar{ Name: flagSkipDefaultRoleCreationName, Target: &c.flagSkipDefaultRoleCreation, - Usage: "If set, a role granting the anonymous user access to log into auth methods and a few other actions within the newly-created scope will not automatically be created", + Usage: "Deprecated: If set, a role granting the anonymous user access to log into auth methods and a few other actions within the newly-created scope will not automatically be created", }) case flagPrimaryAuthMethodIdName: f.StringVar(&base.StringVar{ @@ -70,6 +81,18 @@ func extraFlagsFuncImpl(c *Command, set *base.FlagSets, f *base.FlagSet) { Target: &c.flagStoragePolicyId, Usage: "The public ID of the Storage Policy to attach to this scope. Can only attach to the global scope and an Org scope.", }) + case flagCreateAdminRoleName: + f.BoolVar(&base.BoolVar{ + Name: flagCreateAdminRoleName, + Target: &c.flagCreateAdminRole, + Usage: "If set, a role granting the current user access to administer the newly-created scope will automatically be created", + }) + case flagCreateDefaultRoleName: + f.BoolVar(&base.BoolVar{ + Name: flagCreateDefaultRoleName, + Target: &c.flagCreateDefaultRole, + Usage: "If set, a role granting the anonymous user access to log into auth methods and a few other actions within the newly-created scope will automatically be created", + }) } } } @@ -84,12 +107,38 @@ func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]scopes.Opt } } - if c.flagSkipAdminRoleCreation { - *opts = append(*opts, scopes.WithSkipAdminRoleCreation(c.flagSkipAdminRoleCreation)) + if version.SupportsFeature(version.Binary, version.CreateDefaultAndAdminRoles) { + if c.flagCreateAdminRole && c.flagSkipAdminRoleCreation { + c.UI.Error("Cannot set both --create-admin-role and --skip-admin-role-creation to true") + } + if c.flagCreateDefaultRole && c.flagSkipDefaultRoleCreation { + c.UI.Error("Cannot set both --create-default-role and --skip-default-role-creation to true") + } + if !c.flagCreateAdminRole && !c.flagSkipAdminRoleCreation { + c.UI.Output("Warning: --skip-admin-role-creation is deprecated and will be removed in a future version. Use --create-admin-role instead.") + *opts = append(*opts, scopes.WithSkipAdminRoleCreation(!c.flagSkipAdminRoleCreation)) + } + if !c.flagCreateDefaultRole && !c.flagSkipDefaultRoleCreation { + c.UI.Output("Warning: --skip-default-role-creation is deprecated and will be removed in a future version. Use --create-default-role instead.") + *opts = append(*opts, scopes.WithSkipDefaultRoleCreation(!c.flagSkipDefaultRoleCreation)) + } + if c.flagCreateAdminRole { + *opts = append(*opts, scopes.WithCreateAdminRole(c.flagCreateAdminRole)) + } + if c.flagCreateDefaultRole { + *opts = append(*opts, scopes.WithCreateDefaultRole(c.flagCreateDefaultRole)) + } } - if c.flagSkipDefaultRoleCreation { - *opts = append(*opts, scopes.WithSkipDefaultRoleCreation(c.flagSkipDefaultRoleCreation)) + + if version.SupportsFeature(version.Binary, version.SkipDefaultAndAdminRoleCreation) { + if c.flagSkipAdminRoleCreation { + *opts = append(*opts, scopes.WithSkipAdminRoleCreation(c.flagSkipAdminRoleCreation)) + } + if c.flagSkipDefaultRoleCreation { + *opts = append(*opts, scopes.WithSkipDefaultRoleCreation(c.flagSkipDefaultRoleCreation)) + } } + if c.flagPrimaryAuthMethodId != "" { *opts = append(*opts, scopes.WithPrimaryAuthMethodId(c.flagPrimaryAuthMethodId)) } diff --git a/internal/daemon/controller/handlers/roles/role_service_test.go b/internal/daemon/controller/handlers/roles/role_service_test.go index 17c0535d52..87bf183cc6 100644 --- a/internal/daemon/controller/handlers/roles/role_service_test.go +++ b/internal/daemon/controller/handlers/roles/role_service_test.go @@ -220,7 +220,7 @@ func TestList(t *testing.T) { var totalRoles []*pb.Role for i := 0; i < 10; i++ { or := iam.TestRole(t, conn, oWithRoles.GetPublicId()) - _ = iam.TestRoleGrantScope(t, conn, or.GetPublicId(), globals.GrantScopeChildren) + _ = iam.TestRoleGrantScope(t, conn, or, globals.GrantScopeChildren) wantOrgRoles = append(wantOrgRoles, &pb.Role{ Id: or.GetPublicId(), ScopeId: or.GetScopeId(), @@ -2791,7 +2791,7 @@ func TestAddGrantScopes(t *testing.T) { assert, require := assert.New(t), require.New(t) role := iam.TestRole(t, conn, tc.scopeId, iam.WithGrantScopeIds([]string{"testing-none"})) for _, e := range tc.existing { - _ = iam.TestRoleGrantScope(t, conn, role.GetPublicId(), e) + _ = iam.TestRoleGrantScope(t, conn, role, e) } req := &pbs.AddRoleGrantScopesRequest{ Id: role.GetPublicId(), @@ -3129,7 +3129,7 @@ func TestSetGrantScopes(t *testing.T) { assert, require := assert.New(t), require.New(t) role := iam.TestRole(t, conn, tc.scopeId, iam.WithGrantScopeIds([]string{"testing-none"})) for _, e := range tc.existing { - _ = iam.TestRoleGrantScope(t, conn, role.GetPublicId(), e) + _ = iam.TestRoleGrantScope(t, conn, role, e) } req := &pbs.SetRoleGrantScopesRequest{ Id: role.GetPublicId(), @@ -3326,7 +3326,7 @@ func TestRemoveGrantScopes(t *testing.T) { assert, require := assert.New(t), require.New(t) role := iam.TestRole(t, conn, tc.scopeId, iam.WithGrantScopeIds([]string{"testing-none"})) for _, e := range tc.existing { - _ = iam.TestRoleGrantScope(t, conn, role.GetPublicId(), e) + _ = iam.TestRoleGrantScope(t, conn, role, e) } req := &pbs.RemoveRoleGrantScopesRequest{ Id: role.GetPublicId(), diff --git a/internal/daemon/controller/handlers/scopes/scope_service.go b/internal/daemon/controller/handlers/scopes/scope_service.go index a04a56df04..dfd7f5c046 100644 --- a/internal/daemon/controller/handlers/scopes/scope_service.go +++ b/internal/daemon/controller/handlers/scopes/scope_service.go @@ -42,6 +42,7 @@ import ( "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/boundary/internal/util" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" + "github.com/hashicorp/boundary/version" wrappingKms "github.com/hashicorp/go-kms-wrapping/extras/kms/v2" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -650,8 +651,25 @@ func (s *Service) createInRepo(ctx context.Context, authResults auth.VerifyResul if item.GetDescription() != nil { opts = append(opts, iam.WithDescription(item.GetDescription().GetValue())) } - opts = append(opts, iam.WithSkipAdminRoleCreation(req.GetSkipAdminRoleCreation())) - opts = append(opts, iam.WithSkipDefaultRoleCreation(req.GetSkipDefaultRoleCreation())) + + if req.GetCreateDefaultRole() && req.GetSkipDefaultRoleCreation() { + return nil, handlers.InvalidArgumentErrorf("Cannot set both create_default_role and skip_default_role_creation to true.", map[string]string{"create_default_role": "Cannot set both create_default_role and skip_default_role_creation to true."}) + } + if !req.GetCreateDefaultRole() && !req.GetSkipDefaultRoleCreation() { + } + + if version.SupportsFeature(version.Binary, version.CreateDefaultAndAdminRoles) { + opts = append(opts, + iam.WithCreateAdminRole(req.GetCreateAdminRole()), + iam.WithCreateDefaultRole(req.GetCreateDefaultRole()), + ) + } + if version.SupportsFeature(version.Binary, version.SkipDefaultAndAdminRoleCreation) { + opts = append(opts, + iam.WithSkipAdminRoleCreation(req.GetSkipAdminRoleCreation()), + iam.WithSkipDefaultRoleCreation(req.GetSkipDefaultRoleCreation()), + ) + } parentScope := authResults.Scope var iamScope *iam.Scope diff --git a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go index 7079a10ded..018044aadd 100644 --- a/internal/daemon/controller/handlers/targets/tcp/target_service_test.go +++ b/internal/daemon/controller/handlers/targets/tcp/target_service_test.go @@ -1295,7 +1295,7 @@ func TestCreate(t *testing.T) { r := iam.TestRole(t, conn, "global") _ = iam.TestUserRole(t, conn, r.GetPublicId(), at.GetIamUserId()) _ = iam.TestRoleGrant(t, conn, r.GetPublicId(), "ids=*;type=*;actions=*") - _ = iam.TestRoleGrantScope(t, conn, r.GetPublicId(), globals.GrantScopeDescendants) + _ = iam.TestRoleGrantScope(t, conn, r, globals.GrantScopeDescendants) // Ensure we are using the OSS worker filter function. This prevents us from // running tests in parallel. diff --git a/internal/db/schema/migrations/oss/postgres/0/06_iam.up.sql b/internal/db/schema/migrations/oss/postgres/0/06_iam.up.sql index 4f5816fd2e..254a44d736 100644 --- a/internal/db/schema/migrations/oss/postgres/0/06_iam.up.sql +++ b/internal/db/schema/migrations/oss/postgres/0/06_iam.up.sql @@ -332,6 +332,7 @@ create table iam_role ( ); -- Grants are immutable, which is enforced via the trigger below + -- Altered in 100/05_iam_grant.up.sql to add constraint on canonical_grant create table iam_role_grant ( create_time wt_timestamp, role_id wt_role_id -- pk diff --git a/internal/db/schema/migrations/oss/postgres/100/01_iam_role_global.up.sql b/internal/db/schema/migrations/oss/postgres/100/01_iam_role_global.up.sql new file mode 100644 index 0000000000..88b6be1ed4 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/100/01_iam_role_global.up.sql @@ -0,0 +1,252 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + + -- Create the enumeration table for the grant scope types for the global iam_role + create table iam_role_global_grant_scope_enm ( + name text primary key + constraint only_predefined_scope_types_allowed + check( + name in ( + 'descendants', + 'children', + 'individual' + ) + ) + ); + comment on table iam_role_global_grant_scope_enm is + 'iam_role_global_grant_scope_enm is an enumeration table for role grant scope types for the iam_role_global table.'; + + -- Insert the predefined grant scope types for iam_role_global + insert into iam_role_global_grant_scope_enm (name) + values + ('descendants'), + ('children'), + ('individual'); + + create function insert_role_subtype() returns trigger + as $$ + begin + insert into iam_role + (public_id, scope_id) + values + (new.public_id, new.scope_id); + return new; + end; + $$ language plpgsql; + comment on function insert_role_subtype() is + 'insert_role_subtype is used to automatically insert a row into the iam_role table ' + 'whenever a row is inserted into the subtype table'; + + create function insert_grant_scope_update_time() returns trigger + as $$ + begin + if new.grant_scope is distinct from old.grant_scope then + new.grant_scope_update_time = now(); + end if; + return new; + end; + $$ language plpgsql; + comment on function insert_grant_scope_update_time() is + 'insert_grant_scope_update_time is used to automatically update the grant_scope_update_time ' + 'of the subtype table whenever the grant_scope column is updated'; + + create function insert_grant_this_role_scope_update_time() returns trigger + as $$ + begin + if new.grant_this_role_scope is distinct from old.grant_this_role_scope then + new.grant_this_role_scope_update_time = now(); + end if; + return new; + end; + $$ language plpgsql; + comment on function insert_grant_this_role_scope_update_time() is + 'insert_grant_this_role_scope_update_time is used to automatically update the grant_scope_update_time ' + 'of the subtype table whenever the grant_this_role_scope column is updated'; + + +-- Add trigger to update the new update_time column on every iam_role subtype update. + create function update_iam_role_table_update_time() returns trigger + as $$ + begin + update iam_role set update_time = new.update_time where public_id = new.public_id; + return new; + end; + $$ language plpgsql; + comment on function update_iam_role_table_update_time() is + 'update_iam_role_table_update_time is used to automatically update the update_time ' + 'of the base table whenever one of the subtype iam_role tables are updated'; + + create function delete_iam_role_subtype() returns trigger + as $$ + begin + delete + from iam_role + where public_id = old.public_id; + return null; -- result is ignored since this is an after trigger + end; + $$ language plpgsql; + comment on function delete_iam_role_subtype() is + 'delete_iam_role_subtype is used to automatically delete associated iam_role entry' + 'since domain implementation performs deletion on the child table which does not cleanup the base iam_role table '; + + -- global iam_role must have a scope_id of global. + -- + -- grant_this_role_scope indicates if the role can apply its grants to the scope. + -- grant_scope indicates the scope of the grants. + -- grant_scope can be 'descendants', 'children', or 'individual'. + -- + -- grant_this_role_scope_update_time and grant_scope_update_time are used to track + -- the last time the grant_this_role_scope and grant_scope columns were updated. + -- This is used to represent the grant scope create_time column from the + -- iam_role_grant_scope table in 83/01_iam_role_grant_scope.up.sql. + -- This matches the representation of the existing create_time field at the + -- role domain layer that indicates when the grant scope was created. + create table iam_role_global ( + public_id wt_role_id primary key + constraint iam_role_fkey + references iam_role(public_id) + on delete cascade + on update cascade, + scope_id wt_scope_id not null + constraint iam_scope_global_fkey + references iam_scope_global(scope_id) + on delete cascade + on update cascade, + name text, + description text, + grant_this_role_scope boolean not null default false, + grant_scope text not null + constraint iam_role_global_grant_scope_enm_fkey + references iam_role_global_grant_scope_enm(name) + on delete restrict + on update cascade, + version wt_version, + grant_this_role_scope_update_time wt_timestamp, + grant_scope_update_time wt_timestamp, + create_time wt_timestamp, + update_time wt_timestamp, + constraint iam_role_global_grant_scope_public_id_uq + unique(grant_scope, public_id), + constraint iam_role_global_name_scope_id_uq + unique(name, scope_id) + ); + comment on table iam_role_global is + 'iam_role_global is the subtype table for the global role. grant_this_role_scope_update_time and grant_scope_update_time are used to track the last time the grant_this_role_scope and grant_scope columns were updated.'; + + create trigger insert_role_subtype before insert on iam_role_global + for each row execute procedure insert_role_subtype(); + + create trigger insert_grant_scope_update_time before insert on iam_role_global + for each row execute procedure insert_grant_scope_update_time(); + + create trigger insert_grant_this_role_scope_update_time before insert on iam_role_global + for each row execute procedure insert_grant_this_role_scope_update_time(); + + create trigger update_iam_role_global_grant_scope_update_time before update on iam_role_global + for each row execute procedure insert_grant_scope_update_time(); + + create trigger update_iam_role_global_grant_this_role_scope_update_time before update on iam_role_global + for each row execute procedure insert_grant_this_role_scope_update_time(); + + create trigger update_iam_role_global_base_table_update_time after update on iam_role_global + for each row execute procedure update_iam_role_table_update_time(); + + create trigger delete_iam_role_subtype after delete on iam_role_global + for each row execute procedure delete_iam_role_subtype(); + + create trigger default_create_time_column before insert on iam_role_global + for each row execute procedure default_create_time(); + + create trigger update_time_column before update on iam_role_global + for each row execute procedure update_time_column(); + + create trigger update_version_column after update on iam_role_global + for each row execute procedure update_version_column(); + + create trigger immutable_columns before update on iam_role_global + for each row execute procedure immutable_columns('scope_id', 'create_time'); + + + create table iam_role_global_individual_org_grant_scope ( + role_id wt_role_id + constraint iam_role_global_fkey + references iam_role_global(public_id) + on delete cascade + on update cascade, + scope_id wt_scope_id not null + constraint iam_scope_org_fkey + references iam_scope_org(scope_id) + on delete cascade + on update cascade, + -- grant_scope is used for constraint checking. + -- This restricts the grant_scope to be 'individual' + -- and since it is also a foreign key to the iam_role_global + -- grant_scope, it ensures that iam_role_global is set to 'individual' + -- if this table is populated for the corresponding role. + grant_scope text not null + constraint only_individual_grant_scope_allowed + check( + grant_scope = 'individual' + ), + constraint iam_role_global_grant_scope_fkey + foreign key (role_id, grant_scope) + references iam_role_global(public_id, grant_scope) + on delete cascade + on update cascade, + create_time wt_timestamp, + primary key(role_id, scope_id) + ); + comment on table iam_role_global_individual_org_grant_scope is + 'iam_role_global_individual_org_grant_scope is a list of individually granted org scope to global roles with grant_scope of individual.'; + + create trigger default_create_time_column before insert on iam_role_global_individual_org_grant_scope + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on iam_role_global_individual_org_grant_scope + for each row execute procedure immutable_columns('role_id', 'scope_id', 'grant_scope', 'create_time'); + + create table iam_role_global_individual_project_grant_scope ( + role_id wt_role_id + constraint iam_role_global_fkey + references iam_role_global(public_id) + on delete cascade + on update cascade, + scope_id wt_scope_id not null + constraint iam_scope_project_fkey + references iam_scope_project(scope_id) + on delete cascade + on update cascade, + -- grant_scope is used for constraint checking. + -- This restricts the grant_scope to be 'individual' + -- and since it is also a foreign key to the iam_role_global + -- grant_scope, it ensures that iam_role_global is set to 'individual' + -- if this table is populated for the corresponding role. + -- both children and individual are allowed for this global role + -- because projects can be individually in addition to children + -- which grants all orgs + grant_scope text not null + constraint only_individual_or_children_grant_scope_allowed + check( + grant_scope in ('individual', 'children') + ), + constraint iam_role_global_grant_scope_fkey + foreign key (role_id, grant_scope) + references iam_role_global(public_id, grant_scope) + on delete cascade + on update cascade, + create_time wt_timestamp, + primary key(role_id, scope_id) + ); + comment on table iam_role_global_individual_project_grant_scope is + 'iam_role_global_individual_project_grant_scope is a list of individually granted project scope table to global role with grant_scope of individual or children.'; + + create trigger default_create_time_column before insert on iam_role_global_individual_project_grant_scope + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on iam_role_global_individual_project_grant_scope + for each row execute procedure immutable_columns('role_id', 'scope_id', 'create_time'); + + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/100/02_iam_role_org.up.sql b/internal/db/schema/migrations/oss/postgres/100/02_iam_role_org.up.sql new file mode 100644 index 0000000000..f99e263a77 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/100/02_iam_role_org.up.sql @@ -0,0 +1,152 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + + -- Create the enumeration table for the grant scope types for the org iam_role + create table iam_role_org_grant_scope_enm ( + name text primary key + constraint only_predefined_scope_types_allowed + check( + name in ( + 'children', + 'individual' + ) + ) + ); + comment on table iam_role_org_grant_scope_enm is + 'iam_role_org_grant_scope_enm is an enumeration table for role grant scope types for the iam_role_org table.'; + + -- Insert the predefined grant scope types for iam_role_org + insert into iam_role_org_grant_scope_enm (name) + values + ('children'), + ('individual'); + + + create table iam_role_org ( + public_id wt_role_id primary key + constraint iam_role_fkey + references iam_role(public_id) + on delete cascade + on update cascade, + scope_id wt_scope_id not null + constraint iam_scope_org_fkey + references iam_scope_org(scope_id) + on delete cascade + on update cascade, + name text, + description text, + grant_this_role_scope boolean not null default false, + grant_scope text not null + constraint iam_role_org_grant_scope_enm_fkey + references iam_role_org_grant_scope_enm(name) + on delete restrict + on update cascade, + version wt_version, + grant_this_role_scope_update_time wt_timestamp, + grant_scope_update_time wt_timestamp, + create_time wt_timestamp, + update_time wt_timestamp, + constraint iam_role_org_grant_scope_public_id_uq + unique(grant_scope, public_id), + constraint iam_role_org_name_scope_id_uq + unique(name, scope_id) + ); + comment on table iam_role_org is + 'iam_role_org is a subtype table of the iam_role table. It is used to store roles that are scoped to an org.'; + + create trigger insert_role_subtype before insert on iam_role_org + for each row execute procedure insert_role_subtype(); + + create trigger insert_iam_role_org_grant_scope_update_time before insert on iam_role_org + for each row execute procedure insert_grant_scope_update_time(); + + create trigger insert_iam_role_org_grant_this_role_scope_update_time before insert on iam_role_org + for each row execute procedure insert_grant_this_role_scope_update_time(); + + create trigger update_iam_role_org_grant_scope_update_time before update on iam_role_org + for each row execute procedure insert_grant_scope_update_time(); + + create trigger update_iam_role_org_grant_this_role_scope_update_time before update on iam_role_org + for each row execute procedure insert_grant_this_role_scope_update_time(); + + create trigger update_iam_role_org_base_table_update_time after update on iam_role_org + for each row execute procedure update_iam_role_table_update_time(); + + create trigger delete_iam_role_subtype after delete on iam_role_org + for each row execute procedure delete_iam_role_subtype(); + + create trigger default_create_time_column before insert on iam_role_org + for each row execute procedure default_create_time(); + + create trigger update_time_column before update on iam_role_org + for each row execute procedure update_time_column(); + + create trigger update_version_column after update on iam_role_org + for each row execute procedure update_version_column(); + + create trigger immutable_columns before update on iam_role_org + for each row execute procedure immutable_columns('scope_id', 'create_time'); + + create table iam_role_org_individual_grant_scope ( + role_id wt_role_id + constraint iam_role_org_fkey + references iam_role_org(public_id) + on delete cascade + on update cascade, + scope_id wt_scope_id not null + constraint iam_scope_project_fkey + references iam_scope_project(scope_id) + on delete cascade + on update cascade, + -- grant_scope is used for constraint checking. + -- This restricts the grant_scope to be 'individual' + -- and since it is also a foreign key to the iam_role_org + -- grant_scope, it ensures that iam_role_org is set to 'individual' + -- if this table is populated for the corresponding role. + grant_scope text not null + constraint only_individual_grant_scope_allowed + check( + grant_scope = 'individual' + ), + constraint iam_role_org_grant_scope_fkey + foreign key (role_id, grant_scope) + references iam_role_org(public_id, grant_scope) + on delete cascade + on update cascade, + create_time wt_timestamp, + primary key(role_id, scope_id) + ); + comment on table iam_role_org_individual_grant_scope is + 'iam_role_org_individual_grant_scope is the subtype table for the org role with grant_scope as individual.'; + + create trigger default_create_time_column before insert on iam_role_org_individual_grant_scope + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on iam_role_org_individual_grant_scope + for each row execute procedure immutable_columns('role_id', 'grant_scope', 'scope_id', 'create_time'); + + -- ensure the project's parent is the role's scope + create function ensure_project_belongs_to_role_org() returns trigger + as $$ + begin + perform + from iam_scope_project + join iam_role_org + on iam_role_org.scope_id = iam_scope_project.parent_id + where iam_scope_project.scope_id = new.scope_id + and iam_role_org.public_id = new.role_id; + if not found then + raise exception 'project scope_id % not found in org', new.scope_id; + end if; + return new; + end; + $$ language plpgsql; + comment on function ensure_project_belongs_to_role_org() is + 'ensure_project_belongs_to_role_org ensures the project belongs to the org of the role.'; + + create trigger ensure_project_belongs_to_role_org before insert or update on iam_role_org_individual_grant_scope + for each row execute procedure ensure_project_belongs_to_role_org(); + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/100/03_iam_role_project.up.sql b/internal/db/schema/migrations/oss/postgres/100/03_iam_role_project.up.sql new file mode 100644 index 0000000000..7082f2da07 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/100/03_iam_role_project.up.sql @@ -0,0 +1,49 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + + create table iam_role_project ( + public_id wt_role_id primary key + constraint iam_role_fkey + references iam_role(public_id) + on delete cascade + on update cascade, + scope_id wt_scope_id not null + constraint iam_scope_project_fkey + references iam_scope_project(scope_id) + on delete cascade + on update cascade, + name text, + description text, + version wt_version, + create_time wt_timestamp, + update_time wt_timestamp, + constraint iam_role_project_name_scope_id_uq + unique(name, scope_id) + ); + comment on table iam_role_project is + 'iam_role_project is a subtype table of the iam_role table. It is used to store roles that are scoped to a project.'; + + create trigger insert_role_subtype before insert on iam_role_project + for each row execute procedure insert_role_subtype(); + + create trigger default_create_time_column before insert on iam_role_project + for each row execute procedure default_create_time(); + + create trigger update_time_column before update on iam_role_project + for each row execute procedure update_time_column(); + + create trigger update_version_column after update on iam_role_project + for each row execute procedure update_version_column(); + + create trigger update_iam_role_project_base_table_update_time after update on iam_role_project + for each row execute procedure update_iam_role_table_update_time(); + + create trigger delete_iam_role_subtype after delete on iam_role_project + for each row execute procedure delete_iam_role_subtype(); + + create trigger immutable_columns before update on iam_role_project + for each row execute procedure immutable_columns('scope_id', 'create_time'); + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/100/04_resource.up.sql b/internal/db/schema/migrations/oss/postgres/100/04_resource.up.sql new file mode 100644 index 0000000000..7d8d3e7304 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/100/04_resource.up.sql @@ -0,0 +1,71 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + + create table iam_grant_resource_enm ( + name text primary key + constraint only_predefined_resource_types_allowed + check( + name in ( + '*', + 'alias', + 'auth-method', + 'auth-token', + 'account', + 'billing', + 'controller', + 'credential', + 'credential-library', + 'credential-store', + 'group', + 'host', + 'host-catalog', + 'host-set', + 'managed-group', + 'policy', + 'role', + 'scope', + 'session', + 'session-recording', + 'storage-bucket', + 'target', + 'unknown', + 'user', + 'worker' + ) + ) + ); + comment on table iam_grant_resource_enm is + 'iam_grant_resource_enm is an enumeration table for resource types.'; + + -- Insert the predefined resource types + insert into iam_grant_resource_enm (name) + values + ('*'), + ('alias'), + ('auth-method'), + ('auth-token'), + ('account'), + ('billing'), + ('controller'), + ('credential'), + ('credential-library'), + ('credential-store'), + ('group'), + ('host'), + ('host-catalog'), + ('host-set'), + ('managed-group'), + ('policy'), + ('role'), + ('scope'), + ('session'), + ('session-recording'), + ('storage-bucket'), + ('target'), + ('unknown'), + ('user'), + ('worker'); + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/100/05_iam_grant.up.sql b/internal/db/schema/migrations/oss/postgres/100/05_iam_grant.up.sql new file mode 100644 index 0000000000..ed57023044 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/100/05_iam_grant.up.sql @@ -0,0 +1,85 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + + -- wt_canonical_grant domain represents Boundary canonical grant. + -- A canonical grant is a semicolon-separated list of key=value pairs. + -- e.g. "ids=*;type=role;actions=read;output_fields=id,name" + create domain wt_canonical_grant as text + check( + value ~ '^(?:[^;=]+=[^;=]+)(?:;[^;=]+=[^;=]+)*?$' + ); + comment on domain wt_canonical_grant is + 'A canonical grant is a semicolon-separated list of key=value pairs.'; + + -- iam_grant is the root table for a grant value object. + -- A grant can only reference a single resource, including the special + -- strings "*" to indicate "all" resources, and "unknown" when no resource is set. + create table iam_grant ( + canonical_grant wt_canonical_grant primary key, + resource text not null + constraint iam_grant_resource_enm_fkey + references iam_grant_resource_enm(name) + on delete restrict + on update cascade + ); + comment on table iam_grant is + 'iam_grant is the root table for a grant value object. A grant can only reference a single resource, including the special strings "*" to indicate "all" resources, and "unknown" when no resource is set.'; + + create index iam_grant_resource_ix + on iam_grant (resource); + + -- set_resource sets the resource column based on the "type" token in the canonical_grant. + create function set_resource() returns trigger + as $$ + declare type_matches text[]; + begin + -- Extract all "type" tokens from the canonical_grant string + with + parts (p) as ( + select p + from regexp_split_to_table(new.canonical_grant, ';') as p + ), + kv (k, v) as ( + select part[1] as k, + part[2] as v + from parts, + regexp_split_to_array(parts.p, '=') as part + ) + select array_agg(v) + into type_matches + from kv + where k = 'type'; + + -- if there are multiple canonical grant types specified, throw an error. + -- Ensure that the canonical_grant type is only referencing a single resource + if type_matches is not null and array_length(type_matches, 1) > 1 then + raise exception 'multiple type tokens in grant. only one type expected: %', new.canonical_grant; + elsif type_matches is not null and array_length(type_matches, 1) = 1 then + new.resource := type_matches[1]; + else + new.resource := 'unknown'; + end if; + return new; + end + $$ language plpgsql; + comment on function set_resource() is + 'set_resource sets the resource column based on the "type" token. A valid grant without a type token results in resource being set to "unknown".'; + + create trigger set_resource before insert on iam_grant + for each row execute procedure set_resource(); + + -- TODO (Bo 04/14/2025): this constraint cannot be applied as some canonical grants may not exist during the first migration to the new schema + -- need to move canonical grants onto `iam_grants` table to make this constraint work + + -- Add a foreign key constraint to the iam_role_grant table to ensure that the canonical_grant exists in the iam_grant table. + -- Alter to add foreign key constraint to the iam_role_grant table defined in 01/06_iam.up.sql + alter table iam_role_grant + add constraint iam_grant_fkey + foreign key (canonical_grant) + references iam_grant(canonical_grant) + on delete cascade + on update cascade; + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/100/06_iam_role_grant.up.sql b/internal/db/schema/migrations/oss/postgres/100/06_iam_role_grant.up.sql new file mode 100644 index 0000000000..daba8684dc --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/100/06_iam_role_grant.up.sql @@ -0,0 +1,26 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + + create index iam_role_grant_canonical_grant_ix + on iam_role_grant (canonical_grant); + + create function upsert_canonical_grant() returns trigger + as $$ + begin + insert into iam_grant + (canonical_grant) + values + (new.canonical_grant) + on conflict do nothing; + return new; + end + $$ language plpgsql; + comment on function upsert_canonical_grant() is + 'upsert_canonical_grant is a trigger function that inserts a row into the iam_grant table if the canonical_grant does not exist.'; + + create trigger upsert_canonical_grant before insert on iam_role_grant + for each row execute procedure upsert_canonical_grant(); + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/oss/postgres/100/07_oplog_ticket.up.sql b/internal/db/schema/migrations/oss/postgres/100/07_oplog_ticket.up.sql new file mode 100644 index 0000000000..5fec75f024 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/100/07_oplog_ticket.up.sql @@ -0,0 +1,10 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; +insert into oplog_ticket + (name, version) + values ('iam_role_global',1), + ('iam_role_org', 1), + ('iam_role_project', 1); +commit; \ No newline at end of file diff --git a/internal/db/sqltest/Makefile b/internal/db/sqltest/Makefile index 3633401728..b85c76b996 100644 --- a/internal/db/sqltest/Makefile +++ b/internal/db/sqltest/Makefile @@ -17,28 +17,29 @@ endif PROVE_OPTS ?= TESTS ?= tests/setup/*.sql \ - tests/org/*.sql \ - tests/wh/*/*.sql \ - tests/sentinel/*.sql \ - tests/credential/*/*.sql \ - tests/session/*.sql \ tests/account/*/*.sql \ - tests/target/*.sql \ - tests/controller/*.sql \ - tests/hcp/*/*.sql \ + tests/alias/*.sql \ + tests/auth/*/*.sql \ tests/census/*.sql \ - tests/kms/*.sql \ - tests/storage/*.sql \ + tests/controller/*.sql \ + tests/credential/*/*.sql \ tests/domain/*.sql \ + tests/hcp/*/*.sql \ tests/history/*.sql \ - tests/recording/*.sql \ - tests/alias/*.sql \ - tests/auth/*/*.sql \ - tests/purge/*.sql \ + tests/host/*.sql \ + tests/iam/*.sql \ + tests/kms/*.sql \ + tests/org/*.sql \ tests/pagination/*.sql \ tests/policy/*.sql \ - tests/host/*.sql \ - tests/server/*.sql + tests/purge/*.sql \ + tests/recording/*.sql \ + tests/sentinel/*.sql \ + tests/server/*.sql \ + tests/session/*.sql \ + tests/storage/*.sql \ + tests/target/*.sql \ + tests/wh/*/*.sql \ POSTGRES_DOCKER_IMAGE_BASE ?= postgres @@ -116,4 +117,4 @@ clean: docker stop $(SQL_TEST_CONTAINER_NAME) || true docker rm -v $(SQL_TEST_CONTAINER_NAME) || true -.PHONY: all clean test database-up run-tests +.PHONY: all clean test database-up run-tests \ No newline at end of file diff --git a/internal/db/sqltest/initdb.d/01_colors_persona.sql b/internal/db/sqltest/initdb.d/01_colors_persona.sql index fba8d7d947..f767decc63 100644 --- a/internal/db/sqltest/initdb.d/01_colors_persona.sql +++ b/internal/db/sqltest/initdb.d/01_colors_persona.sql @@ -135,19 +135,19 @@ begin; ('r_gg____shop', 'global'); insert into iam_role_grant - (role_id, canonical_grant, raw_grant) - values - ('r_gg_____buy', 'type=*;action=purchase', 'purchase anything'), - ('r_gg____shop', 'type=*;action=view', 'view anything'), - ('r_go____name', 'type=color;action=name', 'name colors'), - ('r_gp____spec', 'type=color;action=inspect', 'inspect colors'), - ('r_oo_____art', 'type=color;action=create', 'create color'), - ('r_op_bc__art', 'type=color;action=create', 'create color'), - ('r_op_rc__art', 'type=color;action=create', 'create color'), - ('r_op_gc__art', 'type=color;action=create', 'create color'), - ('r_pp_bc__mix', 'type=color;action=mix', 'mix color'), - ('r_pp_rc__mix', 'type=color;action=mix', 'mix color'), - ('r_pp_gc__mix', 'type=color;action=mix', 'mix color'); + (role_id, canonical_grant, raw_grant) + values + ('r_gg_____buy', 'ids=*;type=*;actions=update', 'ids=*;type=*;actions=update'), + ('r_gg____shop', 'ids=*;type=*;actions=read;output_fields=id', 'ids=*;type=*;actions=read;output_fields=id'), + ('r_go____name', 'ids=*;type=group;actions=create,update,read,list', 'ids=*;type=group;actions=create,update,read,'), + ('r_gp____spec', 'ids=*;type=group;actions=delete', 'ids=*;type=group;actions=delete'), + ('r_oo_____art', 'ids=*;type=group;actions=create', 'ids=*;type=group;actions=create'), + ('r_op_bc__art', 'ids=*;type=auth-token;actions=create', 'ids=*;type=auth-token;actions=create'), + ('r_op_rc__art', 'ids=*;type=target;actions=create', 'ids=*;type=targets;actions=create'), + ('r_op_gc__art', 'ids=*;type=auth-method;actions=authenticate', 'ids=*;type=auth-method;actions=create'), + ('r_pp_bc__mix', 'ids=*;type=group;actions=add-members', 'ids=*;type=group;actions=add-members'), + ('r_pp_rc__mix', 'ids=*;type=group;actions=set-members', 'ids=*;type=group;actions=set-members'), + ('r_pp_gc__mix', 'ids=*;type=group;actions=delete-members', 'ids=*;type=group;actions=delete-members'); insert into iam_group_role (role_id, principal_id) diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index 8303e442e8..5d536174df 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -54,15 +54,17 @@ begin; ('g___wb-group', 'u_____warren'), ('g___ws-group', 'u_____waylon'); - insert into iam_role + insert into iam_role_org + (scope_id, public_id, name, grant_scope) + values + ('o_____widget', 'r_op_sw__eng', 'Small Widget Engineer', 'individual'), + ('o_____widget', 'r_oo_____eng', 'Widget Engineer', 'individual'); + + insert into iam_role_project (scope_id, public_id, name) values - -- ('global', 'r_gg_____buy', 'Purchaser'), - -- ('global', 'r_gg____shop', 'Shopper'), ('p____bwidget', 'r_pp_bw__bld', 'Widget Builder'), - ('p____swidget', 'r_pp_sw__bld', 'Widget Builder'), - ('o_____widget', 'r_op_sw__eng', 'Small Widget Engineer'), - ('o_____widget', 'r_oo_____eng', 'Widget Engineer'); + ('p____swidget', 'r_pp_sw__bld', 'Widget Builder'); insert into iam_role_grant_scope (role_id, scope_id_or_special) @@ -75,14 +77,12 @@ begin; insert into iam_role_grant (role_id, canonical_grant, raw_grant) values - -- ('r_gg_____buy', 'type=*;action=purchase', 'purchase anything'), - -- ('r_gg____shop', 'type=*;action=view', 'view anything'), - ('r_oo_____eng', 'type=widget;action=design', 'design widget'), - ('r_op_sw__eng', 'type=widget;action=design', 'design widget'), - ('r_op_sw__eng', 'type=widget;action=tune', 'tune widget'), - ('r_op_sw__eng', 'type=widget;action=clean', 'clean widget'), - ('r_pp_bw__bld', 'type=widget;action=build', 'build widget'), - ('r_pp_sw__bld', 'type=widget;action=build', 'build widget'); + ('r_oo_____eng', 'ids=*;type=alias;actions=create,update', 'ids=*;type=alias;actions=create,update'), + ('r_op_sw__eng', 'ids=*;type=target;actions=add-credential-sources,remove-credential-sources,set-credential-sources', 'ids=*;type=target;actions=add-credential-sources,remove-credential-sources,set-credential-source'), + ('r_op_sw__eng', 'ids=*;type=target;actions=add-host-sources,remove-host-sources,set-host-sources', 'ids=*;type=target;actions=add-host-sources,remove-host-sources,set-host-sources'), + ('r_op_sw__eng', 'ids=*;type=host-catalog;actions=read,list', 'ids=*;type=host-catalog;actions=read,list'), + ('r_pp_bw__bld', 'ids=*;type=credential-library;actions=create,delete', 'ids=*;type=credential-library;actions=create,delete'), + ('r_pp_sw__bld', 'ids=*;type=scope;actions=no-op,list', 'ids=*;type=scope;actions=no-op,list'); insert into iam_group_role (role_id, principal_id) diff --git a/internal/db/sqltest/tests/iam/iam_grant.sql b/internal/db/sqltest/tests/iam/iam_grant.sql new file mode 100644 index 0000000000..59e2d549e3 --- /dev/null +++ b/internal/db/sqltest/tests/iam/iam_grant.sql @@ -0,0 +1,303 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; +select plan(29); +select wtt_load('widgets', 'iam'); + +-- insert canonical_grant with valid resource +-- validate the resource is set to 'scope' +prepare insert_grant_scope as + insert into iam_grant + (canonical_grant) + values + ('type=scope;others=stuff'); +select lives_ok('insert_grant_scope'); +select is( + (select resource + from iam_grant + where canonical_grant = 'type=scope;others=stuff'), + 'scope', + 'resource should be set to "scope" by set_resource() trigger' +); + +-- insert invalid canonical_grant which does not match the wt_canonical_grant domain +-- the insert should fail because the canonical_grant is malformed +prepare insert_malformed_grant as + insert into iam_grant + (canonical_grant) + values + ('no_type_at_all'); +select throws_like( + 'insert_malformed_grant', + 'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"', + 'inserting a grant that is malformed should fail' +); + +-- insert invalid canonical_grant with trailing semicolon +-- the insert should fail because the the canonical_grant has a trailing semicolon +prepare insert_grant_with_trailing_semicolon as + insert into iam_grant + (canonical_grant) + values + ('ids=*;type=role;actions=*;'); +select throws_like( + 'insert_grant_with_trailing_semicolon', + 'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"', + 'inserting a grant with trailing semicolon should fail' +); + +-- insert canonical_grant with type=role,group +-- the insert should fail because the resource is not a single value +prepare insert_grant_role as + insert into iam_grant + (canonical_grant) + values + ('type=role,group;foo=bar'); +select throws_like( + 'insert_grant_role', + 'insert or update on table "iam_grant" violates foreign key constraint "iam_grant_resource_enm_fkey"', + 'inserting a resource not in iam_grant_resource_enm should fail because type is not a single value' +); + +-- the set_resource() trigger will set resource='bogus', but we did not insert 'bogus' +-- into resource_enm, so it should fail. +prepare insert_grant_bogus as + insert into iam_grant + (canonical_grant) + values + ('type=bogus;some=thing'); +select throws_like( + 'insert_grant_bogus', + 'insert or update on table "iam_grant" violates foreign key constraint "iam_grant_resource_enm_fkey"', + 'inserting a resource not in iam_grant_resource_enm should fail' +); + +-- the set_resource() trigger will set resource='bogus', but we did not insert 'bogus' +-- into resource_enm, so it should fail. +prepare insert_grant_bogus_with_action as + insert into iam_grant + (canonical_grant) + values + ('type=bogus;actions=create'); +select throws_like( + 'insert_grant_bogus_with_action', + 'insert or update on table "iam_grant" violates foreign key constraint "iam_grant_resource_enm_fkey"', + 'inserting a resource not in iam_grant_resource_enm should fail' +); + +-- insert a duplicate canonical_grant +-- validate that the primary key constraint is enforced +prepare insert_dup_grant_1 as + insert into iam_grant + (canonical_grant) + values + ('ids=*;type=credential-library;actions=create'); +select lives_ok('insert_dup_grant_1'); +prepare insert_dup_grant_2 as + insert into iam_grant + (canonical_grant) + values + ('ids=*;type=credential-library;actions=create'); +select throws_like( + 'insert_dup_grant_2', + 'duplicate key value violates unique constraint "iam_grant_pkey"', + 'primary key (canonical_grant) is enforced' +); + +-- insert a canonical grant string with wildcards for id, type, actions, and output_fields +-- validate that the resource is set to '*' +prepare insert_grant_wildcard as + insert into iam_grant + (canonical_grant) + values + ('ids=*;type=*;actions=*;output_fields=*'); +select lives_ok('insert_grant_wildcard'); +select is( + (select resource + from iam_grant + where canonical_grant = 'ids=*;type=*;actions=*;output_fields=*'), + '*', + 'resource should be set to "*" if type is "*"' +); + +-- insert a canonical grant string with single action, single id, single output_field and type=host +-- validate that the resource is set to 'host' +prepare insert_grant_single_action as + insert into iam_grant + (canonical_grant) + values + ('ids=o_1234;type=host;actions=create;output_fields=id'); +select lives_ok('insert_grant_single_action'); +select is( + (select resource + from iam_grant + where canonical_grant = 'ids=o_1234;type=host;actions=create;output_fields=id'), + 'host', + 'resource should be set to "host" if type is "host"' +); + +-- insert a canonical grant string with type=group, multiple actions, single type and multiple output_fields +-- validate that the resource is set to 'group' +prepare insert_grant_role_multiple_actions as + insert into iam_grant + (canonical_grant) + values + ('ids=o_1234,o_4567;type=group;actions=create,update;output_fields=id,name'); +select lives_ok('insert_grant_role_multiple_actions'); +select is( + (select resource + from iam_grant + where canonical_grant = 'ids=o_1234,o_4567;type=group;actions=create,update;output_fields=id,name'), + 'group', + 'resource should be set to "group" if type is "group"' +); + +-- insert a canonical grant string with with multiple types +-- the insert should fail because the resource is not a single value +prepare insert_grant_multiple_types as + insert into iam_grant + (canonical_grant) + values + ('ids=o_1234,o_4567;type=target,role,group;actions=create,update;output_fields=id,name'); +select throws_like( + 'insert_grant_multiple_types', + 'insert or update on table "iam_grant" violates foreign key constraint "iam_grant_resource_enm_fkey"', + 'inserting a resource not in iam_grant_resource_enm should fail because type is not a single value' +); + +-- insert a canonical grant string with type with dash +-- validate that the resource is set to 'credential-library' +prepare insert_grant_with_dash as + insert into iam_grant + (canonical_grant) + values + ('ids=*;type=credential-library;actions=create;output_fields=id'); +select lives_ok('insert_grant_with_dash'); +select is( + (select resource + from iam_grant + where canonical_grant = 'ids=*;type=credential-library;actions=create;output_fields=id'), + 'credential-library', + 'resource should be set to "credential-library" if type is "credential-library"' +); + +-- insert a canonical grant string with type with underscore +-- the insert should fail because a resource with underscore is not in the resource_enm table +prepare insert_grant_with_underscore as + insert into iam_grant + (canonical_grant) + values + ('ids=*;type=credential_library;actions=create;output_fields=id'); +select throws_like( + 'insert_grant_with_underscore', + 'insert or update on table "iam_grant" violates foreign key constraint "iam_grant_resource_enm_fkey"', + 'inserting a a resource with underscore should fail' +); + +-- insert a canonical grant string with type malformed with no semicolon +-- the insert should fail because the type is malformed +prepare insert_grant_malformed_type_with_no_semicolon as + insert into iam_grant + (canonical_grant) + values + ('ids=*;type=credential-library actions=create;output_fields=id'); +select throws_like( + 'insert_grant_malformed_type_with_no_semicolon', + 'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"', + 'inserting a resource with a malformed type should fail' +); + +-- insert a canonical grant string with type malformed with no equals sign +prepare insert_grant_malformed_type_with_no_equals as + insert into iam_grant + (canonical_grant) + values + ('ids=*;type;actions=create;output_fields=id'); +select throws_like( + 'insert_grant_malformed_type_with_no_equals', + 'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"', + 'inserting a resource with a malformed type should fail' +); + +-- insert a canonical grant string with type malformed with no value +prepare insert_grant_malformed_type_with_no_value as + insert into iam_grant + (canonical_grant) + values + ('ids=*;type=;actions=create;output_fields=id'); +select throws_like( + 'insert_grant_malformed_type_with_no_value', + 'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"', + 'inserting a resource with a malformed type should fail' +); + +-- insert a canonical grant string with with no type +prepare insert_grant_malformed_type_with_no_type as + insert into iam_grant + (canonical_grant) + values + ('ids=*;actions=create;output_fields=id'); +select lives_ok('insert_grant_malformed_type_with_no_type'); +select is( + (select resource + from iam_grant + where canonical_grant = 'ids=*;actions=create;output_fields=id'), + 'unknown', + 'resource should default to "unknown" if the type is not found' +); + +-- insert a canonical grant string with type malformed with double ids semicolon +prepare insert_grant_malformed_type_with_double_ids_semicolon as + insert into iam_grant + (canonical_grant) + values + ('ids=*;;type=credential-library;actions=create;output_fields=id'); +select throws_like( + 'insert_grant_malformed_type_with_double_ids_semicolon', + 'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"', + 'inserting a resource with a malformed type should fail' +); + +-- insert a canonical grant string with type at the end of the string +prepare insert_grant_with_type_at_the_end as + insert into iam_grant + (canonical_grant) + values + ('ids=*;actions=create;output_fields=id;type=credential-library'); +select lives_ok('insert_grant_with_type_at_the_end'); +select is( + (select resource + from iam_grant + where canonical_grant = 'ids=*;actions=create;output_fields=id;type=credential-library'), + 'credential-library', + 'resource should be set to "credential-library" if type is "credential-library"' +); + +-- insert a canonical grant string with type malformed with semicolon after type +prepare insert_grant_malformed_type_with_semicolon_after_type as + insert into iam_grant + (canonical_grant) + values + ('ids=*;type;=credential-library;actions=create;output_fields=id;'); +select throws_like( + 'insert_grant_malformed_type_with_semicolon_after_type', + 'value for domain wt_canonical_grant violates check constraint "wt_canonical_grant_check"', + 'inserting a resource with a malformed type should fail' +); + +-- insert a canonical grant string with multiple type tokens +-- the insert should fail because there are multiple type tokens +prepare insert_grant_multiple_type_specified as + insert into iam_grant + (canonical_grant) + values + ('ids=o_1234,o_4567;type=target;type=session;actions=create,update;output_fields=id,name'); +select throws_like( + 'insert_grant_multiple_type_specified', + 'multiple type tokens in grant. only one type expected: ids=o_1234,o_4567;type=target;type=session;actions=create,update;output_fields=id,name', + 'inserting a resource with multiple type tokens should fail' +); + +select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/iam/iam_role_global.sql b/internal/db/sqltest/tests/iam/iam_role_global.sql new file mode 100644 index 0000000000..2c029efcce --- /dev/null +++ b/internal/db/sqltest/tests/iam/iam_role_global.sql @@ -0,0 +1,425 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + select plan(44); + select wtt_load('widgets', 'iam'); + + -------------------------------------------------------------------------------- + -- 1) testing iam_role_global table constraints and insert_role_subtype + -------------------------------------------------------------------------------- + + -- 1a) insert a valid row -> should succeed and insert_role_subtype trigger + -- r_1111111111 is a global role with grant_scope=descendants + -- r_2222222222 is a global role with grant_scope=children + -- r_3333333333 is a global role with grant_scope=individual + prepare insert_valid_global_role as + insert into iam_role_global + (public_id, scope_id, grant_this_role_scope, grant_scope) + values + ('r_1111111111', 'global', true, 'descendants'), + ('r_2222222222', 'global', true, 'children'), + ('r_3333333333', 'global', true, 'individual'); + select lives_ok('insert_valid_global_role'); + + + -- verify it also created a row in base iam_role + select is(count(*), 1::bigint) from iam_role where public_id = 'r_1111111111'; + select is(count(*), 1::bigint) from iam_role where public_id = 'r_2222222222'; + select is(count(*), 1::bigint) from iam_role where public_id = 'r_3333333333'; + + -- 1b) try duplicate (public_id, grant_scope) => unique violation + prepare insert_dup_public_id_grant_scope as + insert into iam_role_global + (public_id, scope_id, grant_this_role_scope, grant_scope) + values + ('r_1111111111', 'global', true, 'children'); + select throws_like( + 'insert_dup_public_id_grant_scope', + 'duplicate key value violates unique constraint "iam_role_pkey"', + 'unique(public_id) is enforced' + ); + + -- 1c) invalid grant_scope (not in iam_role_global_grant_scope_enm table) + prepare insert_invalid_grant_scope as + insert into iam_role_global + (public_id, scope_id, grant_this_role_scope, grant_scope) + values + ('r_globeglobe', 'global', true, 'invalid_grant_scope'); + select throws_like( + 'insert_invalid_grant_scope', + 'insert or update on table "iam_role_global" violates foreign key constraint "iam_role_global_grant_scope_enm_fkey"', + 'invalid grant_scope must fail foreign key to iam_role_global_grant_scope_enm' + ); + + -- 1d) invalid scope_id -> must reference iam_scope_global(scope_id) + prepare insert_bad_scope_id as + insert into iam_role_global + (public_id, scope_id, grant_this_role_scope, grant_scope) + values + ('r_globeglobe', 'does_not_exist', true, 'individual'); + select throws_like( + 'insert_bad_scope_id', + 'insert or update on table "iam_role" violates foreign key constraint "iam_scope_scope_id_fkey"', + 'scope_id must exist in iam_scope_global(scope_id)' + ); + + -------------------------------------------------------------------------------- + -- 2) testing insert_grant_scope_update_time trigger + -------------------------------------------------------------------------------- + + -- 2a) insert a new row (grant_this_role_scope, grant_scope) => should initialize + prepare insert_with_grant_scope_update_time_set as + insert into iam_role_global + (public_id, scope_id, grant_scope, grant_scope_update_time) + values + ('r_4444444444', 'global', 'descendants', null); + select lives_ok('insert_with_grant_scope_update_time_set'); + + -- 2b) check if grant_scope_update_time is set + select is( + (select grant_scope_update_time is not null from iam_role_global where public_id = 'r_4444444444'), + true, + 'grant_scope_update_time should be set with the default timestamp right after insert' + ); + + -- 2c) update grant_this_role_scope => trigger should update grant_scope_update_time timestamp + prepare update_grant_this_role_scope as + update iam_role_global + set grant_this_role_scope = true + where public_id = 'r_4444444444'; + select lives_ok('update_grant_this_role_scope'); + select is( + (select grant_scope_update_time is not null from iam_role_global where public_id = 'r_4444444444'), + true, + 'grant_scope_update_time should be set with the default timestamp right after insert' + ); + + -------------------------------------------------------------------------------- + -- 3) testing iam_role_global_individual_org_grant_scope table constraints + -------------------------------------------------------------------------------- + + -- 3a) insert invalid row: grant_scope = 'descendants' + prepare insert_invalid_individual_org_grant_scope_descendants as + insert into iam_role_global_individual_org_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'descendants', 'p____bwidget'); + select throws_like( + 'insert_invalid_individual_org_grant_scope_descendants', + 'new row for relation "iam_role_global_individual_org_grant_scope" violates check constraint "only_individual_grant_scope_allowed"', + 'check(grant_scope = "individual") is enforced' + ); + + -- 3b) insert invalid row: grant_scope = 'children' + prepare insert_invalid_individual_org_grant_scope_children as + insert into iam_role_global_individual_org_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'children', 'p____bwidget'); + select throws_like( + 'insert_invalid_individual_org_grant_scope_children', + 'new row for relation "iam_role_global_individual_org_grant_scope" violates check constraint "only_individual_grant_scope_allowed"', + 'check(grant_scope = "individual") is enforced' + ); + + -- 3c) insert invalid row with a scope_id that is not global + prepare insert_invalid_iam_role_global_individual_org_grant_scope as + insert into iam_role_global_individual_org_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'individual', 'o_1111111111'); + select throws_like( + 'insert_invalid_iam_role_global_individual_org_grant_scope', + 'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_scope_org_fkey"', + 'foreign key also enforces matching grant_scope=individual in iam_role_global' + ); + + -- 3d) insert invalid row where scope_id is 'global' + prepare insert_iam_role_global_individual_scope_id as + insert into iam_role_global_individual_org_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'individual', 'global'); + select throws_like( + 'insert_iam_role_global_individual_scope_id', + 'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_scope_org_fkey"', + 'check(scope_id != ''global'') is enforced' + ); + + -- 3e) insert invalid row where scope_id is a project + prepare insert_invalid_project_into_individual_org_scope as + insert into iam_role_global_individual_org_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'individual', 'p____bwidgetp____bwidget'); + select throws_like( + 'insert_iam_role_global_individual_scope_id', + 'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_scope_org_fkey"', + 'foreign key also enforces matching grant_scope=individual in iam_role_global' + ); + + -- 3f) insert into iam_role_global_individual_org_grant_scope when role grant_scope is 'children' + prepare insert_invalid_individual_org_scope as + insert into iam_role_global_individual_org_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_2222222222', 'individual', 'o_____widget'); + select throws_like( + 'insert_invalid_individual_org_scope', + 'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_role_global_grant_scope_fkey"', + 'foreign key also enforces matching grant_scope=individual in iam_role_global' + ); + + -- 3g) insert into iam_role_global_individual_org_grant_scope when role grant_scope is descendants is not allowed + -- r_1111111111 is a global role with grant_scope=descendants + prepare iam_role_global_individual_org_grant_scope_role_descendants as + insert into iam_role_global_individual_org_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_1111111111', 'individual', 'o_____widget'); + select throws_like( + 'iam_role_global_individual_org_grant_scope_role_descendants', + 'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_role_global_grant_scope_fkey"', + 'foreign key also enforces matching grant_scope=individual in iam_role_global' + ); + + + -- 3h) insert into iam_role_global_individual_org_grant_scope when role grant_scope is children is not allowed + -- r_2222222222 is a global role with grant_scope=children + prepare iam_role_global_individual_org_grant_scope_role_children as + insert into iam_role_global_individual_org_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_2222222222', 'individual', 'o_____widget'); + select throws_like( + 'iam_role_global_individual_org_grant_scope_role_children', + 'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_role_global_grant_scope_fkey"', + 'foreign key also enforces matching grant_scope=individual in iam_role_global' + ); + + + --3i) insert entry with role_id that does not exist in iam_role_global + prepare insert_invalid_role_id as + insert into iam_role_global_individual_org_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_1231231231', 'individual', 'p____bwidget'); + select throws_like( + 'insert_invalid_role_id', + 'insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_role_global_fkey"', + 'foreign key enforces that role exists in iam_role_global' + ); + + -- 3j) insert into iam_role_global_individual_org_grant_scope when role grant_scope is 'individual' + -- r_3333333333 is a global role with grant_scope=individual + prepare insert_valid_individual_org_scope as + insert into iam_role_global_individual_org_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'individual', 'o_____widget'); + select lives_ok('insert_valid_individual_org_scope'); + + + + -------------------------------------------------------------------------------- + -- 4) testing iam_role_global_individual_project_grant_scope table constraints + -------------------------------------------------------------------------------- + + -- 4a) insert invalid row: grant_scope = 'descendants' + prepare insert_invalid_individual_project_grant_scope_descendants as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'descendants', 'p____bwidget'); + select throws_like( + 'insert_invalid_individual_project_grant_scope_descendants', + 'new row for relation "iam_role_global_individual_project_grant_scope" violates check constraint "only_individual_or_children_grant_scope_allowed"', + 'check(grant_scope in ["children", "individual"]) is enforced' + ); + + -- 4b) insert invalid row: grant_scope = 'children' + prepare insert_invalid_individual_project_grant_scope_children as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'children', 'p____bwidget'); + select throws_like( + 'insert_invalid_individual_project_grant_scope_children', + 'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_role_global_grant_scope_fkey"', + 'foreign key to grant_scope in iam_role_global is enforced' + ); + + -- 4c) insert invalid row with a scope_id project does not exist + prepare insert_invalid_iam_role_global_individual_project_grant_scope as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'individual', 'p_1111111111'); + select throws_like( + 'insert_invalid_iam_role_global_individual_project_grant_scope', + 'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_scope_project_fkey"', + 'foreign key also enforces matching grant_scope=individual in iam_role_global' + ); + + -- 4d) insert invalid row where scope_id is 'global' + prepare insert_invalid_project_grant_scope_global as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'individual', 'global'); + select throws_like( + 'insert_invalid_project_grant_scope_global', + 'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_scope_project_fkey"', + 'check(scope_id != ''global'') is enforced' + ); + + -- 4e) insert invalid row where scope_id is an org + prepare insert_invalid_org_into_individual_proj_scope as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'individual', 'o_____widget'); + select throws_like( + 'insert_invalid_org_into_individual_proj_scope', + 'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_scope_project_fkey"', + 'foreign key enforces that scope_id is a project scope' + ); + + -- 4f) insert into iam_role_global_individual_project_grant_scope when role grant_scope is descendants is not allowed + -- r_1111111111 is a global role with grant_scope=descendants + prepare iam_role_global_individual_project_grant_scope_role_descendants as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_1111111111', 'individual', 'p_____widget'); + select throws_like( + 'iam_role_global_individual_project_grant_scope_role_descendants', + 'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_scope_project_fkey"', + 'foreign key enforces that scope_id is a project scope' + ); + + -- 4g) insert entry with role_id that does not exist in iam_role_global + prepare insert_invalid_role_id_proj_grants_scope as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_1231231231', 'individual', 'p____bwidget'); + select throws_like( + 'insert_invalid_role_id_proj_grants_scope', + 'insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_role_global_fkey"', + 'foreign key enforces that role exists in iam_role_global' + ); + + + -- 4h) insert into iam_role_global_individual_project_grant_scope when role grant_scope is 'children' is valid + -- r_2222222222 is a global role with grant_scope=children + prepare insert_valid_individual_project_scope_children_grants as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_2222222222', 'children', 'p____bwidget'); + select lives_ok('insert_valid_individual_project_scope_children_grants'); + + -- 4i) insert into iam_role_global_individual_project_grant_scope when role grant_scope is 'individual' + -- r_3333333333 is a global role with grant_scope=individual + prepare insert_valid_individual_project_scope as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_3333333333', 'individual', 'p____bwidget'); + select lives_ok('insert_valid_individual_project_scope'); + + + + -- 4j) update to iam_role_global.grant_scope from children to individual cascades to iam_role_global_individual_project_grant_scope.grant_scope + -- r_5555555555 is a global role with grant_scope=children + prepare insert_valid_global_role_children_grants as + insert into iam_role_global + (public_id, scope_id, grant_this_role_scope, grant_scope) + values + ('r_5555555555', 'global', true, 'children'); + select lives_ok('insert_valid_global_role_children_grants'); + + -- create a row in iam_role_global_individual_project_grant_scope with grant_scope=children + prepare insert_valid_project_scope_to_children_grants as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_5555555555', 'children', 'p____bwidget'); + select lives_ok('insert_valid_project_scope_to_children_grants'); + + -- take away children grants by updating to iam_role_global to individual + prepare update_grant_scope_to_individual as + update iam_role_global + set grant_scope = 'individual' + where public_id = 'r_5555555555'; + select lives_ok('update_grant_scope_to_individual'); + + -- check that the update cascaded to iam_role_global_individual_project_grant_scope + -- and that the grant_scope is now individual + select is(count(*), 1::bigint) from iam_role_global_individual_project_grant_scope where role_id = 'r_5555555555' and grant_scope = 'individual'; + + + -- 4k) update to iam_role_global.grant_scope from children to individual cascades to iam_role_global_individual_project_grant_scope.grant_scope + -- r_6666666666 is a global role with grant_scope=individual and is granted individual org and project + prepare insert_valid_global_role_individual_grants as + insert into iam_role_global + (public_id, scope_id, grant_this_role_scope, grant_scope) + values + ('r_6666666666', 'global', true, 'individual'); + select lives_ok('insert_valid_global_role_individual_grants'); + + -- create a row in iam_role_global_individual_project_grant_scope with grant_scope=individual + prepare insert_valid_project_scope_to_individual_grants as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_6666666666', 'individual', 'p____bwidget'); + select lives_ok('insert_valid_project_scope_to_individual_grants'); + + -- take away children grants by updating to iam_role_global to children + prepare update_grant_scope_to_children as + update iam_role_global + set grant_scope = 'children' + where public_id = 'r_6666666666'; + select lives_ok('update_grant_scope_to_children'); + + -- verify that iam_role_global.grant_scope is updated to children + select is(count(*), 1::bigint) from iam_role_global where public_id = 'r_6666666666' and grant_scope = 'children'; + -- check that the update cascaded to iam_role_global_individual_project_grant_scope and iam_role_global_individual_org_grant_scope + -- and that the grant_scope is now children + select is(count(*), 1::bigint) from iam_role_global_individual_project_grant_scope where role_id = 'r_6666666666' and scope_id = 'p____bwidget' and grant_scope = 'children'; + + + -- 4l) update to iam_role_global.grant_scope from children to individual sets + -- individually granted project scope in iam_role_global_individual_project_grant_scope grant_scope to children + prepare insert_r8_valid_global_scope_to_individual_grants as + insert into iam_role_global + (public_id, scope_id, grant_this_role_scope, grant_scope) + values + ('r_8888888888', 'global', true, 'children'); + select lives_ok('insert_r8_valid_global_scope_to_individual_grants'); + + -- create a row in iam_role_global_individual_project_grant_scope with grant_scope=children + prepare insert_r8_valid_project_scope_to_children_grants as + insert into iam_role_global_individual_project_grant_scope + (role_id, grant_scope, scope_id) + values + ('r_8888888888', 'children', 'p____bwidget'); + select lives_ok('insert_r8_valid_project_scope_to_children_grants'); + + -- take away children grants by updating to iam_role_global to individual + prepare update_r8_grant_scope_to_individual as + update iam_role_global + set grant_scope = 'individual' + where public_id = 'r_8888888888'; + select lives_ok('update_r8_grant_scope_to_individual'); + + -- verify that iam_role_global.grant_scope is updated to individual + select is(count(*), 1::bigint) from iam_role_global where public_id = 'r_8888888888' and grant_scope = 'individual'; + -- check that the update deletes all individual grant scopes in iam_role_global_individual_org_grant_scope and iam_role_global_individual_project_grant_scope + select is(count(*), 1::bigint) from iam_role_global_individual_project_grant_scope where role_id = 'r_8888888888' and grant_scope = 'individual' and scope_id = 'p____bwidget'; + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/iam/iam_role_org.sql b/internal/db/sqltest/tests/iam/iam_role_org.sql new file mode 100644 index 0000000000..ff225d57d8 --- /dev/null +++ b/internal/db/sqltest/tests/iam/iam_role_org.sql @@ -0,0 +1,218 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; + select plan(22); + select wtt_load('widgets', 'iam'); + + ------------------------------------------------------------------------------ + -- 1) testing iam_role_org table constraints and insert_role_subtype + ------------------------------------------------------------------------------ + + -- 1a) insert a valid row -> should succeed and fire insert_role_subtype trigger + prepare insert_valid_org_role as + insert into iam_role_org + (public_id, scope_id, grant_this_role_scope, grant_scope) + values + ('r_org_1111111111', + 'o_____widget', + true, + 'children' + ); + + select lives_ok('insert_valid_org_role'); + + -- verify it also created a row in base iam_role + select is( + (select count(*) from iam_role where public_id = 'r_org_1111111111'), + 1::bigint, + 'insert_role_subtype trigger inserted a row into iam_role' + ); + + -- 1b) try duplicate (public_id, grant_scope) => unique violation + prepare insert_dup_public_id_grant_scope as + insert into iam_role_org + (public_id, scope_id, grant_this_role_scope, grant_scope) + values + ('r_org_1111111111', 'o_____widget', true, 'children'); + select throws_like( + 'insert_dup_public_id_grant_scope', + 'duplicate key value violates unique constraint "iam_role_pkey"', + 'unique(public_id, grant_scope) is enforced on iam_role_org' + ); + + -- 1c) invalid grant_scope (not in iam_role_org_grant_scope_enm) + prepare insert_invalid_grant_scope as + insert into iam_role_org + (public_id, scope_id, grant_this_role_scope, grant_scope) + values + ('r_org_bad_grscope', 'o_____widget', true, 'invalid_scope'); + select throws_like( + 'insert_invalid_grant_scope', + 'insert or update on table "iam_role_org" violates foreign key constraint "iam_role_org_grant_scope_enm_fkey"', + 'invalid grant_scope must fail foreign key to iam_role_org_grant_scope_enm' + ); + + -- 1d) invalid scope_id -> must reference iam_scope_org(scope_id) + prepare insert_bad_scope_id as + insert into iam_role_org + (public_id, scope_id, grant_this_role_scope, grant_scope) + values + ('r_org_bad_scope', 'x_no_such_scope', true, 'children'); + select throws_like( + 'insert_bad_scope_id', + 'insert or update on table "iam_role" violates foreign key constraint "iam_scope_scope_id_fkey"', + 'scope_id must exist in iam_scope_org(scope_id)' + ); + + -- 1e) attempt referencing a project scope from iam_scope_project, expecting an error + prepare insert_wrong_scope_type as + insert into iam_role_org + (public_id, scope_id, grant_scope, grant_this_role_scope) + values + ('r_org_wrong_scope', 'p____bwidget', 'children', true); + select throws_like( + 'insert_wrong_scope_type', + 'insert or update on table "iam_role_org" violates foreign key constraint "iam_scope_org_fkey"', + 'must reference an org scope, not a project scope' + ); + + ------------------------------------------------------------------------------ + -- 2) testing grant_scope_update_time trigger + ------------------------------------------------------------------------------ + + -- 2a) insert a row -> expect it to set grant_scope_update_time (if triggered on insert) + prepare insert_with_grant_scope_update_time as + insert into iam_role_org + (public_id, scope_id, grant_scope, grant_scope_update_time) + values + ('r_org_2222222222', 'o_____widget', 'individual', null); + select lives_ok('insert_with_grant_scope_update_time'); + + select is( + (select grant_scope_update_time is not null + from iam_role_org + where public_id = 'r_org_2222222222'), + true, + 'grant_scope_update_time should be set right after insert if the trigger sets it' + ); + + -- 2b) update grant_this_role_scope => trigger should update grant_this_role_scope_update_time + + -- update grant_this_role_scope_update_time to default + prepare reset_grant_this_role_scope_update_time as + update iam_role_org + set grant_this_role_scope_update_time = '1970-01-01 00:00:00' + where public_id = 'r_org_2222222222'; + select lives_ok('reset_grant_this_role_scope_update_time'); + + prepare update_grant_this_role_scope as + update iam_role_org + set grant_this_role_scope = true + where public_id = 'r_org_2222222222'; + select lives_ok('update_grant_this_role_scope'); + + select is( + (select grant_this_role_scope_update_time is not null + from iam_role_org + where public_id = 'r_org_2222222222'), + true, + 'grant_this_role_scope_update_time should be updated after changing grant_this_role_scope' + ); + + select is( + (select grant_this_role_scope_update_time = now() + from iam_role_org + where public_id = 'r_org_2222222222'), + true, + 'grant_this_role_scope_update_time should be updated after changing grant_this_role_scope' + ); + + + -- 2c) update grant_scope => trigger should update grant_this_role_scope_update_time + + prepare reset_grant_scope_update_time as + update iam_role_org + set grant_scope_update_time = '1970-01-01 00:00:00' + where public_id = 'r_org_2222222222'; + select lives_ok('reset_grant_scope_update_time'); + + prepare update_grant_scope as + update iam_role_org + set grant_scope = 'children' + where public_id = 'r_org_2222222222'; + select lives_ok('update_grant_scope'); + + select is( + (select grant_scope_update_time is not null + from iam_role_org + where public_id = 'r_org_2222222222'), + true, + 'grant_scope_update_time should be updated after changing grant_scope' + ); + + select is( + (select grant_scope_update_time = now() + from iam_role_org + where public_id = 'r_org_2222222222'), + true, + 'grant_scope_update_time should be updated after changing grant_scope' + ); + + ------------------------------------------------------------------------------ + -- 3) testing iam_role_global_individual_grant_scope table + ------------------------------------------------------------------------------ + + -- 3a) insert a valid row -> should succeed + prepare update_iam_role_org_to_individual_grant_scope as + update iam_role_org + set grant_scope = 'individual' + where public_id = 'r_op_sw__eng'; + select lives_ok('update_iam_role_org_to_individual_grant_scope'); + + prepare insert_valid_row as + insert into iam_role_org_individual_grant_scope (role_id, grant_scope, scope_id) + values ('r_op_sw__eng', 'individual', 'p____bwidget'); + select lives_ok('insert_valid_row'); + + -- 3b) verify individual grant scope was inserted + select is( + (select count(*) from iam_role_org_individual_grant_scope + where role_id = 'r_op_sw__eng' + and grant_scope = 'individual' + and scope_id = 'p____bwidget'), + 1::bigint, + 'individual grant scope was inserted' + ); + + -- 3c) verify create_time is set by trigger + select isnt( + (select create_time from iam_role_org_individual_grant_scope + where role_id = 'r_op_sw__eng' + and scope_id = 'p____bwidget'), + null, + 'create_time should be set on insert' + ); + + -- 3d) negative test: grant_scope != 'individual' + prepare insert_bad_grant_scope as + insert into iam_role_org_individual_grant_scope (role_id, grant_scope, scope_id) + values ('r_op_sw__eng', 'children', 'p____bwidget'); + select throws_like( + 'insert_bad_grant_scope', + 'new row for relation "iam_role_org_individual_grant_scope" violates check constraint "only_individual_grant_scope_allowed"', + 'grant_scope must be "individual"' + ); + + -- 3e) negative test: referencing a project scope that belongs to another org + prepare insert_wrong_role_project as + insert into iam_role_org_individual_grant_scope (role_id, grant_scope, scope_id) + values ('o_____widget', 'children', 'invalid_project'); + select throws_like( + 'insert_wrong_role_project', + 'project scope_id invalid_project not found in org', + 'ensure_project_belongs_to_role_org trigger enforces matching org' + ); + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/iam/iam_role_project.sql b/internal/db/sqltest/tests/iam/iam_role_project.sql new file mode 100644 index 0000000000..70a17213db --- /dev/null +++ b/internal/db/sqltest/tests/iam/iam_role_project.sql @@ -0,0 +1,94 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: BUSL-1.1 + +begin; +select plan(8); +select wtt_load('widgets', 'iam'); + +-------------------------------------------------------------------------------- +-- 1) test valid inserts +-------------------------------------------------------------------------------- +prepare insert_valid_project_role as + insert into iam_role_project + (public_id, scope_id) + values + ('r_proj_1111111111', 'p____bwidget'); + +select lives_ok('insert_valid_project_role'); + +-- verify the row actually got inserted in iam_role_project +select is( + (select count(*) from iam_role_project where public_id = 'r_proj_1111111111'), + 1::bigint, + 'one valid row inserted into iam_role_project' +); + +-- check that insert_role_subtype trigger created a corresponding row in iam_role +select is( + (select count(*) from iam_role where public_id = 'r_proj_1111111111'), + 1::bigint, + 'insert_role_subtype trigger inserted a row into iam_role' +); + +-- verify create_time is set (default_create_time_column trigger) +select isnt( + (select create_time from iam_role_project where public_id = 'r_proj_1111111111'), + null, + 'create_time is auto-set on insert' +); + +-------------------------------------------------------------------------------- +-- 2) test invalid inserts +-------------------------------------------------------------------------------- + +-- 2a) invalid project scope (not in iam_scope_project) +prepare insert_invalid_scope as + insert into iam_role_project + (public_id, scope_id) + values + ('r_proj_2222222222', 'o_1111111111'); +select throws_like( + 'insert_invalid_scope', + 'insert or update on table "iam_role" violates foreign key constraint "iam_scope_scope_id_fkey"', + 'must reference a valid project scope' +); + +-- 2b) duplicate primary ke +prepare insert_duplicate_role_id as + insert into iam_role_project + (public_id, scope_id) + values + ('r_proj_1111111111', 'p____bwidget'); +select throws_like( + 'insert_duplicate_role_id', + 'duplicate key value violates unique constraint "iam_role_pkey"', + 'primary key (public_id) is enforced' +); + +-------------------------------------------------------------------------------- +-- 3) test triggers for immutable_columns +-------------------------------------------------------------------------------- + +-- 3a) try updating immutable columns: scope_id, create_time +prepare update_scope_id as + update iam_role_project + set scope_id = 'p____bwidget2' + where public_id = 'r_proj_1111111111'; +select throws_like( + 'update_scope_id', + 'immutable column: iam_role_project.scope_id', + 'immutable_columns trigger prevents changing scope_id' +); + +prepare update_create_time as + update iam_role_project + set create_time = null + where public_id = 'r_proj_1111111111'; +select throws_like( + 'update_create_time', + 'immutable column: iam_role_project.create_time', + 'immutable_columns trigger prevents changing create_time' +); + +select * from finish(); +rollback; diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index f4d8e62c9a..e1b5b792f1 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -3672,6 +3672,20 @@ "in": "query", "required": false, "type": "boolean" + }, + { + "name": "create_admin_role", + "description": "", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "create_default_role", + "description": "", + "in": "query", + "required": false, + "type": "boolean" } ], "tags": [ diff --git a/internal/gen/controller/api/services/scope_service.pb.go b/internal/gen/controller/api/services/scope_service.pb.go index 55d318c401..f488831aca 100644 --- a/internal/gen/controller/api/services/scope_service.pb.go +++ b/internal/gen/controller/api/services/scope_service.pb.go @@ -312,6 +312,8 @@ type CreateScopeRequest struct { SkipAdminRoleCreation bool `protobuf:"varint,1,opt,name=skip_admin_role_creation,json=skipAdminRoleCreation,proto3" json:"skip_admin_role_creation,omitempty" class:"public"` // @gotags: `class:"public"` SkipDefaultRoleCreation bool `protobuf:"varint,2,opt,name=skip_default_role_creation,json=skipDefaultRoleCreation,proto3" json:"skip_default_role_creation,omitempty" class:"public"` // @gotags: `class:"public"` Item *scopes.Scope `protobuf:"bytes,3,opt,name=item,proto3" json:"item,omitempty"` + CreateAdminRole bool `protobuf:"varint,4,opt,name=create_admin_role,json=createAdminRole,proto3" json:"create_admin_role,omitempty" class:"public"` // @gotags: `class:"public"` + CreateDefaultRole bool `protobuf:"varint,5,opt,name=create_default_role,json=createDefaultRole,proto3" json:"create_default_role,omitempty" class:"public"` // @gotags: `class:"public"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -367,6 +369,20 @@ func (x *CreateScopeRequest) GetItem() *scopes.Scope { return nil } +func (x *CreateScopeRequest) GetCreateAdminRole() bool { + if x != nil { + return x.CreateAdminRole + } + return false +} + +func (x *CreateScopeRequest) GetCreateDefaultRole() bool { + if x != nil { + return x.CreateDefaultRole + } + return false +} + type CreateScopeResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Uri string `protobuf:"bytes,1,opt,name=uri,proto3" json:"uri,omitempty" class:"public" eventstream:"observation"` // @gotags: `class:"public" eventstream:"observation"` @@ -1223,7 +1239,7 @@ var file_controller_api_services_v1_scope_service_proto_rawDesc = []byte{ 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x5f, 0x69, 0x64, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xc9, 0x01, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xa5, 0x02, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x37, 0x0a, 0x18, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, @@ -1236,275 +1252,280 @@ var file_controller_api_services_v1_scope_service_proto_rawDesc = []byte{ 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x04, 0x69, 0x74, 0x65, - 0x6d, 0x22, 0x66, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x3d, 0x0a, 0x04, 0x69, 0x74, - 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, - 0x6f, 0x70, 0x65, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0xa1, 0x01, 0x0a, 0x12, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x3d, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, + 0x6d, 0x12, 0x2a, 0x0a, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x64, 0x6d, 0x69, + 0x6e, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x2e, 0x0a, + 0x13, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, + 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x22, 0x66, 0x0a, + 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x3d, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, + 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0xa1, 0x01, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x3d, 0x0a, 0x04, + 0x69, 0x74, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x3c, 0x0a, 0x0b, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x0b, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x22, 0x54, 0x0a, 0x13, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3d, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, - 0x3c, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, - 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x22, 0x54, 0x0a, - 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, + 0x24, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x15, 0x0a, 0x13, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, + 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x21, 0x0a, 0x0f, + 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, + 0x51, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, - 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x04, 0x69, - 0x74, 0x65, 0x6d, 0x22, 0x24, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, - 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x15, 0x0a, 0x13, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x21, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x22, 0x51, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, - 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4b, 0x65, 0x79, 0x52, - 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x46, 0x0a, 0x11, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, - 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, - 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, - 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x77, 0x72, 0x61, 0x70, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x72, 0x65, 0x77, 0x72, 0x61, 0x70, 0x22, 0x14, - 0x0a, 0x12, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x41, 0x0a, 0x24, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x4a, 0x6f, 0x62, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, - 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x22, 0x7b, 0x0a, 0x25, 0x4c, 0x69, 0x73, 0x74, 0x4b, - 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x52, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x3c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, - 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x05, 0x69, - 0x74, 0x65, 0x6d, 0x73, 0x22, 0x5b, 0x0a, 0x18, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x4b, - 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6b, - 0x65, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, - 0x64, 0x22, 0x31, 0x0a, 0x19, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x4b, 0x65, 0x79, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x22, 0x72, 0x0a, 0x1a, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, - 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x70, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, - 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x64, 0x12, 0x18, - 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x5c, 0x0a, 0x1b, 0x41, 0x74, 0x74, 0x61, - 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, - 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, - 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x46, 0x0a, 0x1a, 0x44, 0x65, 0x74, 0x61, 0x63, 0x68, - 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x5c, - 0x0a, 0x1b, 0x44, 0x65, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, - 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, + 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x05, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x22, 0x46, 0x0a, 0x11, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x77, 0x72, 0x61, 0x70, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x06, 0x72, 0x65, 0x77, 0x72, 0x61, 0x70, 0x22, 0x14, 0x0a, 0x12, 0x52, 0x6f, + 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x41, 0x0a, 0x24, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x49, 0x64, 0x22, 0x7b, 0x0a, 0x25, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x4a, 0x6f, 0x62, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x05, + 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0xae, 0x16, 0x0a, - 0x0c, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x9d, 0x01, - 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, + 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, + 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x22, 0x5b, 0x0a, 0x18, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x4b, 0x65, 0x79, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6b, 0x65, 0x79, 0x5f, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x6b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x31, 0x0a, + 0x19, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x22, 0x72, 0x0a, 0x1a, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2a, + 0x0a, 0x11, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x5c, 0x0a, 0x1b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x04, 0x69, 0x74, + 0x65, 0x6d, 0x22, 0x46, 0x0a, 0x1a, 0x44, 0x65, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x5c, 0x0a, 0x1b, 0x44, 0x65, + 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x69, 0x74, 0x65, + 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, + 0x70, 0x65, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0xae, 0x16, 0x0a, 0x0c, 0x53, 0x63, 0x6f, + 0x70, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x9d, 0x01, 0x0a, 0x08, 0x47, 0x65, + 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x36, 0x92, 0x41, 0x16, 0x12, 0x14, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, + 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x17, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, + 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xbe, 0x01, 0x0a, 0x0a, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51, 0x92, 0x41, 0x3c, 0x12, 0x3a, 0x4c, 0x69, + 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x20, 0x77, + 0x69, 0x74, 0x68, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x20, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, + 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0xaa, 0x01, 0x0a, 0x0b, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x63, + 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x63, + 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3a, 0x92, 0x41, 0x19, + 0x12, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, + 0x6c, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x3a, + 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x0a, 0x2f, 0x76, 0x31, + 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0xa8, 0x01, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x92, 0x41, 0x16, 0x12, 0x14, 0x47, 0x65, 0x74, 0x73, - 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x0f, 0x2f, 0x76, - 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xbe, 0x01, - 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x2d, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, - 0x6f, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x6f, - 0x70, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51, 0x92, 0x41, 0x3c, - 0x12, 0x3a, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x53, 0x63, 0x6f, 0x70, - 0x65, 0x73, 0x20, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x53, 0x63, - 0x6f, 0x70, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x20, - 0x74, 0x68, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0xaa, - 0x01, 0x0a, 0x0b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2e, - 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, - 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x3a, 0x92, 0x41, 0x19, 0x12, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, - 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x18, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, - 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0xa8, 0x01, 0x0a, 0x0b, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, - 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, - 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38, 0x92, 0x41, - 0x12, 0x12, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, - 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, - 0x04, 0x69, 0x74, 0x65, 0x6d, 0x32, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, - 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0x9c, 0x01, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, - 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, - 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x92, 0x41, 0x12, 0x12, 0x10, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x11, 0x2a, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, - 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x12, 0xa6, 0x01, 0x0a, 0x08, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, - 0x79, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x38, 0x92, 0x41, 0x12, 0x12, 0x10, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, + 0x6d, 0x32, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x69, + 0x64, 0x7d, 0x12, 0x9c, 0x01, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, + 0x70, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3f, 0x92, - 0x41, 0x1b, 0x12, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x6b, 0x65, 0x79, - 0x73, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x1b, 0x12, 0x19, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, - 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6b, 0x65, 0x79, 0x73, 0x12, 0xae, - 0x01, 0x0a, 0x0a, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x2d, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, - 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, - 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x41, 0x92, 0x41, - 0x1d, 0x12, 0x1b, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x6b, 0x65, - 0x79, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x1b, 0x3a, 0x01, 0x2a, 0x22, 0x16, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, - 0x70, 0x65, 0x73, 0x3a, 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x2d, 0x6b, 0x65, 0x79, 0x73, 0x12, - 0xa4, 0x02, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, - 0x73, 0x12, 0x40, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, - 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, - 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x7e, 0x92, 0x41, 0x3c, 0x12, 0x3a, 0x4c, 0x69, 0x73, - 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x6b, - 0x65, 0x79, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x64, 0x65, 0x73, 0x74, 0x72, - 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6a, 0x6f, 0x62, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x61, - 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x39, 0x12, 0x37, 0x2f, - 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x73, 0x63, 0x6f, 0x70, 0x65, - 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6b, 0x65, 0x79, 0x2d, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2d, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x2d, 0x6a, 0x6f, 0x62, 0x73, 0x12, 0xaa, 0x03, 0x0a, 0x11, 0x44, 0x65, 0x73, 0x74, 0x72, - 0x6f, 0x79, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, - 0x79, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa7, 0x02, 0x92, 0x41, 0xfa, 0x01, - 0x12, 0xf7, 0x01, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, - 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, - 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x6d, 0x61, 0x79, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, - 0x61, 0x6e, 0x20, 0x61, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x6f, 0x75, 0x73, 0x20, - 0x6a, 0x6f, 0x62, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x72, 0x65, 0x2d, 0x65, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x64, 0x61, 0x74, 0x61, 0x20, 0x65, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, - 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x20, 0x55, 0x73, 0x65, 0x20, 0x47, 0x45, 0x54, 0x20, 0x2f, 0x76, - 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, - 0x69, 0x64, 0x7d, 0x3a, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6b, 0x65, 0x79, 0x2d, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x2d, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x2d, 0x6a, 0x6f, 0x62, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, - 0x20, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6a, 0x6f, 0x62, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, - 0x3a, 0x01, 0x2a, 0x22, 0x1e, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x3a, - 0x64, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x2d, 0x6b, 0x65, 0x79, 0x2d, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x12, 0xf6, 0x01, 0x0a, 0x13, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, - 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x36, 0x2e, 0x63, 0x6f, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x2c, 0x92, 0x41, 0x12, 0x12, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x73, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, + 0x2a, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x69, 0x64, + 0x7d, 0x12, 0xa6, 0x01, 0x0a, 0x08, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x2b, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x53, - 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x6e, 0x92, 0x41, - 0x35, 0x12, 0x33, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, - 0x65, 0x20, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, - 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x30, 0x3a, 0x01, 0x2a, 0x62, - 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x25, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, - 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x2d, 0x73, 0x74, - 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2d, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0xf8, 0x01, 0x0a, - 0x13, 0x44, 0x65, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, - 0x6c, 0x69, 0x63, 0x79, 0x12, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, - 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x74, 0x61, 0x63, 0x68, - 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x70, 0x92, 0x41, 0x37, 0x12, 0x35, 0x44, 0x65, 0x74, 0x61, - 0x63, 0x68, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, - 0x65, 0x64, 0x20, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x20, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, - 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x30, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, - 0x22, 0x25, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x69, 0x64, - 0x7d, 0x3a, 0x64, 0x65, 0x74, 0x61, 0x63, 0x68, 0x2d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, - 0x2d, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x1a, 0xa3, 0x03, 0x92, 0x41, 0x9f, 0x03, 0x0a, 0x0d, - 0x53, 0x63, 0x6f, 0x70, 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x8f, 0x02, - 0x41, 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x20, 0x61, 0x63, 0x74, 0x73, 0x20, 0x61, 0x73, 0x20, - 0x61, 0x20, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x42, 0x6f, 0x75, - 0x6e, 0x64, 0x61, 0x72, 0x79, 0x20, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x65, 0x64, 0x20, 0x75, 0x73, - 0x69, 0x6e, 0x67, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6e, 0x63, 0x65, 0x70, 0x74, 0x20, - 0x6f, 0x66, 0x20, 0x61, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x20, - 0x49, 0x74, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x68, 0x69, 0x65, - 0x72, 0x61, 0x72, 0x63, 0x68, 0x69, 0x63, 0x61, 0x6c, 0x20, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x75, 0x72, 0x65, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x6c, 0x65, 0x74, 0x73, 0x20, 0x79, 0x6f, - 0x75, 0x20, 0x6f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x65, 0x20, 0x79, 0x6f, 0x75, 0x72, 0x20, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x20, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x20, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x20, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x20, 0x42, 0x6f, 0x75, 0x6e, 0x64, - 0x61, 0x72, 0x79, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x20, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x73, 0x20, - 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x65, 0x74, - 0x20, 0x79, 0x6f, 0x75, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x20, 0x73, 0x63, 0x6f, 0x70, - 0x65, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2e, 0x1a, - 0x7c, 0x0a, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x20, 0x73, 0x63, - 0x6f, 0x70, 0x65, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x42, 0x6f, 0x75, 0x6e, - 0x64, 0x61, 0x72, 0x79, 0x20, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x20, 0x6d, 0x6f, 0x64, 0x65, - 0x6c, 0x12, 0x4a, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x76, 0x65, 0x6c, - 0x6f, 0x70, 0x65, 0x72, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x64, 0x6f, 0x63, 0x73, - 0x2f, 0x63, 0x6f, 0x6e, 0x63, 0x65, 0x70, 0x74, 0x73, 0x2f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x2d, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x42, 0x4d, 0x5a, - 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3f, 0x92, 0x41, 0x1b, 0x12, 0x19, + 0x4c, 0x69, 0x73, 0x74, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x6b, 0x65, 0x79, 0x73, 0x20, 0x69, 0x6e, + 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x12, + 0x19, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, + 0x3a, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6b, 0x65, 0x79, 0x73, 0x12, 0xae, 0x01, 0x0a, 0x0a, 0x52, + 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x41, 0x92, 0x41, 0x1d, 0x12, 0x1b, 0x52, + 0x6f, 0x74, 0x61, 0x74, 0x65, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x6b, 0x65, 0x79, 0x73, 0x20, 0x69, + 0x6e, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, + 0x3a, 0x01, 0x2a, 0x22, 0x16, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x3a, + 0x72, 0x6f, 0x74, 0x61, 0x74, 0x65, 0x2d, 0x6b, 0x65, 0x79, 0x73, 0x12, 0xa4, 0x02, 0x0a, 0x1d, + 0x4c, 0x69, 0x73, 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, + 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x73, 0x12, 0x40, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4b, + 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x41, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, + 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x6f, 0x62, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x7e, 0x92, 0x41, 0x3c, 0x12, 0x3a, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, + 0x6c, 0x6c, 0x20, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x20, 0x6a, 0x6f, 0x62, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, + 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x39, 0x12, 0x37, 0x2f, 0x76, 0x31, 0x2f, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x7d, + 0x3a, 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6b, 0x65, 0x79, 0x2d, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x2d, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x6a, 0x6f, + 0x62, 0x73, 0x12, 0xaa, 0x03, 0x0a, 0x11, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x4b, 0x65, + 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x4b, 0x65, 0x79, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x73, 0x74, + 0x72, 0x6f, 0x79, 0x4b, 0x65, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa7, 0x02, 0x92, 0x41, 0xfa, 0x01, 0x12, 0xf7, 0x01, 0x44, + 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, + 0x66, 0x69, 0x65, 0x64, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x20, 0x69, 0x6e, 0x20, 0x61, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x20, 0x54, 0x68, 0x69, + 0x73, 0x20, 0x6d, 0x61, 0x79, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x61, 0x6e, 0x20, 0x61, + 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x6f, 0x75, 0x73, 0x20, 0x6a, 0x6f, 0x62, 0x20, + 0x74, 0x68, 0x61, 0x74, 0x20, 0x72, 0x65, 0x2d, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x73, + 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x64, 0x61, 0x74, 0x61, 0x20, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, + 0x66, 0x69, 0x65, 0x64, 0x20, 0x6b, 0x65, 0x79, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x2e, 0x20, 0x55, 0x73, 0x65, 0x20, 0x47, 0x45, 0x54, 0x20, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, + 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x7d, 0x3a, + 0x6c, 0x69, 0x73, 0x74, 0x2d, 0x6b, 0x65, 0x79, 0x2d, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x2d, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x6a, 0x6f, 0x62, + 0x73, 0x20, 0x74, 0x6f, 0x20, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x20, 0x70, 0x65, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x20, 0x64, 0x65, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x20, 0x6a, 0x6f, 0x62, 0x73, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x3a, 0x01, 0x2a, 0x22, + 0x1e, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x3a, 0x64, 0x65, 0x73, 0x74, + 0x72, 0x6f, 0x79, 0x2d, 0x6b, 0x65, 0x79, 0x2d, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, + 0xf6, 0x01, 0x0a, 0x13, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x74, 0x74, + 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x6e, 0x92, 0x41, 0x35, 0x12, 0x33, 0x41, + 0x74, 0x74, 0x61, 0x63, 0x68, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x70, 0x65, 0x63, + 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x20, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, + 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x30, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, + 0x6d, 0x22, 0x25, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x69, + 0x64, 0x7d, 0x3a, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x2d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x2d, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0xf8, 0x01, 0x0a, 0x13, 0x44, 0x65, 0x74, + 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x12, 0x36, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, + 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x74, 0x61, 0x63, 0x68, 0x53, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x70, 0x92, 0x41, 0x37, 0x12, 0x35, 0x44, 0x65, 0x74, 0x61, 0x63, 0x68, 0x65, 0x73, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x64, 0x20, 0x53, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x20, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x20, 0x66, 0x72, + 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x30, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x25, 0x2f, 0x76, + 0x31, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x64, 0x65, + 0x74, 0x61, 0x63, 0x68, 0x2d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2d, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x1a, 0xa3, 0x03, 0x92, 0x41, 0x9f, 0x03, 0x0a, 0x0d, 0x53, 0x63, 0x6f, 0x70, + 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x8f, 0x02, 0x41, 0x20, 0x73, 0x63, + 0x6f, 0x70, 0x65, 0x20, 0x61, 0x63, 0x74, 0x73, 0x20, 0x61, 0x73, 0x20, 0x61, 0x20, 0x70, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, + 0x79, 0x20, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x65, 0x64, 0x20, 0x75, 0x73, 0x69, 0x6e, 0x67, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6e, 0x63, 0x65, 0x70, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x61, + 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x20, 0x49, 0x74, 0x20, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x68, 0x69, 0x65, 0x72, 0x61, 0x72, 0x63, + 0x68, 0x69, 0x63, 0x61, 0x6c, 0x20, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x75, 0x72, 0x65, 0x20, + 0x74, 0x68, 0x61, 0x74, 0x20, 0x6c, 0x65, 0x74, 0x73, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x6f, 0x72, + 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x65, 0x20, 0x79, 0x6f, 0x75, 0x72, 0x20, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x20, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x20, + 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x20, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2e, + 0x20, 0x54, 0x68, 0x65, 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x73, 0x20, 0x65, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x6c, 0x65, 0x74, 0x20, 0x79, 0x6f, 0x75, + 0x20, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x20, 0x69, + 0x6e, 0x20, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2e, 0x1a, 0x7c, 0x0a, 0x2e, 0x52, + 0x65, 0x61, 0x64, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, + 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, + 0x20, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x20, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x4a, 0x68, + 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x2f, 0x63, 0x6f, 0x6e, + 0x63, 0x65, 0x70, 0x74, 0x73, 0x2f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2d, 0x6d, 0x6f, 0x64, + 0x65, 0x6c, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/iam/account.go b/internal/iam/account.go index 6781c753cf..6ada6fbcf6 100644 --- a/internal/iam/account.go +++ b/internal/iam/account.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/oplog" - "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/internal/types/resource" "google.golang.org/protobuf/proto" ) @@ -57,8 +57,8 @@ func (a *authAccount) VetForWrite(ctx context.Context, r db.Reader, opType db.Op return nil } -func (a *authAccount) validScopeTypes() []scope.Type { - return []scope.Type{scope.Global, scope.Org} +func (a *authAccount) getResourceType() resource.Type { + return resource.Account } // GetScope returns the scope for the auth account. diff --git a/internal/iam/group.go b/internal/iam/group.go index a75ee7191a..a129b74fd3 100644 --- a/internal/iam/group.go +++ b/internal/iam/group.go @@ -12,7 +12,6 @@ import ( "github.com/hashicorp/boundary/internal/iam/store" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" - "github.com/hashicorp/boundary/internal/types/scope" "google.golang.org/protobuf/proto" ) @@ -78,8 +77,8 @@ func (g *Group) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, return nil } -func (g *Group) validScopeTypes() []scope.Type { - return []scope.Type{scope.Global, scope.Org, scope.Project} +func (g *Group) getResourceType() resource.Type { + return resource.Group } // GetScope returns the scope for the Group. diff --git a/internal/iam/immutable_fields_test.go b/internal/iam/immutable_fields_test.go index 3633d90ad5..52da6656c6 100644 --- a/internal/iam/immutable_fields_test.go +++ b/internal/iam/immutable_fields_test.go @@ -8,8 +8,10 @@ import ( "strings" "testing" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/timestamp" + iamstore "github.com/hashicorp/boundary/internal/iam/store" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" @@ -69,7 +71,6 @@ func TestScope_ImmutableFields(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) orig := new.Clone() @@ -129,7 +130,6 @@ func TestConcreteScope_ImmutableFields(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) updateStmt := strings.Replace(update, "{{rep}}", tt.tableName, -1) @@ -192,7 +192,6 @@ func TestUser_ImmutableFields(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) orig := new.Clone() @@ -212,7 +211,85 @@ func TestUser_ImmutableFields(t *testing.T) { } } -func TestRole_ImmutableFields(t *testing.T) { +func Test_globalRole_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + w := db.New(conn) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + _, proj := TestScopes(t, repo) + + ctx := context.Background() + roleId, err := newRoleId(ctx) + require.NoError(t, err) + testGlobalRole := &globalRole{ + GlobalRole: &iamstore.GlobalRole{ + PublicId: roleId, + ScopeId: globals.GlobalPrefix, + GrantScope: globals.GrantScopeIndividual, + GrantThisRoleScope: true, + }, + } + require.NoError(t, w.Create(ctx, testGlobalRole)) + require.NotEmpty(t, testGlobalRole.PublicId) + + tests := []struct { + name string + update *globalRole + fieldMask []string + }{ + { + name: "public_id", + update: func() *globalRole { + c := testGlobalRole.Clone().(*globalRole) + c.PublicId = "r_thisIsNotAValidId" + return c + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create_time", + update: func() *globalRole { + c := testGlobalRole.Clone().(*globalRole) + c.CreateTime = &ts + return c + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "scope_id", + update: func() *globalRole { + c := testGlobalRole.Clone().(*globalRole) + c.ScopeId = proj.PublicId + return c + }(), + fieldMask: []string{"ScopeId"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + orig := testGlobalRole.Clone() + err := w.LookupById(context.Background(), orig) + require.NoError(err) + + rowsUpdated, err := w.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true)) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := testGlobalRole.Clone() + err = w.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig.(*globalRole), after.(*globalRole))) + }) + } +} + +func Test_orgRole_ImmutableFields(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) @@ -222,35 +299,49 @@ func TestRole_ImmutableFields(t *testing.T) { ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} org, proj := TestScopes(t, repo) - new := TestRole(t, conn, org.PublicId) + + ctx := context.Background() + orgRoleId, err := newRoleId(ctx) + require.NoError(t, err) + + testOrgRole := &orgRole{ + OrgRole: &iamstore.OrgRole{ + PublicId: orgRoleId, + ScopeId: org.PublicId, + GrantScope: globals.GrantScopeIndividual, + GrantThisRoleScope: true, + }, + } + require.NoError(t, w.Create(ctx, testOrgRole)) + require.NotEmpty(t, testOrgRole.PublicId) tests := []struct { name string - update *Role + update *orgRole fieldMask []string }{ { name: "public_id", - update: func() *Role { - c := new.Clone().(*Role) + update: func() *orgRole { + c := testOrgRole.Clone().(*orgRole) c.PublicId = "r_thisIsNotAValidId" return c }(), fieldMask: []string{"PublicId"}, }, { - name: "create time", - update: func() *Role { - c := new.Clone().(*Role) + name: "create_time", + update: func() *orgRole { + c := testOrgRole.Clone().(*orgRole) c.CreateTime = &ts return c }(), fieldMask: []string{"CreateTime"}, }, { - name: "scope id", - update: func() *Role { - c := new.Clone().(*Role) + name: "scope_id", + update: func() *orgRole { + c := testOrgRole.Clone().(*orgRole) c.ScopeId = proj.PublicId return c }(), @@ -258,10 +349,9 @@ func TestRole_ImmutableFields(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - orig := new.Clone() + orig := testOrgRole.Clone() err := w.LookupById(context.Background(), orig) require.NoError(err) @@ -269,11 +359,88 @@ func TestRole_ImmutableFields(t *testing.T) { require.Error(err) assert.Equal(0, rowsUpdated) - after := new.Clone() + after := testOrgRole.Clone() + err = w.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig.(*orgRole), after.(*orgRole))) + }) + } +} + +func Test_projRole_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + w := db.New(conn) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + _, proj := TestScopes(t, repo) + _, proj2 := TestScopes(t, repo) + + ctx := context.Background() + projRoleId, err := newRoleId(ctx) + require.NoError(t, err) + testProjectRole := &projectRole{ + ProjectRole: &iamstore.ProjectRole{ + PublicId: projRoleId, + ScopeId: proj.PublicId, + }, + } + require.NoError(t, w.Create(ctx, testProjectRole)) + require.NotEmpty(t, testProjectRole.PublicId) + + tests := []struct { + name string + update *projectRole + fieldMask []string + }{ + { + name: "public_id", + update: func() *projectRole { + c := testProjectRole.Clone().(*projectRole) + c.PublicId = "r_thisIsNotAValidId" + return c + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create_time", + update: func() *projectRole { + c := testProjectRole.Clone().(*projectRole) + c.CreateTime = &ts + return c + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "scope_id", + update: func() *projectRole { + c := testProjectRole.Clone().(*projectRole) + c.ScopeId = proj2.PublicId + return c + }(), + fieldMask: []string{"ScopeId"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + orig := testProjectRole.Clone() + err := w.LookupById(context.Background(), orig) + require.NoError(err) + + rowsUpdated, err := w.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true)) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := testProjectRole.Clone() err = w.LookupById(context.Background(), after) require.NoError(err) - assert.True(proto.Equal(orig.(*Role), after.(*Role))) + assert.True(proto.Equal(orig.(*projectRole), after.(*projectRole))) }) } } @@ -324,7 +491,6 @@ func TestGroup_ImmutableFields(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) orig := new.Clone() diff --git a/internal/iam/options.go b/internal/iam/options.go index 990da7ace0..a89691b9f1 100644 --- a/internal/iam/options.go +++ b/internal/iam/options.go @@ -42,7 +42,9 @@ type options struct { withReader db.Reader withWriter db.Writer withStartPageAfterItem pagination.Item - withTestCacheMultiGrantTuples *[]multiGrantTuple + withTestCacheMultiGrantTuples *[]MultiGrantTuple + withCreateDefaultRole bool + withCreateAdminRole bool } func getDefaultOptions() options { @@ -119,6 +121,22 @@ func WithSkipAdminRoleCreation(enable bool) Option { } } +// WithCreateAdminRole provides an option to enable the automatic +// creation of an admin role when a new scope is created. +func WithCreateAdminRole(enable bool) Option { + return func(o *options) { + o.withCreateAdminRole = enable + } +} + +// WithCreateDefaultRole provides an option to enable the automatic +// creation of a default role when a new scope is created. +func WithCreateDefaultRole(enable bool) Option { + return func(o *options) { + o.withCreateDefaultRole = enable + } +} + // WithSkipDefaultRoleCreation provides an option to disable the automatic // creation of a default role when a new scope is created. func WithSkipDefaultRoleCreation(enable bool) Option { @@ -177,7 +195,7 @@ func WithStartPageAfterItem(item pagination.Item) Option { } } -func withTestCacheMultiGrantTuples(cache *[]multiGrantTuple) Option { +func WithTestCacheMultiGrantTuples(cache *[]MultiGrantTuple) Option { return func(o *options) { o.withTestCacheMultiGrantTuples = cache } diff --git a/internal/iam/query.go b/internal/iam/query.go index 74af0b713e..d24c438510 100644 --- a/internal/iam/query.go +++ b/internal/iam/query.go @@ -221,4 +221,158 @@ const ( estimateCountScopes = ` select reltuples::bigint as estimate from pg_class where oid in ('iam_scope'::regclass) ` + + scopeIdFromRoleIdQuery = ` + select scope_id + from iam_role + where public_id = @public_id;` + + listRolesQuery = ` +with +combined_role_types (role_id) as ( + select public_id + from iam_role + where %s -- the where clause is programmatically generated + order by update_time desc, public_id desc + limit @limit +) +select public_id, + scope_id, + name, + description, + create_time, + update_time, + version + from iam_role_global + where public_id = any(select role_id from combined_role_types) + union all +select public_id, + scope_id, + name, + description, + create_time, + update_time, + version + from iam_role_org + where public_id = any(select role_id from combined_role_types) + union all +select public_id, + scope_id, + name, + description, + create_time, + update_time, + version + from iam_role_project + where public_id = any(select role_id from combined_role_types) + order by update_time desc, public_id desc +` + + roleGrantsScopeQuery = ` +with +global_roles (role_id) as ( + select public_id as role_id + from iam_role_global + where public_id = any($1) +), +org_roles (role_id) as ( + select public_id as role_id + from iam_role_org + where public_id = any($1) +), +proj_roles (role_id) as ( + select public_id as role_id + from iam_role_project + where public_id = any($1) +), +global_role_this_grants (role_id, scope_id_or_special, create_time) as ( + select public_id as role_id, + 'this' as scope_id_or_special, + grant_this_role_scope_update_time as create_time + from iam_role_global + where public_id = any (select role_id from global_roles) + and grant_this_role_scope = true +), +org_role_this_grants (role_id, scope_id_or_special, create_time) as ( + select public_id as role_id, + 'this' as scope_id_or_special, + grant_this_role_scope_update_time as create_time + from iam_role_org + where public_id = any (select role_id from org_roles) + and grant_this_role_scope = true +), +proj_role_this_grants (role_id, scope_id_or_special, create_time) as ( + select public_id as role_id, + 'this' as scope_id_or_special, + create_time as create_time + from iam_role_project + where public_id = any (select role_id from proj_roles) +), +global_role_special_grants (role_id, scope_id_or_special, create_time) as ( + select public_id as role_id, + grant_scope as scope_id_or_special, + grant_this_role_scope_update_time as create_time + from iam_role_global + where public_id = any (select role_id from global_roles) + and grant_scope != 'individual' +), +org_role_special_grants (role_id, scope_id_or_special, create_time) as ( + select public_id as role_id, + grant_scope as scope_id_or_special, + grant_this_role_scope_update_time as create_time + from iam_role_org + where public_id = any (select role_id from org_roles) + and grant_scope != 'individual' +), +global_role_individual_org_grants (role_id, scope_id_or_special, create_time) as ( + select role_id as role_id, + scope_id as scope_id_or_special, + create_time as create_time + from iam_role_global_individual_org_grant_scope + where role_id = any (select role_id from global_roles) +), +global_role_individual_proj_grants (role_id, scope_id_or_special, create_time) as ( + select role_id as role_id, + scope_id as scope_id_or_special, + create_time as create_time + from iam_role_global_individual_project_grant_scope + where role_id = any (select role_id from global_roles) +), +org_role_individual_grants (role_id, scope_id_or_special, create_time) as ( + select role_id as role_id, + scope_id as scope_id_or_special, + create_time as create_time + from iam_role_org_individual_grant_scope + where role_id = any (select role_id from org_roles) +), +final (role_id, scope_id_or_special, create_time) as ( + select role_id, scope_id_or_special, create_time + from global_role_this_grants + union + select role_id, scope_id_or_special, create_time + from org_role_this_grants + union + select role_id, scope_id_or_special, create_time + from proj_role_this_grants + union + select role_id, scope_id_or_special, create_time + from global_role_special_grants + union + select role_id, scope_id_or_special, create_time + from org_role_special_grants + union + select role_id, scope_id_or_special, create_time + from global_role_individual_org_grants + union + select role_id, scope_id_or_special, create_time + from global_role_individual_proj_grants + union + select role_id, scope_id_or_special, create_time + from org_role_individual_grants +) +select role_id, + scope_id_or_special, + create_time + from final; +` ) diff --git a/internal/iam/repository_principal_role.go b/internal/iam/repository_principal_role.go index c261c60846..b683e71910 100644 --- a/internal/iam/repository_principal_role.go +++ b/internal/iam/repository_principal_role.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/types/scope" ) // AddPrincipalRoles provides the ability to add principals (userIds and @@ -35,7 +36,6 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV if len(userIds) == 0 && len(groupIds) == 0 && len(managedGroupIds) == 0 { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing any of users, groups, or managed groups to add") } - newUserRoles := make([]*UserRole, 0, len(userIds)) for _, id := range userIds { usrRole, err := NewUserRole(ctx, roleId, id) @@ -60,15 +60,12 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV } newManagedGrpRoles = append(newManagedGrpRoles, managedGrpRole) } - - role := allocRole() - role.PublicId = roleId - scope, err := role.GetScope(ctx, r.reader) + scp, err := getRoleScope(ctx, r.reader, roleId) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId))) } - oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog) + oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } @@ -80,15 +77,33 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { msgs := make([]*oplog.Message, 0, 2) - roleTicket, err := w.GetTicket(ctx, &role) + var updatedRole Resource + switch scp.GetType() { + case scope.Global.String(): + g := allocGlobalRole() + g.PublicId = roleId + g.Version = roleVersion + 1 + updatedRole = &g + case scope.Org.String(): + o := allocOrgRole() + o.PublicId = roleId + o.Version = roleVersion + 1 + updatedRole = &o + case scope.Project.String(): + p := allocProjectRole() + p.PublicId = roleId + p.Version = roleVersion + 1 + updatedRole = &p + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId())) + } + roleTicket, err := w.GetTicket(ctx, updatedRole) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - updatedRole := allocRole() - updatedRole.PublicId = roleId - updatedRole.Version = roleVersion + 1 + var roleOplogMsg oplog.Message - rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) + rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version")) } @@ -119,8 +134,8 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV } metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, - "scope-id": []string{scope.PublicId}, - "scope-type": []string{scope.Type}, + "scope-id": []string{scp.PublicId}, + "scope-type": []string{scp.Type}, "resource-public-id": []string{roleId}, } if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil { @@ -160,9 +175,6 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV if roleVersion == 0 { return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") } - role := allocRole() - role.PublicId = roleId - // it's "safe" to do this lookup outside the DoTx transaction because we // have a roleVersion so the principals can’t change without the version // changing. @@ -170,7 +182,7 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) } - toSet, err := r.PrincipalsToSet(ctx, &role, userIds, groupIds, managedGroupIds) + toSet, err := r.PrincipalsToSet(ctx, &Role{PublicId: roleId}, userIds, groupIds, managedGroupIds) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) } @@ -180,11 +192,11 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV return toSet.UnchangedPrincipalRoles, db.NoRowsAffected, nil } - scope, err := role.GetScope(ctx, r.reader) + scp, err := getRoleScope(ctx, r.reader, roleId) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId))) } - oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog) + oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } @@ -199,15 +211,32 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV // we need a roleTicket, which won't be redeemed until all the other // writes are successful. We can't just use a single ticket because // we need to write oplog entries for deletes and adds - roleTicket, err := w.GetTicket(ctx, &role) + var updatedRole Resource + switch scp.GetType() { + case scope.Global.String(): + g := allocGlobalRole() + g.PublicId = roleId + g.Version = roleVersion + 1 + updatedRole = &g + case scope.Org.String(): + o := allocOrgRole() + o.PublicId = roleId + o.Version = roleVersion + 1 + updatedRole = &o + case scope.Project.String(): + p := allocProjectRole() + p.PublicId = roleId + p.Version = roleVersion + 1 + updatedRole = &p + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId())) + } + roleTicket, err := w.GetTicket(ctx, updatedRole) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket for role")) } - updatedRole := allocRole() - updatedRole.PublicId = roleId - updatedRole.Version = roleVersion + 1 var roleOplogMsg oplog.Message - rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) + rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) if err != nil { return errors.Wrap(ctx, err, op) } @@ -217,8 +246,8 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV msgs := make([]*oplog.Message, 0, 5) metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_UPDATE.String()}, - "scope-id": []string{scope.PublicId}, - "scope-type": []string{scope.Type}, + "scope-id": []string{scp.PublicId}, + "scope-type": []string{scp.Type}, "resource-public-id": []string{roleId}, } msgs = append(msgs, &roleOplogMsg) @@ -335,9 +364,12 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro if roleVersion == 0 { return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") } - role := allocRole() - role.PublicId = roleId + var roleResource Resource + scp, err := getRoleScope(ctx, r.reader, roleId) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId))) + } deleteUserRoles := make([]*UserRole, 0, len(userIds)) for _, id := range userIds { usrRole, err := NewUserRole(ctx, roleId, id) @@ -363,11 +395,7 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro deleteManagedGrpRoles = append(deleteManagedGrpRoles, managedGrpRole) } - scope, err := role.GetScope(ctx, r.reader) - if err != nil { - return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope to create metadata", roleId))) - } - oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog) + oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog) if err != nil { return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } @@ -379,15 +407,32 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { msgs := make([]*oplog.Message, 0, 2) - roleTicket, err := w.GetTicket(ctx, &role) + var updatedRole Resource + switch scp.Type { + case scope.Global.String(): + g := allocGlobalRole() + g.PublicId = roleId + g.Version = roleVersion + 1 + updatedRole = &g + case scope.Org.String(): + o := allocOrgRole() + o.PublicId = roleId + o.Version = roleVersion + 1 + updatedRole = &o + case scope.Project.String(): + p := allocProjectRole() + p.PublicId = roleId + p.Version = roleVersion + 1 + updatedRole = &p + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown role resource type %T", roleResource)) + } + roleTicket, err := w.GetTicket(ctx, updatedRole) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - updatedRole := allocRole() - updatedRole.PublicId = roleId - updatedRole.Version = roleVersion + 1 var roleOplogMsg oplog.Message - rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) + rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version")) } @@ -433,8 +478,8 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro } metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_DELETE.String()}, - "scope-id": []string{scope.PublicId}, - "scope-type": []string{scope.Type}, + "scope-id": []string{scp.PublicId}, + "scope-type": []string{scp.Type}, "resource-public-id": []string{roleId}, } if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil { diff --git a/internal/iam/repository_principal_role_test.go b/internal/iam/repository_principal_role_test.go index f2c24563a0..6375adbc62 100644 --- a/internal/iam/repository_principal_role_test.go +++ b/internal/iam/repository_principal_role_test.go @@ -6,6 +6,7 @@ package iam import ( "context" "sort" + "strings" "testing" "time" @@ -24,6 +25,7 @@ func TestRepository_AddPrincipalRoles(t *testing.T) { wrapper := db.TestWrapper(t) repo := TestRepo(t, conn, wrapper) staticOrg, staticProj := TestScopes(t, repo) + globalRole := TestRole(t, conn, globals.GlobalPrefix) orgRole := TestRole(t, conn, staticOrg.PublicId) projRole := TestRole(t, conn, staticProj.PublicId) createScopesFn := func() (orgs []string, projects []string) { @@ -134,7 +136,7 @@ func TestRepository_AddPrincipalRoles(t *testing.T) { orgs, projects := createScopesFn() var userIds, groupIds []string - for _, roleId := range []string{orgRole.PublicId, projRole.PublicId} { + for _, roleId := range []string{globalRole.PublicId, orgRole.PublicId, projRole.PublicId} { origRole, _, _, _, err := repo.LookupRole(context.Background(), roleId) require.NoError(err) @@ -260,7 +262,9 @@ func TestRepository_ListPrincipalRoles(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - db.TestDeleteWhere(t, conn, func() any { r := allocRole(); return &r }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocGlobalRole(); return &i }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocOrgRole(); return &i }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocProjectRole(); return &i }(), "1=1") role := TestRole(t, conn, tt.createScopeId) userRoles := make([]string, 0, tt.createCnt) groupRoles := make([]string, 0, tt.createCnt) @@ -299,7 +303,7 @@ func TestRepository_DeletePrincipalRoles(t *testing.T) { rw := db.New(conn) wrapper := db.TestWrapper(t) repo := TestRepo(t, conn, wrapper) - org, _ := TestScopes(t, repo) + org, proj := TestScopes(t, repo) type args struct { role *Role @@ -319,7 +323,19 @@ func TestRepository_DeletePrincipalRoles(t *testing.T) { wantIsErr errors.Code }{ { - name: "valid", + name: "valid-global", + args: args{ + role: TestRole(t, conn, globals.GlobalPrefix), + createUserCnt: 5, + createGroupCnt: 5, + deleteUserCnt: 5, + deleteGroupCnt: 5, + }, + wantRowsDeleted: 10, + wantErr: false, + }, + { + name: "valid-org", args: args{ role: TestRole(t, conn, org.PublicId), createUserCnt: 5, @@ -330,6 +346,18 @@ func TestRepository_DeletePrincipalRoles(t *testing.T) { wantRowsDeleted: 10, wantErr: false, }, + { + name: "valid-proj", + args: args{ + role: TestRole(t, conn, proj.PublicId), + createUserCnt: 5, + createGroupCnt: 5, + deleteUserCnt: 5, + deleteGroupCnt: 5, + }, + wantRowsDeleted: 10, + wantErr: false, + }, { name: "valid-keeping-some", args: args{ @@ -558,7 +586,31 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { wantErr bool }{ { - name: "clear", + name: "clear-global", + setup: setupFn, + args: args{ + role: TestRole(t, conn, globals.GlobalPrefix), + roleVersion: 2, // yep, since setupFn will increment it to 2 + userIds: []string{}, + groupIds: []string{}, + }, + wantErr: false, + wantAffectedRows: 12, + }, + { + name: "clear-org", + setup: setupFn, + args: args{ + role: TestRole(t, conn, org.PublicId), + roleVersion: 2, // yep, since setupFn will increment it to 2 + userIds: []string{}, + groupIds: []string{}, + }, + wantErr: false, + wantAffectedRows: 12, + }, + { + name: "clear-proj", setup: setupFn, args: args{ role: TestRole(t, conn, proj.PublicId), @@ -570,7 +622,35 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { wantAffectedRows: 12, }, { - name: "no change", + name: "global no change", + setup: setupFn, + args: args{ + role: TestRole(t, conn, globals.GlobalPrefix), + roleVersion: 2, // yep, since setupFn will increment it to 2 + userIds: []string{}, + groupIds: []string{}, + addToOrigUsers: true, + addToOrigGrps: true, + }, + wantErr: false, + wantAffectedRows: 0, + }, + { + name: "org no change", + setup: setupFn, + args: args{ + role: TestRole(t, conn, org.PublicId), + roleVersion: 2, // yep, since setupFn will increment it to 2 + userIds: []string{}, + groupIds: []string{}, + addToOrigUsers: true, + addToOrigGrps: true, + }, + wantErr: false, + wantAffectedRows: 0, + }, + { + name: "proj no change", setup: setupFn, args: args{ role: TestRole(t, conn, proj.PublicId), @@ -584,7 +664,35 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { wantAffectedRows: 0, }, { - name: "add users and grps", + name: "global add users and grps", + setup: setupFn, + args: args{ + role: TestRole(t, conn, globals.GlobalPrefix), + roleVersion: 2, // yep, since setupFn will increment it to 2 + userIds: []string{testUser.PublicId}, + groupIds: []string{testGrp.PublicId}, + addToOrigUsers: true, + addToOrigGrps: true, + }, + wantErr: false, + wantAffectedRows: 2, + }, + { + name: "org add users and grps", + setup: setupFn, + args: args{ + role: TestRole(t, conn, org.PublicId), + roleVersion: 2, // yep, since setupFn will increment it to 2 + userIds: []string{testUser.PublicId}, + groupIds: []string{testGrp.PublicId}, + addToOrigUsers: true, + addToOrigGrps: true, + }, + wantErr: false, + wantAffectedRows: 2, + }, + { + name: "proj add users and grps", setup: setupFn, args: args{ role: TestRole(t, conn, proj.PublicId), @@ -598,7 +706,33 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { wantAffectedRows: 2, }, { - name: "add users and grps with zero version", + name: "global add users and grps with zero version", + setup: setupFn, + args: args{ + role: TestRole(t, conn, globals.GlobalPrefix), + roleVersion: 0, // yep, since setupFn will increment it to 2 + userIds: []string{testUser.PublicId}, + groupIds: []string{testGrp.PublicId}, + addToOrigUsers: true, + addToOrigGrps: true, + }, + wantErr: true, + }, + { + name: "org add users and grps with zero version", + setup: setupFn, + args: args{ + role: TestRole(t, conn, org.PublicId), + roleVersion: 0, // yep, since setupFn will increment it to 2 + userIds: []string{testUser.PublicId}, + groupIds: []string{testGrp.PublicId}, + addToOrigUsers: true, + addToOrigGrps: true, + }, + wantErr: true, + }, + { + name: "proj add users and grps with zero version", setup: setupFn, args: args{ role: TestRole(t, conn, proj.PublicId), @@ -611,7 +745,35 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { wantErr: true, }, { - name: "remove existing and add users and grps", + name: "global remove existing and add users and grps", + setup: setupFn, + args: args{ + role: TestRole(t, conn, globals.GlobalPrefix), + roleVersion: 2, // yep, since setupFn will increment it to 2 + userIds: []string{testUser.PublicId}, + groupIds: []string{testGrp.PublicId}, + addToOrigUsers: false, + addToOrigGrps: false, + }, + wantErr: false, + wantAffectedRows: 14, + }, + { + name: "org remove existing and add users and grps", + setup: setupFn, + args: args{ + role: TestRole(t, conn, org.PublicId), + roleVersion: 2, // yep, since setupFn will increment it to 2 + userIds: []string{testUser.PublicId}, + groupIds: []string{testGrp.PublicId}, + addToOrigUsers: false, + addToOrigGrps: false, + }, + wantErr: false, + wantAffectedRows: 14, + }, + { + name: "proj remove existing and add users and grps", setup: setupFn, args: args{ role: TestRole(t, conn, proj.PublicId), @@ -665,7 +827,7 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { r, _, _, _, err := repo.LookupRole(context.Background(), tt.args.role.PublicId) require.NoError(err) - if tt.name != "no change" { + if !strings.Contains(tt.name, "no change") { assert.Equalf(tt.args.roleVersion+1, r.Version, "%s unexpected version: %d/%d", tt.name, tt.args.roleVersion+1, r.Version) assert.Equalf(origRole.Version, r.Version-1, "%s unexpected version: %d/%d", tt.name, origRole.Version, r.Version-1) } diff --git a/internal/iam/repository_role.go b/internal/iam/repository_role.go index 9250d420a0..dd02b74dde 100644 --- a/internal/iam/repository_role.go +++ b/internal/iam/repository_role.go @@ -14,6 +14,8 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam/store" + "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/boundary/internal/util" "github.com/hashicorp/go-dbw" ) @@ -26,8 +28,6 @@ func (r *Repository) CreateRole(ctx context.Context, role *Role, opt ...Option) switch { case role == nil: return nil, nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing role") - case role.Role == nil: - return nil, nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing role store") case role.PublicId != "": return nil, nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty") case role.ScopeId == "": @@ -38,10 +38,45 @@ func (r *Repository) CreateRole(ctx context.Context, role *Role, opt ...Option) if err != nil { return nil, nil, nil, nil, errors.Wrap(ctx, err, op) } - c := role.Clone().(*Role) - c.PublicId = id - var resource Resource + var roleToCreate Resource + switch { + case strings.HasPrefix(role.GetScopeId(), globals.GlobalPrefix): + roleToCreate = &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: id, + ScopeId: role.ScopeId, + Name: role.Name, + Description: role.Description, + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + case strings.HasPrefix(role.GetScopeId(), globals.OrgPrefix): + roleToCreate = &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: id, + ScopeId: role.ScopeId, + Name: role.Name, + Description: role.Description, + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + case strings.HasPrefix(role.GetScopeId(), globals.ProjectPrefix): + roleToCreate = &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: id, + ScopeId: role.ScopeId, + Name: role.Name, + Description: role.Description, + }, + } + default: + return nil, nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "invalid scope type") + } + + var createdRole *Role var pr []*PrincipalRole var rg []*RoleGrant var grantScopes []*RoleGrantScope @@ -50,16 +85,12 @@ func (r *Repository) CreateRole(ctx context.Context, role *Role, opt ...Option) db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, writer db.Writer) error { - resource, err = r.create(ctx, c, WithReaderWriter(reader, writer)) + res, err := r.create(ctx, roleToCreate, WithReaderWriter(reader, writer)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("while creating role")) } - _, _, err = r.SetRoleGrantScopes(ctx, id, resource.(*Role).Version, []string{globals.GrantScopeThis}, WithReaderWriter(reader, writer)) - if err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("while setting grant scopes")) - } // Do a fresh lookup to get all return values - resource, pr, rg, grantScopes, err = r.LookupRole(ctx, resource.(*Role).PublicId, WithReaderWriter(reader, writer)) + createdRole, pr, rg, grantScopes, err = r.LookupRole(ctx, res.GetPublicId(), WithReaderWriter(reader, writer)) if err != nil { return errors.Wrap(ctx, err, op) } @@ -69,9 +100,9 @@ func (r *Repository) CreateRole(ctx context.Context, role *Role, opt ...Option) if errors.IsUniqueError(err) { return nil, nil, nil, nil, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf("role %s already exists in scope %s", role.Name, role.ScopeId)) } - return nil, nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for %s", c.PublicId))) + return nil, nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for %s", roleToCreate.GetPublicId()))) } - return resource.(*Role), pr, rg, grantScopes, nil + return createdRole, pr, rg, grantScopes, nil } // UpdateRole will update a role in the repository and return the written role. @@ -85,9 +116,6 @@ func (r *Repository) UpdateRole(ctx context.Context, role *Role, version uint32, if role == nil { return nil, nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role") } - if role.Role == nil { - return nil, nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role store") - } if role.PublicId == "" { return nil, nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id") } @@ -125,15 +153,46 @@ func (r *Repository) UpdateRole(ctx context.Context, role *Role, version uint32, db.StdRetryCnt, db.ExpBackoff{}, func(read db.Reader, w db.Writer) error { - var err error - c := role.Clone().(*Role) - resource = c // If we don't have dbMask or nullFields, we'll return this + scopeType, err := getRoleScopeType(ctx, read, role.PublicId) + if err != nil { + return errors.Wrap(ctx, err, op) + } + var res Resource + switch scopeType { + case scope.Global: + res = &globalRole{GlobalRole: &store.GlobalRole{ + PublicId: role.GetPublicId(), + ScopeId: role.GetScopeId(), + Name: role.GetName(), + Description: role.GetDescription(), + Version: role.GetVersion(), + }} + case scope.Org: + res = &orgRole{OrgRole: &store.OrgRole{ + PublicId: role.GetPublicId(), + ScopeId: role.GetScopeId(), + Name: role.GetName(), + Description: role.GetDescription(), + Version: role.GetVersion(), + }} + case scope.Project: + res = &projectRole{ProjectRole: &store.ProjectRole{ + PublicId: role.GetPublicId(), + ScopeId: role.GetScopeId(), + Name: role.GetName(), + Description: role.GetDescription(), + Version: role.GetVersion(), + }} + case scope.Unknown: + return errors.New(ctx, errors.Unknown, op, fmt.Sprintf("unknown scope type for role: %s", role.PublicId)) + } + + resource = res // If we don't have dbMask or nullFields, we'll return this if len(dbMask) > 0 || len(nullFields) > 0 { - resource, rowsUpdated, err = r.update(ctx, c, version, dbMask, nullFields, WithReaderWriter(read, w)) + resource, rowsUpdated, err = r.update(ctx, res, version, dbMask, nullFields, WithReaderWriter(read, w)) if err != nil { return errors.Wrap(ctx, err, op) } - version = resource.(*Role).Version } // Do a fresh lookup since version may have gone up by 1 or 2 based @@ -147,10 +206,11 @@ func (r *Repository) UpdateRole(ctx context.Context, role *Role, version uint32, ) if err != nil { if errors.IsUniqueError(err) { - return nil, nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf("role %s already exists in org %s", role.Name, role.ScopeId)) + return nil, nil, nil, nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf("role %s already exists in scope %s", role.Name, role.ScopeId)) } return nil, nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for %s", role.PublicId))) } + return resource.(*Role), pr, rg, grantScopes, rowsUpdated, nil } @@ -164,16 +224,49 @@ func (r *Repository) LookupRole(ctx context.Context, withPublicId string, opt .. return nil, nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id") } opts := getOpts(opt...) - role := allocRole() - role.PublicId = withPublicId var pr []*PrincipalRole var rg []*RoleGrant var rgs []*RoleGrantScope + var role *Role lookupFunc := func(read db.Reader, w db.Writer) error { - if err := read.LookupByPublicId(ctx, &role); err != nil { + scopeType, err := getRoleScopeType(ctx, read, withPublicId) + if err != nil { + return errors.Wrap(ctx, err, op) + } + var res Resource + switch scopeType { + case scope.Global: + gRole := allocGlobalRole() + gRole.PublicId = withPublicId + res = &gRole + case scope.Org: + oRole := allocOrgRole() + oRole.PublicId = withPublicId + res = &oRole + case scope.Project: + pRole := allocProjectRole() + pRole.PublicId = withPublicId + res = &pRole + case scope.Unknown: + return errors.New(ctx, errors.Unknown, op, fmt.Sprintf("unknown scope type for role: %s", role.PublicId)) + } + + if err := read.LookupByPublicId(ctx, res); err != nil { return errors.Wrap(ctx, err, op) } + + switch res.(type) { + case *globalRole: + role = res.(*globalRole).toRole() + case *orgRole: + role = res.(*orgRole).toRole() + case *projectRole: + role = res.(*projectRole).toRole() + default: + return errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unknown role type %T", res))) + } + repo, err := NewRepository(ctx, read, w, r.kms) if err != nil { return errors.Wrap(ctx, err, op) @@ -186,7 +279,7 @@ func (r *Repository) LookupRole(ctx context.Context, withPublicId string, opt .. if err != nil { return errors.Wrap(ctx, err, op) } - rgs, err = repo.ListRoleGrantScopes(ctx, []string{withPublicId}) + rgs, err = listRoleGrantScopes(ctx, read, []string{withPublicId}) if err != nil { return errors.Wrap(ctx, err, op) } @@ -213,7 +306,7 @@ func (r *Repository) LookupRole(ctx context.Context, withPublicId string, opt .. } return nil, nil, nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for %s", withPublicId))) } - return &role, pr, rg, rgs, nil + return role, pr, rg, rgs, nil } // DeleteRole will delete a role from the repository. @@ -222,12 +315,29 @@ func (r *Repository) DeleteRole(ctx context.Context, withPublicId string, _ ...O if withPublicId == "" { return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id") } - role := allocRole() - role.PublicId = withPublicId - if err := r.reader.LookupByPublicId(ctx, &role); err != nil { - return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", withPublicId))) + scopeType, err := getRoleScopeType(ctx, r.reader, withPublicId) + if err != nil { + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("cannot find scope for role %s", withPublicId)) } - rowsDeleted, err := r.delete(ctx, &role) + + var res Resource + switch scopeType { + case scope.Global: + gRole := allocGlobalRole() + gRole.PublicId = withPublicId + res = &gRole + case scope.Org: + oRole := allocOrgRole() + oRole.PublicId = withPublicId + res = &oRole + case scope.Project: + pRole := allocProjectRole() + pRole.PublicId = withPublicId + res = &pRole + default: + return db.NoRowsAffected, errors.New(ctx, errors.Unknown, op, fmt.Sprintf("unknown scope type for role: %s", withPublicId)) + } + rowsDeleted, err := r.delete(ctx, res) if err != nil { return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", withPublicId))) } @@ -250,11 +360,11 @@ func (r *Repository) listRoles(ctx context.Context, withScopeIds []string, opt . case opts.withLimit < 0: return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "limit must be non-negative") } - var args []any + args = append(args, sql.Named("limit", limit)) + whereClause := "scope_id in @scope_ids" args = append(args, sql.Named("scope_ids", withScopeIds)) - if opts.withStartPageAfterItem != nil { whereClause = fmt.Sprintf("(create_time, public_id) < (@last_item_create_time, @last_item_id) and %s", whereClause) args = append(args, @@ -262,8 +372,7 @@ func (r *Repository) listRoles(ctx context.Context, withScopeIds []string, opt . sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()), ) } - dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("create_time desc, public_id desc")} - return r.queryRoles(ctx, whereClause, args, dbOpts...) + return r.queryRoles(ctx, whereClause, args) } // listRolesRefresh lists roles in the given scopes and supports the @@ -278,9 +387,7 @@ func (r *Repository) listRolesRefresh(ctx context.Context, updatedAfter time.Tim case len(withScopeIds) == 0: return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") } - opts := getOpts(opt...) - limit := r.defaultLimit switch { case opts.withLimit > 0: @@ -291,6 +398,7 @@ func (r *Repository) listRolesRefresh(ctx context.Context, updatedAfter time.Tim } var args []any + args = append(args, sql.Named("limit", limit)) whereClause := "update_time > @updated_after_time and scope_id in @scope_ids" args = append(args, sql.Named("updated_after_time", timestamp.New(updatedAfter)), @@ -303,38 +411,46 @@ func (r *Repository) listRolesRefresh(ctx context.Context, updatedAfter time.Tim sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()), ) } - - dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("update_time desc, public_id desc")} - return r.queryRoles(ctx, whereClause, args, dbOpts...) + return r.queryRoles(ctx, whereClause, args) } -func (r *Repository) queryRoles(ctx context.Context, whereClause string, args []any, opt ...db.Option) ([]*Role, time.Time, error) { +func (r *Repository) queryRoles(ctx context.Context, whereClause string, args []any) ([]*Role, time.Time, error) { const op = "iam.(Repository).queryRoles" - var transactionTimestamp time.Time + query := fmt.Sprintf(listRolesQuery, whereClause) var retRoles []*Role var retRoleGrantScopes []*RoleGrantScope - if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(rd db.Reader, w db.Writer) error { - var inRet []*Role - if err := rd.SearchWhere(ctx, &inRet, whereClause, args, opt...); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query roles")) + var transactionTimestamp time.Time + _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(rd db.Reader, w db.Writer) error { + rows, err := rd.Query(ctx, query, args) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("failed to execute list roles ")) + } + for rows.Next() { + var role Role + if err := rd.ScanRows(ctx, rows, &role); err != nil { + return errors.Wrap(ctx, err, op) + } + retRoles = append(retRoles, &role) + } + if rows.Err() != nil { + return errors.Wrap(ctx, rows.Err(), op) } - retRoles = inRet - var err error if len(retRoles) > 0 { roleIds := make([]string, 0, len(retRoles)) for _, retRole := range retRoles { roleIds = append(roleIds, retRole.PublicId) } - retRoleGrantScopes, err = r.ListRoleGrantScopes(ctx, roleIds, WithReaderWriter(rd, w)) + retRoleGrantScopes, err = listRoleGrantScopes(ctx, r.reader, roleIds) if err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query role grant scopes")) + return errors.Wrap(ctx, err, op) } } transactionTimestamp, err = rd.Now(ctx) return err - }); err != nil { - return nil, time.Time{}, err + }) + if err != nil { + return nil, time.Time{}, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query roles")) } roleGrantScopesMap := make(map[string][]*RoleGrantScope) for _, rgs := range retRoleGrantScopes { @@ -389,3 +505,84 @@ func (r *Repository) estimatedRoleCount(ctx context.Context) (int, error) { } return count, nil } + +// getRoleScopeType returns scope.Type of the roleId by reading it from the base type iam_role table +// use this to get scope ID to determine which of the role subtype tables to operate on +func getRoleScopeType(ctx context.Context, r db.Reader, roleId string) (scope.Type, error) { + const op = "iam.getRoleScopeType" + if roleId == "" { + return scope.Unknown, errors.New(ctx, errors.InvalidParameter, op, "missing role id") + } + if r == nil { + return scope.Unknown, errors.New(ctx, errors.InvalidParameter, op, "missing db.Reader") + } + rows, err := r.Query(ctx, scopeIdFromRoleIdQuery, []any{sql.Named("public_id", roleId)}) + if err != nil { + return scope.Unknown, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed to lookup role scope for :%s", roleId))) + } + var scopeIds []string + for rows.Next() { + if err := r.ScanRows(ctx, rows, &scopeIds); err != nil { + return scope.Unknown, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed scan results from querying role scope for :%s", roleId))) + } + } + if err := rows.Err(); err != nil { + return scope.Unknown, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unexpected error scanning results from querying role scope for :%s", roleId))) + } + if len(scopeIds) == 0 { + return scope.Unknown, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("role %s not found", roleId)) + } + if len(scopeIds) > 1 { + return scope.Unknown, errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("expected 1 row but got: %d", len(scopeIds))) + } + scopeId := scopeIds[0] + switch { + case strings.HasPrefix(scopeId, globals.GlobalPrefix): + return scope.Global, nil + case strings.HasPrefix(scopeId, globals.OrgPrefix): + return scope.Org, nil + case strings.HasPrefix(scopeId, globals.ProjectPrefix): + return scope.Project, nil + default: + return scope.Unknown, fmt.Errorf("unknown scope type for role %s", roleId) + } +} + +// getRoleScope returns scope of the role +func getRoleScope(ctx context.Context, r db.Reader, roleId string) (*Scope, error) { + const op = "iam.getRoleScope" + if roleId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role id") + } + if r == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing db.Reader") + } + rows, err := r.Query(ctx, scopeIdFromRoleIdQuery, []any{sql.Named("public_id", roleId)}) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed to lookup role scope for :%s", roleId))) + } + var scopeIds []string + for rows.Next() { + if err := r.ScanRows(ctx, rows, &scopeIds); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed scan results from querying role scope for :%s", roleId))) + } + } + if err := rows.Err(); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unexpected error scanning results from querying role scope for :%s", roleId))) + } + + if len(scopeIds) == 0 { + return nil, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("role %s not found", roleId)) + } + if len(scopeIds) > 1 { + return nil, errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("expected 1 row but got: %d", len(scopeIds))) + } + + scp := AllocScope() + scp.PublicId = scopeIds[0] + err = r.LookupByPublicId(ctx, &scp) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to lookup role scope")) + } + return &scp, nil +} diff --git a/internal/iam/repository_role_grant.go b/internal/iam/repository_role_grant.go index 92ed50914b..bda056d984 100644 --- a/internal/iam/repository_role_grant.go +++ b/internal/iam/repository_role_grant.go @@ -15,9 +15,10 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/types/scope" ) -// AddRoleGrant will add role grants associated with the role ID in the +// AddRoleGrants will add role grants associated with the role ID in the // repository. No options are currently supported. Zero is not a valid value for // the WithVersion option and will return an error. func (r *Repository) AddRoleGrants(ctx context.Context, roleId string, roleVersion uint32, grants []string, _ ...Option) ([]*RoleGrant, error) { @@ -31,8 +32,6 @@ func (r *Repository) AddRoleGrants(ctx context.Context, roleId string, roleVersi if roleVersion == 0 { return nil, errors.New(ctx, errors.InvalidParameter, op, "missing version") } - role := allocRole() - role.PublicId = roleId newRoleGrants := make([]*RoleGrant, 0, len(grants)) for _, grant := range grants { @@ -43,32 +42,47 @@ func (r *Repository) AddRoleGrants(ctx context.Context, roleId string, roleVersi newRoleGrants = append(newRoleGrants, roleGrant) } - scope, err := role.GetScope(ctx, r.reader) + scp, err := getRoleScope(ctx, r.reader, roleId) if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId))) + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId))) } - oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog) + oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } - _, err = r.writer.DoTx( ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { msgs := make([]*oplog.Message, 0, 2) - roleTicket, err := w.GetTicket(ctx, &role) + + var updatedRole Resource + switch scp.GetType() { + case scope.Global.String(): + g := allocGlobalRole() + g.PublicId = roleId + g.Version = roleVersion + 1 + updatedRole = &g + case scope.Org.String(): + o := allocOrgRole() + o.PublicId = roleId + o.Version = roleVersion + 1 + updatedRole = &o + case scope.Project.String(): + p := allocProjectRole() + p.PublicId = roleId + p.Version = roleVersion + 1 + updatedRole = &p + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId())) + } + roleTicket, err := w.GetTicket(ctx, updatedRole) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - - // We need to update the role version as that's the aggregate - updatedRole := allocRole() - updatedRole.PublicId = roleId - updatedRole.Version = uint32(roleVersion) + 1 var roleOplogMsg oplog.Message - rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) + rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version")) } @@ -84,8 +98,8 @@ func (r *Repository) AddRoleGrants(ctx context.Context, roleId string, roleVersi metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, - "scope-id": []string{scope.PublicId}, - "scope-type": []string{scope.Type}, + "scope-id": []string{scp.PublicId}, + "scope-type": []string{scp.Type}, "resource-public-id": []string{roleId}, } if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil { @@ -116,14 +130,11 @@ func (r *Repository) DeleteRoleGrants(ctx context.Context, roleId string, roleVe if roleVersion == 0 { return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") } - role := allocRole() - role.PublicId = roleId - - scope, err := role.GetScope(ctx, r.reader) + scp, err := getRoleScope(ctx, r.reader, roleId) if err != nil { - return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope to create metadata", roleId))) + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId))) } - oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog) + oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog) if err != nil { return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } @@ -135,15 +146,32 @@ func (r *Repository) DeleteRoleGrants(ctx context.Context, roleId string, roleVe db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { msgs := make([]*oplog.Message, 0, 2) - roleTicket, err := w.GetTicket(ctx, &role) + var updatedRole Resource + switch scp.GetType() { + case scope.Global.String(): + g := allocGlobalRole() + g.PublicId = roleId + g.Version = roleVersion + 1 + updatedRole = &g + case scope.Org.String(): + o := allocOrgRole() + o.PublicId = roleId + o.Version = roleVersion + 1 + updatedRole = &o + case scope.Project.String(): + p := allocProjectRole() + p.PublicId = roleId + p.Version = roleVersion + 1 + updatedRole = &p + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId())) + } + roleTicket, err := w.GetTicket(ctx, updatedRole) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - updatedRole := allocRole() - updatedRole.PublicId = roleId - updatedRole.Version = uint32(roleVersion) + 1 var roleOplogMsg oplog.Message - rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) + rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version")) } @@ -200,8 +228,8 @@ func (r *Repository) DeleteRoleGrants(ctx context.Context, roleId string, roleVe metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_DELETE.String()}, - "scope-id": []string{scope.PublicId}, - "scope-type": []string{scope.Type}, + "scope-id": []string{scp.PublicId}, + "scope-type": []string{scp.Type}, "resource-public-id": []string{roleId}, } if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil { @@ -234,9 +262,6 @@ func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersi return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing grants") } - role := allocRole() - role.PublicId = roleId - // TODO(mgaffney) 08/2020: Use SQL to calculate changes. // NOTE: Set calculation can safely take place out of the transaction since @@ -292,12 +317,11 @@ func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersi if len(addRoleGrants) == 0 && len(deleteRoleGrants) == 0 { return currentRoleGrants, db.NoRowsAffected, nil } - - scope, err := role.GetScope(ctx, r.reader) + scp, err := getRoleScope(ctx, r.reader, roleId) if err != nil { - return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId))) + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId))) } - oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog) + oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } @@ -309,15 +333,33 @@ func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersi db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { msgs := make([]*oplog.Message, 0, 2) - roleTicket, err := w.GetTicket(ctx, &role) + var updatedRole Resource + switch scp.GetType() { + case scope.Global.String(): + g := allocGlobalRole() + g.PublicId = roleId + g.Version = roleVersion + 1 + updatedRole = &g + case scope.Org.String(): + o := allocOrgRole() + o.PublicId = roleId + o.Version = roleVersion + 1 + updatedRole = &o + case scope.Project.String(): + p := allocProjectRole() + p.PublicId = roleId + p.Version = roleVersion + 1 + updatedRole = &p + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId())) + } + roleTicket, err := w.GetTicket(ctx, updatedRole) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - updatedRole := allocRole() - updatedRole.PublicId = roleId - updatedRole.Version = roleVersion + 1 + var roleOplogMsg oplog.Message - rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) + rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version")) } @@ -351,8 +393,8 @@ func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersi metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_DELETE.String(), oplog.OpType_OP_TYPE_CREATE.String()}, - "scope-id": []string{scope.PublicId}, - "scope-type": []string{scope.Type}, + "scope-id": []string{scp.PublicId}, + "scope-type": []string{scp.Type}, "resource-public-id": []string{roleId}, } if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil { @@ -387,32 +429,7 @@ func (r *Repository) ListRoleGrants(ctx context.Context, roleId string, opt ...O return roleGrants, nil } -// ListRoleGrantScopes returns the grant scopes for the roleId and supports the WithLimit -// option. -func (r *Repository) ListRoleGrantScopes(ctx context.Context, roleIds []string, opt ...Option) ([]*RoleGrantScope, error) { - const op = "iam.(Repository).ListRoleGrantScopes" - if len(roleIds) == 0 { - return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role ids") - } - query := "?" - var args []any - for i, roleId := range roleIds { - if roleId == "" { - return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role ids") - } - if i > 0 { - query = query + ", ?" - } - args = append(args, roleId) - } - var roleGrantScopes []*RoleGrantScope - if err := r.list(ctx, &roleGrantScopes, fmt.Sprintf("role_id in (%s)", query), args, opt...); err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup role grant scopes")) - } - return roleGrantScopes, nil -} - -type multiGrantTuple struct { +type MultiGrantTuple struct { RoleId string RoleScopeId string RoleParentScopeId string @@ -441,7 +458,7 @@ func (r *Repository) GrantsForUser(ctx context.Context, userId string, opt ...Op query = fmt.Sprintf(grantsForUserQuery, authUser) } - var grants []multiGrantTuple + var grants []MultiGrantTuple rows, err := r.reader.Query(ctx, query, []any{userId}) if err != nil { return nil, errors.Wrap(ctx, err, op) @@ -477,7 +494,7 @@ func (r *Repository) GrantsForUser(ctx context.Context, userId string, opt ...Op if opts.withTestCacheMultiGrantTuples != nil { for i, grant := range grants { - grant.testStableSort() + grant.TestStableSort() grants[i] = grant } *opts.withTestCacheMultiGrantTuples = grants @@ -486,7 +503,7 @@ func (r *Repository) GrantsForUser(ctx context.Context, userId string, opt ...Op return ret, nil } -func (m *multiGrantTuple) testStableSort() { +func (m *MultiGrantTuple) TestStableSort() { grantScopeIds := strings.Split(m.GrantScopeIds, "^") sort.Strings(grantScopeIds) m.GrantScopeIds = strings.Join(grantScopeIds, "^") diff --git a/internal/iam/repository_role_grant_ext_test.go b/internal/iam/repository_role_grant_ext_test.go index f618fce8d7..246c070c66 100644 --- a/internal/iam/repository_role_grant_ext_test.go +++ b/internal/iam/repository_role_grant_ext_test.go @@ -8,14 +8,19 @@ import ( "encoding/json" "fmt" mathrand "math/rand" + "strings" "testing" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/ldap/store" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/perms" + "github.com/hashicorp/boundary/internal/types/action" + "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -389,3 +394,2005 @@ func TestGrantsForUserRandomized(t *testing.T) { ", roles from ldap managed groups", rolesFromLdapManagedGroups) } } + +func TestGrantsForUser_DirectAssociation(t *testing.T) { + ctx := context.Background() + + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + + repo := iam.TestRepo(t, conn, wrap) + user := iam.TestUser(t, repo, "global") + user2 := iam.TestUser(t, repo, "global") + + testUserRole := func(roleId, userId string) func() { + return func() { iam.TestUserRole(t, conn, roleId, userId) } + } + + // Create a series of scopes with roles in each. We'll create two of each + // kind to ensure we're not just picking up the first role in each. + + // The first org/project set contains direct grants, but without + // inheritance. We create two roles in each project. + + // Org1, Project1a, Project1b + directGrantOrg1, directGrantProj1a, directGrantProj1b := iam.SetupDirectGrantScopes(t, conn, repo) + + // user + directGrantOrg1Role := iam.TestRole(t, conn, directGrantOrg1.PublicId) + directGrantOrg1RoleGrant1 := "ids=*;type=group;actions=*" + directGrantOrg1RoleGrant2 := "ids=*;type=group;actions=create,list" + grantRoleAndAssociate(t, conn, directGrantOrg1Role.PublicId, testUserRole(directGrantOrg1Role.PublicId, user.PublicId), + directGrantOrg1RoleGrant1, directGrantOrg1RoleGrant2, + ) + + // user2 + directGrantOrg1Role2 := iam.TestRole(t, conn, directGrantOrg1.PublicId) + directGrantOrg1RoleGrant3 := "ids=*;type=group;actions=update" + grantRoleAndAssociate(t, conn, directGrantOrg1Role2.PublicId, testUserRole(directGrantOrg1Role2.PublicId, user2.PublicId), directGrantOrg1RoleGrant3) + + // user + directGrantProj1aRole := iam.TestRole(t, conn, directGrantProj1a.PublicId) + directGrantProj1aRoleGrant := "ids=*;type=group;actions=add-members,read" + grantRoleAndAssociate(t, conn, directGrantProj1aRole.PublicId, testUserRole(directGrantProj1aRole.PublicId, user.PublicId), directGrantProj1aRoleGrant) + + directGrantProj1bRole := iam.TestRole(t, conn, directGrantProj1b.PublicId) + directGrantProj1bRoleGrant := "ids=*;type=group;actions=list,read" + grantRoleAndAssociate(t, conn, directGrantProj1bRole.PublicId, testUserRole(directGrantProj1bRole.PublicId, user.PublicId), directGrantProj1bRoleGrant) + + // user2 + directGrantProj1aRole2 := iam.TestRole(t, conn, directGrantProj1a.PublicId) + directGrantProj1aRoleGrant2 := "ids=*;type=group;actions=set-members" + grantRoleAndAssociate(t, conn, directGrantProj1aRole2.PublicId, testUserRole(directGrantProj1aRole2.PublicId, user2.PublicId), directGrantProj1aRoleGrant2) + + directGrantProj1bRole2 := iam.TestRole(t, conn, directGrantProj1b.PublicId) + directGrantProj1bRoleGrant2 := "ids=*;type=group;actions=delete" + grantRoleAndAssociate(t, conn, directGrantProj1bRole2.PublicId, testUserRole(directGrantProj1bRole2.PublicId, user2.PublicId), directGrantProj1bRoleGrant2) + + // Org2, Project2a, Project2b + directGrantOrg2, directGrantProj2a, directGrantProj2b := iam.SetupDirectGrantScopes(t, conn, repo) + + // user + directGrantOrg2Role := iam.TestRole(t, conn, directGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeThis, + directGrantProj2a.PublicId, + })) + directGrantOrg2RoleGrant1 := "ids=*;type=group;actions=*" + directGrantOrg2RoleGrant2 := "ids=*;type=group;actions=list,read" + grantRoleAndAssociate(t, conn, directGrantOrg2Role.PublicId, testUserRole(directGrantOrg2Role.PublicId, user.PublicId), + directGrantOrg2RoleGrant1, directGrantOrg2RoleGrant2, + ) + + directGrantProj2aRole := iam.TestRole(t, conn, directGrantProj2a.PublicId) + directGrantProj2aRoleGrant := "ids=hcst_abcd1234,hcst_1234abcd;actions=*" + grantRoleAndAssociate(t, conn, directGrantProj2aRole.PublicId, testUserRole(directGrantProj2aRole.PublicId, user.PublicId), directGrantProj2aRoleGrant) + + directGrantProj2bRole := iam.TestRole(t, conn, directGrantProj2b.PublicId) + directGrantProj2bRoleGrant := "ids=cs_abcd1234;actions=read,update" + grantRoleAndAssociate(t, conn, directGrantProj2bRole.PublicId, testUserRole(directGrantProj2bRole.PublicId, user.PublicId), directGrantProj2bRoleGrant) + + // user2 + directGrantOrg2Role2 := iam.TestRole(t, conn, directGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeThis, + directGrantProj2a.PublicId, + })) + directGrantOrg2RoleGrant3 := "ids=*;type=group;actions=add-members" + grantRoleAndAssociate(t, conn, directGrantOrg2Role2.PublicId, testUserRole(directGrantOrg2Role2.PublicId, user2.PublicId), directGrantOrg2RoleGrant3) + + directGrantProj2aRole2 := iam.TestRole(t, conn, directGrantProj2a.PublicId) + directGrantProj2aRoleGrant2 := "ids=hcst_abcd1234,hcst_1234abcd;actions=*" + grantRoleAndAssociate(t, conn, directGrantProj2aRole2.PublicId, testUserRole(directGrantProj2aRole2.PublicId, user2.PublicId), directGrantProj2aRoleGrant2) + + directGrantProj2bRole2 := iam.TestRole(t, conn, directGrantProj2b.PublicId) + directGrantProj2bRoleGrant2 := "ids=cs_abcd1234;actions=read,update" + grantRoleAndAssociate(t, conn, directGrantProj2bRole2.PublicId, testUserRole(directGrantProj2bRole2.PublicId, user2.PublicId), directGrantProj2bRoleGrant2) + + // For the second set we create a couple of orgs/projects and then use globals.GrantScopeChildren + // + // child org 1 + childGrantOrg1, _ := iam.SetupChildGrantScopes(t, conn, repo) + + // user + childGrantOrg1Role := iam.TestRole(t, conn, childGrantOrg1.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg1RoleGrant := "ids=*;type=group;actions=add-members,remove-members" + grantRoleAndAssociate(t, conn, childGrantOrg1Role.PublicId, testUserRole(childGrantOrg1Role.PublicId, user.PublicId), childGrantOrg1RoleGrant) + + // user2 + childGrantOrg1Role2 := iam.TestRole(t, conn, childGrantOrg1.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg1RoleGrant2 := "ids=*;type=group;actions=read" + grantRoleAndAssociate(t, conn, childGrantOrg1Role2.PublicId, testUserRole(childGrantOrg1Role2.PublicId, user2.PublicId), childGrantOrg1RoleGrant2) + + // child org 2 + childGrantOrg2, _ := iam.SetupChildGrantScopes(t, conn, repo) + + // user + childGrantOrg2Role := iam.TestRole(t, conn, childGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg2RoleGrant1 := "ids=*;type=group;actions=set-members" + childGrantOrg2RoleGrant2 := "ids=*;type=group;actions=delete" + grantRoleAndAssociate(t, conn, childGrantOrg2Role.PublicId, testUserRole(childGrantOrg2Role.PublicId, user.PublicId), + childGrantOrg2RoleGrant1, childGrantOrg2RoleGrant2, + ) + + // user2 + childGrantOrg2Role2 := iam.TestRole(t, conn, childGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg2RoleGrant3 := "ids=*;type=group;actions=set-members" + grantRoleAndAssociate(t, conn, childGrantOrg2Role2.PublicId, testUserRole(childGrantOrg2Role2.PublicId, user2.PublicId), childGrantOrg2RoleGrant3) + + // Finally, let's create some roles at global scope with children and descendants grants + // + // user + childGrantGlobalRole := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantGlobalRoleGrant := "ids=*;type=group;actions=*" + grantRoleAndAssociate(t, conn, childGrantGlobalRole.PublicId, testUserRole(childGrantGlobalRole.PublicId, user.PublicId), childGrantGlobalRoleGrant) + + // user2 + childGrantGlobalRole2 := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantGlobalRoleGrant2 := "ids=*;type=group;actions=list" + grantRoleAndAssociate(t, conn, childGrantGlobalRole2.PublicId, testUserRole(childGrantGlobalRole2.PublicId, user2.PublicId), childGrantGlobalRoleGrant2) + + // user + descendantGrantGlobalRole := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeDescendants, + })) + descendantGrantGlobalRoleGrant := "ids=*;type=group;actions=*" + grantRoleAndAssociate(t, conn, descendantGrantGlobalRole.PublicId, testUserRole(descendantGrantGlobalRole.PublicId, user.PublicId), descendantGrantGlobalRoleGrant) + + // user2 + descendantGrantGlobalRole2 := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeDescendants, + })) + descendantGrantGlobalRoleGrant2 := "ids=*;type=group;actions=add-members" + grantRoleAndAssociate(t, conn, descendantGrantGlobalRole2.PublicId, testUserRole(descendantGrantGlobalRole2.PublicId, user2.PublicId), descendantGrantGlobalRoleGrant2) + + t.Run("db-grants", func(t *testing.T) { + // Here we should see exactly what the DB has returned, before we do some + // local exploding of grants and grant scopes + expMultiGrantTuples := map[string][]iam.MultiGrantTuple{ + user.PublicId: { + // No grants from noOrg/noProj + // Direct org1/2: + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeThis, + Grants: strings.Join([]string{directGrantOrg1RoleGrant1, directGrantOrg1RoleGrant2}, "^"), + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: strings.Join([]string{globals.GrantScopeThis, directGrantProj2a.PublicId}, "^"), + Grants: strings.Join([]string{directGrantOrg2RoleGrant1, directGrantOrg2RoleGrant2}, "^"), + }, + // Proj orgs 1/2: + { + RoleId: directGrantProj1aRole.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1aRoleGrant, + }, + { + RoleId: directGrantProj1bRole.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1bRoleGrant, + }, + { + RoleId: directGrantProj2aRole.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2aRoleGrant, + }, + { + RoleId: directGrantProj2bRole.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2bRoleGrant, + }, + // Child grants from orgs 1/2: + { + RoleId: childGrantOrg1Role.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantOrg1RoleGrant, + }, + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: strings.Join([]string{childGrantOrg2RoleGrant1, childGrantOrg2RoleGrant2}, "^"), + }, + // Children of global and descendants of global + { + RoleId: descendantGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeDescendants, + Grants: descendantGrantGlobalRoleGrant, + }, + { + RoleId: childGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantGlobalRoleGrant, + }, + }, + user2.PublicId: { + // No grants from noOrg/noProj + // Direct org1/2: + { + RoleId: directGrantOrg1Role2.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantOrg1RoleGrant3, + }, + { + RoleId: directGrantOrg2Role2.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: strings.Join([]string{globals.GrantScopeThis, directGrantProj2a.PublicId}, "^"), + Grants: directGrantOrg2RoleGrant3, + }, + // Proj orgs 1/2: + { + RoleId: directGrantProj1aRole2.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1aRoleGrant2, + }, + { + RoleId: directGrantProj1bRole2.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1bRoleGrant2, + }, + { + RoleId: directGrantProj2aRole2.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2aRoleGrant2, + }, + { + RoleId: directGrantProj2bRole2.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2bRoleGrant2, + }, + // Child grants from orgs 1/2: + { + RoleId: childGrantOrg1Role2.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantOrg1RoleGrant2, + }, + { + RoleId: childGrantOrg2Role2.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantOrg2RoleGrant3, + }, + // Children of global and descendants of global + { + RoleId: descendantGrantGlobalRole2.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeDescendants, + Grants: descendantGrantGlobalRoleGrant2, + }, + { + RoleId: childGrantGlobalRole2.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantGlobalRoleGrant2, + }, + }, + } + for userId, tuples := range expMultiGrantTuples { + for i, tuple := range tuples { + tuple.TestStableSort() + expMultiGrantTuples[userId][i] = tuple + } + multiGrantTuplesCache := new([]iam.MultiGrantTuple) + _, err := repo.GrantsForUser(ctx, userId, iam.WithTestCacheMultiGrantTuples(multiGrantTuplesCache)) + require.NoError(t, err) + + assert.ElementsMatch(t, *multiGrantTuplesCache, expMultiGrantTuples[userId]) + } + }) + + t.Run("exploded-grants", func(t *testing.T) { + // We expect to see: + // + // * No grants from noOrg/noProj + // * Grants from direct orgs/projs: + // * directGrantOrg1/directGrantOrg2 on org and respective projects (6 grants total per org) + // * directGrantProj on respective projects (4 grants total) + expGrantTuples := []perms.GrantTuple{ + // No grants from noOrg/noProj + // Grants from direct org1 to org1/proj1a/proj1b: + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Grant: directGrantOrg1RoleGrant1, + }, + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Grant: directGrantOrg1RoleGrant2, + }, + // Grants from direct org 1 proj 1a: + { + RoleId: directGrantProj1aRole.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1a.PublicId, + Grant: directGrantProj1aRoleGrant, + }, + // Grant from direct org 1 proj 1 b: + { + RoleId: directGrantProj1bRole.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1b.PublicId, + Grant: directGrantProj1bRoleGrant, + }, + + // Grants from direct org2 to org2/proj2a/proj2b: + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Grant: directGrantOrg2RoleGrant1, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantOrg2RoleGrant1, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Grant: directGrantOrg2RoleGrant2, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantOrg2RoleGrant2, + }, + // Grants from direct org 2 proj 2a: + { + RoleId: directGrantProj2aRole.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantProj2aRoleGrant, + }, + // Grant from direct org 2 proj 2 b: + { + RoleId: directGrantProj2bRole.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2b.PublicId, + Grant: directGrantProj2bRoleGrant, + }, + // Child grants from child org1 to proj1a/proj1b: + { + RoleId: childGrantOrg1Role.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg1RoleGrant, + }, + // Child grants from child org2 to proj2a/proj2b: + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg2RoleGrant1, + }, + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg2RoleGrant2, + }, + + // Grants from global to every org: + { + RoleId: childGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantGlobalRoleGrant, + }, + + // Grants from global to every org and project: + { + RoleId: descendantGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Grant: descendantGrantGlobalRoleGrant, + }, + } + + multiGrantTuplesCache := new([]iam.MultiGrantTuple) + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId, iam.WithTestCacheMultiGrantTuples(multiGrantTuplesCache)) + require.NoError(t, err) + assert.ElementsMatch(t, grantTuples, expGrantTuples) + }) + + t.Run("acl-grants", func(t *testing.T) { + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId) + require.NoError(t, err) + grants := make([]perms.Grant, 0, len(grantTuples)) + for _, gt := range grantTuples { + grant, err := perms.Parse(ctx, gt) + require.NoError(t, err) + grants = append(grants, grant) + } + acl := perms.NewACL(grants...) + + t.Run("descendant-grants", func(t *testing.T) { + descendantGrants := acl.DescendantsGrants() + expDescendantGrants := []perms.AclGrant{ + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + } + assert.ElementsMatch(t, descendantGrants, expDescendantGrants) + }) + + t.Run("child-grants", func(t *testing.T) { + childrenGrants := acl.ChildrenScopeGrantMap() + expChildrenGrants := map[string][]perms.AclGrant{ + childGrantOrg1.PublicId: { + { + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.AddMembers: true, action.RemoveMembers: true}, + }, + }, + childGrantOrg2.PublicId: { + { + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.SetMembers: true}, + }, + { + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.Delete: true}, + }, + }, + scope.Global.String(): { + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + }, + } + assert.Len(t, childrenGrants, len(expChildrenGrants)) + for k, v := range childrenGrants { + assert.ElementsMatch(t, v, expChildrenGrants[k]) + } + }) + + t.Run("direct-grants", func(t *testing.T) { + directGrants := acl.DirectScopeGrantMap() + expDirectGrants := map[string][]perms.AclGrant{ + directGrantOrg1.PublicId: { + { + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.Create: true, action.List: true}, + }, + }, + directGrantProj1a.PublicId: { + { + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1a.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.AddMembers: true, action.Read: true}, + }, + }, + directGrantProj1b.PublicId: { + { + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1b.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantOrg2.PublicId: { + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantProj2a.PublicId: { + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + { + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Id: "hcst_abcd1234", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Id: "hcst_1234abcd", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.All: true}, + }, + }, + directGrantProj2b.PublicId: { + { + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2b.PublicId, + Id: "cs_abcd1234", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.Update: true, action.Read: true}, + }, + }, + } + + assert.Len(t, directGrants, len(expDirectGrants)) + for k, v := range directGrants { + assert.ElementsMatch(t, v, expDirectGrants[k]) + } + }) + }) +} + +func TestGrantsForUser_Group(t *testing.T) { + ctx := context.Background() + + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + + repo := iam.TestRepo(t, conn, wrap) + user := iam.TestUser(t, repo, "global") + user2 := iam.TestUser(t, repo, "global") + group := iam.TestGroup(t, conn, "global") + group2 := iam.TestGroup(t, conn, "global") + iam.TestGroupMember(t, conn, group.PublicId, user.PublicId) + iam.TestGroupMember(t, conn, group2.PublicId, user2.PublicId) + + testGroupRole := func(roleId, groupId string) func() { + return func() { iam.TestGroupRole(t, conn, roleId, groupId) } + } + + // Create a series of scopes with roles in each. We'll create two of each + // kind to ensure we're not just picking up the first role in each. + + // The first org/project set contains direct grants, but without + // inheritance. We create two roles in each project. + directGrantOrg1, directGrantProj1a, directGrantProj1b := iam.SetupDirectGrantScopes(t, conn, repo) + + // group + directGrantOrg1Role := iam.TestRole(t, conn, directGrantOrg1.PublicId) + directGrantOrg1RoleGrant1 := "ids=*;type=group;actions=*" + directGrantOrg1RoleGrant2 := "ids=*;type=group;actions=create,list" + grantRoleAndAssociate(t, conn, directGrantOrg1Role.PublicId, testGroupRole(directGrantOrg1Role.PublicId, group.PublicId), + directGrantOrg1RoleGrant1, directGrantOrg1RoleGrant2, + ) + + // group2 + directGrantOrg1Role2 := iam.TestRole(t, conn, directGrantOrg1.PublicId) + directGrantOrg1RoleGrant3 := "ids=*;type=group;actions=update" + grantRoleAndAssociate(t, conn, directGrantOrg1Role2.PublicId, testGroupRole(directGrantOrg1Role2.PublicId, group2.PublicId), directGrantOrg1RoleGrant3) + + // group + directGrantProj1aRole := iam.TestRole(t, conn, directGrantProj1a.PublicId) + directGrantProj1aRoleGrant := "ids=*;type=group;actions=add-members,read" + grantRoleAndAssociate(t, conn, directGrantProj1aRole.PublicId, testGroupRole(directGrantProj1aRole.PublicId, group.PublicId), directGrantProj1aRoleGrant) + + directGrantProj1bRole := iam.TestRole(t, conn, directGrantProj1b.PublicId) + directGrantProj1bRoleGrant := "ids=*;type=group;actions=list,read" + grantRoleAndAssociate(t, conn, directGrantProj1bRole.PublicId, testGroupRole(directGrantProj1bRole.PublicId, group.PublicId), directGrantProj1bRoleGrant) + + // group2 + directGrantProj1aRole2 := iam.TestRole(t, conn, directGrantProj1a.PublicId) + directGrantProj1aRoleGrant2 := "ids=*;type=group;actions=set-members" + grantRoleAndAssociate(t, conn, directGrantProj1aRole2.PublicId, testGroupRole(directGrantProj1aRole2.PublicId, group2.PublicId), directGrantProj1aRoleGrant2) + + directGrantProj1bRole2 := iam.TestRole(t, conn, directGrantProj1b.PublicId) + directGrantProj1bRoleGrant2 := "ids=*;type=group;actions=delete" + grantRoleAndAssociate(t, conn, directGrantProj1bRole2.PublicId, testGroupRole(directGrantProj1bRole2.PublicId, group2.PublicId), directGrantProj1bRoleGrant2) + + directGrantOrg2, directGrantProj2a, directGrantProj2b := iam.SetupDirectGrantScopes(t, conn, repo) + + // group + directGrantOrg2Role := iam.TestRole(t, conn, directGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeThis, + directGrantProj2a.PublicId, + })) + directGrantOrg2RoleGrant1 := "ids=*;type=group;actions=*" + directGrantOrg2RoleGrant2 := "ids=*;type=group;actions=list,read" + grantRoleAndAssociate(t, conn, directGrantOrg2Role.PublicId, testGroupRole(directGrantOrg2Role.PublicId, group.PublicId), + directGrantOrg2RoleGrant1, directGrantOrg2RoleGrant2, + ) + + directGrantProj2aRole := iam.TestRole(t, conn, directGrantProj2a.PublicId) + directGrantProj2aRoleGrant := "ids=hcst_abcd1234,hcst_1234abcd;actions=*" + grantRoleAndAssociate(t, conn, directGrantProj2aRole.PublicId, testGroupRole(directGrantProj2aRole.PublicId, group.PublicId), directGrantProj2aRoleGrant) + + directGrantProj2bRole := iam.TestRole(t, conn, directGrantProj2b.PublicId) + directGrantProj2bRoleGrant := "ids=cs_abcd1234;actions=read,update" + grantRoleAndAssociate(t, conn, directGrantProj2bRole.PublicId, testGroupRole(directGrantProj2bRole.PublicId, group.PublicId), directGrantProj2bRoleGrant) + + // group2 + directGrantOrg2Role2 := iam.TestRole(t, conn, directGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeThis, + directGrantProj2a.PublicId, + })) + directGrantOrg2RoleGrant3 := "ids=*;type=group;actions=add-members" + grantRoleAndAssociate(t, conn, directGrantOrg2Role2.PublicId, testGroupRole(directGrantOrg2Role2.PublicId, group2.PublicId), directGrantOrg2RoleGrant3) + + directGrantProj2aRole2 := iam.TestRole(t, conn, directGrantProj2a.PublicId) + directGrantProj2aRoleGrant2 := "ids=hcst_abcd1234,hcst_1234abcd;actions=*" + grantRoleAndAssociate(t, conn, directGrantProj2aRole2.PublicId, testGroupRole(directGrantProj2aRole2.PublicId, group2.PublicId), directGrantProj2aRoleGrant2) + + directGrantProj2bRole2 := iam.TestRole(t, conn, directGrantProj2b.PublicId) + directGrantProj2bRoleGrant2 := "ids=cs_abcd1234;actions=read,update" + grantRoleAndAssociate(t, conn, directGrantProj2bRole2.PublicId, testGroupRole(directGrantProj2bRole2.PublicId, group2.PublicId), directGrantProj2bRoleGrant2) + + // For the second set we create a couple of orgs/projects and then use + // globals.GrantScopeChildren. + childGrantOrg1, _ := iam.SetupChildGrantScopes(t, conn, repo) + + // group + childGrantOrg1Role := iam.TestRole(t, conn, childGrantOrg1.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg1RoleGrant := "ids=*;type=group;actions=add-members,remove-members" + grantRoleAndAssociate(t, conn, childGrantOrg1Role.PublicId, testGroupRole(childGrantOrg1Role.PublicId, group.PublicId), childGrantOrg1RoleGrant) + + // group2 + childGrantOrg1Role2 := iam.TestRole(t, conn, childGrantOrg1.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg1RoleGrant2 := "ids=*;type=group;actions=read" + grantRoleAndAssociate(t, conn, childGrantOrg1Role2.PublicId, testGroupRole(childGrantOrg1Role2.PublicId, group2.PublicId), childGrantOrg1RoleGrant2) + + childGrantOrg2, _ := iam.SetupChildGrantScopes(t, conn, repo) + + // group + childGrantOrg2Role := iam.TestRole(t, conn, childGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg2RoleGrant1 := "ids=*;type=group;actions=set-members" + childGrantOrg2RoleGrant2 := "ids=*;type=group;actions=delete" + grantRoleAndAssociate(t, conn, childGrantOrg2Role.PublicId, testGroupRole(childGrantOrg2Role.PublicId, group.PublicId), + childGrantOrg2RoleGrant1, childGrantOrg2RoleGrant2, + ) + + // group2 + childGrantOrg2Role2 := iam.TestRole(t, conn, childGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg2RoleGrant3 := "ids=*;type=group;actions=set-members" + grantRoleAndAssociate(t, conn, childGrantOrg2Role2.PublicId, testGroupRole(childGrantOrg2Role2.PublicId, group2.PublicId), childGrantOrg2RoleGrant3) + + // Finally, let's create some roles at global scope with children and descendants grants + + // group + childGrantGlobalRole := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantGlobalRoleGrant := "ids=*;type=group;actions=*" + grantRoleAndAssociate(t, conn, childGrantGlobalRole.PublicId, testGroupRole(childGrantGlobalRole.PublicId, group.PublicId), childGrantGlobalRoleGrant) + + // group2 + childGrantGlobalRole2 := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantGlobalRoleGrant2 := "ids=*;type=group;actions=list" + grantRoleAndAssociate(t, conn, childGrantGlobalRole2.PublicId, testGroupRole(childGrantGlobalRole2.PublicId, group2.PublicId), childGrantGlobalRoleGrant2) + + // group + descendantGrantGlobalRole := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeDescendants, + })) + descendantGrantGlobalRoleGrant := "ids=*;type=group;actions=*" + grantRoleAndAssociate(t, conn, descendantGrantGlobalRole.PublicId, testGroupRole(descendantGrantGlobalRole.PublicId, group.PublicId), descendantGrantGlobalRoleGrant) + + // group2 + descendantGrantGlobalRole2 := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeDescendants, + })) + descendantGrantGlobalRoleGrant2 := "ids=*;type=group;actions=add-members" + grantRoleAndAssociate(t, conn, descendantGrantGlobalRole2.PublicId, testGroupRole(descendantGrantGlobalRole2.PublicId, group2.PublicId), descendantGrantGlobalRoleGrant2) + + t.Run("db-grants", func(t *testing.T) { + // Here we should see exactly what the DB has returned, before we do some + // local exploding of grants and grant scopes + expMultiGrantTuples := map[string][]iam.MultiGrantTuple{ + user.PublicId: { + // No grants from noOrg/noProj + // Direct org1/2: + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeThis, + Grants: strings.Join([]string{directGrantOrg1RoleGrant1, directGrantOrg1RoleGrant2}, "^"), + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: strings.Join([]string{globals.GrantScopeThis, directGrantProj2a.PublicId}, "^"), + Grants: strings.Join([]string{directGrantOrg2RoleGrant1, directGrantOrg2RoleGrant2}, "^"), + }, + // Proj orgs 1/2: + { + RoleId: directGrantProj1aRole.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1aRoleGrant, + }, + { + RoleId: directGrantProj1bRole.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1bRoleGrant, + }, + { + RoleId: directGrantProj2aRole.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2aRoleGrant, + }, + { + RoleId: directGrantProj2bRole.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2bRoleGrant, + }, + // Child grants from orgs 1/2: + { + RoleId: childGrantOrg1Role.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantOrg1RoleGrant, + }, + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: strings.Join([]string{childGrantOrg2RoleGrant1, childGrantOrg2RoleGrant2}, "^"), + }, + // Children of global and descendants of global + { + RoleId: descendantGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeDescendants, + Grants: descendantGrantGlobalRoleGrant, + }, + { + RoleId: childGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantGlobalRoleGrant, + }, + }, + user2.PublicId: { + // No grants from noOrg/noProj + // Direct org1/2: + { + RoleId: directGrantOrg1Role2.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantOrg1RoleGrant3, + }, + { + RoleId: directGrantOrg2Role2.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: strings.Join([]string{globals.GrantScopeThis, directGrantProj2a.PublicId}, "^"), + Grants: directGrantOrg2RoleGrant3, + }, + // Proj orgs 1/2: + { + RoleId: directGrantProj1aRole2.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1aRoleGrant2, + }, + { + RoleId: directGrantProj1bRole2.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1bRoleGrant2, + }, + { + RoleId: directGrantProj2aRole2.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2aRoleGrant2, + }, + { + RoleId: directGrantProj2bRole2.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2bRoleGrant2, + }, + // Child grants from orgs 1/2: + { + RoleId: childGrantOrg1Role2.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantOrg1RoleGrant2, + }, + { + RoleId: childGrantOrg2Role2.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantOrg2RoleGrant3, + }, + // Children of global and descendants of global + { + RoleId: descendantGrantGlobalRole2.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeDescendants, + Grants: descendantGrantGlobalRoleGrant2, + }, + { + RoleId: childGrantGlobalRole2.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantGlobalRoleGrant2, + }, + }, + } + for userId, tuples := range expMultiGrantTuples { + for i, tuple := range tuples { + tuple.TestStableSort() + expMultiGrantTuples[userId][i] = tuple + } + multiGrantTuplesCache := new([]iam.MultiGrantTuple) + _, err := repo.GrantsForUser(ctx, userId, iam.WithTestCacheMultiGrantTuples(multiGrantTuplesCache)) + require.NoError(t, err) + + assert.ElementsMatch(t, *multiGrantTuplesCache, expMultiGrantTuples[userId]) + } + }) + + t.Run("exploded-grants", func(t *testing.T) { + // We expect to see: + // + // * No grants from noOrg/noProj + // * Grants from direct orgs/projs: + // * directGrantOrg1/directGrantOrg2 on org and respective projects (6 grants total per org) + // * directGrantProj on respective projects (4 grants total) + expGrantTuples := []perms.GrantTuple{ + // No grants from noOrg/noProj + // Grants from direct org1 to org1/proj1a/proj1b: + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Grant: directGrantOrg1RoleGrant1, + }, + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Grant: directGrantOrg1RoleGrant2, + }, + // Grants from direct org 1 proj 1a: + { + RoleId: directGrantProj1aRole.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1a.PublicId, + Grant: directGrantProj1aRoleGrant, + }, + // Grant from direct org 1 proj 1 b: + { + RoleId: directGrantProj1bRole.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1b.PublicId, + Grant: directGrantProj1bRoleGrant, + }, + + // Grants from direct org2 to org2/proj2a/proj2b: + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Grant: directGrantOrg2RoleGrant1, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantOrg2RoleGrant1, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Grant: directGrantOrg2RoleGrant2, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantOrg2RoleGrant2, + }, + // Grants from direct org 2 proj 2a: + { + RoleId: directGrantProj2aRole.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantProj2aRoleGrant, + }, + // Grant from direct org 2 proj 2 b: + { + RoleId: directGrantProj2bRole.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2b.PublicId, + Grant: directGrantProj2bRoleGrant, + }, + // Child grants from child org1 to proj1a/proj1b: + { + RoleId: childGrantOrg1Role.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg1RoleGrant, + }, + // Child grants from child org2 to proj2a/proj2b: + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg2RoleGrant1, + }, + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg2RoleGrant2, + }, + + // Grants from global to every org: + { + RoleId: childGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantGlobalRoleGrant, + }, + + // Grants from global to every org and project: + { + RoleId: descendantGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Grant: descendantGrantGlobalRoleGrant, + }, + } + + multiGrantTuplesCache := new([]iam.MultiGrantTuple) + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId, iam.WithTestCacheMultiGrantTuples(multiGrantTuplesCache)) + require.NoError(t, err) + assert.ElementsMatch(t, grantTuples, expGrantTuples) + }) + + t.Run("acl-grants", func(t *testing.T) { + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId) + require.NoError(t, err) + grants := make([]perms.Grant, 0, len(grantTuples)) + for _, gt := range grantTuples { + grant, err := perms.Parse(ctx, gt) + require.NoError(t, err) + grants = append(grants, grant) + } + acl := perms.NewACL(grants...) + + t.Run("descendant-grants", func(t *testing.T) { + descendantGrants := acl.DescendantsGrants() + expDescendantGrants := []perms.AclGrant{ + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + } + assert.ElementsMatch(t, descendantGrants, expDescendantGrants) + }) + + t.Run("child-grants", func(t *testing.T) { + childrenGrants := acl.ChildrenScopeGrantMap() + expChildrenGrants := map[string][]perms.AclGrant{ + childGrantOrg1.PublicId: { + { + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.AddMembers: true, action.RemoveMembers: true}, + }, + }, + childGrantOrg2.PublicId: { + { + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.SetMembers: true}, + }, + { + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.Delete: true}, + }, + }, + scope.Global.String(): { + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + }, + } + assert.Len(t, childrenGrants, len(expChildrenGrants)) + for k, v := range childrenGrants { + assert.ElementsMatch(t, v, expChildrenGrants[k]) + } + }) + + t.Run("direct-grants", func(t *testing.T) { + directGrants := acl.DirectScopeGrantMap() + expDirectGrants := map[string][]perms.AclGrant{ + directGrantOrg1.PublicId: { + { + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.Create: true, action.List: true}, + }, + }, + directGrantProj1a.PublicId: { + { + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1a.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.AddMembers: true, action.Read: true}, + }, + }, + directGrantProj1b.PublicId: { + { + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1b.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantOrg2.PublicId: { + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantProj2a.PublicId: { + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + { + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Id: "hcst_abcd1234", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Id: "hcst_1234abcd", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.All: true}, + }, + }, + directGrantProj2b.PublicId: { + { + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2b.PublicId, + Id: "cs_abcd1234", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.Update: true, action.Read: true}, + }, + }, + } + + assert.Len(t, directGrants, len(expDirectGrants)) + for k, v := range directGrants { + assert.ElementsMatch(t, v, expDirectGrants[k]) + } + }) + }) +} + +func TestGrantsForUser_ManagedGroup(t *testing.T) { + ctx := context.Background() + + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + + repo := iam.TestRepo(t, conn, wrap) + kmsCache := kms.TestKms(t, conn, wrap) + + o, _ := iam.TestScopes( + t, + repo, + iam.WithSkipAdminRoleCreation(true), + iam.WithSkipDefaultRoleCreation(true), + ) + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + oidcAuthMethod := oidc.TestAuthMethod( + t, conn, databaseWrapper, "global", oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + testManagedGroupRole := func(roleId, managedGrpId string) func() { + return func() { iam.TestManagedGroupRole(t, conn, roleId, managedGrpId) } + } + + // oidcManagedGroup + oidcAcct := oidc.TestAccount(t, conn, oidcAuthMethod, "sub") + oidcManagedGroup := oidc.TestManagedGroup(t, conn, oidcAuthMethod, `"/token/sub" matches ".*"`) + user := iam.TestUser(t, repo, "global", iam.WithAccountIds(oidcAcct.PublicId)) + oidc.TestManagedGroupMember(t, conn, oidcManagedGroup.GetPublicId(), oidcAcct.GetPublicId()) + + // oidcManagedGroup2 + oidcAcct2 := oidc.TestAccount(t, conn, oidcAuthMethod, "sub no.2") + oidcManagedGroup2 := oidc.TestManagedGroup(t, conn, oidcAuthMethod, `"/token/sub" matches ".*"`) + user2 := iam.TestUser(t, repo, "global", iam.WithAccountIds(oidcAcct2.PublicId)) + oidc.TestManagedGroupMember(t, conn, oidcManagedGroup2.GetPublicId(), oidcAcct2.GetPublicId()) + + // Create a series of scopes with roles in each. We'll create two of each + // kind to ensure we're not just picking up the first role in each. + + // The first org/project set contains direct grants, but without + // inheritance. We create two roles in each project. + directGrantOrg1, directGrantProj1a, directGrantProj1b := iam.SetupDirectGrantScopes(t, conn, repo) + + // oidcManagedGroup + directGrantOrg1Role := iam.TestRole(t, conn, directGrantOrg1.PublicId) + directGrantOrg1RoleGrant1 := "ids=*;type=group;actions=*" + directGrantOrg1RoleGrant2 := "ids=*;type=group;actions=create,list" + grantRoleAndAssociate(t, conn, directGrantOrg1Role.PublicId, testManagedGroupRole(directGrantOrg1Role.PublicId, oidcManagedGroup.PublicId), + directGrantOrg1RoleGrant1, directGrantOrg1RoleGrant2, + ) + + // oidcManagedGroup2 + directGrantOrg1Role2 := iam.TestRole(t, conn, directGrantOrg1.PublicId) + directGrantOrg1RoleGrant3 := "ids=*;type=group;actions=update" + grantRoleAndAssociate(t, conn, directGrantOrg1Role2.PublicId, testManagedGroupRole(directGrantOrg1Role2.PublicId, oidcManagedGroup2.PublicId), directGrantOrg1RoleGrant3) + + // oidcManagedGroup + directGrantProj1aRole := iam.TestRole(t, conn, directGrantProj1a.PublicId) + directGrantProj1aRoleGrant := "ids=*;type=group;actions=add-members,read" + grantRoleAndAssociate(t, conn, directGrantProj1aRole.PublicId, testManagedGroupRole(directGrantProj1aRole.PublicId, oidcManagedGroup.PublicId), directGrantProj1aRoleGrant) + + directGrantProj1bRole := iam.TestRole(t, conn, directGrantProj1b.PublicId) + directGrantProj1bRoleGrant := "ids=*;type=group;actions=list,read" + grantRoleAndAssociate(t, conn, directGrantProj1bRole.PublicId, testManagedGroupRole(directGrantProj1bRole.PublicId, oidcManagedGroup.PublicId), directGrantProj1bRoleGrant) + + // oidcManagedGroup2 + directGrantProj1aRole2 := iam.TestRole(t, conn, directGrantProj1a.PublicId) + directGrantProj1aRoleGrant2 := "ids=*;type=group;actions=set-members" + grantRoleAndAssociate(t, conn, directGrantProj1aRole2.PublicId, testManagedGroupRole(directGrantProj1aRole2.PublicId, oidcManagedGroup2.PublicId), directGrantProj1aRoleGrant2) + + directGrantProj1bRole2 := iam.TestRole(t, conn, directGrantProj1b.PublicId) + directGrantProj1bRoleGrant2 := "ids=*;type=group;actions=delete" + grantRoleAndAssociate(t, conn, directGrantProj1bRole2.PublicId, testManagedGroupRole(directGrantProj1bRole2.PublicId, oidcManagedGroup2.PublicId), directGrantProj1bRoleGrant2) + + directGrantOrg2, directGrantProj2a, directGrantProj2b := iam.SetupDirectGrantScopes(t, conn, repo) + + // oidcManagedGroup + directGrantOrg2Role := iam.TestRole(t, conn, directGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeThis, + directGrantProj2a.PublicId, + })) + directGrantOrg2RoleGrant1 := "ids=*;type=group;actions=*" + directGrantOrg2RoleGrant2 := "ids=*;type=group;actions=list,read" + grantRoleAndAssociate(t, conn, directGrantOrg2Role.PublicId, testManagedGroupRole(directGrantOrg2Role.PublicId, oidcManagedGroup.PublicId), + directGrantOrg2RoleGrant1, directGrantOrg2RoleGrant2, + ) + + directGrantProj2aRole := iam.TestRole(t, conn, directGrantProj2a.PublicId) + directGrantProj2aRoleGrant := "ids=hcst_abcd1234,hcst_1234abcd;actions=*" + grantRoleAndAssociate(t, conn, directGrantProj2aRole.PublicId, testManagedGroupRole(directGrantProj2aRole.PublicId, oidcManagedGroup.PublicId), directGrantProj2aRoleGrant) + + directGrantProj2bRole := iam.TestRole(t, conn, directGrantProj2b.PublicId) + directGrantProj2bRoleGrant := "ids=cs_abcd1234;actions=read,update" + grantRoleAndAssociate(t, conn, directGrantProj2bRole.PublicId, testManagedGroupRole(directGrantProj2bRole.PublicId, oidcManagedGroup.PublicId), directGrantProj2bRoleGrant) + + // oidcManagedGroup2 + directGrantOrg2Role2 := iam.TestRole(t, conn, directGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeThis, + directGrantProj2a.PublicId, + })) + directGrantOrg2RoleGrant3 := "ids=*;type=group;actions=add-members" + grantRoleAndAssociate(t, conn, directGrantOrg2Role2.PublicId, testManagedGroupRole(directGrantOrg2Role2.PublicId, oidcManagedGroup2.PublicId), directGrantOrg2RoleGrant3) + + directGrantProj2aRole2 := iam.TestRole(t, conn, directGrantProj2a.PublicId) + directGrantProj2aRoleGrant2 := "ids=hcst_abcd1234,hcst_1234abcd;actions=*" + grantRoleAndAssociate(t, conn, directGrantProj2aRole2.PublicId, testManagedGroupRole(directGrantProj2aRole2.PublicId, oidcManagedGroup2.PublicId), directGrantProj2aRoleGrant2) + + directGrantProj2bRole2 := iam.TestRole(t, conn, directGrantProj2b.PublicId) + directGrantProj2bRoleGrant2 := "ids=cs_abcd1234;actions=read,update" + grantRoleAndAssociate(t, conn, directGrantProj2bRole2.PublicId, testManagedGroupRole(directGrantProj2bRole2.PublicId, oidcManagedGroup2.PublicId), directGrantProj2bRoleGrant2) + + // For the second set we create a couple of orgs/projects and then use + // globals.GrantScopeChildren. + childGrantOrg1, _ := iam.SetupChildGrantScopes(t, conn, repo) + + // oidcManagedGroup + childGrantOrg1Role := iam.TestRole(t, conn, childGrantOrg1.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg1RoleGrant := "ids=*;type=group;actions=add-members,remove-members" + grantRoleAndAssociate(t, conn, childGrantOrg1Role.PublicId, testManagedGroupRole(childGrantOrg1Role.PublicId, oidcManagedGroup.PublicId), childGrantOrg1RoleGrant) + + // oidcManagedGroup2 + childGrantOrg1Role2 := iam.TestRole(t, conn, childGrantOrg1.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg1RoleGrant2 := "ids=*;type=group;actions=read" + grantRoleAndAssociate(t, conn, childGrantOrg1Role2.PublicId, testManagedGroupRole(childGrantOrg1Role2.PublicId, oidcManagedGroup2.PublicId), childGrantOrg1RoleGrant2) + + childGrantOrg2, _ := iam.SetupChildGrantScopes(t, conn, repo) + + // oidcManagedGroup + childGrantOrg2Role := iam.TestRole(t, conn, childGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg2RoleGrant1 := "ids=*;type=group;actions=set-members" + childGrantOrg2RoleGrant2 := "ids=*;type=group;actions=delete" + grantRoleAndAssociate(t, conn, childGrantOrg2Role.PublicId, testManagedGroupRole(childGrantOrg2Role.PublicId, oidcManagedGroup.PublicId), + childGrantOrg2RoleGrant1, childGrantOrg2RoleGrant2, + ) + + // oidcManagedGroup2 + childGrantOrg2Role2 := iam.TestRole(t, conn, childGrantOrg2.PublicId, + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantOrg2RoleGrant3 := "ids=*;type=group;actions=set-members" + grantRoleAndAssociate(t, conn, childGrantOrg2Role2.PublicId, testManagedGroupRole(childGrantOrg2Role2.PublicId, oidcManagedGroup2.PublicId), childGrantOrg2RoleGrant3) + + // Finally, let's create some roles at global scope with children and descendants grants + + // oidcManagedGroup + childGrantGlobalRole := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantGlobalRoleGrant := "ids=*;type=group;actions=*" + grantRoleAndAssociate(t, conn, childGrantGlobalRole.PublicId, testManagedGroupRole(childGrantGlobalRole.PublicId, oidcManagedGroup.PublicId), childGrantGlobalRoleGrant) + + // oidcManagedGroup2 + childGrantGlobalRole2 := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + childGrantGlobalRoleGrant2 := "ids=*;type=group;actions=list" + grantRoleAndAssociate(t, conn, childGrantGlobalRole2.PublicId, testManagedGroupRole(childGrantGlobalRole2.PublicId, oidcManagedGroup2.PublicId), childGrantGlobalRoleGrant2) + + // oidcManagedGroup + descendantGrantGlobalRole := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeDescendants, + })) + descendantGrantGlobalRoleGrant := "ids=*;type=group;actions=*" + grantRoleAndAssociate(t, conn, descendantGrantGlobalRole.PublicId, testManagedGroupRole(descendantGrantGlobalRole.PublicId, oidcManagedGroup.PublicId), descendantGrantGlobalRoleGrant) + + // oidcManagedGroup2 + descendantGrantGlobalRole2 := iam.TestRole(t, conn, scope.Global.String(), + iam.WithGrantScopeIds([]string{ + globals.GrantScopeDescendants, + })) + descendantGrantGlobalRoleGrant2 := "ids=*;type=group;actions=add-members" + grantRoleAndAssociate(t, conn, descendantGrantGlobalRole2.PublicId, testManagedGroupRole(descendantGrantGlobalRole2.PublicId, oidcManagedGroup2.PublicId), descendantGrantGlobalRoleGrant2) + + t.Run("db-grants", func(t *testing.T) { + // Here we should see exactly what the DB has returned, before we do some + // local exploding of grants and grant scopes + expMultiGrantTuples := map[string][]iam.MultiGrantTuple{ + user.PublicId: { + // No grants from noOrg/noProj + // Direct org1/2: + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeThis, + Grants: strings.Join([]string{directGrantOrg1RoleGrant1, directGrantOrg1RoleGrant2}, "^"), + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: strings.Join([]string{globals.GrantScopeThis, directGrantProj2a.PublicId}, "^"), + Grants: strings.Join([]string{directGrantOrg2RoleGrant1, directGrantOrg2RoleGrant2}, "^"), + }, + // Proj orgs 1/2: + { + RoleId: directGrantProj1aRole.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1aRoleGrant, + }, + { + RoleId: directGrantProj1bRole.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1bRoleGrant, + }, + { + RoleId: directGrantProj2aRole.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2aRoleGrant, + }, + { + RoleId: directGrantProj2bRole.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2bRoleGrant, + }, + // Child grants from orgs 1/2: + { + RoleId: childGrantOrg1Role.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantOrg1RoleGrant, + }, + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: strings.Join([]string{childGrantOrg2RoleGrant1, childGrantOrg2RoleGrant2}, "^"), + }, + // Children of global and descendants of global + { + RoleId: descendantGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeDescendants, + Grants: descendantGrantGlobalRoleGrant, + }, + { + RoleId: childGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantGlobalRoleGrant, + }, + }, + user2.PublicId: { + // No grants from noOrg/noProj + // Direct org1/2: + { + RoleId: directGrantOrg1Role2.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantOrg1RoleGrant3, + }, + { + RoleId: directGrantOrg2Role2.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: strings.Join([]string{globals.GrantScopeThis, directGrantProj2a.PublicId}, "^"), + Grants: directGrantOrg2RoleGrant3, + }, + // Proj orgs 1/2: + { + RoleId: directGrantProj1aRole2.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1aRoleGrant2, + }, + { + RoleId: directGrantProj1bRole2.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1bRoleGrant2, + }, + { + RoleId: directGrantProj2aRole2.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2aRoleGrant2, + }, + { + RoleId: directGrantProj2bRole2.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2bRoleGrant2, + }, + // Child grants from orgs 1/2: + { + RoleId: childGrantOrg1Role2.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantOrg1RoleGrant2, + }, + { + RoleId: childGrantOrg2Role2.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantOrg2RoleGrant3, + }, + // Children of global and descendants of global + { + RoleId: descendantGrantGlobalRole2.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeDescendants, + Grants: descendantGrantGlobalRoleGrant2, + }, + { + RoleId: childGrantGlobalRole2.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantGlobalRoleGrant2, + }, + }, + } + for userId, tuples := range expMultiGrantTuples { + for i, tuple := range tuples { + tuple.TestStableSort() + expMultiGrantTuples[userId][i] = tuple + } + multiGrantTuplesCache := new([]iam.MultiGrantTuple) + _, err := repo.GrantsForUser(ctx, userId, iam.WithTestCacheMultiGrantTuples(multiGrantTuplesCache)) + require.NoError(t, err) + + assert.ElementsMatch(t, *multiGrantTuplesCache, expMultiGrantTuples[userId]) + } + }) + + t.Run("exploded-grants", func(t *testing.T) { + // We expect to see: + // + // * No grants from noOrg/noProj + // * Grants from direct orgs/projs: + // * directGrantOrg1/directGrantOrg2 on org and respective projects (6 grants total per org) + // * directGrantProj on respective projects (4 grants total) + expGrantTuples := []perms.GrantTuple{ + // No grants from noOrg/noProj + // Grants from direct org1 to org1/proj1a/proj1b: + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Grant: directGrantOrg1RoleGrant1, + }, + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Grant: directGrantOrg1RoleGrant2, + }, + // Grants from direct org 1 proj 1a: + { + RoleId: directGrantProj1aRole.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1a.PublicId, + Grant: directGrantProj1aRoleGrant, + }, + // Grant from direct org 1 proj 1 b: + { + RoleId: directGrantProj1bRole.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1b.PublicId, + Grant: directGrantProj1bRoleGrant, + }, + + // Grants from direct org2 to org2/proj2a/proj2b: + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Grant: directGrantOrg2RoleGrant1, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantOrg2RoleGrant1, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Grant: directGrantOrg2RoleGrant2, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantOrg2RoleGrant2, + }, + // Grants from direct org 2 proj 2a: + { + RoleId: directGrantProj2aRole.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantProj2aRoleGrant, + }, + // Grant from direct org 2 proj 2 b: + { + RoleId: directGrantProj2bRole.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2b.PublicId, + Grant: directGrantProj2bRoleGrant, + }, + // Child grants from child org1 to proj1a/proj1b: + { + RoleId: childGrantOrg1Role.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg1RoleGrant, + }, + // Child grants from child org2 to proj2a/proj2b: + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg2RoleGrant1, + }, + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg2RoleGrant2, + }, + + // Grants from global to every org: + { + RoleId: childGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantGlobalRoleGrant, + }, + + // Grants from global to every org and project: + { + RoleId: descendantGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Grant: descendantGrantGlobalRoleGrant, + }, + } + + multiGrantTuplesCache := new([]iam.MultiGrantTuple) + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId, iam.WithTestCacheMultiGrantTuples(multiGrantTuplesCache)) + require.NoError(t, err) + assert.ElementsMatch(t, grantTuples, expGrantTuples) + }) + + t.Run("acl-grants", func(t *testing.T) { + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId) + require.NoError(t, err) + grants := make([]perms.Grant, 0, len(grantTuples)) + for _, gt := range grantTuples { + grant, err := perms.Parse(ctx, gt) + require.NoError(t, err) + grants = append(grants, grant) + } + acl := perms.NewACL(grants...) + + t.Run("descendant-grants", func(t *testing.T) { + descendantGrants := acl.DescendantsGrants() + expDescendantGrants := []perms.AclGrant{ + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + } + assert.ElementsMatch(t, descendantGrants, expDescendantGrants) + }) + + t.Run("child-grants", func(t *testing.T) { + childrenGrants := acl.ChildrenScopeGrantMap() + expChildrenGrants := map[string][]perms.AclGrant{ + childGrantOrg1.PublicId: { + { + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.AddMembers: true, action.RemoveMembers: true}, + }, + }, + childGrantOrg2.PublicId: { + { + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.SetMembers: true}, + }, + { + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.Delete: true}, + }, + }, + scope.Global.String(): { + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + }, + } + assert.Len(t, childrenGrants, len(expChildrenGrants)) + for k, v := range childrenGrants { + assert.ElementsMatch(t, v, expChildrenGrants[k]) + } + }) + + t.Run("direct-grants", func(t *testing.T) { + directGrants := acl.DirectScopeGrantMap() + expDirectGrants := map[string][]perms.AclGrant{ + directGrantOrg1.PublicId: { + { + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.Create: true, action.List: true}, + }, + }, + directGrantProj1a.PublicId: { + { + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1a.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.AddMembers: true, action.Read: true}, + }, + }, + directGrantProj1b.PublicId: { + { + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1b.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantOrg2.PublicId: { + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantProj2a.PublicId: { + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + { + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Id: "hcst_abcd1234", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Id: "hcst_1234abcd", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.All: true}, + }, + }, + directGrantProj2b.PublicId: { + { + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2b.PublicId, + Id: "cs_abcd1234", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.Update: true, action.Read: true}, + }, + }, + } + + assert.Len(t, directGrants, len(expDirectGrants)) + for k, v := range directGrants { + assert.ElementsMatch(t, v, expDirectGrants[k]) + } + }) + }) +} + +// grantRoleAndAssociate link one or more grants to a role and associate the role with a principal (i.e. user, group, or managed group) +func grantRoleAndAssociate(t *testing.T, conn *db.DB, roleId string, roleAssociationFunc func(), grants ...string) { + t.Helper() + for _, grant := range grants { + iam.TestRoleGrant(t, conn, roleId, grant) + } + roleAssociationFunc() +} diff --git a/internal/iam/repository_grant_scope.go b/internal/iam/repository_role_grant_scope.go similarity index 74% rename from internal/iam/repository_grant_scope.go rename to internal/iam/repository_role_grant_scope.go index efdf52aa28..a196955662 100644 --- a/internal/iam/repository_grant_scope.go +++ b/internal/iam/repository_role_grant_scope.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/kms" @@ -29,14 +31,10 @@ func (r *Repository) AddRoleGrantScopes(ctx context.Context, roleId string, role return nil, errors.New(ctx, errors.InvalidParameter, op, "missing version") } - role := allocRole() - role.PublicId = roleId - - scope, err := role.GetScope(ctx, r.reader) + scp, err := getRoleScope(ctx, r.reader, roleId) if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId))) + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId))) } - role.ScopeId = scope.PublicId // Find existing grant scopes roleGrantScopes := []*RoleGrantScope{} @@ -59,14 +57,14 @@ func (r *Repository) AddRoleGrantScopes(ctx context.Context, roleId string, role newRoleGrantScopes := make([]*RoleGrantScope, 0, len(addRoleGrantScopes)) for _, grantScope := range grantScopes { - roleGrantScope, err := NewRoleGrantScope(ctx, role.GetPublicId(), grantScope) + roleGrantScope, err := NewRoleGrantScope(ctx, roleId, grantScope) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant scope")) } newRoleGrantScopes = append(newRoleGrantScopes, roleGrantScope) } - oplogWrapper, err := r.kms.GetWrapper(ctx, role.GetScopeId(), kms.KeyPurposeOplog) + oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } @@ -77,17 +75,35 @@ func (r *Repository) AddRoleGrantScopes(ctx context.Context, roleId string, role db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { msgs := make([]*oplog.Message, 0, 2) - roleTicket, err := w.GetTicket(ctx, &role) + // We need to update the role version as that's the aggregate + var updatedRole Resource + switch scp.GetType() { + case scope.Global.String(): + g := allocGlobalRole() + g.PublicId = roleId + g.Version = roleVersion + 1 + updatedRole = &g + case scope.Org.String(): + o := allocOrgRole() + o.PublicId = roleId + o.Version = roleVersion + 1 + updatedRole = &o + case scope.Project.String(): + p := allocProjectRole() + p.PublicId = roleId + p.Version = roleVersion + 1 + updatedRole = &p + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId())) + } + + roleTicket, err := w.GetTicket(ctx, updatedRole) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - // We need to update the role version as that's the aggregate - updatedRole := allocRole() - updatedRole.PublicId = role.GetPublicId() - updatedRole.Version = uint32(roleVersion + 1) var roleOplogMsg oplog.Message - rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) + rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version")) } @@ -103,9 +119,9 @@ func (r *Repository) AddRoleGrantScopes(ctx context.Context, roleId string, role metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, - "scope-id": []string{scope.PublicId}, - "scope-type": []string{scope.Type}, - "resource-public-id": []string{role.PublicId}, + "scope-id": []string{scp.PublicId}, + "scope-type": []string{scp.Type}, + "resource-public-id": []string{roleId}, } if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) @@ -135,16 +151,12 @@ func (r *Repository) DeleteRoleGrantScopes(ctx context.Context, roleId string, r return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") } - role := allocRole() - role.PublicId = roleId - - scope, err := role.GetScope(ctx, r.reader) + scp, err := getRoleScope(ctx, r.reader, roleId) if err != nil { - return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId))) + return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId))) } - role.ScopeId = scope.PublicId - oplogWrapper, err := r.kms.GetWrapper(ctx, role.GetScopeId(), kms.KeyPurposeOplog) + oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog) if err != nil { return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } @@ -156,17 +168,36 @@ func (r *Repository) DeleteRoleGrantScopes(ctx context.Context, roleId string, r db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { msgs := make([]*oplog.Message, 0, 2) - roleTicket, err := w.GetTicket(ctx, &role) + + // We need to update the role version as that's the aggregate + var updatedRole Resource + switch scp.GetType() { + case scope.Global.String(): + g := allocGlobalRole() + g.PublicId = roleId + g.Version = roleVersion + 1 + updatedRole = &g + case scope.Org.String(): + o := allocOrgRole() + o.PublicId = roleId + o.Version = roleVersion + 1 + updatedRole = &o + case scope.Project.String(): + p := allocProjectRole() + p.PublicId = roleId + p.Version = roleVersion + 1 + updatedRole = &p + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId())) + } + + roleTicket, err := w.GetTicket(ctx, updatedRole) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - // We need to update the role version as that's the aggregate - updatedRole := allocRole() - updatedRole.PublicId = role.GetPublicId() - updatedRole.Version = uint32(roleVersion + 1) var roleOplogMsg oplog.Message - rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) + rowsUpdated, err := w.Update(ctx, updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version")) } @@ -198,9 +229,9 @@ func (r *Repository) DeleteRoleGrantScopes(ctx context.Context, roleId string, r metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, - "scope-id": []string{scope.PublicId}, - "scope-type": []string{scope.Type}, - "resource-public-id": []string{role.PublicId}, + "scope-id": []string{scp.PublicId}, + "scope-type": []string{scp.Type}, + "resource-public-id": []string{updatedRole.GetPublicId()}, } if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) @@ -242,9 +273,10 @@ func (r *Repository) SetRoleGrantScopes(ctx context.Context, roleId string, role needFreshReaderWriter = false } - role := allocRole() - role.PublicId = roleId - + scp, err := getRoleScope(ctx, r.reader, roleId) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId))) + } // NOTE: Set calculation can safely take place out of the transaction since // we are using roleVersion to ensure that we end up operating on the same // set of data from this query to the final set in the transaction function @@ -291,12 +323,7 @@ func (r *Repository) SetRoleGrantScopes(ctx context.Context, roleId string, role if len(addRoleGrantScopes) == 0 && len(deleteRoleGrantScopes) == 0 { return currentRoleGrantScopes, db.NoRowsAffected, nil } - - scope, err := role.GetScope(ctx, reader) - if err != nil { - return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId))) - } - oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog) + oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } @@ -305,13 +332,32 @@ func (r *Repository) SetRoleGrantScopes(ctx context.Context, roleId string, role currentRoleGrantScopes = currentRoleGrantScopes[:0] txFunc := func(rdr db.Reader, wtr db.Writer) error { msgs := make([]*oplog.Message, 0, 2) - roleTicket, err := wtr.GetTicket(ctx, &role) + var updatedRole Resource + switch scp.GetType() { + case scope.Global.String(): + g := allocGlobalRole() + g.PublicId = roleId + g.Version = roleVersion + 1 + updatedRole = &g + case scope.Org.String(): + o := allocOrgRole() + o.PublicId = roleId + o.Version = roleVersion + 1 + updatedRole = &o + case scope.Project.String(): + p := allocProjectRole() + p.PublicId = roleId + p.Version = roleVersion + 1 + updatedRole = &p + default: + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type %s for scope %s", scp.GetType(), scp.GetPublicId())) + } + + roleTicket, err := wtr.GetTicket(ctx, updatedRole) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - updatedRole := allocRole() - updatedRole.PublicId = roleId - updatedRole.Version = roleVersion + 1 + var roleOplogMsg oplog.Message rowsUpdated, err := wtr.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion)) if err != nil { @@ -349,8 +395,8 @@ func (r *Repository) SetRoleGrantScopes(ctx context.Context, roleId string, role metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_DELETE.String(), oplog.OpType_OP_TYPE_CREATE.String()}, - "scope-id": []string{scope.PublicId}, - "scope-type": []string{scope.Type}, + "scope-id": []string{scp.PublicId}, + "scope-type": []string{scp.Type}, "resource-public-id": []string{roleId}, } if err := wtr.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil { @@ -379,3 +425,30 @@ func (r *Repository) SetRoleGrantScopes(ctx context.Context, roleId string, role } return currentRoleGrantScopes, totalRowsDeleted, nil } + +// listRoleGrantScopes returns the grant scopes for the roleId +func listRoleGrantScopes(ctx context.Context, reader db.Reader, roleIds []string) ([]*RoleGrantScope, error) { + const op = "iam.(Repository).listRoleGrantScopes" + if len(roleIds) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role ids") + } + rows, err := reader.Query(ctx, roleGrantsScopeQuery, []any{roleIds}) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query role grant scopes")) + } + + if rows.Err() != nil { + return nil, errors.Wrap(ctx, rows.Err(), op, errors.WithMsg("role grant scope rows error")) + } + var result []*RoleGrantScope + for rows.Next() { + if err := reader.ScanRows(ctx, rows, &result); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed scan results from querying role scope for: %s", roleIds))) + } + } + if err := rows.Err(); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unexpected error scanning results from querying role scope for: %s", roleIds))) + } + + return result, nil +} diff --git a/internal/iam/repository_role_grant_scope_test.go b/internal/iam/repository_role_grant_scope_test.go new file mode 100644 index 0000000000..acfe23ebf6 --- /dev/null +++ b/internal/iam/repository_role_grant_scope_test.go @@ -0,0 +1,332 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package iam + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam/store" + "github.com/stretchr/testify/require" +) + +func Test_lookupRoleScope(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + iamRepo := TestRepo(t, conn, wrap) + rw := db.New(conn) + + org1, proj1 := TestScopes(t, iamRepo) + org2, proj2 := TestScopes(t, iamRepo) + + testcases := []struct { + name string + // setup function that return []*Role that will be used as test input and list of expected RoleGrantScope + setupExpect func(t *testing.T) ([]*Role, []*RoleGrantScope) + wantErr bool + wantErrMsg string + }{ + { + name: "global role no individual scope grants happy path", + setupExpect: func(t *testing.T) ([]*Role, []*RoleGrantScope) { + r := TestRole(t, conn, globals.GlobalPrefix) + gRole := allocGlobalRole() + gRole.PublicId = r.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &gRole)) + gRole.GrantScope = globals.GrantScopeDescendants + _, err := rw.Update(ctx, &gRole, []string{"GrantScope"}, []string{}) + require.NoError(t, err) + return []*Role{r}, []*RoleGrantScope{ + { + RoleId: r.PublicId, + ScopeIdOrSpecial: globals.GrantScopeDescendants, + }, + { + RoleId: r.PublicId, + ScopeIdOrSpecial: globals.GrantScopeThis, + }, + } + }, + wantErr: false, + }, + { + name: "global role without this happy path", + setupExpect: func(t *testing.T) ([]*Role, []*RoleGrantScope) { + r := TestRole(t, conn, globals.GlobalPrefix) + gRole := allocGlobalRole() + gRole.PublicId = r.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &gRole)) + gRole.GrantThisRoleScope = false + gRole.GrantScope = globals.GrantScopeDescendants + _, err := rw.Update(ctx, &gRole, []string{"GrantScope", "GrantThisRoleScope"}, []string{}) + require.NoError(t, err) + return []*Role{r}, []*RoleGrantScope{ + { + RoleId: r.PublicId, + ScopeIdOrSpecial: globals.GrantScopeDescendants, + }, + } + }, + wantErr: false, + }, + { + name: "global role without any grants return empty", + setupExpect: func(t *testing.T) ([]*Role, []*RoleGrantScope) { + r := TestRole(t, conn, globals.GlobalPrefix) + gRole := allocGlobalRole() + gRole.PublicId = r.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &gRole)) + gRole.GrantThisRoleScope = false + _, err := rw.Update(ctx, &gRole, []string{"GrantThisRoleScope"}, []string{}) + require.NoError(t, err) + return []*Role{r}, []*RoleGrantScope{} + }, + wantErr: false, + }, + { + name: "multiple roles with different grants union grants happy path", + setupExpect: func(t *testing.T) ([]*Role, []*RoleGrantScope) { + firstRole := TestRole(t, conn, globals.GlobalPrefix) + secondRole := TestRole(t, conn, org2.PublicId) + + firstRoleOrg1 := &globalRoleIndividualOrgGrantScope{ + GlobalRoleIndividualOrgGrantScope: &store.GlobalRoleIndividualOrgGrantScope{ + RoleId: firstRole.PublicId, + ScopeId: org1.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, firstRoleOrg1)) + firstRoleProj1 := &globalRoleIndividualProjectGrantScope{ + GlobalRoleIndividualProjectGrantScope: &store.GlobalRoleIndividualProjectGrantScope{ + RoleId: firstRole.PublicId, + ScopeId: proj1.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, firstRoleProj1)) + + secondRoleProj2 := &orgRoleIndividualGrantScope{ + OrgRoleIndividualGrantScope: &store.OrgRoleIndividualGrantScope{ + RoleId: secondRole.PublicId, + ScopeId: proj2.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, secondRoleProj2)) + + return []*Role{firstRole, secondRole}, []*RoleGrantScope{ + { + RoleId: firstRole.PublicId, + ScopeIdOrSpecial: org1.PublicId, + }, + { + RoleId: firstRole.PublicId, + ScopeIdOrSpecial: globals.GrantScopeThis, + }, + { + RoleId: firstRole.PublicId, + ScopeIdOrSpecial: proj1.PublicId, + }, + { + RoleId: secondRole.PublicId, + ScopeIdOrSpecial: proj2.PublicId, + }, + { + RoleId: secondRole.PublicId, + ScopeIdOrSpecial: globals.GrantScopeThis, + }, + } + }, + wantErr: false, + }, + { + name: "global role with individual scope grants happy path", + setupExpect: func(t *testing.T) ([]*Role, []*RoleGrantScope) { + r := TestRole(t, conn, globals.GlobalPrefix) + gRole := allocGlobalRole() + gRole.PublicId = r.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &gRole)) + o1 := &globalRoleIndividualOrgGrantScope{ + GlobalRoleIndividualOrgGrantScope: &store.GlobalRoleIndividualOrgGrantScope{ + RoleId: r.PublicId, + ScopeId: org1.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, o1)) + o2 := &globalRoleIndividualOrgGrantScope{ + GlobalRoleIndividualOrgGrantScope: &store.GlobalRoleIndividualOrgGrantScope{ + RoleId: r.PublicId, + ScopeId: org2.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, o2)) + p1 := &globalRoleIndividualProjectGrantScope{ + GlobalRoleIndividualProjectGrantScope: &store.GlobalRoleIndividualProjectGrantScope{ + RoleId: r.PublicId, + ScopeId: proj1.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, p1)) + p2 := &globalRoleIndividualProjectGrantScope{ + GlobalRoleIndividualProjectGrantScope: &store.GlobalRoleIndividualProjectGrantScope{ + RoleId: r.PublicId, + ScopeId: proj2.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, p2)) + return []*Role{r}, []*RoleGrantScope{ + { + RoleId: r.PublicId, + ScopeIdOrSpecial: globals.GrantScopeThis, + }, + { + RoleId: r.PublicId, + ScopeIdOrSpecial: org1.PublicId, + }, + { + RoleId: r.PublicId, + ScopeIdOrSpecial: org2.PublicId, + }, + { + RoleId: r.PublicId, + ScopeIdOrSpecial: proj1.PublicId, + }, + { + RoleId: r.PublicId, + ScopeIdOrSpecial: proj2.PublicId, + }, + } + }, + wantErr: false, + }, + { + name: "multiple role with children and descendants grants happy path", + setupExpect: func(t *testing.T) ([]*Role, []*RoleGrantScope) { + role1 := TestRole(t, conn, globals.GlobalPrefix) + role2 := TestRole(t, conn, org2.PublicId) + + gRole := allocGlobalRole() + gRole.PublicId = role1.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &gRole)) + gRole.GrantScope = globals.GrantScopeDescendants + _, err := rw.Update(ctx, &gRole, []string{"GrantScope"}, []string{}) + require.NoError(t, err) + + oRole := allocOrgRole() + oRole.PublicId = role2.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &oRole)) + oRole.GrantScope = globals.GrantScopeChildren + _, err = rw.Update(ctx, &oRole, []string{"GrantScope"}, []string{}) + require.NoError(t, err) + + return []*Role{role1, role2}, []*RoleGrantScope{ + { + RoleId: role1.PublicId, + ScopeIdOrSpecial: globals.GrantScopeThis, + }, + { + RoleId: role1.PublicId, + ScopeIdOrSpecial: globals.GrantScopeDescendants, + }, + { + RoleId: role2.PublicId, + ScopeIdOrSpecial: globals.GrantScopeThis, + }, + { + RoleId: role2.PublicId, + ScopeIdOrSpecial: globals.GrantScopeChildren, + }, + } + }, + wantErr: false, + }, + { + name: "multiple project role happy path", + setupExpect: func(t *testing.T) ([]*Role, []*RoleGrantScope) { + role1 := TestRole(t, conn, proj1.PublicId) + role2 := TestRole(t, conn, proj2.PublicId) + return []*Role{role1, role2}, []*RoleGrantScope{ + { + RoleId: role1.PublicId, + ScopeIdOrSpecial: globals.GrantScopeThis, + }, + { + RoleId: role2.PublicId, + ScopeIdOrSpecial: globals.GrantScopeThis, + }, + } + }, + wantErr: false, + }, + { + name: "role does not exist returns empty grant scope", + setupExpect: func(t *testing.T) ([]*Role, []*RoleGrantScope) { + invalidRole := &Role{ + PublicId: "r_123456", + ScopeId: globals.GlobalPrefix, + } + return []*Role{invalidRole}, []*RoleGrantScope{} + }, + wantErr: false, + }, + { + name: "bad role id returns error", + setupExpect: func(t *testing.T) ([]*Role, []*RoleGrantScope) { + invalidRole := &Role{ + PublicId: "", + ScopeId: globals.GlobalPrefix, + } + return []*Role{invalidRole}, []*RoleGrantScope{} + }, + wantErr: true, + wantErrMsg: `iam.(Repository).listRoleGrantScopes: missing role ids: parameter violation: error #100`, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + roles, expect := tc.setupExpect(t) + var roleIds []string + for _, r := range roles { + roleIds = append(roleIds, r.PublicId) + } + expectRoleScopeMap := roleScopesToMap(t, expect) + got, err := listRoleGrantScopes(ctx, rw, roleIds) + if tc.wantErr { + require.Error(t, err) + require.ErrorContains(t, err, tc.wantErrMsg) + return + } + require.NoError(t, err) + gotRoleScopeMap := roleScopesToMap(t, got) + require.Equal(t, len(expectRoleScopeMap), len(gotRoleScopeMap)) + for roleId, expectRoleScopes := range expectRoleScopeMap { + gotRoleScopes, found := gotRoleScopeMap[roleId] + require.True(t, found) + require.ElementsMatch(t, expectRoleScopes, gotRoleScopes) + } + }) + } +} + +func roleScopesToMap(t *testing.T, roleGrantScopes []*RoleGrantScope) map[string][]string { + t.Helper() + m := map[string][]string{} + for _, e := range roleGrantScopes { + scopes, ok := m[e.RoleId] + if !ok { + m[e.RoleId] = []string{e.ScopeIdOrSpecial} + continue + } + m[e.RoleId] = append(scopes, e.ScopeIdOrSpecial) + } + return m +} diff --git a/internal/iam/repository_role_grant_test.go b/internal/iam/repository_role_grant_test.go index ba90363692..7d04c7fa58 100644 --- a/internal/iam/repository_role_grant_test.go +++ b/internal/iam/repository_role_grant_test.go @@ -199,7 +199,9 @@ func TestRepository_ListRoleGrants(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - db.TestDeleteWhere(t, conn, func() any { r := allocRole(); return &r }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocGlobalRole(); return &i }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocOrgRole(); return &i }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocProjectRole(); return &i }(), "1=1") role := TestRole(t, conn, tt.createGrantScopeId) roleGrants := make([]string, 0, tt.createCnt) for i := 0; i < tt.createCnt; i++ { @@ -589,42 +591,12 @@ func TestGrantsForUser(t *testing.T) { // The first org/project do not have any direct grants to the user. They // contain roles but the user is not a principal. - noGrantOrg1, noGrantProj1 := TestScopes( - t, - repo, - WithSkipAdminRoleCreation(true), - WithSkipDefaultRoleCreation(true), - ) - noGrantOrg1Role := TestRole(t, conn, noGrantOrg1.PublicId) - TestRoleGrant(t, conn, noGrantOrg1Role.PublicId, "ids=*;type=scope;actions=*") - noGrantProj1Role := TestRole(t, conn, noGrantProj1.PublicId) - TestRoleGrant(t, conn, noGrantProj1Role.PublicId, "ids=*;type=*;actions=*") - noGrantOrg2, noGrantProj2 := TestScopes( - t, - repo, - WithSkipAdminRoleCreation(true), - WithSkipDefaultRoleCreation(true), - ) - noGrantOrg2Role := TestRole(t, conn, noGrantOrg2.PublicId) - TestRoleGrant(t, conn, noGrantOrg2Role.PublicId, "ids=*;type=scope;actions=*") - noGrantProj2Role := TestRole(t, conn, noGrantProj2.PublicId) - TestRoleGrant(t, conn, noGrantProj2Role.PublicId, "ids=*;type=*;actions=*") + noGrantOrg1, noGrantProj1 := SetupNoGrantScopes(t, conn, repo) + noGrantOrg2, noGrantProj2 := SetupNoGrantScopes(t, conn, repo) // The second org/project set contains direct grants, but without // inheritance. We create two roles in each project. - directGrantOrg1, directGrantProj1a := TestScopes( - t, - repo, - WithSkipAdminRoleCreation(true), - WithSkipDefaultRoleCreation(true), - ) - directGrantProj1b := TestProject( - t, - repo, - directGrantOrg1.PublicId, - WithSkipAdminRoleCreation(true), - WithSkipDefaultRoleCreation(true), - ) + directGrantOrg1, directGrantProj1a, directGrantProj1b := SetupDirectGrantScopes(t, conn, repo) directGrantOrg1Role := TestRole(t, conn, directGrantOrg1.PublicId) TestUserRole(t, conn, directGrantOrg1Role.PublicId, user.PublicId) directGrantOrg1RoleGrant1 := "ids=*;type=*;actions=*" @@ -641,19 +613,7 @@ func TestGrantsForUser(t *testing.T) { directGrantProj1bRoleGrant := "ids=*;type=session;actions=list,read" TestRoleGrant(t, conn, directGrantProj1bRole.PublicId, directGrantProj1bRoleGrant) - directGrantOrg2, directGrantProj2a := TestScopes( - t, - repo, - WithSkipAdminRoleCreation(true), - WithSkipDefaultRoleCreation(true), - ) - directGrantProj2b := TestProject( - t, - repo, - directGrantOrg2.PublicId, - WithSkipAdminRoleCreation(true), - WithSkipDefaultRoleCreation(true), - ) + directGrantOrg2, directGrantProj2a, directGrantProj2b := SetupDirectGrantScopes(t, conn, repo) directGrantOrg2Role := TestRole(t, conn, directGrantOrg2.PublicId, WithGrantScopeIds([]string{ globals.GrantScopeThis, @@ -676,12 +636,7 @@ func TestGrantsForUser(t *testing.T) { // For the third set we create a couple of orgs/projects and then use // globals.GrantScopeChildren. - childGrantOrg1, childGrantOrg1Proj := TestScopes( - t, - repo, - WithSkipAdminRoleCreation(true), - WithSkipDefaultRoleCreation(true), - ) + childGrantOrg1, childGrantOrg1Proj := SetupChildGrantScopes(t, conn, repo) childGrantOrg1Role := TestRole(t, conn, childGrantOrg1.PublicId, WithGrantScopeIds([]string{ globals.GrantScopeChildren, @@ -689,12 +644,8 @@ func TestGrantsForUser(t *testing.T) { TestUserRole(t, conn, childGrantOrg1Role.PublicId, user.PublicId) childGrantOrg1RoleGrant := "ids=*;type=host-set;actions=add-hosts,remove-hosts" TestRoleGrant(t, conn, childGrantOrg1Role.PublicId, childGrantOrg1RoleGrant) - childGrantOrg2, childGrantOrg2Proj := TestScopes( - t, - repo, - WithSkipAdminRoleCreation(true), - WithSkipDefaultRoleCreation(true), - ) + + childGrantOrg2, childGrantOrg2Proj := SetupChildGrantScopes(t, conn, repo) childGrantOrg2Role := TestRole(t, conn, childGrantOrg2.PublicId, WithGrantScopeIds([]string{ globals.GrantScopeChildren, @@ -725,7 +676,7 @@ func TestGrantsForUser(t *testing.T) { t.Run("db-grants", func(t *testing.T) { // Here we should see exactly what the DB has returned, before we do some // local exploding of grants and grant scopes - expMultiGrantTuples := []multiGrantTuple{ + expMultiGrantTuples := []MultiGrantTuple{ // No grants from noOrg/noProj // Direct org1/2: { @@ -801,14 +752,13 @@ func TestGrantsForUser(t *testing.T) { }, } for i, tuple := range expMultiGrantTuples { - tuple.testStableSort() + tuple.TestStableSort() expMultiGrantTuples[i] = tuple } - multiGrantTuplesCache := new([]multiGrantTuple) - _, err := repo.GrantsForUser(ctx, user.PublicId, withTestCacheMultiGrantTuples(multiGrantTuplesCache)) + multiGrantTuplesCache := new([]MultiGrantTuple) + _, err := repo.GrantsForUser(ctx, user.PublicId, WithTestCacheMultiGrantTuples(multiGrantTuplesCache)) require.NoError(t, err) - // log.Println("multiGrantTuplesCache", pretty.Sprint(*multiGrantTuplesCache)) assert.ElementsMatch(t, *multiGrantTuplesCache, expMultiGrantTuples) }) @@ -939,8 +889,8 @@ func TestGrantsForUser(t *testing.T) { }, } - multiGrantTuplesCache := new([]multiGrantTuple) - grantTuples, err := repo.GrantsForUser(ctx, user.PublicId, withTestCacheMultiGrantTuples(multiGrantTuplesCache)) + multiGrantTuplesCache := new([]MultiGrantTuple) + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId, WithTestCacheMultiGrantTuples(multiGrantTuplesCache)) require.NoError(t, err) assert.ElementsMatch(t, grantTuples, expGrantTuples) }) @@ -1121,14 +1071,7 @@ func TestGrantsForUser(t *testing.T) { }, }, } - /* - log.Println("org1", directGrantOrg1.PublicId) - log.Println("proj1a", directGrantProj1a.PublicId) - log.Println("proj1b", directGrantProj1b.PublicId) - log.Println("org2", directGrantOrg2.PublicId) - log.Println("proj2a", directGrantProj2a.PublicId) - log.Println("proj2b", directGrantProj2b.PublicId) - */ + assert.Len(t, directGrants, len(expDirectGrants)) for k, v := range directGrants { assert.ElementsMatch(t, v, expDirectGrants[k]) @@ -1536,3 +1479,953 @@ func TestGrantsForUser(t *testing.T) { } }) } + +func TestGrantsForUser_Group(t *testing.T) { + ctx := context.Background() + + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + + repo := TestRepo(t, conn, wrap) + user := TestUser(t, repo, "global") + group := TestGroup(t, conn, "global") + + TestGroupMember(t, conn, group.PublicId, user.PublicId) + + // Create a series of scopes with roles in each. We'll create two of each + // kind to ensure we're not just picking up the first role in each. + + // The first org/project do not have any direct grants to the user. They + // contain roles but the user is not a principal. + noGrantOrg1, noGrantProj1 := SetupNoGrantScopes(t, conn, repo) + noGrantOrg2, noGrantProj2 := SetupNoGrantScopes(t, conn, repo) + + // The second org/project set contains direct grants, but without + // inheritance. We create two roles in each project. + directGrantOrg1, directGrantProj1a, directGrantProj1b := SetupDirectGrantScopes(t, conn, repo) + directGrantOrg1Role := TestRole(t, conn, directGrantOrg1.PublicId) + TestGroupRole(t, conn, directGrantOrg1Role.PublicId, group.PublicId) + directGrantOrg1RoleGrant1 := "ids=*;type=*;actions=*" + TestRoleGrant(t, conn, directGrantOrg1Role.PublicId, directGrantOrg1RoleGrant1) + directGrantOrg1RoleGrant2 := "ids=*;type=role;actions=list,read" + TestRoleGrant(t, conn, directGrantOrg1Role.PublicId, directGrantOrg1RoleGrant2) + + directGrantProj1aRole := TestRole(t, conn, directGrantProj1a.PublicId) + TestGroupRole(t, conn, directGrantProj1aRole.PublicId, group.PublicId) + directGrantProj1aRoleGrant := "ids=*;type=target;actions=authorize-session,read" + TestRoleGrant(t, conn, directGrantProj1aRole.PublicId, directGrantProj1aRoleGrant) + directGrantProj1bRole := TestRole(t, conn, directGrantProj1b.PublicId) + TestGroupRole(t, conn, directGrantProj1bRole.PublicId, group.PublicId) + directGrantProj1bRoleGrant := "ids=*;type=session;actions=list,read" + TestRoleGrant(t, conn, directGrantProj1bRole.PublicId, directGrantProj1bRoleGrant) + + directGrantOrg2, directGrantProj2a, directGrantProj2b := SetupDirectGrantScopes(t, conn, repo) + directGrantOrg2Role := TestRole(t, conn, directGrantOrg2.PublicId, + WithGrantScopeIds([]string{ + globals.GrantScopeThis, + directGrantProj2a.PublicId, + })) + TestGroupRole(t, conn, directGrantOrg2Role.PublicId, group.PublicId) + directGrantOrg2RoleGrant1 := "ids=*;type=user;actions=*" + TestRoleGrant(t, conn, directGrantOrg2Role.PublicId, directGrantOrg2RoleGrant1) + directGrantOrg2RoleGrant2 := "ids=*;type=group;actions=list,read" + TestRoleGrant(t, conn, directGrantOrg2Role.PublicId, directGrantOrg2RoleGrant2) + + directGrantProj2aRole := TestRole(t, conn, directGrantProj2a.PublicId) + TestGroupRole(t, conn, directGrantProj2aRole.PublicId, group.PublicId) + directGrantProj2aRoleGrant := "ids=hcst_abcd1234,hcst_1234abcd;actions=*" + TestRoleGrant(t, conn, directGrantProj2aRole.PublicId, directGrantProj2aRoleGrant) + directGrantProj2bRole := TestRole(t, conn, directGrantProj2b.PublicId) + TestGroupRole(t, conn, directGrantProj2bRole.PublicId, group.PublicId) + directGrantProj2bRoleGrant := "ids=cs_abcd1234;actions=read,update" + TestRoleGrant(t, conn, directGrantProj2bRole.PublicId, directGrantProj2bRoleGrant) + + // For the third set we create a couple of orgs/projects and then use + // globals.GrantScopeChildren. + childGrantOrg1, childGrantOrg1Proj := SetupChildGrantScopes(t, conn, repo) + childGrantOrg1Role := TestRole(t, conn, childGrantOrg1.PublicId, + WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + TestGroupRole(t, conn, childGrantOrg1Role.PublicId, group.PublicId) + childGrantOrg1RoleGrant := "ids=*;type=host-set;actions=add-hosts,remove-hosts" + TestRoleGrant(t, conn, childGrantOrg1Role.PublicId, childGrantOrg1RoleGrant) + + childGrantOrg2, childGrantOrg2Proj := SetupChildGrantScopes(t, conn, repo) + childGrantOrg2Role := TestRole(t, conn, childGrantOrg2.PublicId, + WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + TestGroupRole(t, conn, childGrantOrg2Role.PublicId, group.PublicId) + childGrantOrg2RoleGrant1 := "ids=*;type=session;actions=cancel:self" + TestRoleGrant(t, conn, childGrantOrg2Role.PublicId, childGrantOrg2RoleGrant1) + childGrantOrg2RoleGrant2 := "ids=*;type=session;actions=read:self" + TestRoleGrant(t, conn, childGrantOrg2Role.PublicId, childGrantOrg2RoleGrant2) + + // Finally, let's create some roles at global scope with children and + // descendants grants + childGrantGlobalRole := TestRole(t, conn, scope.Global.String(), + WithGrantScopeIds([]string{ + globals.GrantScopeChildren, + })) + TestUserRole(t, conn, childGrantGlobalRole.PublicId, globals.AnyAuthenticatedUserId) + childGrantGlobalRoleGrant := "ids=*;type=account;actions=*" + TestRoleGrant(t, conn, childGrantGlobalRole.PublicId, childGrantGlobalRoleGrant) + descendantGrantGlobalRole := TestRole(t, conn, scope.Global.String(), + WithGrantScopeIds([]string{ + globals.GrantScopeDescendants, + })) + TestUserRole(t, conn, descendantGrantGlobalRole.PublicId, globals.AnyAuthenticatedUserId) + descendantGrantGlobalRoleGrant := "ids=*;type=credential;actions=*" + TestRoleGrant(t, conn, descendantGrantGlobalRole.PublicId, descendantGrantGlobalRoleGrant) + + t.Run("db-grants", func(t *testing.T) { + // Here we should see exactly what the DB has returned, before we do some + // local exploding of grants and grant scopes + expMultiGrantTuples := []MultiGrantTuple{ + // No grants from noOrg/noProj + // Direct org1/2: + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeThis, + Grants: strings.Join([]string{directGrantOrg1RoleGrant1, directGrantOrg1RoleGrant2}, "^"), + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: strings.Join([]string{globals.GrantScopeThis, directGrantProj2a.PublicId}, "^"), + Grants: strings.Join([]string{directGrantOrg2RoleGrant1, directGrantOrg2RoleGrant2}, "^"), + }, + // Proj orgs 1/2: + { + RoleId: directGrantProj1aRole.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1aRoleGrant, + }, + { + RoleId: directGrantProj1bRole.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj1bRoleGrant, + }, + { + RoleId: directGrantProj2aRole.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2aRoleGrant, + }, + { + RoleId: directGrantProj2bRole.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeIds: globals.GrantScopeThis, + Grants: directGrantProj2bRoleGrant, + }, + // Child grants from orgs 1/2: + { + RoleId: childGrantOrg1Role.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantOrg1RoleGrant, + }, + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: strings.Join([]string{childGrantOrg2RoleGrant1, childGrantOrg2RoleGrant2}, "^"), + }, + // Children of global and descendants of global + { + RoleId: descendantGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeDescendants, + Grants: descendantGrantGlobalRoleGrant, + }, + { + RoleId: childGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeIds: globals.GrantScopeChildren, + Grants: childGrantGlobalRoleGrant, + }, + } + for i, tuple := range expMultiGrantTuples { + tuple.TestStableSort() + expMultiGrantTuples[i] = tuple + } + multiGrantTuplesCache := new([]MultiGrantTuple) + _, err := repo.GrantsForUser(ctx, user.PublicId, WithTestCacheMultiGrantTuples(multiGrantTuplesCache)) + require.NoError(t, err) + + assert.ElementsMatch(t, *multiGrantTuplesCache, expMultiGrantTuples) + }) + + t.Run("exploded-grants", func(t *testing.T) { + // We expect to see: + // + // * No grants from noOrg/noProj + // * Grants from direct orgs/projs: + // * directGrantOrg1/directGrantOrg2 on org and respective projects (6 grants total per org) + // * directGrantProj on respective projects (4 grants total) + expGrantTuples := []perms.GrantTuple{ + // No grants from noOrg/noProj + // Grants from direct org1 to org1/proj1a/proj1b: + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Grant: directGrantOrg1RoleGrant1, + }, + { + RoleId: directGrantOrg1Role.PublicId, + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Grant: directGrantOrg1RoleGrant2, + }, + // Grants from direct org 1 proj 1a: + { + RoleId: directGrantProj1aRole.PublicId, + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1a.PublicId, + Grant: directGrantProj1aRoleGrant, + }, + // Grant from direct org 1 proj 1 b: + { + RoleId: directGrantProj1bRole.PublicId, + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1b.PublicId, + Grant: directGrantProj1bRoleGrant, + }, + + // Grants from direct org2 to org2/proj2a/proj2b: + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Grant: directGrantOrg2RoleGrant1, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantOrg2RoleGrant1, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Grant: directGrantOrg2RoleGrant2, + }, + { + RoleId: directGrantOrg2Role.PublicId, + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantOrg2RoleGrant2, + }, + // Grants from direct org 2 proj 2a: + { + RoleId: directGrantProj2aRole.PublicId, + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Grant: directGrantProj2aRoleGrant, + }, + // Grant from direct org 2 proj 2 b: + { + RoleId: directGrantProj2bRole.PublicId, + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2b.PublicId, + Grant: directGrantProj2bRoleGrant, + }, + // Child grants from child org1 to proj1a/proj1b: + { + RoleId: childGrantOrg1Role.PublicId, + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg1RoleGrant, + }, + // Child grants from child org2 to proj2a/proj2b: + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg2RoleGrant1, + }, + { + RoleId: childGrantOrg2Role.PublicId, + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantOrg2RoleGrant2, + }, + + // Grants from global to every org: + { + RoleId: childGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Grant: childGrantGlobalRoleGrant, + }, + + // Grants from global to every org and project: + { + RoleId: descendantGrantGlobalRole.PublicId, + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Grant: descendantGrantGlobalRoleGrant, + }, + } + + multiGrantTuplesCache := new([]MultiGrantTuple) + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId, WithTestCacheMultiGrantTuples(multiGrantTuplesCache)) + require.NoError(t, err) + assert.ElementsMatch(t, grantTuples, expGrantTuples) + }) + + t.Run("acl-grants", func(t *testing.T) { + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId) + require.NoError(t, err) + grants := make([]perms.Grant, 0, len(grantTuples)) + for _, gt := range grantTuples { + grant, err := perms.Parse(ctx, gt) + require.NoError(t, err) + grants = append(grants, grant) + } + acl := perms.NewACL(grants...) + + t.Run("descendant-grants", func(t *testing.T) { + descendantGrants := acl.DescendantsGrants() + expDescendantGrants := []perms.AclGrant{ + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeDescendants, + Id: "*", + Type: resource.Credential, + ActionSet: perms.ActionSet{action.All: true}, + }, + } + assert.ElementsMatch(t, descendantGrants, expDescendantGrants) + }) + + t.Run("child-grants", func(t *testing.T) { + childrenGrants := acl.ChildrenScopeGrantMap() + expChildrenGrants := map[string][]perms.AclGrant{ + childGrantOrg1.PublicId: { + { + RoleScopeId: childGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.HostSet, + ActionSet: perms.ActionSet{action.AddHosts: true, action.RemoveHosts: true}, + }, + }, + childGrantOrg2.PublicId: { + { + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Session, + ActionSet: perms.ActionSet{action.CancelSelf: true}, + }, + { + RoleScopeId: childGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Session, + ActionSet: perms.ActionSet{action.ReadSelf: true}, + }, + }, + scope.Global.String(): { + { + RoleScopeId: scope.Global.String(), + GrantScopeId: globals.GrantScopeChildren, + Id: "*", + Type: resource.Account, + ActionSet: perms.ActionSet{action.All: true}, + }, + }, + } + assert.Len(t, childrenGrants, len(expChildrenGrants)) + for k, v := range childrenGrants { + assert.ElementsMatch(t, v, expChildrenGrants[k]) + } + }) + + t.Run("direct-grants", func(t *testing.T) { + directGrants := acl.DirectScopeGrantMap() + expDirectGrants := map[string][]perms.AclGrant{ + directGrantOrg1.PublicId: { + { + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Id: "*", + Type: resource.All, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg1.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg1.PublicId, + Id: "*", + Type: resource.Role, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantProj1a.PublicId: { + { + RoleScopeId: directGrantProj1a.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1a.PublicId, + Id: "*", + Type: resource.Target, + ActionSet: perms.ActionSet{action.AuthorizeSession: true, action.Read: true}, + }, + }, + directGrantProj1b.PublicId: { + { + RoleScopeId: directGrantProj1b.PublicId, + RoleParentScopeId: directGrantOrg1.PublicId, + GrantScopeId: directGrantProj1b.PublicId, + Id: "*", + Type: resource.Session, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantOrg2.PublicId: { + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Id: "*", + Type: resource.User, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantOrg2.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + }, + directGrantProj2a.PublicId: { + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Id: "*", + Type: resource.User, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantOrg2.PublicId, + RoleParentScopeId: scope.Global.String(), + GrantScopeId: directGrantProj2a.PublicId, + Id: "*", + Type: resource.Group, + ActionSet: perms.ActionSet{action.List: true, action.Read: true}, + }, + { + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Id: "hcst_abcd1234", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.All: true}, + }, + { + RoleScopeId: directGrantProj2a.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2a.PublicId, + Id: "hcst_1234abcd", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.All: true}, + }, + }, + directGrantProj2b.PublicId: { + { + RoleScopeId: directGrantProj2b.PublicId, + RoleParentScopeId: directGrantOrg2.PublicId, + GrantScopeId: directGrantProj2b.PublicId, + Id: "cs_abcd1234", + Type: resource.Unknown, + ActionSet: perms.ActionSet{action.Update: true, action.Read: true}, + }, + }, + } + + assert.Len(t, directGrants, len(expDirectGrants)) + for k, v := range directGrants { + assert.ElementsMatch(t, v, expDirectGrants[k]) + } + }) + }) + t.Run("real-world", func(t *testing.T) { + // These tests cases crib from the initial setup of the grants, and + // include a number of cases to ensure the ones that should work do and + // various that should not do not + type testCase struct { + name string + res perms.Resource + act action.Type + shouldWork bool + } + testCases := []testCase{} + + // These test cases should fail because the grants are in roles where + // the user is not a principal + { + testCases = append(testCases, testCase{ + name: "nogrant-a", + res: perms.Resource{ + ScopeId: noGrantOrg1.PublicId, + Id: "u_abcd1234", + Type: resource.Scope, + ParentScopeId: scope.Global.String(), + }, + act: action.Read, + }, testCase{ + name: "nogrant-b", + res: perms.Resource{ + ScopeId: noGrantProj1.PublicId, + Id: "u_abcd1234", + Type: resource.User, + ParentScopeId: noGrantOrg1.String(), + }, + act: action.Read, + }, testCase{ + name: "nogrant-c", + res: perms.Resource{ + ScopeId: noGrantOrg2.PublicId, + Id: "u_abcd1234", + Type: resource.Scope, + ParentScopeId: scope.Global.String(), + }, + act: action.Read, + }, testCase{ + name: "nogrant-d", + res: perms.Resource{ + ScopeId: noGrantProj2.PublicId, + Id: "u_abcd1234", + Type: resource.User, + ParentScopeId: noGrantOrg2.String(), + }, + act: action.Read, + }, + ) + } + // These test cases are for org1 and its projects where the grants are + // direct, not via children/descendants. They test some actions that + // should work and some that shouldn't. + { + testCases = append(testCases, testCase{ + name: "direct-a", + res: perms.Resource{ + ScopeId: directGrantOrg1.PublicId, + Id: "u_abcd1234", + Type: resource.User, + ParentScopeId: scope.Global.String(), + }, + act: action.Read, + shouldWork: true, + }, testCase{ + name: "direct-b", + res: perms.Resource{ + ScopeId: directGrantOrg1.PublicId, + Id: "r_abcd1234", + Type: resource.Role, + ParentScopeId: scope.Global.String(), + }, + act: action.Read, + shouldWork: true, + }, testCase{ + name: "direct-c", + res: perms.Resource{ + ScopeId: directGrantProj1a.PublicId, + Id: "ttcp_abcd1234", + Type: resource.Target, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.AuthorizeSession, + shouldWork: true, + }, testCase{ + name: "direct-d", + res: perms.Resource{ + ScopeId: directGrantProj1a.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.Read, + }, testCase{ + name: "direct-e", + res: perms.Resource{ + ScopeId: directGrantProj1b.PublicId, + Id: "ttcp_abcd1234", + Type: resource.Target, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.AuthorizeSession, + }, testCase{ + name: "direct-f", + res: perms.Resource{ + ScopeId: directGrantProj1b.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.Read, + shouldWork: true, + }, + ) + } + // These test cases are for org2 and its projects where the grants are + // direct, not via children/descendants. They test some actions that + // should work and some that shouldn't. + { + testCases = append(testCases, testCase{ + name: "direct-g", + res: perms.Resource{ + ScopeId: directGrantOrg2.PublicId, + Id: "u_abcd1234", + Type: resource.User, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "direct-m", + res: perms.Resource{ + ScopeId: directGrantOrg2.PublicId, + Id: "g_abcd1234", + Type: resource.Group, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + }, testCase{ + name: "direct-h", + res: perms.Resource{ + ScopeId: directGrantOrg2.PublicId, + Id: "acct_abcd1234", + Type: resource.Account, + ParentScopeId: scope.Global.String(), + }, + act: action.Delete, + shouldWork: true, + }, testCase{ + name: "direct-i", + res: perms.Resource{ + ScopeId: directGrantProj2a.PublicId, + Type: resource.Group, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.List, + shouldWork: true, + }, testCase{ + name: "direct-j", + res: perms.Resource{ + ScopeId: directGrantProj2a.PublicId, + Id: "r_abcd1234", + Type: resource.Role, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Read, + }, testCase{ + name: "direct-n", + res: perms.Resource{ + ScopeId: directGrantProj2a.PublicId, + Id: "u_abcd1234", + Type: resource.User, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Read, + shouldWork: true, + }, testCase{ + name: "direct-k", + res: perms.Resource{ + ScopeId: directGrantProj2a.PublicId, + Id: "hcst_abcd1234", + Type: resource.HostCatalog, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Read, + shouldWork: true, + }, testCase{ + name: "direct-l", + res: perms.Resource{ + ScopeId: directGrantProj2b.PublicId, + Id: "cs_abcd1234", + Type: resource.CredentialStore, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Update, + shouldWork: true, + }, + testCase{ + name: "direct-m", + res: perms.Resource{ + ScopeId: directGrantProj2b.PublicId, + Id: "cl_abcd1234", + Type: resource.CredentialLibrary, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Update, + }, + ) + } + // These test cases are child grants + { + testCases = append(testCases, testCase{ + name: "children-a", + res: perms.Resource{ + ScopeId: scope.Global.String(), + Id: "a_abcd1234", + Type: resource.Account, + }, + act: action.Update, + }, testCase{ + name: "children-b", + res: perms.Resource{ + ScopeId: noGrantOrg1.PublicId, + Id: "a_abcd1234", + Type: resource.Account, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "children-c", + res: perms.Resource{ + ScopeId: directGrantOrg1.PublicId, + Id: "a_abcd1234", + Type: resource.Account, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "children-d", + res: perms.Resource{ + ScopeId: directGrantOrg2.PublicId, + Id: "a_abcd1234", + Type: resource.Account, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "children-e", + res: perms.Resource{ + ScopeId: childGrantOrg2.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: scope.Global.String(), + }, + act: action.CancelSelf, + }, testCase{ + name: "children-f", + res: perms.Resource{ + ScopeId: childGrantOrg1Proj.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: childGrantOrg1.PublicId, + }, + act: action.CancelSelf, + }, testCase{ + name: "children-g", + res: perms.Resource{ + ScopeId: childGrantOrg2Proj.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: childGrantOrg2.PublicId, + }, + act: action.CancelSelf, + shouldWork: true, + }, testCase{ + name: "children-h", + res: perms.Resource{ + ScopeId: childGrantOrg2Proj.PublicId, + Id: "s_abcd1234", + Type: resource.Session, + ParentScopeId: childGrantOrg2.PublicId, + }, + act: action.CancelSelf, + shouldWork: true, + }, testCase{ + name: "children-i", + res: perms.Resource{ + ScopeId: childGrantOrg1.PublicId, + Id: "hsst_abcd1234", + Type: resource.HostSet, + ParentScopeId: scope.Global.String(), + }, + act: action.AddHosts, + }, testCase{ + name: "children-j", + res: perms.Resource{ + ScopeId: childGrantOrg1Proj.PublicId, + Id: "hsst_abcd1234", + Type: resource.HostSet, + ParentScopeId: childGrantOrg1.PublicId, + }, + act: action.AddHosts, + shouldWork: true, + }, testCase{ + name: "children-k", + res: perms.Resource{ + ScopeId: childGrantOrg2Proj.PublicId, + Id: "hsst_abcd1234", + Type: resource.HostSet, + ParentScopeId: childGrantOrg2.PublicId, + }, + act: action.AddHosts, + }, + ) + } + // These test cases are global descendants grants + { + testCases = append(testCases, testCase{ + name: "descendants-a", + res: perms.Resource{ + ScopeId: scope.Global.String(), + Id: "cs_abcd1234", + Type: resource.Credential, + }, + act: action.Update, + }, testCase{ + name: "descendants-b", + res: perms.Resource{ + ScopeId: noGrantProj1.PublicId, + Id: "cs_abcd1234", + Type: resource.Credential, + ParentScopeId: noGrantOrg1.PublicId, + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "descendants-c", + res: perms.Resource{ + ScopeId: directGrantOrg2.PublicId, + Id: "cs_abcd1234", + Type: resource.Credential, + ParentScopeId: scope.Global.String(), + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "descendants-d", + res: perms.Resource{ + ScopeId: directGrantProj1a.PublicId, + Id: "cs_abcd1234", + Type: resource.Credential, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "descendants-e", + res: perms.Resource{ + ScopeId: directGrantProj1a.PublicId, + Id: "cs_abcd1234", + Type: resource.Credential, + ParentScopeId: directGrantOrg1.PublicId, + }, + act: action.Update, + shouldWork: true, + }, testCase{ + name: "descendants-f", + res: perms.Resource{ + ScopeId: directGrantProj2b.PublicId, + Id: "cs_abcd1234", + Type: resource.Credential, + ParentScopeId: directGrantOrg2.PublicId, + }, + act: action.Update, + shouldWork: true, + }, + ) + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + grantTuples, err := repo.GrantsForUser(ctx, user.PublicId) + require.NoError(t, err) + grants := make([]perms.Grant, 0, len(grantTuples)) + for _, gt := range grantTuples { + grant, err := perms.Parse(ctx, gt) + require.NoError(t, err) + grants = append(grants, grant) + } + acl := perms.NewACL(grants...) + assert.True(t, acl.Allowed(tc.res, tc.act, "u_abc123").Authorized == tc.shouldWork) + }) + } + }) +} + +func SetupNoGrantScopes(t *testing.T, conn *db.DB, repo *Repository) (noGrantOrg, noGrantProj *Scope) { + t.Helper() + noGrantOrg, noGrantProj = TestScopes( + t, + repo, + WithSkipAdminRoleCreation(true), + WithSkipDefaultRoleCreation(true), + ) + noGrantOrg1Role := TestRole(t, conn, noGrantOrg.PublicId) + TestRoleGrant(t, conn, noGrantOrg1Role.PublicId, "ids=*;type=scope;actions=*") + noGrantProj1Role := TestRole(t, conn, noGrantProj.PublicId) + TestRoleGrant(t, conn, noGrantProj1Role.PublicId, "ids=*;type=*;actions=*") + return +} + +func SetupDirectGrantScopes(t *testing.T, conn *db.DB, repo *Repository) (directGrantOrg, directGrantProjA, directGrantProjB *Scope) { + t.Helper() + directGrantOrg, directGrantProjA = TestScopes( + t, + repo, + WithSkipAdminRoleCreation(true), + WithSkipDefaultRoleCreation(true), + ) + directGrantProjB = TestProject( + t, + repo, + directGrantOrg.PublicId, + WithSkipAdminRoleCreation(true), + WithSkipDefaultRoleCreation(true), + ) + return +} + +func SetupChildGrantScopes(t *testing.T, conn *db.DB, repo *Repository) (childGrantOrg, childGrantOrgProj *Scope) { + t.Helper() + childGrantOrg, childGrantOrgProj = TestScopes( + t, + repo, + WithSkipAdminRoleCreation(true), + WithSkipDefaultRoleCreation(true), + ) + return +} diff --git a/internal/iam/repository_role_test.go b/internal/iam/repository_role_test.go index b39dd391bd..31a3f948f2 100644 --- a/internal/iam/repository_role_test.go +++ b/internal/iam/repository_role_test.go @@ -6,19 +6,21 @@ package iam import ( "context" "fmt" + "strings" "testing" "time" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/db" dbassert "github.com/hashicorp/boundary/internal/db/assert" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam/store" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/go-uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" ) func TestRepository_CreateRole(t *testing.T) { @@ -31,6 +33,7 @@ func TestRepository_CreateRole(t *testing.T) { id := testId(t) org, proj := TestScopes(t, repo) + dupeOrg, dupeProj := TestScopes(t, repo) type args struct { role *Role @@ -39,11 +42,22 @@ func TestRepository_CreateRole(t *testing.T) { tests := []struct { name string args args - wantDup bool + dupeSetup func(*testing.T) *Role wantErr bool wantErrMsg string wantIsError errors.Code }{ + { + name: "valid-global", + args: args{ + role: func() *Role { + r, err := NewRole(ctx, globals.GlobalPrefix, WithName("valid-global"+id), WithDescription(id)) + assert.NoError(t, err) + return r + }(), + }, + wantErr: false, + }, { name: "valid-org", args: args{ @@ -90,71 +104,132 @@ func TestRepository_CreateRole(t *testing.T) { wantIsError: errors.InvalidParameter, }, { - name: "nil-store", + name: "bad-scope-id", args: args{ role: func() *Role { - return &Role{ - Role: nil, - } + r, err := NewRole(ctx, id) + assert.NoError(t, err) + return r }(), }, wantErr: true, - wantErrMsg: "iam.(Repository).CreateRole: missing role store: parameter violation: error #100", + wantErrMsg: "iam.(Repository).CreateRole: invalid scope type: parameter violation: error #100", wantIsError: errors.InvalidParameter, }, { - name: "bad-scope-id", + name: "global-dup-name", args: args{ role: func() *Role { - r, err := NewRole(ctx, id) + r, err := NewRole(ctx, globals.GlobalPrefix, WithName("global-dup-name"+id), WithDescription(id)) assert.NoError(t, err) return r }(), + opt: []Option{WithName("dup-name" + id)}, + }, + dupeSetup: func(t *testing.T) *Role { + r, err := NewRole(ctx, globals.GlobalPrefix, WithName("global-dup-name"+id), WithDescription(id)) + require.NoError(t, err) + dup, _, _, _, err := repo.CreateRole(context.Background(), r) + require.NoError(t, err) + require.NotNil(t, dup) + return r }, wantErr: true, - wantErrMsg: "iam.(Repository).create: error getting metadata: iam.(Repository).stdMetadata: unable to get scope: iam.LookupScope: db.LookupWhere: record not found, search issue: error #1100", - wantIsError: errors.RecordNotFound, + wantErrMsg: "already exists in scope ", + wantIsError: errors.NotUnique, }, { - name: "dup-name", + name: "org-dup-name", args: args{ role: func() *Role { - r, err := NewRole(ctx, org.PublicId, WithName("dup-name"+id), WithDescription(id)) + r, err := NewRole(ctx, org.PublicId, WithName("org-dup-name"+id), WithDescription(id)) assert.NoError(t, err) return r }(), - opt: []Option{WithName("dup-name" + id)}, + opt: []Option{WithName("org-dup-name" + id)}, + }, + dupeSetup: func(t *testing.T) *Role { + r, err := NewRole(ctx, org.PublicId, WithName("org-dup-name"+id), WithDescription(id)) + require.NoError(t, err) + dup, _, _, _, err := repo.CreateRole(context.Background(), r) + require.NoError(t, err) + require.NotNil(t, dup) + return r }, - wantDup: true, wantErr: true, wantErrMsg: "already exists in scope ", wantIsError: errors.NotUnique, }, { - name: "dup-name-but-diff-scope", + name: "proj-dup-name", + args: args{ + role: func() *Role { + r, err := NewRole(ctx, proj.PublicId, WithName("proj-dup-name"+id), WithDescription(id)) + assert.NoError(t, err) + return r + }(), + opt: []Option{WithName("proj-dup-name" + id)}, + }, + dupeSetup: func(t *testing.T) *Role { + r, err := NewRole(ctx, proj.PublicId, WithName("proj-dup-name"+id), WithDescription(id)) + require.NoError(t, err) + dup, _, _, _, err := repo.CreateRole(context.Background(), r) + require.NoError(t, err) + require.NotNil(t, dup) + return r + }, + wantErr: true, + wantErrMsg: "already exists in scope ", + wantIsError: errors.NotUnique, + }, + { + name: "dup-name-but-diff-org", + args: args{ + role: func() *Role { + r, err := NewRole(ctx, org.PublicId, WithName("dup-name-but-diff-org"+id), WithDescription(id)) + assert.NoError(t, err) + return r + }(), + opt: []Option{WithName("dup-name-but-diff-scope" + id)}, + }, + dupeSetup: func(t *testing.T) *Role { + r, err := NewRole(ctx, dupeOrg.PublicId, WithName("dup-name-but-diff-org"+id), WithDescription(id)) + require.NoError(t, err) + dup, _, _, _, err := repo.CreateRole(context.Background(), r) + require.NoError(t, err) + require.NotNil(t, dup) + return r + }, + wantErr: false, + }, + { + name: "dup-name-but-diff-proj", args: args{ role: func() *Role { - r, err := NewRole(ctx, proj.PublicId, WithName("dup-name-but-diff-scope"+id), WithDescription(id)) + r, err := NewRole(ctx, proj.PublicId, WithName("dup-name-but-diff-proj"+id), WithDescription(id)) assert.NoError(t, err) return r }(), opt: []Option{WithName("dup-name-but-diff-scope" + id)}, }, - wantDup: true, + dupeSetup: func(t *testing.T) *Role { + r, err := NewRole(ctx, dupeProj.PublicId, WithName("dup-name-but-diff-proj"+id), WithDescription(id)) + require.NoError(t, err) + dup, _, _, _, err := repo.CreateRole(context.Background(), r) + require.NoError(t, err) + require.NotNil(t, dup) + return r + }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) - - if tt.wantDup { - dup, err := NewRole(ctx, org.PublicId, tt.args.opt...) - assert.NoError(err) - dup, _, _, _, err = repo.CreateRole(context.Background(), dup, tt.args.opt...) - assert.NoError(err) - assert.NotNil(dup) + if tt.dupeSetup != nil { + _ = tt.dupeSetup(t) } + grp, _, _, _, err := repo.CreateRole(context.Background(), tt.args.role, tt.args.opt...) if tt.wantErr { assert.Error(err) @@ -169,7 +244,7 @@ func TestRepository_CreateRole(t *testing.T) { foundGrp, _, _, _, err := repo.LookupRole(context.Background(), grp.PublicId) assert.NoError(err) - assert.True(proto.Equal(foundGrp, grp)) + assert.Equal(foundGrp, grp) err = db.TestVerifyOplog(t, rw, grp.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) assert.NoError(err) @@ -188,6 +263,7 @@ func TestRepository_UpdateRole(t *testing.T) { require.NoError(t, err) org, proj := TestScopes(t, repo) + dupeOrg, dupeProj := TestScopes(t, repo) u := TestUser(t, repo, org.GetPublicId()) pubId := func(s string) *string { return &s } @@ -200,11 +276,24 @@ func TestRepository_UpdateRole(t *testing.T) { ScopeId string PublicId *string } + + // used to create a role during setup + type newRoleArg struct { + ScopeId string + Name string + Description string + } + tests := []struct { - name string - newScopeId string - newRoleOpts []Option - args args + name string + args args + // REQUIRED: newRoleArgs is used to create a role to be updated + // the result roleId from this create call will be passed to the update call + // unless args.PublicId is set + newRoleArgs *newRoleArg + // OPTIONAL: dupeArgs is used to create a role for before role in `newRoleArgs` role is created + // used for testing duplicate names + dupeArgs *newRoleArg wantRowsUpdate int wantErr bool wantErrMsg string @@ -212,127 +301,293 @@ func TestRepository_UpdateRole(t *testing.T) { wantDup bool }{ { - name: "valid", + name: "valid global", args: args{ - name: "valid" + id, + name: "valid global" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: globals.GlobalPrefix, + }, + newRoleArgs: &newRoleArg{ + ScopeId: globals.GlobalPrefix, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "valid org", + args: args{ + name: "valid org" + id, fieldMaskPaths: []string{"Name"}, ScopeId: org.PublicId, }, - newScopeId: org.PublicId, + newRoleArgs: &newRoleArg{ + ScopeId: org.PublicId, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "valid project", + args: args{ + name: "valid project" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + newRoleArgs: &newRoleArg{ + ScopeId: proj.PublicId, + }, wantErr: false, wantRowsUpdate: 1, }, + { - name: "valid-no-op", + name: "valid global no-op", args: args{ - name: "valid-no-op" + id, + name: "valid-global-no-op" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: globals.GlobalPrefix, + }, + newRoleArgs: &newRoleArg{ + ScopeId: globals.GlobalPrefix, + Name: "valid-global-no-op" + id, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "valid org no-op", + args: args{ + name: "valid-org-no-op" + id, fieldMaskPaths: []string{"Name"}, ScopeId: org.PublicId, }, - newScopeId: org.PublicId, - newRoleOpts: []Option{WithName("valid-no-op" + id)}, + newRoleArgs: &newRoleArg{ + ScopeId: org.PublicId, + Name: "valid-org-no-op" + id, + }, wantErr: false, wantRowsUpdate: 1, }, { - name: "not-found", + name: "valid project no-op", args: args{ - name: "not-found" + id, + name: "valid-project-no-op" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + newRoleArgs: &newRoleArg{ + ScopeId: proj.PublicId, + Name: "valid-project-no-op" + id, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "not-found-global", + args: args{ + name: "global-not-found" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: globals.GlobalPrefix, + PublicId: func() *string { s := "1"; return &s }(), + }, + newRoleArgs: &newRoleArg{ + ScopeId: globals.GlobalPrefix, + }, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "error #1100", + wantIsError: errors.RecordNotFound, + }, + { + name: "not-found-org", + args: args{ + name: "org-not-found" + id, fieldMaskPaths: []string{"Name"}, ScopeId: org.PublicId, PublicId: func() *string { s := "1"; return &s }(), }, - newScopeId: org.PublicId, + newRoleArgs: &newRoleArg{ + ScopeId: org.PublicId, + }, + wantErr: true, + wantRowsUpdate: 0, + wantErrMsg: "error #1100", + wantIsError: errors.RecordNotFound, + }, + { + name: "not-found-project", + args: args{ + name: "proj-not-found" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + PublicId: func() *string { s := "1"; return &s }(), + }, + newRoleArgs: &newRoleArg{ + ScopeId: proj.PublicId, + }, wantErr: true, wantRowsUpdate: 0, wantErrMsg: "error #1100", wantIsError: errors.RecordNotFound, }, { - name: "null-name", + name: "global-null-name", + args: args{ + name: "", + fieldMaskPaths: []string{"Name"}, + ScopeId: globals.GlobalPrefix, + }, + newRoleArgs: &newRoleArg{ + Name: "global-null-name" + id, + ScopeId: globals.GlobalPrefix, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "org-null-name", args: args{ name: "", fieldMaskPaths: []string{"Name"}, ScopeId: org.PublicId, }, - newScopeId: org.PublicId, - newRoleOpts: []Option{WithName("null-name" + id)}, + newRoleArgs: &newRoleArg{ + Name: "org-null-name" + id, + ScopeId: org.PublicId, + }, wantErr: false, wantRowsUpdate: 1, }, { - name: "null-description", + name: "proj-null-name", args: args{ name: "", + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + newRoleArgs: &newRoleArg{ + Name: "proj-null-name" + id, + ScopeId: proj.PublicId, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "global-null-description", + args: args{ + description: "", + fieldMaskPaths: []string{"Description"}, + ScopeId: globals.GlobalPrefix, + }, + newRoleArgs: &newRoleArg{ + Description: "hello", + ScopeId: globals.GlobalPrefix, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "org-null-description", + args: args{ + description: "", fieldMaskPaths: []string{"Description"}, ScopeId: org.PublicId, }, - newScopeId: org.PublicId, - newRoleOpts: []Option{WithDescription("null-description" + id)}, + newRoleArgs: &newRoleArg{ + Description: "hello", + ScopeId: org.PublicId, + }, wantErr: false, wantRowsUpdate: 1, }, { - name: "empty-field-mask", + name: "project-null-description", args: args{ - name: "valid" + id, + description: "", + fieldMaskPaths: []string{"Description"}, + ScopeId: proj.PublicId, + }, + newRoleArgs: &newRoleArg{ + Description: "hello", + ScopeId: proj.PublicId, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "input-validation-empty-field-mask", + args: args{ + name: "valid-global" + id, fieldMaskPaths: []string{}, - ScopeId: org.PublicId, + ScopeId: globals.GlobalPrefix, + }, + newRoleArgs: &newRoleArg{ + Name: "valid-global" + id, + ScopeId: globals.GlobalPrefix, }, - newScopeId: org.PublicId, wantErr: true, wantRowsUpdate: 0, wantErrMsg: "iam.(Repository).UpdateRole: empty field mask, parameter violation: error #104", wantIsError: errors.EmptyFieldMask, }, { - name: "nil-fieldmask", + name: "input-validation-nil-fieldmask", args: args{ name: "valid" + id, fieldMaskPaths: nil, ScopeId: org.PublicId, }, - newScopeId: org.PublicId, + newRoleArgs: &newRoleArg{ + Name: "valid" + id, + ScopeId: globals.GlobalPrefix, + }, wantErr: true, wantRowsUpdate: 0, wantErrMsg: "iam.(Repository).UpdateRole: empty field mask, parameter violation: error #104", wantIsError: errors.EmptyFieldMask, }, { - name: "read-only-fields", + name: "input-validation-read-only-fields", args: args{ - name: "valid" + id, + name: "read-only-fields" + id, fieldMaskPaths: []string{"CreateTime"}, ScopeId: org.PublicId, }, - newScopeId: org.PublicId, + newRoleArgs: &newRoleArg{ + Name: "read-only-fields" + id, + ScopeId: globals.GlobalPrefix, + }, wantErr: true, wantRowsUpdate: 0, wantErrMsg: "iam.(Repository).UpdateRole: invalid field mask: CreateTime: parameter violation: error #103", wantIsError: errors.InvalidFieldMask, }, { - name: "unknown-fields", + name: "input-validation-unknown-fields", args: args{ name: "valid" + id, fieldMaskPaths: []string{"Alice"}, ScopeId: org.PublicId, }, - newScopeId: org.PublicId, + newRoleArgs: &newRoleArg{ + ScopeId: globals.GlobalPrefix, + }, wantErr: true, wantRowsUpdate: 0, wantErrMsg: "iam.(Repository).UpdateRole: invalid field mask: Alice: parameter violation: error #103", wantIsError: errors.InvalidFieldMask, }, { - name: "no-public-id", + name: "input-validation-no-public-id", args: args{ name: "valid" + id, fieldMaskPaths: []string{"Name"}, ScopeId: org.PublicId, PublicId: pubId(""), }, - newScopeId: org.PublicId, + newRoleArgs: &newRoleArg{ + ScopeId: org.PublicId, + }, wantErr: true, wantErrMsg: "iam.(Repository).UpdateRole: missing public id: parameter violation: error #100", wantIsError: errors.InvalidParameter, @@ -344,7 +599,9 @@ func TestRepository_UpdateRole(t *testing.T) { name: "proj-scope-id" + id, ScopeId: proj.PublicId, }, - newScopeId: org.PublicId, + newRoleArgs: &newRoleArg{ + ScopeId: proj.PublicId, + }, wantErr: true, wantErrMsg: "iam.(Repository).UpdateRole: empty field mask, parameter violation: error #104", wantIsError: errors.EmptyFieldMask, @@ -356,51 +613,153 @@ func TestRepository_UpdateRole(t *testing.T) { fieldMaskPaths: []string{"Name"}, ScopeId: "", }, - newScopeId: org.PublicId, + newRoleArgs: &newRoleArg{ + ScopeId: org.PublicId, + }, + wantErr: false, + wantRowsUpdate: 1, + }, + { + name: "global-dup-name", + args: args{ + name: "global-dup-name" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: globals.GlobalPrefix, + }, + newRoleArgs: &newRoleArg{ + ScopeId: globals.GlobalPrefix, + }, + dupeArgs: &newRoleArg{ + ScopeId: globals.GlobalPrefix, + Name: "global-dup-name" + id, + }, + wantErr: true, + wantDup: true, + wantErrMsg: " already exists in scope " + globals.GlobalPrefix, + wantIsError: errors.NotUnique, + }, + { + name: "org-dup-name", + args: args{ + name: "org-dup-name" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: org.PublicId, + }, + newRoleArgs: &newRoleArg{ + ScopeId: org.PublicId, + }, + dupeArgs: &newRoleArg{ + ScopeId: org.PublicId, + Name: "org-dup-name" + id, + }, + wantErr: true, + wantDup: true, + wantErrMsg: " already exists in scope " + org.PublicId, + wantIsError: errors.NotUnique, + }, + { + name: "proj-dup-name", + args: args{ + name: "proj-dup-name" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + newRoleArgs: &newRoleArg{ + ScopeId: proj.PublicId, + }, + dupeArgs: &newRoleArg{ + ScopeId: proj.PublicId, + Name: "proj-dup-name" + id, + }, + wantErr: true, + wantDup: true, + wantErrMsg: " already exists in scope " + proj.PublicId, + wantIsError: errors.NotUnique, + }, + { + name: "global-org-dup-name-in-diff-scope", + args: args{ + name: "global-org-dup-name-in-diff-scope" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: globals.GlobalPrefix, + }, + newRoleArgs: &newRoleArg{ + ScopeId: globals.GlobalPrefix, + }, + dupeArgs: &newRoleArg{ + ScopeId: org.PublicId, + Name: "global-org-dup-name-in-diff-scope" + id, + }, wantErr: false, wantRowsUpdate: 1, + wantDup: true, }, { - name: "dup-name-in-diff-scope", + name: "org-proj-dup-name-in-diff-scope", args: args{ - name: "dup-name-in-diff-scope" + id, + name: "org-proj-dup-name-in-diff-scope" + id, fieldMaskPaths: []string{"Name"}, ScopeId: proj.PublicId, }, - newScopeId: proj.PublicId, - newRoleOpts: []Option{WithName("dup-name-in-diff-scope-pre-update" + id)}, + newRoleArgs: &newRoleArg{ + ScopeId: proj.PublicId, + }, + dupeArgs: &newRoleArg{ + ScopeId: org.PublicId, + Name: "org-proj-dup-name-in-diff-scope" + id, + }, wantErr: false, wantRowsUpdate: 1, wantDup: true, }, { - name: "dup-name", + name: "org-dup-name-in-diff-scope", args: args{ - name: "dup-name" + id, + name: "org-dup-name-in-diff-scope" + id, fieldMaskPaths: []string{"Name"}, ScopeId: org.PublicId, }, - newScopeId: org.PublicId, - wantErr: true, - wantDup: true, - wantErrMsg: " already exists in org " + org.PublicId, - wantIsError: errors.NotUnique, + newRoleArgs: &newRoleArg{ + ScopeId: org.PublicId, + }, + dupeArgs: &newRoleArg{ + ScopeId: dupeOrg.PublicId, + Name: "org-dup-name-in-diff-scope" + id, + }, + wantErr: false, + wantRowsUpdate: 1, + wantDup: true, + }, + { + name: "project-dup-name-in-diff-scope", + args: args{ + name: "project-dup-name-in-diff-scope" + id, + fieldMaskPaths: []string{"Name"}, + ScopeId: proj.PublicId, + }, + newRoleArgs: &newRoleArg{ + ScopeId: proj.PublicId, + }, + dupeArgs: &newRoleArg{ + ScopeId: dupeProj.PublicId, + Name: "project-dup-name-in-diff-scope" + id, + }, + wantErr: false, + wantRowsUpdate: 1, + wantDup: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { require, assert := require.New(t), assert.New(t) - if tt.wantDup { - r := TestRole(t, conn, org.PublicId) - _ = TestUserRole(t, conn, r.GetPublicId(), u.GetPublicId()) - _ = TestRoleGrant(t, conn, r.GetPublicId(), "ids=*;type=*;actions=*") - r.Name = tt.args.name - _, _, _, _, _, err := repo.UpdateRole(context.Background(), r, r.Version, tt.args.fieldMaskPaths, tt.args.opt...) - assert.NoError(err) + if tt.dupeArgs != nil { + dupedRole := TestRole(t, conn, tt.dupeArgs.ScopeId, WithName(tt.dupeArgs.Name), WithDescription(tt.dupeArgs.Description)) + _ = TestUserRole(t, conn, dupedRole.GetPublicId(), u.GetPublicId()) + _ = TestRoleGrant(t, conn, dupedRole.GetPublicId(), "ids=*;type=*;actions=*") } - r := TestRole(t, conn, tt.newScopeId, tt.newRoleOpts...) - ur := TestUserRole(t, conn, r.GetPublicId(), u.GetPublicId()) + testRole := TestRole(t, conn, tt.newRoleArgs.ScopeId, WithName(tt.newRoleArgs.Name), WithDescription(tt.newRoleArgs.Description)) + ur := TestUserRole(t, conn, testRole.GetPublicId(), u.GetPublicId()) princRole := &PrincipalRole{PrincipalRoleView: &store.PrincipalRoleView{ Type: UserRoleType.String(), CreateTime: ur.CreateTime, @@ -410,31 +769,33 @@ func TestRepository_UpdateRole(t *testing.T) { ScopedPrincipalId: ur.PrincipalId, RoleScopeId: org.GetPublicId(), }} - if tt.newScopeId != org.GetPublicId() { + if tt.newRoleArgs.ScopeId != u.ScopeId { // If the project is in a different scope from the created user we need to update the // scope specific fields. - princRole.RoleScopeId = tt.newScopeId - princRole.ScopedPrincipalId = fmt.Sprintf("%s:%s", org.PublicId, ur.PrincipalId) + princRole.RoleScopeId = tt.newRoleArgs.ScopeId + princRole.ScopedPrincipalId = fmt.Sprintf("%s:%s", u.ScopeId, ur.PrincipalId) } - rGrant := TestRoleGrant(t, conn, r.GetPublicId(), "ids=*;type=*;actions=*") - updateRole := allocRole() - updateRole.PublicId = r.PublicId - if tt.args.PublicId != nil { - updateRole.PublicId = *tt.args.PublicId - } + rGrant := TestRoleGrant(t, conn, testRole.GetPublicId(), "ids=*;type=*;actions=*") + + updateRole := Role{} + updateRole.PublicId = testRole.PublicId updateRole.ScopeId = tt.args.ScopeId updateRole.Name = tt.args.name updateRole.Description = tt.args.description - roleAfterUpdate, principals, grants, _, updatedRows, err := repo.UpdateRole(context.Background(), &updateRole, r.Version, tt.args.fieldMaskPaths, tt.args.opt...) + if tt.args.PublicId != nil { + updateRole.PublicId = *tt.args.PublicId + } + + roleAfterUpdate, principals, grants, _, updatedRows, err := repo.UpdateRole(context.Background(), &updateRole, testRole.Version, tt.args.fieldMaskPaths, tt.args.opt...) if tt.wantErr { require.Error(err) assert.True(errors.Match(errors.T(tt.wantIsError), err)) assert.Nil(roleAfterUpdate) assert.Equal(0, updatedRows) assert.Contains(err.Error(), tt.wantErrMsg) - err = db.TestVerifyOplog(t, rw, r.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + err = db.TestVerifyOplog(t, rw, testRole.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) assert.Error(err) assert.True(errors.IsNotFoundError(err)) return @@ -444,27 +805,47 @@ func TestRepository_UpdateRole(t *testing.T) { assert.Equal(tt.wantRowsUpdate, updatedRows) assert.Equal([]*PrincipalRole{princRole}, principals) assert.Equal([]*RoleGrant{rGrant}, grants) - switch tt.name { - case "valid-no-op": - assert.Equal(r.UpdateTime, roleAfterUpdate.UpdateTime) + switch { + case strings.Contains(tt.name, "no-op"): + assert.Equal(testRole.UpdateTime, roleAfterUpdate.UpdateTime) default: - assert.NotEqual(r.UpdateTime, roleAfterUpdate.UpdateTime) + assert.NotEqual(testRole.UpdateTime, roleAfterUpdate.UpdateTime) } - foundRole, _, _, _, err := repo.LookupRole(context.Background(), r.PublicId) + foundRole, _, _, _, err := repo.LookupRole(context.Background(), testRole.PublicId) assert.NoError(err) - assert.True(proto.Equal(roleAfterUpdate, foundRole)) + assert.Equal(roleAfterUpdate, foundRole) underlyingDB, err := conn.SqlDB(ctx) require.NoError(err) dbassert := dbassert.New(t, underlyingDB) + + var dbRole any + switch { + case strings.HasPrefix(foundRole.ScopeId, globals.GlobalPrefix): + g := allocGlobalRole() + g.PublicId = foundRole.PublicId + require.NoError(rw.LookupByPublicId(ctx, &g)) + dbRole = &g + case strings.HasPrefix(foundRole.ScopeId, globals.OrgPrefix): + o := allocOrgRole() + o.PublicId = foundRole.PublicId + require.NoError(rw.LookupByPublicId(ctx, &o)) + dbRole = &o + case strings.HasPrefix(foundRole.ScopeId, globals.ProjectPrefix): + p := allocProjectRole() + p.PublicId = foundRole.PublicId + require.NoError(rw.LookupByPublicId(ctx, &p)) + dbRole = &p + } if tt.args.name == "" { - assert.Equal(foundRole.Name, "") - dbassert.IsNull(foundRole, "name") + + assert.Equal("", foundRole.Name) + dbassert.IsNull(dbRole, "name") } if tt.args.description == "" { - assert.Equal(foundRole.Description, "") - dbassert.IsNull(foundRole, "description") + assert.Equal("", foundRole.Description) + dbassert.IsNull(dbRole, "description") } - err = db.TestVerifyOplog(t, rw, r.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + err = db.TestVerifyOplog(t, rw, testRole.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) assert.NoError(err) }) } @@ -477,7 +858,7 @@ func TestRepository_DeleteRole(t *testing.T) { rw := db.New(conn) wrapper := db.TestWrapper(t) repo := TestRepo(t, conn, wrapper) - org, _ := TestScopes(t, repo) + org, proj := TestScopes(t, repo) roleId, err := newRoleId(ctx) require.NoError(t, err) @@ -494,18 +875,34 @@ func TestRepository_DeleteRole(t *testing.T) { wantErrMsg string }{ { - name: "valid", + name: "valid global", + args: args{ + role: TestRole(t, conn, globals.GlobalPrefix), + }, + wantRowsDeleted: 1, + wantErr: false, + }, + { + name: "valid org", args: args{ role: TestRole(t, conn, org.PublicId), }, wantRowsDeleted: 1, wantErr: false, }, + { + name: "valid project", + args: args{ + role: TestRole(t, conn, proj.PublicId), + }, + wantRowsDeleted: 1, + wantErr: false, + }, { name: "no-public-id", args: args{ role: func() *Role { - r := allocRole() + r := Role{} return &r }(), }, @@ -526,7 +923,7 @@ func TestRepository_DeleteRole(t *testing.T) { }, wantRowsDeleted: 0, wantErr: true, - wantErrMsg: "iam.(Repository).DeleteRole: failed for " + roleId + ": db.LookupById: record not found, search issue: error #1100", + wantErrMsg: fmt.Sprintf("iam.(Repository).DeleteRole: cannot find scope for role %s: iam.getRoleScopeType: role %s not found: search issue: error #1100", roleId, roleId), }, } for _, tt := range tests { @@ -634,7 +1031,9 @@ func TestRepository_listRoles(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) t.Cleanup(func() { - db.TestDeleteWhere(t, conn, func() any { r := allocRole(); return &r }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocGlobalRole(); return &i }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocOrgRole(); return &i }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocProjectRole(); return &i }(), "1=1") }) testRoles := []*Role{} for i := 0; i < tt.createCnt; i++ { @@ -750,7 +1149,9 @@ func TestRepository_ListRoles_Multiple_Scopes(t *testing.T) { repo := TestRepo(t, conn, wrapper) org, proj := TestScopes(t, repo) - db.TestDeleteWhere(t, conn, func() any { i := allocRole(); return &i }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocGlobalRole(); return &i }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocOrgRole(); return &i }(), "1=1") + db.TestDeleteWhere(t, conn, func() any { i := allocProjectRole(); return &i }(), "1=1") const numPerScope = 10 var total int @@ -853,3 +1254,265 @@ func Test_estimatedRoleCount(t *testing.T) { require.NoError(t, err) assert.Equal(t, 0, numItems) } + +func Test_getRoleScopeType(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + ctx := context.Background() + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + org, proj := TestScopes(t, repo) + type arg struct { + roleId string + dbReader db.Reader + } + testcases := []struct { + name string + inputRoleId func(t *testing.T) arg + expect scope.Type + wantErr bool + wantErrMsg string + }{ + { + name: "valid role global scope", + inputRoleId: func(t *testing.T) arg { + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: grpId, + ScopeId: globals.GlobalPrefix, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeDescendants, + }, + } + require.NoError(t, rw.Create(ctx, r)) + return arg{ + roleId: r.PublicId, + dbReader: rw, + } + }, + expect: scope.Global, + }, + { + name: "valid role org scope", + inputRoleId: func(t *testing.T) arg { + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: grpId, + ScopeId: org.PublicId, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, r)) + return arg{ + roleId: r.PublicId, + dbReader: rw, + } + }, + expect: scope.Org, + }, + { + name: "valid role project scope", + inputRoleId: func(t *testing.T) arg { + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: grpId, + ScopeId: proj.PublicId, + }, + } + require.NoError(t, rw.Create(ctx, r)) + return arg{ + roleId: r.PublicId, + dbReader: rw, + } + }, + expect: scope.Project, + }, + { + name: "role does not exist returns error", + inputRoleId: func(t *testing.T) arg { + return arg{ + roleId: "r_123456", + dbReader: rw, + } + }, + wantErr: true, + wantErrMsg: `iam.getRoleScopeType: role r_123456 not found: search issue: error #1100`, + }, + { + name: "missing role id returns error", + inputRoleId: func(t *testing.T) arg { + return arg{ + roleId: "", + dbReader: rw, + } + }, + wantErr: true, + wantErrMsg: `iam.getRoleScopeType: missing role id: parameter violation: error #100`, + }, + { + name: "missing db.Reader returns error", + inputRoleId: func(t *testing.T) arg { + return arg{ + roleId: "r_123456", + dbReader: nil, + } + }, + wantErr: true, + wantErrMsg: `iam.getRoleScopeType: missing db.Reader: parameter violation: error #100`, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + args := tc.inputRoleId(t) + got, err := getRoleScopeType(ctx, args.dbReader, args.roleId) + if tc.wantErr { + require.Error(t, err) + require.Equal(t, tc.wantErrMsg, err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, tc.expect, got) + }) + } +} + +func Test_getRoleScope(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + ctx := context.Background() + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + + globalScope := AllocScope() + globalScope.PublicId = globals.GlobalPrefix + require.NoError(t, rw.LookupByPublicId(ctx, &globalScope)) + org, proj := TestScopes(t, repo) + type arg struct { + roleId string + dbReader db.Reader + } + testcases := []struct { + name string + inputRoleId func(t *testing.T) arg + expect *Scope + wantErr bool + wantErrMsg string + }{ + { + name: "valid role global scope", + inputRoleId: func(t *testing.T) arg { + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: grpId, + ScopeId: globals.GlobalPrefix, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeDescendants, + }, + } + require.NoError(t, rw.Create(ctx, r)) + return arg{ + roleId: r.PublicId, + dbReader: rw, + } + }, + expect: &globalScope, + }, + { + name: "valid role org scope", + inputRoleId: func(t *testing.T) arg { + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: grpId, + ScopeId: org.PublicId, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, r)) + return arg{ + roleId: r.PublicId, + dbReader: rw, + } + }, + expect: org, + }, + { + name: "valid role project scope", + inputRoleId: func(t *testing.T) arg { + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: grpId, + ScopeId: proj.PublicId, + }, + } + require.NoError(t, rw.Create(ctx, r)) + return arg{ + roleId: r.PublicId, + dbReader: rw, + } + }, + expect: proj, + }, + { + name: "role does not exist returns error", + inputRoleId: func(t *testing.T) arg { + return arg{ + roleId: "r_123456", + dbReader: rw, + } + }, + wantErr: true, + wantErrMsg: `iam.getRoleScope: role r_123456 not found: search issue: error #1100`, + }, + { + name: "missing role id returns error", + inputRoleId: func(t *testing.T) arg { + return arg{ + roleId: "", + dbReader: rw, + } + }, + wantErr: true, + wantErrMsg: `iam.getRoleScope: missing role id: parameter violation: error #100`, + }, + { + name: "missing db.Reader returns error", + inputRoleId: func(t *testing.T) arg { + return arg{ + roleId: "r_123456", + dbReader: nil, + } + }, + wantErr: true, + wantErrMsg: `iam.getRoleScope: missing db.Reader: parameter violation: error #100`, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + args := tc.inputRoleId(t) + got, err := getRoleScope(ctx, args.dbReader, args.roleId) + if tc.wantErr { + require.Error(t, err) + require.Equal(t, tc.wantErrMsg, err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, tc.expect.String(), got.String()) + }) + } +} diff --git a/internal/iam/repository_scope.go b/internal/iam/repository_scope.go index 5c115e66d3..7b42c23f86 100644 --- a/internal/iam/repository_scope.go +++ b/internal/iam/repository_scope.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam/store" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/boundary/internal/types/resource" @@ -89,13 +90,61 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o var adminRolePublicId string var adminRoleMetadata oplog.Metadata - var adminRole *Role + var adminRole Resource var adminRoleRaw any switch { + case opts.withCreateAdminRole: + adminRolePublicId, err = newRoleId(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating public id for new admin role")) + } + switch s.Type { + case scope.Global.String(): + adminRole = &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: adminRolePublicId, + ScopeId: scopePublicId, + Name: "Administration", + Description: fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", scopePublicId, userId), + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } + case scope.Org.String(): + adminRole = &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: adminRolePublicId, + ScopeId: scopePublicId, + Name: "Administration", + Description: fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", scopePublicId, userId), + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } + case scope.Project.String(): + adminRole = &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: adminRolePublicId, + ScopeId: scopePublicId, + Name: "Administration", + Description: fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", scopePublicId, userId), + }, + } + } + + adminRoleRaw = adminRole + adminRoleMetadata = oplog.Metadata{ + "resource-public-id": []string{adminRolePublicId}, + "scope-id": []string{scopePublicId}, + "scope-type": []string{s.Type}, + "resource-type": []string{resource.Role.String()}, + "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, + } case userId == "", userId == globals.AnonymousUserId, userId == globals.AnyAuthenticatedUserId, userId == globals.RecoveryUserId, + // TODO: This option will be deprecated in 0.22 and will become the default case opts.withSkipAdminRoleCreation: // TODO: Cause a log entry. The repo doesn't have a logger right now, // and ideally we will be using context to pass around log info scoped @@ -107,17 +156,44 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o _ = adminRole default: - adminRole, err = NewRole(ctx, scopePublicId) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error instantiating new admin role")) - } adminRolePublicId, err = newRoleId(ctx) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating public id for new admin role")) } - adminRole.PublicId = adminRolePublicId - adminRole.Name = "Administration" - adminRole.Description = fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", scopePublicId, userId) + switch s.Type { + case scope.Global.String(): + adminRole = &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: adminRolePublicId, + ScopeId: scopePublicId, + Name: "Administration", + Description: fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", scopePublicId, userId), + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } + case scope.Org.String(): + adminRole = &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: adminRolePublicId, + ScopeId: scopePublicId, + Name: "Administration", + Description: fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", scopePublicId, userId), + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } + case scope.Project.String(): + adminRole = &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: adminRolePublicId, + ScopeId: scopePublicId, + Name: "Administration", + Description: fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", scopePublicId, userId), + }, + } + } + adminRoleRaw = adminRole adminRoleMetadata = oplog.Metadata{ "resource-public-id": []string{adminRolePublicId}, @@ -130,25 +206,45 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o var defaultRolePublicId string var defaultRoleMetadata oplog.Metadata - var defaultRole *Role + var defaultRole Resource var defaultRoleRaw any - if !opts.withSkipDefaultRoleCreation { - defaultRole, err = NewRole(ctx, scopePublicId) - if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error instantiating new default role")) - } + if opts.withCreateDefaultRole || !opts.withSkipDefaultRoleCreation { defaultRolePublicId, err = newRoleId(ctx) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error generating public id for new default role")) } - defaultRole.PublicId = defaultRolePublicId switch s.Type { + case scope.Global.String(): + defaultRole = &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: defaultRolePublicId, + ScopeId: scopePublicId, + Name: "Default Grants", + Description: fmt.Sprintf("Role created for login capability, account self-management, and other default grants for users of scope %s at its creation time", scopePublicId), + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } + case scope.Org.String(): + defaultRole = &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: defaultRolePublicId, + ScopeId: scopePublicId, + Name: "Default Grants", + Description: fmt.Sprintf("Role created for login capability, account self-management, and other default grants for users of scope %s at its creation time", scopePublicId), + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } case scope.Project.String(): - defaultRole.Name = "Default Grants" - defaultRole.Description = fmt.Sprintf("Role created to provide default grants to users of scope %s at its creation time", scopePublicId) - default: - defaultRole.Name = "Login and Default Grants" - defaultRole.Description = fmt.Sprintf("Role created for login capability, account self-management, and other default grants for users of scope %s at its creation time", scopePublicId) + defaultRole = &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: defaultRolePublicId, + ScopeId: scopePublicId, + Name: "Default Grants", + Description: fmt.Sprintf("Role created to provide default grants to users of scope %s at its creation time", scopePublicId), + }, + } } defaultRoleRaw = defaultRole defaultRoleMetadata = oplog.Metadata{ @@ -207,8 +303,18 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o return errors.Wrap(ctx, err, op, errors.WithMsg("error creating role")) } - adminRole = adminRoleRaw.(*Role) - + var adminRoleVersion uint32 + switch s.Type { + case scope.Global.String(): + adminRoleVersion = adminRoleRaw.(*globalRole).GetVersion() + adminRole = adminRoleRaw.(*globalRole) + case scope.Org.String(): + adminRoleVersion = adminRoleRaw.(*orgRole).GetVersion() + adminRole = adminRoleRaw.(*orgRole) + case scope.Project.String(): + adminRoleVersion = adminRoleRaw.(*projectRole).GetVersion() + adminRole = adminRoleRaw.(*projectRole) + } msgs := make([]*oplog.Message, 0, 4) roleTicket, err := w.GetTicket(ctx, adminRole) if err != nil { @@ -217,7 +323,7 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o // We need to update the role version as that's the aggregate var roleOplogMsg oplog.Message - rowsUpdated, err := w.Update(ctx, adminRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&adminRole.Version)) + rowsUpdated, err := w.Update(ctx, adminRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&adminRoleVersion)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version for adding grant")) } @@ -237,16 +343,6 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o } msgs = append(msgs, roleGrantOplogMsgs...) - roleGrantScope, err := NewRoleGrantScope(ctx, adminRolePublicId, globals.GrantScopeThis) - if err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant scope")) - } - roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, 1) - if err := w.CreateItems(ctx, []*RoleGrantScope{roleGrantScope}, db.NewOplogMsgs(&roleGrantScopeOplogMsgs)); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add grant scope")) - } - msgs = append(msgs, roleGrantScopeOplogMsgs...) - rolePrincipal, err := NewUserRole(ctx, adminRolePublicId, userId) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role user")) @@ -261,7 +357,7 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, "scope-id": []string{s.PublicId}, "scope-type": []string{s.Type}, - "resource-public-id": []string{adminRole.PublicId}, + "resource-public-id": []string{adminRole.GetPublicId()}, } if err := w.WriteOplogEntryWith(ctx, childOplogWrapper, roleTicket, metadata, msgs); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) @@ -280,7 +376,15 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o return errors.Wrap(ctx, err, op, errors.WithMsg("error creating role")) } - defaultRole = defaultRoleRaw.(*Role) + var defaultRoleVersion uint32 + switch s.Type { + case scope.Global.String(): + defaultRoleVersion = defaultRoleRaw.(*globalRole).GetVersion() + case scope.Org.String(): + defaultRoleVersion = defaultRoleRaw.(*orgRole).GetVersion() + case scope.Project.String(): + defaultRoleVersion = defaultRoleRaw.(*projectRole).GetVersion() + } msgs := make([]*oplog.Message, 0, 7) roleTicket, err := w.GetTicket(ctx, defaultRole) @@ -290,7 +394,7 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o // We need to update the role version as that's the aggregate var roleOplogMsg oplog.Message - rowsUpdated, err := w.Update(ctx, defaultRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&defaultRole.Version)) + rowsUpdated, err := w.Update(ctx, defaultRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&defaultRoleVersion)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version for adding grant")) } @@ -370,21 +474,11 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o msgs = append(msgs, roleUserOplogMsgs...) } - roleGrantScope, err := NewRoleGrantScope(ctx, defaultRolePublicId, globals.GrantScopeThis) - if err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant scope")) - } - roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, 1) - if err := w.CreateItems(ctx, []*RoleGrantScope{roleGrantScope}, db.NewOplogMsgs(&roleGrantScopeOplogMsgs)); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add grant scope")) - } - msgs = append(msgs, roleGrantScopeOplogMsgs...) - metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, "scope-id": []string{s.PublicId}, "scope-type": []string{s.Type}, - "resource-public-id": []string{defaultRole.PublicId}, + "resource-public-id": []string{defaultRole.GetPublicId()}, } if err := w.WriteOplogEntryWith(ctx, childOplogWrapper, roleTicket, metadata, msgs); err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) diff --git a/internal/iam/resource.go b/internal/iam/resource.go index 11590231dd..8016edc508 100644 --- a/internal/iam/resource.go +++ b/internal/iam/resource.go @@ -49,7 +49,7 @@ type Cloneable interface { type ResourceWithScope interface { GetPublicId() string GetScopeId() string - validScopeTypes() []scope.Type + getResourceType() resource.Type } // LookupScope looks up the resource's scope @@ -98,7 +98,11 @@ func validateScopeForWrite(ctx context.Context, r db.Reader, resource ResourceWi return errors.Wrap(ctx, err, op) } validScopeType := false - for _, t := range resource.validScopeTypes() { + validScopes, err := scope.AllowedIn(ctx, resource.getResourceType()) + if err != nil { + return errors.Wrap(ctx, err, op) + } + for _, t := range validScopes { if ps.Type == t.String() { validScopeType = true } diff --git a/internal/iam/role.go b/internal/iam/role.go index ed508eb207..0fcaaad786 100644 --- a/internal/iam/role.go +++ b/internal/iam/role.go @@ -10,28 +10,96 @@ import ( "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam/store" + "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" - "github.com/hashicorp/boundary/internal/types/scope" "google.golang.org/protobuf/proto" ) const ( - defaultRoleTableName = "iam_role" + defaultRoleTableName = "iam_role" + defaultGlobalRoleTableName = "iam_role_global" + defaultOrgRoleTableName = "iam_role_org" + defaultProjectRoleTableName = "iam_role_project" ) // Roles are granted permissions and assignable to Users and Groups. type Role struct { - *store.Role + PublicId string + ScopeId string + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 GrantScopes []*RoleGrantScope `gorm:"-"` - tableName string `gorm:"-"` +} + +func (role *Role) GetPublicId() string { + if role == nil { + return "" + } + return role.PublicId +} + +func (role *Role) GetScopeId() string { + if role == nil { + return "" + } + return role.ScopeId +} + +func (role *Role) GetName() string { + if role == nil { + return "" + } + return role.Name +} + +func (role *Role) GetDescription() string { + if role == nil { + return "" + } + return role.Description +} + +func (role *Role) GetCreateTime() *timestamp.Timestamp { + if role == nil { + return nil + } + return role.CreateTime +} + +func (role *Role) GetUpdateTime() *timestamp.Timestamp { + if role == nil { + return nil + } + return role.UpdateTime +} + +func (role *Role) GetVersion() uint32 { + if role == nil { + return 0 + } + return role.Version } // ensure that Role implements the interfaces of: Resource, Cloneable, and db.VetForWriter. var ( - _ Resource = (*Role)(nil) - _ Cloneable = (*Role)(nil) - _ db.VetForWriter = (*Role)(nil) + _ Resource = (*globalRole)(nil) + _ Cloneable = (*globalRole)(nil) + _ db.VetForWriter = (*globalRole)(nil) + _ oplog.ReplayableMessage = (*globalRole)(nil) + + _ Resource = (*orgRole)(nil) + _ Cloneable = (*orgRole)(nil) + _ db.VetForWriter = (*orgRole)(nil) + _ oplog.ReplayableMessage = (*orgRole)(nil) + + _ Resource = (*projectRole)(nil) + _ Cloneable = (*projectRole)(nil) + _ db.VetForWriter = (*projectRole)(nil) + _ oplog.ReplayableMessage = (*projectRole)(nil) ) // NewRole creates a new in memory role with a scope (project/org) @@ -43,33 +111,13 @@ func NewRole(ctx context.Context, scopeId string, opt ...Option) (*Role, error) } opts := getOpts(opt...) r := &Role{ - Role: &store.Role{ - ScopeId: scopeId, - Name: opts.withName, - Description: opts.withDescription, - }, + ScopeId: scopeId, + Name: opts.withName, + Description: opts.withDescription, } return r, nil } -func allocRole() Role { - return Role{ - Role: &store.Role{}, - } -} - -// Clone creates a clone of the Role. -func (role *Role) Clone() any { - cp := proto.Clone(role.Role) - ret := &Role{ - Role: cp.(*store.Role), - } - for _, grantScope := range role.GrantScopes { - ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) - } - return ret -} - // VetForWrite implements db.VetForWrite() interface. func (role *Role) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { const op = "iam.(Role).VetForWrite" @@ -82,8 +130,8 @@ func (role *Role) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType return nil } -func (role *Role) validScopeTypes() []scope.Type { - return []scope.Type{scope.Global, scope.Org, scope.Project} +func (role *Role) getResourceType() resource.Type { + return resource.Role } // GetScope returns the scope for the Role. @@ -106,21 +154,6 @@ func (*Role) Actions() map[string]action.Type { return ret } -// TableName returns the tablename to override the default gorm table name. -func (role *Role) TableName() string { - if role.tableName != "" { - return role.tableName - } - return defaultRoleTableName -} - -// SetTableName sets the tablename and satisfies the ReplayableMessage -// interface. If the caller attempts to set the name to "" the name will be -// reset to the default name. -func (role *Role) SetTableName(n string) { - role.tableName = n -} - type deletedRole struct { PublicId string `gorm:"primary_key"` DeleteTime *timestamp.Timestamp @@ -130,3 +163,249 @@ type deletedRole struct { func (s *deletedRole) TableName() string { return "iam_role_deleted" } + +// globalRole is a type embedding store.GlobalRole used to interact with iam_role_global table which contains +// all iam_role entries that are created in global-level scopes through gorm. +type globalRole struct { + *store.GlobalRole + GrantScopes []*RoleGrantScope `gorm:"-"` + tableName string `gorm:"-"` +} + +func (g *globalRole) TableName() string { + if g.tableName != "" { + return g.tableName + } + return defaultGlobalRoleTableName +} + +func (g *globalRole) SetTableName(n string) { + g.tableName = n +} + +func (g *globalRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + const op = "iam.(globalRole).VetForWrite" + if g.PublicId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing public id") + } + if err := validateScopeForWrite(ctx, r, g, opType, opt...); err != nil { + return errors.Wrap(ctx, err, op) + } + return nil +} + +func allocGlobalRole() globalRole { + return globalRole{ + GlobalRole: &store.GlobalRole{}, + } +} + +func (g *globalRole) Clone() any { + cp := proto.Clone(g.GlobalRole) + ret := &globalRole{ + GlobalRole: cp.(*store.GlobalRole), + } + for _, grantScope := range g.GrantScopes { + ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) + } + return ret +} + +func (g *globalRole) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { + return LookupScope(ctx, r, g) +} +func (g *globalRole) GetResourceType() resource.Type { return resource.Role } +func (g *globalRole) getResourceType() resource.Type { return resource.Role } +func (g *globalRole) Actions() map[string]action.Type { + ret := CrudlActions() + ret[action.AddGrants.String()] = action.AddGrants + ret[action.RemoveGrants.String()] = action.RemoveGrants + ret[action.SetGrants.String()] = action.SetGrants + ret[action.AddPrincipals.String()] = action.AddPrincipals + ret[action.RemovePrincipals.String()] = action.RemovePrincipals + ret[action.SetPrincipals.String()] = action.SetPrincipals + return ret +} + +func (g *globalRole) toRole() *Role { + if g == nil { + return nil + } + ret := &Role{ + PublicId: g.GetPublicId(), + ScopeId: g.GetScopeId(), + Name: g.GetName(), + Description: g.GetDescription(), + CreateTime: g.GetCreateTime(), + UpdateTime: g.GetUpdateTime(), + Version: g.GetVersion(), + } + for _, grantScope := range g.GrantScopes { + ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) + } + return ret +} + +// orgRole is a type embedding store.OrgRole used to interact with iam_role_org table which contains +// all iam_role entries that are created in org-level scopes through gorm. +type orgRole struct { + *store.OrgRole + GrantScopes []*RoleGrantScope `gorm:"-"` + tableName string `gorm:"-"` +} + +func (o *orgRole) TableName() string { + if o.tableName != "" { + return o.tableName + } + return defaultOrgRoleTableName +} + +func (o *orgRole) SetTableName(n string) { + o.tableName = n +} + +func (o *orgRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + const op = "iam.(orgRole).VetForWrite" + if o.PublicId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing public id") + } + if err := validateScopeForWrite(ctx, r, o, opType, opt...); err != nil { + return errors.Wrap(ctx, err, op) + } + return nil +} + +func allocOrgRole() orgRole { + return orgRole{ + OrgRole: &store.OrgRole{}, + } +} + +func (o *orgRole) Clone() any { + cp := proto.Clone(o.OrgRole) + ret := &orgRole{ + OrgRole: cp.(*store.OrgRole), + } + for _, grantScope := range o.GrantScopes { + ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) + } + return ret +} + +func (o *orgRole) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { + return LookupScope(ctx, r, o) +} +func (o *orgRole) GetResourceType() resource.Type { return resource.Role } +func (o *orgRole) getResourceType() resource.Type { return resource.Role } +func (o *orgRole) Actions() map[string]action.Type { + ret := CrudlActions() + ret[action.AddGrants.String()] = action.AddGrants + ret[action.RemoveGrants.String()] = action.RemoveGrants + ret[action.SetGrants.String()] = action.SetGrants + ret[action.AddPrincipals.String()] = action.AddPrincipals + ret[action.RemovePrincipals.String()] = action.RemovePrincipals + ret[action.SetPrincipals.String()] = action.SetPrincipals + return ret +} + +func (o *orgRole) toRole() *Role { + if o == nil { + return nil + } + ret := &Role{ + PublicId: o.GetPublicId(), + ScopeId: o.GetScopeId(), + Name: o.GetName(), + Description: o.GetDescription(), + CreateTime: o.GetCreateTime(), + UpdateTime: o.GetUpdateTime(), + Version: o.GetVersion(), + } + for _, grantScope := range o.GrantScopes { + ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) + } + return ret +} + +// projectRole is a type embedding store.ProjectRole used to interact with iam_role_project table which contains +// all iam_role entries that are created in project-level scopes through gorm. +type projectRole struct { + *store.ProjectRole + GrantScopes []*RoleGrantScope `gorm:"-"` + tableName string `gorm:"-"` +} + +func (p *projectRole) TableName() string { + if p.tableName != "" { + return p.tableName + } + return defaultProjectRoleTableName +} + +func (p *projectRole) SetTableName(n string) { + p.tableName = n +} + +func (p *projectRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + const op = "iam.(projectRole).VetForWrite" + if p.PublicId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing public id") + } + if err := validateScopeForWrite(ctx, r, p, opType, opt...); err != nil { + return errors.Wrap(ctx, err, op) + } + return nil +} + +func allocProjectRole() projectRole { + return projectRole{ + ProjectRole: &store.ProjectRole{}, + } +} + +func (p *projectRole) Clone() any { + cp := proto.Clone(p.ProjectRole) + ret := &projectRole{ + ProjectRole: cp.(*store.ProjectRole), + } + for _, grantScope := range p.GrantScopes { + ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) + } + return ret +} + +func (p *projectRole) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { + return LookupScope(ctx, r, p) +} +func (p *projectRole) GetResourceType() resource.Type { return resource.Role } +func (p *projectRole) getResourceType() resource.Type { return resource.Role } +func (p *projectRole) Actions() map[string]action.Type { + ret := CrudlActions() + ret[action.AddGrants.String()] = action.AddGrants + ret[action.RemoveGrants.String()] = action.RemoveGrants + ret[action.SetGrants.String()] = action.SetGrants + ret[action.AddPrincipals.String()] = action.AddPrincipals + ret[action.RemovePrincipals.String()] = action.RemovePrincipals + ret[action.SetPrincipals.String()] = action.SetPrincipals + return ret +} + +func (p *projectRole) toRole() *Role { + if p == nil { + return nil + } + ret := &Role{ + PublicId: p.GetPublicId(), + ScopeId: p.GetScopeId(), + Name: p.GetName(), + Description: p.GetDescription(), + CreateTime: p.GetCreateTime(), + UpdateTime: p.GetUpdateTime(), + Version: p.GetVersion(), + } + for _, grantScope := range p.GrantScopes { + ret.GrantScopes = append(ret.GrantScopes, grantScope.Clone().(*RoleGrantScope)) + } + return ret +} diff --git a/internal/iam/role_grant_scope.go b/internal/iam/role_grant_scope.go index 7b058225a1..020ac519ca 100644 --- a/internal/iam/role_grant_scope.go +++ b/internal/iam/role_grant_scope.go @@ -6,30 +6,72 @@ package iam import ( "context" "fmt" + "strings" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam/store" + "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" "google.golang.org/protobuf/proto" ) -const defaultRoleGrantScopeTable = "iam_role_grant_scope" - -// RoleGrantScope defines the grant scopes that are assigned to a role -type RoleGrantScope struct { - *store.RoleGrantScope - tableName string `gorm:"-"` -} +const ( + defaultRoleGrantScopeTable = "iam_role_grant_scope" + defaultGlobalRoleIndividualOrgGrantScopeTable = "iam_role_global_individual_org_grant_scope" + defaultGlobalRoleIndividualProjectGrantScopeTable = "iam_role_global_individual_project_grant_scope" + defaultOrgRoleIndividualGrantScopeTable = "iam_role_org_individual_grant_scope" +) // ensure that RoleGrantScope implements the interfaces of: Cloneable and db.VetForWriter var ( _ Cloneable = (*RoleGrantScope)(nil) _ db.VetForWriter = (*RoleGrantScope)(nil) + + _ Cloneable = (*globalRoleIndividualOrgGrantScope)(nil) + _ db.VetForWriter = (*globalRoleIndividualOrgGrantScope)(nil) + _ oplog.ReplayableMessage = (*globalRoleIndividualOrgGrantScope)(nil) + + _ Cloneable = (*globalRoleIndividualProjectGrantScope)(nil) + _ db.VetForWriter = (*globalRoleIndividualProjectGrantScope)(nil) + _ oplog.ReplayableMessage = (*globalRoleIndividualProjectGrantScope)(nil) + + _ Cloneable = (*orgRoleIndividualGrantScope)(nil) + _ db.VetForWriter = (*orgRoleIndividualGrantScope)(nil) + _ oplog.ReplayableMessage = (*orgRoleIndividualGrantScope)(nil) ) +// RoleGrantScope defines the grant scopes that are assigned to a role +type RoleGrantScope struct { + CreateTime *timestamp.Timestamp + RoleId string + ScopeIdOrSpecial string +} + +func (r *RoleGrantScope) GetCreateTime() *timestamp.Timestamp { + if r == nil { + return nil + } + return r.CreateTime +} + +func (r *RoleGrantScope) GetRoleId() string { + if r == nil { + return "" + } + return r.RoleId +} + +func (r *RoleGrantScope) GetScopeIdOrSpecial() string { + if r == nil { + return "" + } + return r.ScopeIdOrSpecial +} + // NewRoleGrantScope creates a new in memory role grant scope. No options are // supported. func NewRoleGrantScope(ctx context.Context, roleId string, grantScope string, _ ...Option) (*RoleGrantScope, error) { @@ -48,28 +90,19 @@ func NewRoleGrantScope(ctx context.Context, roleId string, grantScope string, _ default: return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown grant scope id %q", grantScope)) } - rgs := &RoleGrantScope{ - RoleGrantScope: &store.RoleGrantScope{ - RoleId: roleId, - ScopeIdOrSpecial: grantScope, - }, + RoleId: roleId, + ScopeIdOrSpecial: grantScope, } - return rgs, nil } -func allocRoleGrantScope() RoleGrantScope { - return RoleGrantScope{ - RoleGrantScope: &store.RoleGrantScope{}, - } -} - // Clone creates a clone of the RoleGrantScope func (g *RoleGrantScope) Clone() any { - cp := proto.Clone(g.RoleGrantScope) return &RoleGrantScope{ - RoleGrantScope: cp.(*store.RoleGrantScope), + CreateTime: g.CreateTime, + RoleId: g.RoleId, + ScopeIdOrSpecial: g.ScopeIdOrSpecial, } } @@ -96,17 +129,122 @@ func (g *RoleGrantScope) VetForWrite(ctx context.Context, _ db.Reader, _ db.OpTy return nil } -// TableName returns the tablename to override the default gorm table name -func (g *RoleGrantScope) TableName() string { +// globalRoleIndividualOrgGrantScope defines the grant org scopes +// that are assigned to a global role +type globalRoleIndividualOrgGrantScope struct { + *store.GlobalRoleIndividualOrgGrantScope + tableName string `gorm:"-"` +} + +func (g *globalRoleIndividualOrgGrantScope) TableName() string { + if g.tableName != "" { + return g.tableName + } + return defaultGlobalRoleIndividualOrgGrantScopeTable +} + +func (g *globalRoleIndividualOrgGrantScope) SetTableName(name string) { + g.tableName = name +} + +func (g *globalRoleIndividualOrgGrantScope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + const op = "iam.(GlobalRoleIndividualOrgGrantScope).VetForWrite" + if g.RoleId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing role id") + } + if g.ScopeId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if globals.ResourceInfoFromPrefix(g.ScopeId).Type != resource.Scope && + strings.HasPrefix(g.String(), globals.OrgPrefix) { + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid scope ID %s", g.ScopeId)) + } + return nil +} + +func (g *globalRoleIndividualOrgGrantScope) Clone() any { + cp := proto.Clone(g.GlobalRoleIndividualOrgGrantScope) + return &globalRoleIndividualOrgGrantScope{ + GlobalRoleIndividualOrgGrantScope: cp.(*store.GlobalRoleIndividualOrgGrantScope), + } +} + +// globalRoleIndividualProjectGrantScope defines the grant project scopes +// that are assigned to a global role +type globalRoleIndividualProjectGrantScope struct { + *store.GlobalRoleIndividualProjectGrantScope + tableName string `gorm:"-"` +} + +func (g *globalRoleIndividualProjectGrantScope) TableName() string { + if g.tableName != "" { + return g.tableName + } + return defaultGlobalRoleIndividualProjectGrantScopeTable +} + +func (g *globalRoleIndividualProjectGrantScope) SetTableName(name string) { + g.tableName = name +} + +func (g *globalRoleIndividualProjectGrantScope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + const op = "iam.(GlobalRoleIndividualProjectGrantScope).VetForWrite" + if g.RoleId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing role id") + } + if g.ScopeId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if globals.ResourceInfoFromPrefix(g.ScopeId).Type != resource.Scope && + strings.HasPrefix(g.String(), globals.ProjectPrefix) { + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid scope ID %s", g.ScopeId)) + } + return nil +} + +func (g *globalRoleIndividualProjectGrantScope) Clone() any { + cp := proto.Clone(g.GlobalRoleIndividualProjectGrantScope) + return &globalRoleIndividualProjectGrantScope{ + GlobalRoleIndividualProjectGrantScope: cp.(*store.GlobalRoleIndividualProjectGrantScope), + } +} + +// OrgRoleIndividualGrantScope defines the grant project scopes +// that are assigned to an org role +type orgRoleIndividualGrantScope struct { + *store.OrgRoleIndividualGrantScope + tableName string `gorm:"-"` +} + +func (g *orgRoleIndividualGrantScope) TableName() string { if g.tableName != "" { return g.tableName } - return defaultRoleGrantScopeTable + return defaultOrgRoleIndividualGrantScopeTable +} + +func (g *orgRoleIndividualGrantScope) SetTableName(name string) { + g.tableName = name } -// SetTableName sets the tablename and satisfies the ReplayableMessage -// interface. If the caller attempts to set the name to "" the name will be -// reset to the default name. -func (g *RoleGrantScope) SetTableName(n string) { - g.tableName = n +func (g *orgRoleIndividualGrantScope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + const op = "iam.(OrgRoleIndividualGrantScope).VetForWrite" + if g.RoleId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing role id") + } + if g.ScopeId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing scope id") + } + if globals.ResourceInfoFromPrefix(g.ScopeId).Type != resource.Scope && + strings.HasPrefix(g.String(), globals.ProjectPrefix) { + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid scope ID %s", g.ScopeId)) + } + return nil +} + +func (g *orgRoleIndividualGrantScope) Clone() any { + cp := proto.Clone(g.OrgRoleIndividualGrantScope) + return &orgRoleIndividualGrantScope{ + OrgRoleIndividualGrantScope: cp.(*store.OrgRoleIndividualGrantScope), + } } diff --git a/internal/iam/role_grant_scope_test.go b/internal/iam/role_grant_scope_test.go new file mode 100644 index 0000000000..bf4cfe5585 --- /dev/null +++ b/internal/iam/role_grant_scope_test.go @@ -0,0 +1,416 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package iam + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam/store" + "github.com/stretchr/testify/require" +) + +func Test_globalRoleIndividualOrgGrantScope(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + iamRepo := TestRepo(t, conn, wrap) + rw := db.New(conn) + org, proj := TestScopes(t, iamRepo) + testcases := []struct { + name string + setup func(t *testing.T) *globalRoleIndividualOrgGrantScope + wantErr bool + wantErrMsg string + }{ + { + name: "happy path grant scope individual", + setup: func(t *testing.T) *globalRoleIndividualOrgGrantScope { + r := TestRole(t, conn, globals.GlobalPrefix) + return &globalRoleIndividualOrgGrantScope{ + GlobalRoleIndividualOrgGrantScope: &store.GlobalRoleIndividualOrgGrantScope{ + RoleId: r.PublicId, + ScopeId: org.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: false, + }, + { + name: "error only individual is allowed in grant_scope", + setup: func(t *testing.T) *globalRoleIndividualOrgGrantScope { + r := TestRole(t, conn, globals.GlobalPrefix) + gRole := allocGlobalRole() + gRole.PublicId = r.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &gRole)) + + gRole.GrantScope = globals.GrantScopeChildren + updated, err := rw.Update(ctx, &gRole, []string{"GrantScope"}, []string{}) + require.NoError(t, err) + require.Equal(t, 1, updated) + return &globalRoleIndividualOrgGrantScope{ + GlobalRoleIndividualOrgGrantScope: &store.GlobalRoleIndividualOrgGrantScope{ + RoleId: r.PublicId, + ScopeId: org.PublicId, + GrantScope: globals.GrantScopeChildren, + }, + } + }, + wantErr: true, + wantErrMsg: `db.Create: only_individual_grant_scope_allowed constraint failed: check constraint violated: integrity violation: error #1000`, + }, + { + name: "error mismatch grant_scope", + setup: func(t *testing.T) *globalRoleIndividualOrgGrantScope { + r := TestRole(t, conn, globals.GlobalPrefix) + gRole := allocGlobalRole() + gRole.PublicId = r.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &gRole)) + return &globalRoleIndividualOrgGrantScope{ + GlobalRoleIndividualOrgGrantScope: &store.GlobalRoleIndividualOrgGrantScope{ + RoleId: r.PublicId, + ScopeId: org.PublicId, + GrantScope: globals.GrantScopeChildren, + }, + } + }, + wantErr: true, + wantErrMsg: `db.Create: only_individual_grant_scope_allowed constraint failed: check constraint violated: integrity violation: error #1000`, + }, + { + name: "error trying to add project grant scope", + setup: func(t *testing.T) *globalRoleIndividualOrgGrantScope { + r := TestRole(t, conn, globals.GlobalPrefix) + return &globalRoleIndividualOrgGrantScope{ + GlobalRoleIndividualOrgGrantScope: &store.GlobalRoleIndividualOrgGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_scope_org_fkey": integrity violation: error #1003`, + }, + { + name: "error cannot add GlobalRoleIndividualOrgGrantScope for org role", + setup: func(t *testing.T) *globalRoleIndividualOrgGrantScope { + r := TestRole(t, conn, org.PublicId) + return &globalRoleIndividualOrgGrantScope{ + GlobalRoleIndividualOrgGrantScope: &store.GlobalRoleIndividualOrgGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_role_global_fkey": integrity violation: error #1003`, + }, + { + name: "error cannot add GlobalRoleIndividualOrgGrantScope for proj role", + setup: func(t *testing.T) *globalRoleIndividualOrgGrantScope { + r := TestRole(t, conn, proj.PublicId) + return &globalRoleIndividualOrgGrantScope{ + GlobalRoleIndividualOrgGrantScope: &store.GlobalRoleIndividualOrgGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_global_individual_org_grant_scope" violates foreign key constraint "iam_role_global_fkey": integrity violation: error #1003`, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + grantScope := tc.setup(t) + err := rw.Create(ctx, grantScope) + if tc.wantErr { + require.Error(t, err) + require.ErrorContains(t, err, tc.wantErrMsg) + return + } + require.NoError(t, err) + }) + } +} + +func Test_GlobalRoleIndividualProjectGrantScope(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + iamRepo := TestRepo(t, conn, wrap) + rw := db.New(conn) + org, proj := TestScopes(t, iamRepo) + testcases := []struct { + name string + setup func(t *testing.T) *globalRoleIndividualProjectGrantScope + wantErr bool + wantErrMsg string + }{ + { + name: "happy path grant scope individual", + setup: func(t *testing.T) *globalRoleIndividualProjectGrantScope { + r := TestRole(t, conn, globals.GlobalPrefix) + return &globalRoleIndividualProjectGrantScope{ + GlobalRoleIndividualProjectGrantScope: &store.GlobalRoleIndividualProjectGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: false, + }, + { + name: "happy path grant_scope children is allowed", + setup: func(t *testing.T) *globalRoleIndividualProjectGrantScope { + r := TestRole(t, conn, globals.GlobalPrefix) + gRole := allocGlobalRole() + gRole.PublicId = r.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &gRole)) + + gRole.GrantScope = globals.GrantScopeChildren + updated, err := rw.Update(ctx, &gRole, []string{"GrantScope"}, []string{}) + require.NoError(t, err) + require.Equal(t, 1, updated) + return &globalRoleIndividualProjectGrantScope{ + GlobalRoleIndividualProjectGrantScope: &store.GlobalRoleIndividualProjectGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeChildren, + }, + } + }, + wantErr: false, + }, + { + name: "error mismatch grant_scope", + setup: func(t *testing.T) *globalRoleIndividualProjectGrantScope { + r := TestRole(t, conn, globals.GlobalPrefix) + return &globalRoleIndividualProjectGrantScope{ + GlobalRoleIndividualProjectGrantScope: &store.GlobalRoleIndividualProjectGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeChildren, + }, + } + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_role_global_grant_scope_fkey": integrity violation: error #1003`, + }, + { + name: "error trying to add org grant scope", + setup: func(t *testing.T) *globalRoleIndividualProjectGrantScope { + r := TestRole(t, conn, globals.GlobalPrefix) + return &globalRoleIndividualProjectGrantScope{ + GlobalRoleIndividualProjectGrantScope: &store.GlobalRoleIndividualProjectGrantScope{ + RoleId: r.PublicId, + ScopeId: org.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_scope_project_fkey": integrity violation: error #1003`, + }, + { + name: "error cannot add GlobalRoleIndividualProjectGrantScope for org role", + setup: func(t *testing.T) *globalRoleIndividualProjectGrantScope { + r := TestRole(t, conn, org.PublicId) + return &globalRoleIndividualProjectGrantScope{ + GlobalRoleIndividualProjectGrantScope: &store.GlobalRoleIndividualProjectGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_role_global_fkey": integrity violation: error #1003`, + }, + { + name: "error cannot add GlobalRoleIndividualProjectGrantScope for proj role", + setup: func(t *testing.T) *globalRoleIndividualProjectGrantScope { + r := TestRole(t, conn, proj.PublicId) + return &globalRoleIndividualProjectGrantScope{ + GlobalRoleIndividualProjectGrantScope: &store.GlobalRoleIndividualProjectGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_global_individual_project_grant_scope" violates foreign key constraint "iam_role_global_fkey": integrity violation: error #1003`, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + grantScope := tc.setup(t) + err := rw.Create(ctx, grantScope) + if tc.wantErr { + require.Error(t, err) + require.ErrorContains(t, err, tc.wantErrMsg) + return + } + require.NoError(t, err) + }) + } +} + +func Test_OrgRoleIndividualGrantScope(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrap := db.TestWrapper(t) + iamRepo := TestRepo(t, conn, wrap) + rw := db.New(conn) + org, proj := TestScopes(t, iamRepo) + _, proj2 := TestScopes(t, iamRepo) + testcases := []struct { + name string + setup func(t *testing.T) *orgRoleIndividualGrantScope + wantErr bool + wantErrMsg string + }{ + { + name: "happy path grant scope individual", + setup: func(t *testing.T) *orgRoleIndividualGrantScope { + r := TestRole(t, conn, org.PublicId) + return &orgRoleIndividualGrantScope{ + OrgRoleIndividualGrantScope: &store.OrgRoleIndividualGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: false, + }, + { + name: "error only individual is allowed in grant_scope", + setup: func(t *testing.T) *orgRoleIndividualGrantScope { + r := TestRole(t, conn, org.PublicId) + gRole := allocOrgRole() + gRole.PublicId = r.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &gRole)) + + gRole.GrantScope = globals.GrantScopeChildren + updated, err := rw.Update(ctx, &gRole, []string{"GrantScope"}, []string{}) + require.NoError(t, err) + require.Equal(t, 1, updated) + return &orgRoleIndividualGrantScope{ + OrgRoleIndividualGrantScope: &store.OrgRoleIndividualGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeChildren, + }, + } + }, + wantErr: true, + wantErrMsg: "db.Create: only_individual_grant_scope_allowed constraint failed: check constraint violated: integrity violation: error #1000", + }, + { + name: "error only iam_role_org.grant_scope individual is allowed in grant_scope", + setup: func(t *testing.T) *orgRoleIndividualGrantScope { + r := TestRole(t, conn, org.PublicId) + gRole := allocOrgRole() + gRole.PublicId = r.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &gRole)) + + gRole.GrantScope = globals.GrantScopeChildren + updated, err := rw.Update(ctx, &gRole, []string{"GrantScope"}, []string{}) + require.NoError(t, err) + require.Equal(t, 1, updated) + return &orgRoleIndividualGrantScope{ + OrgRoleIndividualGrantScope: &store.OrgRoleIndividualGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_org_individual_grant_scope" violates foreign key constraint "iam_role_org_grant_scope_fkey": integrity violation: error #1003`, + }, + { + name: "error mismatch grant_scope", + setup: func(t *testing.T) *orgRoleIndividualGrantScope { + r := TestRole(t, conn, org.PublicId) + return &orgRoleIndividualGrantScope{ + OrgRoleIndividualGrantScope: &store.OrgRoleIndividualGrantScope{ + RoleId: r.PublicId, + ScopeId: proj.PublicId, + GrantScope: globals.GrantScopeChildren, + }, + } + }, + wantErr: true, + wantErrMsg: `db.Create: only_individual_grant_scope_allowed constraint failed: check constraint violated: integrity violation: error #1000`, + }, + { + name: "error trying to add org scope to grant_scope", + setup: func(t *testing.T) *orgRoleIndividualGrantScope { + r := TestRole(t, conn, org.PublicId) + return &orgRoleIndividualGrantScope{ + OrgRoleIndividualGrantScope: &store.OrgRoleIndividualGrantScope{ + RoleId: r.PublicId, + ScopeId: org.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: true, + wantErrMsg: fmt.Sprintf("db.Create: project scope_id %s not found in org: integrity violation: error #1104", org.PublicId), + }, + { + name: "error cannot add proj not belong to org", + setup: func(t *testing.T) *orgRoleIndividualGrantScope { + r := TestRole(t, conn, org.PublicId) + return &orgRoleIndividualGrantScope{ + OrgRoleIndividualGrantScope: &store.OrgRoleIndividualGrantScope{ + RoleId: r.PublicId, + ScopeId: org.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: true, + wantErrMsg: fmt.Sprintf("db.Create: project scope_id %s not found in org: integrity violation: error #1104", org.PublicId), + }, + { + name: "error cannot add proj not belong to org", + setup: func(t *testing.T) *orgRoleIndividualGrantScope { + r := TestRole(t, conn, org.PublicId) + return &orgRoleIndividualGrantScope{ + OrgRoleIndividualGrantScope: &store.OrgRoleIndividualGrantScope{ + RoleId: r.PublicId, + ScopeId: proj2.PublicId, + GrantScope: globals.GrantScopeIndividual, + }, + } + }, + wantErr: true, + wantErrMsg: fmt.Sprintf("db.Create: project scope_id %s not found in org: integrity violation: error #1104", proj2.PublicId), + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + grantScope := tc.setup(t) + err := rw.Create(ctx, grantScope) + if tc.wantErr { + require.Error(t, err) + require.ErrorContains(t, err, tc.wantErrMsg) + return + } + require.NoError(t, err) + }) + } +} diff --git a/internal/iam/role_test.go b/internal/iam/role_test.go index b0e0993146..bd7df70bdc 100644 --- a/internal/iam/role_test.go +++ b/internal/iam/role_test.go @@ -9,11 +9,11 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/db" - dbassert "github.com/hashicorp/boundary/internal/db/assert" + "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam/store" - "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/stretchr/testify/assert" @@ -99,437 +99,693 @@ func TestNewRole(t *testing.T) { } } -func Test_RoleCreate(t *testing.T) { +func TestRole_Actions(t *testing.T) { + assert := assert.New(t) + r := &Role{} + a := r.Actions() + assert.Equal(a[action.Create.String()], action.Create) + assert.Equal(a[action.Update.String()], action.Update) + assert.Equal(a[action.Read.String()], action.Read) + assert.Equal(a[action.Delete.String()], action.Delete) + assert.Equal(a[action.AddGrants.String()], action.AddGrants) + assert.Equal(a[action.RemoveGrants.String()], action.RemoveGrants) + assert.Equal(a[action.SetGrants.String()], action.SetGrants) + assert.Equal(a[action.AddPrincipals.String()], action.AddPrincipals) + assert.Equal(a[action.RemovePrincipals.String()], action.RemovePrincipals) + assert.Equal(a[action.SetPrincipals.String()], action.SetPrincipals) +} + +func TestRole_ResourceType(t *testing.T) { + assert := assert.New(t) + r := &Role{} + ty := r.GetResourceType() + assert.Equal(ty, resource.Role) +} + +func TestRole_GetScope(t *testing.T) { t.Parallel() - ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) repo := TestRepo(t, conn, wrapper) org, proj := TestScopes(t, repo) + + t.Run("valid-org", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w := db.New(conn) + role := TestRole(t, conn, org.PublicId) + scope, err := role.GetScope(context.Background(), w) + require.NoError(err) + assert.True(proto.Equal(org, scope)) + }) + t.Run("valid-proj", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w := db.New(conn) + role := TestRole(t, conn, proj.PublicId) + scope, err := role.GetScope(context.Background(), w) + require.NoError(err) + assert.True(proto.Equal(proj, scope)) + }) +} + +func Test_globalRole_Create(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") type args struct { - role *Role + role *globalRole } + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + org, proj := TestScopes(t, repo) + rw := db.New(conn) tests := []struct { - name string - args args - wantDup bool - wantErr bool - wantErrMsg string - wantIsError error + name string + args args + wantErr bool + wantErrMsg string }{ { - name: "valid-with-org", + name: "can create valid role in global grants scope individual", args: args{ - role: func() *Role { - id := testId(t) - role, err := NewRole(ctx, org.PublicId, WithName(id), WithDescription("description-"+id)) - require.NoError(t, err) + role: func() *globalRole { + randomUuid := testId(t) grpId, err := newRoleId(ctx) require.NoError(t, err) - role.PublicId = grpId - return role + r := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: grpId, + ScopeId: globals.GlobalPrefix, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + return r }(), }, wantErr: false, }, { - name: "valid-with-proj", + name: "can create valid role in global grants scope descendants", args: args{ - role: func() *Role { - id := testId(t) - role, err := NewRole(ctx, proj.PublicId, WithName(id), WithDescription("description-"+id)) + role: func() *globalRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) require.NoError(t, err) + r := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: grpId, + ScopeId: globals.GlobalPrefix, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeDescendants, + }, + } + return r + }(), + }, + wantErr: false, + }, + { + name: "can create valid role in global grants scope children", + args: args{ + role: func() *globalRole { + randomUuid := testId(t) grpId, err := newRoleId(ctx) require.NoError(t, err) - role.PublicId = grpId - return role + r := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: grpId, + ScopeId: globals.GlobalPrefix, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeChildren, + }, + } + return r }(), }, wantErr: false, }, { - name: "valid-with-dup-null-names-and-descriptions", + name: "cannot create role in org", + args: args{ + role: func() *globalRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: grpId, + ScopeId: org.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeDescendants, + }, + } + return r + }(), + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_global" violates foreign key constraint "iam_scope_global_fkey": integrity violation: error #1003`, + }, + { + name: "cannot create role in project", args: args{ - role: func() *Role { - role, err := NewRole(ctx, org.PublicId) + role: func() *globalRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) require.NoError(t, err) - roleId, err := newRoleId(ctx) + r := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: grpId, + ScopeId: proj.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeDescendants, + }, + } + return r + }(), + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_global" violates foreign key constraint "iam_scope_global_fkey": integrity violation: error #1003`, + }, + { + name: "invalid grants scope", + args: args{ + role: func() *globalRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) require.NoError(t, err) - role.PublicId = roleId - return role + r := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: grpId, + ScopeId: globals.GlobalPrefix, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: "invalid-scope", + }, + } + return r }(), }, - wantDup: true, - wantErr: false, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_global" violates foreign key constraint "iam_role_global_grant_scope_enm_fkey": integrity violation: error #1003`, }, { - name: "bad-scope-id", + name: "duplicate name not allowed", args: args{ - role: func() *Role { - id := testId(t) - role, err := NewRole(ctx, id) + role: func() *globalRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) require.NoError(t, err) - roleId, err := newRoleId(ctx) + // create a role then return the cloned role with different name + r := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: grpId, + ScopeId: globals.GlobalPrefix, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeDescendants, + }, + } + + require.NoError(t, rw.Create(ctx, r)) + newId, err := newRoleId(ctx) require.NoError(t, err) - role.PublicId = roleId - return role + dupeName := r.Clone().(*globalRole) + dupeName.PublicId = newId + dupeName.GrantThisRoleScope = false + dupeName.GrantScope = globals.GrantScopeChildren + return dupeName }(), }, wantErr: true, - wantErrMsg: "iam.validateScopeForWrite: scope is not found: search issue: error #1100", + wantErrMsg: `db.Create: duplicate key value violates unique constraint "iam_role_global_name_scope_id_uq": unique constraint violation: integrity violation: error #1002`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - w := db.New(conn) - if tt.wantDup { - r := tt.args.role.Clone().(*Role) - roleId, err := newRoleId(ctx) - require.NoError(err) - r.PublicId = roleId - err = w.Create(ctx, r) - require.NoError(err) - } - r := tt.args.role.Clone().(*Role) - err := w.Create(ctx, r) + r := tt.args.role.Clone().(*globalRole) + err := rw.Create(ctx, r) if tt.wantErr { - require.Error(err) - assert.Contains(err.Error(), tt.wantErrMsg) + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErrMsg) return } - assert.NoError(err) - assert.NotEmpty(tt.args.role.PublicId) + require.NoError(t, err) + require.NotEmpty(t, tt.args.role.PublicId) - foundGrp := allocRole() + foundGrp := allocGlobalRole() foundGrp.PublicId = tt.args.role.PublicId - err = w.LookupByPublicId(ctx, &foundGrp) - require.NoError(err) - assert.Empty(cmp.Diff(r, &foundGrp, protocmp.Transform())) + err = rw.LookupByPublicId(ctx, &foundGrp) + require.NoError(t, err) + require.Empty(t, cmp.Diff(r, &foundGrp, protocmp.Transform())) + + require.NotEmpty(t, foundGrp.GrantScopeUpdateTime) + require.NotEmpty(t, foundGrp.GrantThisRoleScopeUpdateTime) + require.NotZero(t, foundGrp.CreateTime.AsTime()) + require.NotZero(t, foundGrp.UpdateTime.AsTime()) + + baseRole := &testBaseRole{PublicId: tt.args.role.PublicId} + err = rw.LookupByPublicId(ctx, baseRole) + require.NoError(t, err) + require.Equal(t, foundGrp.CreateTime.AsTime(), baseRole.CreateTime.AsTime()) + require.Equal(t, foundGrp.UpdateTime.AsTime(), baseRole.UpdateTime.AsTime()) }) } } -func Test_RoleUpdate(t *testing.T) { +func Test_globalRole_Update(t *testing.T) { t.Parallel() ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") - wrapper := db.TestWrapper(t) - repo := TestRepo(t, conn, wrapper) - id := testId(t) - org, proj := TestScopes(t, repo) rw := db.New(conn) - type args struct { - name string - description string - fieldMaskPaths []string - nullPaths []string - scopeId string - scopeIdOverride string - opts []db.Option + type arg struct { + updateRole *globalRole + fieldMask []string + nullPath []string + opts []db.Option } tests := []struct { name string - args args - wantRowsUpdate int + setupOriginal func(t *testing.T) *globalRole + createInput func(t *testing.T, original *globalRole) arg wantErr bool + wantRowsUpdate int wantErrMsg string - wantDup bool }{ { - name: "valid", - args: args{ - name: "valid" + id, - fieldMaskPaths: []string{"Name"}, - scopeId: org.PublicId, + name: "can update name and description", + setupOriginal: func(t *testing.T) *globalRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: roleId, + ScopeId: globals.GlobalPrefix, + Name: testId(t), + Description: testId(t), + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original + }, + createInput: func(t *testing.T, original *globalRole) arg { + updated := original.Clone().(*globalRole) + updated.Name = testId(t) + updated.Description = testId(t) + return arg{ + updateRole: updated, + fieldMask: []string{"name", "description"}, + nullPath: []string{}, + opts: []db.Option{}, + } }, - wantErr: false, wantRowsUpdate: 1, + wantErr: false, }, { - name: "proj-scope-id", - args: args{ - name: "proj-scope-id" + id, - fieldMaskPaths: []string{"ScopeId"}, - scopeId: proj.PublicId, - scopeIdOverride: org.PublicId, + name: "can update grant_scope", + setupOriginal: func(t *testing.T) *globalRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: roleId, + ScopeId: globals.GlobalPrefix, + Name: testId(t), + Description: "desc", + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original }, - wantErr: true, - wantErrMsg: "iam.validateScopeForWrite: not allowed to change a resource's scope: parameter violation: error #100", - }, - { - name: "proj-scope-id-not-in-mask", - args: args{ - name: "proj-scope-id" + id, - fieldMaskPaths: []string{"Name"}, - scopeId: proj.PublicId, + createInput: func(t *testing.T, original *globalRole) arg { + updated := original.Clone().(*globalRole) + updated.GrantScope = globals.GrantScopeChildren + return arg{ + updateRole: updated, + fieldMask: []string{"GrantScope"}, + nullPath: []string{}, + opts: []db.Option{}, + } }, - wantErr: false, wantRowsUpdate: 1, - }, - { - name: "empty-scope-id", - args: args{ - name: "empty-scope-id" + id, - fieldMaskPaths: []string{"Name"}, - scopeId: "", - }, wantErr: false, - wantRowsUpdate: 1, }, { - name: "dup-name", - args: args{ - name: "dup-name" + id, - fieldMaskPaths: []string{"Name"}, - scopeId: org.PublicId, + name: "can set name and description null", + setupOriginal: func(t *testing.T) *globalRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: roleId, + ScopeId: globals.GlobalPrefix, + Name: testId(t), + Description: "desc", + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original }, - wantErr: true, - wantDup: true, - wantErrMsg: `db.Update: duplicate key value violates unique constraint "iam_role_name_scope_id_uq": unique constraint violation: integrity violation: error #1002`, - }, - { - name: "set description null", - args: args{ - name: "set description null" + id, - fieldMaskPaths: []string{"Name"}, - nullPaths: []string{"Description"}, - scopeId: org.PublicId, + createInput: func(t *testing.T, original *globalRole) arg { + updated := original.Clone().(*globalRole) + updated.Name = "" + updated.Description = "" + return arg{ + updateRole: updated, + fieldMask: []string{}, + nullPath: []string{"name", "description"}, + opts: []db.Option{}, + } }, - wantErr: false, wantRowsUpdate: 1, + wantErr: false, }, { - name: "set name null", - args: args{ - description: "set description null" + id, - fieldMaskPaths: []string{"Description"}, - nullPaths: []string{"Name"}, - scopeId: org.PublicId, + name: "can update grant_this_role_scope", + setupOriginal: func(t *testing.T) *globalRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: roleId, + ScopeId: globals.GlobalPrefix, + Name: testId(t), + Description: "desc", + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original + }, + createInput: func(t *testing.T, original *globalRole) arg { + updated := original.Clone().(*globalRole) + updated.GrantThisRoleScope = false + return arg{ + updateRole: updated, + fieldMask: []string{"GrantThisRoleScope"}, + nullPath: []string{}, + opts: []db.Option{}, + } }, - wantDup: true, - wantErr: false, wantRowsUpdate: 1, + wantErr: false, }, { - name: "set description null", - args: args{ - name: "set name null" + id, - fieldMaskPaths: []string{"Name"}, - nullPaths: []string{"Description"}, - scopeId: org.PublicId, + name: "can update version", + setupOriginal: func(t *testing.T) *globalRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: roleId, + ScopeId: globals.GlobalPrefix, + Name: testId(t), + Description: "desc", + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original + }, + createInput: func(t *testing.T, original *globalRole) arg { + updated := original.Clone().(*globalRole) + // version will be overridden by trigger + updated.Version = 123456 + return arg{ + updateRole: updated, + fieldMask: []string{"version"}, + nullPath: []string{}, + opts: []db.Option{}, + } }, - wantErr: false, wantRowsUpdate: 1, + wantErr: false, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - if tt.wantDup { - r := TestRole(t, conn, tt.args.scopeId, WithName(tt.args.name)) - _, err := rw.Update(context.Background(), r, tt.args.fieldMaskPaths, tt.args.nullPaths) - require.NoError(err) - } - - id := testId(t) - scopeId := tt.args.scopeId - if scopeId == "" { - scopeId = org.PublicId - } - role := TestRole(t, conn, scopeId, WithDescription(id), WithName(id)) - - updateRole := allocRole() - updateRole.PublicId = role.PublicId - updateRole.ScopeId = tt.args.scopeId - if tt.args.scopeIdOverride != "" { - updateRole.ScopeId = tt.args.scopeIdOverride - } - updateRole.Name = tt.args.name - updateRole.Description = tt.args.description - - updatedRows, err := rw.Update(context.Background(), &updateRole, tt.args.fieldMaskPaths, tt.args.nullPaths, tt.args.opts...) + original := tt.setupOriginal(t) + args := tt.createInput(t, original) + updatedRows, err := rw.Update(context.Background(), args.updateRole, args.fieldMask, args.nullPath, args.opts...) if tt.wantErr { - require.Error(err) - assert.Equal(0, updatedRows) - assert.Contains(err.Error(), tt.wantErrMsg) - err = db.TestVerifyOplog(t, rw, role.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) - require.Error(err) - assert.Contains(err.Error(), "record not found") + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErrMsg) return } - require.NoError(err) - assert.Equal(tt.wantRowsUpdate, updatedRows) - assert.NotEqual(role.UpdateTime, updateRole.UpdateTime) - foundRole := allocRole() - foundRole.PublicId = role.GetPublicId() - err = rw.LookupByPublicId(context.Background(), &foundRole) - require.NoError(err) - assert.True(proto.Equal(updateRole, foundRole)) - if len(tt.args.nullPaths) != 0 { - underlyingDB, err := conn.SqlDB(ctx) - require.NoError(err) - dbassert := dbassert.New(t, underlyingDB) - for _, f := range tt.args.nullPaths { - dbassert.IsNull(&foundRole, f) - } + require.NoError(t, err) + require.NotEmpty(t, args.updateRole.PublicId) + if tt.wantRowsUpdate == 0 { + require.Zero(t, updatedRows) + return + } + require.Equal(t, tt.wantRowsUpdate, updatedRows) + foundGrp := allocGlobalRole() + foundGrp.PublicId = original.PublicId + err = rw.LookupByPublicId(ctx, &foundGrp) + require.NoError(t, err) + require.Empty(t, cmp.Diff(args.updateRole, &foundGrp, protocmp.Transform())) + baseRole := &testBaseRole{PublicId: original.PublicId} + err = rw.LookupByPublicId(ctx, baseRole) + require.NoError(t, err) + require.Equal(t, original.Version+1, foundGrp.Version) + require.Equal(t, foundGrp.UpdateTime.AsTime(), baseRole.UpdateTime.AsTime()) + // assert other update time as necessary + if original.GrantThisRoleScope != args.updateRole.GrantThisRoleScope { + require.Greater(t, foundGrp.GrantThisRoleScopeUpdateTime.AsTime(), original.GrantThisRoleScopeUpdateTime.AsTime()) + } + if original.GrantScope != args.updateRole.GrantScope { + require.Greater(t, foundGrp.GrantScopeUpdateTime.AsTime(), original.GrantScopeUpdateTime.AsTime()) } }) } - t.Run("update dup names in diff scopes", func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - id := testId(t) - _ = TestRole(t, conn, org.PublicId, WithDescription(id), WithName(id)) - projRole := TestRole(t, conn, proj.PublicId, WithName(id)) - updatedRows, err := rw.Update(context.Background(), projRole, []string{"Name"}, nil) - require.NoError(err) - assert.Equal(1, updatedRows) - - foundRole := allocRole() - foundRole.PublicId = projRole.GetPublicId() - err = rw.LookupByPublicId(context.Background(), &foundRole) - require.NoError(err) - assert.Equal(id, projRole.Name) - }) - t.Run("attempt scope id update", func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - role := TestRole(t, conn, org.PublicId, WithDescription(id), WithName(id)) - updateRole := allocRole() - updateRole.PublicId = role.PublicId - updateRole.ScopeId = proj.PublicId - updatedRows, err := rw.Update(context.Background(), &updateRole, []string{"ScopeId"}, nil, db.WithSkipVetForWrite(true)) - require.Error(err) - assert.Equal(0, updatedRows) - assert.Contains(err.Error(), "immutable column: iam_role.scope_id: integrity violation: error #1003") - }) } -func Test_RoleDelete(t *testing.T) { +func Test_globalRole_Delete(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") - wrapper := db.TestWrapper(t) - repo := TestRepo(t, conn, wrapper) rw := db.New(conn) - id := testId(t) - org, _ := TestScopes(t, repo) - tests := []struct { name string - role *Role + setupRole func(ctx context.Context, t *testing.T) *globalRole wantRowsDeleted int wantErr bool wantErrMsg string }{ { - name: "valid", - role: TestRole(t, conn, org.PublicId), + name: "valid", + setupRole: func(ctx context.Context, t *testing.T) *globalRole { + role := &globalRole{ + GlobalRole: &store.GlobalRole{ + ScopeId: globals.GlobalPrefix, + Name: "test-role", + Description: "description", + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + id, err := newRoleId(ctx) + require.NoError(t, err) + role.PublicId = id + require.NoError(t, rw.Create(ctx, role)) + require.NotEmpty(t, role.PublicId) + return role + }, wantErr: false, wantRowsDeleted: 1, }, { - name: "bad-id", - role: func() *Role { r := allocRole(); r.PublicId = id; return &r }(), + name: "role-does-not-exist", + setupRole: func(ctx context.Context, t *testing.T) *globalRole { + id, err := newRoleId(ctx) + require.NoError(t, err) + role := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: id, + ScopeId: globals.GlobalPrefix, + Name: "test-role", + Description: "description", + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + return role + }, wantErr: false, wantRowsDeleted: 0, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - deleteRole := allocRole() - deleteRole.PublicId = tt.role.GetPublicId() - deletedRows, err := rw.Delete(context.Background(), &deleteRole) - if tt.wantErr { - require.Error(err) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + createdRole := tc.setupRole(context.Background(), t) + deletedRows, err := rw.Delete(context.Background(), &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: createdRole.PublicId, + }, + }) + if tc.wantErr { + require.Error(t, err) return } - require.NoError(err) - if tt.wantRowsDeleted == 0 { - assert.Equal(tt.wantRowsDeleted, deletedRows) + require.NoError(t, err) + if tc.wantRowsDeleted == 0 { + require.Equal(t, tc.wantRowsDeleted, deletedRows) return } - assert.Equal(tt.wantRowsDeleted, deletedRows) - foundRole := allocRole() - foundRole.PublicId = tt.role.GetPublicId() - err = rw.LookupByPublicId(context.Background(), &foundRole) - require.Error(err) - assert.True(errors.IsNotFoundError(err)) + require.Equal(t, tc.wantRowsDeleted, deletedRows) + foundRole := &globalRole{GlobalRole: &store.GlobalRole{PublicId: createdRole.PublicId}} + err = rw.LookupByPublicId(context.Background(), foundRole) + require.Error(t, err) + require.True(t, errors.IsNotFoundError(err)) + + // also check that base table was deleted + baseRole := &testBaseRole{PublicId: createdRole.PublicId} + err = rw.LookupByPublicId(context.Background(), baseRole) + require.Error(t, err) + require.True(t, errors.IsNotFoundError(err)) }) } } -func TestRole_Actions(t *testing.T) { - assert := assert.New(t) - r := &Role{} +func Test_globalRole_Actions(t *testing.T) { + r := allocGlobalRole() a := r.Actions() - assert.Equal(a[action.Create.String()], action.Create) - assert.Equal(a[action.Update.String()], action.Update) - assert.Equal(a[action.Read.String()], action.Read) - assert.Equal(a[action.Delete.String()], action.Delete) - assert.Equal(a[action.AddGrants.String()], action.AddGrants) - assert.Equal(a[action.RemoveGrants.String()], action.RemoveGrants) - assert.Equal(a[action.SetGrants.String()], action.SetGrants) - assert.Equal(a[action.AddPrincipals.String()], action.AddPrincipals) - assert.Equal(a[action.RemovePrincipals.String()], action.RemovePrincipals) - assert.Equal(a[action.SetPrincipals.String()], action.SetPrincipals) + assert.Equal(t, a[action.Create.String()], action.Create) + assert.Equal(t, a[action.Update.String()], action.Update) + assert.Equal(t, a[action.Read.String()], action.Read) + assert.Equal(t, a[action.Delete.String()], action.Delete) + assert.Equal(t, a[action.AddGrants.String()], action.AddGrants) + assert.Equal(t, a[action.RemoveGrants.String()], action.RemoveGrants) + assert.Equal(t, a[action.SetGrants.String()], action.SetGrants) + assert.Equal(t, a[action.AddPrincipals.String()], action.AddPrincipals) + assert.Equal(t, a[action.RemovePrincipals.String()], action.RemovePrincipals) + assert.Equal(t, a[action.SetPrincipals.String()], action.SetPrincipals) } -func TestRole_ResourceType(t *testing.T) { - assert := assert.New(t) - r := &Role{} - ty := r.GetResourceType() - assert.Equal(ty, resource.Role) +func Test_globalRole_ResourceType(t *testing.T) { + t.Parallel() + role := allocGlobalRole() + result := role.GetResourceType() + assert.Equal(t, result, resource.Role) } -func TestRole_GetScope(t *testing.T) { +func Test_globalRole_GetScope(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") - wrapper := db.TestWrapper(t) - repo := TestRepo(t, conn, wrapper) - org, proj := TestScopes(t, repo) - - t.Run("valid-org", func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - w := db.New(conn) - role := TestRole(t, conn, org.PublicId) - scope, err := role.GetScope(context.Background(), w) - require.NoError(err) - assert.True(proto.Equal(org, scope)) - }) - t.Run("valid-proj", func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - w := db.New(conn) - role := TestRole(t, conn, proj.PublicId) - scope, err := role.GetScope(context.Background(), w) - require.NoError(err) - assert.True(proto.Equal(proj, scope)) - }) + ctx := context.Background() + rw := db.New(conn) + role := globalRole{ + GlobalRole: &store.GlobalRole{ + ScopeId: globals.GlobalPrefix, + Name: "test-role", + Description: "description", + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + id, err := newRoleId(ctx) + require.NoError(t, err) + role.PublicId = id + require.NoError(t, rw.Create(ctx, role)) + require.NotEmpty(t, role.PublicId) + scope, err := role.GetScope(context.Background(), rw) + require.NoError(t, err) + require.Equal(t, scope.GetPublicId(), role.GetScopeId()) } -func TestRole_Clone(t *testing.T) { +func Test_globalRole_Clone(t *testing.T) { t.Parallel() - conn, _ := db.TestSetup(t, "postgres") - wrapper := db.TestWrapper(t) - repo := TestRepo(t, conn, wrapper) - org, _ := TestScopes(t, repo) + role1 := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: "r_123456", + ScopeId: "global", + Name: "test-role", + Description: "description", + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeChildren, + GrantThisRoleScopeUpdateTime: timestamp.New(time.Date(2025, 2, 12, 3, 15, 15, 30, time.UTC)), + GrantScopeUpdateTime: timestamp.New(time.Date(2025, 3, 12, 3, 15, 15, 30, time.UTC)), + CreateTime: timestamp.New(time.Date(2025, 4, 12, 3, 15, 15, 30, time.UTC)), + UpdateTime: timestamp.New(time.Date(2025, 5, 12, 3, 15, 15, 30, time.UTC)), + Version: 1, + }, + GrantScopes: []*RoleGrantScope{ + { + CreateTime: timestamp.New(time.Date(2025, 3, 12, 3, 15, 15, 30, time.UTC)), + RoleId: "r_123456", + ScopeIdOrSpecial: "children", + }, + { + CreateTime: timestamp.New(time.Date(2025, 1, 14, 3, 15, 15, 30, time.UTC)), + RoleId: "r_123456", + ScopeIdOrSpecial: "p_123456", + }, + }, + } + role2 := &globalRole{ + GlobalRole: &store.GlobalRole{ + PublicId: "r_abcdef", + ScopeId: "o_123456", + Name: "another-role", + Description: "different description", + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeChildren, + GrantThisRoleScopeUpdateTime: timestamp.New(time.Date(2024, 2, 12, 3, 15, 15, 30, time.UTC)), + GrantScopeUpdateTime: timestamp.New(time.Date(2024, 3, 12, 3, 15, 15, 30, time.UTC)), + CreateTime: timestamp.New(time.Date(2024, 4, 12, 3, 15, 15, 30, time.UTC)), + UpdateTime: timestamp.New(time.Date(2024, 5, 12, 3, 15, 15, 30, time.UTC)), + Version: 1, + }, + GrantScopes: []*RoleGrantScope{ + { + CreateTime: timestamp.New(time.Date(2024, 3, 12, 3, 15, 15, 30, time.UTC)), + RoleId: "r_abcdef", + ScopeIdOrSpecial: "children", + }, + { + CreateTime: timestamp.New(time.Date(2024, 1, 14, 3, 15, 15, 30, time.UTC)), + RoleId: "r_abcdef", + ScopeIdOrSpecial: "p_abcdef", + }, + }, + } + require.NotEqual(t, role1, role2) + t.Run("valid", func(t *testing.T) { - assert := assert.New(t) - role := TestRole(t, conn, org.PublicId, WithDescription("this is a test role")) - cp := role.Clone() - assert.True(proto.Equal(cp.(*Role).Role, role.Role)) + clone := role1.Clone() + assert.True(t, proto.Equal(clone.(*globalRole).GlobalRole, role1.GlobalRole)) + assert.Equal(t, clone.(*globalRole).GrantScopes, role1.GrantScopes) }) t.Run("not-equal", func(t *testing.T) { - assert := assert.New(t) - role := TestRole(t, conn, org.PublicId) - role2 := TestRole(t, conn, org.PublicId) - cp := role.Clone() - assert.True(!proto.Equal(cp.(*Role).Role, role2.Role)) + role1Clone := role1.Clone() + assert.True(t, !proto.Equal(role1Clone.(*globalRole).GlobalRole, role2.GlobalRole)) + assert.NotEqual(t, role1Clone.(*globalRole).GrantScopes, role2.GrantScopes) }) } -func TestRole_SetTableName(t *testing.T) { - defaultTableName := defaultRoleTableName +func Test_globalRole_SetTableName(t *testing.T) { + defaultTableName := defaultGlobalRoleTableName tests := []struct { name string initialName string @@ -551,15 +807,1309 @@ func TestRole_SetTableName(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - def := allocRole() - require.Equal(defaultTableName, def.TableName()) - s := &Role{ - Role: &store.Role{}, - tableName: tt.initialName, + def := allocGlobalRole() + require.Equal(t, defaultTableName, def.TableName()) + s := &globalRole{ + GlobalRole: &store.GlobalRole{}, + tableName: tt.initialName, } s.SetTableName(tt.setNameTo) - assert.Equal(tt.want, s.TableName()) + assert.Equal(t, tt.want, s.TableName()) }) } } + +func Test_orgRole_Create(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + type args struct { + role *orgRole + } + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + org, proj := TestScopes(t, repo) + org2 := TestOrg(t, repo) + rw := db.New(conn) + tests := []struct { + name string + args args + wantErr bool + wantErrMsg string + }{ + { + name: "can create valid role in org grants scope individual", + args: args{ + role: func() *orgRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: grpId, + ScopeId: org.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + return r + }(), + }, + wantErr: false, + }, + { + name: "can create valid role in org grants scope children", + args: args{ + role: func() *orgRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: grpId, + ScopeId: org.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeChildren, + }, + } + return r + }(), + }, + wantErr: false, + }, + { + name: "duplicate name different scope allowed", + args: args{ + role: func() *orgRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + // create a role then return the cloned role with different name + r := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: grpId, + ScopeId: org.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeChildren, + }, + } + + require.NoError(t, rw.Create(ctx, r)) + newId, err := newRoleId(ctx) + require.NoError(t, err) + dupeName := r.Clone().(*orgRole) + dupeName.PublicId = newId + dupeName.ScopeId = org2.PublicId + dupeName.GrantThisRoleScope = false + dupeName.GrantScope = globals.GrantScopeChildren + return dupeName + }(), + }, + wantErr: false, + }, + { + name: "cannot create valid role in org grants scope descendants", + args: args{ + role: func() *orgRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: grpId, + ScopeId: org.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeDescendants, + }, + } + return r + }(), + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_org" violates foreign key constraint "iam_role_org_grant_scope_enm_fkey": integrity violation: error #1003`, + }, + { + name: "cannot create role in org with descendants grants", + args: args{ + role: func() *orgRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: grpId, + ScopeId: org.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeDescendants, + }, + } + return r + }(), + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_org" violates foreign key constraint "iam_role_org_grant_scope_enm_fkey": integrity violation: error #1003`, + }, + { + name: "cannot create role in global", + args: args{ + role: func() *orgRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: grpId, + ScopeId: globals.GlobalPrefix, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeChildren, + }, + } + return r + }(), + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_org" violates foreign key constraint "iam_scope_org_fkey": integrity violation: error #1003`, + }, + { + name: "cannot create role in project", + args: args{ + role: func() *orgRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: grpId, + ScopeId: proj.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeDescendants, + }, + } + return r + }(), + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_org" violates foreign key constraint "iam_scope_org_fkey": integrity violation: error #1003`, + }, + { + name: "invalid grants scope", + args: args{ + role: func() *orgRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: grpId, + ScopeId: org.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: "invalid-scope", + }, + } + return r + }(), + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_org" violates foreign key constraint "iam_role_org_grant_scope_enm_fkey": integrity violation: error #1003`, + }, + { + name: "duplicate name not allowed", + args: args{ + role: func() *orgRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + // create a role then return the cloned role with different name + r := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: grpId, + ScopeId: org.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeChildren, + }, + } + + require.NoError(t, rw.Create(ctx, r)) + newId, err := newRoleId(ctx) + require.NoError(t, err) + dupeName := r.Clone().(*orgRole) + dupeName.PublicId = newId + dupeName.GrantThisRoleScope = false + dupeName.GrantScope = globals.GrantScopeChildren + return dupeName + }(), + }, + wantErr: true, + wantErrMsg: `db.Create: duplicate key value violates unique constraint "iam_role_org_name_scope_id_uq": unique constraint violation: integrity violation: error #1002`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.args.role.Clone().(*orgRole) + err := rw.Create(ctx, r) + if tt.wantErr { + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErrMsg) + return + } + require.NoError(t, err) + require.NotEmpty(t, tt.args.role.PublicId) + + foundGrp := allocOrgRole() + foundGrp.PublicId = tt.args.role.PublicId + err = rw.LookupByPublicId(ctx, &foundGrp) + require.NoError(t, err) + require.Empty(t, cmp.Diff(r, &foundGrp, protocmp.Transform())) + + require.NotEmpty(t, foundGrp.GrantScopeUpdateTime) + require.NotEmpty(t, foundGrp.GrantThisRoleScopeUpdateTime) + require.NotZero(t, foundGrp.CreateTime.AsTime()) + require.NotZero(t, foundGrp.UpdateTime.AsTime()) + + baseRole := testBaseRole{PublicId: tt.args.role.PublicId} + err = rw.LookupByPublicId(ctx, &baseRole) + require.NoError(t, err) + require.Equal(t, foundGrp.CreateTime.AsTime(), baseRole.CreateTime.AsTime()) + require.Equal(t, foundGrp.UpdateTime.AsTime(), baseRole.UpdateTime.AsTime()) + }) + } +} + +func Test_orgRole_Update(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + org := TestOrg(t, repo) + type arg struct { + updateRole *orgRole + fieldMask []string + nullPath []string + opts []db.Option + } + tests := []struct { + name string + setupOriginal func(t *testing.T) *orgRole + createInput func(t *testing.T, original *orgRole) arg + wantErr bool + wantRowsUpdate int + wantErrMsg string + }{ + { + name: "can update name and description", + setupOriginal: func(t *testing.T) *orgRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: roleId, + ScopeId: org.PublicId, + Name: testId(t), + Description: testId(t), + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original + }, + createInput: func(t *testing.T, original *orgRole) arg { + updated := original.Clone().(*orgRole) + updated.Name = testId(t) + updated.Description = testId(t) + return arg{ + updateRole: updated, + fieldMask: []string{"name", "description"}, + nullPath: []string{}, + opts: []db.Option{}, + } + }, + wantRowsUpdate: 1, + wantErr: false, + }, + { + name: "can update grant_scope", + setupOriginal: func(t *testing.T) *orgRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: roleId, + ScopeId: org.PublicId, + Name: testId(t), + Description: "desc", + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original + }, + createInput: func(t *testing.T, original *orgRole) arg { + updated := original.Clone().(*orgRole) + updated.GrantScope = globals.GrantScopeChildren + return arg{ + updateRole: updated, + fieldMask: []string{"GrantScope"}, + nullPath: []string{}, + opts: []db.Option{}, + } + }, + wantRowsUpdate: 1, + wantErr: false, + }, + { + name: "can set name and description null", + setupOriginal: func(t *testing.T) *orgRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: roleId, + ScopeId: org.PublicId, + Name: testId(t), + Description: "desc", + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original + }, + createInput: func(t *testing.T, original *orgRole) arg { + updated := original.Clone().(*orgRole) + updated.Name = "" + updated.Description = "" + return arg{ + updateRole: updated, + fieldMask: []string{}, + nullPath: []string{"name", "description"}, + opts: []db.Option{}, + } + }, + wantRowsUpdate: 1, + wantErr: false, + }, + { + name: "can update grant_this_role_scope", + setupOriginal: func(t *testing.T) *orgRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: roleId, + ScopeId: org.PublicId, + Name: testId(t), + Description: "desc", + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original + }, + createInput: func(t *testing.T, original *orgRole) arg { + updated := original.Clone().(*orgRole) + updated.GrantThisRoleScope = false + return arg{ + updateRole: updated, + fieldMask: []string{"GrantThisRoleScope"}, + nullPath: []string{}, + opts: []db.Option{}, + } + }, + wantRowsUpdate: 1, + wantErr: false, + }, + { + name: "can update version", + setupOriginal: func(t *testing.T) *orgRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: roleId, + ScopeId: org.PublicId, + Name: testId(t), + Description: "desc", + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original + }, + createInput: func(t *testing.T, original *orgRole) arg { + updated := original.Clone().(*orgRole) + updated.Version = uint32(15) + return arg{ + updateRole: updated, + fieldMask: []string{"version"}, + nullPath: []string{}, + opts: []db.Option{}, + } + }, + wantRowsUpdate: 1, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := tt.setupOriginal(t) + args := tt.createInput(t, original) + updatedRows, err := rw.Update(context.Background(), args.updateRole, args.fieldMask, args.nullPath, args.opts...) + if tt.wantErr { + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErrMsg) + return + } + require.NoError(t, err) + require.NotEmpty(t, args.updateRole.PublicId) + if tt.wantRowsUpdate == 0 { + require.Zero(t, updatedRows) + return + } + require.Equal(t, tt.wantRowsUpdate, updatedRows) + + foundGrp := allocOrgRole() + foundGrp.PublicId = original.PublicId + err = rw.LookupByPublicId(ctx, &foundGrp) + require.NoError(t, err) + require.Empty(t, cmp.Diff(args.updateRole, &foundGrp, protocmp.Transform())) + // also check that base table was deleted + baseRole := &testBaseRole{PublicId: original.PublicId} + err = rw.LookupByPublicId(ctx, baseRole) + require.NoError(t, err) + require.Equal(t, foundGrp.UpdateTime.AsTime(), baseRole.UpdateTime.AsTime()) + require.Equal(t, original.Version+1, foundGrp.Version) + require.Equal(t, foundGrp.UpdateTime.AsTime(), baseRole.UpdateTime.AsTime()) + // assert other update time as necessary + if original.GrantThisRoleScope != args.updateRole.GrantThisRoleScope { + require.Greater(t, foundGrp.GrantThisRoleScopeUpdateTime.AsTime(), original.GrantThisRoleScopeUpdateTime.AsTime()) + } + if original.GrantScope != args.updateRole.GrantScope { + require.Greater(t, foundGrp.GrantScopeUpdateTime.AsTime(), original.GrantScopeUpdateTime.AsTime()) + } + }) + } +} + +func Test_orgRole_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + org := TestOrg(t, repo) + tests := []struct { + name string + setupRole func(ctx context.Context, t *testing.T) *orgRole + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + setupRole: func(ctx context.Context, t *testing.T) *orgRole { + role := &orgRole{ + OrgRole: &store.OrgRole{ + ScopeId: org.PublicId, + Name: "test-role", + Description: "description", + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + id, err := newRoleId(ctx) + require.NoError(t, err) + role.PublicId = id + require.NoError(t, rw.Create(ctx, role)) + require.NotEmpty(t, role.PublicId) + return role + }, + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "role-does-not-exist", + setupRole: func(ctx context.Context, t *testing.T) *orgRole { + id, err := newRoleId(ctx) + require.NoError(t, err) + role := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: id, + ScopeId: org.PublicId, + Name: "test-role", + Description: "description", + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + return role + }, + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + createdRole := tc.setupRole(context.Background(), t) + deletedRows, err := rw.Delete(context.Background(), &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: createdRole.PublicId, + }, + }) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + if tc.wantRowsDeleted == 0 { + require.Equal(t, tc.wantRowsDeleted, deletedRows) + return + } + require.Equal(t, tc.wantRowsDeleted, deletedRows) + foundRole := &orgRole{OrgRole: &store.OrgRole{PublicId: createdRole.PublicId}} + err = rw.LookupByPublicId(context.Background(), foundRole) + require.Error(t, err) + require.True(t, errors.IsNotFoundError(err)) + + // also check that base table was deleted + baseRole := &testBaseRole{PublicId: createdRole.PublicId} + err = rw.LookupByPublicId(context.Background(), baseRole) + require.Error(t, err) + require.True(t, errors.IsNotFoundError(err)) + }) + } +} + +func Test_orgRole_Actions(t *testing.T) { + r := &orgRole{} + a := r.Actions() + assert.Equal(t, a[action.Create.String()], action.Create) + assert.Equal(t, a[action.Update.String()], action.Update) + assert.Equal(t, a[action.Read.String()], action.Read) + assert.Equal(t, a[action.Delete.String()], action.Delete) + assert.Equal(t, a[action.AddGrants.String()], action.AddGrants) + assert.Equal(t, a[action.RemoveGrants.String()], action.RemoveGrants) + assert.Equal(t, a[action.SetGrants.String()], action.SetGrants) + assert.Equal(t, a[action.AddPrincipals.String()], action.AddPrincipals) + assert.Equal(t, a[action.RemovePrincipals.String()], action.RemovePrincipals) + assert.Equal(t, a[action.SetPrincipals.String()], action.SetPrincipals) +} + +func Test_orgRole_ResourceType(t *testing.T) { + t.Parallel() + role := orgRole{} + result := role.GetResourceType() + assert.Equal(t, result, resource.Role) +} + +func Test_orgRole_GetScope(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + ctx := context.Background() + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + org := TestOrg(t, repo) + role := orgRole{ + OrgRole: &store.OrgRole{ + ScopeId: org.PublicId, + Name: "test-role", + Description: "description", + GrantThisRoleScope: false, + GrantScope: globals.GrantScopeIndividual, + }, + } + id, err := newRoleId(ctx) + require.NoError(t, err) + role.PublicId = id + require.NoError(t, rw.Create(ctx, role)) + require.NotEmpty(t, role.PublicId) + scope, err := role.GetScope(context.Background(), rw) + require.NoError(t, err) + require.True(t, proto.Equal(org, scope)) +} + +func Test_orgRole_Clone(t *testing.T) { + t.Parallel() + role1 := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: "r_123456", + ScopeId: "o_123456", + Name: "test-role", + Description: "description", + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeChildren, + GrantThisRoleScopeUpdateTime: timestamp.New(time.Date(2025, 2, 12, 3, 15, 15, 30, time.UTC)), + GrantScopeUpdateTime: timestamp.New(time.Date(2025, 3, 12, 3, 15, 15, 30, time.UTC)), + CreateTime: timestamp.New(time.Date(2025, 4, 12, 3, 15, 15, 30, time.UTC)), + UpdateTime: timestamp.New(time.Date(2025, 5, 12, 3, 15, 15, 30, time.UTC)), + Version: 1, + }, + GrantScopes: []*RoleGrantScope{ + { + CreateTime: timestamp.New(time.Date(2025, 3, 12, 3, 15, 15, 30, time.UTC)), + RoleId: "r_123456", + ScopeIdOrSpecial: "children", + }, + { + CreateTime: timestamp.New(time.Date(2025, 1, 14, 3, 15, 15, 30, time.UTC)), + RoleId: "r_123456", + ScopeIdOrSpecial: "p_123456", + }, + }, + } + role2 := &orgRole{ + OrgRole: &store.OrgRole{ + PublicId: "r_abcdef", + ScopeId: "o_abcdef", + Name: "another-role", + Description: "different description", + GrantThisRoleScope: true, + GrantScope: globals.GrantScopeChildren, + GrantThisRoleScopeUpdateTime: timestamp.New(time.Date(2024, 2, 12, 3, 15, 15, 30, time.UTC)), + GrantScopeUpdateTime: timestamp.New(time.Date(2024, 3, 12, 3, 15, 15, 30, time.UTC)), + CreateTime: timestamp.New(time.Date(2024, 4, 12, 3, 15, 15, 30, time.UTC)), + UpdateTime: timestamp.New(time.Date(2024, 5, 12, 3, 15, 15, 30, time.UTC)), + Version: 1, + }, + GrantScopes: []*RoleGrantScope{ + { + CreateTime: timestamp.New(time.Date(2024, 3, 12, 3, 15, 15, 30, time.UTC)), + RoleId: "r_abcdef", + ScopeIdOrSpecial: "children", + }, + { + CreateTime: timestamp.New(time.Date(2024, 1, 14, 3, 15, 15, 30, time.UTC)), + RoleId: "r_abcdef", + ScopeIdOrSpecial: "p_abcdef", + }, + }, + } + require.NotEqual(t, role1, role2) + + t.Run("valid", func(t *testing.T) { + clone := role1.Clone() + assert.True(t, proto.Equal(clone.(*orgRole).OrgRole, role1.OrgRole)) + assert.Equal(t, clone.(*orgRole).GrantScopes, role1.GrantScopes) + }) + t.Run("not-equal", func(t *testing.T) { + role1Clone := role1.Clone() + assert.True(t, !proto.Equal(role1Clone.(*orgRole).OrgRole, role2.OrgRole)) + assert.NotEqual(t, role1Clone.(*orgRole).GrantScopes, role2.GrantScopes) + }) +} + +func Test_orgRole_SetTableName(t *testing.T) { + defaultTableName := defaultOrgRoleTableName + tests := []struct { + name string + initialName string + setNameTo string + want string + }{ + { + name: "new-name", + initialName: "", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + initialName: "initial", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + def := orgRole{} + require.Equal(t, defaultTableName, def.TableName()) + s := &orgRole{ + OrgRole: &store.OrgRole{}, + tableName: tt.initialName, + } + s.SetTableName(tt.setNameTo) + assert.Equal(t, tt.want, s.TableName()) + }) + } +} + +func Test_projectRole_Create(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + type args struct { + role *projectRole + } + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + org, proj := TestScopes(t, repo) + proj2 := TestProject(t, repo, org.PublicId) + rw := db.New(conn) + tests := []struct { + name string + args args + wantErr bool + wantErrMsg string + }{ + { + name: "can create valid role in project", + args: args{ + role: func() *projectRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: grpId, + ScopeId: proj.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + }, + } + return r + }(), + }, + wantErr: false, + }, + { + name: "duplicate name different scope allowed", + args: args{ + role: func() *projectRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + // create a role then return the cloned role with different name + r := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: grpId, + ScopeId: proj.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + }, + } + + require.NoError(t, rw.Create(ctx, r)) + newId, err := newRoleId(ctx) + require.NoError(t, err) + dupeName := r.Clone().(*projectRole) + dupeName.PublicId = newId + dupeName.ScopeId = proj2.PublicId + return dupeName + }(), + }, + wantErr: false, + }, + { + name: "cannot create role in global", + args: args{ + role: func() *projectRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: grpId, + ScopeId: globals.GlobalPrefix, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + }, + } + return r + }(), + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_project" violates foreign key constraint "iam_scope_project_fkey": integrity violation: error #1003`, + }, + { + name: "cannot create role in org", + args: args{ + role: func() *projectRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + r := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: grpId, + ScopeId: org.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + }, + } + return r + }(), + }, + wantErr: true, + wantErrMsg: `db.Create: insert or update on table "iam_role_project" violates foreign key constraint "iam_scope_project_fkey": integrity violation: error #1003`, + }, + { + name: "duplicate name same scope not allowed", + args: args{ + role: func() *projectRole { + randomUuid := testId(t) + grpId, err := newRoleId(ctx) + require.NoError(t, err) + // create a role then return the cloned role with different name + r := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: grpId, + ScopeId: proj.PublicId, + Name: "name" + randomUuid, + Description: "desc" + randomUuid, + }, + } + + require.NoError(t, rw.Create(ctx, r)) + newId, err := newRoleId(ctx) + require.NoError(t, err) + dupeName := r.Clone().(*projectRole) + dupeName.PublicId = newId + return dupeName + }(), + }, + wantErr: true, + wantErrMsg: `db.Create: duplicate key value violates unique constraint "iam_role_project_name_scope_id_uq": unique constraint violation: integrity violation: error #1002`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.args.role.Clone().(*projectRole) + err := rw.Create(ctx, r) + if tt.wantErr { + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErrMsg) + return + } + require.NoError(t, err) + require.NotEmpty(t, tt.args.role.PublicId) + + foundGrp := allocProjectRole() + foundGrp.PublicId = tt.args.role.PublicId + err = rw.LookupByPublicId(ctx, &foundGrp) + require.NoError(t, err) + require.Empty(t, cmp.Diff(r, &foundGrp, protocmp.Transform())) + require.NotZero(t, foundGrp.CreateTime.AsTime()) + require.NotZero(t, foundGrp.UpdateTime.AsTime()) + baseRole := &testBaseRole{PublicId: tt.args.role.PublicId} + err = rw.LookupByPublicId(ctx, baseRole) + require.NoError(t, err) + require.Equal(t, foundGrp.CreateTime.AsTime(), baseRole.CreateTime.AsTime()) + require.Equal(t, foundGrp.UpdateTime.AsTime(), baseRole.UpdateTime.AsTime()) + }) + } +} + +func Test_projectRole_Update(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + _, proj := TestScopes(t, repo) + type arg struct { + updateRole *projectRole + fieldMask []string + nullPath []string + opts []db.Option + } + tests := []struct { + name string + setupOriginal func(t *testing.T) *projectRole + createInput func(t *testing.T, original *projectRole) arg + wantErr bool + wantRowsUpdate int + wantErrMsg string + }{ + { + name: "can update name and description", + setupOriginal: func(t *testing.T) *projectRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: roleId, + ScopeId: proj.PublicId, + Name: testId(t), + Description: testId(t), + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original + }, + createInput: func(t *testing.T, original *projectRole) arg { + updated := original.Clone().(*projectRole) + updated.Name = testId(t) + updated.Description = testId(t) + return arg{ + updateRole: updated, + fieldMask: []string{"name", "description"}, + nullPath: []string{}, + opts: []db.Option{}, + } + }, + wantRowsUpdate: 1, + wantErr: false, + }, + { + name: "can set name and description null", + setupOriginal: func(t *testing.T) *projectRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: roleId, + ScopeId: proj.PublicId, + Name: testId(t), + Description: "desc", + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original + }, + createInput: func(t *testing.T, original *projectRole) arg { + updated := original.Clone().(*projectRole) + updated.Name = "" + updated.Description = "" + return arg{ + updateRole: updated, + fieldMask: []string{}, + nullPath: []string{"name", "description"}, + opts: []db.Option{}, + } + }, + wantRowsUpdate: 1, + wantErr: false, + }, + { + name: "can update version", + setupOriginal: func(t *testing.T) *projectRole { + roleId, err := newRoleId(ctx) + require.NoError(t, err) + original := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: roleId, + ScopeId: proj.PublicId, + Name: testId(t), + Description: "desc", + }, + } + require.NoError(t, rw.Create(ctx, original)) + return original + }, + createInput: func(t *testing.T, original *projectRole) arg { + updated := original.Clone().(*projectRole) + updated.Version = uint32(15) + return arg{ + updateRole: updated, + fieldMask: []string{"version"}, + nullPath: []string{}, + opts: []db.Option{}, + } + }, + wantRowsUpdate: 1, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := tt.setupOriginal(t) + args := tt.createInput(t, original) + updatedRows, err := rw.Update(context.Background(), args.updateRole, args.fieldMask, args.nullPath, args.opts...) + if tt.wantErr { + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErrMsg) + return + } + require.NoError(t, err) + require.NotEmpty(t, args.updateRole.PublicId) + if tt.wantRowsUpdate == 0 { + require.Zero(t, updatedRows) + return + } + require.Equal(t, tt.wantRowsUpdate, updatedRows) + + foundGrp := allocProjectRole() + foundGrp.PublicId = original.PublicId + err = rw.LookupByPublicId(ctx, &foundGrp) + require.NoError(t, err) + require.Empty(t, cmp.Diff(args.updateRole, &foundGrp, protocmp.Transform())) + baseRole := &testBaseRole{PublicId: original.PublicId} + err = rw.LookupByPublicId(ctx, baseRole) + require.NoError(t, err) + require.Equal(t, foundGrp.UpdateTime.AsTime(), baseRole.UpdateTime.AsTime()) + require.Equal(t, original.Version+1, foundGrp.Version) + require.Equal(t, foundGrp.UpdateTime.AsTime(), baseRole.UpdateTime.AsTime()) + }) + } +} + +func Test_projectRole_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + _, proj := TestScopes(t, repo) + tests := []struct { + name string + setupRole func(ctx context.Context, t *testing.T) *projectRole + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + setupRole: func(ctx context.Context, t *testing.T) *projectRole { + role := &projectRole{ + ProjectRole: &store.ProjectRole{ + ScopeId: proj.PublicId, + Name: "test-role", + Description: "description", + }, + } + id, err := newRoleId(ctx) + require.NoError(t, err) + role.PublicId = id + require.NoError(t, rw.Create(ctx, role)) + require.NotEmpty(t, role.PublicId) + return role + }, + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "role-does-not-exist", + setupRole: func(ctx context.Context, t *testing.T) *projectRole { + id, err := newRoleId(ctx) + require.NoError(t, err) + role := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: id, + ScopeId: proj.PublicId, + Name: "test-role", + Description: "description", + }, + } + return role + }, + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + createdRole := tc.setupRole(context.Background(), t) + deletedRows, err := rw.Delete(context.Background(), &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: createdRole.PublicId, + }, + }) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + if tc.wantRowsDeleted == 0 { + require.Equal(t, tc.wantRowsDeleted, deletedRows) + return + } + require.Equal(t, tc.wantRowsDeleted, deletedRows) + foundRole := &projectRole{ProjectRole: &store.ProjectRole{PublicId: createdRole.PublicId}} + err = rw.LookupByPublicId(context.Background(), foundRole) + require.Error(t, err) + require.True(t, errors.IsNotFoundError(err)) + + // also check that base table was deleted + baseRole := &testBaseRole{PublicId: createdRole.PublicId} + err = rw.LookupByPublicId(context.Background(), baseRole) + require.Error(t, err) + require.True(t, errors.IsNotFoundError(err)) + }) + } +} + +func Test_projectRole_Actions(t *testing.T) { + r := &projectRole{} + a := r.Actions() + assert.Equal(t, a[action.Create.String()], action.Create) + assert.Equal(t, a[action.Update.String()], action.Update) + assert.Equal(t, a[action.Read.String()], action.Read) + assert.Equal(t, a[action.Delete.String()], action.Delete) + assert.Equal(t, a[action.AddGrants.String()], action.AddGrants) + assert.Equal(t, a[action.RemoveGrants.String()], action.RemoveGrants) + assert.Equal(t, a[action.SetGrants.String()], action.SetGrants) + assert.Equal(t, a[action.AddPrincipals.String()], action.AddPrincipals) + assert.Equal(t, a[action.RemovePrincipals.String()], action.RemovePrincipals) + assert.Equal(t, a[action.SetPrincipals.String()], action.SetPrincipals) +} + +func Test_projectRole_ResourceType(t *testing.T) { + t.Parallel() + role := projectRole{} + result := role.GetResourceType() + assert.Equal(t, result, resource.Role) +} + +func Test_projectRole_GetScope(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + ctx := context.Background() + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo := TestRepo(t, conn, wrapper) + _, proj := TestScopes(t, repo) + role := projectRole{ + ProjectRole: &store.ProjectRole{ + ScopeId: proj.PublicId, + Name: "test-role", + Description: "description", + }, + } + id, err := newRoleId(ctx) + require.NoError(t, err) + role.PublicId = id + require.NoError(t, rw.Create(ctx, role)) + require.NotEmpty(t, role.PublicId) + scope, err := role.GetScope(context.Background(), rw) + require.NoError(t, err) + require.True(t, proto.Equal(proj, scope)) +} + +func Test_projectRole_Clone(t *testing.T) { + t.Parallel() + role1 := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: "r_123456", + ScopeId: "p_123456", + Name: "test-role", + Description: "description", + CreateTime: timestamp.New(time.Date(2025, 4, 12, 3, 15, 15, 30, time.UTC)), + UpdateTime: timestamp.New(time.Date(2025, 5, 12, 3, 15, 15, 30, time.UTC)), + Version: 1, + }, + GrantScopes: []*RoleGrantScope{ + { + CreateTime: timestamp.New(time.Date(2025, 3, 12, 3, 15, 15, 30, time.UTC)), + RoleId: "r_123456", + ScopeIdOrSpecial: "this", + }, + }, + } + role2 := &projectRole{ + ProjectRole: &store.ProjectRole{ + PublicId: "r_abcdef", + ScopeId: "o_123456", + Name: "another-role", + Description: "different description", + CreateTime: timestamp.New(time.Date(2024, 4, 12, 3, 15, 15, 30, time.UTC)), + UpdateTime: timestamp.New(time.Date(2024, 5, 12, 3, 15, 15, 30, time.UTC)), + Version: 1, + }, + GrantScopes: []*RoleGrantScope{ + { + CreateTime: timestamp.New(time.Date(2024, 3, 12, 3, 15, 15, 30, time.UTC)), + RoleId: "r_abcdef", + ScopeIdOrSpecial: "children", + }, + { + CreateTime: timestamp.New(time.Date(2024, 1, 14, 3, 15, 15, 30, time.UTC)), + RoleId: "r_abcdef", + ScopeIdOrSpecial: "p_abcdef", + }, + }, + } + require.NotEqual(t, role1, role2) + + t.Run("valid", func(t *testing.T) { + clone := role1.Clone() + assert.True(t, proto.Equal(clone.(*projectRole).ProjectRole, role1.ProjectRole)) + assert.Equal(t, clone.(*projectRole).GrantScopes, role1.GrantScopes) + }) + t.Run("not-equal", func(t *testing.T) { + role1Clone := role1.Clone() + assert.True(t, !proto.Equal(role1Clone.(*projectRole).ProjectRole, role2.ProjectRole)) + assert.NotEqual(t, role1Clone.(*projectRole).GrantScopes, role2.GrantScopes) + }) +} + +func Test_projectRole_SetTableName(t *testing.T) { + defaultTableName := defaultProjectRoleTableName + tests := []struct { + name string + initialName string + setNameTo string + want string + }{ + { + name: "new-name", + initialName: "", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + initialName: "initial", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + def := projectRole{} + require.Equal(t, defaultTableName, def.TableName()) + s := &projectRole{ + ProjectRole: &store.ProjectRole{}, + tableName: tt.initialName, + } + s.SetTableName(tt.setNameTo) + assert.Equal(t, tt.want, s.TableName()) + }) + } +} + +// testBaseRole is used to interact with `iam_role` table for testing purposes +type testBaseRole struct { + PublicId string `gorm:"primary_key"` + ScopeId string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp +} + +func (t *testBaseRole) GetPublicId() string { + if t == nil { + return "" + } + return t.PublicId +} + +func (t *testBaseRole) TableName() string { + return "iam_role" +} diff --git a/internal/iam/store/role_global.pb.go b/internal/iam/store/role_global.pb.go new file mode 100644 index 0000000000..9c259877a9 --- /dev/null +++ b/internal/iam/store/role_global.pb.go @@ -0,0 +1,291 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.1 +// protoc (unknown) +// source: controller/storage/iam/store/v1/role_global.proto + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// GlobalRole represents roles created in global scope +type GlobalRole struct { + state protoimpl.MessageState `protogen:"open.v1"` + // public_id is used to access the Role via an API + // @inject_tag: gorm:"primary_key" + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // scope id for the role + // @inject_tag: `gorm:"default:null"` + ScopeId string `protobuf:"bytes,2,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` + // name is the optional friendly name used to + // access the Role via an API + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description of the role + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // control if this role is granted access to its role scope + // @inject_tag: `gorm:"default:true"` + GrantThisRoleScope bool `protobuf:"varint,5,opt,name=grant_this_role_scope,json=grantThisRoleScope,proto3" json:"grant_this_role_scope,omitempty" gorm:"default:true"` + // control type of grant scope granted to this role ['descendant', 'children', 'individual'] + // @inject_tag: `gorm:"default:null"` + GrantScope string `protobuf:"bytes,6,opt,name=grant_scope,json=grantScope,proto3" json:"grant_scope,omitempty" gorm:"default:null"` + // timestamp when grant_this_role_scope was last updated + // @inject_tag: `gorm:"default:current_timestamp"` + GrantThisRoleScopeUpdateTime *timestamp.Timestamp `protobuf:"bytes,7,opt,name=grant_this_role_scope_update_time,json=grantThisRoleScopeUpdateTime,proto3" json:"grant_this_role_scope_update_time,omitempty" gorm:"default:current_timestamp"` + // timestamp when grant_scope was last updated + // @inject_tag: `gorm:"default:current_timestamp"` + GrantScopeUpdateTime *timestamp.Timestamp `protobuf:"bytes,8,opt,name=grant_scope_update_time,json=grantScopeUpdateTime,proto3" json:"grant_scope_update_time,omitempty" gorm:"default:current_timestamp"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,9,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,10,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // version allows optimistic locking of the role when modifying the role + // itself and when modifying dependent items like principal roles. + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,11,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GlobalRole) Reset() { + *x = GlobalRole{} + mi := &file_controller_storage_iam_store_v1_role_global_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GlobalRole) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GlobalRole) ProtoMessage() {} + +func (x *GlobalRole) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_role_global_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GlobalRole.ProtoReflect.Descriptor instead. +func (*GlobalRole) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_role_global_proto_rawDescGZIP(), []int{0} +} + +func (x *GlobalRole) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *GlobalRole) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *GlobalRole) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GlobalRole) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *GlobalRole) GetGrantThisRoleScope() bool { + if x != nil { + return x.GrantThisRoleScope + } + return false +} + +func (x *GlobalRole) GetGrantScope() string { + if x != nil { + return x.GrantScope + } + return "" +} + +func (x *GlobalRole) GetGrantThisRoleScopeUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.GrantThisRoleScopeUpdateTime + } + return nil +} + +func (x *GlobalRole) GetGrantScopeUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.GrantScopeUpdateTime + } + return nil +} + +func (x *GlobalRole) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *GlobalRole) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *GlobalRole) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +var File_controller_storage_iam_store_v1_role_global_proto protoreflect.FileDescriptor + +var file_controller_storage_iam_store_v1_role_global_proto_rawDesc = []byte{ + 0x0a, 0x31, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x1f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x69, 0x61, 0x6d, 0x2e, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, + 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, + 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x8c, 0x05, 0x0a, 0x0a, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x52, 0x6f, 0x6c, 0x65, + 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x19, 0x0a, + 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, + 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x31, 0x0a, 0x15, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x74, 0x68, 0x69, 0x73, 0x5f, 0x72, + 0x6f, 0x6c, 0x65, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x12, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x54, 0x68, 0x69, 0x73, 0x52, 0x6f, 0x6c, 0x65, 0x53, 0x63, + 0x6f, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, + 0x63, 0x6f, 0x70, 0x65, 0x12, 0x73, 0x0a, 0x21, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x74, 0x68, + 0x69, 0x73, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, + 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x1c, 0x67, 0x72, 0x61, + 0x6e, 0x74, 0x54, 0x68, 0x69, 0x73, 0x52, 0x6f, 0x6c, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x61, 0x0a, 0x17, 0x67, 0x72, 0x61, + 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x14, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x63, 0x6f, + 0x70, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, + 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_controller_storage_iam_store_v1_role_global_proto_rawDescOnce sync.Once + file_controller_storage_iam_store_v1_role_global_proto_rawDescData = file_controller_storage_iam_store_v1_role_global_proto_rawDesc +) + +func file_controller_storage_iam_store_v1_role_global_proto_rawDescGZIP() []byte { + file_controller_storage_iam_store_v1_role_global_proto_rawDescOnce.Do(func() { + file_controller_storage_iam_store_v1_role_global_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_iam_store_v1_role_global_proto_rawDescData) + }) + return file_controller_storage_iam_store_v1_role_global_proto_rawDescData +} + +var file_controller_storage_iam_store_v1_role_global_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_iam_store_v1_role_global_proto_goTypes = []any{ + (*GlobalRole)(nil), // 0: controller.storage.iam.store.v1.GlobalRole + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_iam_store_v1_role_global_proto_depIdxs = []int32{ + 1, // 0: controller.storage.iam.store.v1.GlobalRole.grant_this_role_scope_update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // 1: controller.storage.iam.store.v1.GlobalRole.grant_scope_update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // 2: controller.storage.iam.store.v1.GlobalRole.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // 3: controller.storage.iam.store.v1.GlobalRole.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_controller_storage_iam_store_v1_role_global_proto_init() } +func file_controller_storage_iam_store_v1_role_global_proto_init() { + if File_controller_storage_iam_store_v1_role_global_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_iam_store_v1_role_global_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_iam_store_v1_role_global_proto_goTypes, + DependencyIndexes: file_controller_storage_iam_store_v1_role_global_proto_depIdxs, + MessageInfos: file_controller_storage_iam_store_v1_role_global_proto_msgTypes, + }.Build() + File_controller_storage_iam_store_v1_role_global_proto = out.File + file_controller_storage_iam_store_v1_role_global_proto_rawDesc = nil + file_controller_storage_iam_store_v1_role_global_proto_goTypes = nil + file_controller_storage_iam_store_v1_role_global_proto_depIdxs = nil +} diff --git a/internal/iam/store/role_global_individual_org_grant_scope.pb.go b/internal/iam/store/role_global_individual_org_grant_scope.pb.go new file mode 100644 index 0000000000..6495ec8c10 --- /dev/null +++ b/internal/iam/store/role_global_individual_org_grant_scope.pb.go @@ -0,0 +1,186 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.1 +// protoc (unknown) +// source: controller/storage/iam/store/v1/role_global_individual_org_grant_scope.proto + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GlobalRoleIndividualOrgGrantScope struct { + state protoimpl.MessageState `protogen:"open.v1"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,1,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // role_id is the ID of the role this is a part of + // @inject_tag: `gorm:"primary_key"` + RoleId string `protobuf:"bytes,2,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty" gorm:"primary_key"` + // scope_id is the string grant scope value as provided by the user + // + // @inject_tag: `gorm:"primary_key"` + ScopeId string `protobuf:"bytes,3,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"primary_key"` + // grant_scope control type of grant scope granted to this role ['individual'] + // + // @inject_tag: `gorm:"default:null"` + GrantScope string `protobuf:"bytes,4,opt,name=grant_scope,json=grantScope,proto3" json:"grant_scope,omitempty" gorm:"default:null"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GlobalRoleIndividualOrgGrantScope) Reset() { + *x = GlobalRoleIndividualOrgGrantScope{} + mi := &file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GlobalRoleIndividualOrgGrantScope) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GlobalRoleIndividualOrgGrantScope) ProtoMessage() {} + +func (x *GlobalRoleIndividualOrgGrantScope) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GlobalRoleIndividualOrgGrantScope.ProtoReflect.Descriptor instead. +func (*GlobalRoleIndividualOrgGrantScope) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDescGZIP(), []int{0} +} + +func (x *GlobalRoleIndividualOrgGrantScope) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *GlobalRoleIndividualOrgGrantScope) GetRoleId() string { + if x != nil { + return x.RoleId + } + return "" +} + +func (x *GlobalRoleIndividualOrgGrantScope) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *GlobalRoleIndividualOrgGrantScope) GetGrantScope() string { + if x != nil { + return x.GrantScope + } + return "" +} + +var File_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto protoreflect.FileDescriptor + +var file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDesc = []byte{ + 0x0a, 0x4c, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x69, 0x6e, + 0x64, 0x69, 0x76, 0x69, 0x64, 0x75, 0x61, 0x6c, 0x5f, 0x6f, 0x72, 0x67, 0x5f, 0x67, 0x72, 0x61, + 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1f, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x2e, 0x69, 0x61, 0x6d, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, + 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, + 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0xc5, 0x01, 0x0a, 0x21, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x52, 0x6f, 0x6c, 0x65, 0x49, + 0x6e, 0x64, 0x69, 0x76, 0x69, 0x64, 0x75, 0x61, 0x6c, 0x4f, 0x72, 0x67, 0x47, 0x72, 0x61, 0x6e, + 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6c, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x74, + 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x67, 0x72, + 0x61, 0x6e, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDescOnce sync.Once + file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDescData = file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDesc +) + +func file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDescGZIP() []byte { + file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDescOnce.Do(func() { + file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDescData) + }) + return file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDescData +} + +var file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_goTypes = []any{ + (*GlobalRoleIndividualOrgGrantScope)(nil), // 0: controller.storage.iam.store.v1.GlobalRoleIndividualOrgGrantScope + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_depIdxs = []int32{ + 1, // 0: controller.storage.iam.store.v1.GlobalRoleIndividualOrgGrantScope.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_init() } +func file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_init() { + if File_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_goTypes, + DependencyIndexes: file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_depIdxs, + MessageInfos: file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_msgTypes, + }.Build() + File_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto = out.File + file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_rawDesc = nil + file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_goTypes = nil + file_controller_storage_iam_store_v1_role_global_individual_org_grant_scope_proto_depIdxs = nil +} diff --git a/internal/iam/store/role_global_individual_project_grant_scope.pb.go b/internal/iam/store/role_global_individual_project_grant_scope.pb.go new file mode 100644 index 0000000000..d1dac5c8c0 --- /dev/null +++ b/internal/iam/store/role_global_individual_project_grant_scope.pb.go @@ -0,0 +1,189 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.1 +// protoc (unknown) +// source: controller/storage/iam/store/v1/role_global_individual_project_grant_scope.proto + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GlobalRoleIndividualProjectGrantScope struct { + state protoimpl.MessageState `protogen:"open.v1"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,1,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // role_id is the ID of the role this is a part of + // @inject_tag: `gorm:"primary_key"` + RoleId string `protobuf:"bytes,2,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty" gorm:"primary_key"` + // scope_id is the string grant scope value as provided by the user + // + // @inject_tag: `gorm:"primary_key"` + ScopeId string `protobuf:"bytes,3,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"primary_key"` + // grant_scope control type of grant scope granted to this role ['children', 'individual'] + // + // @inject_tag: `gorm:"default:null"` + GrantScope string `protobuf:"bytes,4,opt,name=grant_scope,json=grantScope,proto3" json:"grant_scope,omitempty" gorm:"default:null"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GlobalRoleIndividualProjectGrantScope) Reset() { + *x = GlobalRoleIndividualProjectGrantScope{} + mi := &file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GlobalRoleIndividualProjectGrantScope) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GlobalRoleIndividualProjectGrantScope) ProtoMessage() {} + +func (x *GlobalRoleIndividualProjectGrantScope) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GlobalRoleIndividualProjectGrantScope.ProtoReflect.Descriptor instead. +func (*GlobalRoleIndividualProjectGrantScope) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDescGZIP(), []int{0} +} + +func (x *GlobalRoleIndividualProjectGrantScope) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *GlobalRoleIndividualProjectGrantScope) GetRoleId() string { + if x != nil { + return x.RoleId + } + return "" +} + +func (x *GlobalRoleIndividualProjectGrantScope) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *GlobalRoleIndividualProjectGrantScope) GetGrantScope() string { + if x != nil { + return x.GrantScope + } + return "" +} + +var File_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto protoreflect.FileDescriptor + +var file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDesc = []byte{ + 0x0a, 0x50, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x69, 0x6e, + 0x64, 0x69, 0x76, 0x69, 0x64, 0x75, 0x61, 0x6c, 0x5f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x1f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x69, 0x61, 0x6d, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc9, 0x01, 0x0a, 0x25, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x52, + 0x6f, 0x6c, 0x65, 0x49, 0x6e, 0x64, 0x69, 0x76, 0x69, 0x64, 0x75, 0x61, 0x6c, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x4b, + 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x72, + 0x6f, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, + 0x6c, 0x65, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, + 0x1f, 0x0a, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, + 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, + 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDescOnce sync.Once + file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDescData = file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDesc +) + +func file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDescGZIP() []byte { + file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDescOnce.Do(func() { + file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDescData) + }) + return file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDescData +} + +var file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_goTypes = []any{ + (*GlobalRoleIndividualProjectGrantScope)(nil), // 0: controller.storage.iam.store.v1.GlobalRoleIndividualProjectGrantScope + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_depIdxs = []int32{ + 1, // 0: controller.storage.iam.store.v1.GlobalRoleIndividualProjectGrantScope.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { + file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_init() +} +func file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_init() { + if File_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_goTypes, + DependencyIndexes: file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_depIdxs, + MessageInfos: file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_msgTypes, + }.Build() + File_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto = out.File + file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_rawDesc = nil + file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_goTypes = nil + file_controller_storage_iam_store_v1_role_global_individual_project_grant_scope_proto_depIdxs = nil +} diff --git a/internal/iam/store/role_org.pb.go b/internal/iam/store/role_org.pb.go new file mode 100644 index 0000000000..05f2966311 --- /dev/null +++ b/internal/iam/store/role_org.pb.go @@ -0,0 +1,290 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.1 +// protoc (unknown) +// source: controller/storage/iam/store/v1/role_org.proto + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// OrgRole represent roles that are created at an org scope +type OrgRole struct { + state protoimpl.MessageState `protogen:"open.v1"` + // public_id is used to access the Role via an API + // @inject_tag: gorm:"primary_key" + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // scope id for the role + // @inject_tag: `gorm:"default:null"` + ScopeId string `protobuf:"bytes,2,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` + // name is the optional friendly name used to + // access the Role via an API + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description of the role + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // control if this role is granted access to its role scope + // @inject_tag: `gorm:"default:true"` + GrantThisRoleScope bool `protobuf:"varint,5,opt,name=grant_this_role_scope,json=grantThisRoleScope,proto3" json:"grant_this_role_scope,omitempty" gorm:"default:true"` + // control type of grant scope granted to this role ['descendant', 'children', 'individual'] + // @inject_tag: `gorm:"default:null"` + GrantScope string `protobuf:"bytes,6,opt,name=grant_scope,json=grantScope,proto3" json:"grant_scope,omitempty" gorm:"default:null"` + // timestamp when grant_this_role_scope was last updated + // @inject_tag: `gorm:"default:current_timestamp"` + GrantThisRoleScopeUpdateTime *timestamp.Timestamp `protobuf:"bytes,7,opt,name=grant_this_role_scope_update_time,json=grantThisRoleScopeUpdateTime,proto3" json:"grant_this_role_scope_update_time,omitempty" gorm:"default:current_timestamp"` + // timestamp when grant_scope was last updated + // @inject_tag: `gorm:"default:current_timestamp"` + GrantScopeUpdateTime *timestamp.Timestamp `protobuf:"bytes,8,opt,name=grant_scope_update_time,json=grantScopeUpdateTime,proto3" json:"grant_scope_update_time,omitempty" gorm:"default:current_timestamp"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,9,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,10,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // version allows optimistic locking of the role when modifying the role + // itself and when modifying dependent items like principal roles. + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,11,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OrgRole) Reset() { + *x = OrgRole{} + mi := &file_controller_storage_iam_store_v1_role_org_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OrgRole) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OrgRole) ProtoMessage() {} + +func (x *OrgRole) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_role_org_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OrgRole.ProtoReflect.Descriptor instead. +func (*OrgRole) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_role_org_proto_rawDescGZIP(), []int{0} +} + +func (x *OrgRole) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *OrgRole) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *OrgRole) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *OrgRole) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *OrgRole) GetGrantThisRoleScope() bool { + if x != nil { + return x.GrantThisRoleScope + } + return false +} + +func (x *OrgRole) GetGrantScope() string { + if x != nil { + return x.GrantScope + } + return "" +} + +func (x *OrgRole) GetGrantThisRoleScopeUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.GrantThisRoleScopeUpdateTime + } + return nil +} + +func (x *OrgRole) GetGrantScopeUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.GrantScopeUpdateTime + } + return nil +} + +func (x *OrgRole) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *OrgRole) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *OrgRole) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +var File_controller_storage_iam_store_v1_role_org_proto protoreflect.FileDescriptor + +var file_controller_storage_iam_store_v1_role_org_proto_rawDesc = []byte{ + 0x0a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6f, 0x72, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x1f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2e, 0x69, 0x61, 0x6d, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, + 0x31, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, + 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2f, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x89, + 0x05, 0x0a, 0x07, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x49, 0x64, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, + 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x0a, 0x15, 0x67, 0x72, + 0x61, 0x6e, 0x74, 0x5f, 0x74, 0x68, 0x69, 0x73, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x73, 0x63, + 0x6f, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x67, 0x72, 0x61, 0x6e, 0x74, + 0x54, 0x68, 0x69, 0x73, 0x52, 0x6f, 0x6c, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1f, 0x0a, + 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x73, + 0x0a, 0x21, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x74, 0x68, 0x69, 0x73, 0x5f, 0x72, 0x6f, 0x6c, + 0x65, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x1c, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x54, 0x68, 0x69, 0x73, + 0x52, 0x6f, 0x6c, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x61, 0x0a, 0x17, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x14, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_iam_store_v1_role_org_proto_rawDescOnce sync.Once + file_controller_storage_iam_store_v1_role_org_proto_rawDescData = file_controller_storage_iam_store_v1_role_org_proto_rawDesc +) + +func file_controller_storage_iam_store_v1_role_org_proto_rawDescGZIP() []byte { + file_controller_storage_iam_store_v1_role_org_proto_rawDescOnce.Do(func() { + file_controller_storage_iam_store_v1_role_org_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_iam_store_v1_role_org_proto_rawDescData) + }) + return file_controller_storage_iam_store_v1_role_org_proto_rawDescData +} + +var file_controller_storage_iam_store_v1_role_org_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_iam_store_v1_role_org_proto_goTypes = []any{ + (*OrgRole)(nil), // 0: controller.storage.iam.store.v1.OrgRole + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_iam_store_v1_role_org_proto_depIdxs = []int32{ + 1, // 0: controller.storage.iam.store.v1.OrgRole.grant_this_role_scope_update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // 1: controller.storage.iam.store.v1.OrgRole.grant_scope_update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // 2: controller.storage.iam.store.v1.OrgRole.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // 3: controller.storage.iam.store.v1.OrgRole.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_controller_storage_iam_store_v1_role_org_proto_init() } +func file_controller_storage_iam_store_v1_role_org_proto_init() { + if File_controller_storage_iam_store_v1_role_org_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_iam_store_v1_role_org_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_iam_store_v1_role_org_proto_goTypes, + DependencyIndexes: file_controller_storage_iam_store_v1_role_org_proto_depIdxs, + MessageInfos: file_controller_storage_iam_store_v1_role_org_proto_msgTypes, + }.Build() + File_controller_storage_iam_store_v1_role_org_proto = out.File + file_controller_storage_iam_store_v1_role_org_proto_rawDesc = nil + file_controller_storage_iam_store_v1_role_org_proto_goTypes = nil + file_controller_storage_iam_store_v1_role_org_proto_depIdxs = nil +} diff --git a/internal/iam/store/role_org_individual_grant_scope.pb.go b/internal/iam/store/role_org_individual_grant_scope.pb.go new file mode 100644 index 0000000000..6084d5aedc --- /dev/null +++ b/internal/iam/store/role_org_individual_grant_scope.pb.go @@ -0,0 +1,185 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.1 +// protoc (unknown) +// source: controller/storage/iam/store/v1/role_org_individual_grant_scope.proto + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type OrgRoleIndividualGrantScope struct { + state protoimpl.MessageState `protogen:"open.v1"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,1,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // role_id is the ID of the role this is a part of + // @inject_tag: `gorm:"primary_key"` + RoleId string `protobuf:"bytes,2,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty" gorm:"primary_key"` + // scope_id is the string grant scope value as provided by the user + // + // @inject_tag: `gorm:"primary_key"` + ScopeId string `protobuf:"bytes,3,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"primary_key"` + // grant_scope control type of grant scope granted to this role ['individual'] + // + // @inject_tag: `gorm:"default:null"` + GrantScope string `protobuf:"bytes,4,opt,name=grant_scope,json=grantScope,proto3" json:"grant_scope,omitempty" gorm:"default:null"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OrgRoleIndividualGrantScope) Reset() { + *x = OrgRoleIndividualGrantScope{} + mi := &file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OrgRoleIndividualGrantScope) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OrgRoleIndividualGrantScope) ProtoMessage() {} + +func (x *OrgRoleIndividualGrantScope) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OrgRoleIndividualGrantScope.ProtoReflect.Descriptor instead. +func (*OrgRoleIndividualGrantScope) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDescGZIP(), []int{0} +} + +func (x *OrgRoleIndividualGrantScope) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *OrgRoleIndividualGrantScope) GetRoleId() string { + if x != nil { + return x.RoleId + } + return "" +} + +func (x *OrgRoleIndividualGrantScope) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *OrgRoleIndividualGrantScope) GetGrantScope() string { + if x != nil { + return x.GrantScope + } + return "" +} + +var File_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto protoreflect.FileDescriptor + +var file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDesc = []byte{ + 0x0a, 0x45, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x6e, 0x64, 0x69, 0x76, + 0x69, 0x64, 0x75, 0x61, 0x6c, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x69, 0x61, 0x6d, 0x2e, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbf, 0x01, 0x0a, 0x1b, 0x4f, 0x72, + 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x49, 0x6e, 0x64, 0x69, 0x76, 0x69, 0x64, 0x75, 0x61, 0x6c, 0x47, + 0x72, 0x61, 0x6e, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6c, 0x65, 0x49, 0x64, 0x12, + 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x67, 0x72, + 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDescOnce sync.Once + file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDescData = file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDesc +) + +func file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDescGZIP() []byte { + file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDescOnce.Do(func() { + file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDescData) + }) + return file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDescData +} + +var file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_goTypes = []any{ + (*OrgRoleIndividualGrantScope)(nil), // 0: controller.storage.iam.store.v1.OrgRoleIndividualGrantScope + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_depIdxs = []int32{ + 1, // 0: controller.storage.iam.store.v1.OrgRoleIndividualGrantScope.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_init() } +func file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_init() { + if File_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_goTypes, + DependencyIndexes: file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_depIdxs, + MessageInfos: file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_msgTypes, + }.Build() + File_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto = out.File + file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_rawDesc = nil + file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_goTypes = nil + file_controller_storage_iam_store_v1_role_org_individual_grant_scope_proto_depIdxs = nil +} diff --git a/internal/iam/store/role_project.pb.go b/internal/iam/store/role_project.pb.go new file mode 100644 index 0000000000..76d3f9efa1 --- /dev/null +++ b/internal/iam/store/role_project.pb.go @@ -0,0 +1,230 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.1 +// protoc (unknown) +// source: controller/storage/iam/store/v1/role_project.proto + +package store + +import ( + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ProjectRole represent roles that are created at a project scope +type ProjectRole struct { + state protoimpl.MessageState `protogen:"open.v1"` + // public_id is used to access the Role via an API + // @inject_tag: gorm:"primary_key" + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // scope id for the role + // @inject_tag: `gorm:"default:null"` + ScopeId string `protobuf:"bytes,2,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` + // name is the optional friendly name used to + // access the Role via an API + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description of the role + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,6,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // version allows optimistic locking of the role when modifying the role + // itself and when modifying dependent items like principal roles. + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProjectRole) Reset() { + *x = ProjectRole{} + mi := &file_controller_storage_iam_store_v1_role_project_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProjectRole) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectRole) ProtoMessage() {} + +func (x *ProjectRole) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_role_project_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProjectRole.ProtoReflect.Descriptor instead. +func (*ProjectRole) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_role_project_proto_rawDescGZIP(), []int{0} +} + +func (x *ProjectRole) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *ProjectRole) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *ProjectRole) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ProjectRole) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ProjectRole) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *ProjectRole) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *ProjectRole) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +var File_controller_storage_iam_store_v1_role_project_proto protoreflect.FileDescriptor + +var file_controller_storage_iam_store_v1_role_project_proto_rawDesc = []byte{ + 0x0a, 0x32, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x69, 0x61, 0x6d, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, + 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0xe1, 0x02, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x6f, + 0x6c, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, + 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_iam_store_v1_role_project_proto_rawDescOnce sync.Once + file_controller_storage_iam_store_v1_role_project_proto_rawDescData = file_controller_storage_iam_store_v1_role_project_proto_rawDesc +) + +func file_controller_storage_iam_store_v1_role_project_proto_rawDescGZIP() []byte { + file_controller_storage_iam_store_v1_role_project_proto_rawDescOnce.Do(func() { + file_controller_storage_iam_store_v1_role_project_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_iam_store_v1_role_project_proto_rawDescData) + }) + return file_controller_storage_iam_store_v1_role_project_proto_rawDescData +} + +var file_controller_storage_iam_store_v1_role_project_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_iam_store_v1_role_project_proto_goTypes = []any{ + (*ProjectRole)(nil), // 0: controller.storage.iam.store.v1.ProjectRole + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_iam_store_v1_role_project_proto_depIdxs = []int32{ + 1, // 0: controller.storage.iam.store.v1.ProjectRole.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // 1: controller.storage.iam.store.v1.ProjectRole.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_controller_storage_iam_store_v1_role_project_proto_init() } +func file_controller_storage_iam_store_v1_role_project_proto_init() { + if File_controller_storage_iam_store_v1_role_project_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_iam_store_v1_role_project_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_iam_store_v1_role_project_proto_goTypes, + DependencyIndexes: file_controller_storage_iam_store_v1_role_project_proto_depIdxs, + MessageInfos: file_controller_storage_iam_store_v1_role_project_proto_msgTypes, + }.Build() + File_controller_storage_iam_store_v1_role_project_proto = out.File + file_controller_storage_iam_store_v1_role_project_proto_rawDesc = nil + file_controller_storage_iam_store_v1_role_project_proto_goTypes = nil + file_controller_storage_iam_store_v1_role_project_proto_depIdxs = nil +} diff --git a/internal/iam/testing.go b/internal/iam/testing.go index 970a920acb..a3303fdb29 100644 --- a/internal/iam/testing.go +++ b/internal/iam/testing.go @@ -6,12 +6,15 @@ package iam import ( "context" "crypto/rand" + "slices" + "strings" "testing" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/store" "github.com/hashicorp/boundary/internal/db" dbassert "github.com/hashicorp/boundary/internal/db/assert" + iamstore "github.com/hashicorp/boundary/internal/iam/store" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/types/scope" wrapping "github.com/hashicorp/go-kms-wrapping/v2" @@ -197,25 +200,65 @@ func TestRole(t testing.TB, conn *db.DB, scopeId string, opt ...Option) *Role { require := require.New(t) rw := db.New(conn) - role, err := NewRole(ctx, scopeId, opt...) - require.NoError(err) id, err := newRoleId(ctx) require.NoError(err) - role.PublicId = id - require.NoError(rw.Create(ctx, role)) - require.NotEmpty(role.PublicId) - grantScopeIds := opts.withGrantScopeIds - if len(grantScopeIds) == 0 { - grantScopeIds = []string{globals.GrantScopeThis} + grantsThis := false + if len(grantScopeIds) == 0 || slices.Contains(grantScopeIds, globals.GrantScopeThis) { + grantsThis = true + } + var role *Role + switch { + case strings.HasPrefix(scopeId, globals.GlobalPrefix): + g := &globalRole{ + GlobalRole: &iamstore.GlobalRole{ + PublicId: id, + ScopeId: scopeId, + Name: opts.withName, + Description: opts.withDescription, + GrantThisRoleScope: grantsThis, + GrantScope: globals.GrantScopeIndividual, // handled by TestRoleGrantScope later + }, + } + require.NoError(rw.Create(ctx, g)) + require.NotEmpty(g.PublicId) + role = g.toRole() + case strings.HasPrefix(scopeId, globals.OrgPrefix): + o := &orgRole{ + OrgRole: &iamstore.OrgRole{ + PublicId: id, + ScopeId: scopeId, + Name: opts.withName, + Description: opts.withDescription, + GrantThisRoleScope: grantsThis, + GrantScope: globals.GrantScopeIndividual, // handled by TestRoleGrantScope later + }, + } + require.NoError(rw.Create(ctx, o)) + require.NotEmpty(o.PublicId) + role = o.toRole() + case strings.HasPrefix(scopeId, globals.ProjectPrefix): + p := &projectRole{ + ProjectRole: &iamstore.ProjectRole{ + PublicId: id, + ScopeId: scopeId, + Name: opts.withName, + Description: opts.withDescription, + }, + } + require.NoError(rw.Create(ctx, p)) + require.NotEmpty(p.PublicId) + role = p.toRole() + default: + t.Logf("invalid scope id: %s", scopeId) + t.FailNow() } + for _, gsi := range grantScopeIds { if gsi == "testing-none" { continue } - gs, err := NewRoleGrantScope(ctx, id, gsi) - require.NoError(err) - require.NoError(rw.Create(ctx, gs)) + gs := TestRoleGrantScope(t, conn, role, gsi) role.GrantScopes = append(role.GrantScopes, gs) } require.Equal(opts.withDescription, role.Description) @@ -228,23 +271,60 @@ func TestRole(t testing.TB, conn *db.DB, scopeId string, opt ...Option) *Role { // this function does not provide any default grant scope unlike TestRole func TestRoleWithGrants(t testing.TB, conn *db.DB, scopeId string, grantScopeIDs []string, grants []string) *Role { t.Helper() - ctx := context.Background() require := require.New(t) rw := db.New(conn) - - role, err := NewRole(ctx, scopeId) - require.NoError(err) + grantsThis := false + if slices.Contains(grantScopeIDs, globals.GrantScopeThis) || len(grantScopeIDs) == 0 { + grantsThis = true + } id, err := newRoleId(ctx) require.NoError(err) - role.PublicId = id - require.NoError(rw.Create(ctx, role)) - require.NotEmpty(role.PublicId) - + var role *Role + switch { + case strings.HasPrefix(scopeId, globals.GlobalPrefix): + g := &globalRole{ + GlobalRole: &iamstore.GlobalRole{ + PublicId: id, + ScopeId: scopeId, + GrantThisRoleScope: grantsThis, + GrantScope: globals.GrantScopeIndividual, // handled by TestRoleGrantScope call after this + }, + } + require.NoError(rw.Create(ctx, g)) + require.NotEmpty(g.PublicId) + role = g.toRole() + case strings.HasPrefix(scopeId, globals.OrgPrefix): + o := &orgRole{ + OrgRole: &iamstore.OrgRole{ + PublicId: id, + ScopeId: scopeId, + GrantThisRoleScope: grantsThis, + GrantScope: globals.GrantScopeIndividual, // handled by TestRoleGrantScope call after this + }, + } + require.NoError(rw.Create(ctx, o)) + require.NotEmpty(o.PublicId) + role = o.toRole() + case strings.HasPrefix(scopeId, globals.ProjectPrefix): + p := &projectRole{ + ProjectRole: &iamstore.ProjectRole{ + PublicId: id, + ScopeId: scopeId, + }, + } + require.NoError(rw.Create(ctx, p)) + require.NotEmpty(p.PublicId) + role = p.toRole() + default: + t.Logf("invalid scope id: %s", scopeId) + t.FailNow() + } for _, gsi := range grantScopeIDs { - gs, err := NewRoleGrantScope(ctx, id, gsi) - require.NoError(err) - require.NoError(rw.Create(ctx, gs)) + if gsi == "testing-none" { + continue + } + gs := TestRoleGrantScope(t, conn, role, gsi) role.GrantScopes = append(role.GrantScopes, gs) } for _, g := range grants { @@ -265,15 +345,170 @@ func TestRoleGrant(t testing.TB, conn *db.DB, roleId, grant string, opt ...Optio return g } -func TestRoleGrantScope(t testing.TB, conn *db.DB, roleId, grantScopeId string, opt ...Option) *RoleGrantScope { +func TestRoleGrantScope(t testing.TB, conn *db.DB, r *Role, grantScopeId string, opt ...Option) *RoleGrantScope { t.Helper() - require := require.New(t) + switch grantScopeId { + case globals.GrantScopeThis: + return testRoleGrantScopeThis(t, conn, r) + case globals.GrantScopeDescendants, globals.GrantScopeChildren: + return testRoleGrantScopeSpecial(t, conn, r, grantScopeId) + default: + return testRoleGrantScopeIndividual(t, conn, r, grantScopeId) + } +} + +// testRoleGrantScopeThis is a utility function for adding 'this' to a role's Grant scopes +// this function is not meant to be called directly - use `TestRoleGrantScope` or `TestRoleWithGrants` +func testRoleGrantScopeThis(t testing.TB, conn *db.DB, r *Role) *RoleGrantScope { rw := db.New(conn) + ctx := context.Background() + var result *RoleGrantScope + switch { + case strings.HasPrefix(r.ScopeId, globals.GlobalPrefix): + g := allocGlobalRole() + g.PublicId = r.PublicId + g.GrantThisRoleScope = true + _, err := rw.Update(ctx, &g, []string{"GrantThisRoleScope"}, []string{}) + require.NoError(t, err) + result = &RoleGrantScope{ + CreateTime: g.GrantThisRoleScopeUpdateTime, + RoleId: g.PublicId, + ScopeIdOrSpecial: globals.GrantScopeThis, + } + case strings.HasPrefix(r.ScopeId, globals.OrgPrefix): + o := allocOrgRole() + o.PublicId = r.PublicId + o.GrantThisRoleScope = true + _, err := rw.Update(ctx, &o, []string{"GrantThisRoleScope"}, []string{}) + require.NoError(t, err) + result = &RoleGrantScope{ + CreateTime: o.GrantThisRoleScopeUpdateTime, + RoleId: o.PublicId, + ScopeIdOrSpecial: globals.GrantScopeThis, + } + case strings.HasPrefix(r.ScopeId, globals.ProjectPrefix): + // roles in project scopes are automatically granted 'this' + result = &RoleGrantScope{ + CreateTime: r.CreateTime, + RoleId: r.PublicId, + ScopeIdOrSpecial: globals.GrantScopeThis, + } + default: + t.Logf("invalid scope type for this grant: %s", r.ScopeId) + t.FailNow() + } + return result +} - gs, err := NewRoleGrantScope(context.Background(), roleId, grantScopeId, opt...) - require.NoError(err) - require.NoError(rw.Create(context.Background(), gs)) - return gs +// testRoleGrantScopeThis is a utility function for adding special scopes (children, descendants) to a role's Grant scopes +// this function is not meant to be called directly - use `TestRoleGrantScope` or `TestRoleWithGrants` +func testRoleGrantScopeSpecial(t testing.TB, conn *db.DB, r *Role, grantScopeId string) *RoleGrantScope { + rw := db.New(conn) + ctx := context.Background() + allowedGrantScopeId := []string{ + globals.GrantScopeChildren, + globals.GrantScopeDescendants, + } + // ensure that only special scopes are passed in here + require.Contains(t, allowedGrantScopeId, grantScopeId) + var result *RoleGrantScope + switch { + case strings.HasPrefix(r.ScopeId, globals.GlobalPrefix): + g := allocGlobalRole() + g.PublicId = r.PublicId + g.GrantScope = grantScopeId + _, err := rw.Update(ctx, &g, []string{"GrantScope"}, []string{}) + require.NoError(t, err) + result = &RoleGrantScope{ + CreateTime: g.GrantScopeUpdateTime, + RoleId: g.PublicId, + ScopeIdOrSpecial: grantScopeId, + } + case strings.HasPrefix(r.ScopeId, globals.OrgPrefix): + // 'descendants' grants isn't allowed for org but not handling that case to reduce code duplication and + // the constraint check in the DB will return an error anyway + o := allocOrgRole() + o.PublicId = r.PublicId + o.GrantScope = grantScopeId + _, err := rw.Update(ctx, &o, []string{"GrantScope"}, []string{}) + require.NoError(t, err) + result = &RoleGrantScope{ + CreateTime: o.GrantThisRoleScopeUpdateTime, + RoleId: o.PublicId, + ScopeIdOrSpecial: grantScopeId, + } + default: + t.Logf("invalid scope type for children grant: %s", r.ScopeId) + t.FailNow() + } + return result +} + +func testRoleGrantScopeIndividual(t testing.TB, conn *db.DB, r *Role, grantScopeId string) *RoleGrantScope { + rw := db.New(conn) + ctx := context.Background() + + var result *RoleGrantScope + switch { + case strings.HasPrefix(r.ScopeId, globals.GlobalPrefix): + // perform a read to get 'role.GrantScope' because there are two allowed values: [children, individual] + g := allocGlobalRole() + g.PublicId = r.PublicId + require.NoError(t, rw.LookupByPublicId(ctx, &g)) + switch { + case strings.HasPrefix(grantScopeId, globals.OrgPrefix): + orgGrantScope := &globalRoleIndividualOrgGrantScope{ + GlobalRoleIndividualOrgGrantScope: &iamstore.GlobalRoleIndividualOrgGrantScope{ + RoleId: g.PublicId, + ScopeId: grantScopeId, + GrantScope: g.GrantScope, + }, + } + require.NoError(t, rw.Create(ctx, orgGrantScope)) + result = &RoleGrantScope{ + CreateTime: orgGrantScope.CreateTime, + RoleId: orgGrantScope.RoleId, + ScopeIdOrSpecial: orgGrantScope.ScopeId, + } + case strings.HasPrefix(grantScopeId, globals.ProjectPrefix): + projGrantScope := &globalRoleIndividualProjectGrantScope{ + GlobalRoleIndividualProjectGrantScope: &iamstore.GlobalRoleIndividualProjectGrantScope{ + RoleId: g.PublicId, + ScopeId: grantScopeId, + GrantScope: g.GrantScope, + }, + } + require.NoError(t, rw.Create(ctx, projGrantScope)) + result = &RoleGrantScope{ + CreateTime: projGrantScope.CreateTime, + RoleId: projGrantScope.RoleId, + ScopeIdOrSpecial: projGrantScope.ScopeId, + } + default: + t.Logf("invalid scope id for global role invidual grant scope: %s", grantScopeId) + t.FailNow() + } + case strings.HasPrefix(r.ScopeId, globals.OrgPrefix): + o := &orgRoleIndividualGrantScope{ + OrgRoleIndividualGrantScope: &iamstore.OrgRoleIndividualGrantScope{ + RoleId: r.PublicId, + ScopeId: grantScopeId, + GrantScope: globals.GrantScopeIndividual, + }, + } + require.NoError(t, rw.Create(ctx, o)) + result = &RoleGrantScope{ + CreateTime: o.CreateTime, + RoleId: o.RoleId, + ScopeIdOrSpecial: o.ScopeId, + } + default: + t.Logf("invalid scope for individual scope grant: %s", r.ScopeId) + t.FailNow() + return nil + } + + return result } // TestGroup creates a group suitable for testing. diff --git a/internal/iam/user.go b/internal/iam/user.go index 88f4ba11de..65fee86260 100644 --- a/internal/iam/user.go +++ b/internal/iam/user.go @@ -12,7 +12,6 @@ import ( "github.com/hashicorp/boundary/internal/iam/store" "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" - "github.com/hashicorp/boundary/internal/types/scope" "google.golang.org/protobuf/proto" ) @@ -81,8 +80,8 @@ func (u *User) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, o return nil } -func (u *User) validScopeTypes() []scope.Type { - return []scope.Type{scope.Global, scope.Org} +func (u *User) getResourceType() resource.Type { + return resource.User } // GetScope returns the scope for the User diff --git a/internal/perms/acl.go b/internal/perms/acl.go index 694c6cff88..ab20af5d07 100644 --- a/internal/perms/acl.go +++ b/internal/perms/acl.go @@ -333,7 +333,7 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option r.Id == "" && grant.Type == r.Type && grant.Type != resource.Unknown && - resource.TopLevelType(r.Type) && + r.Type.TopLevelType() && (action.List.IsActionOrParent(aType) || action.Create.IsActionOrParent(aType)): @@ -357,7 +357,7 @@ func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option grant.Id == r.Pin && grant.Type != resource.Unknown && (grant.Type == r.Type || grant.Type == resource.All) && - !resource.TopLevelType(r.Type): + !r.Type.TopLevelType(): found = true } diff --git a/internal/perms/grants.go b/internal/perms/grants.go index d8662a1f7a..e8ca1fef1c 100644 --- a/internal/perms/grants.go +++ b/internal/perms/grants.go @@ -603,12 +603,12 @@ func Parse(ctx context.Context, tuple GrantTuple, opt ...Option) (Grant, error) } case resource.All: // Verify that the ID is a type that has child types - if !resource.HasChildTypes(idType) { + if !idType.HasChildTypes() { return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains an id that does not support child types", grant.CanonicalString())) } default: // Specified resource type, verify it's a child - if resource.Parent(grant.typ) != idType { + if grant.typ.Parent() != idType { return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains type %s that is not a child type of the type (%s) of the specified id", grant.CanonicalString(), grant.typ.String(), idType.String())) } } @@ -681,7 +681,7 @@ func Parse(ctx context.Context, tuple GrantTuple, opt ...Option) (Grant, error) Type: grant.typ, ParentScopeId: parentScopeId, } - if !resource.TopLevelType(grant.typ) { + if !grant.typ.TopLevelType() { r.Pin = grantIds[i] } for k := range grant.actions { diff --git a/internal/proto/controller/api/services/v1/scope_service.proto b/internal/proto/controller/api/services/v1/scope_service.proto index 316e1362f5..5d963282f1 100644 --- a/internal/proto/controller/api/services/v1/scope_service.proto +++ b/internal/proto/controller/api/services/v1/scope_service.proto @@ -203,6 +203,8 @@ message CreateScopeRequest { bool skip_admin_role_creation = 1; // @gotags: `class:"public"` bool skip_default_role_creation = 2; // @gotags: `class:"public"` resources.scopes.v1.Scope item = 3; + bool create_admin_role = 4; // @gotags: `class:"public"` + bool create_default_role = 5; // @gotags: `class:"public"` } message CreateScopeResponse { diff --git a/internal/proto/controller/storage/iam/store/v1/role_global.proto b/internal/proto/controller/storage/iam/store/v1/role_global.proto new file mode 100644 index 0000000000..bc042247d8 --- /dev/null +++ b/internal/proto/controller/storage/iam/store/v1/role_global.proto @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +syntax = "proto3"; + +package controller.storage.iam.store.v1; + +import "controller/custom_options/v1/options.proto"; +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/iam/store;store"; + +// GlobalRole represents roles created in global scope +message GlobalRole { + // public_id is used to access the Role via an API + // @inject_tag: gorm:"primary_key" + string public_id = 1; + + // scope id for the role + // @inject_tag: `gorm:"default:null"` + string scope_id = 2; + + // name is the optional friendly name used to + // access the Role via an API + // @inject_tag: `gorm:"default:null"` + string name = 3 [(custom_options.v1.mask_mapping) = { + this: "name" + that: "name" + }]; + + // description of the role + // @inject_tag: `gorm:"default:null"` + string description = 4 [(custom_options.v1.mask_mapping) = { + this: "description" + that: "description" + }]; + + // control if this role is granted access to its role scope + // @inject_tag: `gorm:"default:true"` + bool grant_this_role_scope = 5; + + // control type of grant scope granted to this role ['descendant', 'children', 'individual'] + // @inject_tag: `gorm:"default:null"` + string grant_scope = 6; + + // timestamp when grant_this_role_scope was last updated + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp grant_this_role_scope_update_time = 7; + + // timestamp when grant_scope was last updated + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp grant_scope_update_time = 8; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 9; + + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 10; + + // version allows optimistic locking of the role when modifying the role + // itself and when modifying dependent items like principal roles. + // @inject_tag: `gorm:"default:null"` + uint32 version = 11; +} diff --git a/internal/proto/controller/storage/iam/store/v1/role_global_individual_org_grant_scope.proto b/internal/proto/controller/storage/iam/store/v1/role_global_individual_org_grant_scope.proto new file mode 100644 index 0000000000..4374afcb40 --- /dev/null +++ b/internal/proto/controller/storage/iam/store/v1/role_global_individual_org_grant_scope.proto @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +syntax = "proto3"; + +package controller.storage.iam.store.v1; + +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/iam/store;store"; + +message GlobalRoleIndividualOrgGrantScope { + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 1; + + // role_id is the ID of the role this is a part of + // @inject_tag: `gorm:"primary_key"` + string role_id = 2; + + // scope_id is the string grant scope value as provided by the user + // + // @inject_tag: `gorm:"primary_key"` + string scope_id = 3; + + // grant_scope control type of grant scope granted to this role ['individual'] + // + // @inject_tag: `gorm:"default:null"` + string grant_scope = 4; +} diff --git a/internal/proto/controller/storage/iam/store/v1/role_global_individual_project_grant_scope.proto b/internal/proto/controller/storage/iam/store/v1/role_global_individual_project_grant_scope.proto new file mode 100644 index 0000000000..b32a5d9d57 --- /dev/null +++ b/internal/proto/controller/storage/iam/store/v1/role_global_individual_project_grant_scope.proto @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +syntax = "proto3"; + +package controller.storage.iam.store.v1; + +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/iam/store;store"; + +message GlobalRoleIndividualProjectGrantScope { + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 1; + + // role_id is the ID of the role this is a part of + // @inject_tag: `gorm:"primary_key"` + string role_id = 2; + + // scope_id is the string grant scope value as provided by the user + // + // @inject_tag: `gorm:"primary_key"` + string scope_id = 3; + + // grant_scope control type of grant scope granted to this role ['children', 'individual'] + // + // @inject_tag: `gorm:"default:null"` + string grant_scope = 4; +} diff --git a/internal/proto/controller/storage/iam/store/v1/role_org.proto b/internal/proto/controller/storage/iam/store/v1/role_org.proto new file mode 100644 index 0000000000..7d550a77a7 --- /dev/null +++ b/internal/proto/controller/storage/iam/store/v1/role_org.proto @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +syntax = "proto3"; + +package controller.storage.iam.store.v1; + +import "controller/custom_options/v1/options.proto"; +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/iam/store;store"; + +// OrgRole represent roles that are created at an org scope +message OrgRole { + // public_id is used to access the Role via an API + // @inject_tag: gorm:"primary_key" + string public_id = 1; + + // scope id for the role + // @inject_tag: `gorm:"default:null"` + string scope_id = 2; + + // name is the optional friendly name used to + // access the Role via an API + // @inject_tag: `gorm:"default:null"` + string name = 3 [(custom_options.v1.mask_mapping) = { + this: "name" + that: "name" + }]; + + // description of the role + // @inject_tag: `gorm:"default:null"` + string description = 4 [(custom_options.v1.mask_mapping) = { + this: "description" + that: "description" + }]; + + // control if this role is granted access to its role scope + // @inject_tag: `gorm:"default:true"` + bool grant_this_role_scope = 5; + + // control type of grant scope granted to this role ['descendant', 'children', 'individual'] + // @inject_tag: `gorm:"default:null"` + string grant_scope = 6; + + // timestamp when grant_this_role_scope was last updated + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp grant_this_role_scope_update_time = 7; + + // timestamp when grant_scope was last updated + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp grant_scope_update_time = 8; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 9; + + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 10; + + // version allows optimistic locking of the role when modifying the role + // itself and when modifying dependent items like principal roles. + // @inject_tag: `gorm:"default:null"` + uint32 version = 11; +} diff --git a/internal/proto/controller/storage/iam/store/v1/role_org_individual_grant_scope.proto b/internal/proto/controller/storage/iam/store/v1/role_org_individual_grant_scope.proto new file mode 100644 index 0000000000..b93a0a0ffa --- /dev/null +++ b/internal/proto/controller/storage/iam/store/v1/role_org_individual_grant_scope.proto @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +syntax = "proto3"; + +package controller.storage.iam.store.v1; + +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/iam/store;store"; + +message OrgRoleIndividualGrantScope { + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 1; + + // role_id is the ID of the role this is a part of + // @inject_tag: `gorm:"primary_key"` + string role_id = 2; + + // scope_id is the string grant scope value as provided by the user + // + // @inject_tag: `gorm:"primary_key"` + string scope_id = 3; + + // grant_scope control type of grant scope granted to this role ['individual'] + // + // @inject_tag: `gorm:"default:null"` + string grant_scope = 4; +} diff --git a/internal/proto/controller/storage/iam/store/v1/role_project.proto b/internal/proto/controller/storage/iam/store/v1/role_project.proto new file mode 100644 index 0000000000..d996e28c70 --- /dev/null +++ b/internal/proto/controller/storage/iam/store/v1/role_project.proto @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +syntax = "proto3"; + +package controller.storage.iam.store.v1; + +import "controller/custom_options/v1/options.proto"; +import "controller/storage/timestamp/v1/timestamp.proto"; + +option go_package = "github.com/hashicorp/boundary/internal/iam/store;store"; + +// ProjectRole represent roles that are created at a project scope +message ProjectRole { + // public_id is used to access the Role via an API + // @inject_tag: gorm:"primary_key" + string public_id = 1; + + // scope id for the role + // @inject_tag: `gorm:"default:null"` + string scope_id = 2; + + // name is the optional friendly name used to + // access the Role via an API + // @inject_tag: `gorm:"default:null"` + string name = 3 [(custom_options.v1.mask_mapping) = { + this: "name" + that: "name" + }]; + + // description of the role + // @inject_tag: `gorm:"default:null"` + string description = 4 [(custom_options.v1.mask_mapping) = { + this: "description" + that: "description" + }]; + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 5; + + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 6; + + // version allows optimistic locking of the role when modifying the role + // itself and when modifying dependent items like principal roles. + // @inject_tag: `gorm:"default:null"` + uint32 version = 7; +} diff --git a/internal/types/resource/resource.go b/internal/types/resource/resource.go index 247a81c2a1..0d6af3152d 100644 --- a/internal/types/resource/resource.go +++ b/internal/types/resource/resource.go @@ -45,6 +45,7 @@ const ( // * The Test_AnonRestrictions test: update the following line to include the last resource: // for i := resource.Type(1); i <= resource.; i++ { // * The prefixes and mappings in globals/prefixes.go + // * The AllowedIn function & its test in the scope package ) func (r Type) MarshalJSON() ([]byte, error) { @@ -142,8 +143,8 @@ var Map = map[string]Type{ // Parent returns the parent type for a given type; if there is no parent, it // returns the incoming type -func Parent(in Type) Type { - switch in { +func (r Type) Parent() Type { + switch r { case Account, ManagedGroup: return AuthMethod case HostSet, Host: @@ -151,13 +152,13 @@ func Parent(in Type) Type { case CredentialLibrary, Credential: return CredentialStore } - return in + return r } // HasChildTypes indicates whether this is a type that has child resource types; // it's essentially the inverse of Parent -func HasChildTypes(in Type) bool { - switch in { +func (r Type) HasChildTypes() bool { + switch r { case AuthMethod, HostCatalog, CredentialStore: return true } @@ -166,8 +167,8 @@ func HasChildTypes(in Type) bool { // TopLevelType indicates whether this is a type that supports collection // actions, e.g. Create/List -func TopLevelType(typ Type) bool { - switch typ { +func (r Type) TopLevelType() bool { + switch r { case AuthMethod, AuthToken, CredentialStore, diff --git a/internal/types/resource/resource_test.go b/internal/types/resource/resource_test.go index 5f302c57f3..00933cb50a 100644 --- a/internal/types/resource/resource_test.go +++ b/internal/types/resource/resource_test.go @@ -137,9 +137,9 @@ func Test_Resource(t *testing.T) { t.Run(tt.typeString, func(t *testing.T) { assert.Equalf(t, tt.want, Map[tt.typeString], "unexpected type for %s", tt.typeString) assert.Equalf(t, tt.typeString, tt.want.String(), "unexpected string for %s", tt.typeString) - assert.Equalf(t, tt.topLevelType, TopLevelType(tt.want), "unexpected top level type types for %s", tt.typeString) - assert.Equalf(t, tt.hasChildTypes, HasChildTypes(tt.want), "unexpected has child types for %s", tt.typeString) - parent := Parent(tt.want) + assert.Equalf(t, tt.topLevelType, tt.want.TopLevelType(), "unexpected top level type types for %s", tt.typeString) + assert.Equalf(t, tt.hasChildTypes, tt.want.HasChildTypes(), "unexpected has child types for %s", tt.typeString) + parent := tt.want.Parent() if tt.parent == Unknown { assert.Equal(t, tt.want, parent) } else { diff --git a/internal/types/scope/scope.go b/internal/types/scope/scope.go index 1e77e0840d..94a642a315 100644 --- a/internal/types/scope/scope.go +++ b/internal/types/scope/scope.go @@ -3,16 +3,23 @@ package scope -import "github.com/hashicorp/boundary/globals" +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/types/resource" +) // Type defines the possible types for Scopes type Type uint const ( - Unknown Type = 0 - Global Type = 1 - Org Type = 2 - Project Type = 3 + Unknown Type = iota + Global + Org + Project ) func (s Type) String() string { @@ -38,3 +45,24 @@ var Map = map[string]Type{ Org.String(): Org, Project.String(): Project, } + +// AllowedIn returns the set of Scopes a known Resource type is allowed in. +func AllowedIn(ctx context.Context, r resource.Type) ([]Type, error) { + const op = "scope.AllowedIn" + switch r { + case resource.Alias, resource.Billing, resource.Worker: + return []Type{Global}, nil + case resource.Account, resource.AuthMethod, resource.AuthToken, resource.ManagedGroup, resource.Policy, resource.Scope, resource.SessionRecording, resource.StorageBucket, resource.User: + return []Type{Global, Org}, nil + case resource.Group, resource.Role: + return []Type{Global, Org, Project}, nil + case resource.CredentialLibrary, resource.Credential, resource.CredentialStore, resource.HostCatalog, resource.HostSet, resource.Host, resource.Session, resource.Target: + return []Type{Project}, nil + case resource.Unknown: + return nil, errors.New(ctx, errors.InvalidParameter, op, "unknown resource type") + case resource.All: + return nil, errors.New(ctx, errors.InvalidParameter, op, "resource type '*' is not supported") + default: + return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid resource type: %d", r)) + } +} diff --git a/internal/types/scope/scope_test.go b/internal/types/scope/scope_test.go index 42c426197c..5e39c8ff57 100644 --- a/internal/types/scope/scope_test.go +++ b/internal/types/scope/scope_test.go @@ -4,10 +4,14 @@ package scope import ( + "context" "testing" "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/types/resource" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_Map(t *testing.T) { @@ -51,3 +55,150 @@ func Test_Map(t *testing.T) { }) } } + +func Test_AllowedIn(t *testing.T) { + tests := []struct { + testName string + resource resource.Type + wantScopes []Type + wantErr error + }{ + { + testName: "Account", + resource: resource.Account, + wantScopes: []Type{Global, Org}, + }, + { + testName: "Alias", + resource: resource.Alias, + wantScopes: []Type{Global}, + }, + { + testName: "All", + resource: resource.All, + wantErr: errors.New(context.Background(), errors.InvalidParameter, "scope.AllowedIn", "resource type '*' is not supported"), + }, + { + testName: "AuthMethod", + resource: resource.AuthMethod, + wantScopes: []Type{Global, Org}, + }, + { + testName: "AuthToken", + resource: resource.AuthToken, + wantScopes: []Type{Global, Org}, + }, + { + testName: "Billing", + resource: resource.Billing, + wantScopes: []Type{Global}, + }, + { + testName: "CredentialLibrary", + resource: resource.CredentialLibrary, + wantScopes: []Type{Project}, + }, + { + testName: "Credential", + resource: resource.Credential, + wantScopes: []Type{Project}, + }, + { + testName: "CredentialStore", + resource: resource.CredentialStore, + wantScopes: []Type{Project}, + }, + { + testName: "Group", + resource: resource.Group, + wantScopes: []Type{Global, Org, Project}, + }, + { + testName: "HostCatalog", + resource: resource.HostCatalog, + wantScopes: []Type{Project}, + }, + { + testName: "HostSet", + resource: resource.HostSet, + wantScopes: []Type{Project}, + }, + { + testName: "Host", + resource: resource.Host, + wantScopes: []Type{Project}, + }, + { + testName: "ManagedGroup", + resource: resource.ManagedGroup, + wantScopes: []Type{Global, Org}, + }, + { + testName: "Policy", + resource: resource.Policy, + wantScopes: []Type{Global, Org}, + }, + { + testName: "Role", + resource: resource.Role, + wantScopes: []Type{Global, Org, Project}, + }, + { + testName: "Scope", + resource: resource.Scope, + wantScopes: []Type{Global, Org}, + }, + { + testName: "SessionRecording", + resource: resource.SessionRecording, + wantScopes: []Type{Global, Org}, + }, + { + testName: "Session", + resource: resource.Session, + wantScopes: []Type{Project}, + }, + { + testName: "StorageBucket", + resource: resource.StorageBucket, + wantScopes: []Type{Global, Org}, + }, + { + testName: "Target", + resource: resource.Target, + wantScopes: []Type{Project}, + }, + { + testName: "Unknown", + resource: resource.Unknown, + wantErr: errors.New(context.Background(), errors.InvalidParameter, "scope.AllowedIn", "unknown resource type"), + }, + { + testName: "User", + resource: resource.User, + wantScopes: []Type{Global, Org}, + }, + { + testName: "Worker", + resource: resource.Worker, + wantScopes: []Type{Global}, + }, + { + testName: "Invalid resource type", + resource: 999, + wantErr: errors.New(context.Background(), errors.InvalidParameter, "scope.AllowedIn", "invalid resource type: 999"), + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + scopes, err := AllowedIn(context.Background(), tt.resource) + if tt.wantErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr.Error()) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantScopes, scopes) + }) + } +} diff --git a/internal/website/permstable/permstable.go b/internal/website/permstable/permstable.go index 70d58eb07d..b05f195fc2 100644 --- a/internal/website/permstable/permstable.go +++ b/internal/website/permstable/permstable.go @@ -83,7 +83,7 @@ func main() { } var pin string - if parent := resource.Parent(res); parent != res { + if parent := res.Parent(); parent != res { pin = parent.String() } collectionEndpoints := &Endpoint{ diff --git a/version/feature_manager.go b/version/feature_manager.go index 18c29ee199..607b1097c6 100644 --- a/version/feature_manager.go +++ b/version/feature_manager.go @@ -25,6 +25,8 @@ const ( PluginDelete LocalStorageState StorageBucketCredentialState + CreateDefaultAndAdminRoles + SkipDefaultAndAdminRoleCreation ) var featureMap map[Feature]MetadataConstraint @@ -97,6 +99,19 @@ func init() { featureMap[StorageBucketCredentialState] = MetadataConstraint{ Constraints: mustNewConstraints(">= 0.17.0"), } + + // Support using CreateDefaultRole and CreateAdminRole in the CLI + // and API. This is a breaking change in the API, so we need to + // ensure that the API version is >= 0.20.0 before we allow it. + featureMap[CreateDefaultAndAdminRoles] = MetadataConstraint{ + Constraints: mustNewConstraints(">= 0.20.0"), + } + + // Warn until 0.22.0 about using the now-deprecated withSkipAdminRoleCreation + // and withSkipDefaultRoleCreation options; after that disallow it + featureMap[SkipDefaultAndAdminRoleCreation] = MetadataConstraint{ + Constraints: mustNewConstraints("< 0.22.0"), + } } func mustNewConstraints(v string) gvers.Constraints {