AccessControl: Add endpoint to get user permissions (#45309)

* AccessControl: Add endpoint to get user permissions

Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
Co-authored-by: Kalle Persson <kalle.persson@grafana.com>
Co-authored-by: Eric Leijonmarck <eric.leijonmarck@gmail.com>
Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>

* Fix SA tests

* Linter is wrong :p

* Wait I was wrong

* Adding the route for teams:creator too

Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
Co-authored-by: Kalle Persson <kalle.persson@grafana.com>
Co-authored-by: Eric Leijonmarck <eric.leijonmarck@gmail.com>
Co-authored-by: Alexander Zobnin <alexanderzobnin@gmail.com>
This commit is contained in:
Gabriel MABILLE 2022-02-11 17:40:43 +01:00 committed by GitHub
parent 689df761e6
commit 6fbf346747
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 110 additions and 39 deletions

View File

@ -279,12 +279,13 @@ type accessControlScenarioContext struct {
}
func setAccessControlPermissions(acmock *accesscontrolmock.Mock, perms []*accesscontrol.Permission, org int64) {
acmock.GetUserPermissionsFunc = func(_ context.Context, u *models.SignedInUser) ([]*accesscontrol.Permission, error) {
if u.OrgId == org {
return perms, nil
acmock.GetUserPermissionsFunc =
func(_ context.Context, u *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
if u.OrgId == org {
return perms, nil
}
return nil, nil
}
return nil, nil
}
}
// setInitCtxSignedInUser sets a copy of the user in initCtx
@ -370,7 +371,8 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont
require.NoError(t, err)
hs.TeamPermissionsService = teamPermissionService
} else {
ac := ossaccesscontrol.ProvideService(hs.Features, &usagestats.UsageStatsMock{T: t}, database.ProvideService(db))
ac := ossaccesscontrol.ProvideService(hs.Features, &usagestats.UsageStatsMock{T: t},
database.ProvideService(db), routing.NewRouteRegister())
hs.AccessControl = ac
// Perform role registration
err := hs.declareFixedRoles()

View File

@ -76,7 +76,8 @@ func (hs *HTTPServer) getDataSourceAccessControlMetadata(c *models.ReqContext, d
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser,
accesscontrol.Options{ReloadCache: false})
if err != nil || len(userPermissions) == 0 {
return nil, err
}

View File

@ -625,7 +625,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
}
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, ac.Options{ReloadCache: false})
if err != nil {
return nil, err
}

View File

