RBAC: Add actionsets struct and write path (#86108)

* Add actionsets struct and failing test

* update from review

* review comments

* review comments update

* refactor: create interface

* actionset service

* fix tests

* move from wireoss to wire

* Apply suggestions from code review

remove unnecessary comments

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

* nil for the actionsetservice

* Revert "nil for the actionsetservice"

This reverts commit e3d3cc8171.

---------

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
Eric Leijonmarck 2024-04-19 15:38:14 +01:00 committed by GitHub
parent a057e8be06
commit ddabef9895
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 165 additions and 29 deletions

View File

@ -25,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
@ -459,10 +460,10 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
cfg := setting.NewCfg()
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc)
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, resourcepermissions.NewActionSetService())
require.NoError(b, err)
dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions(
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc)
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, resourcepermissions.NewActionSetService())
require.NoError(b, err)
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(

View File

@ -37,6 +37,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/annotations/annotationsimpl"
"github.com/grafana/grafana/pkg/services/anonymous/anonimpl/anonstore"
@ -379,6 +380,7 @@ var wireBasicSet = wire.NewSet(
// Kubernetes API server
grafanaapiserver.WireSet,
apiregistry.WireSet,
resourcepermissions.NewActionSetService,
)
var wireSet = wire.NewSet(

View File

@ -393,7 +393,8 @@ func setupTestEnv(t testing.TB) (*AccessControlStore, rs.Store, user.Service, te
cfg.AutoAssignOrgRole = "Viewer"
cfg.AutoAssignOrgId = 1
acstore := ProvideService(sql)
permissionStore := rs.NewStore(sql, featuremgmt.WithFeatures())
asService := rs.NewActionSetService()
permissionStore := rs.NewStore(sql, featuremgmt.WithFeatures(), &asService)
teamService, err := teamimpl.ProvideService(sql, cfg)
require.NoError(t, err)
orgService, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil))

View File

@ -47,7 +47,7 @@ var (
func ProvideTeamPermissions(
cfg *setting.Cfg, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB,
ac accesscontrol.AccessControl, license licensing.Licensing, service accesscontrol.Service,
teamService team.Service, userService user.Service,
teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService,
) (*TeamPermissionsService, error) {
options := resourcepermissions.Options{
Resource: "teams",
@ -103,7 +103,7 @@ func ProvideTeamPermissions(
},
}
srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService)
srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService, actionSetService)
if err != nil {
return nil, err
}
@ -142,7 +142,7 @@ func getDashboardAdminActions(features featuremgmt.FeatureToggles) []string {
func ProvideDashboardPermissions(
cfg *setting.Cfg, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl,
license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service,
teamService team.Service, userService user.Service,
teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService,
) (*DashboardPermissionsService, error) {
getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*dashboards.Dashboard, error) {
query := &dashboards.GetDashboardQuery{UID: resourceID, OrgID: orgID}
@ -207,7 +207,7 @@ func ProvideDashboardPermissions(
RoleGroup: "Dashboards",
}
srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService)
srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService, actionSetService)
if err != nil {
return nil, err
}
@ -237,7 +237,7 @@ var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFol
func ProvideFolderPermissions(
cfg *setting.Cfg, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, accesscontrol accesscontrol.AccessControl,
license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service,
teamService team.Service, userService user.Service,
teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService,
) (*FolderPermissionsService, error) {
options := resourcepermissions.Options{
Resource: "folders",
@ -273,7 +273,7 @@ func ProvideFolderPermissions(
WriterRoleName: "Folder permission writer",
RoleGroup: "Folders",
}
srv, err := resourcepermissions.New(cfg, options, features, router, license, accesscontrol, service, sql, teamService, userService)
srv, err := resourcepermissions.New(cfg, options, features, router, license, accesscontrol, service, sql, teamService, userService, actionSetService)
if err != nil {
return nil, err
}
@ -337,7 +337,7 @@ type ServiceAccountPermissionsService struct {
func ProvideServiceAccountPermissions(
cfg *setting.Cfg, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl,
license licensing.Licensing, serviceAccountRetrieverService *retriever.Service, service accesscontrol.Service,
teamService team.Service, userService user.Service,
teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService,
) (*ServiceAccountPermissionsService, error) {
options := resourcepermissions.Options{
Resource: "serviceaccounts",
@ -364,7 +364,7 @@ func ProvideServiceAccountPermissions(
RoleGroup: "Service accounts",
}
srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService)
srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService, actionSetService)
if err != nil {
return nil, err
}

View File

@ -57,7 +57,7 @@ type Store interface {
func New(cfg *setting.Cfg,
options Options, features featuremgmt.FeatureToggles, router routing.RouteRegister, license licensing.Licensing,
ac accesscontrol.AccessControl, service accesscontrol.Service, sqlStore db.DB,
teamService team.Service, userService user.Service,
teamService team.Service, userService user.Service, actionSetService ActionSetService,
) (*Service, error) {
permissions := make([]string, 0, len(options.PermissionsToActions))
actionSet := make(map[string]struct{})
@ -66,6 +66,7 @@ func New(cfg *setting.Cfg,
for _, a := range actions {
actionSet[a] = struct{}{}
}
actionSetService.StoreActionSet(options.Resource, permission, actions)
}
// Sort all permissions based on action length. Will be used when mapping between actions to permissions
@ -80,7 +81,7 @@ func New(cfg *setting.Cfg,
s := &Service{
ac: ac,
store: NewStore(sqlStore, features),
store: NewStore(sqlStore, features, &actionSetService),
options: options,
license: license,
permissions: permissions,

View File

@ -246,7 +246,7 @@ func setupTestEnvironment(t *testing.T, ops Options) (*Service, db.DB, *setting.
acService := &actest.FakeService{}
service, err := New(
cfg, ops, featuremgmt.WithFeatures(), routing.NewRouteRegister(), license,
ac, acService, sql, teamSvc, userSvc,
ac, acService, sql, teamSvc, userSvc, NewActionSetService(),
)
require.NoError(t, err)

View File

@ -7,6 +7,7 @@ import (
"time"
"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/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
@ -16,13 +17,14 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func NewStore(sql db.DB, features featuremgmt.FeatureToggles) *store {
return &store{sql, features}
func NewStore(sql db.DB, features featuremgmt.FeatureToggles, actionsetService *ActionSetService) *store {
return &store{sql, features, *actionsetService}
}
type store struct {
sql db.DB
features featuremgmt.FeatureToggles
sql db.DB
features featuremgmt.FeatureToggles
actionSetService ActionSetService
}
type flatResourcePermission struct {
@ -266,7 +268,7 @@ func (s *store) setResourcePermission(
return nil, err
}
if err := s.createPermissions(sess, role.ID, cmd.Resource, cmd.ResourceID, cmd.ResourceAttribute, missing); err != nil {
if err := s.createPermissions(sess, role.ID, cmd.Resource, cmd.ResourceID, cmd.ResourceAttribute, missing, cmd.Permission); err != nil {
return nil, err
}
@ -657,11 +659,12 @@ func (s *store) getPermissions(sess *db.Session, resource, resourceID, resourceA
return result, nil
}
func (s *store) createPermissions(sess *db.Session, roleID int64, resource, resourceID, resourceAttribute string, actions map[string]struct{}) error {
func (s *store) createPermissions(sess *db.Session, roleID int64, resource, resourceID, resourceAttribute string, actions map[string]struct{}, permission string) error {
if len(actions) == 0 {
return nil
}
permissions := make([]accesscontrol.Permission, 0, len(actions))
for action := range actions {
p := managedPermission(action, resource, resourceID, resourceAttribute)
p.RoleID = roleID
@ -670,6 +673,18 @@ func (s *store) createPermissions(sess *db.Session, roleID int64, resource, reso
p.Kind, p.Attribute, p.Identifier = p.SplitScope()
permissions = append(permissions, p)
}
/*
Add ACTION SET of managed permissions to in-memory store
*/
if s.features.IsEnabled(context.TODO(), featuremgmt.FlagAccessActionSets) {
actionSetName := s.actionSetService.GetActionSetName(resource, permission)
p := managedPermission(actionSetName, resource, resourceID, resourceAttribute)
p.RoleID = roleID
p.Created = time.Now()
p.Updated = time.Now()
p.Kind, p.Attribute, p.Identifier = p.SplitScope()
permissions = append(permissions, p)
}
if _, err := sess.InsertMulti(&permissions); err != nil {
return err
@ -703,3 +718,63 @@ func managedPermission(action, resource string, resourceID, resourceAttribute st
Scope: accesscontrol.Scope(resource, resourceAttribute, resourceID),
}
}
/*
ACTION SETS
Stores actionsets IN MEMORY
*/
// ActionSet is a struct that represents a set of actions that can be performed on a resource.
// 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 {
GetActionSet(actionName string) []string
GetActionSetName(resource, permission string) string
StoreActionSet(resource, permission string, actions []string)
}
type ActionSet struct {
Action string `json:"action"`
Actions []string `json:"actions"`
}
// InMemoryActionSets is an in-memory implementation of the ActionSetService.
type InMemoryActionSets struct {
log log.Logger
actionSets 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"),
}
}
// GetActionSet returns the action set for the given action.
func (s *InMemoryActionSets) GetActionSet(actionName string) []string {
actionSet, ok := s.actionSets[actionName]
if !ok {
return nil
}
return actionSet
}
func (s *InMemoryActionSets) StoreActionSet(resource, permission string, actions []string) {
s.log.Debug("storing action set\n")
name := s.GetActionSetName(resource, permission)
actionSet := &ActionSet{
Action: name,
Actions: actions,
}
s.actionSets[actionSet.Action] = actions
s.log.Debug("stored action set actionname \n", 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 {
// lower cased
resource = strings.ToLower(resource)
permission = strings.ToLower(permission)
return fmt.Sprintf("%s:%s", resource, permission)
}

View File

@ -559,7 +559,8 @@ func seedResourcePermissions(
func setupTestEnv(t testing.TB) (*store, db.DB, *setting.Cfg) {
sql := db.InitTestDB(t)
return NewStore(sql, featuremgmt.WithFeatures()), sql, sql.Cfg
asService := NewActionSetService()
return NewStore(sql, featuremgmt.WithFeatures(), &asService), sql, sql.Cfg
}
func TestStore_IsInherited(t *testing.T) {
@ -753,3 +754,50 @@ func retrievePermissionsHelper(store *store, t *testing.T) []orgPermission {
require.NoError(t, err)
return permissions
}
func TestStore_ResourcePermissionsActionSets(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
type actionSetTest struct {
desc string
orgID int64
actionSet ActionSet
}
tests := []actionSetTest{
{
desc: "should be able to store actionset",
orgID: 1,
actionSet: ActionSet{
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})
_, 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)
})
}
}

View File

@ -107,8 +107,9 @@ 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())
permissionsStore := resourcepermissions.NewStore(env.SQLStore, featuremgmt.WithFeatures(), &asService)
_, err := permissionsStore.SetUserResourcePermission(context.Background(),
accesscontrol.GlobalOrgID,
accesscontrol.User{ID: testUserId},

View File

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

View File

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

View File

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

View File

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