RBAC: Split up service into several components (#54002)

* RBAC: Rename interface to Store

* RBAC: Move ranme scopeInjector

* RBAC: Rename files to service

* RBAC: Rename to service

* RBAC: Split up accesscontrol into two components

* RBAC: Add DeclareFixedRoles to AccessControl interface

* Wire: Fix wire bindings

* RBAC: Move resolvers to root

* RBAC: Remove invalid test

* RBAC: Inject access control service

* RBAC: Implement the RoleRegistry interface in fake
This commit is contained in:
Karl Persson 2022-08-24 13:29:17 +02:00 committed by GitHub
parent 211c9991c5
commit 55c7b8add2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 769 additions and 958 deletions

View File

@ -366,6 +366,7 @@ func setupHTTPServerWithCfgDb(
var acmock *accesscontrolmock.Mock
var ac accesscontrol.AccessControl
var acService accesscontrol.Service
// Defining the accesscontrol service has to be done before registering routes
if useFakeAccessControl {
@ -374,13 +375,15 @@ func setupHTTPServerWithCfgDb(
acmock = acmock.WithDisabled()
}
ac = acmock
acService = acmock
} else {
var err error
ac, err = ossaccesscontrol.ProvideService(cfg, database.ProvideService(db), routeRegister)
acService, err = ossaccesscontrol.ProvideService(cfg, database.ProvideService(db), routeRegister)
require.NoError(t, err)
ac = ossaccesscontrol.ProvideAccessControl(cfg, acService)
}
teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, ac, license)
teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, ac, license, acService)
require.NoError(t, err)
// Create minimal HTTP Server
@ -395,6 +398,7 @@ func setupHTTPServerWithCfgDb(
SQLStore: store,
License: &licensing.OSSLicensingService{},
AccessControl: ac,
accesscontrolService: acService,
teamPermissionsService: teamPermissionService,
searchUsersService: searchusers.ProvideUsersService(filters.ProvideOSSSearchUserFilter(), usertest.NewUserServiceFake()),
DashboardService: dashboardservice.ProvideDashboardService(
@ -410,7 +414,7 @@ func setupHTTPServerWithCfgDb(
}
require.NoError(t, hs.declareFixedRoles())
require.NoError(t, hs.AccessControl.(accesscontrol.RoleRegistry).RegisterFixedRoles(context.Background()))
require.NoError(t, hs.accesscontrolService.(accesscontrol.RoleRegistry).RegisterFixedRoles(context.Background()))
// Instantiate a new Server
m := web.New()
@ -423,7 +427,7 @@ func setupHTTPServerWithCfgDb(
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), initCtx))
})
m.Use(accesscontrol.LoadPermissionsMiddleware(hs.AccessControl))
m.Use(accesscontrol.LoadPermissionsMiddleware(hs.accesscontrolService))
// Register all routes
hs.registerRoutes()

View File

