// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package roles_test import ( "context" "errors" "fmt" "slices" "sort" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/roles" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/server" "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/roles" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" "github.com/hashicorp/boundary/version" "github.com/kr/pretty" "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var testAuthorizedActions = []string{"no-op", "read", "update", "delete", "add-principals", "set-principals", "remove-principals", "add-grants", "set-grants", "remove-grants", "add-grant-scopes", "set-grant-scopes", "remove-grant-scopes"} func createDefaultRolesAndRepo(t *testing.T) (*iam.Role, *iam.Role, func() (*iam.Repository, error)) { t.Helper() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } o, p := iam.TestScopes(t, iamRepo) or := iam.TestRole(t, conn, o.GetPublicId(), iam.WithDescription("default"), iam.WithName("default"), iam.WithGrantScopeIds([]string{p.GetPublicId()})) pr := iam.TestRole(t, conn, p.GetPublicId(), iam.WithDescription("default"), iam.WithName("default")) return or, pr, repoFn } func equalPrincipals(role *pb.Role, principals []string) bool { if len(role.Principals) != len(principals) { return false } for _, principal := range principals { var foundInPrincipals bool var foundInPrincipalIds bool for _, v := range role.Principals { if v.Id == principal { foundInPrincipals = true } } for _, v := range role.PrincipalIds { if v == principal { foundInPrincipalIds = true } } if !foundInPrincipals || !foundInPrincipalIds { return false } } return true } func TestGet(t *testing.T) { assert, require := assert.New(t), require.New(t) or, pr, repoFn := createDefaultRolesAndRepo(t) toMerge := &pbs.GetRoleRequest{ Id: or.GetPublicId(), } wantOrgRole := &pb.Role{ Id: or.GetPublicId(), ScopeId: or.GetScopeId(), Scope: &scopes.ScopeInfo{Id: or.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: or.GetName()}, Description: &wrapperspb.StringValue{Value: or.GetDescription()}, GrantScopeIds: []string{pr.GetScopeId()}, CreatedTime: or.CreateTime.GetTimestamp(), UpdatedTime: or.UpdateTime.GetTimestamp(), Version: or.GetVersion(), AuthorizedActions: testAuthorizedActions, } wantProjRole := &pb.Role{ Id: pr.GetPublicId(), ScopeId: pr.GetScopeId(), Scope: &scopes.ScopeInfo{Id: pr.GetScopeId(), Type: scope.Project.String(), ParentScopeId: or.GetScopeId()}, Name: &wrapperspb.StringValue{Value: pr.GetName()}, Description: &wrapperspb.StringValue{Value: pr.GetDescription()}, GrantScopeIds: []string{globals.GrantScopeThis}, CreatedTime: pr.CreateTime.GetTimestamp(), UpdatedTime: pr.UpdateTime.GetTimestamp(), Version: pr.GetVersion(), AuthorizedActions: testAuthorizedActions, } cases := []struct { name string scopeId string req *pbs.GetRoleRequest res *pbs.GetRoleResponse err error }{ { name: "Get an Existing Role", scopeId: or.GetScopeId(), req: &pbs.GetRoleRequest{Id: or.GetPublicId()}, res: &pbs.GetRoleResponse{Item: wantOrgRole}, }, { name: "Get a non existent Role", req: &pbs.GetRoleRequest{Id: globals.RolePrefix + "_DoesntExis"}, res: nil, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Wrong id prefix", req: &pbs.GetRoleRequest{Id: "j_1234567890"}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "space in id", req: &pbs.GetRoleRequest{Id: globals.RolePrefix + "_1 23456789"}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Project Scoped Get an Existing Role", scopeId: pr.GetScopeId(), req: &pbs.GetRoleRequest{Id: pr.GetPublicId()}, res: &pbs.GetRoleResponse{Item: wantProjRole}, }, { name: "Project Scoped Get a non existent Role", scopeId: pr.GetScopeId(), req: &pbs.GetRoleRequest{Id: globals.RolePrefix + "_DoesntExis"}, res: nil, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Project Scoped Wrong id prefix", scopeId: pr.GetScopeId(), req: &pbs.GetRoleRequest{Id: "j_1234567890"}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Project Scoped space in id", scopeId: pr.GetScopeId(), req: &pbs.GetRoleRequest{Id: globals.RolePrefix + "_1 23456789"}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { req := proto.Clone(toMerge).(*pbs.GetRoleRequest) proto.Merge(req, tc.req) s, err := roles.NewService(context.Background(), repoFn, 1000) require.NoError(err, "Couldn't create new role service.") got, gErr := s.GetRole(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "GetRole(%+v) got error %v, wanted %v", req, gErr, tc.err) } assert.Empty(cmp.Diff( got, tc.res, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), ), "GetRole(%q) got response\n%q, wanted\n%q", req, got, tc.res) }) } } func TestList(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } oNoRoles, pWithRoles := iam.TestScopes(t, iamRepo, iam.WithSkipDefaultRoleCreation(true)) oWithRoles, pNoRoles := iam.TestScopes(t, iamRepo, iam.WithSkipDefaultRoleCreation(true)) var wantOrgRoles []*pb.Role var wantProjRoles []*pb.Role var totalRoles []*pb.Role for i := 0; i < 10; i++ { or := iam.TestRole(t, conn, oWithRoles.GetPublicId()) _ = iam.TestRoleGrantScope(t, conn, or.GetPublicId(), globals.GrantScopeChildren) wantOrgRoles = append(wantOrgRoles, &pb.Role{ Id: or.GetPublicId(), ScopeId: or.GetScopeId(), Scope: &scopes.ScopeInfo{Id: or.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, CreatedTime: or.GetCreateTime().GetTimestamp(), UpdatedTime: or.GetUpdateTime().GetTimestamp(), Version: or.GetVersion(), AuthorizedActions: testAuthorizedActions, GrantScopeIds: []string{"this", "children"}, }) totalRoles = append(totalRoles, wantOrgRoles[i]) pr := iam.TestRole(t, conn, pWithRoles.GetPublicId()) wantProjRoles = append(wantProjRoles, &pb.Role{ Id: pr.GetPublicId(), ScopeId: pr.GetScopeId(), Scope: &scopes.ScopeInfo{Id: pr.GetScopeId(), Type: scope.Project.String(), ParentScopeId: oNoRoles.GetPublicId()}, CreatedTime: pr.GetCreateTime().GetTimestamp(), UpdatedTime: pr.GetUpdateTime().GetTimestamp(), Version: pr.GetVersion(), AuthorizedActions: testAuthorizedActions, GrantScopeIds: []string{"this"}, }) totalRoles = append(totalRoles, wantProjRoles[i]) } slices.Reverse(wantOrgRoles) slices.Reverse(wantProjRoles) slices.Reverse(totalRoles) // Run analyze to update postgres estimates sqlDb, err := conn.SqlDB(ctx) require.NoError(t, err) _, err = sqlDb.ExecContext(ctx, "analyze") require.NoError(t, err) cases := []struct { name string req *pbs.ListRolesRequest res *pbs.ListRolesResponse err error }{ { name: "List Many Role", req: &pbs.ListRolesRequest{ScopeId: oWithRoles.GetPublicId()}, res: &pbs.ListRolesResponse{ Items: wantOrgRoles, EstItemCount: uint32(len(wantOrgRoles)), ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "List No Roles", req: &pbs.ListRolesRequest{ScopeId: oNoRoles.GetPublicId()}, res: &pbs.ListRolesResponse{ ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "Paginate listing", req: &pbs.ListRolesRequest{ScopeId: "global", Recursive: true, PageSize: 2}, res: &pbs.ListRolesResponse{ Items: totalRoles[:2], EstItemCount: uint32(len(totalRoles)), ResponseType: "delta", SortBy: "created_time", SortDir: "desc", }, }, { name: "List Many Project Role", req: &pbs.ListRolesRequest{ScopeId: pWithRoles.GetPublicId()}, res: &pbs.ListRolesResponse{ Items: wantProjRoles, EstItemCount: uint32(len(wantProjRoles)), ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "List No Project Roles", req: &pbs.ListRolesRequest{ScopeId: pNoRoles.GetPublicId()}, res: &pbs.ListRolesResponse{ ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "List proj roles recursively", req: &pbs.ListRolesRequest{ScopeId: pWithRoles.GetPublicId(), Recursive: true}, res: &pbs.ListRolesResponse{ Items: wantProjRoles, EstItemCount: uint32(len(wantProjRoles)), ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "List org roles recursively", req: &pbs.ListRolesRequest{ScopeId: oNoRoles.GetPublicId(), Recursive: true}, res: &pbs.ListRolesResponse{ Items: wantProjRoles, EstItemCount: uint32(len(wantProjRoles)), ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "List global roles recursively", req: &pbs.ListRolesRequest{ScopeId: "global", Recursive: true}, res: &pbs.ListRolesResponse{ Items: totalRoles, EstItemCount: uint32(len(totalRoles)), ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "Filter to org roles", req: &pbs.ListRolesRequest{ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, oWithRoles.GetPublicId())}, res: &pbs.ListRolesResponse{ Items: wantOrgRoles, EstItemCount: uint32(len(wantOrgRoles)), ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "Filter to proj roles", req: &pbs.ListRolesRequest{ScopeId: "global", Recursive: true, Filter: fmt.Sprintf(`"/item/scope/id"==%q`, pWithRoles.GetPublicId())}, res: &pbs.ListRolesResponse{ Items: wantProjRoles, EstItemCount: uint32(len(wantProjRoles)), ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "Filter to no roles", req: &pbs.ListRolesRequest{ScopeId: pWithRoles.GetPublicId(), Filter: `"/item/id"=="doesnt match"`}, res: &pbs.ListRolesResponse{ ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "Filter Bad Format", req: &pbs.ListRolesRequest{ScopeId: pWithRoles.GetPublicId(), Filter: `"//id/"=="bad"`}, err: handlers.InvalidArgumentErrorf("bad format", nil), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) s, err := roles.NewService(context.Background(), repoFn, 1000) require.NoError(err, "Couldn't create new role service.") // Test the non-anon case got, gErr := s.ListRoles(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err)) return } require.NoError(gErr) assert.Empty(cmp.Diff( got, tc.res, protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListRolesResponse{}, "list_token"), cmpopts.SortSlices(func(a, b string) bool { return a < b }), ), "ListRoles(%q) got response %q, wanted %q", tc.req, got, tc.res) // Test the anon case got, gErr = s.ListRoles(auth.DisabledAuthTestContext(repoFn, tc.req.GetScopeId(), auth.WithUserId(globals.AnonymousUserId)), tc.req) require.NoError(gErr) assert.Len(got.Items, len(tc.res.Items)) for _, item := range got.GetItems() { require.Nil(item.CreatedTime) require.Nil(item.PrincipalIds) require.Nil(item.Principals) require.Nil(item.Grants) require.Nil(item.GrantStrings) require.Nil(item.UpdatedTime) require.Zero(item.Version) } }) } } func roleToProto(r *iam.Role, scope *scopes.ScopeInfo, authorizedActions []string) *pb.Role { ret := &pb.Role{ Id: r.GetPublicId(), ScopeId: r.GetScopeId(), Scope: scope, CreatedTime: r.GetCreateTime().GetTimestamp(), UpdatedTime: r.GetUpdateTime().GetTimestamp(), Version: r.GetVersion(), AuthorizedActions: testAuthorizedActions, } for _, r := range r.GrantScopes { ret.GrantScopeIds = append(ret.GrantScopeIds, r.ScopeIdOrSpecial) } return ret } func TestListPagination(t *testing.T) { // Set database read timeout to avoid duplicates in response oldReadTimeout := globals.RefreshReadLookbackDuration globals.RefreshReadLookbackDuration = 0 t.Cleanup(func() { globals.RefreshReadLookbackDuration = oldReadTimeout }) ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") sqlDB, err := conn.SqlDB(ctx) require.NoError(t, err) wrapper := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrapper) rw := db.New(conn) iamRepo := iam.TestRepo(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iamRepo, nil } tokenRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kms) } serversRepoFn := func() (*server.Repository, error) { return server.NewRepository(ctx, rw, rw, kms) } tokenRepo, err := tokenRepoFn() require.NoError(t, err) // Run analyze to update postgres meta tables _, err = sqlDB.ExecContext(ctx, "analyze") require.NoError(t, err) oNoRoles, pWithRoles := iam.TestScopes(t, iamRepo, iam.WithSkipDefaultRoleCreation(true)) oWithRoles, pNoRoles := iam.TestScopes(t, iamRepo, iam.WithSkipDefaultRoleCreation(true)) authMethod := password.TestAuthMethods(t, conn, "global", 1)[0] acct := password.TestAccount(t, conn, authMethod.GetPublicId(), "test_user") u := iam.TestUser(t, iamRepo, "global", iam.WithAccountIds(acct.PublicId)) var allRoles []*pb.Role for _, scope := range []*iam.Scope{oWithRoles, oNoRoles, pWithRoles, pNoRoles} { allowedRole := iam.TestRole(t, conn, scope.GetPublicId()) iam.TestRoleGrant(t, conn, allowedRole.GetPublicId(), "ids=*;type=*;actions=*") iam.TestUserRole(t, conn, allowedRole.GetPublicId(), u.GetPublicId()) allRoles = append(allRoles, roleToProto(allowedRole, &scopes.ScopeInfo{ Id: scope.GetPublicId(), ParentScopeId: scope.GetParentId(), Type: scope.GetType(), }, testAuthorizedActions, )) } at, err := tokenRepo.CreateAuthToken(ctx, u, acct.GetPublicId()) require.NoError(t, err) // Test without anon user requestInfo := authpb.RequestInfo{ TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: at.GetPublicId(), Token: at.GetToken(), } requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) var safeToDeleteRole string orgScopeInfo := &scopes.ScopeInfo{Id: oWithRoles.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()} projScopeInfo := &scopes.ScopeInfo{Id: pWithRoles.GetPublicId(), Type: scope.Project.String(), ParentScopeId: pWithRoles.GetParentId()} for i := 0; i < 10; i++ { or := iam.TestRole(t, conn, oWithRoles.GetPublicId()) allRoles = append(allRoles, roleToProto(or, orgScopeInfo, testAuthorizedActions)) pr := iam.TestRole(t, conn, pWithRoles.GetPublicId()) allRoles = append(allRoles, roleToProto(pr, projScopeInfo, testAuthorizedActions)) safeToDeleteRole = pr.GetPublicId() } slices.Reverse(allRoles) a, err := roles.NewService(ctx, iamRepoFn, 1000) require.NoError(t, err, "Couldn't create new user service.") // Run analyze to update postgres estimates _, err = sqlDB.ExecContext(context.Background(), "analyze") require.NoError(t, err) itemCount := uint32(len(allRoles)) testPageSize := int((itemCount - 2) / 2) // Start paginating, recursively req := &pbs.ListRolesRequest{ ScopeId: "global", Recursive: true, Filter: "", ListToken: "", PageSize: uint32(testPageSize), } got, err := a.ListRoles(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), testPageSize) // Compare without comparing the list token assert.Empty(t, cmp.Diff( got, &pbs.ListRolesResponse{ Items: allRoles[0:testPageSize], ResponseType: "delta", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, // In addition to the added roles, there are the roles added // by the test setup when specifying the permissions of the // requester EstItemCount: itemCount, }, protocmp.SortRepeated(func(a, b string) bool { return strings.Compare(a, b) < 0 }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListRolesResponse{}, "list_token"), ), ) // Request second page req.ListToken = got.ListToken got, err = a.ListRoles(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), testPageSize) // Compare without comparing the list token assert.Empty(t, cmp.Diff( got, &pbs.ListRolesResponse{ Items: allRoles[testPageSize : testPageSize*2], ResponseType: "delta", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: itemCount, }, protocmp.Transform(), protocmp.SortRepeated(func(a, b string) bool { return strings.Compare(a, b) < 0 }), protocmp.IgnoreFields(&pbs.ListRolesResponse{}, "list_token"), ), ) // Request rest of results req.ListToken = got.ListToken req.PageSize = 10 got, err = a.ListRoles(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), 2) // Compare without comparing the list token assert.Empty(t, cmp.Diff( got, &pbs.ListRolesResponse{ Items: allRoles[testPageSize*2:], ResponseType: "complete", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: itemCount, }, protocmp.Transform(), protocmp.SortRepeated(func(a, b string) bool { return strings.Compare(a, b) < 0 }), protocmp.IgnoreFields(&pbs.ListRolesResponse{}, "list_token"), ), ) // Update 2 roles and see them in the refresh r1 := allRoles[len(allRoles)-1] r1.Description = wrapperspb.String("updated1") resp1, err := a.UpdateRole(ctx, &pbs.UpdateRoleRequest{ Id: r1.GetId(), Item: &pb.Role{Description: r1.GetDescription(), Version: r1.GetVersion()}, UpdateMask: &field_mask.FieldMask{Paths: []string{"description"}}, }) require.NoError(t, err) r1.UpdatedTime = resp1.GetItem().GetUpdatedTime() r1.Version = resp1.GetItem().GetVersion() allRoles = append([]*pb.Role{r1}, allRoles[:len(allRoles)-1]...) r2 := allRoles[len(allRoles)-1] r2.Description = wrapperspb.String("updated2") resp2, err := a.UpdateRole(ctx, &pbs.UpdateRoleRequest{ Id: r2.GetId(), Item: &pb.Role{Description: r2.GetDescription(), Version: r2.GetVersion()}, UpdateMask: &field_mask.FieldMask{Paths: []string{"description"}}, }) require.NoError(t, err) r2.UpdatedTime = resp2.GetItem().GetUpdatedTime() r2.Version = resp2.GetItem().GetVersion() allRoles = append([]*pb.Role{r2}, allRoles[:len(allRoles)-1]...) // Run analyze to update postgres estimates _, err = sqlDB.ExecContext(ctx, "analyze") require.NoError(t, err) // Request updated results req.ListToken = got.ListToken req.PageSize = 1 got, err = a.ListRoles(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), 1) // Compare without comparing the list token assert.Empty(t, cmp.Diff( got, &pbs.ListRolesResponse{ Items: []*pb.Role{allRoles[0]}, ResponseType: "delta", SortBy: "updated_time", SortDir: "desc", RemovedIds: nil, EstItemCount: itemCount, }, protocmp.Transform(), protocmp.SortRepeated(func(a, b string) bool { return strings.Compare(a, b) < 0 }), protocmp.IgnoreFields(&pbs.ListRolesResponse{}, "list_token"), ), ) // Get next page req.ListToken = got.ListToken got, err = a.ListRoles(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), 1) // Compare without comparing the list token assert.Empty(t, cmp.Diff( got, &pbs.ListRolesResponse{ Items: []*pb.Role{allRoles[1]}, ResponseType: "complete", SortBy: "updated_time", SortDir: "desc", RemovedIds: nil, EstItemCount: itemCount, }, protocmp.Transform(), protocmp.SortRepeated(func(a, b string) bool { return strings.Compare(a, b) < 0 }), protocmp.IgnoreFields(&pbs.ListRolesResponse{}, "list_token"), ), ) // Request new page with filter requiring looping // to fill the page. req.ListToken = "" req.PageSize = 1 req.Filter = fmt.Sprintf(`"/item/id"==%q or "/item/id"==%q`, allRoles[len(allRoles)-2].Id, allRoles[len(allRoles)-1].Id) got, err = a.ListRoles(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), 1) assert.Empty(t, cmp.Diff( got, &pbs.ListRolesResponse{ Items: []*pb.Role{allRoles[len(allRoles)-2]}, ResponseType: "delta", SortBy: "created_time", SortDir: "desc", // Should be empty again RemovedIds: nil, EstItemCount: itemCount, }, protocmp.Transform(), protocmp.SortRepeated(func(a, b string) bool { return strings.Compare(a, b) < 0 }), protocmp.IgnoreFields(&pbs.ListRolesResponse{}, "list_token"), ), ) req.ListToken = got.ListToken // Get the second page got, err = a.ListRoles(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), 1) assert.Empty(t, cmp.Diff( got, &pbs.ListRolesResponse{ Items: []*pb.Role{allRoles[len(allRoles)-1]}, ResponseType: "complete", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: itemCount, }, protocmp.Transform(), protocmp.SortRepeated(func(a, b string) bool { return strings.Compare(a, b) < 0 }), protocmp.IgnoreFields(&pbs.ListRolesResponse{}, "list_token"), ), ) _, err = iamRepo.DeleteRole(ctx, safeToDeleteRole) require.NoError(t, err) req.ListToken = got.ListToken got, err = a.ListRoles(ctx, req) require.NoError(t, err) assert.Empty(t, cmp.Diff( got, &pbs.ListRolesResponse{ Items: nil, ResponseType: "complete", SortBy: "updated_time", SortDir: "desc", RemovedIds: []string{safeToDeleteRole}, EstItemCount: itemCount, }, protocmp.Transform(), protocmp.SortRepeated(func(a, b string) bool { return strings.Compare(a, b) < 0 }), protocmp.IgnoreFields(&pbs.ListRolesResponse{}, "list_token"), ), ) // Create unauthenticated user unauthAt := authtoken.TestAuthToken(t, conn, kms, oWithRoles.GetPublicId()) unauthR := iam.TestRole(t, conn, pWithRoles.GetPublicId()) _ = iam.TestUserRole(t, conn, unauthR.GetPublicId(), unauthAt.GetIamUserId()) // Make a request with the unauthenticated user, // ensure the response contains the pagination parameters. requestInfo = authpb.RequestInfo{ TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: unauthAt.GetPublicId(), Token: unauthAt.GetToken(), } requestContext = context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) _, err = a.ListRoles(ctx, &pbs.ListRolesRequest{ ScopeId: "global", Recursive: true, }) require.Error(t, err) assert.ErrorIs(t, handlers.ForbiddenError(), err) } func TestDelete(t *testing.T) { or, pr, repoFn := createDefaultRolesAndRepo(t) s, err := roles.NewService(context.Background(), repoFn, 1000) require.NoError(t, err, "Error when getting new role service.") cases := []struct { name string scopeId string req *pbs.DeleteRoleRequest res *pbs.DeleteRoleResponse err error }{ { name: "Delete an Existing Role", scopeId: or.GetScopeId(), req: &pbs.DeleteRoleRequest{ Id: or.GetPublicId(), }, }, { name: "Delete bad role id", scopeId: or.GetScopeId(), req: &pbs.DeleteRoleRequest{ Id: globals.RolePrefix + "_doesntexis", }, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Bad Role Id formatting", scopeId: or.GetScopeId(), req: &pbs.DeleteRoleRequest{ Id: "bad_format", }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Project Scoped Delete an Existing Role", scopeId: pr.GetScopeId(), req: &pbs.DeleteRoleRequest{ Id: pr.GetPublicId(), }, }, { name: "Project Scoped Delete bad Role id", scopeId: pr.GetScopeId(), req: &pbs.DeleteRoleRequest{ Id: globals.RolePrefix + "_doesntexis", }, err: handlers.ApiErrorWithCode(codes.NotFound), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) got, gErr := s.DeleteRole(auth.DisabledAuthTestContext(repoFn, tc.scopeId), tc.req) if tc.err != nil { require.NotNil(gErr) assert.True(errors.Is(gErr, tc.err), "DeleteRole(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } assert.EqualValuesf(tc.res, got, "DeleteRole(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } func TestDelete_twice(t *testing.T) { assert, require := assert.New(t), require.New(t) or, pr, repoFn := createDefaultRolesAndRepo(t) s, err := roles.NewService(context.Background(), repoFn, 1000) require.NoError(err, "Error when getting new role service") req := &pbs.DeleteRoleRequest{ Id: or.GetPublicId(), } ctx := auth.DisabledAuthTestContext(repoFn, or.GetScopeId()) _, gErr := s.DeleteRole(ctx, req) assert.NoError(gErr, "First attempt") _, gErr = s.DeleteRole(ctx, req) assert.Error(gErr, "Second attempt") assert.True(errors.Is(gErr, handlers.ApiErrorWithCode(codes.NotFound)), "Expected permission denied for the second delete.") projReq := &pbs.DeleteRoleRequest{ Id: pr.GetPublicId(), } ctx = auth.DisabledAuthTestContext(repoFn, pr.GetScopeId()) _, gErr = s.DeleteRole(ctx, projReq) assert.NoError(gErr, "First attempt") _, gErr = s.DeleteRole(ctx, projReq) assert.Error(gErr, "Second attempt") assert.True(errors.Is(gErr, handlers.ApiErrorWithCode(codes.NotFound)), "Expected permission denied for the second delete.") } func TestCreate(t *testing.T) { defaultOrgRole, defaultProjRole, repoFn := createDefaultRolesAndRepo(t) defaultCreated := defaultOrgRole.GetCreateTime().GetTimestamp().AsTime() toMerge := &pbs.CreateRoleRequest{} cases := []struct { name string req *pbs.CreateRoleRequest res *pbs.CreateRoleResponse err error }{ { name: "Create a valid Role", req: &pbs.CreateRoleRequest{Item: &pb.Role{ ScopeId: defaultOrgRole.GetScopeId(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, }}, res: &pbs.CreateRoleResponse{ Uri: fmt.Sprintf("roles/%s_", globals.RolePrefix), Item: &pb.Role{ ScopeId: defaultOrgRole.GetScopeId(), Scope: &scopes.ScopeInfo{Id: defaultOrgRole.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, GrantScopeIds: []string{globals.GrantScopeThis}, Version: 2, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Create a valid Global Role", req: &pbs.CreateRoleRequest{Item: &pb.Role{ ScopeId: scope.Global.String(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, }}, res: &pbs.CreateRoleResponse{ Uri: fmt.Sprintf("roles/%s_", globals.RolePrefix), Item: &pb.Role{ ScopeId: scope.Global.String(), Scope: &scopes.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, GrantScopeIds: []string{globals.GrantScopeThis}, Version: 2, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Create a valid Project Scoped Role", req: &pbs.CreateRoleRequest{ Item: &pb.Role{ ScopeId: defaultProjRole.GetScopeId(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.CreateRoleResponse{ Uri: fmt.Sprintf("roles/%s_", globals.RolePrefix), Item: &pb.Role{ ScopeId: defaultProjRole.GetScopeId(), Scope: &scopes.ScopeInfo{Id: defaultProjRole.GetScopeId(), Type: scope.Project.String(), ParentScopeId: defaultOrgRole.GetScopeId()}, Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, GrantScopeIds: []string{globals.GrantScopeThis}, Version: 2, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Can't specify Id", req: &pbs.CreateRoleRequest{Item: &pb.Role{ ScopeId: defaultProjRole.GetScopeId(), Id: globals.RolePrefix + "_notallowed", }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Can't specify Created Time", req: &pbs.CreateRoleRequest{Item: &pb.Role{ ScopeId: defaultProjRole.GetScopeId(), CreatedTime: timestamppb.Now(), }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Can't specify Update Time", req: &pbs.CreateRoleRequest{Item: &pb.Role{ ScopeId: defaultProjRole.GetScopeId(), UpdatedTime: timestamppb.Now(), }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) req := proto.Clone(toMerge).(*pbs.CreateRoleRequest) proto.Merge(req, tc.req) s, err := roles.NewService(context.Background(), repoFn, 1000) require.NoError(err, "Error when getting new role service.") got, gErr := s.CreateRole(auth.DisabledAuthTestContext(repoFn, tc.req.GetItem().GetScopeId()), req) if tc.err != nil { require.NotNil(gErr) assert.True(errors.Is(gErr, tc.err), "CreateRole(%+v) got error %v, wanted %v", req, gErr, tc.err) } if got != nil { assert.Contains(got.GetUri(), tc.res.Uri) assert.True(strings.HasPrefix(got.GetItem().GetId(), globals.RolePrefix+"_"), "Expected %q to have the prefix %q", got.GetItem().GetId(), globals.RolePrefix+"_") gotCreateTime := got.GetItem().GetCreatedTime().AsTime() gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() // Verify it is a role created after the test setup's default role assert.True(gotCreateTime.After(defaultCreated), "New role should have been created after default role. Was created %v, which is after %v", gotCreateTime, defaultCreated) assert.True(gotUpdateTime.After(defaultCreated), "New role should have been updated after default role. Was updated %v, which is after %v", gotUpdateTime, defaultCreated) // Clear all values which are hard to compare against. got.Uri, tc.res.Uri = "", "" got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } assert.Empty(cmp.Diff( got, tc.res, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), ), "CreateRole(%q) got response\n%q, wanted\n%q", req, got, tc.res) }) } } func TestUpdate(t *testing.T) { ctx := context.Background() grantString := "ids=*;type=*;actions=*" g, err := perms.Parse(context.Background(), perms.GrantTuple{RoleScopeId: "global", GrantScopeId: "global", Grant: grantString}) require.NoError(t, err) _, actions := g.Actions() grant := &pb.Grant{ Raw: grantString, Canonical: g.CanonicalString(), Json: &pb.GrantJson{ Id: g.Id(), Ids: g.Ids(), Type: g.Type().String(), Actions: actions, }, } conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } o, p := iam.TestScopes(t, iamRepo) u := iam.TestUser(t, iamRepo, o.GetPublicId()) or := iam.TestRole(t, conn, o.GetPublicId(), iam.WithDescription("default"), iam.WithName("default"), iam.WithGrantScopeIds([]string{p.GetPublicId()})) _ = iam.TestRoleGrant(t, conn, or.GetPublicId(), grantString) _ = iam.TestUserRole(t, conn, or.GetPublicId(), u.GetPublicId()) pr := iam.TestRole(t, conn, p.GetPublicId(), iam.WithDescription("default"), iam.WithName("default")) _ = iam.TestRoleGrant(t, conn, pr.GetPublicId(), grantString) _ = iam.TestUserRole(t, conn, pr.GetPublicId(), u.GetPublicId()) principal := &pb.Principal{ Id: u.GetPublicId(), Type: iam.UserRoleType.String(), ScopeId: u.GetScopeId(), } orVersion := or.Version prVersion := pr.Version tested, err := roles.NewService(ctx, repoFn, 0) require.NoError(t, err, "Error when getting new role service.") resetRoles := func(proj bool) { repo, err := repoFn() require.NoError(t, err, "Couldn't get a new repo") if proj { pr, _, _, _, _, err = repo.UpdateRole(context.Background(), pr, prVersion, []string{"Name", "Description"}) require.NoError(t, err, "Failed to reset the role") prVersion = pr.Version } else { or, _, _, _, _, err = repo.UpdateRole(context.Background(), or, orVersion, []string{"Name", "Description"}) require.NoError(t, err, "Failed to reset the role") orVersion = or.Version } } created := or.GetCreateTime().GetTimestamp().AsTime() toMerge := &pbs.UpdateRoleRequest{ Id: or.GetPublicId(), } cases := []struct { name string scopeId string req *pbs.UpdateRoleRequest res *pbs.UpdateRoleResponse err error }{ { name: "Update an Existing Role", scopeId: or.GetScopeId(), req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "description"}, }, Item: &pb.Role{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.UpdateRoleResponse{ Item: &pb.Role{ Id: or.GetPublicId(), ScopeId: or.GetScopeId(), Scope: &scopes.ScopeInfo{Id: or.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: or.GetCreateTime().GetTimestamp(), GrantScopeIds: []string{p.GetPublicId()}, GrantStrings: []string{grant.GetRaw()}, Grants: []*pb.Grant{grant}, PrincipalIds: []string{u.GetPublicId()}, Principals: []*pb.Principal{principal}, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Multiple Paths in single string", scopeId: or.GetScopeId(), req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name,description"}, }, Item: &pb.Role{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.UpdateRoleResponse{ Item: &pb.Role{ Id: or.GetPublicId(), ScopeId: or.GetScopeId(), Scope: &scopes.ScopeInfo{Id: or.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: or.GetCreateTime().GetTimestamp(), GrantScopeIds: []string{p.GetPublicId()}, GrantStrings: []string{grant.GetRaw()}, Grants: []*pb.Grant{grant}, PrincipalIds: []string{u.GetPublicId()}, Principals: []*pb.Principal{principal}, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Update an Existing Project Scoped Role", scopeId: pr.GetScopeId(), req: &pbs.UpdateRoleRequest{ Id: pr.GetPublicId(), UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "description"}, }, Item: &pb.Role{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.UpdateRoleResponse{ Item: &pb.Role{ Id: pr.GetPublicId(), ScopeId: pr.GetScopeId(), Scope: &scopes.ScopeInfo{Id: pr.GetScopeId(), Type: scope.Project.String(), ParentScopeId: or.GetScopeId()}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: pr.GetCreateTime().GetTimestamp(), GrantScopeIds: []string{globals.GrantScopeThis}, GrantStrings: []string{grant.GetRaw()}, Grants: []*pb.Grant{grant}, PrincipalIds: []string{u.GetPublicId()}, Principals: []*pb.Principal{principal}, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Multiple Paths in single string", scopeId: pr.GetScopeId(), req: &pbs.UpdateRoleRequest{ Id: pr.GetPublicId(), UpdateMask: &field_mask.FieldMask{ Paths: []string{"name,description"}, }, Item: &pb.Role{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.UpdateRoleResponse{ Item: &pb.Role{ Id: pr.GetPublicId(), ScopeId: pr.GetScopeId(), Scope: &scopes.ScopeInfo{Id: pr.GetScopeId(), Type: scope.Project.String(), ParentScopeId: or.GetScopeId()}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: pr.GetCreateTime().GetTimestamp(), GrantScopeIds: []string{globals.GrantScopeThis}, GrantStrings: []string{grant.GetRaw()}, Grants: []*pb.Grant{grant}, PrincipalIds: []string{u.GetPublicId()}, Principals: []*pb.Principal{principal}, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "No Update Mask", req: &pbs.UpdateRoleRequest{ Item: &pb.Role{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "No Paths in Mask", scopeId: or.GetScopeId(), req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{Paths: []string{}}, Item: &pb.Role{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Only non-existent paths in Mask", scopeId: or.GetScopeId(), req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistent_field"}}, Item: &pb.Role{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Unset Name", scopeId: or.GetScopeId(), req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name"}, }, Item: &pb.Role{ Description: &wrapperspb.StringValue{Value: "ignored"}, }, }, res: &pbs.UpdateRoleResponse{ Item: &pb.Role{ Id: or.GetPublicId(), ScopeId: or.GetScopeId(), Scope: &scopes.ScopeInfo{Id: or.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Description: &wrapperspb.StringValue{Value: "default"}, CreatedTime: or.GetCreateTime().GetTimestamp(), GrantScopeIds: []string{p.GetPublicId()}, GrantStrings: []string{grant.GetRaw()}, Grants: []*pb.Grant{grant}, PrincipalIds: []string{u.GetPublicId()}, Principals: []*pb.Principal{principal}, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Update Only Name", scopeId: or.GetScopeId(), req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name"}, }, Item: &pb.Role{ Name: &wrapperspb.StringValue{Value: "updated"}, Description: &wrapperspb.StringValue{Value: "ignored"}, }, }, res: &pbs.UpdateRoleResponse{ Item: &pb.Role{ Id: or.GetPublicId(), ScopeId: or.GetScopeId(), Scope: &scopes.ScopeInfo{Id: or.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: "updated"}, Description: &wrapperspb.StringValue{Value: "default"}, CreatedTime: or.GetCreateTime().GetTimestamp(), GrantScopeIds: []string{p.GetPublicId()}, GrantStrings: []string{grant.GetRaw()}, Grants: []*pb.Grant{grant}, PrincipalIds: []string{u.GetPublicId()}, Principals: []*pb.Principal{principal}, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Update Only Description", scopeId: or.GetScopeId(), req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"description"}, }, Item: &pb.Role{ Name: &wrapperspb.StringValue{Value: "ignored"}, Description: &wrapperspb.StringValue{Value: "notignored"}, }, }, res: &pbs.UpdateRoleResponse{ Item: &pb.Role{ Id: or.GetPublicId(), ScopeId: or.GetScopeId(), Scope: &scopes.ScopeInfo{Id: or.GetScopeId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Name: &wrapperspb.StringValue{Value: "default"}, Description: &wrapperspb.StringValue{Value: "notignored"}, CreatedTime: or.GetCreateTime().GetTimestamp(), GrantScopeIds: []string{p.GetPublicId()}, GrantStrings: []string{grant.GetRaw()}, Grants: []*pb.Grant{grant}, PrincipalIds: []string{u.GetPublicId()}, Principals: []*pb.Principal{principal}, AuthorizedActions: testAuthorizedActions, }, }, }, { name: "Update a Non Existing Role", req: &pbs.UpdateRoleRequest{ Id: globals.RolePrefix + "_DoesntExis", UpdateMask: &field_mask.FieldMask{ Paths: []string{"description"}, }, Item: &pb.Role{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Cant change Id", req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"id"}, }, Item: &pb.Role{ Id: globals.RolePrefix + "_somethinge", Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "new desc"}, }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant specify Created Time", req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"created_time"}, }, Item: &pb.Role{ CreatedTime: timestamppb.Now(), }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant specify Updated Time", req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"updated_time"}, }, Item: &pb.Role{ UpdatedTime: timestamppb.Now(), }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant specify grants", req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"grants"}, }, Item: &pb.Role{ GrantStrings: []string{"anything"}, }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant specify principals", req: &pbs.UpdateRoleRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"principal_ids"}, }, Item: &pb.Role{ PrincipalIds: []string{"u_0987654321"}, }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { ver := orVersion if tc.req.Id == pr.PublicId { ver = prVersion } tc.req.Item.Version = ver assert, require := assert.New(t), require.New(t) req := proto.Clone(toMerge).(*pbs.UpdateRoleRequest) proto.Merge(req, tc.req) got, gErr := tested.UpdateRole(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "UpdateRole(%+v) got error %v, wanted %v", req, gErr, tc.err) } if tc.err == nil { defer resetRoles(req.Id == pr.PublicId) } if got != nil { assert.NotNilf(tc.res, "Expected UpdateRole response to be nil, but was %v", got) gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() require.NoError(err, "Error converting proto to timestamp") // Verify it is a role updated after it was created assert.True(gotUpdateTime.After(created), "Updated role should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) // Clear or set all values which are hard to compare against. got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil tc.res.Item.Version = got.Item.Version if req.Id == or.PublicId { orVersion = got.Item.Version } else { prVersion = got.Item.Version } } assert.Empty(cmp.Diff( got, tc.res, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), ), "UpdateRole(%q) got response\n%q,\nwanted\n%q", req, got, tc.res) }) } } func TestAddPrincipal(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } o, p := iam.TestScopes(t, iamRepo) s, err := roles.NewService(ctx, repoFn, 0) require.NoError(t, err, "Error when getting new role service.") 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]), ) ldapAuthMethod := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"}) ldapManagedGroup := ldap.TestManagedGroup(t, conn, ldapAuthMethod, []string{"admin"}) users := []*iam.User{ iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), } groups := []*iam.Group{ iam.TestGroup(t, conn, o.GetPublicId()), 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 addManagedGroups []string resultUsers []string resultGroups []string resultManagedGroups []string wantErr bool }{ { name: "Add user on empty role", setup: func(r *iam.Role) {}, addUsers: []string{users[1].GetPublicId()}, resultUsers: []string{users[1].GetPublicId()}, }, { name: "Add user on populated role", setup: func(r *iam.Role) { iam.TestUserRole(t, conn, r.GetPublicId(), users[0].GetPublicId()) }, addUsers: []string{users[1].GetPublicId()}, resultUsers: []string{users[0].GetPublicId(), users[1].GetPublicId()}, }, { name: "Add empty on populated role", setup: func(r *iam.Role) { iam.TestUserRole(t, conn, r.GetPublicId(), users[0].GetPublicId()) iam.TestUserRole(t, conn, r.GetPublicId(), users[1].GetPublicId()) }, wantErr: true, }, { name: "Add group on empty role", setup: func(r *iam.Role) {}, addGroups: []string{groups[1].GetPublicId()}, resultGroups: []string{groups[1].GetPublicId()}, }, { name: "Add group on populated role", setup: func(r *iam.Role) { iam.TestGroupRole(t, conn, r.GetPublicId(), groups[0].GetPublicId()) }, addGroups: []string{groups[1].GetPublicId()}, resultGroups: []string{groups[0].GetPublicId(), groups[1].GetPublicId()}, }, { name: "Add duplicate group on populated role", setup: func(r *iam.Role) { iam.TestGroupRole(t, conn, r.GetPublicId(), groups[0].GetPublicId()) }, 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 ldap managed group on populated role", setup: func(r *iam.Role) { iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[0].GetPublicId()) }, addManagedGroups: []string{ldapManagedGroup.GetPublicId()}, resultManagedGroups: []string{managedGroups[0].GetPublicId(), ldapManagedGroup.GetPublicId()}, }, { name: "Add invalid u_recovery on role", setup: func(r *iam.Role) {}, addUsers: []string{globals.RecoveryUserId}, wantErr: true, }, } for _, tc := range addCases { for _, scope := range []*iam.Scope{o, p} { t.Run(tc.name+"_"+scope.GetType(), func(t *testing.T) { role := iam.TestRole(t, conn, scope.GetPublicId()) tc.setup(role) req := &pbs.AddRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), PrincipalIds: append(tc.addUsers, append(tc.addGroups, tc.addManagedGroups...)...), } got, err := s.AddRolePrincipals(auth.DisabledAuthTestContext(repoFn, o.GetPublicId()), req) if tc.wantErr { assert.Error(t, err) return } s, ok := status.FromError(err) 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, append(tc.resultGroups, tc.resultManagedGroups...)...))) }) } } role := iam.TestRole(t, conn, p.GetPublicId()) failCases := []struct { name string req *pbs.AddRolePrincipalsRequest err error }{ { name: "Bad Role Id", req: &pbs.AddRolePrincipalsRequest{ Id: "bad id", Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Bad Principal Id", req: &pbs.AddRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), PrincipalIds: []string{"invalid"}, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "u_recovery Id", req: &pbs.AddRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), PrincipalIds: []string{globals.RecoveryUserId}, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.AddRolePrincipals(auth.DisabledAuthTestContext(repoFn, p.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "AddRolePrincipals(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } }) } } func TestSetPrincipal(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } s, err := roles.NewService(context.Background(), repoFn, 1000) 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]), ) ldapAuthMethod := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"}) ldapManagedGroup := ldap.TestManagedGroup(t, conn, ldapAuthMethod, []string{"admin"}) users := []*iam.User{ iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), } groups := []*iam.Group{ iam.TestGroup(t, conn, o.GetPublicId()), 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 setManagedGroups []string resultUsers []string resultGroups []string resultManagedGroups []string wantErr bool }{ { name: "Set user on empty role", setup: func(r *iam.Role) {}, setUsers: []string{users[1].GetPublicId()}, resultUsers: []string{users[1].GetPublicId()}, }, { name: "Set user on populated role", setup: func(r *iam.Role) { iam.TestUserRole(t, conn, r.GetPublicId(), users[0].GetPublicId()) }, setUsers: []string{users[1].GetPublicId()}, resultUsers: []string{users[1].GetPublicId()}, }, { name: "Set duplicate user on populated role", setup: func(r *iam.Role) { iam.TestUserRole(t, conn, r.GetPublicId(), users[0].GetPublicId()) }, setUsers: []string{users[1].GetPublicId(), users[1].GetPublicId()}, resultUsers: []string{users[1].GetPublicId()}, }, { name: "Set empty on populated role", setup: func(r *iam.Role) { iam.TestUserRole(t, conn, r.GetPublicId(), users[0].GetPublicId()) iam.TestUserRole(t, conn, r.GetPublicId(), users[1].GetPublicId()) }, setUsers: []string{}, resultUsers: nil, }, { name: "Set group on empty role", setup: func(r *iam.Role) {}, setGroups: []string{groups[1].GetPublicId()}, resultGroups: []string{groups[1].GetPublicId()}, }, { name: "Set group on populated role", setup: func(r *iam.Role) { iam.TestGroupRole(t, conn, r.GetPublicId(), groups[0].GetPublicId()) }, 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 LDAP managed group on populated role", setup: func(r *iam.Role) { iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[0].GetPublicId()) }, setManagedGroups: []string{ldapManagedGroup.GetPublicId()}, resultManagedGroups: []string{ldapManagedGroup.GetPublicId()}, }, { name: "Set invalid u_recovery on role", setup: func(r *iam.Role) {}, setUsers: []string{globals.RecoveryUserId}, wantErr: true, }, } for _, tc := range setCases { for _, scope := range []*iam.Scope{o, p} { t.Run(tc.name+"_"+scope.GetType(), func(t *testing.T) { role := iam.TestRole(t, conn, scope.GetPublicId()) tc.setup(role) req := &pbs.SetRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), PrincipalIds: append(tc.setUsers, append(tc.setGroups, tc.setManagedGroups...)...), } got, err := s.SetRolePrincipals(auth.DisabledAuthTestContext(repoFn, o.GetPublicId()), req) if tc.wantErr { assert.Error(t, err) return } s, ok := status.FromError(err) 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, append(tc.resultGroups, tc.resultManagedGroups...)...))) }) } } role := iam.TestRole(t, conn, p.GetPublicId()) failCases := []struct { name string req *pbs.SetRolePrincipalsRequest err error }{ { name: "Bad Role Id", req: &pbs.SetRolePrincipalsRequest{ Id: "bad id", Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Bad Principal Id", req: &pbs.SetRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), PrincipalIds: []string{"invalid"}, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "u_recovery", req: &pbs.SetRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), PrincipalIds: []string{globals.RecoveryUserId}, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.SetRolePrincipals(auth.DisabledAuthTestContext(repoFn, p.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "SetRolePrincipals(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } }) } } func TestRemovePrincipal(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } s, err := roles.NewService(ctx, repoFn, 0) require.NoError(t, err, "Error when getting new role service.") o, p := iam.TestScopes(t, iamRepo) 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]), ) ldapAuthMethod := ldap.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, []string{"ldaps://ldap1"}) ldapManagedGroup := ldap.TestManagedGroup(t, conn, ldapAuthMethod, []string{"admin"}) users := []*iam.User{ iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), iam.TestUser(t, iamRepo, o.GetPublicId()), } groups := []*iam.Group{ iam.TestGroup(t, conn, o.GetPublicId()), 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 removeManagedGroups []string resultUsers []string resultGroups []string resultManagedGroups []string wantErr bool }{ { name: "Remove user on empty role", setup: func(r *iam.Role) {}, removeUsers: []string{users[1].GetPublicId()}, wantErr: true, }, { name: "Remove 1 of 2 users from role", setup: func(r *iam.Role) { iam.TestUserRole(t, conn, r.GetPublicId(), users[0].GetPublicId()) iam.TestUserRole(t, conn, r.GetPublicId(), users[1].GetPublicId()) }, removeUsers: []string{users[1].GetPublicId()}, resultUsers: []string{users[0].GetPublicId()}, }, { name: "Remove 1 duplicate user of 2 users from role", setup: func(r *iam.Role) { iam.TestUserRole(t, conn, r.GetPublicId(), users[0].GetPublicId()) iam.TestUserRole(t, conn, r.GetPublicId(), users[1].GetPublicId()) }, removeUsers: []string{users[1].GetPublicId(), users[1].GetPublicId()}, resultUsers: []string{users[0].GetPublicId()}, }, { name: "Remove all users from role", setup: func(r *iam.Role) { iam.TestUserRole(t, conn, r.GetPublicId(), users[0].GetPublicId()) iam.TestUserRole(t, conn, r.GetPublicId(), users[1].GetPublicId()) }, removeUsers: []string{users[0].GetPublicId(), users[1].GetPublicId()}, resultUsers: []string{}, }, { name: "Remove empty on populated role", setup: func(r *iam.Role) { iam.TestUserRole(t, conn, r.GetPublicId(), users[0].GetPublicId()) }, wantErr: true, }, { name: "Remove group on empty role", setup: func(r *iam.Role) {}, removeGroups: []string{groups[1].GetPublicId()}, wantErr: true, }, { name: "Remove 1 of 2 groups from role", setup: func(r *iam.Role) { iam.TestGroupRole(t, conn, r.GetPublicId(), groups[0].GetPublicId()) iam.TestGroupRole(t, conn, r.GetPublicId(), groups[1].GetPublicId()) }, removeGroups: []string{groups[1].GetPublicId()}, resultGroups: []string{groups[0].GetPublicId()}, }, { name: "Remove 1 duplicate group of 2 groups from role", setup: func(r *iam.Role) { iam.TestGroupRole(t, conn, r.GetPublicId(), groups[0].GetPublicId()) iam.TestGroupRole(t, conn, r.GetPublicId(), groups[1].GetPublicId()) }, removeGroups: []string{groups[1].GetPublicId(), groups[1].GetPublicId()}, resultGroups: []string{groups[0].GetPublicId()}, }, { name: "Remove all groups from role", setup: func(r *iam.Role) { iam.TestGroupRole(t, conn, r.GetPublicId(), groups[0].GetPublicId()) iam.TestGroupRole(t, conn, r.GetPublicId(), groups[1].GetPublicId()) }, 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{}, }, { name: "Remove LDAP managed groups from role", setup: func(r *iam.Role) { iam.TestManagedGroupRole(t, conn, r.GetPublicId(), ldapManagedGroup.GetPublicId()) iam.TestManagedGroupRole(t, conn, r.GetPublicId(), managedGroups[0].GetPublicId()) }, removeManagedGroups: []string{ldapManagedGroup.GetPublicId()}, resultManagedGroups: []string{managedGroups[0].GetPublicId()}, }, } for _, tc := range addCases { for _, scope := range []*iam.Scope{o, p} { t.Run(tc.name+"_"+scope.GetType(), func(t *testing.T) { role := iam.TestRole(t, conn, scope.GetPublicId()) tc.setup(role) req := &pbs.RemoveRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), PrincipalIds: append(tc.removeUsers, append(tc.removeGroups, tc.removeManagedGroups...)...), } got, err := s.RemoveRolePrincipals(auth.DisabledAuthTestContext(repoFn, o.GetPublicId()), req) if tc.wantErr { assert.Error(t, err) return } s, ok := status.FromError(err) require.True(t, ok) require.NoError(t, err, "Got error: %v", s) assert.True(t, equalPrincipals(got.GetItem(), append(tc.resultUsers, append(tc.resultGroups, tc.resultManagedGroups...)...))) }) } } role := iam.TestRole(t, conn, p.GetPublicId()) failCases := []struct { name string req *pbs.RemoveRolePrincipalsRequest err error }{ { name: "Bad Role Id", req: &pbs.RemoveRolePrincipalsRequest{ Id: "bad id", Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Bad User Id", req: &pbs.RemoveRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), PrincipalIds: []string{"g_validgroup", "invaliduser"}, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Bad Group Id", req: &pbs.RemoveRolePrincipalsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), PrincipalIds: []string{"u_validuser", "invalidgroup"}, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.RemoveRolePrincipals(auth.DisabledAuthTestContext(repoFn, p.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "AddRolePrincipals(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } }) } } func checkEqualGrants(t *testing.T, expected []string, got *pb.Role) { require, assert := require.New(t), assert.New(t) require.Equal(len(expected), len(got.GrantStrings)) require.Equal(len(expected), len(got.Grants)) // sort expected and got to ensure they are in the same order sort.Strings(expected) sort.Slice(got.GrantStrings, func(i, j int) bool { return got.GrantStrings[i] < got.GrantStrings[j] }) for i, v := range expected { parsed, err := perms.Parse(context.Background(), perms.GrantTuple{RoleScopeId: "o_abc123", GrantScopeId: "o_abc123", Grant: v}) require.NoError(err) assert.Equal(expected[i], got.GrantStrings[i]) assert.Equal(expected[i], got.Grants[i].GetRaw()) assert.Equal(v, got.Grants[i].GetCanonical()) j := got.Grants[i].GetJson() require.NotNil(j) assert.Equal(parsed.Id(), j.GetId()) assert.Equal(parsed.Ids(), j.GetIds()) assert.Equal(parsed.Type().String(), j.GetType()) _, acts := parsed.Actions() assert.Equal(acts, j.GetActions()) } } func TestAddGrants(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } s, err := roles.NewService(context.Background(), repoFn, 1000) require.NoError(t, err, "Error when getting new role service.") addCases := []struct { name string existing []string add []string result []string wantErr bool wantErrContains string }{ { name: "Add grant on empty role", add: []string{"ids=*;type=*;actions=delete"}, result: []string{"ids=*;type=*;actions=delete"}, }, { name: "Add grant on role with grant", existing: []string{"ids=u_foo;actions=read"}, add: []string{"ids=*;type=*;actions=delete"}, result: []string{"ids=u_foo;actions=read", "ids=*;type=*;actions=delete"}, }, { name: "Add duplicate grant on role with grant", existing: []string{"ids=u_fooaA1;actions=read"}, add: []string{"ids=*;type=*;actions=delete", "ids=*;type=*;actions=delete"}, result: []string{"ids=u_fooaA1;actions=read", "ids=*;type=*;actions=delete"}, }, { name: "Add grant matching existing grant", existing: []string{"ids=u_foo;actions=read", "ids=*;type=*;actions=delete"}, add: []string{"ids=*;type=*;actions=delete"}, wantErr: true, }, { name: "Check add-host-sets deprecation", existing: []string{"ids=u_foo;actions=read", "ids=*;type=*;actions=delete"}, add: []string{"ids=*;type=target;actions=add-host-sets"}, wantErr: true, wantErrContains: "Use \\\"add-host-sources\\\" instead", }, { name: "Check id field deprecation", existing: []string{"id=u_fooaA1;actions=read"}, add: []string{"id=*;type=*;actions=delete"}, result: []string{"id=u_fooaA1;actions=read", "id=*;type=*;actions=delete"}, wantErr: func() bool { return !version.SupportsFeature(version.Binary, version.SupportIdInGrants) }(), }, } for _, tc := range addCases { o, p := iam.TestScopes(t, iamRepo) for _, scope := range []*iam.Scope{o, p} { t.Run(tc.name+"_"+scope.Type, func(t *testing.T) { assert, require := assert.New(t), require.New(t) role := iam.TestRole(t, conn, scope.GetPublicId()) for _, e := range tc.existing { _ = iam.TestRoleGrant(t, conn, role.GetPublicId(), e) } req := &pbs.AddRoleGrantsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), } scopeId := o.GetPublicId() if o != scope { scopeId = p.GetPublicId() } req.GrantStrings = tc.add got, err := s.AddRoleGrants(auth.DisabledAuthTestContext(repoFn, scopeId), req) if tc.wantErr { assert.Error(err) if tc.wantErrContains != "" { assert.Contains(err.Error(), tc.wantErrContains) } return } require.NoError(err) checkEqualGrants(t, tc.result, got.GetItem()) }) } } _, p := iam.TestScopes(t, iamRepo) role := iam.TestRole(t, conn, p.GetPublicId()) failCases := []struct { name string req *pbs.AddRoleGrantsRequest err error }{ { name: "Bad Version", req: &pbs.AddRoleGrantsRequest{ Id: role.GetPublicId(), GrantStrings: []string{"ids=*;type=*;actions=create"}, Version: role.GetVersion() + 2, }, err: handlers.ApiErrorWithCode(codes.Internal), }, { name: "Bad Role Id", req: &pbs.AddRoleGrantsRequest{ Id: "bad id", GrantStrings: []string{"ids=*;type=*;actions=create"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Unparseable Grant", req: &pbs.AddRoleGrantsRequest{ Id: role.GetPublicId(), GrantStrings: []string{"ids=*;type=*;actions=create", "unparseable"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Empty Grant", req: &pbs.AddRoleGrantsRequest{ Id: role.GetPublicId(), GrantStrings: []string{"ids=*;type=*;actions=create", ""}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.AddRoleGrants(auth.DisabledAuthTestContext(repoFn, p.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "AddRoleGrants(%+v) got error %#v, wanted %#v", tc.req, gErr, tc.err) } }) } } func TestSetGrants(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } s, err := roles.NewService(context.Background(), repoFn, 1000) require.NoError(t, err, "Error when getting new role service.") setCases := []struct { name string existing []string set []string result []string wantErr bool wantErrContains string }{ { name: "Set grant on empty role", set: []string{"ids=*;type=*;actions=delete"}, result: []string{"ids=*;type=*;actions=delete"}, }, { name: "Set grant on role with grant", existing: []string{"ids=u_foo;actions=read"}, set: []string{"ids=*;type=*;actions=delete"}, result: []string{"ids=*;type=*;actions=delete"}, }, { name: "Set grant matching existing grant", existing: []string{"ids=u_foo;actions=read", "ids=*;type=*;actions=delete"}, set: []string{"ids=*;type=*;actions=delete"}, result: []string{"ids=*;type=*;actions=delete"}, }, { name: "Set duplicate grant matching existing grant", existing: []string{"ids=u_foo;actions=read", "ids=*;type=*;actions=delete"}, set: []string{"ids=*;type=*;actions=delete", "ids=*;type=*;actions=delete"}, result: []string{"ids=*;type=*;actions=delete"}, }, { name: "Set empty on role", existing: []string{"ids=hcst_foo;type=*;actions=read", "ids=*;type=*;actions=delete"}, set: nil, result: nil, }, { name: "Check add-host-sets deprecation", existing: []string{"ids=u_foo;actions=read", "ids=*;type=*;actions=delete"}, set: []string{"ids=*;type=target;actions=add-host-sets"}, wantErr: true, wantErrContains: "Use \\\"add-host-sources\\\" instead", }, { name: "Check id field deprecation", existing: []string{"id=u_fooaA1;actions=read"}, set: []string{"id=*;type=*;actions=delete"}, result: []string{"id=*;type=*;actions=delete"}, wantErr: func() bool { return !version.SupportsFeature(version.Binary, version.SupportIdInGrants) }(), }, } for _, tc := range setCases { o, p := iam.TestScopes(t, iamRepo) for _, scope := range []*iam.Scope{o, p} { t.Run(tc.name+"_"+scope.Type, func(t *testing.T) { assert, require := assert.New(t), require.New(t) role := iam.TestRole(t, conn, scope.GetPublicId()) for _, e := range tc.existing { _ = iam.TestRoleGrant(t, conn, role.GetPublicId(), e) } req := &pbs.SetRoleGrantsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), } scopeId := o.GetPublicId() if o != scope { scopeId = p.GetPublicId() } req.GrantStrings = tc.set got, err := s.SetRoleGrants(auth.DisabledAuthTestContext(repoFn, scopeId), req) if tc.wantErr { assert.Error(err) if tc.wantErrContains != "" { assert.Contains(err.Error(), tc.wantErrContains) } return } require.NoError(err, "Got error %v", err) checkEqualGrants(t, tc.result, got.GetItem()) }) } } _, p := iam.TestScopes(t, iamRepo) role := iam.TestRole(t, conn, p.GetPublicId()) failCases := []struct { name string req *pbs.SetRoleGrantsRequest err error }{ { name: "Bad Version", req: &pbs.SetRoleGrantsRequest{ Id: role.GetPublicId(), GrantStrings: []string{"ids=*;type=*;actions=create"}, Version: role.GetVersion() + 2, }, err: handlers.ApiErrorWithCode(codes.Internal), }, { name: "Bad Role Id", req: &pbs.SetRoleGrantsRequest{ Id: "bad id", GrantStrings: []string{"ids=*;type=*;actions=create"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Unparsable grant", req: &pbs.SetRoleGrantsRequest{ Id: role.GetPublicId(), GrantStrings: []string{"ids=*;type=*;actions=create", "unparseable"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.SetRoleGrants(auth.DisabledAuthTestContext(repoFn, p.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "SetRoleGrants(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } }) } } func TestRemoveGrants(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } s, err := roles.NewService(context.Background(), repoFn, 1000) require.NoError(t, err, "Error when getting new role service.") removeCases := []struct { name string existing []string remove []string result []string wantErr bool }{ { name: "Remove all", existing: []string{"ids=hcst_1;type=*;actions=read"}, remove: []string{"ids=hcst_1;type=*;actions=read"}, }, { name: "Remove partial", existing: []string{"ids=hcst_1;type=*;actions=read", "ids=hcst_2;type=*;actions=delete"}, remove: []string{"ids=hcst_1;type=*;actions=read"}, result: []string{"ids=hcst_2;type=*;actions=delete"}, }, { name: "Remove duplicate", existing: []string{"ids=hcst_1;type=*;actions=read", "ids=hcst_2;type=*;actions=delete"}, remove: []string{"ids=hcst_1;type=*;actions=read", "ids=hcst_1;type=*;actions=read"}, result: []string{"ids=hcst_2;type=*;actions=delete"}, }, { name: "Remove non existent", existing: []string{"ids=hcst_2;type=*;actions=delete"}, remove: []string{"ids=hcst_1;type=*;actions=read"}, result: []string{"ids=hcst_2;type=*;actions=delete"}, }, { name: "Remove from empty role", existing: []string{}, remove: []string{"ids=hcst_1;type=*;actions=read"}, result: nil, }, } for _, tc := range removeCases { o, p := iam.TestScopes(t, iamRepo) for _, scope := range []*iam.Scope{o, p} { t.Run(tc.name+"_"+scope.Type, func(t *testing.T) { assert, require := assert.New(t), require.New(t) role := iam.TestRole(t, conn, scope.GetPublicId()) for _, e := range tc.existing { _ = iam.TestRoleGrant(t, conn, role.GetPublicId(), e) } req := &pbs.RemoveRoleGrantsRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), } scopeId := o.GetPublicId() if o != scope { scopeId = p.GetPublicId() } req.GrantStrings = tc.remove got, err := s.RemoveRoleGrants(auth.DisabledAuthTestContext(repoFn, scopeId), req) if tc.wantErr { assert.Error(err) return } s, ok := status.FromError(err) assert.True(ok) require.NoError(err, "Got error %v", s) checkEqualGrants(t, tc.result, got.GetItem()) }) } } _, p := iam.TestScopes(t, iamRepo) role := iam.TestRole(t, conn, p.GetPublicId()) failCases := []struct { name string req *pbs.RemoveRoleGrantsRequest err error }{ { name: "Bad Version", req: &pbs.RemoveRoleGrantsRequest{ Id: role.GetPublicId(), GrantStrings: []string{"ids=hcst_2;type=*;actions=create"}, Version: role.GetVersion() + 2, }, err: handlers.ApiErrorWithCode(codes.Internal), }, { name: "Bad Role Id", req: &pbs.RemoveRoleGrantsRequest{ Id: "bad id", GrantStrings: []string{"ids=*;type=*;actions=create"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Empty Grant", req: &pbs.RemoveRoleGrantsRequest{ Id: role.GetPublicId(), GrantStrings: []string{"ids=*;type=*;actions=create", ""}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Unparseable Grant", req: &pbs.RemoveRoleGrantsRequest{ Id: role.GetPublicId(), GrantStrings: []string{"ids=*;type=*;actions=create", ";unparsable=2"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.RemoveRoleGrants(auth.DisabledAuthTestContext(repoFn, p.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "RemoveRoleGrants(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } }) } } func TestAddGrantScopes(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } s, err := roles.NewService(context.Background(), repoFn, 1000) require.NoError(t, err, "Error when getting new role service.") o, p := iam.TestScopes(t, iamRepo) o2, p2 := iam.TestScopes(t, iamRepo) addCases := []struct { name string scopeId string existing []string add []string result []string wantErr bool wantErrContains string }{ { name: "Add grant scopes on empty role - global", scopeId: scope.Global.String(), add: []string{"this", "descendants"}, result: []string{"this", "descendants"}, }, { name: "Add grant scopes on role with grant scopes - global", scopeId: scope.Global.String(), existing: []string{"global", o.PublicId}, add: []string{"children", p.PublicId}, result: []string{"global", o.PublicId, p.PublicId, "children"}, }, { name: "Add duplicate grant on role with grant scopes - global", scopeId: scope.Global.String(), existing: []string{"this"}, add: []string{"children", "children"}, result: []string{"this", "children"}, }, { name: "Add other org/proj scopes - global", scopeId: scope.Global.String(), existing: []string{"this"}, add: []string{o2.PublicId, p2.PublicId}, result: []string{"this", o2.PublicId, p2.PublicId}, }, { name: "Add grant scope matching existing grant scopes - global", scopeId: scope.Global.String(), existing: []string{"this"}, add: []string{"this", "children"}, wantErr: true, }, { name: "Add invalid grant scope - global", scopeId: scope.Global.String(), existing: []string{"this"}, add: []string{"p_foobar1234", "children"}, wantErr: true, }, { name: "Add this to scope grant scope - global", scopeId: scope.Global.String(), existing: []string{scope.Global.String()}, add: []string{"this"}, wantErr: true, }, { name: "Add scope to this grant scope - global", scopeId: scope.Global.String(), existing: []string{"this"}, add: []string{scope.Global.String()}, wantErr: true, }, { name: "Add children when descendants exists - global", scopeId: scope.Global.String(), existing: []string{"descendants"}, add: []string{"children"}, wantErr: true, }, { name: "Add descendants when children exists - global", scopeId: scope.Global.String(), existing: []string{"children"}, add: []string{"descendants"}, wantErr: true, }, { name: "Add grant scopes on empty role - org", scopeId: o.PublicId, add: []string{"this", "children"}, result: []string{"this", "children"}, }, { name: "Add grant scopes on role with grant scopes - org", scopeId: o.PublicId, existing: []string{o.PublicId, p.PublicId}, add: []string{"children"}, result: []string{o.PublicId, p.PublicId, "children"}, }, { name: "Add duplicate grant on role with grant scopes - org", scopeId: o.PublicId, existing: []string{"this"}, add: []string{"children", "children"}, result: []string{"this", "children"}, }, { name: "Add grant scope matching existing grant scopes - org", scopeId: o.PublicId, existing: []string{"this"}, add: []string{"this", "children"}, wantErr: true, }, { name: "Add invalid grant scope - org", scopeId: o.PublicId, existing: []string{"this"}, add: []string{"p_foobar1234", "children"}, wantErr: true, }, { name: "Add invalid grant scope - org - descendants", scopeId: o.PublicId, existing: []string{"this"}, add: []string{"descendants", "children"}, wantErr: true, }, { name: "Add this to scope grant scope - org", scopeId: o.PublicId, existing: []string{o.PublicId}, add: []string{"this"}, wantErr: true, }, { name: "Add scope to this grant scope - org", scopeId: o.PublicId, existing: []string{"this"}, add: []string{o.PublicId}, wantErr: true, }, { name: "Add other org/proj scopes - org", scopeId: o.PublicId, existing: []string{"this"}, add: []string{p2.PublicId}, wantErr: true, }, { name: "Add grant scopes on empty role - proj with id", scopeId: p.PublicId, add: []string{p.PublicId}, result: []string{p.PublicId}, }, { name: "Add grant scopes on empty role - proj with this", scopeId: p.PublicId, add: []string{"this"}, result: []string{"this"}, }, { name: "Add duplicate scopes on empty role - proj", scopeId: p.PublicId, add: []string{"this", "this"}, result: []string{"this"}, }, { name: "Add invalid grant scope - proj", scopeId: p.PublicId, existing: []string{"this"}, add: []string{"p_foobar1234"}, wantErr: true, }, { name: "Add invalid grant scope - proj - descendants", scopeId: p.PublicId, existing: []string{"this"}, add: []string{"descendants"}, wantErr: true, }, { name: "Add invalid grant scope - proj - children", scopeId: p.PublicId, existing: []string{"this"}, add: []string{"children"}, wantErr: true, }, { name: "Add this to scope grant scope - proj", scopeId: p.PublicId, existing: []string{p.PublicId}, add: []string{"this"}, wantErr: true, }, { name: "Add scope to this grant scope - proj", scopeId: p.PublicId, existing: []string{"this"}, add: []string{p.PublicId}, wantErr: true, }, { name: "Add other org/proj scopes - proj", scopeId: p.PublicId, existing: []string{}, add: []string{p2.PublicId}, wantErr: true, }, } for _, tc := range addCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) role := iam.TestRole(t, conn, tc.scopeId, iam.WithGrantScopeIds([]string{"testing-none"})) for _, e := range tc.existing { _ = iam.TestRoleGrantScope(t, conn, role.GetPublicId(), e) } req := &pbs.AddRoleGrantScopesRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), } req.GrantScopeIds = tc.add got, err := s.AddRoleGrantScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req) if tc.wantErr { assert.Error(err) if tc.wantErrContains != "" { assert.Contains(err.Error(), tc.wantErrContains) } return } require.NoError(err) require.ElementsMatch(tc.result, got.GetItem().GetGrantScopeIds()) }) } _, p = iam.TestScopes(t, iamRepo) role := iam.TestRole(t, conn, p.GetPublicId(), iam.WithGrantScopeIds([]string{"testing-none"})) failCases := []struct { name string req *pbs.AddRoleGrantScopesRequest err error }{ { name: "Bad Version", req: &pbs.AddRoleGrantScopesRequest{ Id: role.GetPublicId(), GrantScopeIds: []string{"this"}, Version: role.GetVersion() + 2, }, err: handlers.ApiErrorWithCode(codes.Internal), }, { name: "Bad Role Id", req: &pbs.AddRoleGrantScopesRequest{ Id: "bad id", GrantScopeIds: []string{"this"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Invalid scope ID", req: &pbs.AddRoleGrantScopesRequest{ Id: role.GetPublicId(), GrantScopeIds: []string{"p_foobar1234"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Empty Grant Scope ID", req: &pbs.AddRoleGrantScopesRequest{ Id: role.GetPublicId(), GrantScopeIds: []string{"this", ""}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.AddRoleGrantScopes(auth.DisabledAuthTestContext(repoFn, p.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "AddRoleGrantScopes(%+v) got error %#v, wanted %#v", tc.req, gErr, tc.err) } }) } } func TestSetGrantScopes(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } s, err := roles.NewService(context.Background(), repoFn, 1000) require.NoError(t, err, "Error when getting new role service.") o, p := iam.TestScopes(t, iamRepo) o2, p2 := iam.TestScopes(t, iamRepo) setCases := []struct { name string scopeId string existing []string set []string result []string wantErr bool wantErrContains string }{ { name: "Set grant scopes on empty role - global", scopeId: scope.Global.String(), set: []string{"this", "descendants"}, result: []string{"this", "descendants"}, }, { name: "Set grant scopes on role with grant scopes - global", scopeId: scope.Global.String(), existing: []string{"global", o.PublicId}, set: []string{"children", p.PublicId}, result: []string{"children", p.PublicId}, }, { name: "Set duplicate grant on role with grant scopes - global", scopeId: scope.Global.String(), existing: []string{"this"}, set: []string{"children", "children"}, result: []string{"children"}, }, { name: "Set grant scope matching existing grant scopes - global", scopeId: scope.Global.String(), existing: []string{"this"}, set: []string{"this", "children"}, result: []string{"this", "children"}, }, { name: "Set this to scope grant scope - global", scopeId: scope.Global.String(), existing: []string{scope.Global.String()}, set: []string{"this", "children"}, result: []string{"this", "children"}, }, { name: "Set scope to this grant scope - global", scopeId: scope.Global.String(), existing: []string{"this"}, set: []string{scope.Global.String(), "children"}, result: []string{scope.Global.String(), "children"}, }, { name: "Set other org/proj scopes - global", scopeId: scope.Global.String(), existing: []string{"this"}, set: []string{"this", o2.PublicId, p2.PublicId}, result: []string{"this", o2.PublicId, p2.PublicId}, }, { name: "Set invalid grant scope - global", scopeId: scope.Global.String(), existing: []string{"this"}, set: []string{"p_foobar1234", "children"}, wantErr: true, }, { name: "Set both on grant scope - global", scopeId: scope.Global.String(), existing: []string{}, set: []string{scope.Global.String(), "children", "this"}, wantErr: true, }, { name: "Set both grant scope - global", scopeId: scope.Global.String(), existing: []string{}, set: []string{"this", scope.Global.String(), "children"}, wantErr: true, }, { name: "Set children and descendants - global", scopeId: scope.Global.String(), existing: []string{}, set: []string{"children", "descendants"}, wantErr: true, }, { name: "Set descendants and children- global", scopeId: scope.Global.String(), existing: []string{}, set: []string{"descendants", "children"}, wantErr: true, }, { name: "Set grant scopes on empty role - org", scopeId: o.PublicId, set: []string{"this", "children"}, result: []string{"this", "children"}, }, { name: "Set grant scopes on role with grant scopes - org", scopeId: o.PublicId, existing: []string{o.PublicId, p.PublicId}, set: []string{"children"}, result: []string{"children"}, }, { name: "Set duplicate grant on role with grant scopes - org", scopeId: o.PublicId, existing: []string{"this"}, set: []string{p.PublicId, "children", "children"}, result: []string{p.PublicId, "children"}, }, { name: "Set grant scope matching existing grant scopes - org", scopeId: o.PublicId, existing: []string{"this"}, set: []string{"this", "children"}, result: []string{"this", "children"}, }, { name: "Set this to scope grant scope - org", scopeId: o.PublicId, existing: []string{o.PublicId}, set: []string{"this", "children"}, result: []string{"this", "children"}, }, { name: "Set scope to this grant scope - org", scopeId: o.PublicId, existing: []string{"this"}, set: []string{o.PublicId, "children"}, result: []string{o.PublicId, "children"}, }, { name: "Set invalid grant scope - org", scopeId: o.PublicId, existing: []string{"this"}, set: []string{"p_foobar1234", "children"}, wantErr: true, }, { name: "Set invalid grant scope - org - descendants", scopeId: o.PublicId, existing: []string{"this"}, set: []string{"descendants", "children"}, wantErr: true, }, { name: "Set both on grant scope - org", scopeId: o.PublicId, existing: []string{}, set: []string{o.PublicId, "children", "this"}, wantErr: true, }, { name: "Set both grant scope - org", scopeId: o.PublicId, existing: []string{}, set: []string{"this", o.PublicId, "children"}, wantErr: true, }, { name: "Set grant scopes on empty role - proj with id", scopeId: p.PublicId, set: []string{p.PublicId}, result: []string{p.PublicId}, }, { name: "Set grant scopes on empty role - proj with this", scopeId: p.PublicId, set: []string{"this"}, result: []string{"this"}, }, { name: "Set duplicate scopes on empty role - proj", scopeId: p.PublicId, set: []string{"this", "this"}, result: []string{"this"}, }, { name: "Set this to scope grant scope - proj", scopeId: p.PublicId, existing: []string{p.PublicId}, set: []string{"this"}, result: []string{"this"}, }, { name: "Set scope to this grant scope - proj", scopeId: p.PublicId, existing: []string{"this"}, set: []string{p.PublicId}, result: []string{p.PublicId}, }, { name: "Set other org/proj scopes - org", scopeId: o.PublicId, existing: []string{"this"}, set: []string{"this", p2.PublicId}, wantErr: true, }, { name: "Set invalid grant scope - proj", scopeId: p.PublicId, existing: []string{"this"}, set: []string{"p_foobar1234"}, wantErr: true, }, { name: "Set invalid grant scope - proj - descendants", scopeId: p.PublicId, existing: []string{"this"}, set: []string{"descendants"}, wantErr: true, }, { name: "Set invalid grant scope - proj - children", scopeId: p.PublicId, existing: []string{"this"}, set: []string{"children"}, wantErr: true, }, { name: "Set both on grant scope - proj", scopeId: p.PublicId, existing: []string{}, set: []string{p.PublicId, "this"}, wantErr: true, }, { name: "Set both grant scope - proj", scopeId: p.PublicId, existing: []string{}, set: []string{"this", p.PublicId}, wantErr: true, }, { name: "Set other org/proj scopes - proj", scopeId: p.PublicId, existing: []string{}, set: []string{p2.PublicId}, wantErr: true, }, } for _, tc := range setCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) role := iam.TestRole(t, conn, tc.scopeId, iam.WithGrantScopeIds([]string{"testing-none"})) for _, e := range tc.existing { _ = iam.TestRoleGrantScope(t, conn, role.GetPublicId(), e) } req := &pbs.SetRoleGrantScopesRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), } req.GrantScopeIds = tc.set got, err := s.SetRoleGrantScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req) if tc.wantErr { assert.Error(err) if tc.wantErrContains != "" { assert.Contains(err.Error(), tc.wantErrContains) } return } require.NoError(err) require.ElementsMatch(tc.result, got.GetItem().GetGrantScopeIds()) }) } role := iam.TestRole(t, conn, p.GetPublicId(), iam.WithGrantScopeIds([]string{"testing-none"})) failCases := []struct { name string req *pbs.SetRoleGrantScopesRequest err error }{ { name: "Bad Version", req: &pbs.SetRoleGrantScopesRequest{ Id: role.GetPublicId(), GrantScopeIds: []string{"this"}, Version: role.GetVersion() + 2, }, err: handlers.ApiErrorWithCode(codes.Internal), }, { name: "Bad Role Id", req: &pbs.SetRoleGrantScopesRequest{ Id: "bad id", GrantScopeIds: []string{"this"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Invalid scope ID", req: &pbs.SetRoleGrantScopesRequest{ Id: role.GetPublicId(), GrantScopeIds: []string{"p_foobar1234"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Empty Grant Scope ID", req: &pbs.SetRoleGrantScopesRequest{ Id: role.GetPublicId(), GrantScopeIds: []string{"this", ""}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.SetRoleGrantScopes(auth.DisabledAuthTestContext(repoFn, p.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "SetRoleGrantScopes(%+v) got error %#v, wanted %#v", tc.req, gErr, tc.err) } }) } } func TestRemoveGrantScopes(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } s, err := roles.NewService(context.Background(), repoFn, 1000) require.NoError(t, err, "Error when getting new role service.") o, p := iam.TestScopes(t, iamRepo) removeCases := []struct { name string scopeId string existing []string remove []string result []string wantErr bool }{ { name: "Remove all - global", scopeId: scope.Global.String(), existing: []string{"this", "descendants"}, remove: []string{"this", "descendants"}, }, { name: "Remove partial - global", scopeId: scope.Global.String(), existing: []string{"this", o.PublicId, "descendants", p.PublicId}, remove: []string{o.PublicId, p.PublicId}, result: []string{"this", "descendants"}, }, { name: "Remove duplicate - global", scopeId: scope.Global.String(), existing: []string{"this", "children", o.PublicId}, remove: []string{"children", o.PublicId, "children"}, result: []string{"this"}, }, { name: "Remove non existent - global", scopeId: scope.Global.String(), existing: []string{"this", "descendants"}, remove: []string{"p_foobar1234"}, result: []string{"this", "descendants"}, }, { name: "Remove from empty role - global", scopeId: scope.Global.String(), existing: []string{}, remove: []string{"this"}, result: nil, }, { name: "Remove all - org", scopeId: o.PublicId, existing: []string{"this", "children"}, remove: []string{"this", "children"}, }, { name: "Remove partial - org", scopeId: o.PublicId, existing: []string{"children", o.PublicId, p.PublicId}, remove: []string{"children"}, result: []string{o.PublicId, p.PublicId}, }, { name: "Remove duplicate - org", scopeId: o.PublicId, existing: []string{"this", p.PublicId, "children"}, remove: []string{"children", p.PublicId, "children"}, result: []string{"this"}, }, { name: "Remove non existent - org", scopeId: o.PublicId, existing: []string{"this", "children"}, remove: []string{"p_foobar1234"}, result: []string{"this", "children"}, }, { name: "Remove from empty role - org", scopeId: o.PublicId, existing: []string{}, remove: []string{"this"}, result: nil, }, { name: "Remove all - proj", scopeId: p.PublicId, existing: []string{p.PublicId}, remove: []string{p.PublicId}, }, { name: "Remove duplicate - proj", scopeId: p.PublicId, existing: []string{"this"}, remove: []string{"this", "this"}, result: []string{}, }, { name: "Remove non existent - proj", scopeId: p.PublicId, existing: []string{"this"}, remove: []string{"p_foobar1234"}, result: []string{"this"}, }, { name: "Remove from empty role - proj", scopeId: p.PublicId, existing: []string{}, remove: []string{"this"}, result: nil, }, } for _, tc := range removeCases { t.Run(tc.name+"_", func(t *testing.T) { assert, require := assert.New(t), require.New(t) role := iam.TestRole(t, conn, tc.scopeId, iam.WithGrantScopeIds([]string{"testing-none"})) for _, e := range tc.existing { _ = iam.TestRoleGrantScope(t, conn, role.GetPublicId(), e) } req := &pbs.RemoveRoleGrantScopesRequest{ Id: role.GetPublicId(), Version: role.GetVersion(), } req.GrantScopeIds = tc.remove got, err := s.RemoveRoleGrantScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req) if tc.wantErr { assert.Error(err) return } s, ok := status.FromError(err) assert.True(ok) require.NoError(err, "Got error %v", s) require.ElementsMatch(tc.result, got.GetItem().GetGrantScopeIds()) }) } role := iam.TestRole(t, conn, p.GetPublicId(), iam.WithGrantScopeIds([]string{"testing-none"})) failCases := []struct { name string req *pbs.RemoveRoleGrantScopesRequest err error }{ { name: "Bad Version", req: &pbs.RemoveRoleGrantScopesRequest{ Id: role.GetPublicId(), GrantScopeIds: []string{"this"}, Version: role.GetVersion() + 2, }, err: handlers.ApiErrorWithCode(codes.Internal), }, { name: "Bad Role Id", req: &pbs.RemoveRoleGrantScopesRequest{ Id: "bad id", GrantScopeIds: []string{"this"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Empty Grant Scope ID", req: &pbs.RemoveRoleGrantScopesRequest{ Id: role.GetPublicId(), GrantScopeIds: []string{"this", ""}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Invalid Grant Scope ID", req: &pbs.RemoveRoleGrantScopesRequest{ Id: role.GetPublicId(), GrantScopeIds: []string{"r_foobar1234"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range failCases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) _, gErr := s.RemoveRoleGrantScopes(auth.DisabledAuthTestContext(repoFn, p.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "RemoveRoleGrantScopes(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } }) } }