Zanzana: Initial dashboard search (#93093)

* Zanzana: Search in a background and compare results

* refactor

* Search with check

* instrument zanzana client

* add single_read option

* refactor

* refactor move check into separate function

* Fix tests

* refactor

* refactor getFindDashboardsFn

* add resource type to span attributes

* run ListObjects concurrently

* Use list and search in less cases

* adjust metrics buckets

* refactor: move Check and ListObjects to AccessControl implementation

* Revert "Fix tests"

This reverts commit b0c2f072a2.

* refactor: use own types for Check and ListObjects inside accesscontrol package

* Fix search scenario with low limit and empty query string

* more accurate search with checks

* revert

* fix linter

* Revert "revert"

This reverts commit ee5f14eea8.

* add search errors metric

* fix query performance under some conditions

* simplify check strategy

* fix pagination

* refactor findDashboardsZanzanaList

* Iterate over multiple pages while making check request

* refactor listUserResources

* avoid unnecessary db call

* remove unused zclient

* Add notes for SkipAccessControlFilter

* use more accurate check loop

* always use check for search with provided UIDs

* rename single_read to zanzana_only_evaluation

* refactor

* update go workspace

* fix linter

* don't use deprecated fields

* refactor

* fail if no org specified

* refactor

* initial integration tests

* Fix tests

* fix linter errors

* fix linter

* Fix tests

* review suggestions

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* fix limit

* refactor

* refactor tests

* fix db config in tests

* fix migrator (postgres)

---------

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
Alexander Zobnin
2024-10-04 12:27:10 +02:00
committed by GitHub
co-authored by Gabriel MABILLE
parent f403bc57d5
commit 5d724c2482
18 changed files with 619 additions and 29 deletions
+3 -2
View File
@@ -6,12 +6,11 @@ import (
"strconv"
"strings"
"github.com/grafana/authlib/claims"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/authn"
@@ -32,6 +31,8 @@ type AccessControl interface {
// This is useful when we don't want to reuse any pre-configured resolvers
// for a authorization call.
WithoutResolvers() AccessControl
Check(ctx context.Context, req CheckRequest) (bool, error)
ListObjects(ctx context.Context, req ListObjectsRequest) ([]string, error)
}
type Service interface {
@@ -119,26 +119,24 @@ func (a *AccessControl) evaluateZanzana(ctx context.Context, user identity.Reque
return eval.EvaluateCustom(func(action, scope string) (bool, error) {
kind, _, identifier := accesscontrol.SplitScope(scope)
key, ok := zanzana.TranslateToTuple(user.GetUID(), action, kind, identifier, user.GetOrgID())
tupleKey, ok := zanzana.TranslateToTuple(user.GetUID(), action, kind, identifier, user.GetOrgID())
if !ok {
// unsupported translation
return false, errAccessNotImplemented
}
a.log.Debug("evaluating zanzana", "user", key.User, "relation", key.Relation, "object", key.Object)
res, err := a.zclient.Check(ctx, &openfgav1.CheckRequest{
TupleKey: &openfgav1.CheckRequestTupleKey{
User: key.User,
Relation: key.Relation,
Object: key.Object,
},
a.log.Debug("evaluating zanzana", "user", tupleKey.User, "relation", tupleKey.Relation, "object", tupleKey.Object)
allowed, err := a.Check(ctx, accesscontrol.CheckRequest{
User: tupleKey.User,
Relation: tupleKey.Relation,
Object: tupleKey.Object,
})
if err != nil {
return false, err
}
return res.Allowed, nil
return allowed, nil
})
}
@@ -221,3 +219,30 @@ func (a *AccessControl) debug(ctx context.Context, ident identity.Requester, msg
a.log.FromContext(ctx).Debug(msg, "id", ident.GetID(), "orgID", ident.GetOrgID(), "permissions", eval.GoString())
}
func (a *AccessControl) Check(ctx context.Context, req accesscontrol.CheckRequest) (bool, error) {
key := &openfgav1.CheckRequestTupleKey{
User: req.User,
Relation: req.Relation,
Object: req.Object,
}
in := &openfgav1.CheckRequest{TupleKey: key}
res, err := a.zclient.Check(ctx, in)
if err != nil {
return false, err
}
return res.Allowed, err
}
func (a *AccessControl) ListObjects(ctx context.Context, req accesscontrol.ListObjectsRequest) ([]string, error) {
in := &openfgav1.ListObjectsRequest{
Type: req.Type,
User: req.User,
Relation: req.Relation,
}
res, err := a.zclient.ListObjects(ctx, in)
if err != nil {
return nil, err
}
return res.Objects, err
}
+1 -2
View File
@@ -8,11 +8,10 @@ import (
"strings"
"time"
"github.com/grafana/authlib/claims"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/db"
@@ -75,6 +75,14 @@ func (f FakeAccessControl) Evaluate(ctx context.Context, user identity.Requester
func (f FakeAccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) {
}
func (f FakeAccessControl) Check(ctx context.Context, in accesscontrol.CheckRequest) (bool, error) {
return false, nil
}
func (f FakeAccessControl) ListObjects(ctx context.Context, in accesscontrol.ListObjectsRequest) ([]string, error) {
return nil, nil
}
func (f FakeAccessControl) WithoutResolvers() accesscontrol.AccessControl {
return f
}
+12 -10
View File
@@ -94,12 +94,12 @@ func (z *ZanzanaSynchroniser) Sync(ctx context.Context) error {
func managedPermissionsCollector(store db.DB) TupleCollector {
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
const collectorID = "managed"
const query = `
query := `
SELECT u.uid as user_uid, t.uid as team_uid, p.action, p.kind, p.identifier, r.org_id
FROM permission p
INNER JOIN role r ON p.role_id = r.id
LEFT JOIN user_role ur ON r.id = ur.role_id
LEFT JOIN user u ON u.id = ur.user_id
LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ur.user_id
LEFT JOIN team_role tr ON r.id = tr.role_id
LEFT JOIN team t ON tr.team_id = t.id
LEFT JOIN builtin_role br ON r.id = br.role_id
@@ -156,11 +156,11 @@ func teamMembershipCollector(store db.DB) TupleCollector {
defer span.End()
const collectorID = "team_membership"
const query = `
query := `
SELECT t.uid as team_uid, u.uid as user_uid, tm.permission
FROM team_member tm
INNER JOIN team t ON tm.team_id = t.id
INNER JOIN user u ON tm.user_id = u.id
INNER JOIN ` + store.GetDialect().Quote("user") + ` u ON tm.user_id = u.id
`
type membership struct {
@@ -253,8 +253,10 @@ func dashboardFolderCollector(store db.DB) TupleCollector {
defer span.End()
const collectorID = "folder"
const query = `
SELECT org_id, uid, folder_uid, is_folder FROM dashboard WHERE is_folder = 0 AND folder_uid IS NOT NULL
query := `
SELECT org_id, uid, folder_uid, is_folder FROM dashboard
WHERE is_folder = ` + store.GetDialect().BooleanStr(false) + `
AND folder_uid IS NOT NULL
`
type dashboard struct {
OrgID int64 `xorm:"org_id"`
@@ -426,10 +428,10 @@ func customRolesCollector(store db.DB) TupleCollector {
func basicRoleAssignemtCollector(store db.DB) TupleCollector {
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
const collectorID = "basic_role_assignment"
const query = `
query := `
SELECT ou.org_id, u.uid as user_uid, ou.role as org_role, u.is_admin
FROM org_user ou
LEFT JOIN user u ON u.id = ou.user_id
LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ou.user_id
`
type Assignment struct {
OrgID int64 `xorm:"org_id"`
@@ -474,11 +476,11 @@ func basicRoleAssignemtCollector(store db.DB) TupleCollector {
func userRoleAssignemtCollector(store db.DB) TupleCollector {
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
const collectorID = "user_role_assignment"
const query = `
query := `
SELECT ur.org_id, u.uid AS user_uid, r.uid AS role_uid, r.name AS role_name
FROM user_role ur
LEFT JOIN role r ON r.id = ur.role_id
LEFT JOIN user u ON u.id = ur.user_id
LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ur.user_id
WHERE r.name NOT LIKE 'managed:%'
`
+8
View File
@@ -266,6 +266,14 @@ func (m *Mock) SyncUserRoles(ctx context.Context, orgID int64, cmd accesscontrol
return nil
}
func (m *Mock) Check(ctx context.Context, in accesscontrol.CheckRequest) (bool, error) {
return false, nil
}
func (m *Mock) ListObjects(ctx context.Context, in accesscontrol.ListObjectsRequest) ([]string, error) {
return nil, nil
}
// WithoutResolvers implements fullAccessControl.
func (m *Mock) WithoutResolvers() accesscontrol.AccessControl {
return m
+12
View File
@@ -585,3 +585,15 @@ type QueryWithOrg struct {
OrgId *int64 `json:"orgId"`
Global bool `json:"global"`
}
type CheckRequest struct {
User string
Relation string
Object string
}
type ListObjectsRequest struct {
Type string
Relation string
User string
}