package service import ( "context" "fmt" "strings" "time" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/alerting" "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/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) var ( provisionerPermissions = []accesscontrol.Permission{ {Action: dashboards.ActionFoldersCreate}, {Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}, {Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll}, {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersAll}, } // DashboardServiceImpl implements the DashboardService interface _ dashboards.DashboardService = (*DashboardServiceImpl)(nil) ) type DashboardServiceImpl struct { cfg *setting.Cfg log log.Logger dashboardStore dashboards.Store folderStore dashboards.FolderStore dashAlertExtractor alerting.DashAlertExtractor features featuremgmt.FeatureToggles folderPermissions accesscontrol.FolderPermissionsService dashboardPermissions accesscontrol.DashboardPermissionsService ac accesscontrol.AccessControl } func ProvideDashboardService( cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore dashboards.FolderStore, dashAlertExtractor alerting.DashAlertExtractor, features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl, ) *DashboardServiceImpl { ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(dashboardStore, folderStore)) ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(dashboardStore, folderStore)) return &DashboardServiceImpl{ cfg: cfg, log: log.New("dashboard-service"), dashboardStore: dashboardStore, dashAlertExtractor: dashAlertExtractor, features: features, folderPermissions: folderPermissionsService, dashboardPermissions: dashboardPermissionsService, ac: ac, } } func (dr *DashboardServiceImpl) GetProvisionedDashboardData(ctx context.Context, name string) ([]*dashboards.DashboardProvisioning, error) { return dr.dashboardStore.GetProvisionedDashboardData(ctx, name) } func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*dashboards.DashboardProvisioning, error) { return dr.dashboardStore.GetProvisionedDataByDashboardID(ctx, dashboardID) } func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*dashboards.DashboardProvisioning, error) { return dr.dashboardStore.GetProvisionedDataByDashboardUID(ctx, orgID, dashboardUID) } func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*dashboards.SaveDashboardCommand, error) { dash := dto.Dashboard dash.OrgID = dto.OrgID dash.Title = strings.TrimSpace(dash.Title) dash.Data.Set("title", dash.Title) dash.SetUID(strings.TrimSpace(dash.UID)) if dash.Title == "" { return nil, dashboards.ErrDashboardTitleEmpty } if dash.IsFolder && dash.FolderID > 0 { return nil, dashboards.ErrDashboardFolderCannotHaveParent } if dash.IsFolder && strings.EqualFold(dash.Title, dashboards.RootFolderName) { return nil, dashboards.ErrDashboardFolderNameExists } if !util.IsValidShortUID(dash.UID) { return nil, dashboards.ErrDashboardInvalidUid } else if util.IsShortUIDTooLong(dash.UID) { return nil, dashboards.ErrDashboardUidTooLong } if err := validateDashboardRefreshInterval(dash); err != nil { return nil, err } if shouldValidateAlerts { dashAlertInfo := alerting.DashAlertInfo{Dash: dash, User: dto.User, OrgID: dash.OrgID} if err := dr.dashAlertExtractor.ValidateAlerts(ctx, dashAlertInfo); err != nil { return nil, err } } isParentFolderChanged, err := dr.dashboardStore.ValidateDashboardBeforeSave(ctx, dash, dto.Overwrite) if err != nil { return nil, err } if isParentFolderChanged { // Check that the user is allowed to add a dashboard to the folder guardian, err := guardian.NewByDashboard(ctx, dash, dto.OrgID, dto.User) if err != nil { return nil, err } if canSave, err := guardian.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canSave { if err != nil { return nil, err } return nil, dashboards.ErrDashboardUpdateAccessDenied } } if validateProvisionedDashboard { provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dash.ID) if err != nil { return nil, err } if provisionedData != nil { return nil, dashboards.ErrDashboardCannotSaveProvisionedDashboard } } guard, err := getGuardianForSavePermissionCheck(ctx, dash, dto.User) if err != nil { return nil, err } if dash.ID == 0 { if canCreate, err := guard.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canCreate { if err != nil { return nil, err } return nil, dashboards.ErrDashboardUpdateAccessDenied } } else { if canSave, err := guard.CanSave(); err != nil || !canSave { if err != nil { return nil, err } return nil, dashboards.ErrDashboardUpdateAccessDenied } } cmd := &dashboards.SaveDashboardCommand{ Dashboard: dash.Data, Message: dto.Message, OrgID: dto.OrgID, Overwrite: dto.Overwrite, UserID: dto.User.UserID, FolderID: dash.FolderID, IsFolder: dash.IsFolder, PluginID: dash.PluginID, } if !dto.UpdatedAt.IsZero() { cmd.UpdatedAt = dto.UpdatedAt } return cmd, nil } func (dr *DashboardServiceImpl) UpdateDashboardACL(ctx context.Context, uid int64, items []*dashboards.DashboardACL) error { return dr.dashboardStore.UpdateDashboardACL(ctx, uid, items) } func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error { return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd) } // getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard // It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck() func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashboard, user *user.SignedInUser) (guardian.DashboardGuardian, error) { newDashboard := d.ID == 0 if newDashboard { // if it's a new dashboard/folder check the parent folder permissions guard, err := guardian.New(ctx, d.FolderID, d.OrgID, user) if err != nil { return nil, err } return guard, nil } guard, err := guardian.NewByDashboard(ctx, d, d.OrgID, user) if err != nil { return nil, err } return guard, nil } func validateDashboardRefreshInterval(dash *dashboards.Dashboard) error { if setting.MinRefreshInterval == "" { return nil } refresh := dash.Data.Get("refresh").MustString("") if refresh == "" { // since no refresh is set it is a valid refresh rate return nil } minRefreshInterval, err := gtime.ParseDuration(setting.MinRefreshInterval) if err != nil { return fmt.Errorf("parsing min refresh interval %q failed: %w", setting.MinRefreshInterval, err) } d, err := gtime.ParseDuration(refresh) if err != nil { return fmt.Errorf("parsing refresh duration %q failed: %w", refresh, err) } if d < minRefreshInterval { return dashboards.ErrDashboardRefreshIntervalTooShort } return nil } func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO, provisioning *dashboards.DashboardProvisioning) (*dashboards.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for provisioned dashboard to minimum refresh interval", "dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions) cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, setting.IsLegacyAlertingEnabled(), false) if err != nil { return nil, err } // dashboard dash, err := dr.dashboardStore.SaveProvisionedDashboard(ctx, *cmd, provisioning) if err != nil { return nil, err } // alerts dashAlertInfo := alerting.DashAlertInfo{ User: dto.User, Dash: dash, OrgID: dto.OrgID, } // extract/save legacy alerts only if legacy alerting is enabled if setting.IsLegacyAlertingEnabled() { alerts, err := dr.dashAlertExtractor.GetAlerts(ctx, dashAlertInfo) if err != nil { return nil, err } err = dr.dashboardStore.SaveAlerts(ctx, dash.ID, alerts) if err != nil { return nil, err } } if dto.Dashboard.ID == 0 { if err := dr.setDefaultPermissions(ctx, dto, dash, true); err != nil { dr.log.Error("Could not make user admin", "dashboard", dash.Title, "user", dto.User.UserID, "error", err) } } return dash, nil } func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.Dashboard, error) { dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions) cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false, false) if err != nil { return nil, err } dash, err := dr.dashboardStore.SaveDashboard(ctx, *cmd) if err != nil { return nil, err } dashAlertInfo := alerting.DashAlertInfo{ User: dto.User, Dash: dash, OrgID: dto.OrgID, } // extract/save legacy alerts only if legacy alerting is enabled if setting.IsLegacyAlertingEnabled() { alerts, err := dr.dashAlertExtractor.GetAlerts(ctx, dashAlertInfo) if err != nil { return nil, err } err = dr.dashboardStore.SaveAlerts(ctx, dash.ID, alerts) if err != nil { return nil, err } } if dto.Dashboard.ID == 0 { if err := dr.setDefaultPermissions(ctx, dto, dash, true); err != nil { dr.log.Error("Could not make user admin", "dashboard", dash.Title, "user", dto.User.UserID, "error", err) } } return dash, nil } func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO, allowUiUpdate bool) (*dashboards.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", "dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, setting.IsLegacyAlertingEnabled(), !allowUiUpdate) if err != nil { return nil, err } dash, err := dr.dashboardStore.SaveDashboard(ctx, *cmd) if err != nil { return nil, fmt.Errorf("saving dashboard failed: %w", err) } dashAlertInfo := alerting.DashAlertInfo{ User: dto.User, Dash: dash, OrgID: dto.OrgID, } // extract/save legacy alerts only if legacy alerting is enabled if setting.IsLegacyAlertingEnabled() { alerts, err := dr.dashAlertExtractor.GetAlerts(ctx, dashAlertInfo) if err != nil { return nil, err } err = dr.dashboardStore.SaveAlerts(ctx, dash.ID, alerts) if err != nil { return nil, err } } // new dashboard created if dto.Dashboard.ID == 0 { if err := dr.setDefaultPermissions(ctx, dto, dash, false); err != nil { dr.log.Error("Could not make user admin", "dashboard", dash.Title, "user", dto.User.UserID, "error", err) } } return dash, nil } // DeleteDashboard removes dashboard from the DB. Errors out if the dashboard was provisioned. Should be used for // operations by the user where we want to make sure user does not delete provisioned dashboard. func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error { return dr.deleteDashboard(ctx, dashboardId, orgId, true) } func (dr *DashboardServiceImpl) GetDashboardByPublicUid(ctx context.Context, dashboardPublicUid string) (*dashboards.Dashboard, error) { return nil, nil } func (dr *DashboardServiceImpl) MakeUserAdmin(ctx context.Context, orgID int64, userID int64, dashboardID int64, setViewAndEditPermissions bool) error { rtEditor := org.RoleEditor rtViewer := org.RoleViewer items := []*dashboards.DashboardACL{ { OrgID: orgID, DashboardID: dashboardID, UserID: userID, Permission: models.PERMISSION_ADMIN, Created: time.Now(), Updated: time.Now(), }, } if setViewAndEditPermissions { items = append(items, &dashboards.DashboardACL{ OrgID: orgID, DashboardID: dashboardID, Role: &rtEditor, Permission: models.PERMISSION_EDIT, Created: time.Now(), Updated: time.Now(), }, &dashboards.DashboardACL{ OrgID: orgID, DashboardID: dashboardID, Role: &rtViewer, Permission: models.PERMISSION_VIEW, Created: time.Now(), Updated: time.Now(), }, ) } if err := dr.dashboardStore.UpdateDashboardACL(ctx, dashboardID, items); err != nil { return err } return nil } // DeleteProvisionedDashboard removes dashboard from the DB even if it is provisioned. func (dr *DashboardServiceImpl) DeleteProvisionedDashboard(ctx context.Context, dashboardId int64, orgId int64) error { return dr.deleteDashboard(ctx, dashboardId, orgId, false) } func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId int64, orgId int64, validateProvisionedDashboard bool) error { if validateProvisionedDashboard { provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dashboardId) if err != nil { return fmt.Errorf("%v: %w", "failed to check if dashboard is provisioned", err) } if provisionedData != nil { return dashboards.ErrDashboardCannotDeleteProvisionedDashboard } } cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId} return dr.dashboardStore.DeleteDashboard(ctx, cmd) } func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) ( *dashboards.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", "dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false, true) if err != nil { return nil, err } dash, err := dr.dashboardStore.SaveDashboard(ctx, *cmd) if err != nil { return nil, err } if err := dr.setDefaultPermissions(ctx, dto, dash, false); err != nil { dr.log.Error("Could not make user admin", "dashboard", dash.Title, "user", dto.User.UserID, "error", err) } return dash, nil } // UnprovisionDashboard removes info about dashboard being provisioned. Used after provisioning configs are changed // and provisioned dashboards are left behind but not deleted. func (dr *DashboardServiceImpl) UnprovisionDashboard(ctx context.Context, dashboardId int64) error { return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId) } func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) error { return dr.dashboardStore.GetDashboardsByPluginID(ctx, query) } func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *dashboards.Dashboard, provisioned bool) error { inFolder := dash.FolderID > 0 if !accesscontrol.IsDisabled(dr.cfg) { var permissions []accesscontrol.SetResourcePermissionCommand if !provisioned && dto.User.IsRealUser() && !dto.User.IsAnonymous { permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ UserID: dto.User.UserID, Permission: models.PERMISSION_ADMIN.String(), }) } if !inFolder { permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ {BuiltinRole: string(org.RoleEditor), Permission: models.PERMISSION_EDIT.String()}, {BuiltinRole: string(org.RoleViewer), Permission: models.PERMISSION_VIEW.String()}, }...) } svc := dr.dashboardPermissions if dash.IsFolder { svc = dr.folderPermissions } _, err := svc.SetPermissions(ctx, dto.OrgID, dash.UID, permissions...) if err != nil { return err } } else if dr.cfg.EditorsCanAdmin && !provisioned && dto.User.IsRealUser() && !dto.User.IsAnonymous { if err := dr.MakeUserAdmin(ctx, dto.OrgID, dto.User.UserID, dash.ID, !inFolder); err != nil { return err } } return nil } func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) error { _, err := dr.dashboardStore.GetDashboard(ctx, query) return err } func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) error { return dr.dashboardStore.GetDashboardUIDByID(ctx, query) } func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) error { return dr.dashboardStore.GetDashboards(ctx, query) } func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { return dr.dashboardStore.FindDashboards(ctx, query) } func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error { res, err := dr.FindDashboards(ctx, query) if err != nil { return err } makeQueryResult(query, res) return nil } func getHitType(item dashboards.DashboardSearchProjection) models.HitType { var hitType models.HitType if item.IsFolder { hitType = models.DashHitFolder } else { hitType = models.DashHitDB } return hitType } func makeQueryResult(query *models.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) { query.Result = make([]*models.Hit, 0) hits := make(map[int64]*models.Hit) for _, item := range res { hit, exists := hits[item.ID] if !exists { hit = &models.Hit{ ID: item.ID, UID: item.UID, Title: item.Title, URI: "db/" + item.Slug, URL: dashboards.GetDashboardFolderURL(item.IsFolder, item.UID, item.Slug), Type: getHitType(item), FolderID: item.FolderID, FolderUID: item.FolderUID, FolderTitle: item.FolderTitle, Tags: []string{}, } if item.FolderID > 0 { hit.FolderURL = dashboards.GetFolderURL(item.FolderUID, item.FolderSlug) } if query.Sort.MetaName != "" { hit.SortMeta = item.SortMeta hit.SortMetaName = query.Sort.MetaName } query.Result = append(query.Result, hit) hits[item.ID] = hit } if len(item.Term) > 0 { hit.Tags = append(hit.Tags, item.Term) } } } func (dr *DashboardServiceImpl) GetDashboardACLInfoList(ctx context.Context, query *dashboards.GetDashboardACLInfoListQuery) error { return dr.dashboardStore.GetDashboardACLInfoList(ctx, query) } func (dr *DashboardServiceImpl) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) { return dr.dashboardStore.HasAdminPermissionInDashboardsOrFolders(ctx, query) } func (dr *DashboardServiceImpl) HasEditPermissionInFolders(ctx context.Context, query *folder.HasEditPermissionInFoldersQuery) (bool, error) { return dr.dashboardStore.HasEditPermissionInFolders(ctx, query) } func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) error { return dr.dashboardStore.GetDashboardTags(ctx, query) } func (dr *DashboardServiceImpl) DeleteACLByUser(ctx context.Context, userID int64) error { return dr.dashboardStore.DeleteACLByUser(ctx, userID) } func (dr DashboardServiceImpl) CountDashboardsInFolder(ctx context.Context, query *dashboards.CountDashboardsInFolderQuery) (int64, error) { u, err := appcontext.User(ctx) if err != nil { return 0, err } folder, err := dr.folderStore.GetFolderByUID(ctx, u.OrgID, query.FolderUID) if err != nil { return 0, err } return dr.dashboardStore.CountDashboardsInFolder(ctx, &dashboards.CountDashboardsInFolderRequest{FolderID: folder.ID, OrgID: u.OrgID}) }