Access control: endpoint for searching single user permissions (#59669)

* initial commit

* clean up

* fix a bug and add tests

* more tests

* undo some unintended changes

* undo some unintended changes

* linting

* PR feedback - add user ID to search options

* simplify the query

* Apply suggestions from code review

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* remove unneeded formatting changes

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
Ieva
2022-12-14 10:53:25 +00:00
committed by GitHub
parent d9d94ebc56
commit 6aa5a79cad
8 changed files with 363 additions and 21 deletions

View File

@ -30,6 +30,8 @@ type Service interface {
SearchUsersPermissions(ctx context.Context, user *user.SignedInUser, orgID int64, options SearchOptions) (map[int64][]Permission, error)
// ClearUserPermissionCache removes the permission cache entry for the given user
ClearUserPermissionCache(user *user.SignedInUser)
// SearchUserPermissions returns single user's permissions filtered by an action prefix or an action
SearchUserPermissions(ctx context.Context, orgID int64, filterOptions SearchOptions) ([]Permission, error)
// DeleteUserPermissions removes all permissions user has in org and all permission to that user
// If orgID is set to 0 remove permissions from all orgs
DeleteUserPermissions(ctx context.Context, orgID, userID int64) error
@ -53,6 +55,7 @@ type SearchOptions struct {
ActionPrefix string // Needed for the PoC v1, it's probably going to be removed.
Action string
Scope string
UserID int64 // ID for the user for which to return information, if none is specified information is returned for all users.
}
type TeamPermissionsService interface {

View File

@ -59,8 +59,8 @@ func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheSer
type store interface {
GetUserPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error)
SearchUsersPermissions(ctx context.Context, orgID int64, option accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error)
GetUsersBasicRoles(ctx context.Context, orgID int64) (map[int64][]string, error)
SearchUsersPermissions(ctx context.Context, orgID int64, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error)
GetUsersBasicRoles(ctx context.Context, userFilter []int64, orgID int64) (map[int64][]string, error)
DeleteUserPermissions(ctx context.Context, orgID, userID int64) error
}
@ -264,7 +264,7 @@ func (s *Service) SearchUsersPermissions(ctx context.Context, user *user.SignedI
}
}
usersRoles, err := s.store.GetUsersBasicRoles(ctx, orgID)
usersRoles, err := s.store.GetUsersBasicRoles(ctx, nil, orgID)
if err != nil {
return nil, err
}
@ -330,3 +330,87 @@ func (s *Service) SearchUsersPermissions(ctx context.Context, user *user.SignedI
return res, nil
}
func (s *Service) SearchUserPermissions(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error) {
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
defer timer.ObserveDuration()
if searchOptions.UserID == 0 {
return nil, fmt.Errorf("expected user ID to be specified")
}
if permissions, success := s.searchUserPermissionsFromCache(orgID, searchOptions); success {
return permissions, nil
}
return s.searchUserPermissions(ctx, orgID, searchOptions)
}
func (s *Service) searchUserPermissions(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error) {
// Get permissions for user's basic roles from RAM
roleList, err := s.store.GetUsersBasicRoles(ctx, []int64{searchOptions.UserID}, orgID)
if err != nil {
return nil, fmt.Errorf("could not fetch basic roles for the user: %w", err)
}
var roles []string
var ok bool
if roles, ok = roleList[searchOptions.UserID]; !ok {
return nil, fmt.Errorf("found no basic roles for user %d in organisation %d", searchOptions.UserID, orgID)
}
permissions := make([]accesscontrol.Permission, 0)
for _, builtin := range roles {
if basicRole, ok := s.roles[builtin]; ok {
for _, permission := range basicRole.Permissions {
if PermissionMatchesSearchOptions(permission, searchOptions) {
permissions = append(permissions, permission)
}
}
}
}
// Get permissions from the DB
dbPermissions, err := s.store.SearchUsersPermissions(ctx, orgID, searchOptions)
if err != nil {
return nil, err
}
permissions = append(permissions, dbPermissions[searchOptions.UserID]...)
return permissions, nil
}
func (s *Service) searchUserPermissionsFromCache(orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, bool) {
// Create a temp signed in user object to retrieve cache key
tempUser := &user.SignedInUser{
UserID: searchOptions.UserID,
OrgID: orgID,
}
key, err := permissionCacheKey(tempUser)
if err != nil {
s.log.Debug("could not obtain cache key to search user permissions", "error", err.Error())
return nil, false
}
permissions, ok := s.cache.Get(key)
if !ok {
return nil, false
}
s.log.Debug("using cached permissions", "key", key)
filteredPermissions := make([]accesscontrol.Permission, 0)
for _, permission := range permissions.([]accesscontrol.Permission) {
if PermissionMatchesSearchOptions(permission, searchOptions) {
filteredPermissions = append(filteredPermissions, permission)
}
}
return filteredPermissions, true
}
func PermissionMatchesSearchOptions(permission accesscontrol.Permission, searchOptions accesscontrol.SearchOptions) bool {
if searchOptions.Scope != "" && permission.Scope != searchOptions.Scope {
return false
}
if searchOptions.Action != "" {
return permission.Action == searchOptions.Action
}
return strings.HasPrefix(permission.Action, searchOptions.ActionPrefix)
}

View File

@ -523,6 +523,159 @@ func TestService_SearchUsersPermissions(t *testing.T) {
}
}
func TestService_SearchUserPermissions(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
searchOption accesscontrol.SearchOptions
ramRoles map[string]*accesscontrol.RoleDTO // BasicRole => RBAC BasicRole
storedPerms map[int64][]accesscontrol.Permission // UserID => Permissions
storedRoles map[int64][]string // UserID => Roles
want []accesscontrol.Permission
wantErr bool
}{
{
name: "ram only",
searchOption: accesscontrol.SearchOptions{
ActionPrefix: "teams",
UserID: 2,
},
ramRoles: map[string]*accesscontrol.RoleDTO{
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsCreate},
}},
string(roletype.RoleAdmin): {Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
}},
accesscontrol.RoleGrafanaAdmin: {Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
}},
},
storedRoles: map[int64][]string{
1: {string(roletype.RoleEditor)},
2: {string(roletype.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
},
want: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
},
{
name: "stored only",
searchOption: accesscontrol.SearchOptions{
ActionPrefix: "teams",
UserID: 2,
},
storedPerms: map[int64][]accesscontrol.Permission{
1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
},
storedRoles: map[int64][]string{
1: {string(roletype.RoleEditor)},
2: {string(roletype.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
},
want: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
},
},
{
name: "ram and stored",
searchOption: accesscontrol.SearchOptions{
ActionPrefix: "teams",
UserID: 2,
},
ramRoles: map[string]*accesscontrol.RoleDTO{
string(roletype.RoleAdmin): {Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
}},
accesscontrol.RoleGrafanaAdmin: {Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
}},
},
storedPerms: map[int64][]accesscontrol.Permission{
1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"}},
},
storedRoles: map[int64][]string{
1: {string(roletype.RoleEditor)},
2: {string(roletype.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
},
want: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"},
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
},
},
{
name: "check action prefix filter works correctly",
searchOption: accesscontrol.SearchOptions{
ActionPrefix: "teams",
UserID: 1,
},
ramRoles: map[string]*accesscontrol.RoleDTO{
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
{Action: accesscontrol.ActionUsersCreate},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
{Action: accesscontrol.ActionAnnotationsRead, Scope: "annotations:*"},
}},
},
storedRoles: map[int64][]string{
1: {string(roletype.RoleEditor)},
},
want: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
},
},
{
name: "check action filter works correctly",
searchOption: accesscontrol.SearchOptions{
Action: accesscontrol.ActionTeamsRead,
UserID: 1,
},
ramRoles: map[string]*accesscontrol.RoleDTO{
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
{Action: accesscontrol.ActionUsersCreate},
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
{Action: accesscontrol.ActionAnnotationsRead, Scope: "annotations:*"},
}},
},
storedRoles: map[int64][]string{
1: {string(roletype.RoleEditor)},
},
want: []accesscontrol.Permission{
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ac := setupTestEnv(t)
ac.roles = tt.ramRoles
ac.store = actest.FakeStore{
ExpectedUsersPermissions: tt.storedPerms,
ExpectedUsersRoles: tt.storedRoles,
}
got, err := ac.searchUserPermissions(ctx, 1, tt.searchOption)
if tt.wantErr {
require.NotNil(t, err)
return
}
require.Nil(t, err)
assert.ElementsMatch(t, got, tt.want)
})
}
}
func TestPermissionCacheKey(t *testing.T) {
testcases := []struct {
name string

View File

@ -11,10 +11,12 @@ var _ accesscontrol.Service = new(FakeService)
var _ accesscontrol.RoleRegistry = new(FakeService)
type FakeService struct {
ExpectedErr error
ExpectedDisabled bool
ExpectedPermissions []accesscontrol.Permission
ExpectedUsersPermissions map[int64][]accesscontrol.Permission
ExpectedErr error
ExpectedDisabled bool
ExpectedCachedPermissions bool
ExpectedPermissions []accesscontrol.Permission
ExpectedFilteredUserPermissions []accesscontrol.Permission
ExpectedUsersPermissions map[int64][]accesscontrol.Permission
}
func (f FakeService) GetUsageStats(ctx context.Context) map[string]interface{} {
@ -29,6 +31,10 @@ func (f FakeService) SearchUsersPermissions(ctx context.Context, user *user.Sign
return f.ExpectedUsersPermissions, f.ExpectedErr
}
func (f FakeService) SearchUserPermissions(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error) {
return f.ExpectedFilteredUserPermissions, f.ExpectedErr
}
func (f FakeService) ClearUserPermissionCache(user *user.SignedInUser) {}
func (f FakeService) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error {
@ -81,7 +87,7 @@ func (f FakeStore) SearchUsersPermissions(ctx context.Context, orgID int64, opti
return f.ExpectedUsersPermissions, f.ExpectedErr
}
func (f FakeStore) GetUsersBasicRoles(ctx context.Context, orgID int64) (map[int64][]string, error) {
func (f FakeStore) GetUsersBasicRoles(ctx context.Context, userFilter []int64, orgID int64) (map[int64][]string, error) {
return f.ExpectedUsersRoles, f.ExpectedErr
}

View File

@ -2,6 +2,7 @@ package api
import (
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
@ -9,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/web"
)
func NewAccessControlAPI(router routing.RouteRegister, accesscontrol ac.AccessControl, service ac.Service,
@ -35,8 +37,11 @@ func (api *AccessControlAPI) RegisterAPIEndpoints() {
rr.Get("/user/actions", middleware.ReqSignedIn, routing.Wrap(api.getUserActions))
rr.Get("/user/permissions", middleware.ReqSignedIn, routing.Wrap(api.getUserPermissions))
if api.features.IsEnabled(featuremgmt.FlagAccessControlOnCall) {
userIDScope := ac.Scope("users", "id", ac.Parameter(":userID"))
rr.Get("/users/permissions/search", authorize(middleware.ReqSignedIn,
ac.EvalPermission(ac.ActionUsersPermissionsRead)), routing.Wrap(api.SearchUsersPermissions))
ac.EvalPermission(ac.ActionUsersPermissionsRead)), routing.Wrap(api.searchUsersPermissions))
rr.Get("/user/:userID/permissions/search", authorize(middleware.ReqSignedIn,
ac.EvalPermission(ac.ActionUsersPermissionsRead, userIDScope)), routing.Wrap(api.searchUserPermissions))
}
})
}
@ -66,7 +71,7 @@ func (api *AccessControlAPI) getUserPermissions(c *models.ReqContext) response.R
}
// GET /api/access-control/users/permissions
func (api *AccessControlAPI) SearchUsersPermissions(c *models.ReqContext) response.Response {
func (api *AccessControlAPI) searchUsersPermissions(c *models.ReqContext) response.Response {
searchOptions := ac.SearchOptions{
ActionPrefix: c.Query("actionPrefix"),
Action: c.Query("action"),
@ -91,3 +96,30 @@ func (api *AccessControlAPI) SearchUsersPermissions(c *models.ReqContext) respon
return response.JSON(http.StatusOK, permsByAction)
}
// GET /api/access-control/user/:userID/permissions/search
func (api *AccessControlAPI) searchUserPermissions(c *models.ReqContext) response.Response {
userIDString := web.Params(c.Req)[":userID"]
userID, err := strconv.ParseInt(userIDString, 10, 64)
if err != nil {
response.Error(http.StatusBadRequest, "user ID is invalid", err)
}
searchOptions := ac.SearchOptions{
ActionPrefix: c.Query("actionPrefix"),
Action: c.Query("action"),
Scope: c.Query("scope"),
UserID: userID,
}
// Validate inputs
if (searchOptions.ActionPrefix != "") == (searchOptions.Action != "") {
return response.JSON(http.StatusBadRequest, "provide one of 'action' or 'actionPrefix'")
}
permissions, err := api.Service.SearchUserPermissions(c.Req.Context(), c.OrgID, searchOptions)
if err != nil {
response.Error(http.StatusInternalServerError, "could not search user permissions", err)
}
return response.JSON(http.StatusOK, ac.GroupScopesByAction(permissions))
}

View File

@ -86,11 +86,12 @@ func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID i
INNER JOIN (
SELECT u.id AS user_id
FROM ` + s.sql.GetDialect().Quote("user") + ` AS u WHERE u.is_admin
) AS sa ON 1 = 1
) AS sa ON 1 = 1
WHERE br.role = ?
) AS up
WHERE (org_id = ? OR org_id = ?)
`
params := []interface{}{accesscontrol.RoleGrafanaAdmin, accesscontrol.GlobalOrgID, orgID}
if options.ActionPrefix != "" {
@ -106,6 +107,11 @@ func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID i
params = append(params, options.Scope)
}
if options.UserID != 0 {
q += ` AND user_id = ?`
params = append(params, options.UserID)
}
return sess.SQL(q, params...).
Find(&dbPerms)
}); err != nil {
@ -121,7 +127,7 @@ func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID i
}
// GetUsersBasicRoles returns the list of user basic roles (Admin, Editor, Viewer, Grafana Admin) indexed by UserID
func (s *AccessControlStore) GetUsersBasicRoles(ctx context.Context, orgID int64) (map[int64][]string, error) {
func (s *AccessControlStore) GetUsersBasicRoles(ctx context.Context, userFilter []int64, orgID int64) (map[int64][]string, error) {
type UserOrgRole struct {
UserID int64 `xorm:"id"`
OrgRole string `xorm:"role"`
@ -132,12 +138,19 @@ func (s *AccessControlStore) GetUsersBasicRoles(ctx context.Context, orgID int64
// Find roles
q := `
SELECT u.id, ou.role, u.is_admin
FROM ` + s.sql.GetDialect().Quote("user") + ` AS u
FROM ` + s.sql.GetDialect().Quote("user") + ` AS u
LEFT JOIN org_user AS ou ON u.id = ou.user_id
WHERE u.is_admin OR ou.org_id = ?
WHERE (u.is_admin OR ou.org_id = ?)
`
params := []interface{}{orgID}
if len(userFilter) > 0 {
q += "AND u.id IN (?" + strings.Repeat(",?", len(userFilter)-1) + ")"
for _, u := range userFilter {
params = append(params, u)
}
}
return sess.SQL(q, orgID).Find(&dbRoles)
return sess.SQL(q, params...).Find(&dbRoles)
}); err != nil {
return nil, err
}

View File

@ -412,6 +412,34 @@ func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) {
{Action: "teams:read", Scope: "teams:id:200"}},
},
},
{
name: "all assignments for one user by actionPrefix",
users: []testUser{
{orgRole: org.RoleAdmin, isAdmin: true},
{orgRole: org.RoleEditor, isAdmin: false},
},
permCmds: []rs.SetResourcePermissionsCommand{
// User assignments
{User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("1")},
{User: accesscontrol.User{ID: 2, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("2")},
// Team assignments
{TeamID: 1, SetResourcePermissionCommand: readTeamPerm("10")},
{TeamID: 2, SetResourcePermissionCommand: readTeamPerm("20")},
// Basic Assignments
{BuiltinRole: string(org.RoleAdmin), SetResourcePermissionCommand: readTeamPerm("100")},
{BuiltinRole: string(org.RoleEditor), SetResourcePermissionCommand: readTeamPerm("200")},
// Server Admin Assignment
{BuiltinRole: accesscontrol.RoleGrafanaAdmin, SetResourcePermissionCommand: readTeamPerm("1000")},
},
options: accesscontrol.SearchOptions{
ActionPrefix: "teams:",
UserID: 1,
},
wantPerm: map[int64][]accesscontrol.Permission{
1: {{Action: "teams:read", Scope: "teams:id:1"}, {Action: "teams:read", Scope: "teams:id:10"},
{Action: "teams:read", Scope: "teams:id:100"}, {Action: "teams:read", Scope: "teams:id:1000"}},
},
},
{
name: "filter permissions by action prefix",
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: true}},
@ -519,10 +547,11 @@ func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) {
func TestAccessControlStore_GetUsersBasicRoles(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
users []testUser
wantRoles map[int64][]string
wantErr bool
name string
users []testUser
userFilter []int64
wantRoles map[int64][]string
wantErr bool
}{
{
name: "user with basic role",
@ -548,6 +577,17 @@ func TestAccessControlStore_GetUsersBasicRoles(t *testing.T) {
2: {accesscontrol.RoleGrafanaAdmin},
},
},
{
name: "when filtered to one user, returns results only for that user",
userFilter: []int64{2},
users: []testUser{
{orgRole: org.RoleAdmin, isAdmin: false},
{orgRole: org.RoleEditor, isAdmin: true},
},
wantRoles: map[int64][]string{
2: {string(org.RoleEditor), accesscontrol.RoleGrafanaAdmin},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -555,7 +595,7 @@ func TestAccessControlStore_GetUsersBasicRoles(t *testing.T) {
dbUsers := createUsersAndTeams(t, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users)
// Test
dbRoles, err := acStore.GetUsersBasicRoles(ctx, 1)
dbRoles, err := acStore.GetUsersBasicRoles(ctx, tt.userFilter, 1)
if tt.wantErr {
require.NotNil(t, err)
return

View File

@ -29,6 +29,7 @@ type Calls struct {
RegisterAttributeScopeResolver []interface{}
DeleteUserPermissions []interface{}
SearchUsersPermissions []interface{}
SearchUserPermissions []interface{}
}
type Mock struct {
@ -54,6 +55,7 @@ type Mock struct {
RegisterScopeAttributeResolverFunc func(string, accesscontrol.ScopeAttributeResolver)
DeleteUserPermissionsFunc func(context.Context, int64) error
SearchUsersPermissionsFunc func(context.Context, *user.SignedInUser, int64, accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error)
SearchUserPermissionsFunc func(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error)
scopeResolvers accesscontrol.Resolvers
}
@ -215,7 +217,7 @@ func (m *Mock) DeleteUserPermissions(ctx context.Context, orgID, userID int64) e
return nil
}
// GetSimplifiedUsersPermissions returns all users' permissions filtered by an action prefix
// SearchUsersPermissions returns all users' permissions filtered by an action prefix
func (m *Mock) SearchUsersPermissions(ctx context.Context, user *user.SignedInUser, orgID int64, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
m.Calls.SearchUsersPermissions = append(m.Calls.SearchUsersPermissions, []interface{}{ctx, user, orgID, options})
// Use override if provided
@ -224,3 +226,12 @@ func (m *Mock) SearchUsersPermissions(ctx context.Context, user *user.SignedInUs
}
return nil, nil
}
func (m *Mock) SearchUserPermissions(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error) {
m.Calls.SearchUserPermissions = append(m.Calls.SearchUserPermissions, []interface{}{ctx, orgID, searchOptions})
// Use override if provided
if m.SearchUserPermissionsFunc != nil {
return m.SearchUserPermissionsFunc(ctx, orgID, searchOptions)
}
return nil, nil
}