@ -182,6 +182,7 @@ type HTTPServer struct {
tempUserService tempUser.Service
loginAttemptService loginAttempt.Service
orgService org.Service
accesscontrolService accesscontrol.Service
}
type ServerOptions struct {
@ -217,7 +218,9 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
starService star.Service, csrfService csrf.Service, coremodels *registry.Base,
playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, secretsPluginManager plugins.SecretsPluginManager,
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service, loginAttemptService loginAttempt.Service, orgService org.Service) (*HTTPServer, error) {
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service, loginAttemptService loginAttempt.Service, orgService org.Service,
accesscontrolService accesscontrol.Service,
) (*HTTPServer, error) {
web.Env = cfg.Env
m := web.New()
@ -308,6 +311,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
tempUserService: tempUserService,
loginAttemptService: loginAttemptService,
orgService: orgService,
accesscontrolService: accesscontrolService,
}
if hs.Listener != nil {
hs.log.Debug("Using provided listener")
@ -560,7 +564,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(hs.ContextHandler.Middleware)
m.Use(middleware.OrgRedirect(hs.Cfg, hs.SQLStore))
m.Use(accesscontrol.LoadPermissionsMiddleware(hs.AccessControl))
m.Use(accesscontrol.LoadPermissionsMiddleware(hs.accesscontrolService))
// needs to be after context handler
if hs.Cfg.EnforceDomain {

View File

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

View File

@ -411,14 +411,14 @@ func (hs *HTTPServer) removeOrgUserHelper(ctx context.Context, cmd *models.Remov
if cmd.UserWasDeleted {
// This should be called from appropriate service when moved
if err := hs.AccessControl.DeleteUserPermissions(ctx, accesscontrol.GlobalOrgID, cmd.UserId); err != nil {
if err := hs.accesscontrolService.DeleteUserPermissions(ctx, accesscontrol.GlobalOrgID, cmd.UserId); err != nil {
hs.log.Warn("failed to delete permissions for user", "userID", cmd.UserId, "orgID", accesscontrol.GlobalOrgID, "err", err)
}
return response.Success("User deleted")
}
// This should be called from appropriate service when moved
if err := hs.AccessControl.DeleteUserPermissions(ctx, cmd.OrgId, cmd.UserId); err != nil {
if err := hs.accesscontrolService.DeleteUserPermissions(ctx, cmd.OrgId, cmd.UserId); err != nil {
hs.log.Warn("failed to delete permissions for user", "userID", cmd.UserId, "orgID", cmd.OrgId, "err", err)
}

View File

@ -960,7 +960,7 @@ func TestDeleteOrgUsersAPIEndpoint_AccessControl(t *testing.T) {
assert.Len(t, getUsersQuery.Result, tc.expectedUserCount)
// check all permissions for user is removed in org
permission, err := sc.hs.AccessControl.GetUserPermissions(context.Background(), &user.SignedInUser{UserID: tc.targetUserId, OrgID: tc.targetOrg}, accesscontrol.Options{})
permission, err := sc.hs.accesscontrolService.GetUserPermissions(context.Background(), &user.SignedInUser{UserID: tc.targetUserId, OrgID: tc.targetOrg}, accesscontrol.Options{})
require.NoError(t, err)
assert.Len(t, permission, 0)
}

View File

@ -323,6 +323,8 @@ var wireSet = wire.NewSet(
wire.Bind(new(db.DB), new(*sqlstore.SQLStore)),
prefimpl.ProvideService,
opentsdb.ProvideService,
ossaccesscontrol.ProvideAccessControl,
wire.Bind(new(accesscontrol.AccessControl), new(*ossaccesscontrol.AccessControl)),
)
func Initialize(cfg *setting.Cfg) (Runner, error) {

View File

@ -52,8 +52,8 @@ var wireExtsSet = wire.NewSet(
wire.Bind(new(models.UserTokenService), new(*auth.UserAuthTokenService)),
wire.Bind(new(models.UserTokenBackgroundService), new(*auth.UserAuthTokenService)),
ossaccesscontrol.ProvideService,
wire.Bind(new(accesscontrol.RoleRegistry), new(*ossaccesscontrol.OSSAccessControlService)),
wire.Bind(new(accesscontrol.AccessControl), new(*ossaccesscontrol.OSSAccessControlService)),
wire.Bind(new(accesscontrol.Service), new(*ossaccesscontrol.Service)),
wire.Bind(new(accesscontrol.RoleRegistry), new(*ossaccesscontrol.Service)),
thumbs.ProvideCrawlerAuthSetupService,
wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)),
validations.ProvideValidator,
@ -75,7 +75,7 @@ var wireExtsSet = wire.NewSet(
provider.ProvideService,
wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)),
acdb.ProvideService,
wire.Bind(new(accesscontrol.PermissionsStore), new(*acdb.AccessControlStore)),
wire.Bind(new(accesscontrol.Store), new(*acdb.AccessControlStore)),
ldap.ProvideGroupsService,
wire.Bind(new(ldap.Groups), new(*ldap.OSSGroups)),
permissions.ProvideDatasourcePermissionsService,

View File

@ -55,7 +55,7 @@ func testServer(t *testing.T, services ...registry.BackgroundService) *Server {
secretMigrationService := &migrations.SecretMigrationServiceImpl{
ServerLockService: serverLockService,
}
s, err := newServer(Options{}, setting.NewCfg(), nil, &ossaccesscontrol.OSSAccessControlService{}, nil, backgroundsvcs.NewBackgroundServiceRegistry(services...), secretMigrationService, usertest.NewUserServiceFake())
s, err := newServer(Options{}, setting.NewCfg(), nil, &ossaccesscontrol.Service{}, nil, backgroundsvcs.NewBackgroundServiceRegistry(services...), secretMigrationService, usertest.NewUserServiceFake())
require.NoError(t, err)
// Required to skip configuration initialization that causes
// DI errors in this test.

View File

@ -8,7 +8,7 @@ import (
func ProvideUsageStatsProvidersRegistry(
thumbsService thumbs.Service,
accesscontrol accesscontrol.AccessControl,
accesscontrol accesscontrol.Service,
) *UsageStatsProvidersRegistry {
return NewUsageStatsProvidersRegistry(
thumbsService,

View File

@ -323,6 +323,8 @@ var wireBasicSet = wire.NewSet(
secretsMigrations.ProvideSecretMigrationService,
wire.Bind(new(secretsMigrations.SecretMigrationService), new(*secretsMigrations.SecretMigrationServiceImpl)),
userauthimpl.ProvideService,
ossaccesscontrol.ProvideAccessControl,
wire.Bind(new(accesscontrol.AccessControl), new(*ossaccesscontrol.AccessControl)),
)
var wireSet = wire.NewSet(

View File

@ -47,8 +47,8 @@ var wireExtsBasicSet = wire.NewSet(
setting.ProvideProvider,
wire.Bind(new(setting.Provider), new(*setting.OSSImpl)),
ossaccesscontrol.ProvideService,
wire.Bind(new(accesscontrol.RoleRegistry), new(*ossaccesscontrol.OSSAccessControlService)),
wire.Bind(new(accesscontrol.AccessControl), new(*ossaccesscontrol.OSSAccessControlService)),
wire.Bind(new(accesscontrol.RoleRegistry), new(*ossaccesscontrol.Service)),
wire.Bind(new(accesscontrol.Service), new(*ossaccesscontrol.Service)),
thumbs.ProvideCrawlerAuthSetupService,
wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)),
validations.ProvideValidator,
@ -74,7 +74,7 @@ var wireExtsBasicSet = wire.NewSet(
provider.ProvideService,
wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)),
acdb.ProvideService,
wire.Bind(new(accesscontrol.PermissionsStore), new(*acdb.AccessControlStore)),
wire.Bind(new(accesscontrol.Store), new(*acdb.AccessControlStore)),
osskmsproviders.ProvideService,
wire.Bind(new(kmsproviders.Service), new(osskmsproviders.Service)),
ldap.ProvideGroupsService,

View File

@ -12,33 +12,32 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
type Options struct {
ReloadCache bool
}
type AccessControl interface {
registry.ProvidesUsageStats
// Evaluate evaluates access to the given resources.
Evaluate(ctx context.Context, user *user.SignedInUser, evaluator Evaluator) (bool, error)
// GetUserPermissions returns user permissions with only action and scope fields set.
GetUserPermissions(ctx context.Context, user *user.SignedInUser, options Options) ([]Permission, error)
//IsDisabled returns if access control is enabled or not
IsDisabled() bool
// DeclareFixedRoles allows the caller to declare, to the service, fixed roles and their
// assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
DeclareFixedRoles(...RoleRegistration) error
// RegisterScopeAttributeResolver allows the caller to register a scope resolver for a
// specific scope prefix (ex: datasources:name:)
RegisterScopeAttributeResolver(scopePrefix string, resolver ScopeAttributeResolver)
RegisterScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver)
// DeclareFixedRoles allows the caller to declare, to the service, fixed roles and their
// assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
// FIXME: Remove from access control interface and inject service where this is needed
DeclareFixedRoles(registrations ...RoleRegistration) error
//IsDisabled returns if access control is enabled or not
IsDisabled() bool
}
type Service interface {
registry.ProvidesUsageStats
// GetUserPermissions returns user permissions with only action and scope fields set.
GetUserPermissions(ctx context.Context, user *user.SignedInUser, options Options) ([]Permission, error)
// DeleteUserPermissions removes all permissions user has in org and all permission to that user
// If orgID is set to 0 remove permissions from all orgs
DeleteUserPermissions(ctx context.Context, orgID, userID int64) error
// DeclareFixedRoles allows the caller to declare, to the service, fixed roles and their
// assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
DeclareFixedRoles(registrations ...RoleRegistration) error
//IsDisabled returns if access control is enabled or not
IsDisabled() bool
}
type RoleRegistry interface {
@ -46,7 +45,11 @@ type RoleRegistry interface {
RegisterFixedRoles(ctx context.Context) error
}
type PermissionsStore interface {
type Options struct {
ReloadCache bool
}
type Store interface {
// GetUserPermissions returns user permissions with only action and scope fields set.
GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]Permission, error)
DeleteUserPermissions(ctx context.Context, orgID, userID int64) error

View File

@ -0,0 +1,64 @@
package actest
import (
"context"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/user"
)
var _ accesscontrol.Service = new(FakeService)
var _ accesscontrol.RoleRegistry = new(FakeService)
type FakeService struct {
ExpectedErr error
ExpectedDisabled bool
ExpectedPermissions []accesscontrol.Permission
}
func (f FakeService) GetUsageStats(ctx context.Context) map[string]interface{} {
return map[string]interface{}{}
}
func (f FakeService) GetUserPermissions(ctx context.Context, user *user.SignedInUser, options accesscontrol.Options) ([]accesscontrol.Permission, error) {
return f.ExpectedPermissions, f.ExpectedErr
}
func (f FakeService) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error {
return f.ExpectedErr
}
func (f FakeService) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error {
return f.ExpectedErr
}
func (f FakeService) RegisterFixedRoles(ctx context.Context) error {
return f.ExpectedErr
}
func (f FakeService) IsDisabled() bool {
return f.ExpectedDisabled
}
var _ accesscontrol.AccessControl = new(FakeAccessControl)
type FakeAccessControl struct {
ExpectedErr error
ExpectedDisabled bool
ExpectedEvaluate bool
}
func (f FakeAccessControl) Evaluate(ctx context.Context, user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
return f.ExpectedEvaluate, f.ExpectedErr
}
func (f FakeAccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) {
}
func (f FakeAccessControl) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error {
return f.ExpectedErr
}
func (f FakeAccessControl) IsDisabled() bool {
return f.ExpectedDisabled
}

View File

@ -10,9 +10,16 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
)
func NewAccessControlAPI(router routing.RouteRegister, service ac.Service) *AccessControlAPI {
return &AccessControlAPI{
RouteRegister: router,
Service: service,
}
}
type AccessControlAPI struct {
Service ac.Service
RouteRegister routing.RouteRegister
AccessControl ac.AccessControl
}
func (api *AccessControlAPI) RegisterAPIEndpoints() {
@ -24,7 +31,7 @@ func (api *AccessControlAPI) RegisterAPIEndpoints() {
// 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(),
permissions, err := api.Service.GetUserPermissions(c.Req.Context(),
c.SignedInUser, ac.Options{ReloadCache: reloadCache})
if err != nil {
response.JSON(http.StatusInternalServerError, err)

View File

@ -62,7 +62,7 @@ type injectTestCase struct {
desc string
expected bool
evaluator Evaluator
params ScopeParams
params scopeParams
permissions map[string][]string
}
@ -72,7 +72,7 @@ func TestPermission_Inject(t *testing.T) {
desc: "should inject field",
expected: true,
evaluator: EvalPermission("orgs:read", Scope("orgs", Field("OrgID"))),
params: ScopeParams{
params: scopeParams{
OrgID: 3,
},
permissions: map[string][]string{
@ -83,7 +83,7 @@ func TestPermission_Inject(t *testing.T) {
desc: "should inject correct param",
expected: true,
evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
params: ScopeParams{
params: scopeParams{
URLParams: map[string]string{
":id": "10",
":reportId": "1",
@ -97,7 +97,7 @@ func TestPermission_Inject(t *testing.T) {
desc: "should fail for nil params",
expected: false,
evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
params: ScopeParams{},
params: scopeParams{},
permissions: map[string][]string{
"reports:read": {"reports:1"},
},
@ -106,7 +106,7 @@ func TestPermission_Inject(t *testing.T) {
desc: "should inject several parameters to one permission",
expected: true,
evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"), Parameter(":reportId2"))),
params: ScopeParams{
params: scopeParams{
URLParams: map[string]string{
":reportId": "report",
":reportId2": "report2",
@ -120,7 +120,7 @@ func TestPermission_Inject(t *testing.T) {
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
injected, err := test.evaluator.MutateScopes(context.TODO(), ScopeInjector(test.params))
injected, err := test.evaluator.MutateScopes(context.TODO(), scopeInjector(test.params))
assert.NoError(t, err)
ok := injected.Evaluate(test.permissions)
assert.Equal(t, test.expected, ok)
@ -185,7 +185,7 @@ func TestAll_Inject(t *testing.T) {
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
EvalPermission("settings:read", Scope("settings", Parameter(":settingsId"))),
),
params: ScopeParams{
params: scopeParams{
URLParams: map[string]string{
":id": "10",
":settingsId": "3",
@ -204,7 +204,7 @@ func TestAll_Inject(t *testing.T) {
EvalPermission("orgs:read", Scope("orgs", Field("OrgID"))),
EvalPermission("orgs:read", Scope("orgs", Parameter(":orgId"))),
),
params: ScopeParams{
params: scopeParams{
OrgID: 3,
URLParams: map[string]string{
":orgId": "4",
@ -221,7 +221,7 @@ func TestAll_Inject(t *testing.T) {
EvalPermission("settings:read", Scope("reports", Parameter(":settingsId"))),
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
),
params: ScopeParams{},
params: scopeParams{},
permissions: map[string][]string{
"reports:read": {"reports:1"},
"settings:read": {"settings:3"},
@ -231,7 +231,7 @@ func TestAll_Inject(t *testing.T) {
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
injected, err := test.evaluator.MutateScopes(context.TODO(), ScopeInjector(test.params))
injected, err := test.evaluator.MutateScopes(context.TODO(), scopeInjector(test.params))
assert.NoError(t, err)
ok := injected.Evaluate(test.permissions)
assert.NoError(t, err)
@ -295,7 +295,7 @@ func TestAny_Inject(t *testing.T) {
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
EvalPermission("settings:read", Scope("settings", Parameter(":settingsId"))),
),
params: ScopeParams{
params: scopeParams{
URLParams: map[string]string{
":id": "10",
":settingsId": "3",
@ -314,7 +314,7 @@ func TestAny_Inject(t *testing.T) {
EvalPermission("orgs:read", Scope("orgs", Field("OrgID"))),
EvalPermission("orgs:read", Scope("orgs", Parameter(":orgId"))),
),
params: ScopeParams{
params: scopeParams{
OrgID: 3,
URLParams: map[string]string{
":orgId": "4",
@ -331,7 +331,7 @@ func TestAny_Inject(t *testing.T) {
EvalPermission("settings:read", Scope("reports", Parameter(":settingsId"))),
EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))),
),
params: ScopeParams{},
params: scopeParams{},
permissions: map[string][]string{
"reports:read": {"reports:1"},
"settings:read": {"settings:3"},
@ -341,7 +341,7 @@ func TestAny_Inject(t *testing.T) {
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
injected, err := test.evaluator.MutateScopes(context.TODO(), ScopeInjector(test.params))
injected, err := test.evaluator.MutateScopes(context.TODO(), scopeInjector(test.params))
assert.NoError(t, err)
ok := injected.Evaluate(test.permissions)
assert.NoError(t, err)

View File

@ -1,10 +1,12 @@
package accesscontrol
import (
"bytes"
"context"
"fmt"
"net/http"
"strconv"
"text/template"
"time"
"github.com/grafana/grafana/pkg/models"
@ -27,7 +29,7 @@ func Middleware(ac AccessControl) func(web.Handler, Evaluator) web.Handler {
}
func authorize(c *models.ReqContext, ac AccessControl, user *user.SignedInUser, evaluator Evaluator) {
injected, err := evaluator.MutateScopes(c.Req.Context(), ScopeInjector(ScopeParams{
injected, err := evaluator.MutateScopes(c.Req.Context(), scopeInjector(scopeParams{
OrgID: c.OrgID,
URLParams: web.Params(c.Req),
}))
@ -148,13 +150,13 @@ func UseGlobalOrg(c *models.ReqContext) (int64, error) {
return GlobalOrgID, nil
}
func LoadPermissionsMiddleware(ac AccessControl) web.Handler {
func LoadPermissionsMiddleware(service Service) web.Handler {
return func(c *models.ReqContext) {
if ac.IsDisabled() {
if service.IsDisabled() {
return
}
permissions, err := ac.GetUserPermissions(c.Req.Context(), c.SignedInUser,
permissions, err := service.GetUserPermissions(c.Req.Context(), c.SignedInUser,
Options{ReloadCache: false})
if err != nil {
c.JsonApiErr(http.StatusForbidden, "", err)
@ -167,3 +169,24 @@ func LoadPermissionsMiddleware(ac AccessControl) web.Handler {
c.SignedInUser.Permissions[c.OrgID] = GroupScopesByAction(permissions)
}
}
// scopeParams holds the parameters used to fill in scope templates
type scopeParams struct {
OrgID int64
URLParams map[string]string
}
// scopeInjector inject request params into the templated scopes. e.g. "settings:" + eval.Parameters(":id")
func scopeInjector(params scopeParams) ScopeAttributeMutator {
return func(_ context.Context, scope string) ([]string, error) {
tmpl, err := template.New("scope").Parse(scope)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err = tmpl.Execute(&buf, params); err != nil {
return nil, err
}
return []string{buf.String()}, nil
}
}

View File

@ -3,13 +3,14 @@ package mock
import (
"context"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/user"
)
type fullAccessControl interface {
accesscontrol.AccessControl
GetUserBuiltInRoles(user *user.SignedInUser) []string
accesscontrol.Service
RegisterFixedRoles(context.Context) error
}
@ -45,7 +46,7 @@ type Mock struct {
RegisterScopeAttributeResolverFunc func(string, accesscontrol.ScopeAttributeResolver)
DeleteUserPermissionsFunc func(context.Context, int64) error
scopeResolvers accesscontrol.ScopeResolvers
scopeResolvers accesscontrol.Resolvers
}
// Ensure the mock stays in line with the interface
@ -57,29 +58,29 @@ func New() *Mock {
disabled: false,
permissions: []accesscontrol.Permission{},
builtInRoles: []string{},
scopeResolvers: accesscontrol.NewScopeResolvers(),
scopeResolvers: accesscontrol.NewResolvers(log.NewNopLogger()),
}
return mock
}
func (m Mock) GetUsageStats(ctx context.Context) map[string]interface{} {
func (m *Mock) GetUsageStats(ctx context.Context) map[string]interface{} {
return make(map[string]interface{})
}
func (m Mock) WithPermissions(permissions []accesscontrol.Permission) *Mock {
func (m *Mock) WithPermissions(permissions []accesscontrol.Permission) *Mock {
m.permissions = permissions
return &m
return m
}
func (m Mock) WithDisabled() *Mock {
func (m *Mock) WithDisabled() *Mock {
m.disabled = true
return &m
return m
}
func (m Mock) WithBuiltInRoles(builtInRoles []string) *Mock {
func (m *Mock) WithBuiltInRoles(builtInRoles []string) *Mock {
m.builtInRoles = builtInRoles
return &m
return m
}
// Evaluate evaluates access to the given resource.
@ -148,21 +149,6 @@ func (m *Mock) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration
return nil
}
// GetUserBuiltInRoles returns the list of organizational roles ("Viewer", "Editor", "Admin")
// or "Grafana Admin" associated to a user
// This mock returns m.builtInRoles unless an override is provided.
func (m *Mock) GetUserBuiltInRoles(user *user.SignedInUser) []string {
m.Calls.GetUserBuiltInRoles = append(m.Calls.GetUserBuiltInRoles, []interface{}{user})
// Use override if provided
if m.GetUserBuiltInRolesFunc != nil {
return m.GetUserBuiltInRolesFunc(user)
}
// Otherwise return the BuiltInRoles list
return m.builtInRoles
}
// RegisterFixedRoles registers all roles declared to AccessControl
// This mock returns no error unless an override is provided.
func (m *Mock) RegisterFixedRoles(ctx context.Context) error {

View File

@ -214,12 +214,6 @@ type GetUserPermissionsQuery struct {
TeamIDs []int64
}
// ScopeParams holds the parameters used to fill in scope templates
type ScopeParams struct {
OrgID int64
URLParams map[string]string
}
// ResourcePermission is structure that holds all actions that either a team / user / builtin-role
// can perform against specific resource.
type ResourcePermission struct {

View File

@ -0,0 +1,66 @@
package ossaccesscontrol
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
var _ accesscontrol.AccessControl = new(AccessControl)
func ProvideAccessControl(cfg *setting.Cfg, service accesscontrol.Service) *AccessControl {
logger := log.New("accesscontrol")
return &AccessControl{
cfg, logger, accesscontrol.NewResolvers(logger), service,
}
}
type AccessControl struct {
cfg *setting.Cfg
log log.Logger
resolvers accesscontrol.Resolvers
service accesscontrol.Service
}
func (a *AccessControl) Evaluate(ctx context.Context, user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
timer := prometheus.NewTimer(metrics.MAccessEvaluationsSummary)
defer timer.ObserveDuration()
metrics.MAccessEvaluationCount.Inc()
if user.Permissions == nil {
user.Permissions = map[int64]map[string][]string{}
}
if _, ok := user.Permissions[user.OrgID]; !ok {
permissions, err := a.service.GetUserPermissions(ctx, user, accesscontrol.Options{ReloadCache: true})
if err != nil {
return false, err
}
user.Permissions[user.OrgID] = accesscontrol.GroupScopesByAction(permissions)
}
resolvedEvaluator, err := evaluator.MutateScopes(ctx, a.resolvers.GetScopeAttributeMutator(user.OrgID))
if err != nil {
return false, err
}
return resolvedEvaluator.Evaluate(user.Permissions[user.OrgID]), nil
}
func (a *AccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) {
a.resolvers.AddScopeAttributeResolver(prefix, resolver)
}
func (a *AccessControl) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error {
// FIXME: Remove wrapped call
return a.service.DeclareFixedRoles(registrations...)
}
func (a *AccessControl) IsDisabled() bool {
return accesscontrol.IsDisabled(a.cfg)
}

View File

@ -0,0 +1,84 @@
package ossaccesscontrol
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
)
func TestAccessControl_Evaluate(t *testing.T) {
type testCase struct {
desc string
user user.SignedInUser
evaluator accesscontrol.Evaluator
resolverPrefix string
expected bool
expectedErr error
resolver accesscontrol.ScopeAttributeResolver
}
tests := []testCase{
{
desc: "expect user to have access when correct permission is stored on user",
user: user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {accesscontrol.ActionTeamsWrite: {"teams:*"}},
},
},
evaluator: accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite, "teams:id:1"),
expected: true,
},
{
desc: "expect user to not have access without required permissions",
user: user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {accesscontrol.ActionTeamsWrite: {"teams:*"}},
},
},
evaluator: accesscontrol.EvalPermission(accesscontrol.ActionOrgUsersWrite, "users:id:1"),
expected: false,
},
{
desc: "expect user to have access when resolver translate scope",
user: user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {accesscontrol.ActionTeamsWrite: {"another:scope"}},
},
},
evaluator: accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite, "teams:id:1"),
resolverPrefix: "teams:id:",
resolver: accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
return []string{"another:scope"}, nil
}),
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
fakeService := actest.FakeService{}
ac := ProvideAccessControl(setting.NewCfg(), fakeService)
if tt.resolver != nil {
ac.RegisterScopeAttributeResolver(tt.resolverPrefix, tt.resolver)
}
hasAccess, err := ac.Evaluate(context.Background(), &tt.user, tt.evaluator)
assert.Equal(t, tt.expected, hasAccess)
if tt.expectedErr != nil {
assert.Equal(t, tt.expectedErr, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@ -1,205 +0,0 @@
package ossaccesscontrol
import (
"context"
"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/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
)
func ProvideService(cfg *setting.Cfg, store accesscontrol.PermissionsStore, routeRegister routing.RouteRegister) (*OSSAccessControlService, error) {
var errDeclareRoles error
s := ProvideOSSAccessControl(cfg, store)
if !s.IsDisabled() {
api := api.AccessControlAPI{
RouteRegister: routeRegister,
AccessControl: s,
}
api.RegisterAPIEndpoints()
errDeclareRoles = accesscontrol.DeclareFixedRoles(s)
}
return s, errDeclareRoles
}
func ProvideOSSAccessControl(cfg *setting.Cfg, store accesscontrol.PermissionsStore) *OSSAccessControlService {
s := &OSSAccessControlService{
cfg: cfg,
store: store,
log: log.New("accesscontrol"),
scopeResolvers: accesscontrol.NewScopeResolvers(),
roles: accesscontrol.BuildBasicRoleDefinitions(),
}
return s
}
// OSSAccessControlService is the service implementing role based access control.
type OSSAccessControlService struct {
log log.Logger
cfg *setting.Cfg
scopeResolvers accesscontrol.ScopeResolvers
store accesscontrol.PermissionsStore
registrations accesscontrol.RegistrationList
roles map[string]*accesscontrol.RoleDTO
}
func (ac *OSSAccessControlService) IsDisabled() bool {
if ac.cfg == nil {
return true
}
return !ac.cfg.RBACEnabled
}
func (ac *OSSAccessControlService) GetUsageStats(_ context.Context) map[string]interface{} {
return map[string]interface{}{
"stats.oss.accesscontrol.enabled.count": ac.getUsageMetrics(),
}
}
func (ac *OSSAccessControlService) getUsageMetrics() interface{} {
if ac.IsDisabled() {
return 0
}
return 1
}
// Evaluate evaluates access to the given resources
func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
timer := prometheus.NewTimer(metrics.MAccessEvaluationsSummary)
defer timer.ObserveDuration()
metrics.MAccessEvaluationCount.Inc()
if user.Permissions == nil {
user.Permissions = map[int64]map[string][]string{}
}
if _, ok := user.Permissions[user.OrgID]; !ok {
permissions, err := ac.GetUserPermissions(ctx, user, accesscontrol.Options{ReloadCache: true})
if err != nil {
return false, err
}
user.Permissions[user.OrgID] = accesscontrol.GroupScopesByAction(permissions)
}
attributeMutator := ac.scopeResolvers.GetScopeAttributeMutator(user.OrgID)
resolvedEvaluator, err := evaluator.MutateScopes(ctx, attributeMutator)
if err != nil {
return false, err
}
return resolvedEvaluator.Evaluate(user.Permissions[user.OrgID]), nil
}
var actionsToFetch = append(
TeamAdminActions, append(DashboardAdminActions, FolderAdminActions...)...,
)
// GetUserPermissions returns user permissions based on built-in roles
func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user *user.SignedInUser, _ accesscontrol.Options) ([]accesscontrol.Permission, error) {
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
defer timer.ObserveDuration()
permissions := ac.getFixedPermissions(ctx, user)
dbPermissions, err := ac.store.GetUserPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
OrgID: user.OrgID,
UserID: user.UserID,
Roles: accesscontrol.GetOrgRoles(user),
TeamIDs: user.Teams,
Actions: actionsToFetch,
})
if err != nil {
return nil, err
}
permissions = append(permissions, dbPermissions...)
keywordMutator := ac.scopeResolvers.GetScopeKeywordMutator(user)
for i := range permissions {
// if the permission has a keyword in its scope it will be resolved
permissions[i].Scope, err = keywordMutator(ctx, permissions[i].Scope)
if err != nil {
return nil, err
}
}
return permissions, nil
}
func (ac *OSSAccessControlService) getFixedPermissions(ctx context.Context, user *user.SignedInUser) []accesscontrol.Permission {
permissions := make([]accesscontrol.Permission, 0)
for _, builtin := range accesscontrol.GetOrgRoles(user) {
if basicRole, ok := ac.roles[builtin]; ok {
permissions = append(permissions, basicRole.Permissions...)
}
}
return permissions
}
// RegisterFixedRoles registers all declared roles in RAM
func (ac *OSSAccessControlService) RegisterFixedRoles(ctx context.Context) error {
// If accesscontrol is disabled no need to register roles
if ac.IsDisabled() {
return nil
}
ac.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
ac.registerFixedRole(registration.Role, registration.Grants)
return true
})
return nil
}
// RegisterFixedRole saves a fixed role and assigns it to built-in roles
func (ac *OSSAccessControlService) registerFixedRole(role accesscontrol.RoleDTO, builtInRoles []string) {
for br := range accesscontrol.BuiltInRolesWithParents(builtInRoles) {
if basicRole, ok := ac.roles[br]; ok {
basicRole.Permissions = append(basicRole.Permissions, role.Permissions...)
} else {
ac.log.Error("Unknown builtin role", "builtInRole", br)
}
}
}
// DeclareFixedRoles allow the caller to declare, to the service, fixed roles and their assignments
// to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
func (ac *OSSAccessControlService) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error {
// If accesscontrol is disabled no need to register roles
if ac.IsDisabled() {
return nil
}
for _, r := range registrations {
err := accesscontrol.ValidateFixedRole(r.Role)
if err != nil {
return err
}
err = accesscontrol.ValidateBuiltInRoles(r.Grants)
if err != nil {
return err
}
ac.registrations.Append(r)
}
return nil
}
// RegisterScopeAttributeResolver allows the caller to register scope resolvers for a
// specific scope prefix (ex: datasources:name:)
func (ac *OSSAccessControlService) RegisterScopeAttributeResolver(scopePrefix string, resolver accesscontrol.ScopeAttributeResolver) {
ac.scopeResolvers.AddScopeAttributeResolver(scopePrefix, resolver)
}
func (ac *OSSAccessControlService) DeleteUserPermissions(ctx context.Context, orgID int64, userID int64) error {
return ac.store.DeleteUserPermissions(ctx, orgID, userID)
}

View File

@ -1,495 +0,0 @@
package ossaccesscontrol
import (
"context"
"testing"
"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/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
func setupTestEnv(t testing.TB) *OSSAccessControlService {
t.Helper()
cfg := setting.NewCfg()
cfg.RBACEnabled = true
ac := &OSSAccessControlService{
cfg: cfg,
log: log.New("accesscontrol"),
registrations: accesscontrol.RegistrationList{},
scopeResolvers: accesscontrol.NewScopeResolvers(),
store: database.ProvideService(sqlstore.InitTestDB(t)),
roles: accesscontrol.BuildBasicRoleDefinitions(),
}
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
return ac
}
// extractRawPermissionsHelper extracts action and scope fields only from a permission slice
func extractRawPermissionsHelper(perms []accesscontrol.Permission) []accesscontrol.Permission {
res := make([]accesscontrol.Permission, len(perms))
for i, p := range perms {
res[i] = accesscontrol.Permission{Action: p.Action, Scope: p.Scope}
}
return res
}
type evaluatingPermissionsTestCase struct {
desc string
user userTestCase
endpoints []endpointTestCase
evalResult bool
}
type userTestCase struct {
name string
orgRole org.RoleType
isGrafanaAdmin bool
}
type endpointTestCase struct {
evaluator accesscontrol.Evaluator
}
func TestEvaluatingPermissions(t *testing.T) {
testCases := []evaluatingPermissionsTestCase{
{
desc: "should successfully evaluate access to the endpoint",
user: userTestCase{
name: "testuser",
orgRole: org.RoleViewer,
isGrafanaAdmin: true,
},
endpoints: []endpointTestCase{
{evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersDisable, accesscontrol.ScopeGlobalUsersAll)},
{evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersEnable, accesscontrol.ScopeGlobalUsersAll)},
},
evalResult: true,
},
{
desc: "should restrict access to the unauthorized endpoints",
user: userTestCase{
name: "testuser",
orgRole: org.RoleViewer,
isGrafanaAdmin: false,
},
endpoints: []endpointTestCase{
{evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersCreate, accesscontrol.ScopeGlobalUsersAll)},
},
evalResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
ac := setupTestEnv(t)
// Use OSS roles for this test to pass
err := accesscontrol.DeclareFixedRoles(ac)
require.NoError(t, err)
errRegisterRoles := ac.RegisterFixedRoles(context.Background())
require.NoError(t, errRegisterRoles)
user := &user.SignedInUser{
UserID: 1,
OrgID: 1,
Name: tc.user.name,
OrgRole: tc.user.orgRole,
IsGrafanaAdmin: tc.user.isGrafanaAdmin,
}
for _, endpoint := range tc.endpoints {
result, err := ac.Evaluate(context.Background(), user, endpoint.evaluator)
require.NoError(t, err)
assert.Equal(t, tc.evalResult, result)
}
})
}
}
func TestUsageMetrics(t *testing.T) {
tests := []struct {
name string
enabled bool
expectedValue int
}{
{
name: "Expecting metric with value 0",
enabled: false,
expectedValue: 0,
},
{
name: "Expecting metric with value 1",
enabled: true,
expectedValue: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.RBACEnabled = tt.enabled
s, errInitAc := ProvideService(
cfg,
database.ProvideService(sqlstore.InitTestDB(t)),
routing.NewRouteRegister(),
)
require.NoError(t, errInitAc)
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
})
}
}
func TestOSSAccessControlService_RegisterFixedRole(t *testing.T) {
perm := accesscontrol.Permission{Action: "test:test", Scope: "test:*"}
role := accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:test:test",
Permissions: []accesscontrol.Permission{perm},
}
builtInRoles := []string{"Editor"}
// Admin is going to get the role as well
includedBuiltInRoles := []string{"Editor", "Admin"}
// Grafana Admin and Viewer won't get the role
excludedbuiltInRoles := []string{"Viewer", "Grafana Admin"}
ac := setupTestEnv(t)
ac.registerFixedRole(role, builtInRoles)
for _, br := range includedBuiltInRoles {
builtinRole, ok := ac.roles[br]
assert.True(t, ok)
assert.Contains(t, builtinRole.Permissions, perm)
}
for _, br := range excludedbuiltInRoles {
builtinRole, ok := ac.roles[br]
assert.True(t, ok)
assert.NotContains(t, builtinRole.Permissions, perm)
}
}
func TestOSSAccessControlService_DeclareFixedRoles(t *testing.T) {
tests := []struct {
name string
registrations []accesscontrol.RoleRegistration
wantErr bool
err error
}{
{
name: "should work with empty list",
wantErr: false,
},
{
name: "should add registration",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
},
Grants: []string{"Admin"},
},
},
wantErr: false,
},
{
name: "should fail registration invalid role name",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "custom:test:test",
},
Grants: []string{"Admin"},
},
},
wantErr: true,
err: accesscontrol.ErrFixedRolePrefixMissing,
},
{
name: "should fail registration invalid builtin role assignment",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
},
Grants: []string{"WrongAdmin"},
},
},
wantErr: true,
err: accesscontrol.ErrInvalidBuiltinRole,
},
{
name: "should add multiple registrations at once",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
},
Grants: []string{"Admin"},
},
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test2:test2",
},
Grants: []string{"Admin"},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ac := setupTestEnv(t)
// Reset the registations
ac.registrations = accesscontrol.RegistrationList{}
// Test
err := ac.DeclareFixedRoles(tt.registrations...)
if tt.wantErr {
require.Error(t, err)
assert.ErrorIs(t, err, tt.err)
return
}
require.NoError(t, err)
registrationCnt := 0
ac.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
registrationCnt++
return true
})
assert.Equal(t, len(tt.registrations), registrationCnt,
"expected service registration list to contain all test registrations")
})
}
}
func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
tests := []struct {
name string
token models.Licensing
registrations []accesscontrol.RoleRegistration
wantErr bool
}{
{
name: "should work with empty list",
},
{
name: "should register and assign role",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
Permissions: []accesscontrol.Permission{{Action: "test:test"}},
},
Grants: []string{"Editor"},
},
},
wantErr: false,
},
{
name: "should register and assign multiple roles",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
Permissions: []accesscontrol.Permission{{Action: "test:test"}},
},
Grants: []string{"Editor"},
},
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test2:test2",
Permissions: []accesscontrol.Permission{
{Action: "test:test2"},
{Action: "test:test3", Scope: "test:*"},
},
},
Grants: []string{"Viewer"},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ac := setupTestEnv(t)
ac.registrations.Append(tt.registrations...)
// Test
err := ac.RegisterFixedRoles(context.Background())
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
// Check
for _, registration := range tt.registrations {
// Check builtin roles (parents included) have been granted with the permissions
for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) {
builtinRole, ok := ac.roles[br]
assert.True(t, ok)
for _, expectedPermission := range registration.Role.Permissions {
assert.Contains(t, builtinRole.Permissions, expectedPermission)
}
}
}
})
}
}
func TestOSSAccessControlService_GetUserPermissions(t *testing.T) {
testUser := user.SignedInUser{
UserID: 2,
OrgID: 3,
OrgName: "TestOrg",
OrgRole: org.RoleViewer,
Login: "testUser",
Name: "Test User",
Email: "testuser@example.org",
}
registration := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
UID: "fixed:test:test",
Name: "fixed:test:test",
Description: "Test role",
Permissions: []accesscontrol.Permission{},
},
Grants: []string{"Viewer"},
}
tests := []struct {
name string
user user.SignedInUser
rawPerm accesscontrol.Permission
wantPerm accesscontrol.Permission
wantErr bool
}{
{
name: "Translate users:self",
user: testUser,
rawPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:self"},
wantPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:id:2"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup
ac := setupTestEnv(t)
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
err := ac.DeclareFixedRoles(registration)
require.NoError(t, err)
err = ac.RegisterFixedRoles(context.Background())
require.NoError(t, err)
// Test
userPerms, err := ac.GetUserPermissions(context.Background(), &tt.user, accesscontrol.Options{})
if tt.wantErr {
assert.Error(t, err, "Expected an error with GetUserPermissions.")
return
}
require.NoError(t, err, "Did not expect an error with GetUserPermissions.")
rawUserPerms := extractRawPermissionsHelper(userPerms)
assert.Contains(t, rawUserPerms, tt.wantPerm, "Expected resolution of raw permission")
assert.NotContains(t, rawUserPerms, tt.rawPerm, "Expected raw permission to have been resolved")
})
}
}
func TestOSSAccessControlService_Evaluate(t *testing.T) {
testUser := user.SignedInUser{
UserID: 2,
OrgID: 3,
OrgName: "TestOrg",
OrgRole: org.RoleViewer,
Login: "testUser",
Name: "Test User",
Email: "testuser@example.org",
}
registration := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
UID: "fixed:test:test",
Name: "fixed:test:test",
Description: "Test role",
Permissions: []accesscontrol.Permission{},
},
Grants: []string{"Viewer"},
}
userLoginScopeSolver := accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) {
if initialScope == "users:login:testUser" {
return []string{"users:id:2"}, nil
}
return []string{initialScope}, nil
})
tests := []struct {
name string
user user.SignedInUser
rawPerm accesscontrol.Permission
evaluator accesscontrol.Evaluator
wantAccess bool
wantErr bool
}{
{
name: "Should translate users:self",
user: testUser,
rawPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:self"},
evaluator: accesscontrol.EvalPermission("users:read", "users:id:2"),
wantAccess: true,
wantErr: false,
},
{
name: "Should translate users:login:testUser",
user: testUser,
rawPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:id:2"},
evaluator: accesscontrol.EvalPermission("users:read", "users:login:testUser"),
wantAccess: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup
ac := setupTestEnv(t)
ac.RegisterScopeAttributeResolver("users:login:", userLoginScopeSolver)
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
err := ac.DeclareFixedRoles(registration)
require.NoError(t, err)
err = ac.RegisterFixedRoles(context.Background())
require.NoError(t, err)
// Test
hasAccess, err := ac.Evaluate(context.TODO(), &tt.user, tt.evaluator)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantAccess, hasAccess)
})
}
}

