Zanzana: Pass parent folder for the checks in search queries (#94541)
* Pass parent folder as a contextual tuple in Check request * Search by listing folders and dashboards * skip dashboards listing if limit reached * remove unused * add some comments * only add ContextualTuples if parent provided * Remove parent relation for dashboards from schema and perform separate checks
This commit is contained in:
@@ -3,12 +3,15 @@ package acimpl
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
@@ -127,6 +130,7 @@ func (a *AccessControl) evaluateZanzana(ctx context.Context, user identity.Reque
|
||||
|
||||
a.log.Debug("evaluating zanzana", "user", tupleKey.User, "relation", tupleKey.Relation, "object", tupleKey.Object)
|
||||
allowed, err := a.Check(ctx, accesscontrol.CheckRequest{
|
||||
// Namespace: claims.OrgNamespaceFormatter(user.GetOrgID()),
|
||||
User: tupleKey.User,
|
||||
Relation: tupleKey.Relation,
|
||||
Object: tupleKey.Object,
|
||||
@@ -226,12 +230,44 @@ func (a *AccessControl) Check(ctx context.Context, req accesscontrol.CheckReques
|
||||
Relation: req.Relation,
|
||||
Object: req.Object,
|
||||
}
|
||||
in := &openfgav1.CheckRequest{TupleKey: key}
|
||||
|
||||
in := &openfgav1.CheckRequest{
|
||||
TupleKey: key,
|
||||
}
|
||||
|
||||
// Check direct access to resource first
|
||||
res, err := a.zclient.Check(ctx, in)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res.Allowed, err
|
||||
|
||||
// no need to check folder access
|
||||
if res.Allowed || req.Parent == "" {
|
||||
return res.Allowed, nil
|
||||
}
|
||||
|
||||
// Check access through the parent folder
|
||||
ns, err := claims.ParseNamespace(req.Namespace)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
folderKey := &openfgav1.CheckRequestTupleKey{
|
||||
User: req.User,
|
||||
Relation: zanzana.TranslateToFolderRelation(req.Relation, req.ObjectType),
|
||||
Object: zanzana.NewScopedTupleEntry(zanzana.TypeFolder, req.Parent, "", strconv.FormatInt(ns.OrgID, 10)),
|
||||
}
|
||||
|
||||
folderReq := &openfgav1.CheckRequest{
|
||||
TupleKey: folderKey,
|
||||
}
|
||||
|
||||
folderRes, err := a.zclient.Check(ctx, folderReq)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return folderRes.Allowed, nil
|
||||
}
|
||||
|
||||
func (a *AccessControl) ListObjects(ctx context.Context, req accesscontrol.ListObjectsRequest) ([]string, error) {
|
||||
|
||||
@@ -39,7 +39,6 @@ func NewZanzanaSynchroniser(client zanzana.Client, store db.DB, collectors ...Tu
|
||||
teamMembershipCollector(store),
|
||||
managedPermissionsCollector(store),
|
||||
folderTreeCollector(store),
|
||||
dashboardFolderCollector(store),
|
||||
basicRolesCollector(store),
|
||||
customRolesCollector(store),
|
||||
basicRoleAssignemtCollector(store),
|
||||
@@ -58,6 +57,7 @@ func NewZanzanaSynchroniser(client zanzana.Client, store db.DB, collectors ...Tu
|
||||
// Sync runs all collectors and tries to write all collected tuples.
|
||||
// It will skip over any "sync group" that has already been written.
|
||||
func (z *ZanzanaSynchroniser) Sync(ctx context.Context) error {
|
||||
z.log.Info("Starting zanzana permissions sync")
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.migrator.Sync")
|
||||
defer span.End()
|
||||
|
||||
@@ -246,47 +246,6 @@ func folderTreeCollector(store db.DB) TupleCollector {
|
||||
}
|
||||
}
|
||||
|
||||
// dashboardFolderCollector collects information about dashboards parent folders
|
||||
func dashboardFolderCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.migrator.dashboardFolderCollector")
|
||||
defer span.End()
|
||||
|
||||
const collectorID = "folder"
|
||||
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"`
|
||||
UID string `xorm:"uid"`
|
||||
ParentUID string `xorm:"folder_uid"`
|
||||
}
|
||||
|
||||
var dashboards []dashboard
|
||||
err := store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(query).Find(&dashboards)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, d := range dashboards {
|
||||
tuple := &openfgav1.TupleKey{
|
||||
User: zanzana.NewScopedTupleEntry(zanzana.TypeFolder, d.ParentUID, "", strconv.FormatInt(d.OrgID, 10)),
|
||||
Object: zanzana.NewScopedTupleEntry(zanzana.TypeDashboard, d.UID, "", strconv.FormatInt(d.OrgID, 10)),
|
||||
Relation: zanzana.RelationParent,
|
||||
}
|
||||
|
||||
tuples[collectorID] = append(tuples[collectorID], tuple)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// basicRolesCollector migrates basic roles to OpenFGA tuples
|
||||
func basicRolesCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
|
||||
@@ -589,9 +589,12 @@ type QueryWithOrg struct {
|
||||
}
|
||||
|
||||
type CheckRequest struct {
|
||||
User string
|
||||
Relation string
|
||||
Object string
|
||||
Namespace string
|
||||
User string
|
||||
Relation string
|
||||
Object string
|
||||
ObjectType string
|
||||
Parent string
|
||||
}
|
||||
|
||||
type ListObjectsRequest struct {
|
||||
|
||||
Reference in New Issue
Block a user