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
|
userSvc = userMock
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
acService, err = acimpl.ProvideService(cfg, db, routeRegister, localcache.ProvideService(), featuremgmt.WithFeatures())
|
|
||||||
require.NoError(t, err)
|
|
||||||
ac = acimpl.ProvideAccessControl(cfg)
|
ac = acimpl.ProvideAccessControl(cfg)
|
||||||
userSvc, err = userimpl.ProvideService(db, nil, cfg, teamimpl.ProvideService(db, cfg), localcache.ProvideService(), quotatest.New(false, nil))
|
userSvc, err = userimpl.ProvideService(db, nil, cfg, teamimpl.ProvideService(db, cfg), localcache.ProvideService(), quotatest.New(false, nil))
|
||||||
require.NoError(t, err)
|
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)
|
teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, ac, license, acService, teamService, userSvc)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -380,7 +380,8 @@ func TestGetOrgUsersAPIEndpoint_AccessControlMetadata(t *testing.T) {
|
|||||||
"org.users:write": true,
|
"org.users:write": true,
|
||||||
"org.users:add": true,
|
"org.users:add": true,
|
||||||
"org.users:read": true,
|
"org.users:read": true,
|
||||||
"org.users:remove": true},
|
"org.users:remove": true,
|
||||||
|
"users.permissions:read": true},
|
||||||
user: testServerAdminViewer,
|
user: testServerAdminViewer,
|
||||||
targetOrg: testServerAdminViewer.OrgID,
|
targetOrg: testServerAdminViewer.OrgID,
|
||||||
},
|
},
|
||||||
|
@ -26,6 +26,8 @@ type Service interface {
|
|||||||
registry.ProvidesUsageStats
|
registry.ProvidesUsageStats
|
||||||
// GetUserPermissions returns user permissions with only action and scope fields set.
|
// GetUserPermissions returns user permissions with only action and scope fields set.
|
||||||
GetUserPermissions(ctx context.Context, user *user.SignedInUser, options Options) ([]Permission, error)
|
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 removes the permission cache entry for the given user
|
||||||
ClearUserPermissionCache(user *user.SignedInUser)
|
ClearUserPermissionCache(user *user.SignedInUser)
|
||||||
// DeleteUserPermissions removes all permissions user has in org and all permission to that user
|
// DeleteUserPermissions removes all permissions user has in org and all permission to that user
|
||||||
@ -47,6 +49,12 @@ type Options struct {
|
|||||||
ReloadCache bool
|
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 {
|
type TeamPermissionsService interface {
|
||||||
GetPermissions(ctx context.Context, user *user.SignedInUser, resourceID string) ([]ResourcePermission, error)
|
GetPermissions(ctx context.Context, user *user.SignedInUser, resourceID string) ([]ResourcePermission, error)
|
||||||
SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error)
|
SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error)
|
||||||
|
@ -3,6 +3,8 @@ package acimpl
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"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,
|
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)
|
service := ProvideOSSService(cfg, database.ProvideService(store), cache, features)
|
||||||
|
|
||||||
if !accesscontrol.IsDisabled(cfg) {
|
if !accesscontrol.IsDisabled(cfg) {
|
||||||
api.NewAccessControlAPI(routeRegister, service).RegisterAPIEndpoints()
|
api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints()
|
||||||
if err := accesscontrol.DeclareFixedRoles(service); err != nil {
|
if err := accesscontrol.DeclareFixedRoles(service); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -58,6 +60,8 @@ func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheSer
|
|||||||
|
|
||||||
type store interface {
|
type store interface {
|
||||||
GetUserPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]accesscontrol.Permission, error)
|
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
|
DeleteUserPermissions(ctx context.Context, orgID, userID int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,3 +248,90 @@ func (s *Service) DeclarePluginRoles(_ context.Context, ID, name string, regs []
|
|||||||
|
|
||||||
return nil
|
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/localcache"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/models/roletype"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"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/accesscontrol/database"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
@ -65,6 +67,7 @@ func TestUsageMetrics(t *testing.T) {
|
|||||||
db.InitTestDB(t),
|
db.InitTestDB(t),
|
||||||
routing.NewRouteRegister(),
|
routing.NewRouteRegister(),
|
||||||
localcache.ProvideService(),
|
localcache.ProvideService(),
|
||||||
|
actest.FakeAccessControl{},
|
||||||
featuremgmt.WithFeatures(),
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
require.NoError(t, errInitAc)
|
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) {
|
func TestPermissionCacheKey(t *testing.T) {
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -14,6 +14,7 @@ type FakeService struct {
|
|||||||
ExpectedErr error
|
ExpectedErr error
|
||||||
ExpectedDisabled bool
|
ExpectedDisabled bool
|
||||||
ExpectedPermissions []accesscontrol.Permission
|
ExpectedPermissions []accesscontrol.Permission
|
||||||
|
ExpectedUsersPermissions map[int64][]accesscontrol.Permission
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f FakeService) GetUsageStats(ctx context.Context) map[string]interface{} {
|
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
|
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) ClearUserPermissionCache(user *user.SignedInUser) {}
|
||||||
|
|
||||||
func (f FakeService) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error {
|
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 {
|
func (f FakeAccessControl) IsDisabled() bool {
|
||||||
return f.ExpectedDisabled
|
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/middleware"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
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{
|
return &AccessControlAPI{
|
||||||
RouteRegister: router,
|
RouteRegister: router,
|
||||||
Service: service,
|
Service: service,
|
||||||
|
AccessControl: accesscontrol,
|
||||||
|
features: features,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessControlAPI struct {
|
type AccessControlAPI struct {
|
||||||
Service ac.Service
|
Service ac.Service
|
||||||
|
AccessControl ac.AccessControl
|
||||||
RouteRegister routing.RouteRegister
|
RouteRegister routing.RouteRegister
|
||||||
|
features *featuremgmt.FeatureManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *AccessControlAPI) RegisterAPIEndpoints() {
|
func (api *AccessControlAPI) RegisterAPIEndpoints() {
|
||||||
|
authorize := ac.Middleware(api.AccessControl)
|
||||||
// Users
|
// Users
|
||||||
api.RouteRegister.Group("/api/access-control", func(rr routing.RouteRegister) {
|
api.RouteRegister.Group("/api/access-control", func(rr routing.RouteRegister) {
|
||||||
rr.Get("/user/actions", middleware.ReqSignedIn, routing.Wrap(api.getUserActions))
|
rr.Get("/user/actions", middleware.ReqSignedIn, routing.Wrap(api.getUserActions))
|
||||||
rr.Get("/user/permissions", middleware.ReqSignedIn, routing.Wrap(api.getUserPermissions))
|
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))
|
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"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"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/services/user"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/grafana/grafana/pkg/web/webtest"
|
"github.com/grafana/grafana/pkg/web/webtest"
|
||||||
@ -38,7 +39,7 @@ func TestAPI_getUserActions(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
acSvc := actest.FakeService{ExpectedPermissions: tt.permissions}
|
acSvc := actest.FakeService{ExpectedPermissions: tt.permissions}
|
||||||
api := NewAccessControlAPI(routing.NewRouteRegister(), acSvc)
|
api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, featuremgmt.WithFeatures())
|
||||||
api.RegisterAPIEndpoints()
|
api.RegisterAPIEndpoints()
|
||||||
|
|
||||||
server := webtest.NewServer(t, api.RouteRegister)
|
server := webtest.NewServer(t, api.RouteRegister)
|
||||||
@ -91,7 +92,7 @@ func TestAPI_getUserPermissions(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
acSvc := actest.FakeService{ExpectedPermissions: tt.permissions}
|
acSvc := actest.FakeService{ExpectedPermissions: tt.permissions}
|
||||||
api := NewAccessControlAPI(routing.NewRouteRegister(), acSvc)
|
api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, featuremgmt.WithFeatures())
|
||||||
api.RegisterAPIEndpoints()
|
api.RegisterAPIEndpoints()
|
||||||
|
|
||||||
server := webtest.NewServer(t, api.RouteRegister)
|
server := webtest.NewServer(t, api.RouteRegister)
|
||||||
|
@ -55,6 +55,110 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces
|
|||||||
return result, err
|
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 {
|
func (s *AccessControlStore) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error {
|
||||||
err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
|
err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
roleDeleteQuery := "DELETE FROM user_role WHERE user_id = ?"
|
roleDeleteQuery := "DELETE FROM user_role WHERE user_id = ?"
|
||||||
|
@ -2,20 +2,24 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"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/models"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
rs "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
rs "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"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"
|
||||||
"github.com/grafana/grafana/pkg/services/team/teamimpl"
|
"github.com/grafana/grafana/pkg/services/team/teamimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||||
)
|
)
|
||||||
|
|
||||||
type getUserPermissionsTestCase struct {
|
type getUserPermissionsTestCase struct {
|
||||||
@ -82,7 +86,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
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)
|
user, team := createUserAndTeam(t, sql, teamSvc, tt.orgID)
|
||||||
|
|
||||||
@ -145,7 +149,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) {
|
|||||||
|
|
||||||
func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
|
func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
|
||||||
t.Run("expect permissions in all orgs to be deleted", func(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)
|
user, _ := createUserAndTeam(t, sql, teamSvc, 1)
|
||||||
|
|
||||||
// generate permissions in org 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) {
|
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)
|
user, _ := createUserAndTeam(t, sql, teamSvc, 1)
|
||||||
|
|
||||||
// generate permissions in org 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()
|
t.Helper()
|
||||||
|
|
||||||
user, err := sql.CreateUser(context.Background(), user.CreateUserCommand{
|
user, err := userSrv.Create(context.Background(), &user.CreateUserCommand{
|
||||||
Login: "user",
|
Login: "user",
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
})
|
})
|
||||||
@ -243,10 +247,339 @@ func createUserAndTeam(t *testing.T, sql *sqlstore.SQLStore, teamSvc team.Servic
|
|||||||
return user, team
|
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)
|
sql, cfg := db.InitTestDBwithCfg(t)
|
||||||
acstore := ProvideService(sql)
|
acstore := ProvideService(sql)
|
||||||
permissionStore := rs.NewStore(sql)
|
permissionStore := rs.NewStore(sql)
|
||||||
teamService := teamimpl.ProvideService(sql, cfg)
|
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{}
|
RegisterFixedRoles []interface{}
|
||||||
RegisterAttributeScopeResolver []interface{}
|
RegisterAttributeScopeResolver []interface{}
|
||||||
DeleteUserPermissions []interface{}
|
DeleteUserPermissions []interface{}
|
||||||
|
SearchUsersPermissions []interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mock struct {
|
type Mock struct {
|
||||||
@ -52,6 +53,7 @@ type Mock struct {
|
|||||||
RegisterFixedRolesFunc func() error
|
RegisterFixedRolesFunc func() error
|
||||||
RegisterScopeAttributeResolverFunc func(string, accesscontrol.ScopeAttributeResolver)
|
RegisterScopeAttributeResolverFunc func(string, accesscontrol.ScopeAttributeResolver)
|
||||||
DeleteUserPermissionsFunc func(context.Context, int64) error
|
DeleteUserPermissionsFunc func(context.Context, int64) error
|
||||||
|
SearchUsersPermissionsFunc func(context.Context, *user.SignedInUser, int64, accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error)
|
||||||
|
|
||||||
scopeResolvers accesscontrol.Resolvers
|
scopeResolvers accesscontrol.Resolvers
|
||||||
}
|
}
|
||||||
@ -212,3 +214,13 @@ func (m *Mock) DeleteUserPermissions(ctx context.Context, orgID, userID int64) e
|
|||||||
}
|
}
|
||||||
return nil
|
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"
|
ActionUsersLogout = "users:logout"
|
||||||
ActionUsersQuotasList = "users.quotas:read"
|
ActionUsersQuotasList = "users.quotas:read"
|
||||||
ActionUsersQuotasUpdate = "users.quotas:write"
|
ActionUsersQuotasUpdate = "users.quotas:write"
|
||||||
|
ActionUsersPermissionsRead = "users.permissions:read"
|
||||||
|
|
||||||
// Org actions
|
// Org actions
|
||||||
ActionOrgsRead = "orgs:read"
|
ActionOrgsRead = "orgs:read"
|
||||||
|
@ -71,6 +71,10 @@ var (
|
|||||||
Action: ActionOrgUsersRead,
|
Action: ActionOrgUsersRead,
|
||||||
Scope: ScopeUsersAll,
|
Scope: ScopeUsersAll,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Action: ActionUsersPermissionsRead,
|
||||||
|
Scope: ScopeUsersAll,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user