RBAC: Add an endpoint to list all user permissions (#57644)

* RBAC: Add an endpoint to see all user permissions

Co-authored-by: Joey Orlando <joey.orlando@grafana.com>

* Fix mock

* Add feature flag

* Fix merging

* Return normal permissions instead of simplified ones

* Fix test

* Fix tests

* Fix tests

* Create benchtests

* Split function to get basic roles

* Comments

* Reorg

* Add two more tests to the bench

* bench comment

* Re-ran the test

* Rename GetUsersPermissions to SearchUsersPermissions and prepare search options

* Remove from model unused struct

* Start adding option to get permissions by Action+Scope

* Wrong import

* Action and Scope

* slightly tweak users permissions actionPrefix query param validation logic

* Fix xor check

* Lint

* Account for suggeston

Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>

* Add search

* Remove comment on global scope

* use union all and update test to make it run on all dbs

* Fix MySQL needs a space

* Account for suggestion.

Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>

Co-authored-by: Joey Orlando <joey.orlando@grafana.com>
Co-authored-by: Joey Orlando <joseph.t.orlando@gmail.com>
Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
pull/59581/head
Gabriel MABILLE 3 years ago committed by GitHub
parent fee50be1bb
commit bf49c20050
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      pkg/api/common_test.go
  2. 9
      pkg/api/org_users_test.go
  3. 8
      pkg/services/accesscontrol/accesscontrol.go
  4. 95
      pkg/services/accesscontrol/acimpl/service.go
  5. 210
      pkg/services/accesscontrol/acimpl/service_bench_test.go
  6. 150
      pkg/services/accesscontrol/acimpl/service_test.go
  7. 34
      pkg/services/accesscontrol/actest/fake.go
  8. 40
      pkg/services/accesscontrol/api/api.go
  9. 5
      pkg/services/accesscontrol/api/api_test.go
  10. 104
      pkg/services/accesscontrol/database/database.go
  11. 349
      pkg/services/accesscontrol/database/database_test.go
  12. 12
      pkg/services/accesscontrol/mock/mock.go
  13. 1
      pkg/services/accesscontrol/models.go
  14. 4
      pkg/services/accesscontrol/roles.go

@ -404,11 +404,11 @@ func setupHTTPServerWithCfgDb(
userSvc = userMock
} else {
var err error
acService, err = acimpl.ProvideService(cfg, db, routeRegister, localcache.ProvideService(), featuremgmt.WithFeatures())
require.NoError(t, err)
ac = acimpl.ProvideAccessControl(cfg)
userSvc, err = userimpl.ProvideService(db, nil, cfg, teamimpl.ProvideService(db, cfg), localcache.ProvideService(), quotatest.New(false, nil))
require.NoError(t, err)
acService, err = acimpl.ProvideService(cfg, db, routeRegister, localcache.ProvideService(), ac, featuremgmt.WithFeatures())
require.NoError(t, err)
}
teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, ac, license, acService, teamService, userSvc)
require.NoError(t, err)

@ -377,10 +377,11 @@ func TestGetOrgUsersAPIEndpoint_AccessControlMetadata(t *testing.T) {
enableAccessControl: true,
expectedCode: http.StatusOK,
expectedMetadata: map[string]bool{
"org.users:write": true,
"org.users:add": true,
"org.users:read": true,
"org.users:remove": true},
"org.users:write": true,
"org.users:add": true,
"org.users:read": true,
"org.users:remove": true,
"users.permissions:read": true},
user: testServerAdminViewer,
targetOrg: testServerAdminViewer.OrgID,
},

@ -26,6 +26,8 @@ type Service interface {
registry.ProvidesUsageStats
// GetUserPermissions returns user permissions with only action and scope fields set.
GetUserPermissions(ctx context.Context, user *user.SignedInUser, options Options) ([]Permission, error)
// SearchUsersPermissions returns all users' permissions filtered by an action prefix
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)
// DeleteUserPermissions removes all permissions user has in org and all permission to that user
@ -47,6 +49,12 @@ type Options struct {
ReloadCache bool
}
type SearchOptions struct {
ActionPrefix string // Needed for the PoC v1, it's probably going to be removed.
Action string
Scope string
}
type TeamPermissionsService interface {
GetPermissions(ctx context.Context, user *user.SignedInUser, resourceID string) ([]ResourcePermission, error)
SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error)

