RBAC: Allow role registration for plugins (#57387)

* Picking role registration from OnCall POC branch

* Fix test

* Remove include actions from this PR

* Removing unused permission

* Adding test to DeclarePluginRoles

* Add testcase to RegisterFixed role

* Additional test case

* Adding tests to validate plugins roles

* Add test to plugin loader

* Nit.

* Scuemata validation

* Changing the design to decouple accesscontrol from plugin management

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>

* Fixing tests

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Add missing files

Co-authored-by: Jguer <joao.guerreiro@grafana.com>

* Remove feature toggle check from loader

* Remove feature toggleimport

* Feedback

Co-Authored-By: marefr <marcus.efraimsson@gmail.com>

* Fix test'

* Make plugins.RoleRegistry interface typed

* Remove comment question

* No need for json tags anymore

* Nit. log

* Adding the schema validation

* Remove group to take plugin Name instead

* Revert sqlstore -> db

* Nit.

* Nit. on tests

Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>

* Update pkg/services/accesscontrol/plugins.go

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Log message

Co-Authored-By: marefr <marcus.efraimsson@gmail.com>

* Log message

Co-Authored-By: marefr <marcus.efraimsson@gmail.com>

* Remove unecessary method. Update test name.

Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>

* Fix linting

* Update cue descriptions

* Fix test

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>
Co-authored-by: Jguer <joao.guerreiro@grafana.com>
Co-authored-by: marefr <marcus.efraimsson@gmail.com>
Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
This commit is contained in:
Gabriel MABILLE
2022-11-07 11:30:45 +01:00
committed by GitHub
co-authored by Kalle Persson Jguer marefr ievaVasiljeva
parent 334b498632
commit 30fae33f66
22 changed files with 852 additions and 32 deletions
+46 -8
View File
@@ -12,20 +12,26 @@ import (
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
var _ plugins.RoleRegistry = &Service{}
const (
cacheTTL = 10 * time.Second
)
func ProvideService(cfg *setting.Cfg, store db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService) (*Service, error) {
service := ProvideOSSService(cfg, database.ProvideService(store), cache)
func ProvideService(cfg *setting.Cfg, store db.DB, routeRegister routing.RouteRegister, cache *localcache.CacheService,
features *featuremgmt.FeatureManager) (*Service, error) {
service := ProvideOSSService(cfg, database.ProvideService(store), cache, features)
if !accesscontrol.IsDisabled(cfg) {
api.NewAccessControlAPI(routeRegister, service).RegisterAPIEndpoints()
@@ -37,13 +43,14 @@ func ProvideService(cfg *setting.Cfg, store db.DB, routeRegister routing.RouteRe
return service, nil
}
func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheService) *Service {
func ProvideOSSService(cfg *setting.Cfg, store store, cache *localcache.CacheService, features *featuremgmt.FeatureManager) *Service {
s := &Service{
cfg: cfg,
store: store,
log: log.New("accesscontrol.service"),
cache: cache,
roles: accesscontrol.BuildBasicRoleDefinitions(),
cfg: cfg,
store: store,
log: log.New("accesscontrol.service"),
cache: cache,
roles: accesscontrol.BuildBasicRoleDefinitions(),
features: features,
}
return s
@@ -62,6 +69,7 @@ type Service struct {
cache *localcache.CacheService
registrations accesscontrol.RegistrationList
roles map[string]*accesscontrol.RoleDTO
features *featuremgmt.FeatureManager
}
func (s *Service) GetUsageStats(_ context.Context) map[string]interface{} {
@@ -198,3 +206,33 @@ func permissionCacheKey(user *user.SignedInUser) (string, error) {
}
return fmt.Sprintf("rbac-permissions-%s", key), nil
}
// DeclarePluginRoles allow the caller to declare, to the service, plugin roles and their assignments
// to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
func (s *Service) DeclarePluginRoles(_ context.Context, ID, name string, regs []plugins.RoleRegistration) error {
// If accesscontrol is disabled no need to register roles
if accesscontrol.IsDisabled(s.cfg) {
return nil
}
// Protect behind feature toggle
if !s.features.IsEnabled(featuremgmt.FlagAccessControlOnCall) {
return nil
}
acRegs := pluginutils.ToRegistrations(name, regs)
for _, r := range acRegs {
if err := pluginutils.ValidatePluginRole(ID, r.Role); err != nil {
return err
}
if err := accesscontrol.ValidateBuiltInRoles(r.Grants); err != nil {
return err
}
s.log.Debug("Registering plugin role", "role", r.Role.Name)
s.registrations.Append(r)
}
return nil
}
+159 -16
View File
@@ -12,8 +12,10 @@ import (
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
@@ -29,6 +31,7 @@ func setupTestEnv(t testing.TB) *Service {
registrations: accesscontrol.RegistrationList{},
store: database.ProvideService(db.InitTestDB(t)),
roles: accesscontrol.BuildBasicRoleDefinitions(),
features: featuremgmt.WithFeatures(),
}
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
return ac
@@ -62,6 +65,7 @@ func TestUsageMetrics(t *testing.T) {
db.InitTestDB(t),
routing.NewRouteRegister(),
localcache.ProvideService(),
featuremgmt.WithFeatures(),
)
require.NoError(t, errInitAc)
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
@@ -84,9 +88,7 @@ func TestService_DeclareFixedRoles(t *testing.T) {
name: "should add registration",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
},
Role: accesscontrol.RoleDTO{Name: "fixed:test:test"},
Grants: []string{"Admin"},
},
},
@@ -96,9 +98,7 @@ func TestService_DeclareFixedRoles(t *testing.T) {
name: "should fail registration invalid role name",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "custom:test:test",
},
Role: accesscontrol.RoleDTO{Name: "custom:test:test"},
Grants: []string{"Admin"},
},
},
@@ -106,12 +106,10 @@ func TestService_DeclareFixedRoles(t *testing.T) {
err: accesscontrol.ErrFixedRolePrefixMissing,
},
{
name: "should fail registration invalid builtin role assignment",
name: "should fail registration invalid basic role assignment",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
},
Role: accesscontrol.RoleDTO{Name: "fixed:test:test"},
Grants: []string{"WrongAdmin"},
},
},
@@ -122,15 +120,11 @@ func TestService_DeclareFixedRoles(t *testing.T) {
name: "should add multiple registrations at once",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test:test",
},
Role: accesscontrol.RoleDTO{Name: "fixed:test:test"},
Grants: []string{"Admin"},
},
{
Role: accesscontrol.RoleDTO{
Name: "fixed:test2:test2",
},
Role: accesscontrol.RoleDTO{Name: "fixed:test2:test2"},
Grants: []string{"Admin"},
},
},
@@ -164,6 +158,132 @@ func TestService_DeclareFixedRoles(t *testing.T) {
}
}
func TestService_DeclarePluginRoles(t *testing.T) {
tests := []struct {
name string
pluginID string
registrations []plugins.RoleRegistration
wantErr bool
err error
}{
{
name: "should work with empty list",
wantErr: false,
},
{
name: "should add registration",
pluginID: "test-app",
registrations: []plugins.RoleRegistration{
{
Role: plugins.Role{Name: "plugins:test-app:test"},
Grants: []string{"Admin"},
},
},
wantErr: false,
},
{
name: "should fail registration invalid role name",
pluginID: "test-app",
registrations: []plugins.RoleRegistration{
{
Role: plugins.Role{Name: "invalid.plugins:test-app:test"},
Grants: []string{"Admin"},
},
},
wantErr: true,
err: &accesscontrol.ErrorInvalidRole{},
},
{
name: "should add registration with valid permissions",
pluginID: "test-app",
registrations: []plugins.RoleRegistration{
{
Role: plugins.Role{
Name: "plugins:test-app:test",
Permissions: []plugins.Permission{
{Action: "plugins.app:access"},
{Action: "test-app:read"},
{Action: "test-app.resource:read"},
},
},
Grants: []string{"Admin"},
},
},
wantErr: false,
},
{
name: "should fail registration invalid permission action",
pluginID: "test-app",
registrations: []plugins.RoleRegistration{
{
Role: plugins.Role{
Name: "plugins:test-app:test",
Permissions: []plugins.Permission{
{Action: "invalid.test-app.resource:read"},
},
},
Grants: []string{"Admin"},
},
},
wantErr: true,
err: &accesscontrol.ErrorInvalidRole{},
},
{
name: "should fail registration invalid basic role assignment",
pluginID: "test-app",
registrations: []plugins.RoleRegistration{
{
Role: plugins.Role{Name: "plugins:test-app:test"},
Grants: []string{"WrongAdmin"},
},
},
wantErr: true,
err: accesscontrol.ErrInvalidBuiltinRole,
},
{
name: "should add multiple registrations at once",
pluginID: "test-app",
registrations: []plugins.RoleRegistration{
{
Role: plugins.Role{Name: "plugins:test-app:test"},
Grants: []string{"Admin"},
},
{
Role: plugins.Role{Name: "plugins:test-app:test2"},
Grants: []string{"Admin"},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ac := setupTestEnv(t)
ac.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
// Reset the registations
ac.registrations = accesscontrol.RegistrationList{}
// Test
err := ac.DeclarePluginRoles(context.Background(), tt.pluginID, tt.pluginID, 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
@@ -210,6 +330,29 @@ func TestService_RegisterFixedRoles(t *testing.T) {
},
wantErr: false,
},
{
name: "should register and assign fixed and plugins roles",
registrations: []accesscontrol.RoleRegistration{
{
Role: accesscontrol.RoleDTO{
Name: "plugins:test-app:test",
Permissions: []accesscontrol.Permission{{Action: "test-app: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 {