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:
Gabriel MABILLE 2024-02-16 11:42:36 +01:00 committed by GitHub
parent fe0fc08b93
commit 846eadff63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 78 additions and 126 deletions

View File

@ -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)

View File

@ -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)
} }

View File

@ -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 {

View File

@ -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,
} }

View File

@ -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{

View File

@ -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")
} }

View File

@ -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"}},

View File

@ -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...).

View File

@ -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"},

View File

@ -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"),

View File

@ -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.",

View File

@ -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)

View File

@ -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),