View File

@ -37,7 +37,7 @@ var (
func ProvideTeamPermissions(
cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore,
ac accesscontrol.AccessControl, license models.Licensing,
ac accesscontrol.AccessControl, license models.Licensing, service accesscontrol.Service,
) (*TeamPermissionsService, error) {
options := resourcepermissions.Options{
Resource: "teams",
@ -93,7 +93,7 @@ func ProvideTeamPermissions(
},
}
srv, err := resourcepermissions.New(options, cfg, router, license, ac, sql)
srv, err := resourcepermissions.New(options, cfg, router, license, ac, service, sql)
if err != nil {
return nil, err
}
@ -109,8 +109,8 @@ var DashboardEditActions = append(DashboardViewActions, []string{dashboards.Acti
var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.ActionDashboardsPermissionsRead, dashboards.ActionDashboardsPermissionsWrite}...)
func ProvideDashboardPermissions(
cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore,
ac accesscontrol.AccessControl, license models.Licensing, dashboardStore dashboards.Store,
cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, ac accesscontrol.AccessControl,
license models.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service,
) (*DashboardPermissionsService, error) {
getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*models.Dashboard, error) {
query := &models.GetDashboardQuery{Uid: resourceID, OrgId: orgID}
@ -164,7 +164,7 @@ func ProvideDashboardPermissions(
RoleGroup: "Dashboards",
}
srv, err := resourcepermissions.New(options, cfg, router, license, ac, sql)
srv, err := resourcepermissions.New(options, cfg, router, license, ac, service, sql)
if err != nil {
return nil, err
}
@ -187,8 +187,8 @@ var FolderEditActions = append(FolderViewActions, []string{
var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFoldersPermissionsRead, dashboards.ActionFoldersPermissionsWrite}...)
func ProvideFolderPermissions(
cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore,
accesscontrol accesscontrol.AccessControl, license models.Licensing, dashboardStore dashboards.Store,
cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, accesscontrol accesscontrol.AccessControl,
license models.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service,
) (*FolderPermissionsService, error) {
options := resourcepermissions.Options{
Resource: "folders",
@ -219,7 +219,7 @@ func ProvideFolderPermissions(
WriterRoleName: "Folder permission writer",
RoleGroup: "Folders",
}
srv, err := resourcepermissions.New(options, cfg, router, license, accesscontrol, sql)
srv, err := resourcepermissions.New(options, cfg, router, license, accesscontrol, service, sql)
if err != nil {
return nil, err
}
@ -263,8 +263,8 @@ type ServiceAccountPermissionsService struct {
}
func ProvideServiceAccountPermissions(
cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore,
ac accesscontrol.AccessControl, license models.Licensing, serviceAccountStore serviceaccounts.Store,
cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, ac accesscontrol.AccessControl,
license models.Licensing, serviceAccountStore serviceaccounts.Store, service accesscontrol.Service,
) (*ServiceAccountPermissionsService, error) {
options := resourcepermissions.Options{
Resource: "serviceaccounts",
@ -291,7 +291,7 @@ func ProvideServiceAccountPermissions(
RoleGroup: "Service accounts",
}
srv, err := resourcepermissions.New(options, cfg, router, license, ac, sql)
srv, err := resourcepermissions.New(options, cfg, router, license, ac, service, sql)
if err != nil {
return nil, err
}

View File

@ -0,0 +1,141 @@
package ossaccesscontrol
import (
"context"
"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/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
)
func ProvideService(cfg *setting.Cfg, store accesscontrol.Store, routeRegister routing.RouteRegister) (*Service, error) {
service := ProvideOSSService(cfg, store)
if !accesscontrol.IsDisabled(cfg) {
api.NewAccessControlAPI(routeRegister, service).RegisterAPIEndpoints()
if err := accesscontrol.DeclareFixedRoles(service); err != nil {
return nil, err
}
}
return service, nil
}
func ProvideOSSService(cfg *setting.Cfg, store accesscontrol.Store) *Service {
s := &Service{
cfg: cfg,
store: store,
log: log.New("accesscontrol.service"),
roles: accesscontrol.BuildBasicRoleDefinitions(),
}
return s
}
// Service is the service implementing role based access control.
type Service struct {
log log.Logger
cfg *setting.Cfg
store accesscontrol.Store
registrations accesscontrol.RegistrationList
roles map[string]*accesscontrol.RoleDTO
}
func (s *Service) GetUsageStats(_ context.Context) map[string]interface{} {
enabled := 0
if !accesscontrol.IsDisabled(s.cfg) {
enabled = 1
}
return map[string]interface{}{
"stats.oss.accesscontrol.enabled.count": enabled,
}
}
var actionsToFetch = append(
TeamAdminActions, append(DashboardAdminActions, FolderAdminActions...)...,
)
// GetUserPermissions returns user permissions based on built-in roles
func (s *Service) GetUserPermissions(ctx context.Context, user *user.SignedInUser, _ accesscontrol.Options) ([]accesscontrol.Permission, error) {
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
defer timer.ObserveDuration()
permissions := make([]accesscontrol.Permission, 0)
for _, builtin := range accesscontrol.GetOrgRoles(user) {
if basicRole, ok := s.roles[builtin]; ok {
permissions = append(permissions, basicRole.Permissions...)
}
}
dbPermissions, err := s.store.GetUserPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
OrgID: user.OrgID,
UserID: user.UserID,
Roles: accesscontrol.GetOrgRoles(user),
TeamIDs: user.Teams,
Actions: actionsToFetch,
})
if err != nil {
return nil, err
}
return append(permissions, dbPermissions...), nil
}
func (s *Service) DeleteUserPermissions(ctx context.Context, orgID int64, userID int64) error {
return s.store.DeleteUserPermissions(ctx, orgID, userID)
}
// DeclareFixedRoles allow the caller to declare, to the service, fixed roles and their assignments
// to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
func (s *Service) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error {
// If accesscontrol is disabled no need to register roles
if accesscontrol.IsDisabled(s.cfg) {
return nil
}
for _, r := range registrations {
err := accesscontrol.ValidateFixedRole(r.Role)
if err != nil {
return err
}
err = accesscontrol.ValidateBuiltInRoles(r.Grants)
if err != nil {
return err
}
s.registrations.Append(r)
}
return nil
}
// RegisterFixedRoles registers all declared roles in RAM
func (s *Service) RegisterFixedRoles(ctx context.Context) error {
// If accesscontrol is disabled no need to register roles
if accesscontrol.IsDisabled(s.cfg) {
return nil
}
s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) {
if basicRole, ok := s.roles[br]; ok {
basicRole.Permissions = append(basicRole.Permissions, registration.Role.Permissions...)
} else {
s.log.Error("Unknown builtin role", "builtInRole", br)
}
}
return true
})
return nil
}
func (s *Service) IsDisabled() bool {
return accesscontrol.IsDisabled(s.cfg)
}

View File

@ -0,0 +1,239 @@
package ossaccesscontrol
import (
"context"
"testing"
"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/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
func setupTestEnv(t testing.TB) *Service {
t.Helper()
cfg := setting.NewCfg()
cfg.RBACEnabled = true
ac := &Service{
cfg: cfg,
log: log.New("accesscontrol"),
registrations: accesscontrol.RegistrationList{},
store: database.ProvideService(sqlstore.InitTestDB(t)),
roles: accesscontrol.BuildBasicRoleDefinitions(),
}
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
return ac
}
func TestUsageMetrics(t *testing.T) {
tests := []struct {
name string
enabled bool
expectedValue int
}{
{
name: "Expecting metric with value 0",
enabled: false,
expectedValue: 0,
},
{
name: "Expecting metric with value 1",
enabled: true,
expectedValue: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.RBACEnabled = tt.enabled
s, errInitAc := ProvideService(
cfg,
database.ProvideService(sqlstore.InitTestDB(t)),
routing.NewRouteRegister(),
)
require.NoError(t, errInitAc)
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
})
}
}
func TestService_DeclareFixedRoles(t *testing.T) {
tests := []struct {
name string
registrations []accesscontrol.RoleRegistration
wantErr bool
err error
}{
{
name: "should work with empty list",
wantErr: false,
},
{
name: "should add registration",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
},
Grants: []string{"Admin"},
},
},
wantErr: false,
},
{
name: "should fail registration invalid role name",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "custom:test:test",
},
Grants: []string{"Admin"},
},
},
wantErr: true,
err: accesscontrol.ErrFixedRolePrefixMissing,
},
{
name: "should fail registration invalid builtin role assignment",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
},
Grants: []string{"WrongAdmin"},
},
},
wantErr: true,
err: accesscontrol.ErrInvalidBuiltinRole,
},
{
name: "should add multiple registrations at once",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
},
Grants: []string{"Admin"},
},
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test2:test2",
},
Grants: []string{"Admin"},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ac := setupTestEnv(t)
// Reset the registations
ac.registrations = accesscontrol.RegistrationList{}
// Test
err := ac.DeclareFixedRoles(tt.registrations...)
if tt.wantErr {
require.Error(t, err)
assert.ErrorIs(t, err, tt.err)
return
}
require.NoError(t, err)
registrationCnt := 0
ac.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
registrationCnt++
return true
})
assert.Equal(t, len(tt.registrations), registrationCnt,
"expected service registration list to contain all test registrations")
})
}
}
func TestService_RegisterFixedRoles(t *testing.T) {
tests := []struct {
name string
token models.Licensing
registrations []accesscontrol.RoleRegistration
wantErr bool
}{
{
name: "should work with empty list",
},
{
name: "should register and assign role",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
Permissions: []accesscontrol.Permission{{Action: "test:test"}},
},
Grants: []string{"Editor"},
},
},
wantErr: false,
},
{
name: "should register and assign multiple roles",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
Permissions: []accesscontrol.Permission{{Action: "test:test"}},
},
Grants: []string{"Editor"},
},
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test2:test2",
Permissions: []accesscontrol.Permission{
{Action: "test:test2"},
{Action: "test:test3", Scope: "test:*"},
},
},
Grants: []string{"Viewer"},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ac := setupTestEnv(t)
ac.registrations.Append(tt.registrations...)
// Test
err := ac.RegisterFixedRoles(context.Background())
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
// Check
for _, registration := range tt.registrations {
// Check builtin roles (parents included) have been granted with the permissions
for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) {
builtinRole, ok := ac.roles[br]
assert.True(t, ok)
for _, expectedPermission := range registration.Role.Permissions {
assert.Contains(t, builtinRole.Permissions, expectedPermission)
}
}
}
})
}
}

