From ea5cf7c51fa458954ea2fdc8fb2d4bd8cdbcd5fe Mon Sep 17 00:00:00 2001 From: "Arati R." <33031346+suntala@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:48:56 +0200 Subject: [PATCH] Unified Storage /Folders: Allow Unified Storage subfolders creation (#94327) * Add parents field to folder DTO * Allow subfolder creation when folder flag is enabled * Update UnstructuredToLegacyFolder * Include parents field when creating folder --- pkg/api/folder.go | 101 ++++++++++++++++------- pkg/registry/apis/folders/conversions.go | 44 ++++++++-- pkg/storage/unified/sql/server.go | 14 ++++ 3 files changed, 125 insertions(+), 34 deletions(-) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 8c0d02742aa..12b2e6b2d41 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -635,6 +635,8 @@ type folderK8sHandler struct { // #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 } //----------------------------------------------------------------------------------------- @@ -648,6 +650,7 @@ func newFolderK8sHandler(hs *HTTPServer) *folderK8sHandler { clientConfigProvider: hs.clientConfigProvider, accesscontrolService: hs.accesscontrolService, userService: hs.userService, + folderService: hs.folderService, } } @@ -700,7 +703,7 @@ func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) { } fk8s.accesscontrolService.ClearUserPermissionCache(c.SignedInUser) - folderDTO, err := fk8s.newToFolderDto(c, *out) + folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID()) if err != nil { fk8s.writeError(c, err) return @@ -795,10 +798,11 @@ func (fk8s *folderK8sHandler) writeError(c *contextmodel.ReqContext, err error) errhttp.Write(c.Req.Context(), err, c.Resp) } -func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item unstructured.Unstructured) (dtos.Folder, error) { +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) + f := internalfolders.UnstructuredToLegacyFolder(item, orgID) fDTO, err := internalfolders.UnstructuredToLegacyFolderDTO(item) if err != nil { @@ -817,8 +821,8 @@ func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item un return userID, nil } - toDTO := func(f *folder.Folder, checkCanView bool) (dtos.Folder, error) { - g, err := guardian.NewByFolder(c.Req.Context(), f, c.SignedInUser.GetOrgID(), c.SignedInUser) + 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 } @@ -830,6 +834,8 @@ func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item un // 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 { @@ -845,7 +851,7 @@ func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item un updater = fk8s.getUserLogin(ctx, id) } - acMetadata, _ := fk8s.getFolderACMetadata(c, f) + acMetadata, _ := fk8s.getFolderACMetadata(c, fold) if checkCanView { canView, _ := g.CanView() @@ -865,6 +871,9 @@ func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item un 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 } @@ -875,24 +884,54 @@ func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item un return dtos.Folder{}, err } - // TODO: handle parents - /* - parents, err := fk8s.folderService.GetParents(ctx, folder.GetParentsQuery{UID: f.UID, OrgID: f.OrgID}) + 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 { - // log the error instead of failing - fk8s.log.Error("failed to fetch folder parents", "folder", f.UID, "org", f.OrgID, "error", err) + return dtos.Folder{}, err } + } - folderDTO.Parents = make([]dtos.Folder, 0, len(parents)) - for _, f := range parents { - DTO, err := toDTO(f, true) - if err != nil { - // fk8s.log.Error("failed to convert folder to DTO", "folder", f.UID, "org", f.OrgID, "error", err) - continue + // #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 } - folderDTO.Parents = append(folderDTO.Parents, DTO) } - */ + 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 } @@ -914,20 +953,24 @@ func (fk8s *folderK8sHandler) getFolderACMetadata(c *contextmodel.ReqContext, f return nil, nil } - folderIDs := map[string]bool{f.UID: true} - - // TODO: handle parents - /* - parents, err := fk8s.folderService.GetParents(c.Req.Context(), folder.GetParentsQuery{UID: f.UID, OrgID: c.SignedInUser.GetOrgID()}) + 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 - } - */ + 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{} diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index 125e5838a79..0deabc23ed3 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -50,13 +50,47 @@ func LegacyUpdateCommandToUnstructured(cmd folder.UpdateFolderCommand) unstructu return obj } -func UnstructuredToLegacyFolder(item unstructured.Unstructured) *folder.Folder { +func UnstructuredToLegacyFolder(item unstructured.Unstructured, orgID int64) *folder.Folder { + // #TODO reduce duplication of the different conversion functions spec := item.Object["spec"].(map[string]any) - return &folder.Folder{ - UID: item.GetName(), - Title: spec["title"].(string), - // #TODO add other fields + uid := item.GetName() + title := spec["title"].(string) + + meta, err := utils.MetaAccessor(&item) + if err != nil { + return nil + } + + id, err := getLegacyID(meta) + if err != nil { + return nil + } + + created, err := getCreated(meta) + if err != nil { + return nil + } + + // avoid panic + var createdTime time.Time + if created != nil { + createdTime = created.Local() + } + + f := &folder.Folder{ + UID: uid, + Title: title, + ID: id, + ParentUID: meta.GetFolder(), + // #TODO add created by field if necessary + // CreatedBy: meta.GetCreatedBy(), + // UpdatedBy: meta.GetCreatedBy(), + URL: getURL(meta, title), + Created: createdTime, + Updated: createdTime, + OrgID: orgID, } + return f } func UnstructuredToLegacyFolderDTO(item unstructured.Unstructured) (*dtos.Folder, error) { diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index 7fcef520fc1..f2cd8f27c5b 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -1,6 +1,9 @@ package sql import ( + "context" + + "github.com/grafana/authlib/claims" infraDB "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -31,5 +34,16 @@ func NewResourceServer(db infraDB.DB, cfg *setting.Cfg, features featuremgmt.Fea opts.Index = resource.NewResourceIndexServer() } + if features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) { + opts.WriteAccess = resource.WriteAccessHooks{ + Folder: func(ctx context.Context, user claims.AuthInfo, uid string) bool { + // #TODO build on the logic here + // #TODO only enable write access when the resource being written in the folder + // is another folder + return true + }, + } + } + return resource.NewResourceServer(opts) }