@ -118,7 +118,7 @@ func (hs *HTTPServer) getUserAccessControlMetadata(c *models.ReqContext, resourc
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
if err != nil || len(userPermissions) == 0 {
return nil, err
}

View File

@ -110,7 +110,7 @@ func (hs *HTTPServer) getTeamsAccessControlMetadata(c *models.ReqContext, teamID
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
if err != nil || len(userPermissions) == 0 {
hs.log.Warn("could not fetch accesscontrol metadata for teams", "error", err)
return nil, err
@ -175,7 +175,7 @@ func (hs *HTTPServer) getTeamAccessControlMetadata(c *models.ReqContext, teamID
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
if err != nil || len(userPermissions) == 0 {
hs.log.Warn("could not fetch accesscontrol metadata", "team", teamID, "error", err)
return nil, err

View File

@ -64,7 +64,7 @@ func (hs *HTTPServer) getGlobalUserAccessControlMetadata(c *models.ReqContext, u
return nil, nil
}
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser)
userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
if err != nil || len(userPermissions) == 0 {
return nil, err
}

View File

@ -7,12 +7,16 @@ import (
"github.com/grafana/grafana/pkg/models"
)
type Options struct {
ReloadCache bool
}
type AccessControl interface {
// Evaluate evaluates access to the given resources.
Evaluate(ctx context.Context, user *models.SignedInUser, evaluator Evaluator) (bool, error)
// GetUserPermissions returns user permissions.
GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*Permission, error)
GetUserPermissions(ctx context.Context, user *models.SignedInUser, options Options) ([]*Permission, error)
// GetUserRoles returns user roles.
GetUserRoles(ctx context.Context, user *models.SignedInUser) ([]*RoleDTO, error)

View File

@ -0,0 +1,34 @@
package api
import (
"net/http"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
)
type AccessControlAPI struct {
RouteRegister routing.RouteRegister
AccessControl ac.AccessControl
}
func (api *AccessControlAPI) RegisterAPIEndpoints() {
// Users
api.RouteRegister.Get("/api/access-control/user/permissions",
middleware.ReqSignedIn, routing.Wrap(api.getUsersPermissions))
}
// GET /api/access-control/user/permissions
func (api *AccessControlAPI) getUsersPermissions(c *models.ReqContext) response.Response {
reloadCache := c.QueryBool("reloadcache")
permissions, err := api.AccessControl.GetUserPermissions(c.Req.Context(),
c.SignedInUser, ac.Options{ReloadCache: reloadCache})
if err != nil {
response.JSON(http.StatusInternalServerError, err)
}
return response.JSON(http.StatusOK, ac.BuildPermissionsMap(permissions))
}

View File

@ -156,7 +156,8 @@ func LoadPermissionsMiddleware(ac accesscontrol.AccessControl) web.Handler {
return
}
permissions, err := ac.GetUserPermissions(c.Req.Context(), c.SignedInUser)
permissions, err := ac.GetUserPermissions(c.Req.Context(), c.SignedInUser,
accesscontrol.Options{ReloadCache: false})
if err != nil {
c.JsonApiErr(http.StatusForbidden, "", err)
return

View File

@ -39,7 +39,7 @@ type Mock struct {
// Override functions
EvaluateFunc func(context.Context, *models.SignedInUser, accesscontrol.Evaluator) (bool, error)
GetUserPermissionsFunc func(context.Context, *models.SignedInUser) ([]*accesscontrol.Permission, error)
GetUserPermissionsFunc func(context.Context, *models.SignedInUser, accesscontrol.Options) ([]*accesscontrol.Permission, error)
GetUserRolesFunc func(context.Context, *models.SignedInUser) ([]*accesscontrol.RoleDTO, error)
IsDisabledFunc func() bool
DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error
@ -86,7 +86,7 @@ func (m *Mock) Evaluate(ctx context.Context, user *models.SignedInUser, evaluato
return m.EvaluateFunc(ctx, user, evaluator)
}
// Otherwise perform an actual evaluation of the permissions
permissions, err := m.GetUserPermissions(ctx, user)
permissions, err := m.GetUserPermissions(ctx, user, accesscontrol.Options{ReloadCache: false})
if err != nil {
return false, err
}
@ -95,11 +95,12 @@ func (m *Mock) Evaluate(ctx context.Context, user *models.SignedInUser, evaluato
// GetUserPermissions returns user permissions.
// This mock return m.permissions unless an override is provided.
func (m *Mock) GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.Permission, error) {
m.Calls.GetUserPermissions = append(m.Calls.GetUserPermissions, []interface{}{ctx, user})
func (m *Mock) GetUserPermissions(ctx context.Context, user *models.SignedInUser,
opts accesscontrol.Options) ([]*accesscontrol.Permission, error) {
m.Calls.GetUserPermissions = append(m.Calls.GetUserPermissions, []interface{}{ctx, user, opts})
// Use override if provided
if m.GetUserPermissionsFunc != nil {
return m.GetUserPermissionsFunc(ctx, user)
return m.GetUserPermissionsFunc(ctx, user, opts)
}
// Otherwise return the Permissions list
return m.permissions, nil

View File

@ -4,19 +4,29 @@ import (
"context"
"errors"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourceservices"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/prometheus/client_golang/prometheus"
)
func ProvideService(features featuremgmt.FeatureToggles, usageStats usagestats.Service, provider accesscontrol.PermissionsProvider) *OSSAccessControlService {
func ProvideService(features featuremgmt.FeatureToggles, usageStats usagestats.Service,
provider accesscontrol.PermissionsProvider, routeRegister routing.RouteRegister) *OSSAccessControlService {
s := ProvideOSSAccessControl(features, usageStats, provider)
s.registerUsageMetrics()
if !s.IsDisabled() {
api := api.AccessControlAPI{
RouteRegister: routeRegister,
AccessControl: s,
}
api.RegisterAPIEndpoints()
}
return s
}
@ -75,7 +85,7 @@ func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *models.Si
}
if _, ok := user.Permissions[user.OrgId]; !ok {
permissions, err := ac.GetUserPermissions(ctx, user)
permissions, err := ac.GetUserPermissions(ctx, user, accesscontrol.Options{ReloadCache: true})
if err != nil {
return false, err
}
@ -96,7 +106,7 @@ func (ac *OSSAccessControlService) GetUserRoles(ctx context.Context, user *model
}
// GetUserPermissions returns user permissions based on built-in roles
func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
defer timer.ObserveDuration()

View File

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
@ -152,6 +153,7 @@ func TestUsageMetrics(t *testing.T) {
featuremgmt.WithFeatures("accesscontrol", tt.enabled),
&usagestats.UsageStatsMock{T: t},
database.ProvideService(sqlstore.InitTestDB(t)),
routing.NewRouteRegister(),
)
report, err := s.usageStats.GetUsageReport(context.Background())
assert.Nil(t, err)
@ -543,7 +545,7 @@ func TestOSSAccessControlService_GetUserPermissions(t *testing.T) {
require.NoError(t, err)
// Test
userPerms, err := ac.GetUserPermissions(context.Background(), &tt.user)
userPerms, err := ac.GetUserPermissions(context.Background(), &tt.user, accesscontrol.Options{})
if tt.wantErr {
assert.Error(t, err, "Expected an error with GetUserPermissions.")
return

View File

@ -163,7 +163,7 @@ func (api *ServiceAccountsAPI) getAccessControlMetadata(c *models.ReqContext, sa
return nil, nil
}
userPermissions, err := api.accesscontrol.GetUserPermissions(c.Req.Context(), c.SignedInUser)
userPermissions, err := api.accesscontrol.GetUserPermissions(c.Req.Context(), c.SignedInUser, accesscontrol.Options{ReloadCache: false})
if err != nil || len(userPermissions) == 0 {
api.log.Warn("could not fetch accesscontrol metadata for teams", "error", err)
return nil, err

View File

@ -51,7 +51,7 @@ func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) {
user: tests.TestUser{Login: "servicetest1@admin", IsServiceAccount: true},
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionDelete, Scope: serviceaccounts.ScopeAll}}, nil
},
false,
@ -75,7 +75,7 @@ func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) {
user: tests.TestUser{Login: "servicetest2@admin", IsServiceAccount: true},
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{}, nil
},
false,
@ -137,7 +137,7 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
user: &tests.TestUser{Login: "servicetest1@admin", IsServiceAccount: true},
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionRead, Scope: serviceaccounts.ScopeAll}}, nil
},
false,
@ -149,7 +149,7 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
user: &tests.TestUser{Login: "servicetest2@admin", IsServiceAccount: true},
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{}, nil
},
false,
@ -162,7 +162,7 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
Id: 12,
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionRead, Scope: serviceaccounts.ScopeAll}}, nil
},
false,

View File

@ -64,7 +64,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) {
desc: "should be ok to create serviceaccount token with scope all permissions",
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: serviceaccounts.ScopeAll}}, nil
},
false,
@ -76,7 +76,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) {
desc: "serviceaccount token should match SA orgID and SA provided in parameters even if specified in body",
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: serviceaccounts.ScopeAll}}, nil
},
false,
@ -88,7 +88,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) {
desc: "should be ok to create serviceaccount token with scope id permissions",
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, nil
},
false,
@ -100,7 +100,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) {
desc: "should be forbidden to create serviceaccount token if wrong scoped",
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:2"}}, nil
},
false,
@ -171,7 +171,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) {
keyName: "Test1",
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, nil
},
false,
@ -183,7 +183,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) {
keyName: "Test2",
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: serviceaccounts.ScopeAll}}, nil
},
false,
@ -195,7 +195,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) {
keyName: "Test3",
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
func(c context.Context, siu *models.SignedInUser, _ accesscontrol.Options) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:10"}}, nil
},
false,

