mirror of
https://github.com/grafana/grafana.git
synced 2024-12-26 08:51:33 -06:00
65fc7cf004
* Use user UID as identifier instead of ID * Remove malformed error
953 lines
30 KiB
Go
953 lines
30 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/client-go/dynamic"
|
|
|
|
"github.com/grafana/grafana/pkg/api/apierrors"
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/api/response"
|
|
"github.com/grafana/grafana/pkg/api/routing"
|
|
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
|
"github.com/grafana/grafana/pkg/infra/slugify"
|
|
internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
|
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
"github.com/grafana/grafana/pkg/services/guardian"
|
|
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
|
"github.com/grafana/grafana/pkg/services/search"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
"github.com/grafana/grafana/pkg/util/errhttp"
|
|
"github.com/grafana/grafana/pkg/web"
|
|
)
|
|
|
|
const REDACTED = "redacted"
|
|
|
|
func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authorize func(accesscontrol.Evaluator) web.Handler) {
|
|
// #TODO add back auth part
|
|
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
|
idScope := dashboards.ScopeFoldersProvider.GetResourceScope(accesscontrol.Parameter(":id"))
|
|
uidScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.Parameter(":uid"))
|
|
folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
|
|
folderRoute.Get("/id/:id", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, idScope)), routing.Wrap(hs.GetFolderByID))
|
|
|
|
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
|
folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
|
|
folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
|
|
folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder))
|
|
folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
|
|
folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
|
|
|
|
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
|
|
folderPermissionRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, uidScope)), routing.Wrap(hs.GetFolderPermissionList))
|
|
folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
|
|
})
|
|
})
|
|
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) {
|
|
// Use k8s client to implement legacy API
|
|
handler := newFolderK8sHandler(hs)
|
|
folderRoute.Post("/", handler.createFolder)
|
|
} else {
|
|
folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
|
|
}
|
|
})
|
|
}
|
|
|
|
// swagger:route GET /folders folders getFolders
|
|
//
|
|
// Get all folders.
|
|
//
|
|
// It returns all folders that the authenticated user has permission to view.
|
|
// If nested folders are enabled, it expects an additional query parameter with the parent folder UID
|
|
// and returns the immediate subfolders that the authenticated user has permission to view.
|
|
// If the parameter is not supplied then it returns immediate subfolders under the root
|
|
// that the authenticated user has permission to view.
|
|
//
|
|
// Responses:
|
|
// 200: getFoldersResponse
|
|
// 401: unauthorisedError
|
|
// 403: forbiddenError
|
|
// 500: internalServerError
|
|
func (hs *HTTPServer) GetFolders(c *contextmodel.ReqContext) response.Response {
|
|
permission := dashboardaccess.PERMISSION_VIEW
|
|
if c.Query("permission") == "Edit" {
|
|
permission = dashboardaccess.PERMISSION_EDIT
|
|
}
|
|
|
|
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
|
|
q := &folder.GetChildrenQuery{
|
|
OrgID: c.SignedInUser.GetOrgID(),
|
|
Limit: c.QueryInt64("limit"),
|
|
Page: c.QueryInt64("page"),
|
|
UID: c.Query("parentUid"),
|
|
Permission: permission,
|
|
SignedInUser: c.SignedInUser,
|
|
}
|
|
|
|
folders, err := hs.folderService.GetChildren(c.Req.Context(), q)
|
|
if err != nil {
|
|
return apierrors.ToFolderErrorResponse(err)
|
|
}
|
|
|
|
hits := make([]dtos.FolderSearchHit, 0)
|
|
for _, f := range folders {
|
|
hits = append(hits, dtos.FolderSearchHit{
|
|
ID: f.ID, // nolint:staticcheck
|
|
UID: f.UID,
|
|
Title: f.Title,
|
|
ParentUID: f.ParentUID,
|
|
})
|
|
metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetFolders).Inc()
|
|
}
|
|
|
|
return response.JSON(http.StatusOK, hits)
|
|
}
|
|
|
|
hits, err := hs.searchFolders(c, permission)
|
|
if err != nil {
|
|
return apierrors.ToFolderErrorResponse(err)
|
|
}
|
|
|
|
return response.JSON(http.StatusOK, hits)
|
|
}
|
|
|
|
// swagger:route GET /folders/{folder_uid} folders getFolderByUID
|
|
//
|
|
// Get folder by uid.
|
|
//
|
|
// Responses:
|
|
// 200: folderResponse
|
|
// 401: unauthorisedError
|
|
// 403: forbiddenError
|
|
// 404: notFoundError
|
|
// 500: internalServerError
|
|
func (hs *HTTPServer) GetFolderByUID(c *contextmodel.ReqContext) response.Response {
|
|
uid := web.Params(c.Req)[":uid"]
|
|
folder, err := hs.folderService.Get(c.Req.Context(), &folder.GetFolderQuery{OrgID: c.SignedInUser.GetOrgID(), UID: &uid, SignedInUser: c.SignedInUser})
|
|
if err != nil {
|
|
return apierrors.ToFolderErrorResponse(err)
|
|
}
|
|
|
|
folderDTO, err := hs.newToFolderDto(c, folder)
|
|
if err != nil {
|
|
return response.Err(err)
|
|
}
|
|
|
|
return response.JSON(http.StatusOK, folderDTO)
|
|
}
|
|
|
|
// swagger:route GET /folders/id/{folder_id} folders getFolderByID
|
|
//
|
|
// Get folder by id.
|
|
//
|
|
// Returns the folder identified by id. This is deprecated.
|
|
// Please refer to [updated API](#/folders/getFolderByUID) instead
|
|
//
|
|
// Deprecated: true
|
|
//
|
|
// Responses:
|
|
// 200: folderResponse
|
|
// 401: unauthorisedError
|
|
// 403: forbiddenError
|
|
// 404: notFoundError
|
|
// 500: internalServerError
|
|
func (hs *HTTPServer) GetFolderByID(c *contextmodel.ReqContext) response.Response {
|
|
id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
|
|
if err != nil {
|
|
return response.Error(http.StatusBadRequest, "id is invalid", err)
|
|
}
|
|
metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetFolderByID).Inc()
|
|
// nolint:staticcheck
|
|
folder, err := hs.folderService.Get(c.Req.Context(), &folder.GetFolderQuery{ID: &id, OrgID: c.SignedInUser.GetOrgID(), SignedInUser: c.SignedInUser})
|
|
if err != nil {
|
|
return apierrors.ToFolderErrorResponse(err)
|
|
}
|
|
|
|
folderDTO, err := hs.newToFolderDto(c, folder)
|
|
if err != nil {
|
|
return response.Err(err)
|
|
}
|
|
return response.JSON(http.StatusOK, folderDTO)
|
|
}
|
|
|
|
// swagger:route POST /folders folders createFolder
|
|
//
|
|
// Create folder.
|
|
//
|
|
// If nested folders are enabled then it additionally expects the parent folder UID.
|
|
//
|
|
// Responses:
|
|
// 200: folderResponse
|
|
// 400: badRequestError
|
|
// 401: unauthorisedError
|
|
// 403: forbiddenError
|
|
// 409: conflictError
|
|
// 500: internalServerError
|
|
func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response {
|
|
cmd := folder.CreateFolderCommand{}
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
}
|
|
cmd.OrgID = c.SignedInUser.GetOrgID()
|
|
cmd.SignedInUser = c.SignedInUser
|
|
|
|
folder, err := hs.folderService.Create(c.Req.Context(), &cmd)
|
|
if err != nil {
|
|
return apierrors.ToFolderErrorResponse(err)
|
|
}
|
|
|
|
// Clear permission cache for the user who's created the folder, so that new permissions are fetched for their next call
|
|
// Required for cases when caller wants to immediately interact with the newly created object
|
|
hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
|
|
|
|
folderDTO, err := hs.newToFolderDto(c, folder)
|
|
if err != nil {
|
|
return response.Err(err)
|
|
}
|
|
|
|
// TODO set ParentUID if nested folders are enabled
|
|
return response.JSON(http.StatusOK, folderDTO)
|
|
}
|
|
|
|
// swagger:route POST /folders/{folder_uid}/move folders moveFolder
|
|
//
|
|
// Move folder.
|
|
//
|
|
// Responses:
|
|
// 200: folderResponse
|
|
// 401: unauthorisedError
|
|
// 403: forbiddenError
|
|
// 404: notFoundError
|
|
// 500: internalServerError
|
|
func (hs *HTTPServer) MoveFolder(c *contextmodel.ReqContext) response.Response {
|
|
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
|
|
cmd := folder.MoveFolderCommand{}
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
}
|
|
var err error
|
|
|
|
cmd.OrgID = c.SignedInUser.GetOrgID()
|
|
cmd.UID = web.Params(c.Req)[":uid"]
|
|
cmd.SignedInUser = c.SignedInUser
|
|
theFolder, err := hs.folderService.Move(c.Req.Context(), &cmd)
|
|
if err != nil {
|
|
return response.ErrOrFallback(http.StatusInternalServerError, "move folder failed", err)
|
|
}
|
|
|
|
folderDTO, err := hs.newToFolderDto(c, theFolder)
|
|
if err != nil {
|
|
return response.Err(err)
|
|
}
|
|
return response.JSON(http.StatusOK, folderDTO)
|
|
}
|
|
result := map[string]string{}
|
|
result["message"] = "To use this service, you need to activate nested folder feature."
|
|
return response.JSON(http.StatusNotFound, result)
|
|
}
|
|
|
|
// swagger:route PUT /folders/{folder_uid} folders updateFolder
|
|
//
|
|
// Update folder.
|
|
//
|
|
// Responses:
|
|
// 200: folderResponse
|
|
// 400: badRequestError
|
|
// 401: unauthorisedError
|
|
// 403: forbiddenError
|
|
// 404: notFoundError
|
|
// 409: conflictError
|
|
// 500: internalServerError
|
|
func (hs *HTTPServer) UpdateFolder(c *contextmodel.ReqContext) response.Response {
|
|
cmd := folder.UpdateFolderCommand{}
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
|
}
|
|
|
|
cmd.OrgID = c.SignedInUser.GetOrgID()
|
|
cmd.UID = web.Params(c.Req)[":uid"]
|
|
cmd.SignedInUser = c.SignedInUser
|
|
result, err := hs.folderService.Update(c.Req.Context(), &cmd)
|
|
if err != nil {
|
|
return apierrors.ToFolderErrorResponse(err)
|
|
}
|
|
folderDTO, err := hs.newToFolderDto(c, result)
|
|
if err != nil {
|
|
return response.Err(err)
|
|
}
|
|
|
|
return response.JSON(http.StatusOK, folderDTO)
|
|
}
|
|
|
|
// swagger:route DELETE /folders/{folder_uid} folders deleteFolder
|
|
//
|
|
// Delete folder.
|
|
//
|
|
// Deletes an existing folder identified by UID along with all dashboards (and their alerts) stored in the folder. This operation cannot be reverted.
|
|
// If nested folders are enabled then it also deletes all the subfolders.
|
|
//
|
|
// Responses:
|
|
// 200: deleteFolderResponse
|
|
// 400: badRequestError
|
|
// 401: unauthorisedError
|
|
// 403: forbiddenError
|
|
// 404: notFoundError
|
|
// 500: internalServerError
|
|
func (hs *HTTPServer) DeleteFolder(c *contextmodel.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed
|
|
err := hs.LibraryElementService.DeleteLibraryElementsInFolder(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"])
|
|
if err != nil {
|
|
if errors.Is(err, model.ErrFolderHasConnectedLibraryElements) {
|
|
return response.Error(http.StatusForbidden, "Folder could not be deleted because it contains library elements in use", err)
|
|
}
|
|
return apierrors.ToFolderErrorResponse(err)
|
|
}
|
|
/* TODO: after a decision regarding folder deletion permissions has been made
|
|
(https://github.com/grafana/grafana-enterprise/issues/5144),
|
|
remove the previous call to hs.LibraryElementService.DeleteLibraryElementsInFolder
|
|
and remove "user" from the signature of DeleteInFolder in the folder RegistryService.
|
|
Context: https://github.com/grafana/grafana/pull/69149#discussion_r1235057903
|
|
*/
|
|
|
|
uid := web.Params(c.Req)[":uid"]
|
|
err = hs.folderService.Delete(c.Req.Context(), &folder.DeleteFolderCommand{UID: uid, OrgID: c.SignedInUser.GetOrgID(), ForceDeleteRules: c.QueryBool("forceDeleteRules"), SignedInUser: c.SignedInUser})
|
|
if err != nil {
|
|
return apierrors.ToFolderErrorResponse(err)
|
|
}
|
|
|
|
return response.JSON(http.StatusOK, util.DynMap{
|
|
"message": "Folder deleted",
|
|
})
|
|
}
|
|
|
|
// swagger:route GET /folders/{folder_uid}/counts folders getFolderDescendantCounts
|
|
//
|
|
// Gets the count of each descendant of a folder by kind. The folder is identified by UID.
|
|
//
|
|
// Responses:
|
|
// 200: getFolderDescendantCountsResponse
|
|
// 401: unauthorisedError
|
|
// 403: forbiddenError
|
|
// 404: notFoundError
|
|
// 500: internalServerError
|
|
func (hs *HTTPServer) GetFolderDescendantCounts(c *contextmodel.ReqContext) response.Response {
|
|
uid := web.Params(c.Req)[":uid"]
|
|
counts, err := hs.folderService.GetDescendantCounts(c.Req.Context(), &folder.GetDescendantCountsQuery{OrgID: c.SignedInUser.GetOrgID(), UID: &uid, SignedInUser: c.SignedInUser})
|
|
if err != nil {
|
|
return apierrors.ToFolderErrorResponse(err)
|
|
}
|
|
|
|
return response.JSON(http.StatusOK, counts)
|
|
}
|
|
func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, f *folder.Folder) (dtos.Folder, error) {
|
|
ctx := c.Req.Context()
|
|
toDTO := func(f *folder.Folder, checkCanView bool) (dtos.Folder, error) {
|
|
g, err := guardian.NewByFolder(c.Req.Context(), f, c.SignedInUser.GetOrgID(), c.SignedInUser)
|
|
if err != nil {
|
|
return dtos.Folder{}, err
|
|
}
|
|
|
|
canEdit, _ := g.CanEdit()
|
|
canSave, _ := g.CanSave()
|
|
canAdmin, _ := g.CanAdmin()
|
|
canDelete, _ := g.CanDelete()
|
|
|
|
// Finding creator and last updater of the folder
|
|
updater, creator := anonString, anonString
|
|
if f.CreatedBy > 0 {
|
|
creator = hs.getUserLogin(ctx, f.CreatedBy)
|
|
}
|
|
if f.UpdatedBy > 0 {
|
|
updater = hs.getUserLogin(ctx, f.UpdatedBy)
|
|
}
|
|
|
|
acMetadata, _ := hs.getFolderACMetadata(c, f)
|
|
|
|
if checkCanView {
|
|
canView, _ := g.CanView()
|
|
if !canView {
|
|
return dtos.Folder{
|
|
UID: REDACTED,
|
|
Title: REDACTED,
|
|
}, nil
|
|
}
|
|
}
|
|
metrics.MFolderIDsAPICount.WithLabelValues(metrics.NewToFolderDTO).Inc()
|
|
return dtos.Folder{
|
|
ID: f.ID, // nolint:staticcheck
|
|
UID: f.UID,
|
|
Title: f.Title,
|
|
URL: f.URL,
|
|
HasACL: f.HasACL,
|
|
CanSave: canSave,
|
|
CanEdit: canEdit,
|
|
CanAdmin: canAdmin,
|
|
CanDelete: canDelete,
|
|
CreatedBy: creator,
|
|
Created: f.Created,
|
|
UpdatedBy: updater,
|
|
Updated: f.Updated,
|
|
Version: f.Version,
|
|
AccessControl: acMetadata,
|
|
ParentUID: f.ParentUID,
|
|
}, nil
|
|
}
|
|
|
|
// no need to check view permission for the starting folder since it's already checked by the callers
|
|
folderDTO, err := toDTO(f, false)
|
|
if err != nil {
|
|
return dtos.Folder{}, err
|
|
}
|
|
|
|
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
|
|
return folderDTO, nil
|
|
}
|
|
|
|
parents, err := hs.folderService.GetParents(ctx, folder.GetParentsQuery{UID: f.UID, OrgID: f.OrgID})
|
|
if err != nil {
|
|
// log the error instead of failing
|
|
hs.log.Error("failed to fetch folder parents", "folder", f.UID, "org", f.OrgID, "error", err)
|
|
}
|
|
|
|
folderDTO.Parents = make([]dtos.Folder, 0, len(parents))
|
|
for _, f := range parents {
|
|
DTO, err := toDTO(f, true)
|
|
if err != nil {
|
|
hs.log.Error("failed to convert folder to DTO", "folder", f.UID, "org", f.OrgID, "error", err)
|
|
continue
|
|
}
|
|
folderDTO.Parents = append(folderDTO.Parents, DTO)
|
|
}
|
|
|
|
return folderDTO, nil
|
|
}
|
|
|
|
func (hs *HTTPServer) getFolderACMetadata(c *contextmodel.ReqContext, f *folder.Folder) (accesscontrol.Metadata, error) {
|
|
if !c.QueryBool("accesscontrol") {
|
|
return nil, nil
|
|
}
|
|
|
|
parents, err := hs.folderService.GetParents(c.Req.Context(), folder.GetParentsQuery{UID: f.UID, OrgID: c.SignedInUser.GetOrgID()})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
folderIDs := map[string]bool{f.UID: true}
|
|
for _, p := range parents {
|
|
folderIDs[p.UID] = true
|
|
}
|
|
|
|
allMetadata := getMultiAccessControlMetadata(c, dashboards.ScopeFoldersPrefix, folderIDs)
|
|
metadata := map[string]bool{}
|
|
// Flatten metadata - if any parent has a permission, the child folder inherits it
|
|
for _, md := range allMetadata {
|
|
for action := range md {
|
|
metadata[action] = true
|
|
}
|
|
}
|
|
return metadata, nil
|
|
}
|
|
|
|
func (hs *HTTPServer) searchFolders(c *contextmodel.ReqContext, permission dashboardaccess.PermissionType) ([]dtos.FolderSearchHit, error) {
|
|
searchQuery := search.Query{
|
|
SignedInUser: c.SignedInUser,
|
|
DashboardIds: make([]int64, 0),
|
|
FolderIds: make([]int64, 0), // nolint:staticcheck
|
|
Limit: c.QueryInt64("limit"),
|
|
OrgId: c.SignedInUser.GetOrgID(),
|
|
Type: "dash-folder",
|
|
Permission: permission,
|
|
Page: c.QueryInt64("page"),
|
|
}
|
|
|
|
hits, err := hs.SearchService.SearchHandler(c.Req.Context(), &searchQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
folderHits := make([]dtos.FolderSearchHit, 0)
|
|
for _, hit := range hits {
|
|
folderHits = append(folderHits, dtos.FolderSearchHit{
|
|
ID: hit.ID, // nolint:staticcheck
|
|
UID: hit.UID,
|
|
Title: hit.Title,
|
|
})
|
|
metrics.MFolderIDsAPICount.WithLabelValues(metrics.SearchFolders).Inc()
|
|
}
|
|
|
|
return folderHits, nil
|
|
}
|
|
|
|
// swagger:parameters getFolders
|
|
type GetFoldersParams struct {
|
|
// Limit the maximum number of folders to return
|
|
// in:query
|
|
// required:false
|
|
// default:1000
|
|
Limit int64 `json:"limit"`
|
|
// Page index for starting fetching folders
|
|
// in:query
|
|
// required:false
|
|
// default:1
|
|
Page int64 `json:"page"`
|
|
// The parent folder UID
|
|
// in:query
|
|
// required:false
|
|
ParentUID string `json:"parentUid"`
|
|
// Set to `Edit` to return folders that the user can edit
|
|
// in:query
|
|
// required: false
|
|
// default:View
|
|
// Enum: Edit,View
|
|
Permission string `json:"permission"`
|
|
}
|
|
|
|
// swagger:parameters getFolderByUID
|
|
type GetFolderByUIDParams struct {
|
|
// in:path
|
|
// required:true
|
|
FolderUID string `json:"folder_uid"`
|
|
}
|
|
|
|
// swagger:parameters updateFolder
|
|
type UpdateFolderParams struct {
|
|
// in:path
|
|
// required:true
|
|
FolderUID string `json:"folder_uid"`
|
|
// To change the unique identifier (uid), provide another one.
|
|
// To overwrite an existing folder with newer version, set `overwrite` to `true`.
|
|
// Provide the current version to safelly update the folder: if the provided version differs from the stored one the request will fail, unless `overwrite` is `true`.
|
|
//
|
|
// in:body
|
|
// required:true
|
|
Body folder.UpdateFolderCommand `json:"body"`
|
|
}
|
|
|
|
// swagger:parameters getFolderByID
|
|
type GetFolderByIDParams struct {
|
|
// in:path
|
|
// required:true
|
|
//
|
|
// Deprecated: use FolderUID instead
|
|
FolderID int64 `json:"folder_id"`
|
|
}
|
|
|
|
// swagger:parameters createFolder
|
|
type CreateFolderParams struct {
|
|
// in:body
|
|
// required:true
|
|
Body folder.CreateFolderCommand `json:"body"`
|
|
}
|
|
|
|
// swagger:parameters moveFolder
|
|
type MoveFolderParams struct {
|
|
// in:path
|
|
// required:true
|
|
FolderUID string `json:"folder_uid"`
|
|
// in:body
|
|
// required:true
|
|
Body folder.MoveFolderCommand `json:"body"`
|
|
}
|
|
|
|
// swagger:parameters deleteFolder
|
|
type DeleteFolderParams struct {
|
|
// in:path
|
|
// required:true
|
|
FolderUID string `json:"folder_uid"`
|
|
// If `true` any Grafana 8 Alerts under this folder will be deleted.
|
|
// Set to `false` so that the request will fail if the folder contains any Grafana 8 Alerts.
|
|
// in:query
|
|
// required:false
|
|
// default:false
|
|
ForceDeleteRules bool `json:"forceDeleteRules"`
|
|
}
|
|
|
|
// swagger:response getFoldersResponse
|
|
type GetFoldersResponse struct {
|
|
// The response message
|
|
// in: body
|
|
Body []dtos.FolderSearchHit `json:"body"`
|
|
}
|
|
|
|
// swagger:response folderResponse
|
|
type FolderResponse struct {
|
|
// The response message
|
|
// in: body
|
|
Body dtos.Folder `json:"body"`
|
|
}
|
|
|
|
// swagger:response deleteFolderResponse
|
|
type DeleteFolderResponse struct {
|
|
// The response message
|
|
// in: body
|
|
Body struct {
|
|
// ID Identifier of the deleted folder.
|
|
// required: true
|
|
// example: 65
|
|
ID int64 `json:"id"`
|
|
|
|
// Title of the deleted folder.
|
|
// required: true
|
|
// example: My Folder
|
|
Title string `json:"title"`
|
|
|
|
// Message Message of the deleted folder.
|
|
// required: true
|
|
// example: Folder My Folder deleted
|
|
Message string `json:"message"`
|
|
} `json:"body"`
|
|
}
|
|
|
|
// swagger:parameters getFolderDescendantCounts
|
|
type GetFolderDescendantCountsParams struct {
|
|
// in:path
|
|
// required:true
|
|
FolderUID string `json:"folder_uid"`
|
|
}
|
|
|
|
// swagger:response getFolderDescendantCountsResponse
|
|
type GetFolderDescendantCountsResponse struct {
|
|
// The response message
|
|
// in: body
|
|
Body folder.DescendantCounts `json:"body"`
|
|
}
|
|
|
|
type folderK8sHandler struct {
|
|
namespacer request.NamespaceMapper
|
|
gvr schema.GroupVersionResource
|
|
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
|
|
// #TODO check if it makes more sense to move this to FolderAPIBuilder
|
|
accesscontrolService accesscontrol.Service
|
|
userService user.Service
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------------------
|
|
// Folder k8s wrapper functions
|
|
//-----------------------------------------------------------------------------------------
|
|
|
|
func newFolderK8sHandler(hs *HTTPServer) *folderK8sHandler {
|
|
return &folderK8sHandler{
|
|
gvr: folderalpha1.FolderResourceInfo.GroupVersionResource(),
|
|
namespacer: request.GetNamespaceMapper(hs.Cfg),
|
|
clientConfigProvider: hs.clientConfigProvider,
|
|
accesscontrolService: hs.accesscontrolService,
|
|
userService: hs.userService,
|
|
}
|
|
}
|
|
|
|
// #TODO uncomment when we reinstate their corresponding routes
|
|
// func (fk8s *folderK8sHandler) searchFolders(c *contextmodel.ReqContext) {
|
|
// client, ok := fk8s.getClient(c)
|
|
// if !ok {
|
|
// return // error is already sent
|
|
// }
|
|
// out, err := client.List(c.Req.Context(), v1.ListOptions{})
|
|
// if err != nil {
|
|
// fk8s.writeError(c, err)
|
|
// return
|
|
// }
|
|
|
|
// query := strings.ToUpper(c.Query("query"))
|
|
// folders := []folder.Folder{}
|
|
// for _, item := range out.Items {
|
|
// p := internalfolders.UnstructuredToLegacyFolder(item)
|
|
// if p == nil {
|
|
// continue
|
|
// }
|
|
// if query != "" && !strings.Contains(strings.ToUpper(p.Title), query) {
|
|
// continue // query filter
|
|
// }
|
|
// folders = append(folders, *p)
|
|
// }
|
|
// c.JSON(http.StatusOK, folders)
|
|
// }
|
|
|
|
func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) {
|
|
client, ok := fk8s.getClient(c)
|
|
if !ok {
|
|
return // error is already sent
|
|
}
|
|
cmd := folder.CreateFolderCommand{}
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
|
|
return
|
|
}
|
|
obj, err := internalfolders.LegacyCreateCommandToUnstructured(cmd)
|
|
if err != nil {
|
|
fk8s.writeError(c, err)
|
|
return
|
|
}
|
|
out, err := client.Create(c.Req.Context(), &obj, v1.CreateOptions{})
|
|
if err != nil {
|
|
fk8s.writeError(c, err)
|
|
return
|
|
}
|
|
|
|
fk8s.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
|
|
folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID())
|
|
if err != nil {
|
|
fk8s.writeError(c, err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, folderDTO)
|
|
}
|
|
|
|
// func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) {
|
|
// client, ok := fk8s.getClient(c)
|
|
// if !ok {
|
|
// return // error is already sent
|
|
// }
|
|
// uid := web.Params(c.Req)[":uid"]
|
|
// out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{})
|
|
// if err != nil {
|
|
// fk8s.writeError(c, err)
|
|
// return
|
|
// }
|
|
|
|
// folderDTO, err := fk8s.newToFolderDto(c, *out)
|
|
// if err != nil {
|
|
// fk8s.writeError(c, err)
|
|
// return
|
|
// }
|
|
|
|
// c.JSON(http.StatusOK, folderDTO)
|
|
// }
|
|
|
|
// func (fk8s *folderK8sHandler) deleteFolder(c *contextmodel.ReqContext) {
|
|
// client, ok := fk8s.getClient(c)
|
|
// if !ok {
|
|
// return // error is already sent
|
|
// }
|
|
// uid := web.Params(c.Req)[":uid"]
|
|
// err := client.Delete(c.Req.Context(), uid, v1.DeleteOptions{})
|
|
// if err != nil {
|
|
// fk8s.writeError(c, err)
|
|
// return
|
|
// }
|
|
// c.JSON(http.StatusOK, "")
|
|
// }
|
|
|
|
// func (fk8s *folderK8sHandler) updateFolder(c *contextmodel.ReqContext) {
|
|
// client, ok := fk8s.getClient(c)
|
|
// if !ok {
|
|
// return // error is already sent
|
|
// }
|
|
// uid := web.Params(c.Req)[":uid"]
|
|
// cmd := folder.UpdateFolderCommand{}
|
|
// if err := web.Bind(c.Req, &cmd); err != nil {
|
|
// c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
|
|
// return
|
|
// }
|
|
// obj := internalfolders.LegacyUpdateCommandToUnstructured(cmd)
|
|
// obj.SetName(uid)
|
|
// out, err := client.Update(c.Req.Context(), &obj, v1.UpdateOptions{})
|
|
// if err != nil {
|
|
// fk8s.writeError(c, err)
|
|
// return
|
|
// }
|
|
|
|
// folderDTO, err := fk8s.newToFolderDto(c, *out)
|
|
// if err != nil {
|
|
// fk8s.writeError(c, err)
|
|
// return
|
|
// }
|
|
|
|
// c.JSON(http.StatusOK, folderDTO)
|
|
// }
|
|
|
|
//-----------------------------------------------------------------------------------------
|
|
// Utility functions
|
|
//-----------------------------------------------------------------------------------------
|
|
|
|
func (fk8s *folderK8sHandler) getClient(c *contextmodel.ReqContext) (dynamic.ResourceInterface, bool) {
|
|
dyn, err := dynamic.NewForConfig(fk8s.clientConfigProvider.GetDirectRestConfig(c))
|
|
if err != nil {
|
|
c.JsonApiErr(500, "client", err)
|
|
return nil, false
|
|
}
|
|
return dyn.Resource(fk8s.gvr).Namespace(fk8s.namespacer(c.OrgID)), true
|
|
}
|
|
|
|
func (fk8s *folderK8sHandler) writeError(c *contextmodel.ReqContext, err error) {
|
|
//nolint:errorlint
|
|
statusError, ok := err.(*k8sErrors.StatusError)
|
|
if ok {
|
|
c.JsonApiErr(int(statusError.Status().Code), statusError.Status().Message, err)
|
|
return
|
|
}
|
|
errhttp.Write(c.Req.Context(), err, c.Resp)
|
|
}
|
|
|
|
func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item unstructured.Unstructured, orgID int64) (dtos.Folder, error) {
|
|
// #TODO revisit how/where we get orgID
|
|
ctx := c.Req.Context()
|
|
|
|
f := internalfolders.UnstructuredToLegacyFolder(item, orgID)
|
|
|
|
fDTO, err := internalfolders.UnstructuredToLegacyFolderDTO(item)
|
|
if err != nil {
|
|
return dtos.Folder{}, err
|
|
}
|
|
|
|
// #TODO Is there a preexisting function we can use instead, something along the lines of UserIdentifier?
|
|
toUID := func(rawIdentifier string) string {
|
|
parts := strings.Split(rawIdentifier, ":")
|
|
if len(parts) < 2 {
|
|
return ""
|
|
}
|
|
return parts[1]
|
|
}
|
|
|
|
toDTO := func(fold *folder.Folder, checkCanView bool) (dtos.Folder, error) {
|
|
g, err := guardian.NewByFolder(c.Req.Context(), fold, c.SignedInUser.GetOrgID(), c.SignedInUser)
|
|
if err != nil {
|
|
return dtos.Folder{}, err
|
|
}
|
|
|
|
canEdit, _ := g.CanEdit()
|
|
canSave, _ := g.CanSave()
|
|
canAdmin, _ := g.CanAdmin()
|
|
canDelete, _ := g.CanDelete()
|
|
|
|
// Finding creator and last updater of the folder
|
|
updater, creator := anonString, anonString
|
|
// #TODO refactor the various conversions of the folder so that we either set created by in folder.Folder or
|
|
// we convert from unstructured to folder DTO without an intermediate conversion to folder.Folder
|
|
if len(fDTO.CreatedBy) > 0 {
|
|
creator = fk8s.getUserLogin(ctx, toUID(fDTO.CreatedBy), orgID)
|
|
}
|
|
if len(fDTO.UpdatedBy) > 0 {
|
|
updater = fk8s.getUserLogin(ctx, toUID(fDTO.UpdatedBy), orgID)
|
|
}
|
|
|
|
acMetadata, _ := fk8s.getFolderACMetadata(c, fold)
|
|
|
|
if checkCanView {
|
|
canView, _ := g.CanView()
|
|
if !canView {
|
|
return dtos.Folder{
|
|
UID: REDACTED,
|
|
Title: REDACTED,
|
|
}, nil
|
|
}
|
|
}
|
|
metrics.MFolderIDsAPICount.WithLabelValues(metrics.NewToFolderDTO).Inc()
|
|
|
|
fDTO.CanSave = canSave
|
|
fDTO.CanEdit = canEdit
|
|
fDTO.CanAdmin = canAdmin
|
|
fDTO.CanDelete = canDelete
|
|
fDTO.CreatedBy = creator
|
|
fDTO.UpdatedBy = updater
|
|
fDTO.AccessControl = acMetadata
|
|
fDTO.OrgID = f.OrgID
|
|
// #TODO version doesn't seem to be used--confirm or set it properly
|
|
fDTO.Version = 1
|
|
|
|
return *fDTO, nil
|
|
}
|
|
|
|
// no need to check view permission for the starting folder since it's already checked by the callers
|
|
folderDTO, err := toDTO(f, false)
|
|
if err != nil {
|
|
return dtos.Folder{}, err
|
|
}
|
|
|
|
if len(f.Fullpath) == 0 || len(f.FullpathUIDs) == 0 {
|
|
return folderDTO, nil
|
|
}
|
|
|
|
parentsFullPath, err := internalfolders.GetParentTitles(f.Fullpath)
|
|
if err != nil {
|
|
return dtos.Folder{}, err
|
|
}
|
|
parentsFullPathUIDs := strings.Split(f.FullpathUIDs, "/")
|
|
|
|
// The first part of the path is the newly created folder which we don't need to include
|
|
// in the parents field
|
|
if len(parentsFullPath) < 2 || len(parentsFullPathUIDs) < 2 {
|
|
return folderDTO, nil
|
|
}
|
|
|
|
parents := []dtos.Folder{}
|
|
for i, v := range parentsFullPath[1:] {
|
|
slug := slugify.Slugify(v)
|
|
uid := parentsFullPathUIDs[1:][i]
|
|
url := dashboards.GetFolderURL(uid, slug)
|
|
|
|
parents = append(parents, dtos.Folder{
|
|
UID: uid,
|
|
OrgID: c.SignedInUser.GetOrgID(),
|
|
Title: v,
|
|
URL: url,
|
|
})
|
|
}
|
|
|
|
folderDTO.Parents = parents
|
|
|
|
return folderDTO, nil
|
|
}
|
|
|
|
func (fk8s *folderK8sHandler) getUserLogin(ctx context.Context, userUID string, orgID int64) string {
|
|
ctx, span := tracer.Start(ctx, "api.getUserLogin")
|
|
defer span.End()
|
|
|
|
query := user.GetUserByUIDQuery{
|
|
UID: userUID,
|
|
OrgID: orgID,
|
|
}
|
|
user, err := fk8s.userService.GetByUID(ctx, &query)
|
|
if err != nil {
|
|
return anonString
|
|
}
|
|
return user.Login
|
|
}
|
|
|
|
func (fk8s *folderK8sHandler) getFolderACMetadata(c *contextmodel.ReqContext, f *folder.Folder) (accesscontrol.Metadata, error) {
|
|
if !c.QueryBool("accesscontrol") {
|
|
return nil, nil
|
|
}
|
|
|
|
if len(f.FullpathUIDs) == 0 {
|
|
return map[string]bool{}, nil
|
|
}
|
|
|
|
parentsFullPathUIDs := strings.Split(f.FullpathUIDs, "/")
|
|
// The first part of the path is the newly created folder which we don't need to check here
|
|
if len(parentsFullPathUIDs) < 2 {
|
|
return map[string]bool{}, nil
|
|
}
|
|
|
|
folderIDs := map[string]bool{f.UID: true}
|
|
for _, uid := range parentsFullPathUIDs[1:] {
|
|
folderIDs[uid] = true
|
|
}
|
|
|
|
allMetadata := getMultiAccessControlMetadata(c, dashboards.ScopeFoldersPrefix, folderIDs)
|
|
metadata := map[string]bool{}
|
|
// Flatten metadata - if any parent has a permission, the child folder inherits it
|
|
for _, md := range allMetadata {
|
|
for action := range md {
|
|
metadata[action] = true
|
|
}
|
|
}
|
|
return metadata, nil
|
|
}
|