mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 19:00:54 -06:00
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:
parent
211c9991c5
commit
55c7b8add2
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
|
||||
func ProvideUsageStatsProvidersRegistry(
|
||||
thumbsService thumbs.Service,
|
||||
accesscontrol accesscontrol.AccessControl,
|
||||
accesscontrol accesscontrol.Service,
|
||||
) *UsageStatsProvidersRegistry {
|
||||
return NewUsageStatsProvidersRegistry(
|
||||
thumbsService,
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
64
pkg/services/accesscontrol/actest/fake.go
Normal file
64
pkg/services/accesscontrol/actest/fake.go
Normal 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
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
66
pkg/services/accesscontrol/ossaccesscontrol/accesscontrol.go
Normal file
66
pkg/services/accesscontrol/ossaccesscontrol/accesscontrol.go
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
141
pkg/services/accesscontrol/ossaccesscontrol/service.go
Normal file
141
pkg/services/accesscontrol/ossaccesscontrol/service.go
Normal 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)
|
||||
}
|
239
pkg/services/accesscontrol/ossaccesscontrol/service_test.go
Normal file
239
pkg/services/accesscontrol/ossaccesscontrol/service_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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{}
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -23,7 +23,7 @@ var _ FutureAuthService = (*simpleSQLAuthService)(nil)
|
||||
|
||||
type simpleSQLAuthService struct {
|
||||
sql *sqlstore.SQLStore
|
||||
ac accesscontrol.AccessControl
|
||||
ac accesscontrol.Service
|
||||
}
|
||||
|
||||
type dashIdQueryResult struct {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user