@ -3,6 +3,8 @@ package acimpl
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
@ -30,11 +32,11 @@ const (
)
func ProvideService(cfg *setting.Cfg, store db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
features *featuremgmt.FeatureManager) (*Service, error) {
accessControl accesscontrol.AccessControl, features *featuremgmt.FeatureManager) (*Service, error) {
service := ProvideOSSService(cfg, database.ProvideService(store), cache, features)
if !accesscontrol.IsDisabled(cfg) {
api.NewAccessControlAPI(routeRegister, service).RegisterAPIEndpoints()
api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints()
if err := accesscontrol.DeclareFixedRoles(service); err != nil {
return nil, err
}
@ -58,6 +60,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)
DeleteUserPermissions(ctx context.Context, orgID, userID int64) error
}
@ -244,3 +248,90 @@ func (s *Service) DeclarePluginRoles(_ context.Context, ID, name string, regs []
return nil
}
// SearchUsersPermissions returns all users' permissions filtered by action prefixes
func (s *Service) SearchUsersPermissions(ctx context.Context, user *user.SignedInUser, orgID int64,
options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
// Filter ram permissions
basicPermissions := map[string][]accesscontrol.Permission{}
for role, basicRole := range s.roles {
for i := range basicRole.Permissions {
if options.ActionPrefix != "" {
if strings.HasPrefix(basicRole.Permissions[i].Action, options.ActionPrefix) {
basicPermissions[role] = append(basicPermissions[role], basicRole.Permissions[i])
}
}
if options.Action != "" {
if basicRole.Permissions[i].Action == options.Action {
basicPermissions[role] = append(basicPermissions[role], basicRole.Permissions[i])
}
}
}
}
usersRoles, err := s.store.GetUsersBasicRoles(ctx, orgID)
if err != nil {
return nil, err
}
// Get managed permissions (DB)
usersPermissions, err := s.store.SearchUsersPermissions(ctx, orgID, options)
if err != nil {
return nil, err
}
// helper to filter out permissions the signed in users cannot see
canView := func() func(userID int64) bool {
siuPermissions, ok := user.Permissions[orgID]
if !ok {
return func(_ int64) bool { return false }
}
scopes, ok := siuPermissions[accesscontrol.ActionUsersPermissionsRead]
if !ok {
return func(_ int64) bool { return false }
}
ids := map[int64]bool{}
for i := range scopes {
if strings.HasSuffix(scopes[i], "*") {
return func(_ int64) bool { return true }
}
parts := strings.Split(scopes[i], ":")
if len(parts) != 3 {
continue
}
id, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
continue
}
ids[id] = true
}
return func(userID int64) bool { return ids[userID] }
}()
// Merge stored (DB) and basic role permissions (RAM)
// Assumes that all users with stored permissions have org roles
res := map[int64][]accesscontrol.Permission{}
for userID, roles := range usersRoles {
if !canView(userID) {
continue
}
perms := []accesscontrol.Permission{}
for i := range roles {
basicPermission, ok := basicPermissions[roles[i]]
if !ok {
continue
}
perms = append(perms, basicPermission...)
}
if dbPerms, ok := usersPermissions[userID]; ok {
perms = append(perms, dbPerms...)
}
if len(perms) > 0 {
res[userID] = perms
}
}
return res, nil
}

