mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
fee50be1bb
commit
bf49c20050
@ -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
|
||||
}
|
||||
|
210
pkg/services/accesscontrol/acimpl/service_bench_test.go
Normal file
210
pkg/services/accesscontrol/acimpl/service_bench_test.go
Normal file
@ -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…
Reference in New Issue
Block a user