mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
6380a01543
commit
105313f5c2
@ -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)
|
folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, sc.db, features, supportbundlestest.NewFakeBundleService(), nil)
|
||||||
|
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
|
actionSets := resourcepermissions.NewActionSetService(ac)
|
||||||
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
|
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)
|
require.NoError(b, err)
|
||||||
dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions(
|
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)
|
require.NoError(b, err)
|
||||||
|
|
||||||
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
|
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -48,6 +49,11 @@ func (a *AccessControl) Evaluate(ctx context.Context, user identity.Requester, e
|
|||||||
return false, nil
|
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)
|
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
|
// Test evaluation without scope resolver first, this will prevent 403 for wildcard scopes when resource does not exist
|
||||||
if evaluator.Evaluate(permissions) {
|
if evaluator.Evaluate(permissions) {
|
||||||
@ -70,6 +76,10 @@ func (a *AccessControl) RegisterScopeAttributeResolver(prefix string, resolver a
|
|||||||
a.resolvers.AddScopeAttributeResolver(prefix, resolver)
|
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) {
|
func (a *AccessControl) debug(ctx context.Context, ident identity.Requester, msg string, eval accesscontrol.Evaluator) {
|
||||||
namespace, id := ident.GetNamespacedID()
|
namespace, id := ident.GetNamespacedID()
|
||||||
a.log.FromContext(ctx).Debug(msg, "namespace", namespace, "id", id, "orgID", ident.GetOrgID(), "permissions", eval.GoString())
|
a.log.FromContext(ctx).Debug(msg, "namespace", namespace, "id", id, "orgID", ident.GetOrgID(), "permissions", eval.GoString())
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
package acimpl
|
package acimpl_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"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/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
@ -19,7 +24,8 @@ func TestAccessControl_Evaluate(t *testing.T) {
|
|||||||
resolverPrefix string
|
resolverPrefix string
|
||||||
expected bool
|
expected bool
|
||||||
expectedErr error
|
expectedErr error
|
||||||
resolver accesscontrol.ScopeAttributeResolver
|
scopeResolver accesscontrol.ScopeAttributeResolver
|
||||||
|
actionSets map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []testCase{
|
tests := []testCase{
|
||||||
@ -55,19 +61,127 @@ func TestAccessControl_Evaluate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
evaluator: accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite, "teams:id:1"),
|
evaluator: accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite, "teams:id:1"),
|
||||||
resolverPrefix: "teams:id:",
|
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
|
return []string{"another:scope"}, nil
|
||||||
}),
|
}),
|
||||||
expected: true,
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
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 {
|
if tt.scopeResolver != nil {
|
||||||
ac.RegisterScopeAttributeResolver(tt.resolverPrefix, tt.resolver)
|
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)
|
hasAccess, err := ac.Evaluate(context.Background(), &tt.user, tt.evaluator)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package database
|
package database_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||||
rs "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
rs "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
"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"
|
||||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
"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/supportbundles/supportbundlestest"
|
||||||
"github.com/grafana/grafana/pkg/services/team"
|
"github.com/grafana/grafana/pkg/services/team"
|
||||||
"github.com/grafana/grafana/pkg/services/team/teamimpl"
|
"github.com/grafana/grafana/pkg/services/team/teamimpl"
|
||||||
@ -91,9 +93,9 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
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 {
|
for _, id := range tt.userPermissions {
|
||||||
_, err := permissionStore.SetUserResourcePermission(context.Background(), tt.orgID, accesscontrol.User{ID: user.ID}, rs.SetResourcePermissionCommand{
|
_, 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)
|
require.NoError(t, err)
|
||||||
assert.Len(t, permissions, tt.expected)
|
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) {
|
func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
||||||
return strings.Split(scope, ":"), nil
|
return strings.Split(scope, ":"), nil
|
||||||
})
|
})
|
||||||
@ -195,7 +197,7 @@ func TestAccessControlStore_GetTeamsPermissions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
store, permissionStore, _, teamSvc, _ := setupTestEnv(t)
|
store, permissionStore, _, teamSvc, _, _ := setupTestEnv(t)
|
||||||
|
|
||||||
teams := make([]team.Team, 0)
|
teams := make([]team.Team, 0)
|
||||||
for i := 0; i < len(tt.teamsPermissions); i++ {
|
for i := 0; i < len(tt.teamsPermissions); i++ {
|
||||||
@ -240,8 +242,8 @@ func TestAccessControlStore_GetTeamsPermissions(t *testing.T) {
|
|||||||
|
|
||||||
func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
|
func TestAccessControlStore_DeleteUserPermissions(t *testing.T) {
|
||||||
t.Run("expect permissions in all orgs to be deleted", func(t *testing.T) {
|
t.Run("expect permissions in all orgs to be deleted", func(t *testing.T) {
|
||||||
store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t)
|
store, permissionsStore, usrSvc, teamSvc, _, sql := setupTestEnv(t)
|
||||||
user, _ := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1)
|
user, _ := createUserAndTeam(t, sql, usrSvc, teamSvc, 1)
|
||||||
|
|
||||||
// generate permissions in org 1
|
// generate permissions in org 1
|
||||||
_, err := permissionsStore.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: user.ID}, rs.SetResourcePermissionCommand{
|
_, 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) {
|
t.Run("expect permissions in org 1 to be deleted", func(t *testing.T) {
|
||||||
store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t)
|
store, permissionsStore, usrSvc, teamSvc, _, sql := setupTestEnv(t)
|
||||||
user, _ := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1)
|
user, _ := createUserAndTeam(t, sql, usrSvc, teamSvc, 1)
|
||||||
|
|
||||||
// generate permissions in org 1
|
// generate permissions in org 1
|
||||||
_, err := permissionsStore.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: user.ID}, rs.SetResourcePermissionCommand{
|
_, 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) {
|
func TestAccessControlStore_DeleteTeamPermissions(t *testing.T) {
|
||||||
t.Run("expect permissions related to team to be deleted", func(t *testing.T) {
|
t.Run("expect permissions related to team to be deleted", func(t *testing.T) {
|
||||||
store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t)
|
store, permissionsStore, usrSvc, teamSvc, _, sql := setupTestEnv(t)
|
||||||
user, team := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1)
|
user, team := createUserAndTeam(t, sql, usrSvc, teamSvc, 1)
|
||||||
|
|
||||||
// grant permission to the team
|
// grant permission to the team
|
||||||
_, err := permissionsStore.SetTeamResourcePermission(context.Background(), 1, team.ID, rs.SetResourcePermissionCommand{
|
_, 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)
|
assert.Len(t, permissions, 0)
|
||||||
})
|
})
|
||||||
t.Run("expect permissions not related to team to be kept", func(t *testing.T) {
|
t.Run("expect permissions not related to team to be kept", func(t *testing.T) {
|
||||||
store, permissionsStore, usrSvc, teamSvc, _ := setupTestEnv(t)
|
store, permissionsStore, usrSvc, teamSvc, _, sql := setupTestEnv(t)
|
||||||
user, team := createUserAndTeam(t, store.sql, usrSvc, teamSvc, 1)
|
user, team := createUserAndTeam(t, sql, usrSvc, teamSvc, 1)
|
||||||
|
|
||||||
// grant permission to the team
|
// grant permission to the team
|
||||||
_, err := permissionsStore.SetTeamResourcePermission(context.Background(), 1, team.ID, rs.SetResourcePermissionCommand{
|
_, 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
|
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)
|
sql, cfg := db.InitTestDBWithCfg(t)
|
||||||
cfg.AutoAssignOrg = true
|
cfg.AutoAssignOrg = true
|
||||||
cfg.AutoAssignOrgRole = "Viewer"
|
cfg.AutoAssignOrgRole = "Viewer"
|
||||||
cfg.AutoAssignOrgId = 1
|
cfg.AutoAssignOrgId = 1
|
||||||
acstore := ProvideService(sql)
|
acstore := database.ProvideService(sql)
|
||||||
asService := rs.NewActionSetService()
|
permissionStore := rs.NewStore(sql, featuremgmt.WithFeatures())
|
||||||
permissionStore := rs.NewStore(sql, featuremgmt.WithFeatures(), &asService)
|
|
||||||
teamService, err := teamimpl.ProvideService(sql, cfg, tracing.InitializeTracerForTest())
|
teamService, err := teamimpl.ProvideService(sql, cfg, tracing.InitializeTracerForTest())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
orgService, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil))
|
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(),
|
quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(),
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return acstore, permissionStore, userService, teamService, orgService
|
return acstore, permissionStore, userService, teamService, orgService, sql
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) {
|
func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) {
|
||||||
@ -735,8 +736,8 @@ func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
acStore, permissionsStore, userSvc, teamSvc, orgSvc := setupTestEnv(t)
|
acStore, permissionsStore, userSvc, teamSvc, orgSvc, sql := setupTestEnv(t)
|
||||||
dbUsers := createUsersAndTeams(t, acStore.sql, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users)
|
dbUsers := createUsersAndTeams(t, sql, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users)
|
||||||
|
|
||||||
// Switch userID and TeamID by the real stored ones
|
// Switch userID and TeamID by the real stored ones
|
||||||
for i := range tt.permCmds {
|
for i := range tt.permCmds {
|
||||||
@ -815,8 +816,8 @@ func TestAccessControlStore_GetUsersBasicRoles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
acStore, _, userSvc, teamSvc, orgSvc := setupTestEnv(t)
|
acStore, _, userSvc, teamSvc, orgSvc, sql := setupTestEnv(t)
|
||||||
dbUsers := createUsersAndTeams(t, acStore.sql, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users)
|
dbUsers := createUsersAndTeams(t, sql, helperServices{userSvc, teamSvc, orgSvc}, 1, tt.users)
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
dbRoles, err := acStore.GetUsersBasicRoles(ctx, tt.userFilter, 1)
|
dbRoles, err := acStore.GetUsersBasicRoles(ctx, tt.userFilter, 1)
|
||||||
|
@ -16,6 +16,9 @@ type Evaluator interface {
|
|||||||
Evaluate(permissions map[string][]string) bool
|
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 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)
|
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
|
// String returns a string representation of permission required by the evaluator
|
||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
fmt.GoStringer
|
fmt.GoStringer
|
||||||
@ -107,6 +110,17 @@ func (p permissionEvaluator) MutateScopes(ctx context.Context, mutate ScopeAttri
|
|||||||
return EvalPermission(p.Action, scopes...), nil
|
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 {
|
func (p permissionEvaluator) String() string {
|
||||||
return p.Action
|
return p.Action
|
||||||
}
|
}
|
||||||
@ -157,6 +171,16 @@ func (a allEvaluator) MutateScopes(ctx context.Context, mutate ScopeAttributeMut
|
|||||||
return EvalAll(modified...), nil
|
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 {
|
func (a allEvaluator) String() string {
|
||||||
permissions := make([]string, 0, len(a.allOf))
|
permissions := make([]string, 0, len(a.allOf))
|
||||||
for _, e := range a.allOf {
|
for _, e := range a.allOf {
|
||||||
@ -218,6 +242,16 @@ func (a anyEvaluator) MutateScopes(ctx context.Context, mutate ScopeAttributeMut
|
|||||||
return EvalAny(modified...), nil
|
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 {
|
func (a anyEvaluator) String() string {
|
||||||
permissions := make([]string, 0, len(a.anyOf))
|
permissions := make([]string, 0, len(a.anyOf))
|
||||||
for _, e := range a.anyOf {
|
for _, e := range a.anyOf {
|
||||||
|
@ -288,9 +288,9 @@ var DatasourceQueryActions = []string{
|
|||||||
datasources.ActionQuery,
|
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{
|
return &DatasourcePermissionsService{
|
||||||
store: resourcepermissions.NewStore(db, features, &actionSetService),
|
store: resourcepermissions.NewStore(db, features),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,10 @@ type ScopeAttributeResolver interface {
|
|||||||
Resolve(ctx context.Context, orgID int64, scope string) ([]string, error)
|
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
|
// ScopeAttributeResolverFunc is an adapter to allow functions to implement ScopeAttributeResolver interface
|
||||||
type ScopeAttributeResolverFunc func(ctx context.Context, orgID int64, scope string) ([]string, error)
|
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 ScopeAttributeMutator func(context.Context, string) ([]string, error)
|
||||||
|
|
||||||
|
type ActionSetResolver func(context.Context, string) []string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ttl = 30 * time.Second
|
ttl = 30 * time.Second
|
||||||
cleanInterval = 2 * time.Minute
|
cleanInterval = 2 * time.Minute
|
||||||
@ -41,6 +47,7 @@ type Resolvers struct {
|
|||||||
log log.Logger
|
log log.Logger
|
||||||
cache *localcache.CacheService
|
cache *localcache.CacheService
|
||||||
attributeResolvers map[string]ScopeAttributeResolver
|
attributeResolvers map[string]ScopeAttributeResolver
|
||||||
|
actionResolver ActionResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Resolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) {
|
func (s *Resolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) {
|
||||||
@ -48,6 +55,10 @@ func (s *Resolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttri
|
|||||||
s.attributeResolvers[prefix] = resolver
|
s.attributeResolvers[prefix] = resolver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Resolvers) SetActionResolver(resolver ActionResolver) {
|
||||||
|
s.actionResolver = resolver
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Resolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMutator {
|
func (s *Resolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMutator {
|
||||||
return func(ctx context.Context, scope string) ([]string, error) {
|
return func(ctx context.Context, scope string) ([]string, error) {
|
||||||
key := getScopeCacheKey(orgID, scope)
|
key := getScopeCacheKey(orgID, scope)
|
||||||
@ -77,3 +88,15 @@ func (s *Resolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMutator
|
|||||||
func getScopeCacheKey(orgID int64, scope string) string {
|
func getScopeCacheKey(orgID int64, scope string) string {
|
||||||
return fmt.Sprintf("%s-%v", scope, orgID)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -66,7 +66,9 @@ func New(cfg *setting.Cfg,
|
|||||||
for _, a := range actions {
|
for _, a := range actions {
|
||||||
actionSet[a] = struct{}{}
|
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
|
// 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{
|
s := &Service{
|
||||||
ac: ac,
|
ac: ac,
|
||||||
store: NewStore(sqlStore, features, &actionSetService),
|
store: NewStore(sqlStore, features),
|
||||||
options: options,
|
options: options,
|
||||||
license: license,
|
license: license,
|
||||||
permissions: permissions,
|
permissions: permissions,
|
||||||
|
@ -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) {
|
func setupTestEnvironment(t *testing.T, ops Options) (*Service, user.Service, team.Service) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@ -245,11 +341,11 @@ func setupTestEnvironment(t *testing.T, ops Options) (*Service, user.Service, te
|
|||||||
|
|
||||||
license := licensingtest.NewFakeLicensing()
|
license := licensingtest.NewFakeLicensing()
|
||||||
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
|
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
|
||||||
ac := acimpl.ProvideAccessControl(cfg)
|
ac := acimpl.ProvideAccessControl(setting.NewCfg())
|
||||||
acService := &actest.FakeService{}
|
acService := &actest.FakeService{}
|
||||||
service, err := New(
|
service, err := New(
|
||||||
cfg, ops, featuremgmt.WithFeatures(), routing.NewRouteRegister(), license,
|
cfg, ops, featuremgmt.WithFeatures(), routing.NewRouteRegister(), license,
|
||||||
ac, acService, sql, teamSvc, userSvc, NewActionSetService(),
|
ac, acService, sql, teamSvc, userSvc, NewActionSetService(ac),
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"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/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
@ -17,14 +18,14 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewStore(sql db.DB, features featuremgmt.FeatureToggles, actionsetService *ActionSetService) *store {
|
func NewStore(sql db.DB, features featuremgmt.FeatureToggles) *store {
|
||||||
return &store{sql, features, *actionsetService}
|
store := &store{sql: sql, features: features}
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
type store struct {
|
type store struct {
|
||||||
sql db.DB
|
sql db.DB
|
||||||
features featuremgmt.FeatureToggles
|
features featuremgmt.FeatureToggles
|
||||||
actionSetService ActionSetService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type flatResourcePermission struct {
|
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
|
Add ACTION SET of managed permissions to in-memory store
|
||||||
*/
|
*/
|
||||||
if s.features.IsEnabled(context.TODO(), featuremgmt.FlagAccessActionSets) && permission != "" {
|
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 := managedPermission(actionSetName, resource, resourceID, resourceAttribute)
|
||||||
p.RoleID = roleID
|
p.RoleID = roleID
|
||||||
p.Created = time.Now()
|
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.
|
// 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 {
|
type ActionSetService interface {
|
||||||
|
accesscontrol.ActionResolver
|
||||||
|
|
||||||
GetActionSet(actionName string) []string
|
GetActionSet(actionName string) []string
|
||||||
GetActionSetName(resource, permission string) string
|
//GetActionSetName(resource, permission string) string
|
||||||
StoreActionSet(resource, permission string, actions []string)
|
StoreActionSet(resource, permission string, actions []string)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -747,21 +750,47 @@ type ActionSet struct {
|
|||||||
|
|
||||||
// InMemoryActionSets is an in-memory implementation of the ActionSetService.
|
// InMemoryActionSets is an in-memory implementation of the ActionSetService.
|
||||||
type InMemoryActionSets struct {
|
type InMemoryActionSets struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
actionSets map[string][]string
|
actionSetToActions map[string][]string
|
||||||
|
actionToActionSets map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewActionSetService returns a new instance of InMemoryActionSetService.
|
// NewActionSetService returns a new instance of InMemoryActionSetService.
|
||||||
func NewActionSetService() ActionSetService {
|
func NewActionSetService(a *acimpl.AccessControl) ActionSetService {
|
||||||
return &InMemoryActionSets{
|
actionSets := &InMemoryActionSets{
|
||||||
actionSets: make(map[string][]string),
|
log: log.New("resourcepermissions.actionsets"),
|
||||||
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.
|
// GetActionSet returns the action set for the given action.
|
||||||
func (s *InMemoryActionSets) GetActionSet(actionName string) []string {
|
func (s *InMemoryActionSets) GetActionSet(actionName string) []string {
|
||||||
actionSet, ok := s.actionSets[actionName]
|
actionSet, ok := s.actionSetToActions[actionName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -769,17 +798,24 @@ func (s *InMemoryActionSets) GetActionSet(actionName string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *InMemoryActionSets) StoreActionSet(resource, permission string, actions []string) {
|
func (s *InMemoryActionSets) StoreActionSet(resource, permission string, actions []string) {
|
||||||
s.log.Debug("storing action set\n")
|
name := GetActionSetName(resource, permission)
|
||||||
name := s.GetActionSetName(resource, permission)
|
|
||||||
actionSet := &ActionSet{
|
actionSet := &ActionSet{
|
||||||
Action: name,
|
Action: name,
|
||||||
Actions: actions,
|
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.
|
// 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
|
// lower cased
|
||||||
resource = strings.ToLower(resource)
|
resource = strings.ToLower(resource)
|
||||||
permission = strings.ToLower(permission)
|
permission = strings.ToLower(permission)
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"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/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
@ -563,8 +564,7 @@ func seedResourcePermissions(
|
|||||||
|
|
||||||
func setupTestEnv(t testing.TB) (*store, db.DB, *setting.Cfg) {
|
func setupTestEnv(t testing.TB) (*store, db.DB, *setting.Cfg) {
|
||||||
sql, cfg := db.InitTestDBWithCfg(t)
|
sql, cfg := db.InitTestDBWithCfg(t)
|
||||||
asService := NewActionSetService()
|
return NewStore(sql, featuremgmt.WithFeatures()), sql, cfg
|
||||||
return NewStore(sql, featuremgmt.WithFeatures(), &asService), sql, cfg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStore_IsInherited(t *testing.T) {
|
func TestStore_IsInherited(t *testing.T) {
|
||||||
@ -759,49 +759,85 @@ func retrievePermissionsHelper(store *store, t *testing.T) []orgPermission {
|
|||||||
return permissions
|
return permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStore_ResourcePermissionsActionSets(t *testing.T) {
|
func TestStore_StoreActionSet(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skipping integration test")
|
t.Skip("skipping integration test")
|
||||||
}
|
}
|
||||||
|
|
||||||
type actionSetTest struct {
|
type actionSetTest struct {
|
||||||
desc string
|
desc string
|
||||||
orgID int64
|
resource string
|
||||||
actionSet ActionSet
|
action string
|
||||||
|
actions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []actionSetTest{
|
tests := []actionSetTest{
|
||||||
{
|
{
|
||||||
desc: "should be able to store actionset",
|
desc: "should be able to store action set",
|
||||||
orgID: 1,
|
resource: "folders",
|
||||||
actionSet: ActionSet{
|
action: "edit",
|
||||||
Actions: []string{"folders:read", "folders:write"},
|
actions: []string{"folders:read", "folders:write"},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
store, _, _ := setupTestEnv(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{
|
actionSetName := GetActionSetName(tt.resource, tt.action)
|
||||||
{
|
actionSet := asService.GetActionSet(actionSetName)
|
||||||
User: accesscontrol.User{ID: 1},
|
require.Equal(t, tt.actions, actionSet)
|
||||||
SetResourcePermissionCommand: SetResourcePermissionCommand{
|
})
|
||||||
Actions: tt.actionSet.Actions,
|
}
|
||||||
Resource: "folders",
|
}
|
||||||
Permission: "edit",
|
|
||||||
ResourceID: "1",
|
func TestStore_ResolveActionSet(t *testing.T) {
|
||||||
ResourceAttribute: "uid",
|
if testing.Short() {
|
||||||
},
|
t.Skip("skipping integration test")
|
||||||
},
|
}
|
||||||
}, ResourceHooks{})
|
|
||||||
require.NoError(t, err)
|
actionSetService := NewActionSetService(acimpl.ProvideAccessControl(setting.NewCfg()))
|
||||||
|
actionSetService.StoreActionSet("folders", "edit", []string{"folders:read", "folders:write", "dashboards:read", "dashboards:write"})
|
||||||
actionname := fmt.Sprintf("%s:%s", "folders", "edit")
|
actionSetService.StoreActionSet("folders", "view", []string{"folders:read", "dashboards:read"})
|
||||||
actionSet := store.actionSetService.GetActionSet(actionname)
|
actionSetService.StoreActionSet("dashboards", "view", []string{"dashboards:read"})
|
||||||
require.Equal(t, tt.actionSet.Actions, actionSet)
|
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -306,8 +306,7 @@ func TestIntegrationSilenceAuth(t *testing.T) {
|
|||||||
apiClient := newAlertingApiClient(grafanaListedAddr, randomLogin, randomLogin)
|
apiClient := newAlertingApiClient(grafanaListedAddr, randomLogin, randomLogin)
|
||||||
|
|
||||||
// Set permissions.
|
// Set permissions.
|
||||||
asService := resourcepermissions.NewActionSetService()
|
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
|
||||||
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
|
|
||||||
for _, cmd := range tt.permissions {
|
for _, cmd := range tt.permissions {
|
||||||
_, err := permissionsStore.SetUserResourcePermission(
|
_, err := permissionsStore.SetUserResourcePermission(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
|
@ -107,9 +107,8 @@ func TestBacktesting(t *testing.T) {
|
|||||||
require.Equalf(t, http.StatusForbidden, status, "Response: %s", body)
|
require.Equalf(t, http.StatusForbidden, status, "Response: %s", body)
|
||||||
})
|
})
|
||||||
|
|
||||||
asService := resourcepermissions.NewActionSetService()
|
|
||||||
// access control permissions store
|
// access control permissions store
|
||||||
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
|
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
|
||||||
_, err := permissionsStore.SetUserResourcePermission(context.Background(),
|
_, err := permissionsStore.SetUserResourcePermission(context.Background(),
|
||||||
accesscontrol.GlobalOrgID,
|
accesscontrol.GlobalOrgID,
|
||||||
accesscontrol.User{ID: testUserId},
|
accesscontrol.User{ID: testUserId},
|
||||||
|
@ -674,9 +674,8 @@ func TestIntegrationPrometheusRulesPermissions(t *testing.T) {
|
|||||||
|
|
||||||
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
|
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
|
||||||
|
|
||||||
asService := resourcepermissions.NewActionSetService()
|
|
||||||
// access control permissions store
|
// 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.
|
// Create the namespace we'll save our alerts to.
|
||||||
apiClient.CreateFolder(t, "folder1", "folder1")
|
apiClient.CreateFolder(t, "folder1", "folder1")
|
||||||
|
@ -52,8 +52,7 @@ func TestIntegrationAlertRulePermissions(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
|
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
|
||||||
asService := resourcepermissions.NewActionSetService()
|
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
|
||||||
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
|
|
||||||
|
|
||||||
// Create a user to make authenticated requests
|
// Create a user to make authenticated requests
|
||||||
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
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)
|
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
|
||||||
asService := resourcepermissions.NewActionSetService()
|
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
|
||||||
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
|
|
||||||
|
|
||||||
// Create a user to make authenticated requests
|
// Create a user to make authenticated requests
|
||||||
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
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)
|
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
|
||||||
asService := resourcepermissions.NewActionSetService()
|
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
|
||||||
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
|
|
||||||
|
|
||||||
// Create a user to make authenticated requests
|
// Create a user to make authenticated requests
|
||||||
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||||
@ -1415,8 +1412,7 @@ func TestIntegrationRuleUpdate(t *testing.T) {
|
|||||||
AppModeProduction: true,
|
AppModeProduction: true,
|
||||||
})
|
})
|
||||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||||
asService := resourcepermissions.NewActionSetService()
|
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
|
||||||
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
|
|
||||||
|
|
||||||
// Create a user to make authenticated requests
|
// Create a user to make authenticated requests
|
||||||
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||||
|
@ -275,8 +275,7 @@ func TestGrafanaRuleConfig(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// access control permissions store
|
// access control permissions store
|
||||||
asService := resourcepermissions.NewActionSetService()
|
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures())
|
||||||
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
|
|
||||||
_, err := permissionsStore.SetUserResourcePermission(context.Background(),
|
_, err := permissionsStore.SetUserResourcePermission(context.Background(),
|
||||||
accesscontrol.GlobalOrgID,
|
accesscontrol.GlobalOrgID,
|
||||||
accesscontrol.User{ID: testUserId},
|
accesscontrol.User{ID: testUserId},
|
||||||
|
@ -65,8 +65,7 @@ func TestGetFolders(t *testing.T) {
|
|||||||
viewerClient := tests.GetClient(grafanaListedAddr, "viewer", "viewer")
|
viewerClient := tests.GetClient(grafanaListedAddr, "viewer", "viewer")
|
||||||
|
|
||||||
// access control permissions store
|
// access control permissions store
|
||||||
actionSetService := resourcepermissions.NewActionSetService()
|
permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures())
|
||||||
permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures(), &actionSetService)
|
|
||||||
|
|
||||||
numberOfFolders := 5
|
numberOfFolders := 5
|
||||||
indexWithoutPermission := 3
|
indexWithoutPermission := 3
|
||||||
|
Loading…
Reference in New Issue
Block a user