Files
grafana/pkg/services/authz/zanzana/server/server_test.go
T
Jo 318a0ebb36 IAM: Authorize writes to zanzana on token permissions (#115645)
* validate writes to zanzana, not reads

* lint ignore
2025-12-31 09:15:00 +00:00

240 lines
8.1 KiB
Go

package server
import (
"context"
"testing"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
authnlib "github.com/grafana/authlib/authn"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
"github.com/grafana/grafana/pkg/services/authz/zanzana/store"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util/testutil"
)
const (
namespace = "default"
dashboardGroup = "dashboard.grafana.app"
dashboardResource = "dashboards"
folderGroup = "folder.grafana.app"
folderResource = "folders"
teamGroup = "iam.grafana.app"
teamResource = "teams"
userGroup = "iam.grafana.app"
userResource = "users"
serviceAccountGroup = "iam.grafana.app"
serviceAccountResource = "serviceaccounts"
statusSubresource = "status"
)
func setup(t *testing.T, srv *Server) *Server {
// seed tuples
tuples := []*openfgav1.TupleKey{
common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "", "1"),
common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "", "1"),
common.NewGroupResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource, ""),
common.NewGroupResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource, ""),
common.NewResourceTuple("user:3", common.RelationSetView, dashboardGroup, dashboardResource, "", "1"),
common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "1"),
common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "", "3"),
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"),
common.NewFolderResourceTuple("user:9", common.RelationCreate, dashboardGroup, dashboardResource, "", "5"),
common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "10"),
common.NewResourceTuple("user:10", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "11"),
common.NewGroupResourceTuple("user:11", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource),
common.NewFolderResourceTuple("user:12", common.RelationGet, dashboardGroup, dashboardResource, statusSubresource, "5"),
common.NewFolderResourceTuple("user:13", common.RelationGet, folderGroup, folderResource, statusSubresource, "5"),
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"),
common.NewFolderTuple("user:18", common.RelationCreate, "general"),
common.NewFolderResourceTuple("user:18", common.RelationCreate, dashboardGroup, dashboardResource, "", "general"),
}
return setupOpenFGADatabase(t, srv, tuples)
}
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestIntegrationServer(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
// Create a test-specific config to avoid migration conflicts
cfg := setting.NewCfg()
// Use a test-specific database to avoid migration conflicts
testStore := sqlstore.NewTestStore(t, sqlstore.WithCfg(cfg))
// Hack to skip these tests on mysql 5.7
if testStore.GetDialect().DriverName() == migrator.MySQL {
if supported, err := testStore.RecursiveQueriesAreSupported(); !supported || err != nil {
t.Skip("skipping integration test")
}
}
srv := setupOpenFGAServer(t, testStore, cfg)
t.Run("test check", func(t *testing.T) {
setup(t, srv)
testCheck(t, srv)
})
t.Run("test list", func(t *testing.T) {
setup(t, srv)
testList(t, srv)
})
t.Run("test list streaming", func(t *testing.T) {
setup(t, srv)
srv.cfg.UseStreamedListObjects = true
testList(t, srv)
srv.cfg.UseStreamedListObjects = false
})
t.Run("test batch check", func(t *testing.T) {
setup(t, srv)
testBatchCheck(t, srv)
})
t.Run("test mutate", func(t *testing.T) {
testMutate(t, srv)
})
t.Run("test mutate folders", func(t *testing.T) {
testMutateFolders(t, srv)
})
t.Run("test mutate resource permissions", func(t *testing.T) {
testMutateResourcePermissions(t, srv)
})
t.Run("test mutate org roles", func(t *testing.T) {
testMutateOrgRoles(t, srv)
})
t.Run("test query folders", func(t *testing.T) {
testQueryFolders(t, srv)
})
t.Run("test mutate role bindings", func(t *testing.T) {
testMutateRoleBindings(t, srv)
})
t.Run("test mutate team bindings", func(t *testing.T) {
testMutateTeamBindings(t, srv)
})
t.Run("test mutate roles", func(t *testing.T) {
testMutateRoles(t, srv)
})
}
func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
t.Helper()
store, err := store.NewEmbeddedStore(cfg, testDB, log.NewNopLogger())
require.NoError(t, err)
openfga, err := NewOpenFGAServer(cfg.ZanzanaServer, store)
require.NoError(t, err)
srv, err := NewServer(cfg.ZanzanaServer, openfga, log.NewNopLogger(), tracing.NewNoopTracerService(), prometheus.NewRegistry())
require.NoError(t, err)
return srv
}
func setupOpenFGADatabase(t *testing.T, srv *Server, tuples []*openfgav1.TupleKey) *Server {
t.Helper()
storeInf, err := srv.getStoreInfo(context.Background(), namespace)
require.NoError(t, err)
// Clean up any existing store
_, err = srv.openfga.DeleteStore(context.Background(), &openfgav1.DeleteStoreRequest{
StoreId: storeInf.ID,
})
require.NoError(t, err)
// seed tuples
writes := &openfgav1.WriteRequestWrites{
TupleKeys: tuples,
OnDuplicate: "ignore",
}
// First, try to delete any existing tuples to avoid conflicts
deletes := make([]*openfgav1.TupleKeyWithoutCondition, 0, len(writes.TupleKeys))
for _, tupleKey := range writes.TupleKeys {
deletes = append(deletes, &openfgav1.TupleKeyWithoutCondition{
User: tupleKey.User,
Relation: tupleKey.Relation,
Object: tupleKey.Object,
})
}
// Try to delete existing tuples (ignore errors if they don't exist)
_, err = srv.openfga.Write(context.Background(), &openfgav1.WriteRequest{
StoreId: storeInf.ID,
AuthorizationModelId: storeInf.ModelID,
Deletes: &openfgav1.WriteRequestDeletes{
TupleKeys: deletes,
OnMissing: "ignore",
},
})
require.NoError(t, err)
// Now write the new tuples
_, err = srv.openfga.Write(context.Background(), &openfgav1.WriteRequest{
StoreId: storeInf.ID,
AuthorizationModelId: storeInf.ModelID,
Writes: writes,
})
require.NoError(t, err)
return srv
}
func newContextWithNamespace() context.Context {
return newContextWithNamespaceAndPermissions()
}
func newContextWithNamespaceAndPermissions(perms ...string) context.Context {
ctx := context.Background()
ctx = claims.WithAuthInfo(ctx, authnlib.NewAccessTokenAuthInfo(authnlib.Claims[authnlib.AccessTokenClaims]{
Rest: authnlib.AccessTokenClaims{
Namespace: "*",
Permissions: perms,
DelegatedPermissions: perms,
},
}))
return ctx
}
func newContextWithZanzanaUpdatePermission() context.Context {
return newContextWithNamespaceAndPermissions(zanzana.TokenPermissionUpdate)
}