Skip to content

Folders: Migrate getFolder API to app platform #107617

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4ea5a35
Add /children endpoint
aocenas Jun 18, 2025
fe84c8c
Update folder client
aocenas Jun 19, 2025
a9c2f4c
Add comment
aocenas Jun 19, 2025
d867638
Add feature toggle
aocenas Jun 20, 2025
187e977
Add new version of useFoldersQuery
aocenas Jun 20, 2025
db5b914
Merge branch 'main' into aocenas/folders/migrate-api
Clarity-89 Jun 26, 2025
c70aaff
Error handling
Clarity-89 Jun 26, 2025
8e8ad40
Format
Clarity-89 Jun 26, 2025
2a00788
Rename feature toggle
aocenas Jul 1, 2025
c7f3908
Remove options and move root folder constant
aocenas Jul 1, 2025
eed14cd
Merge remote-tracking branch 'origin/aocenas/folders/migrate-api' int…
aocenas Jul 1, 2025
34bc654
Fix feature toggle merge
aocenas Jul 1, 2025
48af59b
Merge branch 'main' into aocenas/folders/migrate-api
aocenas Jul 1, 2025
d1d959d
Add feature toggle again
aocenas Jul 1, 2025
421958b
Rename useFoldersQuery files
aocenas Jul 1, 2025
d529ed1
Update API spec
aocenas Jul 1, 2025
ea906e8
Fix test
aocenas Jul 1, 2025
634cd2a
Add test
aocenas Jul 2, 2025
b86b98d
Migrate delete folder button
aocenas Jul 3, 2025
845bbe6
useGetFolderQueryFacade
aocenas Jul 3, 2025
a873b99
Merge branch 'main' into aocenas/folders/migrate-api-2
aocenas Jul 3, 2025
e7b4f3e
Use getFolder facade hook
aocenas Jul 4, 2025
06f0baa
Recreate legacy getFolder from the APIs
aocenas Jul 9, 2025
802fa46
Merge branch 'main' into aocenas/folders/migrate-api-2
aocenas Jul 9, 2025
7b30613
Fix imports
aocenas Jul 9, 2025
bd65f06
Add comment
aocenas Jul 9, 2025
2f5b2c9
Merge branch 'main' into aocenas/folders/migrate-api-2
Clarity-89 Jul 10, 2025
c8423b4
Rename function
aocenas Jul 10, 2025
dc229e0
Simulate virtual folders in the API client
aocenas Jul 14, 2025
320f469
Translations
aocenas Jul 14, 2025
7c6dbdd
Merge branch 'main' into aocenas/folders/migrate-api-2
aocenas Jul 14, 2025
9ba694d
Update test
aocenas Jul 14, 2025
18b12c7
Move the hook out of the index file
aocenas Jul 15, 2025
8fdb275
Fix undefined in test
aocenas Jul 16, 2025
24e5b2b
Better status combining
aocenas Jul 16, 2025
152612b
Merge branch 'main' into aocenas/folders/migrate-api-2
aocenas Jul 17, 2025
c8b3dd7
Use real access api for virtual folders
aocenas Jul 18, 2025
641c091
Add basic test for the hook
aocenas Jul 18, 2025
d715ddb
Remove commented import
aocenas Jul 21, 2025
d1b2fb9
Merge branch 'main' into aocenas/folders/migrate-api-2
aocenas Jul 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Recreate legacy getFolder from the APIs
  • Loading branch information
aocenas committed Jul 9, 2025
commit 06f0baae4931b979c9d3a829be2c1374aeccce7b
9 changes: 5 additions & 4 deletions apps/folder/pkg/apis/folder/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ type FolderInfo struct {
type FolderAccessInfo struct {
metav1.TypeMeta `json:",inline"`

CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CanDelete bool `json:"canDelete"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CanDelete bool `json:"canDelete"`
AccessControl map[string]bool `json:"accessControl,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reasoning behind adding this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it is returned in the old API. The main point of this step in the migration is to use the new API but providing the same data as old one, before going on and refactoring the front end to use the more granular APIs better.

}

// +k8s:deepcopy-gen=true
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions apps/folder/pkg/apis/folder/v1beta1/zz_generated.openapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion pkg/registry/apis/folders/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,10 @@ func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.API
getter: storage[resourceInfo.StoragePath()].(rest.Getter), // Get the parents
}
storage[resourceInfo.StoragePath("counts")] = &subCountREST{searcher: b.searcher}
storage[resourceInfo.StoragePath("access")] = &subAccessREST{b.folderSvc, b.ac}
storage[resourceInfo.StoragePath("access")] = &subAccessREST{
getter: storage[resourceInfo.StoragePath()].(rest.Getter),
ac: b.ac,
}

// Adds a path to return children of a given folder
storage[resourceInfo.StoragePath("children")] = &subChildrenREST{
Expand Down
57 changes: 39 additions & 18 deletions pkg/registry/apis/folders/sub_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package folders

import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"net/http"

"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -10,14 +12,12 @@ import (
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
)

type subAccessREST struct {
service folder.Service
ac accesscontrol.AccessControl
getter rest.Getter
ac accesscontrol.AccessControl
}

var _ = rest.Connecter(&subAccessREST{})
Expand Down Expand Up @@ -47,37 +47,58 @@ func (r *subAccessREST) NewConnectOptions() (runtime.Object, bool, string) {
}

func (r *subAccessREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}

// Can view is managed here (and in the Authorizer)
f, err := r.service.Get(ctx, &folder.GetFolderQuery{
UID: &name,
OrgID: ns.OrgID,
SignedInUser: user,
})
obj, err := r.getter.Get(ctx, name, &metav1.GetOptions{})
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: This probably isn't good. I assume this skips some access validation that before happened inside the service.Get so still need to check that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok seems like the folder API authorizer is actually checking the request before it gets to the handler. So at this point it does not need additional access checks.

if err != nil {
return nil, err
}

folderObj, ok := obj.(*folders.Folder)
if !ok {
return nil, fmt.Errorf("got something else than folders.Folder")
}

folderUID := folderObj.ObjectMeta.Name

return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
access := &folders.FolderAccessInfo{}
canEditEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
canEditEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID))
access.CanEdit, _ = r.ac.Evaluate(ctx, user, canEditEvaluator)
access.CanSave = access.CanEdit
canAdminEvaluator := accesscontrol.EvalAll(
accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID)),
accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID)),
accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)),
accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)),
)
access.CanAdmin, _ = r.ac.Evaluate(ctx, user, canAdminEvaluator)
canDeleteEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
canDeleteEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID))
access.CanDelete, _ = r.ac.Evaluate(ctx, user, canDeleteEvaluator)

// Cargo culted from pkg/api/folder.go#getFolderACMetadata
allMetadata := getFolderAccessControl(ctx, r.getter, user, folderObj)
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
}
}

access.AccessControl = metadata

responder.Object(http.StatusOK, access)
}), nil
}

func getFolderAccessControl(ctx context.Context, folderGetter rest.Getter, user identity.Requester, f *folders.Folder) map[string]accesscontrol.Metadata {
parents := getFolderParents(ctx, folderGetter, f)
folderIDs := map[string]bool{f.ObjectMeta.Name: true}

for _, p := range parents.Items {
folderIDs[p.Name] = true
}
return accesscontrol.GetResourcesMetadata(ctx, user.GetPermissions(), dashboards.ScopeFoldersPrefix, folderIDs)
}
46 changes: 1 addition & 45 deletions pkg/registry/apis/folders/sub_parents.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,53 +54,9 @@ func (r *subParentsREST) Connect(ctx context.Context, name string, opts runtime.
}

return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
info := r.parents(ctx, folder)
info := getFolderParents(ctx, r.getter, folder)
// Start from the root
slices.Reverse(info.Items)
responder.Object(http.StatusOK, info)
}), nil
}

func (r *subParentsREST) parents(ctx context.Context, folder *folders.Folder) *folders.FolderInfoList {
info := &folders.FolderInfoList{
Items: []folders.FolderInfo{},
}
for folder != nil {
parent := getParent(folder)
descr := ""
if folder.Spec.Description != nil {
descr = *folder.Spec.Description
}
info.Items = append(info.Items, folders.FolderInfo{
Name: folder.Name,
Title: folder.Spec.Title,
Description: descr,
Parent: parent,
})
if parent == "" {
break
}

obj, err := r.getter.Get(ctx, parent, &metav1.GetOptions{})
if err != nil {
info.Items = append(info.Items, folders.FolderInfo{
Name: parent,
Detached: true,
Description: err.Error(),
})
break
}

parentFolder, ok := obj.(*folders.Folder)
if !ok {
info.Items = append(info.Items, folders.FolderInfo{
Name: parent,
Detached: true,
Description: fmt.Sprintf("expected folder, found: %T", obj),
})
break
}
folder = parentFolder
}
return info
}
57 changes: 57 additions & 0 deletions pkg/registry/apis/folders/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package folders

import (
"context"
"fmt"
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/registry/rest"
)

// getFolderParents gets a list of info objects for each parent of a Folder
// TODO: There are some other implementations that seem to do similar thing like pkg/services/folder/service.go#GetParents.
//
// Not sure if they should be merged somehow or not.
func getFolderParents(ctx context.Context, folderGetter rest.Getter, folder *folders.Folder) *folders.FolderInfoList {
info := &folders.FolderInfoList{
Items: []folders.FolderInfo{},
}
for folder != nil {
parent := getParent(folder)
descr := ""
if folder.Spec.Description != nil {
descr = *folder.Spec.Description
}
info.Items = append(info.Items, folders.FolderInfo{
Name: folder.Name,
Title: folder.Spec.Title,
Description: descr,
Parent: parent,
})
if parent == "" {
break
}

obj, err := folderGetter.Get(ctx, parent, &metav1.GetOptions{})
if err != nil {
info.Items = append(info.Items, folders.FolderInfo{
Name: parent,
Detached: true,
Description: err.Error(),
})
break
}

parentFolder, ok := obj.(*folders.Folder)
if !ok {
info.Items = append(info.Items, folders.FolderInfo{
Name: parent,
Detached: true,
Description: fmt.Sprintf("expected folder, found: %T", obj),
})
break
}
folder = parentFolder
}
return info
}
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,13 @@
"canDelete"
],
"properties": {
"accessControl": {
"type": "object",
"additionalProperties": {
"type": "boolean",
"default": false
}
},
"apiVersion": {
"description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
"type": "string"
Expand Down
3 changes: 3 additions & 0 deletions public/app/api/clients/folder/v1beta1/endpoints.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,9 @@ export type Status = {
};
export type Patch = object;
export type FolderAccessInfo = {
accessControl?: {
[key: string]: boolean;
};
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
apiVersion?: string;
canAdmin: boolean;
Expand Down
Loading