@ -0,0 +1,210 @@
package acimpl
import (
"context"
"fmt"
"testing"
"time"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
const batchSize = 500
func batch(count, size int, eachFn func(start, end int) error) error {
for i := 0; i < count; {
end := i + size
if end > count {
end = count
}
if err := eachFn(i, end); err != nil {
return err
}
i = end
}
return nil
}
func setupBenchEnv(b *testing.B, usersCount, resourceCount int) (accesscontrol.Service, *user.SignedInUser) {
now := time.Now()
sqlStore := db.InitTestDB(b)
store := database.ProvideService(sqlStore)
acService := &Service{
cfg: setting.NewCfg(),
log: log.New("accesscontrol-test"),
registrations: accesscontrol.RegistrationList{},
store: store,
roles: accesscontrol.BuildBasicRoleDefinitions(),
}
// Prepare default permissions
action1 := "resources:action1"
err := acService.DeclareFixedRoles(accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{Name: "fixed:test:role", Permissions: []accesscontrol.Permission{{Action: action1}}},
Grants: []string{string(org.RoleViewer)},
})
require.NoError(b, err)
err = acService.RegisterFixedRoles(context.Background())
require.NoError(b, err)
// Prepare managed permissions
action2 := "resources:action2"
users := make([]user.User, 0, usersCount)
orgUsers := make([]org.OrgUser, 0, usersCount)
roles := make([]accesscontrol.Role, 0, usersCount)
userRoles := make([]accesscontrol.UserRole, 0, usersCount)
permissions := make([]accesscontrol.Permission, 0, resourceCount*usersCount)
for u := 1; u < usersCount+1; u++ {
users = append(users, user.User{
ID: int64(u),
Name: fmt.Sprintf("user%v", u),
Login: fmt.Sprintf("user%v", u),
Email: fmt.Sprintf("user%v@example.org", u),
Created: now,
Updated: now,
})
orgUsers = append(orgUsers, org.OrgUser{
ID: int64(u),
UserID: int64(u),
OrgID: 1,
Role: org.RoleViewer,
Created: now,
Updated: now,
})
roles = append(roles, accesscontrol.Role{
ID: int64(u),
UID: fmt.Sprintf("managed_users_%v_permissions", u),
Name: fmt.Sprintf("managed:users:%v:permissions", u),
Version: 1,
Created: now,
Updated: now,
})
userRoles = append(userRoles, accesscontrol.UserRole{
ID: int64(u),
OrgID: 1,
RoleID: int64(u),
UserID: int64(u),
Created: now,
})
for r := 1; r < resourceCount+1; r++ {
permissions = append(permissions, accesscontrol.Permission{
RoleID: int64(u),
Action: action2,
Scope: fmt.Sprintf("resources:id:%v", r),
Created: now,
Updated: now,
})
}
}
// Populate store
if err := batch(len(roles), batchSize, func(start, end int) error {
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
if _, err := sess.Insert(users[start:end]); err != nil {
return err
}
if _, err := sess.Insert(orgUsers[start:end]); err != nil {
return err
}
if _, err := sess.Insert(roles[start:end]); err != nil {
return err
}
_, err := sess.Insert(userRoles[start:end])
return err
})
return err
}); err != nil {
require.NoError(b, err, "could not insert users and roles")
return nil, nil
}
if err := batch(len(permissions), batchSize, func(start, end int) error {
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
_, err := sess.Insert(permissions[start:end])
return err
})
return err
}); err != nil {
require.NoError(b, err, "could not insert permissions")
return nil, nil
}
// Allow signed in user to view all users permissions in the worst way
userPermissions := map[string][]string{}
for u := 1; u < usersCount+1; u++ {
userPermissions[accesscontrol.ActionUsersPermissionsRead] =
append(userPermissions[accesscontrol.ActionUsersPermissionsRead], fmt.Sprintf("users:id:%v", u))
}
return acService, &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{1: userPermissions}}
}
func benchSearchUsersPermissions(b *testing.B, usersCount, resourceCount int) {
acService, siu := setupBenchEnv(b, usersCount, resourceCount)
b.ResetTimer()
for n := 0; n < b.N; n++ {
usersPermissions, err := acService.SearchUsersPermissions(context.Background(), siu, 1, accesscontrol.SearchOptions{ActionPrefix: "resources:"})
require.NoError(b, err)
require.Len(b, usersPermissions, usersCount)
for _, permissions := range usersPermissions {
// action1 on all resource + action2
require.Len(b, permissions, resourceCount+1)
}
}
}
// Lots of resources
func BenchmarkSearchUsersPermissions_10_1K(b *testing.B) { benchSearchUsersPermissions(b, 10, 1000) } // ~0.047s/op
func BenchmarkSearchUsersPermissions_10_10K(b *testing.B) { benchSearchUsersPermissions(b, 10, 10000) } // ~0.5s/op
func BenchmarkSearchUsersPermissions_10_100K(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
benchSearchUsersPermissions(b, 10, 100000)
} // ~4.6s/op
func BenchmarkSearchUsersPermissions_10_1M(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
benchSearchUsersPermissions(b, 10, 1000000)
} // ~55.36s/op
// Lots of users (most probable case)
func BenchmarkSearchUsersPermissions_1K_10(b *testing.B) { benchSearchUsersPermissions(b, 1000, 10) } // ~0.056s/op
func BenchmarkSearchUsersPermissions_10K_10(b *testing.B) { benchSearchUsersPermissions(b, 10000, 10) } // ~0.58s/op
func BenchmarkSearchUsersPermissions_100K_10(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
benchSearchUsersPermissions(b, 100000, 10)
} // ~6.21s/op
func BenchmarkSearchUsersPermissions_1M_10(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
benchSearchUsersPermissions(b, 1000000, 10)
} // ~57s/op
// Lots of both
func BenchmarkSearchUsersPermissions_10K_100(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
benchSearchUsersPermissions(b, 10000, 100)
} // ~1.45s/op
func BenchmarkSearchUsersPermissions_10K_1K(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
benchSearchUsersPermissions(b, 10000, 1000)
} // ~50s/op

