Port over 162 and 165 to master (#176)

Co-authored-by: Jim Lambert <jlambert@hashicorp.com>
Co-authored-by: Michael Gaffney <mgaffney@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2020-07-10 15:46:26 -04:00
1 parent 1ddc59f81e
commit bcdcaffa6f
48 files changed
+2791 -772

No files matched your search

+1
View File
@@ -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
+6
View File
@@ -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.
+46 -39
View File
@@ -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)
}
})
}
}
}
+212 -85
View File
@@ -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 <scope_id>:<principal_id> 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
@@ -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;
@@ -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;
@@ -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;
+190 -84
View File
@@ -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 <scope_id>:<principal_id> 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
+11 -11
View File
@@ -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) {
+5 -5
View File
@@ -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())
})
}
+4
View File
@@ -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",
@@ -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() }
+3
View File
@@ -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
+3 -3
View File
@@ -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",
+4 -3
View File
@@ -8,9 +8,10 @@ import (
)
const (
UserPrefix = "u"
GroupPrefix = "g"
RolePrefix = "r"
UserPrefix = "u"
GroupPrefix = "g"
RolePrefix = "r"
RoleGrantPrefix = "rg"
)
func newRoleId() (string, error) {
+20 -3
View File
@@ -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 {
+7
View File
@@ -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)
})
}
+38 -62
View File
@@ -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
}
}
+101 -107
View File
@@ -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)
}
+1 -1
View File
@@ -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,
},
}
+58 -68
View File
@@ -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
}
+123 -89
View File
@@ -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)
+10 -7
View File
@@ -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,
)
+368
View File
@@ -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
}
+441
View File
@@ -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)
}
+3 -2
View File
@@ -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":
+4 -4
View File
@@ -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))
+14 -6
View File
@@ -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.
+124
View File
@@ -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
}
}
+263
View File
@@ -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))
})
}
+118 -23
View File
@@ -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) {
+59 -56
View File
@@ -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 (
+19 -6
View File
@@ -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 (
+205
View File
@@ -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
}
+20 -8
View File
@@ -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
+46 -4
View File
@@ -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)
}
+4 -4
View File
@@ -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
+44 -5
View File
@@ -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) {
+3 -3
View File
@@ -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",
+9 -9
View File
@@ -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,
},
},
},
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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.")
}
@@ -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.")
+37 -19
View File
@@ -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]
}
+26 -2
View File
@@ -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) {