diff --git a/pkg/api/folder.go b/pkg/api/folder.go index d8709daf367..abe935889ce 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -188,6 +188,9 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response } func (hs *HTTPServer) setDefaultFolderPermissions(ctx context.Context, orgID int64, user identity.Requester, folder *folder.Folder) error { + if !hs.Cfg.RBAC.PermissionsOnCreation("folder") { + return nil + } var permissions []accesscontrol.SetResourcePermissionCommand var userID int64 diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 2501b2c615d..556eb1c28e1 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -550,7 +550,7 @@ func (hs *HTTPServer) hasPluginRequestedPermissions(c *contextmodel.ReqContext, hs.log.Debug("check installer's permissions, plugin wants to register an external service") evaluator := evalAllPermissions(plugin.JSONData.IAM.Permissions) hasAccess := ac.HasGlobalAccess(hs.AccessControl, hs.authnService, c) - if hs.Cfg.RBACSingleOrganization { + if hs.Cfg.RBAC.SingleOrganization { // In a single organization setup, no need for a global check hasAccess = ac.HasAccess(hs.AccessControl, c) } diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 2b01e7427bb..3258000d063 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -87,7 +87,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) { hs.Cfg = setting.NewCfg() hs.Cfg.PluginAdminEnabled = tc.pluginAdminEnabled hs.Cfg.PluginAdminExternalManageEnabled = tc.pluginAdminExternalManageEnabled - hs.Cfg.RBACSingleOrganization = tc.singleOrganization + hs.Cfg.RBAC.SingleOrganization = tc.singleOrganization hs.orgService = &orgtest.FakeOrgService{ExpectedOrg: &org.Org{}} hs.accesscontrolService = &actest.FakeService{} @@ -743,7 +743,7 @@ func TestHTTPServer_hasPluginRequestedPermissions(t *testing.T) { require.NoError(t, err) hs.Cfg = setting.NewCfg() - hs.Cfg.RBACSingleOrganization = tt.singleOrg + hs.Cfg.RBAC.SingleOrganization = tt.singleOrg hs.pluginStore = &pluginstore.FakePluginStore{ PluginList: []pluginstore.Plugin{tt.plugin}, } diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 41fe41bd214..485c98dfe36 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -362,6 +362,20 @@ func GetOrgRoles(user identity.Requester) []string { return roles } +// PermissionsForActions generate Permissions for all actions provided scoped to provided scope. +func PermissionsForActions(actions []string, scope string) []Permission { + permissions := make([]Permission, len(actions)) + + for i, action := range actions { + permissions[i] = Permission{ + Action: action, + Scope: scope, + } + } + + return permissions +} + func BackgroundUser(name string, orgID int64, role org.RoleType, permissions []Permission) identity.Requester { return &user.SignedInUser{ OrgID: orgID, diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 6b2b9f3f074..31512dfbf5f 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -119,7 +119,7 @@ func (s *Service) GetUserPermissions(ctx context.Context, user identity.Requeste timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary) defer timer.ObserveDuration() - if !s.cfg.RBACPermissionCache || !user.HasUniqueId() { + if !s.cfg.RBAC.PermissionCache || !user.HasUniqueId() { return s.getUserPermissions(ctx, user, options) } diff --git a/pkg/services/accesscontrol/middleware.go b/pkg/services/accesscontrol/middleware.go index 59e117a0895..3fd16fa8807 100644 --- a/pkg/services/accesscontrol/middleware.go +++ b/pkg/services/accesscontrol/middleware.go @@ -233,7 +233,7 @@ func UseGlobalOrg(c *contextmodel.ReqContext) (int64, error) { // UseGlobalOrSingleOrg returns the global organization or the current organization in a single organization setup func UseGlobalOrSingleOrg(cfg *setting.Cfg) OrgIDGetter { return func(c *contextmodel.ReqContext) (int64, error) { - if cfg.RBACSingleOrganization { + if cfg.RBAC.SingleOrganization { return c.GetOrgID(), nil } return GlobalOrgID, nil @@ -271,7 +271,7 @@ func UseGlobalOrgFromRequestData(cfg *setting.Cfg) OrgIDGetter { // We only check permissions in the global organization if we are not running a SingleOrganization setup // That allows Organization Admins to modify global roles and make global assignments. - if query.Global && !cfg.RBACSingleOrganization { + if query.Global && !cfg.RBAC.SingleOrganization { return GlobalOrgID, nil } @@ -284,7 +284,7 @@ func UseGlobalOrgFromRequestParams(cfg *setting.Cfg) OrgIDGetter { return func(c *contextmodel.ReqContext) (int64, error) { // We only check permissions in the global organization if we are not running a SingleOrganization setup // That allows Organization Admins to modify global roles and make global assignments, and is intended for use in hosted Grafana. - if c.QueryBool("global") && !cfg.RBACSingleOrganization { + if c.QueryBool("global") && !cfg.RBAC.SingleOrganization { return GlobalOrgID, nil } diff --git a/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go b/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go new file mode 100644 index 00000000000..8f4636c7564 --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go @@ -0,0 +1,177 @@ +package ossaccesscontrol + +import ( + "context" + "errors" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "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/folder" + "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/services/team" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" +) + +type DashboardPermissionsService struct { + *resourcepermissions.Service +} + +var DashboardViewActions = []string{dashboards.ActionDashboardsRead} +var DashboardEditActions = append(DashboardViewActions, []string{dashboards.ActionDashboardsWrite, dashboards.ActionDashboardsDelete}...) +var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.ActionDashboardsPermissionsRead, dashboards.ActionDashboardsPermissionsWrite}...) + +func getDashboardViewActions(features featuremgmt.FeatureToggles) []string { + if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { + return append(DashboardViewActions, accesscontrol.ActionAnnotationsRead) + } + return DashboardViewActions +} + +func getDashboardEditActions(features featuremgmt.FeatureToggles) []string { + if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { + return append(DashboardEditActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...) + } + return DashboardEditActions +} + +func getDashboardAdminActions(features featuremgmt.FeatureToggles) []string { + if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { + return append(DashboardAdminActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...) + } + return DashboardAdminActions +} + +func registerDashboardRoles(cfg *setting.Cfg, features featuremgmt.FeatureToggles, service accesscontrol.Service) error { + if !cfg.RBAC.PermissionsWildcardSeed("dashboard") { + return nil + } + + viewer := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:dashboards:viewer", + DisplayName: "Viewer", + Description: "View all dashboards", + Group: "Dashboards", + Permissions: accesscontrol.PermissionsForActions(getDashboardViewActions(features), dashboards.ScopeDashboardsAll), + Hidden: true, + }, + Grants: []string{"Viewer"}, + } + + editor := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:dashboards:editor", + DisplayName: "Editor", + Description: "Edit all dashboards.", + Group: "Dashboards", + Permissions: accesscontrol.PermissionsForActions(getDashboardEditActions(features), dashboards.ScopeDashboardsAll), + Hidden: true, + }, + Grants: []string{"Editor"}, + } + + admin := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:dashboards:admin", + DisplayName: "Admin", + Description: "Administer all dashboards.", + Group: "Dashboards", + Permissions: accesscontrol.PermissionsForActions(getDashboardAdminActions(features), dashboards.ScopeDashboardsAll), + Hidden: true, + }, + Grants: []string{"Admin"}, + } + + return service.DeclareFixedRoles(viewer, editor, admin) +} + +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, actionSetService resourcepermissions.ActionSetService, +) (*DashboardPermissionsService, error) { + getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*dashboards.Dashboard, error) { + query := &dashboards.GetDashboardQuery{UID: resourceID, OrgID: orgID} + queryResult, err := dashboardStore.GetDashboard(ctx, query) + if err != nil { + return nil, err + } + return queryResult, nil + } + + if err := registerDashboardRoles(cfg, features, service); err != nil { + return nil, err + } + + options := resourcepermissions.Options{ + Resource: "dashboards", + ResourceAttribute: "uid", + ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { + dashboard, err := getDashboard(ctx, orgID, resourceID) + if err != nil { + return err + } + + if dashboard.IsFolder { + return errors.New("not found") + } + + return nil + }, + InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) { + wildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix) + scopes := []string(wildcards) + + dashboard, err := getDashboard(ctx, orgID, resourceID) + if err != nil { + return nil, err + } + metrics.MFolderIDsServiceCount.WithLabelValues(metrics.AccessControl).Inc() + // nolint:staticcheck + if dashboard.FolderUID != "" { + query := &dashboards.GetDashboardQuery{UID: dashboard.FolderUID, OrgID: orgID} + queryResult, err := dashboardStore.GetDashboard(ctx, query) + if err != nil { + return nil, err + } + parentScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(queryResult.UID) + + nestedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, queryResult.UID, folderService) + if err != nil { + return nil, err + } + + scopes = append(scopes, parentScope) + scopes = append(scopes, nestedScopes...) + return scopes, nil + } + return append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)), nil + }, + Assignments: resourcepermissions.Assignments{ + Users: true, + Teams: true, + BuiltInRoles: true, + ServiceAccounts: true, + }, + PermissionsToActions: map[string][]string{ + "View": getDashboardViewActions(features), + "Edit": getDashboardEditActions(features), + "Admin": getDashboardAdminActions(features), + }, + ReaderRoleName: "Dashboard permission reader", + WriterRoleName: "Dashboard permission writer", + RoleGroup: "Dashboards", + } + + srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + return &DashboardPermissionsService{srv}, nil +} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/datasource.go b/pkg/services/accesscontrol/ossaccesscontrol/datasource.go new file mode 100644 index 00000000000..d51fcbe8ceb --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/datasource.go @@ -0,0 +1,87 @@ +package ossaccesscontrol + +import ( + "context" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" +) + +// DatasourceQueryActions contains permissions to read information +// about a data source and submit arbitrary queries to it. +var DatasourceQueryActions = []string{ + datasources.ActionRead, + datasources.ActionQuery, +} + +func ProvideDatasourcePermissionsService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, db db.DB) *DatasourcePermissionsService { + return &DatasourcePermissionsService{ + store: resourcepermissions.NewStore(cfg, db, features), + } +} + +var _ accesscontrol.DatasourcePermissionsService = new(DatasourcePermissionsService) + +type DatasourcePermissionsService struct { + store resourcepermissions.Store +} + +func (e DatasourcePermissionsService) GetPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) { + return nil, nil +} + +func (e DatasourcePermissionsService) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { + return nil, nil +} + +func (e DatasourcePermissionsService) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { + return nil, nil +} + +func (e DatasourcePermissionsService) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, permission string) (*accesscontrol.ResourcePermission, error) { + return nil, nil +} + +// SetPermissions sets managed permissions for a datasource in OSS. This ensures that Viewers and Editors maintain query access to a data source +// if an OSS/unlicensed instance is upgraded to Enterprise/licensed. +// https://github.com/grafana/identity-access-team/issues/672 +func (e DatasourcePermissionsService) SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) { + dbCommands := make([]resourcepermissions.SetResourcePermissionsCommand, 0, len(commands)) + for _, cmd := range commands { + // Only set query permissions for built-in roles; do not set permissions for data sources with * as UID, as this would grant wildcard permissions + if cmd.Permission != "Query" || cmd.BuiltinRole == "" || resourceID == "*" { + continue + } + actions := DatasourceQueryActions + + dbCommands = append(dbCommands, resourcepermissions.SetResourcePermissionsCommand{ + BuiltinRole: cmd.BuiltinRole, + SetResourcePermissionCommand: resourcepermissions.SetResourcePermissionCommand{ + Actions: actions, + Resource: datasources.ScopeRoot, + ResourceID: resourceID, + ResourceAttribute: "uid", + Permission: cmd.Permission, + }, + }) + } + + return e.store.SetResourcePermissions(ctx, orgID, dbCommands, resourcepermissions.ResourceHooks{}) +} + +func (e DatasourcePermissionsService) DeleteResourcePermissions(ctx context.Context, orgID int64, resourceID string) error { + return e.store.DeleteResourcePermissions(ctx, orgID, &resourcepermissions.DeleteResourcePermissionsCmd{ + Resource: datasources.ScopeRoot, + ResourceAttribute: "uid", + ResourceID: resourceID, + }) +} + +func (e DatasourcePermissionsService) MapActions(permission accesscontrol.ResourcePermission) string { + return "" +} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/folder.go b/pkg/services/accesscontrol/ossaccesscontrol/folder.go new file mode 100644 index 00000000000..b24e3ce5ef5 --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/folder.go @@ -0,0 +1,133 @@ +package ossaccesscontrol + +import ( + "context" + "errors" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "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/folder" + "github.com/grafana/grafana/pkg/services/libraryelements" + "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/services/team" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" +) + +type FolderPermissionsService struct { + *resourcepermissions.Service +} + +var FolderViewActions = []string{dashboards.ActionFoldersRead, accesscontrol.ActionAlertingRuleRead, libraryelements.ActionLibraryPanelsRead, accesscontrol.ActionAlertingSilencesRead} +var FolderEditActions = append(FolderViewActions, []string{ + dashboards.ActionFoldersWrite, + dashboards.ActionFoldersDelete, + dashboards.ActionDashboardsCreate, + accesscontrol.ActionAlertingRuleCreate, + accesscontrol.ActionAlertingRuleUpdate, + accesscontrol.ActionAlertingRuleDelete, + accesscontrol.ActionAlertingSilencesCreate, + accesscontrol.ActionAlertingSilencesWrite, + libraryelements.ActionLibraryPanelsCreate, + libraryelements.ActionLibraryPanelsWrite, + libraryelements.ActionLibraryPanelsDelete, +}...) +var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFoldersPermissionsRead, dashboards.ActionFoldersPermissionsWrite}...) + +func registerFolderRoles(cfg *setting.Cfg, features featuremgmt.FeatureToggles, service accesscontrol.Service) error { + if !cfg.RBAC.PermissionsWildcardSeed("folder") { + return nil + } + + viewer := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:folders:viewer", + DisplayName: "Viewer", + Description: "View all folders and dashboards.", + Group: "Folders", + Permissions: accesscontrol.PermissionsForActions(append(getDashboardViewActions(features), FolderViewActions...), dashboards.ScopeFoldersAll), + Hidden: true, + }, + Grants: []string{"Viewer"}, + } + + editor := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:folders:editor", + DisplayName: "Editor", + Description: "Edit all folders and dashboards.", + Group: "Folders", + Permissions: accesscontrol.PermissionsForActions(append(getDashboardEditActions(features), FolderEditActions...), dashboards.ScopeFoldersAll), + Hidden: true, + }, + Grants: []string{"Editor"}, + } + + admin := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: "fixed:folders:admin", + DisplayName: "Admin", + Description: "Administer all folders and dashboards", + Group: "folders", + Permissions: accesscontrol.PermissionsForActions(append(getDashboardAdminActions(features), FolderAdminActions...), dashboards.ScopeFoldersAll), + Hidden: true, + }, + Grants: []string{"Admin"}, + } + + return service.DeclareFixedRoles(viewer, editor, admin) +} + +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, actionSetService resourcepermissions.ActionSetService, +) (*FolderPermissionsService, error) { + if err := registerFolderRoles(cfg, features, service); err != nil { + return nil, err + } + + options := resourcepermissions.Options{ + Resource: "folders", + ResourceAttribute: "uid", + ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { + query := &dashboards.GetDashboardQuery{UID: resourceID, OrgID: orgID} + queryResult, err := dashboardStore.GetDashboard(ctx, query) + if err != nil { + return err + } + + if !queryResult.IsFolder { + return errors.New("not found") + } + + return nil + }, + InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) { + return dashboards.GetInheritedScopes(ctx, orgID, resourceID, folderService) + }, + Assignments: resourcepermissions.Assignments{ + Users: true, + Teams: true, + BuiltInRoles: true, + ServiceAccounts: true, + }, + PermissionsToActions: map[string][]string{ + "View": append(getDashboardViewActions(features), FolderViewActions...), + "Edit": append(getDashboardEditActions(features), FolderEditActions...), + "Admin": append(getDashboardAdminActions(features), FolderAdminActions...), + }, + ReaderRoleName: "Folder permission reader", + WriterRoleName: "Folder permission writer", + RoleGroup: "Folders", + } + srv, err := resourcepermissions.New(cfg, options, features, router, license, accesscontrol, service, sql, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + return &FolderPermissionsService{srv}, nil +} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go deleted file mode 100644 index 02361171c82..00000000000 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ /dev/null @@ -1,417 +0,0 @@ -package ossaccesscontrol - -import ( - "context" - "errors" - "fmt" - "strconv" - - "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" - "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/libraryelements" - "github.com/grafana/grafana/pkg/services/licensing" - "github.com/grafana/grafana/pkg/services/serviceaccounts" - "github.com/grafana/grafana/pkg/services/serviceaccounts/retriever" - "github.com/grafana/grafana/pkg/services/team" - "github.com/grafana/grafana/pkg/services/team/teamimpl" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" -) - -type TeamPermissionsService struct { - *resourcepermissions.Service -} - -var ( - TeamMemberActions = []string{ - accesscontrol.ActionTeamsRead, - } - - TeamAdminActions = []string{ - accesscontrol.ActionTeamsRead, - accesscontrol.ActionTeamsDelete, - accesscontrol.ActionTeamsWrite, - accesscontrol.ActionTeamsPermissionsRead, - accesscontrol.ActionTeamsPermissionsWrite, - } -) - -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, actionSetService resourcepermissions.ActionSetService, -) (*TeamPermissionsService, error) { - options := resourcepermissions.Options{ - Resource: "teams", - ResourceAttribute: "id", - OnlyManaged: true, - ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { - id, err := strconv.ParseInt(resourceID, 10, 64) - if err != nil { - return err - } - - _, err = teamService.GetTeamByID(context.Background(), &team.GetTeamByIDQuery{ - OrgID: orgID, - ID: id, - }) - if err != nil { - return err - } - - return nil - }, - Assignments: resourcepermissions.Assignments{ - Users: true, - Teams: false, - BuiltInRoles: false, - }, - PermissionsToActions: map[string][]string{ - "Member": TeamMemberActions, - "Admin": TeamAdminActions, - }, - ReaderRoleName: "Team permission reader", - WriterRoleName: "Team permission writer", - RoleGroup: "Teams", - OnSetUser: func(session *db.Session, orgID int64, user accesscontrol.User, resourceID, permission string) error { - teamId, err := strconv.ParseInt(resourceID, 10, 64) - if err != nil { - return err - } - switch permission { - case "Member": - return teamimpl.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, 0) - case "Admin": - return teamimpl.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, dashboardaccess.PERMISSION_ADMIN) - case "": - return teamimpl.RemoveTeamMemberHook(session, &team.RemoveTeamMemberCommand{ - OrgID: orgID, - UserID: user.ID, - TeamID: teamId, - }) - default: - return fmt.Errorf("invalid team permission type %s", permission) - } - }, - } - - srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService, actionSetService) - if err != nil { - return nil, err - } - return &TeamPermissionsService{srv}, nil -} - -type DashboardPermissionsService struct { - *resourcepermissions.Service -} - -var DashboardViewActions = []string{dashboards.ActionDashboardsRead} -var DashboardEditActions = append(DashboardViewActions, []string{dashboards.ActionDashboardsWrite, dashboards.ActionDashboardsDelete}...) -var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.ActionDashboardsPermissionsRead, dashboards.ActionDashboardsPermissionsWrite}...) - -func getDashboardViewActions(features featuremgmt.FeatureToggles) []string { - if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { - return append(DashboardViewActions, accesscontrol.ActionAnnotationsRead) - } - return DashboardViewActions -} - -func getDashboardEditActions(features featuremgmt.FeatureToggles) []string { - if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { - return append(DashboardEditActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...) - } - return DashboardEditActions -} - -func getDashboardAdminActions(features featuremgmt.FeatureToggles) []string { - if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { - return append(DashboardAdminActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...) - } - return DashboardAdminActions -} - -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, actionSetService resourcepermissions.ActionSetService, -) (*DashboardPermissionsService, error) { - getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*dashboards.Dashboard, error) { - query := &dashboards.GetDashboardQuery{UID: resourceID, OrgID: orgID} - queryResult, err := dashboardStore.GetDashboard(ctx, query) - if err != nil { - return nil, err - } - return queryResult, nil - } - - options := resourcepermissions.Options{ - Resource: "dashboards", - ResourceAttribute: "uid", - ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { - dashboard, err := getDashboard(ctx, orgID, resourceID) - if err != nil { - return err - } - - if dashboard.IsFolder { - return errors.New("not found") - } - - return nil - }, - InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) { - wildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix) - scopes := []string(wildcards) - - dashboard, err := getDashboard(ctx, orgID, resourceID) - if err != nil { - return nil, err - } - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.AccessControl).Inc() - // nolint:staticcheck - if dashboard.FolderUID != "" { - query := &dashboards.GetDashboardQuery{UID: dashboard.FolderUID, OrgID: orgID} - queryResult, err := dashboardStore.GetDashboard(ctx, query) - if err != nil { - return nil, err - } - parentScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(queryResult.UID) - - nestedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, queryResult.UID, folderService) - if err != nil { - return nil, err - } - - scopes = append(scopes, parentScope) - scopes = append(scopes, nestedScopes...) - return scopes, nil - } - return append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)), nil - }, - Assignments: resourcepermissions.Assignments{ - Users: true, - Teams: true, - BuiltInRoles: true, - ServiceAccounts: true, - }, - PermissionsToActions: map[string][]string{ - "View": getDashboardViewActions(features), - "Edit": getDashboardEditActions(features), - "Admin": getDashboardAdminActions(features), - }, - ReaderRoleName: "Dashboard permission reader", - WriterRoleName: "Dashboard permission writer", - RoleGroup: "Dashboards", - } - - srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService, actionSetService) - if err != nil { - return nil, err - } - return &DashboardPermissionsService{srv}, nil -} - -type FolderPermissionsService struct { - *resourcepermissions.Service -} - -var FolderViewActions = []string{dashboards.ActionFoldersRead, accesscontrol.ActionAlertingRuleRead, libraryelements.ActionLibraryPanelsRead, accesscontrol.ActionAlertingSilencesRead} -var FolderEditActions = append(FolderViewActions, []string{ - dashboards.ActionFoldersWrite, - dashboards.ActionFoldersDelete, - dashboards.ActionDashboardsCreate, - accesscontrol.ActionAlertingRuleCreate, - accesscontrol.ActionAlertingRuleUpdate, - accesscontrol.ActionAlertingRuleDelete, - accesscontrol.ActionAlertingSilencesCreate, - accesscontrol.ActionAlertingSilencesWrite, - libraryelements.ActionLibraryPanelsCreate, - libraryelements.ActionLibraryPanelsWrite, - libraryelements.ActionLibraryPanelsDelete, -}...) -var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFoldersPermissionsRead, dashboards.ActionFoldersPermissionsWrite}...) - -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, actionSetService resourcepermissions.ActionSetService, -) (*FolderPermissionsService, error) { - options := resourcepermissions.Options{ - Resource: "folders", - ResourceAttribute: "uid", - ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { - query := &dashboards.GetDashboardQuery{UID: resourceID, OrgID: orgID} - queryResult, err := dashboardStore.GetDashboard(ctx, query) - if err != nil { - return err - } - - if !queryResult.IsFolder { - return errors.New("not found") - } - - return nil - }, - InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) { - return dashboards.GetInheritedScopes(ctx, orgID, resourceID, folderService) - }, - Assignments: resourcepermissions.Assignments{ - Users: true, - Teams: true, - BuiltInRoles: true, - ServiceAccounts: true, - }, - PermissionsToActions: map[string][]string{ - "View": append(getDashboardViewActions(features), FolderViewActions...), - "Edit": append(getDashboardEditActions(features), FolderEditActions...), - "Admin": append(getDashboardAdminActions(features), FolderAdminActions...), - }, - ReaderRoleName: "Folder permission reader", - WriterRoleName: "Folder permission writer", - RoleGroup: "Folders", - } - srv, err := resourcepermissions.New(cfg, options, features, router, license, accesscontrol, service, sql, teamService, userService, actionSetService) - if err != nil { - return nil, err - } - return &FolderPermissionsService{srv}, nil -} - -// DatasourceQueryActions contains permissions to read information -// about a data source and submit arbitrary queries to it. -var DatasourceQueryActions = []string{ - datasources.ActionRead, - datasources.ActionQuery, -} - -func ProvideDatasourcePermissionsService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, db db.DB) *DatasourcePermissionsService { - return &DatasourcePermissionsService{ - store: resourcepermissions.NewStore(cfg, db, features), - } -} - -var _ accesscontrol.DatasourcePermissionsService = new(DatasourcePermissionsService) - -type DatasourcePermissionsService struct { - store resourcepermissions.Store -} - -func (e DatasourcePermissionsService) GetPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) { - return nil, nil -} - -func (e DatasourcePermissionsService) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { - return nil, nil -} - -func (e DatasourcePermissionsService) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { - return nil, nil -} - -func (e DatasourcePermissionsService) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, permission string) (*accesscontrol.ResourcePermission, error) { - return nil, nil -} - -// SetPermissions sets managed permissions for a datasource in OSS. This ensures that Viewers and Editors maintain query access to a data source -// if an OSS/unlicensed instance is upgraded to Enterprise/licensed. -// https://github.com/grafana/identity-access-team/issues/672 -func (e DatasourcePermissionsService) SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) { - dbCommands := make([]resourcepermissions.SetResourcePermissionsCommand, 0, len(commands)) - for _, cmd := range commands { - // Only set query permissions for built-in roles; do not set permissions for data sources with * as UID, as this would grant wildcard permissions - if cmd.Permission != "Query" || cmd.BuiltinRole == "" || resourceID == "*" { - continue - } - actions := DatasourceQueryActions - - dbCommands = append(dbCommands, resourcepermissions.SetResourcePermissionsCommand{ - BuiltinRole: cmd.BuiltinRole, - SetResourcePermissionCommand: resourcepermissions.SetResourcePermissionCommand{ - Actions: actions, - Resource: datasources.ScopeRoot, - ResourceID: resourceID, - ResourceAttribute: "uid", - Permission: cmd.Permission, - }, - }) - } - - return e.store.SetResourcePermissions(ctx, orgID, dbCommands, resourcepermissions.ResourceHooks{}) -} - -func (e DatasourcePermissionsService) DeleteResourcePermissions(ctx context.Context, orgID int64, resourceID string) error { - return e.store.DeleteResourcePermissions(ctx, orgID, &resourcepermissions.DeleteResourcePermissionsCmd{ - Resource: datasources.ScopeRoot, - ResourceAttribute: "uid", - ResourceID: resourceID, - }) -} - -func (e DatasourcePermissionsService) MapActions(permission accesscontrol.ResourcePermission) string { - return "" -} - -var ( - ServiceAccountEditActions = []string{ - serviceaccounts.ActionRead, - serviceaccounts.ActionWrite, - } - ServiceAccountAdminActions = []string{ - serviceaccounts.ActionRead, - serviceaccounts.ActionWrite, - serviceaccounts.ActionDelete, - serviceaccounts.ActionPermissionsRead, - serviceaccounts.ActionPermissionsWrite, - } -) - -type ServiceAccountPermissionsService struct { - *resourcepermissions.Service -} - -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, actionSetService resourcepermissions.ActionSetService, -) (*ServiceAccountPermissionsService, error) { - options := resourcepermissions.Options{ - Resource: "serviceaccounts", - ResourceAttribute: "id", - ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { - id, err := strconv.ParseInt(resourceID, 10, 64) - if err != nil { - return err - } - _, err = serviceAccountRetrieverService.RetrieveServiceAccount(ctx, orgID, id) - return err - }, - Assignments: resourcepermissions.Assignments{ - Users: true, - Teams: true, - BuiltInRoles: false, - }, - PermissionsToActions: map[string][]string{ - "Edit": ServiceAccountEditActions, - "Admin": ServiceAccountAdminActions, - }, - ReaderRoleName: "Service account permission reader", - WriterRoleName: "Service account permission writer", - RoleGroup: "Service accounts", - } - - srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService, actionSetService) - if err != nil { - return nil, err - } - return &ServiceAccountPermissionsService{srv}, nil -} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/service_account.go b/pkg/services/accesscontrol/ossaccesscontrol/service_account.go new file mode 100644 index 00000000000..077ed4609f0 --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/service_account.go @@ -0,0 +1,73 @@ +package ossaccesscontrol + +import ( + "context" + "strconv" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/retriever" + "github.com/grafana/grafana/pkg/services/team" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + ServiceAccountEditActions = []string{ + serviceaccounts.ActionRead, + serviceaccounts.ActionWrite, + } + ServiceAccountAdminActions = []string{ + serviceaccounts.ActionRead, + serviceaccounts.ActionWrite, + serviceaccounts.ActionDelete, + serviceaccounts.ActionPermissionsRead, + serviceaccounts.ActionPermissionsWrite, + } +) + +type ServiceAccountPermissionsService struct { + *resourcepermissions.Service +} + +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, actionSetService resourcepermissions.ActionSetService, +) (*ServiceAccountPermissionsService, error) { + options := resourcepermissions.Options{ + Resource: "serviceaccounts", + ResourceAttribute: "id", + ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { + id, err := strconv.ParseInt(resourceID, 10, 64) + if err != nil { + return err + } + _, err = serviceAccountRetrieverService.RetrieveServiceAccount(ctx, orgID, id) + return err + }, + Assignments: resourcepermissions.Assignments{ + Users: true, + Teams: true, + BuiltInRoles: false, + }, + PermissionsToActions: map[string][]string{ + "Edit": ServiceAccountEditActions, + "Admin": ServiceAccountAdminActions, + }, + ReaderRoleName: "Service account permission reader", + WriterRoleName: "Service account permission writer", + RoleGroup: "Service accounts", + } + + srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + return &ServiceAccountPermissionsService{srv}, nil +} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/team.go b/pkg/services/accesscontrol/ossaccesscontrol/team.go new file mode 100644 index 00000000000..b6c185895d5 --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/team.go @@ -0,0 +1,103 @@ +package ossaccesscontrol + +import ( + "context" + "fmt" + "strconv" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/services/team" + "github.com/grafana/grafana/pkg/services/team/teamimpl" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" +) + +type TeamPermissionsService struct { + *resourcepermissions.Service +} + +var ( + TeamMemberActions = []string{ + accesscontrol.ActionTeamsRead, + } + + TeamAdminActions = []string{ + accesscontrol.ActionTeamsRead, + accesscontrol.ActionTeamsDelete, + accesscontrol.ActionTeamsWrite, + accesscontrol.ActionTeamsPermissionsRead, + accesscontrol.ActionTeamsPermissionsWrite, + } +) + +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, actionSetService resourcepermissions.ActionSetService, +) (*TeamPermissionsService, error) { + options := resourcepermissions.Options{ + Resource: "teams", + ResourceAttribute: "id", + OnlyManaged: true, + ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { + id, err := strconv.ParseInt(resourceID, 10, 64) + if err != nil { + return err + } + + _, err = teamService.GetTeamByID(context.Background(), &team.GetTeamByIDQuery{ + OrgID: orgID, + ID: id, + }) + if err != nil { + return err + } + + return nil + }, + Assignments: resourcepermissions.Assignments{ + Users: true, + Teams: false, + BuiltInRoles: false, + }, + PermissionsToActions: map[string][]string{ + "Member": TeamMemberActions, + "Admin": TeamAdminActions, + }, + ReaderRoleName: "Team permission reader", + WriterRoleName: "Team permission writer", + RoleGroup: "Teams", + OnSetUser: func(session *db.Session, orgID int64, user accesscontrol.User, resourceID, permission string) error { + teamId, err := strconv.ParseInt(resourceID, 10, 64) + if err != nil { + return err + } + switch permission { + case "Member": + return teamimpl.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, 0) + case "Admin": + return teamimpl.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, dashboardaccess.PERMISSION_ADMIN) + case "": + return teamimpl.RemoveTeamMemberHook(session, &team.RemoveTeamMemberCommand{ + OrgID: orgID, + UserID: user.ID, + TeamID: teamId, + }) + default: + return fmt.Errorf("invalid team permission type %s", permission) + } + }, + } + + srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + return &TeamPermissionsService{srv}, nil +} diff --git a/pkg/services/accesscontrol/resourcepermissions/store.go b/pkg/services/accesscontrol/resourcepermissions/store.go index ef19434c420..208aa5d5fbc 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store.go +++ b/pkg/services/accesscontrol/resourcepermissions/store.go @@ -691,7 +691,7 @@ func (s *store) createPermissions(sess *db.Session, roleID int64, cmd SetResourc // if we have actionset feature enabled and are only working with action sets // skip adding the missing actions to the permissions table - if !(s.shouldStoreActionSet(resource, permission) && s.cfg.OnlyStoreAccessActionSets) { + if !(s.shouldStoreActionSet(resource, permission) && s.cfg.RBAC.OnlyStoreAccessActionSets) { for action := range missingActions { p := managedPermission(action, resource, resourceID, resourceAttribute) p.RoleID = roleID diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index cacdaec4c50..6937a1ea4ad 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -488,6 +488,15 @@ func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, que } func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *dashboards.Dashboard, provisioned bool) { + resource := "dashboard" + if dash.IsFolder { + resource = "folder" + } + + if !dr.cfg.RBAC.PermissionsOnCreation(resource) { + return + } + metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() // nolint:staticcheck inFolder := dash.FolderID > 0 @@ -524,6 +533,10 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto * } func (dr *DashboardServiceImpl) setDefaultFolderPermissions(ctx context.Context, cmd *folder.CreateFolderCommand, f *folder.Folder, provisioned bool) { + if !dr.cfg.RBAC.PermissionsOnCreation("folder") { + return + } + inFolder := f.ParentUID != "" var permissions []accesscontrol.SetResourcePermissionCommand diff --git a/pkg/services/datasources/service/datasource.go b/pkg/services/datasources/service/datasource.go index e731776656c..f9bcfd835ff 100644 --- a/pkg/services/datasources/service/datasource.go +++ b/pkg/services/datasources/service/datasource.go @@ -280,20 +280,24 @@ func (s *Service) AddDataSource(ctx context.Context, cmd *datasources.AddDataSou return err } - // This belongs in Data source permissions, and we probably want - // to do this with a hook in the store and rollback on fail. - // We can't use events, because there's no way to communicate - // failure, and we want "not being able to set default perms" - // to fail the creation. - permissions := []accesscontrol.SetResourcePermissionCommand{ - {BuiltinRole: "Viewer", Permission: "Query"}, - {BuiltinRole: "Editor", Permission: "Query"}, + if s.cfg.RBAC.PermissionsOnCreation("datasource") { + // This belongs in Data source permissions, and we probably want + // to do this with a hook in the store and rollback on fail. + // We can't use events, because there's no way to communicate + // failure, and we want "not being able to set default perms" + // to fail the creation. + permissions := []accesscontrol.SetResourcePermissionCommand{ + {BuiltinRole: "Viewer", Permission: "Query"}, + {BuiltinRole: "Editor", Permission: "Query"}, + } + if cmd.UserID != 0 { + permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{UserID: cmd.UserID, Permission: "Admin"}) + } + _, err = s.permissionsService.SetPermissions(ctx, cmd.OrgID, dataSource.UID, permissions...) + return err } - if cmd.UserID != 0 { - permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{UserID: cmd.UserID, Permission: "Admin"}) - } - _, err = s.permissionsService.SetPermissions(ctx, cmd.OrgID, dataSource.UID, permissions...) - return err + + return nil }) } diff --git a/pkg/services/datasources/service/datasource_test.go b/pkg/services/datasources/service/datasource_test.go index a4c6afdc2db..b70871164b0 100644 --- a/pkg/services/datasources/service/datasource_test.go +++ b/pkg/services/datasources/service/datasource_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "gopkg.in/ini.v1" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" @@ -570,7 +571,11 @@ func TestService_DeleteDataSource(t *testing.T) { permissionSvc.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil).Once() permissionSvc.On("DeleteResourcePermissions", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() - dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, featuremgmt.WithFeatures(), acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) + f := ini.Empty() + f.Section("rbac").Key("resources_with_managed_permissions_on_creation").SetValue("datasource") + cfg, err := setting.NewCfgFromINIFile(f) + require.NoError(t, err) + dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) require.NoError(t, err) // First add the datasource diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index 1841f0c1b37..69b9c31e413 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -7,10 +7,10 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware/requestmeta" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/authn" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" @@ -98,25 +98,25 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *contextmodel.ReqContext) return response.ErrOrFallback(http.StatusInternalServerError, "Failed to create service account", err) } - namespace, identifier := c.SignedInUser.GetNamespacedID() + if api.cfg.RBAC.PermissionsOnCreation("service-account") { + if c.SignedInUser.GetID().IsNamespace(authn.NamespaceUser) { + userID, err := c.SignedInUser.GetID().ParseInt() + if err != nil { + return response.Error(http.StatusInternalServerError, "Failed to parse user id", err) + } - if namespace == identity.NamespaceUser { - userID, err := identity.IntIdentifier(namespace, identifier) - if err != nil { - return response.Error(http.StatusInternalServerError, "Failed to parse user id", err) - } + if _, err := api.permissionService.SetUserPermission(c.Req.Context(), + c.SignedInUser.GetOrgID(), accesscontrol.User{ID: userID}, + strconv.FormatInt(serviceAccount.Id, 10), "Admin"); err != nil { + return response.Error(http.StatusInternalServerError, "Failed to set permissions for service account creator", err) + } - if _, err := api.permissionService.SetUserPermission(c.Req.Context(), - c.SignedInUser.GetOrgID(), accesscontrol.User{ID: userID}, - strconv.FormatInt(serviceAccount.Id, 10), "Admin"); err != nil { - return response.Error(http.StatusInternalServerError, "Failed to set permissions for service account creator", err) + // Clear permission cache for the user who's created the service account, so that new permissions are fetched for their next call + // Required for cases when caller wants to immediately interact with the newly created object + api.accesscontrolService.ClearUserPermissionCache(c.SignedInUser) } } - // Clear permission cache for the user who's created the service account, so that new permissions are fetched for their next call - // Required for cases when caller wants to immediately interact with the newly created object - api.accesscontrolService.ClearUserPermissionCache(c.SignedInUser) - return response.JSON(http.StatusCreated, serviceAccount) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index be3204b4aea..e4fa61fbde8 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -322,9 +322,6 @@ type Cfg struct { // GrafanaJavascriptAgent config GrafanaJavascriptAgent GrafanaJavascriptAgent - // accessactionsets - OnlyStoreAccessActionSets bool - // Data sources DataSourceLimit int // Number of queries to be executed concurrently. Only for the datasource supports concurrency. @@ -467,14 +464,7 @@ type Cfg struct { OAuth2ServerGeneratedKeyTypeForClient string OAuth2ServerAccessTokenLifespan time.Duration - // Access Control - RBACPermissionCache bool - // Enable Permission validation during role creation and provisioning - RBACPermissionValidationEnabled bool - // Reset basic roles permissions on start-up - RBACResetBasicRoles bool - // RBAC single organization. This configuration option is subject to change. - RBACSingleOrganization bool + RBAC RBACSettings Zanzana ZanzanaSettings @@ -1116,7 +1106,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { readOAuth2ServerSettings(cfg) - readAccessControlSettings(iniFile, cfg) + cfg.readRBACSettings() cfg.readZanzanaSettings() @@ -1657,15 +1647,6 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { return nil } -func readAccessControlSettings(iniFile *ini.File, cfg *Cfg) { - rbac := iniFile.Section("rbac") - cfg.RBACPermissionCache = rbac.Key("permission_cache").MustBool(true) - cfg.RBACPermissionValidationEnabled = rbac.Key("permission_validation_enabled").MustBool(false) - cfg.RBACResetBasicRoles = rbac.Key("reset_basic_roles").MustBool(false) - cfg.RBACSingleOrganization = rbac.Key("single_organization").MustBool(false) - cfg.OnlyStoreAccessActionSets = rbac.Key("only_store_access_action_sets").MustBool(false) -} - func readOAuth2ServerSettings(cfg *Cfg) { oauth2Srv := cfg.SectionWithEnvOverrides("oauth2_server") cfg.OAuth2ServerEnabled = oauth2Srv.Key("enabled").MustBool(false) diff --git a/pkg/setting/settings_rbac.go b/pkg/setting/settings_rbac.go new file mode 100644 index 00000000000..393f9db3409 --- /dev/null +++ b/pkg/setting/settings_rbac.go @@ -0,0 +1,61 @@ +package setting + +import ( + "github.com/grafana/grafana/pkg/util" +) + +type RBACSettings struct { + // Enable permission cache + PermissionCache bool + // Enable Permission validation during role creation and provisioning + PermissionValidationEnabled bool + // Reset basic roles permissions on start-up + ResetBasicRoles bool + // RBAC single organization. This configuration option is subject to change. + SingleOrganization bool + + OnlyStoreAccessActionSets bool + + // set of resources that should generate managed permissions when created + resourcesWithPermissionsOnCreation map[string]struct{} + + // set of resources that should we should seed wildcard scopes for + resourcesWithWildcardSeed map[string]struct{} +} + +func (cfg *Cfg) readRBACSettings() { + s := RBACSettings{} + + rbac := cfg.Raw.Section("rbac") + s.PermissionCache = rbac.Key("permission_cache").MustBool(true) + s.PermissionValidationEnabled = rbac.Key("permission_validation_enabled").MustBool(false) + s.ResetBasicRoles = rbac.Key("reset_basic_roles").MustBool(false) + s.SingleOrganization = rbac.Key("single_organization").MustBool(false) + s.OnlyStoreAccessActionSets = rbac.Key("only_store_access_action_sets").MustBool(false) + + // List of resources to generate managed permissions for upon resource creation (dashboard, folder, service-account, datasource) + resources := util.SplitString(rbac.Key("resources_with_managed_permissions_on_creation").MustString("dashboard, folder, service-account, datasource")) + s.resourcesWithPermissionsOnCreation = map[string]struct{}{} + for _, resource := range resources { + s.resourcesWithPermissionsOnCreation[resource] = struct{}{} + } + + // List of resources to seed managed permission wildcards for (dashboard, folder, datasource) + resources = util.SplitString(rbac.Key("resources_with_seeded_wildcard_access").MustString("")) + s.resourcesWithWildcardSeed = map[string]struct{}{} + for _, resource := range resources { + s.resourcesWithWildcardSeed[resource] = struct{}{} + } + + cfg.RBAC = s +} + +func (r RBACSettings) PermissionsOnCreation(resource string) bool { + _, ok := r.resourcesWithPermissionsOnCreation[resource] + return ok +} + +func (r RBACSettings) PermissionsWildcardSeed(resource string) bool { + _, ok := r.resourcesWithWildcardSeed[resource] + return ok +}