mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
RBAC Search: Replace userLogin
filter by namespacedID
filter (#81810)
* Add namespace ID * Refactor and add tests * Rename maxOneOption -> atMostOneOption * Add ToDo * Remove UserLogin & UserID for NamespaceID Co-authored-by: jguer <joao.guerreiro@grafana.com> * Remove unecessary import of the userSvc * Update pkg/services/accesscontrol/acimpl/service.go * fix 1 -> userID * Update pkg/services/accesscontrol/accesscontrol.go --------- Co-authored-by: jguer <joao.guerreiro@grafana.com>
This commit is contained in:
parent
fe0fc08b93
commit
846eadff63
@ -46,7 +46,6 @@ import (
|
|||||||
"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"
|
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
"github.com/grafana/grafana/pkg/web/webtest"
|
"github.com/grafana/grafana/pkg/web/webtest"
|
||||||
@ -444,7 +443,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
|||||||
license := licensingtest.NewFakeLicensing()
|
license := licensingtest.NewFakeLicensing()
|
||||||
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
|
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
|
||||||
|
|
||||||
acSvc := acimpl.ProvideOSSService(sc.cfg, acdb.ProvideService(sc.db), localcache.ProvideService(), usertest.NewUserServiceFake(), features)
|
acSvc := acimpl.ProvideOSSService(sc.cfg, acdb.ProvideService(sc.db), localcache.ProvideService(), features)
|
||||||
|
|
||||||
quotaSrv := quotatest.New(false, nil)
|
quotaSrv := quotatest.New(false, nil)
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ func initializeConflictResolver(cmd *utils.ContextCommandLine, f Formatter, ctx
|
|||||||
return nil, fmt.Errorf("%v: %w", "failed to get user service", err)
|
return nil, fmt.Errorf("%v: %w", "failed to get user service", err)
|
||||||
}
|
}
|
||||||
routing := routing.ProvideRegister()
|
routing := routing.ProvideRegister()
|
||||||
acService, err := acimpl.ProvideService(cfg, s, routing, nil, nil, nil, features)
|
acService, err := acimpl.ProvideService(cfg, s, routing, nil, nil, features)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to get access control", err)
|
return nil, fmt.Errorf("%v: %w", "failed to get access control", err)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@ package accesscontrol
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
@ -57,8 +59,7 @@ type SearchOptions struct {
|
|||||||
ActionPrefix string // Needed for the PoC v1, it's probably going to be removed.
|
ActionPrefix string // Needed for the PoC v1, it's probably going to be removed.
|
||||||
Action string
|
Action string
|
||||||
Scope string
|
Scope string
|
||||||
UserLogin string // Login for which to return information, if none is specified information is returned for all users.
|
NamespacedID string // ID of the identity (ex: user:3, service-account:4)
|
||||||
UserID int64 // ID for the user for which to return information, if none is specified information is returned for all users.
|
|
||||||
wildcards Wildcards // private field computed based on the Scope
|
wildcards Wildcards // private field computed based on the Scope
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,17 +78,26 @@ func (s *SearchOptions) Wildcards() []string {
|
|||||||
return s.wildcards
|
return s.wildcards
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SearchOptions) ResolveUserLogin(ctx context.Context, userSvc user.Service) error {
|
func (s *SearchOptions) ComputeUserID() (int64, error) {
|
||||||
if s.UserLogin == "" {
|
if s.NamespacedID == "" {
|
||||||
return nil
|
return 0, errors.New("namespacedID must be set")
|
||||||
}
|
}
|
||||||
// Resolve userLogin -> userID
|
// Split namespaceID into namespace and ID
|
||||||
dbUsr, err := userSvc.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: s.UserLogin})
|
parts := strings.Split(s.NamespacedID, ":")
|
||||||
|
// Validate namespace ID format
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return 0, fmt.Errorf("invalid namespaced ID: %s", s.NamespacedID)
|
||||||
|
}
|
||||||
|
// Validate namespace type is user or service account
|
||||||
|
if parts[0] != identity.NamespaceUser && parts[0] != identity.NamespaceServiceAccount {
|
||||||
|
return 0, fmt.Errorf("invalid namespace: %s", parts[0])
|
||||||
|
}
|
||||||
|
// Validate namespace ID is a number
|
||||||
|
id, err := strconv.ParseInt(parts[1], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, fmt.Errorf("invalid namespaced ID: %s", s.NamespacedID)
|
||||||
}
|
}
|
||||||
s.UserID = dbUsr.ID
|
return id, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyncUserRolesCommand struct {
|
type SyncUserRolesCommand struct {
|
||||||
|
@ -42,8 +42,8 @@ var SharedWithMeFolderPermission = accesscontrol.Permission{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
|
func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
|
||||||
accessControl accesscontrol.AccessControl, userSvc user.Service, features featuremgmt.FeatureToggles) (*Service, error) {
|
accessControl accesscontrol.AccessControl, features featuremgmt.FeatureToggles) (*Service, error) {
|
||||||
service := ProvideOSSService(cfg, database.ProvideService(db), cache, userSvc, features)
|
service := ProvideOSSService(cfg, database.ProvideService(db), cache, features)
|
||||||
|
|
||||||
api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints()
|
api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints()
|
||||||
if err := accesscontrol.DeclareFixedRoles(service, cfg); err != nil {
|
if err := accesscontrol.DeclareFixedRoles(service, cfg); err != nil {
|
||||||
@ -61,7 +61,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegis
|
|||||||
return service, nil
|
return service, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheService, userSvc user.Service, features featuremgmt.FeatureToggles) *Service {
|
func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheService, features featuremgmt.FeatureToggles) *Service {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
cache: cache,
|
cache: cache,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@ -69,7 +69,6 @@ func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheSer
|
|||||||
log: log.New("accesscontrol.service"),
|
log: log.New("accesscontrol.service"),
|
||||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||||
store: store,
|
store: store,
|
||||||
userSvc: userSvc,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
@ -94,7 +93,6 @@ type Service struct {
|
|||||||
registrations accesscontrol.RegistrationList
|
registrations accesscontrol.RegistrationList
|
||||||
roles map[string]*accesscontrol.RoleDTO
|
roles map[string]*accesscontrol.RoleDTO
|
||||||
store store
|
store store
|
||||||
userSvc user.Service
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
|
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
|
||||||
@ -245,21 +243,20 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs
|
|||||||
// SearchUsersPermissions returns all users' permissions filtered by action prefixes
|
// SearchUsersPermissions returns all users' permissions filtered by action prefixes
|
||||||
func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Requester,
|
func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Requester,
|
||||||
options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
|
options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
|
||||||
if options.UserLogin != "" {
|
if options.NamespacedID != "" {
|
||||||
// Resolve userLogin -> userID
|
userID, err := options.ComputeUserID()
|
||||||
if err := options.ResolveUserLogin(ctx, s.userSvc); err != nil {
|
if err != nil {
|
||||||
|
s.log.Error("Failed to resolve user ID", "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
options.UserLogin = ""
|
|
||||||
}
|
|
||||||
if options.UserID > 0 {
|
|
||||||
// Reroute to the user specific implementation of search permissions
|
// Reroute to the user specific implementation of search permissions
|
||||||
// because it leverages the user permission cache.
|
// because it leverages the user permission cache.
|
||||||
userPerms, err := s.SearchUserPermissions(ctx, usr.GetOrgID(), options)
|
userPerms, err := s.SearchUserPermissions(ctx, usr.GetOrgID(), options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return map[int64][]accesscontrol.Permission{options.UserID: userPerms}, nil
|
return map[int64][]accesscontrol.Permission{userID: userPerms}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
timer := prometheus.NewTimer(metrics.MAccessSearchPermissionsSummary)
|
timer := prometheus.NewTimer(metrics.MAccessSearchPermissionsSummary)
|
||||||
@ -346,15 +343,8 @@ func (s *Service) SearchUserPermissions(ctx context.Context, orgID int64, search
|
|||||||
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
|
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
|
||||||
defer timer.ObserveDuration()
|
defer timer.ObserveDuration()
|
||||||
|
|
||||||
if searchOptions.UserLogin != "" {
|
if searchOptions.NamespacedID == "" {
|
||||||
// Resolve userLogin -> userID
|
return nil, fmt.Errorf("expected namespaced ID to be specified")
|
||||||
if err := searchOptions.ResolveUserLogin(ctx, s.userSvc); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if searchOptions.UserID == 0 {
|
|
||||||
return nil, fmt.Errorf("expected user ID or login to be specified")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if permissions, success := s.searchUserPermissionsFromCache(orgID, searchOptions); success {
|
if permissions, success := s.searchUserPermissionsFromCache(orgID, searchOptions); success {
|
||||||
@ -364,15 +354,20 @@ func (s *Service) SearchUserPermissions(ctx context.Context, orgID int64, search
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) searchUserPermissions(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error) {
|
func (s *Service) searchUserPermissions(ctx context.Context, orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, error) {
|
||||||
|
userID, err := searchOptions.ComputeUserID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Get permissions for user's basic roles from RAM
|
// Get permissions for user's basic roles from RAM
|
||||||
roleList, err := s.store.GetUsersBasicRoles(ctx, []int64{searchOptions.UserID}, orgID)
|
roleList, err := s.store.GetUsersBasicRoles(ctx, []int64{userID}, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not fetch basic roles for the user: %w", err)
|
return nil, fmt.Errorf("could not fetch basic roles for the user: %w", err)
|
||||||
}
|
}
|
||||||
var roles []string
|
var roles []string
|
||||||
var ok bool
|
var ok bool
|
||||||
if roles, ok = roleList[searchOptions.UserID]; !ok {
|
if roles, ok = roleList[userID]; !ok {
|
||||||
return nil, fmt.Errorf("found no basic roles for user %d in organisation %d", searchOptions.UserID, orgID)
|
return nil, fmt.Errorf("found no basic roles for user %d in organisation %d", userID, orgID)
|
||||||
}
|
}
|
||||||
permissions := make([]accesscontrol.Permission, 0)
|
permissions := make([]accesscontrol.Permission, 0)
|
||||||
for _, builtin := range roles {
|
for _, builtin := range roles {
|
||||||
@ -390,15 +385,20 @@ func (s *Service) searchUserPermissions(ctx context.Context, orgID int64, search
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
permissions = append(permissions, dbPermissions[searchOptions.UserID]...)
|
permissions = append(permissions, dbPermissions[userID]...)
|
||||||
|
|
||||||
return permissions, nil
|
return permissions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) searchUserPermissionsFromCache(orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, bool) {
|
func (s *Service) searchUserPermissionsFromCache(orgID int64, searchOptions accesscontrol.SearchOptions) ([]accesscontrol.Permission, bool) {
|
||||||
|
userID, err := searchOptions.ComputeUserID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
// Create a temp signed in user object to retrieve cache key
|
// Create a temp signed in user object to retrieve cache key
|
||||||
tempUser := &user.SignedInUser{
|
tempUser := &user.SignedInUser{
|
||||||
UserID: searchOptions.UserID,
|
UserID: userID,
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,11 +15,11 @@ import (
|
|||||||
"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/actest"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/licensing"
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||||
)
|
)
|
||||||
@ -40,7 +40,6 @@ func setupTestEnv(t testing.TB) *Service {
|
|||||||
registrations: accesscontrol.RegistrationList{},
|
registrations: accesscontrol.RegistrationList{},
|
||||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||||
store: database.ProvideService(db.InitTestDB(t)),
|
store: database.ProvideService(db.InitTestDB(t)),
|
||||||
userSvc: usertest.NewUserServiceFake(),
|
|
||||||
}
|
}
|
||||||
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
|
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
|
||||||
return ac
|
return ac
|
||||||
@ -65,7 +64,6 @@ func TestUsageMetrics(t *testing.T) {
|
|||||||
cfg,
|
cfg,
|
||||||
database.ProvideService(db.InitTestDB(t)),
|
database.ProvideService(db.InitTestDB(t)),
|
||||||
localcache.ProvideService(),
|
localcache.ProvideService(),
|
||||||
usertest.NewUserServiceFake(),
|
|
||||||
featuremgmt.WithFeatures(),
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
|
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
|
||||||
@ -537,9 +535,9 @@ func TestService_SearchUsersPermissions(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// This test is not exactly representative as normally the store would return
|
// This test is not exactly representative as normally the store would return
|
||||||
// only the user's basic roles and the user's stored permissions
|
// only the user's basic roles and the user's stored permissions
|
||||||
name: "check userID filter works correctly",
|
name: "check namespacedId filter works correctly",
|
||||||
siuPermissions: listAllPerms,
|
siuPermissions: listAllPerms,
|
||||||
searchOption: accesscontrol.SearchOptions{UserID: 1},
|
searchOption: accesscontrol.SearchOptions{NamespacedID: identity.NamespaceServiceAccount + ":1"},
|
||||||
ramRoles: map[string]*accesscontrol.RoleDTO{
|
ramRoles: map[string]*accesscontrol.RoleDTO{
|
||||||
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
|
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
|
||||||
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
|
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
|
||||||
@ -564,47 +562,11 @@ func TestService_SearchUsersPermissions(t *testing.T) {
|
|||||||
1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}, {Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"}},
|
1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}, {Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// This test is not exactly representative as normally the store would return
|
|
||||||
// only the user's basic roles and the user's stored permissions
|
|
||||||
name: "check userLogin filter works correctly",
|
|
||||||
siuPermissions: listAllPerms,
|
|
||||||
searchOption: accesscontrol.SearchOptions{UserLogin: "testUser"},
|
|
||||||
ramRoles: map[string]*accesscontrol.RoleDTO{
|
|
||||||
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
|
|
||||||
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"},
|
|
||||||
}},
|
|
||||||
string(roletype.RoleAdmin): {Permissions: []accesscontrol.Permission{
|
|
||||||
{Action: accesscontrol.ActionTeamsWrite, 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{
|
|
||||||
2: {{Action: accesscontrol.ActionTeamsWrite, Scope: "teams:*"},
|
|
||||||
{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"},
|
|
||||||
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"},
|
|
||||||
{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:*"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ac := setupTestEnv(t)
|
ac := setupTestEnv(t)
|
||||||
|
|
||||||
// Resolve user login to id 2
|
|
||||||
ac.userSvc = &usertest.FakeUserService{ExpectedUser: &user.User{ID: 2}}
|
|
||||||
|
|
||||||
ac.roles = tt.ramRoles
|
ac.roles = tt.ramRoles
|
||||||
ac.store = actest.FakeStore{
|
ac.store = actest.FakeStore{
|
||||||
ExpectedUsersPermissions: tt.storedPerms,
|
ExpectedUsersPermissions: tt.storedPerms,
|
||||||
@ -645,7 +607,7 @@ func TestService_SearchUserPermissions(t *testing.T) {
|
|||||||
name: "ram only",
|
name: "ram only",
|
||||||
searchOption: accesscontrol.SearchOptions{
|
searchOption: accesscontrol.SearchOptions{
|
||||||
ActionPrefix: "teams",
|
ActionPrefix: "teams",
|
||||||
UserID: 2,
|
NamespacedID: identity.NamespaceUser + ":2",
|
||||||
},
|
},
|
||||||
ramRoles: map[string]*accesscontrol.RoleDTO{
|
ramRoles: map[string]*accesscontrol.RoleDTO{
|
||||||
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
|
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
|
||||||
@ -670,7 +632,7 @@ func TestService_SearchUserPermissions(t *testing.T) {
|
|||||||
name: "stored only",
|
name: "stored only",
|
||||||
searchOption: accesscontrol.SearchOptions{
|
searchOption: accesscontrol.SearchOptions{
|
||||||
ActionPrefix: "teams",
|
ActionPrefix: "teams",
|
||||||
UserID: 2,
|
NamespacedID: identity.NamespaceUser + ":2",
|
||||||
},
|
},
|
||||||
storedPerms: map[int64][]accesscontrol.Permission{
|
storedPerms: map[int64][]accesscontrol.Permission{
|
||||||
1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
|
1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}},
|
||||||
@ -690,7 +652,7 @@ func TestService_SearchUserPermissions(t *testing.T) {
|
|||||||
name: "ram and stored",
|
name: "ram and stored",
|
||||||
searchOption: accesscontrol.SearchOptions{
|
searchOption: accesscontrol.SearchOptions{
|
||||||
ActionPrefix: "teams",
|
ActionPrefix: "teams",
|
||||||
UserID: 2,
|
NamespacedID: identity.NamespaceUser + ":2",
|
||||||
},
|
},
|
||||||
ramRoles: map[string]*accesscontrol.RoleDTO{
|
ramRoles: map[string]*accesscontrol.RoleDTO{
|
||||||
string(roletype.RoleAdmin): {Permissions: []accesscontrol.Permission{
|
string(roletype.RoleAdmin): {Permissions: []accesscontrol.Permission{
|
||||||
@ -720,7 +682,7 @@ func TestService_SearchUserPermissions(t *testing.T) {
|
|||||||
name: "check action prefix filter works correctly",
|
name: "check action prefix filter works correctly",
|
||||||
searchOption: accesscontrol.SearchOptions{
|
searchOption: accesscontrol.SearchOptions{
|
||||||
ActionPrefix: "teams",
|
ActionPrefix: "teams",
|
||||||
UserID: 1,
|
NamespacedID: identity.NamespaceUser + ":1",
|
||||||
},
|
},
|
||||||
ramRoles: map[string]*accesscontrol.RoleDTO{
|
ramRoles: map[string]*accesscontrol.RoleDTO{
|
||||||
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
|
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
|
||||||
@ -741,8 +703,8 @@ func TestService_SearchUserPermissions(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "check action filter works correctly",
|
name: "check action filter works correctly",
|
||||||
searchOption: accesscontrol.SearchOptions{
|
searchOption: accesscontrol.SearchOptions{
|
||||||
Action: accesscontrol.ActionTeamsRead,
|
Action: accesscontrol.ActionTeamsRead,
|
||||||
UserID: 1,
|
NamespacedID: identity.NamespaceUser + ":1",
|
||||||
},
|
},
|
||||||
ramRoles: map[string]*accesscontrol.RoleDTO{
|
ramRoles: map[string]*accesscontrol.RoleDTO{
|
||||||
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
|
string(roletype.RoleEditor): {Permissions: []accesscontrol.Permission{
|
||||||
|
@ -2,7 +2,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
@ -69,30 +68,17 @@ func (api *AccessControlAPI) getUserPermissions(c *contextmodel.ReqContext) resp
|
|||||||
// GET /api/access-control/users/permissions/search
|
// GET /api/access-control/users/permissions/search
|
||||||
func (api *AccessControlAPI) searchUsersPermissions(c *contextmodel.ReqContext) response.Response {
|
func (api *AccessControlAPI) searchUsersPermissions(c *contextmodel.ReqContext) response.Response {
|
||||||
searchOptions := ac.SearchOptions{
|
searchOptions := ac.SearchOptions{
|
||||||
UserLogin: c.Query("userLogin"),
|
|
||||||
ActionPrefix: c.Query("actionPrefix"),
|
ActionPrefix: c.Query("actionPrefix"),
|
||||||
Action: c.Query("action"),
|
Action: c.Query("action"),
|
||||||
Scope: c.Query("scope"),
|
Scope: c.Query("scope"),
|
||||||
}
|
NamespacedID: c.Query("namespacedId"),
|
||||||
|
|
||||||
userIDString := c.Query("userId")
|
|
||||||
if userIDString != "" {
|
|
||||||
userID, err := strconv.ParseInt(userIDString, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return response.Error(http.StatusBadRequest, "user ID is invalid", err)
|
|
||||||
}
|
|
||||||
searchOptions.UserID = userID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
if (searchOptions.ActionPrefix != "") && (searchOptions.Action != "") {
|
if searchOptions.ActionPrefix != "" && searchOptions.Action != "" {
|
||||||
return response.JSON(http.StatusBadRequest, "'action' and 'actionPrefix' are mutually exclusive")
|
return response.JSON(http.StatusBadRequest, "'action' and 'actionPrefix' are mutually exclusive")
|
||||||
}
|
}
|
||||||
if (searchOptions.UserLogin != "") && (searchOptions.UserID > 0) {
|
if searchOptions.NamespacedID == "" && searchOptions.ActionPrefix == "" && searchOptions.Action == "" {
|
||||||
return response.JSON(http.StatusBadRequest, "'userId' and 'userLogin' are mutually exclusive")
|
|
||||||
}
|
|
||||||
if searchOptions.UserID <= 0 && searchOptions.UserLogin == "" &&
|
|
||||||
searchOptions.ActionPrefix == "" && searchOptions.Action == "" {
|
|
||||||
return response.JSON(http.StatusBadRequest, "at least one search option must be provided")
|
return response.JSON(http.StatusBadRequest, "at least one search option must be provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,31 +139,21 @@ func TestAccessControlAPI_searchUsersPermissions(t *testing.T) {
|
|||||||
expectedCode: http.StatusBadRequest,
|
expectedCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "Should reject if conflicting user filters are provided",
|
desc: "Should work with valid namespacedId filter provided",
|
||||||
filters: "?userLogin=admin&userId=2",
|
filters: "?namespacedId=service-account:2",
|
||||||
expectedCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Should reject if invalid userID is provided",
|
|
||||||
filters: "?userId=invalid",
|
|
||||||
expectedCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "Should work with valid filter provided",
|
|
||||||
filters: "?userId=2",
|
|
||||||
permissions: map[int64][]ac.Permission{2: {{Action: "users:read", Scope: "users:*"}}},
|
permissions: map[int64][]ac.Permission{2: {{Action: "users:read", Scope: "users:*"}}},
|
||||||
expectedCode: http.StatusOK,
|
expectedCode: http.StatusOK,
|
||||||
expectedOutput: map[int64]map[string][]string{2: {"users:read": {"users:*"}}},
|
expectedOutput: map[int64]map[string][]string{2: {"users:read": {"users:*"}}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "Should reduce permissions",
|
desc: "Should reduce permissions",
|
||||||
filters: "?userId=2",
|
filters: "?namespacedId=service-account:2",
|
||||||
permissions: map[int64][]ac.Permission{2: {{Action: "users:read", Scope: "users:id:1"}, {Action: "users:read", Scope: "users:*"}}},
|
permissions: map[int64][]ac.Permission{2: {{Action: "users:read", Scope: "users:id:1"}, {Action: "users:read", Scope: "users:*"}}},
|
||||||
expectedCode: http.StatusOK,
|
expectedCode: http.StatusOK,
|
||||||
expectedOutput: map[int64]map[string][]string{2: {"users:read": {"users:*"}}},
|
expectedOutput: map[int64]map[string][]string{2: {"users:read": {"users:*"}}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "Should work with valid action filter",
|
desc: "Should work with valid action prefix filter",
|
||||||
filters: "?actionPrefix=users:",
|
filters: "?actionPrefix=users:",
|
||||||
permissions: map[int64][]ac.Permission{
|
permissions: map[int64][]ac.Permission{
|
||||||
1: {{Action: "users:write", Scope: "users:id:1"}},
|
1: {{Action: "users:write", Scope: "users:id:1"}},
|
||||||
|
@ -113,10 +113,13 @@ func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID i
|
|||||||
params = append(params, scopes[i])
|
params = append(params, scopes[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if options.NamespacedID != "" {
|
||||||
if options.UserID != 0 {
|
userID, err := options.ComputeUserID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
q += ` AND user_id = ?`
|
q += ` AND user_id = ?`
|
||||||
params = append(params, options.UserID)
|
params = append(params, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sess.SQL(q, params...).
|
return sess.SQL(q, params...).
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||||
"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/auth/identity"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
@ -468,7 +469,7 @@ func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) {
|
|||||||
},
|
},
|
||||||
options: accesscontrol.SearchOptions{
|
options: accesscontrol.SearchOptions{
|
||||||
ActionPrefix: "teams:",
|
ActionPrefix: "teams:",
|
||||||
UserID: 1,
|
NamespacedID: identity.NamespaceUser + ":1",
|
||||||
},
|
},
|
||||||
wantPerm: map[int64][]accesscontrol.Permission{
|
wantPerm: map[int64][]accesscontrol.Permission{
|
||||||
1: {{Action: "teams:read", Scope: "teams:id:1"}, {Action: "teams:read", Scope: "teams:id:10"},
|
1: {{Action: "teams:read", Scope: "teams:id:1"}, {Action: "teams:read", Scope: "teams:id:10"},
|
||||||
|
@ -83,7 +83,7 @@ func setupTestEnv(t *testing.T) *TestEnv {
|
|||||||
cache: localcache.New(cacheExpirationTime, cacheCleanupInterval),
|
cache: localcache.New(cacheExpirationTime, cacheCleanupInterval),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
accessControl: acimpl.ProvideAccessControl(cfg),
|
accessControl: acimpl.ProvideAccessControl(cfg),
|
||||||
acService: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), env.UserService, fmgt),
|
acService: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt),
|
||||||
memstore: storage.NewMemoryStore(),
|
memstore: storage.NewMemoryStore(),
|
||||||
sqlstore: env.OAuthStore,
|
sqlstore: env.OAuthStore,
|
||||||
logger: log.New("oauthserver.test"),
|
logger: log.New("oauthserver.test"),
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/ory/fosite/handler/oauth2"
|
"github.com/ory/fosite/handler/oauth2"
|
||||||
|
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
"github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver"
|
"github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver"
|
||||||
"github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/utils"
|
"github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver/utils"
|
||||||
"github.com/grafana/grafana/pkg/services/team"
|
"github.com/grafana/grafana/pkg/services/team"
|
||||||
@ -224,7 +225,8 @@ func (s *OAuth2ServiceImpl) handleJWTBearer(ctx context.Context, accessRequest f
|
|||||||
|
|
||||||
// filteredUserPermissions gets the user permissions and applies the actions filter
|
// filteredUserPermissions gets the user permissions and applies the actions filter
|
||||||
func (s *OAuth2ServiceImpl) filteredUserPermissions(ctx context.Context, userID int64, actionsFilter map[string]bool) ([]ac.Permission, error) {
|
func (s *OAuth2ServiceImpl) filteredUserPermissions(ctx context.Context, userID int64, actionsFilter map[string]bool) ([]ac.Permission, error) {
|
||||||
permissions, err := s.acService.SearchUserPermissions(ctx, oauthserver.TmpOrgID, ac.SearchOptions{UserID: userID})
|
permissions, err := s.acService.SearchUserPermissions(ctx, oauthserver.TmpOrgID,
|
||||||
|
ac.SearchOptions{NamespacedID: fmt.Sprintf("%s:%d", identity.NamespaceUser, userID)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &fosite.RFC6749Error{
|
return nil, &fosite.RFC6749Error{
|
||||||
DescriptionField: "The permissions scope could not be processed.",
|
DescriptionField: "The permissions scope could not be processed.",
|
||||||
|
@ -64,7 +64,7 @@ func NewTestMigrationStore(t testing.TB, sqlStore *sqlstore.SQLStore, cfg *setti
|
|||||||
userSvc, err := userimpl.ProvideService(sqlStore, orgService, cfg, teamSvc, cache, quotaService, bundleregistry.ProvideService())
|
userSvc, err := userimpl.ProvideService(sqlStore, orgService, cfg, teamSvc, cache, quotaService, bundleregistry.ProvideService())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
acSvc, err := acimpl.ProvideService(cfg, sqlStore, routing.ProvideRegister(), cache, ac, userSvc, features)
|
acSvc, err := acimpl.ProvideService(cfg, sqlStore, routing.ProvideRegister(), cache, ac, features)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dashboardStore, err := database.ProvideDashboardStore(sqlStore, sqlStore.Cfg, features, tagimpl.ProvideService(sqlStore), quotaService)
|
dashboardStore, err := database.ProvideDashboardStore(sqlStore, sqlStore.Cfg, features, tagimpl.ProvideService(sqlStore), quotaService)
|
||||||
|
@ -20,7 +20,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||||
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
|
sa "github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -44,7 +43,7 @@ func setupTestEnv(t *testing.T) *TestEnv {
|
|||||||
}
|
}
|
||||||
logger := log.New("extsvcaccounts.test")
|
logger := log.New("extsvcaccounts.test")
|
||||||
env.S = &ExtSvcAccountsService{
|
env.S = &ExtSvcAccountsService{
|
||||||
acSvc: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), usertest.NewUserServiceFake(), fmgt),
|
acSvc: acimpl.ProvideOSSService(cfg, env.AcStore, localcache.New(0, 0), fmgt),
|
||||||
features: fmgt,
|
features: fmgt,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
metrics: newMetrics(nil, env.SaSvc, logger),
|
metrics: newMetrics(nil, env.SaSvc, logger),
|
||||||
|
Loading…
Reference in New Issue
Block a user