Zanzana: Fix dashboard access evaluation in folders (#114718)

* Zanzana: Fix dashboard access evaluation in folders

* add negative test

* Fix listing
This commit is contained in:
Alexander Zobnin
2025-12-04 09:36:03 +01:00
committed by GitHub
parent 1ba57a505a
commit f4fbbcc4f4
5 changed files with 86 additions and 0 deletions
@@ -12,6 +12,7 @@ import (
"go.opentelemetry.io/otel/codes"
"google.golang.org/protobuf/types/known/structpb"
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
)
@@ -148,6 +149,18 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation string, res
folderRelation = common.SubresourceRelation(relation)
)
if isFolderPermissionBasedResource(resource.GroupResource()) {
// Check if resource inherits permissions from the folder (like dashboards in a folder)
res, err := s.openfgaCheck(ctx, store, subject, relation, folderIdent, contextuals, resourceCtx)
if err != nil {
return nil, err
}
if res.GetAllowed() {
return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil
}
}
if folderIdent != "" && common.IsSubresourceRelation(folderRelation) {
// Check if subject has access as a sub resource for the folder
res, err := s.openfgaCheck(ctx, store, subject, folderRelation, folderIdent, contextuals, resourceCtx)
@@ -196,3 +209,12 @@ func (s *Server) openfgaCheck(ctx context.Context, store *storeInfo, subject, re
return res, nil
}
var folderPermissionBasedResources = map[string]bool{
// dashboard.grafana.app/dashboards
common.FormatGroupResource(dashboardV1.DashboardResourceInfo.GroupResource().Group, dashboardV1.DashboardResourceInfo.GroupResource().Resource, ""): true,
}
func isFolderPermissionBasedResource(resource string) bool {
return folderPermissionBasedResources[resource]
}
@@ -184,4 +184,36 @@ func testCheck(t *testing.T, server *Server) {
require.NoError(t, err)
assert.True(t, res.GetAllowed())
})
t.Run("user:17 should be able to view dashboards in folder 4 and all subfolders", func(t *testing.T) {
// Check for folders
res, err := server.Check(newContextWithNamespace(), newReq("user:17", utils.VerbGet, folderGroup, folderResource, "", "", "4"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(newContextWithNamespace(), newReq("user:17", utils.VerbGet, folderGroup, folderResource, "", "", "5"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
res, err = server.Check(newContextWithNamespace(), newReq("user:17", utils.VerbGet, folderGroup, folderResource, "", "", "6"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
// Check for dashboards
res, err = server.Check(newContextWithNamespace(), newReq("user:17", utils.VerbGet, dashboardGroup, dashboardResource, "", "4", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed(), "user should be able to view dashboards in folder 4")
res, err = server.Check(newContextWithNamespace(), newReq("user:17", utils.VerbGet, dashboardGroup, dashboardResource, "", "5", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed(), "user should be able to view dashboards in folder 5")
res, err = server.Check(newContextWithNamespace(), newReq("user:17", utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed(), "user should be able to view dashboards in folder 6")
res, err = server.Check(newContextWithNamespace(), newReq("user:17", utils.VerbGet, "foo.grafana.app", "bar", "", "4", "1"))
require.NoError(t, err)
assert.False(t, res.GetAllowed(), "user should not be able to view other resources in folder 4")
})
}
@@ -153,6 +153,25 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation string, reso
folders = res.GetObjects()
}
// Special case for folder permission based resources (like dashboards in a folder)
if isFolderPermissionBasedResource(resource.GroupResource()) {
res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
Type: common.TypeFolder,
Relation: relation,
User: subject,
Context: resourceCtx,
ContextualTuples: contextuals,
})
if err != nil {
return nil, err
}
folders = append(folders, res.GetObjects()...)
}
// 2. List all resource directly assigned to subject
var objects []string
if resource.IsValidRelation(relation) {
@@ -155,4 +155,15 @@ func testList(t *testing.T, server *Server) {
assert.Contains(t, res.GetItems(), "1")
})
t.Run("user:17 should be able to list all dashboards in folder 4 and all subfolders", func(t *testing.T) {
res, err := server.List(newContextWithNamespace(), newList("user:17", dashboardGroup, dashboardResource, ""))
require.NoError(t, err)
assert.Len(t, res.GetItems(), 0)
assert.Len(t, res.GetFolders(), 3)
assert.Contains(t, res.GetFolders(), "4")
assert.Contains(t, res.GetFolders(), "5")
assert.Contains(t, res.GetFolders(), "6")
})
}
@@ -57,6 +57,7 @@ func setup(t *testing.T, srv *Server) *Server {
common.NewFolderResourceTuple("user:5", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "1"),
common.NewFolderTuple("user:6", common.RelationGet, "1"),
common.NewGroupResourceTuple("user:7", common.RelationGet, folderGroup, folderResource, ""),
// folder-4 -> folder-5 -> folder-6
common.NewFolderParentTuple("5", "4"),
common.NewFolderParentTuple("6", "5"),
common.NewFolderResourceTuple("user:8", common.RelationSetEdit, dashboardGroup, dashboardResource, "", "5"),
@@ -69,6 +70,7 @@ func setup(t *testing.T, srv *Server) *Server {
common.NewTypedResourceTuple("user:14", common.RelationGet, common.TypeTeam, teamGroup, teamResource, statusSubresource, "1"),
common.NewTypedResourceTuple("user:15", common.RelationGet, common.TypeUser, userGroup, userResource, statusSubresource, "1"),
common.NewTypedResourceTuple("user:16", common.RelationGet, common.TypeServiceAccount, serviceAccountGroup, serviceAccountResource, statusSubresource, "1"),
common.NewFolderTuple("user:17", common.RelationSetView, "4"),
}
return setupOpenFGADatabase(t, srv, tuples)