mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			867 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			867 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Go
		
	
	
	
| package folderimpl
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/grafana/grafana/pkg/bus"
 | |
| 	"github.com/grafana/grafana/pkg/events"
 | |
| 	"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/services/sqlstore"
 | |
| 	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 | |
| 	"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
 | |
| 	dashboardStore       dashboards.Store
 | |
| 	dashboardFolderStore dashboards.FolderStore
 | |
| 	searchService        *search.SearchService
 | |
| 	features             featuremgmt.FeatureToggles
 | |
| 	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,
 | |
| 	dashboardStore dashboards.Store,
 | |
| 	folderStore dashboards.FolderStore,
 | |
| 	db db.DB, // DB for the (new) nested folder store
 | |
| 	features featuremgmt.FeatureToggles,
 | |
| 	folderPermissionsService accesscontrol.FolderPermissionsService,
 | |
| 	searchService *search.SearchService,
 | |
| ) folder.Service {
 | |
| 	ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(dashboardStore, folderStore))
 | |
| 	ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(dashboardStore, folderStore))
 | |
| 	store := ProvideStore(db, cfg, features)
 | |
| 	svr := &Service{
 | |
| 		cfg:                  cfg,
 | |
| 		log:                  log.New("folder-service"),
 | |
| 		dashboardStore:       dashboardStore,
 | |
| 		dashboardFolderStore: folderStore,
 | |
| 		store:                store,
 | |
| 		searchService:        searchService,
 | |
| 		features:             features,
 | |
| 		permissions:          folderPermissionsService,
 | |
| 		accessControl:        ac,
 | |
| 		bus:                  bus,
 | |
| 	}
 | |
| 	if features.IsEnabled(featuremgmt.FlagNestedFolders) {
 | |
| 		svr.DBMigration(db)
 | |
| 	}
 | |
| 	return svr
 | |
| }
 | |
| 
 | |
| func (s *Service) DBMigration(db db.DB) {
 | |
| 	ctx := context.Background()
 | |
| 	err := db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
 | |
| 		var err error
 | |
| 		if db.GetDialect().DriverName() == migrator.SQLite {
 | |
| 			_, err = sess.Exec("INSERT OR REPLACE INTO folder (id, uid, org_id, title, created, updated) SELECT id, uid, org_id, title, created, updated FROM dashboard WHERE is_folder = 1")
 | |
| 		} else if db.GetDialect().DriverName() == migrator.Postgres {
 | |
| 			_, err = sess.Exec("INSERT INTO folder (id, uid, org_id, title, created, updated) SELECT id, uid, org_id, title, created, updated FROM dashboard WHERE is_folder = true ON CONFLICT DO NOTHING")
 | |
| 		} else {
 | |
| 			_, err = sess.Exec("INSERT IGNORE INTO folder (id, uid, org_id, title, created, updated) SELECT id, uid, org_id, title, created, updated FROM dashboard WHERE is_folder = 1")
 | |
| 		}
 | |
| 		return err
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		s.log.Error("DB migration on folder service start failed.")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (s *Service) Get(ctx context.Context, cmd *folder.GetFolderQuery) (*folder.Folder, error) {
 | |
| 	if cmd.SignedInUser == nil {
 | |
| 		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
 | |
| 	}
 | |
| 
 | |
| 	var dashFolder *folder.Folder
 | |
| 	var err error
 | |
| 	switch {
 | |
| 	case cmd.UID != nil:
 | |
| 		dashFolder, err = s.getFolderByUID(ctx, cmd.SignedInUser, cmd.OrgID, *cmd.UID)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	case cmd.ID != nil:
 | |
| 		dashFolder, err = s.getFolderByID(ctx, cmd.SignedInUser, *cmd.ID, cmd.OrgID)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	case cmd.Title != nil:
 | |
| 		dashFolder, err = s.getFolderByTitle(ctx, cmd.SignedInUser, cmd.OrgID, *cmd.Title)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	default:
 | |
| 		return nil, folder.ErrBadRequest.Errorf("either on of UID, ID, Title fields must be present")
 | |
| 	}
 | |
| 
 | |
| 	if !s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
 | |
| 		return dashFolder, nil
 | |
| 	}
 | |
| 
 | |
| 	if cmd.ID != nil {
 | |
| 		cmd.ID = nil
 | |
| 		cmd.UID = &dashFolder.UID
 | |
| 	}
 | |
| 	f, err := s.store.Get(ctx, *cmd)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// do not get guardian by the folder ID because it differs from the nested folder ID
 | |
| 	// and the legacy folder ID has been associated with the permissions:
 | |
| 	// use the folde UID instead that is the same for both
 | |
| 	g, err := guardian.NewByUID(ctx, f.UID, f.OrgID, cmd.SignedInUser)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if canView, err := g.CanView(); err != nil || !canView {
 | |
| 		if err != nil {
 | |
| 			return nil, toFolderError(err)
 | |
| 		}
 | |
| 		return nil, dashboards.ErrFolderAccessDenied
 | |
| 	}
 | |
| 
 | |
| 	// always expose the dashboard store sequential ID
 | |
| 	f.ID = dashFolder.ID
 | |
| 
 | |
| 	return f, err
 | |
| }
 | |
| 
 | |
| func (s *Service) GetChildren(ctx context.Context, cmd *folder.GetChildrenQuery) ([]*folder.Folder, error) {
 | |
| 	if cmd.SignedInUser == nil {
 | |
| 		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
 | |
| 	}
 | |
| 
 | |
| 	if s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
 | |
| 		children, err := s.store.GetChildren(ctx, *cmd)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		filtered := make([]*folder.Folder, 0, len(children))
 | |
| 		for _, f := range children {
 | |
| 			// fetch folder from dashboard store
 | |
| 			dashFolder, err := s.dashboardFolderStore.GetFolderByUID(ctx, f.OrgID, f.UID)
 | |
| 			if err != nil {
 | |
| 				s.log.Error("failed to fetch folder by UID: %s from dashboard store", f.UID, err)
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			g, err := guardian.New(ctx, f.ID, f.OrgID, cmd.SignedInUser)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 			canView, err := g.CanView()
 | |
| 			if err != nil || canView {
 | |
| 				// always expose the dashboard store sequential ID
 | |
| 				f.ID = dashFolder.ID
 | |
| 				filtered = append(filtered, f)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return filtered, nil
 | |
| 	}
 | |
| 
 | |
| 	searchQuery := search.Query{
 | |
| 		SignedInUser: cmd.SignedInUser,
 | |
| 		DashboardIds: make([]int64, 0),
 | |
| 		FolderIds:    make([]int64, 0),
 | |
| 		Limit:        cmd.Limit,
 | |
| 		OrgId:        cmd.OrgID,
 | |
| 		Type:         "dash-folder",
 | |
| 		Permission:   models.PERMISSION_VIEW,
 | |
| 		Page:         cmd.Page,
 | |
| 	}
 | |
| 
 | |
| 	if err := s.searchService.SearchHandler(ctx, &searchQuery); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	folders := make([]*folder.Folder, 0)
 | |
| 
 | |
| 	for _, hit := range searchQuery.Result {
 | |
| 		folders = append(folders, &folder.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.dashboardFolderStore.GetFolderByID(ctx, orgID, id)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// do not get guardian by the folder ID because it differs from the nested folder ID
 | |
| 	// and the legacy folder ID has been associated with the permissions:
 | |
| 	// use the folde UID instead that is the same for both
 | |
| 	g, err := guardian.NewByUID(ctx, dashFolder.UID, orgID, user)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	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.dashboardFolderStore.GetFolderByUID(ctx, orgID, uid)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// do not get guardian by the folder ID because it differs from the nested folder ID
 | |
| 	// and the legacy folder ID has been associated with the permissions:
 | |
| 	// use the folde UID instead that is the same for both
 | |
| 	g, err := guardian.NewByUID(ctx, dashFolder.UID, orgID, user)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	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.dashboardFolderStore.GetFolderByTitle(ctx, orgID, title)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	g, err := guardian.NewByUID(ctx, dashFolder.UID, orgID, user)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	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) {
 | |
| 	logger := s.log.FromContext(ctx)
 | |
| 
 | |
| 	dashFolder := dashboards.NewDashboardFolder(cmd.Title)
 | |
| 	dashFolder.OrgID = cmd.OrgID
 | |
| 
 | |
| 	trimmedUID := strings.TrimSpace(cmd.UID)
 | |
| 	if trimmedUID == accesscontrol.GeneralFolderUID {
 | |
| 		return nil, dashboards.ErrFolderInvalidUID
 | |
| 	}
 | |
| 
 | |
| 	dashFolder.SetUID(trimmedUID)
 | |
| 
 | |
| 	if cmd.SignedInUser == nil {
 | |
| 		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
 | |
| 	}
 | |
| 	user := cmd.SignedInUser
 | |
| 	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.BuildSaveDashboardCommand(ctx, dto)
 | |
| 	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.dashboardFolderStore.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 {
 | |
| 		logger.Error("Could not make user admin", "folder", createdFolder.Title, "user", userID, "error", permissionErr)
 | |
| 	}
 | |
| 
 | |
| 	var nestedFolder *folder.Folder
 | |
| 	if s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
 | |
| 		cmd := &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: cmd.Description,
 | |
| 			ParentUID:   cmd.ParentUID,
 | |
| 		}
 | |
| 		nestedFolder, err = s.nestedFolderCreate(ctx, cmd)
 | |
| 		if err != nil {
 | |
| 			// We'll log the error and also roll back the previously-created
 | |
| 			// (legacy) folder.
 | |
| 			logger.Error("error saving folder to nested folder store", "error", err)
 | |
| 			// do not shallow create error if the legacy folder delete fails
 | |
| 			if deleteErr := s.dashboardStore.DeleteDashboard(ctx, &dashboards.DeleteDashboardCommand{
 | |
| 				ID:    createdFolder.ID,
 | |
| 				OrgID: createdFolder.OrgID,
 | |
| 			}); deleteErr != nil {
 | |
| 				logger.Error("error deleting folder after failed save to nested folder store", "error", err)
 | |
| 			}
 | |
| 			return dashboards.FromDashboard(dash), err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	f := dashboards.FromDashboard(dash)
 | |
| 	if nestedFolder != nil && nestedFolder.ParentUID != "" {
 | |
| 		f.ParentUID = nestedFolder.ParentUID
 | |
| 	}
 | |
| 	return f, nil
 | |
| }
 | |
| 
 | |
| func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
 | |
| 	if cmd.SignedInUser == nil {
 | |
| 		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
 | |
| 	}
 | |
| 	user := cmd.SignedInUser
 | |
| 
 | |
| 	dashFolder, err := s.legacyUpdate(ctx, cmd)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if !s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
 | |
| 		return dashFolder, nil
 | |
| 	}
 | |
| 
 | |
| 	if cmd.NewUID != nil && *cmd.NewUID != "" {
 | |
| 		if !util.IsValidShortUID(*cmd.NewUID) {
 | |
| 			return nil, dashboards.ErrDashboardInvalidUid
 | |
| 		} else if util.IsShortUIDTooLong(*cmd.NewUID) {
 | |
| 			return nil, dashboards.ErrDashboardUidTooLong
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	foldr, err := s.store.Update(ctx, folder.UpdateFolderCommand{
 | |
| 		UID:            cmd.UID,
 | |
| 		OrgID:          cmd.OrgID,
 | |
| 		NewUID:         cmd.NewUID,
 | |
| 		NewTitle:       cmd.NewTitle,
 | |
| 		NewDescription: cmd.NewDescription,
 | |
| 		SignedInUser:   user,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// always expose the dashboard store sequential ID
 | |
| 	foldr.ID = dashFolder.ID
 | |
| 
 | |
| 	return foldr, nil
 | |
| }
 | |
| 
 | |
| func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
 | |
| 	logger := s.log.FromContext(ctx)
 | |
| 
 | |
| 	query := dashboards.GetDashboardQuery{OrgID: cmd.OrgID, UID: cmd.UID}
 | |
| 	_, 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
 | |
| 	}
 | |
| 
 | |
| 	if cmd.SignedInUser == nil {
 | |
| 		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
 | |
| 	}
 | |
| 	user := cmd.SignedInUser
 | |
| 
 | |
| 	prepareForUpdate(dashFolder, cmd.OrgID, cmd.SignedInUser.UserID, cmd)
 | |
| 
 | |
| 	dto := &dashboards.SaveDashboardDTO{
 | |
| 		Dashboard: dashFolder,
 | |
| 		OrgID:     cmd.OrgID,
 | |
| 		User:      cmd.SignedInUser,
 | |
| 		Overwrite: cmd.Overwrite,
 | |
| 	}
 | |
| 
 | |
| 	saveDashboardCmd, err := s.BuildSaveDashboardCommand(ctx, dto)
 | |
| 	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.dashboardFolderStore.GetFolderByID(ctx, cmd.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:     cmd.OrgID,
 | |
| 		}); err != nil {
 | |
| 			logger.Error("failed to publish FolderTitleUpdated event", "folder", foldr.Title, "user", user.UserID, "error", err)
 | |
| 		}
 | |
| 	}
 | |
| 	return foldr, nil
 | |
| }
 | |
| 
 | |
| // prepareForUpdate updates an existing dashboard model from command into model for folder update
 | |
| func prepareForUpdate(dashFolder *dashboards.Dashboard, orgId int64, userId int64, cmd *folder.UpdateFolderCommand) {
 | |
| 	dashFolder.OrgID = orgId
 | |
| 
 | |
| 	title := dashFolder.Title
 | |
| 	if cmd.NewTitle != nil && *cmd.NewTitle != "" {
 | |
| 		title = *cmd.NewTitle
 | |
| 	}
 | |
| 	dashFolder.Title = strings.TrimSpace(title)
 | |
| 	dashFolder.Data.Set("title", dashFolder.Title)
 | |
| 
 | |
| 	if cmd.NewUID != nil && *cmd.NewUID != "" {
 | |
| 		dashFolder.SetUID(*cmd.NewUID)
 | |
| 	}
 | |
| 
 | |
| 	dashFolder.SetVersion(cmd.Version)
 | |
| 	dashFolder.IsFolder = true
 | |
| 
 | |
| 	if userId == 0 {
 | |
| 		userId = -1
 | |
| 	}
 | |
| 
 | |
| 	dashFolder.UpdatedBy = userId
 | |
| 	dashFolder.UpdateSlug()
 | |
| }
 | |
| 
 | |
| func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
 | |
| 	logger := s.log.FromContext(ctx)
 | |
| 	if cmd.SignedInUser == nil {
 | |
| 		return folder.ErrBadRequest.Errorf("missing signed in user")
 | |
| 	}
 | |
| 
 | |
| 	if s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
 | |
| 		err := s.nestedFolderDelete(ctx, cmd)
 | |
| 		if err != nil {
 | |
| 			logger.Error("the delete folder on folder table failed with err: ", "error", err)
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	dashFolder, err := s.dashboardFolderStore.GetFolderByUID(ctx, cmd.OrgID, cmd.UID)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	guard, err := guardian.NewByUID(ctx, dashFolder.UID, cmd.OrgID, cmd.SignedInUser)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if canSave, err := guard.CanDelete(); err != nil || !canSave {
 | |
| 		if err != nil {
 | |
| 			return toFolderError(err)
 | |
| 		}
 | |
| 		return dashboards.ErrFolderAccessDenied
 | |
| 	}
 | |
| 
 | |
| 	return s.legacyDelete(ctx, cmd, dashFolder)
 | |
| }
 | |
| 
 | |
| func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderCommand, dashFolder *folder.Folder) error {
 | |
| 	deleteCmd := dashboards.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) {
 | |
| 	if cmd.SignedInUser == nil {
 | |
| 		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
 | |
| 	}
 | |
| 
 | |
| 	g, err := guardian.NewByUID(ctx, cmd.UID, cmd.OrgID, cmd.SignedInUser)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if canSave, err := g.CanSave(); err != nil || !canSave {
 | |
| 		if err != nil {
 | |
| 			return nil, toFolderError(err)
 | |
| 		}
 | |
| 		return nil, dashboards.ErrFolderAccessDenied
 | |
| 	}
 | |
| 
 | |
| 	// here we get the folder, we need to get the height of current folder
 | |
| 	// and the depth of the new parent folder, the sum can't bypass 8
 | |
| 	folderHeight, err := s.store.GetHeight(ctx, cmd.UID, cmd.OrgID, &cmd.NewParentUID)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	parents, err := s.store.GetParents(ctx, folder.GetParentsQuery{UID: cmd.NewParentUID, OrgID: cmd.OrgID})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// current folder height + current folder + parent folder + parent folder depth should be less than or equal 8
 | |
| 	if folderHeight+len(parents)+2 > folder.MaxNestedFolderDepth {
 | |
| 		return nil, folder.ErrMaximumDepthReached
 | |
| 	}
 | |
| 
 | |
| 	// if the current folder is already a parent of newparent, we should return error
 | |
| 	for _, parent := range parents {
 | |
| 		if parent.UID == cmd.UID {
 | |
| 			return nil, folder.ErrCircularReference
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return s.store.Update(ctx, folder.UpdateFolderCommand{
 | |
| 		UID:          cmd.UID,
 | |
| 		OrgID:        cmd.OrgID,
 | |
| 		NewParentUID: &cmd.NewParentUID,
 | |
| 		SignedInUser: cmd.SignedInUser,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
 | |
| 	logger := s.log.FromContext(ctx)
 | |
| 	if cmd.SignedInUser == nil {
 | |
| 		return folder.ErrBadRequest.Errorf("missing signed in user")
 | |
| 	}
 | |
| 
 | |
| 	_, err := s.Get(ctx, &folder.GetFolderQuery{
 | |
| 		UID:          &cmd.UID,
 | |
| 		OrgID:        cmd.OrgID,
 | |
| 		SignedInUser: cmd.SignedInUser,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	folders, err := s.store.GetChildren(ctx, folder.GetChildrenQuery{UID: cmd.UID, OrgID: cmd.OrgID})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	for _, f := range folders {
 | |
| 		logger.Info("deleting subfolder", "org_id", f.OrgID, "uid", f.UID)
 | |
| 		err := s.nestedFolderDelete(ctx, &folder.DeleteFolderCommand{UID: f.UID, OrgID: f.OrgID, ForceDeleteRules: cmd.ForceDeleteRules, SignedInUser: cmd.SignedInUser})
 | |
| 		if err != nil {
 | |
| 			logger.Error("failed deleting subfolder", "org_id", f.OrgID, "uid", f.UID, "error", err)
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	logger.Info("deleting folder", "org_id", cmd.OrgID, "uid", cmd.UID)
 | |
| 	err = s.store.Delete(ctx, cmd.UID, cmd.OrgID)
 | |
| 	if err != nil {
 | |
| 		logger.Info("failed deleting folder", "org_id", cmd.OrgID, "uid", cmd.UID, "err", err)
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // MakeUserAdmin is copy of DashboardServiceImpl.MakeUserAdmin
 | |
| func (s *Service) MakeUserAdmin(ctx context.Context, orgID int64, userID, folderID int64, setViewAndEditPermissions bool) error {
 | |
| 	rtEditor := org.RoleEditor
 | |
| 	rtViewer := org.RoleViewer
 | |
| 
 | |
| 	items := []*dashboards.DashboardACL{
 | |
| 		{
 | |
| 			OrgID:       orgID,
 | |
| 			DashboardID: folderID,
 | |
| 			UserID:      userID,
 | |
| 			Permission:  models.PERMISSION_ADMIN,
 | |
| 			Created:     time.Now(),
 | |
| 			Updated:     time.Now(),
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	if setViewAndEditPermissions {
 | |
| 		items = append(items,
 | |
| 			&dashboards.DashboardACL{
 | |
| 				OrgID:       orgID,
 | |
| 				DashboardID: folderID,
 | |
| 				Role:        &rtEditor,
 | |
| 				Permission:  models.PERMISSION_EDIT,
 | |
| 				Created:     time.Now(),
 | |
| 				Updated:     time.Now(),
 | |
| 			},
 | |
| 			&dashboards.DashboardACL{
 | |
| 				OrgID:       orgID,
 | |
| 				DashboardID: folderID,
 | |
| 				Role:        &rtViewer,
 | |
| 				Permission:  models.PERMISSION_VIEW,
 | |
| 				Created:     time.Now(),
 | |
| 				Updated:     time.Now(),
 | |
| 			},
 | |
| 		)
 | |
| 	}
 | |
| 
 | |
| 	if err := s.dashboardStore.UpdateDashboardACL(ctx, folderID, items); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // BuildSaveDashboardCommand is a simplified version on DashboardServiceImpl.BuildSaveDashboardCommand
 | |
| // keeping only the meaningful functionality for folders
 | |
| func (s *Service) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*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
 | |
| 	}
 | |
| 
 | |
| 	isParentFolderChanged, err := s.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
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	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
 | |
| }
 | |
| 
 | |
| // 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 (s *Service) nestedFolderCreate(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
 | |
| 	if cmd.ParentUID != "" {
 | |
| 		if err := s.validateParent(ctx, cmd.OrgID, cmd.ParentUID, cmd.UID); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 	return s.store.Create(ctx, *cmd)
 | |
| }
 | |
| 
 | |
| func (s *Service) validateParent(ctx context.Context, orgID int64, parentUID string, UID string) error {
 | |
| 	ancestors, err := s.store.GetParents(ctx, folder.GetParentsQuery{UID: parentUID, OrgID: orgID})
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get parents: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	if len(ancestors) == folder.MaxNestedFolderDepth {
 | |
| 		return folder.ErrMaximumDepthReached
 | |
| 	}
 | |
| 
 | |
| 	// Create folder under itself is not allowed
 | |
| 	if parentUID == UID {
 | |
| 		return folder.ErrCircularReference
 | |
| 	}
 | |
| 
 | |
| 	// check there is no circular reference
 | |
| 	for _, ancestor := range ancestors {
 | |
| 		if ancestor.UID == UID {
 | |
| 			return folder.ErrCircularReference
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| 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
 | |
| }
 |