From bcdcaffa6f4ab8d3c380a9410fd6bbdf99838d38 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 10 Jul 2020 15:46:26 -0400 Subject: [PATCH] Port over 162 and 165 to master (#176) Co-authored-by: Jim Lambert Co-authored-by: Michael Gaffney --- Makefile | 1 + api/roles/role.gen.go | 6 + internal/db/domains_test.go | 85 ++-- internal/db/migrations/postgres.gen.go | 297 ++++++++---- .../postgres/01_domain_types.down.sql | 2 + .../postgres/01_domain_types.up.sql | 14 + .../db/migrations/postgres/06_iam.down.sql | 7 +- internal/db/migrations/postgres/06_iam.up.sql | 274 +++++++---- internal/db/read_writer.go | 22 +- internal/db/read_writer_test.go | 10 +- internal/gen/controller.swagger.json | 4 + .../controller/api/resources/roles/role.pb.go | 65 ++- internal/iam/docs.go | 3 + internal/iam/group_test.go | 6 +- internal/iam/ids.go | 7 +- internal/iam/options.go | 23 +- internal/iam/options_test.go | 7 + internal/iam/principal_role.go | 100 ++-- internal/iam/principal_role_test.go | 208 ++++----- internal/iam/repository_group_test.go | 2 +- internal/iam/repository_principal_role.go | 126 +++-- .../iam/repository_principal_role_test.go | 212 +++++---- internal/iam/repository_role.go | 17 +- internal/iam/repository_role_grant.go | 368 +++++++++++++++ internal/iam/repository_role_grant_test.go | 441 ++++++++++++++++++ internal/iam/repository_role_test.go | 5 +- internal/iam/repository_user_test.go | 8 +- internal/iam/role.go | 20 +- internal/iam/role_grant.go | 124 +++++ internal/iam/role_grant_test.go | 263 +++++++++++ internal/iam/role_test.go | 141 +++++- internal/iam/store/principal_role.pb.go | 115 ++--- internal/iam/store/role.pb.go | 25 +- internal/iam/store/role_grant.pb.go | 205 ++++++++ internal/iam/testing.go | 28 +- internal/iam/testing_test.go | 50 +- internal/iam/user.go | 8 +- internal/iam/user_test.go | 49 +- internal/perms/acl_test.go | 6 +- internal/perms/grants_test.go | 18 +- .../api/resources/roles/v1/role.proto | 15 +- .../storage/iam/store/v1/principal_role.proto | 20 +- .../storage/iam/store/v1/role.proto | 5 + .../storage/iam/store/v1/role_grant.proto | 26 ++ .../controller/handlers/roles/role_service.go | 9 + .../handlers/roles/role_service_test.go | 32 +- internal/types/action/action.go | 56 ++- internal/types/action/action_test.go | 28 +- 48 files changed, 2791 insertions(+), 772 deletions(-) create mode 100644 internal/iam/docs.go create mode 100644 internal/iam/repository_role_grant.go create mode 100644 internal/iam/repository_role_grant_test.go create mode 100644 internal/iam/role_grant.go create mode 100644 internal/iam/role_grant_test.go create mode 100644 internal/iam/store/role_grant.pb.go create mode 100644 internal/proto/local/controller/storage/iam/store/v1/role_grant.proto diff --git a/Makefile b/Makefile index 9816217d2..aac8bda01 100644 --- a/Makefile +++ b/Makefile @@ -70,6 +70,7 @@ protobuild: @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_grant.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/api/roles/role.gen.go b/api/roles/role.gen.go index 048909ec3..13d13cf3b 100644 --- a/api/roles/role.gen.go +++ b/api/roles/role.gen.go @@ -31,6 +31,12 @@ type Role struct { UpdatedTime time.Time `json:"updated_time,omitempty"` // Whether the resource is disabled Disabled *bool `json:"disabled,omitempty"` + // The scope the grants will apply to. If the role is at the global scope, + // this can be an organization or project. If the role is at an organization + // scope, this can be a project within the organization. It is invalid for + // this to be anything other than the role's scope when the role's scope is + // a project. + GrantScopeId *string `json:"grant_scope_id,omitempty"` // The version can be used in subsiquent write requests to ensure this resource // has not changed and to fail the write if it has. // Output only. diff --git a/internal/db/domains_test.go b/internal/db/domains_test.go index de6f3b1aa..4ce328d49 100644 --- a/internal/db/domains_test.go +++ b/internal/db/domains_test.go @@ -1,6 +1,7 @@ package db import ( + "strings" "testing" "time" @@ -10,16 +11,16 @@ import ( "github.com/stretchr/testify/require" ) -func TestDomain_PublicId(t *testing.T) { +func TestDomain_PublicPrivateId(t *testing.T) { const ( - createTable = ` -create table if not exists test_table_public_id ( + createTableBase = ` +create table if not exists test_table_{{rep}}_id ( id bigint generated always as identity primary key, - public_id wt_public_id + {{rep}}_id wt_{{rep}}_id ); ` - insert = ` -insert into test_table_public_id (public_id) + insertBase = ` +insert into test_table_{{rep}}_id ({{rep}}_id) values ($1) returning id; ` @@ -27,41 +28,47 @@ returning id; conn, _ := TestSetup(t, "postgres") db := conn.DB() - if _, err := db.Exec(createTable); err != nil { - t.Fatalf("query: \n%s\n error: %s", createTable, err) - } - failTests := []string{ - " ", - "bar", - "00000009", - } - for _, tt := range failTests { - value := tt - t.Run(tt, func(t *testing.T) { - t.Logf("insert value: %q", value) - if _, err := db.Query(insert, value); err == nil { - t.Errorf("want error, got no error for inserting public_id: %s", value) - } - }) - } + for _, typ := range []string{"public", "private"} { + createTable := strings.Replace(createTableBase, "{{rep}}", typ, -1) + insert := strings.Replace(insertBase, "{{rep}}", typ, -1) - okTests := []string{ - "l1Ocw0TpHn800CekIxIXlmQqRDgFDfYl", - "00000000010000000002000000000312", - "00000000010000000002000000000032", - "12345678901234567890123456789012", - "ec2_12345678901234567890123456789012", - "prj_12345678901234567890123456789012", - } - for _, tt := range okTests { - value := tt - t.Run(tt, func(t *testing.T) { - t.Logf("insert value: %q", value) - if _, err := db.Query(insert, value); err != nil { - t.Errorf("%s: want no error, got error %v", value, err) - } - }) + if _, err := db.Exec(createTable); err != nil { + t.Fatalf("query: \n%s\n error: %s", createTable, err) + } + + failTests := []string{ + " ", + "bar", + "00000009", + } + for _, tt := range failTests { + value := tt + t.Run(tt, func(t *testing.T) { + t.Logf("insert value: %q", value) + if _, err := db.Query(insert, value); err == nil { + t.Errorf("want error, got no error for inserting public_id: %s", value) + } + }) + } + + okTests := []string{ + "l1Ocw0TpHn800CekIxIXlmQqRDgFDfYl", + "00000000010000000002000000000312", + "00000000010000000002000000000032", + "12345678901234567890123456789012", + "ec2_12345678901234567890123456789012", + "prj_12345678901234567890123456789012", + } + for _, tt := range okTests { + value := tt + t.Run(tt, func(t *testing.T) { + t.Logf("insert value: %q", value) + if _, err := db.Query(insert, value); err != nil { + t.Errorf("%s: want no error, got error %v", value, err) + } + }) + } } } diff --git a/internal/db/migrations/postgres.gen.go b/internal/db/migrations/postgres.gen.go index a0cdc713a..e38c47265 100644 --- a/internal/db/migrations/postgres.gen.go +++ b/internal/db/migrations/postgres.gen.go @@ -12,7 +12,9 @@ begin; drop domain wt_timestamp; drop domain wt_public_id; +drop domain wt_private_id; drop domain wt_scope_id; +drop domain wt_user_id; drop domain wt_version; drop function default_create_time; @@ -36,6 +38,13 @@ check( comment on domain wt_public_id is 'Random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; +create domain wt_private_id as text +check( + length(trim(value)) > 10 +); +comment on domain wt_private_id is +'Random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; + create domain wt_scope_id as text check( length(trim(value)) > 10 or value = 'global' @@ -43,6 +52,13 @@ check( comment on domain wt_scope_id is '"global" or random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; +create domain wt_user_id as text +check( + length(trim(value)) > 10 or value = 'u_anon' or value = 'u_auth' +); +comment on domain wt_scope_id is +'"u_anon", "u_auth", or random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; + create domain wt_timestamp as timestamp with time zone default current_timestamp; @@ -430,14 +446,19 @@ drop table iam_role cascade; drop view iam_principal_role cascade; drop table iam_group_role cascade; drop table iam_user_role cascade; +drop table iam_role_grant 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 grant_scope_id_valid cascade; drop function immutable_scope_id_func cascade; +drop function disallow_global_scope_deletion cascade; +drop function user_scope_id_valid cascade; +drop function iam_immutable_role_grant cascade; +drop function disallow_iam_anon_auth_deletion cascade; COMMIT; @@ -660,7 +681,7 @@ insert into iam_scope (public_id, name, type, description) create table iam_user ( - public_id wt_public_id not null primary key, + public_id wt_user_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, @@ -688,6 +709,63 @@ begin end; $$ language plpgsql; +create or replace function + grant_scope_id_valid() + returns trigger +as $$ +declare parent_scope_id text; +declare role_scope_type text; +begin + -- There is a not-null constraint so ensure that if the value passed in is + -- empty we simply set to the scope ID + if new.grant_scope_id = '' or new.grant_scope_id is null then + new.grant_scope_id = new.scope_id; + end if; + -- If the scopes match, it's allowed + if new.grant_scope_id = new.scope_id then + return new; + end if; + -- Fetch the type of scope + select isc.type from iam_scope isc where isc.public_id = new.scope_id into role_scope_type; + -- Always allowed + if role_scope_type = 'global' then + return new; + end if; + -- Never allowed; the case where it's set to the same scope ID as the project + -- itself is covered above + if role_scope_type = 'project' then + raise exception 'invalid to set grant_scope_id to non-same scope_id when role scope type is project'; + end if; + if role_scope_type = 'organization' then + -- Look up the parent scope ID for the scope ID given + select isc.parent_id from iam_scope isc where isc.public_id = new.grant_scope_id into parent_scope_id; + -- Allow iff the grant scope ID's parent matches the role's scope ID; that + -- is, match if the role belongs to a direct child scope of this + -- organization + if parent_scope_id = new.scope_id then + return new; + end if; + raise exception 'grant_scope_id is not a child project of the role scope'; + end if; + raise exception 'unknown scope type'; +end; +$$ language plpgsql; + +create or replace function + disallow_iam_anon_auth_deletion() + returns trigger +as $$ +begin + if old.public_id = 'u_anon' then + raise exception 'deletion of anonymous user not allowed'; + end if; + if old.public_id = 'u_auth' then + raise exception 'deletion of authenticated user not allowed'; + end if; + return old; +end; +$$ language plpgsql; + create trigger ensure_user_scope_id_valid before @@ -716,13 +794,27 @@ before update on iam_user for each row execute procedure iam_immutable_scope_id_func(); +create trigger + iam_user_disallow_anon_auth_deletion +before +delete on iam_user + for each row execute procedure disallow_iam_anon_auth_deletion(); + +-- TODO: Do we want to disallow changing the name or description? +insert into iam_user (public_id, name, description, scope_id) + values ('u_anon', 'anonymous', 'The anonymous user matches any request, whether authenticated or not', 'global'); + +insert into iam_user (public_id, name, description, scope_id) + values ('u_auth', 'authenticated', 'The authenticated user matches any user that has a valid token', 'global'); + create table iam_role ( - public_id wt_public_id not null primary key, + public_id wt_public_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, description text, scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, + grant_scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, unique(name, scope_id), disabled boolean not null default false, -- version allows optimistic locking of the role when modifying the role @@ -736,6 +828,45 @@ create table iam_role ( unique(scope_id, public_id) ); + -- Grants are immutable, which is enforced via the trigger below + create table iam_role_grant ( + create_time wt_timestamp, + role_id wt_public_id -- pk + references iam_role(public_id) + on delete cascade + on update cascade, + canonical_grant text -- pk + check( + length(trim(canonical_grant)) > 0 + ), + raw_grant text not null + check( + length(trim(raw_grant)) > 0 + ), + primary key(role_id, canonical_grant) + ); + +-- iam_immutable_role_grant() ensures that grants assigned to roles are immutable. +create or replace function + iam_immutable_role_grant() + returns trigger +as $$ +begin + raise exception 'role grants are immutable'; +end; +$$ language plpgsql; + +create trigger immutable_role_grant +before +update on iam_role_grant + for each row execute procedure iam_immutable_role_grant(); + +create trigger + default_create_time_column +before +insert on iam_role_grant + for each row execute procedure default_create_time(); + create trigger immutable_scope_id before update on iam_role @@ -745,8 +876,8 @@ create trigger update_version_column after update on iam_role for each row execute procedure update_version_column(); - -create trigger + +create trigger update_time_column before update on iam_role for each row execute procedure update_time_column(); @@ -763,6 +894,12 @@ before insert on iam_role for each row execute procedure default_create_time(); +create trigger + ensure_grant_scope_id_valid +before +insert or update on iam_role + for each row execute procedure grant_scope_id_valid(); + create table iam_group ( public_id wt_public_id not null primary key, create_time wt_timestamp, @@ -798,88 +935,86 @@ 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(). +-- iam_user_role contains roles that have been assigned to users. Users can be +-- from any scope. 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_scope_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_scope_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) + role_id wt_public_id + references iam_role(public_id) on delete cascade on update cascade, - foreign key (scope_id, principal_id) - references iam_group(scope_id, public_id) + principal_id wt_user_id + references iam_user(public_id) on delete cascade - on update cascade + on update cascade, + primary key (role_id, principal_id) ); +-- iam_group_role contains roles that have been assigned to groups. +-- Groups can be from any scope. 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, + role_id wt_public_id + references iam_role(public_id) + on delete cascade + on update cascade, + principal_id wt_public_id + references iam_group(public_id) + on delete cascade + on update cascade, + primary key (role_id, principal_id) + ); + +-- get_scoped_principal_id is used by the iam_principle_role view as a convient +-- way to create : to reference principals from +-- other scopes than the role's scope. +create or replace function get_scoped_principal_id(role_scope text, principal_scope text, principal_id text) returns text +as $$ +begin + if role_scope = principal_scope then + return principal_id; + end if; + return principal_scope || ':' || principal_id; +end; +$$ language plpgsql; + -- 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; +select + ur.create_time, + ur.principal_id, + ur.role_id, + u.scope_id as principal_scope_id, + r.scope_id as role_scope_id, + get_scoped_principal_id(r.scope_id, u.scope_id, ur.principal_id) as scoped_principal_id, + 'user' as type +from + iam_user_role ur, + iam_role r, + iam_user u +where + ur.role_id = r.public_id and + u.public_id = ur.principal_id +union +select + gr.create_time, + gr.principal_id, + gr.role_id, + g.scope_id as principal_scope_id, + r.scope_id as role_scope_id, + get_scoped_principal_id(r.scope_id, g.scope_id, gr.principal_id) as scoped_principal_id, + 'group' as type +from + iam_group_role gr, + iam_role r, + iam_group g +where + gr.role_id = r.public_id and + g.public_id = gr.principal_id; -- iam_immutable_role() ensures that roles assigned to principals are immutable. create or replace function @@ -887,18 +1022,10 @@ create or replace function 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 diff --git a/internal/db/migrations/postgres/01_domain_types.down.sql b/internal/db/migrations/postgres/01_domain_types.down.sql index b8fb53ec5..4552416d9 100644 --- a/internal/db/migrations/postgres/01_domain_types.down.sql +++ b/internal/db/migrations/postgres/01_domain_types.down.sql @@ -2,7 +2,9 @@ begin; drop domain wt_timestamp; drop domain wt_public_id; +drop domain wt_private_id; drop domain wt_scope_id; +drop domain wt_user_id; drop domain wt_version; drop function default_create_time; diff --git a/internal/db/migrations/postgres/01_domain_types.up.sql b/internal/db/migrations/postgres/01_domain_types.up.sql index d6e2dea18..d85c77979 100644 --- a/internal/db/migrations/postgres/01_domain_types.up.sql +++ b/internal/db/migrations/postgres/01_domain_types.up.sql @@ -7,6 +7,13 @@ check( comment on domain wt_public_id is 'Random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; +create domain wt_private_id as text +check( + length(trim(value)) > 10 +); +comment on domain wt_private_id is +'Random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; + create domain wt_scope_id as text check( length(trim(value)) > 10 or value = 'global' @@ -14,6 +21,13 @@ check( comment on domain wt_scope_id is '"global" or random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; +create domain wt_user_id as text +check( + length(trim(value)) > 10 or value = 'u_anon' or value = 'u_auth' +); +comment on domain wt_scope_id is +'"u_anon", "u_auth", or random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; + create domain wt_timestamp as timestamp with time zone default current_timestamp; diff --git a/internal/db/migrations/postgres/06_iam.down.sql b/internal/db/migrations/postgres/06_iam.down.sql index 2994f7244..e4c615ade 100644 --- a/internal/db/migrations/postgres/06_iam.down.sql +++ b/internal/db/migrations/postgres/06_iam.down.sql @@ -11,13 +11,18 @@ drop table iam_role cascade; drop view iam_principal_role cascade; drop table iam_group_role cascade; drop table iam_user_role cascade; +drop table iam_role_grant 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 grant_scope_id_valid cascade; drop function immutable_scope_id_func cascade; +drop function disallow_global_scope_deletion cascade; +drop function user_scope_id_valid cascade; +drop function iam_immutable_role_grant cascade; +drop function disallow_iam_anon_auth_deletion cascade; COMMIT; diff --git a/internal/db/migrations/postgres/06_iam.up.sql b/internal/db/migrations/postgres/06_iam.up.sql index 757f062c6..956a4fffc 100644 --- a/internal/db/migrations/postgres/06_iam.up.sql +++ b/internal/db/migrations/postgres/06_iam.up.sql @@ -212,7 +212,7 @@ insert into iam_scope (public_id, name, type, description) create table iam_user ( - public_id wt_public_id not null primary key, + public_id wt_user_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, @@ -240,6 +240,63 @@ begin end; $$ language plpgsql; +create or replace function + grant_scope_id_valid() + returns trigger +as $$ +declare parent_scope_id text; +declare role_scope_type text; +begin + -- There is a not-null constraint so ensure that if the value passed in is + -- empty we simply set to the scope ID + if new.grant_scope_id = '' or new.grant_scope_id is null then + new.grant_scope_id = new.scope_id; + end if; + -- If the scopes match, it's allowed + if new.grant_scope_id = new.scope_id then + return new; + end if; + -- Fetch the type of scope + select isc.type from iam_scope isc where isc.public_id = new.scope_id into role_scope_type; + -- Always allowed + if role_scope_type = 'global' then + return new; + end if; + -- Never allowed; the case where it's set to the same scope ID as the project + -- itself is covered above + if role_scope_type = 'project' then + raise exception 'invalid to set grant_scope_id to non-same scope_id when role scope type is project'; + end if; + if role_scope_type = 'organization' then + -- Look up the parent scope ID for the scope ID given + select isc.parent_id from iam_scope isc where isc.public_id = new.grant_scope_id into parent_scope_id; + -- Allow iff the grant scope ID's parent matches the role's scope ID; that + -- is, match if the role belongs to a direct child scope of this + -- organization + if parent_scope_id = new.scope_id then + return new; + end if; + raise exception 'grant_scope_id is not a child project of the role scope'; + end if; + raise exception 'unknown scope type'; +end; +$$ language plpgsql; + +create or replace function + disallow_iam_anon_auth_deletion() + returns trigger +as $$ +begin + if old.public_id = 'u_anon' then + raise exception 'deletion of anonymous user not allowed'; + end if; + if old.public_id = 'u_auth' then + raise exception 'deletion of authenticated user not allowed'; + end if; + return old; +end; +$$ language plpgsql; + create trigger ensure_user_scope_id_valid before @@ -268,13 +325,27 @@ before update on iam_user for each row execute procedure iam_immutable_scope_id_func(); +create trigger + iam_user_disallow_anon_auth_deletion +before +delete on iam_user + for each row execute procedure disallow_iam_anon_auth_deletion(); + +-- TODO: Do we want to disallow changing the name or description? +insert into iam_user (public_id, name, description, scope_id) + values ('u_anon', 'anonymous', 'The anonymous user matches any request, whether authenticated or not', 'global'); + +insert into iam_user (public_id, name, description, scope_id) + values ('u_auth', 'authenticated', 'The authenticated user matches any user that has a valid token', 'global'); + create table iam_role ( - public_id wt_public_id not null primary key, + public_id wt_public_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, description text, scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, + grant_scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, unique(name, scope_id), disabled boolean not null default false, -- version allows optimistic locking of the role when modifying the role @@ -288,6 +359,45 @@ create table iam_role ( unique(scope_id, public_id) ); + -- Grants are immutable, which is enforced via the trigger below + create table iam_role_grant ( + create_time wt_timestamp, + role_id wt_public_id -- pk + references iam_role(public_id) + on delete cascade + on update cascade, + canonical_grant text -- pk + check( + length(trim(canonical_grant)) > 0 + ), + raw_grant text not null + check( + length(trim(raw_grant)) > 0 + ), + primary key(role_id, canonical_grant) + ); + +-- iam_immutable_role_grant() ensures that grants assigned to roles are immutable. +create or replace function + iam_immutable_role_grant() + returns trigger +as $$ +begin + raise exception 'role grants are immutable'; +end; +$$ language plpgsql; + +create trigger immutable_role_grant +before +update on iam_role_grant + for each row execute procedure iam_immutable_role_grant(); + +create trigger + default_create_time_column +before +insert on iam_role_grant + for each row execute procedure default_create_time(); + create trigger immutable_scope_id before update on iam_role @@ -297,8 +407,8 @@ create trigger update_version_column after update on iam_role for each row execute procedure update_version_column(); - -create trigger + +create trigger update_time_column before update on iam_role for each row execute procedure update_time_column(); @@ -315,6 +425,12 @@ before insert on iam_role for each row execute procedure default_create_time(); +create trigger + ensure_grant_scope_id_valid +before +insert or update on iam_role + for each row execute procedure grant_scope_id_valid(); + create table iam_group ( public_id wt_public_id not null primary key, create_time wt_timestamp, @@ -350,88 +466,86 @@ 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(). +-- iam_user_role contains roles that have been assigned to users. Users can be +-- from any scope. 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_scope_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_scope_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) + role_id wt_public_id + references iam_role(public_id) on delete cascade on update cascade, - foreign key (scope_id, principal_id) - references iam_group(scope_id, public_id) + principal_id wt_user_id + references iam_user(public_id) on delete cascade - on update cascade + on update cascade, + primary key (role_id, principal_id) ); +-- iam_group_role contains roles that have been assigned to groups. +-- Groups can be from any scope. 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, + role_id wt_public_id + references iam_role(public_id) + on delete cascade + on update cascade, + principal_id wt_public_id + references iam_group(public_id) + on delete cascade + on update cascade, + primary key (role_id, principal_id) + ); + +-- get_scoped_principal_id is used by the iam_principle_role view as a convient +-- way to create : to reference principals from +-- other scopes than the role's scope. +create or replace function get_scoped_principal_id(role_scope text, principal_scope text, principal_id text) returns text +as $$ +begin + if role_scope = principal_scope then + return principal_id; + end if; + return principal_scope || ':' || principal_id; +end; +$$ language plpgsql; + -- 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; +select + ur.create_time, + ur.principal_id, + ur.role_id, + u.scope_id as principal_scope_id, + r.scope_id as role_scope_id, + get_scoped_principal_id(r.scope_id, u.scope_id, ur.principal_id) as scoped_principal_id, + 'user' as type +from + iam_user_role ur, + iam_role r, + iam_user u +where + ur.role_id = r.public_id and + u.public_id = ur.principal_id +union +select + gr.create_time, + gr.principal_id, + gr.role_id, + g.scope_id as principal_scope_id, + r.scope_id as role_scope_id, + get_scoped_principal_id(r.scope_id, g.scope_id, gr.principal_id) as scoped_principal_id, + 'group' as type +from + iam_group_role gr, + iam_role r, + iam_group g +where + gr.role_id = r.public_id and + g.public_id = gr.principal_id; -- iam_immutable_role() ensures that roles assigned to principals are immutable. create or replace function @@ -439,18 +553,10 @@ create or replace function 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 diff --git a/internal/db/read_writer.go b/internal/db/read_writer.go index 1f953461b..b0ffc6fdc 100644 --- a/internal/db/read_writer.go +++ b/internal/db/read_writer.go @@ -220,10 +220,10 @@ func (rw *Db) lookupAfterWrite(ctx context.Context, i interface{}, opt ...Option // cannot be used together. WithLookup with to force a lookup after create. func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { if rw.underlying == nil { - return fmt.Errorf("create: missing underlying db %w", ErrNilParameter) + return fmt.Errorf("create: missing underlying db: %w", ErrNilParameter) } if isNil(i) { - return fmt.Errorf("create: interface is missing %w", ErrNilParameter) + return fmt.Errorf("create: interface is missing: %w", ErrNilParameter) } opts := GetOpts(opt...) withOplog := opts.withOplog @@ -234,7 +234,7 @@ func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { // let's validate oplog options before we start writing to the database _, err := validateOplogArgs(i, opts) if err != nil { - return fmt.Errorf("create: oplog validation failed %w", err) + return fmt.Errorf("create: oplog validation failed: %w", err) } } // these fields should be nil, since they are not writeable and we want the @@ -244,7 +244,7 @@ func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { 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) + return fmt.Errorf("create: vet for write failed: %w", err) } } } @@ -257,7 +257,7 @@ func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { } } if err := rw.underlying.Create(i).Error; err != nil { - return fmt.Errorf("create: failed %w", err) + return fmt.Errorf("create: failed: %w", err) } if withOplog { if err := rw.addOplog(ctx, CreateOp, opts, ticket, i); err != nil { @@ -267,12 +267,12 @@ func (rw *Db) Create(ctx context.Context, i interface{}, opt ...Option) error { if opts.newOplogMsg != nil { msg, err := rw.newOplogMessage(ctx, CreateOp, i) if err != nil { - return fmt.Errorf("create: returning oplog failed %w", err) + return fmt.Errorf("create: returning oplog failed: %w", err) } *opts.newOplogMsg = *msg } if err := rw.lookupAfterWrite(ctx, i, opt...); err != nil { - return fmt.Errorf("create: lookup error %w", err) + return fmt.Errorf("create: lookup error: %w", err) } return nil } @@ -406,13 +406,13 @@ func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string // let's validate oplog options before we start writing to the database _, err := validateOplogArgs(i, opts) if err != nil { - return NoRowsAffected, fmt.Errorf("update: oplog validation failed %w", err) + return NoRowsAffected, fmt.Errorf("update: oplog validation 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) + return NoRowsAffected, fmt.Errorf("update: vet for write failed: %w", err) } } } @@ -436,9 +436,9 @@ func (rw *Db) Update(ctx context.Context, i interface{}, fieldMaskPaths []string } if underlying.Error != nil { if err == gorm.ErrRecordNotFound { - return NoRowsAffected, fmt.Errorf("update: failed %w", ErrRecordNotFound) + return NoRowsAffected, fmt.Errorf("update: failed: %w", ErrRecordNotFound) } - return NoRowsAffected, fmt.Errorf("update: failed %w", underlying.Error) + return NoRowsAffected, fmt.Errorf("update: failed: %w", underlying.Error) } rowsUpdated := int(underlying.RowsAffected) if rowsUpdated > 0 && (withOplog || opts.newOplogMsg != nil) { diff --git a/internal/db/read_writer_test.go b/internal/db/read_writer_test.go index 1e3b64bcf..6c508747d 100644 --- a/internal/db/read_writer_test.go +++ b/internal/db/read_writer_test.go @@ -442,7 +442,7 @@ func TestDb_Update(t *testing.T) { ) require.Error(err) assert.Equal(0, rowsUpdated) - assert.Equal("update: oplog validation failed error no wrapper WithOplog: nil parameter", err.Error()) + assert.Equal("update: oplog validation failed: error no wrapper WithOplog: nil parameter", err.Error()) }) t.Run("no-metadata-WithOplog", func(t *testing.T) { assert, require := assert.New(t), require.New(t) @@ -459,7 +459,7 @@ func TestDb_Update(t *testing.T) { ) require.Error(err) assert.Equal(0, rowsUpdated) - assert.Equal("update: oplog validation failed error no metadata for WithOplog: invalid parameter", err.Error()) + assert.Equal("update: oplog validation failed: error no metadata for WithOplog: invalid parameter", err.Error()) }) } @@ -640,7 +640,7 @@ func TestDb_Create(t *testing.T) { ), ) require.Error(err) - assert.Equal("create: oplog validation failed error no wrapper WithOplog: nil parameter", err.Error()) + assert.Equal("create: oplog validation failed: error no wrapper WithOplog: nil parameter", err.Error()) }) t.Run("no-metadata-WithOplog", func(t *testing.T) { assert, require := assert.New(t), require.New(t) @@ -659,7 +659,7 @@ func TestDb_Create(t *testing.T) { ), ) require.Error(err) - assert.Equal("create: oplog validation failed error no metadata for WithOplog: invalid parameter", err.Error()) + assert.Equal("create: oplog validation failed: error no metadata for WithOplog: invalid parameter", err.Error()) }) t.Run("nil-tx", func(t *testing.T) { assert, require := assert.New(t), require.New(t) @@ -671,7 +671,7 @@ func TestDb_Create(t *testing.T) { user.Name = "foo-" + id err = w.Create(context.Background(), user) require.Error(err) - assert.Equal("create: missing underlying db nil parameter", err.Error()) + assert.Equal("create: missing underlying db: nil parameter", err.Error()) }) } diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index 0dc94b715..87e22abf2 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -3120,6 +3120,10 @@ "format": "boolean", "title": "Whether the resource is disabled" }, + "grant_scope_id": { + "type": "string", + "description": "The scope the grants will apply to. If the role is at the global scope,\nthis can be an organization or project. If the role is at an organization\nscope, this can be a project within the organization. It is invalid for\nthis to be anything other than the role's scope when the role's scope is\na project." + }, "version": { "type": "integer", "format": "int64", diff --git a/internal/gen/controller/api/resources/roles/role.pb.go b/internal/gen/controller/api/resources/roles/role.pb.go index 53d834fa1..697e81b35 100644 --- a/internal/gen/controller/api/resources/roles/role.pb.go +++ b/internal/gen/controller/api/resources/roles/role.pb.go @@ -48,18 +48,24 @@ type Role struct { UpdatedTime *timestamp.Timestamp `protobuf:"bytes,5,opt,name=updated_time,proto3" json:"updated_time,omitempty"` // Whether the resource is disabled Disabled *wrappers.BoolValue `protobuf:"bytes,6,opt,name=disabled,proto3" json:"disabled,omitempty"` + // The scope the grants will apply to. If the role is at the global scope, + // this can be an organization or project. If the role is at an organization + // scope, this can be a project within the organization. It is invalid for + // this to be anything other than the role's scope when the role's scope is + // a project. + GrantScopeId *wrappers.StringValue `protobuf:"bytes,7,opt,name=grant_scope_id,proto3" json:"grant_scope_id,omitempty"` // The version can be used in subsiquent write requests to ensure this resource // has not changed and to fail the write if it has. // Output only. - Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty"` + Version uint32 `protobuf:"varint,8,opt,name=version,proto3" json:"version,omitempty"` // The principals that are assigned this role. // Output only. - UserIds []string `protobuf:"bytes,8,rep,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"` + UserIds []string `protobuf:"bytes,9,rep,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"` // Output only. - GroupIds []string `protobuf:"bytes,9,rep,name=group_ids,json=groupIds,proto3" json:"group_ids,omitempty"` + GroupIds []string `protobuf:"bytes,10,rep,name=group_ids,json=groupIds,proto3" json:"group_ids,omitempty"` // The grants that this role provides for it's principals. // Output only. - Grants []string `protobuf:"bytes,10,rep,name=grants,proto3" json:"grants,omitempty"` + Grants []string `protobuf:"bytes,11,rep,name=grants,proto3" json:"grants,omitempty"` } func (x *Role) Reset() { @@ -136,6 +142,13 @@ func (x *Role) GetDisabled() *wrappers.BoolValue { return nil } +func (x *Role) GetGrantScopeId() *wrappers.StringValue { + if x != nil { + return x.GrantScopeId + } + return nil +} + func (x *Role) GetVersion() uint32 { if x != nil { return x.Version @@ -176,7 +189,7 @@ var file_controller_api_resources_roles_v1_role_proto_rawDesc = []byte{ 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x22, 0xaa, 0x03, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x74, 0x6f, 0x22, 0xf0, 0x03, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x30, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, @@ -196,19 +209,24 @@ var file_controller_api_resources_roles_v1_role_proto_rawDesc = []byte{ 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x64, 0x69, 0x73, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x19, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, - 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x42, - 0x53, 0x5a, 0x51, 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, 0x67, 0x65, 0x6e, 0x2f, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x3b, 0x72, - 0x6f, 0x6c, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x44, 0x0a, 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x67, 0x72, 0x61, + 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, + 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, + 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0a, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x16, 0x0a, + 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x67, + 0x72, 0x61, 0x6e, 0x74, 0x73, 0x42, 0x53, 0x5a, 0x51, 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, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x72, + 0x6f, 0x6c, 0x65, 0x73, 0x3b, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -236,11 +254,12 @@ var file_controller_api_resources_roles_v1_role_proto_depIdxs = []int32{ 2, // 2: controller.api.resources.roles.v1.Role.created_time:type_name -> google.protobuf.Timestamp 2, // 3: controller.api.resources.roles.v1.Role.updated_time:type_name -> google.protobuf.Timestamp 3, // 4: controller.api.resources.roles.v1.Role.disabled:type_name -> google.protobuf.BoolValue - 5, // [5:5] is the sub-list for method output_type - 5, // [5:5] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 1, // 5: controller.api.resources.roles.v1.Role.grant_scope_id:type_name -> google.protobuf.StringValue + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_controller_api_resources_roles_v1_role_proto_init() } diff --git a/internal/iam/docs.go b/internal/iam/docs.go new file mode 100644 index 000000000..55322cbf7 --- /dev/null +++ b/internal/iam/docs.go @@ -0,0 +1,3 @@ +// iam package is for identity access management of watchtower. It includes typical +// iam resources like Organizations, Projects, Users, Groups, etc. +package iam diff --git a/internal/iam/group_test.go b/internal/iam/group_test.go index 1e538f97b..a9caaf3ad 100644 --- a/internal/iam/group_test.go +++ b/internal/iam/group_test.go @@ -166,7 +166,7 @@ func Test_GroupCreate(t *testing.T) { }(), }, wantErr: true, - wantErrMsg: "create: vet for write failed scope is not found", + wantErrMsg: "create: vet for write failed: scope is not found", }, } @@ -240,7 +240,7 @@ func Test_GroupUpdate(t *testing.T) { ScopeId: proj.PublicId, }, wantErr: true, - wantErrMsg: "update: vet for write failed not allowed to change a resource's scope", + wantErrMsg: "update: vet for write failed: not allowed to change a resource's scope", }, { name: "proj-scope-id-not-in-mask", @@ -271,7 +271,7 @@ func Test_GroupUpdate(t *testing.T) { }, wantErr: true, wantDup: true, - wantErrMsg: `update: failed pq: duplicate key value violates unique constraint "iam_group_name_scope_id_key"`, + wantErrMsg: `update: failed: pq: duplicate key value violates unique constraint "iam_group_name_scope_id_key"`, }, { name: "set description null", diff --git a/internal/iam/ids.go b/internal/iam/ids.go index edcf5970c..e37db7aa7 100644 --- a/internal/iam/ids.go +++ b/internal/iam/ids.go @@ -8,9 +8,10 @@ import ( ) const ( - UserPrefix = "u" - GroupPrefix = "g" - RolePrefix = "r" + UserPrefix = "u" + GroupPrefix = "g" + RolePrefix = "r" + RoleGrantPrefix = "rg" ) func newRoleId() (string, error) { diff --git a/internal/iam/options.go b/internal/iam/options.go index 9d6de2998..94f81076b 100644 --- a/internal/iam/options.go +++ b/internal/iam/options.go @@ -18,25 +18,34 @@ type options struct { withName string withScope *Scope withDescription string - withParentId *string + withGroupGrants bool withLimit int withAutoVivify bool + withGrantScopeId string withSkipVetForWrite bool } func getDefaultOptions() options { return options{ withPublicId: "", + withName: "", withScope: nil, withDescription: "", - withName: "", - withParentId: nil, + withGroupGrants: false, withLimit: 0, withAutoVivify: false, + withGrantScopeId: "", withSkipVetForWrite: false, } } +// WithGroupGrants provides and option to include group grants +func WithGroupGrants(enable bool) Option { + return func(o *options) { + o.withGroupGrants = enable + } +} + // WithPublicId provides an optional public id func WithPublicId(id string) Option { return func(o *options) { @@ -82,6 +91,14 @@ func WithAutoVivify(enable bool) Option { } } +// WithGrantScopeId provides an option to specify the scope ID for grants in +// roles. +func WithGrantScopeId(id string) Option { + return func(o *options) { + o.withGrantScopeId = id + } +} + // WithSkipVetForWrite provides an option to allow skipping vet checks to allow // testing lower-level SQL triggers and constraints func WithSkipVetForWrite(enable bool) Option { diff --git a/internal/iam/options_test.go b/internal/iam/options_test.go index e40c13d99..689bfd94f 100644 --- a/internal/iam/options_test.go +++ b/internal/iam/options_test.go @@ -65,4 +65,11 @@ func Test_GetOpts(t *testing.T) { testOpts.withAutoVivify = true assert.Equal(opts, testOpts) }) + t.Run("WithGrantScopeId", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithGrantScopeId("o_1234")) + testOpts := getDefaultOptions() + testOpts.withGrantScopeId = "o_1234" + assert.Equal(opts, testOpts) + }) } diff --git a/internal/iam/principal_role.go b/internal/iam/principal_role.go index 042b00deb..7a131e19f 100644 --- a/internal/iam/principal_role.go +++ b/internal/iam/principal_role.go @@ -27,25 +27,21 @@ func (r RoleType) String() string { }[r] } -const principalRoleViewDefaultTable = "iam_principal_role" +const ( + principalRoleViewDefaultTable = "iam_principal_role" + userRoleDefaultTable = "iam_user_role" + groupRoleDefaultTable = "iam_group_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 { +// PrincipalRole provides a common way to return roles regardless of their +// underlying type. +type PrincipalRole struct { *store.PrincipalRoleView tableName string `gorm:"-"` } // TableName provides an overridden gorm table name for principal roles. -func (v *principalRoleView) TableName() string { +func (v *PrincipalRole) TableName() string { if v.tableName != "" { return v.tableName } @@ -54,7 +50,7 @@ func (v *principalRoleView) TableName() string { // 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) { +func (v *PrincipalRole) SetTableName(n string) { switch n { case "": v.tableName = principalRoleViewDefaultTable @@ -63,29 +59,20 @@ func (v *principalRoleView) SetTableName(n string) { } } -func (v principalRoleView) Clone() interface{} { - cp := proto.Clone(v.PrincipalRoleView) - return &principalRoleView{ - PrincipalRoleView: cp.(*store.PrincipalRoleView), - } -} - -// UserRole is a role assigned to a user +// UserRole is a user assigned to a role type UserRole struct { *store.UserRole tableName string `gorm:"-"` } -// ensure that UserRole implements the interfaces of: Clonable, AssignedRole and db.VetForWriter +// ensure that UserRole implements the interfaces of: Clonable 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) { +// NewUserRole creates a new user role in memory. No options are supported +// currently. +func NewUserRole(roleId, userId string, opt ...Option) (*UserRole, error) { if roleId == "" { return nil, fmt.Errorf("new user role: missing role id %w", db.ErrInvalidParameter) } @@ -96,16 +83,10 @@ func NewUserRole(scopeId, roleId, userId string, opt ...Option) (PrincipalRole, 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{}, @@ -120,9 +101,7 @@ func (r *UserRole) Clone() interface{} { } } -// 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. +// VetForWrite implements db.VetForWrite() interface for user roles. 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) @@ -139,32 +118,34 @@ func (r *UserRole) TableName() string { if r.tableName != "" { return r.tableName } - return "iam_user_role" + return userRoleDefaultTable } -// SetTableName sets the tablename and satisfies the ReplayableMessage interface +// 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 (r *UserRole) SetTableName(n string) { - if r.tableName != "" { + switch n { + case "": + r.tableName = userRoleDefaultTable + default: r.tableName = n } } -// GroupRole is a role assigned to a group +// GroupRole is a group assigned to a role type GroupRole struct { *store.GroupRole tableName string `gorm:"-"` } -// ensure that GroupRole implements the interfaces of: Clonable, AssignedRole and db.VetForWriter +// ensure that GroupRole implements the interfaces of: Clonable 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) { +// NewGroupRole creates a new group role in memory. No options are supported +// currently. +func NewGroupRole(roleId, groupId string, opt ...Option) (*GroupRole, error) { if roleId == "" { return nil, fmt.Errorf("new group role: missing role id %w", db.ErrInvalidParameter) } @@ -175,16 +156,10 @@ func NewGroupRole(scopeId, roleId, groupId string, opt ...Option) (PrincipalRole 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{}, @@ -199,9 +174,7 @@ func (r *GroupRole) Clone() interface{} { } } -// 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. +// VetForWrite implements db.VetForWrite() interface for group roles. 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) @@ -218,13 +191,16 @@ func (r *GroupRole) TableName() string { if r.tableName != "" { return r.tableName } - return "iam_group_role" + return groupRoleDefaultTable } -// SetTableName sets the tablename and satisfies the ReplayableMessage interface -// for group roles. +// 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 (r *GroupRole) SetTableName(n string) { - if r.tableName != "" { + switch n { + case "": + r.tableName = groupRoleDefaultTable + default: r.tableName = n } } diff --git a/internal/iam/principal_role_test.go b/internal/iam/principal_role_test.go index a4bed7d31..df6dbd769 100644 --- a/internal/iam/principal_role_test.go +++ b/internal/iam/principal_role_test.go @@ -21,54 +21,48 @@ func TestNewUserRole(t *testing.T) { user := TestUser(t, conn, org.PublicId) type args struct { - roleId string - userId string - scopeId string - opt []Option + roleId string + userId string + opt []Option } tests := []struct { name string args args - want PrincipalRole + want *UserRole wantErr bool wantIsErr error }{ { name: "valid-org", args: args{ - roleId: orgRole.PublicId, - userId: user.PublicId, - scopeId: org.PublicId, + roleId: orgRole.PublicId, + userId: user.PublicId, }, - want: func() PrincipalRole { + want: func() *UserRole { 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, + roleId: projRole.PublicId, + userId: user.PublicId, }, - want: func() PrincipalRole { + want: func() *UserRole { 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, + roleId: "", + userId: user.PublicId, }, want: nil, wantErr: true, @@ -77,9 +71,8 @@ func TestNewUserRole(t *testing.T) { { name: "empty-user-id", args: args{ - roleId: orgRole.PublicId, - userId: "", - scopeId: org.PublicId, + roleId: orgRole.PublicId, + userId: "", }, want: nil, wantErr: true, @@ -89,7 +82,7 @@ func TestNewUserRole(t *testing.T) { 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...) + got, err := NewUserRole(tt.args.roleId, tt.args.userId, tt.args.opt...) if tt.wantErr { require.Error(err) assert.True(errors.Is(err, tt.wantIsErr)) @@ -123,9 +116,9 @@ func TestUserRole_Create(t *testing.T) { role: func() *UserRole { role := TestRole(t, conn, org.PublicId) principal := TestUser(t, conn, org.PublicId) - principalRole, err := NewUserRole(org.PublicId, role.PublicId, principal.PublicId) + principalRole, err := NewUserRole(role.PublicId, principal.PublicId) require.NoError(t, err) - return principalRole.(*UserRole) + return principalRole }(), }, wantErr: false, @@ -136,38 +129,36 @@ func TestUserRole_Create(t *testing.T) { role: func() *UserRole { role := TestRole(t, conn, proj.PublicId) principal := TestUser(t, conn, org.PublicId) - principalRole, err := NewUserRole(proj.PublicId, role.PublicId, principal.PublicId) + principalRole, err := NewUserRole(role.PublicId, principal.PublicId) require.NoError(t, err) - return principalRole.(*UserRole) + return principalRole }(), }, wantErr: false, }, { - name: "invalid-org", + name: "cross-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) + principalRole, err := NewUserRole(role.PublicId, principal.PublicId) require.NoError(t, err) - return principalRole.(*UserRole) + return principalRole }(), }, - wantErr: true, }, { - name: "invalid-proj", + name: "cross-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) + principalRole, err := NewUserRole(role.PublicId, principal.PublicId) require.NoError(t, err) - return principalRole.(*UserRole) + return principalRole }(), }, - wantErr: true, }, { name: "bad-role-id", @@ -175,13 +166,13 @@ func TestUserRole_Create(t *testing.T) { role: func() *UserRole { id := testId(t) principal := TestUser(t, conn, org.PublicId) - principalRole, err := NewUserRole(org.PublicId, id, principal.PublicId) + principalRole, err := NewUserRole(id, principal.PublicId) require.NoError(t, err) - return principalRole.(*UserRole) + return principalRole }(), }, wantErr: true, - wantErrMsg: "create: failed pq: user and role do not belong to the same organization", + wantErrMsg: "create: failed: pq: insert or update on table \"iam_user_role\" violates foreign key constraint \"iam_user_role_role_id_fkey\"", }, { name: "bad-user-id", @@ -189,13 +180,13 @@ func TestUserRole_Create(t *testing.T) { role: func() *UserRole { id := testId(t) role := TestRole(t, conn, proj.PublicId) - principalRole, err := NewUserRole(proj.PublicId, role.PublicId, id) + principalRole, err := NewUserRole(role.PublicId, id) require.NoError(t, err) - return principalRole.(*UserRole) + return principalRole }(), }, wantErr: true, - wantErrMsg: "create: failed pq: user and role do not belong to the same organization", + wantErrMsg: "create: failed: pq: insert or update on table \"iam_user_role\" violates foreign key constraint \"iam_user_role_principal_id_fkey\"", }, { name: "missing-role-id", @@ -206,13 +197,12 @@ func TestUserRole_Create(t *testing.T) { 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", + wantErrMsg: "create: vet for write failed: new user role: missing role id invalid parameter", wantIsErr: db.ErrInvalidParameter, }, { @@ -224,13 +214,12 @@ func TestUserRole_Create(t *testing.T) { 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", + wantErrMsg: "create: vet for write failed: new user role: missing user id invalid parameter", wantIsErr: db.ErrInvalidParameter, }, { @@ -239,14 +228,14 @@ func TestUserRole_Create(t *testing.T) { role: func() *UserRole { role := TestRole(t, conn, org.PublicId) principal := TestUser(t, conn, org.PublicId) - principalRole, err := NewUserRole(org.PublicId, role.PublicId, principal.PublicId) + principalRole, err := NewUserRole(role.PublicId, principal.PublicId) require.NoError(t, err) - return principalRole.(*UserRole) + return principalRole }(), }, wantDup: true, wantErr: true, - wantErrMsg: `create: failed pq: duplicate key value violates unique constraint "iam_user_role_pkey"`, + wantErrMsg: `create: failed: pq: duplicate key value violates unique constraint "iam_user_role_pkey"`, }, } @@ -290,7 +279,7 @@ func TestUserRole_Update(t *testing.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) + userRole := TestUserRole(t, conn, r.PublicId, u.PublicId) updateRole := userRole.Clone().(*UserRole) updateRole.PrincipalId = u2.PublicId updatedRows, err := rw.Update(context.Background(), updateRole, []string{"PrincipalId"}, nil) @@ -317,7 +306,7 @@ func TestUserRole_Delete(t *testing.T) { }{ { name: "valid", - role: TestUserRole(t, conn, org.PublicId, r.PublicId, u.PublicId), + role: TestUserRole(t, conn, r.PublicId, u.PublicId), wantErr: false, wantRowsDeleted: 1, }, @@ -361,7 +350,7 @@ func TestUserRole_Clone(t *testing.T) { 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) + userRole := TestUserRole(t, conn, role.PublicId, user.PublicId) cp := userRole.Clone() assert.True(proto.Equal(cp.(*UserRole).UserRole, userRole.UserRole)) }) @@ -369,21 +358,13 @@ func TestUserRole_Clone(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) + userRole := TestUserRole(t, conn, role.PublicId, user.PublicId) + userRole2 := TestUserRole(t, conn, 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") @@ -395,13 +376,12 @@ func TestNewGroupRole(t *testing.T) { type args struct { roleId string groupId string - scopeId string opt []Option } tests := []struct { name string args args - want PrincipalRole + want *GroupRole wantErr bool wantIsErr error }{ @@ -410,13 +390,11 @@ func TestNewGroupRole(t *testing.T) { args: args{ roleId: orgRole.PublicId, groupId: group.PublicId, - scopeId: org.PublicId, }, - want: func() PrincipalRole { + want: func() *GroupRole { r := allocGroupRole() r.RoleId = orgRole.PublicId r.PrincipalId = group.PublicId - r.ScopeId = org.PublicId return &r }(), }, @@ -425,13 +403,11 @@ func TestNewGroupRole(t *testing.T) { args: args{ roleId: projRole.PublicId, groupId: group.PublicId, - scopeId: proj.PublicId, }, - want: func() PrincipalRole { + want: func() *GroupRole { r := allocGroupRole() r.RoleId = projRole.PublicId r.PrincipalId = group.PublicId - r.ScopeId = proj.PublicId return &r }(), }, @@ -440,7 +416,6 @@ func TestNewGroupRole(t *testing.T) { args: args{ roleId: "", groupId: group.PublicId, - scopeId: org.PublicId, }, want: nil, wantErr: true, @@ -451,7 +426,6 @@ func TestNewGroupRole(t *testing.T) { args: args{ roleId: orgRole.PublicId, groupId: "", - scopeId: org.PublicId, }, want: nil, wantErr: true, @@ -461,7 +435,7 @@ func TestNewGroupRole(t *testing.T) { 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...) + got, err := NewGroupRole(tt.args.roleId, tt.args.groupId, tt.args.opt...) if tt.wantErr { require.Error(err) assert.True(errors.Is(err, tt.wantIsErr)) @@ -477,6 +451,7 @@ func TestGroupRole_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 *GroupRole } @@ -494,9 +469,9 @@ func TestGroupRole_Create(t *testing.T) { role: func() *GroupRole { role := TestRole(t, conn, org.PublicId) principal := TestGroup(t, conn, org.PublicId) - principalRole, err := NewGroupRole(org.PublicId, role.PublicId, principal.PublicId) + principalRole, err := NewGroupRole(role.PublicId, principal.PublicId) require.NoError(t, err) - return principalRole.(*GroupRole) + return principalRole }(), }, wantErr: false, @@ -507,26 +482,50 @@ func TestGroupRole_Create(t *testing.T) { role: func() *GroupRole { role := TestRole(t, conn, proj.PublicId) principal := TestGroup(t, conn, proj.PublicId) - principalRole, err := NewGroupRole(proj.PublicId, role.PublicId, principal.PublicId) + principalRole, err := NewGroupRole(role.PublicId, principal.PublicId) require.NoError(t, err) - return principalRole.(*GroupRole) + return principalRole }(), }, wantErr: false, }, + { + name: "cross-org", + args: args{ + role: func() *GroupRole { + role := TestRole(t, conn, org2.PublicId) + principal := TestGroup(t, conn, org.PublicId) + principalRole, err := NewGroupRole(role.PublicId, principal.PublicId) + require.NoError(t, err) + return principalRole + }(), + }, + }, + { + name: "cross-proj", + args: args{ + role: func() *GroupRole { + role := TestRole(t, conn, proj2.PublicId) + principal := TestGroup(t, conn, org.PublicId) + principalRole, err := NewGroupRole(role.PublicId, principal.PublicId) + require.NoError(t, err) + return principalRole + }(), + }, + }, { 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) + principalRole, err := NewGroupRole(id, principal.PublicId) require.NoError(t, err) - return principalRole.(*GroupRole) + return principalRole }(), }, 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"`, + wantErrMsg: "create: failed: pq: insert or update on table \"iam_group_role\" violates foreign key constraint \"iam_group_role_role_id_fkey\"", }, { name: "bad-user-id", @@ -534,13 +533,13 @@ func TestGroupRole_Create(t *testing.T) { role: func() *GroupRole { id := testId(t) role := TestRole(t, conn, proj.PublicId) - principalRole, err := NewGroupRole(org.PublicId, role.PublicId, id) + principalRole, err := NewGroupRole(role.PublicId, id) require.NoError(t, err) - return principalRole.(*GroupRole) + return principalRole }(), }, 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"`, + wantErrMsg: "create: failed: pq: insert or update on table \"iam_group_role\" violates foreign key constraint \"iam_group_role_principal_id_fkey\"", }, { name: "missing-role-id", @@ -551,13 +550,12 @@ func TestGroupRole_Create(t *testing.T) { 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", + wantErrMsg: "create: vet for write failed: new group role: missing role id invalid parameter", wantIsErr: db.ErrInvalidParameter, }, { @@ -569,13 +567,12 @@ func TestGroupRole_Create(t *testing.T) { 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", + wantErrMsg: "create: vet for write failed: new group role: missing user id invalid parameter", wantIsErr: db.ErrInvalidParameter, }, { @@ -584,14 +581,14 @@ func TestGroupRole_Create(t *testing.T) { role: func() *GroupRole { role := TestRole(t, conn, org.PublicId) principal := TestGroup(t, conn, org.PublicId) - principalRole, err := NewGroupRole(org.PublicId, role.PublicId, principal.PublicId) + principalRole, err := NewGroupRole(role.PublicId, principal.PublicId) require.NoError(t, err) - return principalRole.(*GroupRole) + return principalRole }(), }, wantDup: true, wantErr: true, - wantErrMsg: `create: failed pq: duplicate key value violates unique constraint`, + wantErrMsg: `create: failed: pq: duplicate key value violates unique constraint`, }, { name: "dup-at-proj", @@ -599,14 +596,14 @@ func TestGroupRole_Create(t *testing.T) { role: func() *GroupRole { role := TestRole(t, conn, proj.PublicId) principal := TestGroup(t, conn, proj.PublicId) - principalRole, err := NewGroupRole(proj.PublicId, role.PublicId, principal.PublicId) + principalRole, err := NewGroupRole(role.PublicId, principal.PublicId) require.NoError(t, err) - return principalRole.(*GroupRole) + return principalRole }(), }, wantDup: true, wantErr: true, - wantErrMsg: `create: failed pq: duplicate key value violates unique constraint`, + wantErrMsg: `create: failed: pq: duplicate key value violates unique constraint`, }, } @@ -650,8 +647,8 @@ func TestGroupRole_Update(t *testing.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) + groupRole := TestGroupRole(t, conn, r.PublicId, g.PublicId) + updateRole := groupRole.Clone().(*GroupRole) updateRole.PrincipalId = g2.PublicId updatedRows, err := rw.Update(context.Background(), updateRole, []string{"PrincipalId"}, nil) require.Error(err) @@ -677,13 +674,18 @@ func TestGroupRole_Delete(t *testing.T) { }{ { name: "valid", - role: TestGroupRole(t, conn, org.PublicId, r.PublicId, g.PublicId), + role: TestGroupRole(t, conn, 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 }(), + name: "bad-id", + role: func() *GroupRole { + r := allocGroupRole() + r.PrincipalId = id + r.RoleId = id + return &r + }(), wantErr: false, wantRowsDeleted: 0, }, @@ -721,7 +723,7 @@ func TestGroupRole_Clone(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) + grpRole := TestGroupRole(t, conn, role.PublicId, grp.PublicId) cp := grpRole.Clone() assert.True(proto.Equal(cp.(*GroupRole).GroupRole, grpRole.GroupRole)) }) @@ -731,17 +733,9 @@ func TestGroupRole_Clone(t *testing.T) { 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) + grpRole := TestGroupRole(t, conn, role.PublicId, grp.PublicId) + grpRole2 := TestGroupRole(t, conn, 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_group_test.go b/internal/iam/repository_group_test.go index 94b3f2fb1..10e76cd6f 100644 --- a/internal/iam/repository_group_test.go +++ b/internal/iam/repository_group_test.go @@ -389,7 +389,7 @@ func TestRepository_UpdateGroup(t *testing.T) { }, newScopeId: org.PublicId, wantErr: true, - wantErrMsg: `update: failed pq: scope_id cannot be set`, + wantErrMsg: `update: failed: pq: scope_id cannot be set`, directUpdate: true, }, } diff --git a/internal/iam/repository_principal_role.go b/internal/iam/repository_principal_role.go index a0b806ca4..748b66482 100644 --- a/internal/iam/repository_principal_role.go +++ b/internal/iam/repository_principal_role.go @@ -10,17 +10,9 @@ import ( // 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. +// roleVersion or an error will be returned. The list of current PrincipalRoles +// after the adds will be returned on success. func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleVersion uint32, 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) } @@ -30,14 +22,14 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV role := allocRole() role.PublicId = roleId - scope, err := role.GetScope(ctx, r.reader) + roleScope, 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) + usrRole, err := NewUserRole(roleId, id) if err != nil { return nil, fmt.Errorf("add principal roles: unable to create in memory user role: %w", err) } @@ -45,13 +37,13 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV } newGrpRoles := make([]interface{}, 0, len(groupIds)) for _, id := range groupIds { - grpRole, err := NewGroupRole(scope.PublicId, roleId, id) + grpRole, err := NewGroupRole(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) } - + var currentPrincipals []PrincipalRole _, err = r.writer.DoTx( ctx, db.StdRetryCnt, @@ -90,27 +82,32 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV } metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, - "scope-id": []string{scope.PublicId}, - "scope-type": []string{scope.Type}, + "scope-id": []string{roleScope.PublicId}, + "scope-type": []string{roleScope.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) } + // we need a new repo, that's using the same reader/writer as this TxHandler + txRepo := &Repository{ + reader: reader, + writer: w, + wrapper: r.wrapper, + // intentionally not setting the defaultLimit, so we'll get all + // the principal roles without a limit + } + currentPrincipals, err = txRepo.ListPrincipalRoles(ctx, roleId) + if err != nil { + return fmt.Errorf("add principal roles: unable to retrieve current principal roles after adds: %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 + return currentPrincipals, nil } // SetPrincipalRoles will set the role's principals. Set add and/or delete @@ -118,13 +115,6 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV // requested. If both userIds and groupIds are empty, the principal roles will // be cleared. func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleVersion uint32, 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) } @@ -143,23 +133,8 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV } // 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 + if len(toSet.unchangedPrincipalRoles) > 0 { + return toSet.unchangedPrincipalRoles, db.NoRowsAffected, nil } var currentPrincipals []PrincipalRole @@ -169,7 +144,6 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV 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 @@ -246,8 +220,15 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV 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) + // we need a new repo, that's using the same reader/writer as this TxHandler + txRepo := &Repository{ + reader: reader, + writer: w, + wrapper: r.wrapper, + // intentionally not setting the defaultLimit, so we'll get all + // the principal roles without a limit + } + currentPrincipals, err = txRepo.ListPrincipalRoles(ctx, roleId) if err != nil { return fmt.Errorf("set principal roles: unable to retrieve current principal roles after sets: %w", err) } @@ -277,7 +258,7 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro } deleteUserRoles := make([]interface{}, 0, len(userIds)) for _, id := range userIds { - usrRole, err := NewUserRole(scope.PublicId, roleId, id) + usrRole, err := NewUserRole(roleId, id) if err != nil { return db.NoRowsAffected, fmt.Errorf("delete principal roles: unable to create in memory user role: %w", err) } @@ -285,7 +266,7 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro } deleteGrpRoles := make([]interface{}, 0, len(groupIds)) for _, id := range groupIds { - grpRole, err := NewGroupRole(scope.PublicId, roleId, id) + grpRole, err := NewGroupRole(roleId, id) if err != nil { return db.NoRowsAffected, fmt.Errorf("delete principal roles: unable to create in memory group role: %w", err) } @@ -362,14 +343,12 @@ func (r *Repository) ListPrincipalRoles(ctx context.Context, roleId string, opt if roleId == "" { return nil, fmt.Errorf("lookup principal roles: missing role id: %w", db.ErrInvalidParameter) } - var roles []principalRoleView + var roles []PrincipalRole 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) - } + principals = append(principals, roles...) return principals, nil } @@ -378,8 +357,12 @@ type principalSet struct { addGroupRoles []interface{} deleteUserRoles []interface{} deleteGroupRoles []interface{} + // unchangedPrincipalRoles is set iff there are no changes, that is, the + // length of all other members is zero + unchangedPrincipalRoles []PrincipalRole } +// TODO: Should this be moved inside the transaction, at this point? 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) @@ -393,11 +376,11 @@ func (r *Repository) principalsToSet(ctx context.Context, role *Role, userIds, g for _, p := range existing { switch p.GetType() { case UserRoleType.String(): - existingUsers[p.GetPrincipalId()] = p + existingUsers[p.PrincipalId] = p case GroupRoleType.String(): - existingGroups[p.GetPrincipalId()] = p + existingGroups[p.PrincipalId] = p default: - return nil, fmt.Errorf("%s is unknown principal type %s", p.GetPrincipalId(), p.GetType()) + return nil, fmt.Errorf("%s is unknown principal type %s", p.PrincipalId, p.GetType()) } } var newUserRoles []interface{} @@ -405,7 +388,7 @@ func (r *Repository) principalsToSet(ctx context.Context, role *Role, userIds, g for _, id := range userIds { userIdsMap[id] = struct{}{} if _, ok := existingUsers[id]; !ok { - usrRole, err := NewUserRole(role.ScopeId, role.PublicId, id) + usrRole, err := NewUserRole(role.PublicId, id) if err != nil { return nil, fmt.Errorf("unable to create in memory user role for add: %w", err) } @@ -417,7 +400,7 @@ func (r *Repository) principalsToSet(ctx context.Context, role *Role, userIds, g for _, id := range groupIds { groupIdsMap[id] = struct{}{} if _, ok := existingGroups[id]; !ok { - grpRole, err := NewGroupRole(role.ScopeId, role.PublicId, id) + grpRole, err := NewGroupRole(role.PublicId, id) if err != nil { return nil, fmt.Errorf("unable to create in memory group role for add: %w", err) } @@ -426,8 +409,8 @@ func (r *Repository) principalsToSet(ctx context.Context, role *Role, userIds, g } var deleteUserRoles []interface{} for _, p := range existingUsers { - if _, ok := userIdsMap[p.GetPrincipalId()]; !ok { - usrRole, err := NewUserRole(p.GetScopeId(), p.GetRoleId(), p.GetPrincipalId()) + if _, ok := userIdsMap[p.PrincipalId]; !ok { + usrRole, err := NewUserRole(p.GetRoleId(), p.GetPrincipalId()) if err != nil { return nil, fmt.Errorf("unable to create in memory user role for delete: %w", err) } @@ -436,18 +419,25 @@ func (r *Repository) principalsToSet(ctx context.Context, role *Role, userIds, g } var deleteGrpRoles []interface{} for _, p := range existingGroups { - if _, ok := groupIdsMap[p.GetPrincipalId()]; !ok { - grpRole, err := NewGroupRole(p.GetScopeId(), p.GetRoleId(), p.GetPrincipalId()) + if _, ok := groupIdsMap[p.PrincipalId]; !ok { + grpRole, err := NewGroupRole(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{ + + toSet := &principalSet{ addUserRoles: newUserRoles, addGroupRoles: newGrpRoles, deleteUserRoles: deleteUserRoles, deleteGroupRoles: deleteGrpRoles, - }, nil + } + + if len(toSet.addUserRoles) == 0 && len(toSet.addGroupRoles) == 0 && len(toSet.deleteUserRoles) == 0 && len(toSet.deleteGroupRoles) == 0 { + toSet.unchangedPrincipalRoles = existing + } + + return toSet, nil } diff --git a/internal/iam/repository_principal_role_test.go b/internal/iam/repository_principal_role_test.go index 7ab97faea..4e0cd7ed2 100644 --- a/internal/iam/repository_principal_role_test.go +++ b/internal/iam/repository_principal_role_test.go @@ -19,31 +19,43 @@ func TestRepository_AddPrincipalRoles(t *testing.T) { rw := db.New(conn) wrapper := db.TestWrapper(t) repo, err := NewRepository(rw, rw, wrapper) + staticOrg, staticProj := TestScopes(t, conn) + orgRole := TestRole(t, conn, staticOrg.PublicId) + projRole := TestRole(t, conn, staticProj.PublicId) require.NoError(t, err) - org, proj := TestScopes(t, conn) - role := TestRole(t, conn, proj.PublicId) - createUsersFn := func() []string { - results := []string{} + createScopesFn := func() (orgs []string, projects []string) { for i := 0; i < 5; i++ { - u := TestUser(t, conn, org.PublicId) + org, proj := TestScopes(t, conn) + orgs = append(orgs, org.PublicId) + projects = append(projects, proj.PublicId) + } + return + } + createUsersFn := func(orgs []string) []string { + results := []string{} + for org := 0; org < 5; org++ { + u := TestUser(t, conn, orgs[org]) results = append(results, u.PublicId) } return results } - createGrpsFn := func() []string { + createGrpsFn := func(orgs, projects []string) []string { results := []string{} - for i := 0; i < 5; i++ { - g := TestGroup(t, conn, proj.PublicId) + for org := 0; org < 5; org++ { + g := TestGroup(t, conn, orgs[org]) results = append(results, g.PublicId) + for proj := 0; proj < 5; proj++ { + g := TestGroup(t, conn, projects[proj]) + results = append(results, g.PublicId) + } } return results } type args struct { - roleId string - roleVersion uint32 - userIds []string - groupIds []string - opt []Option + roleVersion uint32 + wantUserIds bool + wantGroupIds bool + opt []Option } tests := []struct { name string @@ -54,50 +66,41 @@ func TestRepository_AddPrincipalRoles(t *testing.T) { { name: "valid-both-users-and-groups", args: args{ - roleId: role.PublicId, - roleVersion: 1, - userIds: createUsersFn(), - groupIds: createGrpsFn(), + roleVersion: 1, + wantUserIds: true, + wantGroupIds: true, }, wantErr: false, }, { name: "valid-just-groups", args: args{ - roleId: role.PublicId, - roleVersion: 2, - userIds: nil, - groupIds: createGrpsFn(), + roleVersion: 2, + wantGroupIds: true, }, wantErr: false, }, { name: "valid-just-users", args: args{ - roleId: role.PublicId, roleVersion: 3, - userIds: createUsersFn(), - groupIds: nil, + wantUserIds: true, }, wantErr: false, }, { name: "bad-version", args: args{ - roleId: role.PublicId, - roleVersion: 1000, - userIds: createUsersFn(), - groupIds: createGrpsFn(), + roleVersion: 1000, + wantUserIds: true, + wantGroupIds: true, }, wantErr: true, }, { name: "no-principals", args: args{ - roleId: role.PublicId, roleVersion: 1, - userIds: nil, - groupIds: nil, }, wantErr: true, }, @@ -107,43 +110,55 @@ func TestRepository_AddPrincipalRoles(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()) + orgs, projects := createScopesFn() + var userIds, groupIds []string + + for _, roleId := range []string{orgRole.PublicId, projRole.PublicId} { + if tt.args.wantUserIds { + userIds = createUsersFn(orgs) + u := TestUser(t, conn, staticOrg.PublicId) + if roleId == orgRole.PublicId { + userIds = append(userIds, u.PublicId) + } else { + userIds = append(userIds, u.PublicId) + } } - 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) + if tt.args.wantGroupIds { + groupIds = createGrpsFn(orgs, projects) + g := TestGroup(t, conn, staticProj.PublicId) + if roleId == projRole.PublicId { + groupIds = append(groupIds, g.PublicId) + } else { + groupIds = append(groupIds, g.PublicId) + } + } + got, err := repo.AddPrincipalRoles(context.Background(), roleId, tt.args.roleVersion, userIds, 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.ScopedPrincipalId] = r + } + err = db.TestVerifyOplog(t, rw, roleId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) 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()) + foundRoles, err := repo.ListPrincipalRoles(context.Background(), roleId) + require.NoError(err) + for _, r := range foundRoles { + principalId := r.ScopedPrincipalId + require.NoError(err) + assert.NotEmpty(gotPrincipal[principalId]) + assert.Equal(gotPrincipal[principalId].GetRoleId(), r.GetRoleId()) + assert.Equal(gotPrincipal[principalId].GetPrincipalScopeId(), r.GetPrincipalScopeId()) + assert.Equal(gotPrincipal[principalId].GetType(), r.GetType()) + } } - }) } } @@ -361,18 +376,35 @@ func TestRepository_DeletePrincipalRoles(t *testing.T) { wantIsErr: db.ErrInvalidParameter, }, } + createScopesFn := func() (orgs []string, projects []string) { + for i := 0; i < 5; i++ { + org, proj := TestScopes(t, conn) + orgs = append(orgs, org.PublicId) + projects = append(projects, proj.PublicId) + } + return + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) + orgs, projects := createScopesFn() userIds := make([]string, 0, tt.args.createUserCnt) - for i := 0; i < tt.args.createUserCnt; i++ { + if tt.args.createUserCnt > 0 { u := TestUser(t, conn, org.PublicId) userIds = append(userIds, u.PublicId) + for i := 0; i < tt.args.createUserCnt-1; i++ { + u := TestUser(t, conn, orgs[i]) + 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) + if tt.args.createGroupCnt > 0 { + g := TestGroup(t, conn, org.PublicId) groupIds = append(groupIds, g.PublicId) + for i := 0; i < tt.args.createGroupCnt-1; i++ { + g := TestGroup(t, conn, projects[i]) + groupIds = append(groupIds, g.PublicId) + } } principalRoles, err := repo.AddPrincipalRoles(context.Background(), tt.args.role.PublicId, 1, userIds, groupIds, tt.args.opt...) require.NoError(err) @@ -419,7 +451,6 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) - repo, err := NewRepository(rw, rw, wrapper) require.NoError(t, err) @@ -433,6 +464,8 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { u := TestUser(t, conn, org.PublicId) results = append(results, u.PublicId) } + results = append(results, "u_anon") + results = append(results, "u_auth") return results } createGrpsFn := func() []string { @@ -446,7 +479,8 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { setupFn := func(role *Role) ([]string, []string) { users := createUsersFn() grps := createGrpsFn() - _, err := repo.AddPrincipalRoles(context.Background(), role.PublicId, 1, users, grps) + var err error + _, err = repo.AddPrincipalRoles(context.Background(), role.PublicId, 1, users, grps) require.NoError(t, err) return users, grps } @@ -466,18 +500,18 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { 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: "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: 12, + // }, { name: "no change", setup: setupFn, @@ -518,7 +552,7 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { addToOrigGrps: false, }, wantErr: false, - wantAffectedRows: 12, + wantAffectedRows: 14, }, } for _, tt := range tests { @@ -528,32 +562,31 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { 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...) + tt.args.userIds = append(tt.args.userIds, origUsers...) } if tt.args.addToOrigGrps { - setGrps = append(setGrps, origGrps...) + tt.args.groupIds = append(tt.args.groupIds, origGrps...) } - got, affectedRows, err := repo.SetPrincipalRoles(context.Background(), tt.args.role.PublicId, tt.args.roleVersion, setUsers, setGrps, tt.args.opt...) + got, affectedRows, err := repo.SetPrincipalRoles(context.Background(), tt.args.role.PublicId, tt.args.roleVersion, tt.args.userIds, tt.args.groupIds, tt.args.opt...) if tt.wantErr { require.Error(err) return } require.NoError(err) assert.Equal(tt.wantAffectedRows, affectedRows) + assert.Equal(len(tt.args.userIds)+len(tt.args.groupIds), len(got)) var gotIds []string for _, r := range got { - gotIds = append(gotIds, r.GetPrincipalId()) + gotIds = append(gotIds, r.PrincipalId) } 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) + assert.Equal(wantIds, gotIds) }) } } @@ -663,6 +696,7 @@ func TestRepository_principalsToSet(t *testing.T) { assert.Empty(got.addGroupRoles) assert.Empty(got.deleteUserRoles) assert.Empty(got.deleteGroupRoles) + assert.Equal(len(users)+len(grps), len(got.unchangedPrincipalRoles)) }) t.Run("mixed", func(t *testing.T) { require := require.New(t) diff --git a/internal/iam/repository_role.go b/internal/iam/repository_role.go index 576e03c9a..02b473da8 100644 --- a/internal/iam/repository_role.go +++ b/internal/iam/repository_role.go @@ -39,11 +39,12 @@ func (r *Repository) CreateRole(ctx context.Context, role *Role, opt ...Option) return resource.(*Role), err } -// UpdateRole will update a role in the repository and return the written -// role. fieldMaskPaths provides field_mask.proto paths for fields that should -// be updated. Fields will be set to NULL if the field is a zero value and -// included in fieldMask. Name and Description are the only updatable fields, -// If no updatable fields are included in the fieldMaskPaths, then an error is returned. +// UpdateRole will update a role in the repository and return the written role. +// fieldMaskPaths provides field_mask.proto paths for fields that should be +// updated. Fields will be set to NULL if the field is a zero value and +// included in fieldMask. Name, Description, and GrantScopeId are the only +// updatable fields, If no updatable fields are included in the fieldMaskPaths, +// then an error is returned. func (r *Repository) UpdateRole(ctx context.Context, role *Role, fieldMaskPaths []string, opt ...Option) (*Role, int, error) { if role == nil { return nil, db.NoRowsAffected, fmt.Errorf("update role: missing role %w", db.ErrNilParameter) @@ -58,6 +59,7 @@ func (r *Repository) UpdateRole(ctx context.Context, role *Role, fieldMaskPaths switch { case strings.EqualFold("name", f): case strings.EqualFold("description", f): + case strings.EqualFold("grantscopeid", f): default: return nil, db.NoRowsAffected, fmt.Errorf("update role: field: %s: %w", f, db.ErrInvalidFieldMask) } @@ -65,8 +67,9 @@ func (r *Repository) UpdateRole(ctx context.Context, role *Role, fieldMaskPaths var dbMask, nullFields []string dbMask, nullFields = buildUpdatePaths( map[string]interface{}{ - "name": role.Name, - "description": role.Description, + "name": role.Name, + "description": role.Description, + "grantscopeid": role.GrantScopeId, }, fieldMaskPaths, ) diff --git a/internal/iam/repository_role_grant.go b/internal/iam/repository_role_grant.go new file mode 100644 index 000000000..c19c80371 --- /dev/null +++ b/internal/iam/repository_role_grant.go @@ -0,0 +1,368 @@ +package iam + +import ( + "context" + "fmt" + + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/oplog" + "github.com/hashicorp/watchtower/internal/perms" + "github.com/hashicorp/watchtower/internal/types/scope" +) + +// AddRoleGrant will add role grants associated with the role ID in the +// repository. No options are currently supported. +func (r *Repository) AddRoleGrants(ctx context.Context, roleId string, roleVersion uint32, grants []string, opt ...Option) ([]*RoleGrant, error) { + if roleId == "" { + return nil, fmt.Errorf("add role grants: missing role id %w", db.ErrInvalidParameter) + } + if len(grants) == 0 { + return nil, fmt.Errorf("add role grants: missing grants: %w", db.ErrInvalidParameter) + } + role := allocRole() + role.PublicId = roleId + + newRoleGrants := make([]interface{}, 0, len(grants)) + for _, grant := range grants { + roleGrant, err := NewRoleGrant(roleId, grant) + if err != nil { + return nil, fmt.Errorf("add role grants: unable to create in memory role grant: %w", err) + } + newRoleGrants = append(newRoleGrants, roleGrant) + } + + _, 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("unable to get ticket: %w", err) + } + + // We need to update the role version as that's the aggregate + updatedRole := allocRole() + updatedRole.PublicId = roleId + updatedRole.Version = uint32(roleVersion) + 1 + var roleOplogMsg oplog.Message + rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(roleVersion)) + if err != nil { + return fmt.Errorf("unable to update role version: %w", err) + } + if rowsUpdated != 1 { + return fmt.Errorf("updated role and %d rows updated", rowsUpdated) + } + msgs = append(msgs, &roleOplogMsg) + roleGrantOplogMsgs := make([]*oplog.Message, 0, len(newRoleGrants)) + if err := w.CreateItems(ctx, newRoleGrants, db.NewOplogMsgs(&roleGrantOplogMsgs)); err != nil { + return fmt.Errorf("unable to add grants: %w", err) + } + msgs = append(msgs, roleGrantOplogMsgs...) + + s, err := role.GetScope(ctx, r.reader) + if err != nil { + return fmt.Errorf("add role grants: unable to get role %s scope: %w", roleId, err) + } + + metadata := oplog.Metadata{ + "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, + "scope-id": []string{s.PublicId}, + "scope-type": []string{s.Type}, + "resource-public-id": []string{roleId}, + } + if err := w.WriteOplogEntryWith(ctx, r.wrapper, roleTicket, metadata, msgs); err != nil { + return fmt.Errorf("unable to write oplog: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, fmt.Errorf("add role grants: error creating grants: %w", err) + } + roleGrants := make([]*RoleGrant, 0, len(newRoleGrants)) + for _, grant := range newRoleGrants { + roleGrants = append(roleGrants, grant.(*RoleGrant)) + } + return roleGrants, nil +} + +// DeleteRoleGrants deletes grants (as strings) from a role (roleId). The role's +// current db version must match the roleVersion or an error will be returned. +func (r *Repository) DeleteRoleGrants(ctx context.Context, roleId string, roleVersion uint32, grants []string, opt ...Option) (int, error) { + if roleId == "" { + return 0, fmt.Errorf("delete role grants: missing role id %w", db.ErrInvalidParameter) + } + if len(grants) == 0 { + return 0, fmt.Errorf("delete role grants: missing grants: %w", db.ErrInvalidParameter) + } + role := allocRole() + role.PublicId = roleId + + 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 role grants: 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 role grants: unable to update role version: %w", err) + } + if rowsUpdated != 1 { + return fmt.Errorf("delete roles grants: updated role and %d rows updated", rowsUpdated) + } + msgs = append(msgs, &roleOplogMsg) + + // Find existing grants + roleGrants := []*RoleGrant{} + if err := reader.SearchWhere(ctx, &roleGrants, "role_id = ?", []interface{}{roleId}); err != nil { + return fmt.Errorf("delete role grants: unable to search for grants: %w", err) + } + found := map[string]bool{} + for _, rg := range roleGrants { + found[rg.CanonicalGrant] = true + } + + // Check incoming grants to see if they exist and if so add to + // delete slice + deleteRoleGrants := make([]interface{}, 0, len(grants)) + for _, grant := range grants { + // Use a fake scope, just want to get out a canonical string + perm, err := perms.Parse( + perms.Scope{ + Id: "s_abcd1234", + Type: scope.Organization, + }, + "", + grant, + ) + if err != nil { + return fmt.Errorf("delete role grants: error parsing grant string: %w", err) + } + // We don't have what they want to delete, so ignore it + if !found[perm.CanonicalString()] { + continue + } + + roleGrant, err := NewRoleGrant(roleId, grant) + if err != nil { + return fmt.Errorf("delete role grants: unable to create in memory role grant: %w", err) + } + deleteRoleGrants = append(deleteRoleGrants, roleGrant) + } + + if len(deleteRoleGrants) == 0 { + return nil + } + + roleGrantOplogMsgs := make([]*oplog.Message, 0, len(deleteRoleGrants)) + rowsDeleted, err := w.DeleteItems(ctx, deleteRoleGrants, db.NewOplogMsgs(&roleGrantOplogMsgs)) + if err != nil { + return fmt.Errorf("delete role grants: unable to delete role grant: %w", err) + } + if rowsDeleted != len(deleteRoleGrants) { + return fmt.Errorf("delete role grants: role grants deleted %d did not match request for %d", rowsDeleted, len(deleteRoleGrants)) + } + totalRowsDeleted = rowsDeleted + msgs = append(msgs, roleGrantOplogMsgs...) + + s, err := role.GetScope(ctx, r.reader) + if err != nil { + return fmt.Errorf("delete role grants: unable to get role %s scope: %w", roleId, err) + } + + metadata := oplog.Metadata{ + "op-type": []string{oplog.OpType_OP_TYPE_DELETE.String()}, + "scope-id": []string{s.PublicId}, + "scope-type": []string{s.Type}, + "resource-public-id": []string{roleId}, + } + if err := w.WriteOplogEntryWith(ctx, r.wrapper, roleTicket, metadata, msgs); err != nil { + return fmt.Errorf("delete role grants: unable to write oplog: %w", err) + } + + return nil + }, + ) + if err != nil { + return db.NoRowsAffected, fmt.Errorf("delete role grants: error deleting role grants: %w", err) + } + return totalRowsDeleted, nil +} + +// SetRoleGrants sets grants on a role (roleId). The role's current db version +// must match the roleVersion or an error will be returned. +func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersion uint32, grants []string, opt ...Option) ([]*RoleGrant, int, error) { + if roleId == "" { + return nil, 0, fmt.Errorf("set role grants: missing role id %w", db.ErrInvalidParameter) + } + + // Explicitly set to zero clears, but treat nil as a mistake + if grants == nil { + return nil, 0, fmt.Errorf("set role grants: nil grants: %w", db.ErrNilParameter) + } + + role := allocRole() + role.PublicId = roleId + + // NOTE: Set calculation can safely take place out of the transaction since + // we are using roleVersion to ensure that we end up operating on the same + // set of data from this query to the final set in the transaction function + + // Find existing grants + roleGrants := []*RoleGrant{} + if err := r.reader.SearchWhere(ctx, &roleGrants, "role_id = ?", []interface{}{roleId}); err != nil { + return nil, 0, fmt.Errorf("set role grants: unable to search for grants: %w", err) + } + found := map[string]*RoleGrant{} + for _, rg := range roleGrants { + found[rg.CanonicalGrant] = rg + } + + // Check incoming grants to see if they exist and if so act appropriately + currentRoleGrants := make([]*RoleGrant, 0, len(grants)+len(found)) + addRoleGrants := make([]interface{}, 0, len(grants)) + deleteRoleGrants := make([]interface{}, 0, len(grants)) + for _, grant := range grants { + // Use a fake scope, just want to get out a canonical string + perm, err := perms.Parse( + perms.Scope{ + Id: "s_abcd1234", + Type: scope.Organization, + }, + "", + grant, + ) + if err != nil { + return nil, 0, fmt.Errorf("set role grants: error parsing grant string: %w", err) + } + canonicalString := perm.CanonicalString() + + rg, ok := found[canonicalString] + if ok { + // If we have an exact match, do nothing, we want to keep + // it, but remove from found + currentRoleGrants = append(currentRoleGrants, rg) + delete(found, canonicalString) + continue + } + + // Not found, so add + rg, err = NewRoleGrant(roleId, grant) + if err != nil { + return nil, 0, fmt.Errorf("set role grants: unable to create in memory role grant: %w", err) + } + addRoleGrants = append(addRoleGrants, rg) + currentRoleGrants = append(currentRoleGrants, rg) + } + + if len(found) > 0 { + for _, rg := range found { + deleteRoleGrants = append(deleteRoleGrants, rg) + } + } + + if len(addRoleGrants) == 0 && len(deleteRoleGrants) == 0 { + return currentRoleGrants, 0, nil + } + + 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("set role grants: 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("set role grants: unable to update role version: %w", err) + } + if rowsUpdated != 1 { + return fmt.Errorf("set roles grants: updated role and %d rows updated", rowsUpdated) + } + msgs = append(msgs, &roleOplogMsg) + + // Write the new ones in + if len(addRoleGrants) > 0 { + roleGrantOplogMsgs := make([]*oplog.Message, 0, len(addRoleGrants)) + if err := w.CreateItems(ctx, addRoleGrants, db.NewOplogMsgs(&roleGrantOplogMsgs)); err != nil { + return fmt.Errorf("unable to add grants during set: %w", err) + } + msgs = append(msgs, roleGrantOplogMsgs...) + } + + // Anything we didn't take out of found needs to be removed + if len(deleteRoleGrants) > 0 { + roleGrantOplogMsgs := make([]*oplog.Message, 0, len(deleteRoleGrants)) + rowsDeleted, err := w.DeleteItems(ctx, deleteRoleGrants, db.NewOplogMsgs(&roleGrantOplogMsgs)) + if err != nil { + return fmt.Errorf("set role grants: unable to delete role grant: %w", err) + } + if rowsDeleted != len(deleteRoleGrants) { + return fmt.Errorf("set role grants: role grants deleted %d did not match request for %d", rowsDeleted, len(deleteRoleGrants)) + } + totalRowsDeleted = rowsDeleted + msgs = append(msgs, roleGrantOplogMsgs...) + } + + s, err := role.GetScope(ctx, r.reader) + if err != nil { + return fmt.Errorf("set role grants: unable to get role %s scope: %w", roleId, err) + } + + metadata := oplog.Metadata{ + "op-type": []string{oplog.OpType_OP_TYPE_DELETE.String(), oplog.OpType_OP_TYPE_CREATE.String()}, + "scope-id": []string{s.PublicId}, + "scope-type": []string{s.Type}, + "resource-public-id": []string{roleId}, + } + if err := w.WriteOplogEntryWith(ctx, r.wrapper, roleTicket, metadata, msgs); err != nil { + return fmt.Errorf("set role grants: unable to write oplog: %w", err) + } + + currentRoleGrants, err = r.ListRoleGrants(ctx, roleId) + if err != nil { + return fmt.Errorf("set role grants: unable to retrieve current role grants after set: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, db.NoRowsAffected, fmt.Errorf("set role grants: error set role grants: %w", err) + } + return currentRoleGrants, totalRowsDeleted, nil +} + +// ListRoleGrants returns the grants for the roleId and supports the WithLimit +// option. +func (r *Repository) ListRoleGrants(ctx context.Context, roleId string, opt ...Option) ([]*RoleGrant, error) { + if roleId == "" { + return nil, fmt.Errorf("add role grants: missing role id %w", db.ErrInvalidParameter) + } + var roleGrants []*RoleGrant + if err := r.list(ctx, &roleGrants, "role_id = ?", []interface{}{roleId}, opt...); err != nil { + return nil, fmt.Errorf("lookup role grants: unable to lookup role grants: %w", err) + } + return roleGrants, nil +} diff --git a/internal/iam/repository_role_grant_test.go b/internal/iam/repository_role_grant_test.go new file mode 100644 index 000000000..8df0d8167 --- /dev/null +++ b/internal/iam/repository_role_grant_test.go @@ -0,0 +1,441 @@ +package iam + +import ( + "context" + "errors" + "fmt" + "math/rand" + "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_AddRoleGrants(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) + _, proj := TestScopes(t, conn) + role := TestRole(t, conn, proj.PublicId) + createGrantsFn := func() []string { + results := []string{} + for i := 0; i < 5; i++ { + g := fmt.Sprintf("id=hc_%d;actions=*", i) + results = append(results, g) + } + return results + } + type args struct { + roleId string + roleVersion uint32 + grants []string + opt []Option + } + tests := []struct { + name string + args args + wantErr bool + wantErrIs error + }{ + { + name: "valid", + args: args{ + roleId: role.PublicId, + roleVersion: 1, + grants: createGrantsFn(), + }, + wantErr: false, + }, + { + name: "no-grants", + args: args{ + roleId: role.PublicId, + roleVersion: 2, + grants: nil, + }, + wantErr: true, + }, + { + name: "bad-version", + args: args{ + roleId: role.PublicId, + roleVersion: 1000, + grants: createGrantsFn(), + }, + 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(allocRoleGrant()).Error) + got, err := repo.AddRoleGrants(context.Background(), tt.args.roleId, tt.args.roleVersion, tt.args.grants, 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) + gotRoleGrant := map[string]*RoleGrant{} + for _, r := range got { + gotRoleGrant[r.CanonicalGrant] = r + } + + err = db.TestVerifyOplog(t, rw, role.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + assert.NoError(err) + + foundRoleGrants, err := repo.ListRoleGrants(context.Background(), role.PublicId) + require.NoError(err) + // Create a map of grants to check against + grantSet := make(map[string]bool, len(tt.args.grants)) + for _, grant := range tt.args.grants { + grantSet[grant] = true + } + for _, r := range foundRoleGrants { + roleGrant := gotRoleGrant[r.CanonicalGrant] + assert.NotEmpty(roleGrant) + assert.Equal(roleGrant.GetRoleId(), r.GetRoleId()) + assert.NotEmpty(grantSet[roleGrant.CanonicalGrant]) + delete(grantSet, roleGrant.CanonicalGrant) + } + assert.Empty(grantSet) + }) + } +} + +func TestRepository_ListRoleGrants(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) + roleGrants := make([]string, 0, tt.createCnt) + for i := 0; i < tt.createCnt; i++ { + roleGrants = append(roleGrants, fmt.Sprintf("id=h_%d;actions=*", i)) + } + testRoles, err := repo.AddRoleGrants(context.Background(), role.PublicId, role.Version, roleGrants, 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.ListRoleGrants(context.Background(), roleId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assert.Equal(tt.wantCnt, len(got)) + }) + } +} + +func TestRepository_DeleteRoleGrants(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 + grantStringsOverride []string + createCnt int + deleteCnt 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), + createCnt: 5, + deleteCnt: 5, + }, + wantRowsDeleted: 5, + wantErr: false, + }, + { + name: "valid-keeping-some", + args: args{ + role: TestRole(t, conn, org.PublicId), + createCnt: 5, + deleteCnt: 3, + }, + wantRowsDeleted: 3, + wantErr: false, + }, + + { + name: "no-deletes", + args: args{ + role: TestRole(t, conn, org.PublicId), + createCnt: 5, + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + + { + name: "not-found", + args: args{ + role: TestRole(t, conn, org.PublicId), + roleIdOverride: func() *string { id := testId(t); return &id }(), + createCnt: 5, + deleteCnt: 5, + }, + wantRowsDeleted: 0, + wantErr: true, + }, + { + name: "missing-role-id", + args: args{ + role: TestRole(t, conn, org.PublicId), + roleIdOverride: func() *string { id := ""; return &id }(), + createCnt: 5, + deleteCnt: 5, + }, + wantRowsDeleted: 0, + wantErr: true, + wantIsErr: db.ErrInvalidParameter, + }, + { + name: "invalid-grant-strings", + args: args{ + role: TestRole(t, conn, org.PublicId), + grantStringsOverride: []string{"id=s_87;actions=*"}, + createCnt: 5, + deleteCnt: 3, + }, + wantRowsDeleted: 2, + }, + } + 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(allocRoleGrant()).Error) + grants := make([]*RoleGrant, 0, tt.args.createCnt) + grantStrings := make([]string, 0, tt.args.createCnt) + for i := 0; i < tt.args.createCnt; i++ { + g, err := NewRoleGrant(tt.args.role.PublicId, fmt.Sprintf("actions=*;id=s_%d", i), tt.args.opt...) + require.NoError(err) + grantStrings = append(grantStrings, g.RawGrant) + grants = append(grants, g) + } + roleGrants, err := repo.AddRoleGrants(context.Background(), tt.args.role.PublicId, 1, grantStrings, tt.args.opt...) + require.NoError(err) + assert.Equal(tt.args.createCnt, len(roleGrants)) + + deleteCanonicalGrants := make([]string, 0, tt.args.deleteCnt) + deleteGrants := make([]string, 0, tt.args.deleteCnt) + for i := 0; i < tt.args.deleteCnt; i++ { + deleteCanonicalGrants = append(deleteCanonicalGrants, grants[i].CanonicalGrant) + deleteGrants = append(deleteGrants, fmt.Sprintf("id=s_%d;actions=*", i)) + } + for i, override := range tt.args.grantStringsOverride { + deleteCanonicalGrants = deleteCanonicalGrants[1:] + deleteGrants[i] = override + } + + var roleId string + switch { + case tt.args.roleIdOverride != nil: + roleId = *tt.args.roleIdOverride + default: + roleId = tt.args.role.PublicId + } + deletedRows, err := repo.DeleteRoleGrants(context.Background(), roleId, 2, deleteGrants, 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) + + roleGrants = []*RoleGrant{} + require.NoError(repo.reader.SearchWhere(context.Background(), &roleGrants, "role_id = ?", []interface{}{roleId})) + found := map[string]bool{} + for _, rg := range roleGrants { + found[rg.CanonicalGrant] = true + } + for _, i := range deleteCanonicalGrants { + assert.False(found[i]) + } + + 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_SetRoleGrants_Randomize(t *testing.T) { + t.Parallel() + require := require.New(t) + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo, err := NewRepository(rw, rw, wrapper) + require.NoError(err) + org, _ := TestScopes(t, conn) + role := TestRole(t, conn, org.PublicId) + require.NoError(conn.Where("1=1").Delete(allocRoleGrant()).Error) + + type roleGrantWrapper struct { + grantString string + enabled bool + } + + totalCnt := 30 + grants := make([]*roleGrantWrapper, 0, totalCnt) + for i := 0; i < totalCnt; i++ { + g, err := NewRoleGrant(role.PublicId, fmt.Sprintf("id=s_%d;actions=*", i)) + require.NoError(err) + grants = append(grants, &roleGrantWrapper{ + grantString: g.RawGrant, + }) + } + + // Each loop will set some number of role grants to enabled or disabled + // randomly and then validate after setting that what's set matches + var grantsToSet []string + var expected map[string]bool + for i := 1; i <= totalCnt; i++ { + grantsToSet = make([]string, 0, totalCnt) + expected = make(map[string]bool, totalCnt) + rand.Seed(time.Now().UnixNano()) + for _, rgw := range grants { + rgw.enabled = rand.Int()%2 == 0 + if rgw.enabled { + grantsToSet = append(grantsToSet, rgw.grantString) + expected[rgw.grantString] = true + } + } + + // First time, run a couple of error conditions + if i == 1 { + _, _, err := repo.SetRoleGrants(context.Background(), "", 1, []string{}) + require.Error(err) + _, _, err = repo.SetRoleGrants(context.Background(), role.PublicId, 1, nil) + require.Error(err) + } + + _, _, err := repo.SetRoleGrants(context.Background(), role.PublicId, uint32(i), grantsToSet) + require.NoError(err) + + roleGrants, err := repo.ListRoleGrants(context.Background(), role.PublicId) + require.NoError(err) + require.Equal(len(grantsToSet), len(roleGrants)) + for _, rg := range roleGrants { + require.Contains(expected, rg.CanonicalGrant) + delete(expected, rg.CanonicalGrant) + } + require.Empty(expected) + } + + // At the end, set to explicitly empty and make sure all are cleared out + _, _, err = repo.SetRoleGrants(context.Background(), role.PublicId, uint32(totalCnt+1), []string{}) + require.NoError(err) + + roleGrants, err := repo.ListRoleGrants(context.Background(), role.PublicId) + require.NoError(err) + require.Empty(roleGrants) +} diff --git a/internal/iam/repository_role_test.go b/internal/iam/repository_role_test.go index 43ff77131..2c2599584 100644 --- a/internal/iam/repository_role_test.go +++ b/internal/iam/repository_role_test.go @@ -380,7 +380,7 @@ func TestRepository_UpdateRole(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert := assert.New(t) + require, assert := require.New(t), assert.New(t) if tt.wantDup { r := TestRole(t, conn, org.PublicId) r.Name = tt.args.name @@ -413,7 +413,8 @@ func TestRepository_UpdateRole(t *testing.T) { assert.True(errors.Is(db.ErrRecordNotFound, err)) return } - assert.NoError(err) + require.NoError(err) + require.NotNil(roleAfterUpdate) assert.Equal(tt.wantRowsUpdate, updatedRows) switch tt.name { case "valid-no-op": diff --git a/internal/iam/repository_user_test.go b/internal/iam/repository_user_test.go index f2a9eeba9..650e4529f 100644 --- a/internal/iam/repository_user_test.go +++ b/internal/iam/repository_user_test.go @@ -286,11 +286,11 @@ func TestRepository_UpdateUser(t *testing.T) { args: args{ name: "modified-scope" + id, fieldMaskPaths: []string{"ScopeId"}, - ScopeId: proj.PublicId, + ScopeId: "global", opt: []Option{WithSkipVetForWrite(true)}, }, wantErr: true, - wantErrMsg: `update: failed pq: scope_id cannot be set`, + wantErrMsg: `update: failed: pq: scope_id cannot be set`, directUpdate: true, }, } @@ -339,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.True(strings.Contains(err.Error(), tt.wantErrMsg)) + assert.True(strings.Contains(err.Error(), tt.wantErrMsg), "%s", err.Error()) } err = db.TestVerifyOplog(t, rw, u.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) require.Error(err) @@ -513,7 +513,7 @@ func TestRepository_ListUsers(t *testing.T) { 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(allocUser()).Error) + require.NoError(conn.Where("public_id != 'u_anon' and public_id != 'u_auth'").Delete(allocUser()).Error) testUsers := []*User{} for i := 0; i < tt.createCnt; i++ { testUsers = append(testUsers, TestUser(t, conn, org.PublicId)) diff --git a/internal/iam/role.go b/internal/iam/role.go index 8932cab11..251b7b755 100644 --- a/internal/iam/role.go +++ b/internal/iam/role.go @@ -25,7 +25,7 @@ var _ Clonable = (*Role)(nil) var _ db.VetForWriter = (*Role)(nil) // NewRole creates a new in memory role with a scope (project/organization) -// allowed options include: withDescripion, WithName. +// allowed options include: withDescripion, WithName, withGrantScopeId. func NewRole(scopeId string, opt ...Option) (*Role, error) { if scopeId == "" { return nil, fmt.Errorf("new role: missing scope id %w", db.ErrInvalidParameter) @@ -33,9 +33,10 @@ func NewRole(scopeId string, opt ...Option) (*Role, error) { opts := getOpts(opt...) r := &Role{ Role: &store.Role{ - ScopeId: scopeId, - Name: opts.withName, - Description: opts.withDescription, + ScopeId: scopeId, + Name: opts.withName, + Description: opts.withDescription, + GrantScopeId: opts.withGrantScopeId, }, } return r, nil @@ -78,9 +79,16 @@ func (role *Role) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { // ResourceType returns the type of the Role. func (*Role) ResourceType() resource.Type { return resource.Role } -// Actions returns the available actions for Role. +// Actions returns the available actions for Role. func (*Role) Actions() map[string]action.Type { - return CrudActions() + ret := CrudlActions() + ret[action.AddGrants.String()] = action.AddGrants + ret[action.RemoveGrants.String()] = action.RemoveGrants + ret[action.SetGrants.String()] = action.SetGrants + ret[action.AddPrincipals.String()] = action.AddPrincipals + ret[action.RemovePrincipals.String()] = action.RemovePrincipals + ret[action.SetPrincipals.String()] = action.SetPrincipals + return ret } // TableName returns the tablename to override the default gorm table name. diff --git a/internal/iam/role_grant.go b/internal/iam/role_grant.go new file mode 100644 index 000000000..6aba03a60 --- /dev/null +++ b/internal/iam/role_grant.go @@ -0,0 +1,124 @@ +package iam + +import ( + "context" + "fmt" + + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/iam/store" + "github.com/hashicorp/watchtower/internal/perms" + "github.com/hashicorp/watchtower/internal/types/resource" + "github.com/hashicorp/watchtower/internal/types/scope" + "google.golang.org/protobuf/proto" +) + +const defaultRoleGrantTable = "iam_role_grant" + +// RoleGrant defines the grants that are assigned to a role +type RoleGrant struct { + *store.RoleGrant + tableName string `gorm:"-"` +} + +// ensure that RoleGrant implements the interfaces of: Clonable and db.VetForWriter +var _ Clonable = (*RoleGrant)(nil) +var _ db.VetForWriter = (*RoleGrant)(nil) + +// NewRoleGrant creates a new in memory role grant +func NewRoleGrant(roleId string, grant string, opt ...Option) (*RoleGrant, error) { + if roleId == "" { + return nil, fmt.Errorf("new role grant: role id is not set: %w", db.ErrNilParameter) + } + if grant == "" { + return nil, fmt.Errorf("new role grant: grant is empty: %w", db.ErrNilParameter) + } + + // Validate that the grant parses successfully. Note that we fake the scope + // here to avoid a lookup as the scope is only relevant at actual ACL + // checking time and we just care that it parses correctly. + perm, err := perms.Parse( + perms.Scope{ + Id: "s_abcd1234", + Type: scope.Organization, + }, + "", + grant, + ) + if err != nil { + return nil, fmt.Errorf("new role grant: error parsing grant string: %w", err) + } + rg := &RoleGrant{ + RoleGrant: &store.RoleGrant{ + RoleId: roleId, + RawGrant: grant, + CanonicalGrant: perm.CanonicalString(), + }, + } + return rg, nil +} + +func allocRoleGrant() RoleGrant { + return RoleGrant{ + RoleGrant: &store.RoleGrant{}, + } +} + +// Clone creates a clone of the RoleGrant +func (g *RoleGrant) Clone() interface{} { + cp := proto.Clone(g.RoleGrant) + return &RoleGrant{ + RoleGrant: cp.(*store.RoleGrant), + } +} + +// VetForWrite implements db.VetForWrite() interface +func (g *RoleGrant) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + if g.RawGrant == "" { + return fmt.Errorf("vet role grant for writing: grant is empty: %w", db.ErrNilParameter) + } + + // Validate that the grant parses successfully. Note that we fake the scope + // here to avoid a lookup as the scope is only relevant at actual ACL + // checking time and we just care that it parses correctly. We may have + // already done this in NewRoleGrant, but we re-check and set it here + // anyways because it should still be part of the vetting process. + perm, err := perms.Parse( + perms.Scope{ + Id: "s_abcd1234", + Type: scope.Organization, + }, + "", + g.RawGrant, + ) + if err != nil { + return fmt.Errorf("vet role grant for writing: error parsing grant string: %w", err) + } + canonical := perm.CanonicalString() + if g.CanonicalGrant != "" && g.CanonicalGrant != canonical { + return fmt.Errorf("vet role grant for writing: existing canonical grant and derived one do not match: %w", err) + } + g.CanonicalGrant = canonical + + return nil +} + +// ResourceType returns the type of the RoleGrant +func (*RoleGrant) ResourceType() resource.Type { return resource.RoleGrant } + +// TableName returns the tablename to override the default gorm table name +func (g *RoleGrant) TableName() string { + if g.tableName != "" { + return g.tableName + } + return defaultRoleGrantTable +} + +// SetTableName sets the tablename and satisfies the ReplayableMessage interface +func (g *RoleGrant) SetTableName(n string) { + switch n { + case "": + g.tableName = defaultRoleGrantTable + default: + g.tableName = n + } +} diff --git a/internal/iam/role_grant_test.go b/internal/iam/role_grant_test.go new file mode 100644 index 000000000..4c1ddfefe --- /dev/null +++ b/internal/iam/role_grant_test.go @@ -0,0 +1,263 @@ +package iam + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/iam/store" + "github.com/hashicorp/watchtower/internal/types/resource" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestRoleGrant_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + _, proj := TestScopes(t, conn) + projRole := TestRole(t, conn, proj.PublicId) + + type args struct { + roleId string + grant string + opt []Option + } + tests := []struct { + name string + args args + want *RoleGrant + wantErr bool + wantIsErr error + create bool + }{ + { + name: "nil-role", + args: args{ + roleId: "", + grant: "id=*;actions=*", + }, + wantErr: true, + wantIsErr: db.ErrNilParameter, + }, + { + name: "nil-grant", + args: args{ + roleId: projRole.PublicId, + grant: "", + }, + wantErr: true, + wantIsErr: db.ErrNilParameter, + create: true, + }, + { + name: "valid", + args: args{ + roleId: projRole.PublicId, + grant: "id=*;actions=*", + }, + want: func() *RoleGrant { + g := allocRoleGrant() + g.RoleId = projRole.PublicId + g.RawGrant = "id=*;actions=*" + g.CanonicalGrant = "id=*;actions=*" + return &g + }(), + create: true, + }, + { + name: "valid-reversed-grant", + args: args{ + roleId: projRole.PublicId, + grant: "actions=*;id=*", + }, + want: func() *RoleGrant { + g := allocRoleGrant() + g.RoleId = projRole.PublicId + g.RawGrant = "actions=*;id=*" + g.CanonicalGrant = "id=*;actions=*" + return &g + }(), + create: 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(allocRoleGrant()).Error) + got, err := NewRoleGrant(tt.args.roleId, tt.args.grant, 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) + if tt.create { + err = db.New(conn).Create(context.Background(), got) + if tt.wantErr { + assert.Error(err) + return + } else { + assert.NoError(err) + } + assert.Equal(tt.want.CanonicalGrant, got.CanonicalGrant) + + // also ensure duplicate grants aren't allowed + g2 := got.Clone().(*RoleGrant) + assert.Error(db.New(conn).Create(context.Background(), g2)) + } + }) + } +} + +func TestRoleGrant_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) + roleGrant := TestRoleGrant(t, conn, r.PublicId, "id=*;actions=*") + updateRoleGrant := roleGrant.Clone().(*RoleGrant) + updateRoleGrant.RawGrant = "actions=*;id=*" + updatedRows, err := rw.Update(context.Background(), updateRoleGrant, []string{"RawGrant"}, nil) + require.Error(err) + assert.Equal(0, updatedRows) + }) +} + +func TestRoleGrant_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + + _, proj := TestScopes(t, conn) + projRole := TestRole(t, conn, proj.PublicId) + + type args struct { + roleId string + grant string + } + tests := []struct { + name string + args args + want *RoleGrant + wantErr bool + wantsErrStr string + deletedRows int + }{ + { + name: "nil-role", + args: args{ + roleId: "", + grant: "id=*;actions=*", + }, + wantErr: true, + wantsErrStr: "primary key is not set", + }, + { + name: "nil-grant", + args: args{ + roleId: projRole.PublicId, + grant: "", + }, + // Note: Gorm's primary key checking is only for what it considers + // the main primary key field, hence this doesn't error as the above + // case does + }, + { + name: "invalid", + args: args{ + roleId: projRole.PublicId, + grant: "id=a_edcb;actions=create,update", + }, + }, + { + name: "valid", + args: args{ + roleId: projRole.PublicId, + grant: "id=a_bcde;actions=create,update", + }, + deletedRows: 1, + }, + } + 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(allocRoleGrant()).Error) + rg, err := NewRoleGrant(projRole.PublicId, "id=a_bcde;actions=create,update") + require.NoError(err) + require.NoError(rw.Create(context.Background(), rg)) + rg, err = NewRoleGrant(projRole.PublicId, "id=a_cdef;actions=create,update") + require.NoError(err) + require.NoError(rw.Create(context.Background(), rg)) + + rg = &RoleGrant{ + RoleGrant: &store.RoleGrant{ + RoleId: tt.args.roleId, + RawGrant: tt.args.grant, + CanonicalGrant: tt.args.grant, + }, + } + deleted, err := rw.Delete(context.Background(), rg) + if tt.wantErr { + require.Error(err) + assert.True(strings.Contains(err.Error(), tt.wantsErrStr)) + return + } + require.NoError(err) + assert.Equal(tt.deletedRows, deleted) + }) + } +} + +func TestRoleGrant_ResourceType(t *testing.T) { + assert := assert.New(t) + r := &RoleGrant{} + ty := r.ResourceType() + assert.Equal(ty, resource.RoleGrant) +} + +func TestRoleGrant_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + t.Run("valid", func(t *testing.T) { + assert := assert.New(t) + s := testOrg(t, conn, "", "") + role := TestRole(t, conn, s.PublicId) + + g, err := NewRoleGrant(role.PublicId, "id=*;actions=*") + assert.NoError(err) + assert.NotNil(g) + assert.Equal(g.RoleId, role.PublicId) + assert.Equal(g.RawGrant, "id=*;actions=*") + + cp := g.Clone() + assert.True(proto.Equal(cp.(*RoleGrant).RoleGrant, g.RoleGrant)) + }) + t.Run("not-equal", func(t *testing.T) { + assert := assert.New(t) + s := testOrg(t, conn, "", "") + role := TestRole(t, conn, s.PublicId) + + g, err := NewRoleGrant(role.PublicId, "id=*;actions=*") + assert.NoError(err) + assert.NotNil(g) + assert.Equal(g.RoleId, role.PublicId) + assert.Equal(g.RawGrant, "id=*;actions=*") + + g2, err := NewRoleGrant(role.PublicId, "id=foo;actions=read") + assert.NoError(err) + assert.NotNil(g2) + assert.Equal(g2.RoleId, role.PublicId) + assert.Equal(g2.RawGrant, "id=foo;actions=read") + + cp := g.Clone() + assert.True(!proto.Equal(cp.(*RoleGrant).RoleGrant, g2.RoleGrant)) + }) +} diff --git a/internal/iam/role_test.go b/internal/iam/role_test.go index 945d3a985..c1ec714c1 100644 --- a/internal/iam/role_test.go +++ b/internal/iam/role_test.go @@ -166,7 +166,7 @@ func Test_RoleCreate(t *testing.T) { }(), }, wantErr: true, - wantErrMsg: "create: vet for write failed scope is not found", + wantErrMsg: "create: vet for write failed: scope is not found", }, } @@ -206,14 +206,17 @@ func Test_RoleUpdate(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") id := testId(t) org, proj := TestScopes(t, conn) + org2, proj2 := TestScopes(t, conn) rw := db.New(conn) type args struct { - name string - description string - fieldMaskPaths []string - nullPaths []string - ScopeId string - opts []db.Option + name string + description string + fieldMaskPaths []string + nullPaths []string + scopeId string + grantScopeId string + scopeIdOverride string + opts []db.Option } tests := []struct { name string @@ -228,7 +231,7 @@ func Test_RoleUpdate(t *testing.T) { args: args{ name: "valid" + id, fieldMaskPaths: []string{"Name"}, - ScopeId: org.PublicId, + scopeId: org.PublicId, }, wantErr: false, wantRowsUpdate: 1, @@ -236,19 +239,20 @@ func Test_RoleUpdate(t *testing.T) { { name: "proj-scope-id", args: args{ - name: "proj-scope-id" + id, - fieldMaskPaths: []string{"ScopeId"}, - ScopeId: proj.PublicId, + name: "proj-scope-id" + id, + fieldMaskPaths: []string{"ScopeId"}, + scopeId: proj.PublicId, + scopeIdOverride: org.PublicId, }, wantErr: true, - wantErrMsg: "update: vet for write failed not allowed to change a resource's scope", + wantErrMsg: "update: vet for write failed: not allowed to change a resource's scope", }, { name: "proj-scope-id-not-in-mask", args: args{ name: "proj-scope-id" + id, fieldMaskPaths: []string{"Name"}, - ScopeId: proj.PublicId, + scopeId: proj.PublicId, }, wantErr: false, wantRowsUpdate: 1, @@ -258,7 +262,7 @@ func Test_RoleUpdate(t *testing.T) { args: args{ name: "empty-scope-id" + id, fieldMaskPaths: []string{"Name"}, - ScopeId: "", + scopeId: "", }, wantErr: false, wantRowsUpdate: 1, @@ -268,11 +272,11 @@ func Test_RoleUpdate(t *testing.T) { args: args{ name: "dup-name" + id, fieldMaskPaths: []string{"Name"}, - ScopeId: org.PublicId, + scopeId: org.PublicId, }, wantErr: true, wantDup: true, - wantErrMsg: `update: failed pq: duplicate key value violates unique constraint "iam_role_name_scope_id_key"`, + wantErrMsg: `update: failed: pq: duplicate key value violates unique constraint "iam_role_name_scope_id_key"`, }, { name: "set description null", @@ -280,7 +284,7 @@ func Test_RoleUpdate(t *testing.T) { name: "set description null" + id, fieldMaskPaths: []string{"Name"}, nullPaths: []string{"Description"}, - ScopeId: org.PublicId, + scopeId: org.PublicId, }, wantErr: false, wantRowsUpdate: 1, @@ -291,7 +295,7 @@ func Test_RoleUpdate(t *testing.T) { description: "set description null" + id, fieldMaskPaths: []string{"Description"}, nullPaths: []string{"Name"}, - ScopeId: org.PublicId, + scopeId: org.PublicId, }, wantDup: true, wantErr: false, @@ -303,17 +307,91 @@ func Test_RoleUpdate(t *testing.T) { name: "set name null" + id, fieldMaskPaths: []string{"Name"}, nullPaths: []string{"Description"}, - ScopeId: org.PublicId, + scopeId: org.PublicId, }, wantErr: false, wantRowsUpdate: 1, }, + { + name: "set grant scope in project with same scope", + args: args{ + name: "set grant scope in project with same scope", + fieldMaskPaths: []string{"Name", "GrantScopeId"}, + scopeId: proj.PublicId, + grantScopeId: proj.PublicId, + }, + wantRowsUpdate: 1, + }, + { + name: "set grant scope in org with same scope", + args: args{ + name: "set grant scope in org with same scope", + fieldMaskPaths: []string{"Name", "GrantScopeId"}, + scopeId: org2.PublicId, + grantScopeId: org2.PublicId, + }, + wantRowsUpdate: 1, + }, + { + name: "set grant scope in project with different scope", + args: args{ + name: "set grant scope in project with different scope", + fieldMaskPaths: []string{"GrantScopeId"}, + scopeId: proj.PublicId, + grantScopeId: proj2.PublicId, + }, + wantErr: true, + wantErrMsg: "update: failed: pq: invalid to set grant_scope_id to non-same scope_id when role scope type is project", + }, + { + name: "set grant scope in org", + args: args{ + name: "set grant scope in org", + fieldMaskPaths: []string{"Name", "GrantScopeId"}, + scopeId: org2.PublicId, + grantScopeId: proj2.PublicId, + }, + wantRowsUpdate: 1, + }, + { + name: "set grant scope in external project", + args: args{ + name: "set grant scope in external project", + fieldMaskPaths: []string{"GrantScopeId"}, + scopeId: org.PublicId, + grantScopeId: proj2.PublicId, + }, + wantErr: true, + wantErrMsg: "update: failed: pq: grant_scope_id is not a child project of the role scope", + }, + { + name: "set grant scope in global", + args: args{ + name: "set grant scope in global", + fieldMaskPaths: []string{"GrantScopeId"}, + scopeId: org.PublicId, + grantScopeId: "global", + }, + wantErr: true, + wantErrMsg: "update: failed: pq: grant_scope_id is not a child project of the role scope", + }, + { + name: "set grant scope to parent", + args: args{ + name: "set grant scope to parent", + fieldMaskPaths: []string{"GrantScopeId"}, + scopeId: proj2.PublicId, + grantScopeId: org2.PublicId, + }, + wantErr: true, + wantErrMsg: "update: failed: pq: invalid to set grant_scope_id to non-same scope_id when role scope type is project", + }, { name: "attempt scope id update", args: args{ name: "valid" + id, fieldMaskPaths: []string{"ScopeId"}, - ScopeId: proj.PublicId, + scopeId: proj.PublicId, opts: []db.Option{db.WithSkipVetForWrite(true)}, }, wantErr: true, @@ -325,19 +403,27 @@ func Test_RoleUpdate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) if tt.wantDup { - r := TestRole(t, conn, org.PublicId, WithName(tt.args.name)) + r := TestRole(t, conn, tt.args.scopeId, WithName(tt.args.name)) _, err := rw.Update(context.Background(), r, tt.args.fieldMaskPaths, tt.args.nullPaths) require.NoError(err) } id := testId(t) - role := TestRole(t, conn, org.PublicId, WithDescription(id), WithName(id)) + scopeId := tt.args.scopeId + if scopeId == "" { + scopeId = org.PublicId + } + role := TestRole(t, conn, scopeId, WithDescription(id), WithName(id)) updateRole := allocRole() updateRole.PublicId = role.PublicId - updateRole.ScopeId = tt.args.ScopeId + updateRole.ScopeId = tt.args.scopeId + if tt.args.scopeIdOverride != "" { + updateRole.ScopeId = tt.args.scopeIdOverride + } updateRole.Name = tt.args.name updateRole.Description = tt.args.description + updateRole.GrantScopeId = tt.args.grantScopeId updatedRows, err := rw.Update(context.Background(), &updateRole, tt.args.fieldMaskPaths, tt.args.nullPaths, tt.args.opts...) if tt.wantErr { @@ -356,6 +442,9 @@ func Test_RoleUpdate(t *testing.T) { foundRole.PublicId = role.GetPublicId() err = rw.LookupByPublicId(context.Background(), &foundRole) require.NoError(err) + if tt.args.grantScopeId == "" { + updateRole.GrantScopeId = role.ScopeId + } assert.True(proto.Equal(updateRole, foundRole)) if len(tt.args.nullPaths) != 0 { dbassert := dbassert.New(t, rw) @@ -442,6 +531,12 @@ func TestRole_Actions(t *testing.T) { assert.Equal(a[action.Update.String()], action.Update) assert.Equal(a[action.Read.String()], action.Read) assert.Equal(a[action.Delete.String()], action.Delete) + assert.Equal(a[action.AddGrants.String()], action.AddGrants) + assert.Equal(a[action.RemoveGrants.String()], action.RemoveGrants) + assert.Equal(a[action.SetGrants.String()], action.SetGrants) + assert.Equal(a[action.AddPrincipals.String()], action.AddPrincipals) + assert.Equal(a[action.RemovePrincipals.String()], action.RemovePrincipals) + assert.Equal(a[action.SetPrincipals.String()], action.SetPrincipals) } func TestRole_ResourceType(t *testing.T) { diff --git a/internal/iam/store/principal_role.pb.go b/internal/iam/store/principal_role.pb.go index cbf232649..1d2432105 100644 --- a/internal/iam/store/principal_role.pb.go +++ b/internal/iam/store/principal_role.pb.go @@ -40,9 +40,6 @@ type UserRole struct { // 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() { @@ -98,13 +95,6 @@ func (x *UserRole) GetPrincipalId() string { return "" } -func (x *UserRole) GetScopeId() string { - if x != nil { - return x.ScopeId - } - return "" -} - type GroupRole struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -119,9 +109,6 @@ type GroupRole struct { // 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() { @@ -177,13 +164,6 @@ func (x *GroupRole) GetPrincipalId() string { return "" } -func (x *GroupRole) GetScopeId() string { - if x != nil { - return x.ScopeId - } - return "" -} - type PrincipalRoleView struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -199,9 +179,15 @@ type PrincipalRoleView struct { 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 + // principal_scope_id of the principal // @inject_tag: `gorm:"default:null"` - ScopeId string `protobuf:"bytes,5,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"` + PrincipalScopeId string `protobuf:"bytes,5,opt,name=principal_scope_id,json=principalScopeId,proto3" json:"principal_scope_id,omitempty" gorm:"default:null"` + // role_scope_id of the role + // @inject_tag: `gorm:"default:null"` + RoleScopeId string `protobuf:"bytes,6,opt,name=role_scope_id,json=roleScopeId,proto3" json:"role_scope_id,omitempty" gorm:"default:null"` + // scoped_principal_id of the principal + // @inject_tag: `gorm:"default:null"` + ScopedPrincipalId string `protobuf:"bytes,7,opt,name=scoped_principal_id,json=scopedPrincipalId,proto3" json:"scoped_principal_id,omitempty" gorm:"default:null"` } func (x *PrincipalRoleView) Reset() { @@ -264,9 +250,23 @@ func (x *PrincipalRoleView) GetPrincipalId() string { return "" } -func (x *PrincipalRoleView) GetScopeId() string { +func (x *PrincipalRoleView) GetPrincipalScopeId() string { if x != nil { - return x.ScopeId + return x.PrincipalScopeId + } + return "" +} + +func (x *PrincipalRoleView) GetRoleScopeId() string { + if x != nil { + return x.RoleScopeId + } + return "" +} + +func (x *PrincipalRoleView) GetScopedPrincipalId() string { + if x != nil { + return x.ScopedPrincipalId } return "" } @@ -285,7 +285,7 @@ var file_controller_storage_iam_store_v1_principal_role_proto_rawDesc = []byte{ 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, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x93, 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, @@ -294,37 +294,40 @@ var file_controller_storage_iam_store_v1_principal_role_proto_rawDesc = []byte{ 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, + 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x49, 0x64, 0x22, 0x94, 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, 0x22, 0xb2, 0x02, 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, 0x2c, 0x0a, 0x12, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, + 0x61, 0x6c, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x10, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x53, 0x63, 0x6f, 0x70, + 0x65, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x6f, 0x6c, 0x65, + 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x64, 0x5f, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x64, 0x50, 0x72, 0x69, 0x6e, + 0x63, 0x69, 0x70, 0x61, 0x6c, 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 ( diff --git a/internal/iam/store/role.pb.go b/internal/iam/store/role.pb.go index 56587999a..3da7e4be2 100644 --- a/internal/iam/store/role.pb.go +++ b/internal/iam/store/role.pb.go @@ -57,6 +57,10 @@ type Role struct { // 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"` + // grant_scope_id is used for delegating access; it defines a scope other than + // the role's scope that is used when compiling these grants into an ACL + // @inject_tag: `gorm:"default:null"` + GrantScopeId string `protobuf:"bytes,11,opt,name=grant_scope_id,json=grantScopeId,proto3" json:"grant_scope_id,omitempty" gorm:"default:null"` } func (x *Role) Reset() { @@ -147,6 +151,13 @@ func (x *Role) GetVersion() uint32 { return 0 } +func (x *Role) GetGrantScopeId() string { + if x != nil { + return x.GrantScopeId + } + return "" +} + var File_controller_storage_iam_store_v1_role_proto protoreflect.FileDescriptor var file_controller_storage_iam_store_v1_role_proto_rawDesc = []byte{ @@ -160,7 +171,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, 0xc4, 0x02, 0x0a, 0x04, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xea, 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, @@ -181,11 +192,13 @@ var file_controller_storage_iam_store_v1_role_proto_rawDesc = []byte{ 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 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, + 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x67, 0x72, 0x61, 0x6e, + 0x74, 0x53, 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 ( diff --git a/internal/iam/store/role_grant.pb.go b/internal/iam/store/role_grant.pb.go new file mode 100644 index 000000000..4cab7e29e --- /dev/null +++ b/internal/iam/store/role_grant.pb.go @@ -0,0 +1,205 @@ +// 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/role_grant.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 RoleGrant 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 ID of the role this is a part of + // @inject_tag: gorm:"primary_key" + RoleId string `protobuf:"bytes,2,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty" gorm:"primary_key"` + // raw_grant is the string grant value as provided by the user + // @inject_tag: `gorm:"default:null"` + RawGrant string `protobuf:"bytes,3,opt,name=raw_grant,json=rawGrant,proto3" json:"raw_grant,omitempty" gorm:"default:null"` + // canonical_grant is the canonical string representation of the grant value. + // We use this as the unique constraint. + // @inject_tag: gorm:"primary_key" + CanonicalGrant string `protobuf:"bytes,4,opt,name=canonical_grant,json=canonicalGrant,proto3" json:"canonical_grant,omitempty" gorm:"primary_key"` +} + +func (x *RoleGrant) Reset() { + *x = RoleGrant{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_iam_store_v1_role_grant_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RoleGrant) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoleGrant) ProtoMessage() {} + +func (x *RoleGrant) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_role_grant_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 RoleGrant.ProtoReflect.Descriptor instead. +func (*RoleGrant) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_role_grant_proto_rawDescGZIP(), []int{0} +} + +func (x *RoleGrant) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *RoleGrant) GetRoleId() string { + if x != nil { + return x.RoleId + } + return "" +} + +func (x *RoleGrant) GetRawGrant() string { + if x != nil { + return x.RawGrant + } + return "" +} + +func (x *RoleGrant) GetCanonicalGrant() string { + if x != nil { + return x.CanonicalGrant + } + return "" +} + +var File_controller_storage_iam_store_v1_role_grant_proto protoreflect.FileDescriptor + +var file_controller_storage_iam_store_v1_role_grant_proto_rawDesc = []byte{ + 0x0a, 0x30, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x1f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x69, 0x61, 0x6d, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, + 0x2e, 0x76, 0x31, 0x1a, 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, 0xb7, 0x01, 0x0a, 0x09, 0x52, 0x6f, 0x6c, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 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, 0x1b, 0x0a, 0x09, 0x72, 0x61, 0x77, 0x5f, 0x67, 0x72, 0x61, + 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x61, 0x77, 0x47, 0x72, 0x61, + 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x5f, + 0x67, 0x72, 0x61, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x61, 0x6e, + 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x47, 0x72, 0x61, 0x6e, 0x74, 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_role_grant_proto_rawDescOnce sync.Once + file_controller_storage_iam_store_v1_role_grant_proto_rawDescData = file_controller_storage_iam_store_v1_role_grant_proto_rawDesc +) + +func file_controller_storage_iam_store_v1_role_grant_proto_rawDescGZIP() []byte { + file_controller_storage_iam_store_v1_role_grant_proto_rawDescOnce.Do(func() { + file_controller_storage_iam_store_v1_role_grant_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_iam_store_v1_role_grant_proto_rawDescData) + }) + return file_controller_storage_iam_store_v1_role_grant_proto_rawDescData +} + +var file_controller_storage_iam_store_v1_role_grant_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_storage_iam_store_v1_role_grant_proto_goTypes = []interface{}{ + (*RoleGrant)(nil), // 0: controller.storage.iam.store.v1.RoleGrant + (*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_iam_store_v1_role_grant_proto_depIdxs = []int32{ + 1, // 0: controller.storage.iam.store.v1.RoleGrant.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_controller_storage_iam_store_v1_role_grant_proto_init() } +func file_controller_storage_iam_store_v1_role_grant_proto_init() { + if File_controller_storage_iam_store_v1_role_grant_proto != nil { + return + } + file_controller_storage_iam_store_v1_scope_proto_init() + if !protoimpl.UnsafeEnabled { + file_controller_storage_iam_store_v1_role_grant_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RoleGrant); 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_role_grant_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_iam_store_v1_role_grant_proto_goTypes, + DependencyIndexes: file_controller_storage_iam_store_v1_role_grant_proto_depIdxs, + MessageInfos: file_controller_storage_iam_store_v1_role_grant_proto_msgTypes, + }.Build() + File_controller_storage_iam_store_v1_role_grant_proto = out.File + file_controller_storage_iam_store_v1_role_grant_proto_rawDesc = nil + file_controller_storage_iam_store_v1_role_grant_proto_goTypes = nil + file_controller_storage_iam_store_v1_role_grant_proto_depIdxs = nil +} diff --git a/internal/iam/testing.go b/internal/iam/testing.go index 391d3cbda..4077d107c 100644 --- a/internal/iam/testing.go +++ b/internal/iam/testing.go @@ -88,7 +88,7 @@ func testPublicId(t *testing.T, prefix string) string { } // TestUser creates a user suitable for testing. -func TestUser(t *testing.T, conn *gorm.DB, orgId string, opt ...Option) *User { +func TestUser(t *testing.T, conn *gorm.DB, scopeId string, opt ...Option) *User { t.Helper() require := assert.New(t) rw := db.New(conn) @@ -96,7 +96,7 @@ func TestUser(t *testing.T, conn *gorm.DB, orgId string, opt ...Option) *User { repo, err := NewRepository(rw, rw, wrapper) require.NoError(err) - user, err := NewUser(orgId, opt...) + user, err := NewUser(scopeId, opt...) require.NoError(err) user, err = repo.CreateUser(context.Background(), user) require.NoError(err) @@ -125,6 +125,18 @@ func TestRole(t *testing.T, conn *gorm.DB, scopeId string, opt ...Option) *Role return role } +func TestRoleGrant(t *testing.T, conn *gorm.DB, roleId, grant string, opt ...Option) *RoleGrant { + t.Helper() + require := require.New(t) + rw := db.New(conn) + + g, err := NewRoleGrant(roleId, grant, opt...) + require.NoError(err) + err = rw.Create(context.Background(), g) + require.NoError(err) + return g +} + // TestGroup creates a group suitable for testing. func TestGroup(t *testing.T, conn *gorm.DB, scopeId string, opt ...Option) *Group { t.Helper() @@ -142,28 +154,28 @@ 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 { +func TestUserRole(t *testing.T, conn *gorm.DB, roleId, userId string, opt ...Option) *UserRole { t.Helper() require := require.New(t) rw := db.New(conn) - r, err := NewUserRole(scopeId, roleId, userId, opt...) + r, err := NewUserRole(roleId, userId, opt...) require.NoError(err) err = rw.Create(context.Background(), r) require.NoError(err) - return r.(*UserRole) + return r } -func TestGroupRole(t *testing.T, conn *gorm.DB, scopeId, roleId, grpId string, opt ...Option) *GroupRole { +func TestGroupRole(t *testing.T, conn *gorm.DB, roleId, grpId string, opt ...Option) *GroupRole { t.Helper() require := require.New(t) rw := db.New(conn) - r, err := NewGroupRole(scopeId, roleId, grpId, opt...) + r, err := NewGroupRole(roleId, grpId, opt...) require.NoError(err) err = rw.Create(context.Background(), r) require.NoError(err) - return r.(*GroupRole) + return r } // testAuthAccount is a temporary test function. TODO - replace with an auth diff --git a/internal/iam/testing_test.go b/internal/iam/testing_test.go index a8136cceb..057a2270d 100644 --- a/internal/iam/testing_test.go +++ b/internal/iam/testing_test.go @@ -86,24 +86,52 @@ func Test_TestRole(t *testing.T) { assert.NotEmpty(projRole.PublicId) } +func Test_TestRoleGrant(t *testing.T) { + t.Helper() + require := require.New(t) + conn, _ := db.TestSetup(t, "postgres") + _, proj := TestScopes(t, conn) + projRole := TestRole(t, conn, proj.PublicId) + + grant := TestRoleGrant(t, conn, projRole.PublicId, "actions=*;id=*") + require.NotNil(grant) + require.Equal(projRole.PublicId, grant.RoleId) + require.Equal("actions=*;id=*", grant.RawGrant) + require.Equal("id=*;actions=*", grant.CanonicalGrant) +} + func Test_TestUserRole(t *testing.T) { t.Helper() require := require.New(t) conn, _ := db.TestSetup(t, "postgres") org, proj := TestScopes(t, conn) + org2, proj2 := TestScopes(t, conn) + orgRole := TestRole(t, conn, org.PublicId) projRole := TestRole(t, conn, proj.PublicId) + org2Role := TestRole(t, conn, org2.PublicId) + proj2Role := TestRole(t, conn, proj2.PublicId) user := TestUser(t, conn, org.PublicId) - userRole := TestUserRole(t, conn, org.PublicId, orgRole.PublicId, user.PublicId) + userRole := TestUserRole(t, conn, 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) + userRole = TestUserRole(t, conn, projRole.PublicId, user.PublicId) require.NotNil(userRole) require.Equal(projRole.PublicId, userRole.RoleId) require.Equal(user.PublicId, userRole.PrincipalId) + + userRole = TestUserRole(t, conn, org2Role.PublicId, user.PublicId) + require.NotNil(userRole) + require.Equal(org2Role.PublicId, userRole.RoleId) + require.Equal(user.PublicId, userRole.PrincipalId) + + userRole = TestUserRole(t, conn, proj2Role.PublicId, user.PublicId) + require.NotNil(userRole) + require.Equal(proj2Role.PublicId, userRole.RoleId) + require.Equal(user.PublicId, userRole.PrincipalId) } func Test_TestGroupRole(t *testing.T) { @@ -111,19 +139,33 @@ func Test_TestGroupRole(t *testing.T) { require := require.New(t) conn, _ := db.TestSetup(t, "postgres") org, proj := TestScopes(t, conn) + org2, proj2 := TestScopes(t, conn) + orgRole := TestRole(t, conn, org.PublicId) orgGroup := TestGroup(t, conn, org.PublicId) + org2Group := TestGroup(t, conn, org2.PublicId) projRole := TestRole(t, conn, proj.PublicId) projGroup := TestGroup(t, conn, proj.PublicId) + proj2Group := TestGroup(t, conn, proj2.PublicId) - groupRole := TestGroupRole(t, conn, org.PublicId, orgRole.PublicId, orgGroup.PublicId) + groupRole := TestGroupRole(t, conn, 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) + groupRole = TestGroupRole(t, conn, projRole.PublicId, projGroup.PublicId) require.NotNil(groupRole) require.Equal(projRole.PublicId, groupRole.RoleId) require.Equal(projGroup.PublicId, groupRole.PrincipalId) + + groupRole = TestGroupRole(t, conn, orgRole.PublicId, org2Group.PublicId) + require.NotNil(groupRole) + require.Equal(orgRole.PublicId, groupRole.RoleId) + require.Equal(org2Group.PublicId, groupRole.PrincipalId) + + groupRole = TestGroupRole(t, conn, projRole.PublicId, proj2Group.PublicId) + require.NotNil(groupRole) + require.Equal(projRole.PublicId, groupRole.RoleId) + require.Equal(proj2Group.PublicId, groupRole.PrincipalId) } diff --git a/internal/iam/user.go b/internal/iam/user.go index 27cb248df..0c67c0028 100644 --- a/internal/iam/user.go +++ b/internal/iam/user.go @@ -26,16 +26,16 @@ var _ db.VetForWriter = (*User)(nil) // NewUser creates a new in memory user and allows options: // WithName - to specify the user's friendly name and WithDescription - to // specify a user description -func NewUser(organizationPublicId string, opt ...Option) (*User, error) { +func NewUser(scopeId string, opt ...Option) (*User, error) { opts := getOpts(opt...) - if organizationPublicId == "" { - return nil, fmt.Errorf("new user: missing organization id %w", db.ErrInvalidParameter) + if scopeId == "" { + return nil, fmt.Errorf("new user: missing scope id %w", db.ErrInvalidParameter) } u := &User{ User: &store.User{ Name: opts.withName, Description: opts.withDescription, - ScopeId: organizationPublicId, + ScopeId: scopeId, }, } return u, nil diff --git a/internal/iam/user_test.go b/internal/iam/user_test.go index 4ef750920..bedb65e0c 100644 --- a/internal/iam/user_test.go +++ b/internal/iam/user_test.go @@ -54,7 +54,7 @@ func TestNewUser(t *testing.T) { opt: []Option{WithName(id)}, }, wantErr: true, - wantErrMsg: "new user: missing organization id invalid parameter", + wantErrMsg: "new user: missing scope id invalid parameter", }, } for _, tt := range tests { @@ -62,7 +62,7 @@ func TestNewUser(t *testing.T) { assert, require := assert.New(t), require.New(t) got, err := NewUser(tt.args.organizationPublicId, tt.args.opt...) if tt.wantErr { - assert.Error(err) + require.Error(err) assert.Equal(tt.wantErrMsg, err.Error()) return } @@ -73,6 +73,21 @@ func TestNewUser(t *testing.T) { } } +func Test_UserHardcoded(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + assert, require := assert.New(t), require.New(t) + w := db.New(conn) + for _, v := range []string{"u_anon", "u_auth"} { + foundUser := allocUser() + foundUser.PublicId = v + err := w.LookupByPublicId(context.Background(), &foundUser) + require.NoError(err) + assert.Equal("global", foundUser.ScopeId) + assert.Equal(v, foundUser.PublicId) + } +} + func Test_UserCreate(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") @@ -106,7 +121,19 @@ func Test_UserCreate(t *testing.T) { user.PublicId = id err = w.Create(context.Background(), user) require.Error(err) - assert.Equal("create: vet for write failed scope is not found", err.Error()) + assert.Equal("create: vet for write failed: scope is not found", err.Error()) + }) + t.Run("bad-scope", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w := db.New(conn) + user, err := NewUser("global") + require.NoError(err) + id, err := newUserId() + require.NoError(err) + user.PublicId = id + err = w.Create(context.Background(), user) + require.Error(err) + assert.Equal("create: vet for write failed: global not a valid scope type for this resource", err.Error()) }) } @@ -152,7 +179,7 @@ func Test_UserUpdate(t *testing.T) { ScopeId: proj.PublicId, }, wantErr: true, - wantErrMsg: "update: vet for write failed not allowed to change a resource's scope", + wantErrMsg: "update: vet for write failed: not allowed to change a resource's scope", }, { name: "proj-scope-id-not-in-mask", @@ -183,7 +210,7 @@ func Test_UserUpdate(t *testing.T) { }, wantErr: true, wantDup: true, - wantErrMsg: `update: failed pq: duplicate key value violates unique constraint "iam_user_name_scope_id_key"`, + wantErrMsg: `update: failed: pq: duplicate key value violates unique constraint "iam_user_name_scope_id_key"`, }, } for _, tt := range tests { @@ -251,6 +278,18 @@ func Test_UserDelete(t *testing.T) { wantErr: false, wantRowsDeleted: 0, }, + { + name: "anon-user", + user: func() *User { u := allocUser(); u.PublicId = "u_anon"; return &u }(), + wantErr: true, + wantRowsDeleted: 0, + }, + { + name: "auth-user", + user: func() *User { u := allocUser(); u.PublicId = "u_auth"; return &u }(), + wantErr: true, + wantRowsDeleted: 0, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/perms/acl_test.go b/internal/perms/acl_test.go index d0f7766ee..154ae4798 100644 --- a/internal/perms/acl_test.go +++ b/internal/perms/acl_test.go @@ -44,7 +44,7 @@ func Test_ACLAllowed(t *testing.T) { grants: []string{ "type=host-set;actions=list,create", "type=host;actions=*", - "id=*;actions=authen", + "id=*;actions=authenticate", }, }, { @@ -179,7 +179,7 @@ func Test_ACLAllowed(t *testing.T) { scopeGrants: commonGrants, actionsAllowed: []actionAllowed{ {action: action.List}, - {action: action.Authen, allowed: true}, + {action: action.Authenticate, allowed: true}, {action: action.Delete}, }, }, @@ -189,7 +189,7 @@ func Test_ACLAllowed(t *testing.T) { scopeGrants: commonGrants, actionsAllowed: []actionAllowed{ {action: action.List}, - {action: action.Authen}, + {action: action.Authenticate}, {action: action.Delete}, }, userId: "u_abcd1234", diff --git a/internal/perms/grants_test.go b/internal/perms/grants_test.go index 5b0a5f33a..d11f013d7 100644 --- a/internal/perms/grants_test.go +++ b/internal/perms/grants_test.go @@ -54,24 +54,24 @@ func Test_ActionParsingValidation(t *testing.T) { { name: "all valid plus all", input: Grant{ - actionsBeingParsed: []string{"list", "create", "update", "*", "read", "delete", "authen", "connect"}, + actionsBeingParsed: []string{"list", "create", "update", "*", "read", "delete", "authenticate", "connect"}, }, errResult: `"*" cannot be specified with other actions`, }, { name: "all valid", input: Grant{ - actionsBeingParsed: []string{"list", "create", "update", "read", "delete", "authen", "connect"}, + actionsBeingParsed: []string{"list", "create", "update", "read", "delete", "authenticate", "connect"}, }, result: Grant{ actions: map[action.Type]bool{ - action.List: true, - action.Create: true, - action.Update: true, - action.Read: true, - action.Delete: true, - action.Authen: true, - action.Connect: true, + action.List: true, + action.Create: true, + action.Update: true, + action.Read: true, + action.Delete: true, + action.Authenticate: true, + action.Connect: true, }, }, }, diff --git a/internal/proto/local/controller/api/resources/roles/v1/role.proto b/internal/proto/local/controller/api/resources/roles/v1/role.proto index 199791b69..d5af6e09b 100644 --- a/internal/proto/local/controller/api/resources/roles/v1/role.proto +++ b/internal/proto/local/controller/api/resources/roles/v1/role.proto @@ -30,18 +30,25 @@ message Role { // Whether the resource is disabled google.protobuf.BoolValue disabled = 6; + // The scope the grants will apply to. If the role is at the global scope, + // this can be an organization or project. If the role is at an organization + // scope, this can be a project within the organization. It is invalid for + // this to be anything other than the role's scope when the role's scope is + // a project. + google.protobuf.StringValue grant_scope_id = 7 [json_name="grant_scope_id"]; + // The version can be used in subsiquent write requests to ensure this resource // has not changed and to fail the write if it has. // Output only. - uint32 version = 7; + uint32 version = 8; // The principals that are assigned this role. // Output only. - repeated string user_ids = 8; + repeated string user_ids = 9; // Output only. - repeated string group_ids = 9; + repeated string group_ids = 10; // The grants that this role provides for it's principals. // Output only. - repeated string grants = 10; + repeated string grants = 11; } 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 index f6b6af94c..a9e2b554a 100644 --- a/internal/proto/local/controller/storage/iam/store/v1/principal_role.proto +++ b/internal/proto/local/controller/storage/iam/store/v1/principal_role.proto @@ -19,10 +19,6 @@ message UserRole { // 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 { @@ -37,10 +33,6 @@ message GroupRole { // 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 { @@ -58,7 +50,15 @@ message PrincipalRoleView { // @inject_tag: gorm:"primary_key" string principal_id = 4; - // scope_id of the principal role + // principal_scope_id of the principal // @inject_tag: `gorm:"default:null"` - string scope_id = 5; + string principal_scope_id = 5; + + // role_scope_id of the role + // @inject_tag: `gorm:"default:null"` + string role_scope_id = 6; + + // scoped_principal_id of the principal + // @inject_tag: `gorm:"default:null"` + string scoped_principal_id = 7; } \ 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 cfd4686fb..666634783 100644 --- a/internal/proto/local/controller/storage/iam/store/v1/role.proto +++ b/internal/proto/local/controller/storage/iam/store/v1/role.proto @@ -40,4 +40,9 @@ message Role { // itself and when modifying dependent items like principal roles. // @inject_tag: `gorm:"default:null"` uint32 version = 10; + + // grant_scope_id is used for delegating access; it defines a scope other than + // the role's scope that is used when compiling these grants into an ACL + // @inject_tag: `gorm:"default:null"` + string grant_scope_id = 11; } diff --git a/internal/proto/local/controller/storage/iam/store/v1/role_grant.proto b/internal/proto/local/controller/storage/iam/store/v1/role_grant.proto new file mode 100644 index 000000000..70051d709 --- /dev/null +++ b/internal/proto/local/controller/storage/iam/store/v1/role_grant.proto @@ -0,0 +1,26 @@ +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 RoleGrant { + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 1; + + // role_id is the ID of the role this is a part of + // @inject_tag: gorm:"primary_key" + string role_id = 2; + + // raw_grant is the string grant value as provided by the user + // @inject_tag: `gorm:"default:null"` + string raw_grant = 3; + + // canonical_grant is the canonical string representation of the grant value. + // We use this as the unique constraint. + // @inject_tag: gorm:"primary_key" + string canonical_grant = 4; +} diff --git a/internal/servers/controller/handlers/roles/role_service.go b/internal/servers/controller/handlers/roles/role_service.go index c7be9b2d9..aab49c8bc 100644 --- a/internal/servers/controller/handlers/roles/role_service.go +++ b/internal/servers/controller/handlers/roles/role_service.go @@ -282,6 +282,9 @@ func (s Service) addPrinciplesInRepo(ctx context.Context, roleId string, userIds return nil, status.Errorf(codes.Internal, "Unable to add principles to role: %v.", err) } out, pr, err := repo.LookupRole(ctx, roleId) + if err != nil { + return nil, status.Errorf(codes.Internal, "Unable to look up role: %v.", err) + } if out == nil { return nil, status.Error(codes.Internal, "Unable to lookup role after adding principles to it.") } @@ -298,6 +301,9 @@ func (s Service) setPrinciplesInRepo(ctx context.Context, roleId string, userIds return nil, status.Errorf(codes.Internal, "Unable to set principles on role: %v.", err) } out, pr, err := repo.LookupRole(ctx, roleId) + if err != nil { + return nil, status.Errorf(codes.Internal, "Unable to look up role: %v.", err) + } if out == nil { return nil, status.Error(codes.Internal, "Unable to lookup role after setting principles for it.") } @@ -314,6 +320,9 @@ func (s Service) removePrinciplesInRepo(ctx context.Context, roleId string, user return nil, status.Errorf(codes.Internal, "Unable to remove principles from role: %v.", err) } out, pr, err := repo.LookupRole(ctx, roleId) + if err != nil { + return nil, status.Errorf(codes.Internal, "Unable to look up role: %v.", err) + } if out == nil { return nil, status.Error(codes.Internal, "Unable to lookup role after removing principles from it.") } diff --git a/internal/servers/controller/handlers/roles/role_service_test.go b/internal/servers/controller/handlers/roles/role_service_test.go index a381ee9fb..ba1a4d614 100644 --- a/internal/servers/controller/handlers/roles/role_service_test.go +++ b/internal/servers/controller/handlers/roles/role_service_test.go @@ -807,24 +807,24 @@ func TestAddPrincipal(t *testing.T) { orWithUser := iam.TestRole(t, conn, o.GetPublicId()) assignedUser := iam.TestUser(t, conn, o.GetPublicId()) - _ = iam.TestUserRole(t, conn, o.GetPublicId(), orWithUser.GetPublicId(), assignedUser.GetPublicId()) + _ = iam.TestUserRole(t, conn, o.GetPublicId(), orWithUser.GetPublicId(), iam.WithPublicId(assignedUser.GetPublicId())) ou1 := iam.TestUser(t, conn, o.GetPublicId()) ou2 := iam.TestUser(t, conn, o.GetPublicId()) orWithGroup := iam.TestRole(t, conn, o.GetPublicId()) assignedOG := iam.TestGroup(t, conn, o.GetPublicId()) - _ = iam.TestGroupRole(t, conn, o.GetPublicId(), orWithGroup.GetPublicId(), assignedOG.GetPublicId()) + _ = iam.TestGroupRole(t, conn, o.GetPublicId(), orWithGroup.GetPublicId(), iam.WithPublicId(assignedOG.GetPublicId())) og1 := iam.TestGroup(t, conn, o.GetPublicId()) og2 := iam.TestGroup(t, conn, o.GetPublicId()) prWithUser := iam.TestRole(t, conn, p.GetPublicId()) - _ = iam.TestUserRole(t, conn, p.GetPublicId(), prWithUser.GetPublicId(), assignedUser.GetPublicId()) + _ = iam.TestUserRole(t, conn, p.GetPublicId(), prWithUser.GetPublicId(), iam.WithPublicId(assignedUser.GetPublicId())) prWithGroup := iam.TestRole(t, conn, p.GetPublicId()) assignedPG := iam.TestGroup(t, conn, p.GetPublicId()) - _ = iam.TestGroupRole(t, conn, p.GetPublicId(), prWithGroup.GetPublicId(), assignedPG.GetPublicId()) + _ = iam.TestGroupRole(t, conn, p.GetPublicId(), prWithGroup.GetPublicId(), iam.WithPublicId(assignedPG.GetPublicId())) pg1 := iam.TestGroup(t, conn, p.GetPublicId()) pg2 := iam.TestGroup(t, conn, p.GetPublicId()) @@ -1060,24 +1060,24 @@ func TestSetPrincipal(t *testing.T) { orWithUser := iam.TestRole(t, conn, o.GetPublicId()) assignedUser := iam.TestUser(t, conn, o.GetPublicId()) - _ = iam.TestUserRole(t, conn, o.GetPublicId(), orWithUser.GetPublicId(), assignedUser.GetPublicId()) + _ = iam.TestUserRole(t, conn, o.GetPublicId(), orWithUser.GetPublicId(), iam.WithPublicId(assignedUser.GetPublicId())) ou1 := iam.TestUser(t, conn, o.GetPublicId()) ou2 := iam.TestUser(t, conn, o.GetPublicId()) orWithGroup := iam.TestRole(t, conn, o.GetPublicId()) assignedOG := iam.TestGroup(t, conn, o.GetPublicId()) - _ = iam.TestGroupRole(t, conn, o.GetPublicId(), orWithGroup.GetPublicId(), assignedOG.GetPublicId()) + _ = iam.TestGroupRole(t, conn, o.GetPublicId(), orWithGroup.GetPublicId(), iam.WithPublicId(assignedOG.GetPublicId())) og1 := iam.TestGroup(t, conn, o.GetPublicId()) og2 := iam.TestGroup(t, conn, o.GetPublicId()) prWithUser := iam.TestRole(t, conn, p.GetPublicId()) - _ = iam.TestUserRole(t, conn, p.GetPublicId(), prWithUser.GetPublicId(), assignedUser.GetPublicId()) + _ = iam.TestUserRole(t, conn, p.GetPublicId(), prWithUser.GetPublicId(), iam.WithPublicId(assignedUser.GetPublicId())) prWithGroup := iam.TestRole(t, conn, p.GetPublicId()) assignedPG := iam.TestGroup(t, conn, p.GetPublicId()) - _ = iam.TestGroupRole(t, conn, p.GetPublicId(), prWithGroup.GetPublicId(), assignedPG.GetPublicId()) + _ = iam.TestGroupRole(t, conn, p.GetPublicId(), prWithGroup.GetPublicId(), iam.WithPublicId(assignedPG.GetPublicId())) pg1 := iam.TestGroup(t, conn, p.GetPublicId()) pg2 := iam.TestGroup(t, conn, p.GetPublicId()) @@ -1315,20 +1315,20 @@ func TestRemovePrincipal(t *testing.T) { ou1 := iam.TestUser(t, conn, o.GetPublicId()) ou2 := iam.TestUser(t, conn, o.GetPublicId()) - _ = iam.TestUserRole(t, conn, o.GetPublicId(), orgUserRoles.GetPublicId(), ou1.GetPublicId()) - _ = iam.TestUserRole(t, conn, o.GetPublicId(), orgUserRoles.GetPublicId(), ou2.GetPublicId()) - _ = iam.TestUserRole(t, conn, p.GetPublicId(), projUserRoles.GetPublicId(), ou1.GetPublicId()) - _ = iam.TestUserRole(t, conn, p.GetPublicId(), projUserRoles.GetPublicId(), ou2.GetPublicId()) + _ = iam.TestUserRole(t, conn, o.GetPublicId(), orgUserRoles.GetPublicId(), iam.WithPublicId(ou1.GetPublicId())) + _ = iam.TestUserRole(t, conn, o.GetPublicId(), orgUserRoles.GetPublicId(), iam.WithPublicId(ou2.GetPublicId())) + _ = iam.TestUserRole(t, conn, p.GetPublicId(), projUserRoles.GetPublicId(), iam.WithPublicId(ou1.GetPublicId())) + _ = iam.TestUserRole(t, conn, p.GetPublicId(), projUserRoles.GetPublicId(), iam.WithPublicId(ou2.GetPublicId())) og1 := iam.TestGroup(t, conn, o.GetPublicId()) og2 := iam.TestGroup(t, conn, o.GetPublicId()) - _ = iam.TestGroupRole(t, conn, o.GetPublicId(), orgGroupRoles.GetPublicId(), og1.GetPublicId()) - _ = iam.TestGroupRole(t, conn, o.GetPublicId(), orgGroupRoles.GetPublicId(), og2.GetPublicId()) + _ = iam.TestGroupRole(t, conn, o.GetPublicId(), orgGroupRoles.GetPublicId(), iam.WithPublicId(og1.GetPublicId())) + _ = iam.TestGroupRole(t, conn, o.GetPublicId(), orgGroupRoles.GetPublicId(), iam.WithPublicId(og2.GetPublicId())) pg1 := iam.TestGroup(t, conn, p.GetPublicId()) pg2 := iam.TestGroup(t, conn, p.GetPublicId()) - _ = iam.TestGroupRole(t, conn, p.GetPublicId(), projGroupRoles.GetPublicId(), pg1.GetPublicId()) - _ = iam.TestGroupRole(t, conn, p.GetPublicId(), projGroupRoles.GetPublicId(), pg2.GetPublicId()) + _ = iam.TestGroupRole(t, conn, p.GetPublicId(), projGroupRoles.GetPublicId(), iam.WithPublicId(pg1.GetPublicId())) + _ = iam.TestGroupRole(t, conn, p.GetPublicId(), projGroupRoles.GetPublicId(), iam.WithPublicId(pg2.GetPublicId())) s, err := roles.NewService(repoFn) require.NoError(t, err, "Error when getting new role service.") diff --git a/internal/types/action/action.go b/internal/types/action/action.go index 900245185..e4fc9aaf9 100644 --- a/internal/types/action/action.go +++ b/internal/types/action/action.go @@ -6,27 +6,39 @@ type Type int // not using iota intentionally, since the values are stored in the db as well. const ( - Unknown Type = 0 - List Type = 1 - Create Type = 2 - Update Type = 3 - Read Type = 4 - Delete Type = 5 - Authen Type = 6 - All Type = 7 - Connect Type = 8 + Unknown Type = 0 + List Type = 1 + Create Type = 2 + Update Type = 3 + Read Type = 4 + Delete Type = 5 + Authenticate Type = 6 + All Type = 7 + Connect Type = 8 + AddGrants Type = 9 + RemoveGrants Type = 10 + SetGrants Type = 11 + AddPrincipals Type = 12 + SetPrincipals Type = 13 + RemovePrincipals Type = 14 ) var Map = map[string]Type{ - "unknown": Unknown, - "list": List, - "create": Create, - "update": Update, - "read": Read, - "delete": Delete, - "authen": Authen, - "*": All, - "connect": Connect, + "unknown": Unknown, + "list": List, + "create": Create, + "update": Update, + "read": Read, + "delete": Delete, + "authenticate": Authenticate, + "*": All, + "connect": Connect, + "add-grants": AddGrants, + "remove-grants": RemoveGrants, + "set-grants": SetGrants, + "add-principals": AddPrincipals, + "set-principals": SetPrincipals, + "remove-principals": RemovePrincipals, } func (a Type) String() string { @@ -37,8 +49,14 @@ func (a Type) String() string { "update", "read", "delete", - "authen", + "authenticate", "*", "connect", + "add-grants", + "remove-grants", + "set-grants", + "add-principals", + "set-principals", + "remove-principals", }[a] } diff --git a/internal/types/action/action_test.go b/internal/types/action/action_test.go index e0b276866..ab8d81125 100644 --- a/internal/types/action/action_test.go +++ b/internal/types/action/action_test.go @@ -36,8 +36,8 @@ func TestAction(t *testing.T) { want: "delete", }, { - action: Authen, - want: "authen", + action: Authenticate, + want: "authenticate", }, { action: All, @@ -47,6 +47,30 @@ func TestAction(t *testing.T) { action: Connect, want: "connect", }, + { + action: AddGrants, + want: "add-grants", + }, + { + action: RemoveGrants, + want: "remove-grants", + }, + { + action: SetGrants, + want: "set-grants", + }, + { + action: AddPrincipals, + want: "add-principals", + }, + { + action: RemovePrincipals, + want: "remove-principals", + }, + { + action: SetPrincipals, + want: "set-principals", + }, } for _, tt := range tests { t.Run(tt.want, func(t *testing.T) {