Skip to content

Commit

Permalink
RBAC: Add actionsets struct and write path (grafana#86108)
Browse files Browse the repository at this point in the history
* 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 e3d3cc8.

---------

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
  • Loading branch information
eleijonmarck and IevaVasiljeva authored Apr 19, 2024
1 parent a057e8b commit ddabef9
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 29 deletions.
5 changes: 3 additions & 2 deletions pkg/api/folder_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions pkg/server/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -379,6 +380,7 @@ var wireBasicSet = wire.NewSet(
// Kubernetes API server
grafanaapiserver.WireSet,
apiregistry.WireSet,
resourcepermissions.NewActionSetService,
)

var wireSet = wire.NewSet(
Expand Down
3 changes: 2 additions & 1 deletion pkg/services/accesscontrol/database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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",
Expand All @@ -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
}
Expand Down
5 changes: 3 additions & 2 deletions pkg/services/accesscontrol/resourcepermissions/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand All @@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
87 changes: 81 additions & 6 deletions pkg/services/accesscontrol/resourcepermissions/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
50 changes: 49 additions & 1 deletion pkg/services/accesscontrol/resourcepermissions/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
})
}
}
3 changes: 2 additions & 1 deletion pkg/tests/api/alerting/api_backtesting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
3 changes: 2 additions & 1 deletion pkg/tests/api/alerting/api_prometheus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading

0 comments on commit ddabef9

Please sign in to comment.