diff --git a/services/settings/pkg/service/v0/service.go b/services/settings/pkg/service/v0/service.go index abdd37624e2..f5313165133 100644 --- a/services/settings/pkg/service/v0/service.go +++ b/services/settings/pkg/service/v0/service.go @@ -355,7 +355,8 @@ func (g Service) ListRoleAssignmentsFiltered(ctx context.Context, req *settingss accountUUID := getValidatedAccountUUID(ctx, filters[0].GetAccountUuid()) r, err = g.manager.ListRoleAssignments(accountUUID) case settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE: - err = fmt.Errorf("filtering by role not implemented") + roleID := filters[0].GetRoleId() + r, err = g.manager.ListRoleAssignmentsByRole(roleID) } if err != nil { return merrors.NotFound(g.id, "%s", err) diff --git a/services/settings/pkg/service/v0/service_test.go b/services/settings/pkg/service/v0/service_test.go index 9baa93cdba6..34ee83e9125 100644 --- a/services/settings/pkg/service/v0/service_test.go +++ b/services/settings/pkg/service/v0/service_test.go @@ -235,7 +235,7 @@ func TestListPermissionsOfOtherUser(t *testing.T) { assert.Contains(t, err.Error(), req.AccountUuid) } -func TestListRoleAssignmentsFiltered(t *testing.T) { +func TestListRoleAssignmentsFilteredValidation(t *testing.T) { manager := &mocks.Manager{} svc := Service{ manager: manager, @@ -336,3 +336,117 @@ func TestListRoleAssignmentsFiltered(t *testing.T) { }) } } + +func TestListRoleAssignmentsFilteredByAccount(t *testing.T) { + accountUUID := "61445573-4dbe-4d56-88dc-88ab47aceba7" + + tests := map[string]struct { + result []*settingsmsg.UserRoleAssignment + err error + status int32 + }{ + "handles manager error": { + result: nil, + err: assert.AnError, + status: http.StatusNotFound, + }, + "succeeds with results": { + result: []*settingsmsg.UserRoleAssignment{ + { + Id: "00000000-0000-0000-0000-000000000001", + AccountUuid: accountUUID, + RoleId: "aceb15b8-7486-479f-ae32-c91118e07a39", + }, + }, + err: nil, + status: http.StatusOK, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + manager := &mocks.Manager{} + svc := Service{ + manager: manager, + } + manager.On("ListRoleAssignments", mock.Anything).Return(test.result, test.err) + req := &v0.ListRoleAssignmentsFilteredRequest{ + Filters: []*settingsmsg.UserRoleAssignmentFilter{ + { + Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT, + Term: &settingsmsg.UserRoleAssignmentFilter_AccountUuid{ + AccountUuid: accountUUID, + }, + }, + }, + } + res := v0.ListRoleAssignmentsResponse{} + err := svc.ListRoleAssignmentsFiltered(ctxWithUUID, req, &res) + switch test.err { + case nil: + assert.Nil(t, err) + default: + merr, ok := merrors.As(err) + assert.True(t, ok) + assert.Equal(t, int32(test.status), merr.Code) + } + }) + } +} + +func TestListRoleAssignmentsFilteredByRole(t *testing.T) { + roleID := "61445573-4dbe-4d56-88dc-88ab47aceba7" + + tests := map[string]struct { + result []*settingsmsg.UserRoleAssignment + err error + status int32 + }{ + "handles manager error": { + result: nil, + err: assert.AnError, + status: http.StatusNotFound, + }, + "succeeds with results": { + result: []*settingsmsg.UserRoleAssignment{ + { + Id: "00000000-0000-0000-0000-000000000001", + AccountUuid: "aceb15b8-7486-479f-ae32-c91118e07a39", + RoleId: roleID, + }, + }, + err: nil, + status: http.StatusOK, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + manager := &mocks.Manager{} + svc := Service{ + manager: manager, + } + manager.On("ListRoleAssignmentsByRole", mock.Anything).Return(test.result, test.err) + req := &v0.ListRoleAssignmentsFilteredRequest{ + Filters: []*settingsmsg.UserRoleAssignmentFilter{ + { + Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE, + Term: &settingsmsg.UserRoleAssignmentFilter_RoleId{ + RoleId: roleID, + }, + }, + }, + } + res := v0.ListRoleAssignmentsResponse{} + err := svc.ListRoleAssignmentsFiltered(ctxWithUUID, req, &res) + switch test.err { + case nil: + assert.Nil(t, err) + default: + merr, ok := merrors.As(err) + assert.True(t, ok) + assert.Equal(t, int32(test.status), merr.Code) + } + }) + } +} diff --git a/services/settings/pkg/settings/mocks/manager.go b/services/settings/pkg/settings/mocks/manager.go index b4d6b43c638..e5b8c97104a 100644 --- a/services/settings/pkg/settings/mocks/manager.go +++ b/services/settings/pkg/settings/mocks/manager.go @@ -256,6 +256,64 @@ func (_c *Manager_ListRoleAssignments_Call) RunAndReturn(run func(string) ([]*v0 return _c } +// ListRoleAssignmentsByRole provides a mock function with given fields: roleID +func (_m *Manager) ListRoleAssignmentsByRole(roleID string) ([]*v0.UserRoleAssignment, error) { + ret := _m.Called(roleID) + + if len(ret) == 0 { + panic("no return value specified for ListRoleAssignmentsByRole") + } + + var r0 []*v0.UserRoleAssignment + var r1 error + if rf, ok := ret.Get(0).(func(string) ([]*v0.UserRoleAssignment, error)); ok { + return rf(roleID) + } + if rf, ok := ret.Get(0).(func(string) []*v0.UserRoleAssignment); ok { + r0 = rf(roleID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*v0.UserRoleAssignment) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Manager_ListRoleAssignmentsByRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRoleAssignmentsByRole' +type Manager_ListRoleAssignmentsByRole_Call struct { + *mock.Call +} + +// ListRoleAssignmentsByRole is a helper method to define mock.On call +// - roleID string +func (_e *Manager_Expecter) ListRoleAssignmentsByRole(roleID interface{}) *Manager_ListRoleAssignmentsByRole_Call { + return &Manager_ListRoleAssignmentsByRole_Call{Call: _e.mock.On("ListRoleAssignmentsByRole", roleID)} +} + +func (_c *Manager_ListRoleAssignmentsByRole_Call) Run(run func(roleID string)) *Manager_ListRoleAssignmentsByRole_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Manager_ListRoleAssignmentsByRole_Call) Return(_a0 []*v0.UserRoleAssignment, _a1 error) *Manager_ListRoleAssignmentsByRole_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Manager_ListRoleAssignmentsByRole_Call) RunAndReturn(run func(string) ([]*v0.UserRoleAssignment, error)) *Manager_ListRoleAssignmentsByRole_Call { + _c.Call.Return(run) + return _c +} + // ListValues provides a mock function with given fields: bundleID, accountUUID func (_m *Manager) ListValues(bundleID string, accountUUID string) ([]*v0.Value, error) { ret := _m.Called(bundleID, accountUUID) diff --git a/services/settings/pkg/settings/settings.go b/services/settings/pkg/settings/settings.go index abfc0d2d289..38ed93da630 100644 --- a/services/settings/pkg/settings/settings.go +++ b/services/settings/pkg/settings/settings.go @@ -63,6 +63,7 @@ type ValueManager interface { // RoleAssignmentManager is a role assignment service interface for abstraction of storage implementations type RoleAssignmentManager interface { ListRoleAssignments(accountUUID string) ([]*settingsmsg.UserRoleAssignment, error) + ListRoleAssignmentsByRole(roleID string) ([]*settingsmsg.UserRoleAssignment, error) WriteRoleAssignment(accountUUID, roleID string) (*settingsmsg.UserRoleAssignment, error) RemoveRoleAssignment(assignmentID string) error } diff --git a/services/settings/pkg/store/metadata/assignments.go b/services/settings/pkg/store/metadata/assignments.go index 45b1601c991..cae93cfbc30 100644 --- a/services/settings/pkg/store/metadata/assignments.go +++ b/services/settings/pkg/store/metadata/assignments.go @@ -62,6 +62,59 @@ func (s *Store) ListRoleAssignments(accountUUID string) ([]*settingsmsg.UserRole return ass, nil } +// ListRoleAssignments returns all role assignmentes matching the give roleID +func (s *Store) ListRoleAssignmentsByRole(roleID string) ([]*settingsmsg.UserRoleAssignment, error) { + s.Init() + ctx := context.TODO() + accountIDs, err := s.mdc.ReadDir(ctx, accountsFolderLocation) + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + return make([]*settingsmsg.UserRoleAssignment, 0), nil + default: + return nil, err + } + ass := make([]*settingsmsg.UserRoleAssignment, 0, len(accountIDs)) + + // This is very inefficient, with the current layout we need to iterated through all + // account folders and read each assignment file in there to check if that contains + // the give role ID. + for _, account := range accountIDs { + assIDs, err := s.mdc.ReadDir(ctx, accountPath(account)) + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + return make([]*settingsmsg.UserRoleAssignment, 0), nil + default: + return nil, err + } + + for _, assID := range assIDs { + b, err := s.mdc.SimpleDownload(ctx, assignmentPath(account, assID)) + switch err.(type) { + case nil: + // continue + case errtypes.NotFound: + continue + default: + return nil, err + } + + a := &settingsmsg.UserRoleAssignment{} + err = json.Unmarshal(b, a) + if err != nil { + return nil, err + } + if a.GetRoleId() == roleID { + ass = append(ass, a) + } + } + } + return ass, nil +} + // WriteRoleAssignment appends the given role assignment to the existing assignments of the respective account. func (s *Store) WriteRoleAssignment(accountUUID, roleID string) (*settingsmsg.UserRoleAssignment, error) { s.Init() diff --git a/services/settings/pkg/store/metadata/assignments_test.go b/services/settings/pkg/store/metadata/assignments_test.go index 18d246a46a4..fc430177264 100644 --- a/services/settings/pkg/store/metadata/assignments_test.go +++ b/services/settings/pkg/store/metadata/assignments_test.go @@ -14,8 +14,12 @@ import ( ) var ( - einstein = "a4d07560-a670-4be9-8d60-9b547751a208" - //marie = "3c054db3-eec1-4ca4-b985-bc56dcf560cb" + einstein = "00000000-0000-0000-0000-000000000001" + marie = "00000000-0000-0000-0000-000000000002" + moss = "00000000-0000-0000-0000-000000000003" + + role1 = "11111111-1111-1111-1111-111111111111" + role2 = "22222222-2222-2222-2222-222222222222" s = &Store{ Logger: logger, @@ -149,6 +153,79 @@ func TestAssignmentUniqueness(t *testing.T) { } } +func TestListRoleAssignmentByRole(t *testing.T) { + type assignment struct { + userID string + roleID string + } + + var scenarios = map[string]struct { + assignments []assignment + queryRole string + numResults int + }{ + "just 2 assignments": { + assignments: []assignment{ + { + userID: einstein, + roleID: role1, + }, { + userID: marie, + roleID: role1, + }, + }, + queryRole: role1, + numResults: 2, + }, + "no assignments match": { + assignments: []assignment{ + { + userID: einstein, + roleID: role1, + }, { + userID: marie, + roleID: role1, + }, + }, + queryRole: role2, + numResults: 0, + }, + "only one assignment matches": { + assignments: []assignment{ + { + userID: einstein, + roleID: role1, + }, { + userID: marie, + roleID: role1, + }, { + userID: moss, + roleID: role2, + }, + }, + queryRole: role2, + numResults: 1, + }, + } + + for name, scenario := range scenarios { + scenario := scenario + t.Run(name, func(t *testing.T) { + for _, a := range scenario.assignments { + ass, err := s.WriteRoleAssignment(a.userID, a.roleID) + require.NoError(t, err) + require.Equal(t, ass.RoleId, a.roleID) + } + + list, err := s.ListRoleAssignmentsByRole(scenario.queryRole) + require.NoError(t, err) + require.Equal(t, scenario.numResults, len(list)) + for _, ass := range list { + require.Equal(t, ass.RoleId, scenario.queryRole) + } + }) + } +} func TestDeleteAssignment(t *testing.T) { var scenarios = []struct { name string