Zanzana: Remove usage from legacy access control (#98883)

* Zanzana: Remove usage from legacy access control

* remove unused

* remove zanzana client from services where it's not used

* remove unused metrics

* fix linter
This commit is contained in:
Alexander Zobnin
2025-01-14 10:26:15 +01:00
committed by GitHub
parent 7480c9eb54
commit cbb688e910
58 changed files with 139 additions and 790 deletions
@@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
@@ -887,7 +886,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) {
orgID := int64(1)
insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false)
ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient())
ac := acimpl.ProvideAccessControl(features)
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
fStore := folderimpl.ProvideStore(sqlStore)
folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore,
@@ -1005,7 +1004,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
orgID := int64(1)
insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false)
ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient())
ac := acimpl.ProvideAccessControl(features)
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
fStore := folderimpl.ProvideStore(sqlStore)
@@ -26,6 +26,7 @@ import (
"k8s.io/client-go/dynamic"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
@@ -37,7 +38,6 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/datasources"
@@ -83,7 +83,6 @@ type DashboardServiceImpl struct {
folderPermissions accesscontrol.FolderPermissionsService
dashboardPermissions accesscontrol.DashboardPermissionsService
ac accesscontrol.AccessControl
zclient zanzana.Client
k8sclient dashboardK8sHandler
metrics *dashboardsMetrics
}
@@ -109,7 +108,7 @@ func ProvideDashboardServiceImpl(
cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore folder.FolderStore,
features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl,
folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer, zclient zanzana.Client,
folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer,
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient,
quotaService quota.Service, orgService org.Service,
) (*DashboardServiceImpl, error) {
@@ -128,7 +127,6 @@ func ProvideDashboardServiceImpl(
folderPermissions: folderPermissionsService,
dashboardPermissions: dashboardPermissionsService,
ac: ac,
zclient: zclient,
folderStore: folderStore,
folderService: folderSvc,
orgService: orgService,
@@ -1214,19 +1212,12 @@ func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *das
ctx, span := tracer.Start(ctx, "dashboards.service.SearchDashboards")
defer span.End()
var res []dashboards.DashboardSearchProjection
var err error
if dr.features.IsEnabled(ctx, featuremgmt.FlagZanzana) {
res, err = dr.FindDashboardsZanzana(ctx, query)
} else {
res, err = dr.FindDashboards(ctx, query)
}
res, err := dr.FindDashboards(ctx, query)
if err != nil {
return nil, err
}
hits := makeQueryResult(query, res)
return hits, nil
}
@@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -884,7 +883,6 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
zanzana.NewNoopClient(),
nil,
nil,
nil,
@@ -956,7 +954,6 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
zanzana.NewNoopClient(),
nil,
nil,
nil,
@@ -987,7 +984,6 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
zanzana.NewNoopClient(),
nil,
nil,
nil,
@@ -1037,7 +1033,6 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
zanzana.NewNoopClient(),
nil,
nil,
nil,
@@ -1094,7 +1089,6 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
zanzana.NewNoopClient(),
nil,
nil,
nil,
-255
View File
@@ -1,255 +0,0 @@
package service
import (
"context"
"errors"
"time"
"github.com/prometheus/client_golang/prometheus"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/dashboards"
)
const (
defaultQueryLimit = 1000
// If search query string shorter than this value, then "List, then check" strategy will be used
listQueryLengthThreshold = 8
// If query limit set to value higher than this value, then "List, then check" strategy will be used
listQueryLimitThreshold = 50
)
type searchResult struct {
runner string
result []dashboards.DashboardSearchProjection
err error
duration time.Duration
}
func (dr *DashboardServiceImpl) FindDashboardsZanzana(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
if dr.cfg.Zanzana.ZanzanaOnlyEvaluation {
return dr.findDashboardsZanzanaOnly(ctx, *query)
}
return dr.findDashboardsZanzanaCompare(ctx, *query)
}
func (dr *DashboardServiceImpl) findDashboardsZanzanaOnly(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
timer := prometheus.NewTimer(dr.metrics.searchRequestsDuration.WithLabelValues("zanzana"))
defer timer.ObserveDuration()
return dr.findDashboardsZanzana(ctx, query)
}
func (dr *DashboardServiceImpl) findDashboardsZanzanaCompare(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
result := make(chan searchResult, 2)
go func() {
timer := prometheus.NewTimer(dr.metrics.searchRequestsDuration.WithLabelValues("zanzana"))
defer timer.ObserveDuration()
start := time.Now()
queryZanzana := query
res, err := dr.findDashboardsZanzana(ctx, queryZanzana)
result <- searchResult{"zanzana", res, err, time.Since(start)}
}()
go func() {
timer := prometheus.NewTimer(dr.metrics.searchRequestsDuration.WithLabelValues("grafana"))
defer timer.ObserveDuration()
start := time.Now()
res, err := dr.FindDashboards(ctx, &query)
result <- searchResult{"grafana", res, err, time.Since(start)}
}()
first, second := <-result, <-result
close(result)
if second.runner == "grafana" {
first, second = second, first
}
if second.err != nil {
dr.log.Error("zanzana search failed", "error", second.err)
dr.metrics.searchRequestStatusTotal.WithLabelValues("error").Inc()
} else if len(first.result) != len(second.result) {
dr.metrics.searchRequestStatusTotal.WithLabelValues("error").Inc()
dr.log.Warn(
"zanzana search result does not match grafana",
"grafana_result_len", len(first.result),
"zanana_result_len", len(second.result),
"grafana_duration", first.duration,
"zanzana_duration", second.duration,
)
} else {
dr.metrics.searchRequestStatusTotal.WithLabelValues("success").Inc()
dr.log.Debug("zanzana search is correct", "result_len", len(first.result), "grafana_duration", first.duration, "zanzana_duration", second.duration)
}
return first.result, first.err
}
type checkDashboardsFn func(context.Context, dashboards.FindPersistedDashboardsQuery, []dashboards.DashboardSearchProjection, int64) ([]dashboards.DashboardSearchProjection, error)
func (dr *DashboardServiceImpl) findDashboardsZanzana(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
if len(query.Title) <= listQueryLengthThreshold || query.Limit > listQueryLimitThreshold {
checkCompileFn, err := dr.getCheckCompileFn(ctx, query)
if err != nil {
return nil, err
}
return dr.findDashboardsZanzanaGeneric(ctx, query, checkCompileFn)
}
return dr.findDashboardsZanzanaGeneric(ctx, query, dr.checkDashboardsBatch)
}
// findDashboardsZanzanaGeneric runs search query in the database and then check if resultls
// available to user by calling provided checkFn function. It could be check-based or compile (list) - based.
func (dr *DashboardServiceImpl) findDashboardsZanzanaGeneric(ctx context.Context, query dashboards.FindPersistedDashboardsQuery, checkFn checkDashboardsFn) ([]dashboards.DashboardSearchProjection, error) {
ctx, span := tracer.Start(ctx, "dashboards.service.findDashboardsZanzanaCheck")
defer span.End()
result := make([]dashboards.DashboardSearchProjection, 0, query.Limit)
query.SkipAccessControlFilter = true
// Remember initial query limit
limit := query.Limit
// Set limit to default to prevent pagination issues
query.Limit = defaultQueryLimit
if query.Page == 0 {
query.Page = 1
}
for len(result) < int(limit) {
findRes, err := dr.FindDashboards(ctx, &query)
if err != nil {
return nil, err
}
remains := limit - int64(len(result))
res, err := checkFn(ctx, query, findRes, remains)
if err != nil {
return nil, err
}
result = append(result, res...)
query.Page++
// Stop when last page reached
if len(findRes) < defaultQueryLimit {
break
}
}
return result, nil
}
func (dr *DashboardServiceImpl) checkDashboardsBatch(ctx context.Context, query dashboards.FindPersistedDashboardsQuery, searchRes []dashboards.DashboardSearchProjection, remains int64) ([]dashboards.DashboardSearchProjection, error) {
ctx, span := tracer.Start(ctx, "dashboards.service.checkDashboardsBatch")
defer span.End()
if len(searchRes) == 0 {
return nil, nil
}
batchReqItems := make([]*authzextv1.BatchCheckItem, 0, len(searchRes))
for _, d := range searchRes {
// FIXME: support different access levels
kind := zanzana.KindDashboards
action := dashboards.ActionDashboardsRead
if d.IsFolder {
kind = zanzana.KindFolders
action = dashboards.ActionFoldersRead
}
checkReq, ok := zanzana.TranslateToCheckRequest("", action, kind, d.FolderUID, d.UID)
if !ok {
continue
}
batchReqItems = append(batchReqItems, &authzextv1.BatchCheckItem{
Verb: checkReq.Verb,
Group: checkReq.Group,
Resource: checkReq.Resource,
Name: checkReq.Name,
Folder: checkReq.Folder,
Subresource: checkReq.Subresource,
})
}
batchReq := authzextv1.BatchCheckRequest{
Namespace: query.SignedInUser.GetNamespace(),
Subject: query.SignedInUser.GetUID(),
Items: batchReqItems,
}
res, err := dr.zclient.BatchCheck(ctx, &batchReq)
if err != nil {
return nil, err
}
result := make([]dashboards.DashboardSearchProjection, 0)
for _, d := range searchRes {
if len(result) >= int(remains) {
break
}
kind := zanzana.KindDashboards
if d.IsFolder {
kind = zanzana.KindFolders
}
groupResource := zanzana.TranslateToGroupResource(kind)
if group, ok := res.Groups[groupResource]; ok {
if allowed := group.Items[d.UID]; allowed {
result = append(result, d)
}
}
}
return result, nil
}
func (dr *DashboardServiceImpl) getCheckCompileFn(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) (checkDashboardsFn, error) {
// List available folders
namespace := query.SignedInUser.GetNamespace()
req, ok := zanzana.TranslateToListRequest(namespace, dashboards.ActionFoldersRead, zanzana.KindFolders)
if !ok {
return nil, errors.New("resource type not supported")
}
folderChecker, err := dr.zclient.Compile(ctx, query.SignedInUser, *req)
if err != nil {
return nil, err
}
// List available dashboards
req, ok = zanzana.TranslateToListRequest(namespace, dashboards.ActionDashboardsRead, zanzana.KindDashboards)
if !ok {
return nil, errors.New("resource type not supported")
}
dashboardChecker, err := dr.zclient.Compile(ctx, query.SignedInUser, *req)
if err != nil {
return nil, err
}
return func(_ context.Context, _ dashboards.FindPersistedDashboardsQuery, searchRes []dashboards.DashboardSearchProjection, remains int64) ([]dashboards.DashboardSearchProjection, error) {
result := make([]dashboards.DashboardSearchProjection, 0)
for _, d := range searchRes {
if len(result) >= int(remains) {
break
}
allowed := false
if d.IsFolder {
allowed = folderChecker(namespace, d.UID, d.FolderUID)
} else {
allowed = dashboardChecker(namespace, d.UID, d.FolderUID)
}
if allowed {
result = append(result, d)
}
}
return result, nil
}, nil
}
@@ -1,155 +0,0 @@
package service
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/authz"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
func TestIntegrationDashboardServiceZanzana(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("Zanzana enabled", func(t *testing.T) {
features := featuremgmt.WithFeatures(featuremgmt.FlagZanzana)
db, cfg := db.InitTestDBWithCfg(t)
// Hack to skip these tests on mysql 5.7
if db.GetDialect().DriverName() == migrator.MySQL {
if supported, err := db.RecursiveQueriesAreSupported(); !supported || err != nil {
t.Skip("skipping integration test")
}
}
// Enable zanzana and run in embedded mode (part of grafana server)
cfg.Zanzana.ZanzanaOnlyEvaluation = true
cfg.Zanzana.Mode = setting.ZanzanaModeEmbedded
cfg.Zanzana.ConcurrentChecks = 10
_, err := cfg.Raw.Section("rbac").NewKey("resources_with_managed_permissions_on_creation", "dashboard, folder")
require.NoError(t, err)
quotaService := quotatest.New(false, nil)
tagService := tagimpl.ProvideService(db)
folderStore := folderimpl.ProvideDashboardFolderStore(db)
fStore := folderimpl.ProvideStore(db)
dashboardStore, err := database.ProvideDashboardStore(db, cfg, features, tagService)
require.NoError(t, err)
zclient, err := authz.ProvideZanzana(cfg, db, features)
require.NoError(t, err)
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zclient)
service, err := ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore,
featuremgmt.WithFeatures(),
accesscontrolmock.NewMockedPermissionsService(),
accesscontrolmock.NewMockedPermissionsService(),
ac,
foldertest.NewFakeService(),
fStore,
nil,
zclient,
nil,
nil,
nil,
quotaService,
nil,
)
require.NoError(t, err)
guardianMock := &guardian.FakeDashboardGuardian{
CanSaveValue: true,
}
guardian.MockDashboardGuardian(guardianMock)
createDashboards(t, service, 100, "test-a")
createDashboards(t, service, 100, "test-b")
folderImplStore := folderimpl.ProvideStore(db)
folderService := folderimpl.ProvideService(
folderImplStore,
ac,
bus.ProvideBus(tracing.InitializeTracerForTest()),
dashboardStore,
folderStore,
db,
featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
supportbundlestest.NewFakeBundleService(),
cfg,
nil,
tracing.InitializeTracerForTest(),
)
// Sync Grafana DB with zanzana (migrate data)
tracer := tracing.InitializeTracerForTest()
lock := serverlock.ProvideService(db, tracer)
zanzanaSyncronizer := dualwrite.NewZanzanaReconciler(cfg, zclient, db, lock, folderService)
err = zanzanaSyncronizer.ReconcileSync(context.Background())
require.NoError(t, err)
query := &dashboards.FindPersistedDashboardsQuery{
Title: "test-a",
Limit: 1000,
SignedInUser: &user.SignedInUser{
OrgID: 1,
UserID: 1,
UserUID: "test1",
Namespace: "default",
},
}
res, err := service.FindDashboardsZanzana(context.Background(), query)
require.NoError(t, err)
assert.Equal(t, 0, len(res))
})
}
func createDashboard(t *testing.T, service dashboards.DashboardService, uid, title string) {
dto := &dashboards.SaveDashboardDTO{
OrgID: 1,
// User: user,
User: &user.SignedInUser{
OrgID: 1,
UserID: 1,
},
}
dto.Dashboard = dashboards.NewDashboard(title)
dto.Dashboard.SetUID(uid)
_, err := service.SaveDashboard(context.Background(), dto, false)
require.NoError(t, err)
}
func createDashboards(t *testing.T, service dashboards.DashboardService, number int, prefix string) {
for i := 0; i < number; i++ {
title := fmt.Sprintf("%s-%d", prefix, i)
uid := fmt.Sprintf("dash-%s", title)
createDashboard(t, service, uid, title)
}
}