The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/folder/folderimpl/folder.go

498 lines
14 KiB

package folderimpl
import (
"context"
"errors"
"strings"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/infra/db"
"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/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
type Service struct {
store store
log log.Logger
cfg *setting.Cfg
dashboardService dashboards.DashboardService
dashboardStore dashboards.Store
searchService *search.SearchService
features *featuremgmt.FeatureManager
permissions accesscontrol.FolderPermissionsService
accessControl accesscontrol.AccessControl
// bus is currently used to publish events that cause scheduler to update rules.
bus bus.Bus
}
func ProvideService(
ac accesscontrol.AccessControl,
bus bus.Bus,
cfg *setting.Cfg,
dashboardService dashboards.DashboardService,
dashboardStore dashboards.Store,
db db.DB, // DB for the (new) nested folder store
features *featuremgmt.FeatureManager,
folderPermissionsService accesscontrol.FolderPermissionsService,
searchService *search.SearchService,
) folder.Service {
ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(dashboardStore))
ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(dashboardStore))
store := ProvideStore(db, cfg, features)
return &Service{
cfg: cfg,
log: log.New("folder-service"),
dashboardService: dashboardService,
dashboardStore: dashboardStore,
store: store,
searchService: searchService,
features: features,
permissions: folderPermissionsService,
accessControl: ac,
bus: bus,
}
}
func (s *Service) Get(ctx context.Context, cmd *folder.GetFolderQuery) (*folder.Folder, error) {
user, err := appcontext.User(ctx)
if err != nil {
return nil, err
}
if s.cfg.IsFeatureToggleEnabled(featuremgmt.FlagNestedFolders) {
if ok, err := s.accessControl.Evaluate(ctx, user, accesscontrol.EvalPermission(
dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(*cmd.UID),
)); !ok {
if err != nil {
return nil, toFolderError(err)
}
return nil, dashboards.ErrFolderAccessDenied
}
return s.store.Get(ctx, *cmd)
}
switch {
case cmd.UID != nil:
return s.getFolderByUID(ctx, user, cmd.OrgID, *cmd.UID)
case cmd.ID != nil:
return s.getFolderByID(ctx, user, *cmd.ID, cmd.OrgID)
case cmd.Title != nil:
return s.getFolderByTitle(ctx, user, cmd.OrgID, *cmd.Title)
default:
return nil, folder.ErrBadRequest.Errorf("either on of UID, ID, Title fields must be present")
}
}
func (s *Service) GetFolders(ctx context.Context, user *user.SignedInUser, orgID int64, limit int64, page int64) ([]*models.Folder, error) {
searchQuery := search.Query{
SignedInUser: user,
DashboardIds: make([]int64, 0),
FolderIds: make([]int64, 0),
Limit: limit,
OrgId: orgID,
Type: "dash-folder",
Permission: models.PERMISSION_VIEW,
Page: page,
}
if err := s.searchService.SearchHandler(ctx, &searchQuery); err != nil {
return nil, err
}
folders := make([]*models.Folder, 0)
for _, hit := range searchQuery.Result {
folders = append(folders, &models.Folder{
Id: hit.ID,
Uid: hit.UID,
Title: hit.Title,
})
}
return folders, nil
}
func (s *Service) getFolderByID(ctx context.Context, user *user.SignedInUser, id int64, orgID int64) (*folder.Folder, error) {
if id == 0 {
return &folder.Folder{ID: id, Title: "General"}, nil
}
dashFolder, err := s.dashboardStore.GetFolderByID(ctx, orgID, id)
if err != nil {
return nil, err
}
g := guardian.New(ctx, dashFolder.ID, orgID, user)
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
}
return nil, dashboards.ErrFolderAccessDenied
}
return dashFolder, nil
}
func (s *Service) getFolderByUID(ctx context.Context, user *user.SignedInUser, orgID int64, uid string) (*folder.Folder, error) {
dashFolder, err := s.dashboardStore.GetFolderByUID(ctx, orgID, uid)
if err != nil {
return nil, err
}
g := guardian.New(ctx, dashFolder.ID, orgID, user)
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
}
return nil, dashboards.ErrFolderAccessDenied
}
return dashFolder, nil
}
func (s *Service) getFolderByTitle(ctx context.Context, user *user.SignedInUser, orgID int64, title string) (*folder.Folder, error) {
dashFolder, err := s.dashboardStore.GetFolderByTitle(ctx, orgID, title)
if err != nil {
return nil, err
}
g := guardian.New(ctx, dashFolder.ID, orgID, user)
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
}
return nil, dashboards.ErrFolderAccessDenied
}
return dashFolder, nil
}
func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
dashFolder := models.NewDashboardFolder(cmd.Title)
dashFolder.OrgId = cmd.OrgID
trimmedUID := strings.TrimSpace(cmd.UID)
if trimmedUID == accesscontrol.GeneralFolderUID {
return nil, dashboards.ErrFolderInvalidUID
}
dashFolder.SetUid(trimmedUID)
user, err := appcontext.User(ctx)
if err != nil {
return nil, err
}
userID := user.UserID
if userID == 0 {
userID = -1
}
dashFolder.CreatedBy = userID
dashFolder.UpdatedBy = userID
dashFolder.UpdateSlug()
dto := &dashboards.SaveDashboardDTO{
Dashboard: dashFolder,
OrgId: cmd.OrgID,
User: user,
}
saveDashboardCmd, err := s.dashboardService.BuildSaveDashboardCommand(ctx, dto, false, false)
if err != nil {
return nil, toFolderError(err)
}
dash, err := s.dashboardStore.SaveDashboard(ctx, *saveDashboardCmd)
if err != nil {
return nil, toFolderError(err)
}
var createdFolder *folder.Folder
createdFolder, err = s.dashboardStore.GetFolderByID(ctx, cmd.OrgID, dash.Id)
if err != nil {
return nil, err
}
var permissionErr error
if !accesscontrol.IsDisabled(s.cfg) {
var permissions []accesscontrol.SetResourcePermissionCommand
if user.IsRealUser() && !user.IsAnonymous {
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
UserID: userID, Permission: models.PERMISSION_ADMIN.String(),
})
}
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: models.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: models.PERMISSION_VIEW.String()},
}...)
_, permissionErr = s.permissions.SetPermissions(ctx, cmd.OrgID, createdFolder.UID, permissions...)
} else if s.cfg.EditorsCanAdmin && user.IsRealUser() && !user.IsAnonymous {
permissionErr = s.MakeUserAdmin(ctx, cmd.OrgID, userID, createdFolder.ID, true)
}
if permissionErr != nil {
s.log.Error("Could not make user admin", "folder", createdFolder.Title, "user", userID, "error", permissionErr)
}
if s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
var description string
if dash.Data != nil {
description = dash.Data.Get("description").MustString()
}
parentUID := folder.RootFolderUID
if cmd.ParentUID != "" {
parentUID = cmd.ParentUID
}
_, err := s.store.Create(ctx, folder.CreateFolderCommand{
// TODO: Today, if a UID isn't specified, the dashboard store
// generates a new UID. The new folder store will need to do this as
// well, but for now we take the UID from the newly created folder.
UID: dash.Uid,
OrgID: cmd.OrgID,
Title: cmd.Title,
Description: description,
ParentUID: parentUID,
})
if err != nil {
// We'll log the error and also roll back the previously-created
// (legacy) folder.
s.log.Error("error saving folder to nested folder store", err)
err = s.DeleteFolder(ctx, &folder.DeleteFolderCommand{UID: createdFolder.UID, OrgID: cmd.OrgID, ForceDeleteRules: true})
if err != nil {
s.log.Error("error deleting folder after failed save to nested folder store", err)
}
return folder.FromDashboard(dash), err
}
// The folder UID is specified (or generated) during creation, so we'll
// stop here and return the created model.Folder.
}
return folder.FromDashboard(dash), nil
}
func (s *Service) Update(ctx context.Context, user *user.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) (*folder.Folder, error) {
foldr, err := s.legacyUpdate(ctx, user, orgID, existingUid, cmd)
if err != nil {
return nil, err
}
if s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
if cmd.Uid != "" {
if !util.IsValidShortUID(cmd.Uid) {
return nil, dashboards.ErrDashboardInvalidUid
} else if util.IsShortUIDTooLong(cmd.Uid) {
return nil, dashboards.ErrDashboardUidTooLong
}
}
getFolder, err := s.store.Get(ctx, folder.GetFolderQuery{
UID: &existingUid,
OrgID: orgID,
})
if err != nil {
return nil, err
}
foldr, err := s.store.Update(ctx, folder.UpdateFolderCommand{
Folder: getFolder,
NewUID: &cmd.Uid,
NewTitle: &cmd.Title,
NewDescription: &cmd.Description,
})
if err != nil {
return nil, err
}
return foldr, nil
}
return foldr, nil
}
func (s *Service) legacyUpdate(ctx context.Context, user *user.SignedInUser, orgID int64, existingUid string, cmd *models.UpdateFolderCommand) (*folder.Folder, error) {
query := models.GetDashboardQuery{OrgId: orgID, Uid: existingUid}
_, err := s.dashboardStore.GetDashboard(ctx, &query)
if err != nil {
return nil, toFolderError(err)
}
dashFolder := query.Result
currentTitle := dashFolder.Title
if !dashFolder.IsFolder {
return nil, dashboards.ErrFolderNotFound
}
cmd.UpdateDashboardModel(dashFolder, orgID, user.UserID)
dto := &dashboards.SaveDashboardDTO{
Dashboard: dashFolder,
OrgId: orgID,
User: user,
Overwrite: cmd.Overwrite,
}
saveDashboardCmd, err := s.dashboardService.BuildSaveDashboardCommand(ctx, dto, false, false)
if err != nil {
return nil, toFolderError(err)
}
dash, err := s.dashboardStore.SaveDashboard(ctx, *saveDashboardCmd)
if err != nil {
return nil, toFolderError(err)
}
var foldr *folder.Folder
foldr, err = s.dashboardStore.GetFolderByID(ctx, orgID, dash.Id)
if err != nil {
return nil, err
}
if currentTitle != foldr.Title {
if err := s.bus.Publish(ctx, &events.FolderTitleUpdated{
Timestamp: foldr.Updated,
Title: foldr.Title,
ID: dash.Id,
UID: dash.Uid,
OrgID: orgID,
}); err != nil {
s.log.Error("failed to publish FolderTitleUpdated event", "folder", foldr.Title, "user", user.UserID, "error", err)
}
}
return foldr, nil
}
func (s *Service) DeleteFolder(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
if s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
err := s.Delete(ctx, cmd)
if err != nil {
s.log.Error("the delete folder on folder table failed with err: ", err.Error())
}
}
user, err := appcontext.User(ctx)
if err != nil {
return err
}
dashFolder, err := s.dashboardStore.GetFolderByUID(ctx, cmd.OrgID, cmd.UID)
if err != nil {
return err
}
guard := guardian.New(ctx, dashFolder.ID, cmd.OrgID, user)
if canSave, err := guard.CanDelete(); err != nil || !canSave {
if err != nil {
return toFolderError(err)
}
return dashboards.ErrFolderAccessDenied
}
deleteCmd := models.DeleteDashboardCommand{OrgId: cmd.OrgID, Id: dashFolder.ID, ForceDeleteFolderRules: cmd.ForceDeleteRules}
if err := s.dashboardStore.DeleteDashboard(ctx, &deleteCmd); err != nil {
return toFolderError(err)
}
return nil
}
func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
foldr, err := s.Get(ctx, &folder.GetFolderQuery{
UID: &cmd.UID,
OrgID: cmd.OrgID,
})
if err != nil {
return nil, err
}
return s.store.Update(ctx, folder.UpdateFolderCommand{
Folder: foldr,
// NewParentUID: &cmd.NewParentUID,
})
}
func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
_, err := s.Get(ctx, &folder.GetFolderQuery{
UID: &cmd.UID,
OrgID: cmd.OrgID,
})
if err != nil {
return err
}
folders, err := s.store.GetChildren(ctx, folder.GetTreeQuery{UID: cmd.UID, OrgID: cmd.OrgID})
if err != nil {
return err
}
for _, f := range folders {
err := s.Delete(ctx, &folder.DeleteFolderCommand{UID: f.UID, OrgID: f.OrgID, ForceDeleteRules: cmd.ForceDeleteRules})
if err != nil {
return err
}
}
err = s.store.Delete(ctx, cmd.UID, cmd.OrgID)
if err != nil {
return err
}
return nil
}
func (s *Service) GetParents(ctx context.Context, cmd *folder.GetParentsQuery) ([]*folder.Folder, error) {
// check the flag, if old - do whatever did before
// for new only the store
return s.store.GetParents(ctx, *cmd)
}
func (s *Service) GetTree(ctx context.Context, cmd *folder.GetTreeQuery) ([]*folder.Folder, error) {
// check the flag, if old - do whatever did before
// for new only the store
return s.store.GetChildren(ctx, *cmd)
}
func (s *Service) MakeUserAdmin(ctx context.Context, orgID int64, userID, folderID int64, setViewAndEditPermissions bool) error {
return s.dashboardService.MakeUserAdmin(ctx, orgID, userID, folderID, setViewAndEditPermissions)
}
func toFolderError(err error) error {
if errors.Is(err, dashboards.ErrDashboardTitleEmpty) {
return dashboards.ErrFolderTitleEmpty
}
if errors.Is(err, dashboards.ErrDashboardUpdateAccessDenied) {
return dashboards.ErrFolderAccessDenied
}
if errors.Is(err, dashboards.ErrDashboardWithSameNameInFolderExists) {
return dashboards.ErrFolderSameNameExists
}
if errors.Is(err, dashboards.ErrDashboardWithSameUIDExists) {
return dashboards.ErrFolderWithSameUIDExists
}
if errors.Is(err, dashboards.ErrDashboardVersionMismatch) {
return dashboards.ErrFolderVersionMismatch
}
if errors.Is(err, dashboards.ErrDashboardNotFound) {
return dashboards.ErrFolderNotFound
}
if errors.Is(err, dashboards.ErrDashboardFailedGenerateUniqueUid) {
err = dashboards.ErrFolderFailedGenerateUniqueUid
}
return err
}