diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 20bc1aa5e4e..812c9d5ed09 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -276,12 +276,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 @@ -367,7 +368,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() diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index b24612f763f..5fedd4d412a 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -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 } diff --git a/pkg/api/index.go b/pkg/api/index.go index da0c9c45bf1..0796e3ede01 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -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 } diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 8bbd2a1eba4..dfc70933943 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -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 } diff --git a/pkg/api/team.go b/pkg/api/team.go index 95160280446..2c71458d919 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -112,7 +112,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 @@ -177,7 +177,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 diff --git a/pkg/api/user.go b/pkg/api/user.go index fac5aa8acbf..64f609fff43 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -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 } diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 3bf514d6371..f9272d17fb5 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -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) diff --git a/pkg/services/accesscontrol/api/api.go b/pkg/services/accesscontrol/api/api.go new file mode 100644 index 00000000000..4477dfe9eca --- /dev/null +++ b/pkg/services/accesscontrol/api/api.go @@ -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)) +} diff --git a/pkg/services/accesscontrol/middleware/middleware.go b/pkg/services/accesscontrol/middleware/middleware.go index ca5427b2c3b..96f4d3696d1 100644 --- a/pkg/services/accesscontrol/middleware/middleware.go +++ b/pkg/services/accesscontrol/middleware/middleware.go @@ -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 diff --git a/pkg/services/accesscontrol/mock/mock.go b/pkg/services/accesscontrol/mock/mock.go index 5460749f9b9..26125e2f532 100644 --- a/pkg/services/accesscontrol/mock/mock.go +++ b/pkg/services/accesscontrol/mock/mock.go @@ -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 diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go index 755c2f9e005..efe6dc98118 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go @@ -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() diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go index 103e86d498a..7ff45b1e87c 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go @@ -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 diff --git a/pkg/services/serviceaccounts/api/api_test.go b/pkg/services/serviceaccounts/api/api_test.go index 4749974901a..a426e06d51b 100644 --- a/pkg/services/serviceaccounts/api/api_test.go +++ b/pkg/services/serviceaccounts/api/api_test.go @@ -50,7 +50,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, @@ -74,7 +74,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, @@ -134,7 +134,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, @@ -146,7 +146,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, @@ -159,7 +159,7 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) { userID: 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, diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index 4451c265ba6..424802508e7 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -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 { diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index bf719821487..b9dfcaca92a 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -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 */ diff --git a/public/app/features/teams/CreateTeam.tsx b/public/app/features/teams/CreateTeam.tsx index 77505494cce..47373c38d76 100644 --- a/public/app/features/teams/CreateTeam.tsx +++ b/public/app/features/teams/CreateTeam.tsx @@ -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 { 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}`); } }; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 5a0e068f20d..43f9e0bb861 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -228,7 +228,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')), },