diff --git a/Makefile b/Makefile index 7228b866a..9816217d2 100644 --- a/Makefile +++ b/Makefile @@ -67,6 +67,9 @@ protobuild: @protoc --proto_path=internal/proto/local --proto_path=internal/proto/third_party --swagger_out=logtostderr=true,disable_default_errors=true,include_package_in_tags=true,fqn_for_swagger_name=true,allow_merge,merge_file_name=controller:internal/gen/. internal/proto/local/controller/api/services/v1/*.proto @protoc-go-inject-tag -input=./internal/oplog/store/oplog.pb.go @protoc-go-inject-tag -input=./internal/oplog/oplog_test/oplog_test.pb.go + @protoc-go-inject-tag -input=./internal/iam/store/group.pb.go + @protoc-go-inject-tag -input=./internal/iam/store/role.pb.go + @protoc-go-inject-tag -input=./internal/iam/store/principal_role.pb.go @protoc-go-inject-tag -input=./internal/iam/store/role.pb.go @protoc-go-inject-tag -input=./internal/iam/store/user.pb.go @protoc-go-inject-tag -input=./internal/iam/store/scope.pb.go diff --git a/internal/db/migrations/postgres.gen.go b/internal/db/migrations/postgres.gen.go index f8748f743..8e89a6b27 100644 --- a/internal/db/migrations/postgres.gen.go +++ b/internal/db/migrations/postgres.gen.go @@ -245,14 +245,14 @@ values ('iam_group_member_user', 1), ('iam_role', 1), ('iam_role_grant', 1), - ('iam_role_group', 1), - ('iam_role_user', 1), + ('iam_group_role', 1), + ('iam_user_role', 1), ('db_test_user', 1), ('db_test_car', 1), ('db_test_rental', 1), ('db_test_scooter', 1), - ('auth_account', 1); -; + ('auth_account', 1), + ('iam_principal_role', 1); commit; @@ -418,11 +418,17 @@ drop table iam_scope_organization cascade; drop table iam_scope cascade; drop table iam_scope_type_enm cascade; drop table iam_role cascade; - +drop view iam_principal_role cascade; +drop table iam_group_role cascade; +drop table iam_user_role cascade; drop function iam_sub_names cascade; drop function iam_immutable_scope_type_func cascade; drop function iam_sub_scopes_func cascade; +drop function iam_immutable_role cascade; +drop function iam_user_role_scope_check cascade; +drop function iam_group_role_scope_check cascade; +drop function immutable_scope_id_func cascade; COMMIT; @@ -443,7 +449,24 @@ values ('organization'), ('project'); - + +create or replace function + iam_immutable_scope_id_func() + returns trigger +as $$ +begin + if new.scope_id is distinct from old.scope_id then + raise exception 'scope_id cannot be set to %', new.scope_id; + end if; + return new; +end; +$$ language plpgsql; + +comment on function + iam_immutable_scope_id_func() +is + 'function used in before update triggers to make scope_id column is immutable'; + create table iam_scope ( public_id wt_public_id primary key, create_time wt_timestamp, @@ -573,7 +596,6 @@ before update on iam_scope for each row execute procedure iam_sub_names(); - create table iam_user ( public_id wt_public_id not null primary key, create_time wt_timestamp, @@ -607,6 +629,11 @@ before insert on iam_user for each row execute procedure default_create_time(); +create trigger immutable_scope_id_user +before +update on iam_user + for each row execute procedure iam_immutable_scope_id_func(); + create table iam_role ( public_id wt_public_id not null primary key, create_time wt_timestamp, @@ -615,9 +642,23 @@ create table iam_role ( description text, scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, unique(name, scope_id), - disabled boolean not null default false + disabled boolean not null default false, + -- version allows optimistic locking of the role when modifying the role + -- itself and when modifying dependent items like principal roles. + -- TODO (jlambert 6/2020) add before update trigger to automatically + -- increment the version when needed. This trigger can be addded when PR + -- #126 is merged and update_version_column() is available. + version bigint not null default 1, + + -- add unique index so a composite fk can be declared. + unique(scope_id, public_id) ); +create trigger + update_version_column +after update on iam_role + for each row execute procedure update_version_column(); + create trigger update_time_column before update on iam_role @@ -643,7 +684,9 @@ create table iam_group ( description text, scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, unique(name, scope_id), - disabled boolean not null default false + disabled boolean not null default false, + -- add unique index so a composite fk can be declared. + unique(scope_id, public_id) ); create trigger @@ -662,7 +705,147 @@ create trigger before insert on iam_group for each row execute procedure default_create_time(); + +create trigger immutable_scope_id_group +before +update on iam_group + for each row execute procedure iam_immutable_scope_id_func(); + +-- iam_user_role contains roles that have been assigned to users. Users can only +-- be assigned roles which are within its organization, or the role is within a project within its +-- organization. There's no way to declare this constraint, so it will be +-- maintained with a before insert trigger using iam_user_role_scope_check(). +-- The rows in this table must be immutable after insert, which will be ensured +-- with a before update trigger using iam_immutable_role(). +create table iam_user_role ( + create_time wt_timestamp, + scope_id wt_public_id not null, + role_id wt_public_id not null, + principal_id wt_public_id not null references iam_user(public_id) on delete cascade on update cascade, + primary key (role_id, principal_id), + foreign key (scope_id, role_id) + references iam_role(scope_id, public_id) + on delete cascade + on update cascade + ); + +-- iam_group_role contains roles that have been assigned to groups. Groups can +-- only be assigned roles which are within its scope (organization or project) +-- and that integrity can be maintained with a foreign key. The rows in this +-- table must be immutable after insert, which will be ensured with a before +-- update trigger using iam_immutable_role(). +create table iam_group_role ( + create_time wt_timestamp, + scope_id wt_public_id not null, + role_id wt_public_id not null, + principal_id wt_public_id not null, + primary key (role_id, principal_id), + foreign key (scope_id, role_id) + references iam_role(scope_id, public_id) + on delete cascade + on update cascade, + foreign key (scope_id, principal_id) + references iam_group(scope_id, public_id) + on delete cascade + on update cascade + ); + +-- iam_principle_role provides a consolidated view all principal roles assigned +-- (user and group roles). +create view iam_principal_role as +select + -- intentionally using * to specify the view which requires that the concrete role assignment tables match + *, 'user' as type +from iam_user_role +union +select + -- intentionally using * to specify the view which requires that the concrete role assignment tables match + *, 'group' as type +from iam_group_role; + +-- iam_user_role_scope_check() ensures that the user is only assigned roles +-- which are within its organization, or the role is within a project within its +-- organization. +create or replace function + iam_user_role_scope_check() + returns trigger +as $$ +declare cnt int; +begin + select count(*) into cnt + from iam_user + where + public_id = new.principal_id and + scope_id in( + -- check to see if they have the same org scope + select s.public_id + from iam_scope s, iam_role r + where s.public_id = r.scope_id and r.public_id = new.role_id and r.scope_id = new.scope_id + union + -- check to see if the role has a parent that's the same org + select s.parent_id as public_id + from iam_scope s, iam_role r + where s.public_id = r.scope_id and r.public_id = new.role_id and r.scope_id = new.scope_id + ); + if cnt = 0 then + raise exception 'user and role do not belong to the same organization'; + end if; + return new; +end; +$$ language plpgsql; + +-- iam_immutable_role() ensures that roles assigned to principals are immutable. +create or replace function + iam_immutable_role() + returns trigger +as $$ +begin + if row(new.*) is distinct from row(old.*) then + raise exception 'roles are immutable'; + end if; + return new; +end; +$$ language plpgsql; + +create trigger iam_user_role_scope_check +before +insert on iam_user_role + for each row execute procedure iam_user_role_scope_check(); + +create trigger immutable_role +before +update on iam_user_role + for each row execute procedure iam_immutable_role(); + +create trigger + immutable_create_time +before +update on iam_user_role + for each row execute procedure immutable_create_time_func(); +create trigger + default_create_time_column +before +insert on iam_user_role + for each row execute procedure default_create_time(); + +create trigger immutable_role +before +update on iam_group_role + for each row execute procedure iam_immutable_role(); + +create trigger + immutable_create_time +before +update on iam_group_role + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on iam_group_role + for each row execute procedure default_create_time(); + commit; `), diff --git a/internal/db/migrations/postgres/02_oplog.up.sql b/internal/db/migrations/postgres/02_oplog.up.sql index 0838c4018..ba84a1241 100644 --- a/internal/db/migrations/postgres/02_oplog.up.sql +++ b/internal/db/migrations/postgres/02_oplog.up.sql @@ -94,14 +94,14 @@ values ('iam_group_member_user', 1), ('iam_role', 1), ('iam_role_grant', 1), - ('iam_role_group', 1), - ('iam_role_user', 1), + ('iam_group_role', 1), + ('iam_user_role', 1), ('db_test_user', 1), ('db_test_car', 1), ('db_test_rental', 1), ('db_test_scooter', 1), - ('auth_account', 1); -; + ('auth_account', 1), + ('iam_principal_role', 1); commit; diff --git a/internal/db/migrations/postgres/06_iam.down.sql b/internal/db/migrations/postgres/06_iam.down.sql index f110cf3fc..d50411316 100644 --- a/internal/db/migrations/postgres/06_iam.down.sql +++ b/internal/db/migrations/postgres/06_iam.down.sql @@ -7,10 +7,16 @@ drop table iam_scope_organization cascade; drop table iam_scope cascade; drop table iam_scope_type_enm cascade; drop table iam_role cascade; - +drop view iam_principal_role cascade; +drop table iam_group_role cascade; +drop table iam_user_role cascade; drop function iam_sub_names cascade; drop function iam_immutable_scope_type_func cascade; drop function iam_sub_scopes_func cascade; +drop function iam_immutable_role cascade; +drop function iam_user_role_scope_check cascade; +drop function iam_group_role_scope_check cascade; +drop function immutable_scope_id_func cascade; COMMIT; diff --git a/internal/db/migrations/postgres/06_iam.up.sql b/internal/db/migrations/postgres/06_iam.up.sql index 34b7c4346..d07d8bccd 100644 --- a/internal/db/migrations/postgres/06_iam.up.sql +++ b/internal/db/migrations/postgres/06_iam.up.sql @@ -10,7 +10,24 @@ values ('organization'), ('project'); - + +create or replace function + iam_immutable_scope_id_func() + returns trigger +as $$ +begin + if new.scope_id is distinct from old.scope_id then + raise exception 'scope_id cannot be set to %', new.scope_id; + end if; + return new; +end; +$$ language plpgsql; + +comment on function + iam_immutable_scope_id_func() +is + 'function used in before update triggers to make scope_id column is immutable'; + create table iam_scope ( public_id wt_public_id primary key, create_time wt_timestamp, @@ -140,7 +157,6 @@ before update on iam_scope for each row execute procedure iam_sub_names(); - create table iam_user ( public_id wt_public_id not null primary key, create_time wt_timestamp, @@ -174,6 +190,11 @@ before insert on iam_user for each row execute procedure default_create_time(); +create trigger immutable_scope_id_user +before +update on iam_user + for each row execute procedure iam_immutable_scope_id_func(); + create table iam_role ( public_id wt_public_id not null primary key, create_time wt_timestamp, @@ -182,9 +203,23 @@ create table iam_role ( description text, scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, unique(name, scope_id), - disabled boolean not null default false + disabled boolean not null default false, + -- version allows optimistic locking of the role when modifying the role + -- itself and when modifying dependent items like principal roles. + -- TODO (jlambert 6/2020) add before update trigger to automatically + -- increment the version when needed. This trigger can be addded when PR + -- #126 is merged and update_version_column() is available. + version bigint not null default 1, + + -- add unique index so a composite fk can be declared. + unique(scope_id, public_id) ); +create trigger + update_version_column +after update on iam_role + for each row execute procedure update_version_column(); + create trigger update_time_column before update on iam_role @@ -210,7 +245,9 @@ create table iam_group ( description text, scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, unique(name, scope_id), - disabled boolean not null default false + disabled boolean not null default false, + -- add unique index so a composite fk can be declared. + unique(scope_id, public_id) ); create trigger @@ -229,5 +266,145 @@ create trigger before insert on iam_group for each row execute procedure default_create_time(); + +create trigger immutable_scope_id_group +before +update on iam_group + for each row execute procedure iam_immutable_scope_id_func(); + +-- iam_user_role contains roles that have been assigned to users. Users can only +-- be assigned roles which are within its organization, or the role is within a project within its +-- organization. There's no way to declare this constraint, so it will be +-- maintained with a before insert trigger using iam_user_role_scope_check(). +-- The rows in this table must be immutable after insert, which will be ensured +-- with a before update trigger using iam_immutable_role(). +create table iam_user_role ( + create_time wt_timestamp, + scope_id wt_public_id not null, + role_id wt_public_id not null, + principal_id wt_public_id not null references iam_user(public_id) on delete cascade on update cascade, + primary key (role_id, principal_id), + foreign key (scope_id, role_id) + references iam_role(scope_id, public_id) + on delete cascade + on update cascade + ); + +-- iam_group_role contains roles that have been assigned to groups. Groups can +-- only be assigned roles which are within its scope (organization or project) +-- and that integrity can be maintained with a foreign key. The rows in this +-- table must be immutable after insert, which will be ensured with a before +-- update trigger using iam_immutable_role(). +create table iam_group_role ( + create_time wt_timestamp, + scope_id wt_public_id not null, + role_id wt_public_id not null, + principal_id wt_public_id not null, + primary key (role_id, principal_id), + foreign key (scope_id, role_id) + references iam_role(scope_id, public_id) + on delete cascade + on update cascade, + foreign key (scope_id, principal_id) + references iam_group(scope_id, public_id) + on delete cascade + on update cascade + ); + +-- iam_principle_role provides a consolidated view all principal roles assigned +-- (user and group roles). +create view iam_principal_role as +select + -- intentionally using * to specify the view which requires that the concrete role assignment tables match + *, 'user' as type +from iam_user_role +union +select + -- intentionally using * to specify the view which requires that the concrete role assignment tables match + *, 'group' as type +from iam_group_role; + +-- iam_user_role_scope_check() ensures that the user is only assigned roles +-- which are within its organization, or the role is within a project within its +-- organization. +create or replace function + iam_user_role_scope_check() + returns trigger +as $$ +declare cnt int; +begin + select count(*) into cnt + from iam_user + where + public_id = new.principal_id and + scope_id in( + -- check to see if they have the same org scope + select s.public_id + from iam_scope s, iam_role r + where s.public_id = r.scope_id and r.public_id = new.role_id and r.scope_id = new.scope_id + union + -- check to see if the role has a parent that's the same org + select s.parent_id as public_id + from iam_scope s, iam_role r + where s.public_id = r.scope_id and r.public_id = new.role_id and r.scope_id = new.scope_id + ); + if cnt = 0 then + raise exception 'user and role do not belong to the same organization'; + end if; + return new; +end; +$$ language plpgsql; + +-- iam_immutable_role() ensures that roles assigned to principals are immutable. +create or replace function + iam_immutable_role() + returns trigger +as $$ +begin + if row(new.*) is distinct from row(old.*) then + raise exception 'roles are immutable'; + end if; + return new; +end; +$$ language plpgsql; + +create trigger iam_user_role_scope_check +before +insert on iam_user_role + for each row execute procedure iam_user_role_scope_check(); + +create trigger immutable_role +before +update on iam_user_role + for each row execute procedure iam_immutable_role(); + +create trigger + immutable_create_time +before +update on iam_user_role + for each row execute procedure immutable_create_time_func(); +create trigger + default_create_time_column +before +insert on iam_user_role + for each row execute procedure default_create_time(); + +create trigger immutable_role +before +update on iam_group_role + for each row execute procedure iam_immutable_role(); + +create trigger + immutable_create_time +before +update on iam_group_role + for each row execute procedure immutable_create_time_func(); + +create trigger + default_create_time_column +before +insert on iam_group_role + for each row execute procedure default_create_time(); + commit; diff --git a/internal/db/option.go b/internal/db/option.go index ef47e58bd..4c688ba86 100644 --- a/internal/db/option.go +++ b/internal/db/option.go @@ -34,6 +34,8 @@ type Options struct { // WithVersion must be accessible from other packages WithVersion int + + withSkipVetForWrite bool } type oplogOpts struct { @@ -124,3 +126,11 @@ func WithVersion(version int) Option { o.WithVersion = version } } + +// WithSkipVetForWrite provides an option to allow skipping vet checks to allow +// testing lower-level SQL triggers and constraints +func WithSkipVetForWrite(enable bool) Option { + return func(o *Options) { + o.withSkipVetForWrite = enable + } +} diff --git a/internal/db/option_test.go b/internal/db/option_test.go index c1832e108..181a343dd 100644 --- a/internal/db/option_test.go +++ b/internal/db/option_test.go @@ -132,4 +132,18 @@ func Test_getOpts(t *testing.T) { testOpts.WithVersion = 2 assert.Equal(opts, testOpts) }) + t.Run("WithSkipVetForWrite", func(t *testing.T) { + assert := assert.New(t) + // test default of false + opts := GetOpts() + testOpts := getDefaultOptions() + testOpts.withSkipVetForWrite = false + assert.Equal(opts, testOpts) + + // try setting to false + opts = GetOpts(WithSkipVetForWrite(true)) + testOpts = getDefaultOptions() + testOpts.withSkipVetForWrite = true + assert.Equal(opts, testOpts) + }) } diff --git a/internal/db/read_writer.go b/internal/db/read_writer.go index d3794e96f..1f953461b 100644 --- a/internal/db/read_writer.go +++ b/internal/db/read_writer.go @@ -241,9 +241,11 @@ func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { // db to manage them setFieldsToNil(i, []string{"CreateTime", "UpdateTime"}) - if vetter, ok := i.(VetForWriter); ok { - if err := vetter.VetForWrite(ctx, rw, CreateOp); err != nil { - return fmt.Errorf("create: vet for write failed %w", err) + if !opts.withSkipVetForWrite { + if vetter, ok := i.(VetForWriter); ok { + if err := vetter.VetForWrite(ctx, rw, CreateOp); err != nil { + return fmt.Errorf("create: vet for write failed %w", err) + } } } var ticket *store.Ticket @@ -407,9 +409,11 @@ func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string return NoRowsAffected, fmt.Errorf("update: oplog validation failed %w", err) } } - if vetter, ok := i.(VetForWriter); ok { - if err := vetter.VetForWrite(ctx, rw, UpdateOp, WithFieldMaskPaths(fieldMaskPaths), WithNullPaths(setToNullPaths)); err != nil { - return NoRowsAffected, fmt.Errorf("update: vet for write failed %w", err) + if !opts.withSkipVetForWrite { + if vetter, ok := i.(VetForWriter); ok { + if err := vetter.VetForWrite(ctx, rw, UpdateOp, WithFieldMaskPaths(fieldMaskPaths), WithNullPaths(setToNullPaths)); err != nil { + return NoRowsAffected, fmt.Errorf("update: vet for write failed %w", err) + } } } var ticket *store.Ticket diff --git a/internal/iam/options.go b/internal/iam/options.go index 957d5e2d5..9d6de2998 100644 --- a/internal/iam/options.go +++ b/internal/iam/options.go @@ -14,24 +14,26 @@ type Option func(*options) // options = how options are represented type options struct { - withPublicId string - withName string - withScope *Scope - withDescription string - withParentId *string - withLimit int - withAutoVivify bool + withPublicId string + withName string + withScope *Scope + withDescription string + withParentId *string + withLimit int + withAutoVivify bool + withSkipVetForWrite bool } func getDefaultOptions() options { return options{ - withPublicId: "", - withScope: nil, - withDescription: "", - withName: "", - withParentId: nil, - withLimit: 0, - withAutoVivify: false, + withPublicId: "", + withScope: nil, + withDescription: "", + withName: "", + withParentId: nil, + withLimit: 0, + withAutoVivify: false, + withSkipVetForWrite: false, } } @@ -79,3 +81,11 @@ func WithAutoVivify(enable bool) Option { o.withAutoVivify = enable } } + +// WithSkipVetForWrite provides an option to allow skipping vet checks to allow +// testing lower-level SQL triggers and constraints +func WithSkipVetForWrite(enable bool) Option { + return func(o *options) { + o.withSkipVetForWrite = enable + } +} diff --git a/internal/iam/principal_role.go b/internal/iam/principal_role.go new file mode 100644 index 000000000..042b00deb --- /dev/null +++ b/internal/iam/principal_role.go @@ -0,0 +1,230 @@ +package iam + +import ( + "context" + "fmt" + + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/iam/store" + "google.golang.org/protobuf/proto" +) + +// RoleType defines the possible types for roles. +type RoleType uint32 + +const ( + UnknownRoleType RoleType = 0 + UserRoleType RoleType = 1 + GroupRoleType RoleType = 2 +) + +// String returns a string representation of the role type. +func (r RoleType) String() string { + return [...]string{ + "unknown", + "user", + "group", + }[r] +} + +const principalRoleViewDefaultTable = "iam_principal_role" + +// PrincipalRole declares a common interface for all roles assigned to resources (Users and Groups). +type PrincipalRole interface { + GetRoleId() string + GetPrincipalId() string + GetType() string + GetScopeId() string + Clone() interface{} +} + +// principalRoleView provides a common way to return roles regardless of their underlying type. +type principalRoleView struct { + *store.PrincipalRoleView + tableName string `gorm:"-"` +} + +// TableName provides an overridden gorm table name for principal roles. +func (v *principalRoleView) TableName() string { + if v.tableName != "" { + return v.tableName + } + return principalRoleViewDefaultTable +} + +// SetTableName sets the table name for the resource. If the caller attempts to +// set the name to "" the name will be reset to the default name. +func (v *principalRoleView) SetTableName(n string) { + switch n { + case "": + v.tableName = principalRoleViewDefaultTable + default: + v.tableName = n + } +} + +func (v principalRoleView) Clone() interface{} { + cp := proto.Clone(v.PrincipalRoleView) + return &principalRoleView{ + PrincipalRoleView: cp.(*store.PrincipalRoleView), + } +} + +// UserRole is a role assigned to a user +type UserRole struct { + *store.UserRole + tableName string `gorm:"-"` +} + +// ensure that UserRole implements the interfaces of: Clonable, AssignedRole and db.VetForWriter +var _ Clonable = (*UserRole)(nil) +var _ PrincipalRole = (*UserRole)(nil) +var _ db.VetForWriter = (*UserRole)(nil) + +// NewUserRole creates a new user role in memory. Users can be assigned roles +// which are within its organization, or the role is within a project within its +// organization. This relationship will not be enforced until the user role is +// written to the database. No options are supported currently. +func NewUserRole(scopeId, roleId, userId string, opt ...Option) (PrincipalRole, error) { + if roleId == "" { + return nil, fmt.Errorf("new user role: missing role id %w", db.ErrInvalidParameter) + } + if userId == "" { + return nil, fmt.Errorf("new user role: missing user id %w", db.ErrInvalidParameter) + } + return &UserRole{ + UserRole: &store.UserRole{ + PrincipalId: userId, + RoleId: roleId, + ScopeId: scopeId, + }, + }, nil +} + +// GetType returns the user role type. +func (r *UserRole) GetType() string { + return UserRoleType.String() +} + +func allocUserRole() UserRole { + return UserRole{ + UserRole: &store.UserRole{}, + } +} + +// Clone creates a clone of the UserRole. +func (r *UserRole) Clone() interface{} { + cp := proto.Clone(r.UserRole) + return &UserRole{ + UserRole: cp.(*store.UserRole), + } +} + +// VetForWrite implements db.VetForWrite() interface for user roles. The +// constraint between user and role scopes will be enforced by the database via +// constraints and triggers. +func (role *UserRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if role.RoleId == "" { + return fmt.Errorf("new user role: missing role id %w", db.ErrInvalidParameter) + } + if role.PrincipalId == "" { + return fmt.Errorf("new user role: missing user id %w", db.ErrInvalidParameter) + } + return nil +} + +// TableName returns the tablename to override the default gorm table name for +// user roles. +func (r *UserRole) TableName() string { + if r.tableName != "" { + return r.tableName + } + return "iam_user_role" +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage interface +func (r *UserRole) SetTableName(n string) { + if r.tableName != "" { + r.tableName = n + } +} + +// GroupRole is a role assigned to a group +type GroupRole struct { + *store.GroupRole + tableName string `gorm:"-"` +} + +// ensure that GroupRole implements the interfaces of: Clonable, AssignedRole and db.VetForWriter +var _ Clonable = (*GroupRole)(nil) +var _ PrincipalRole = (*GroupRole)(nil) +var _ db.VetForWriter = (*GroupRole)(nil) + +// NewGroupRole creates a new group role in memory. Groups can only be +// assigned roles within its scope (org or project). This relationship will not +// be enforced until the group role is written to the database. No options are +// supported currently. +func NewGroupRole(scopeId, roleId, groupId string, opt ...Option) (PrincipalRole, error) { + if roleId == "" { + return nil, fmt.Errorf("new group role: missing role id %w", db.ErrInvalidParameter) + } + if groupId == "" { + return nil, fmt.Errorf("new group role: missing group id %w", db.ErrInvalidParameter) + } + return &GroupRole{ + GroupRole: &store.GroupRole{ + PrincipalId: groupId, + RoleId: roleId, + ScopeId: scopeId, + }, + }, nil +} + +// GetType returns the group role type. +func (r *GroupRole) GetType() string { + return GroupRoleType.String() +} + +func allocGroupRole() GroupRole { + return GroupRole{ + GroupRole: &store.GroupRole{}, + } +} + +// Clone creates a clone of the GroupRole. +func (r *GroupRole) Clone() interface{} { + cp := proto.Clone(r.GroupRole) + return &GroupRole{ + GroupRole: cp.(*store.GroupRole), + } +} + +// VetForWrite implements db.VetForWrite() interface for group roles. The +// constraint between groups and role scopes will be enforced by the database via +// constraints and triggers. +func (role *GroupRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if role.RoleId == "" { + return fmt.Errorf("new group role: missing role id %w", db.ErrInvalidParameter) + } + if role.PrincipalId == "" { + return fmt.Errorf("new group role: missing user id %w", db.ErrInvalidParameter) + } + return nil +} + +// TableName returns the tablename to override the default gorm table name for +// group roles. +func (r *GroupRole) TableName() string { + if r.tableName != "" { + return r.tableName + } + return "iam_group_role" +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage interface +// for group roles. +func (r *GroupRole) SetTableName(n string) { + if r.tableName != "" { + r.tableName = n + } +} diff --git a/internal/iam/principal_role_test.go b/internal/iam/principal_role_test.go new file mode 100644 index 000000000..a4bed7d31 --- /dev/null +++ b/internal/iam/principal_role_test.go @@ -0,0 +1,747 @@ +package iam + +import ( + "context" + "errors" + "testing" + + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/iam/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestNewUserRole(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + org, proj := TestScopes(t, conn) + orgRole := TestRole(t, conn, org.PublicId) + projRole := TestRole(t, conn, proj.PublicId) + user := TestUser(t, conn, org.PublicId) + + type args struct { + roleId string + userId string + scopeId string + opt []Option + } + tests := []struct { + name string + args args + want PrincipalRole + wantErr bool + wantIsErr error + }{ + { + name: "valid-org", + args: args{ + roleId: orgRole.PublicId, + userId: user.PublicId, + scopeId: org.PublicId, + }, + want: func() PrincipalRole { + r := allocUserRole() + r.RoleId = orgRole.PublicId + r.PrincipalId = user.PublicId + r.ScopeId = org.PublicId + return &r + }(), + }, + { + name: "valid-proj", + args: args{ + roleId: projRole.PublicId, + userId: user.PublicId, + scopeId: proj.PublicId, + }, + want: func() PrincipalRole { + r := allocUserRole() + r.RoleId = projRole.PublicId + r.PrincipalId = user.PublicId + r.ScopeId = proj.PublicId + return &r + }(), + }, + { + name: "empty-role-id", + args: args{ + roleId: "", + userId: user.PublicId, + scopeId: org.PublicId, + }, + want: nil, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "empty-user-id", + args: args{ + roleId: orgRole.PublicId, + userId: "", + scopeId: org.PublicId, + }, + want: nil, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewUserRole(tt.args.scopeId, tt.args.roleId, tt.args.userId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + assert.True(errors.Is(err, tt.wantIsErr)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + }) + } +} + +func TestUserRole_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + org, proj := TestScopes(t, conn) + org2, proj2 := TestScopes(t, conn) + type args struct { + role *UserRole + } + tests := []struct { + name string + args args + wantDup bool + wantErr bool + wantErrMsg string + wantIsErr error + }{ + { + name: "valid-with-org", + args: args{ + role: func() *UserRole { + role := TestRole(t, conn, org.PublicId) + principal := TestUser(t, conn, org.PublicId) + principalRole, err := NewUserRole(org.PublicId, role.PublicId, principal.PublicId) + require.NoError(t, err) + return principalRole.(*UserRole) + }(), + }, + wantErr: false, + }, + { + name: "valid-with-proj", + args: args{ + role: func() *UserRole { + role := TestRole(t, conn, proj.PublicId) + principal := TestUser(t, conn, org.PublicId) + principalRole, err := NewUserRole(proj.PublicId, role.PublicId, principal.PublicId) + require.NoError(t, err) + return principalRole.(*UserRole) + }(), + }, + wantErr: false, + }, + { + name: "invalid-org", + args: args{ + role: func() *UserRole { + role := TestRole(t, conn, org2.PublicId) + principal := TestUser(t, conn, org.PublicId) + principalRole, err := NewUserRole(org.PublicId, role.PublicId, principal.PublicId) + require.NoError(t, err) + return principalRole.(*UserRole) + }(), + }, + wantErr: true, + }, + { + name: "invalid-proj", + args: args{ + role: func() *UserRole { + role := TestRole(t, conn, proj2.PublicId) + principal := TestUser(t, conn, org.PublicId) + principalRole, err := NewUserRole(proj.PublicId, role.PublicId, principal.PublicId) + require.NoError(t, err) + return principalRole.(*UserRole) + }(), + }, + wantErr: true, + }, + { + name: "bad-role-id", + args: args{ + role: func() *UserRole { + id := testId(t) + principal := TestUser(t, conn, org.PublicId) + principalRole, err := NewUserRole(org.PublicId, id, principal.PublicId) + require.NoError(t, err) + return principalRole.(*UserRole) + }(), + }, + wantErr: true, + wantErrMsg: "create: failed pq: user and role do not belong to the same organization", + }, + { + name: "bad-user-id", + args: args{ + role: func() *UserRole { + id := testId(t) + role := TestRole(t, conn, proj.PublicId) + principalRole, err := NewUserRole(proj.PublicId, role.PublicId, id) + require.NoError(t, err) + return principalRole.(*UserRole) + }(), + }, + wantErr: true, + wantErrMsg: "create: failed pq: user and role do not belong to the same organization", + }, + { + name: "missing-role-id", + args: args{ + role: func() *UserRole { + principal := TestUser(t, conn, org.PublicId) + return &UserRole{ + UserRole: &store.UserRole{ + RoleId: "", + PrincipalId: principal.PublicId, + ScopeId: org.PublicId, + }, + } + }(), + }, + wantErr: true, + wantErrMsg: "create: vet for write failed new user role: missing role id invalid parameter", + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "missing-user-id", + args: args{ + role: func() *UserRole { + role := TestRole(t, conn, proj.PublicId) + return &UserRole{ + UserRole: &store.UserRole{ + RoleId: role.PublicId, + PrincipalId: "", + ScopeId: org.PublicId, + }, + } + }(), + }, + wantErr: true, + wantErrMsg: "create: vet for write failed new user role: missing user id invalid parameter", + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "dup-at-org", + args: args{ + role: func() *UserRole { + role := TestRole(t, conn, org.PublicId) + principal := TestUser(t, conn, org.PublicId) + principalRole, err := NewUserRole(org.PublicId, role.PublicId, principal.PublicId) + require.NoError(t, err) + return principalRole.(*UserRole) + }(), + }, + wantDup: true, + wantErr: true, + wantErrMsg: `create: failed pq: duplicate key value violates unique constraint "iam_user_role_pkey"`, + }, + } + + 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().(*UserRole) + err := w.Create(context.Background(), r) + require.NoError(err) + } + r := tt.args.role.Clone().(*UserRole) + err := w.Create(context.Background(), r) + if tt.wantErr { + require.Error(err) + assert.Contains(err.Error(), tt.wantErrMsg) + if tt.wantIsErr != nil { + assert.True(errors.Is(err, tt.wantIsErr)) + } + return + } + assert.NoError(err) + + found := allocUserRole() + err = w.LookupWhere(context.Background(), &found, "role_id = ? and principal_id = ?", r.RoleId, r.PrincipalId) + require.NoError(err) + assert.Equal(r, &found) + }) + } +} + +func TestUserRole_Update(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + org, _ := TestScopes(t, conn) + rw := db.New(conn) + + t.Run("updates not allowed", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + r := TestRole(t, conn, org.PublicId) + u := TestUser(t, conn, org.PublicId) + u2 := TestUser(t, conn, org.PublicId) + userRole := TestUserRole(t, conn, org.PublicId, r.PublicId, u.PublicId) + updateRole := userRole.Clone().(*UserRole) + updateRole.PrincipalId = u2.PublicId + updatedRows, err := rw.Update(context.Background(), updateRole, []string{"PrincipalId"}, nil) + require.Error(err) + assert.Equal(0, updatedRows) + }) +} + +func TestUserRole_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + id := testId(t) + org, _ := TestScopes(t, conn) + u := TestUser(t, conn, org.PublicId) + r := TestRole(t, conn, org.PublicId) + + tests := []struct { + name string + role *UserRole + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + role: TestUserRole(t, conn, org.PublicId, r.PublicId, u.PublicId), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + role: func() *UserRole { r := allocUserRole(); r.PrincipalId = id; r.RoleId = id; return &r }(), + 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 := allocUserRole() + deleteRole.RoleId = tt.role.GetRoleId() + deleteRole.PrincipalId = tt.role.GetPrincipalId() + deletedRows, err := rw.Delete(context.Background(), &deleteRole) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + found := allocUserRole() + err = rw.LookupWhere(context.Background(), &found, "role_id = ? and principal_id = ?", tt.role.GetRoleId(), tt.role.GetPrincipalId()) + require.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestUserRole_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + org, proj := TestScopes(t, conn) + user := TestUser(t, conn, org.PublicId) + t.Run("valid", func(t *testing.T) { + assert := assert.New(t) + role := TestRole(t, conn, org.PublicId) + userRole := TestUserRole(t, conn, org.PublicId, role.PublicId, user.PublicId) + cp := userRole.Clone() + assert.True(proto.Equal(cp.(*UserRole).UserRole, userRole.UserRole)) + }) + t.Run("not-equal", func(t *testing.T) { + assert := assert.New(t) + role := TestRole(t, conn, org.PublicId) + role2 := TestRole(t, conn, proj.PublicId) + userRole := TestUserRole(t, conn, org.PublicId, role.PublicId, user.PublicId) + userRole2 := TestUserRole(t, conn, proj.PublicId, role2.PublicId, user.PublicId) + cp := userRole.Clone() + assert.True(!proto.Equal(cp.(*UserRole).UserRole, userRole2.UserRole)) + }) +} + +func TestUserRole_GetType(t *testing.T) { + t.Parallel() + assert := assert.New(t) + r := &UserRole{} + ty := r.GetType() + assert.Equal("user", ty) +} + +func TestNewGroupRole(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + org, proj := TestScopes(t, conn) + orgRole := TestRole(t, conn, org.PublicId) + projRole := TestRole(t, conn, proj.PublicId) + group := TestGroup(t, conn, org.PublicId) + + type args struct { + roleId string + groupId string + scopeId string + opt []Option + } + tests := []struct { + name string + args args + want PrincipalRole + wantErr bool + wantIsErr error + }{ + { + name: "valid-org", + args: args{ + roleId: orgRole.PublicId, + groupId: group.PublicId, + scopeId: org.PublicId, + }, + want: func() PrincipalRole { + r := allocGroupRole() + r.RoleId = orgRole.PublicId + r.PrincipalId = group.PublicId + r.ScopeId = org.PublicId + return &r + }(), + }, + { + name: "valid-proj", + args: args{ + roleId: projRole.PublicId, + groupId: group.PublicId, + scopeId: proj.PublicId, + }, + want: func() PrincipalRole { + r := allocGroupRole() + r.RoleId = projRole.PublicId + r.PrincipalId = group.PublicId + r.ScopeId = proj.PublicId + return &r + }(), + }, + { + name: "empty-role-id", + args: args{ + roleId: "", + groupId: group.PublicId, + scopeId: org.PublicId, + }, + want: nil, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "empty-group-id", + args: args{ + roleId: orgRole.PublicId, + groupId: "", + scopeId: org.PublicId, + }, + want: nil, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewGroupRole(tt.args.scopeId, tt.args.roleId, tt.args.groupId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + assert.True(errors.Is(err, tt.wantIsErr)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + }) + } +} + +func TestGroupRole_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + org, proj := TestScopes(t, conn) + type args struct { + role *GroupRole + } + tests := []struct { + name string + args args + wantDup bool + wantErr bool + wantErrMsg string + wantIsErr error + }{ + { + name: "valid-with-org", + args: args{ + role: func() *GroupRole { + role := TestRole(t, conn, org.PublicId) + principal := TestGroup(t, conn, org.PublicId) + principalRole, err := NewGroupRole(org.PublicId, role.PublicId, principal.PublicId) + require.NoError(t, err) + return principalRole.(*GroupRole) + }(), + }, + wantErr: false, + }, + { + name: "valid-with-proj", + args: args{ + role: func() *GroupRole { + role := TestRole(t, conn, proj.PublicId) + principal := TestGroup(t, conn, proj.PublicId) + principalRole, err := NewGroupRole(proj.PublicId, role.PublicId, principal.PublicId) + require.NoError(t, err) + return principalRole.(*GroupRole) + }(), + }, + wantErr: false, + }, + { + name: "bad-role-id", + args: args{ + role: func() *GroupRole { + id := testId(t) + principal := TestGroup(t, conn, org.PublicId) + principalRole, err := NewGroupRole(org.PublicId, id, principal.PublicId) + require.NoError(t, err) + return principalRole.(*GroupRole) + }(), + }, + wantErr: true, + wantErrMsg: `create: failed pq: insert or update on table "iam_group_role" violates foreign key constraint "iam_group_role_scope_id_role_id_fkey"`, + }, + { + name: "bad-user-id", + args: args{ + role: func() *GroupRole { + id := testId(t) + role := TestRole(t, conn, proj.PublicId) + principalRole, err := NewGroupRole(org.PublicId, role.PublicId, id) + require.NoError(t, err) + return principalRole.(*GroupRole) + }(), + }, + wantErr: true, + wantErrMsg: `create: failed pq: insert or update on table "iam_group_role" violates foreign key constraint "iam_group_role_scope_id_role_id_fkey"`, + }, + { + name: "missing-role-id", + args: args{ + role: func() *GroupRole { + principal := TestGroup(t, conn, org.PublicId) + return &GroupRole{ + GroupRole: &store.GroupRole{ + RoleId: "", + PrincipalId: principal.PublicId, + ScopeId: org.PublicId, + }, + } + }(), + }, + wantErr: true, + wantErrMsg: "create: vet for write failed new group role: missing role id invalid parameter", + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "missing-user-id", + args: args{ + role: func() *GroupRole { + role := TestRole(t, conn, proj.PublicId) + return &GroupRole{ + GroupRole: &store.GroupRole{ + RoleId: role.PublicId, + PrincipalId: "", + ScopeId: proj.PublicId, + }, + } + }(), + }, + wantErr: true, + wantErrMsg: "create: vet for write failed new group role: missing user id invalid parameter", + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "dup-at-org", + args: args{ + role: func() *GroupRole { + role := TestRole(t, conn, org.PublicId) + principal := TestGroup(t, conn, org.PublicId) + principalRole, err := NewGroupRole(org.PublicId, role.PublicId, principal.PublicId) + require.NoError(t, err) + return principalRole.(*GroupRole) + }(), + }, + wantDup: true, + wantErr: true, + wantErrMsg: `create: failed pq: duplicate key value violates unique constraint`, + }, + { + name: "dup-at-proj", + args: args{ + role: func() *GroupRole { + role := TestRole(t, conn, proj.PublicId) + principal := TestGroup(t, conn, proj.PublicId) + principalRole, err := NewGroupRole(proj.PublicId, role.PublicId, principal.PublicId) + require.NoError(t, err) + return principalRole.(*GroupRole) + }(), + }, + wantDup: true, + wantErr: true, + wantErrMsg: `create: failed pq: duplicate key value violates unique constraint`, + }, + } + + 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().(*GroupRole) + err := w.Create(context.Background(), r) + require.NoError(err) + } + r := tt.args.role.Clone().(*GroupRole) + err := w.Create(context.Background(), r) + if tt.wantErr { + require.Error(err) + assert.Contains(err.Error(), tt.wantErrMsg) + if tt.wantIsErr != nil { + assert.True(errors.Is(err, tt.wantIsErr)) + } + return + } + assert.NoError(err) + + found := allocGroupRole() + err = w.LookupWhere(context.Background(), &found, "role_id = ? and principal_id = ?", r.RoleId, r.PrincipalId) + require.NoError(err) + assert.Equal(r, &found) + }) + } +} + +func TestGroupRole_Update(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + org, _ := TestScopes(t, conn) + rw := db.New(conn) + + t.Run("updates not allowed", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + r := TestRole(t, conn, org.PublicId) + g := TestGroup(t, conn, org.PublicId) + g2 := TestGroup(t, conn, org.PublicId) + userRole := TestGroupRole(t, conn, org.PublicId, r.PublicId, g.PublicId) + updateRole := userRole.Clone().(*GroupRole) + updateRole.PrincipalId = g2.PublicId + updatedRows, err := rw.Update(context.Background(), updateRole, []string{"PrincipalId"}, nil) + require.Error(err) + assert.Equal(0, updatedRows) + }) +} + +func TestGroupRole_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + id := testId(t) + org, _ := TestScopes(t, conn) + g := TestGroup(t, conn, org.PublicId) + r := TestRole(t, conn, org.PublicId) + + tests := []struct { + name string + role *GroupRole + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + role: TestGroupRole(t, conn, org.PublicId, r.PublicId, g.PublicId), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + role: func() *GroupRole { r := allocGroupRole(); r.PrincipalId = id; r.RoleId = id; r.ScopeId = id; return &r }(), + 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 := allocGroupRole() + deleteRole.RoleId = tt.role.GetRoleId() + deleteRole.PrincipalId = tt.role.GetPrincipalId() + deletedRows, err := rw.Delete(context.Background(), &deleteRole) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + found := allocGroupRole() + err = rw.LookupWhere(context.Background(), &found, "role_id = ? and principal_id = ?", tt.role.GetRoleId(), tt.role.GetPrincipalId()) + require.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + }) + } +} + +func TestGroupRole_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + org, proj := TestScopes(t, conn) + t.Run("valid", func(t *testing.T) { + assert := assert.New(t) + grp := TestGroup(t, conn, org.PublicId) + role := TestRole(t, conn, org.PublicId) + grpRole := TestGroupRole(t, conn, org.PublicId, role.PublicId, grp.PublicId) + cp := grpRole.Clone() + assert.True(proto.Equal(cp.(*GroupRole).GroupRole, grpRole.GroupRole)) + }) + t.Run("not-equal", func(t *testing.T) { + assert := assert.New(t) + grp := TestGroup(t, conn, org.PublicId) + grp2 := TestGroup(t, conn, proj.PublicId) + role := TestRole(t, conn, org.PublicId) + role2 := TestRole(t, conn, proj.PublicId) + grpRole := TestGroupRole(t, conn, org.PublicId, role.PublicId, grp.PublicId) + grpRole2 := TestGroupRole(t, conn, proj.PublicId, role2.PublicId, grp2.PublicId) + cp := grpRole.Clone() + assert.True(!proto.Equal(cp.(*GroupRole).GroupRole, grpRole2.GroupRole)) + }) +} + +func TestGroupRole_GetType(t *testing.T) { + t.Parallel() + assert := assert.New(t) + r := &GroupRole{} + ty := r.GetType() + assert.Equal("group", ty) +} diff --git a/internal/iam/repository.go b/internal/iam/repository.go index 8827c4ea5..094286376 100644 --- a/internal/iam/repository.go +++ b/internal/iam/repository.go @@ -111,6 +111,12 @@ func (r *Repository) update(ctx context.Context, resource Resource, fieldMaskPat } metadata["op-type"] = []string{oplog.OpType_OP_TYPE_UPDATE.String()} + dbOpts := []db.Option{db.WithOplog(r.wrapper, metadata)} + opts := getOpts(opt...) + if opts.withSkipVetForWrite { + dbOpts = append(dbOpts, db.WithSkipVetForWrite(true)) + } + var rowsUpdated int var returnedResource interface{} _, err = r.writer.DoTx( @@ -125,7 +131,7 @@ func (r *Repository) update(ctx context.Context, resource Resource, fieldMaskPat returnedResource, fieldMaskPaths, setToNullPaths, - db.WithOplog(r.wrapper, metadata), + dbOpts..., ) if err == nil && rowsUpdated > 1 { // return err, which will result in a rollback of the update diff --git a/internal/iam/repository_group_test.go b/internal/iam/repository_group_test.go index 4801af932..94b3f2fb1 100644 --- a/internal/iam/repository_group_test.go +++ b/internal/iam/repository_group_test.go @@ -202,6 +202,7 @@ func TestRepository_UpdateGroup(t *testing.T) { wantErrMsg string wantIsError error wantDup bool + directUpdate bool }{ { name: "valid", @@ -378,6 +379,19 @@ func TestRepository_UpdateGroup(t *testing.T) { wantErrMsg: " already exists in organization " + org.PublicId, wantIsError: db.ErrNotUnique, }, + { + name: "modified-scope", + args: args{ + name: "modified-scope" + id, + fieldMaskPaths: []string{"ScopeId"}, + ScopeId: proj.PublicId, + opt: []Option{WithSkipVetForWrite(true)}, + }, + newScopeId: org.PublicId, + wantErr: true, + wantErrMsg: `update: failed pq: scope_id cannot be set`, + directUpdate: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -400,13 +414,25 @@ func TestRepository_UpdateGroup(t *testing.T) { updateGrp.Name = tt.args.name updateGrp.Description = tt.args.description - userAfterUpdate, updatedRows, err := repo.UpdateGroup(context.Background(), &updateGrp, tt.args.fieldMaskPaths, tt.args.opt...) + var groupAfterUpdate *Group + var updatedRows int + var err error + if tt.directUpdate { + g := updateGrp.Clone() + var resource interface{} + resource, updatedRows, err = repo.update(context.Background(), g.(*Group), tt.args.fieldMaskPaths, nil, tt.args.opt...) + if err == nil { + groupAfterUpdate = resource.(*Group) + } + } else { + groupAfterUpdate, updatedRows, err = repo.UpdateGroup(context.Background(), &updateGrp, tt.args.fieldMaskPaths, tt.args.opt...) + } if tt.wantErr { assert.Error(err) if tt.wantIsError != nil { assert.True(errors.Is(err, tt.wantIsError)) } - assert.Nil(userAfterUpdate) + assert.Nil(groupAfterUpdate) assert.Equal(0, updatedRows) assert.Contains(err.Error(), tt.wantErrMsg) err = db.TestVerifyOplog(t, rw, u.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) @@ -418,13 +444,13 @@ func TestRepository_UpdateGroup(t *testing.T) { assert.Equal(tt.wantRowsUpdate, updatedRows) switch tt.name { case "valid-no-op": - assert.Equal(u.UpdateTime, userAfterUpdate.UpdateTime) + assert.Equal(u.UpdateTime, groupAfterUpdate.UpdateTime) default: - assert.NotEqual(u.UpdateTime, userAfterUpdate.UpdateTime) + assert.NotEqual(u.UpdateTime, groupAfterUpdate.UpdateTime) } foundGrp, err := repo.LookupGroup(context.Background(), u.PublicId) assert.NoError(err) - assert.True(proto.Equal(userAfterUpdate, foundGrp)) + assert.True(proto.Equal(groupAfterUpdate, foundGrp)) dbassert := dbassert.New(t, rw) if tt.args.name == "" { dbassert.IsNull(foundGrp, "name") diff --git a/internal/iam/repository_principal_role.go b/internal/iam/repository_principal_role.go new file mode 100644 index 000000000..d0d756f13 --- /dev/null +++ b/internal/iam/repository_principal_role.go @@ -0,0 +1,453 @@ +package iam + +import ( + "context" + "fmt" + + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/oplog" +) + +// AddPrincipalRoles provides the ability to add principals (userIds and +// groupIds) to a role (roleId). The role's current db version must match the +// roleVersion or an error will be returned. The roles and groups must be in +// the same scope. User can only be added to roles which are within the user's +// organization, or the role is within a project within the user's organization. +func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleVersion int, userIds, groupIds []string, opt ...Option) ([]PrincipalRole, error) { + // NOTE - we are intentionally not going to check that the scopes are + // correct for the userIds and groupIds, given the roleId. We are going to + // rely on the database constraints and triggers to maintain the integrity + // of these scope relationships. The users and role need to either be in + // the same organization or the role needs to be in a project of the user's + // org. The groups and role have to be in the same scope (org or project). + // There are constraints and triggers to enforce these relationships. + if roleId == "" { + return nil, fmt.Errorf("add principal roles: missing role id: %w", db.ErrInvalidParameter) + } + if len(userIds) == 0 && len(groupIds) == 0 { + return nil, fmt.Errorf("add principal roles: missing either user or groups to add: %w", db.ErrInvalidParameter) + } + + role := allocRole() + role.PublicId = roleId + scope, err := role.GetScope(ctx, r.reader) + if err != nil { + return nil, fmt.Errorf("add principal roles: unable to get role %s scope: %w", roleId, err) + } + + newUserRoles := make([]interface{}, 0, len(userIds)) + for _, id := range userIds { + usrRole, err := NewUserRole(scope.PublicId, roleId, id) + if err != nil { + return nil, fmt.Errorf("add principal roles: unable to create in memory user role: %w", err) + } + newUserRoles = append(newUserRoles, usrRole) + } + newGrpRoles := make([]interface{}, 0, len(groupIds)) + for _, id := range groupIds { + grpRole, err := NewGroupRole(scope.PublicId, roleId, id) + if err != nil { + return nil, fmt.Errorf("add principal roles: unable to create in memory group role: %w", err) + } + newGrpRoles = append(newGrpRoles, grpRole) + } + + _, 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(&role) + if err != nil { + return fmt.Errorf("add principal roles: unable to get ticket: %w", err) + } + 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)) + if err != nil { + return fmt.Errorf("add principal roles: unable to update role version: %w", err) + } + if rowsUpdated != 1 { + return fmt.Errorf("add principal roles: updated role and %d rows updated", rowsUpdated) + } + msgs = append(msgs, &roleOplogMsg) + if len(newUserRoles) > 0 { + userOplogMsgs := make([]*oplog.Message, 0, len(newUserRoles)) + if err := w.CreateItems(ctx, newUserRoles, db.NewOplogMsgs(&userOplogMsgs)); err != nil { + return fmt.Errorf("add principal roles: unable to add users: %w", err) + } + msgs = append(msgs, userOplogMsgs...) + } + if len(newGrpRoles) > 0 { + grpOplogMsgs := make([]*oplog.Message, 0, len(newGrpRoles)) + if err := w.CreateItems(ctx, newGrpRoles, db.NewOplogMsgs(&grpOplogMsgs)); err != nil { + return fmt.Errorf("add principal roles: unable to add groups: %w", err) + } + msgs = append(msgs, grpOplogMsgs...) + } + 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{roleId}, + } + if err := w.WriteOplogEntryWith(ctx, r.wrapper, roleTicket, metadata, msgs); err != nil { + return fmt.Errorf("add principal roles: unable to write oplog: %w", err) + } + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("add principal roles: error creating roles: %w", err) + } + principalRoles := make([]PrincipalRole, 0, len(newUserRoles)+len(newUserRoles)) + for _, role := range newUserRoles { + principalRoles = append(principalRoles, role.(*UserRole)) + } + for _, role := range newGrpRoles { + principalRoles = append(principalRoles, role.(*GroupRole)) + } + return principalRoles, nil +} + +// SetPrincipalRoles will set the role's principals. Set add and/or delete +// principals as need to reconcile the existing principals with the principals +// requested. If both userIds and groupIds are empty, the principal roles will +// be cleared. +func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleVersion int, userIds, groupIds []string, opt ...Option) ([]PrincipalRole, int, error) { + // NOTE - we are intentionally not going to check that the scopes are + // correct for the userIds and groupIds, given the roleId. We are going to + // rely on the database constraints and triggers to maintain the integrity + // of these scope relationships. The users and role need to either be in + // the same organization or the role needs to be in a project of the user's + // org. The groups and role have to be in the same scope (org or project). + // There are constraints and triggers to enforce these relationships. + if roleId == "" { + return nil, db.NoRowsAffected, fmt.Errorf("set principal roles: missing role id: %w", db.ErrInvalidParameter) + } + role := allocRole() + role.PublicId = roleId + scope, err := role.GetScope(ctx, r.reader) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set principal roles: unable to get role %s scope: %w", roleId, err) + } + // 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. + toSet, err := r.principalsToSet(ctx, &role, userIds, groupIds) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set principal roles: unable to determine set: %w", err) + } + + // handle no change to existing principal roles + if len(toSet.addUserRoles) == 0 && len(toSet.addGroupRoles) == 0 && len(toSet.deleteUserRoles) == 0 && len(toSet.deleteGroupRoles) == 0 { + results := make([]PrincipalRole, 0, len(userIds)+len(groupIds)) + for _, id := range userIds { + role, err := NewUserRole(scope.PublicId, roleId, id) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("add principal roles: unable to create in memory user role: %w", err) + } + results = append(results, role) + } + for _, id := range groupIds { + role, err := NewGroupRole(scope.PublicId, roleId, id) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("add principal roles: unable to create in memory group role: %w", err) + } + results = append(results, role) + } + return results, db.NoRowsAffected, nil + } + + var currentPrincipals []PrincipalRole + var totalRowsAffected int + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + + // 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(&role) + if err != nil { + return fmt.Errorf("set principal roles: unable to get ticket for role: %w", err) + } + 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)) + if err != nil { + return fmt.Errorf("set principal roles: unable to update role version: %w", err) + } + if rowsUpdated != 1 { + return fmt.Errorf("set principal roles: updated role and %d rows updated", rowsUpdated) + } + 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}, + "resource-public-id": []string{roleId}, + } + msgs = append(msgs, &roleOplogMsg) + + if len(toSet.deleteUserRoles) > 0 || len(toSet.deleteGroupRoles) > 0 { + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_DELETE.String()) + if len(toSet.deleteUserRoles) > 0 { + userOplogMsgs := make([]*oplog.Message, 0, len(toSet.deleteUserRoles)) + rowsDeleted, err := w.DeleteItems(ctx, toSet.deleteUserRoles, db.NewOplogMsgs(&userOplogMsgs)) + if err != nil { + return fmt.Errorf("set principal roles: unable to delete user roles: %w", err) + } + if rowsDeleted != len(toSet.deleteUserRoles) { + return fmt.Errorf("set principal roles: user roles deleted %d did not match request for %d", rowsDeleted, len(toSet.deleteUserRoles)) + } + totalRowsAffected += rowsDeleted + msgs = append(msgs, userOplogMsgs...) + } + if len(toSet.deleteGroupRoles) > 0 { + grpOplogMsgs := make([]*oplog.Message, 0, len(toSet.deleteGroupRoles)) + rowsDeleted, err := w.DeleteItems(ctx, toSet.deleteGroupRoles, db.NewOplogMsgs(&grpOplogMsgs)) + if err != nil { + return fmt.Errorf("set principal roles: unable to delete groups: %w", err) + } + if rowsDeleted != len(toSet.deleteGroupRoles) { + return fmt.Errorf("set principal roles: group roles deleted %d did not match request for %d", rowsDeleted, len(toSet.deleteGroupRoles)) + } + totalRowsAffected += rowsDeleted + msgs = append(msgs, grpOplogMsgs...) + } + } + if len(toSet.addUserRoles) > 0 || len(toSet.addGroupRoles) > 0 { + metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_CREATE.String()) + if len(toSet.addUserRoles) > 0 { + userOplogMsgs := make([]*oplog.Message, 0, len(toSet.addUserRoles)) + if err := w.CreateItems(ctx, toSet.addUserRoles, db.NewOplogMsgs(&userOplogMsgs)); err != nil { + return fmt.Errorf("set principal roles: unable to add users: %w", err) + } + totalRowsAffected += len(toSet.addUserRoles) + msgs = append(msgs, userOplogMsgs...) + } + if len(toSet.addGroupRoles) > 0 { + grpOplogMsgs := make([]*oplog.Message, 0, len(toSet.addGroupRoles)) + if err := w.CreateItems(ctx, toSet.addGroupRoles, db.NewOplogMsgs(&grpOplogMsgs)); err != nil { + return fmt.Errorf("set principal roles: unable to add groups: %w", err) + } + totalRowsAffected += len(toSet.addGroupRoles) + msgs = append(msgs, grpOplogMsgs...) + } + } + if err := w.WriteOplogEntryWith(ctx, r.wrapper, roleTicket, metadata, msgs); err != nil { + return fmt.Errorf("set principal roles: unable to write oplog for additions: %w", err) + } + + currentPrincipals, err = r.ListPrincipalRoles(ctx, roleId) + if err != nil { + return fmt.Errorf("set principal roles: unable to retrieve current principal roles after sets: %w", err) + } + return nil + }) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set principal roles: unable to set principals: %w", err) + } + return currentPrincipals, totalRowsAffected, nil +} + +// DeletePrincipalRoles principals (userIds and/or groupIds) from a role +// (roleId). The role's current db version must match the roleVersion or an +// error will be returned. +func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, roleVersion int, userIds, groupIds []string, opt ...Option) (int, error) { + if roleId == "" { + return db.NoRowsAffected, fmt.Errorf("delete principal roles: missing role id: %w", db.ErrInvalidParameter) + } + if len(userIds) == 0 && len(groupIds) == 0 { + return db.NoRowsAffected, fmt.Errorf("delete principal roles: missing either user or groups to delete: %w", db.ErrInvalidParameter) + } + role := allocRole() + role.PublicId = roleId + scope, err := role.GetScope(ctx, r.reader) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete principal roles: unable to get role %s scope to create metadata: %w", roleId, err) + } + deleteUserRoles := make([]interface{}, 0, len(userIds)) + for _, id := range userIds { + usrRole, err := NewUserRole(scope.PublicId, roleId, id) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete principal roles: unable to create in memory user role: %w", err) + } + deleteUserRoles = append(deleteUserRoles, usrRole) + } + deleteGrpRoles := make([]interface{}, 0, len(groupIds)) + for _, id := range groupIds { + grpRole, err := NewGroupRole(scope.PublicId, roleId, id) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete principal roles: unable to create in memory group role: %w", err) + } + deleteGrpRoles = append(deleteGrpRoles, grpRole) + } + + var totalRowsDeleted int + _, 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(&role) + if err != nil { + return fmt.Errorf("delete principal roles: unable to get ticket: %w", err) + } + 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)) + if err != nil { + return fmt.Errorf("delete principal roles: unable to update role version: %w", err) + } + if rowsUpdated != 1 { + return fmt.Errorf("delete principal roles: updated role and %d rows updated", rowsUpdated) + } + msgs = append(msgs, &roleOplogMsg) + if len(deleteUserRoles) > 0 { + userOplogMsgs := make([]*oplog.Message, 0, len(deleteUserRoles)) + rowsDeleted, err := w.DeleteItems(ctx, deleteUserRoles, db.NewOplogMsgs(&userOplogMsgs)) + if err != nil { + return fmt.Errorf("delete principal roles: unable to delete user roles: %w", err) + } + if rowsDeleted != len(deleteUserRoles) { + return fmt.Errorf("delete principal roles: user roles deleted %d did not match request for %d", rowsDeleted, len(deleteUserRoles)) + } + totalRowsDeleted += rowsDeleted + msgs = append(msgs, userOplogMsgs...) + } + if len(deleteGrpRoles) > 0 { + grpOplogMsgs := make([]*oplog.Message, 0, len(deleteGrpRoles)) + rowsDeleted, err := w.DeleteItems(ctx, deleteGrpRoles, db.NewOplogMsgs(&grpOplogMsgs)) + if err != nil { + return fmt.Errorf("delete principal roles: unable to delete groups: %w", err) + } + if rowsDeleted != len(deleteGrpRoles) { + return fmt.Errorf("delete principal roles: group roles deleted %d did not match request for %d", rowsDeleted, len(deleteGrpRoles)) + } + totalRowsDeleted += rowsDeleted + msgs = append(msgs, grpOplogMsgs...) + } + metadata := oplog.Metadata{ + "op-type": []string{oplog.OpType_OP_TYPE_DELETE.String()}, + "scope-id": []string{scope.PublicId}, + "scope-type": []string{scope.Type}, + "resource-public-id": []string{roleId}, + } + if err := w.WriteOplogEntryWith(ctx, r.wrapper, roleTicket, metadata, msgs); err != nil { + return fmt.Errorf("delete principal roles: unable to write oplog: %w", err) + } + return nil + }, + ) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete principal roles: error deleting principal roles: %w", err) + } + return totalRowsDeleted, nil +} + +// ListPrincipalRoles returns the principal roles for the roleId and supports the WithLimit option. +func (r *Repository) ListPrincipalRoles(ctx context.Context, roleId string, opt ...Option) ([]PrincipalRole, error) { + if roleId == "" { + return nil, fmt.Errorf("lookup principal roles: missing role id: %w", db.ErrInvalidParameter) + } + var roles []principalRoleView + if err := r.list(ctx, &roles, "role_id = ?", []interface{}{roleId}, opt...); err != nil { + return nil, fmt.Errorf("lookup principal role: unable to lookup roles: %w", err) + } + principals := make([]PrincipalRole, 0, len(roles)) + for _, r := range roles { + principals = append(principals, r) + } + return principals, nil +} + +type principalSet struct { + addUserRoles []interface{} + addGroupRoles []interface{} + deleteUserRoles []interface{} + deleteGroupRoles []interface{} +} + +func (r *Repository) principalsToSet(ctx context.Context, role *Role, userIds, groupIds []string) (*principalSet, error) { + if role == nil { + return nil, fmt.Errorf("missing role: %w", db.ErrNilParameter) + } + existing, err := r.ListPrincipalRoles(ctx, role.PublicId) + if err != nil { + return nil, fmt.Errorf("unable to list existing principal role %s: %w", role.PublicId, err) + } + existingUsers := map[string]PrincipalRole{} + existingGroups := map[string]PrincipalRole{} + for _, p := range existing { + switch p.GetType() { + case UserRoleType.String(): + existingUsers[p.GetPrincipalId()] = p + case GroupRoleType.String(): + existingGroups[p.GetPrincipalId()] = p + default: + return nil, fmt.Errorf("%s is unknown principal type %s", p.GetPrincipalId(), p.GetType()) + } + } + var newUserRoles []interface{} + userIdsMap := map[string]struct{}{} + for _, id := range userIds { + userIdsMap[id] = struct{}{} + if _, ok := existingUsers[id]; !ok { + usrRole, err := NewUserRole(role.ScopeId, role.PublicId, id) + if err != nil { + return nil, fmt.Errorf("unable to create in memory user role for add: %w", err) + } + newUserRoles = append(newUserRoles, usrRole) + } + } + var newGrpRoles []interface{} + groupIdsMap := map[string]struct{}{} + for _, id := range groupIds { + groupIdsMap[id] = struct{}{} + if _, ok := existingGroups[id]; !ok { + grpRole, err := NewGroupRole(role.ScopeId, role.PublicId, id) + if err != nil { + return nil, fmt.Errorf("unable to create in memory group role for add: %w", err) + } + newGrpRoles = append(newGrpRoles, grpRole) + } + } + var deleteUserRoles []interface{} + for _, p := range existingUsers { + if _, ok := userIdsMap[p.GetPrincipalId()]; !ok { + usrRole, err := NewUserRole(p.GetScopeId(), p.GetRoleId(), p.GetPrincipalId()) + if err != nil { + return nil, fmt.Errorf("unable to create in memory user role for delete: %w", err) + } + deleteUserRoles = append(deleteUserRoles, usrRole) + } + } + var deleteGrpRoles []interface{} + for _, p := range existingGroups { + if _, ok := groupIdsMap[p.GetPrincipalId()]; !ok { + grpRole, err := NewGroupRole(p.GetScopeId(), p.GetRoleId(), p.GetPrincipalId()) + if err != nil { + return nil, fmt.Errorf("unable to create in memory group role for delete: %w", err) + } + deleteGrpRoles = append(deleteGrpRoles, grpRole) + } + } + return &principalSet{ + addUserRoles: newUserRoles, + addGroupRoles: newGrpRoles, + deleteUserRoles: deleteUserRoles, + deleteGroupRoles: deleteGrpRoles, + }, nil +} diff --git a/internal/iam/repository_principal_role_test.go b/internal/iam/repository_principal_role_test.go new file mode 100644 index 000000000..3ba6d42ca --- /dev/null +++ b/internal/iam/repository_principal_role_test.go @@ -0,0 +1,730 @@ +package iam + +import ( + "context" + "errors" + "sort" + "testing" + "time" + + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_AddPrincipalRoles(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + require.NoError(t, err) + org, proj := TestScopes(t, conn) + role := TestRole(t, conn, proj.PublicId) + createUsersFn := func() []string { + results := []string{} + for i := 0; i < 5; i++ { + u := TestUser(t, conn, org.PublicId) + results = append(results, u.PublicId) + } + return results + } + createGrpsFn := func() []string { + results := []string{} + for i := 0; i < 5; i++ { + g := TestGroup(t, conn, proj.PublicId) + results = append(results, g.PublicId) + } + return results + } + type args struct { + roleId string + roleVersion int + userIds []string + groupIds []string + opt []Option + } + tests := []struct { + name string + args args + wantErr bool + wantErrIs error + }{ + { + name: "valid-both-users-and-groups", + args: args{ + roleId: role.PublicId, + roleVersion: 1, + userIds: createUsersFn(), + groupIds: createGrpsFn(), + }, + wantErr: false, + }, + { + name: "valid-just-groups", + args: args{ + roleId: role.PublicId, + roleVersion: 2, + userIds: nil, + groupIds: createGrpsFn(), + }, + wantErr: false, + }, + { + name: "valid-just-users", + args: args{ + roleId: role.PublicId, + roleVersion: 3, + userIds: createUsersFn(), + groupIds: nil, + }, + wantErr: false, + }, + { + name: "bad-version", + args: args{ + roleId: role.PublicId, + roleVersion: 1000, + userIds: createUsersFn(), + groupIds: createGrpsFn(), + }, + wantErr: true, + }, + { + name: "no-principals", + args: args{ + roleId: role.PublicId, + roleVersion: 1, + userIds: nil, + groupIds: nil, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NoError(conn.Where("1=1").Delete(allocUserRole()).Error) + require.NoError(conn.Where("1=1").Delete(allocGroupRole()).Error) + got, err := repo.AddPrincipalRoles(context.Background(), tt.args.roleId, tt.args.roleVersion, tt.args.userIds, tt.args.groupIds, tt.args.opt...) + if tt.wantErr { + require.Error(err) + if tt.wantErrIs != nil { + assert.Truef(errors.Is(err, tt.wantErrIs), "unexpected error %s", err.Error()) + } + return + } + require.NoError(err) + gotPrincipal := map[string]PrincipalRole{} + for _, r := range got { + gotPrincipal[r.GetPrincipalId()] = r + } + for _, id := range tt.args.userIds { + assert.NotEmpty(gotPrincipal[id]) + u, err := repo.LookupUser(context.Background(), id) + assert.NoError(err) + assert.Equal(id, u.PublicId) + } + for _, id := range tt.args.groupIds { + assert.NotEmpty(gotPrincipal[id]) + g, err := repo.LookupGroup(context.Background(), id) + assert.NoError(err) + assert.Equal(id, g.PublicId) + } + err = db.TestVerifyOplog(t, rw, role.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + assert.NoError(err) + + foundRoles, err := repo.ListPrincipalRoles(context.Background(), role.PublicId) + require.NoError(err) + for _, r := range foundRoles { + assert.NotEmpty(gotPrincipal[r.GetPrincipalId()]) + assert.Equal(gotPrincipal[r.GetPrincipalId()].GetRoleId(), r.GetRoleId()) + assert.Equal(gotPrincipal[r.GetPrincipalId()].GetScopeId(), r.GetScopeId()) + assert.Equal(gotPrincipal[r.GetPrincipalId()].GetType(), r.GetType()) + } + + }) + } +} + +func TestRepository_ListPrincipalRoles(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + const testLimit = 10 + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper, WithLimit(testLimit)) + require.NoError(t, err) + org, proj := TestScopes(t, conn) + + type args struct { + withRoleId string + opt []Option + } + tests := []struct { + name string + createCnt int + createScopeId string + args args + wantCnt int + wantErr bool + }{ + { + name: "no-limit", + createCnt: repo.defaultLimit + 2, + createScopeId: org.PublicId, + args: args{ + opt: []Option{WithLimit(-1)}, + }, + wantCnt: repo.defaultLimit + 2, + wantErr: false, + }, + { + name: "no-limit-proj-group", + createCnt: repo.defaultLimit + 2, + createScopeId: proj.PublicId, + args: args{ + opt: []Option{WithLimit(-1)}, + }, + wantCnt: repo.defaultLimit + 2, + wantErr: false, + }, + { + name: "default-limit", + createCnt: repo.defaultLimit + 2, + createScopeId: org.PublicId, + wantCnt: repo.defaultLimit, + wantErr: false, + }, + { + name: "custom-limit", + createCnt: repo.defaultLimit + 2, + createScopeId: org.PublicId, + args: args{ + opt: []Option{WithLimit(3)}, + }, + wantCnt: 3, + wantErr: false, + }, + { + name: "bad-role-id", + createCnt: 2, + createScopeId: org.PublicId, + args: args{ + withRoleId: "bad-id", + }, + wantCnt: 0, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + require.NoError(conn.Where("1=1").Delete(allocRole()).Error) + role := TestRole(t, conn, tt.createScopeId) + userRoles := make([]string, 0, tt.createCnt) + groupRoles := make([]string, 0, tt.createCnt) + for i := 0; i < tt.createCnt/2; i++ { + u := TestUser(t, conn, org.PublicId) + userRoles = append(userRoles, u.PublicId) + g := TestGroup(t, conn, tt.createScopeId) + groupRoles = append(groupRoles, g.PublicId) + } + testRoles, err := repo.AddPrincipalRoles(context.Background(), role.PublicId, int(role.Version), userRoles, groupRoles, tt.args.opt...) + require.NoError(err) + assert.Equal(tt.createCnt, len(testRoles)) + + var roleId string + switch { + case tt.args.withRoleId != "": + roleId = tt.args.withRoleId + default: + roleId = role.PublicId + } + got, err := repo.ListPrincipalRoles(context.Background(), roleId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assert.Equal(tt.wantCnt, len(got)) + }) + } +} + +func TestRepository_DeletePrincipalRoles(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + require.NoError(t, err) + org, _ := TestScopes(t, conn) + + type args struct { + role *Role + roleIdOverride *string + createUserCnt int + createGroupCnt int + deleteUserCnt int + deleteGroupCnt int + opt []Option + } + tests := []struct { + name string + args args + wantRowsDeleted int + wantErr bool + wantIsErr error + }{ + { + name: "valid", + args: args{ + role: TestRole(t, conn, org.PublicId), + createUserCnt: 5, + createGroupCnt: 5, + deleteUserCnt: 5, + deleteGroupCnt: 5, + }, + wantRowsDeleted: 10, + wantErr: false, + }, + { + name: "valid-keeping-some", + args: args{ + role: TestRole(t, conn, org.PublicId), + createUserCnt: 5, + createGroupCnt: 5, + deleteUserCnt: 2, + deleteGroupCnt: 2, + }, + wantRowsDeleted: 4, + wantErr: false, + }, + { + name: "no-deletes", + args: args{ + role: TestRole(t, conn, org.PublicId), + createUserCnt: 5, + createGroupCnt: 5, + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "just-user-roles", + args: args{ + role: TestRole(t, conn, org.PublicId), + createUserCnt: 5, + deleteUserCnt: 5, + }, + wantRowsDeleted: 5, + wantErr: false, + }, + { + name: "just-group-roles", + args: args{ + role: TestRole(t, conn, org.PublicId), + createGroupCnt: 5, + deleteGroupCnt: 5, + }, + wantRowsDeleted: 5, + wantErr: false, + }, + { + name: "not-found", + args: args{ + role: TestRole(t, conn, org.PublicId), + roleIdOverride: func() *string { id := testId(t); return &id }(), + createUserCnt: 5, + createGroupCnt: 5, + deleteUserCnt: 5, + deleteGroupCnt: 5, + }, + wantRowsDeleted: 0, + wantErr: true, + }, + { + name: "missing-role-id", + args: args{ + role: TestRole(t, conn, org.PublicId), + roleIdOverride: func() *string { id := ""; return &id }(), + createUserCnt: 5, + createGroupCnt: 5, + deleteUserCnt: 5, + deleteGroupCnt: 5, + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + userIds := make([]string, 0, tt.args.createUserCnt) + for i := 0; i < tt.args.createUserCnt; i++ { + u := TestUser(t, conn, org.PublicId) + userIds = append(userIds, u.PublicId) + } + groupIds := make([]string, 0, tt.args.createGroupCnt) + for i := 0; i < tt.args.createGroupCnt; i++ { + g := TestGroup(t, conn, tt.args.role.ScopeId) + groupIds = append(groupIds, g.PublicId) + } + principalRoles, err := repo.AddPrincipalRoles(context.Background(), tt.args.role.PublicId, 1, userIds, groupIds, tt.args.opt...) + require.NoError(err) + assert.Equal(tt.args.createUserCnt+tt.args.createGroupCnt, len(principalRoles)) + + deleteUserIds := make([]string, 0, tt.args.deleteUserCnt) + for i := 0; i < tt.args.deleteUserCnt; i++ { + deleteUserIds = append(deleteUserIds, userIds[i]) + } + deleteGroupIds := make([]string, 0, tt.args.deleteGroupCnt) + for i := 0; i < tt.args.deleteGroupCnt; i++ { + deleteGroupIds = append(deleteGroupIds, groupIds[i]) + } + var roleId string + switch { + case tt.args.roleIdOverride != nil: + roleId = *tt.args.roleIdOverride + default: + roleId = tt.args.role.PublicId + } + deletedRows, err := repo.DeletePrincipalRoles(context.Background(), roleId, 2, deleteUserIds, deleteGroupIds, tt.args.opt...) + if tt.wantErr { + assert.Error(err) + assert.Equal(0, deletedRows) + if tt.wantIsErr != nil { + assert.Truef(errors.Is(err, tt.wantIsErr), "unexpected error %s", err.Error()) + } + err = db.TestVerifyOplog(t, rw, tt.args.role.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.Is(db.ErrRecordNotFound, err)) + return + } + require.NoError(err) + assert.Equal(tt.wantRowsDeleted, deletedRows) + + err = db.TestVerifyOplog(t, rw, tt.args.role.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + assert.NoError(err) + }) + } +} + +func TestRepository_SetPrincipalRoles(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + repo, err := NewRepository(rw, rw, wrapper) + require.NoError(t, err) + + org, proj := TestScopes(t, conn) + testUser := TestUser(t, conn, org.PublicId) + testGrp := TestGroup(t, conn, proj.PublicId) + + createUsersFn := func() []string { + results := []string{} + for i := 0; i < 5; i++ { + u := TestUser(t, conn, org.PublicId) + results = append(results, u.PublicId) + } + return results + } + createGrpsFn := func() []string { + results := []string{} + for i := 0; i < 5; i++ { + g := TestGroup(t, conn, proj.PublicId) + results = append(results, g.PublicId) + } + return results + } + setupFn := func(role *Role) ([]string, []string) { + users := createUsersFn() + grps := createGrpsFn() + _, err := repo.AddPrincipalRoles(context.Background(), role.PublicId, 1, users, grps) + require.NoError(t, err) + return users, grps + } + type args struct { + role *Role + roleVersion int + userIds []string + groupIds []string + addToOrigUsers bool + addToOrigGrps bool + opt []Option + } + tests := []struct { + name string + setup func(*Role) ([]string, []string) + args args + wantAffectedRows int + wantErr bool + }{ + { + name: "clear", + setup: setupFn, + args: args{ + role: TestRole(t, conn, proj.PublicId), + roleVersion: 2, // yep, since setupFn will increment it to 2 + userIds: []string{}, + groupIds: []string{}, + }, + wantErr: false, + wantAffectedRows: 10, + }, + { + name: "no change", + setup: setupFn, + args: args{ + role: TestRole(t, conn, proj.PublicId), + roleVersion: 2, // yep, since setupFn will increment it to 2 + userIds: []string{}, + groupIds: []string{}, + addToOrigUsers: true, + addToOrigGrps: true, + }, + wantErr: false, + wantAffectedRows: 0, + }, + { + name: "add users and grps", + setup: setupFn, + args: args{ + role: TestRole(t, conn, proj.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: "remove existing and add users and grps", + setup: setupFn, + args: args{ + role: TestRole(t, conn, proj.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: 12, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + var origUsers, origGrps []string + if tt.setup != nil { + origUsers, origGrps = tt.setup(tt.args.role) + } + setUsers := tt.args.userIds + setGrps := tt.args.groupIds + if tt.args.addToOrigUsers { + setUsers = append(setUsers, origUsers...) + } + if tt.args.addToOrigGrps { + setGrps = append(setGrps, origGrps...) + } + + got, affectedRows, err := repo.SetPrincipalRoles(context.Background(), tt.args.role.PublicId, tt.args.roleVersion, setUsers, setGrps, tt.args.opt...) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assert.Equal(tt.wantAffectedRows, affectedRows) + var gotIds []string + for _, r := range got { + gotIds = append(gotIds, r.GetPrincipalId()) + } + var wantIds []string + wantIds = append(wantIds, tt.args.userIds...) + wantIds = append(wantIds, tt.args.groupIds...) + sort.Strings(wantIds) + sort.Strings(gotIds) + assert.Equal(wantIds, wantIds) + }) + } +} + +func TestRepository_principalsToSet(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + require.NoError(t, err) + org, proj := TestScopes(t, conn) + createUsersFn := func() []string { + results := []string{} + for i := 0; i < 5; i++ { + u := TestUser(t, conn, org.PublicId) + results = append(results, u.PublicId) + } + return results + } + createGrpsFn := func() []string { + results := []string{} + for i := 0; i < 5; i++ { + g := TestGroup(t, conn, proj.PublicId) + results = append(results, g.PublicId) + } + return results + } + setupFn := func() (*Role, []string, []string) { + users := createUsersFn() + grps := createGrpsFn() + role := TestRole(t, conn, proj.PublicId) + _, err := repo.AddPrincipalRoles(context.Background(), role.PublicId, 1, users, grps) + require.NoError(t, err) + return role, users, grps + } + + type args struct { + userIds []string + groupIds []string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "all new", + args: args{ + userIds: createUsersFn(), + groupIds: createGrpsFn(), + }, + wantErr: false, + }, + { + name: "clear all", + args: args{ + userIds: nil, + groupIds: nil, + }, + wantErr: false, + }, + { + name: "just new users", + args: args{ + userIds: createUsersFn(), + groupIds: nil, + }, + wantErr: false, + }, + { + name: "just new groups", + args: args{ + userIds: nil, + groupIds: createGrpsFn(), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + r, origUsers, origGrps := setupFn() + got, err := repo.principalsToSet(context.Background(), r, tt.args.userIds, tt.args.groupIds) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assertSetResults(t, got, tt.args.userIds, tt.args.groupIds, origUsers, origGrps) + }) + } + t.Run("nil role", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + _, users, grps := setupFn() + got, err := repo.principalsToSet(context.Background(), nil, users, grps) + require.Error(err) + assert.Nil(got) + assert.Truef(errors.Is(err, db.ErrNilParameter), "unexpected error %s", err.Error()) + }) + t.Run("no change", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + r, users, grps := setupFn() + got, err := repo.principalsToSet(context.Background(), r, users, grps) + require.NoError(err) + assert.Empty(got.addUserRoles) + assert.Empty(got.addGroupRoles) + assert.Empty(got.deleteUserRoles) + assert.Empty(got.deleteGroupRoles) + }) + t.Run("mixed", func(t *testing.T) { + require := require.New(t) + r, users, grps := setupFn() + var wantSetUsers, wantSetGrps, wantDeleteUsers, wantDeleteGrps []string + for i, id := range users { + if i < 2 { + wantSetUsers = append(wantSetUsers, id) + } else { + wantDeleteUsers = append(wantDeleteUsers, id) + } + } + for i, id := range grps { + if i < 2 { + wantSetGrps = append(wantSetGrps, id) + } else { + wantDeleteGrps = append(wantDeleteGrps, id) + } + } + newUser := TestUser(t, conn, org.PublicId) + newGrp := TestGroup(t, conn, proj.PublicId) + wantSetUsers = append(wantSetUsers, newUser.PublicId) + wantSetGrps = append(wantSetGrps, newGrp.PublicId) + + got, err := repo.principalsToSet(context.Background(), r, wantSetUsers, wantSetGrps) + require.NoError(err) + assertSetResults(t, got, []string{newUser.PublicId}, []string{newGrp.PublicId}, wantDeleteUsers, wantDeleteGrps) + }) +} + +func assertSetResults(t *testing.T, got *principalSet, wantAddUsers, wantAddGroups, wantDeleteUsers, wantDeleteGroups []string) { + t.Helper() + assert := assert.New(t) + var gotAddUsers []string + for _, r := range got.addUserRoles { + gotAddUsers = append(gotAddUsers, r.(*UserRole).PrincipalId) + } + // sort.Strings(wantAddUsers) + // sort.Strings(gotAddUsers) + assert.Equal(wantAddUsers, gotAddUsers) + + var gotAddGrps []string + for _, r := range got.addGroupRoles { + gotAddGrps = append(gotAddGrps, r.(*GroupRole).PrincipalId) + } + // sort.Strings(wantAddGroups) + // sort.Strings(gotAddGrps) + assert.Equal(wantAddGroups, gotAddGrps) + + var gotDeleteUsers []string + for _, r := range got.deleteUserRoles { + gotDeleteUsers = append(gotDeleteUsers, r.(*UserRole).PrincipalId) + } + sort.Strings(wantDeleteUsers) + sort.Strings(gotDeleteUsers) + assert.Equal(wantDeleteUsers, gotDeleteUsers) + + var gotDeleteGroups []string + for _, r := range got.deleteGroupRoles { + gotDeleteGroups = append(gotDeleteGroups, r.(*GroupRole).PrincipalId) + } + sort.Strings(wantDeleteGroups) + sort.Strings(gotDeleteGroups) + assert.Equal(wantDeleteGroups, gotDeleteGroups) +} diff --git a/internal/iam/repository_user.go b/internal/iam/repository_user.go index d269b3397..bc6330ca7 100644 --- a/internal/iam/repository_user.go +++ b/internal/iam/repository_user.go @@ -66,8 +66,9 @@ func (r *Repository) UpdateUser(ctx context.Context, user *User, fieldMaskPaths if len(dbMask) == 0 && len(nullFields) == 0 { return nil, db.NoRowsAffected, fmt.Errorf("update user: %w", db.ErrEmptyFieldMask) } + u := user.Clone() - resource, rowsUpdated, err := r.update(ctx, u.(*User), dbMask, nullFields) + resource, rowsUpdated, err := r.update(ctx, u.(*User), dbMask, nullFields, opt...) if err != nil { if db.IsUniqueError(err) { return nil, db.NoRowsAffected, fmt.Errorf("update user: user %s already exists in organization %s", user.Name, user.ScopeId) diff --git a/internal/iam/repository_user_test.go b/internal/iam/repository_user_test.go index d7148a8a5..f2a9eeba9 100644 --- a/internal/iam/repository_user_test.go +++ b/internal/iam/repository_user_test.go @@ -136,6 +136,7 @@ func TestRepository_UpdateUser(t *testing.T) { wantErrMsg string wantIsErr error wantDup bool + directUpdate bool }{ { name: "valid", @@ -280,6 +281,18 @@ func TestRepository_UpdateUser(t *testing.T) { wantDup: true, wantErrMsg: `update user: user %s already exists in organization %s`, }, + { + name: "modified-scope", + args: args{ + name: "modified-scope" + id, + fieldMaskPaths: []string{"ScopeId"}, + ScopeId: proj.PublicId, + opt: []Option{WithSkipVetForWrite(true)}, + }, + wantErr: true, + wantErrMsg: `update: failed pq: scope_id cannot be set`, + directUpdate: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -302,7 +315,19 @@ func TestRepository_UpdateUser(t *testing.T) { updateUser.Name = tt.args.name updateUser.Description = tt.args.description - userAfterUpdate, updatedRows, err := repo.UpdateUser(context.Background(), &updateUser, tt.args.fieldMaskPaths, tt.args.opt...) + var userAfterUpdate *User + var updatedRows int + var err error + if tt.directUpdate { + u := updateUser.Clone() + var resource interface{} + resource, updatedRows, err = repo.update(context.Background(), u.(*User), tt.args.fieldMaskPaths, nil, tt.args.opt...) + if err == nil { + userAfterUpdate = resource.(*User) + } + } else { + userAfterUpdate, updatedRows, err = repo.UpdateUser(context.Background(), &updateUser, tt.args.fieldMaskPaths, tt.args.opt...) + } if tt.wantErr { require.Error(err) if tt.wantIsErr != nil { @@ -314,7 +339,7 @@ func TestRepository_UpdateUser(t *testing.T) { case "dup-name": assert.Equal(fmt.Sprintf(tt.wantErrMsg, "dup-name"+id, org.PublicId), err.Error()) default: - assert.Equal(tt.wantErrMsg, err.Error()) + assert.True(strings.Contains(err.Error(), tt.wantErrMsg)) } err = db.TestVerifyOplog(t, rw, u.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) require.Error(err) diff --git a/internal/iam/store/principal_role.pb.go b/internal/iam/store/principal_role.pb.go new file mode 100644 index 000000000..cbf232649 --- /dev/null +++ b/internal/iam/store/principal_role.pb.go @@ -0,0 +1,422 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.24.0 +// protoc v3.12.3 +// source: controller/storage/iam/store/v1/principal_role.proto + +package store + +import ( + proto "github.com/golang/protobuf/proto" + timestamp "github.com/hashicorp/watchtower/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) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type UserRole struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // 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 role of this principal. + // @inject_tag: gorm:"primary_key" + RoleId string `protobuf:"bytes,2,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty" gorm:"primary_key"` + // principal_id is the public_id of the user (which is the principal) + // @inject_tag: gorm:"primary_key" + PrincipalId string `protobuf:"bytes,3,opt,name=principal_id,json=principalId,proto3" json:"principal_id,omitempty" gorm:"primary_key"` + // scope_id of the principal role + // @inject_tag: `gorm:"default:null"` + ScopeId string `protobuf:"bytes,4,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` +} + +func (x *UserRole) Reset() { + *x = UserRole{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UserRole) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserRole) ProtoMessage() {} + +func (x *UserRole) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserRole.ProtoReflect.Descriptor instead. +func (*UserRole) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_principal_role_proto_rawDescGZIP(), []int{0} +} + +func (x *UserRole) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *UserRole) GetRoleId() string { + if x != nil { + return x.RoleId + } + return "" +} + +func (x *UserRole) GetPrincipalId() string { + if x != nil { + return x.PrincipalId + } + return "" +} + +func (x *UserRole) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +type GroupRole struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // 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 role of this principal. + // @inject_tag: gorm:"primary_key" + RoleId string `protobuf:"bytes,2,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty" gorm:"primary_key"` + // principal_id is the public_id of the group (which is the principal) + // @inject_tag: gorm:"primary_key" + PrincipalId string `protobuf:"bytes,3,opt,name=principal_id,json=principalId,proto3" json:"principal_id,omitempty" gorm:"primary_key"` + // scope_id of the principal role + // @inject_tag: `gorm:"default:null"` + ScopeId string `protobuf:"bytes,4,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` +} + +func (x *GroupRole) Reset() { + *x = GroupRole{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GroupRole) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GroupRole) ProtoMessage() {} + +func (x *GroupRole) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GroupRole.ProtoReflect.Descriptor instead. +func (*GroupRole) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_principal_role_proto_rawDescGZIP(), []int{1} +} + +func (x *GroupRole) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *GroupRole) GetRoleId() string { + if x != nil { + return x.RoleId + } + return "" +} + +func (x *GroupRole) GetPrincipalId() string { + if x != nil { + return x.PrincipalId + } + return "" +} + +func (x *GroupRole) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +type PrincipalRoleView struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // 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 role of this principal. + // @inject_tag: gorm:"primary_key" + RoleId string `protobuf:"bytes,2,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty" gorm:"primary_key"` + // Principal type (User or Group) + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + // @inject_tag: gorm:"primary_key" + PrincipalId string `protobuf:"bytes,4,opt,name=principal_id,json=principalId,proto3" json:"principal_id,omitempty" gorm:"primary_key"` + // scope_id of the principal role + // @inject_tag: `gorm:"default:null"` + ScopeId string `protobuf:"bytes,5,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` +} + +func (x *PrincipalRoleView) Reset() { + *x = PrincipalRoleView{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PrincipalRoleView) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrincipalRoleView) ProtoMessage() {} + +func (x *PrincipalRoleView) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrincipalRoleView.ProtoReflect.Descriptor instead. +func (*PrincipalRoleView) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_principal_role_proto_rawDescGZIP(), []int{2} +} + +func (x *PrincipalRoleView) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *PrincipalRoleView) GetRoleId() string { + if x != nil { + return x.RoleId + } + return "" +} + +func (x *PrincipalRoleView) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *PrincipalRoleView) GetPrincipalId() string { + if x != nil { + return x.PrincipalId + } + return "" +} + +func (x *PrincipalRoleView) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +var File_controller_storage_iam_store_v1_principal_role_proto protoreflect.FileDescriptor + +var file_controller_storage_iam_store_v1_principal_role_proto_rawDesc = []byte{ + 0x0a, 0x34, 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, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x5f, 0x72, 0x6f, 0x6c, 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, 0x1a, 0x2b, 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, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xae, 0x01, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, 0x52, 0x6f, + 0x6c, 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, 0x21, 0x0a, 0x0c, 0x70, 0x72, 0x69, 0x6e, + 0x63, 0x69, 0x70, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x22, 0xaf, 0x01, 0x0a, 0x09, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x52, 0x6f, 0x6c, 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, 0x21, 0x0a, 0x0c, 0x70, 0x72, + 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x19, 0x0a, + 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x22, 0xcb, 0x01, 0x0a, 0x11, 0x50, 0x72, 0x69, + 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x52, 0x6f, 0x6c, 0x65, 0x56, 0x69, 0x65, 0x77, 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, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x72, 0x69, 0x6e, + 0x63, 0x69, 0x70, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x42, 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, + 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 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_principal_role_proto_rawDescOnce sync.Once + file_controller_storage_iam_store_v1_principal_role_proto_rawDescData = file_controller_storage_iam_store_v1_principal_role_proto_rawDesc +) + +func file_controller_storage_iam_store_v1_principal_role_proto_rawDescGZIP() []byte { + file_controller_storage_iam_store_v1_principal_role_proto_rawDescOnce.Do(func() { + file_controller_storage_iam_store_v1_principal_role_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_iam_store_v1_principal_role_proto_rawDescData) + }) + return file_controller_storage_iam_store_v1_principal_role_proto_rawDescData +} + +var file_controller_storage_iam_store_v1_principal_role_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_storage_iam_store_v1_principal_role_proto_goTypes = []interface{}{ + (*UserRole)(nil), // 0: controller.storage.iam.store.v1.UserRole + (*GroupRole)(nil), // 1: controller.storage.iam.store.v1.GroupRole + (*PrincipalRoleView)(nil), // 2: controller.storage.iam.store.v1.PrincipalRoleView + (*timestamp.Timestamp)(nil), // 3: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_iam_store_v1_principal_role_proto_depIdxs = []int32{ + 3, // 0: controller.storage.iam.store.v1.UserRole.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 1: controller.storage.iam.store.v1.GroupRole.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 2: controller.storage.iam.store.v1.PrincipalRoleView.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_controller_storage_iam_store_v1_principal_role_proto_init() } +func file_controller_storage_iam_store_v1_principal_role_proto_init() { + if File_controller_storage_iam_store_v1_principal_role_proto != nil { + return + } + file_controller_storage_iam_store_v1_scope_proto_init() + if !protoimpl.UnsafeEnabled { + file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UserRole); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GroupRole); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PrincipalRoleView); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_iam_store_v1_principal_role_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_iam_store_v1_principal_role_proto_goTypes, + DependencyIndexes: file_controller_storage_iam_store_v1_principal_role_proto_depIdxs, + MessageInfos: file_controller_storage_iam_store_v1_principal_role_proto_msgTypes, + }.Build() + File_controller_storage_iam_store_v1_principal_role_proto = out.File + file_controller_storage_iam_store_v1_principal_role_proto_rawDesc = nil + file_controller_storage_iam_store_v1_principal_role_proto_goTypes = nil + file_controller_storage_iam_store_v1_principal_role_proto_depIdxs = nil +} diff --git a/internal/iam/store/role.pb.go b/internal/iam/store/role.pb.go index 6fde76a78..56587999a 100644 --- a/internal/iam/store/role.pb.go +++ b/internal/iam/store/role.pb.go @@ -53,6 +53,10 @@ type Role struct { // disabled is by default false and allows a Role to be marked disabled. // @inject_tag: `gorm:"default:null"` Disabled bool `protobuf:"varint,9,opt,name=disabled,proto3" json:"disabled,omitempty" gorm:"default:null"` + // 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,10,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` } func (x *Role) Reset() { @@ -136,6 +140,13 @@ func (x *Role) GetDisabled() bool { return false } +func (x *Role) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + var File_controller_storage_iam_store_v1_role_proto protoreflect.FileDescriptor var file_controller_storage_iam_store_v1_role_proto_rawDesc = []byte{ @@ -149,7 +160,7 @@ var file_controller_storage_iam_store_v1_role_proto_rawDesc = []byte{ 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2b, 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, - 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xaa, 0x02, 0x0a, 0x04, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc4, 0x02, 0x0a, 0x04, 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, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, @@ -168,11 +179,13 @@ var file_controller_storage_iam_store_v1_role_proto_rawDesc = []byte{ 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, - 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 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, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x42, 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, + 0x74, 0x6f, 0x77, 0x65, 0x72, 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 ( diff --git a/internal/iam/testing.go b/internal/iam/testing.go index 8cc67b21e..391d3cbda 100644 --- a/internal/iam/testing.go +++ b/internal/iam/testing.go @@ -142,6 +142,30 @@ func TestGroup(t *testing.T, conn *gorm.DB, scopeId string, opt ...Option) *Grou return grp } +func TestUserRole(t *testing.T, conn *gorm.DB, scopeId, roleId, userId string, opt ...Option) *UserRole { + t.Helper() + require := require.New(t) + rw := db.New(conn) + r, err := NewUserRole(scopeId, roleId, userId, opt...) + require.NoError(err) + + err = rw.Create(context.Background(), r) + require.NoError(err) + return r.(*UserRole) +} + +func TestGroupRole(t *testing.T, conn *gorm.DB, scopeId, roleId, grpId string, opt ...Option) *GroupRole { + t.Helper() + require := require.New(t) + rw := db.New(conn) + r, err := NewGroupRole(scopeId, roleId, grpId, opt...) + require.NoError(err) + + err = rw.Create(context.Background(), r) + require.NoError(err) + return r.(*GroupRole) +} + // testAuthAccount is a temporary test function. TODO - replace with an auth // subsystem testAuthAccount function. If userId is zero value, then an auth // account will be created with a null IamUserId diff --git a/internal/iam/testing_test.go b/internal/iam/testing_test.go index b18d89f05..a8136cceb 100644 --- a/internal/iam/testing_test.go +++ b/internal/iam/testing_test.go @@ -85,3 +85,45 @@ func Test_TestRole(t *testing.T) { require.NotNil(projRole) assert.NotEmpty(projRole.PublicId) } + +func Test_TestUserRole(t *testing.T) { + t.Helper() + require := require.New(t) + conn, _ := db.TestSetup(t, "postgres") + org, proj := TestScopes(t, conn) + orgRole := TestRole(t, conn, org.PublicId) + projRole := TestRole(t, conn, proj.PublicId) + user := TestUser(t, conn, org.PublicId) + + userRole := TestUserRole(t, conn, org.PublicId, orgRole.PublicId, user.PublicId) + require.NotNil(userRole) + require.Equal(orgRole.PublicId, userRole.RoleId) + require.Equal(user.PublicId, userRole.PrincipalId) + + userRole = TestUserRole(t, conn, proj.PublicId, projRole.PublicId, user.PublicId) + require.NotNil(userRole) + require.Equal(projRole.PublicId, userRole.RoleId) + require.Equal(user.PublicId, userRole.PrincipalId) +} + +func Test_TestGroupRole(t *testing.T) { + t.Helper() + require := require.New(t) + conn, _ := db.TestSetup(t, "postgres") + org, proj := TestScopes(t, conn) + orgRole := TestRole(t, conn, org.PublicId) + orgGroup := TestGroup(t, conn, org.PublicId) + + projRole := TestRole(t, conn, proj.PublicId) + projGroup := TestGroup(t, conn, proj.PublicId) + + groupRole := TestGroupRole(t, conn, org.PublicId, orgRole.PublicId, orgGroup.PublicId) + require.NotNil(groupRole) + require.Equal(orgRole.PublicId, groupRole.RoleId) + require.Equal(orgGroup.PublicId, groupRole.PrincipalId) + + groupRole = TestGroupRole(t, conn, proj.PublicId, projRole.PublicId, projGroup.PublicId) + require.NotNil(groupRole) + require.Equal(projRole.PublicId, groupRole.RoleId) + require.Equal(projGroup.PublicId, groupRole.PrincipalId) +} diff --git a/internal/proto/local/controller/storage/iam/store/v1/principal_role.proto b/internal/proto/local/controller/storage/iam/store/v1/principal_role.proto new file mode 100644 index 000000000..f6b6af94c --- /dev/null +++ b/internal/proto/local/controller/storage/iam/store/v1/principal_role.proto @@ -0,0 +1,64 @@ + +syntax = "proto3"; + +package controller.storage.iam.store.v1; +option go_package = "github.com/hashicorp/watchtower/internal/iam/store;store"; + +import "controller/storage/timestamp/v1/timestamp.proto"; +import "controller/storage/iam/store/v1/scope.proto"; + +message UserRole { + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 1; + + // role_id is the role of this principal. + // @inject_tag: gorm:"primary_key" + string role_id = 2; + + // principal_id is the public_id of the user (which is the principal) + // @inject_tag: gorm:"primary_key" + string principal_id = 3; + + // scope_id of the principal role + // @inject_tag: `gorm:"default:null"` + string scope_id = 4; +} + +message GroupRole { + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 1; + + // role_id is the role of this principal. + // @inject_tag: gorm:"primary_key" + string role_id = 2; + + // principal_id is the public_id of the group (which is the principal) + // @inject_tag: gorm:"primary_key" + string principal_id = 3; + + // scope_id of the principal role + // @inject_tag: `gorm:"default:null"` + string scope_id = 4; +} + +message PrincipalRoleView { + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 1; + + // role_id is the role of this principal. + // @inject_tag: gorm:"primary_key" + string role_id = 2; + + // Principal type (User or Group) + string type = 3; + + // @inject_tag: gorm:"primary_key" + string principal_id = 4; + + // scope_id of the principal role + // @inject_tag: `gorm:"default:null"` + string scope_id = 5; +} \ No newline at end of file diff --git a/internal/proto/local/controller/storage/iam/store/v1/role.proto b/internal/proto/local/controller/storage/iam/store/v1/role.proto index 4552770eb..cfd4686fb 100644 --- a/internal/proto/local/controller/storage/iam/store/v1/role.proto +++ b/internal/proto/local/controller/storage/iam/store/v1/role.proto @@ -35,4 +35,9 @@ message Role { // disabled is by default false and allows a Role to be marked disabled. // @inject_tag: `gorm:"default:null"` bool disabled = 9; + + // 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 = 10; }