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
co-authored by Gabriel MABILLE
parent d9d94ebc56
commit 6aa5a79cad
8 changed files with 363 additions and 21 deletions
+87 -3
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)
}
@@ -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