mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
RBCA: Better separation between action set svc and store (#91491)
better separation between action set svc and store
This commit is contained in:
parent
5bae9f11bc
commit
6e7bc028d0
@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
@ -12,7 +13,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
@ -23,7 +26,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var _ pluginaccesscontrol.ActionSetRegistry = (*InMemoryActionSets)(nil)
|
||||
var _ pluginaccesscontrol.ActionSetRegistry = (ActionSetService)(nil)
|
||||
|
||||
type Store interface {
|
||||
// SetUserResourcePermission sets permission for managed user role on a resource
|
||||
@ -437,7 +440,7 @@ type ActionSetService interface {
|
||||
ResolveAction(action string) []string
|
||||
// ResolveActionSet resolves an action set to a list of corresponding actions.
|
||||
ResolveActionSet(actionSet string) []string
|
||||
|
||||
// StoreActionSet stores action set. If a set with the given name has already been stored, the new actions will be appended to the existing actions.
|
||||
StoreActionSet(name string, actions []string)
|
||||
|
||||
pluginaccesscontrol.ActionSetRegistry
|
||||
@ -450,21 +453,126 @@ type ActionSet struct {
|
||||
Actions []string `json:"actions"`
|
||||
}
|
||||
|
||||
// InMemoryActionSets is an in-memory implementation of the ActionSetService.
|
||||
type InMemoryActionSets struct {
|
||||
features featuremgmt.FeatureToggles
|
||||
log log.Logger
|
||||
actionSetToActions map[string][]string
|
||||
actionToActionSets map[string][]string
|
||||
type ActionSetStore interface {
|
||||
// StoreActionSet stores action set. If a set with the given name has already been stored, the new actions will be appended to the existing actions.
|
||||
StoreActionSet(name string, actions []string)
|
||||
// ResolveActionSet resolves an action set to a list of corresponding actions.
|
||||
ResolveActionSet(actionSet string) []string
|
||||
// ResolveAction returns all the action sets that the action belongs to.
|
||||
ResolveAction(action string) []string
|
||||
// ResolveActionPrefix returns all action sets that include at least one action with the specified prefix
|
||||
ResolveActionPrefix(prefix string) []string
|
||||
// ExpandActionSetsWithFilter takes a set of permissions that might include some action set permissions, and returns a set of permissions with action sets expanded into underlying permissions.
|
||||
// When action sets are expanded into the underlying permissions only those permissions whose action is matched by actionMatcher are included.
|
||||
ExpandActionSetsWithFilter(permissions []accesscontrol.Permission, actionMatcher func(action string) bool) []accesscontrol.Permission
|
||||
}
|
||||
|
||||
type ActionSetSvc struct {
|
||||
features featuremgmt.FeatureToggles
|
||||
store ActionSetStore
|
||||
}
|
||||
|
||||
// NewActionSetService returns a new instance of InMemoryActionSetService.
|
||||
func NewActionSetService(features featuremgmt.FeatureToggles) ActionSetService {
|
||||
actionSets := &InMemoryActionSets{
|
||||
features: features,
|
||||
log: log.New("resourcepermissions.actionsets"),
|
||||
actionSetToActions: make(map[string][]string),
|
||||
actionToActionSets: make(map[string][]string),
|
||||
return &ActionSetSvc{
|
||||
features: features,
|
||||
store: NewInMemoryActionSetStore(features),
|
||||
}
|
||||
return actionSets
|
||||
}
|
||||
|
||||
// ResolveAction returns all the action sets that the action belongs to.
|
||||
func (a *ActionSetSvc) ResolveAction(action string) []string {
|
||||
sets := a.store.ResolveAction(action)
|
||||
filteredSets := make([]string, 0, len(sets))
|
||||
for _, set := range sets {
|
||||
// Only use action sets for folders and dashboards for now
|
||||
// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
|
||||
if !isFolderOrDashboardAction(set) {
|
||||
continue
|
||||
}
|
||||
filteredSets = append(filteredSets, set)
|
||||
}
|
||||
|
||||
return filteredSets
|
||||
}
|
||||
|
||||
// ResolveActionPrefix returns all action sets that include at least one action with the specified prefix
|
||||
func (a *ActionSetSvc) ResolveActionPrefix(actionPrefix string) []string {
|
||||
sets := a.store.ResolveActionPrefix(actionPrefix)
|
||||
filteredSets := make([]string, 0, len(sets))
|
||||
for _, set := range sets {
|
||||
// Only use action sets for folders and dashboards for now
|
||||
// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
|
||||
if !isFolderOrDashboardAction(set) {
|
||||
continue
|
||||
}
|
||||
filteredSets = append(filteredSets, set)
|
||||
}
|
||||
|
||||
return filteredSets
|
||||
}
|
||||
|
||||
// ResolveActionSet resolves an action set to a list of corresponding actions.
|
||||
func (a *ActionSetSvc) ResolveActionSet(actionSet string) []string {
|
||||
// Only use action sets for folders and dashboards for now
|
||||
// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
|
||||
if !isFolderOrDashboardAction(actionSet) {
|
||||
return nil
|
||||
}
|
||||
return a.store.ResolveActionSet(actionSet)
|
||||
}
|
||||
|
||||
// StoreActionSet stores action set. If a set with the given name has already been stored, the new actions will be appended to the existing actions.
|
||||
func (a *ActionSetSvc) StoreActionSet(name string, actions []string) {
|
||||
// To avoid backwards incompatible changes, we don't want to store these actions in the DB
|
||||
// Once action sets are fully enabled, we can include dashboards.ActionFoldersCreate in the list of other folder edit/admin actions
|
||||
// Tracked in https://github.com/grafana/identity-access-team/issues/794
|
||||
if name == "folders:edit" || name == "folders:admin" {
|
||||
if !slices.Contains(a.ResolveActionSet(name), dashboards.ActionFoldersCreate) {
|
||||
actions = append(actions, dashboards.ActionFoldersCreate)
|
||||
}
|
||||
}
|
||||
|
||||
a.store.StoreActionSet(name, actions)
|
||||
}
|
||||
|
||||
// ExpandActionSets takes a set of permissions that might include some action set permissions, and returns a set of permissions with action sets expanded into underlying permissions
|
||||
func (a *ActionSetSvc) ExpandActionSets(permissions []accesscontrol.Permission) []accesscontrol.Permission {
|
||||
actionMatcher := func(_ string) bool {
|
||||
return true
|
||||
}
|
||||
return a.ExpandActionSetsWithFilter(permissions, actionMatcher)
|
||||
}
|
||||
|
||||
// ExpandActionSetsWithFilter works like ExpandActionSets, but it also takes a function for action filtering. When action sets are expanded into the underlying permissions,
|
||||
// only those permissions whose action is matched by actionMatcher are included.
|
||||
func (a *ActionSetSvc) ExpandActionSetsWithFilter(permissions []accesscontrol.Permission, actionMatcher func(action string) bool) []accesscontrol.Permission {
|
||||
return a.store.ExpandActionSetsWithFilter(permissions, actionMatcher)
|
||||
}
|
||||
|
||||
// RegisterActionSets allow the caller to expand the existing action sets with additional permissions
|
||||
// This is intended to be used by plugins, and currently supports extending folder and dashboard action sets
|
||||
func (a *ActionSetSvc) RegisterActionSets(ctx context.Context, pluginID string, registrations []plugins.ActionSet) error {
|
||||
if !a.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) || !a.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) {
|
||||
return nil
|
||||
}
|
||||
for _, reg := range registrations {
|
||||
if err := pluginutils.ValidatePluginActionSet(pluginID, reg); err != nil {
|
||||
return err
|
||||
}
|
||||
a.StoreActionSet(reg.Action, reg.Actions)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isFolderOrDashboardAction(action string) bool {
|
||||
return strings.HasPrefix(action, dashboards.ScopeDashboardsRoot) || strings.HasPrefix(action, dashboards.ScopeFoldersRoot)
|
||||
}
|
||||
|
||||
// GetActionSetName function creates an action set from a list of actions and stores it inmemory.
|
||||
func GetActionSetName(resource, permission string) string {
|
||||
// lower cased
|
||||
resource = strings.ToLower(resource)
|
||||
permission = strings.ToLower(permission)
|
||||
return fmt.Sprintf("%s:%s", resource, permission)
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
@ -314,6 +315,177 @@ func TestService_RegisterActionSets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_RegisterActionSet(t *testing.T) {
|
||||
type actionSetTest struct {
|
||||
desc string
|
||||
features featuremgmt.FeatureToggles
|
||||
pluginID string
|
||||
pluginActions []plugins.ActionSet
|
||||
coreActionSets []ActionSet
|
||||
expectedErr bool
|
||||
expectedActionSets []ActionSet
|
||||
}
|
||||
|
||||
tests := []actionSetTest{
|
||||
{
|
||||
desc: "should be able to register a plugin action set if the right feature toggles are enabled",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
},
|
||||
expectedActionSets: []ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should not register plugin action set if feature toggles are missing",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
},
|
||||
expectedActionSets: []ActionSet{},
|
||||
},
|
||||
{
|
||||
desc: "should be able to register multiple plugin action sets",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"test-app.resource:write", "test-app.resource:delete"},
|
||||
},
|
||||
},
|
||||
expectedActionSets: []ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"test-app.resource:write", "test-app.resource:delete"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "action set actions should be added not replaced",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"test-app.resource:write", "test-app.resource:delete"},
|
||||
},
|
||||
},
|
||||
coreActionSets: []ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"folders:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"folders:write", "folders:delete"},
|
||||
},
|
||||
{
|
||||
Action: "folders:admin",
|
||||
Actions: []string{"folders.permissions:read"},
|
||||
},
|
||||
},
|
||||
expectedActionSets: []ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"folders:read", "test-app.resource:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"folders:write", "test-app.resource:write", "folders:delete", "test-app.resource:delete"},
|
||||
},
|
||||
{
|
||||
Action: "folders:admin",
|
||||
Actions: []string{"folders.permissions:read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should not be able to register an action that doesn't have a plugin prefix",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"users:read", "test-app.resource:delete"},
|
||||
},
|
||||
},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to register action set that is not in the allow list",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:super-admin",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
},
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
asService := NewActionSetService(tt.features)
|
||||
|
||||
err := asService.RegisterActionSets(context.Background(), tt.pluginID, tt.pluginActions)
|
||||
if tt.expectedErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, set := range tt.coreActionSets {
|
||||
asService.StoreActionSet(set.Action, set.Actions)
|
||||
}
|
||||
|
||||
for _, expected := range tt.expectedActionSets {
|
||||
actions := asService.ResolveActionSet(expected.Action)
|
||||
if expected.Action == "folders:edit" || expected.Action == "folders:admin" {
|
||||
expected.Actions = append(expected.Actions, "folders:create")
|
||||
}
|
||||
assert.ElementsMatch(t, expected.Actions, actions)
|
||||
}
|
||||
|
||||
if len(tt.expectedActionSets) == 0 {
|
||||
for _, set := range tt.pluginActions {
|
||||
registeredActions := asService.ResolveActionSet(set.Action)
|
||||
assert.Empty(t, registeredActions, "no actions from plugin action sets should have been registered")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestEnvironment(t *testing.T, ops Options) (*Service, user.Service, team.Service) {
|
||||
t.Helper()
|
||||
|
||||
|
@ -3,15 +3,12 @@ package resourcepermissions
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
@ -744,6 +741,23 @@ func managedPermission(action, resource string, resourceID, resourceAttribute st
|
||||
}
|
||||
}
|
||||
|
||||
// InMemoryActionSets is an in-memory implementation of the ActionSetStore.
|
||||
type InMemoryActionSets struct {
|
||||
features featuremgmt.FeatureToggles
|
||||
log log.Logger
|
||||
actionSetToActions map[string][]string
|
||||
actionToActionSets map[string][]string
|
||||
}
|
||||
|
||||
func NewInMemoryActionSetStore(features featuremgmt.FeatureToggles) *InMemoryActionSets {
|
||||
return &InMemoryActionSets{
|
||||
actionSetToActions: make(map[string][]string),
|
||||
actionToActionSets: make(map[string][]string),
|
||||
log: log.New("resourcepermissions.actionsets"),
|
||||
features: features,
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveActionPrefix returns all action sets that include at least one action with the specified prefix
|
||||
func (s *InMemoryActionSets) ResolveActionPrefix(prefix string) []string {
|
||||
if prefix == "" {
|
||||
@ -753,11 +767,6 @@ func (s *InMemoryActionSets) ResolveActionPrefix(prefix string) []string {
|
||||
sets := make([]string, 0, len(s.actionSetToActions))
|
||||
|
||||
for set, actions := range s.actionSetToActions {
|
||||
// Only use action sets for folders and dashboards for now
|
||||
// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
|
||||
if !isFolderOrDashboardAction(set) {
|
||||
continue
|
||||
}
|
||||
for _, action := range actions {
|
||||
if strings.HasPrefix(action, prefix) {
|
||||
sets = append(sets, set)
|
||||
@ -770,44 +779,13 @@ func (s *InMemoryActionSets) ResolveActionPrefix(prefix string) []string {
|
||||
}
|
||||
|
||||
func (s *InMemoryActionSets) ResolveAction(action string) []string {
|
||||
actionSets := s.actionToActionSets[action]
|
||||
sets := make([]string, 0, len(actionSets))
|
||||
|
||||
for _, actionSet := range actionSets {
|
||||
// Only use action sets for folders and dashboards for now
|
||||
// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
|
||||
if !isFolderOrDashboardAction(actionSet) {
|
||||
continue
|
||||
}
|
||||
sets = append(sets, actionSet)
|
||||
}
|
||||
|
||||
return sets
|
||||
return s.actionToActionSets[action]
|
||||
}
|
||||
|
||||
func (s *InMemoryActionSets) ResolveActionSet(actionSet string) []string {
|
||||
// Only use action sets for folders and dashboards for now
|
||||
// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
|
||||
if !isFolderOrDashboardAction(actionSet) {
|
||||
return nil
|
||||
}
|
||||
return s.actionSetToActions[actionSet]
|
||||
}
|
||||
|
||||
func isFolderOrDashboardAction(action string) bool {
|
||||
return strings.HasPrefix(action, dashboards.ScopeDashboardsRoot) || strings.HasPrefix(action, dashboards.ScopeFoldersRoot)
|
||||
}
|
||||
|
||||
// ExpandActionSets takes a set of permissions that might include some action set permissions, and returns a set of permissions with action sets expanded into underlying permissions
|
||||
func (s *InMemoryActionSets) ExpandActionSets(permissions []accesscontrol.Permission) []accesscontrol.Permission {
|
||||
actionMatcher := func(_ string) bool {
|
||||
return true
|
||||
}
|
||||
return s.ExpandActionSetsWithFilter(permissions, actionMatcher)
|
||||
}
|
||||
|
||||
// ExpandActionSetsWithFilter works like ExpandActionSets, but it also takes a function for action filtering. When action sets are expanded into the underlying permissions,
|
||||
// only those permissions whose action is matched by actionMatcher are included.
|
||||
func (s *InMemoryActionSets) ExpandActionSetsWithFilter(permissions []accesscontrol.Permission, actionMatcher func(action string) bool) []accesscontrol.Permission {
|
||||
var expandedPermissions []accesscontrol.Permission
|
||||
for _, permission := range permissions {
|
||||
@ -828,15 +806,6 @@ func (s *InMemoryActionSets) ExpandActionSetsWithFilter(permissions []accesscont
|
||||
}
|
||||
|
||||
func (s *InMemoryActionSets) StoreActionSet(name string, actions []string) {
|
||||
// To avoid backwards incompatible changes, we don't want to store these actions in the DB
|
||||
// Once action sets are fully enabled, we can include dashboards.ActionFoldersCreate in the list of other folder edit/admin actions
|
||||
// Tracked in https://github.com/grafana/identity-access-team/issues/794
|
||||
if name == "folders:edit" || name == "folders:admin" {
|
||||
if !slices.Contains(s.actionSetToActions[name], dashboards.ActionFoldersCreate) {
|
||||
actions = append(actions, dashboards.ActionFoldersCreate)
|
||||
}
|
||||
}
|
||||
|
||||
s.actionSetToActions[name] = append(s.actionSetToActions[name], actions...)
|
||||
|
||||
for _, action := range actions {
|
||||
@ -847,26 +816,3 @@ func (s *InMemoryActionSets) StoreActionSet(name string, actions []string) {
|
||||
}
|
||||
s.log.Debug("stored action set", "action set name", name)
|
||||
}
|
||||
|
||||
// RegisterActionSets allow the caller to expand the existing action sets with additional permissions
|
||||
// This is intended to be used by plugins, and currently supports extending folder and dashboard action sets
|
||||
func (s *InMemoryActionSets) RegisterActionSets(ctx context.Context, pluginID string, registrations []plugins.ActionSet) error {
|
||||
if !s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) || !s.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) {
|
||||
return nil
|
||||
}
|
||||
for _, reg := range registrations {
|
||||
if err := pluginutils.ValidatePluginActionSet(pluginID, reg); err != nil {
|
||||
return err
|
||||
}
|
||||
s.StoreActionSet(reg.Action, reg.Actions)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActionSetName function creates an action set from a list of actions and stores it inmemory.
|
||||
func GetActionSetName(resource, permission string) string {
|
||||
// lower cased
|
||||
resource = strings.ToLower(resource)
|
||||
permission = strings.ToLower(permission)
|
||||
return fmt.Sprintf("%s:%s", resource, permission)
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@ -782,183 +781,12 @@ func TestStore_StoreActionSet(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
asService := NewActionSetService(featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets))
|
||||
asService := NewInMemoryActionSetStore(featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets))
|
||||
asService.StoreActionSet(GetActionSetName(tt.resource, tt.action), tt.actions)
|
||||
|
||||
actionSetName := GetActionSetName(tt.resource, tt.action)
|
||||
actionSet := asService.ResolveActionSet(actionSetName)
|
||||
require.Equal(t, append(tt.actions, "folders:create"), actionSet)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_RegisterActionSet(t *testing.T) {
|
||||
type actionSetTest struct {
|
||||
desc string
|
||||
features featuremgmt.FeatureToggles
|
||||
pluginID string
|
||||
pluginActions []plugins.ActionSet
|
||||
coreActionSets []ActionSet
|
||||
expectedErr bool
|
||||
expectedActionSets []ActionSet
|
||||
}
|
||||
|
||||
tests := []actionSetTest{
|
||||
{
|
||||
desc: "should be able to register a plugin action set if the right feature toggles are enabled",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
},
|
||||
expectedActionSets: []ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should not register plugin action set if feature toggles are missing",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
},
|
||||
expectedActionSets: []ActionSet{},
|
||||
},
|
||||
{
|
||||
desc: "should be able to register multiple plugin action sets",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"test-app.resource:write", "test-app.resource:delete"},
|
||||
},
|
||||
},
|
||||
expectedActionSets: []ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"test-app.resource:write", "test-app.resource:delete"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "action set actions should be added not replaced",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"test-app.resource:write", "test-app.resource:delete"},
|
||||
},
|
||||
},
|
||||
coreActionSets: []ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"folders:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"folders:write", "folders:delete"},
|
||||
},
|
||||
{
|
||||
Action: "folders:admin",
|
||||
Actions: []string{"folders.permissions:read"},
|
||||
},
|
||||
},
|
||||
expectedActionSets: []ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"folders:read", "test-app.resource:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"folders:write", "test-app.resource:write", "folders:delete", "test-app.resource:delete"},
|
||||
},
|
||||
{
|
||||
Action: "folders:admin",
|
||||
Actions: []string{"folders.permissions:read"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should not be able to register an action that doesn't have a plugin prefix",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:view",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
{
|
||||
Action: "folders:edit",
|
||||
Actions: []string{"users:read", "test-app.resource:delete"},
|
||||
},
|
||||
},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
desc: "should not be able to register action set that is not in the allow list",
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall),
|
||||
pluginID: "test-app",
|
||||
pluginActions: []plugins.ActionSet{
|
||||
{
|
||||
Action: "folders:super-admin",
|
||||
Actions: []string{"test-app.resource:read"},
|
||||
},
|
||||
},
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
asService := NewActionSetService(tt.features)
|
||||
|
||||
err := asService.RegisterActionSets(context.Background(), tt.pluginID, tt.pluginActions)
|
||||
if tt.expectedErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, set := range tt.coreActionSets {
|
||||
asService.StoreActionSet(set.Action, set.Actions)
|
||||
}
|
||||
|
||||
for _, expected := range tt.expectedActionSets {
|
||||
actions := asService.ResolveActionSet(expected.Action)
|
||||
if expected.Action == "folders:edit" || expected.Action == "folders:admin" {
|
||||
expected.Actions = append(expected.Actions, "folders:create")
|
||||
}
|
||||
assert.ElementsMatch(t, expected.Actions, actions)
|
||||
}
|
||||
|
||||
if len(tt.expectedActionSets) == 0 {
|
||||
for _, set := range tt.pluginActions {
|
||||
registeredActions := asService.ResolveActionSet(set.Action)
|
||||
assert.Empty(t, registeredActions, "no actions from plugin action sets should have been registered")
|
||||
}
|
||||
}
|
||||
require.Equal(t, tt.actions, actionSet)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user