diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go index d8c1a2ec0c9..44d4ea28c4c 100644 --- a/pkg/services/authz/zanzana/server/server_check.go +++ b/pkg/services/authz/zanzana/server/server_check.go @@ -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] +} diff --git a/pkg/services/authz/zanzana/server/server_check_test.go b/pkg/services/authz/zanzana/server/server_check_test.go index 528eb483187..bd22a4d6d62 100644 --- a/pkg/services/authz/zanzana/server/server_check_test.go +++ b/pkg/services/authz/zanzana/server/server_check_test.go @@ -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") + }) } diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go index 46f7a9167ad..216e8df933e 100644 --- a/pkg/services/authz/zanzana/server/server_list.go +++ b/pkg/services/authz/zanzana/server/server_list.go @@ -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) { diff --git a/pkg/services/authz/zanzana/server/server_list_test.go b/pkg/services/authz/zanzana/server/server_list_test.go index 2979e4118d4..d105e9827d3 100644 --- a/pkg/services/authz/zanzana/server/server_list_test.go +++ b/pkg/services/authz/zanzana/server/server_list_test.go @@ -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") + }) } diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index 14e8f629729..63cf8ee2a50 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -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)