@ -12,8 +12,10 @@ import (
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
@ -65,6 +67,7 @@ func TestUsageMetrics(t *testing.T) {
db.InitTestDB(t),
routing.NewRouteRegister(),
localcache.ProvideService(),
actest.FakeAccessControl{},
featuremgmt.WithFeatures(),
)
require.NoError(t, errInitAc)
@ -373,6 +376,153 @@ func TestService_RegisterFixedRoles(t *testing.T) {
}
}
func TestService_SearchUsersPermissions(t *testing.T) {
searchOption := accesscontrol.SearchOptions{ActionPrefix: "teams"}
ctx := context.Background()
listAllPerms := map[string][]string{accesscontrol.ActionUsersPermissionsRead: {"users:*"}}
listSomePerms := map[string][]string{accesscontrol.ActionUsersPermissionsRead: {"users:id:2"}}
tests := []struct {
name string
siuPermissions map[string][]string
ramRoles map[string]*accesscontrol.RoleDTO // BasicRole => RBAC BasicRole
storedPerms map[int64][]accesscontrol.Permission // UserID => Permissions
storedRoles map[int64][]string // UserID => Roles
want map[int64][]accesscontrol.Permission
wantErr bool
}{
{
name: "ram only",
siuPermissions: listAllPerms,
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:*"},
}},
},
storedRoles: map[int64][]string{
1: {string(roletype.RoleEditor)},
2: {string(roletype.RoleAdmin), accesscontrol.RoleGrafanaAdmin},
},
want: map[int64][]accesscontrol.Permission{
2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
},
},
{
name: "stored only",
siuPermissions: listAllPerms,
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: map[int64][]accesscontrol.Permission{
1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
},
},
{
name: "ram and stored",
siuPermissions: listAllPerms,
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: 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"},
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
},
},
{
name: "view permission on subset of users only",
siuPermissions: listSomePerms,
ramRoles: map[string]*accesscontrol.RoleDTO{
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: {accesscontrol.RoleGrafanaAdmin},
},
want: map[int64][]accesscontrol.Permission{
2: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
},
},
{
name: "check action filter on RAM permissions works correctly",
siuPermissions: listAllPerms,
ramRoles: map[string]*accesscontrol.RoleDTO{
accesscontrol.RoleGrafanaAdmin: {Permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionUsersCreate},
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"},
}},
},
storedRoles: map[int64][]string{1: {accesscontrol.RoleGrafanaAdmin}},
want: map[int64][]accesscontrol.Permission{
1: {{Action: accesscontrol.ActionTeamsPermissionsRead, 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,
}
siu := &user.SignedInUser{OrgID: 2, Permissions: map[int64]map[string][]string{2: tt.siuPermissions}}
got, err := ac.SearchUsersPermissions(ctx, siu, 2, searchOption)
if tt.wantErr {
require.NotNil(t, err)
return
}
require.Nil(t, err)
require.Len(t, got, len(tt.want), "expected more users permissions")
for userID, wantPerm := range tt.want {
gotPerm, ok := got[userID]
require.True(t, ok, "expected permissions for user", userID)
require.ElementsMatch(t, gotPerm, wantPerm)
}
})
}
}
func TestPermissionCacheKey(t *testing.T) {
testcases := []struct {
name string

@ -11,9 +11,10 @@ var _ accesscontrol.Service = new(FakeService)
var _ accesscontrol.RoleRegistry = new(FakeService)
type FakeService struct {
ExpectedErr error
ExpectedDisabled bool
ExpectedPermissions []accesscontrol.Permission
ExpectedErr error
ExpectedDisabled bool
ExpectedPermissions []accesscontrol.Permission
ExpectedUsersPermissions map[int64][]accesscontrol.Permission
}
func (f FakeService) GetUsageStats(ctx context.Context) map[string]interface{} {
@ -24,6 +25,10 @@ func (f FakeService) GetUserPermissions(ctx context.Context, user *user.SignedIn
return f.ExpectedPermissions, f.ExpectedErr
}
func (f FakeService) SearchUsersPermissions(ctx context.Context, user *user.SignedInUser, orgID int64, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
return f.ExpectedUsersPermissions, f.ExpectedErr
}
func (f FakeService) ClearUserPermissionCache(user *user.SignedInUser) {}
func (f FakeService) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error {
@ -60,3 +65,26 @@ func (f FakeAccessControl) RegisterScopeAttributeResolver(prefix string, resolve
func (f FakeAccessControl) IsDisabled() bool {
return f.ExpectedDisabled
}
type FakeStore struct {
ExpectedUserPermissions []accesscontrol.Permission
ExpectedUsersPermissions map[int64][]accesscontrol.Permission
ExpectedUsersRoles map[int64][]string
ExpectedErr error
}
func (f FakeStore) GetUserPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error) {
return f.ExpectedUserPermissions, f.ExpectedErr
}
func (f FakeStore) SearchUsersPermissions(ctx context.Context, orgID int64, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
return f.ExpectedUsersPermissions, f.ExpectedErr
}
func (f FakeStore) GetUsersBasicRoles(ctx context.Context, orgID int64) (map[int64][]string, error) {
return f.ExpectedUsersRoles, f.ExpectedErr
}
func (f FakeStore) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error {
return f.ExpectedErr
}

@ -8,25 +8,36 @@ import (
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
func NewAccessControlAPI(router routing.RouteRegister, service ac.Service) *AccessControlAPI {
func NewAccessControlAPI(router routing.RouteRegister, accesscontrol ac.AccessControl, service ac.Service,
features *featuremgmt.FeatureManager) *AccessControlAPI {
return &AccessControlAPI{
RouteRegister: router,
Service: service,
AccessControl: accesscontrol,
features: features,
}
}
type AccessControlAPI struct {
Service ac.Service
AccessControl ac.AccessControl
RouteRegister routing.RouteRegister
features *featuremgmt.FeatureManager
}
func (api *AccessControlAPI) RegisterAPIEndpoints() {
authorize := ac.Middleware(api.AccessControl)
// Users
api.RouteRegister.Group("/api/access-control", func(rr routing.RouteRegister) {
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) {
rr.Get("/users/permissions/search", authorize(middleware.ReqSignedIn,
ac.EvalPermission(ac.ActionUsersPermissionsRead)), routing.Wrap(api.SearchUsersPermissions))
}
})
}
@ -53,3 +64,30 @@ func (api *AccessControlAPI) getUserPermissions(c *models.ReqContext) response.R
return response.JSON(http.StatusOK, ac.GroupScopesByAction(permissions))
}
// GET /api/access-control/users/permissions
func (api *AccessControlAPI) SearchUsersPermissions(c *models.ReqContext) response.Response {
searchOptions := ac.SearchOptions{
ActionPrefix: c.Query("actionPrefix"),
Action: c.Query("action"),
Scope: c.Query("scope"),
}
// Validate inputs
if (searchOptions.ActionPrefix != "") == (searchOptions.Action != "") {
return response.JSON(http.StatusBadRequest, "provide one of 'action' or 'actionPrefix'")
}
// Compute metadata
permissions, err := api.Service.SearchUsersPermissions(c.Req.Context(), c.SignedInUser, c.OrgID, searchOptions)
if err != nil {
return response.Error(http.StatusInternalServerError, "could not get org user permissions", err)
}
permsByAction := map[int64]map[string][]string{}
for userID, userPerms := range permissions {
permsByAction[userID] = ac.GroupScopesByAction(userPerms)
}
return response.JSON(http.StatusOK, permsByAction)
}

@ -9,6 +9,7 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web/webtest"
@ -38,7 +39,7 @@ func TestAPI_getUserActions(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
acSvc := actest.FakeService{ExpectedPermissions: tt.permissions}
api := NewAccessControlAPI(routing.NewRouteRegister(), acSvc)
api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, featuremgmt.WithFeatures())
api.RegisterAPIEndpoints()
server := webtest.NewServer(t, api.RouteRegister)
@ -91,7 +92,7 @@ func TestAPI_getUserPermissions(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
acSvc := actest.FakeService{ExpectedPermissions: tt.permissions}
api := NewAccessControlAPI(routing.NewRouteRegister(), acSvc)
api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, featuremgmt.WithFeatures())
api.RegisterAPIEndpoints()
server := webtest.NewServer(t, api.RouteRegister)

@ -55,6 +55,110 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces
return result, err
}
// SearchUsersPermissions returns the list of user permissions indexed by UserID
func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID int64, options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
type UserRBACPermission struct {
UserID int64 `xorm:"user_id"`
Action string `xorm:"action"`
Scope string `xorm:"scope"`
}
dbPerms := make([]UserRBACPermission, 0)
if err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
// Find permissions
q := `
SELECT
user_id,
action,
scope
FROM (
SELECT ur.user_id, ur.org_id, p.action, p.scope
FROM permission AS p
INNER JOIN user_role AS ur on ur.role_id = p.role_id
UNION ALL
SELECT tm.user_id, tr.org_id, p.action, p.scope
FROM permission AS p
INNER JOIN team_role AS tr ON tr.role_id = p.role_id
INNER JOIN team_member AS tm ON tm.team_id = tr.team_id
UNION ALL
SELECT ou.user_id, br.org_id, p.action, p.scope
FROM permission AS p
INNER JOIN builtin_role AS br ON br.role_id = p.role_id
INNER JOIN org_user AS ou ON ou.role = br.role
UNION ALL
SELECT sa.user_id, br.org_id, p.action, p.scope
FROM permission AS p
INNER JOIN builtin_role AS br ON br.role_id = p.role_id
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
WHERE br.role = ?
) AS up
WHERE (org_id = ? OR org_id = ?)
`
params := []interface{}{accesscontrol.RoleGrafanaAdmin, accesscontrol.GlobalOrgID, orgID}
if options.ActionPrefix != "" {
q += ` AND action LIKE ?`
params = append(params, options.ActionPrefix+"%")
}
if options.Action != "" {
q += ` AND action = ?`
params = append(params, options.Action)
}
if options.Scope != "" {
q += ` AND scope = ?`
params = append(params, options.Scope)
}
return sess.SQL(q, params...).
Find(&dbPerms)
}); err != nil {
return nil, err
}
mapped := map[int64][]accesscontrol.Permission{}
for i := range dbPerms {
mapped[dbPerms[i].UserID] = append(mapped[dbPerms[i].UserID], accesscontrol.Permission{Action: dbPerms[i].Action, Scope: dbPerms[i].Scope})
}
return mapped, nil
}
// 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) {
type UserOrgRole struct {
UserID int64 `xorm:"id"`
OrgRole string `xorm:"role"`
IsAdmin bool `xorm:"is_admin"`
}
dbRoles := make([]UserOrgRole, 0)
if err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
// Find roles
q := `
SELECT u.id, ou.role, u.is_admin
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 = ?
`
return sess.SQL(q, orgID).Find(&dbRoles)
}); err != nil {
return nil, err
}
roles := map[int64][]string{}
for i := range dbRoles {
if dbRoles[i].OrgRole != "" {
roles[dbRoles[i].UserID] = []string{dbRoles[i].OrgRole}
}
if dbRoles[i].IsAdmin {
roles[dbRoles[i].UserID] = append(roles[dbRoles[i].UserID], accesscontrol.RoleGrafanaAdmin)
}
}
return roles, nil
}
func (s *AccessControlStore) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error {
err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
roleDeleteQuery := "DELETE FROM user_role WHERE user_id = ?"

@ -2,20 +2,24 @@ package database
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
rs "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/team/teamimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/userimpl"
)
type getUserPermissionsTestCase struct {
@ -82,7 +86,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
store, permissionStore, sql, teamSvc := setupTestEnv(t)
store, permissionStore, sql, teamSvc, _ := setupTestEnv(t)
user, team := createUserAndTeam(t, sql, teamSvc, tt.orgID)
@ -145,7 +149,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) {
func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
t.Run("expect permissions in all orgs to be deleted", func(t *testing.T) {
store, permissionsStore, sql, teamSvc := setupTestEnv(t)
store, permissionsStore, sql, teamSvc, _ := setupTestEnv(t)
user, _ := createUserAndTeam(t, sql, teamSvc, 1)
// generate permissions in org 1
@ -185,7 +189,7 @@ func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
})
t.Run("expect permissions in org 1 to be deleted", func(t *testing.T) {
store, permissionsStore, sql, teamSvc := setupTestEnv(t)
store, permissionsStore, sql, teamSvc, _ := setupTestEnv(t)
user, _ := createUserAndTeam(t, sql, teamSvc, 1)
// generate permissions in org 1
@ -225,10 +229,10 @@ func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
})
}
func createUserAndTeam(t *testing.T, sql *sqlstore.SQLStore, teamSvc team.Service, orgID int64) (*user.User, models.Team) {
func createUserAndTeam(t *testing.T, userSrv user.Service, teamSvc team.Service, orgID int64) (*user.User, models.Team) {
t.Helper()
user, err := sql.CreateUser(context.Background(), user.CreateUserCommand{
user, err := userSrv.Create(context.Background(), &user.CreateUserCommand{
Login: "user",
OrgID: orgID,
})
@ -243,10 +247,339 @@ func createUserAndTeam(t *testing.T, sql *sqlstore.SQLStore, teamSvc team.Servic
return user, team
}
func setupTestEnv(t testing.TB) (*AccessControlStore, rs.Store, *sqlstore.SQLStore, team.Service) {
type helperServices struct {
userSvc user.Service
teamSvc team.Service
orgSvc org.Service
}
type testUser struct {
orgRole org.RoleType
isAdmin bool
}
type dbUser struct {
userID int64
teamID int64
}
func createUsersAndTeams(t *testing.T, svcs helperServices, orgID int64, users []testUser) []dbUser {
t.Helper()
res := []dbUser{}
for i := range users {
user, err := svcs.userSvc.Create(context.Background(), &user.CreateUserCommand{
Login: fmt.Sprintf("user%v", i+1),
OrgID: orgID,
IsAdmin: users[i].isAdmin,
})
require.NoError(t, err)
// User is not member of the org
if users[i].orgRole == "" {
err = svcs.orgSvc.RemoveOrgUser(context.Background(),
&org.RemoveOrgUserCommand{OrgID: orgID, UserID: user.ID})
require.NoError(t, err)
res = append(res, dbUser{userID: user.ID})
continue
}
team, err := svcs.teamSvc.CreateTeam(fmt.Sprintf("team%v", i+1), "", orgID)
require.NoError(t, err)
err = svcs.teamSvc.AddTeamMember(user.ID, orgID, team.Id, false, models.PERMISSION_VIEW)
require.NoError(t, err)
err = svcs.orgSvc.UpdateOrgUser(context.Background(),
&org.UpdateOrgUserCommand{Role: users[i].orgRole, OrgID: orgID, UserID: user.ID})
require.NoError(t, err)
res = append(res, dbUser{userID: user.ID, teamID: team.Id})
}
return res
}
func setupTestEnv(t testing.TB) (*AccessControlStore, rs.Store, user.Service, team.Service, org.Service) {
sql, cfg := db.InitTestDBwithCfg(t)
acstore := ProvideService(sql)
permissionStore := rs.NewStore(sql)
teamService := teamimpl.ProvideService(sql, cfg)
return acstore, permissionStore, sql, teamService
orgService, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil))
require.NoError(t, err)
userService, err := userimpl.ProvideService(sql, orgService, cfg, teamService, localcache.ProvideService(), quotatest.New(false, nil))
require.NoError(t, err)
return acstore, permissionStore, userService, teamService, orgService
}
func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) {
ctx := context.Background()
readTeamPerm := func(teamID string) rs.SetResourcePermissionCommand {
return rs.SetResourcePermissionCommand{
Actions: []string{"teams:read"},
Resource: "teams",
ResourceAttribute: "id",
ResourceID: teamID,
}
}
writeTeamPerm := func(teamID string) rs.SetResourcePermissionCommand {
return rs.SetResourcePermissionCommand{
Actions: []string{"teams:read", "teams:write"},
Resource: "teams",
ResourceAttribute: "id",
ResourceID: teamID,
}
}
readDashPerm := func(dashUID string) rs.SetResourcePermissionCommand {
return rs.SetResourcePermissionCommand{
Actions: []string{"dashboards:read"},
Resource: "dashboards",
ResourceAttribute: "uid",
ResourceID: dashUID,
}
}
tests := []struct {
name string
users []testUser
permCmds []rs.SetResourcePermissionsCommand
options accesscontrol.SearchOptions
wantPerm map[int64][]accesscontrol.Permission
wantErr bool
}{
{
name: "user assignment by actionPrefix",
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: false}},
permCmds: []rs.SetResourcePermissionsCommand{
{User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("1")},
},
options: accesscontrol.SearchOptions{ActionPrefix: "teams:"},
wantPerm: map[int64][]accesscontrol.Permission{1: {{Action: "teams:read", Scope: "teams:id:1"}}},
},
{
name: "users assignment by actionPrefix",
users: []testUser{
{orgRole: org.RoleAdmin, isAdmin: false},
{orgRole: org.RoleEditor, isAdmin: false},
},
permCmds: []rs.SetResourcePermissionsCommand{
{User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: writeTeamPerm("1")},
{User: accesscontrol.User{ID: 2, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("2")},
},
options: accesscontrol.SearchOptions{ActionPrefix: "teams:"},
wantPerm: map[int64][]accesscontrol.Permission{
1: {{Action: "teams:read", Scope: "teams:id:1"}, {Action: "teams:write", Scope: "teams:id:1"}},
2: {{Action: "teams:read", Scope: "teams:id:2"}},
},
},
{
name: "team assignment by actionPrefix",
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: false}},
permCmds: []rs.SetResourcePermissionsCommand{{TeamID: 1, SetResourcePermissionCommand: readTeamPerm("1")}},
options: accesscontrol.SearchOptions{ActionPrefix: "teams:"},
wantPerm: map[int64][]accesscontrol.Permission{1: {{Action: "teams:read", Scope: "teams:id:1"}}},
},
{
name: "basic role assignment by actionPrefix",
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: false}},
permCmds: []rs.SetResourcePermissionsCommand{
{BuiltinRole: string(org.RoleAdmin), SetResourcePermissionCommand: readTeamPerm("1")},
},
options: accesscontrol.SearchOptions{ActionPrefix: "teams:"},
wantPerm: map[int64][]accesscontrol.Permission{1: {{Action: "teams:read", Scope: "teams:id:1"}}},
},
{
name: "server admin assignment by actionPrefix",
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: true}},
permCmds: []rs.SetResourcePermissionsCommand{
{BuiltinRole: accesscontrol.RoleGrafanaAdmin, SetResourcePermissionCommand: readTeamPerm("1")},
},
options: accesscontrol.SearchOptions{ActionPrefix: "teams:"},
wantPerm: map[int64][]accesscontrol.Permission{1: {{Action: "teams:read", Scope: "teams:id:1"}}},
},
{
name: "all assignments 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:"},
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"}},
2: {{Action: "teams:read", Scope: "teams:id:2"}, {Action: "teams:read", Scope: "teams:id:20"},
{Action: "teams:read", Scope: "teams:id:200"}},
},
},
{
name: "filter permissions by action prefix",
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: true}},
permCmds: []rs.SetResourcePermissionsCommand{
// User assignments
{User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("1")},
{User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: readDashPerm("d1")},
// Team assignments
{TeamID: 1, SetResourcePermissionCommand: readTeamPerm("10")},
{TeamID: 1, SetResourcePermissionCommand: readDashPerm("d10")},
// Basic Assignments
{BuiltinRole: string(org.RoleAdmin), SetResourcePermissionCommand: readTeamPerm("100")},
{BuiltinRole: string(org.RoleAdmin), SetResourcePermissionCommand: readDashPerm("d100")},
// Server Admin Assignment
{BuiltinRole: accesscontrol.RoleGrafanaAdmin, SetResourcePermissionCommand: readTeamPerm("1000")},
{BuiltinRole: accesscontrol.RoleGrafanaAdmin, SetResourcePermissionCommand: readDashPerm("d1000")},
},
options: accesscontrol.SearchOptions{ActionPrefix: "teams:"},
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: "include not org member server admin permissions by actionPrefix",
// Three users, one member, one not member but Server Admin, one not member and not server admin
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: false}, {isAdmin: true}, {}},
permCmds: []rs.SetResourcePermissionsCommand{{BuiltinRole: accesscontrol.RoleGrafanaAdmin, SetResourcePermissionCommand: readTeamPerm("1")}},
wantPerm: map[int64][]accesscontrol.Permission{
2: {{Action: "teams:read", Scope: "teams:id:1"}},
},
},
{
name: "user assignment by action",
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: false}},
permCmds: []rs.SetResourcePermissionsCommand{
{User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("1")},
{User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("2")},
},
options: accesscontrol.SearchOptions{Action: "teams:read"},
wantPerm: map[int64][]accesscontrol.Permission{1: {
{Action: "teams:read", Scope: "teams:id:1"},
{Action: "teams:read", Scope: "teams:id:2"}},
},
},
{
name: "user assignment by scope",
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: false}},
permCmds: []rs.SetResourcePermissionsCommand{
{User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("1")},
{User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: writeTeamPerm("1")},
},
options: accesscontrol.SearchOptions{Scope: "teams:id:1"},
wantPerm: map[int64][]accesscontrol.Permission{1: {
{Action: "teams:read", Scope: "teams:id:1"},
{Action: "teams:write", Scope: "teams:id:1"},
}},
},
{
name: "user assignment by action and scope",
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: false}},
permCmds: []rs.SetResourcePermissionsCommand{
{User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("1")},
{User: accesscontrol.User{ID: 1, IsExternal: false}, SetResourcePermissionCommand: readTeamPerm("2")},
},
options: accesscontrol.SearchOptions{Action: "teams:read", Scope: "teams:id:1"},
wantPerm: map[int64][]accesscontrol.Permission{1: {{Action: "teams:read", Scope: "teams:id:1"}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
acStore, permissionsStore, userSvc, teamSvc, orgSvc := setupTestEnv(t)
dbUsers := createUsersAndTeams(t, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users)
// Switch userID and TeamID by the real stored ones
for i := range tt.permCmds {
if tt.permCmds[i].User.ID != 0 {
tt.permCmds[i].User.ID = dbUsers[tt.permCmds[i].User.ID-1].userID
}
if tt.permCmds[i].TeamID != 0 {
tt.permCmds[i].TeamID = dbUsers[tt.permCmds[i].TeamID-1].teamID
}
}
_, err := permissionsStore.SetResourcePermissions(ctx, 1, tt.permCmds, rs.ResourceHooks{})
require.NoError(t, err)
// Test
dbPermissions, err := acStore.SearchUsersPermissions(ctx, 1, tt.options)
if tt.wantErr {
require.NotNil(t, err)
return
}
require.Nil(t, err)
require.Len(t, dbPermissions, len(tt.wantPerm))
for userID, expectedUserPerms := range tt.wantPerm {
dbUserPerms, ok := dbPermissions[dbUsers[userID-1].userID]
require.True(t, ok, "expected permissions for user", userID)
require.ElementsMatch(t, expectedUserPerms, dbUserPerms)
}
})
}
}
func TestAccessControlStore_GetUsersBasicRoles(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
users []testUser
wantRoles map[int64][]string
wantErr bool
}{
{
name: "user with basic role",
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: false}},
wantRoles: map[int64][]string{1: {string(org.RoleAdmin)}},
},
{
name: "one admin, one editor",
users: []testUser{
{orgRole: org.RoleAdmin, isAdmin: false},
{orgRole: org.RoleEditor, isAdmin: false},
},
wantRoles: map[int64][]string{
1: {string(org.RoleAdmin)},
2: {string(org.RoleEditor)},
},
},
{
name: "one org member, one not member but Server Admin, one not member and not server admin",
users: []testUser{{orgRole: org.RoleAdmin, isAdmin: false}, {isAdmin: true}, {}},
wantRoles: map[int64][]string{
1: {string(org.RoleAdmin)},
2: {accesscontrol.RoleGrafanaAdmin},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
acStore, _, userSvc, teamSvc, orgSvc := setupTestEnv(t)
dbUsers := createUsersAndTeams(t, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users)
// Test
dbRoles, err := acStore.GetUsersBasicRoles(ctx, 1)
if tt.wantErr {
require.NotNil(t, err)
return
}
require.Nil(t, err)
require.Len(t, dbRoles, len(tt.wantRoles))
for userID, expectedUserRoles := range tt.wantRoles {
dbUserRoles, ok := dbRoles[dbUsers[userID-1].userID]
require.True(t, ok, "expected organization role for user", userID)
require.ElementsMatch(t, expectedUserRoles, dbUserRoles)
}
})
}
}