View File

@ -1,41 +1,54 @@
package accesscontrol
import (
"bytes"
"context"
"fmt"
"text/template"
"time"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/user"
)
// ScopeAttributeResolver is used to resolve attributes in scopes to one or more scopes that are
// evaluated by logical or. E.g. "dashboards:id:1" -> "dashboards:uid:test-dashboard" or "folder:uid:test-folder"
type ScopeAttributeResolver interface {
Resolve(ctx context.Context, orgID int64, scope string) ([]string, error)
}
// ScopeAttributeResolverFunc is an adapter to allow functions to implement ScopeAttributeResolver interface
type ScopeAttributeResolverFunc func(ctx context.Context, orgID int64, scope string) ([]string, error)
func (f ScopeAttributeResolverFunc) Resolve(ctx context.Context, orgID int64, scope string) ([]string, error) {
return f(ctx, orgID, scope)
}
type ScopeAttributeMutator func(context.Context, string) ([]string, error)
const (
ttl = 30 * time.Second
cleanInterval = 2 * time.Minute
)
func NewScopeResolvers() ScopeResolvers {
return ScopeResolvers{
keywordResolvers: map[string]ScopeKeywordResolver{
"users:self": userSelfResolver,
},
attributeResolvers: map[string]ScopeAttributeResolver{},
func NewResolvers(log log.Logger) Resolvers {
return Resolvers{
log: log,
cache: localcache.New(ttl, cleanInterval),
log: log.New("accesscontrol.resolver"),
attributeResolvers: map[string]ScopeAttributeResolver{},
}
}
type ScopeResolvers struct {
type Resolvers struct {
log log.Logger
cache *localcache.CacheService
keywordResolvers map[string]ScopeKeywordResolver
attributeResolvers map[string]ScopeAttributeResolver
}
func (s *ScopeResolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMutator {
func (s *Resolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) {
s.log.Debug("adding scope attribute resolver for '%v'", prefix)
s.attributeResolvers[prefix] = resolver
}
func (s *Resolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMutator {
return func(ctx context.Context, scope string) ([]string, error) {
key := getScopeCacheKey(orgID, scope)
// Check cache before computing the scope
@ -60,81 +73,7 @@ func (s *ScopeResolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMut
}
}
func (s *ScopeResolvers) GetScopeKeywordMutator(user *user.SignedInUser) ScopeKeywordMutator {
return func(ctx context.Context, scope string) (string, error) {
if resolver, ok := s.keywordResolvers[scope]; ok {
scopes, err := resolver.Resolve(ctx, user)
if err != nil {
return "", fmt.Errorf("could not resolve %v: %w", scope, err)
}
s.log.Debug("resolved scope", "scope", scope, "resolved_scopes", scopes)
return scopes, nil
}
// By default, the scope remains unchanged
return scope, nil
}
}
func (s *ScopeResolvers) AddScopeKeywordResolver(keyword string, resolver ScopeKeywordResolver) {
s.log.Debug("adding scope keyword resolver for '%v'", keyword)
s.keywordResolvers[keyword] = resolver
}
func (s *ScopeResolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) {
s.log.Debug("adding scope attribute resolver for '%v'", prefix)
s.attributeResolvers[prefix] = resolver
}
// ScopeAttributeResolver is used to resolve attributes in scopes to one or more scopes that are
// evaluated by logical or. E.g. "dashboards:id:1" -> "dashboards:uid:test-dashboard" or "folder:uid:test-folder"
type ScopeAttributeResolver interface {
Resolve(ctx context.Context, orgID int64, scope string) ([]string, error)
}
// ScopeAttributeResolverFunc is an adapter to allow functions to implement ScopeAttributeResolver interface
type ScopeAttributeResolverFunc func(ctx context.Context, orgID int64, scope string) ([]string, error)
func (f ScopeAttributeResolverFunc) Resolve(ctx context.Context, orgID int64, scope string) ([]string, error) {
return f(ctx, orgID, scope)
}
type ScopeAttributeMutator func(context.Context, string) ([]string, error)
// ScopeKeywordResolver is used to resolve keywords in scopes e.g. "users:self" -> "user:id:1".
// These type of resolvers is used when fetching stored permissions
type ScopeKeywordResolver interface {
Resolve(ctx context.Context, user *user.SignedInUser) (string, error)
}
// ScopeKeywordResolverFunc is an adapter to allow functions to implement ScopeKeywordResolver interface
type ScopeKeywordResolverFunc func(ctx context.Context, user *user.SignedInUser) (string, error)
func (f ScopeKeywordResolverFunc) Resolve(ctx context.Context, user *user.SignedInUser) (string, error) {
return f(ctx, user)
}
type ScopeKeywordMutator func(context.Context, string) (string, error)
// getScopeCacheKey creates an identifier to fetch and store resolution of scopes in the cache
func getScopeCacheKey(orgID int64, scope string) string {
return fmt.Sprintf("%s-%v", scope, orgID)
}
//ScopeInjector inject request params into the templated scopes. e.g. "settings:" + eval.Parameters(":id")
func ScopeInjector(params ScopeParams) ScopeAttributeMutator {
return func(_ context.Context, scope string) ([]string, error) {
tmpl, err := template.New("scope").Parse(scope)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err = tmpl.Execute(&buf, params); err != nil {
return nil, err
}
return []string{buf.String()}, nil
}
}
var userSelfResolver = ScopeKeywordResolverFunc(func(ctx context.Context, user *user.SignedInUser) (string, error) {
return Scope("users", "id", fmt.Sprintf("%v", user.UserID)), nil
})

View File

@ -4,64 +4,13 @@ import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/assert"
)
func TestResolveKeywordScope(t *testing.T) {
tests := []struct {
name string
user *user.SignedInUser
permission accesscontrol.Permission
want accesscontrol.Permission
wantErr bool
}{
{
name: "no scope",
user: testUser,
permission: accesscontrol.Permission{Action: "users:read"},
want: accesscontrol.Permission{Action: "users:read"},
wantErr: false,
},
{
name: "user if resolution",
user: testUser,
permission: accesscontrol.Permission{Action: "users:read", Scope: "users:self"},
want: accesscontrol.Permission{Action: "users:read", Scope: "users:id:2"},
wantErr: false,
},
}
for _, tt := range tests {
var err error
t.Run(tt.name, func(t *testing.T) {
resolvers := accesscontrol.NewScopeResolvers()
scopeModifier := resolvers.GetScopeKeywordMutator(tt.user)
tt.permission.Scope, err = scopeModifier(context.TODO(), tt.permission.Scope)
if tt.wantErr {
assert.Error(t, err, "expected an error during the resolution of the scope")
return
}
assert.NoError(t, err)
assert.EqualValues(t, tt.want, tt.permission, "permission did not match expected resolution")
})
}
}
var testUser = &user.SignedInUser{
UserID: 2,
OrgID: 3,
OrgName: "TestOrg",
OrgRole: org.RoleViewer,
Login: "testUser",
Name: "Test User",
Email: "testuser@example.org",
}
func TestResolveAttributeScope(t *testing.T) {
func TestResolvers_AttributeScope(t *testing.T) {
// Calls allow us to see how many times the fakeDataSourceResolution has been called
calls := 0
fakeDataSourceResolver := accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) {
@ -142,7 +91,7 @@ func TestResolveAttributeScope(t *testing.T) {
},
}
for _, tt := range tests {
resolvers := accesscontrol.NewScopeResolvers()
resolvers := accesscontrol.NewResolvers(log.NewNopLogger())
// Reset calls counter
calls = 0

View File

@ -48,8 +48,8 @@ type Store interface {
}
func New(
options Options, cfg *setting.Cfg, router routing.RouteRegister,
license models.Licensing, ac accesscontrol.AccessControl, sqlStore *sqlstore.SQLStore,
options Options, cfg *setting.Cfg, router routing.RouteRegister, license models.Licensing,
ac accesscontrol.AccessControl, service accesscontrol.Service, sqlStore *sqlstore.SQLStore,
) (*Service, error) {
var permissions []string
actionSet := make(map[string]struct{})
@ -79,6 +79,7 @@ func New(
permissions: permissions,
actions: actions,
sqlStore: sqlStore,
service: service,
}
s.api = newApi(ac, router, s)
@ -96,6 +97,7 @@ func New(
type Service struct {
cfg *setting.Cfg
ac accesscontrol.AccessControl
service accesscontrol.Service
store Store
api *api
license models.Licensing
@ -334,5 +336,5 @@ func (s *Service) declareFixedRoles() error {
Grants: []string{string(org.RoleAdmin)},
}
return s.ac.DeclareFixedRoles(readerRole, writerRole)
return s.service.DeclareFixedRoles(readerRole, writerRole)
}

View File

@ -222,9 +222,10 @@ func setupTestEnvironment(t *testing.T, permissions []accesscontrol.Permission,
cfg := setting.NewCfg()
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
mock := accesscontrolmock.New().WithPermissions(permissions)
service, err := New(
ops, cfg, routing.NewRouteRegister(), license,
accesscontrolmock.New().WithPermissions(permissions), sql,
accesscontrolmock.New().WithPermissions(permissions), mock, sql,
)
require.NoError(t, err)

View File

@ -170,7 +170,7 @@ var (
)
// Declare OSS roles to the accesscontrol service
func DeclareFixedRoles(ac AccessControl) error {
func DeclareFixedRoles(service Service) error {
ldapReader := RoleRegistration{
Role: ldapReaderRole,
Grants: []string{RoleGrafanaAdmin},
@ -204,7 +204,7 @@ func DeclareFixedRoles(ac AccessControl) error {
Grants: []string{RoleGrafanaAdmin},
}
return ac.DeclareFixedRoles(ldapReader, ldapWriter, orgUsersReader, orgUsersWriter,
return service.DeclareFixedRoles(ldapReader, ldapWriter, orgUsersReader, orgUsersWriter,
settingsReader, statsReader, usersReader, usersWriter)
}

View File

@ -602,10 +602,10 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{})
setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, ac)
require.NoError(t, err)
dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions(
setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{})
setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, ac)
require.NoError(t, err)
if dashboardSvc == nil {
dashboardSvc = &dashboards.FakeDashboardService{}

View File

@ -22,7 +22,7 @@ func ProvideService(
userService user.Service,
quotaService quota.Service,
authInfoService login.AuthInfoService,
accessControl accesscontrol.AccessControl,
accessControl accesscontrol.Service,
) *Implementation {
s := &Implementation{
SQLStore: sqlStore,
@ -40,7 +40,7 @@ type Implementation struct {
AuthInfoService login.AuthInfoService
QuotaService quota.Service
TeamSync login.TeamSyncFunc
accessControl accesscontrol.AccessControl
accessControl accesscontrol.Service
}
// CreateUser creates inserts a new one.

View File

@ -62,8 +62,9 @@ func setupTestServer(
}
var err error
ac, err := ossaccesscontrol.ProvideService(cfg, database.ProvideService(db), rr)
acService, err := ossaccesscontrol.ProvideService(cfg, database.ProvideService(db), rr)
require.NoError(t, err)
ac := ossaccesscontrol.ProvideAccessControl(cfg, acService)
// build mux
m := web.New()

View File

@ -23,7 +23,7 @@ var _ FutureAuthService = (*simpleSQLAuthService)(nil)
type simpleSQLAuthService struct {
sql *sqlstore.SQLStore
ac accesscontrol.AccessControl
ac accesscontrol.Service
}
type dashIdQueryResult struct {

View File

@ -56,7 +56,7 @@ type StandardSearchService struct {
cfg *setting.Cfg
sql *sqlstore.SQLStore
auth FutureAuthService // eventually injected from elsewhere
ac accesscontrol.AccessControl
ac accesscontrol.Service
logger log.Logger
dashboardIndex *searchIndex
@ -64,7 +64,7 @@ type StandardSearchService struct {
reIndexCh chan struct{}
}
func ProvideService(cfg *setting.Cfg, sql *sqlstore.SQLStore, entityEventStore store.EntityEventsService, ac accesscontrol.AccessControl) SearchService {
func ProvideService(cfg *setting.Cfg, sql *sqlstore.SQLStore, entityEventStore store.EntityEventsService, ac accesscontrol.Service) SearchService {
extender := &NoopExtender{}
s := &StandardSearchService{
cfg: cfg,

View File

@ -278,7 +278,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock,
acmock *accesscontrolmock.Mock,
sqlStore *sqlstore.SQLStore, saStore serviceaccounts.Store) (*web.Mux, *ServiceAccountsAPI) {
cfg := setting.NewCfg()
saPermissionService, err := ossaccesscontrol.ProvideServiceAccountPermissions(cfg, routing.NewRouteRegister(), sqlStore, acmock, &licensing.OSSLicensingService{}, saStore)
saPermissionService, err := ossaccesscontrol.ProvideServiceAccountPermissions(cfg, routing.NewRouteRegister(), sqlStore, acmock, &licensing.OSSLicensingService{}, saStore, acmock)
require.NoError(t, err)
a := NewServiceAccountsAPI(cfg, svc, acmock, routerRegister, saStore, saPermissionService)

View File

@ -32,7 +32,7 @@ type Service struct {
teamMemberService teamguardian.TeamGuardian
userAuthService userauth.Service
quotaService quota.Service
accessControlStore accesscontrol.AccessControl
accessControlStore accesscontrol.Service
// TODO remove sqlstore
sqlStore *sqlstore.SQLStore
@ -48,7 +48,7 @@ func ProvideService(
teamMemberService teamguardian.TeamGuardian,
userAuthService userauth.Service,
quotaService quota.Service,
accessControlStore accesscontrol.AccessControl,
accessControlStore accesscontrol.Service,
cfg *setting.Cfg,
ss *sqlstore.SQLStore,
) user.Service {