RBAC: Add userLogin filter to the permission search endpoint (#81137)
* RBAC: Search add user login filter * Switch to a userService resolving instead * Remove unused error * Fallback to use the cache * account for userID filter * Account for the error * snake case * Add test cases * Add api tests * Fix return on error * Re-order imports
This commit is contained in:
@@ -42,8 +42,8 @@ var SharedWithMeFolderPermission = accesscontrol.Permission{
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
|
||||
accessControl accesscontrol.AccessControl, features featuremgmt.FeatureToggles) (*Service, error) {
|
||||
service := ProvideOSSService(cfg, database.ProvideService(db), cache, features)
|
||||
accessControl accesscontrol.AccessControl, userSvc user.Service, features featuremgmt.FeatureToggles) (*Service, error) {
|
||||
service := ProvideOSSService(cfg, database.ProvideService(db), cache, userSvc, features)
|
||||
|
||||
api.NewAccessControlAPI(routeRegister, accessControl, service, features).RegisterAPIEndpoints()
|
||||
if err := accesscontrol.DeclareFixedRoles(service, cfg); err != nil {
|
||||
@@ -63,14 +63,15 @@ func ProvideService(cfg *setting.Cfg, db db.DB, routeRegister routing.RouteRegis
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheService, features featuremgmt.FeatureToggles) *Service {
|
||||
func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheService, userSvc user.Service, features featuremgmt.FeatureToggles) *Service {
|
||||
s := &Service{
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
log: log.New("accesscontrol.service"),
|
||||
cache: cache,
|
||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||
cfg: cfg,
|
||||
features: features,
|
||||
log: log.New("accesscontrol.service"),
|
||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||
store: store,
|
||||
userSvc: userSvc,
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -88,13 +89,14 @@ type store interface {
|
||||
|
||||
// Service is the service implementing role based access control.
|
||||
type Service struct {
|
||||
log log.Logger
|
||||
cfg *setting.Cfg
|
||||
store store
|
||||
cache *localcache.CacheService
|
||||
cfg *setting.Cfg
|
||||
features featuremgmt.FeatureToggles
|
||||
log log.Logger
|
||||
registrations accesscontrol.RegistrationList
|
||||
roles map[string]*accesscontrol.RoleDTO
|
||||
features featuremgmt.FeatureToggles
|
||||
store store
|
||||
userSvc user.Service
|
||||
}
|
||||
|
||||
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
|
||||
@@ -243,8 +245,25 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs
|
||||
}
|
||||
|
||||
// SearchUsersPermissions returns all users' permissions filtered by action prefixes
|
||||
func (s *Service) SearchUsersPermissions(ctx context.Context, user identity.Requester,
|
||||
func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Requester,
|
||||
options accesscontrol.SearchOptions) (map[int64][]accesscontrol.Permission, error) {
|
||||
if options.UserLogin != "" {
|
||||
// Resolve userLogin -> userID
|
||||
if err := options.ResolveUserLogin(ctx, s.userSvc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options.UserLogin = ""
|
||||
}
|
||||
if options.UserID > 0 {
|
||||
// Reroute to the user specific implementation of search permissions
|
||||
// because it leverages the user permission cache.
|
||||
userPerms, err := s.SearchUserPermissions(ctx, usr.GetOrgID(), options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[int64][]accesscontrol.Permission{options.UserID: userPerms}, nil
|
||||
}
|
||||
|
||||
timer := prometheus.NewTimer(metrics.MAccessSearchPermissionsSummary)
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
@@ -258,20 +277,20 @@ func (s *Service) SearchUsersPermissions(ctx context.Context, user identity.Requ
|
||||
}
|
||||
}
|
||||
|
||||
usersRoles, err := s.store.GetUsersBasicRoles(ctx, nil, user.GetOrgID())
|
||||
usersRoles, err := s.store.GetUsersBasicRoles(ctx, nil, usr.GetOrgID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get managed permissions (DB)
|
||||
usersPermissions, err := s.store.SearchUsersPermissions(ctx, user.GetOrgID(), options)
|
||||
usersPermissions, err := s.store.SearchUsersPermissions(ctx, usr.GetOrgID(), 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 := user.GetPermissions()
|
||||
siuPermissions := usr.GetPermissions()
|
||||
if len(siuPermissions) == 0 {
|
||||
return func(_ int64) bool { return false }
|
||||
}
|
||||
@@ -329,8 +348,15 @@ func (s *Service) SearchUserPermissions(ctx context.Context, orgID int64, search
|
||||
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
if searchOptions.UserLogin != "" {
|
||||
// Resolve userLogin -> userID
|
||||
if err := searchOptions.ResolveUserLogin(ctx, s.userSvc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if searchOptions.UserID == 0 {
|
||||
return nil, fmt.Errorf("expected user ID to be specified")
|
||||
return nil, fmt.Errorf("expected user ID or login to be specified")
|
||||
}
|
||||
|
||||
if permissions, success := s.searchUserPermissionsFromCache(orgID, searchOptions); success {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -27,12 +28,14 @@ func setupTestEnv(t testing.TB) *Service {
|
||||
cfg := setting.NewCfg()
|
||||
|
||||
ac := &Service{
|
||||
cache: localcache.ProvideService(),
|
||||
cfg: cfg,
|
||||
features: featuremgmt.WithFeatures(),
|
||||
log: log.New("accesscontrol"),
|
||||
registrations: accesscontrol.RegistrationList{},
|
||||
store: database.ProvideService(db.InitTestDB(t)),
|
||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||
features: featuremgmt.WithFeatures(),
|
||||
store: database.ProvideService(db.InitTestDB(t)),
|
||||
userSvc: usertest.NewUserServiceFake(),
|
||||
}
|
||||
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
|
||||
return ac
|
||||
@@ -57,6 +60,7 @@ func TestUsageMetrics(t *testing.T) {
|
||||
cfg,
|
||||
database.ProvideService(db.InitTestDB(t)),
|
||||
localcache.ProvideService(),
|
||||
usertest.NewUserServiceFake(),
|
||||
featuremgmt.WithFeatures(),
|
||||
)
|
||||
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
|
||||
@@ -525,11 +529,77 @@ func TestService_SearchUsersPermissions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// 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 userID filter works correctly",
|
||||
siuPermissions: listAllPerms,
|
||||
searchOption: accesscontrol.SearchOptions{UserID: 1},
|
||||
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{
|
||||
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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ac := setupTestEnv(t)
|
||||
|
||||
// Resolve user login to id 2
|
||||
ac.userSvc = &usertest.FakeUserService{ExpectedUser: &user.User{ID: 2}}
|
||||
|
||||
ac.roles = tt.ramRoles
|
||||
ac.store = actest.FakeStore{
|
||||
ExpectedUsersPermissions: tt.storedPerms,
|
||||
|
||||
Reference in New Issue
Block a user