From 9ecc3840074c5e51c9dccc153031b481c26b1af6 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 4 Jun 2021 09:44:57 -0500 Subject: [PATCH] Add managed group support to role principals (#1270) --- globals/doc.go | 16 + globals/fields.go | 4 - globals/globals.go | 7 - internal/auth/oidc/ids.go | 5 +- internal/auth/oidc/managed_group_test.go | 2 +- .../repository_managed_group_members_test.go | 4 +- .../oidc/repository_managed_group_test.go | 47 ++- internal/auth/oidc/service_callback_test.go | 10 +- internal/auth/oidc/testing.go | 2 + internal/auth/subtype.go | 3 +- .../migrations/postgres/0/06_iam.up.sql | 3 +- ...4_oidc_managed_group_principal_role.up.sql | 79 ++++ internal/db/schema/postgres_migration.gen.go | 82 +++- internal/iam/ids_test.go | 4 + internal/iam/principal_role.go | 90 +++- internal/iam/principal_role_ext_test.go | 387 ++++++++++++++++++ internal/iam/principal_role_test.go | 40 +- internal/iam/repository_principal_role.go | 206 +++++++--- .../iam/repository_principal_role_ext_test.go | 251 ++++++++++++ .../iam/repository_principal_role_test.go | 170 -------- internal/iam/store/principal_role.pb.go | 171 ++++++-- internal/iam/testing.go | 12 + internal/intglobals/prefixes.go | 12 + .../storage/iam/store/v1/principal_role.proto | 14 + .../managed_groups/managed_group_service.go | 7 +- .../managed_group_service_test.go | 43 +- .../handlers/managed_groups/validate_test.go | 7 +- .../controller/handlers/roles/role_service.go | 19 +- .../handlers/roles/role_service_test.go | 194 +++++++-- 29 files changed, 1510 insertions(+), 381 deletions(-) create mode 100644 globals/doc.go create mode 100644 internal/db/schema/migrations/postgres/9/04_oidc_managed_group_principal_role.up.sql create mode 100644 internal/iam/principal_role_ext_test.go create mode 100644 internal/iam/repository_principal_role_ext_test.go create mode 100644 internal/intglobals/prefixes.go diff --git a/globals/doc.go b/globals/doc.go new file mode 100644 index 000000000..3b9c5beb2 --- /dev/null +++ b/globals/doc.go @@ -0,0 +1,16 @@ +/* +Globals, in the traditional sense, are usually bad. But there are some +situations where we want either consts or known values that are constant per +invocation but invocation may be in various places, such as a test or via CLI, +and placing such a value in any given package leads to import issues. So think +of this package as "freeing us from circular dependency hell". + +This package provides _exported_ globals that might reasonably be used by API +users, Terraform, etc. A separate package, under internal/intglobals, provides +globals that perform a similar function but are only meant for internal use. + +There is no race checking; these values should only ever be set at startup of +Boundary or a test, but available to reference from anywhere. +*/ + +package globals diff --git a/globals/fields.go b/globals/fields.go index b8a467ca5..5a0cb1bfa 100644 --- a/globals/fields.go +++ b/globals/fields.go @@ -1,9 +1,5 @@ package globals -// This set of consts is intended to be a place to collect commonly-used field -// names. Since these might be used outside of the internal code (e.g. by SDK or -// CLI), it's outside internal/ here in globals. Some of these are already const -// elsewhere in the code but we should migrate to using a single source. const ( IdField = "id" VersionField = "version" diff --git a/globals/globals.go b/globals/globals.go index b73da4cee..087e5497c 100644 --- a/globals/globals.go +++ b/globals/globals.go @@ -2,13 +2,6 @@ package globals import "time" -// NOTE: Globals, in the traditional sense, are bad. But there are some -// situations where we want a value that is constant per invocation but -// invocation may be in various places, such as a test or via CLI, and placing -// such a value in any given package leads to import issues. So think of this -// package as "freeing us from circular dependency hell". These values should -// only ever be set at startup, but simply available to reference from anywhere. - const ( TcpProxyV1 = "boundary-tcp-proxy-v1" ServiceTokenV1 = "s1" diff --git a/internal/auth/oidc/ids.go b/internal/auth/oidc/ids.go index 20cd10b02..5b30027b8 100644 --- a/internal/auth/oidc/ids.go +++ b/internal/auth/oidc/ids.go @@ -3,6 +3,7 @@ package oidc import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/intglobals" ) const ( @@ -10,8 +11,6 @@ const ( AuthMethodPrefix = "amoidc" // AccountPrefix defines the prefix for Account public ids. AccountPrefix = "acctoidc" - // ManagedGroupPrefix defines the prefix for ManagedGroup public ids. - ManagedGroupPrefix = "mgoidc" ) func newAuthMethodId() (string, error) { @@ -43,7 +42,7 @@ func newAccountId(authMethodId, issuer, sub string) (string, error) { func newManagedGroupId() (string, error) { const op = "oidc.newManagedGroupId" - id, err := db.NewPublicId(ManagedGroupPrefix) + id, err := db.NewPublicId(intglobals.OidcManagedGroupPrefix) if err != nil { return "", errors.Wrap(err, op) } diff --git a/internal/auth/oidc/managed_group_test.go b/internal/auth/oidc/managed_group_test.go index 33e7b9458..178524668 100644 --- a/internal/auth/oidc/managed_group_test.go +++ b/internal/auth/oidc/managed_group_test.go @@ -26,7 +26,7 @@ func Test_ManagedGroups_RepoValidate(t *testing.T) { }) t.Run("valid", func(t *testing.T) { mg.AuthMethodId = "amoidc_1234567890" - mg.Filter = testFakeManagedGroupFilter + mg.Filter = TestFakeManagedGroupFilter assert.NoError(mg.validate(op)) }) } diff --git a/internal/auth/oidc/repository_managed_group_members_test.go b/internal/auth/oidc/repository_managed_group_members_test.go index cbd502813..863ebdbfe 100644 --- a/internal/auth/oidc/repository_managed_group_members_test.go +++ b/internal/auth/oidc/repository_managed_group_members_test.go @@ -17,8 +17,6 @@ import ( "github.com/stretchr/testify/require" ) -const testFakeManagedGroupFilter = `"/foo" == "bar"` - func Test_ManagedGroupMemberships(t *testing.T) { // This tests both managed group membership functions (set/list) as list is // always called as a return from set and we are validating the values that @@ -62,7 +60,7 @@ func Test_ManagedGroupMemberships(t *testing.T) { for i := 0; i < 100; i++ { mg := oidc.AllocManagedGroup() mg.AuthMethodId = authMethod.PublicId - mg.Filter = testFakeManagedGroupFilter + mg.Filter = oidc.TestFakeManagedGroupFilter got, err := repo.CreateManagedGroup(context.Background(), org.GetPublicId(), mg) require.NoError(t, err) mgs = append(mgs, got) diff --git a/internal/auth/oidc/repository_managed_group_test.go b/internal/auth/oidc/repository_managed_group_test.go index bc7dc7f36..abd99f97c 100644 --- a/internal/auth/oidc/repository_managed_group_test.go +++ b/internal/auth/oidc/repository_managed_group_test.go @@ -13,14 +13,13 @@ import ( dbassert "github.com/hashicorp/boundary/internal/db/assert" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/intglobals" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -const testFakeManagedGroupFilter = `"/foo" == "bar"` - func TestRepository_CreateManagedGroup(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) @@ -91,7 +90,7 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeManagedGroupFilter, + Filter: TestFakeManagedGroupFilter, PublicId: "mgoidc_OOOOOOOOOO", }, }, @@ -103,7 +102,7 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeManagedGroupFilter, + Filter: TestFakeManagedGroupFilter, }, }, wantIsErr: errors.InvalidParameter, @@ -115,13 +114,13 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeManagedGroupFilter, + Filter: TestFakeManagedGroupFilter, }, }, want: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeManagedGroupFilter, + Filter: TestFakeManagedGroupFilter, }, }, }, @@ -131,14 +130,14 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeManagedGroupFilter, + Filter: TestFakeManagedGroupFilter, Name: "test-name-repo", }, }, want: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeManagedGroupFilter, + Filter: TestFakeManagedGroupFilter, Name: "test-name-repo", }, }, @@ -149,7 +148,7 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeManagedGroupFilter, + Filter: TestFakeManagedGroupFilter, Description: ("test-description-repo"), Name: "myname", }, @@ -157,7 +156,7 @@ func TestRepository_CreateManagedGroup(t *testing.T) { want: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeManagedGroupFilter, + Filter: TestFakeManagedGroupFilter, Description: ("test-description-repo"), Name: "myname", }, @@ -169,7 +168,7 @@ func TestRepository_CreateManagedGroup(t *testing.T) { in: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ AuthMethodId: authMethod.PublicId, - Filter: testFakeManagedGroupFilter, + Filter: TestFakeManagedGroupFilter, Description: ("test-description-repo"), Name: "myname", }, @@ -200,7 +199,7 @@ func TestRepository_CreateManagedGroup(t *testing.T) { require.NoError(err) assert.Empty(tt.in.PublicId) require.NotNil(got) - assertPublicId(t, ManagedGroupPrefix, got.PublicId) + assertPublicId(t, intglobals.OidcManagedGroupPrefix, got.PublicId) assert.NotSame(tt.in, got) assert.Equal(tt.want.Name, got.Name) assert.Equal(tt.want.Description, got.Description) @@ -231,7 +230,7 @@ func TestRepository_LookupManagedGroup(t *testing.T) { WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) - mg := TestManagedGroup(t, conn, authMethod, testFakeManagedGroupFilter) + mg := TestManagedGroup(t, conn, authMethod, TestFakeManagedGroupFilter) newMgId, err := newManagedGroupId() require.NoError(t, err) @@ -296,7 +295,7 @@ func TestRepository_DeleteManagedGroup(t *testing.T) { WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) - mg := TestManagedGroup(t, conn, authMethod, testFakeManagedGroupFilter) + mg := TestManagedGroup(t, conn, authMethod, TestFakeManagedGroupFilter) newMgId, err := newManagedGroupId() require.NoError(t, err) tests := []struct { @@ -387,18 +386,18 @@ func TestRepository_ListManagedGroups(t *testing.T) { WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) mgs1 := []*ManagedGroup{ - TestManagedGroup(t, conn, authMethod1, testFakeManagedGroupFilter), - TestManagedGroup(t, conn, authMethod1, testFakeManagedGroupFilter), - TestManagedGroup(t, conn, authMethod1, testFakeManagedGroupFilter), + TestManagedGroup(t, conn, authMethod1, TestFakeManagedGroupFilter), + TestManagedGroup(t, conn, authMethod1, TestFakeManagedGroupFilter), + TestManagedGroup(t, conn, authMethod1, TestFakeManagedGroupFilter), } sort.Slice(mgs1, func(i, j int) bool { return strings.Compare(mgs1[i].PublicId, mgs1[j].PublicId) < 0 }) mgs2 := []*ManagedGroup{ - TestManagedGroup(t, conn, authMethod2, testFakeManagedGroupFilter), - TestManagedGroup(t, conn, authMethod2, testFakeManagedGroupFilter), - TestManagedGroup(t, conn, authMethod2, testFakeManagedGroupFilter), + TestManagedGroup(t, conn, authMethod2, TestFakeManagedGroupFilter), + TestManagedGroup(t, conn, authMethod2, TestFakeManagedGroupFilter), + TestManagedGroup(t, conn, authMethod2, TestFakeManagedGroupFilter), } sort.Slice(mgs2, func(i, j int) bool { return strings.Compare(mgs2[i].PublicId, mgs2[j].PublicId) < 0 @@ -789,7 +788,7 @@ func TestRepository_UpdateManagedGroup(t *testing.T) { version: 1, orig: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ - Filter: testFakeManagedGroupFilter, + Filter: TestFakeManagedGroupFilter, }, }, chgFn: changeFilter(`"/zip" == "zap"`), @@ -865,7 +864,7 @@ func TestRepository_UpdateManagedGroup(t *testing.T) { version: 1, orig: &ManagedGroup{ ManagedGroup: &store.ManagedGroup{ - Filter: testFakeManagedGroupFilter, + Filter: TestFakeManagedGroupFilter, }, }, masks: []string{FilterField}, @@ -923,7 +922,7 @@ func TestRepository_UpdateManagedGroup(t *testing.T) { assert.NoError(err) require.NotNil(repo) - orig := TestManagedGroup(t, conn, am, testFakeManagedGroupFilter, WithName(tt.orig.GetName()), WithDescription(tt.orig.GetDescription())) + orig := TestManagedGroup(t, conn, am, TestFakeManagedGroupFilter, WithName(tt.orig.GetName()), WithDescription(tt.orig.GetDescription())) tt.orig.AuthMethodId = am.PublicId if tt.chgFn != nil { @@ -949,7 +948,7 @@ func TestRepository_UpdateManagedGroup(t *testing.T) { return } require.NotNil(got) - assertPublicId(t, ManagedGroupPrefix, got.PublicId) + assertPublicId(t, intglobals.OidcManagedGroupPrefix, got.PublicId) assert.Equal(tt.wantCount, gotCount, "row count") assert.NotSame(tt.orig, got) assert.Equal(tt.orig.AuthMethodId, got.AuthMethodId) diff --git a/internal/auth/oidc/service_callback_test.go b/internal/auth/oidc/service_callback_test.go index 3fca2f925..8be589e1b 100644 --- a/internal/auth/oidc/service_callback_test.go +++ b/internal/auth/oidc/service_callback_test.go @@ -661,8 +661,8 @@ func Test_ManagedGroupFiltering(t *testing.T) { // Create two managed groups. We'll use these in tests to set filters and // then check that the user belongs to the ones we expect. mgs := []*ManagedGroup{ - TestManagedGroup(t, conn, testAuthMethod, testFakeManagedGroupFilter), - TestManagedGroup(t, conn, testAuthMethod, testFakeManagedGroupFilter), + TestManagedGroup(t, conn, testAuthMethod, TestFakeManagedGroupFilter), + TestManagedGroup(t, conn, testAuthMethod, TestFakeManagedGroupFilter), } // A reusable oidc.Provider for the tests @@ -697,15 +697,15 @@ func Test_ManagedGroupFiltering(t *testing.T) { { name: "no match", filters: []string{ - testFakeManagedGroupFilter, - testFakeManagedGroupFilter, + TestFakeManagedGroupFilter, + TestFakeManagedGroupFilter, }, }, { name: "token match", filters: []string{ `"/token/nonce" == "nonce"`, - testFakeManagedGroupFilter, + TestFakeManagedGroupFilter, }, matchingMgs: mgs[0:1], }, diff --git a/internal/auth/oidc/testing.go b/internal/auth/oidc/testing.go index 48699f145..e4a820f81 100644 --- a/internal/auth/oidc/testing.go +++ b/internal/auth/oidc/testing.go @@ -34,6 +34,8 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +const TestFakeManagedGroupFilter = `"/foo" == "bar"` + // TestAuthMethod creates a test oidc auth method. WithName, WithDescription, // WithMaxAge, WithApiUrl, WithIssuer, WithCertificates, WithAudClaims, and // WithSigningAlgs options are supported. diff --git a/internal/auth/subtype.go b/internal/auth/subtype.go index 5fb193098..49244051c 100644 --- a/internal/auth/subtype.go +++ b/internal/auth/subtype.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/intglobals" ) type Subtype int @@ -92,7 +93,7 @@ func SubtypeFromId(id string) Subtype { return PasswordSubtype case strings.HasPrefix(strings.TrimSpace(id), oidc.AuthMethodPrefix), strings.HasPrefix(strings.TrimSpace(id), oidc.AccountPrefix), - strings.HasPrefix(strings.TrimSpace(id), oidc.ManagedGroupPrefix): + strings.HasPrefix(strings.TrimSpace(id), intglobals.OidcManagedGroupPrefix): return OidcSubtype } return UnknownSubtype diff --git a/internal/db/schema/migrations/postgres/0/06_iam.up.sql b/internal/db/schema/migrations/postgres/0/06_iam.up.sql index 10b32924b..3ab5438b5 100644 --- a/internal/db/schema/migrations/postgres/0/06_iam.up.sql +++ b/internal/db/schema/migrations/postgres/0/06_iam.up.sql @@ -565,6 +565,7 @@ $$ language plpgsql; -- iam_principle_role provides a consolidated view all principal roles assigned -- (user and group roles). +-- REPLACED in 9/04_oidc_managed_group_principal_role create view iam_principal_role as select ur.create_time, @@ -672,7 +673,7 @@ before insert on iam_group_member_user for each row execute procedure recovery_user_not_allowed('member_id'); --- get_scoped_principal_id is used by the iam_group_member view as a convient +-- get_scoped_member_id is used by the iam_group_member view as a convient -- way to create : to reference members from -- other scopes than the group's scope. create or replace function get_scoped_member_id(group_scope text, member_scope text, member_id text) returns text diff --git a/internal/db/schema/migrations/postgres/9/04_oidc_managed_group_principal_role.up.sql b/internal/db/schema/migrations/postgres/9/04_oidc_managed_group_principal_role.up.sql new file mode 100644 index 000000000..810cc955c --- /dev/null +++ b/internal/db/schema/migrations/postgres/9/04_oidc_managed_group_principal_role.up.sql @@ -0,0 +1,79 @@ +begin; + +-- iam_managed_group_role contains roles that have been assigned to managed +-- groups. Managed 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_principal(). +create table iam_managed_group_role ( + create_time wt_timestamp, + role_id wt_role_id + references iam_role(public_id) + on delete cascade + on update cascade, + principal_id wt_public_id + references auth_managed_group(public_id) + on delete cascade + on update cascade, + primary key (role_id, principal_id) + ); + +create trigger immutable_role_principal +before update on iam_managed_group_role + for each row execute procedure iam_immutable_role_principal(); + +create trigger default_create_time_column +before insert on iam_managed_group_role + for each row execute procedure default_create_time(); + +-- iam_principal_role provides a consolidated view all principal roles assigned +-- (user and group roles). +create or replace view iam_principal_role as +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 +union +select + mgr.create_time, + mgr.principal_id, + mgr.role_id, + (select scope_id from auth_method am where am.public_id = amg.auth_method_id) as principal_scope_id, + r.scope_id as role_scope_id, + get_scoped_principal_id(r.scope_id, (select scope_id from auth_method am where am.public_id = amg.auth_method_id), mgr.principal_id) as scoped_principal_id, + 'managed group' as type +from + iam_managed_group_role mgr, + iam_role r, + auth_managed_group amg +where + mgr.role_id = r.public_id and + amg.public_id = mgr.principal_id; + +commit; diff --git a/internal/db/schema/postgres_migration.gen.go b/internal/db/schema/postgres_migration.gen.go index d7623c6d7..44c899164 100644 --- a/internal/db/schema/postgres_migration.gen.go +++ b/internal/db/schema/postgres_migration.gen.go @@ -4,7 +4,7 @@ package schema func init() { migrationStates["postgres"] = migrationState{ - binarySchemaVersion: 9003, + binarySchemaVersion: 9004, upMigrations: map[int][]byte{ 1: []byte(` create domain wt_public_id as text @@ -966,6 +966,7 @@ $$ language plpgsql; -- iam_principle_role provides a consolidated view all principal roles assigned -- (user and group roles). +-- REPLACED in 9/04_oidc_managed_group_principal_role create view iam_principal_role as select ur.create_time, @@ -1073,7 +1074,7 @@ before insert on iam_group_member_user for each row execute procedure recovery_user_not_allowed('member_id'); --- get_scoped_principal_id is used by the iam_group_member view as a convient +-- get_scoped_member_id is used by the iam_group_member view as a convient -- way to create : to reference members from -- other scopes than the group's scope. create or replace function get_scoped_member_id(group_scope text, member_scope text, member_id text) returns text @@ -6562,6 +6563,83 @@ select oidc.member_id from auth_oidc_managed_group_member_account oidc; +`), + 9004: []byte(` +-- iam_managed_group_role contains roles that have been assigned to managed +-- groups. Managed 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_principal(). +create table iam_managed_group_role ( + create_time wt_timestamp, + role_id wt_role_id + references iam_role(public_id) + on delete cascade + on update cascade, + principal_id wt_public_id + references auth_managed_group(public_id) + on delete cascade + on update cascade, + primary key (role_id, principal_id) + ); + +create trigger immutable_role_principal +before update on iam_managed_group_role + for each row execute procedure iam_immutable_role_principal(); + +create trigger default_create_time_column +before insert on iam_managed_group_role + for each row execute procedure default_create_time(); + +-- iam_principal_role provides a consolidated view all principal roles assigned +-- (user and group roles). +create or replace view iam_principal_role as +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 +union +select + mgr.create_time, + mgr.principal_id, + mgr.role_id, + (select scope_id from auth_method am where am.public_id = amg.auth_method_id) as principal_scope_id, + r.scope_id as role_scope_id, + get_scoped_principal_id(r.scope_id, (select scope_id from auth_method am where am.public_id = amg.auth_method_id), mgr.principal_id) as scoped_principal_id, + 'managed group' as type +from + iam_managed_group_role mgr, + iam_role r, + auth_managed_group amg +where + mgr.role_id = r.public_id and + amg.public_id = mgr.principal_id; `), }, } diff --git a/internal/iam/ids_test.go b/internal/iam/ids_test.go index e28ce8fd6..7b8aacebb 100644 --- a/internal/iam/ids_test.go +++ b/internal/iam/ids_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/intglobals" "github.com/hashicorp/boundary/internal/types/scope" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,6 +27,9 @@ func Test_PublicIds(t *testing.T) { require.NoError(t, err) assert.True(t, strings.HasPrefix(id, GroupPrefix+"_")) }) + t.Run("oidc managed group", func(t *testing.T) { + assert.True(t, strings.HasPrefix("mgoidc_1234567890", intglobals.OidcManagedGroupPrefix+"_")) + }) t.Run("scopes", func(t *testing.T) { id, err := newScopeId(scope.Org) require.NoError(t, err) diff --git a/internal/iam/principal_role.go b/internal/iam/principal_role.go index ce0f01968..363bb713e 100644 --- a/internal/iam/principal_role.go +++ b/internal/iam/principal_role.go @@ -13,9 +13,10 @@ import ( type RoleType uint32 const ( - UnknownRoleType RoleType = 0 - UserRoleType RoleType = 1 - GroupRoleType RoleType = 2 + UnknownRoleType RoleType = 0 + UserRoleType RoleType = 1 + GroupRoleType RoleType = 2 + ManagedGroupRoleType RoleType = 3 ) // String returns a string representation of the role type. @@ -24,6 +25,7 @@ func (r RoleType) String() string { "unknown", "user", "group", + "managed group", }[r] } @@ -31,6 +33,7 @@ const ( principalRoleViewDefaultTable = "iam_principal_role" userRoleDefaultTable = "iam_user_role" groupRoleDefaultTable = "iam_group_role" + managedGroupRoleDefaultTable = "iam_managed_group_role" ) // PrincipalRole provides a common way to return roles regardless of their @@ -188,7 +191,7 @@ func (role *GroupRole) VetForWrite(ctx context.Context, r db.Reader, opType db.O return errors.New(errors.InvalidParameter, op, "missing role id") } if role.PrincipalId == "" { - return errors.New(errors.InvalidParameter, op, "missing user id") + return errors.New(errors.InvalidParameter, op, "missing group id") } return nil } @@ -212,3 +215,82 @@ func (r *GroupRole) SetTableName(n string) { r.tableName = n } } + +// ManagedGroupRole is a managed group assigned to a role +type ManagedGroupRole struct { + *store.ManagedGroupRole + tableName string `gorm:"-"` +} + +// ensure that GroupRole implements the interfaces of: Cloneable and +// db.VetForWriter +var ( + _ Cloneable = (*ManagedGroupRole)(nil) + _ db.VetForWriter = (*ManagedGroupRole)(nil) +) + +// NewGroupRole creates a new group role in memory. No options are supported +// currently. +func NewManagedGroupRole(roleId, managedGroupId string, opt ...Option) (*ManagedGroupRole, error) { + const op = "iam.NewManagedGroupRole" + if roleId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing role id") + } + if managedGroupId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing managed group id") + } + return &ManagedGroupRole{ + ManagedGroupRole: &store.ManagedGroupRole{ + PrincipalId: managedGroupId, + RoleId: roleId, + }, + }, nil +} + +// AllocManagedGroupRole returns a new ManagedGroupRole with an initialized +// store. +func AllocManagedGroupRole() ManagedGroupRole { + return ManagedGroupRole{ + ManagedGroupRole: &store.ManagedGroupRole{}, + } +} + +// Clone creates a clone of the ManagedGroupRole. +func (r *ManagedGroupRole) Clone() interface{} { + cp := proto.Clone(r.ManagedGroupRole) + return &ManagedGroupRole{ + ManagedGroupRole: cp.(*store.ManagedGroupRole), + } +} + +// VetForWrite implements db.VetForWrite() interface for managed group roles. +func (role ManagedGroupRole) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, opt ...db.Option) error { + const op = "iam.(ManagedGroupRole).VetForWrite" + if role.RoleId == "" { + return errors.New(errors.InvalidParameter, op, "missing role id") + } + if role.PrincipalId == "" { + return errors.New(errors.InvalidParameter, op, "missing managed group id") + } + return nil +} + +// TableName returns the tablename to override the default gorm table name for +// managed group roles. +func (r *ManagedGroupRole) TableName() string { + if r.tableName != "" { + return r.tableName + } + return managedGroupRoleDefaultTable +} + +// 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 *ManagedGroupRole) SetTableName(n string) { + switch n { + case "": + r.tableName = managedGroupRoleDefaultTable + default: + r.tableName = n + } +} diff --git a/internal/iam/principal_role_ext_test.go b/internal/iam/principal_role_ext_test.go new file mode 100644 index 000000000..58020e462 --- /dev/null +++ b/internal/iam/principal_role_ext_test.go @@ -0,0 +1,387 @@ +package iam_test + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/iam/store" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// This is in an iam_test package because managed groups are an abstract type so +// we need to reach into an implementation for testing, which itself reaches +// into IAM. So this avoids a dependency loop. + +func TestNewManagedGroupRole(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + repo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, repo) + orgRole := iam.TestRole(t, conn, org.PublicId) + const managedGroupId = "mgoidc_1234567890" + + type args struct { + roleId string + ManagedGroupId string + opt []iam.Option + } + tests := []struct { + name string + args args + want *iam.ManagedGroupRole + wantErr bool + wantIsErr errors.Code + }{ + { + name: "valid-org", + args: args{ + roleId: orgRole.PublicId, + ManagedGroupId: managedGroupId, + }, + want: func() *iam.ManagedGroupRole { + r := iam.AllocManagedGroupRole() + r.RoleId = orgRole.PublicId + r.PrincipalId = managedGroupId + return &r + }(), + }, + { + name: "empty-role-id", + args: args{ + roleId: "", + ManagedGroupId: managedGroupId, + }, + want: nil, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "empty-ManagedGroup-id", + args: args{ + roleId: orgRole.PublicId, + ManagedGroupId: "", + }, + want: nil, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := iam.NewManagedGroupRole(tt.args.roleId, tt.args.ManagedGroupId, tt.args.opt...) + if tt.wantErr { + require.Error(err) + assert.True(errors.Match(errors.T(tt.wantIsErr), err)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + }) + } +} + +func TestManagedGroupRole_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + + repo := iam.TestRepo(t, conn, wrap) + org, _ := iam.TestScopes(t, repo) + org2, _ := iam.TestScopes(t, repo) + + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := oidc.TestAuthMethod( + t, conn, databaseWrapper, org.GetPublicId(), oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + mg := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter) + + type args struct { + role *iam.ManagedGroupRole + } + tests := []struct { + name string + args args + wantDup bool + wantErr bool + wantErrMsg string + wantIsErr errors.Code + }{ + { + name: "valid-with-org", + args: args{ + role: func() *iam.ManagedGroupRole { + role := iam.TestRole(t, conn, org.PublicId) + principalRole, err := iam.NewManagedGroupRole(role.PublicId, mg.PublicId) + require.NoError(t, err) + return principalRole + }(), + }, + wantErr: false, + }, + { + name: "cross-org", + args: args{ + role: func() *iam.ManagedGroupRole { + role := iam.TestRole(t, conn, org2.PublicId) + principalRole, err := iam.NewManagedGroupRole(role.PublicId, mg.PublicId) + require.NoError(t, err) + return principalRole + }(), + }, + }, + { + name: "bad-role-id", + args: args{ + role: func() *iam.ManagedGroupRole { + id := testId(t) + principalRole, err := iam.NewManagedGroupRole(id, mg.PublicId) + require.NoError(t, err) + return principalRole + }(), + }, + wantErr: true, + wantErrMsg: "db.Create: create failed: insert or update on table \"iam_managed_group_role\" violates foreign key constraint \"iam_managed_group_role_role_id_fkey\"", + }, + { + name: "bad-principal-id", + args: args{ + role: func() *iam.ManagedGroupRole { + id := testId(t) + role := iam.TestRole(t, conn, org.PublicId) + principalRole, err := iam.NewManagedGroupRole(role.PublicId, id) + require.NoError(t, err) + return principalRole + }(), + }, + wantErr: true, + wantErrMsg: "db.Create: create failed: insert or update on table \"iam_managed_group_role\" violates foreign key constraint \"iam_managed_group_role_principal_id_fkey\"", + }, + { + name: "missing-role-id", + args: args{ + role: func() *iam.ManagedGroupRole { + return &iam.ManagedGroupRole{ + ManagedGroupRole: &store.ManagedGroupRole{ + RoleId: "", + PrincipalId: mg.PublicId, + }, + } + }(), + }, + wantErr: true, + wantErrMsg: "db.Create: iam.(ManagedGroupRole).VetForWrite: missing role id: parameter violation: error #100", + wantIsErr: errors.InvalidParameter, + }, + { + name: "missing-principal-id", + args: args{ + role: func() *iam.ManagedGroupRole { + role := iam.TestRole(t, conn, org.PublicId) + return &iam.ManagedGroupRole{ + ManagedGroupRole: &store.ManagedGroupRole{ + RoleId: role.PublicId, + PrincipalId: "", + }, + } + }(), + }, + wantErr: true, + wantErrMsg: "db.Create: iam.(ManagedGroupRole).VetForWrite: missing managed group id: parameter violation: error #100", + wantIsErr: errors.InvalidParameter, + }, + { + name: "dup-at-org", + args: args{ + role: func() *iam.ManagedGroupRole { + role := iam.TestRole(t, conn, org.PublicId) + principalRole, err := iam.NewManagedGroupRole(role.PublicId, mg.PublicId) + require.NoError(t, err) + return principalRole + }(), + }, + wantDup: true, + wantErr: true, + wantErrMsg: `db.Create: create failed: duplicate key value violates unique constraint`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + w := db.New(conn) + if tt.wantDup { + r := tt.args.role.Clone().(*iam.ManagedGroupRole) + err := w.Create(context.Background(), r) + require.NoError(err) + } + r := tt.args.role.Clone().(*iam.ManagedGroupRole) + err := w.Create(context.Background(), r) + if tt.wantErr { + require.Error(err) + assert.Contains(err.Error(), tt.wantErrMsg) + assert.True(errors.Match(errors.T(tt.wantIsErr), err)) + return + } + assert.NoError(err) + + found := iam.AllocManagedGroupRole() + err = w.LookupWhere(context.Background(), &found, "role_id = ? and principal_id = ?", r.RoleId, r.PrincipalId) + require.NoError(err) + assert.Equal(r, &found) + }) + } +} + +func TestManagedGroupRole_Update(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + + repo := iam.TestRepo(t, conn, wrap) + org, _ := iam.TestScopes(t, repo) + + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := oidc.TestAuthMethod( + t, conn, databaseWrapper, org.GetPublicId(), oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + t.Run("updates not allowed", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + r := iam.TestRole(t, conn, org.PublicId) + mg := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter) + mg2 := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter) + mgr := iam.TestManagedGroupRole(t, conn, r.PublicId, mg.PublicId) + updateRole := mgr.Clone().(*iam.ManagedGroupRole) + updateRole.PrincipalId = mg2.PublicId + updatedRows, err := rw.Update(context.Background(), updateRole, []string{"PrincipalId"}, nil) + require.Error(err) + assert.Equal(0, updatedRows) + }) +} + +func TestManagedGroupRole_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + + repo := iam.TestRepo(t, conn, wrap) + org, _ := iam.TestScopes(t, repo) + + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := oidc.TestAuthMethod( + t, conn, databaseWrapper, org.GetPublicId(), oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + id := testId(t) + mg := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter) + r := iam.TestRole(t, conn, org.PublicId) + + tests := []struct { + name string + role *iam.ManagedGroupRole + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + role: iam.TestManagedGroupRole(t, conn, r.PublicId, mg.PublicId), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + role: func() *iam.ManagedGroupRole { + r := iam.AllocManagedGroupRole() + r.PrincipalId = id + r.RoleId = id + return &r + }(), + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + deleteRole := iam.AllocManagedGroupRole() + deleteRole.RoleId = tt.role.GetRoleId() + deleteRole.PrincipalId = tt.role.GetPrincipalId() + deletedRows, err := rw.Delete(context.Background(), &deleteRole) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + found := iam.AllocManagedGroupRole() + err = rw.LookupWhere(context.Background(), &found, "role_id = ? and principal_id = ?", tt.role.GetRoleId(), tt.role.GetPrincipalId()) + require.Error(err) + assert.True(errors.IsNotFoundError(err)) + }) + } +} + +func TestManagedGroupRole_Clone(t *testing.T) { + t.Parallel() + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + mgr, err := iam.NewManagedGroupRole("r_abc", "mgoidc_abc") + require.NoError(err) + cp := mgr.Clone() + assert.True(proto.Equal(cp.(*iam.ManagedGroupRole).ManagedGroupRole, mgr.ManagedGroupRole)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + mgr, err := iam.NewManagedGroupRole("r_abc", "mgoidc_abc") + require.NoError(err) + mgr2, err := iam.NewManagedGroupRole("r_xyz", "mgoidc_xyz") + require.NoError(err) + cp := mgr.Clone() + + assert.True(!proto.Equal(cp.(*iam.ManagedGroupRole).ManagedGroupRole, mgr2.ManagedGroupRole)) + }) +} diff --git a/internal/iam/principal_role_test.go b/internal/iam/principal_role_test.go index e6af872bb..9f03c1a90 100644 --- a/internal/iam/principal_role_test.go +++ b/internal/iam/principal_role_test.go @@ -571,7 +571,7 @@ func TestGroupRole_Create(t *testing.T) { wantIsErr: errors.InvalidParameter, }, { - name: "missing-user-id", + name: "missing-group-id", args: args{ role: func() *GroupRole { role := TestRole(t, conn, proj.PublicId) @@ -584,7 +584,7 @@ func TestGroupRole_Create(t *testing.T) { }(), }, wantErr: true, - wantErrMsg: "db.Create: iam.(GroupRole).VetForWrite: missing user id: parameter violation: error #100", + wantErrMsg: "db.Create: iam.(GroupRole).VetForWrite: missing group id: parameter violation: error #100", wantIsErr: errors.InvalidParameter, }, { @@ -865,3 +865,39 @@ func TestGroupRole_SetTableName(t *testing.T) { }) } } + +func TestManagedGroupRole_SetTableName(t *testing.T) { + defaultTableName := managedGroupRoleDefaultTable + tests := []struct { + name string + initialName string + setNameTo string + want string + }{ + { + name: "new-name", + initialName: "", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + initialName: "initial", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocManagedGroupRole() + require.Equal(defaultTableName, def.TableName()) + s := &ManagedGroupRole{ + ManagedGroupRole: &store.ManagedGroupRole{}, + tableName: tt.initialName, + } + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/iam/repository_principal_role.go b/internal/iam/repository_principal_role.go index 02797e52d..e412313bd 100644 --- a/internal/iam/repository_principal_role.go +++ b/internal/iam/repository_principal_role.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/intglobals" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" ) @@ -24,12 +25,12 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV if roleVersion == 0 { return nil, errors.New(errors.InvalidParameter, op, "missing version") } - userIds, groupIds, err := splitPrincipals(principalIds) + userIds, groupIds, managedGroupIds, err := splitPrincipals(principalIds) if err != nil { return nil, errors.Wrap(err, op) } - if len(userIds) == 0 && len(groupIds) == 0 { - return nil, errors.New(errors.InvalidParameter, op, "missing either user or groups to add") + if len(userIds) == 0 && len(groupIds) == 0 && len(managedGroupIds) == 0 { + return nil, errors.New(errors.InvalidParameter, op, "missing any of users, groups, or managed groups to add") } newUserRoles := make([]interface{}, 0, len(userIds)) @@ -48,6 +49,14 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV } newGrpRoles = append(newGrpRoles, grpRole) } + newManagedGrpRoles := make([]interface{}, 0, len(managedGroupIds)) + for _, id := range managedGroupIds { + managedGrpRole, err := NewManagedGroupRole(roleId, id) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to create in memory managed group role")) + } + newManagedGrpRoles = append(newManagedGrpRoles, managedGrpRole) + } role := allocRole() role.PublicId = roleId @@ -98,6 +107,13 @@ func (r *Repository) AddPrincipalRoles(ctx context.Context, roleId string, roleV } msgs = append(msgs, grpOplogMsgs...) } + if len(newManagedGrpRoles) > 0 { + managedGrpOplogMsgs := make([]*oplog.Message, 0, len(newManagedGrpRoles)) + if err := w.CreateItems(ctx, newManagedGrpRoles, db.NewOplogMsgs(&managedGrpOplogMsgs)); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to add managed groups")) + } + msgs = append(msgs, managedGrpOplogMsgs...) + } metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()}, "scope-id": []string{scope.PublicId}, @@ -147,18 +163,18 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV // it's "safe" to do this lookup outside the DoTx transaction because we // have a roleVersion so the principals can’t change without the version // changing. - userIds, groupIds, err := splitPrincipals(principalIds) + userIds, groupIds, managedGroupIds, err := splitPrincipals(principalIds) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(err, op) } - toSet, err := r.principalsToSet(ctx, &role, userIds, groupIds) + toSet, err := r.PrincipalsToSet(ctx, &role, userIds, groupIds, managedGroupIds) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(err, op) } // handle no change to existing principal roles - if len(toSet.unchangedPrincipalRoles) > 0 { - return toSet.unchangedPrincipalRoles, db.NoRowsAffected, nil + if len(toSet.UnchangedPrincipalRoles) > 0 { + return toSet.UnchangedPrincipalRoles, db.NoRowsAffected, nil } scope, err := role.GetScope(ctx, r.reader) @@ -204,51 +220,75 @@ func (r *Repository) SetPrincipalRoles(ctx context.Context, roleId string, roleV } msgs = append(msgs, &roleOplogMsg) - if len(toSet.deleteUserRoles) > 0 || len(toSet.deleteGroupRoles) > 0 { + if len(toSet.DeleteUserRoles) > 0 || + len(toSet.DeleteGroupRoles) > 0 || + len(toSet.DeleteManagedGroupRoles) > 0 { metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_DELETE.String()) - if len(toSet.deleteUserRoles) > 0 { - userOplogMsgs := make([]*oplog.Message, 0, len(toSet.deleteUserRoles)) - rowsDeleted, err := w.DeleteItems(ctx, toSet.deleteUserRoles, db.NewOplogMsgs(&userOplogMsgs)) + if len(toSet.DeleteUserRoles) > 0 { + userOplogMsgs := make([]*oplog.Message, 0, len(toSet.DeleteUserRoles)) + rowsDeleted, err := w.DeleteItems(ctx, toSet.DeleteUserRoles, db.NewOplogMsgs(&userOplogMsgs)) if err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to delete user roles")) } - if rowsDeleted != len(toSet.deleteUserRoles) { - return errors.New(errors.MultipleRecords, op, fmt.Sprintf("user roles deleted %d did not match request for %d", rowsDeleted, len(toSet.deleteUserRoles))) + if rowsDeleted != len(toSet.DeleteUserRoles) { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("user roles deleted %d did not match request for %d", rowsDeleted, len(toSet.DeleteUserRoles))) } totalRowsAffected += rowsDeleted msgs = append(msgs, userOplogMsgs...) } - if len(toSet.deleteGroupRoles) > 0 { - grpOplogMsgs := make([]*oplog.Message, 0, len(toSet.deleteGroupRoles)) - rowsDeleted, err := w.DeleteItems(ctx, toSet.deleteGroupRoles, db.NewOplogMsgs(&grpOplogMsgs)) + if len(toSet.DeleteGroupRoles) > 0 { + grpOplogMsgs := make([]*oplog.Message, 0, len(toSet.DeleteGroupRoles)) + rowsDeleted, err := w.DeleteItems(ctx, toSet.DeleteGroupRoles, db.NewOplogMsgs(&grpOplogMsgs)) if err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to delete groups")) } - if rowsDeleted != len(toSet.deleteGroupRoles) { - return errors.New(errors.MultipleRecords, op, fmt.Sprintf("group roles deleted %d did not match request for %d", rowsDeleted, len(toSet.deleteGroupRoles))) + if rowsDeleted != len(toSet.DeleteGroupRoles) { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("group roles deleted %d did not match request for %d", rowsDeleted, len(toSet.DeleteGroupRoles))) } totalRowsAffected += rowsDeleted msgs = append(msgs, grpOplogMsgs...) } + if len(toSet.DeleteManagedGroupRoles) > 0 { + managedGrpOplogMsgs := make([]*oplog.Message, 0, len(toSet.DeleteManagedGroupRoles)) + rowsDeleted, err := w.DeleteItems(ctx, toSet.DeleteManagedGroupRoles, db.NewOplogMsgs(&managedGrpOplogMsgs)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to delete managed groups")) + } + if rowsDeleted != len(toSet.DeleteManagedGroupRoles) { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("managed group roles deleted %d did not match request for %d", rowsDeleted, len(toSet.DeleteManagedGroupRoles))) + } + totalRowsAffected += rowsDeleted + msgs = append(msgs, managedGrpOplogMsgs...) + } } - if len(toSet.addUserRoles) > 0 || len(toSet.addGroupRoles) > 0 { + if len(toSet.AddUserRoles) > 0 || + len(toSet.AddGroupRoles) > 0 || + len(toSet.AddManagedGroupRoles) > 0 { metadata["op-type"] = append(metadata["op-type"], oplog.OpType_OP_TYPE_CREATE.String()) - if len(toSet.addUserRoles) > 0 { - userOplogMsgs := make([]*oplog.Message, 0, len(toSet.addUserRoles)) - if err := w.CreateItems(ctx, toSet.addUserRoles, db.NewOplogMsgs(&userOplogMsgs)); err != nil { + if len(toSet.AddUserRoles) > 0 { + userOplogMsgs := make([]*oplog.Message, 0, len(toSet.AddUserRoles)) + if err := w.CreateItems(ctx, toSet.AddUserRoles, db.NewOplogMsgs(&userOplogMsgs)); err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to add users")) } - totalRowsAffected += len(toSet.addUserRoles) + totalRowsAffected += len(toSet.AddUserRoles) msgs = append(msgs, userOplogMsgs...) } - if len(toSet.addGroupRoles) > 0 { - grpOplogMsgs := make([]*oplog.Message, 0, len(toSet.addGroupRoles)) - if err := w.CreateItems(ctx, toSet.addGroupRoles, db.NewOplogMsgs(&grpOplogMsgs)); err != nil { + if len(toSet.AddGroupRoles) > 0 { + grpOplogMsgs := make([]*oplog.Message, 0, len(toSet.AddGroupRoles)) + if err := w.CreateItems(ctx, toSet.AddGroupRoles, db.NewOplogMsgs(&grpOplogMsgs)); err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to add groups")) } - totalRowsAffected += len(toSet.addGroupRoles) + totalRowsAffected += len(toSet.AddGroupRoles) msgs = append(msgs, grpOplogMsgs...) } + if len(toSet.AddManagedGroupRoles) > 0 { + managedGrpOplogMsgs := make([]*oplog.Message, 0, len(toSet.AddManagedGroupRoles)) + if err := w.CreateItems(ctx, toSet.AddManagedGroupRoles, db.NewOplogMsgs(&managedGrpOplogMsgs)); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to add managed groups")) + } + totalRowsAffected += len(toSet.AddManagedGroupRoles) + msgs = append(msgs, managedGrpOplogMsgs...) + } } if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to write oplog")) @@ -282,12 +322,12 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro if roleId == "" { return db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing role id") } - userIds, groupIds, err := splitPrincipals(principalIds) + userIds, groupIds, managedGroupIds, err := splitPrincipals(principalIds) if err != nil { return db.NoRowsAffected, errors.Wrap(err, op) } - if len(userIds) == 0 && len(groupIds) == 0 { - return db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing either user or groups to delete") + if len(userIds) == 0 && len(groupIds) == 0 && len(managedGroupIds) == 0 { + return db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing any of users, groups, or managed groups to delete") } if roleVersion == 0 { return db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing version") @@ -311,6 +351,14 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro } deleteGrpRoles = append(deleteGrpRoles, grpRole) } + deleteManagedGrpRoles := make([]interface{}, 0, len(managedGroupIds)) + for _, id := range managedGroupIds { + managedGrpRole, err := NewManagedGroupRole(roleId, id) + if err != nil { + return db.NoRowsAffected, errors.Wrap(err, op, errors.WithMsg("unable to create in memory managed group role")) + } + deleteManagedGrpRoles = append(deleteManagedGrpRoles, managedGrpRole) + } scope, err := role.GetScope(ctx, r.reader) if err != nil { @@ -368,6 +416,18 @@ func (r *Repository) DeletePrincipalRoles(ctx context.Context, roleId string, ro totalRowsDeleted += rowsDeleted msgs = append(msgs, grpOplogMsgs...) } + if len(deleteManagedGrpRoles) > 0 { + managedGrpOplogMsgs := make([]*oplog.Message, 0, len(deleteManagedGrpRoles)) + rowsDeleted, err := w.DeleteItems(ctx, deleteManagedGrpRoles, db.NewOplogMsgs(&managedGrpOplogMsgs)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to delete managed groups")) + } + if rowsDeleted != len(deleteManagedGrpRoles) { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("managed group roles deleted %d did not match request for %d", rowsDeleted, len(deleteManagedGrpRoles))) + } + totalRowsDeleted += rowsDeleted + msgs = append(msgs, managedGrpOplogMsgs...) + } metadata := oplog.Metadata{ "op-type": []string{oplog.OpType_OP_TYPE_DELETE.String()}, "scope-id": []string{scope.PublicId}, @@ -401,19 +461,22 @@ func (r *Repository) ListPrincipalRoles(ctx context.Context, roleId string, opt return principals, nil } -type principalSet struct { - addUserRoles []interface{} - addGroupRoles []interface{} - deleteUserRoles []interface{} - deleteGroupRoles []interface{} +type PrincipalSet struct { + AddUserRoles []interface{} + AddGroupRoles []interface{} + AddManagedGroupRoles []interface{} + DeleteUserRoles []interface{} + DeleteGroupRoles []interface{} + DeleteManagedGroupRoles []interface{} // unchangedPrincipalRoles is set iff there are no changes, that is, the // length of all other members is zero - unchangedPrincipalRoles []PrincipalRole + 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) { - const op = "iam.(Repository).principalsToSet" +// PrincipalsToSet sets principals on a role from the given lists. +func (r *Repository) PrincipalsToSet(ctx context.Context, role *Role, userIds, groupIds, managedGroupIds []string) (*PrincipalSet, error) { + const op = "iam.(Repository).PrincipalsToSet" // TODO(mgaffney) 08/2020: Use SQL to calculate changes. if role == nil { return nil, errors.New(errors.InvalidParameter, op, "missing role") @@ -424,12 +487,15 @@ func (r *Repository) principalsToSet(ctx context.Context, role *Role, userIds, g } existingUsers := map[string]PrincipalRole{} existingGroups := map[string]PrincipalRole{} + existingManagedGroups := map[string]PrincipalRole{} for _, p := range existing { switch p.GetType() { case UserRoleType.String(): existingUsers[p.PrincipalId] = p case GroupRoleType.String(): existingGroups[p.PrincipalId] = p + case ManagedGroupRoleType.String(): + existingManagedGroups[p.PrincipalId] = p default: return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("%s is unknown principal type %s", p.PrincipalId, p.GetType())) } @@ -458,6 +524,18 @@ func (r *Repository) principalsToSet(ctx context.Context, role *Role, userIds, g newGrpRoles = append(newGrpRoles, grpRole) } } + var newManagedGrpRoles []interface{} + managedGroupIdsMap := map[string]struct{}{} + for _, id := range managedGroupIds { + managedGroupIdsMap[id] = struct{}{} + if _, ok := existingManagedGroups[id]; !ok { + managedGrpRole, err := NewManagedGroupRole(role.PublicId, id) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to create in memory managed group role for add")) + } + newManagedGrpRoles = append(newManagedGrpRoles, managedGrpRole) + } + } var deleteUserRoles []interface{} for _, p := range existingUsers { if _, ok := userIdsMap[p.PrincipalId]; !ok { @@ -478,35 +556,61 @@ func (r *Repository) principalsToSet(ctx context.Context, role *Role, userIds, g deleteGrpRoles = append(deleteGrpRoles, grpRole) } } - - toSet := &principalSet{ - addUserRoles: newUserRoles, - addGroupRoles: newGrpRoles, - deleteUserRoles: deleteUserRoles, - deleteGroupRoles: deleteGrpRoles, + var deleteManagedGrpRoles []interface{} + for _, p := range existingManagedGroups { + if _, ok := managedGroupIdsMap[p.PrincipalId]; !ok { + managedGrpRole, err := NewManagedGroupRole(p.GetRoleId(), p.GetPrincipalId()) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to create in memory managed group role for delete")) + } + deleteManagedGrpRoles = append(deleteManagedGrpRoles, managedGrpRole) + } } - if len(toSet.addUserRoles) == 0 && len(toSet.addGroupRoles) == 0 && len(toSet.deleteUserRoles) == 0 && len(toSet.deleteGroupRoles) == 0 { - toSet.unchangedPrincipalRoles = existing + toSet := &PrincipalSet{ + AddUserRoles: newUserRoles, + AddGroupRoles: newGrpRoles, + AddManagedGroupRoles: newManagedGrpRoles, + DeleteUserRoles: deleteUserRoles, + DeleteGroupRoles: deleteGrpRoles, + DeleteManagedGroupRoles: deleteManagedGrpRoles, + } + + if len(toSet.AddUserRoles) == 0 && + len(toSet.AddGroupRoles) == 0 && + len(toSet.AddManagedGroupRoles) == 0 && + len(toSet.DeleteUserRoles) == 0 && + len(toSet.DeleteGroupRoles) == 0 && + len(toSet.DeleteManagedGroupRoles) == 0 { + toSet.UnchangedPrincipalRoles = existing } return toSet, nil } -func splitPrincipals(principals []string) ([]string, []string, error) { +func splitPrincipals(principals []string) (users, groups, managedGroups []string, retErr error) { const op = "iam.splitPrincipals" - var users, groups []string for _, principal := range principals { switch { case strings.HasPrefix(principal, UserPrefix): + if users == nil { + users = make([]string, 0, len(principals)) + } users = append(users, principal) - // TODO: This needs to handle all of the kinds of group prefixes (sg_, dg_, etc.) case strings.HasPrefix(principal, GroupPrefix): + if groups == nil { + groups = make([]string, 0, len(principals)) + } groups = append(groups, principal) + case strings.HasPrefix(principal, intglobals.OidcManagedGroupPrefix): + if managedGroups == nil { + managedGroups = make([]string, 0, len(principals)) + } + managedGroups = append(managedGroups, principal) default: - return nil, nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("invalid principal ID %q", principal)) + return nil, nil, nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("invalid principal ID %q", principal)) } } - return users, groups, nil + return } diff --git a/internal/iam/repository_principal_role_ext_test.go b/internal/iam/repository_principal_role_ext_test.go new file mode 100644 index 000000000..8ab944a80 --- /dev/null +++ b/internal/iam/repository_principal_role_ext_test.go @@ -0,0 +1,251 @@ +package iam_test + +import ( + "context" + "sort" + "testing" + + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_PrincipalsToSet(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + + repo := iam.TestRepo(t, conn, wrap) + org, proj := iam.TestScopes(t, repo) + + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := oidc.TestAuthMethod( + t, conn, databaseWrapper, org.GetPublicId(), oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + createUsersFn := func() []string { + results := []string{} + for i := 0; i < 5; i++ { + u := iam.TestUser(t, repo, org.PublicId) + results = append(results, u.PublicId) + } + return results + } + createGrpsFn := func() []string { + results := []string{} + for i := 0; i < 5; i++ { + g := iam.TestGroup(t, conn, proj.PublicId) + results = append(results, g.PublicId) + } + return results + } + createManagedGrpsFn := func() []string { + results := []string{} + for i := 0; i < 5; i++ { + g := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter) + results = append(results, g.PublicId) + } + return results + } + setupFn := func() (*iam.Role, []string, []string, []string) { + users := createUsersFn() + grps := createGrpsFn() + managedGrps := createManagedGrpsFn() + role := iam.TestRole(t, conn, proj.PublicId) + _, err := repo.AddPrincipalRoles(context.Background(), role.PublicId, 1, append(users, append(grps, managedGrps...)...)) + require.NoError(t, err) + return role, users, grps, managedGrps + } + + type args struct { + userIds []string + groupIds []string + managedGroupIds []string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "all new", + args: args{ + userIds: createUsersFn(), + groupIds: createGrpsFn(), + managedGroupIds: createManagedGrpsFn(), + }, + wantErr: false, + }, + { + name: "clear all", + args: args{ + userIds: nil, + groupIds: nil, + managedGroupIds: nil, + }, + wantErr: false, + }, + { + name: "just new users", + args: args{ + userIds: createUsersFn(), + groupIds: nil, + managedGroupIds: nil, + }, + wantErr: false, + }, + { + name: "just new groups", + args: args{ + userIds: nil, + groupIds: createGrpsFn(), + managedGroupIds: nil, + }, + wantErr: false, + }, + { + name: "just new managed groups", + args: args{ + userIds: nil, + groupIds: nil, + managedGroupIds: createManagedGrpsFn(), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + r, origUsers, origGrps, origManagedGrps := setupFn() + got, err := repo.PrincipalsToSet(context.Background(), r, tt.args.userIds, tt.args.groupIds, tt.args.managedGroupIds) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + assertSetResults(t, got, tt.args.userIds, tt.args.groupIds, tt.args.managedGroupIds, origUsers, origGrps, origManagedGrps) + }) + } + t.Run("nil role", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + _, users, grps, managedGrps := setupFn() + got, err := repo.PrincipalsToSet(context.Background(), nil, users, grps, managedGrps) + require.Error(err) + assert.Nil(got) + assert.Truef(errors.Match(errors.T(errors.InvalidParameter), err), "unexpected error %s", err.Error()) + }) + t.Run("no change", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + r, users, grps, managedGrps := setupFn() + got, err := repo.PrincipalsToSet(context.Background(), r, users, grps, managedGrps) + require.NoError(err) + assert.Empty(got.AddUserRoles) + assert.Empty(got.AddGroupRoles) + assert.Empty(got.AddManagedGroupRoles) + assert.Empty(got.DeleteUserRoles) + assert.Empty(got.DeleteGroupRoles) + assert.Empty(got.DeleteManagedGroupRoles) + assert.Equal(len(users)+len(grps)+len(managedGrps), len(got.UnchangedPrincipalRoles)) + }) + t.Run("mixed", func(t *testing.T) { + require := require.New(t) + r, users, grps, managedGrps := setupFn() + var wantSetUsers, wantSetGrps, wantSetManagedGrps, wantDeleteUsers, wantDeleteGrps, wantDeleteManagedGrps []string + for i, id := range users { + if i < 2 { + wantSetUsers = append(wantSetUsers, id) + } else { + wantDeleteUsers = append(wantDeleteUsers, id) + } + } + for i, id := range grps { + if i < 2 { + wantSetGrps = append(wantSetGrps, id) + } else { + wantDeleteGrps = append(wantDeleteGrps, id) + } + } + for i, id := range managedGrps { + if i < 2 { + wantSetManagedGrps = append(wantSetManagedGrps, id) + } else { + wantDeleteManagedGrps = append(wantDeleteManagedGrps, id) + } + } + newUser := iam.TestUser(t, repo, org.PublicId) + newGrp := iam.TestGroup(t, conn, proj.PublicId) + newManagedGrp := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter) + wantSetUsers = append(wantSetUsers, newUser.PublicId) + wantSetGrps = append(wantSetGrps, newGrp.PublicId) + wantSetManagedGrps = append(wantSetManagedGrps, newManagedGrp.PublicId) + + got, err := repo.PrincipalsToSet(context.Background(), r, wantSetUsers, wantSetGrps, wantSetManagedGrps) + require.NoError(err) + assertSetResults(t, got, []string{newUser.PublicId}, []string{newGrp.PublicId}, []string{newManagedGrp.PublicId}, wantDeleteUsers, wantDeleteGrps, wantDeleteManagedGrps) + }) +} + +func assertSetResults(t *testing.T, got *iam.PrincipalSet, wantAddUsers, wantAddGroups, wantAddManagedGroups, wantDeleteUsers, wantDeleteGroups, wantDeleteManagedGroups []string) { + t.Helper() + assert := assert.New(t) + var gotAddUsers []string + for _, r := range got.AddUserRoles { + gotAddUsers = append(gotAddUsers, r.(*iam.UserRole).PrincipalId) + } + // sort.Strings(wantAddUsers) + // sort.Strings(gotAddUsers) + assert.Equal(wantAddUsers, gotAddUsers) + + var gotAddGrps []string + for _, r := range got.AddGroupRoles { + gotAddGrps = append(gotAddGrps, r.(*iam.GroupRole).PrincipalId) + } + // sort.Strings(wantAddGroups) + // sort.Strings(gotAddGrps) + assert.Equal(wantAddGroups, gotAddGrps) + + var gotAddManagedGrps []string + for _, r := range got.AddManagedGroupRoles { + gotAddManagedGrps = append(gotAddManagedGrps, r.(*iam.ManagedGroupRole).PrincipalId) + } + // sort.Strings(wantAddGroups) + // sort.Strings(gotAddGrps) + assert.Equal(wantAddManagedGroups, gotAddManagedGrps) + + var gotDeleteUsers []string + for _, r := range got.DeleteUserRoles { + gotDeleteUsers = append(gotDeleteUsers, r.(*iam.UserRole).PrincipalId) + } + sort.Strings(wantDeleteUsers) + sort.Strings(gotDeleteUsers) + assert.Equal(wantDeleteUsers, gotDeleteUsers) + + var gotDeleteGroups []string + for _, r := range got.DeleteGroupRoles { + gotDeleteGroups = append(gotDeleteGroups, r.(*iam.GroupRole).PrincipalId) + } + sort.Strings(wantDeleteGroups) + sort.Strings(gotDeleteGroups) + assert.Equal(wantDeleteGroups, gotDeleteGroups) + + var gotDeleteManagedGroups []string + for _, r := range got.DeleteManagedGroupRoles { + gotDeleteManagedGroups = append(gotDeleteManagedGroups, r.(*iam.ManagedGroupRole).PrincipalId) + } + sort.Strings(wantDeleteManagedGroups) + sort.Strings(gotDeleteManagedGroups) + assert.Equal(wantDeleteManagedGroups, gotDeleteManagedGroups) +} diff --git a/internal/iam/repository_principal_role_test.go b/internal/iam/repository_principal_role_test.go index e04298b99..9ac480aef 100644 --- a/internal/iam/repository_principal_role_test.go +++ b/internal/iam/repository_principal_role_test.go @@ -677,173 +677,3 @@ func TestRepository_SetPrincipalRoles(t *testing.T) { }) } } - -func TestRepository_principalsToSet(t *testing.T) { - t.Parallel() - conn, _ := db.TestSetup(t, "postgres") - wrapper := db.TestWrapper(t) - repo := TestRepo(t, conn, wrapper) - org, proj := TestScopes(t, repo) - createUsersFn := func() []string { - results := []string{} - for i := 0; i < 5; i++ { - u := TestUser(t, repo, org.PublicId) - results = append(results, u.PublicId) - } - return results - } - createGrpsFn := func() []string { - results := []string{} - for i := 0; i < 5; i++ { - g := TestGroup(t, conn, proj.PublicId) - results = append(results, g.PublicId) - } - return results - } - setupFn := func() (*Role, []string, []string) { - users := createUsersFn() - grps := createGrpsFn() - role := TestRole(t, conn, proj.PublicId) - _, err := repo.AddPrincipalRoles(context.Background(), role.PublicId, 1, append(users, grps...)) - require.NoError(t, err) - return role, users, grps - } - - type args struct { - userIds []string - groupIds []string - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "all new", - args: args{ - userIds: createUsersFn(), - groupIds: createGrpsFn(), - }, - wantErr: false, - }, - { - name: "clear all", - args: args{ - userIds: nil, - groupIds: nil, - }, - wantErr: false, - }, - { - name: "just new users", - args: args{ - userIds: createUsersFn(), - groupIds: nil, - }, - wantErr: false, - }, - { - name: "just new groups", - args: args{ - userIds: nil, - groupIds: createGrpsFn(), - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require := require.New(t) - r, origUsers, origGrps := setupFn() - got, err := repo.principalsToSet(context.Background(), r, tt.args.userIds, tt.args.groupIds) - if tt.wantErr { - require.Error(err) - return - } - require.NoError(err) - assertSetResults(t, got, tt.args.userIds, tt.args.groupIds, origUsers, origGrps) - }) - } - t.Run("nil role", func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - _, users, grps := setupFn() - got, err := repo.principalsToSet(context.Background(), nil, users, grps) - require.Error(err) - assert.Nil(got) - assert.Truef(errors.Match(errors.T(errors.InvalidParameter), err), "unexpected error %s", err.Error()) - }) - t.Run("no change", func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - r, users, grps := setupFn() - got, err := repo.principalsToSet(context.Background(), r, users, grps) - require.NoError(err) - assert.Empty(got.addUserRoles) - assert.Empty(got.addGroupRoles) - assert.Empty(got.deleteUserRoles) - assert.Empty(got.deleteGroupRoles) - assert.Equal(len(users)+len(grps), len(got.unchangedPrincipalRoles)) - }) - t.Run("mixed", func(t *testing.T) { - require := require.New(t) - r, users, grps := setupFn() - var wantSetUsers, wantSetGrps, wantDeleteUsers, wantDeleteGrps []string - for i, id := range users { - if i < 2 { - wantSetUsers = append(wantSetUsers, id) - } else { - wantDeleteUsers = append(wantDeleteUsers, id) - } - } - for i, id := range grps { - if i < 2 { - wantSetGrps = append(wantSetGrps, id) - } else { - wantDeleteGrps = append(wantDeleteGrps, id) - } - } - newUser := TestUser(t, repo, org.PublicId) - newGrp := TestGroup(t, conn, proj.PublicId) - wantSetUsers = append(wantSetUsers, newUser.PublicId) - wantSetGrps = append(wantSetGrps, newGrp.PublicId) - - got, err := repo.principalsToSet(context.Background(), r, wantSetUsers, wantSetGrps) - require.NoError(err) - assertSetResults(t, got, []string{newUser.PublicId}, []string{newGrp.PublicId}, wantDeleteUsers, wantDeleteGrps) - }) -} - -func assertSetResults(t *testing.T, got *principalSet, wantAddUsers, wantAddGroups, wantDeleteUsers, wantDeleteGroups []string) { - t.Helper() - assert := assert.New(t) - var gotAddUsers []string - for _, r := range got.addUserRoles { - gotAddUsers = append(gotAddUsers, r.(*UserRole).PrincipalId) - } - // sort.Strings(wantAddUsers) - // sort.Strings(gotAddUsers) - assert.Equal(wantAddUsers, gotAddUsers) - - var gotAddGrps []string - for _, r := range got.addGroupRoles { - gotAddGrps = append(gotAddGrps, r.(*GroupRole).PrincipalId) - } - // sort.Strings(wantAddGroups) - // sort.Strings(gotAddGrps) - assert.Equal(wantAddGroups, gotAddGrps) - - var gotDeleteUsers []string - for _, r := range got.deleteUserRoles { - gotDeleteUsers = append(gotDeleteUsers, r.(*UserRole).PrincipalId) - } - sort.Strings(wantDeleteUsers) - sort.Strings(gotDeleteUsers) - assert.Equal(wantDeleteUsers, gotDeleteUsers) - - var gotDeleteGroups []string - for _, r := range got.deleteGroupRoles { - gotDeleteGroups = append(gotDeleteGroups, r.(*GroupRole).PrincipalId) - } - sort.Strings(wantDeleteGroups) - sort.Strings(gotDeleteGroups) - assert.Equal(wantDeleteGroups, gotDeleteGroups) -} diff --git a/internal/iam/store/principal_role.pb.go b/internal/iam/store/principal_role.pb.go index fd97ae7d4..83fa05db5 100644 --- a/internal/iam/store/principal_role.pb.go +++ b/internal/iam/store/principal_role.pb.go @@ -159,6 +159,75 @@ func (x *GroupRole) GetPrincipalId() string { return "" } +type ManagedGroupRole struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,1,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // role_id is the role of this principal. + // @inject_tag: gorm:"primary_key" + RoleId string `protobuf:"bytes,2,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty" gorm:"primary_key"` + // principal_id is the public_id of the managed 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"` +} + +func (x *ManagedGroupRole) Reset() { + *x = ManagedGroupRole{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ManagedGroupRole) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ManagedGroupRole) ProtoMessage() {} + +func (x *ManagedGroupRole) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ManagedGroupRole.ProtoReflect.Descriptor instead. +func (*ManagedGroupRole) Descriptor() ([]byte, []int) { + return file_controller_storage_iam_store_v1_principal_role_proto_rawDescGZIP(), []int{2} +} + +func (x *ManagedGroupRole) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *ManagedGroupRole) GetRoleId() string { + if x != nil { + return x.RoleId + } + return "" +} + +func (x *ManagedGroupRole) GetPrincipalId() string { + if x != nil { + return x.PrincipalId + } + return "" +} + type PrincipalRoleView struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -188,7 +257,7 @@ type PrincipalRoleView struct { func (x *PrincipalRoleView) Reset() { *x = PrincipalRoleView{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[2] + mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -201,7 +270,7 @@ func (x *PrincipalRoleView) String() string { func (*PrincipalRoleView) ProtoMessage() {} func (x *PrincipalRoleView) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[2] + mi := &file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -214,7 +283,7 @@ func (x *PrincipalRoleView) ProtoReflect() protoreflect.Message { // Deprecated: Use PrincipalRoleView.ProtoReflect.Descriptor instead. func (*PrincipalRoleView) Descriptor() ([]byte, []int) { - return file_controller_storage_iam_store_v1_principal_role_proto_rawDescGZIP(), []int{2} + return file_controller_storage_iam_store_v1_principal_role_proto_rawDescGZIP(), []int{3} } func (x *PrincipalRoleView) GetCreateTime() *timestamp.Timestamp { @@ -296,30 +365,40 @@ var file_controller_storage_iam_store_v1_principal_role_proto_rawDesc = []byte{ 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, 0x38, 0x5a, 0x36, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, - 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, - 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x70, 0x61, 0x6c, 0x49, 0x64, 0x22, 0x9b, 0x01, 0x0a, 0x10, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x64, 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, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -334,22 +413,24 @@ func file_controller_storage_iam_store_v1_principal_role_proto_rawDescGZIP() []b return file_controller_storage_iam_store_v1_principal_role_proto_rawDescData } -var file_controller_storage_iam_store_v1_principal_role_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_storage_iam_store_v1_principal_role_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_controller_storage_iam_store_v1_principal_role_proto_goTypes = []interface{}{ (*UserRole)(nil), // 0: controller.storage.iam.store.v1.UserRole (*GroupRole)(nil), // 1: controller.storage.iam.store.v1.GroupRole - (*PrincipalRoleView)(nil), // 2: controller.storage.iam.store.v1.PrincipalRoleView - (*timestamp.Timestamp)(nil), // 3: controller.storage.timestamp.v1.Timestamp + (*ManagedGroupRole)(nil), // 2: controller.storage.iam.store.v1.ManagedGroupRole + (*PrincipalRoleView)(nil), // 3: controller.storage.iam.store.v1.PrincipalRoleView + (*timestamp.Timestamp)(nil), // 4: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_iam_store_v1_principal_role_proto_depIdxs = []int32{ - 3, // 0: controller.storage.iam.store.v1.UserRole.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 1: controller.storage.iam.store.v1.GroupRole.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 2: controller.storage.iam.store.v1.PrincipalRoleView.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 4, // 0: controller.storage.iam.store.v1.UserRole.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 1: controller.storage.iam.store.v1.GroupRole.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 2: controller.storage.iam.store.v1.ManagedGroupRole.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 3: controller.storage.iam.store.v1.PrincipalRoleView.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_controller_storage_iam_store_v1_principal_role_proto_init() } @@ -383,6 +464,18 @@ func file_controller_storage_iam_store_v1_principal_role_proto_init() { } } file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ManagedGroupRole); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_iam_store_v1_principal_role_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PrincipalRoleView); i { case 0: return &v.state @@ -401,7 +494,7 @@ func file_controller_storage_iam_store_v1_principal_role_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_iam_store_v1_principal_role_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/iam/testing.go b/internal/iam/testing.go index 5741d2e39..68babe53d 100644 --- a/internal/iam/testing.go +++ b/internal/iam/testing.go @@ -242,6 +242,18 @@ func TestGroupRole(t *testing.T, conn *gorm.DB, roleId, grpId string, opt ...Opt return r } +func TestManagedGroupRole(t *testing.T, conn *gorm.DB, roleId, managedGrpId string, opt ...Option) *ManagedGroupRole { + t.Helper() + require := require.New(t) + rw := db.New(conn) + r, err := NewManagedGroupRole(roleId, managedGrpId, opt...) + require.NoError(err) + + err = rw.Create(context.Background(), r) + require.NoError(err) + return r +} + // testAccount is a temporary test function. TODO - replace with an auth // subsystem testAccount function. If userId is zero value, then an auth // account will be created with a null IamUserId diff --git a/internal/intglobals/prefixes.go b/internal/intglobals/prefixes.go new file mode 100644 index 000000000..8c1e951b4 --- /dev/null +++ b/internal/intglobals/prefixes.go @@ -0,0 +1,12 @@ +package intglobals + +// This set of consts is intended to be a place to collect commonly-used +// prefixes. This can avoid some cross-package dependency issues. Unlike the +// top-level globals package, these are not currently meant to be available to +// clients or test writers. + +const ( + // OidcManagedGroupPrefix defines the prefix for ManagedGroup public ids + // from the OIDC auth method. + OidcManagedGroupPrefix = "mgoidc" +) diff --git a/internal/proto/local/controller/storage/iam/store/v1/principal_role.proto b/internal/proto/local/controller/storage/iam/store/v1/principal_role.proto index 2cbde85ac..de73e6003 100644 --- a/internal/proto/local/controller/storage/iam/store/v1/principal_role.proto +++ b/internal/proto/local/controller/storage/iam/store/v1/principal_role.proto @@ -34,6 +34,20 @@ message GroupRole { string principal_id = 3; } +message ManagedGroupRole { + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 1; + + // role_id is the role of this principal. + // @inject_tag: gorm:"primary_key" + string role_id = 2; + + // principal_id is the public_id of the managed group (which is the principal) + // @inject_tag: gorm:"primary_key" + string principal_id = 3; +} + message PrincipalRoleView { // create_time from the RDBMS // @inject_tag: `gorm:"default:current_timestamp"` diff --git a/internal/servers/controller/handlers/managed_groups/managed_group_service.go b/internal/servers/controller/handlers/managed_groups/managed_group_service.go index af605735d..e85879703 100644 --- a/internal/servers/controller/handlers/managed_groups/managed_group_service.go +++ b/internal/servers/controller/handlers/managed_groups/managed_group_service.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/managedgroups" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/intglobals" "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" @@ -576,7 +577,7 @@ func validateGetRequest(req *pbs.GetManagedGroupRequest) error { if req == nil { return errors.New(errors.InvalidParameter, op, "nil request") } - return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, oidc.ManagedGroupPrefix) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, intglobals.OidcManagedGroupPrefix) } func validateCreateRequest(req *pbs.CreateManagedGroupRequest) error { @@ -639,7 +640,7 @@ func validateUpdateRequest(req *pbs.UpdateManagedGroupRequest) error { } } return badFields - }, oidc.ManagedGroupPrefix) + }, intglobals.OidcManagedGroupPrefix) } func validateDeleteRequest(req *pbs.DeleteManagedGroupRequest) error { @@ -647,7 +648,7 @@ func validateDeleteRequest(req *pbs.DeleteManagedGroupRequest) error { if req == nil { return errors.New(errors.InvalidParameter, op, "nil request") } - return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, oidc.ManagedGroupPrefix) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, intglobals.OidcManagedGroupPrefix) } func validateListRequest(req *pbs.ListManagedGroupsRequest) error { diff --git a/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go b/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go index d8168c9d4..a875e3455 100644 --- a/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go +++ b/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go @@ -19,6 +19,7 @@ import ( scopepb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/intglobals" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/handlers" @@ -42,8 +43,6 @@ var oidcAuthorizedActions = []string{ action.Delete.String(), } -const testFakeManagedGroupFilter = `"/foo" == "bar"` - func TestNewService(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) @@ -107,7 +106,7 @@ func TestGet(t *testing.T) { oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) oidcA := oidc.TestAccount(t, conn, oidcAm, "test-subject") - omg := oidc.TestManagedGroup(t, conn, oidcAm, testFakeManagedGroupFilter) + omg := oidc.TestManagedGroup(t, conn, oidcAm, oidc.TestFakeManagedGroupFilter) // Set up managed group before getting. First get the current // managed group to make sure we have the right version, then ensure @@ -148,7 +147,7 @@ func TestGet(t *testing.T) { }, { name: "Get a non existing oidc managed group", - req: &pbs.GetManagedGroupRequest{Id: oidc.ManagedGroupPrefix + "_DoesntExis"}, + req: &pbs.GetManagedGroupRequest{Id: intglobals.OidcManagedGroupPrefix + "_DoesntExis"}, res: nil, err: handlers.ApiErrorWithCode(codes.NotFound), }, @@ -205,7 +204,7 @@ func TestListOidc(t *testing.T) { var wantSomeManagedGroups []*pb.ManagedGroup for i := 0; i < 3; i++ { - mg := oidc.TestManagedGroup(t, conn, amSomeManagedGroups, testFakeManagedGroupFilter, oidc.WithName(strconv.Itoa(i))) + mg := oidc.TestManagedGroup(t, conn, amSomeManagedGroups, oidc.TestFakeManagedGroupFilter, oidc.WithName(strconv.Itoa(i))) wantSomeManagedGroups = append(wantSomeManagedGroups, &pb.ManagedGroup{ Id: mg.GetPublicId(), AuthMethodId: mg.GetAuthMethodId(), @@ -216,7 +215,7 @@ func TestListOidc(t *testing.T) { Version: 1, Type: auth.OidcSubtype.String(), Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ - "filter": structpb.NewStringValue(testFakeManagedGroupFilter), + "filter": structpb.NewStringValue(oidc.TestFakeManagedGroupFilter), }}, AuthorizedActions: oidcAuthorizedActions, }) @@ -224,7 +223,7 @@ func TestListOidc(t *testing.T) { var wantOtherManagedGroups []*pb.ManagedGroup for i := 0; i < 3; i++ { - mg := oidc.TestManagedGroup(t, conn, amOtherManagedGroups, testFakeManagedGroupFilter, oidc.WithName(strconv.Itoa(i))) + mg := oidc.TestManagedGroup(t, conn, amOtherManagedGroups, oidc.TestFakeManagedGroupFilter, oidc.WithName(strconv.Itoa(i))) wantOtherManagedGroups = append(wantOtherManagedGroups, &pb.ManagedGroup{ Id: mg.GetPublicId(), AuthMethodId: mg.GetAuthMethodId(), @@ -235,7 +234,7 @@ func TestListOidc(t *testing.T) { Version: 1, Type: auth.OidcSubtype.String(), Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ - "filter": structpb.NewStringValue(testFakeManagedGroupFilter), + "filter": structpb.NewStringValue(oidc.TestFakeManagedGroupFilter), }}, AuthorizedActions: oidcAuthorizedActions, }) @@ -352,7 +351,7 @@ func TestDelete(t *testing.T) { oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) - oidcMg := oidc.TestManagedGroup(t, conn, oidcAm, testFakeManagedGroupFilter) + oidcMg := oidc.TestManagedGroup(t, conn, oidcAm, oidc.TestFakeManagedGroupFilter) s, err := managed_groups.NewService(oidcRepoFn) require.NoError(t, err, "Error when getting new user service.") @@ -373,7 +372,7 @@ func TestDelete(t *testing.T) { { name: "Delete bad oidc managed group id", req: &pbs.DeleteManagedGroupRequest{ - Id: oidc.ManagedGroupPrefix + "_doesntexis", + Id: intglobals.OidcManagedGroupPrefix + "_doesntexis", }, err: handlers.ApiErrorWithCode(codes.NotFound), }, @@ -423,7 +422,7 @@ func TestDelete_twice(t *testing.T) { oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) - oidcMg := oidc.TestManagedGroup(t, conn, oidcAm, testFakeManagedGroupFilter) + oidcMg := oidc.TestManagedGroup(t, conn, oidcAm, oidc.TestFakeManagedGroupFilter) s, err := managed_groups.NewService(oidcRepoFn) require.NoError(err, "Error when getting new user service") @@ -465,7 +464,7 @@ func TestCreateOidc(t *testing.T) { ) createAttr := func() *structpb.Struct { - attr := &pb.OidcManagedGroupAttributes{Filter: testFakeManagedGroupFilter} + attr := &pb.OidcManagedGroupAttributes{Filter: oidc.TestFakeManagedGroupFilter} ret, err := handlers.ProtoToStruct(attr) require.NoError(t, err, "Error converting proto to struct.") return ret @@ -489,7 +488,7 @@ func TestCreateOidc(t *testing.T) { }, }, res: &pbs.CreateManagedGroupResponse{ - Uri: fmt.Sprintf("managed-groups/%s_", oidc.ManagedGroupPrefix), + Uri: fmt.Sprintf("managed-groups/%s_", intglobals.OidcManagedGroupPrefix), Item: &pb.ManagedGroup{ AuthMethodId: am.GetPublicId(), Name: &wrapperspb.StringValue{Value: "name"}, @@ -499,7 +498,7 @@ func TestCreateOidc(t *testing.T) { Type: auth.OidcSubtype.String(), Attributes: func() *structpb.Struct { a := createAttr() - a.Fields["filter"] = structpb.NewStringValue(testFakeManagedGroupFilter) + a.Fields["filter"] = structpb.NewStringValue(oidc.TestFakeManagedGroupFilter) return a }(), AuthorizedActions: oidcAuthorizedActions, @@ -515,7 +514,7 @@ func TestCreateOidc(t *testing.T) { }, }, res: &pbs.CreateManagedGroupResponse{ - Uri: fmt.Sprintf("managed-groups/%s_", oidc.ManagedGroupPrefix), + Uri: fmt.Sprintf("managed-groups/%s_", intglobals.OidcManagedGroupPrefix), Item: &pb.ManagedGroup{ AuthMethodId: am.GetPublicId(), Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, @@ -523,7 +522,7 @@ func TestCreateOidc(t *testing.T) { Type: auth.OidcSubtype.String(), Attributes: func() *structpb.Struct { a := createAttr() - a.Fields["filter"] = structpb.NewStringValue(testFakeManagedGroupFilter) + a.Fields["filter"] = structpb.NewStringValue(oidc.TestFakeManagedGroupFilter) return a }(), AuthorizedActions: oidcAuthorizedActions, @@ -547,7 +546,7 @@ func TestCreateOidc(t *testing.T) { req: &pbs.CreateManagedGroupRequest{ Item: &pb.ManagedGroup{ AuthMethodId: am.GetPublicId(), - Id: oidc.ManagedGroupPrefix + "_notallowed", + Id: intglobals.OidcManagedGroupPrefix + "_notallowed", Type: auth.OidcSubtype.String(), Attributes: createAttr(), }, @@ -611,7 +610,7 @@ func TestCreateOidc(t *testing.T) { require.NoError(gErr) if got != nil { assert.Contains(got.GetUri(), tc.res.Uri) - assert.True(strings.HasPrefix(got.GetItem().GetId(), oidc.ManagedGroupPrefix+"_")) + assert.True(strings.HasPrefix(got.GetItem().GetId(), intglobals.OidcManagedGroupPrefix+"_")) // Clear all values which are hard to compare against. got.Uri, tc.res.Uri = "", "" got.Item.Id, tc.res.Item.Id = "", "" @@ -651,7 +650,7 @@ func TestUpdateOidc(t *testing.T) { defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} defaultAttributes := &structpb.Struct{Fields: map[string]*structpb.Value{ - "filter": structpb.NewStringValue(testFakeManagedGroupFilter), + "filter": structpb.NewStringValue(oidc.TestFakeManagedGroupFilter), }} modifiedFilter := `"/token/zip" == "zap"` @@ -666,7 +665,7 @@ func TestUpdateOidc(t *testing.T) { freshManagedGroup := func(t *testing.T) (*oidc.ManagedGroup, func()) { t.Helper() - mg := oidc.TestManagedGroup(t, conn, am, testFakeManagedGroupFilter, oidc.WithName("default"), oidc.WithDescription("default")) + mg := oidc.TestManagedGroup(t, conn, am, oidc.TestFakeManagedGroupFilter, oidc.WithName("default"), oidc.WithDescription("default")) clean := func() { _, err := tested.DeleteManagedGroup(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), @@ -852,7 +851,7 @@ func TestUpdateOidc(t *testing.T) { { name: "Update a Non Existing ManagedGroup", req: &pbs.UpdateManagedGroupRequest{ - Id: oidc.ManagedGroupPrefix + "_DoesntExis", + Id: intglobals.OidcManagedGroupPrefix + "_DoesntExis", UpdateMask: &field_mask.FieldMask{ Paths: []string{globals.DescriptionField}, }, @@ -870,7 +869,7 @@ func TestUpdateOidc(t *testing.T) { Paths: []string{"id"}, }, Item: &pb.ManagedGroup{ - Id: oidc.ManagedGroupPrefix + "_somethinge", + Id: intglobals.OidcManagedGroupPrefix + "_somethinge", Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "new desc"}, }, diff --git a/internal/servers/controller/handlers/managed_groups/validate_test.go b/internal/servers/controller/handlers/managed_groups/validate_test.go index 0ef710844..c1048cc73 100644 --- a/internal/servers/controller/handlers/managed_groups/validate_test.go +++ b/internal/servers/controller/handlers/managed_groups/validate_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/boundary/internal/auth/oidc" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/managedgroups" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/intglobals" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/fieldmaskpb" @@ -98,7 +99,7 @@ func TestValidateUpdateRequest(t *testing.T) { { name: "oidc to password change type", req: &pbs.UpdateManagedGroupRequest{ - Id: oidc.ManagedGroupPrefix + "_1234567890", + Id: intglobals.OidcManagedGroupPrefix + "_1234567890", Item: &pb.ManagedGroup{ Type: auth.PasswordSubtype.String(), }, @@ -108,7 +109,7 @@ func TestValidateUpdateRequest(t *testing.T) { { name: "oidc bad attributes", req: &pbs.UpdateManagedGroupRequest{ - Id: oidc.ManagedGroupPrefix + "_1234567890", + Id: intglobals.OidcManagedGroupPrefix + "_1234567890", Item: &pb.ManagedGroup{ Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "test": structpb.NewStringValue("something"), @@ -120,7 +121,7 @@ func TestValidateUpdateRequest(t *testing.T) { { name: "no error", req: &pbs.UpdateManagedGroupRequest{ - Id: oidc.ManagedGroupPrefix + "_1234567890", + Id: intglobals.OidcManagedGroupPrefix + "_1234567890", UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}}, Item: &pb.ManagedGroup{ Version: 1, diff --git a/internal/servers/controller/handlers/roles/role_service.go b/internal/servers/controller/handlers/roles/role_service.go index 5d3a2bbc8..57dc33594 100644 --- a/internal/servers/controller/handlers/roles/role_service.go +++ b/internal/servers/controller/handlers/roles/role_service.go @@ -11,6 +11,7 @@ import ( pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/iam/store" + "github.com/hashicorp/boundary/internal/intglobals" "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/common" @@ -967,8 +968,10 @@ func validateAddRolePrincipalsRequest(req *pbs.AddRolePrincipalsRequest) error { badFields["principal_ids"] = "Must be non-empty." } for _, id := range req.GetPrincipalIds() { - if !handlers.ValidId(handlers.Id(id), iam.GroupPrefix) && !handlers.ValidId(handlers.Id(id), iam.UserPrefix) { - badFields["principal_ids"] = "Must only have valid group and/or user ids." + if !handlers.ValidId(handlers.Id(id), iam.GroupPrefix) && + !handlers.ValidId(handlers.Id(id), iam.UserPrefix) && + !handlers.ValidId(handlers.Id(id), intglobals.OidcManagedGroupPrefix) { + badFields["principal_ids"] = "Must only have valid user, group, and/or managed group ids." break } if id == "u_recovery" { @@ -991,8 +994,10 @@ func validateSetRolePrincipalsRequest(req *pbs.SetRolePrincipalsRequest) error { badFields["version"] = "Required field." } for _, id := range req.GetPrincipalIds() { - if !handlers.ValidId(handlers.Id(id), iam.GroupPrefix) && !handlers.ValidId(handlers.Id(id), iam.UserPrefix) { - badFields["principal_ids"] = "Must only have valid group and/or user ids." + if !handlers.ValidId(handlers.Id(id), iam.GroupPrefix) && + !handlers.ValidId(handlers.Id(id), iam.UserPrefix) && + !handlers.ValidId(handlers.Id(id), intglobals.OidcManagedGroupPrefix) { + badFields["principal_ids"] = "Must only have valid user, group, and/or managed group ids." break } if id == "u_recovery" { @@ -1018,8 +1023,10 @@ func validateRemoveRolePrincipalsRequest(req *pbs.RemoveRolePrincipalsRequest) e badFields["principal_ids"] = "Must be non-empty." } for _, id := range req.GetPrincipalIds() { - if !handlers.ValidId(handlers.Id(id), iam.GroupPrefix) && !handlers.ValidId(handlers.Id(id), iam.UserPrefix) { - badFields["principal_ids"] = "Must only have valid group and/or user ids." + if !handlers.ValidId(handlers.Id(id), iam.GroupPrefix) && + !handlers.ValidId(handlers.Id(id), iam.UserPrefix) && + !handlers.ValidId(handlers.Id(id), intglobals.OidcManagedGroupPrefix) { + badFields["principal_ids"] = "Must only have valid user, group, and/or managed group ids." break } } diff --git a/internal/servers/controller/handlers/roles/role_service_test.go b/internal/servers/controller/handlers/roles/role_service_test.go index c339e6b10..d9cef7986 100644 --- a/internal/servers/controller/handlers/roles/role_service_test.go +++ b/internal/servers/controller/handlers/roles/role_service_test.go @@ -9,15 +9,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/db" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/roles" "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/servers/controller/handlers/roles" "github.com/hashicorp/boundary/internal/types/scope" + "github.com/kr/pretty" "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -1010,10 +1013,23 @@ func TestAddPrincipal(t *testing.T) { repoFn := func() (*iam.Repository, error) { return iamRepo, nil } + o, p := iam.TestScopes(t, iamRepo) s, err := roles.NewService(repoFn) require.NoError(t, err, "Error when getting new role service.") - o, p := iam.TestScopes(t, iamRepo) + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrap) + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := oidc.TestAuthMethod( + t, conn, databaseWrapper, o.GetPublicId(), oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + users := []*iam.User{ iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), @@ -1024,15 +1040,22 @@ func TestAddPrincipal(t *testing.T) { iam.TestGroup(t, conn, o.GetPublicId()), iam.TestGroup(t, conn, o.GetPublicId()), } + managedGroups := []*oidc.ManagedGroup{ + oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter), + oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter), + oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter), + } addCases := []struct { - name string - setup func(*iam.Role) - addUsers []string - addGroups []string - resultUsers []string - resultGroups []string - wantErr bool + name string + setup func(*iam.Role) + addUsers []string + addGroups []string + addManagedGroups []string + resultUsers []string + resultGroups []string + resultManagedGroups []string + wantErr bool }{ { name: "Add user on empty role", @@ -1078,6 +1101,28 @@ func TestAddPrincipal(t *testing.T) { addGroups: []string{groups[1].GetPublicId(), groups[1].GetPublicId()}, resultGroups: []string{groups[0].GetPublicId(), groups[1].GetPublicId()}, }, + { + name: "Add managed group on empty role", + setup: func(r *iam.Role) {}, + addManagedGroups: []string{managedGroups[1].GetPublicId()}, + resultManagedGroups: []string{managedGroups[1].GetPublicId()}, + }, + { + name: "Add managed group on populated role", + setup: func(r *iam.Role) { + iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[0].GetPublicId()) + }, + addManagedGroups: []string{managedGroups[1].GetPublicId()}, + resultManagedGroups: []string{managedGroups[0].GetPublicId(), managedGroups[1].GetPublicId()}, + }, + { + name: "Add duplicate managed group on populated role", + setup: func(r *iam.Role) { + iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[0].GetPublicId()) + }, + addManagedGroups: []string{managedGroups[1].GetPublicId(), managedGroups[1].GetPublicId()}, + resultManagedGroups: []string{managedGroups[0].GetPublicId(), managedGroups[1].GetPublicId()}, + }, { name: "Add invalid u_recovery on role", setup: func(r *iam.Role) {}, @@ -1094,7 +1139,7 @@ func TestAddPrincipal(t *testing.T) { req := &pbs.AddRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), - PrincipalIds: append(tc.addUsers, tc.addGroups...), + PrincipalIds: append(tc.addUsers, append(tc.addGroups, tc.addManagedGroups...)...), } got, err := s.AddRolePrincipals(auth.DisabledAuthTestContext(repoFn, o.GetPublicId()), req) @@ -1103,10 +1148,10 @@ func TestAddPrincipal(t *testing.T) { return } s, ok := status.FromError(err) - require.True(t, ok) + require.True(t, ok, fmt.Sprintf("Could not run FromError; input was %s", pretty.Sprint(err))) require.NoError(t, err, "Got error: %v", s) - assert.True(t, equalPrincipals(got.GetItem(), append(tc.resultUsers, tc.resultGroups...))) + assert.True(t, equalPrincipals(got.GetItem(), append(tc.resultUsers, append(tc.resultGroups, tc.resultManagedGroups...)...))) }) } } @@ -1168,6 +1213,20 @@ func TestSetPrincipal(t *testing.T) { require.NoError(t, err, "Error when getting new role service.") o, p := iam.TestScopes(t, iamRepo) + + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrap) + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := oidc.TestAuthMethod( + t, conn, databaseWrapper, o.GetPublicId(), oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + users := []*iam.User{ iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), @@ -1178,15 +1237,22 @@ func TestSetPrincipal(t *testing.T) { iam.TestGroup(t, conn, o.GetPublicId()), iam.TestGroup(t, conn, o.GetPublicId()), } + managedGroups := []*oidc.ManagedGroup{ + oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter), + oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter), + oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter), + } setCases := []struct { - name string - setup func(*iam.Role) - setUsers []string - setGroups []string - resultUsers []string - resultGroups []string - wantErr bool + name string + setup func(*iam.Role) + setUsers []string + setGroups []string + setManagedGroups []string + resultUsers []string + resultGroups []string + resultManagedGroups []string + wantErr bool }{ { name: "Set user on empty role", @@ -1233,6 +1299,20 @@ func TestSetPrincipal(t *testing.T) { setGroups: []string{groups[1].GetPublicId()}, resultGroups: []string{groups[1].GetPublicId()}, }, + { + name: "Set managed group on empty role", + setup: func(r *iam.Role) {}, + setManagedGroups: []string{managedGroups[1].GetPublicId()}, + resultManagedGroups: []string{managedGroups[1].GetPublicId()}, + }, + { + name: "Set managed group on populated role", + setup: func(r *iam.Role) { + iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[0].GetPublicId()) + }, + setManagedGroups: []string{managedGroups[1].GetPublicId()}, + resultManagedGroups: []string{managedGroups[1].GetPublicId()}, + }, { name: "Set invalid u_recovery on role", setup: func(r *iam.Role) {}, @@ -1249,7 +1329,7 @@ func TestSetPrincipal(t *testing.T) { req := &pbs.SetRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), - PrincipalIds: append(tc.setUsers, tc.setGroups...), + PrincipalIds: append(tc.setUsers, append(tc.setGroups, tc.setManagedGroups...)...), } got, err := s.SetRolePrincipals(auth.DisabledAuthTestContext(repoFn, o.GetPublicId()), req) @@ -1258,10 +1338,10 @@ func TestSetPrincipal(t *testing.T) { return } s, ok := status.FromError(err) - require.True(t, ok) + require.True(t, ok, fmt.Sprintf("Could not run FromError; input was %s", pretty.Sprint(err))) require.NoError(t, err, "Got error: %v", s) - assert.True(t, equalPrincipals(got.GetItem(), append(tc.resultUsers, tc.resultGroups...))) + assert.True(t, equalPrincipals(got.GetItem(), append(tc.resultUsers, append(tc.resultGroups, tc.resultManagedGroups...)...))) }) } } @@ -1323,6 +1403,20 @@ func TestRemovePrincipal(t *testing.T) { require.NoError(t, err, "Error when getting new role service.") o, p := iam.TestScopes(t, iamRepo) + + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrap) + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := oidc.TestAuthMethod( + t, conn, databaseWrapper, o.GetPublicId(), oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + users := []*iam.User{ iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), @@ -1333,15 +1427,22 @@ func TestRemovePrincipal(t *testing.T) { iam.TestGroup(t, conn, o.GetPublicId()), iam.TestGroup(t, conn, o.GetPublicId()), } + managedGroups := []*oidc.ManagedGroup{ + oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter), + oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter), + oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter), + } addCases := []struct { - name string - setup func(*iam.Role) - removeUsers []string - removeGroups []string - resultUsers []string - resultGroups []string - wantErr bool + name string + setup func(*iam.Role) + removeUsers []string + removeGroups []string + removeManagedGroups []string + resultUsers []string + resultGroups []string + resultManagedGroups []string + wantErr bool }{ { name: "Remove user on empty role", @@ -1416,6 +1517,39 @@ func TestRemovePrincipal(t *testing.T) { removeGroups: []string{groups[0].GetPublicId(), groups[1].GetPublicId()}, resultGroups: []string{}, }, + { + name: "Remove managed group on empty role", + setup: func(r *iam.Role) {}, + removeGroups: []string{groups[1].GetPublicId()}, + wantErr: true, + }, + { + name: "Remove 1 of 2 managed groups from role", + setup: func(r *iam.Role) { + iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[0].GetPublicId()) + iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[1].GetPublicId()) + }, + removeManagedGroups: []string{managedGroups[1].GetPublicId()}, + resultManagedGroups: []string{managedGroups[0].GetPublicId()}, + }, + { + name: "Remove 1 duplicate managed group of 2 managed groups from role", + setup: func(r *iam.Role) { + iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[0].GetPublicId()) + iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[1].GetPublicId()) + }, + removeManagedGroups: []string{managedGroups[1].GetPublicId(), managedGroups[1].GetPublicId()}, + resultManagedGroups: []string{managedGroups[0].GetPublicId()}, + }, + { + name: "Remove all managed groups from role", + setup: func(r *iam.Role) { + iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[0].GetPublicId()) + iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[1].GetPublicId()) + }, + removeManagedGroups: []string{managedGroups[0].GetPublicId(), managedGroups[1].GetPublicId()}, + resultManagedGroups: []string{}, + }, } for _, tc := range addCases { @@ -1426,7 +1560,7 @@ func TestRemovePrincipal(t *testing.T) { req := &pbs.RemoveRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), - PrincipalIds: append(tc.removeUsers, tc.removeGroups...), + PrincipalIds: append(tc.removeUsers, append(tc.removeGroups, tc.removeManagedGroups...)...), } got, err := s.RemoveRolePrincipals(auth.DisabledAuthTestContext(repoFn, o.GetPublicId()), req) @@ -1438,7 +1572,7 @@ func TestRemovePrincipal(t *testing.T) { require.True(t, ok) require.NoError(t, err, "Got error: %v", s) - assert.True(t, equalPrincipals(got.GetItem(), append(tc.resultUsers, tc.resultGroups...))) + assert.True(t, equalPrincipals(got.GetItem(), append(tc.resultUsers, append(tc.resultGroups, tc.resultManagedGroups...)...))) }) } }