diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index a5bef4a4506..b53c9098d01 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -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( diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 4f0c3323d43..56fa778bf6a 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -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( diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index 5cbfae4633d..4c3c2e78aca 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -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)) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index f3910860cb0..693efe7053f 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -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 } diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go index aaabb2ad9c2..4a6f27daec3 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service.go +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -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, diff --git a/pkg/services/accesscontrol/resourcepermissions/service_test.go b/pkg/services/accesscontrol/resourcepermissions/service_test.go index 1cae9fa0b69..8aa2108dc2d 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/service_test.go @@ -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) diff --git a/pkg/services/accesscontrol/resourcepermissions/store.go b/pkg/services/accesscontrol/resourcepermissions/store.go index 33359fdfa4b..f9fc903ae14 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store.go +++ b/pkg/services/accesscontrol/resourcepermissions/store.go @@ -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) +} diff --git a/pkg/services/accesscontrol/resourcepermissions/store_test.go b/pkg/services/accesscontrol/resourcepermissions/store_test.go index e7320798ee8..5774e74adad 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/store_test.go @@ -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) + }) + } +} diff --git a/pkg/tests/api/alerting/api_backtesting_test.go b/pkg/tests/api/alerting/api_backtesting_test.go index 79a9b4ea629..363339af168 100644 --- a/pkg/tests/api/alerting/api_backtesting_test.go +++ b/pkg/tests/api/alerting/api_backtesting_test.go @@ -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}, diff --git a/pkg/tests/api/alerting/api_prometheus_test.go b/pkg/tests/api/alerting/api_prometheus_test.go index a420af56245..da3b58a8aa3 100644 --- a/pkg/tests/api/alerting/api_prometheus_test.go +++ b/pkg/tests/api/alerting/api_prometheus_test.go @@ -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") diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index e75a49a43ab..fb160313cea 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -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{ diff --git a/pkg/tests/api/alerting/api_testing_test.go b/pkg/tests/api/alerting/api_testing_test.go index bbe1219d001..138be67213d 100644 --- a/pkg/tests/api/alerting/api_testing_test.go +++ b/pkg/tests/api/alerting/api_testing_test.go @@ -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}, diff --git a/pkg/tests/api/folders/api_folders_test.go b/pkg/tests/api/folders/api_folders_test.go index 45a10babc99..f42d1dcb112 100644 --- a/pkg/tests/api/folders/api_folders_test.go +++ b/pkg/tests/api/folders/api_folders_test.go @@ -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