diff --git a/pkg/api/dtos/folder.go b/pkg/api/dtos/folder.go index a35167db214..6a8152343e4 100644 --- a/pkg/api/dtos/folder.go +++ b/pkg/api/dtos/folder.go @@ -24,6 +24,8 @@ type Folder struct { AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"` // only used if nested folders are enabled ParentUID string `json:"parentUid,omitempty"` + // the parent folders starting from the root going down + Parents []Folder `json:"parents,omitempty"` } type FolderSearchHit struct { diff --git a/pkg/api/folder.go b/pkg/api/folder.go index c2154956db6..ad15d5371ad 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -316,42 +316,63 @@ func (hs *HTTPServer) GetFolderChildrenCounts(c *contextmodel.ReqContext) respon return response.JSON(http.StatusOK, counts) } +func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, g guardian.DashboardGuardian, f *folder.Folder) dtos.Folder { + ctx := c.Req.Context() + toDTO := func(f *folder.Folder) dtos.Folder { + 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) + + return dtos.Folder{ + Id: f.ID, + 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, + } + } + + folderDTO := toDTO(f) -func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, g guardian.DashboardGuardian, folder *folder.Folder) dtos.Folder { - 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 folder.CreatedBy > 0 { - creator = hs.getUserLogin(c.Req.Context(), folder.CreatedBy) - } - if folder.UpdatedBy > 0 { - updater = hs.getUserLogin(c.Req.Context(), folder.UpdatedBy) - } - - acMetadata, _ := hs.getFolderACMetadata(c, folder) - - return dtos.Folder{ - Id: folder.ID, - Uid: folder.UID, - Title: folder.Title, - Url: folder.URL, - HasACL: folder.HasACL, - CanSave: canSave, - CanEdit: canEdit, - CanAdmin: canAdmin, - CanDelete: canDelete, - CreatedBy: creator, - Created: folder.Created, - UpdatedBy: updater, - Updated: folder.Updated, - Version: folder.Version, - AccessControl: acMetadata, - ParentUID: folder.ParentUID, + if !hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) { + return folderDTO } + + 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 { + folderDTO.Parents = append(folderDTO.Parents, toDTO(f)) + } + + return folderDTO } func (hs *HTTPServer) getFolderACMetadata(c *contextmodel.ReqContext, f *folder.Folder) (accesscontrol.Metadata, error) { diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 196023bb6ec..7d3b12f8808 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -422,3 +422,89 @@ func TestFolderMoveAPIEndpoint(t *testing.T) { }) } } + +func TestFolderGetAPIEndpoint(t *testing.T) { + folderService := &foldertest.FakeService{ + ExpectedFolder: &folder.Folder{ + ID: 1, + UID: "uid", + Title: "uid title", + }, + ExpectedFolders: []*folder.Folder{ + { + UID: "parent", + Title: "parent title", + }, + { + UID: "subfolder", + Title: "subfolder title", + }, + }, + } + setUpRBACGuardian(t) + + type testCase struct { + description string + URL string + features *featuremgmt.FeatureManager + expectedCode int + expectedParentUIDs []string + expectedParentTitles []string + permissions []accesscontrol.Permission + } + tcs := []testCase{ + { + description: "get folder by UID should return parent folders if nested folder are enabled", + URL: "/api/folders/uid", + expectedCode: http.StatusOK, + features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + expectedParentUIDs: []string{"parent", "subfolder"}, + expectedParentTitles: []string{"parent title", "subfolder title"}, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")}, + }, + }, + { + description: "get folder by UID should not return parent folders if nested folder are disabled", + URL: "/api/folders/uid", + expectedCode: http.StatusOK, + features: featuremgmt.WithFeatures(), + expectedParentUIDs: []string{}, + expectedParentTitles: []string{}, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid")}, + }, + }, + } + + for _, tc := range tcs { + srv := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = &setting.Cfg{ + RBACEnabled: true, + } + hs.Features = tc.features + hs.folderService = folderService + }) + + t.Run(tc.description, func(t *testing.T) { + req := srv.NewGetRequest(tc.URL) + req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, tc.permissions)) + resp, err := srv.Send(req) + require.NoError(t, err) + require.Equal(t, tc.expectedCode, resp.StatusCode) + + folder := dtos.Folder{} + err = json.NewDecoder(resp.Body).Decode(&folder) + require.NoError(t, err) + + require.Equal(t, len(folder.Parents), len(tc.expectedParentUIDs)) + require.Equal(t, len(folder.Parents), len(tc.expectedParentTitles)) + + for i := 0; i < len(tc.expectedParentUIDs); i++ { + assert.Equal(t, tc.expectedParentUIDs[i], folder.Parents[i].Uid) + assert.Equal(t, tc.expectedParentTitles[i], folder.Parents[i].Title) + } + require.NoError(t, resp.Body.Close()) + }) + } +} diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index 87dbb54ae10..1040c7a8088 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -48,12 +48,6 @@ func (f *Folder) IsGeneral() bool { return f.ID == GeneralFolder.ID && f.Title == GeneralFolder.Title } -type FolderDTO struct { - Folder - - Children []FolderDTO -} - // 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 { diff --git a/public/api-merged.json b/public/api-merged.json index c0d177f2a0b..93e06f99ba1 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -13565,6 +13565,13 @@ "description": "only used if nested folders are enabled", "type": "string" }, + "parents": { + "description": "the parent folders starting from the root going down", + "type": "array", + "items": { + "$ref": "#/definitions/Folder" + } + }, "title": { "type": "string" }, diff --git a/public/openapi3.json b/public/openapi3.json index 82c55adce95..635e242ae6e 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -4631,6 +4631,13 @@ "description": "only used if nested folders are enabled", "type": "string" }, + "parents": { + "description": "the parent folders starting from the root going down", + "items": { + "$ref": "#/components/schemas/Folder" + }, + "type": "array" + }, "title": { "type": "string" },