diff --git a/internal/db/migrations/postgres.gen.go b/internal/db/migrations/postgres.gen.go index 8e89a6b27f..32c07e7ef3 100644 --- a/internal/db/migrations/postgres.gen.go +++ b/internal/db/migrations/postgres.gen.go @@ -12,6 +12,7 @@ begin; drop domain wt_timestamp; drop domain wt_public_id; +drop domain wt_scope_id; drop domain wt_version; drop function default_create_time; @@ -35,6 +36,13 @@ check( comment on domain wt_public_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' +); +comment on domain wt_scope_id is +'"global" or random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; + create domain wt_timestamp as timestamp with time zone default current_timestamp; @@ -415,6 +423,7 @@ drop table iam_group cascade; drop table iam_user cascade; drop table iam_scope_project cascade; drop table iam_scope_organization cascade; +drop table iam_scope_global cascade; drop table iam_scope cascade; drop table iam_scope_type_enm cascade; drop table iam_role cascade; @@ -440,12 +449,13 @@ COMMIT; begin; create table iam_scope_type_enm ( - string text not null primary key check(string in ('unknown', 'organization', 'project')) + string text not null primary key check(string in ('unknown', 'global', 'organization', 'project')) ); insert into iam_scope_type_enm (string) values ('unknown'), + ('global'), ('organization'), ('project'); @@ -468,32 +478,55 @@ is 'function used in before update triggers to make scope_id column is immutable'; create table iam_scope ( - public_id wt_public_id primary key, + public_id wt_scope_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, type text not null references iam_scope_type_enm(string) check( ( + type = 'global' + and parent_id is null + ) + or ( type = 'organization' - and parent_id = null + and parent_id = 'global' ) or ( type = 'project' and parent_id is not null + and parent_id != 'global' ) ), description text, parent_id text references iam_scope(public_id) on delete cascade on update cascade ); +create table iam_scope_global ( + scope_id wt_scope_id primary key + references iam_scope(public_id) + on delete cascade + on update cascade + check( + scope_id = 'global' + ), + name text unique +); + create table iam_scope_organization ( - scope_id wt_public_id not null unique references iam_scope(public_id) on delete cascade on update cascade, - name text unique, - primary key(scope_id) - ); + scope_id wt_scope_id primary key + references iam_scope(public_id) + on delete cascade + on update cascade, + parent_id wt_scope_id not null + references iam_scope_global(scope_id) + on delete cascade + on update cascade, + name text, + unique (parent_id, name) +); create table iam_scope_project ( - scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, + scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, parent_id wt_public_id not null references iam_scope_organization(scope_id) on delete cascade on update cascade, name text, unique(parent_id, name), @@ -505,13 +538,19 @@ create or replace function returns trigger as $$ declare parent_type int; -begin - if new.type = 'organization' then - insert into iam_scope_organization (scope_id, name) +begin + if new.type = 'global' then + insert into iam_scope_global (scope_id, name) values (new.public_id, new.name); return new; end if; + if new.type = 'organization' then + insert into iam_scope_organization (scope_id, parent_id, name) + values + (new.public_id, new.parent_id, new.name); + return new; + end if; if new.type = 'project' then insert into iam_scope_project (scope_id, parent_id, name) values @@ -522,13 +561,29 @@ begin end; $$ language plpgsql; - create trigger iam_scope_insert after insert on iam_scope for each row execute procedure iam_sub_scopes_func(); +create or replace function + disallow_global_scope_deletion() + returns trigger +as $$ +begin + if old.type = 'global' then + raise exception 'deletion of global scope not allowed'; + end if; + return old; +end; +$$ language plpgsql; + +create trigger + iam_scope_disallow_global_deletion +before +delete on iam_scope + for each row execute procedure disallow_global_scope_deletion(); create or replace function iam_immutable_scope_type_func() @@ -574,8 +629,12 @@ create or replace function iam_sub_names() returns trigger as $$ -begin +begin if new.name != old.name then + if new.type = 'global' then + update iam_scope_global set name = new.name where scope_id = old.public_id; + return new; + end if; if new.type = 'organization' then update iam_scope_organization set name = new.name where scope_id = old.public_id; return new; @@ -596,13 +655,17 @@ before update on iam_scope for each row execute procedure iam_sub_names(); +insert into iam_scope (public_id, name, type, description) + values ('global', 'global', 'global', 'Global Scope'); + + create table iam_user ( public_id wt_public_id not null primary key, create_time wt_timestamp, update_time wt_timestamp, name text, description text, - scope_id wt_public_id not null references iam_scope_organization(scope_id) on delete cascade on update cascade, + 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, @@ -612,6 +675,25 @@ create table iam_user ( unique(scope_id, public_id) ); +create or replace function + user_scope_id_valid() + returns trigger +as $$ +begin + perform from iam_scope where public_id = new.scope_id and type in ('global', 'organization'); + if not found then + raise exception 'invalid scope type for user creation'; + end if; + return new; +end; +$$ language plpgsql; + +create trigger + ensure_user_scope_id_valid +before +insert or update on iam_user + for each row execute procedure user_scope_id_valid(); + create trigger update_time_column before update on iam_user @@ -640,7 +722,7 @@ create table iam_role ( update_time wt_timestamp, name text, description text, - scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, + 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 @@ -682,7 +764,7 @@ create table iam_group ( update_time wt_timestamp, name text, description text, - scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, + 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, -- add unique index so a composite fk can be declared. @@ -719,7 +801,7 @@ update on iam_group -- with a before update trigger using iam_immutable_role(). create table iam_user_role ( create_time wt_timestamp, - scope_id wt_public_id not null, + 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), @@ -736,7 +818,7 @@ create table iam_user_role ( -- update trigger using iam_immutable_role(). create table iam_group_role ( create_time wt_timestamp, - scope_id wt_public_id not null, + 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), @@ -880,7 +962,7 @@ begin; -- base table for auth methods create table auth_method ( public_id wt_public_id primary key, - scope_id wt_public_id not null + scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, @@ -896,7 +978,7 @@ begin; create table auth_account ( public_id wt_public_id primary key, auth_method_id wt_public_id not null, - scope_id wt_public_id not null, + scope_id wt_scope_id not null, iam_user_id wt_public_id, foreign key (scope_id, auth_method_id) references auth_method (scope_id, public_id) @@ -962,7 +1044,7 @@ begin; create table static_host_catalog ( public_id wt_public_id primary key, - scope_id wt_public_id not null + scope_id wt_scope_id not null references iam_scope (public_id) on delete cascade on update cascade, diff --git a/internal/db/migrations/postgres/01_domain_types.down.sql b/internal/db/migrations/postgres/01_domain_types.down.sql index f48bbb2175..b8fb53ec5a 100644 --- a/internal/db/migrations/postgres/01_domain_types.down.sql +++ b/internal/db/migrations/postgres/01_domain_types.down.sql @@ -2,6 +2,7 @@ begin; drop domain wt_timestamp; drop domain wt_public_id; +drop domain wt_scope_id; drop domain wt_version; drop function default_create_time; diff --git a/internal/db/migrations/postgres/01_domain_types.up.sql b/internal/db/migrations/postgres/01_domain_types.up.sql index 14ef65f12b..d6e2dea18c 100644 --- a/internal/db/migrations/postgres/01_domain_types.up.sql +++ b/internal/db/migrations/postgres/01_domain_types.up.sql @@ -7,6 +7,13 @@ check( comment on domain wt_public_id is 'Random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; +create domain wt_scope_id as text +check( + length(trim(value)) > 10 or value = 'global' +); +comment on domain wt_scope_id is +'"global" or random ID generated with github.com/hashicorp/vault/sdk/helper/base62'; + create domain wt_timestamp as timestamp with time zone default current_timestamp; diff --git a/internal/db/migrations/postgres/06_iam.down.sql b/internal/db/migrations/postgres/06_iam.down.sql index d504113163..2994f7244a 100644 --- a/internal/db/migrations/postgres/06_iam.down.sql +++ b/internal/db/migrations/postgres/06_iam.down.sql @@ -4,6 +4,7 @@ drop table iam_group cascade; drop table iam_user cascade; drop table iam_scope_project cascade; drop table iam_scope_organization cascade; +drop table iam_scope_global cascade; drop table iam_scope cascade; drop table iam_scope_type_enm cascade; drop table iam_role cascade; diff --git a/internal/db/migrations/postgres/06_iam.up.sql b/internal/db/migrations/postgres/06_iam.up.sql index d07d8bccd0..51acfeb94e 100644 --- a/internal/db/migrations/postgres/06_iam.up.sql +++ b/internal/db/migrations/postgres/06_iam.up.sql @@ -1,12 +1,13 @@ begin; create table iam_scope_type_enm ( - string text not null primary key check(string in ('unknown', 'organization', 'project')) + string text not null primary key check(string in ('unknown', 'global', 'organization', 'project')) ); insert into iam_scope_type_enm (string) values ('unknown'), + ('global'), ('organization'), ('project'); @@ -29,32 +30,55 @@ is 'function used in before update triggers to make scope_id column is immutable'; create table iam_scope ( - public_id wt_public_id primary key, + public_id wt_scope_id primary key, create_time wt_timestamp, update_time wt_timestamp, name text, type text not null references iam_scope_type_enm(string) check( ( + type = 'global' + and parent_id is null + ) + or ( type = 'organization' - and parent_id = null + and parent_id = 'global' ) or ( type = 'project' and parent_id is not null + and parent_id != 'global' ) ), description text, parent_id text references iam_scope(public_id) on delete cascade on update cascade ); +create table iam_scope_global ( + scope_id wt_scope_id primary key + references iam_scope(public_id) + on delete cascade + on update cascade + check( + scope_id = 'global' + ), + name text unique +); + create table iam_scope_organization ( - scope_id wt_public_id not null unique references iam_scope(public_id) on delete cascade on update cascade, - name text unique, - primary key(scope_id) - ); + scope_id wt_scope_id primary key + references iam_scope(public_id) + on delete cascade + on update cascade, + parent_id wt_scope_id not null + references iam_scope_global(scope_id) + on delete cascade + on update cascade, + name text, + unique (parent_id, name) +); create table iam_scope_project ( - scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, + scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, parent_id wt_public_id not null references iam_scope_organization(scope_id) on delete cascade on update cascade, name text, unique(parent_id, name), @@ -66,13 +90,19 @@ create or replace function returns trigger as $$ declare parent_type int; -begin - if new.type = 'organization' then - insert into iam_scope_organization (scope_id, name) +begin + if new.type = 'global' then + insert into iam_scope_global (scope_id, name) values (new.public_id, new.name); return new; end if; + if new.type = 'organization' then + insert into iam_scope_organization (scope_id, parent_id, name) + values + (new.public_id, new.parent_id, new.name); + return new; + end if; if new.type = 'project' then insert into iam_scope_project (scope_id, parent_id, name) values @@ -83,13 +113,29 @@ begin end; $$ language plpgsql; - create trigger iam_scope_insert after insert on iam_scope for each row execute procedure iam_sub_scopes_func(); +create or replace function + disallow_global_scope_deletion() + returns trigger +as $$ +begin + if old.type = 'global' then + raise exception 'deletion of global scope not allowed'; + end if; + return old; +end; +$$ language plpgsql; + +create trigger + iam_scope_disallow_global_deletion +before +delete on iam_scope + for each row execute procedure disallow_global_scope_deletion(); create or replace function iam_immutable_scope_type_func() @@ -135,8 +181,12 @@ create or replace function iam_sub_names() returns trigger as $$ -begin +begin if new.name != old.name then + if new.type = 'global' then + update iam_scope_global set name = new.name where scope_id = old.public_id; + return new; + end if; if new.type = 'organization' then update iam_scope_organization set name = new.name where scope_id = old.public_id; return new; @@ -157,13 +207,17 @@ before update on iam_scope for each row execute procedure iam_sub_names(); +insert into iam_scope (public_id, name, type, description) + values ('global', 'global', 'global', 'Global Scope'); + + create table iam_user ( public_id wt_public_id not null primary key, create_time wt_timestamp, update_time wt_timestamp, name text, description text, - scope_id wt_public_id not null references iam_scope_organization(scope_id) on delete cascade on update cascade, + 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, @@ -173,6 +227,25 @@ create table iam_user ( unique(scope_id, public_id) ); +create or replace function + user_scope_id_valid() + returns trigger +as $$ +begin + perform from iam_scope where public_id = new.scope_id and type in ('global', 'organization'); + if not found then + raise exception 'invalid scope type for user creation'; + end if; + return new; +end; +$$ language plpgsql; + +create trigger + ensure_user_scope_id_valid +before +insert or update on iam_user + for each row execute procedure user_scope_id_valid(); + create trigger update_time_column before update on iam_user @@ -201,7 +274,7 @@ create table iam_role ( update_time wt_timestamp, name text, description text, - scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, + 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 @@ -243,7 +316,7 @@ create table iam_group ( update_time wt_timestamp, name text, description text, - scope_id wt_public_id not null references iam_scope(public_id) on delete cascade on update cascade, + 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, -- add unique index so a composite fk can be declared. @@ -280,7 +353,7 @@ update on iam_group -- with a before update trigger using iam_immutable_role(). create table iam_user_role ( create_time wt_timestamp, - scope_id wt_public_id not null, + 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), @@ -297,7 +370,7 @@ create table iam_user_role ( -- update trigger using iam_immutable_role(). create table iam_group_role ( create_time wt_timestamp, - scope_id wt_public_id not null, + 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), diff --git a/internal/db/migrations/postgres/07_auth.up.sql b/internal/db/migrations/postgres/07_auth.up.sql index 358f9f60e4..ed992b4464 100644 --- a/internal/db/migrations/postgres/07_auth.up.sql +++ b/internal/db/migrations/postgres/07_auth.up.sql @@ -10,7 +10,7 @@ begin; -- base table for auth methods create table auth_method ( public_id wt_public_id primary key, - scope_id wt_public_id not null + scope_id wt_scope_id not null references iam_scope(public_id) on delete cascade on update cascade, @@ -26,7 +26,7 @@ begin; create table auth_account ( public_id wt_public_id primary key, auth_method_id wt_public_id not null, - scope_id wt_public_id not null, + scope_id wt_scope_id not null, iam_user_id wt_public_id, foreign key (scope_id, auth_method_id) references auth_method (scope_id, public_id) diff --git a/internal/db/migrations/postgres/10_static_host.up.sql b/internal/db/migrations/postgres/10_static_host.up.sql index 5d111d5e5a..11dc7b7716 100644 --- a/internal/db/migrations/postgres/10_static_host.up.sql +++ b/internal/db/migrations/postgres/10_static_host.up.sql @@ -2,7 +2,7 @@ begin; create table static_host_catalog ( public_id wt_public_id primary key, - scope_id wt_public_id not null + scope_id wt_scope_id not null references iam_scope (public_id) on delete cascade on update cascade, diff --git a/internal/iam/repository_scope.go b/internal/iam/repository_scope.go index 8f20b95376..61c79b2aff 100644 --- a/internal/iam/repository_scope.go +++ b/internal/iam/repository_scope.go @@ -22,6 +22,12 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, opt ...Option) ( if s.PublicId != "" { return nil, fmt.Errorf("create scope: public id not empty: %w", db.ErrInvalidParameter) } + switch s.Type { + case scope.Unknown.String(): + return nil, fmt.Errorf("create scope: unknown type: %w", db.ErrInvalidParameter) + case scope.Global.String(): + return nil, fmt.Errorf("create scope: invalid type: %w", db.ErrInvalidParameter) + } opts := getOpts(opt...) var publicId string t := scope.StringToScopeType(s.Type) @@ -110,6 +116,9 @@ func (r *Repository) DeleteScope(ctx context.Context, withPublicId string, opt . if withPublicId == "" { return db.NoRowsAffected, fmt.Errorf("delete scope: missing public id %w", db.ErrInvalidParameter) } + if withPublicId == scope.Global.String() { + return db.NoRowsAffected, fmt.Errorf("delete scope: invalid to delete global scope: %w", db.ErrInvalidParameter) + } scope := allocScope() scope.PublicId = withPublicId rowsDeleted, err := r.delete(ctx, &scope) @@ -138,7 +147,7 @@ func (r *Repository) ListProjects(ctx context.Context, withOrganizationId string // ListOrganizations and supports the WithLimit option. func (r *Repository) ListOrganizations(ctx context.Context, opt ...Option) ([]*Scope, error) { var organizations []*Scope - err := r.list(ctx, &organizations, "type = ?", []interface{}{scope.Organization.String()}, opt...) + err := r.list(ctx, &organizations, "parent_id = ? and type = ?", []interface{}{"global", scope.Organization.String()}, opt...) if err != nil { return nil, fmt.Errorf("list organizations: %w", err) } diff --git a/internal/iam/repository_scope_test.go b/internal/iam/repository_scope_test.go index 64d6fc204d..b688bf3595 100644 --- a/internal/iam/repository_scope_test.go +++ b/internal/iam/repository_scope_test.go @@ -17,7 +17,7 @@ import ( "google.golang.org/protobuf/proto" ) -func Test_Repository_CreateScope(t *testing.T) { +func Test_Repository_Scope_Create(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") t.Run("valid-scope", func(t *testing.T) { @@ -116,7 +116,7 @@ func Test_Repository_CreateScope(t *testing.T) { }) } -func Test_Repository_UpdateScope(t *testing.T) { +func Test_Repository_Scope_Update(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") t.Run("valid-scope", func(t *testing.T) { @@ -180,7 +180,7 @@ func Test_Repository_UpdateScope(t *testing.T) { }) } -func Test_Repository_LookupScope(t *testing.T) { +func Test_Repository_Scope_Lookup(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") t.Run("found-and-not-found", func(t *testing.T) { @@ -206,7 +206,7 @@ func Test_Repository_LookupScope(t *testing.T) { }) } -func Test_Repository_DeleteScope(t *testing.T) { +func Test_Repository_Scope_Delete(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) @@ -462,7 +462,7 @@ func TestRepository_UpdateScope(t *testing.T) { }) } -func TestRepository_ListProjects(t *testing.T) { +func Test_Repository_ListProjects(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") const testLimit = 10 @@ -525,7 +525,7 @@ func TestRepository_ListProjects(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("public_id != ?", org.PublicId).Delete(allocScope()).Error) + require.NoError(conn.Where("public_id != ? and public_id != 'global'", org.PublicId).Delete(allocScope()).Error) testProjects := []*Scope{} for i := 0; i < tt.createCnt; i++ { testProjects = append(testProjects, testProject(t, conn, org.PublicId)) @@ -542,7 +542,7 @@ func TestRepository_ListProjects(t *testing.T) { } } -func TestRepository_ListOrganizations(t *testing.T) { +func Test_Repository_ListOrganizations(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") const testLimit = 10 @@ -589,7 +589,7 @@ func TestRepository_ListOrganizations(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(allocScope()).Error) + require.NoError(conn.Where("type = 'organization'").Delete(allocScope()).Error) testOrgs := []*Scope{} for i := 0; i < tt.createCnt; i++ { testOrgs = append(testOrgs, testOrg(t, conn, "", "")) diff --git a/internal/iam/scope.go b/internal/iam/scope.go index c9f47a858d..7d900b3d73 100644 --- a/internal/iam/scope.go +++ b/internal/iam/scope.go @@ -14,7 +14,7 @@ import ( ) // Scope is used to create a hierarchy of "containers" that encompass the scope of -// an IAM resource. Scopes are Organizations and Projects. +// an IAM resource. Scopes are Global, Organizations and Projects. type Scope struct { *store.Scope @@ -29,6 +29,9 @@ var _ db.VetForWriter = (*Scope)(nil) var _ Clonable = (*Scope)(nil) func NewOrganization(opt ...Option) (*Scope, error) { + global := allocScope() + global.PublicId = "global" + opt = append(opt, withScope(&global)) return newScope(scope.Organization, opt...) } @@ -48,15 +51,20 @@ func NewProject(organizationPublicId string, opt ...Option) (*Scope, error) { // scope's description. WithScope specifies the Scope's parent func newScope(scopeType scope.Type, opt ...Option) (*Scope, error) { opts := getOpts(opt...) - if scopeType == scope.Unknown { - return nil, fmt.Errorf("new scope: unknown scope type %w", db.ErrInvalidParameter) - } - if opts.withScope != nil && opts.withScope.PublicId == "" { - return nil, fmt.Errorf("new scope: with scope's parent id is missing %w", db.ErrInvalidParameter) - } - if scopeType == scope.Project && opts.withScope == nil { - return nil, fmt.Errorf("new scope: project scope is missing its parent %w", db.ErrInvalidParameter) + switch scopeType { + case scope.Unknown: + return nil, fmt.Errorf("new scope: unknown scope type: %w", db.ErrInvalidParameter) + case scope.Global: + return nil, fmt.Errorf("new scope: invalid scope type: %w", db.ErrInvalidParameter) + default: + if opts.withScope == nil { + return nil, fmt.Errorf("new scope: child scope is missing its parent: %w", db.ErrInvalidParameter) + } + if opts.withScope.PublicId == "" { + return nil, fmt.Errorf("new scope: with scope's parent id is missing: %w", db.ErrInvalidParameter) + } } + s := &Scope{ Scope: &store.Scope{ Type: scopeType.String(), @@ -106,10 +114,21 @@ func (s *Scope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, } } if opType == db.CreateOp { - if s.ParentId == "" && s.Type == scope.Project.String() { - return errors.New("project has no organization") - } - if s.Type == scope.Project.String() { + switch { + case s.Type == scope.Global.String(): + return errors.New("global scope cannot be created") + case s.ParentId == "": + switch s.Type { + case scope.Organization.String(): + return errors.New("organization must have global parent") + case scope.Project.String(): + return errors.New("project has no organization") + } + case s.Type == scope.Organization.String(): + if s.ParentId != "global" { + return errors.New(`organization's parent must be "global"`) + } + case s.Type == scope.Project.String(): parentScope := allocScope() parentScope.PublicId = s.ParentId if err := r.LookupByPublicId(ctx, &parentScope, opt...); err != nil { @@ -126,6 +145,8 @@ func (s *Scope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, // ResourceType returns the type of scope func (s *Scope) ResourceType() resource.Type { switch s.Type { + case scope.Global.String(): + return resource.Global case scope.Organization.String(): return resource.Organization case scope.Project.String(): @@ -153,19 +174,19 @@ func (s *Scope) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { return nil, fmt.Errorf("unable to get scope by public id: %w", err) } } - // HANDLE_ORG + // HANDLE_GLOBAL switch s.Type { - case scope.Organization.String(): + case scope.Global.String(): return nil, nil - case scope.Project.String(): + default: var p Scope switch s.ParentId { case "": // no parent id, so use the public_id to find the parent scope. This // won't work for if the scope hasn't been written to the db yet, // like during create but in that case the parent id should be set - // for all scopes which are not organizations, and the organization - // case was handled at HANDLE_ORG + // for all scopes which are not global, and the global case was + // handled at HANDLE_GLOBAL where := "public_id in (select parent_id from iam_scope where public_id = ?)" if err := r.LookupWhere(ctx, &p, where, s.PublicId); err != nil { return nil, fmt.Errorf("unable to lookup parent public id from public id: %w", err) @@ -177,7 +198,6 @@ func (s *Scope) GetScope(ctx context.Context, r db.Reader) (*Scope, error) { } return &p, nil } - return nil, fmt.Errorf("unable to get scope with type %s", s.Type) } // TableName returns the tablename to override the default gorm table name diff --git a/internal/iam/scope_test.go b/internal/iam/scope_test.go index 8cce62057f..188c16d8f4 100644 --- a/internal/iam/scope_test.go +++ b/internal/iam/scope_test.go @@ -2,9 +2,11 @@ package iam import ( "context" + "strings" "testing" "github.com/hashicorp/watchtower/internal/db" + "github.com/hashicorp/watchtower/internal/iam/store" "github.com/hashicorp/watchtower/internal/types/action" "github.com/hashicorp/watchtower/internal/types/resource" "github.com/hashicorp/watchtower/internal/types/scope" @@ -13,7 +15,7 @@ import ( "google.golang.org/protobuf/proto" ) -func Test_NewScope(t *testing.T) { +func TestScope_New(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") t.Run("valid-org-with-project", func(t *testing.T) { @@ -40,17 +42,17 @@ func Test_NewScope(t *testing.T) { s, err := newScope(scope.Unknown) require.Error(err) require.Nil(s) - assert.Equal(err.Error(), "new scope: unknown scope type invalid parameter") + assert.Equal(err.Error(), "new scope: unknown scope type: invalid parameter") }) t.Run("proj-scope-with-no-org", func(t *testing.T) { assert, require := assert.New(t), require.New(t) s, err := NewProject("") require.Error(err) require.Nil(s) - assert.Equal(err.Error(), "error creating new project: new scope: with scope's parent id is missing invalid parameter") + assert.Equal(err.Error(), "error creating new project: new scope: with scope's parent id is missing: invalid parameter") }) } -func Test_ScopeCreate(t *testing.T) { +func TestScope_Create(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") t.Run("valid", func(t *testing.T) { @@ -92,7 +94,7 @@ func Test_ScopeCreate(t *testing.T) { }) } -func Test_ScopeUpdate(t *testing.T) { +func TestScope_Update(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") t.Run("valid", func(t *testing.T) { @@ -116,16 +118,23 @@ func Test_ScopeUpdate(t *testing.T) { assert.Equal(0, updatedRows) }) } -func Test_ScopeGetScope(t *testing.T) { +func TestScope_GetScope(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") t.Run("valid-scope", func(t *testing.T) { assert, require := assert.New(t), require.New(t) w := db.New(conn) org, proj := TestScopes(t, conn) + global := Scope{ + Scope: &store.Scope{Type: scope.Global.String(), PublicId: "global"}, + } + globalScope, err := global.GetScope(context.Background(), w) + require.NoError(err) + assert.Nil(globalScope) + foundScope, err := org.GetScope(context.Background(), w) require.NoError(err) - assert.Nil(foundScope) + assert.Equal(foundScope.PublicId, "global") projectOrg, err := proj.GetScope(context.Background(), w) require.NoError(err) @@ -159,6 +168,7 @@ func TestScope_ResourceType(t *testing.T) { o, err := NewOrganization() require.NoError(t, err) assert.Equal(t, o.ResourceType(), resource.Organization) + assert.Equal(t, o.GetParentId(), resource.Global.String()) } func TestScope_Clone(t *testing.T) { @@ -178,3 +188,55 @@ func TestScope_Clone(t *testing.T) { assert.True(!proto.Equal(cp.(*Scope).Scope, s2.Scope)) }) } + +// TestScope_GlobalErrors tests various expected error conditions related to the +// global scope at a layer below the repository, e.g. within the scope logic or the +// DB itself +func TestScope_GlobalErrors(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + w := db.New(conn) + t.Run("newScope errors", func(t *testing.T) { + // Not allowed + _, err := newScope(scope.Global) + require.Error(t, err) + + // Should fail as there's no scope + _, err = newScope(scope.Organization) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "missing its parent")) + }) + t.Run("creation disallowed at vet time", func(t *testing.T) { + // Not allowed to create + s := allocScope() + s.Type = scope.Global.String() + s.PublicId = "global" + err := s.VetForWrite(context.Background(), nil, db.CreateOp) + require.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "global scope cannot be created")) + }) + t.Run("check org parent at vet time", func(t *testing.T) { + // Org must have global parent + s := allocScope() + s.Type = scope.Organization.String() + s.PublicId = "o_1234" + s.ParentId = "global" + err := s.VetForWrite(context.Background(), nil, db.CreateOp) + require.NoError(t, err) + s.ParentId = "o_2345" + err = s.VetForWrite(context.Background(), nil, db.CreateOp) + require.Error(t, err) + }) + t.Run("not deletable in db", func(t *testing.T) { + // Should not be deletable + s := allocScope() + s.PublicId = "global" + // Add this to validate that we did in fact delete + err := w.LookupById(context.Background(), &s) + require.NoError(t, err) + require.Equal(t, s.Type, scope.Global.String()) + rows, err := w.Delete(context.Background(), &s) + require.Error(t, err) + assert.Equal(t, 0, rows) + }) +} diff --git a/internal/types/resource/resource.go b/internal/types/resource/resource.go index 60d0918881..f5f5262201 100644 --- a/internal/types/resource/resource.go +++ b/internal/types/resource/resource.go @@ -23,6 +23,7 @@ const ( HostSet Type = 16 Host Type = 17 Target Type = 18 + Global Type = 19 ) func (r Type) String() string { @@ -46,6 +47,7 @@ func (r Type) String() string { "host-set", "host", "target", + "global", }[r] } @@ -87,6 +89,8 @@ func StringToResourceType(s string) Type { return Host case Target.String(): return Target + case Global.String(): + return Global default: return Unknown } diff --git a/internal/types/resource/resource_test.go b/internal/types/resource/resource_test.go index f2787f89cb..59a87135d3 100644 --- a/internal/types/resource/resource_test.go +++ b/internal/types/resource/resource_test.go @@ -88,6 +88,10 @@ func Test_Resource(t *testing.T) { typeString: "target", want: Target, }, + { + typeString: "global", + want: Global, + }, } for _, tt := range tests { t.Run(tt.typeString, func(t *testing.T) { diff --git a/internal/types/scope/scope.go b/internal/types/scope/scope.go index 312b7e7ba9..3855ac8d18 100644 --- a/internal/types/scope/scope.go +++ b/internal/types/scope/scope.go @@ -5,13 +5,15 @@ type Type uint32 const ( Unknown Type = 0 - Organization Type = 1 - Project Type = 2 + Global Type = 1 + Organization Type = 2 + Project Type = 3 ) func (s Type) String() string { return [...]string{ "unknown", + "global", "organization", "project", }[s] @@ -20,6 +22,7 @@ func (s Type) String() string { func (s Type) Prefix() string { return [...]string{ "unknown", + "global", "o", "p", }[s] @@ -27,6 +30,8 @@ func (s Type) Prefix() string { func StringToScopeType(s string) Type { switch s { + case Global.String(): + return Global case Organization.String(): return Organization case Project.String(): diff --git a/internal/types/scope/scope_test.go b/internal/types/scope/scope_test.go index eea4cf4e84..7cd6205144 100644 --- a/internal/types/scope/scope_test.go +++ b/internal/types/scope/scope_test.go @@ -13,6 +13,12 @@ func Test_StringToScopeType(t *testing.T) { want Type wantPrefix string }{ + { + name: "global", + s: "global", + want: Global, + wantPrefix: "global", + }, { name: "org", s: "organization",