View File

@ -41,7 +41,9 @@ func (s *ServiceAccountMock) Migrated(ctx context.Context, orgID int64) bool {
return false
}
func SetupMockAccesscontrol(t *testing.T, userpermissionsfunc func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error), disableAccessControl bool) *accesscontrolmock.Mock {
func SetupMockAccesscontrol(t *testing.T,
userpermissionsfunc func(c context.Context, siu *models.SignedInUser, opt accesscontrol.Options) ([]*accesscontrol.Permission, error),
disableAccessControl bool) *accesscontrolmock.Mock {
t.Helper()
acmock := accesscontrolmock.New()
if disableAccessControl {

View File

@ -2,7 +2,7 @@ import config from '../../core/config';
import { extend } from 'lodash';
import { rangeUtil, WithAccessControlMetadata } from '@grafana/data';
import { AccessControlAction, UserPermission } from 'app/types';
import { featureEnabled } from '@grafana/runtime';
import { featureEnabled, getBackendSrv } from '@grafana/runtime';
export class User {
id: number;
@ -66,6 +66,18 @@ export class ContextSrv {
this.minRefreshInterval = config.minRefreshInterval;
}
async fetchUserPermissions() {
try {
if (this.accessControlEnabled()) {
this.user.permissions = await getBackendSrv().get('/api/access-control/user/permissions', {
reloadcache: true,
});
}
} catch (e) {
console.error(e);
}
}
/**
* Indicate the user has been logged out
*/

View File

@ -6,6 +6,7 @@ import { getBackendSrv, locationService } from '@grafana/runtime';
import { connect } from 'react-redux';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';
import { contextSrv } from 'app/core/core';
export interface Props {
navModel: NavModel;
@ -20,6 +21,7 @@ export class CreateTeam extends PureComponent<Props> {
create = async (formModel: TeamDTO) => {
const result = await getBackendSrv().post('/api/teams', formModel);
if (result.teamId) {
await contextSrv.fetchUserPermissions();
locationService.push(`/org/teams/edit/${result.teamId}`);
}
};

View File

@ -237,7 +237,7 @@ export function getAppRoutes(): RouteDescriptor[] {
roles: () =>
contextSrv.evaluatePermission(
() => (config.editorsCanAdmin ? ['Editor', 'Admin'] : ['Admin']),
[AccessControlAction.ActionTeamsRead]
[AccessControlAction.ActionTeamsRead, AccessControlAction.ActionTeamsCreate]
),
component: SafeDynamicImport(() => import(/* webpackChunkName: "TeamPages" */ 'app/features/teams/TeamPages')),
},