mirror of https://github.com/grafana/grafana
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>pull/58132/head
parent
334b498632
commit
30fae33f66
@ -0,0 +1,31 @@ |
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE----- |
||||
Hash: SHA512 |
||||
|
||||
{ |
||||
"manifestVersion": "2.0.0", |
||||
"signatureType": "private", |
||||
"signedByOrg": "gabrielmabille", |
||||
"signedByOrgName": "gabrielmabille", |
||||
"rootUrls": [ |
||||
"http://localhost:3000/" |
||||
], |
||||
"plugin": "test-app", |
||||
"version": "1.0.0", |
||||
"time": 1666953431573, |
||||
"keyId": "7e4d0c6a708866e7", |
||||
"files": { |
||||
"plugin.json": "8017d19868809409e54e70eab116366de263005aa70960d44a12dc4dc5582cee" |
||||
} |
||||
} |
||||
-----BEGIN PGP SIGNATURE----- |
||||
Version: OpenPGP.js v4.10.10 |
||||
Comment: https://openpgpjs.org |
||||
|
||||
wrgEARMKAAYFAmNbsNcAIQkQfk0ManCIZucWIQTzOyW2kQdOhGNlcPN+TQxq |
||||
cIhm5z2+AgYqtKZ4tU/VBo8kOI49LfV85JKunAxPOvfaU3pRseRnWSyRBS0X |
||||
pKI2ekKebOSRZIs+zDPA0qTl1ihOY9bKe52pwwIJAf1IDq1P7G861dFilTuF |
||||
jCHQq6aS3NGy5o1N480Xof8PZdrI/xYDqSoy2F+688FR76ShyAM4B00Skt7c |
||||
9YSCsLx+ |
||||
=cVti |
||||
-----END PGP SIGNATURE----- |
||||
@ -0,0 +1,45 @@ |
||||
{ |
||||
"type": "app", |
||||
"name": "Test App", |
||||
"id": "test-app", |
||||
"info": { |
||||
"description": "Test App", |
||||
"author": { |
||||
"name": "Test Inc.", |
||||
"url": "http://test.com" |
||||
}, |
||||
"keywords": ["test"], |
||||
"links": [], |
||||
"version": "1.0.0", |
||||
"updated": "2015-02-10" |
||||
}, |
||||
"includes": [], |
||||
"roles": [ |
||||
{ |
||||
"role": { |
||||
"name": "plugins.app:test-app:reader", |
||||
"displayName": "test-app reader", |
||||
"description": "View everything in the test-app plugin", |
||||
"permissions": [ |
||||
{ |
||||
"action": "plugins.app:access", |
||||
"scope": "plugins.app:id:test-app" |
||||
}, |
||||
{ |
||||
"action": "test-app.resource:read", |
||||
"scope": "resources:*" |
||||
}, |
||||
{ |
||||
"action": "test-app.otherresource:toggle" |
||||
} |
||||
] |
||||
}, |
||||
"grants": [ |
||||
"Admin" |
||||
] |
||||
} |
||||
], |
||||
"dependencies": { |
||||
"grafanaDependency": ">=8.0.0" |
||||
} |
||||
} |
||||
@ -1,10 +1,46 @@ |
||||
package accesscontrol |
||||
|
||||
import "errors" |
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
) |
||||
|
||||
var ( |
||||
ErrFixedRolePrefixMissing = errors.New("fixed role should be prefixed with '" + FixedRolePrefix + "'") |
||||
ErrInvalidBuiltinRole = errors.New("built-in role is not valid") |
||||
ErrInvalidScope = errors.New("invalid scope") |
||||
ErrResolverNotFound = errors.New("no resolver found") |
||||
ErrPluginIDRequired = errors.New("plugin ID is required") |
||||
) |
||||
|
||||
type ErrorInvalidRole struct{} |
||||
|
||||
func (e *ErrorInvalidRole) Error() string { |
||||
return "role is invalid" |
||||
} |
||||
|
||||
type ErrorRolePrefixMissing struct { |
||||
Role string |
||||
Prefixes []string |
||||
} |
||||
|
||||
func (e *ErrorRolePrefixMissing) Error() string { |
||||
return fmt.Sprintf("expected role '%s' to be prefixed with any of '%v'", e.Role, e.Prefixes) |
||||
} |
||||
|
||||
func (e *ErrorRolePrefixMissing) Unwrap() error { |
||||
return &ErrorInvalidRole{} |
||||
} |
||||
|
||||
type ErrorActionPrefixMissing struct { |
||||
Action string |
||||
Prefixes []string |
||||
} |
||||
|
||||
func (e *ErrorActionPrefixMissing) Error() string { |
||||
return fmt.Sprintf("expected action '%s' to be prefixed with any of '%v'", e.Action, e.Prefixes) |
||||
} |
||||
|
||||
func (e *ErrorActionPrefixMissing) Unwrap() error { |
||||
return &ErrorInvalidRole{} |
||||
} |
||||
|
||||
@ -0,0 +1,62 @@ |
||||
package pluginutils |
||||
|
||||
import ( |
||||
"strings" |
||||
|
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
) |
||||
|
||||
// ValidatePluginPermissions errors when a permission does not match expected pattern for plugins
|
||||
func ValidatePluginPermissions(pluginID string, permissions []ac.Permission) error { |
||||
for i := range permissions { |
||||
if permissions[i].Action != plugins.ActionAppAccess && |
||||
!strings.HasPrefix(permissions[i].Action, pluginID+":") && |
||||
!strings.HasPrefix(permissions[i].Action, pluginID+".") { |
||||
return &ac.ErrorActionPrefixMissing{Action: permissions[i].Action, |
||||
Prefixes: []string{plugins.ActionAppAccess, pluginID + ":", pluginID + "."}} |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// ValidatePluginRole errors when a plugin role does not match expected pattern
|
||||
// or doesn't have permissions matching the expected pattern.
|
||||
func ValidatePluginRole(pluginID string, role ac.RoleDTO) error { |
||||
if pluginID == "" { |
||||
return ac.ErrPluginIDRequired |
||||
} |
||||
if !strings.HasPrefix(role.Name, ac.PluginRolePrefix+pluginID+":") { |
||||
return &ac.ErrorRolePrefixMissing{Role: role.Name, Prefixes: []string{ac.PluginRolePrefix + pluginID + ":"}} |
||||
} |
||||
|
||||
return ValidatePluginPermissions(pluginID, role.Permissions) |
||||
} |
||||
|
||||
func ToRegistrations(pluginName string, regs []plugins.RoleRegistration) []ac.RoleRegistration { |
||||
res := make([]ac.RoleRegistration, 0, len(regs)) |
||||
for i := range regs { |
||||
res = append(res, ac.RoleRegistration{ |
||||
Role: ac.RoleDTO{ |
||||
Version: 1, |
||||
Name: regs[i].Role.Name, |
||||
DisplayName: regs[i].Role.DisplayName, |
||||
Description: regs[i].Role.Description, |
||||
Group: pluginName, |
||||
Permissions: toPermissions(regs[i].Role.Permissions), |
||||
OrgID: ac.GlobalOrgID, |
||||
}, |
||||
Grants: regs[i].Grants, |
||||
}) |
||||
} |
||||
return res |
||||
} |
||||
|
||||
func toPermissions(perms []plugins.Permission) []ac.Permission { |
||||
res := make([]ac.Permission, 0, len(perms)) |
||||
for i := range perms { |
||||
res = append(res, ac.Permission{Action: perms[i].Action, Scope: perms[i].Scope}) |
||||
} |
||||
return res |
||||
} |
||||
@ -0,0 +1,142 @@ |
||||
package pluginutils |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestToRegistrations(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
regs []plugins.RoleRegistration |
||||
want []ac.RoleRegistration |
||||
}{ |
||||
{ |
||||
name: "no registration", |
||||
regs: nil, |
||||
want: []ac.RoleRegistration{}, |
||||
}, |
||||
{ |
||||
name: "registration gets converted successfully", |
||||
regs: []plugins.RoleRegistration{ |
||||
{ |
||||
Role: plugins.Role{ |
||||
Name: "test:name", |
||||
DisplayName: "Test", |
||||
Description: "Test", |
||||
Permissions: []plugins.Permission{ |
||||
{Action: "test:action"}, |
||||
{Action: "test:action", Scope: "test:scope"}, |
||||
}, |
||||
}, |
||||
Grants: []string{"Admin", "Editor"}, |
||||
}, |
||||
{ |
||||
Role: plugins.Role{ |
||||
Name: "test:name", |
||||
Permissions: []plugins.Permission{}, |
||||
}, |
||||
}, |
||||
}, |
||||
want: []ac.RoleRegistration{ |
||||
{ |
||||
Role: ac.RoleDTO{ |
||||
Version: 1, |
||||
Name: "test:name", |
||||
DisplayName: "Test", |
||||
Description: "Test", |
||||
Group: "PluginName", |
||||
Permissions: []ac.Permission{ |
||||
{Action: "test:action"}, |
||||
{Action: "test:action", Scope: "test:scope"}, |
||||
}, |
||||
OrgID: ac.GlobalOrgID, |
||||
}, |
||||
Grants: []string{"Admin", "Editor"}, |
||||
}, |
||||
{ |
||||
Role: ac.RoleDTO{ |
||||
Version: 1, |
||||
Name: "test:name", |
||||
Group: "PluginName", |
||||
Permissions: []ac.Permission{}, |
||||
OrgID: ac.GlobalOrgID, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
got := ToRegistrations("PluginName", tt.regs) |
||||
require.Equal(t, tt.want, got) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestValidatePluginRole(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
pluginID string |
||||
role ac.RoleDTO |
||||
wantErr error |
||||
}{ |
||||
{ |
||||
name: "empty", |
||||
pluginID: "", |
||||
role: ac.RoleDTO{Name: "plugins::"}, |
||||
wantErr: ac.ErrPluginIDRequired, |
||||
}, |
||||
{ |
||||
name: "invalid name", |
||||
pluginID: "test-app", |
||||
role: ac.RoleDTO{Name: "test-app:reader"}, |
||||
wantErr: &ac.ErrorInvalidRole{}, |
||||
}, |
||||
{ |
||||
name: "invalid id in name", |
||||
pluginID: "test-app", |
||||
role: ac.RoleDTO{Name: "plugins:test-app2:reader"}, |
||||
wantErr: &ac.ErrorInvalidRole{}, |
||||
}, |
||||
{ |
||||
name: "valid name", |
||||
pluginID: "test-app", |
||||
role: ac.RoleDTO{Name: "plugins:test-app:reader"}, |
||||
}, |
||||
{ |
||||
name: "invalid permission", |
||||
pluginID: "test-app", |
||||
role: ac.RoleDTO{ |
||||
Name: "plugins:test-app:reader", |
||||
Permissions: []ac.Permission{{Action: "invalidtest-app:read"}}, |
||||
}, |
||||
wantErr: &ac.ErrorInvalidRole{}, |
||||
}, |
||||
{ |
||||
name: "valid permissions", |
||||
pluginID: "test-app", |
||||
role: ac.RoleDTO{ |
||||
Name: "plugins:test-app:reader", |
||||
Permissions: []ac.Permission{ |
||||
{Action: "plugins.app:access"}, |
||||
{Action: "test-app:read"}, |
||||
{Action: "test-app.resources:read"}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
err := ValidatePluginRole(tt.pluginID, tt.role) |
||||
if tt.wantErr != nil { |
||||
require.ErrorIs(t, err, tt.wantErr) |
||||
return |
||||
} |
||||
require.NoError(t, err) |
||||
}) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue