2022-10-11 03:47:53 +08:00
package folderimpl
2018-02-20 20:55:43 +08:00
import (
2021-09-14 22:08:04 +08:00
"context"
2024-02-26 18:27:22 +08:00
"encoding/json"
2020-11-19 21:47:17 +08:00
"errors"
2022-11-25 02:28:53 +08:00
"fmt"
2024-05-02 15:14:12 +08:00
"log/slog"
2023-12-16 01:34:08 +08:00
"runtime"
2021-03-17 23:06:10 +08:00
"strings"
2023-04-14 17:17:23 +08:00
"sync"
2023-12-05 23:13:31 +08:00
"time"
2020-11-19 21:47:17 +08:00
2023-12-05 23:13:31 +08:00
"github.com/prometheus/client_golang/prometheus"
2024-08-13 18:26:26 +08:00
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
2023-11-08 22:28:49 +08:00
"golang.org/x/exp/slices"
2025-01-23 22:25:03 +08:00
"github.com/grafana/dskit/concurrency"
2025-04-24 01:54:35 +08:00
dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
2025-04-15 04:20:10 +08:00
folderv1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
2024-06-13 12:11:35 +08:00
"github.com/grafana/grafana/pkg/apimachinery/identity"
2022-06-18 01:10:49 +08:00
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
2022-11-08 21:59:55 +08:00
"github.com/grafana/grafana/pkg/infra/db"
2024-01-25 18:10:35 +08:00
"github.com/grafana/grafana/pkg/infra/metrics"
2022-03-10 19:58:18 +08:00
"github.com/grafana/grafana/pkg/services/accesscontrol"
2025-01-22 04:42:38 +08:00
"github.com/grafana/grafana/pkg/services/apiserver"
2025-02-12 03:14:25 +08:00
"github.com/grafana/grafana/pkg/services/apiserver/client"
2025-01-14 05:15:35 +08:00
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
2022-02-16 21:15:44 +08:00
"github.com/grafana/grafana/pkg/services/dashboards"
2024-03-15 20:05:27 +08:00
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
2022-03-24 02:40:22 +08:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2022-10-11 03:47:53 +08:00
"github.com/grafana/grafana/pkg/services/folder"
2025-01-24 04:23:59 +08:00
"github.com/grafana/grafana/pkg/services/publicdashboards"
2025-01-30 07:44:42 +08:00
"github.com/grafana/grafana/pkg/services/search/model"
2025-02-19 02:30:11 +08:00
"github.com/grafana/grafana/pkg/services/search/sort"
2024-01-23 00:04:18 +08:00
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
2025-02-06 08:57:26 +08:00
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
2023-04-27 23:00:09 +08:00
"github.com/grafana/grafana/pkg/services/store/entity"
2024-02-26 18:27:22 +08:00
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/services/user"
2025-01-14 05:15:35 +08:00
"github.com/grafana/grafana/pkg/setting"
2025-02-19 22:50:39 +08:00
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
2025-02-14 19:34:52 +08:00
"github.com/grafana/grafana/pkg/storage/unified/resource"
2023-01-30 16:21:27 +08:00
"github.com/grafana/grafana/pkg/util"
2018-02-20 20:55:43 +08:00
)
2024-05-31 16:09:20 +08:00
const FULLPATH_SEPARATOR = "/"
2022-10-11 03:47:53 +08:00
type Service struct {
2025-01-24 04:23:59 +08:00
store folder . Store
unifiedStore folder . Store
db db . DB
log * slog . Logger
dashboardStore dashboards . Store
dashboardFolderStore folder . FolderStore
features featuremgmt . FeatureToggles
accessControl accesscontrol . AccessControl
2025-02-12 03:14:25 +08:00
k8sclient client . K8sHandler
dashboardK8sClient client . K8sHandler
2025-01-24 04:23:59 +08:00
publicDashboardService publicdashboards . ServiceWrapper
2024-08-13 18:26:26 +08:00
// bus is currently used to publish event in case of folder full path change.
// For example when a folder is moved to another folder or when a folder is renamed.
2022-06-18 01:10:49 +08:00
bus bus . Bus
2023-04-14 17:17:23 +08:00
mutex sync . RWMutex
registry map [ string ] folder . RegistryService
2023-12-05 23:13:31 +08:00
metrics * foldersMetrics
2025-05-20 04:25:08 +08:00
tracer trace . Tracer
2018-02-20 20:55:43 +08:00
}
2022-10-11 03:47:53 +08:00
func ProvideService (
2024-09-30 16:28:47 +08:00
store * FolderStoreImpl ,
2022-10-11 03:47:53 +08:00
ac accesscontrol . AccessControl ,
bus bus . Bus ,
dashboardStore dashboards . Store ,
2023-02-01 21:43:21 +08:00
folderStore folder . FolderStore ,
2025-01-27 21:29:47 +08:00
userService user . Service ,
2022-11-08 21:59:55 +08:00
db db . DB , // DB for the (new) nested folder store
2022-11-23 19:16:00 +08:00
features featuremgmt . FeatureToggles ,
2024-02-26 18:27:22 +08:00
supportBundles supportbundles . Service ,
2025-01-24 04:23:59 +08:00
publicDashboardService publicdashboards . ServiceWrapper ,
2025-01-14 05:15:35 +08:00
cfg * setting . Cfg ,
2023-12-05 23:13:31 +08:00
r prometheus . Registerer ,
2025-05-20 04:25:08 +08:00
tracer trace . Tracer ,
2025-02-14 19:34:52 +08:00
resourceClient resource . ResourceClient ,
2025-02-19 22:50:39 +08:00
dual dualwrite . Service ,
2025-02-19 02:30:11 +08:00
sorter sort . Service ,
2025-03-27 22:46:09 +08:00
restConfig apiserver . RestConfigProvider ,
2025-01-07 16:53:09 +08:00
) * Service {
2023-02-08 00:27:20 +08:00
srv := & Service {
2025-01-24 04:23:59 +08:00
log : slog . Default ( ) . With ( "logger" , "folder-service" ) ,
dashboardStore : dashboardStore ,
dashboardFolderStore : folderStore ,
store : store ,
features : features ,
accessControl : ac ,
bus : bus ,
db : db ,
registry : make ( map [ string ] folder . RegistryService ) ,
metrics : newFoldersMetrics ( r ) ,
tracer : tracer ,
publicDashboardService : publicDashboardService ,
2018-02-20 20:55:43 +08:00
}
2024-01-23 00:04:18 +08:00
srv . DBMigration ( db )
2023-01-26 16:21:10 +08:00
2024-02-26 18:27:22 +08:00
supportBundles . RegisterSupportItemCollector ( srv . supportBundleCollector ( ) )
2024-12-31 00:48:35 +08:00
ac . RegisterScopeAttributeResolver ( dashboards . NewFolderIDScopeResolver ( folderStore , srv ) )
ac . RegisterScopeAttributeResolver ( dashboards . NewFolderUIDScopeResolver ( srv ) )
2025-01-22 12:45:59 +08:00
2025-07-30 05:52:57 +08:00
k8sHandler := client . NewK8sHandler (
dual ,
request . GetNamespaceMapper ( cfg ) ,
folderv1 . FolderResourceInfo . GroupVersionResource ( ) ,
restConfig . GetRestConfig ,
dashboardStore ,
userService ,
resourceClient ,
sorter ,
features ,
)
unifiedStore := ProvideUnifiedStore ( k8sHandler , userService , tracer )
srv . unifiedStore = unifiedStore
srv . k8sclient = k8sHandler
dashHandler := client . NewK8sHandler (
dual ,
request . GetNamespaceMapper ( cfg ) ,
dashboardv1 . DashboardResourceInfo . GroupVersionResource ( ) ,
restConfig . GetRestConfig ,
dashboardStore ,
userService ,
resourceClient ,
sorter ,
features ,
)
srv . dashboardK8sClient = dashHandler
2025-02-12 03:14:25 +08:00
2023-02-08 00:27:20 +08:00
return srv
2022-11-12 22:51:46 +08:00
}
2024-01-23 00:04:18 +08:00
func ( s * Service ) DBMigration ( db db . DB ) {
s . log . Debug ( "syncing dashboard and folder tables started" )
ctx := context . Background ( )
err := db . WithDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
var err error
2025-03-12 18:02:00 +08:00
deleteOldFolders := true
2024-01-23 00:04:18 +08:00
if db . GetDialect ( ) . DriverName ( ) == migrator . SQLite {
2024-02-17 03:10:46 +08:00
// covered by UQE_folder_org_id_uid
2024-01-23 00:04:18 +08:00
_ , err = sess . Exec ( `
INSERT INTO folder ( uid , org_id , title , created , updated )
SELECT uid , org_id , title , created , updated FROM dashboard WHERE is_folder = 1
ON CONFLICT DO UPDATE SET title = excluded . title , updated = excluded . updated
` )
} else if db . GetDialect ( ) . DriverName ( ) == migrator . Postgres {
2024-02-17 03:10:46 +08:00
// covered by UQE_folder_org_id_uid
2024-01-23 00:04:18 +08:00
_ , err = sess . Exec ( `
INSERT INTO folder ( uid , org_id , title , created , updated )
SELECT uid , org_id , title , created , updated FROM dashboard WHERE is_folder = true
ON CONFLICT ( uid , org_id ) DO UPDATE SET title = excluded . title , updated = excluded . updated
` )
} else {
2024-02-17 03:10:46 +08:00
// covered by UQE_folder_org_id_uid
2024-01-23 00:04:18 +08:00
_ , err = sess . Exec ( `
INSERT INTO folder ( uid , org_id , title , created , updated )
SELECT * FROM ( SELECT uid , org_id , title , created , updated FROM dashboard WHERE is_folder = 1 ) AS derived
ON DUPLICATE KEY UPDATE title = derived . title , updated = derived . updated
` )
}
if err != nil {
return err
}
2024-02-17 03:10:46 +08:00
2025-03-12 18:02:00 +08:00
if deleteOldFolders {
// covered by UQE_folder_org_id_uid
_ , err = sess . Exec ( `
2024-01-23 00:04:18 +08:00
DELETE FROM folder WHERE NOT EXISTS
( SELECT 1 FROM dashboard WHERE dashboard . uid = folder . uid AND dashboard . org_id = folder . org_id AND dashboard . is_folder = true )
` )
2025-03-12 18:02:00 +08:00
}
2024-01-23 00:04:18 +08:00
return err
} )
if err != nil {
s . log . Error ( "DB migration on folder service start failed." , "err" , err )
}
s . log . Debug ( "syncing dashboard and folder tables finished" )
}
2025-03-29 09:17:50 +08:00
func ( s * Service ) CountFoldersInOrg ( ctx context . Context , orgID int64 ) ( int64 , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.CountFoldersInOrg" )
defer span . End ( )
2025-07-30 05:52:57 +08:00
return s . unifiedStore . CountInOrg ( ctx , orgID )
2025-03-29 09:17:50 +08:00
}
2025-01-30 07:44:42 +08:00
func ( s * Service ) SearchFolders ( ctx context . Context , q folder . SearchFoldersQuery ) ( model . HitList , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.SearchFolders" )
defer span . End ( )
2025-07-30 05:52:57 +08:00
// TODO:
// - implement filtering by alerting folders and k6 folders (see the dashboards store `FindDashboards` method for reference)
// - implement fallback on search client in unistore to go to legacy store (will need to read from dashboard store)
return s . searchFoldersFromApiServer ( ctx , q )
2025-01-30 07:44:42 +08:00
}
2024-01-25 15:27:13 +08:00
func ( s * Service ) GetFolders ( ctx context . Context , q folder . GetFoldersQuery ) ( [ ] * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.GetFolders" )
defer span . End ( )
2025-07-30 05:52:57 +08:00
return s . getFoldersFromApiServer ( ctx , q )
2025-01-14 05:15:35 +08:00
}
func ( s * Service ) GetFoldersLegacy ( ctx context . Context , q folder . GetFoldersQuery ) ( [ ] * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.GetFoldersLegacy" )
defer span . End ( )
2024-01-25 15:27:13 +08:00
if q . SignedInUser == nil {
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2024-09-30 16:28:47 +08:00
qry := folder . NewGetFoldersQuery ( q )
2024-01-25 15:27:13 +08:00
permissions := q . SignedInUser . GetPermissions ( )
folderPermissions := permissions [ dashboards . ActionFoldersRead ]
2024-09-30 16:28:47 +08:00
qry . AncestorUIDs = make ( [ ] string , 0 , len ( folderPermissions ) )
2024-01-27 01:12:45 +08:00
if len ( folderPermissions ) == 0 && ! q . SignedInUser . GetIsGrafanaAdmin ( ) {
return nil , nil
}
2024-01-25 15:27:13 +08:00
for _ , p := range folderPermissions {
if p == dashboards . ScopeFoldersAll {
// no need to query for folders with permissions
// the user has permission to access all folders
2024-09-30 16:28:47 +08:00
qry . AncestorUIDs = nil
2024-01-25 15:27:13 +08:00
break
}
if folderUid , found := strings . CutPrefix ( p , dashboards . ScopeFoldersPrefix ) ; found {
2024-09-30 16:28:47 +08:00
if ! slices . Contains ( qry . AncestorUIDs , folderUid ) {
qry . AncestorUIDs = append ( qry . AncestorUIDs , folderUid )
2024-01-25 15:27:13 +08:00
}
}
}
2024-02-06 22:18:40 +08:00
if ! s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) {
qry . WithFullpath = false // do not request full path if nested folders are disabled
qry . WithFullpathUIDs = false
}
2024-01-25 15:27:13 +08:00
dashFolders , err := s . store . GetFolders ( ctx , qry )
if err != nil {
return nil , folder . ErrInternal . Errorf ( "failed to fetch subfolders: %w" , err )
}
2024-02-06 22:18:40 +08:00
if ! s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) {
if q . WithFullpathUIDs || q . WithFullpath {
for _ , f := range dashFolders { // and fix the full path with folder title (unescaped)
if q . WithFullpath {
f . Fullpath = f . Title
}
if q . WithFullpathUIDs {
f . FullpathUIDs = f . UID
}
}
}
}
2024-07-05 18:19:03 +08:00
return dashFolders , nil
2024-01-25 15:27:13 +08:00
}
2024-01-18 22:12:49 +08:00
func ( s * Service ) Get ( ctx context . Context , q * folder . GetFolderQuery ) ( * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.Get" )
defer span . End ( )
2025-07-30 05:52:57 +08:00
return s . getFromApiServer ( ctx , q )
2025-01-14 05:15:35 +08:00
}
func ( s * Service ) GetLegacy ( ctx context . Context , q * folder . GetFolderQuery ) ( * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.GetLegacy" )
defer span . End ( )
2024-01-18 22:12:49 +08:00
if q . SignedInUser == nil {
2022-11-23 17:13:47 +08:00
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
2022-11-11 21:28:24 +08:00
}
2024-02-16 00:13:14 +08:00
if q . UID != nil && * q . UID == accesscontrol . GeneralFolderUID {
return folder . RootFolder , nil
}
2024-01-18 22:12:49 +08:00
if s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) && q . UID != nil && * q . UID == folder . SharedWithMeFolderUID {
2023-12-05 23:13:31 +08:00
return folder . SharedWithMeFolder . WithURL ( ) , nil
}
2023-11-15 23:30:00 +08:00
// nolint:staticcheck
2025-04-02 02:21:58 +08:00
if q . ID == nil && q . Title == nil && q . UID == nil {
2023-01-06 22:04:17 +08:00
return nil , folder . ErrBadRequest . Errorf ( "either on of UID, ID, Title fields must be present" )
}
2022-11-24 21:59:47 +08:00
2025-04-02 02:21:58 +08:00
// nolint:staticcheck
if ( q . UID != nil && * q . UID == "" ) || ( q . ID != nil && * q . ID == folder . GeneralFolder . ID ) {
return & folder . GeneralFolder , nil
}
f , err := s . dashboardFolderStore . Get ( ctx , * q )
if err != nil {
return nil , err
2023-02-08 23:16:53 +08:00
}
2025-05-15 22:55:19 +08:00
evaluator := accesscontrol . EvalPermission ( dashboards . ActionFoldersRead , dashboards . ScopeFoldersProvider . GetResourceScopeUID ( f . UID ) )
if canView , err := s . accessControl . Evaluate ( ctx , q . SignedInUser , evaluator ) ; err != nil || ! canView {
2023-01-06 22:04:17 +08:00
if err != nil {
return nil , toFolderError ( err )
}
return nil , dashboards . ErrFolderAccessDenied
}
// always expose the dashboard store sequential ID
2024-01-25 18:10:35 +08:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ) . Inc ( )
2023-11-21 04:44:51 +08:00
// nolint:staticcheck
2023-01-06 22:04:17 +08:00
2024-02-14 01:47:46 +08:00
if ! s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) {
2024-10-10 19:22:57 +08:00
f . Fullpath = f . Title // set full path to the folder title (unescaped)
f . FullpathUIDs = f . UID // set full path to the folder UID
2024-02-14 01:47:46 +08:00
}
2023-01-06 22:04:17 +08:00
return f , err
2022-11-11 21:28:24 +08:00
}
2025-05-06 22:20:49 +08:00
func ( s * Service ) setFullpath ( ctx context . Context , f * folder . Folder , forceLegacy bool ) ( * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.setFullpath" )
defer span . End ( )
2024-11-26 22:20:00 +08:00
if f . ParentUID == "" {
return f , nil
}
// Fetch the parent since the permissions for fetching the newly created folder
// are not yet present for the user--this requires a call to ClearUserPermissionCache
2025-01-14 05:15:35 +08:00
var parents [ ] * folder . Folder
var err error
if forceLegacy {
parents , err = s . GetParentsLegacy ( ctx , folder . GetParentsQuery {
UID : f . UID ,
OrgID : f . OrgID ,
} )
} else {
parents , err = s . GetParents ( ctx , folder . GetParentsQuery {
UID : f . UID ,
OrgID : f . OrgID ,
} )
}
2024-11-26 22:20:00 +08:00
if err != nil {
return nil , err
}
// #TODO revisit setting permissions so that we can centralise the logic for escaping slashes in titles
// Escape forward slashes in the title
2025-05-06 22:20:49 +08:00
f . Fullpath , f . FullpathUIDs = computeFullPath ( append ( parents , f ) )
2024-11-26 22:20:00 +08:00
return f , nil
}
2025-04-02 12:30:17 +08:00
func ( s * Service ) GetChildren ( ctx context . Context , q * folder . GetChildrenQuery ) ( [ ] * folder . FolderReference , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.GetChildren" )
defer span . End ( )
2025-07-30 05:52:57 +08:00
return s . getChildrenFromApiServer ( ctx , q )
2025-01-14 05:15:35 +08:00
}
2025-04-02 12:30:17 +08:00
func ( s * Service ) GetChildrenLegacy ( ctx context . Context , q * folder . GetChildrenQuery ) ( [ ] * folder . FolderReference , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.GetChildrenLegacy" )
defer span . End ( )
2024-01-24 21:15:32 +08:00
defer func ( t time . Time ) {
parent := q . UID
if q . UID != folder . SharedWithMeFolderUID {
parent = "folder"
}
s . metrics . foldersGetChildrenRequestsDuration . WithLabelValues ( parent ) . Observe ( time . Since ( t ) . Seconds ( ) )
} ( time . Now ( ) )
2023-12-16 01:34:08 +08:00
if q . SignedInUser == nil {
2022-12-19 16:52:04 +08:00
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2023-12-16 01:34:08 +08:00
if s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) && q . UID == folder . SharedWithMeFolderUID {
2025-01-14 05:15:35 +08:00
return s . GetSharedWithMe ( ctx , q , true )
2023-12-05 23:13:31 +08:00
}
2023-12-16 01:34:08 +08:00
if q . UID == "" {
return s . getRootFolders ( ctx , q )
}
2023-07-08 02:26:01 +08:00
2023-12-16 01:34:08 +08:00
// we only need to check access to the folder
// if the parent is accessible then the subfolders are accessible as well (due to inheritance)
2025-05-15 22:55:19 +08:00
folderScope := dashboards . ScopeFoldersProvider . GetResourceScopeUID ( q . UID )
evaluator := accesscontrol . EvalPermission ( dashboards . ActionFoldersRead , folderScope )
2024-03-15 20:05:27 +08:00
if q . Permission == dashboardaccess . PERMISSION_EDIT {
2025-05-15 22:55:19 +08:00
evaluator = accesscontrol . EvalPermission ( dashboards . ActionFoldersWrite , folderScope )
2024-03-15 20:05:27 +08:00
}
2025-05-15 22:55:19 +08:00
hasAccess , err := s . accessControl . Evaluate ( ctx , q . SignedInUser , evaluator )
2023-12-16 01:34:08 +08:00
if err != nil {
return nil , err
}
2024-03-15 20:05:27 +08:00
if ! hasAccess {
2023-12-16 01:34:08 +08:00
return nil , dashboards . ErrFolderAccessDenied
2023-07-08 02:26:01 +08:00
}
2023-12-16 01:34:08 +08:00
children , err := s . store . GetChildren ( ctx , * q )
2023-01-23 20:09:09 +08:00
if err != nil {
2018-02-20 20:55:43 +08:00
return nil , err
}
2023-08-01 16:04:44 +08:00
childrenUIDs := make ( [ ] string , 0 , len ( children ) )
for _ , f := range children {
childrenUIDs = append ( childrenUIDs , f . UID )
}
2023-12-16 01:34:08 +08:00
dashFolders , err := s . dashboardFolderStore . GetFolders ( ctx , q . OrgID , childrenUIDs )
2023-08-01 16:04:44 +08:00
if err != nil {
return nil , folder . ErrInternal . Errorf ( "failed to fetch subfolders from dashboard store: %w" , err )
}
2023-01-23 20:09:09 +08:00
for _ , f := range children {
// fetch folder from dashboard store
2023-08-01 16:04:44 +08:00
dashFolder , ok := dashFolders [ f . UID ]
if ! ok {
s . log . Error ( "failed to fetch folder by UID from dashboard store" , "uid" , f . UID )
2023-01-23 20:09:09 +08:00
continue
}
2023-08-01 16:04:44 +08:00
2023-07-08 02:26:01 +08:00
// always expose the dashboard store sequential ID
2024-01-25 18:10:35 +08:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ) . Inc ( )
2023-11-21 04:44:51 +08:00
// nolint:staticcheck
2023-07-08 02:26:01 +08:00
f . ID = dashFolder . ID
2023-12-16 01:34:08 +08:00
}
2023-07-08 02:26:01 +08:00
2023-12-16 01:34:08 +08:00
return children , nil
}
2018-02-20 20:55:43 +08:00
2025-04-02 12:30:17 +08:00
func ( s * Service ) getRootFolders ( ctx context . Context , q * folder . GetChildrenQuery ) ( [ ] * folder . FolderReference , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.getRootFolders" )
defer span . End ( )
2023-12-16 01:34:08 +08:00
permissions := q . SignedInUser . GetPermissions ( )
2024-03-15 20:05:27 +08:00
var folderPermissions [ ] string
if q . Permission == dashboardaccess . PERMISSION_EDIT {
folderPermissions = permissions [ dashboards . ActionFoldersWrite ]
} else {
folderPermissions = permissions [ dashboards . ActionFoldersRead ]
}
if len ( folderPermissions ) == 0 && ! q . SignedInUser . GetIsGrafanaAdmin ( ) {
return nil , nil
}
2023-12-16 01:34:08 +08:00
q . FolderUIDs = make ( [ ] string , 0 , len ( folderPermissions ) )
for _ , p := range folderPermissions {
if p == dashboards . ScopeFoldersAll {
// no need to query for folders with permissions
// the user has permission to access all folders
q . FolderUIDs = nil
break
2023-01-23 20:09:09 +08:00
}
2023-12-16 01:34:08 +08:00
if folderUid , found := strings . CutPrefix ( p , dashboards . ScopeFoldersPrefix ) ; found {
if ! slices . Contains ( q . FolderUIDs , folderUid ) {
q . FolderUIDs = append ( q . FolderUIDs , folderUid )
}
2023-05-10 21:05:53 +08:00
}
2023-12-16 01:34:08 +08:00
}
children , err := s . store . GetChildren ( ctx , * q )
if err != nil {
return nil , err
}
childrenUIDs := make ( [ ] string , 0 , len ( children ) )
for _ , f := range children {
childrenUIDs = append ( childrenUIDs , f . UID )
}
dashFolders , err := s . dashboardFolderStore . GetFolders ( ctx , q . OrgID , childrenUIDs )
if err != nil {
return nil , folder . ErrInternal . Errorf ( "failed to fetch subfolders from dashboard store: %w" , err )
}
if err := concurrency . ForEachJob ( ctx , len ( children ) , runtime . NumCPU ( ) , func ( ctx context . Context , i int ) error {
f := children [ i ]
// fetch folder from dashboard store
dashFolder , ok := dashFolders [ f . UID ]
if ! ok {
2025-04-02 12:30:17 +08:00
s . log . Error ( "failed to fetch folder by UID from dashboard store" , "orgID" , q . OrgID , "uid" , f . UID )
2023-01-23 20:09:09 +08:00
}
2023-12-16 01:34:08 +08:00
// always expose the dashboard store sequential ID
2024-01-25 18:10:35 +08:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ) . Inc ( )
2023-12-16 01:34:08 +08:00
// nolint:staticcheck
f . ID = dashFolder . ID
return nil
} ) ; err != nil {
return nil , folder . ErrInternal . Errorf ( "failed to assign folder sequential ID: %w" , err )
2018-02-20 20:55:43 +08:00
}
2023-12-16 01:34:08 +08:00
// add "shared with me" folder on the 1st page
if ( q . Page == 0 || q . Page == 1 ) && len ( q . FolderUIDs ) != 0 {
2025-04-02 12:30:17 +08:00
children = append ( [ ] * folder . FolderReference { folder . SharedWithMeFolder . ToFolderReference ( ) } , children ... )
2023-12-05 23:13:31 +08:00
}
2023-12-16 01:34:08 +08:00
return children , nil
2018-02-20 20:55:43 +08:00
}
2023-11-08 22:28:49 +08:00
// GetSharedWithMe returns folders available to user, which cannot be accessed from the root folders
2025-04-02 12:30:17 +08:00
func ( s * Service ) GetSharedWithMe ( ctx context . Context , q * folder . GetChildrenQuery , forceLegacy bool ) ( [ ] * folder . FolderReference , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.GetSharedWithMe" )
defer span . End ( )
2023-12-05 23:13:31 +08:00
start := time . Now ( )
2025-01-14 05:15:35 +08:00
availableNonRootFolders , err := s . getAvailableNonRootFolders ( ctx , q , forceLegacy )
2023-11-08 22:28:49 +08:00
if err != nil {
2023-12-05 23:13:31 +08:00
s . metrics . sharedWithMeFetchFoldersRequestsDuration . WithLabelValues ( "failure" ) . Observe ( time . Since ( start ) . Seconds ( ) )
2023-11-08 22:28:49 +08:00
return nil , folder . ErrInternal . Errorf ( "failed to fetch subfolders to which the user has explicit access: %w" , err )
}
2025-04-02 12:30:17 +08:00
var rootFolders [ ] * folder . FolderReference
2025-01-14 05:15:35 +08:00
if forceLegacy {
rootFolders , err = s . GetChildrenLegacy ( ctx , & folder . GetChildrenQuery { UID : "" , OrgID : q . OrgID , SignedInUser : q . SignedInUser , Permission : q . Permission } )
} else {
rootFolders , err = s . GetChildren ( ctx , & folder . GetChildrenQuery { UID : "" , OrgID : q . OrgID , SignedInUser : q . SignedInUser , Permission : q . Permission } )
}
2023-11-08 22:28:49 +08:00
if err != nil {
2023-12-05 23:13:31 +08:00
s . metrics . sharedWithMeFetchFoldersRequestsDuration . WithLabelValues ( "failure" ) . Observe ( time . Since ( start ) . Seconds ( ) )
2023-11-08 22:28:49 +08:00
return nil , folder . ErrInternal . Errorf ( "failed to fetch root folders to which the user has access: %w" , err )
}
2024-01-25 20:02:32 +08:00
2025-05-06 22:20:49 +08:00
dedupAvailableNonRootFolders := s . deduplicateAvailableFolders ( ctx , availableNonRootFolders , rootFolders )
2023-12-05 23:13:31 +08:00
s . metrics . sharedWithMeFetchFoldersRequestsDuration . WithLabelValues ( "success" ) . Observe ( time . Since ( start ) . Seconds ( ) )
2025-04-02 12:30:17 +08:00
return dedupAvailableNonRootFolders , nil
2023-11-08 22:28:49 +08:00
}
2025-01-14 05:15:35 +08:00
func ( s * Service ) getAvailableNonRootFolders ( ctx context . Context , q * folder . GetChildrenQuery , forceLegacy bool ) ( [ ] * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.getAvailableNonRootFolders" )
defer span . End ( )
2024-03-15 20:05:27 +08:00
permissions := q . SignedInUser . GetPermissions ( )
var folderPermissions [ ] string
if q . Permission == dashboardaccess . PERMISSION_EDIT {
folderPermissions = permissions [ dashboards . ActionFoldersWrite ]
folderPermissions = append ( folderPermissions , permissions [ dashboards . ActionDashboardsWrite ] ... )
} else {
folderPermissions = permissions [ dashboards . ActionFoldersRead ]
folderPermissions = append ( folderPermissions , permissions [ dashboards . ActionDashboardsRead ] ... )
}
if len ( folderPermissions ) == 0 {
return nil , nil
}
2023-11-08 22:28:49 +08:00
nonRootFolders := make ( [ ] * folder . Folder , 0 )
2023-12-16 01:34:08 +08:00
folderUids := make ( [ ] string , 0 , len ( folderPermissions ) )
2023-11-08 22:28:49 +08:00
for _ , p := range folderPermissions {
2023-12-05 23:13:31 +08:00
if folderUid , found := strings . CutPrefix ( p , dashboards . ScopeFoldersPrefix ) ; found {
2023-11-08 22:28:49 +08:00
if ! slices . Contains ( folderUids , folderUid ) {
folderUids = append ( folderUids , folderUid )
}
}
}
if len ( folderUids ) == 0 {
return nonRootFolders , nil
}
2025-01-14 05:15:35 +08:00
var dashFolders [ ] * folder . Folder
var err error
if forceLegacy {
dashFolders , err = s . GetFoldersLegacy ( ctx , folder . GetFoldersQuery {
UIDs : folderUids ,
OrgID : q . OrgID ,
SignedInUser : q . SignedInUser ,
OrderByTitle : true ,
WithFullpathUIDs : true ,
} )
} else {
dashFolders , err = s . GetFolders ( ctx , folder . GetFoldersQuery {
UIDs : folderUids ,
OrgID : q . OrgID ,
SignedInUser : q . SignedInUser ,
OrderByTitle : true ,
WithFullpathUIDs : true ,
} )
}
2023-11-08 22:28:49 +08:00
if err != nil {
return nil , folder . ErrInternal . Errorf ( "failed to fetch subfolders: %w" , err )
}
for _ , f := range dashFolders {
if f . ParentUID != "" {
nonRootFolders = append ( nonRootFolders , f )
}
}
return nonRootFolders , nil
}
2025-05-06 22:20:49 +08:00
func ( s * Service ) deduplicateAvailableFolders ( ctx context . Context , folders [ ] * folder . Folder , rootFolders [ ] * folder . FolderReference ) [ ] * folder . FolderReference {
2025-04-02 12:30:17 +08:00
foldersRef := make ( [ ] * folder . FolderReference , len ( folders ) )
for i , f := range folders {
foldersRef [ i ] = f . ToFolderReference ( )
}
2025-04-03 17:54:47 +08:00
_ , span := s . tracer . Start ( ctx , "folder.deduplicateAvailableFolders" )
defer span . End ( )
2025-04-02 12:30:17 +08:00
allFolders := append ( foldersRef , rootFolders ... )
foldersDedup := make ( [ ] * folder . FolderReference , 0 )
2024-01-25 20:02:32 +08:00
2023-11-08 22:28:49 +08:00
for _ , f := range folders {
2025-04-02 12:30:17 +08:00
isSubfolder := slices . ContainsFunc ( allFolders , func ( folder * folder . FolderReference ) bool {
2023-11-08 22:28:49 +08:00
return f . ParentUID == folder . UID
} )
if ! isSubfolder {
2024-01-25 20:02:32 +08:00
// Get parents UIDs
parentUIDs := make ( [ ] string , 0 )
pathUIDs := strings . Split ( f . FullpathUIDs , "/" )
for _ , p := range pathUIDs {
if p != "" && p != f . UID {
parentUIDs = append ( parentUIDs , p )
}
2023-11-08 22:28:49 +08:00
}
2024-01-25 20:02:32 +08:00
for _ , parentUID := range parentUIDs {
2025-04-02 12:30:17 +08:00
contains := slices . ContainsFunc ( allFolders , func ( f * folder . FolderReference ) bool {
2024-01-25 20:02:32 +08:00
return f . UID == parentUID
2023-11-08 22:28:49 +08:00
} )
if contains {
isSubfolder = true
break
}
}
}
if ! isSubfolder {
2025-04-02 12:30:17 +08:00
foldersDedup = append ( foldersDedup , f . ToFolderReference ( ) )
2023-11-08 22:28:49 +08:00
}
}
return foldersDedup
}
2023-01-26 16:21:10 +08:00
func ( s * Service ) GetParents ( ctx context . Context , q folder . GetParentsQuery ) ( [ ] * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.GetParents" )
defer span . End ( )
2025-07-30 05:52:57 +08:00
return s . getParentsFromApiServer ( ctx , q )
2025-01-14 05:15:35 +08:00
}
func ( s * Service ) GetParentsLegacy ( ctx context . Context , q folder . GetParentsQuery ) ( [ ] * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.GetParentsLegacy" )
defer span . End ( )
2024-02-16 00:13:14 +08:00
if ! s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) || q . UID == accesscontrol . GeneralFolderUID {
2023-01-26 16:21:10 +08:00
return nil , nil
}
2023-12-05 23:13:31 +08:00
if q . UID == folder . SharedWithMeFolderUID {
return [ ] * folder . Folder { & folder . SharedWithMeFolder } , nil
}
2023-01-26 16:21:10 +08:00
return s . store . GetParents ( ctx , q )
}
2022-11-10 17:41:03 +08:00
func ( s * Service ) Create ( ctx context . Context , cmd * folder . CreateFolderCommand ) ( * folder . Folder , error ) {
2025-07-30 05:52:57 +08:00
return s . createOnApiServer ( ctx , cmd )
2025-01-14 05:15:35 +08:00
}
func ( s * Service ) CreateLegacy ( ctx context . Context , cmd * folder . CreateFolderCommand ) ( * folder . Folder , error ) {
2023-09-06 17:16:10 +08:00
if cmd . SignedInUser == nil || cmd . SignedInUser . IsNil ( ) {
2023-03-20 19:04:22 +08:00
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2023-10-24 15:04:45 +08:00
dashFolder := dashboards . NewDashboardFolder ( cmd . Title )
dashFolder . OrgID = cmd . OrgID
2023-11-15 04:50:27 +08:00
if s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) && cmd . ParentUID != "" {
2023-03-20 19:04:22 +08:00
// Check that the user is allowed to create a subfolder in this folder
2024-08-01 23:20:38 +08:00
parentUIDScope := dashboards . ScopeFoldersProvider . GetResourceScopeUID ( cmd . ParentUID )
legacyEvaluator := accesscontrol . EvalPermission ( dashboards . ActionFoldersWrite , parentUIDScope )
newEvaluator := accesscontrol . EvalPermission ( dashboards . ActionFoldersCreate , parentUIDScope )
evaluator := accesscontrol . EvalAny ( legacyEvaluator , newEvaluator )
2023-03-20 19:04:22 +08:00
hasAccess , evalErr := s . accessControl . Evaluate ( ctx , cmd . SignedInUser , evaluator )
if evalErr != nil {
return nil , evalErr
}
if ! hasAccess {
2024-08-01 23:20:38 +08:00
return nil , dashboards . ErrFolderCreationAccessDenied . Errorf ( "user is missing the permission with action either folders:create or folders:write and scope %s or any of the parent folder scopes" , parentUIDScope )
2023-03-20 19:04:22 +08:00
}
2023-10-24 15:04:45 +08:00
dashFolder . FolderUID = cmd . ParentUID
2023-03-20 19:04:22 +08:00
}
2024-08-01 23:20:38 +08:00
if cmd . ParentUID == "" {
evaluator := accesscontrol . EvalPermission ( dashboards . ActionFoldersCreate , dashboards . ScopeFoldersProvider . GetResourceScopeUID ( folder . GeneralFolderUID ) )
hasAccess , evalErr := s . accessControl . Evaluate ( ctx , cmd . SignedInUser , evaluator )
if evalErr != nil {
return nil , evalErr
}
if ! hasAccess {
return nil , dashboards . ErrFolderCreationAccessDenied . Errorf ( "user is missing the permission with action folders:create and scope folders:uid:general, which is required to create a folder under the root level" )
}
}
2023-12-05 23:13:31 +08:00
if s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) && cmd . UID == folder . SharedWithMeFolderUID {
return nil , folder . ErrBadRequest . Errorf ( "cannot create folder with UID %s" , folder . SharedWithMeFolderUID )
}
2022-11-10 17:41:03 +08:00
trimmedUID := strings . TrimSpace ( cmd . UID )
2022-03-30 21:14:26 +08:00
if trimmedUID == accesscontrol . GeneralFolderUID {
2022-06-30 21:31:54 +08:00
return nil , dashboards . ErrFolderInvalidUID
2022-03-30 21:14:26 +08:00
}
2023-01-16 23:33:55 +08:00
dashFolder . SetUID ( trimmedUID )
2022-11-10 17:41:03 +08:00
2022-11-23 17:13:47 +08:00
user := cmd . SignedInUser
2023-09-06 19:37:54 +08:00
2024-08-09 23:20:24 +08:00
var userID int64
if id , err := identity . UserIdentifier ( cmd . SignedInUser . GetID ( ) ) ; err == nil {
userID = id
2025-04-25 23:50:47 +08:00
} else if ! identity . IsServiceIdentity ( ctx ) {
2024-08-09 23:20:24 +08:00
s . log . Warn ( "User does not belong to a user or service account namespace, using 0 as user ID" , "id" , cmd . SignedInUser . GetID ( ) )
2023-08-30 22:51:18 +08:00
}
2021-03-17 23:06:10 +08:00
if userID == 0 {
userID = - 1
}
2024-08-09 23:20:24 +08:00
2021-03-17 23:06:10 +08:00
dashFolder . CreatedBy = userID
dashFolder . UpdatedBy = userID
dashFolder . UpdateSlug ( )
2018-02-20 20:55:43 +08:00
2022-02-16 21:15:44 +08:00
dto := & dashboards . SaveDashboardDTO {
2018-02-20 20:55:43 +08:00
Dashboard : dashFolder ,
2023-01-16 23:33:55 +08:00
OrgID : cmd . OrgID ,
2022-02-16 21:15:44 +08:00
User : user ,
2018-02-20 20:55:43 +08:00
}
2023-10-24 15:04:45 +08:00
saveDashboardCmd , err := s . buildSaveDashboardCommand ( ctx , dto )
2018-02-20 20:55:43 +08:00
if err != nil {
2021-03-17 23:06:10 +08:00
return nil , toFolderError ( err )
2018-02-20 20:55:43 +08:00
}
2023-11-22 05:06:20 +08:00
var nestedFolder * folder . Folder
var dash * dashboards . Dashboard
2024-12-13 04:08:46 +08:00
var f * folder . Folder
2023-11-22 05:06:20 +08:00
err = s . db . InTransaction ( ctx , func ( ctx context . Context ) error {
if dash , err = s . dashboardStore . SaveDashboard ( ctx , * saveDashboardCmd ) ; err != nil {
return toFolderError ( err )
}
2018-02-20 20:55:43 +08:00
2023-11-22 05:06:20 +08:00
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 ,
2024-01-23 00:03:30 +08:00
Title : dashFolder . Title ,
2023-11-22 05:06:20 +08:00
Description : cmd . Description ,
ParentUID : cmd . ParentUID ,
}
2018-02-20 20:55:43 +08:00
2023-11-22 05:06:20 +08:00
if nestedFolder , err = s . nestedFolderCreate ( ctx , cmd ) ; err != nil {
2024-05-02 15:14:12 +08:00
s . log . ErrorContext ( ctx , "error saving folder to nested folder store" , "error" , err )
2023-11-22 05:06:20 +08:00
return err
2022-11-08 21:59:55 +08:00
}
2023-11-22 05:06:20 +08:00
return nil
} )
2024-12-13 04:08:46 +08:00
if err != nil {
2024-10-01 20:03:02 +08:00
return nil , err
}
2024-12-31 00:48:35 +08:00
f = dashboards . FromDashboard ( dash )
if nestedFolder != nil && nestedFolder . ParentUID != "" {
f . ParentUID = nestedFolder . ParentUID
}
2022-11-24 21:59:47 +08:00
return f , nil
2018-02-20 20:55:43 +08:00
}
2022-12-20 21:00:33 +08:00
func ( s * Service ) Update ( ctx context . Context , cmd * folder . UpdateFolderCommand ) ( * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.Update" )
defer span . End ( )
2025-07-30 05:52:57 +08:00
return s . updateOnApiServer ( ctx , cmd )
2025-01-14 05:15:35 +08:00
}
func ( s * Service ) UpdateLegacy ( ctx context . Context , cmd * folder . UpdateFolderCommand ) ( * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.UpdateLegacy" )
2024-08-13 18:26:26 +08:00
defer span . End ( )
2022-12-20 21:00:33 +08:00
if cmd . SignedInUser == nil {
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
user := cmd . SignedInUser
2023-11-22 05:06:20 +08:00
var dashFolder , foldr * folder . Folder
var err error
err = s . db . InTransaction ( ctx , func ( ctx context . Context ) error {
if dashFolder , err = s . legacyUpdate ( ctx , cmd ) ; err != nil {
return err
}
2022-11-10 21:28:55 +08:00
2023-11-22 05:06:20 +08:00
if foldr , err = s . store . Update ( ctx , folder . UpdateFolderCommand {
UID : cmd . UID ,
OrgID : cmd . OrgID ,
2024-01-23 00:03:30 +08:00
NewTitle : & dashFolder . Title ,
2023-11-22 05:06:20 +08:00
NewDescription : cmd . NewDescription ,
SignedInUser : user ,
} ) ; err != nil {
return err
2023-11-10 20:03:00 +08:00
}
2023-11-22 05:06:20 +08:00
if cmd . NewTitle != nil {
2024-01-25 18:10:35 +08:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ) . Inc ( )
2024-08-13 18:26:26 +08:00
if err := s . publishFolderFullPathUpdatedEvent ( ctx , foldr . Updated , cmd . OrgID , cmd . UID ) ; err != nil {
2023-11-22 05:06:20 +08:00
return err
}
}
return nil
} )
if err != nil {
2024-05-02 15:14:12 +08:00
s . log . ErrorContext ( ctx , "folder update failed" , "folderUID" , cmd . UID , "error" , err )
2023-01-06 22:04:17 +08:00
return nil , err
}
2023-11-22 05:06:20 +08:00
if ! s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) {
return dashFolder , nil
}
2023-01-06 22:04:17 +08:00
// always expose the dashboard store sequential ID
2024-01-25 18:10:35 +08:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ) . Inc ( )
2023-11-21 04:44:51 +08:00
// nolint:staticcheck
2023-01-06 22:04:17 +08:00
foldr . ID = dashFolder . ID
2023-05-16 21:41:14 +08:00
foldr . Version = dashFolder . Version
2023-01-06 22:04:17 +08:00
2022-11-10 21:28:55 +08:00
return foldr , nil
}
2022-12-20 21:00:33 +08:00
func ( s * Service ) legacyUpdate ( ctx context . Context , cmd * folder . UpdateFolderCommand ) ( * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.legacyUpdate" )
defer span . End ( )
2023-01-16 23:33:55 +08:00
query := dashboards . GetDashboardQuery { OrgID : cmd . OrgID , UID : cmd . UID }
2023-01-25 17:36:26 +08:00
queryResult , err := s . dashboardStore . GetDashboard ( ctx , & query )
2022-11-10 21:28:55 +08:00
if err != nil {
return nil , toFolderError ( err )
2018-02-20 20:55:43 +08:00
}
2023-01-25 17:36:26 +08:00
dashFolder := queryResult
2023-10-24 15:04:45 +08:00
if cmd . NewParentUID != nil {
dashFolder . FolderUID = * cmd . NewParentUID
}
2022-03-24 02:40:22 +08:00
if ! dashFolder . IsFolder {
2022-11-10 21:28:55 +08:00
return nil , dashboards . ErrFolderNotFound
2022-03-24 02:40:22 +08:00
}
2022-12-20 21:00:33 +08:00
if cmd . SignedInUser == nil {
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2023-10-06 21:02:34 +08:00
var userID int64
2024-08-09 23:20:24 +08:00
if id , err := identity . UserIdentifier ( cmd . SignedInUser . GetID ( ) ) ; err == nil {
userID = id
2025-04-25 23:50:47 +08:00
} else if ! identity . IsServiceIdentity ( ctx ) {
2024-08-09 23:20:24 +08:00
s . log . Warn ( "User does not belong to a user or service account namespace, using 0 as user ID" , "id" , cmd . SignedInUser . GetID ( ) )
2023-10-06 21:02:34 +08:00
}
prepareForUpdate ( dashFolder , cmd . OrgID , userID , cmd )
2018-02-20 20:55:43 +08:00
2022-02-16 21:15:44 +08:00
dto := & dashboards . SaveDashboardDTO {
2018-02-20 20:55:43 +08:00
Dashboard : dashFolder ,
2023-01-16 23:33:55 +08:00
OrgID : cmd . OrgID ,
2022-12-20 21:00:33 +08:00
User : cmd . SignedInUser ,
2018-02-20 20:55:43 +08:00
Overwrite : cmd . Overwrite ,
}
2023-10-24 15:04:45 +08:00
saveDashboardCmd , err := s . buildSaveDashboardCommand ( ctx , dto )
2018-02-20 20:55:43 +08:00
if err != nil {
2022-11-10 21:28:55 +08:00
return nil , toFolderError ( err )
2018-02-20 20:55:43 +08:00
}
2022-10-11 03:47:53 +08:00
dash , err := s . dashboardStore . SaveDashboard ( ctx , * saveDashboardCmd )
2018-02-20 20:55:43 +08:00
if err != nil {
2022-11-10 21:28:55 +08:00
return nil , toFolderError ( err )
2018-02-20 20:55:43 +08:00
}
2022-11-11 21:28:24 +08:00
var foldr * folder . Folder
2023-01-20 00:38:07 +08:00
foldr , err = s . dashboardFolderStore . GetFolderByID ( ctx , cmd . OrgID , dash . ID )
2018-02-20 20:55:43 +08:00
if err != nil {
2022-11-10 21:28:55 +08:00
return nil , err
2018-02-20 20:55:43 +08:00
}
2022-06-18 01:10:49 +08:00
2022-11-10 21:28:55 +08:00
return foldr , nil
2018-02-20 20:55:43 +08:00
}
2022-12-20 21:00:33 +08:00
// prepareForUpdate updates an existing dashboard model from command into model for folder update
2023-01-16 23:33:55 +08:00
func prepareForUpdate ( dashFolder * dashboards . Dashboard , orgId int64 , userId int64 , cmd * folder . UpdateFolderCommand ) {
dashFolder . OrgID = orgId
2022-12-20 21:00:33 +08:00
title := dashFolder . Title
if cmd . NewTitle != nil && * cmd . NewTitle != "" {
title = * cmd . NewTitle
}
dashFolder . Title = strings . TrimSpace ( title )
dashFolder . Data . Set ( "title" , dashFolder . Title )
dashFolder . SetVersion ( cmd . Version )
dashFolder . IsFolder = true
if userId == 0 {
userId = - 1
}
dashFolder . UpdatedBy = userId
dashFolder . UpdateSlug ( )
}
2022-12-20 23:38:09 +08:00
func ( s * Service ) Delete ( ctx context . Context , cmd * folder . DeleteFolderCommand ) error {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.Delete" )
defer span . End ( )
2025-07-30 05:52:57 +08:00
return s . deleteFromApiServer ( ctx , cmd )
2025-01-14 05:15:35 +08:00
}
func ( s * Service ) DeleteLegacy ( ctx context . Context , cmd * folder . DeleteFolderCommand ) error {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.DeleteLegacy" )
defer span . End ( )
2022-11-23 22:44:45 +08:00
if cmd . SignedInUser == nil {
return folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2023-04-14 17:17:23 +08:00
if cmd . UID == "" {
return folder . ErrBadRequest . Errorf ( "missing UID" )
}
if cmd . OrgID < 1 {
return folder . ErrBadRequest . Errorf ( "invalid orgID" )
}
2023-07-08 02:26:01 +08:00
2025-05-15 22:55:19 +08:00
evaluator := accesscontrol . EvalPermission ( dashboards . ActionFoldersDelete , dashboards . ScopeFoldersProvider . GetResourceScopeUID ( cmd . UID ) )
if canDelete , err := s . accessControl . Evaluate ( ctx , cmd . SignedInUser , evaluator ) ; err != nil || ! canDelete {
2023-07-08 02:26:01 +08:00
if err != nil {
return toFolderError ( err )
}
return dashboards . ErrFolderAccessDenied
}
2024-01-31 00:26:34 +08:00
folders := [ ] string { cmd . UID }
2025-05-15 22:55:19 +08:00
err := s . db . InTransaction ( ctx , func ( ctx context . Context ) error {
2024-01-31 00:26:34 +08:00
descendants , err := s . nestedFolderDelete ( ctx , cmd )
2022-11-23 22:44:45 +08:00
2023-11-10 20:03:00 +08:00
if err != nil {
2024-05-02 15:14:12 +08:00
s . log . ErrorContext ( ctx , "the delete folder on folder table failed with err: " , "error" , err )
2023-11-10 20:03:00 +08:00
return err
2022-11-10 16:42:32 +08:00
}
2024-01-31 00:26:34 +08:00
folders = append ( folders , descendants ... )
2022-11-10 16:42:32 +08:00
2024-01-31 00:26:34 +08:00
if cmd . ForceDeleteRules {
if err := s . deleteChildrenInFolder ( ctx , cmd . OrgID , folders , cmd . SignedInUser ) ; err != nil {
return err
2023-03-15 16:51:37 +08:00
}
2024-01-31 00:26:34 +08:00
} else {
alertRuleSrv , ok := s . registry [ entity . StandardKindAlertRule ]
if ! ok {
return folder . ErrInternal . Errorf ( "no alert rule service found in registry" )
2023-04-14 17:17:23 +08:00
}
2024-01-31 00:26:34 +08:00
alertRulesInFolder , err := alertRuleSrv . CountInFolders ( ctx , cmd . OrgID , folders , cmd . SignedInUser )
if err != nil {
s . log . Error ( "failed to count alert rules in folder" , "error" , err )
2023-03-15 16:51:37 +08:00
return err
}
2024-01-31 00:26:34 +08:00
if alertRulesInFolder > 0 {
return folder . ErrFolderNotEmpty . Errorf ( "folder contains %d alert rules" , alertRulesInFolder )
}
}
2025-01-23 21:36:07 +08:00
err = s . store . Delete ( ctx , [ ] string { cmd . UID } , cmd . OrgID )
if err != nil {
s . log . InfoContext ( ctx , "failed deleting folder" , "org_id" , cmd . OrgID , "uid" , cmd . UID , "err" , err )
return err
}
2024-01-31 00:26:34 +08:00
if err = s . legacyDelete ( ctx , cmd , folders ) ; err != nil {
return err
2018-02-20 20:55:43 +08:00
}
2024-01-31 00:26:34 +08:00
2023-03-15 16:51:37 +08:00
return nil
} )
2018-02-20 20:55:43 +08:00
2023-03-15 16:51:37 +08:00
return err
2022-12-20 23:38:09 +08:00
}
2024-01-31 00:26:34 +08:00
func ( s * Service ) deleteChildrenInFolder ( ctx context . Context , orgID int64 , folderUIDs [ ] string , user identity . Requester ) error {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.deleteChildrenInFolder" )
defer span . End ( )
2023-04-14 17:17:23 +08:00
for _ , v := range s . registry {
2024-01-31 00:26:34 +08:00
if err := v . DeleteInFolders ( ctx , orgID , folderUIDs , user ) ; err != nil {
2023-04-14 17:17:23 +08:00
return err
}
}
return nil
}
2024-01-31 00:26:34 +08:00
func ( s * Service ) legacyDelete ( ctx context . Context , cmd * folder . DeleteFolderCommand , folderUIDs [ ] string ) error {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.legacyDelete" )
defer span . End ( )
2025-04-03 15:52:54 +08:00
// We need a list of dashboard uids inside the folder to delete related public dashboards
dashes , err := s . dashboardStore . FindDashboards ( ctx , & dashboards . FindPersistedDashboardsQuery {
SignedInUser : cmd . SignedInUser ,
FolderUIDs : folderUIDs ,
OrgId : cmd . OrgID ,
Type : searchstore . TypeDashboard ,
} )
if err != nil {
return folder . ErrInternal . Errorf ( "failed to fetch dashboards: %w" , err )
}
2025-01-24 04:23:59 +08:00
2025-04-03 15:52:54 +08:00
dashboardUIDs := make ( [ ] string , 0 , len ( dashes ) )
for _ , dashboard := range dashes {
dashboardUIDs = append ( dashboardUIDs , dashboard . UID )
}
2025-01-24 04:23:59 +08:00
2025-04-03 15:52:54 +08:00
// Delete all public dashboards in the folders
err = s . publicDashboardService . DeleteByDashboardUIDs ( ctx , cmd . OrgID , dashboardUIDs )
if err != nil {
return folder . ErrInternal . Errorf ( "failed to delete public dashboards: %w" , err )
2025-01-24 04:23:59 +08:00
}
2024-01-31 00:26:34 +08:00
// TODO use bulk delete
2025-01-24 04:23:59 +08:00
// Delete all dashboards in the folders
2024-01-31 00:26:34 +08:00
for _ , folderUID := range folderUIDs {
2024-05-17 01:36:26 +08:00
// nolint:staticcheck
2024-05-30 19:21:34 +08:00
deleteCmd := dashboards . DeleteDashboardCommand { OrgID : cmd . OrgID , UID : folderUID , ForceDeleteFolderRules : cmd . ForceDeleteRules }
2024-01-31 00:26:34 +08:00
if err := s . dashboardStore . DeleteDashboard ( ctx , & deleteCmd ) ; err != nil {
return toFolderError ( err )
}
2018-02-20 20:55:43 +08:00
}
2025-01-24 04:23:59 +08:00
2022-11-10 16:42:32 +08:00
return nil
2018-02-20 20:55:43 +08:00
}
2022-11-08 18:33:13 +08:00
func ( s * Service ) Move ( ctx context . Context , cmd * folder . MoveFolderCommand ) ( * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.Move" )
defer span . End ( )
2025-07-30 05:52:57 +08:00
return s . moveOnApiServer ( ctx , cmd )
2025-01-14 05:15:35 +08:00
}
func ( s * Service ) MoveLegacy ( ctx context . Context , cmd * folder . MoveFolderCommand ) ( * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.MoveLegacy" )
2024-08-13 18:26:26 +08:00
defer span . End ( )
2022-11-23 17:13:47 +08:00
if cmd . SignedInUser == nil {
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2024-06-10 22:17:51 +08:00
// k6-specific check to prevent folder move for a k6-app folder and its children
if cmd . UID == accesscontrol . K6FolderUID {
return nil , folder . ErrBadRequest . Errorf ( "k6 project may not be moved" )
}
if f , err := s . store . Get ( ctx , folder . GetFolderQuery { UID : & cmd . UID , OrgID : cmd . OrgID } ) ; err != nil {
return nil , err
} else if f != nil && f . ParentUID == accesscontrol . K6FolderUID {
return nil , folder . ErrBadRequest . Errorf ( "k6 project may not be moved" )
}
2023-03-20 19:04:22 +08:00
// Check that the user is allowed to move the folder to the destination folder
2024-07-24 00:07:27 +08:00
hasAccess , evalErr := s . canMove ( ctx , cmd )
2023-07-10 20:14:21 +08:00
if evalErr != nil {
return nil , evalErr
}
if ! hasAccess {
return nil , dashboards . ErrFolderAccessDenied
2022-12-08 21:49:17 +08:00
}
// 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
2022-12-20 21:00:33 +08:00
folderHeight , err := s . store . GetHeight ( ctx , cmd . UID , cmd . OrgID , & cmd . NewParentUID )
2022-12-08 21:49:17 +08:00
if err != nil {
return nil , err
}
2022-12-20 23:38:09 +08:00
parents , err := s . store . GetParents ( ctx , folder . GetParentsQuery { UID : cmd . NewParentUID , OrgID : cmd . OrgID } )
2022-12-08 21:49:17 +08:00
if err != nil {
return nil , err
}
2023-11-15 17:25:40 +08:00
// height of the folder that is being moved + this current folder itself + depth of the NewParent folder should be less than or equal MaxNestedFolderDepth
if folderHeight + len ( parents ) + 1 > folder . MaxNestedFolderDepth {
2023-04-20 22:47:51 +08:00
return nil , folder . ErrMaximumDepthReached . Errorf ( "failed to move folder" )
2022-12-08 21:49:17 +08:00
}
for _ , parent := range parents {
2024-06-10 22:17:51 +08:00
// if the current folder is already a parent of newparent, we should return error
2022-12-20 21:00:33 +08:00
if parent . UID == cmd . UID {
2023-10-24 15:04:45 +08:00
return nil , folder . ErrCircularReference . Errorf ( "failed to move folder" )
2022-12-08 21:49:17 +08:00
}
}
2023-10-24 15:04:45 +08:00
var f * folder . Folder
if err := s . db . InTransaction ( ctx , func ( ctx context . Context ) error {
if f , err = s . store . Update ( ctx , folder . UpdateFolderCommand {
UID : cmd . UID ,
OrgID : cmd . OrgID ,
2024-06-10 22:17:51 +08:00
NewParentUID : & cmd . NewParentUID ,
2023-10-24 15:04:45 +08:00
SignedInUser : cmd . SignedInUser ,
} ) ; err != nil {
return folder . ErrInternal . Errorf ( "failed to move folder: %w" , err )
}
if _ , err := s . legacyUpdate ( ctx , & folder . UpdateFolderCommand {
UID : cmd . UID ,
OrgID : cmd . OrgID ,
2024-06-10 22:17:51 +08:00
NewParentUID : & cmd . NewParentUID ,
2023-10-24 15:04:45 +08:00
SignedInUser : cmd . SignedInUser ,
// bypass optimistic locking used for dashboards
Overwrite : true ,
} ) ; err != nil {
return folder . ErrInternal . Errorf ( "failed to move legacy folder: %w" , err )
}
2024-08-13 18:26:26 +08:00
if err := s . publishFolderFullPathUpdatedEvent ( ctx , f . Updated , cmd . OrgID , cmd . UID ) ; err != nil {
return err
}
2023-10-24 15:04:45 +08:00
return nil
} ) ; err != nil {
return nil , err
}
return f , nil
2022-11-08 18:33:13 +08:00
}
2024-08-13 18:26:26 +08:00
func ( s * Service ) publishFolderFullPathUpdatedEvent ( ctx context . Context , timestamp time . Time , orgID int64 , folderUID string ) error {
ctx , span := s . tracer . Start ( ctx , "folder.publishFolderFullPathUpdatedEvent" )
defer span . End ( )
descFolders , err := s . store . GetDescendants ( ctx , orgID , folderUID )
if err != nil {
s . log . ErrorContext ( ctx , "Failed to get descendants of the folder" , "folderUID" , folderUID , "orgID" , orgID , "error" , err )
return err
}
uids := make ( [ ] string , 0 , len ( descFolders ) + 1 )
uids = append ( uids , folderUID )
for _ , f := range descFolders {
uids = append ( uids , f . UID )
}
span . AddEvent ( "found folder descendants" , trace . WithAttributes (
attribute . Int64 ( "folders" , int64 ( len ( uids ) ) ) ,
) )
if err := s . bus . Publish ( ctx , & events . FolderFullPathUpdated {
Timestamp : timestamp ,
UIDs : uids ,
OrgID : orgID ,
} ) ; err != nil {
s . log . ErrorContext ( ctx , "Failed to publish FolderFullPathUpdated event" , "folderUID" , folderUID , "orgID" , orgID , "descendantsUIDs" , uids , "error" , err )
return err
}
return nil
}
2024-07-24 00:07:27 +08:00
func ( s * Service ) canMove ( ctx context . Context , cmd * folder . MoveFolderCommand ) ( bool , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.canMove" )
defer span . End ( )
2024-07-24 00:07:27 +08:00
// Check that the user is allowed to move the folder to the destination folder
var evaluator accesscontrol . Evaluator
parentUID := cmd . NewParentUID
if parentUID != "" {
2024-08-01 23:20:38 +08:00
legacyEvaluator := accesscontrol . EvalPermission ( dashboards . ActionFoldersWrite , dashboards . ScopeFoldersProvider . GetResourceScopeUID ( cmd . NewParentUID ) )
newEvaluator := accesscontrol . EvalPermission ( dashboards . ActionFoldersCreate , dashboards . ScopeFoldersProvider . GetResourceScopeUID ( cmd . NewParentUID ) )
evaluator = accesscontrol . EvalAny ( legacyEvaluator , newEvaluator )
2024-07-24 00:07:27 +08:00
} else {
// Evaluate folder creation permission when moving folder to the root level
2024-08-01 23:20:38 +08:00
evaluator = accesscontrol . EvalPermission ( dashboards . ActionFoldersCreate , dashboards . ScopeFoldersProvider . GetResourceScopeUID ( folder . GeneralFolderUID ) )
2024-07-24 00:07:27 +08:00
parentUID = folder . GeneralFolderUID
}
if hasAccess , err := s . accessControl . Evaluate ( ctx , cmd . SignedInUser , evaluator ) ; err != nil {
return false , err
} else if ! hasAccess {
return false , dashboards . ErrMoveAccessDenied . Errorf ( "user does not have permissions to move a folder to folder with UID %s" , parentUID )
}
// Check that the user would not be elevating their permissions by moving a folder to the destination folder
// This is needed for plugins, as different folders can have different plugin configs
// We do this by checking that there are no permissions that user has on the destination parent folder but not on the source folder
// We also need to look at the folder tree for the destination folder, as folder permissions are inherited
newFolderAndParentUIDs , err := s . getFolderAndParentUIDScopes ( ctx , parentUID , cmd . OrgID )
if err != nil {
return false , err
}
permissions := cmd . SignedInUser . GetPermissions ( )
var evaluators [ ] accesscontrol . Evaluator
currentFolderScope := dashboards . ScopeFoldersProvider . GetResourceScopeUID ( cmd . UID )
for action , scopes := range permissions {
for _ , scope := range newFolderAndParentUIDs {
if slices . Contains ( scopes , scope ) {
evaluators = append ( evaluators , accesscontrol . EvalPermission ( action , currentFolderScope ) )
break
}
}
}
if hasAccess , err := s . accessControl . Evaluate ( ctx , cmd . SignedInUser , accesscontrol . EvalAll ( evaluators ... ) ) ; err != nil {
return false , err
} else if ! hasAccess {
return false , dashboards . ErrFolderAccessEscalation . Errorf ( "user cannot move a folder to another folder where they have higher permissions" )
}
return true , nil
}
func ( s * Service ) getFolderAndParentUIDScopes ( ctx context . Context , folderUID string , orgID int64 ) ( [ ] string , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.getFolderAndParentUIDScopes" )
defer span . End ( )
2024-07-24 00:07:27 +08:00
folderAndParentUIDScopes := [ ] string { dashboards . ScopeFoldersProvider . GetResourceScopeUID ( folderUID ) }
if folderUID == folder . GeneralFolderUID {
return folderAndParentUIDScopes , nil
}
folderParents , err := s . store . GetParents ( ctx , folder . GetParentsQuery { UID : folderUID , OrgID : orgID } )
if err != nil {
return nil , err
}
for _ , newParent := range folderParents {
scope := dashboards . ScopeFoldersProvider . GetResourceScopeUID ( newParent . UID )
folderAndParentUIDScopes = append ( folderAndParentUIDScopes , scope )
}
return folderAndParentUIDScopes , nil
}
2023-03-15 16:51:37 +08:00
// nestedFolderDelete inspects the folder referenced by the cmd argument, deletes all the entries for
// its descendant folders (folders which are nested within it either directly or indirectly) from
// the folder store and returns the UIDs for all its descendants.
func ( s * Service ) nestedFolderDelete ( ctx context . Context , cmd * folder . DeleteFolderCommand ) ( [ ] string , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.nestedFolderDelete" )
defer span . End ( )
2024-01-31 00:26:34 +08:00
descendantUIDs := [ ] string { }
2022-11-23 17:13:47 +08:00
if cmd . SignedInUser == nil {
2024-01-31 00:26:34 +08:00
return descendantUIDs , folder . ErrBadRequest . Errorf ( "missing signed in user" )
2022-11-23 17:13:47 +08:00
}
2022-11-10 16:42:32 +08:00
_ , err := s . Get ( ctx , & folder . GetFolderQuery {
2022-11-23 17:13:47 +08:00
UID : & cmd . UID ,
OrgID : cmd . OrgID ,
SignedInUser : cmd . SignedInUser ,
2022-11-08 18:33:13 +08:00
} )
if err != nil {
2024-01-31 00:26:34 +08:00
return descendantUIDs , err
2022-11-10 16:42:32 +08:00
}
2024-01-31 00:26:34 +08:00
descendants , err := s . store . GetDescendants ( ctx , cmd . OrgID , cmd . UID )
2022-11-10 16:42:32 +08:00
if err != nil {
2024-05-02 15:14:12 +08:00
s . log . ErrorContext ( ctx , "failed to get descendant folders" , "error" , err )
2024-01-31 00:26:34 +08:00
return descendantUIDs , err
2022-11-08 18:33:13 +08:00
}
2023-04-14 17:17:23 +08:00
2024-01-31 00:26:34 +08:00
for _ , f := range descendants {
descendantUIDs = append ( descendantUIDs , f . UID )
}
2025-01-23 21:36:07 +08:00
s . log . InfoContext ( ctx , "deleting folder descendants" , "org_id" , cmd . OrgID , "uid" , cmd . UID )
err = s . store . Delete ( ctx , descendantUIDs , cmd . OrgID )
2022-11-08 18:33:13 +08:00
if err != nil {
2025-01-23 21:36:07 +08:00
s . log . InfoContext ( ctx , "failed deleting descendants" , "org_id" , cmd . OrgID , "parent_uid" , cmd . UID , "err" , err )
2024-01-31 00:26:34 +08:00
return descendantUIDs , err
2022-11-08 18:33:13 +08:00
}
2024-01-31 00:26:34 +08:00
return descendantUIDs , nil
2022-11-08 18:33:13 +08:00
}
2024-01-18 22:12:49 +08:00
func ( s * Service ) GetDescendantCounts ( ctx context . Context , q * folder . GetDescendantCountsQuery ) ( folder . DescendantCounts , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.GetDescendantCounts" )
defer span . End ( )
2025-07-30 05:52:57 +08:00
return s . getDescendantCountsFromApiServer ( ctx , q )
2025-01-14 05:15:35 +08:00
}
func ( s * Service ) GetDescendantCountsLegacy ( ctx context . Context , q * folder . GetDescendantCountsQuery ) ( folder . DescendantCounts , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.GetDescendantCountsLegacy" )
defer span . End ( )
2024-01-18 22:12:49 +08:00
if q . SignedInUser == nil {
2023-04-24 21:57:28 +08:00
return nil , folder . ErrBadRequest . Errorf ( "missing signed-in user" )
}
2024-01-31 00:26:34 +08:00
if q . UID == nil || * q . UID == "" {
2023-04-24 21:57:28 +08:00
return nil , folder . ErrBadRequest . Errorf ( "missing UID" )
}
2024-01-18 22:12:49 +08:00
if q . OrgID < 1 {
2023-04-24 21:57:28 +08:00
return nil , folder . ErrBadRequest . Errorf ( "invalid orgID" )
}
2024-01-31 00:26:34 +08:00
folders := [ ] string { * q . UID }
2023-04-27 23:00:09 +08:00
countsMap := make ( folder . DescendantCounts , len ( s . registry ) + 1 )
2023-11-15 04:50:27 +08:00
if s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) {
2024-01-31 00:26:34 +08:00
descendantFolders , err := s . store . GetDescendants ( ctx , q . OrgID , * q . UID )
2023-04-24 21:57:28 +08:00
if err != nil {
2024-05-02 15:14:12 +08:00
s . log . ErrorContext ( ctx , "failed to get descendant folders" , "error" , err )
2023-04-24 21:57:28 +08:00
return nil , err
}
2024-01-31 00:26:34 +08:00
for _ , f := range descendantFolders {
folders = append ( folders , f . UID )
2023-04-27 23:00:09 +08:00
}
2024-01-31 00:26:34 +08:00
countsMap [ entity . StandardKindFolder ] = int64 ( len ( descendantFolders ) )
2023-04-27 23:00:09 +08:00
}
2024-01-31 00:26:34 +08:00
for _ , v := range s . registry {
c , err := v . CountInFolders ( ctx , q . OrgID , folders , q . SignedInUser )
2023-04-27 23:00:09 +08:00
if err != nil {
2024-05-02 15:14:12 +08:00
s . log . ErrorContext ( ctx , "failed to count folder descendants" , "error" , err )
2023-04-27 23:00:09 +08:00
return nil , err
}
2024-01-31 00:26:34 +08:00
countsMap [ v . Kind ( ) ] = c
2023-04-27 23:00:09 +08:00
}
2024-01-31 00:26:34 +08:00
return countsMap , nil
2023-04-27 23:00:09 +08:00
}
2023-10-24 15:04:45 +08:00
// buildSaveDashboardCommand is a simplified version on DashboardServiceImpl.buildSaveDashboardCommand
2023-01-18 23:47:59 +08:00
// keeping only the meaningful functionality for folders
2023-10-24 15:04:45 +08:00
func ( s * Service ) buildSaveDashboardCommand ( ctx context . Context , dto * dashboards . SaveDashboardDTO ) ( * dashboards . SaveDashboardCommand , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.buildSaveDashboardCommand" )
defer span . End ( )
2023-01-18 23:47:59 +08:00
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
}
2023-11-16 19:11:35 +08:00
if strings . EqualFold ( dash . Title , dashboards . RootFolderName ) {
return nil , dashboards . ErrDashboardFolderNameExists
2023-01-18 23:47:59 +08:00
}
2023-11-16 19:11:35 +08:00
if dash . FolderUID != "" {
if _ , err := s . dashboardFolderStore . GetFolderByUID ( ctx , dash . OrgID , dash . FolderUID ) ; err != nil {
return nil , err
}
2023-01-18 23:47:59 +08:00
}
if ! util . IsValidShortUID ( dash . UID ) {
return nil , dashboards . ErrDashboardInvalidUid
} else if util . IsShortUIDTooLong ( dash . UID ) {
return nil , dashboards . ErrDashboardUidTooLong
}
2023-03-20 19:04:22 +08:00
_ , err := s . dashboardStore . ValidateDashboardBeforeSave ( ctx , dash , dto . Overwrite )
2023-01-18 23:47:59 +08:00
if err != nil {
return nil , err
}
2025-05-15 22:55:19 +08:00
var evaluator accesscontrol . Evaluator
// Check write permission for existing dashboards, create permission for new dashboards
2023-01-18 23:47:59 +08:00
if dash . ID == 0 {
2024-01-25 18:10:35 +08:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ) . Inc ( )
2025-05-15 22:55:19 +08:00
parentUID := dash . FolderUID
if parentUID == "" {
parentUID = folder . GeneralFolderUID
2023-01-18 23:47:59 +08:00
}
2025-05-15 22:55:19 +08:00
evaluator = accesscontrol . EvalPermission ( dashboards . ActionFoldersCreate , dashboards . ScopeFoldersProvider . GetResourceScopeUID ( parentUID ) )
2023-01-18 23:47:59 +08:00
} else {
2025-05-15 22:55:19 +08:00
evaluator = accesscontrol . EvalPermission ( dashboards . ActionFoldersWrite , dashboards . ScopeFoldersProvider . GetResourceScopeUID ( dash . UID ) )
}
if hasAccess , err := s . accessControl . Evaluate ( ctx , dto . User , evaluator ) ; err != nil || ! hasAccess {
if err != nil {
return nil , err
2023-01-18 23:47:59 +08:00
}
2025-05-15 22:55:19 +08:00
return nil , dashboards . ErrDashboardUpdateAccessDenied
2023-01-18 23:47:59 +08:00
}
2024-08-09 23:20:24 +08:00
var userID int64
if id , err := identity . UserIdentifier ( dto . User . GetID ( ) ) ; err == nil {
userID = id
2025-04-25 23:50:47 +08:00
} else if ! identity . IsServiceIdentity ( ctx ) {
2024-08-09 23:20:24 +08:00
s . log . Warn ( "User does not belong to a user or service account namespace, using 0 as user ID" , "id" , dto . User . GetID ( ) )
2023-08-30 22:51:18 +08:00
}
2024-01-25 18:10:35 +08:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ) . Inc ( )
2023-01-18 23:47:59 +08:00
cmd := & dashboards . SaveDashboardCommand {
Dashboard : dash . Data ,
Message : dto . Message ,
OrgID : dto . OrgID ,
Overwrite : dto . Overwrite ,
2023-08-30 22:51:18 +08:00
UserID : userID ,
2023-11-15 23:28:50 +08:00
FolderID : dash . FolderID , // nolint:staticcheck
2023-10-24 15:04:45 +08:00
FolderUID : dash . FolderUID ,
2023-01-18 23:47:59 +08:00
IsFolder : dash . IsFolder ,
PluginID : dash . PluginID ,
}
if ! dto . UpdatedAt . IsZero ( ) {
cmd . UpdatedAt = dto . UpdatedAt
}
return cmd , nil
}
2024-05-31 16:09:20 +08:00
// SplitFullpath splits a string into an array of strings using the FULLPATH_SEPARATOR as the delimiter.
// It handles escape characters by appending the separator and the new string if the current string ends with an escape character.
// The resulting array does not contain empty strings.
func SplitFullpath ( s string ) [ ] string {
splitStrings := strings . Split ( s , FULLPATH_SEPARATOR )
result := make ( [ ] string , 0 )
current := ""
for _ , str := range splitStrings {
if strings . HasSuffix ( current , "\\" ) {
// If the current string ends with an escape character, append the separator and the new string
current = current [ : len ( current ) - 1 ] + FULLPATH_SEPARATOR + str
} else {
// If the current string does not end with an escape character, append the current string to the result and start a new current string
if current != "" {
result = append ( result , current )
}
current = str
}
}
// Append the last string to the result
if current != "" {
result = append ( result , current )
}
return result
}
2022-11-24 21:59:47 +08:00
func ( s * Service ) nestedFolderCreate ( ctx context . Context , cmd * folder . CreateFolderCommand ) ( * folder . Folder , error ) {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.nestedFolderCreate" )
defer span . End ( )
2022-11-23 22:44:45 +08:00
if cmd . ParentUID != "" {
2022-12-15 00:07:55 +08:00
if err := s . validateParent ( ctx , cmd . OrgID , cmd . ParentUID , cmd . UID ) ; err != nil {
2022-11-24 21:59:47 +08:00
return nil , err
2022-11-23 22:44:45 +08:00
}
}
2022-11-24 21:59:47 +08:00
return s . store . Create ( ctx , * cmd )
2022-11-23 22:44:45 +08:00
}
2022-12-15 00:07:55 +08:00
func ( s * Service ) validateParent ( ctx context . Context , orgID int64 , parentUID string , UID string ) error {
2025-04-03 17:54:47 +08:00
ctx , span := s . tracer . Start ( ctx , "folder.validateParent" )
defer span . End ( )
2022-11-23 22:44:45 +08:00
ancestors , err := s . store . GetParents ( ctx , folder . GetParentsQuery { UID : parentUID , OrgID : orgID } )
if err != nil {
2022-11-25 02:28:53 +08:00
return fmt . Errorf ( "failed to get parents: %w" , err )
2022-11-23 22:44:45 +08:00
}
2023-11-15 17:25:40 +08:00
if len ( ancestors ) >= folder . MaxNestedFolderDepth {
2023-04-20 22:47:51 +08:00
return folder . ErrMaximumDepthReached . Errorf ( "failed to validate parent folder" )
2022-11-23 22:44:45 +08:00
}
2022-12-15 00:07:55 +08:00
// 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
}
}
2022-11-23 22:44:45 +08:00
return nil
}
2018-02-20 20:55:43 +08:00
func toFolderError ( err error ) error {
2022-06-30 21:31:54 +08:00
if errors . Is ( err , dashboards . ErrDashboardTitleEmpty ) {
return dashboards . ErrFolderTitleEmpty
2018-02-20 20:55:43 +08:00
}
2022-06-30 21:31:54 +08:00
if errors . Is ( err , dashboards . ErrDashboardUpdateAccessDenied ) {
return dashboards . ErrFolderAccessDenied
2018-02-20 20:55:43 +08:00
}
2022-06-30 21:31:54 +08:00
if errors . Is ( err , dashboards . ErrDashboardWithSameUIDExists ) {
return dashboards . ErrFolderWithSameUIDExists
2018-02-20 20:55:43 +08:00
}
2022-06-30 21:31:54 +08:00
if errors . Is ( err , dashboards . ErrDashboardVersionMismatch ) {
return dashboards . ErrFolderVersionMismatch
2018-02-20 20:55:43 +08:00
}
2022-06-30 21:31:54 +08:00
if errors . Is ( err , dashboards . ErrDashboardNotFound ) {
return dashboards . ErrFolderNotFound
2018-02-20 20:55:43 +08:00
}
return err
}
2023-04-14 17:17:23 +08:00
func ( s * Service ) RegisterService ( r folder . RegistryService ) error {
s . mutex . Lock ( )
defer s . mutex . Unlock ( )
s . registry [ r . Kind ( ) ] = r
return nil
}
2024-02-26 18:27:22 +08:00
func ( s * Service ) supportBundleCollector ( ) supportbundles . Collector {
collector := supportbundles . Collector {
UID : "folder-stats" ,
DisplayName : "Folder information" ,
Description : "Folder information for the Grafana instance" ,
IncludedByDefault : false ,
Default : true ,
Fn : func ( ctx context . Context ) ( * supportbundles . SupportItem , error ) {
s . log . Info ( "Generating folder support bundle" )
folders , err := s . GetFolders ( ctx , folder . GetFoldersQuery {
OrgID : 0 ,
SignedInUser : & user . SignedInUser {
Login : "sa-supportbundle" ,
OrgRole : "Admin" ,
IsGrafanaAdmin : true ,
IsServiceAccount : true ,
Permissions : map [ int64 ] map [ string ] [ ] string { accesscontrol . GlobalOrgID : { dashboards . ActionFoldersRead : { dashboards . ScopeFoldersAll } } } ,
} ,
} )
if err != nil {
return nil , err
}
return s . supportItemFromFolders ( folders )
} ,
}
return collector
}
func ( s * Service ) supportItemFromFolders ( folders [ ] * folder . Folder ) ( * supportbundles . SupportItem , error ) {
stats := struct {
Total int ` json:"total" ` // how many folders?
Depths map [ int ] int ` json:"depths" ` // how deep they are?
Children map [ int ] int ` json:"children" ` // how many child folders they have?
Folders [ ] * folder . Folder ` json:"folders" ` // what are they?
} { Total : len ( folders ) , Folders : folders , Children : map [ int ] int { } , Depths : map [ int ] int { } }
// Build parent-child mapping
parents := map [ string ] string { }
children := map [ string ] [ ] string { }
for _ , f := range folders {
parents [ f . UID ] = f . ParentUID
children [ f . ParentUID ] = append ( children [ f . ParentUID ] , f . UID )
}
// Find depths of each folder
for _ , f := range folders {
depth := 0
for uid := f . UID ; uid != "" ; uid = parents [ uid ] {
depth ++
}
stats . Depths [ depth ] += 1
stats . Children [ len ( children [ f . UID ] ) ] += 1
}
b , err := json . MarshalIndent ( stats , "" , " " )
if err != nil {
return nil , err
}
return & supportbundles . SupportItem {
Filename : "folders.json" ,
FileBytes : b ,
} , nil
}