RBAC: Adding action set resolver for RBAC evaluation (#86801)

* add action set resolver

* rename variables

* some fixes and some tests

* more tests

* more tests, and put action set storing behind a feature toggle

* undo change from cfg to feature mgmt - will cover it in a separate PR due to the amount of test changes

* fix dependency cycle, update some tests

* add one more test

* fix for feature toggle check not being set on test configs

* linting fixes

* check that action set name can be split nicely

* clean up tests by turning GetActionSetNames into a function

* undo accidental change

* test fix

* more test fixes
This commit is contained in:
Ieva 2024-05-09 10:18:03 +01:00 committed by GitHub
parent 6380a01543
commit 105313f5c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 445 additions and 101 deletions

View File

@ -461,11 +461,12 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, sc.db, features, supportbundlestest.NewFakeBundleService(), nil)
cfg := setting.NewCfg()
actionSets := resourcepermissions.NewActionSetService(ac)
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, resourcepermissions.NewActionSetService())
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets)
require.NoError(b, err)
dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions(
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, resourcepermissions.NewActionSetService())
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets)
require.NoError(b, err)
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
@ -48,6 +49,11 @@ func (a *AccessControl) Evaluate(ctx context.Context, user identity.Requester, e
return false, nil
}
// TODO update this to use featuremgmt.FeatureToggles instead of checking the config
if a.cfg != nil && a.cfg.IsFeatureToggleEnabled != nil && a.cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccessActionSets) {
evaluator = evaluator.AppendActionSets(ctx, a.resolvers.GetActionSetResolver())
}
a.debug(ctx, user, "Evaluating permissions", evaluator)
// Test evaluation without scope resolver first, this will prevent 403 for wildcard scopes when resource does not exist
if evaluator.Evaluate(permissions) {
@ -70,6 +76,10 @@ func (a *AccessControl) RegisterScopeAttributeResolver(prefix string, resolver a
a.resolvers.AddScopeAttributeResolver(prefix, resolver)
}
func (a *AccessControl) RegisterActionResolver(resolver accesscontrol.ActionResolver) {
a.resolvers.SetActionResolver(resolver)
}
func (a *AccessControl) debug(ctx context.Context, ident identity.Requester, msg string, eval accesscontrol.Evaluator) {
namespace, id := ident.GetNamespacedID()
a.log.FromContext(ctx).Debug(msg, "namespace", namespace, "id", id, "orgID", ident.GetOrgID(), "permissions", eval.GoString())

View File

@ -1,12 +1,17 @@
package acimpl
package acimpl_test
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
@ -19,7 +24,8 @@ func TestAccessControl_Evaluate(t *testing.T) {
resolverPrefix string
expected bool
expectedErr error
resolver accesscontrol.ScopeAttributeResolver
scopeResolver accesscontrol.ScopeAttributeResolver
actionSets map[string][]string
}
tests := []testCase{
@ -55,19 +61,127 @@ func TestAccessControl_Evaluate(t *testing.T) {
},
evaluator: accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite, "teams:id:1"),
resolverPrefix: "teams:id:",
resolver: accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
scopeResolver: accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
return []string{"another:scope"}, nil
}),
expected: true,
},
{
desc: "expect user to have access when resolver translates actions to action sets",
user: user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"folders:edit": {"folders:uid:test_folder"}},
},
},
evaluator: accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, "folders:uid:test_folder"),
actionSets: map[string][]string{
"folders:edit": {dashboards.ActionFoldersWrite, dashboards.ActionFoldersRead, dashboards.ActionDashboardsWrite, dashboards.ActionDashboardsRead},
},
expected: true,
},
{
desc: "expect user to have access when resolver translates scopes, as well as expands actions to action sets",
user: user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"folders:edit": {"folders:uid:test_folder"}},
},
},
evaluator: accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, "dashboards:uid:test_dashboard"),
actionSets: map[string][]string{
"folders:edit": {dashboards.ActionFoldersWrite, dashboards.ActionFoldersRead, dashboards.ActionDashboardsWrite, dashboards.ActionDashboardsRead},
},
resolverPrefix: "dashboards:uid:",
scopeResolver: accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
return []string{"folders:uid:test_folder"}, nil
}),
expected: true,
},
{
desc: "expect user to have access with eval all evaluator when resolver translates scopes, as well as expands actions to action sets",
user: user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"folders:edit": {"folders:uid:test_folder"}},
},
},
evaluator: accesscontrol.EvalAll(
accesscontrol.EvalPermission(dashboards.ActionFoldersRead, "folders:uid:test_folder"),
accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, "dashboards:uid:test_dashboard"),
),
actionSets: map[string][]string{
"folders:edit": {dashboards.ActionFoldersWrite, dashboards.ActionFoldersRead, dashboards.ActionDashboardsWrite, dashboards.ActionDashboardsRead},
},
resolverPrefix: "dashboards:uid:",
scopeResolver: accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
return []string{"folders:uid:test_folder"}, nil
}),
expected: true,
},
{
desc: "expect user to not have access with eval all evaluator with resolvers when not all permissions resolve to permissions that the user has",
user: user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"folders:edit": {"folders:uid:test_folder"}},
},
},
evaluator: accesscontrol.EvalAll(
accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, "datasources:uid:test_ds"),
accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, "dashboards:uid:test_dashboard"),
),
actionSets: map[string][]string{
"folders:edit": {dashboards.ActionFoldersWrite, dashboards.ActionFoldersRead, dashboards.ActionDashboardsWrite, dashboards.ActionDashboardsRead},
},
resolverPrefix: "dashboards:uid:",
scopeResolver: accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
return []string{"folders:uid:test_folder"}, nil
}),
expected: false,
},
{
desc: "expect user to have access with eval any evaluator when resolver translates scopes, as well as expands actions to action sets",
user: user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"folders:edit": {"folders:uid:test_folder"}},
},
},
evaluator: accesscontrol.EvalAny(
accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, "dashboards:uid:test_dashboard"),
accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, "dashboards:uid:test_dashboard"),
),
actionSets: map[string][]string{
"folders:edit": {dashboards.ActionFoldersWrite, dashboards.ActionFoldersRead, dashboards.ActionDashboardsWrite, dashboards.ActionDashboardsRead},
},
resolverPrefix: "dashboards:uid:",
scopeResolver: accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
return []string{"folders:uid:test_folder"}, nil
}),
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
ac := ProvideAccessControl(setting.NewCfg())
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = func(ft string) bool {
return ft == featuremgmt.FlagAccessActionSets
}
ac := acimpl.ProvideAccessControl(cfg)
if tt.resolver != nil {
ac.RegisterScopeAttributeResolver(tt.resolverPrefix, tt.resolver)
if tt.scopeResolver != nil {
ac.RegisterScopeAttributeResolver(tt.resolverPrefix, tt.scopeResolver)
}
if tt.actionSets != nil {
actionSetResolver := resourcepermissions.NewActionSetService(ac)
for actionSet, actions := range tt.actionSets {
splitActionSet := strings.Split(actionSet, ":")
actionSetResolver.StoreActionSet(splitActionSet[0], splitActionSet[1], actions)
}
ac.RegisterActionResolver(actionSetResolver)
}
hasAccess, err := ac.Evaluate(context.Background(), &tt.user, tt.evaluator)

View File

@ -1,4 +1,4 @@
package database
package database_test
import (
"context"
@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
rs "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
@ -20,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/team/teamimpl"
@ -91,9 +93,9 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
store, permissionStore, usrSvc, teamSvc, _ := setupTestEnv(t)
store, permissionStore, usrSvc, teamSvc, _, sql := setupTestEnv(t)
user, team := createUserAndTeam(t, store.sql, usrSvc, teamSvc, tt.orgID)
user, team := createUserAndTeam(t, sql, usrSvc, teamSvc, tt.orgID)
for _, id := range tt.userPermissions {
_, err := permissionStore.SetUserResourcePermission(context.Background(), tt.orgID, accesscontrol.User{ID: user.ID}, rs.SetResourcePermissionCommand{
@ -148,7 +150,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) {
require.NoError(t, err)
assert.Len(t, permissions, tt.expected)
policies, err := GetAccessPolicies(context.Background(), user.OrgID, store.sql.GetSqlxSession(),
policies, err := database.GetAccessPolicies(context.Background(), user.OrgID, sql.GetSqlxSession(),
func(ctx context.Context, orgID int64, scope string) ([]string, error) {
return strings.Split(scope, ":"), nil
})
@ -195,7 +197,7 @@ func TestAccessControlStore_GetTeamsPermissions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
store, permissionStore, _, teamSvc, _ := setupTestEnv(t)
store, permissionStore, _, teamSvc, _, _ := setupTestEnv(t)
teams := make([]team.Team, 0)
for i := 0; i < len(tt.teamsPermissions); i++ {
@ -240,8 +242,8 @@ func TestAccessControlStore_GetTeamsPermissions(t *testing.T) {
func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
t.Run("expect permissions in all orgs to be deleted", func(t *testing.T) {
store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t)
user, _ := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1)
store, permissionsStore, usrSvc, teamSvc, _, sql := setupTestEnv(t)
user, _ := createUserAndTeam(t, sql, usrSvc, teamSvc, 1)
// generate permissions in org 1
_, err := permissionsStore.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: user.ID}, rs.SetResourcePermissionCommand{
@ -280,8 +282,8 @@ func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
})
t.Run("expect permissions in org 1 to be deleted", func(t *testing.T) {
store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t)
user, _ := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1)
store, permissionsStore, usrSvc, teamSvc, _, sql := setupTestEnv(t)
user, _ := createUserAndTeam(t, sql, usrSvc, teamSvc, 1)
// generate permissions in org 1
_, err := permissionsStore.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: user.ID}, rs.SetResourcePermissionCommand{
@ -322,8 +324,8 @@ func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
func TestAccessControlStore_DeleteTeamPermissions(t *testing.T) {
t.Run("expect permissions related to team to be deleted", func(t *testing.T) {
store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t)
user, team := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1)
store, permissionsStore, usrSvc, teamSvc, _, sql := setupTestEnv(t)
user, team := createUserAndTeam(t, sql, usrSvc, teamSvc, 1)
// grant permission to the team
_, err := permissionsStore.SetTeamResourcePermission(context.Background(), 1, team.ID, rs.SetResourcePermissionCommand{
@ -356,8 +358,8 @@ func TestAccessControlStore_DeleteTeamPermissions(t *testing.T) {
assert.Len(t, permissions, 0)
})
t.Run("expect permissions not related to team to be kept", func(t *testing.T) {
store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t)
user, team := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1)
store, permissionsStore, usrSvc, teamSvc, _, sql := setupTestEnv(t)
user, team := createUserAndTeam(t, sql, usrSvc, teamSvc, 1)
// grant permission to the team
_, err := permissionsStore.SetTeamResourcePermission(context.Background(), 1, team.ID, rs.SetResourcePermissionCommand{
@ -468,14 +470,13 @@ func createUsersAndTeams(t *testing.T, store db.DB, svcs helperServices, orgID i
return res
}
func setupTestEnv(t testing.TB) (*AccessControlStore, rs.Store, user.Service, team.Service, org.Service) {
func setupTestEnv(t testing.TB) (*database.AccessControlStore, rs.Store, user.Service, team.Service, org.Service, *sqlstore.SQLStore) {
sql, cfg := db.InitTestDBWithCfg(t)
cfg.AutoAssignOrg = true
cfg.AutoAssignOrgRole = "Viewer"
cfg.AutoAssignOrgId = 1
acstore := ProvideService(sql)
asService := rs.NewActionSetService()
permissionStore := rs.NewStore(sql, featuremgmt.WithFeatures(), &asService)
acstore := database.ProvideService(sql)
permissionStore := rs.NewStore(sql, featuremgmt.WithFeatures())
teamService, err := teamimpl.ProvideService(sql, cfg, tracing.InitializeTracerForTest())
require.NoError(t, err)
orgService, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil))
@ -490,7 +491,7 @@ func setupTestEnv(t testing.TB) (*AccessControlStore, rs.Store, user.Service, te
quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(),
)
require.NoError(t, err)
return acstore, permissionStore, userService, teamService, orgService
return acstore, permissionStore, userService, teamService, orgService, sql
}
func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) {
@ -735,8 +736,8 @@ func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
acStore, permissionsStore, userSvc, teamSvc, orgSvc := setupTestEnv(t)
dbUsers := createUsersAndTeams(t, acStore.sql, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users)
acStore, permissionsStore, userSvc, teamSvc, orgSvc, sql := setupTestEnv(t)
dbUsers := createUsersAndTeams(t, sql, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users)
// Switch userID and TeamID by the real stored ones
for i := range tt.permCmds {
@ -815,8 +816,8 @@ func TestAccessControlStore_GetUsersBasicRoles(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
acStore, _, userSvc, teamSvc, orgSvc := setupTestEnv(t)
dbUsers := createUsersAndTeams(t, acStore.sql, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users)
acStore, _, userSvc, teamSvc, orgSvc, sql := setupTestEnv(t)
dbUsers := createUsersAndTeams(t, sql, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users)
// Test
dbRoles, err := acStore.GetUsersBasicRoles(ctx, tt.userFilter, 1)

View File

@ -16,6 +16,9 @@ type Evaluator interface {
Evaluate(permissions map[string][]string) bool
// MutateScopes executes a sequence of ScopeModifier functions on all embedded scopes of an evaluator and returns a new Evaluator
MutateScopes(ctx context.Context, mutate ScopeAttributeMutator) (Evaluator, error)
// AppendActionSets extends the evaluator with relevant action sets
// (e.g. evaluator checking `folders:write` is extended to check for any of `folders:write`, `folders:edit`, `folders:admin`)
AppendActionSets(ctx context.Context, mutate ActionSetResolver) Evaluator
// String returns a string representation of permission required by the evaluator
fmt.Stringer
fmt.GoStringer
@ -107,6 +110,17 @@ func (p permissionEvaluator) MutateScopes(ctx context.Context, mutate ScopeAttri
return EvalPermission(p.Action, scopes...), nil
}
func (p permissionEvaluator) AppendActionSets(ctx context.Context, resolve ActionSetResolver) Evaluator {
resolvedActions := resolve(ctx, p.Action)
evals := make([]Evaluator, 0, len(resolvedActions))
for _, action := range resolvedActions {
evals = append(evals, EvalPermission(action, p.Scopes...))
}
return EvalAny(evals...)
}
func (p permissionEvaluator) String() string {
return p.Action
}
@ -157,6 +171,16 @@ func (a allEvaluator) MutateScopes(ctx context.Context, mutate ScopeAttributeMut
return EvalAll(modified...), nil
}
func (a allEvaluator) AppendActionSets(ctx context.Context, resolve ActionSetResolver) Evaluator {
evals := make([]Evaluator, 0, len(a.allOf))
for _, e := range a.allOf {
resolvedSets := e.AppendActionSets(ctx, resolve)
evals = append(evals, resolvedSets)
}
return EvalAll(evals...)
}
func (a allEvaluator) String() string {
permissions := make([]string, 0, len(a.allOf))
for _, e := range a.allOf {
@ -218,6 +242,16 @@ func (a anyEvaluator) MutateScopes(ctx context.Context, mutate ScopeAttributeMut
return EvalAny(modified...), nil
}
func (a anyEvaluator) AppendActionSets(ctx context.Context, resolve ActionSetResolver) Evaluator {
evals := make([]Evaluator, 0, len(a.anyOf))
for _, e := range a.anyOf {
resolvedSets := e.AppendActionSets(ctx, resolve)
evals = append(evals, resolvedSets)
}
return EvalAny(evals...)
}
func (a anyEvaluator) String() string {
permissions := make([]string, 0, len(a.anyOf))
for _, e := range a.anyOf {

View File

@ -288,9 +288,9 @@ var DatasourceQueryActions = []string{
datasources.ActionQuery,
}
func ProvideDatasourcePermissionsService(features featuremgmt.FeatureToggles, db db.DB, actionSetService resourcepermissions.ActionSetService) *DatasourcePermissionsService {
func ProvideDatasourcePermissionsService(features featuremgmt.FeatureToggles, db db.DB) *DatasourcePermissionsService {
return &DatasourcePermissionsService{
store: resourcepermissions.NewStore(db, features, &actionSetService),
store: resourcepermissions.NewStore(db, features),
}
}

View File

@ -15,6 +15,10 @@ type ScopeAttributeResolver interface {
Resolve(ctx context.Context, orgID int64, scope string) ([]string, error)
}
type ActionResolver interface {
Resolve(action string) []string
}
// ScopeAttributeResolverFunc is an adapter to allow functions to implement ScopeAttributeResolver interface
type ScopeAttributeResolverFunc func(ctx context.Context, orgID int64, scope string) ([]string, error)
@ -24,6 +28,8 @@ func (f ScopeAttributeResolverFunc) Resolve(ctx context.Context, orgID int64, sc
type ScopeAttributeMutator func(context.Context, string) ([]string, error)
type ActionSetResolver func(context.Context, string) []string
const (
ttl = 30 * time.Second
cleanInterval = 2 * time.Minute
@ -41,6 +47,7 @@ type Resolvers struct {
log log.Logger
cache *localcache.CacheService
attributeResolvers map[string]ScopeAttributeResolver
actionResolver ActionResolver
}
func (s *Resolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) {
@ -48,6 +55,10 @@ func (s *Resolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttri
s.attributeResolvers[prefix] = resolver
}
func (s *Resolvers) SetActionResolver(resolver ActionResolver) {
s.actionResolver = resolver
}
func (s *Resolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMutator {
return func(ctx context.Context, scope string) ([]string, error) {
key := getScopeCacheKey(orgID, scope)
@ -77,3 +88,15 @@ func (s *Resolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMutator
func getScopeCacheKey(orgID int64, scope string) string {
return fmt.Sprintf("%s-%v", scope, orgID)
}
func (s *Resolvers) GetActionSetResolver() ActionSetResolver {
return func(ctx context.Context, action string) []string {
if s.actionResolver == nil {
return []string{action}
}
actionSetActions := s.actionResolver.Resolve(action)
actions := append(actionSetActions, action)
s.log.Debug("Resolved action", "action", action, "resolved_actions", actions)
return actions
}
}

View File

@ -66,7 +66,9 @@ func New(cfg *setting.Cfg,
for _, a := range actions {
actionSet[a] = struct{}{}
}
actionSetService.StoreActionSet(options.Resource, permission, actions)
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
actionSetService.StoreActionSet(options.Resource, permission, actions)
}
}
// Sort all permissions based on action length. Will be used when mapping between actions to permissions
@ -81,7 +83,7 @@ func New(cfg *setting.Cfg,
s := &Service{
ac: ac,
store: NewStore(sqlStore, features, &actionSetService),
store: NewStore(sqlStore, features),
options: options,
license: license,
permissions: permissions,

View File

@ -224,6 +224,102 @@ func TestService_SetPermissions(t *testing.T) {
}
}
func TestService_RegisterActionSets(t *testing.T) {
type registerActionSetsTest struct {
desc string
actionSetsEnabled bool
options Options
expectedActionSets []ActionSet
}
tests := []registerActionSetsTest{
{
desc: "should register folder action sets if action sets are enabled",
actionSetsEnabled: true,
options: Options{
Resource: "folders",
PermissionsToActions: map[string][]string{
"View": {"folders:read", "dashboards:read"},
"Edit": {"folders:read", "dashboards:read", "folders:write", "dashboards:write"},
},
},
expectedActionSets: []ActionSet{
{
Action: "folders:view",
Actions: []string{"folders:read", "dashboards:read"},
},
{
Action: "folders:edit",
Actions: []string{"folders:read", "dashboards:read", "folders:write", "dashboards:write"},
},
},
},
{
desc: "should register dashboard action set if action sets are enabled",
actionSetsEnabled: true,
options: Options{
Resource: "dashboards",
PermissionsToActions: map[string][]string{
"View": {"dashboards:read"},
},
},
expectedActionSets: []ActionSet{
{
Action: "dashboards:view",
Actions: []string{"dashboards:read"},
},
},
},
{
desc: "should not register dashboard action set if action sets are not enabled",
actionSetsEnabled: false,
options: Options{
Resource: "dashboards",
PermissionsToActions: map[string][]string{
"View": {"dashboards:read"},
},
},
expectedActionSets: []ActionSet{},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = func(ft string) bool {
if ft == featuremgmt.FlagAccessActionSets {
return tt.actionSetsEnabled
}
return false
}
ac := acimpl.ProvideAccessControl(cfg)
actionSets := NewActionSetService(ac)
features := featuremgmt.WithFeatures()
if tt.actionSetsEnabled {
features = featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets)
}
_, err := New(
setting.NewCfg(), tt.options, features, routing.NewRouteRegister(), licensingtest.NewFakeLicensing(),
ac, &actest.FakeService{}, db.InitTestDB(t), nil, nil, actionSets,
)
require.NoError(t, err)
if len(tt.expectedActionSets) > 0 {
for _, expectedActionSet := range tt.expectedActionSets {
actionSet := actionSets.GetActionSet(expectedActionSet.Action)
assert.ElementsMatch(t, expectedActionSet.Actions, actionSet)
}
} else {
// Check that action sets have not been registered
for permission := range tt.options.PermissionsToActions {
actionSetName := GetActionSetName(tt.options.Resource, permission)
assert.Nil(t, actionSets.GetActionSet(actionSetName))
}
}
})
}
}
func setupTestEnvironment(t *testing.T, ops Options) (*Service, user.Service, team.Service) {
t.Helper()
@ -245,11 +341,11 @@ func setupTestEnvironment(t *testing.T, ops Options) (*Service, user.Service, te
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
ac := acimpl.ProvideAccessControl(cfg)
ac := acimpl.ProvideAccessControl(setting.NewCfg())
acService := &actest.FakeService{}
service, err := New(
cfg, ops, featuremgmt.WithFeatures(), routing.NewRouteRegister(), license,
ac, acService, sql, teamSvc, userSvc, NewActionSetService(),
ac, acService, sql, teamSvc, userSvc, NewActionSetService(ac),
)
require.NoError(t, err)

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
@ -17,14 +18,14 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func NewStore(sql db.DB, features featuremgmt.FeatureToggles, actionsetService *ActionSetService) *store {
return &store{sql, features, *actionsetService}
func NewStore(sql db.DB, features featuremgmt.FeatureToggles) *store {
store := &store{sql: sql, features: features}
return store
}
type store struct {
sql db.DB
features featuremgmt.FeatureToggles
actionSetService ActionSetService
sql db.DB
features featuremgmt.FeatureToggles
}
type flatResourcePermission struct {
@ -665,7 +666,7 @@ func (s *store) createPermissions(sess *db.Session, roleID int64, resource, reso
Add ACTION SET of managed permissions to in-memory store
*/
if s.features.IsEnabled(context.TODO(), featuremgmt.FlagAccessActionSets) && permission != "" {
actionSetName := s.actionSetService.GetActionSetName(resource, permission)
actionSetName := GetActionSetName(resource, permission)
p := managedPermission(actionSetName, resource, resourceID, resourceAttribute)
p.RoleID = roleID
p.Created = time.Now()
@ -735,8 +736,10 @@ Stores actionsets IN MEMORY
// An example of an action set is "folders:edit" which represents the set of RBAC actions that are granted by edit access to a folder.
type ActionSetService interface {
accesscontrol.ActionResolver
GetActionSet(actionName string) []string
GetActionSetName(resource, permission string) string
//GetActionSetName(resource, permission string) string
StoreActionSet(resource, permission string, actions []string)
}
@ -747,21 +750,47 @@ type ActionSet struct {
// InMemoryActionSets is an in-memory implementation of the ActionSetService.
type InMemoryActionSets struct {
log log.Logger
actionSets map[string][]string
log log.Logger
actionSetToActions map[string][]string
actionToActionSets map[string][]string
}
// NewActionSetService returns a new instance of InMemoryActionSetService.
func NewActionSetService() ActionSetService {
return &InMemoryActionSets{
actionSets: make(map[string][]string),
log: log.New("resourcepermissions.actionsets"),
func NewActionSetService(a *acimpl.AccessControl) ActionSetService {
actionSets := &InMemoryActionSets{
log: log.New("resourcepermissions.actionsets"),
actionSetToActions: make(map[string][]string),
actionToActionSets: make(map[string][]string),
}
a.RegisterActionResolver(actionSets)
return actionSets
}
func (s *InMemoryActionSets) Resolve(action string) []string {
actionSets := s.actionToActionSets[action]
sets := make([]string, 0, len(actionSets))
for _, actionSet := range actionSets {
setParts := strings.Split(actionSet, ":")
if len(setParts) != 2 {
s.log.Debug("skipping resolution for action set with invalid name", "action set", actionSet)
continue
}
prefix := setParts[0]
// 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:query`)
if prefix != "folders" && prefix != "dashboards" {
continue
}
sets = append(sets, actionSet)
}
return sets
}
// GetActionSet returns the action set for the given action.
func (s *InMemoryActionSets) GetActionSet(actionName string) []string {
actionSet, ok := s.actionSets[actionName]
actionSet, ok := s.actionSetToActions[actionName]
if !ok {
return nil
}
@ -769,17 +798,24 @@ func (s *InMemoryActionSets) GetActionSet(actionName string) []string {
}
func (s *InMemoryActionSets) StoreActionSet(resource, permission string, actions []string) {
s.log.Debug("storing action set\n")
name := s.GetActionSetName(resource, permission)
name := GetActionSetName(resource, permission)
actionSet := &ActionSet{
Action: name,
Actions: actions,
}
s.actionSets[actionSet.Action] = actions
s.actionSetToActions[actionSet.Action] = actions
for _, action := range actions {
if _, ok := s.actionToActionSets[action]; !ok {
s.actionToActionSets[action] = []string{}
}
s.actionToActionSets[action] = append(s.actionToActionSets[action], actionSet.Action)
}
s.log.Debug("stored action set", "action set name", actionSet.Action)
}
// GetActionSetName function creates an action set from a list of actions and stores it inmemory.
func (s *InMemoryActionSets) GetActionSetName(resource, permission string) string {
func GetActionSetName(resource, permission string) string {
// lower cased
resource = strings.ToLower(resource)
permission = strings.ToLower(permission)

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
@ -563,8 +564,7 @@ func seedResourcePermissions(
func setupTestEnv(t testing.TB) (*store, db.DB, *setting.Cfg) {
sql, cfg := db.InitTestDBWithCfg(t)
asService := NewActionSetService()
return NewStore(sql, featuremgmt.WithFeatures(), &asService), sql, cfg
return NewStore(sql, featuremgmt.WithFeatures()), sql, cfg
}
func TestStore_IsInherited(t *testing.T) {
@ -759,49 +759,85 @@ func retrievePermissionsHelper(store *store, t *testing.T) []orgPermission {
return permissions
}
func TestStore_ResourcePermissionsActionSets(t *testing.T) {
func TestStore_StoreActionSet(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
type actionSetTest struct {
desc string
orgID int64
actionSet ActionSet
desc string
resource string
action string
actions []string
}
tests := []actionSetTest{
{
desc: "should be able to store actionset",
orgID: 1,
actionSet: ActionSet{
Actions: []string{"folders:read", "folders:write"},
},
desc: "should be able to store action set",
resource: "folders",
action: "edit",
actions: []string{"folders:read", "folders:write"},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
store, _, _ := setupTestEnv(t)
store.features = featuremgmt.WithFeatures([]any{featuremgmt.FlagAccessActionSets})
store.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets)
ac := acimpl.ProvideAccessControl(setting.NewCfg())
asService := NewActionSetService(ac)
asService.StoreActionSet(tt.resource, tt.action, tt.actions)
_, err := store.SetResourcePermissions(context.Background(), 1, []SetResourcePermissionsCommand{
{
User: accesscontrol.User{ID: 1},
SetResourcePermissionCommand: SetResourcePermissionCommand{
Actions: tt.actionSet.Actions,
Resource: "folders",
Permission: "edit",
ResourceID: "1",
ResourceAttribute: "uid",
},
},
}, ResourceHooks{})
require.NoError(t, err)
actionname := fmt.Sprintf("%s:%s", "folders", "edit")
actionSet := store.actionSetService.GetActionSet(actionname)
require.Equal(t, tt.actionSet.Actions, actionSet)
actionSetName := GetActionSetName(tt.resource, tt.action)
actionSet := asService.GetActionSet(actionSetName)
require.Equal(t, tt.actions, actionSet)
})
}
}
func TestStore_ResolveActionSet(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
actionSetService := NewActionSetService(acimpl.ProvideAccessControl(setting.NewCfg()))
actionSetService.StoreActionSet("folders", "edit", []string{"folders:read", "folders:write", "dashboards:read", "dashboards:write"})
actionSetService.StoreActionSet("folders", "view", []string{"folders:read", "dashboards:read"})
actionSetService.StoreActionSet("dashboards", "view", []string{"dashboards:read"})
type actionSetTest struct {
desc string
action string
expectedActionSets []string
}
tests := []actionSetTest{
{
desc: "should return empty list for an action that is not a part of any action sets",
action: "datasources:query",
expectedActionSets: []string{},
},
{
desc: "should be able to resolve one action set for the resource of the same type",
action: "folders:write",
expectedActionSets: []string{"folders:edit"},
},
{
desc: "should be able to resolve multiple action sets for the resource of the same type",
action: "folders:read",
expectedActionSets: []string{"folders:view", "folders:edit"},
},
{
desc: "should be able to resolve multiple action sets for the resource of a different type",
action: "dashboards:read",
expectedActionSets: []string{"folders:view", "folders:edit", "dashboards:view"},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
actionSets := actionSetService.Resolve(tt.action)
require.ElementsMatch(t, tt.expectedActionSets, actionSets)
})
}
}

View File

@ -306,8 +306,7 @@ func TestIntegrationSilenceAuth(t *testing.T) {
apiClient := newAlertingApiClient(grafanaListedAddr, randomLogin, randomLogin)
// Set permissions.
asService := resourcepermissions.NewActionSetService()
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
for _, cmd := range tt.permissions {
_, err := permissionsStore.SetUserResourcePermission(
context.Background(),

View File

@ -107,9 +107,8 @@ func TestBacktesting(t *testing.T) {
require.Equalf(t, http.StatusForbidden, status, "Response: %s", body)
})
asService := resourcepermissions.NewActionSetService()
// access control permissions store
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
_, err := permissionsStore.SetUserResourcePermission(context.Background(),
accesscontrol.GlobalOrgID,
accesscontrol.User{ID: testUserId},

View File

@ -674,9 +674,8 @@ func TestIntegrationPrometheusRulesPermissions(t *testing.T) {
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
asService := resourcepermissions.NewActionSetService()
// access control permissions store
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder1", "folder1")

View File

@ -52,8 +52,7 @@ func TestIntegrationAlertRulePermissions(t *testing.T) {
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
asService := resourcepermissions.NewActionSetService()
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
// Create a user to make authenticated requests
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
@ -337,8 +336,7 @@ func TestIntegrationAlertRuleNestedPermissions(t *testing.T) {
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
asService := resourcepermissions.NewActionSetService()
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
// Create a user to make authenticated requests
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
@ -734,8 +732,7 @@ func TestAlertRulePostExport(t *testing.T) {
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
asService := resourcepermissions.NewActionSetService()
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
// Create a user to make authenticated requests
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
@ -1415,8 +1412,7 @@ func TestIntegrationRuleUpdate(t *testing.T) {
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
asService := resourcepermissions.NewActionSetService()
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
// Create a user to make authenticated requests
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{

View File

@ -275,8 +275,7 @@ func TestGrafanaRuleConfig(t *testing.T) {
})
// access control permissions store
asService := resourcepermissions.NewActionSetService()
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
_, err := permissionsStore.SetUserResourcePermission(context.Background(),
accesscontrol.GlobalOrgID,
accesscontrol.User{ID: testUserId},

View File

@ -65,8 +65,7 @@ func TestGetFolders(t *testing.T) {
viewerClient := tests.GetClient(grafanaListedAddr, "viewer", "viewer")
// access control permissions store
actionSetService := resourcepermissions.NewActionSetService()
permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures(), &actionSetService)
permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures())
numberOfFolders := 5
indexWithoutPermission := 3