package api import ( "context" "errors" "fmt" "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" 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 // #TODO remove after we handle the nested folder case folderService folder.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, folderService: hs.folderService, } } // #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 } toID := func(rawIdentifier string) (int64, error) { parts := strings.Split(rawIdentifier, ":") if len(parts) < 2 { return 0, fmt.Errorf("invalid user identifier") } userID, err := strconv.ParseInt(parts[1], 10, 64) if err != nil { return 0, fmt.Errorf("faild to parse user identifier") } return userID, nil } 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 { id, err := toID(fDTO.CreatedBy) if err != nil { return dtos.Folder{}, err } creator = fk8s.getUserLogin(ctx, id) } if len(fDTO.UpdatedBy) > 0 { id, err := toID(fDTO.UpdatedBy) if err != nil { return dtos.Folder{}, err } updater = fk8s.getUserLogin(ctx, id) } 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 } parents := []*folder.Folder{} if folderDTO.ParentUID != "" { parents, err = fk8s.folderService.GetParents( c.Req.Context(), folder.GetParentsQuery{ UID: folderDTO.UID, OrgID: folderDTO.OrgID, }) if err != nil { return dtos.Folder{}, err } } // #TODO refactor so that we have just one function for converting to folder DTO toParentDTO := 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 } if checkCanView { canView, _ := g.CanView() if !canView { return dtos.Folder{ UID: REDACTED, Title: REDACTED, }, nil } } metrics.MFolderIDsAPICount.WithLabelValues(metrics.NewToFolderDTO).Inc() return dtos.Folder{ UID: fold.UID, Title: fold.Title, URL: fold.URL, }, nil } folderDTO.Parents = make([]dtos.Folder, 0, len(parents)) for _, f := range parents { DTO, err := toParentDTO(f, true) if err != nil { // #TODO add logging // fk8s.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 (fk8s *folderK8sHandler) getUserLogin(ctx context.Context, userID int64) string { ctx, span := tracer.Start(ctx, "api.getUserLogin") defer span.End() query := user.GetUserByIDQuery{ID: userID} user, err := fk8s.userService.GetByID(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 } var err error parents := []*folder.Folder{} if f.ParentUID != "" { parents, err = fk8s.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 }