/
githubmirror
/
grafana
Обзор
Документация
Войти
/
githubmirror
/
grafana
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
pkg/services/folder/model.go
383 строки
14 KB
maicon
Folders: Prevent creating or moving folders in the k6 tree (#130019)
04 авг 2026, 18:11
Не верифицирован
04 авг 2026, 18:11
d34adba
Код
Авторство
О чём код?
package folder import ( "errors" "fmt" "time" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/setting" ) var ErrMaximumDepthReached = errutil.BadRequest("folder.maximum-depth-reached", errutil.WithPublicMessage("Maximum nested folder depth reached")) // Important: keep error msgID stable for other services to consume (provisioning) var ErrBadRequest = errutil.BadRequest("folder.bad-request") var ErrDatabaseError = errutil.Internal("folder.database-error") var ErrConflict = errutil.Conflict("folder.conflict") var ErrInternal = errutil.Internal("folder.internal") var ErrCircularReference = errutil.BadRequest("folder.circular-reference", errutil.WithPublicMessage("Circular reference detected")) var ErrTargetRegistrySrvConflict = errutil.Internal("folder.target-registry-srv-conflict") var ErrFolderNotEmpty = errutil.BadRequest("folder.not-empty", errutil.WithPublicMessage("Folder cannot be deleted: folder is not empty")) var ErrFolderCannotBeMovedToK6 = errutil.BadRequest("folder.cannot-be-moved-to-k6", errutil.WithPublicMessage("Folders cannot be moved into the k6 project")) var ErrFolderCannotBeCreatedInK6 = errutil.BadRequest("folder.cannot-be-created-in-k6", errutil.WithPublicMessage("Folders cannot be created inside the k6 project")) // ErrCyclicReference indicates corrupt storage state, not user input. var ErrCyclicReference = errutil.Internal("folder.cyclic-reference", errutil.WithPublicMessage("Cyclic folder references found")) // TODO: evaluate if we can remove legacy errors and only have k8s ones var ErrTitleEmpty = errors.New("folder title cannot be empty") var ErrInvalidUID = errors.New("invalid uid for folder provided") var ErrFolderCannotBeParentOfItself = errors.New("folder cannot be parent of itself") var ErrVersionMismatch = errors.New("the folder has been changed by someone else") var ErrSameUIDExists = errors.New("a folder/dashboard with the same uid already exists") var ErrAccessDenied = errors.New("access denied to folder") // Wraps legacy errors (/api) into an apiserver (/apis) error for them to be handled as 400. var ErrAPITitleEmpty = errutil.BadRequest("folder.title-empty", errutil.WithPublicMessage("Folder title cannot be empty")). Errorf("%w", ErrTitleEmpty) var ErrAPIInvalidUID = errutil.BadRequest("folder.invalid-uid", errutil.WithPublicMessage("Invalid uid for folder provided")). Errorf("%w", ErrInvalidUID) var ErrAPIFolderCannotBeParentOfItself = errutil.BadRequest("folder.cannot-be-parent-of-itself", errutil.WithPublicMessage("Folder cannot be parent of itself")). Errorf("%w", ErrFolderCannotBeParentOfItself) var ErrMoveAccessDenied = errutil.Forbidden("folders.forbiddenMove", errutil.WithPublicMessage("Access denied to the destination folder")) var ErrAccessEscalation = errutil.Forbidden("folders.accessEscalation", errutil.WithPublicMessage("Cannot move a folder to a folder where you have higher permissions")) var ErrCreationAccessDenied = errutil.Forbidden("folders.forbiddenCreation", errutil.WithPublicMessage("not enough permissions to create a folder in the selected location")) var ErrNameExists = errutil.BadRequest("folder.name-exists", errutil.WithPublicMessage("A folder with that name already exists")) const ( // GeneralFolderUID is the Grafana UID that identifies the root folder. GeneralFolderUID = "general" // LegacyRootFolderUID is the legacy root sentinel — an empty string — still // surfaced by older /api/ responses and stored on resources written before // folder annotations were populated. // // Deprecated: use GeneralFolderUID for the canonical root folder UID. This // constant is retained only for compatibility with legacy API responses and // stored data that still uses the empty-string sentinel. LegacyRootFolderUID = "" // SharedWithMeFolderUID is the UID for the special "Shared with me" folder, // It is not a real folder but used to identify the location of resources that you // can see, but do not have access to the containing folder SharedWithMeFolderUID = "sharedwithme" ) var ErrFolderNotFound = errutil.NotFound("folder.notFound") type Folder struct { // Deprecated: use UID instead ID int64 `xorm:"pk autoincr 'id'"` OrgID int64 `xorm:"org_id"` UID string `xorm:"uid"` ParentUID string `xorm:"parent_uid"` Title string Description string Created time.Time Updated time.Time // TODO: validate if this field is required/relevant to folders. // currently there is no such column Version int URL string UpdatedBy int64 CreatedBy int64 UpdatedByUID string CreatedByUID string HasACL bool Fullpath string `xorm:"fullpath"` FullpathUIDs string `xorm:"fullpath_uids"` // The folder is managed by an external process // NOTE: this is only populated when folders are managed by unified storage // This is not ever used by xorm, but the translation functions flow through this type ManagedBy utils.ManagerKind `json:"managedBy,omitempty"` } type FolderReference struct { // Deprecated: use UID instead ID int64 `xorm:"pk autoincr 'id'"` UID string `xorm:"uid"` Title string ParentUID string `xorm:"parent_uid"` // When the folder belongs to a repository // NOTE: this is only populated when folders are managed by unified storage ManagedBy utils.ManagerKind `json:"managedBy,omitempty"` } var GeneralFolder = Folder{ID: 0, Title: "General"} var RootFolder = &Folder{ID: 0, Title: "Dashboards", UID: GeneralFolderUID, ParentUID: ""} var SharedWithMeFolder = Folder{ Title: "Shared with me", Description: "Dashboards and folders shared with me", UID: SharedWithMeFolderUID, ParentUID: "", ID: -1, } func (f *Folder) IsGeneral() bool { metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc() // nolint:staticcheck return f.ID == GeneralFolder.ID && f.Title == GeneralFolder.Title } // ToLegacyFolderUID maps the GeneralFolderUID ("general") sentinel back to the // empty-for-root convention expected by legacy API responses // (/api/dashboards, /api/folders, /api/search). All other UIDs are returned // unchanged. func ToLegacyFolderUID(uid string) string { if uid == GeneralFolderUID { return LegacyRootFolderUID //nolint:staticcheck } return uid } // IsRootFolderUID reports whether the given folder UID identifies the root // folder. Use this for "does this resource have a parent folder?" checks // where both root sentinels must be treated equivalently: // - "" (legacy empty annotation) // - "general" (canonical root UID) func IsRootFolderUID(uid string) bool { return uid == LegacyRootFolderUID || uid == GeneralFolderUID //nolint:staticcheck } func (f *Folder) WithURL() *Folder { if f == nil || f.URL != "" { return f } // copy of dashboards.GetFolderURL() f.URL = fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, f.UID, slugify.Slugify(f.Title)) return f } func (f *Folder) ToFolderReference() *FolderReference { return &FolderReference{ ID: f.ID, UID: f.UID, Title: f.Title, ParentUID: f.ParentUID, ManagedBy: f.ManagedBy, } } // NewFolder tales a title and returns a Folder with the Created and Updated // fields set to the current time. func NewFolder(title string, description string) *Folder { return &Folder{ Title: title, Description: description, Created: time.Now(), Updated: time.Now(), } } // CreateFolderCommand captures the information required by the folder service // to create a folder. type CreateFolderCommand struct { UID string `json:"uid"` OrgID int64 `json:"-"` Title string `json:"title"` Description string `json:"description"` ParentUID string `json:"parentUid"` SignedInUser identity.Requester `json:"-"` // When running classic file provisioning with folders saved in kubernetes, // folders will be marked with a manager of kind ManagerKindClassicFP // NOTE: this is ignored when running legacy SQL storage // // Deprecated: this should only be used by the legacy file provisioning system ManagerKindClassicFP string `json:"-"` } // UpdateFolderCommand captures the information required by the folder service // to update a folder. Use Move to update a folder's parent folder. type UpdateFolderCommand struct { UID string `json:"-"` OrgID int64 `json:"-"` // NewTitle it's an optional parameter used for overriding the existing folder title NewTitle *string `json:"title"` // keep same json tag with the legacy command for not breaking the existing APIs // NewDescription it's an optional parameter used for overriding the existing folder description NewDescription *string `json:"description"` // keep same json tag with the legacy command for not breaking the existing APIs NewParentUID *string `json:"-"` // Version only used by the legacy folder implementation Version int `json:"version"` // Overwrite only used by the legacy folder implementation Overwrite bool `json:"overwrite"` SignedInUser identity.Requester `json:"-"` // When running classic file provisioning with folders saved in kubernetes, // folders will be marked with a manager of kind ManagerKindClassicFP // NOTE: this is ignored when running legacy SQL storage // // Deprecated: this should only be used by the legacy file provisioning system ManagerKindClassicFP string `json:"-"` } // MoveFolderCommand captures the information required by the folder service // to move a folder. type MoveFolderCommand struct { UID string `json:"-"` NewParentUID string `json:"parentUid"` OrgID int64 `json:"-"` SignedInUser identity.Requester `json:"-"` } // DeleteFolderCommand captures the information required by the folder service // to delete a folder. type DeleteFolderCommand struct { UID string `json:"uid" xorm:"uid"` OrgID int64 `json:"orgId" xorm:"org_id"` ForceDeleteRules bool `json:"forceDeleteRules"` SignedInUser identity.Requester `json:"-"` RemovePermissions bool `json:"-"` } // GetFolderQuery is used for all folder Get requests. Only one of UID, ID, or // Title should be set; if multiple fields are set by the caller the dashboard // service will select the field with the most specificity, in order: ID, UID, // Title. type GetFolderQuery struct { UID *string // Deprecated: use FolderUID instead ID *int64 Title *string ParentUID *string OrgID int64 WithFullpath bool WithFullpathUIDs bool SignedInUser identity.Requester `json:"-"` } type GetFoldersQuery struct { OrgID int64 UIDs []string WithFullpath bool WithFullpathUIDs bool BatchSize uint64 // MetadataOnly serves folders from the search index instead of reading each // full stored object. Only UID, ID, OrgID, ParentUID, Title, ManagedBy and // the full paths are populated. For callers that only need the folder // tree/titles and want to avoid the linear full-object fetch on large orgs. MetadataOnly bool // Pagination options Limit int64 Page int64 // OrderByTitle is used to sort the folders by title // Set to true when ordering is meaningful (used for listing folders) // otherwise better to keep it false since ordering can have a performance impact OrderByTitle bool SignedInUser identity.Requester `json:"-"` } type SearchFoldersQuery struct { OrgID int64 UIDs []string IDs []int64 Title string TitleExactMatch bool Limit int64 SignedInUser identity.Requester `json:"-"` } // GetParentsQuery captures the information required by the folder service to // return a list of all parent folders of a given folder. type GetParentsQuery struct { UID string `xorm:"uid"` OrgID int64 `xorm:"org_id"` } // GetChildrenQuery captures the information required by the folder service to // return a list of child folders of the given folder. type GetChildrenQuery struct { UID string OrgID int64 Depth int64 // Pagination options Limit int64 Page int64 // Permission to filter by Permission dashboardaccess.PermissionType SignedInUser identity.Requester `json:"-"` // array of folder uids to filter by FolderUIDs []string `json:"-"` } // GetDescendantCountsQuery captures the information required by the folder service // to return the count of descendants (direct and indirect) in a folder. type GetDescendantCountsQuery struct { UID *string OrgID int64 SignedInUser identity.Requester `json:"-"` } type DescendantCounts map[string]int64 // SortByPostorder returns the folders in postorder traversal order. // That is, children folders appear before their parents in the returned slice. func SortByPostorder(folders []*Folder) []*Folder { if len(folders) == 0 { return folders } // Build parent-to-children map tree := make(map[string][]*Folder) folderMap := make(map[string]*Folder) for _, f := range folders { folderMap[f.UID] = f tree[f.ParentUID] = append(tree[f.ParentUID], f) } // Find all roots (folders whose parents are not in the result set) var roots []*Folder for _, f := range folders { if folderMap[f.ParentUID] == nil { roots = append(roots, f) } } // Traverse in postorder result := make([]*Folder, 0, len(folders)) visited := make(map[string]bool) var traverse func(f *Folder) traverse = func(f *Folder) { if visited[f.UID] { return } visited[f.UID] = true // First visit all children for _, child := range tree[f.UID] { traverse(child) } // Then add current folder result = append(result, f) } for _, root := range roots { traverse(root) } return result }