2022-10-10 21:47:53 +02:00
package folderimpl
2018-02-20 13:55:43 +01:00
import (
2021-09-14 16:08:04 +02:00
"context"
2024-02-26 11:27:22 +01:00
"encoding/json"
2020-11-19 14:47:17 +01:00
"errors"
2022-11-24 20:28:53 +02:00
"fmt"
2024-05-02 09:14:12 +02:00
"log/slog"
2023-12-15 19:34:08 +02:00
"runtime"
2021-03-17 16:06:10 +01:00
"strings"
2023-04-14 11:17:23 +02:00
"sync"
2023-12-05 16:13:31 +01:00
"time"
2020-11-19 14:47:17 +01:00
2024-10-01 14:03:02 +02:00
"github.com/grafana/authlib/claims"
2024-01-24 16:15:32 +03:00
"github.com/grafana/dskit/concurrency"
2023-12-05 16:13:31 +01:00
"github.com/prometheus/client_golang/prometheus"
2024-08-13 12:26:26 +02:00
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
2023-11-08 15:28:49 +01:00
"golang.org/x/exp/slices"
2024-06-13 07:11:35 +03:00
"github.com/grafana/grafana/pkg/apimachinery/identity"
2022-06-17 13:10:49 -04:00
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
2022-11-08 08:59:55 -05:00
"github.com/grafana/grafana/pkg/infra/db"
2024-01-25 11:10:35 +01:00
"github.com/grafana/grafana/pkg/infra/metrics"
2024-08-13 12:26:26 +02:00
"github.com/grafana/grafana/pkg/infra/tracing"
2022-03-10 12:58:18 +01:00
"github.com/grafana/grafana/pkg/services/accesscontrol"
2022-02-16 14:15:44 +01:00
"github.com/grafana/grafana/pkg/services/dashboards"
2024-03-15 14:05:27 +02:00
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
2022-03-23 19:40:22 +01:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2022-10-10 21:47:53 +02:00
"github.com/grafana/grafana/pkg/services/folder"
2018-02-20 13:55:43 +01:00
"github.com/grafana/grafana/pkg/services/guardian"
2024-10-01 14:03:02 +02:00
"github.com/grafana/grafana/pkg/services/org"
2024-01-22 17:04:18 +01:00
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
2023-04-27 17:00:09 +02:00
"github.com/grafana/grafana/pkg/services/store/entity"
2024-02-26 11:27:22 +01:00
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/services/user"
2024-10-01 14:03:02 +02:00
"github.com/grafana/grafana/pkg/setting"
2023-01-30 09:21:27 +01:00
"github.com/grafana/grafana/pkg/util"
2018-02-20 13:55:43 +01:00
)
2024-05-31 11:09:20 +03:00
const FULLPATH_SEPARATOR = "/"
2022-10-10 21:47:53 +02:00
type Service struct {
2024-09-30 10:28:47 +02:00
store folder . Store
2023-03-15 09:51:37 +01:00
db db . DB
2024-05-02 09:14:12 +02:00
log * slog . Logger
2023-01-19 18:38:07 +02:00
dashboardStore dashboards . Store
2023-02-01 15:43:21 +02:00
dashboardFolderStore folder . FolderStore
2023-01-19 18:38:07 +02:00
features featuremgmt . FeatureToggles
2024-10-01 14:03:02 +02:00
cfg * setting . Cfg
folderPermissions accesscontrol . FolderPermissionsService
2023-01-19 18:38:07 +02:00
accessControl accesscontrol . AccessControl
2024-08-13 12:26:26 +02: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-17 13:10:49 -04:00
bus bus . Bus
2023-04-14 11:17:23 +02:00
mutex sync . RWMutex
registry map [ string ] folder . RegistryService
2023-12-05 16:13:31 +01:00
metrics * foldersMetrics
2024-08-13 12:26:26 +02:00
tracer tracing . Tracer
2018-02-20 13:55:43 +01:00
}
2022-10-10 21:47:53 +02:00
func ProvideService (
2024-09-30 10:28:47 +02:00
store * FolderStoreImpl ,
2022-10-10 21:47:53 +02:00
ac accesscontrol . AccessControl ,
bus bus . Bus ,
dashboardStore dashboards . Store ,
2023-02-01 15:43:21 +02:00
folderStore folder . FolderStore ,
2022-11-08 08:59:55 -05:00
db db . DB , // DB for the (new) nested folder store
2022-11-23 13:16:00 +02:00
features featuremgmt . FeatureToggles ,
2024-10-01 14:03:02 +02:00
cfg * setting . Cfg ,
folderPermissions accesscontrol . FolderPermissionsService ,
2024-02-26 11:27:22 +01:00
supportBundles supportbundles . Service ,
2023-12-05 16:13:31 +01:00
r prometheus . Registerer ,
2024-08-13 12:26:26 +02:00
tracer tracing . Tracer ,
2022-10-10 21:47:53 +02:00
) folder . Service {
2023-02-07 16:27:20 +00:00
srv := & Service {
2024-05-02 09:14:12 +02:00
log : slog . Default (). With ( "logger" , "folder-service" ),
2023-01-19 18:38:07 +02:00
dashboardStore : dashboardStore ,
dashboardFolderStore : folderStore ,
store : store ,
features : features ,
2024-10-01 14:03:02 +02:00
cfg : cfg ,
folderPermissions : folderPermissions ,
2023-01-19 18:38:07 +02:00
accessControl : ac ,
bus : bus ,
2023-03-15 09:51:37 +01:00
db : db ,
2023-04-14 11:17:23 +02:00
registry : make ( map [ string ] folder . RegistryService ),
2023-12-05 16:13:31 +01:00
metrics : newFoldersMetrics ( r ),
2024-08-13 12:26:26 +02:00
tracer : tracer ,
2018-02-20 13:55:43 +01:00
}
2024-01-22 17:04:18 +01:00
srv . DBMigration ( db )
2023-01-26 10:21:10 +02:00
2024-02-26 11:27:22 +01:00
supportBundles . RegisterSupportItemCollector ( srv . supportBundleCollector ())
2024-10-01 14:03:02 +02:00
ac . RegisterScopeAttributeResolver ( dashboards . NewFolderNameScopeResolver ( folderStore , store ))
ac . RegisterScopeAttributeResolver ( dashboards . NewFolderIDScopeResolver ( folderStore , store ))
ac . RegisterScopeAttributeResolver ( dashboards . NewFolderUIDScopeResolver ( store ))
2023-02-07 16:27:20 +00:00
return srv
2022-11-12 15:51:46 +01:00
}
2024-01-22 17:04:18 +01: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
if db . GetDialect (). DriverName () == migrator . SQLite {
2024-02-16 21:10:46 +02:00
// covered by UQE_folder_org_id_uid
2024-01-22 17:04:18 +01: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-16 21:10:46 +02:00
// covered by UQE_folder_org_id_uid
2024-01-22 17:04:18 +01: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-16 21:10:46 +02:00
// covered by UQE_folder_org_id_uid
2024-01-22 17:04:18 +01: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-16 21:10:46 +02:00
// covered by UQE_folder_org_id_uid
2024-01-22 17:04:18 +01:00
_ , err = sess . Exec ( `
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)
` )
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" )
}
2024-01-25 09:27:13 +02:00
func ( s * Service ) GetFolders ( ctx context . Context , q folder . GetFoldersQuery ) ([] * folder . Folder , error ) {
if q . SignedInUser == nil {
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2024-09-30 10:28:47 +02:00
qry := folder . NewGetFoldersQuery ( q )
2024-01-25 09:27:13 +02:00
permissions := q . SignedInUser . GetPermissions ()
folderPermissions := permissions [ dashboards . ActionFoldersRead ]
2024-09-30 10:28:47 +02:00
qry . AncestorUIDs = make ([] string , 0 , len ( folderPermissions ))
2024-01-26 12:12:45 -05:00
if len ( folderPermissions ) == 0 && ! q . SignedInUser . GetIsGrafanaAdmin () {
return nil , nil
}
2024-01-25 09:27:13 +02: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 10:28:47 +02:00
qry . AncestorUIDs = nil
2024-01-25 09:27:13 +02:00
break
}
if folderUid , found := strings . CutPrefix ( p , dashboards . ScopeFoldersPrefix ); found {
2024-09-30 10:28:47 +02:00
if ! slices . Contains ( qry . AncestorUIDs , folderUid ) {
qry . AncestorUIDs = append ( qry . AncestorUIDs , folderUid )
2024-01-25 09:27:13 +02:00
}
}
}
2024-02-06 09:18:40 -05: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 09:27:13 +02: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 09:18:40 -05: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 11:19:03 +01:00
return dashFolders , nil
2024-01-25 09:27:13 +02:00
}
2024-01-18 16:12:49 +02:00
func ( s * Service ) Get ( ctx context . Context , q * folder . GetFolderQuery ) ( * folder . Folder , error ) {
if q . SignedInUser == nil {
2022-11-23 11:13:47 +02:00
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
2022-11-11 14:28:24 +01:00
}
2024-02-15 16:13:14 +00:00
if q . UID != nil && * q . UID == accesscontrol . GeneralFolderUID {
return folder . RootFolder , nil
}
2024-01-18 16:12:49 +02:00
if s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) && q . UID != nil && * q . UID == folder . SharedWithMeFolderUID {
2023-12-05 16:13:31 +01:00
return folder . SharedWithMeFolder . WithURL (), nil
}
2023-01-06 16:04:17 +02:00
var dashFolder * folder . Folder
var err error
switch {
2024-06-18 14:21:40 +03:00
case q . UID != nil :
if * q . UID == "" {
return & folder . GeneralFolder , nil
}
2024-01-18 16:12:49 +02:00
dashFolder , err = s . getFolderByUID ( ctx , q . OrgID , * q . UID )
2022-11-24 15:59:47 +02:00
if err != nil {
return nil , err
}
2023-11-15 10:30:00 -05:00
// nolint:staticcheck
2024-01-18 16:12:49 +02:00
case q . ID != nil :
2024-01-25 11:10:35 +01:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ). Inc ()
2024-01-18 16:12:49 +02:00
dashFolder , err = s . getFolderByID ( ctx , * q . ID , q . OrgID )
2022-12-15 16:34:17 +02:00
if err != nil {
return nil , err
}
2024-01-18 16:12:49 +02:00
case q . Title != nil :
2024-01-25 11:29:56 +02:00
dashFolder , err = s . getFolderByTitle ( ctx , q . OrgID , * q . Title , q . ParentUID )
2023-01-06 16:04:17 +02:00
if err != nil {
return nil , err
2022-11-11 14:28:24 +01:00
}
2023-01-06 16:04:17 +02:00
default :
return nil , folder . ErrBadRequest . Errorf ( "either on of UID, ID, Title fields must be present" )
}
2022-11-24 15:59:47 +02:00
2023-02-08 17:16:53 +02:00
if dashFolder . IsGeneral () {
return dashFolder , nil
}
2023-01-06 16:04:17 +02:00
// 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
2024-01-18 16:12:49 +02:00
g , err := guardian . NewByFolder ( ctx , dashFolder , dashFolder . OrgID , q . SignedInUser )
2023-01-06 16:04:17 +02:00
if err != nil {
return nil , err
2022-11-11 14:28:24 +01:00
}
2023-01-06 16:04:17 +02:00
if canView , err := g . CanView (); err != nil || ! canView {
if err != nil {
return nil , toFolderError ( err )
}
return nil , dashboards . ErrFolderAccessDenied
}
2023-11-14 12:50:27 -08:00
if ! s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) {
2024-02-13 19:47:46 +02:00
dashFolder . Fullpath = dashFolder . Title
2024-10-10 13:22:57 +02:00
dashFolder . FullpathUIDs = dashFolder . UID
2023-02-09 12:19:08 +00:00
return dashFolder , nil
}
2024-02-13 19:47:46 +02:00
2024-01-25 11:10:35 +01:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ). Inc ()
2023-11-15 10:30:00 -05:00
// nolint:staticcheck
2024-01-18 16:12:49 +02:00
if q . ID != nil {
q . ID = nil
q . UID = & dashFolder . UID
2023-02-09 12:19:08 +00:00
}
2024-01-18 16:12:49 +02:00
f , err := s . store . Get ( ctx , * q )
2023-02-09 12:19:08 +00:00
if err != nil {
return nil , err
}
2023-01-06 16:04:17 +02:00
// always expose the dashboard store sequential ID
2024-01-25 11:10:35 +01:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ). Inc ()
2023-11-20 15:44:51 -05:00
// nolint:staticcheck
2023-01-06 16:04:17 +02:00
f . ID = dashFolder . ID
2023-05-16 16:41:14 +03:00
f . Version = dashFolder . Version
2023-01-06 16:04:17 +02:00
2024-02-13 19:47:46 +02:00
if ! s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) {
2024-10-10 13:22:57 +02: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-13 19:47:46 +02:00
}
2023-01-06 16:04:17 +02:00
return f , err
2022-11-11 14:28:24 +01:00
}
2023-12-15 19:34:08 +02:00
func ( s * Service ) GetChildren ( ctx context . Context , q * folder . GetChildrenQuery ) ([] * folder . Folder , error ) {
2024-01-24 16:15:32 +03: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-15 19:34:08 +02:00
if q . SignedInUser == nil {
2022-12-19 10:52:04 +02:00
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2023-12-15 19:34:08 +02:00
if s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) && q . UID == folder . SharedWithMeFolderUID {
return s . GetSharedWithMe ( ctx , q )
2023-12-05 16:13:31 +01:00
}
2023-12-15 19:34:08 +02:00
if q . UID == "" {
return s . getRootFolders ( ctx , q )
}
2023-07-07 21:26:01 +03:00
2023-12-15 19:34:08 +02: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)
g , err := guardian . NewByUID ( ctx , q . UID , q . OrgID , q . SignedInUser )
if err != nil {
return nil , err
}
2023-07-07 21:26:01 +03:00
2024-03-15 14:05:27 +02:00
guardianFunc := g . CanView
if q . Permission == dashboardaccess . PERMISSION_EDIT {
guardianFunc = g . CanEdit
}
hasAccess , err := guardianFunc ()
2023-12-15 19:34:08 +02:00
if err != nil {
return nil , err
}
2024-03-15 14:05:27 +02:00
if ! hasAccess {
2023-12-15 19:34:08 +02:00
return nil , dashboards . ErrFolderAccessDenied
2023-07-07 21:26:01 +03:00
}
2023-12-15 19:34:08 +02:00
children , err := s . store . GetChildren ( ctx , * q )
2023-01-23 14:09:09 +02:00
if err != nil {
2018-02-20 13:55:43 +01:00
return nil , err
}
2023-08-01 11:04:44 +03:00
childrenUIDs := make ([] string , 0 , len ( children ))
for _ , f := range children {
childrenUIDs = append ( childrenUIDs , f . UID )
}
2023-12-15 19:34:08 +02:00
dashFolders , err := s . dashboardFolderStore . GetFolders ( ctx , q . OrgID , childrenUIDs )
2023-08-01 11:04:44 +03:00
if err != nil {
return nil , folder . ErrInternal . Errorf ( "failed to fetch subfolders from dashboard store: %w" , err )
}
2023-01-23 14:09:09 +02:00
for _ , f := range children {
// fetch folder from dashboard store
2023-08-01 11:04:44 +03: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 14:09:09 +02:00
continue
}
2023-08-01 11:04:44 +03:00
2023-07-07 21:26:01 +03:00
// always expose the dashboard store sequential ID
2024-01-25 11:10:35 +01:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ). Inc ()
2023-11-20 15:44:51 -05:00
// nolint:staticcheck
2023-07-07 21:26:01 +03:00
f . ID = dashFolder . ID
2023-12-15 19:34:08 +02:00
}
2023-07-07 21:26:01 +03:00
2023-12-15 19:34:08 +02:00
return children , nil
}
2018-02-20 13:55:43 +01:00
2023-12-15 19:34:08 +02:00
func ( s * Service ) getRootFolders ( ctx context . Context , q * folder . GetChildrenQuery ) ([] * folder . Folder , error ) {
permissions := q . SignedInUser . GetPermissions ()
2024-03-15 14:05:27 +02: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-15 19:34:08 +02: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 14:09:09 +02:00
}
2023-12-15 19:34:08 +02: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 14:05:53 +01:00
}
2023-12-15 19:34:08 +02: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 {
s . log . Error ( "failed to fetch folder by UID from dashboard store" , "orgID" , f . OrgID , "uid" , f . UID )
2023-01-23 14:09:09 +02:00
}
2023-12-15 19:34:08 +02:00
// always expose the dashboard store sequential ID
2024-01-25 11:10:35 +01:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ). Inc ()
2023-12-15 19:34:08 +02: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 13:55:43 +01:00
}
2023-12-15 19:34:08 +02:00
// add "shared with me" folder on the 1st page
if ( q . Page == 0 || q . Page == 1 ) && len ( q . FolderUIDs ) != 0 {
2023-12-29 11:35:15 +01:00
children = append ([] * folder . Folder { & folder . SharedWithMeFolder }, children ... )
2023-12-05 16:13:31 +01:00
}
2023-12-15 19:34:08 +02:00
return children , nil
2018-02-20 13:55:43 +01:00
}
2023-11-08 15:28:49 +01:00
// GetSharedWithMe returns folders available to user, which cannot be accessed from the root folders
2024-01-18 16:12:49 +02:00
func ( s * Service ) GetSharedWithMe ( ctx context . Context , q * folder . GetChildrenQuery ) ([] * folder . Folder , error ) {
2023-12-05 16:13:31 +01:00
start := time . Now ()
2024-03-15 14:05:27 +02:00
availableNonRootFolders , err := s . getAvailableNonRootFolders ( ctx , q )
2023-11-08 15:28:49 +01:00
if err != nil {
2023-12-05 16:13:31 +01:00
s . metrics . sharedWithMeFetchFoldersRequestsDuration . WithLabelValues ( "failure" ). Observe ( time . Since ( start ). Seconds ())
2023-11-08 15:28:49 +01:00
return nil , folder . ErrInternal . Errorf ( "failed to fetch subfolders to which the user has explicit access: %w" , err )
}
2024-03-15 14:05:27 +02:00
rootFolders , err := s . GetChildren ( ctx , & folder . GetChildrenQuery { UID : "" , OrgID : q . OrgID , SignedInUser : q . SignedInUser , Permission : q . Permission })
2023-11-08 15:28:49 +01:00
if err != nil {
2023-12-05 16:13:31 +01:00
s . metrics . sharedWithMeFetchFoldersRequestsDuration . WithLabelValues ( "failure" ). Observe ( time . Since ( start ). Seconds ())
2023-11-08 15:28:49 +01:00
return nil , folder . ErrInternal . Errorf ( "failed to fetch root folders to which the user has access: %w" , err )
}
2024-01-25 15:02:32 +03:00
availableNonRootFolders = s . deduplicateAvailableFolders ( ctx , availableNonRootFolders , rootFolders , q . OrgID )
2023-12-05 16:13:31 +01:00
s . metrics . sharedWithMeFetchFoldersRequestsDuration . WithLabelValues ( "success" ). Observe ( time . Since ( start ). Seconds ())
2023-11-08 15:28:49 +01:00
return availableNonRootFolders , nil
}
2024-03-15 14:05:27 +02:00
func ( s * Service ) getAvailableNonRootFolders ( ctx context . Context , q * folder . GetChildrenQuery ) ([] * folder . Folder , error ) {
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 15:28:49 +01:00
nonRootFolders := make ([] * folder . Folder , 0 )
2023-12-15 19:34:08 +02:00
folderUids := make ([] string , 0 , len ( folderPermissions ))
2023-11-08 15:28:49 +01:00
for _ , p := range folderPermissions {
2023-12-05 16:13:31 +01:00
if folderUid , found := strings . CutPrefix ( p , dashboards . ScopeFoldersPrefix ); found {
2023-11-08 15:28:49 +01:00
if ! slices . Contains ( folderUids , folderUid ) {
folderUids = append ( folderUids , folderUid )
}
}
}
if len ( folderUids ) == 0 {
return nonRootFolders , nil
}
2024-01-25 15:02:32 +03:00
dashFolders , err := s . GetFolders ( ctx , folder . GetFoldersQuery {
UIDs : folderUids ,
2024-03-15 14:05:27 +02:00
OrgID : q . OrgID ,
SignedInUser : q . SignedInUser ,
OrderByTitle : true ,
2024-01-25 15:02:32 +03:00
WithFullpathUIDs : true ,
})
2023-11-08 15:28:49 +01: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
}
2024-01-25 15:02:32 +03:00
func ( s * Service ) deduplicateAvailableFolders ( ctx context . Context , folders [] * folder . Folder , rootFolders [] * folder . Folder , orgID int64 ) [] * folder . Folder {
2023-11-08 15:28:49 +01:00
allFolders := append ( folders , rootFolders ... )
foldersDedup := make ([] * folder . Folder , 0 )
2024-01-25 15:02:32 +03:00
2023-11-08 15:28:49 +01:00
for _ , f := range folders {
isSubfolder := slices . ContainsFunc ( allFolders , func ( folder * folder . Folder ) bool {
return f . ParentUID == folder . UID
})
if ! isSubfolder {
2024-01-25 15:02:32 +03: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 15:28:49 +01:00
}
2024-01-25 15:02:32 +03:00
for _ , parentUID := range parentUIDs {
2023-11-08 15:28:49 +01:00
contains := slices . ContainsFunc ( allFolders , func ( f * folder . Folder ) bool {
2024-01-25 15:02:32 +03:00
return f . UID == parentUID
2023-11-08 15:28:49 +01:00
})
if contains {
isSubfolder = true
break
}
}
}
if ! isSubfolder {
foldersDedup = append ( foldersDedup , f )
}
}
return foldersDedup
}
2023-01-26 10:21:10 +02:00
func ( s * Service ) GetParents ( ctx context . Context , q folder . GetParentsQuery ) ([] * folder . Folder , error ) {
2024-02-15 16:13:14 +00:00
if ! s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) || q . UID == accesscontrol . GeneralFolderUID {
2023-01-26 10:21:10 +02:00
return nil , nil
}
2023-12-05 16:13:31 +01:00
if q . UID == folder . SharedWithMeFolderUID {
return [] * folder . Folder { & folder . SharedWithMeFolder }, nil
}
2023-01-26 10:21:10 +02:00
return s . store . GetParents ( ctx , q )
}
2023-02-09 12:19:08 +00:00
func ( s * Service ) getFolderByID ( ctx context . Context , id int64 , orgID int64 ) ( * folder . Folder , error ) {
2021-07-01 11:40:38 +03:00
if id == 0 {
2023-02-08 17:16:53 +02:00
return & folder . GeneralFolder , nil
2021-07-01 11:40:38 +03:00
}
2022-10-26 10:15:14 -04:00
2023-02-09 12:19:08 +00:00
return s . dashboardFolderStore . GetFolderByID ( ctx , orgID , id )
2018-02-20 13:55:43 +01:00
}
2023-02-09 12:19:08 +00:00
func ( s * Service ) getFolderByUID ( ctx context . Context , orgID int64 , uid string ) ( * folder . Folder , error ) {
return s . dashboardFolderStore . GetFolderByUID ( ctx , orgID , uid )
2021-04-01 11:11:45 +03:00
}
2024-01-25 11:29:56 +02:00
func ( s * Service ) getFolderByTitle ( ctx context . Context , orgID int64 , title string , parentUID * string ) ( * folder . Folder , error ) {
return s . dashboardFolderStore . GetFolderByTitle ( ctx , orgID , title , parentUID )
2018-02-20 13:55:43 +01:00
}
2022-11-10 11:41:03 +02:00
func ( s * Service ) Create ( ctx context . Context , cmd * folder . CreateFolderCommand ) ( * folder . Folder , error ) {
2023-09-06 11:16:10 +02:00
if cmd . SignedInUser == nil || cmd . SignedInUser . IsNil () {
2023-03-20 11:04:22 +00:00
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2023-10-24 10:04:45 +03:00
dashFolder := dashboards . NewDashboardFolder ( cmd . Title )
dashFolder . OrgID = cmd . OrgID
2023-11-14 12:50:27 -08:00
if s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) && cmd . ParentUID != "" {
2023-03-20 11:04:22 +00:00
// Check that the user is allowed to create a subfolder in this folder
2024-08-01 16:20:38 +01: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 11:04:22 +00:00
hasAccess , evalErr := s . accessControl . Evaluate ( ctx , cmd . SignedInUser , evaluator )
if evalErr != nil {
return nil , evalErr
}
if ! hasAccess {
2024-08-01 16:20:38 +01: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 11:04:22 +00:00
}
2023-10-24 10:04:45 +03:00
dashFolder . FolderUID = cmd . ParentUID
2023-03-20 11:04:22 +00:00
}
2024-08-01 16:20:38 +01: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 16:13:31 +01: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 11:41:03 +02:00
trimmedUID := strings . TrimSpace ( cmd . UID )
2022-03-30 15:14:26 +02:00
if trimmedUID == accesscontrol . GeneralFolderUID {
2022-06-30 09:31:54 -04:00
return nil , dashboards . ErrFolderInvalidUID
2022-03-30 15:14:26 +02:00
}
2023-01-16 16:33:55 +01:00
dashFolder . SetUID ( trimmedUID )
2022-11-10 11:41:03 +02:00
2022-11-23 11:13:47 +02:00
user := cmd . SignedInUser
2023-09-06 13:37:54 +02:00
2024-08-09 17:20:24 +02:00
var userID int64
if id , err := identity . UserIdentifier ( cmd . SignedInUser . GetID ()); err == nil {
userID = id
2023-09-06 13:37:54 +02:00
} else {
2024-08-09 17:20:24 +02: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 16:51:18 +02:00
}
2021-03-17 16:06:10 +01:00
if userID == 0 {
userID = - 1
}
2024-08-09 17:20:24 +02:00
2021-03-17 16:06:10 +01:00
dashFolder . CreatedBy = userID
dashFolder . UpdatedBy = userID
dashFolder . UpdateSlug ()
2018-02-20 13:55:43 +01:00
2022-02-16 14:15:44 +01:00
dto := & dashboards . SaveDashboardDTO {
2018-02-20 13:55:43 +01:00
Dashboard : dashFolder ,
2023-01-16 16:33:55 +01:00
OrgID : cmd . OrgID ,
2022-02-16 14:15:44 +01:00
User : user ,
2018-02-20 13:55:43 +01:00
}
2023-10-24 10:04:45 +03:00
saveDashboardCmd , err := s . buildSaveDashboardCommand ( ctx , dto )
2018-02-20 13:55:43 +01:00
if err != nil {
2021-03-17 16:06:10 +01:00
return nil , toFolderError ( err )
2018-02-20 13:55:43 +01:00
}
2023-11-21 13:06:20 -08:00
var nestedFolder * folder . Folder
var dash * dashboards . Dashboard
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 13:55:43 +01:00
2023-11-21 13: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-22 18:03:30 +02:00
Title : dashFolder . Title ,
2023-11-21 13:06:20 -08:00
Description : cmd . Description ,
ParentUID : cmd . ParentUID ,
}
2018-02-20 13:55:43 +01:00
2023-11-21 13:06:20 -08:00
if nestedFolder , err = s . nestedFolderCreate ( ctx , cmd ); err != nil {
2024-05-02 09:14:12 +02:00
s . log . ErrorContext ( ctx , "error saving folder to nested folder store" , "error" , err )
2023-11-21 13:06:20 -08:00
return err
2022-11-08 08:59:55 -05:00
}
2023-11-21 13:06:20 -08:00
return nil
})
if err != nil {
return nil , err
2022-11-08 08:59:55 -05:00
}
2022-11-24 15:59:47 +02:00
2023-01-16 16:33:55 +01:00
f := dashboards . FromDashboard ( dash )
2022-11-24 15:59:47 +02:00
if nestedFolder != nil && nestedFolder . ParentUID != "" {
f . ParentUID = nestedFolder . ParentUID
}
2024-10-01 14:03:02 +02:00
if err = s . setDefaultFolderPermissions ( ctx , cmd . OrgID , user , f ); err != nil {
return nil , err
}
2024-10-10 13:22:57 +02:00
if s . features . IsEnabled ( ctx , featuremgmt . FlagKubernetesFolders ) {
2024-10-16 10:44:09 +02:00
// #TODO is some kind of intermediate conversion required as is the case with user id where
// it gets parsed using UserIdentifier(). Also is there some kind of validation taking place as
// part of the parsing?
f . CreatedByUID = user . GetUID ()
f . UpdatedByUID = user . GetUID ()
2024-10-10 13:22:57 +02: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
parent , err := s . Get ( ctx , & folder . GetFolderQuery {
UID : & f . ParentUID ,
OrgID : f . OrgID ,
WithFullpath : true ,
WithFullpathUIDs : true ,
SignedInUser : user ,
})
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
title := strings . Replace ( f . Title , "/" , "\\/" , - 1 )
f . Fullpath = title + "/" + parent . Fullpath
f . FullpathUIDs = f . UID + "/" + parent . FullpathUIDs
}
2022-11-24 15:59:47 +02:00
return f , nil
2018-02-20 13:55:43 +01:00
}
2024-10-01 14:03:02 +02:00
func ( s * Service ) setDefaultFolderPermissions ( ctx context . Context , orgID int64 , user identity . Requester , folder * folder . Folder ) error {
if ! s . cfg . RBAC . PermissionsOnCreation ( "folder" ) {
return nil
}
var permissions [] accesscontrol . SetResourcePermissionCommand
if user . IsIdentityType ( claims . TypeUser ) {
userID , err := user . GetInternalID ()
if err != nil {
return err
}
permissions = append ( permissions , accesscontrol . SetResourcePermissionCommand {
UserID : userID , Permission : dashboardaccess . PERMISSION_ADMIN . String (),
})
}
isNested := folder . ParentUID != ""
if ! isNested || ! s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) {
permissions = append ( permissions , [] accesscontrol . SetResourcePermissionCommand {
{ BuiltinRole : string ( org . RoleEditor ), Permission : dashboardaccess . PERMISSION_EDIT . String ()},
{ BuiltinRole : string ( org . RoleViewer ), Permission : dashboardaccess . PERMISSION_VIEW . String ()},
} ... )
}
_ , err := s . folderPermissions . SetPermissions ( ctx , orgID , folder . UID , permissions ... )
return err
}
2022-12-20 15:00:33 +02:00
func ( s * Service ) Update ( ctx context . Context , cmd * folder . UpdateFolderCommand ) ( * folder . Folder , error ) {
2024-08-13 12:26:26 +02:00
ctx , span := s . tracer . Start ( ctx , "folder.Update" )
defer span . End ()
2022-12-20 15:00:33 +02:00
if cmd . SignedInUser == nil {
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
user := cmd . SignedInUser
2023-11-21 13: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 14:28:55 +01:00
2023-11-21 13:06:20 -08:00
if foldr , err = s . store . Update ( ctx , folder . UpdateFolderCommand {
UID : cmd . UID ,
OrgID : cmd . OrgID ,
2024-01-22 18:03:30 +02:00
NewTitle : & dashFolder . Title ,
2023-11-21 13:06:20 -08:00
NewDescription : cmd . NewDescription ,
SignedInUser : user ,
}); err != nil {
return err
2023-11-10 13:03:00 +01:00
}
2023-11-21 13:06:20 -08:00
if cmd . NewTitle != nil {
2024-01-25 11:10:35 +01:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ). Inc ()
2024-08-13 12:26:26 +02:00
if err := s . publishFolderFullPathUpdatedEvent ( ctx , foldr . Updated , cmd . OrgID , cmd . UID ); err != nil {
2023-11-21 13:06:20 -08:00
return err
}
}
return nil
})
if err != nil {
2024-05-02 09:14:12 +02:00
s . log . ErrorContext ( ctx , "folder update failed" , "folderUID" , cmd . UID , "error" , err )
2023-01-06 16:04:17 +02:00
return nil , err
}
2023-11-21 13:06:20 -08:00
if ! s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) {
return dashFolder , nil
}
2023-01-06 16:04:17 +02:00
// always expose the dashboard store sequential ID
2024-01-25 11:10:35 +01:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ). Inc ()
2023-11-20 15:44:51 -05:00
// nolint:staticcheck
2023-01-06 16:04:17 +02:00
foldr . ID = dashFolder . ID
2023-05-16 16:41:14 +03:00
foldr . Version = dashFolder . Version
2023-01-06 16:04:17 +02:00
2022-11-10 14:28:55 +01:00
return foldr , nil
}
2022-12-20 15:00:33 +02:00
func ( s * Service ) legacyUpdate ( ctx context . Context , cmd * folder . UpdateFolderCommand ) ( * folder . Folder , error ) {
2023-01-16 16:33:55 +01:00
query := dashboards . GetDashboardQuery { OrgID : cmd . OrgID , UID : cmd . UID }
2023-01-25 10:36:26 +01:00
queryResult , err := s . dashboardStore . GetDashboard ( ctx , & query )
2022-11-10 14:28:55 +01:00
if err != nil {
return nil , toFolderError ( err )
2018-02-20 13:55:43 +01:00
}
2023-01-25 10:36:26 +01:00
dashFolder := queryResult
2023-10-24 10:04:45 +03:00
if cmd . NewParentUID != nil {
dashFolder . FolderUID = * cmd . NewParentUID
}
2022-03-23 19:40:22 +01:00
if ! dashFolder . IsFolder {
2022-11-10 14:28:55 +01:00
return nil , dashboards . ErrFolderNotFound
2022-03-23 19:40:22 +01:00
}
2022-12-20 15:00:33 +02:00
if cmd . SignedInUser == nil {
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2023-10-06 15:02:34 +02:00
var userID int64
2024-08-09 17:20:24 +02:00
if id , err := identity . UserIdentifier ( cmd . SignedInUser . GetID ()); err == nil {
userID = id
} else {
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 15:02:34 +02:00
}
prepareForUpdate ( dashFolder , cmd . OrgID , userID , cmd )
2018-02-20 13:55:43 +01:00
2022-02-16 14:15:44 +01:00
dto := & dashboards . SaveDashboardDTO {
2018-02-20 13:55:43 +01:00
Dashboard : dashFolder ,
2023-01-16 16:33:55 +01:00
OrgID : cmd . OrgID ,
2022-12-20 15:00:33 +02:00
User : cmd . SignedInUser ,
2018-02-20 13:55:43 +01:00
Overwrite : cmd . Overwrite ,
}
2023-10-24 10:04:45 +03:00
saveDashboardCmd , err := s . buildSaveDashboardCommand ( ctx , dto )
2018-02-20 13:55:43 +01:00
if err != nil {
2022-11-10 14:28:55 +01:00
return nil , toFolderError ( err )
2018-02-20 13:55:43 +01:00
}
2022-10-10 21:47:53 +02:00
dash , err := s . dashboardStore . SaveDashboard ( ctx , * saveDashboardCmd )
2018-02-20 13:55:43 +01:00
if err != nil {
2022-11-10 14:28:55 +01:00
return nil , toFolderError ( err )
2018-02-20 13:55:43 +01:00
}
2022-11-11 14:28:24 +01:00
var foldr * folder . Folder
2023-01-19 18:38:07 +02:00
foldr , err = s . dashboardFolderStore . GetFolderByID ( ctx , cmd . OrgID , dash . ID )
2018-02-20 13:55:43 +01:00
if err != nil {
2022-11-10 14:28:55 +01:00
return nil , err
2018-02-20 13:55:43 +01:00
}
2022-06-17 13:10:49 -04:00
2022-11-10 14:28:55 +01:00
return foldr , nil
2018-02-20 13:55:43 +01:00
}
2022-12-20 15:00:33 +02:00
// prepareForUpdate updates an existing dashboard model from command into model for folder update
2023-01-16 16:33:55 +01:00
func prepareForUpdate ( dashFolder * dashboards . Dashboard , orgId int64 , userId int64 , cmd * folder . UpdateFolderCommand ) {
dashFolder . OrgID = orgId
2022-12-20 15:00:33 +02: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 17:38:09 +02:00
func ( s * Service ) Delete ( ctx context . Context , cmd * folder . DeleteFolderCommand ) error {
2022-11-23 16:44:45 +02:00
if cmd . SignedInUser == nil {
return folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2023-04-14 11:17:23 +02:00
if cmd . UID == "" {
return folder . ErrBadRequest . Errorf ( "missing UID" )
}
if cmd . OrgID < 1 {
return folder . ErrBadRequest . Errorf ( "invalid orgID" )
}
2023-07-07 21:26:01 +03:00
guard , err := guardian . NewByUID ( ctx , cmd . 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
}
2024-01-30 18:26:34 +02:00
folders := [] string { cmd . UID }
2023-07-07 21:26:01 +03:00
err = s . db . InTransaction ( ctx , func ( ctx context . Context ) error {
2024-01-30 18:26:34 +02:00
descendants , err := s . nestedFolderDelete ( ctx , cmd )
2022-11-23 16:44:45 +02:00
2023-11-10 13:03:00 +01:00
if err != nil {
2024-05-02 09:14:12 +02:00
s . log . ErrorContext ( ctx , "the delete folder on folder table failed with err: " , "error" , err )
2023-11-10 13:03:00 +01:00
return err
2022-11-10 09:42:32 +01:00
}
2024-01-30 18:26:34 +02:00
folders = append ( folders , descendants ... )
2022-11-10 09:42:32 +01:00
2024-01-30 18:26:34 +02:00
if cmd . ForceDeleteRules {
if err := s . deleteChildrenInFolder ( ctx , cmd . OrgID , folders , cmd . SignedInUser ); err != nil {
return err
2023-03-15 09:51:37 +01:00
}
2024-01-30 18:26:34 +02:00
} else {
alertRuleSrv , ok := s . registry [ entity . StandardKindAlertRule ]
if ! ok {
return folder . ErrInternal . Errorf ( "no alert rule service found in registry" )
2023-04-14 11:17:23 +02:00
}
2024-01-30 18:26:34 +02: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 09:51:37 +01:00
return err
}
2024-01-30 18:26:34 +02:00
if alertRulesInFolder > 0 {
return folder . ErrFolderNotEmpty . Errorf ( "folder contains %d alert rules" , alertRulesInFolder )
}
}
if err = s . legacyDelete ( ctx , cmd , folders ); err != nil {
return err
2018-02-20 13:55:43 +01:00
}
2024-01-30 18:26:34 +02:00
2023-03-15 09:51:37 +01:00
return nil
})
2018-02-20 13:55:43 +01:00
2023-03-15 09:51:37 +01:00
return err
2022-12-20 17:38:09 +02:00
}
2024-01-30 18:26:34 +02:00
func ( s * Service ) deleteChildrenInFolder ( ctx context . Context , orgID int64 , folderUIDs [] string , user identity . Requester ) error {
2023-04-14 11:17:23 +02:00
for _ , v := range s . registry {
2024-01-30 18:26:34 +02:00
if err := v . DeleteInFolders ( ctx , orgID , folderUIDs , user ); err != nil {
2023-04-14 11:17:23 +02:00
return err
}
}
return nil
}
2024-01-30 18:26:34 +02:00
func ( s * Service ) legacyDelete ( ctx context . Context , cmd * folder . DeleteFolderCommand , folderUIDs [] string ) error {
// TODO use bulk delete
for _ , folderUID := range folderUIDs {
2024-05-16 14:36:26 -03:00
// nolint:staticcheck
2024-05-30 14:21:34 +03:00
deleteCmd := dashboards . DeleteDashboardCommand { OrgID : cmd . OrgID , UID : folderUID , ForceDeleteFolderRules : cmd . ForceDeleteRules }
2024-01-30 18:26:34 +02:00
if err := s . dashboardStore . DeleteDashboard ( ctx , & deleteCmd ); err != nil {
return toFolderError ( err )
}
2018-02-20 13:55:43 +01:00
}
2022-11-10 09:42:32 +01:00
return nil
2018-02-20 13:55:43 +01:00
}
2022-11-08 11:33:13 +01:00
func ( s * Service ) Move ( ctx context . Context , cmd * folder . MoveFolderCommand ) ( * folder . Folder , error ) {
2024-08-13 12:26:26 +02:00
ctx , span := s . tracer . Start ( ctx , "folder.Move" )
defer span . End ()
2022-11-23 11:13:47 +02:00
if cmd . SignedInUser == nil {
return nil , folder . ErrBadRequest . Errorf ( "missing signed in user" )
}
2024-06-10 09:17:51 -05: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 11:04:22 +00:00
// Check that the user is allowed to move the folder to the destination folder
2024-07-23 17:07:27 +01:00
hasAccess , evalErr := s . canMove ( ctx , cmd )
2023-07-10 15:14:21 +03: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 15:00:33 +02: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 17:38:09 +02: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 01: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 17:47:51 +03: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 09:17:51 -05:00
// if the current folder is already a parent of newparent, we should return error
2022-12-20 15:00:33 +02:00
if parent . UID == cmd . UID {
2023-10-24 10:04:45 +03:00
return nil , folder . ErrCircularReference . Errorf ( "failed to move folder" )
2022-12-08 21:49:17 +08:00
}
}
2023-10-24 10:04:45 +03: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 09:17:51 -05:00
NewParentUID : & cmd . NewParentUID ,
2023-10-24 10:04:45 +03:00
SignedInUser : cmd . SignedInUser ,
}); err != nil {
2024-01-25 11:29:56 +02:00
if s . db . GetDialect (). IsUniqueConstraintViolation ( err ) {
return folder . ErrConflict . Errorf ( "%w" , dashboards . ErrFolderSameNameExists )
}
2023-10-24 10:04:45 +03:00
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 09:17:51 -05:00
NewParentUID : & cmd . NewParentUID ,
2023-10-24 10:04:45 +03:00
SignedInUser : cmd . SignedInUser ,
// bypass optimistic locking used for dashboards
Overwrite : true ,
}); err != nil {
2024-01-25 11:29:56 +02:00
if s . db . GetDialect (). IsUniqueConstraintViolation ( err ) {
return folder . ErrConflict . Errorf ( "%w" , dashboards . ErrFolderSameNameExists )
}
2023-10-24 10:04:45 +03:00
return folder . ErrInternal . Errorf ( "failed to move legacy folder: %w" , err )
}
2024-08-13 12:26:26 +02:00
if err := s . publishFolderFullPathUpdatedEvent ( ctx , f . Updated , cmd . OrgID , cmd . UID ); err != nil {
return err
}
2023-10-24 10:04:45 +03:00
return nil
}); err != nil {
return nil , err
}
return f , nil
2022-11-08 11:33:13 +01:00
}
2024-08-13 12:26:26 +02: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-23 17:07:27 +01:00
func ( s * Service ) canMove ( ctx context . Context , cmd * folder . MoveFolderCommand ) ( bool , error ) {
// 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 16:20:38 +01: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-23 17:07:27 +01:00
} else {
// Evaluate folder creation permission when moving folder to the root level
2024-08-01 16:20:38 +01:00
evaluator = accesscontrol . EvalPermission ( dashboards . ActionFoldersCreate , dashboards . ScopeFoldersProvider . GetResourceScopeUID ( folder . GeneralFolderUID ))
2024-07-23 17:07:27 +01: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 {
2024-07-31 09:49:09 +01:00
// Skip unexpanded action sets - they have no impact if action sets are not enabled
if ! s . features . IsEnabled ( ctx , featuremgmt . FlagAccessActionSets ) {
if action == "folders:view" || action == "folders:edit" || action == "folders:admin" {
continue
}
}
2024-07-23 17:07:27 +01:00
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 ) {
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 09:51:37 +01: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 ) {
2024-01-30 18:26:34 +02:00
descendantUIDs := [] string {}
2022-11-23 11:13:47 +02:00
if cmd . SignedInUser == nil {
2024-01-30 18:26:34 +02:00
return descendantUIDs , folder . ErrBadRequest . Errorf ( "missing signed in user" )
2022-11-23 11:13:47 +02:00
}
2022-11-10 09:42:32 +01:00
_ , err := s . Get ( ctx , & folder . GetFolderQuery {
2022-11-23 11:13:47 +02:00
UID : & cmd . UID ,
OrgID : cmd . OrgID ,
SignedInUser : cmd . SignedInUser ,
2022-11-08 11:33:13 +01:00
})
if err != nil {
2024-01-30 18:26:34 +02:00
return descendantUIDs , err
2022-11-10 09:42:32 +01:00
}
2024-01-30 18:26:34 +02:00
descendants , err := s . store . GetDescendants ( ctx , cmd . OrgID , cmd . UID )
2022-11-10 09:42:32 +01:00
if err != nil {
2024-05-02 09:14:12 +02:00
s . log . ErrorContext ( ctx , "failed to get descendant folders" , "error" , err )
2024-01-30 18:26:34 +02:00
return descendantUIDs , err
2022-11-08 11:33:13 +01:00
}
2023-04-14 11:17:23 +02:00
2024-01-30 18:26:34 +02:00
for _ , f := range descendants {
descendantUIDs = append ( descendantUIDs , f . UID )
}
2024-05-02 09:14:12 +02:00
s . log . InfoContext ( ctx , "deleting folder and its descendants" , "org_id" , cmd . OrgID , "uid" , cmd . UID )
2024-01-30 18:26:34 +02:00
toDelete := append ( descendantUIDs , cmd . UID )
err = s . store . Delete ( ctx , toDelete , cmd . OrgID )
2022-11-08 11:33:13 +01:00
if err != nil {
2024-05-02 09:14:12 +02:00
s . log . InfoContext ( ctx , "failed deleting folder" , "org_id" , cmd . OrgID , "uid" , cmd . UID , "err" , err )
2024-01-30 18:26:34 +02:00
return descendantUIDs , err
2022-11-08 11:33:13 +01:00
}
2024-01-30 18:26:34 +02:00
return descendantUIDs , nil
2022-11-08 11:33:13 +01:00
}
2024-01-18 16:12:49 +02:00
func ( s * Service ) GetDescendantCounts ( ctx context . Context , q * folder . GetDescendantCountsQuery ) ( folder . DescendantCounts , error ) {
if q . SignedInUser == nil {
2023-04-24 15:57:28 +02:00
return nil , folder . ErrBadRequest . Errorf ( "missing signed-in user" )
}
2024-01-30 18:26:34 +02:00
if q . UID == nil || * q . UID == "" {
2023-04-24 15:57:28 +02:00
return nil , folder . ErrBadRequest . Errorf ( "missing UID" )
}
2024-01-18 16:12:49 +02:00
if q . OrgID < 1 {
2023-04-24 15:57:28 +02:00
return nil , folder . ErrBadRequest . Errorf ( "invalid orgID" )
}
2024-01-30 18:26:34 +02:00
folders := [] string { * q . UID }
2023-04-27 17:00:09 +02:00
countsMap := make ( folder . DescendantCounts , len ( s . registry ) + 1 )
2023-11-14 12:50:27 -08:00
if s . features . IsEnabled ( ctx , featuremgmt . FlagNestedFolders ) {
2024-01-30 18:26:34 +02:00
descendantFolders , err := s . store . GetDescendants ( ctx , q . OrgID , * q . UID )
2023-04-24 15:57:28 +02:00
if err != nil {
2024-05-02 09:14:12 +02:00
s . log . ErrorContext ( ctx , "failed to get descendant folders" , "error" , err )
2023-04-24 15:57:28 +02:00
return nil , err
}
2024-01-30 18:26:34 +02:00
for _ , f := range descendantFolders {
folders = append ( folders , f . UID )
2023-04-27 17:00:09 +02:00
}
2024-01-30 18:26:34 +02:00
countsMap [ entity . StandardKindFolder ] = int64 ( len ( descendantFolders ))
2023-04-27 17:00:09 +02:00
}
2024-01-30 18:26:34 +02:00
for _ , v := range s . registry {
c , err := v . CountInFolders ( ctx , q . OrgID , folders , q . SignedInUser )
2023-04-27 17:00:09 +02:00
if err != nil {
2024-05-02 09:14:12 +02:00
s . log . ErrorContext ( ctx , "failed to count folder descendants" , "error" , err )
2023-04-27 17:00:09 +02:00
return nil , err
}
2024-01-30 18:26:34 +02:00
countsMap [ v . Kind ()] = c
2023-04-27 17:00:09 +02:00
}
2024-01-30 18:26:34 +02:00
return countsMap , nil
2023-04-27 17:00:09 +02:00
}
2023-10-24 10:04:45 +03:00
// buildSaveDashboardCommand is a simplified version on DashboardServiceImpl.buildSaveDashboardCommand
2023-01-18 17:47:59 +02:00
// keeping only the meaningful functionality for folders
2023-10-24 10:04:45 +03:00
func ( s * Service ) buildSaveDashboardCommand ( ctx context . Context , dto * dashboards . SaveDashboardDTO ) ( * dashboards . SaveDashboardCommand , error ) {
2023-01-18 17:47:59 +02: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 11:11:35 +00:00
if strings . EqualFold ( dash . Title , dashboards . RootFolderName ) {
return nil , dashboards . ErrDashboardFolderNameExists
2023-01-18 17:47:59 +02:00
}
2023-11-16 11:11:35 +00:00
if dash . FolderUID != "" {
if _ , err := s . dashboardFolderStore . GetFolderByUID ( ctx , dash . OrgID , dash . FolderUID ); err != nil {
return nil , err
}
2023-01-18 17:47:59 +02:00
}
if ! util . IsValidShortUID ( dash . UID ) {
return nil , dashboards . ErrDashboardInvalidUid
} else if util . IsShortUIDTooLong ( dash . UID ) {
return nil , dashboards . ErrDashboardUidTooLong
}
2023-03-20 11:04:22 +00:00
_ , err := s . dashboardStore . ValidateDashboardBeforeSave ( ctx , dash , dto . Overwrite )
2023-01-18 17:47:59 +02:00
if err != nil {
return nil , err
}
guard , err := getGuardianForSavePermissionCheck ( ctx , dash , dto . User )
if err != nil {
return nil , err
}
if dash . ID == 0 {
2024-01-25 11:10:35 +01:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ). Inc ()
2023-11-15 10:28:50 -05:00
// nolint:staticcheck
2023-01-18 17:47:59 +02:00
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
}
}
2024-08-09 17:20:24 +02:00
var userID int64
if id , err := identity . UserIdentifier ( dto . User . GetID ()); err == nil {
userID = id
2023-09-06 13:37:54 +02:00
} else {
2024-08-09 17:20:24 +02: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 16:51:18 +02:00
}
2024-01-25 11:10:35 +01:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ). Inc ()
2023-01-18 17:47:59 +02:00
cmd := & dashboards . SaveDashboardCommand {
Dashboard : dash . Data ,
Message : dto . Message ,
OrgID : dto . OrgID ,
Overwrite : dto . Overwrite ,
2023-08-30 16:51:18 +02:00
UserID : userID ,
2023-11-15 10:28:50 -05:00
FolderID : dash . FolderID , // nolint:staticcheck
2023-10-24 10:04:45 +03:00
FolderUID : dash . FolderUID ,
2023-01-18 17:47:59 +02:00
IsFolder : dash . IsFolder ,
PluginID : dash . PluginID ,
}
if ! dto . UpdatedAt . IsZero () {
cmd . UpdatedAt = dto . UpdatedAt
}
return cmd , nil
}
2024-05-31 11:09:20 +03: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
}
2023-01-18 17:47:59 +02:00
// getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard
// It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck()
2023-08-30 16:51:18 +02:00
func getGuardianForSavePermissionCheck ( ctx context . Context , d * dashboards . Dashboard , user identity . Requester ) ( guardian . DashboardGuardian , error ) {
2023-01-18 17:47:59 +02:00
newDashboard := d . ID == 0
if newDashboard {
// if it's a new dashboard/folder check the parent folder permissions
2024-01-25 11:10:35 +01:00
metrics . MFolderIDsServiceCount . WithLabelValues ( metrics . Folder ). Inc ()
2023-11-15 10:28:50 -05:00
// nolint:staticcheck
2023-01-18 17:47:59 +02:00
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
2022-02-16 14:15:44 +01:00
}
2022-11-24 15:59:47 +02:00
func ( s * Service ) nestedFolderCreate ( ctx context . Context , cmd * folder . CreateFolderCommand ) ( * folder . Folder , error ) {
2022-11-23 16:44:45 +02: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 15:59:47 +02:00
return nil , err
2022-11-23 16:44:45 +02:00
}
}
2022-11-24 15:59:47 +02:00
return s . store . Create ( ctx , * cmd )
2022-11-23 16:44:45 +02:00
}
2022-12-15 00:07:55 +08:00
func ( s * Service ) validateParent ( ctx context . Context , orgID int64 , parentUID string , UID string ) error {
2022-11-23 16:44:45 +02:00
ancestors , err := s . store . GetParents ( ctx , folder . GetParentsQuery { UID : parentUID , OrgID : orgID })
if err != nil {
2022-11-24 20:28:53 +02:00
return fmt . Errorf ( "failed to get parents: %w" , err )
2022-11-23 16:44:45 +02:00
}
2023-11-15 01:25:40 -08:00
if len ( ancestors ) >= folder . MaxNestedFolderDepth {
2023-04-20 17:47:51 +03:00
return folder . ErrMaximumDepthReached . Errorf ( "failed to validate parent folder" )
2022-11-23 16:44:45 +02: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 16:44:45 +02:00
return nil
}
2018-02-20 13:55:43 +01:00
func toFolderError ( err error ) error {
2022-06-30 09:31:54 -04:00
if errors . Is ( err , dashboards . ErrDashboardTitleEmpty ) {
return dashboards . ErrFolderTitleEmpty
2018-02-20 13:55:43 +01:00
}
2022-06-30 09:31:54 -04:00
if errors . Is ( err , dashboards . ErrDashboardUpdateAccessDenied ) {
return dashboards . ErrFolderAccessDenied
2018-02-20 13:55:43 +01:00
}
2022-06-30 09:31:54 -04:00
if errors . Is ( err , dashboards . ErrDashboardWithSameNameInFolderExists ) {
return dashboards . ErrFolderSameNameExists
2018-02-20 13:55:43 +01:00
}
2022-06-30 09:31:54 -04:00
if errors . Is ( err , dashboards . ErrDashboardWithSameUIDExists ) {
return dashboards . ErrFolderWithSameUIDExists
2018-02-20 13:55:43 +01:00
}
2022-06-30 09:31:54 -04:00
if errors . Is ( err , dashboards . ErrDashboardVersionMismatch ) {
return dashboards . ErrFolderVersionMismatch
2018-02-20 13:55:43 +01:00
}
2022-06-30 09:31:54 -04:00
if errors . Is ( err , dashboards . ErrDashboardNotFound ) {
return dashboards . ErrFolderNotFound
2018-02-20 13:55:43 +01:00
}
return err
}
2023-04-14 11:17:23 +02: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 11:27:22 +01: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
}