@ -28,6 +28,7 @@ type Calls struct {
RegisterFixedRoles []interface{}
RegisterAttributeScopeResolver []interface{}
DeleteUserPermissions []interface{}
SearchUsersPermissions []interface{}
}
type Mock struct {
@ -52,6 +53,7 @@ type Mock struct {
RegisterFixedRolesFunc func() error
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)
scopeResolvers accesscontrol.Resolvers
}
@ -212,3 +214,13 @@ func (m *Mock) DeleteUserPermissions(ctx context.Context, orgID, userID int64) e
}
return nil
}
// GetSimplifiedUsersPermissions 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
if m.SearchUsersPermissionsFunc != nil {
return m.SearchUsersPermissionsFunc(ctx, user, orgID, options)
}
return nil, nil
}

@ -309,6 +309,7 @@ const (
ActionUsersLogout = "users:logout"
ActionUsersQuotasList = "users.quotas:read"
ActionUsersQuotasUpdate = "users.quotas:write"
ActionUsersPermissionsRead = "users.permissions:read"
// Org actions
ActionOrgsRead = "orgs:read"

@ -71,6 +71,10 @@ var (
Action: ActionOrgUsersRead,
Scope: ScopeUsersAll,
},
{
Action: ActionUsersPermissionsRead,
Scope: ScopeUsersAll,
},
},
}

Loading…
Cancel
Save