Zanzana: app platform style write APIs (#112812)

* refactor zanzana client instantiation

* refactor client imports

* POC write API (Mutate)

* fix linter

* delete exisitng folder parents

* refactor common functions

* minor refactor

* groupd operations by type

* atomic folder operations

* use deleteExisting for deletes

* Add tests for folders

* more tests

* resource permissions tests

* add more tests

* fix mock zanzana client

* fix linter

* fix linter

* re-use types from apps

* add some comments to the protobuf
This commit is contained in:
Alexander Zobnin
2025-10-28 11:22:13 +01:00
committed by GitHub
parent 19d88de3cf
commit 7a7fd45bdd
25 changed files with 2108 additions and 502 deletions
@@ -0,0 +1,135 @@
package server
import (
"testing"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/stretchr/testify/require"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
)
func setupMutate(t *testing.T, srv *Server) *Server {
t.Helper()
// seed tuples
tuples := []*openfgav1.TupleKey{
common.NewFolderParentTuple("11", "1"),
common.NewFolderParentTuple("12", "1"),
common.NewFolderParentTuple("111", "11"),
common.NewFolderParentTuple("112", "11"),
common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"),
common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"),
common.NewTypedResourceTuple("user:2", common.RelationGet, common.TypeFolder, folderGroup, folderResource, "", "1"),
}
return setupOpenFGADatabase(t, srv, tuples)
}
func testMutate(t *testing.T, srv *Server) {
setupMutate(t, srv)
t.Run("should perform multiple mutate operations", func(t *testing.T) {
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
Namespace: "default",
Operations: []*v1.MutateOperation{
{
Operation: &v1.MutateOperation_SetFolderParent{
SetFolderParent: &v1.SetFolderParentOperation{
Folder: "new-folder",
Parent: "1",
DeleteExisting: false,
},
},
},
{
Operation: &v1.MutateOperation_CreatePermission{
CreatePermission: &v1.CreatePermissionOperation{
Resource: &v1.Resource{
Group: "dashboard.grafana.app",
Resource: "dashboards",
Name: "foo",
},
Permission: &v1.Permission{
Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser),
Name: "bar",
Verb: common.RelationGet,
},
},
},
},
{
Operation: &v1.MutateOperation_DeletePermission{
DeletePermission: &v1.DeletePermissionOperation{
Resource: &v1.Resource{
Group: "dashboard.grafana.app",
Resource: "dashboards",
Name: "1",
},
Permission: &v1.Permission{
Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser),
Name: "1",
Verb: common.RelationUpdate,
},
},
},
},
{
Operation: &v1.MutateOperation_DeletePermission{
DeletePermission: &v1.DeletePermissionOperation{
Resource: &v1.Resource{
Group: "dashboard.grafana.app",
Resource: "dashboards",
Name: "1",
},
Permission: &v1.Permission{
Kind: string(iamv0.ResourcePermissionSpecPermissionKindUser),
Name: "1",
Verb: common.RelationGet,
},
},
},
},
},
})
require.NoError(t, err)
res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{
Namespace: "default",
TupleKey: &v1.ReadRequestTupleKey{
Object: "folder:new-folder",
Relation: "parent",
},
})
require.NoError(t, err)
require.Len(t, res.Tuples, 1)
require.Equal(t, "folder:new-folder", res.Tuples[0].Key.Object)
require.Equal(t, "parent", res.Tuples[0].Key.Relation)
require.Equal(t, "folder:1", res.Tuples[0].Key.User)
res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{
Namespace: "default",
TupleKey: &v1.ReadRequestTupleKey{
Relation: common.RelationGet,
Object: "resource:dashboard.grafana.app/dashboards/foo",
},
})
require.NoError(t, err)
require.Len(t, res.Tuples, 1)
require.Equal(t, "user:bar", res.Tuples[0].Key.User)
require.Equal(t, common.RelationGet, res.Tuples[0].Key.Relation)
require.Equal(t, "resource:dashboard.grafana.app/dashboards/foo", res.Tuples[0].Key.Object)
res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{
Namespace: "default",
TupleKey: &v1.ReadRequestTupleKey{
Relation: common.RelationGet,
Object: "resource:dashboard.grafana.app/dashboards/1",
},
})
require.NoError(t, err)
require.Len(t, res.Tuples, 0)
})
}