mirror of https://github.com/grafana/grafana
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 linterpull/98957/head
parent
7480c9eb54
commit
cbb688e910
@ -1,53 +0,0 @@ |
||||
package acimpl |
||||
|
||||
import ( |
||||
"sync" |
||||
|
||||
"github.com/prometheus/client_golang/prometheus" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil" |
||||
) |
||||
|
||||
const ( |
||||
metricsSubSystem = "authz" |
||||
metricsNamespace = "grafana" |
||||
) |
||||
|
||||
type acMetrics struct { |
||||
// mAccessEngineEvaluationsSeconds is a summary for evaluating access for a specific engine (RBAC and zanzana)
|
||||
mAccessEngineEvaluationsSeconds *prometheus.HistogramVec |
||||
// mZanzanaEvaluationStatusTotal is a metric for zanzana evaluation status
|
||||
mZanzanaEvaluationStatusTotal *prometheus.CounterVec |
||||
} |
||||
|
||||
var once sync.Once |
||||
|
||||
// TODO: use prometheus.Registerer
|
||||
func initMetrics() *acMetrics { |
||||
m := &acMetrics{} |
||||
once.Do(func() { |
||||
m.mAccessEngineEvaluationsSeconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{ |
||||
Name: "engine_evaluations_seconds", |
||||
Help: "Histogram for evaluation time for the specific access control engine (RBAC and zanzana).", |
||||
Namespace: metricsNamespace, |
||||
Subsystem: metricsSubSystem, |
||||
Buckets: prometheus.ExponentialBuckets(0.00001, 4, 10), |
||||
}, |
||||
[]string{"engine"}, |
||||
) |
||||
|
||||
m.mZanzanaEvaluationStatusTotal = metricutil.NewCounterVecStartingAtZero( |
||||
prometheus.CounterOpts{ |
||||
Name: "zanzana_evaluation_status_total", |
||||
Help: "evaluation status (success or error) for zanzana", |
||||
Namespace: metricsNamespace, |
||||
Subsystem: metricsSubSystem, |
||||
}, []string{"status"}, map[string][]string{"status": {"success", "error"}}) |
||||
|
||||
prometheus.MustRegister( |
||||
m.mAccessEngineEvaluationsSeconds, |
||||
m.mZanzanaEvaluationStatusTotal, |
||||
) |
||||
}) |
||||
return m |
||||
} |
@ -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) |
||||
} |
||||
} |
Loading…
Reference in new issue