mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Zanzana: generic resource only (#96019)
* Remove collectors * Remove zanzana search check, we need to rewrite that part to the new schema * Only use generic resource schema and cleanup code we don't want to keep / need to re-write
This commit is contained in:
parent
9b0644e5c8
commit
f0a5b444e3
@ -31,8 +31,6 @@ 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 {
|
||||
|
@ -3,15 +3,11 @@ 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"
|
||||
@ -121,26 +117,8 @@ func (a *AccessControl) evaluateZanzana(ctx context.Context, user identity.Reque
|
||||
}
|
||||
|
||||
return eval.EvaluateCustom(func(action, scope string) (bool, error) {
|
||||
kind, _, identifier := accesscontrol.SplitScope(scope)
|
||||
tupleKey, ok := zanzana.TranslateToTuple(user.GetUID(), action, kind, identifier, user.GetOrgID())
|
||||
if !ok {
|
||||
// unsupported translation
|
||||
return false, errAccessNotImplemented
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return allowed, nil
|
||||
// FIXME: Implement using new schema / apis
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
@ -223,62 +201,3 @@ 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,
|
||||
}
|
||||
|
||||
// Check direct access to resource first
|
||||
res, err := a.zclient.CheckObject(ctx, in)
|
||||
if err != nil {
|
||||
return false, 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.CheckObject(ctx, folderReq)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return folderRes.Allowed, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -75,14 +75,6 @@ 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
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ func teamMembershipCollector(store db.DB) legacyTupleCollector {
|
||||
}
|
||||
|
||||
// folderTreeCollector collects folder tree structure and writes it as relation tuples
|
||||
func folderTreeCollector2(store db.DB) legacyTupleCollector {
|
||||
func folderTreeCollector(store db.DB) legacyTupleCollector {
|
||||
return func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) {
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.migrator.folderTreeCollector")
|
||||
defer span.End()
|
||||
@ -110,7 +110,7 @@ func folderTreeCollector2(store db.DB) legacyTupleCollector {
|
||||
// managedPermissionsCollector collects managed permissions into provided tuple map.
|
||||
// It will only store actions that are supported by our schema. Managed permissions can
|
||||
// be directly mapped to user/team/role without having to write an intermediate role.
|
||||
func managedPermissionsCollector2(store db.DB, kind string) legacyTupleCollector {
|
||||
func managedPermissionsCollector(store db.DB, kind string) legacyTupleCollector {
|
||||
return func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) {
|
||||
query := `
|
||||
SELECT u.uid as user_uid, t.uid as team_uid, p.action, p.kind, p.identifier, r.org_id
|
||||
@ -193,7 +193,7 @@ func tupleStringWithoutCondition(tuple *openfgav1.TupleKey) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func zanzanaCollector(client zanzana.Client, relations []string) zanzanaTupleCollector {
|
||||
func zanzanaCollector(relations []string) zanzanaTupleCollector {
|
||||
return func(ctx context.Context, client zanzana.Client, object string) (map[string]*openfgav1.TupleKey, error) {
|
||||
// list will use continuation token to collect all tuples for object and relation
|
||||
list := func(relation string) ([]*openfgav1.Tuple, error) {
|
||||
|
@ -2,13 +2,8 @@ package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
@ -17,11 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/accesscontrol/migrator")
|
||||
|
||||
// A TupleCollector is responsible to build and store [openfgav1.TupleKey] into provided tuple map.
|
||||
// They key used should be a unique group key for the collector so we can skip over an already synced group.
|
||||
type TupleCollector func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error
|
||||
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/accesscontrol/reconciler")
|
||||
|
||||
// ZanzanaReconciler is a component to reconcile RBAC permissions to zanzana.
|
||||
// We should rewrite the migration after we have "migrated" all possible actions
|
||||
@ -30,57 +21,39 @@ type ZanzanaReconciler struct {
|
||||
lock *serverlock.ServerLockService
|
||||
log log.Logger
|
||||
client zanzana.Client
|
||||
// collectors are one time best effort migrations that gives up on first conflict.
|
||||
// These are deprecated and everything should move be resourceReconcilers that are periodically synced
|
||||
// between grafana db and zanzana store.
|
||||
collectors []TupleCollector
|
||||
// reconcilers are migrations that tries to reconcile the state of grafana db to zanzana store.
|
||||
// These are run periodically to try to maintain a consistent state.
|
||||
reconcilers []resourceReconciler
|
||||
}
|
||||
|
||||
func NewZanzanaReconciler(client zanzana.Client, store db.DB, lock *serverlock.ServerLockService, collectors ...TupleCollector) *ZanzanaReconciler {
|
||||
// Append shared collectors that is used by both enterprise and oss
|
||||
collectors = append(
|
||||
collectors,
|
||||
managedPermissionsCollector(store),
|
||||
folderTreeCollector(store),
|
||||
basicRolesCollector(store),
|
||||
customRolesCollector(store),
|
||||
basicRoleAssignemtCollector(store),
|
||||
userRoleAssignemtCollector(store),
|
||||
teamRoleAssignemtCollector(store),
|
||||
fixedRoleTuplesCollector(store),
|
||||
)
|
||||
|
||||
func NewZanzanaReconciler(client zanzana.Client, store db.DB, lock *serverlock.ServerLockService) *ZanzanaReconciler {
|
||||
return &ZanzanaReconciler{
|
||||
client: client,
|
||||
lock: lock,
|
||||
log: log.New("zanzana.reconciler"),
|
||||
collectors: collectors,
|
||||
client: client,
|
||||
lock: lock,
|
||||
log: log.New("zanzana.reconciler"),
|
||||
reconcilers: []resourceReconciler{
|
||||
newResourceReconciler(
|
||||
"team memberships",
|
||||
teamMembershipCollector(store),
|
||||
zanzanaCollector(client, []string{zanzana.RelationTeamMember, zanzana.RelationTeamAdmin}),
|
||||
zanzanaCollector([]string{zanzana.RelationTeamMember, zanzana.RelationTeamAdmin}),
|
||||
client,
|
||||
),
|
||||
newResourceReconciler(
|
||||
"folder tree",
|
||||
folderTreeCollector2(store),
|
||||
zanzanaCollector(client, []string{zanzana.RelationParent}),
|
||||
folderTreeCollector(store),
|
||||
zanzanaCollector([]string{zanzana.RelationParent}),
|
||||
client,
|
||||
),
|
||||
newResourceReconciler(
|
||||
"managed folder permissions",
|
||||
managedPermissionsCollector2(store, zanzana.KindFolders),
|
||||
zanzanaCollector(client, zanzana.Folder2Relations),
|
||||
managedPermissionsCollector(store, zanzana.KindFolders),
|
||||
zanzanaCollector(zanzana.FolderRelations),
|
||||
client,
|
||||
),
|
||||
newResourceReconciler(
|
||||
"managed dashboard permissions",
|
||||
managedPermissionsCollector2(store, zanzana.KindDashboards),
|
||||
zanzanaCollector(client, zanzana.ResourceRelations),
|
||||
managedPermissionsCollector(store, zanzana.KindDashboards),
|
||||
zanzanaCollector(zanzana.ResourceRelations),
|
||||
client,
|
||||
),
|
||||
},
|
||||
@ -94,30 +67,6 @@ func (r *ZanzanaReconciler) Sync(ctx context.Context) error {
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.migrator.Sync")
|
||||
defer span.End()
|
||||
|
||||
tuplesMap := make(map[string][]*openfgav1.TupleKey)
|
||||
|
||||
for _, c := range r.collectors {
|
||||
if err := c(ctx, tuplesMap); err != nil {
|
||||
return fmt.Errorf("failed to collect permissions: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for key, tuples := range tuplesMap {
|
||||
if err := batch(tuples, 100, func(items []*openfgav1.TupleKey) error {
|
||||
return r.client.Write(ctx, &openfgav1.WriteRequest{
|
||||
Writes: &openfgav1.WriteRequestWrites{
|
||||
TupleKeys: items,
|
||||
},
|
||||
})
|
||||
}); err != nil {
|
||||
if strings.Contains(err.Error(), "cannot write a tuple which already exists") {
|
||||
r.log.Debug("Skipping already synced permissions", "sync_key", key)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
r.reconcile(ctx)
|
||||
|
||||
return nil
|
||||
@ -164,468 +113,3 @@ func (r *ZanzanaReconciler) reconcile(ctx context.Context) {
|
||||
run(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// managedPermissionsCollector collects managed permissions into provided tuple map.
|
||||
// It will only store actions that are supported by our schema. Managed permissions can
|
||||
// be directly mapped to user/team/role without having to write an intermediate role.
|
||||
func managedPermissionsCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
const collectorID = "managed"
|
||||
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 ` + 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
|
||||
WHERE r.name LIKE 'managed:%'
|
||||
`
|
||||
type Permission struct {
|
||||
RoleName string `xorm:"role_name"`
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
Action string `xorm:"action"`
|
||||
Kind string
|
||||
Identifier string
|
||||
UserUID string `xorm:"user_uid"`
|
||||
TeamUID string `xorm:"team_uid"`
|
||||
}
|
||||
|
||||
var permissions []Permission
|
||||
err := store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(query).Find(&permissions)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range permissions {
|
||||
var subject string
|
||||
if len(p.UserUID) > 0 {
|
||||
subject = zanzana.NewTupleEntry(zanzana.TypeUser, p.UserUID, "")
|
||||
} else if len(p.TeamUID) > 0 {
|
||||
subject = zanzana.NewTupleEntry(zanzana.TypeTeam, p.TeamUID, "member")
|
||||
} else {
|
||||
// FIXME(kalleep): Unsuported role binding (org role). We need to have basic roles in place
|
||||
continue
|
||||
}
|
||||
|
||||
tuple, ok := zanzana.TranslateToTuple(subject, p.Action, p.Kind, p.Identifier, p.OrgID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// our "sync key" is a combination of collectorID and action so we can run this
|
||||
// sync new data when more actions are supported
|
||||
key := fmt.Sprintf("%s-%s", collectorID, p.Action)
|
||||
tuples[key] = append(tuples[key], tuple)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// folderTreeCollector collects folder tree structure and writes it as relation tuples
|
||||
func folderTreeCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
ctx, span := tracer.Start(ctx, "accesscontrol.migrator.folderTreeCollector")
|
||||
defer span.End()
|
||||
|
||||
const collectorID = "folder"
|
||||
const query = `
|
||||
SELECT uid, parent_uid, org_id FROM folder
|
||||
`
|
||||
type folder struct {
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
FolderUID string `xorm:"uid"`
|
||||
ParentUID string `xorm:"parent_uid"`
|
||||
}
|
||||
|
||||
var folders []folder
|
||||
err := store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(query).Find(&folders)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range folders {
|
||||
var tuple *openfgav1.TupleKey
|
||||
if f.ParentUID != "" {
|
||||
tuple = &openfgav1.TupleKey{
|
||||
Object: zanzana.NewScopedTupleEntry(zanzana.TypeFolder, f.FolderUID, "", strconv.FormatInt(f.OrgID, 10)),
|
||||
Relation: zanzana.RelationParent,
|
||||
User: zanzana.NewScopedTupleEntry(zanzana.TypeFolder, f.ParentUID, "", strconv.FormatInt(f.OrgID, 10)),
|
||||
}
|
||||
} else {
|
||||
// Map root folders to org
|
||||
tuple = &openfgav1.TupleKey{
|
||||
Object: zanzana.NewScopedTupleEntry(zanzana.TypeFolder, f.FolderUID, "", strconv.FormatInt(f.OrgID, 10)),
|
||||
Relation: zanzana.RelationOrg,
|
||||
User: zanzana.NewTupleEntry(zanzana.TypeOrg, strconv.FormatInt(f.OrgID, 10), ""),
|
||||
}
|
||||
}
|
||||
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 {
|
||||
const collectorID = "basic_role"
|
||||
const query = `
|
||||
SELECT r.name, r.uid as role_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 builtin_role br ON r.id = br.role_id
|
||||
WHERE r.name LIKE 'basic:%'
|
||||
`
|
||||
type Permission struct {
|
||||
RoleName string `xorm:"role_name"`
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
Action string `xorm:"action"`
|
||||
Kind string
|
||||
Identifier string
|
||||
RoleUID string `xorm:"role_uid"`
|
||||
}
|
||||
|
||||
var permissions []Permission
|
||||
err := store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(query).Find(&permissions)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range permissions {
|
||||
type Org struct {
|
||||
Id int64
|
||||
Name string
|
||||
}
|
||||
var orgs []Org
|
||||
orgsQuery := "SELECT id, name FROM org"
|
||||
err := store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(orgsQuery).Find(&orgs)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Populate basic roles permissions for every org
|
||||
for _, org := range orgs {
|
||||
var subject string
|
||||
if p.RoleUID != "" {
|
||||
subject = zanzana.NewScopedTupleEntry(zanzana.TypeRole, p.RoleUID, "assignee", strconv.FormatInt(org.Id, 10))
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
var tuple *openfgav1.TupleKey
|
||||
ok := false
|
||||
if p.Identifier == "" || p.Identifier == "*" {
|
||||
tuple, ok = zanzana.TranslateToOrgTuple(subject, p.Action, org.Id)
|
||||
} else {
|
||||
tuple, ok = zanzana.TranslateToTuple(subject, p.Action, p.Kind, p.Identifier, org.Id)
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s-%s", collectorID, p.Action)
|
||||
if !slices.ContainsFunc(tuples[key], func(e *openfgav1.TupleKey) bool {
|
||||
// skip duplicated tuples
|
||||
return e.Object == tuple.Object && e.Relation == tuple.Relation && e.User == tuple.User
|
||||
}) {
|
||||
tuples[key] = append(tuples[key], tuple)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// customRolesCollector migrates custom roles to OpenFGA tuples
|
||||
func customRolesCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
const collectorID = "custom_role"
|
||||
const query = `
|
||||
SELECT r.name, r.uid as role_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 builtin_role br ON r.id = br.role_id
|
||||
WHERE r.name NOT LIKE 'basic:%'
|
||||
AND r.name NOT LIKE 'fixed:%'
|
||||
AND r.name NOT LIKE 'managed:%'
|
||||
`
|
||||
type Permission struct {
|
||||
RoleName string `xorm:"role_name"`
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
Action string `xorm:"action"`
|
||||
Kind string
|
||||
Identifier string
|
||||
RoleUID string `xorm:"role_uid"`
|
||||
}
|
||||
|
||||
var permissions []Permission
|
||||
err := store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(query).Find(&permissions)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range permissions {
|
||||
var subject string
|
||||
if p.RoleUID != "" {
|
||||
subject = zanzana.NewScopedTupleEntry(zanzana.TypeRole, p.RoleUID, "assignee", strconv.FormatInt(p.OrgID, 10))
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
var tuple *openfgav1.TupleKey
|
||||
ok := false
|
||||
if p.Identifier == "" || p.Identifier == "*" {
|
||||
tuple, ok = zanzana.TranslateToOrgTuple(subject, p.Action, p.OrgID)
|
||||
} else {
|
||||
tuple, ok = zanzana.TranslateToTuple(subject, p.Action, p.Kind, p.Identifier, p.OrgID)
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s-%s", collectorID, p.Action)
|
||||
if !slices.ContainsFunc(tuples[key], func(e *openfgav1.TupleKey) bool {
|
||||
// skip duplicated tuples
|
||||
return e.Object == tuple.Object && e.Relation == tuple.Relation && e.User == tuple.User
|
||||
}) {
|
||||
tuples[key] = append(tuples[key], tuple)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func basicRoleAssignemtCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
const collectorID = "basic_role_assignment"
|
||||
query := `
|
||||
SELECT ou.org_id, u.uid as user_uid, ou.role as org_role, u.is_admin
|
||||
FROM org_user ou
|
||||
LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ou.user_id
|
||||
`
|
||||
type Assignment struct {
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
UserUID string `xorm:"user_uid"`
|
||||
OrgRole string `xorm:"org_role"`
|
||||
IsAdmin bool `xorm:"is_admin"`
|
||||
}
|
||||
|
||||
var assignments []Assignment
|
||||
err := store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(query).Find(&assignments)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, a := range assignments {
|
||||
var subject string
|
||||
if a.UserUID != "" && a.OrgRole != "" {
|
||||
subject = zanzana.NewTupleEntry(zanzana.TypeUser, a.UserUID, "")
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
roleUID := zanzana.TranslateBasicRole(a.OrgRole)
|
||||
|
||||
tuple := &openfgav1.TupleKey{
|
||||
User: subject,
|
||||
Relation: zanzana.RelationAssignee,
|
||||
Object: zanzana.NewScopedTupleEntry(zanzana.TypeRole, roleUID, "", strconv.FormatInt(a.OrgID, 10)),
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s-%s", collectorID, zanzana.RelationAssignee)
|
||||
tuples[key] = append(tuples[key], tuple)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func userRoleAssignemtCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
const collectorID = "user_role_assignment"
|
||||
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 ` + store.GetDialect().Quote("user") + ` u ON u.id = ur.user_id
|
||||
WHERE r.name NOT LIKE 'managed:%'
|
||||
`
|
||||
|
||||
type Assignment struct {
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
UserUID string `xorm:"user_uid"`
|
||||
RoleUID string `xorm:"role_uid"`
|
||||
RoleName string `xorm:"role_name"`
|
||||
}
|
||||
|
||||
var assignments []Assignment
|
||||
err := store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(query).Find(&assignments)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, a := range assignments {
|
||||
if a.UserUID == "" || a.RoleUID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
subject := zanzana.NewTupleEntry(zanzana.TypeUser, a.UserUID, "")
|
||||
if strings.HasPrefix(a.RoleUID, "fixed_") {
|
||||
// Fixed roles are defined in shema, so they are relations itself. Assignment should look like:
|
||||
// user:<uid> fixed_folders_reader org:1
|
||||
relation := zanzana.TranslateFixedRole(a.RoleName)
|
||||
tuple := &openfgav1.TupleKey{
|
||||
User: subject,
|
||||
Relation: relation,
|
||||
Object: zanzana.NewTupleEntry(zanzana.TypeOrg, strconv.FormatInt(a.OrgID, 10), ""),
|
||||
}
|
||||
key := fmt.Sprintf("%s-%s", collectorID, relation)
|
||||
tuples[key] = append(tuples[key], tuple)
|
||||
} else {
|
||||
tuple := &openfgav1.TupleKey{
|
||||
User: subject,
|
||||
Relation: zanzana.RelationAssignee,
|
||||
Object: zanzana.NewScopedTupleEntry(zanzana.TypeRole, a.RoleUID, "", strconv.FormatInt(a.OrgID, 10)),
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s-%s", collectorID, zanzana.RelationAssignee)
|
||||
tuples[key] = append(tuples[key], tuple)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func teamRoleAssignemtCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
const collectorID = "team_role_assignment"
|
||||
const query = `
|
||||
SELECT tr.org_id, t.uid AS team_uid, r.uid AS role_uid, r.name AS role_name
|
||||
FROM team_role tr
|
||||
LEFT JOIN role r ON r.id = tr.role_id
|
||||
LEFT JOIN team t ON t.id = tr.team_id
|
||||
WHERE r.name NOT LIKE 'managed:%'
|
||||
`
|
||||
|
||||
type Assignment struct {
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
TeamUID string `xorm:"team_uid"`
|
||||
RoleUID string `xorm:"role_uid"`
|
||||
RoleName string `xorm:"role_name"`
|
||||
}
|
||||
|
||||
var assignments []Assignment
|
||||
err := store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(query).Find(&assignments)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, a := range assignments {
|
||||
if a.TeamUID == "" || a.RoleUID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
subject := zanzana.NewTupleEntry(zanzana.TypeTeam, a.TeamUID, "member")
|
||||
if strings.HasPrefix(a.RoleUID, "fixed_") {
|
||||
// Fixed roles are defined in shema, so they are relations itself. Assignment should look like:
|
||||
// team:<uid> fixed_folders_reader org:1
|
||||
relation := zanzana.TranslateFixedRole(a.RoleName)
|
||||
tuple := &openfgav1.TupleKey{
|
||||
User: subject,
|
||||
Relation: relation,
|
||||
Object: zanzana.NewTupleEntry(zanzana.TypeOrg, strconv.FormatInt(a.OrgID, 10), ""),
|
||||
}
|
||||
key := fmt.Sprintf("%s-%s", collectorID, relation)
|
||||
tuples[key] = append(tuples[key], tuple)
|
||||
} else {
|
||||
tuple := &openfgav1.TupleKey{
|
||||
User: subject,
|
||||
Relation: zanzana.RelationAssignee,
|
||||
Object: zanzana.NewScopedTupleEntry(zanzana.TypeRole, a.RoleUID, "", strconv.FormatInt(a.OrgID, 10)),
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s-%s", collectorID, zanzana.RelationAssignee)
|
||||
tuples[key] = append(tuples[key], tuple)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// fixedRoleTuplesCollector migrates fixed roles permissions that cannot be described in schema.
|
||||
// Those are permissions like general folder read and create that have specific resource id.
|
||||
func fixedRoleTuplesCollector(store db.DB) TupleCollector {
|
||||
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||
const collectorID = "fixed_role"
|
||||
type Org struct {
|
||||
Id int64
|
||||
Name string
|
||||
}
|
||||
var orgs []Org
|
||||
orgsQuery := "SELECT id, name FROM org"
|
||||
err := store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(orgsQuery).Find(&orgs)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type Assignment struct {
|
||||
RoleName string
|
||||
Action string
|
||||
Kind string
|
||||
Identifier string
|
||||
}
|
||||
|
||||
assignments := []Assignment{
|
||||
{RoleName: "fixed:dashboards:creator", Action: "folders:read", Kind: "folders", Identifier: "general"},
|
||||
{RoleName: "fixed:dashboards:creator", Action: "dashboards:create", Kind: "folders", Identifier: "general"},
|
||||
{RoleName: "fixed:folders:creator", Action: "folders:create", Kind: "folders", Identifier: "general"},
|
||||
{RoleName: "fixed:folders.general:reader", Action: "folders:read", Kind: "folders", Identifier: "general"},
|
||||
}
|
||||
|
||||
for _, a := range assignments {
|
||||
fixedRole := zanzana.TranslateFixedRole(a.RoleName)
|
||||
subject := zanzana.NewTupleEntry(zanzana.TypeRole, fixedRole, "assignee")
|
||||
|
||||
for _, org := range orgs {
|
||||
tuple, ok := zanzana.TranslateToTuple(subject, a.Action, a.Kind, a.Identifier, org.Id)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s-%s", collectorID, a.Action)
|
||||
tuples[key] = append(tuples[key], tuple)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -270,14 +270,6 @@ 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
|
||||
|
@ -593,18 +593,3 @@ type QueryWithOrg struct {
|
||||
OrgId *int64 `json:"orgId"`
|
||||
Global bool `json:"global"`
|
||||
}
|
||||
|
||||
type CheckRequest struct {
|
||||
Namespace string
|
||||
User string
|
||||
Relation string
|
||||
Object string
|
||||
ObjectType string
|
||||
Parent string
|
||||
}
|
||||
|
||||
type ListObjectsRequest struct {
|
||||
Type string
|
||||
Relation string
|
||||
User string
|
||||
}
|
||||
|
@ -20,9 +20,7 @@ type Client interface {
|
||||
authz.AccessClient
|
||||
List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error)
|
||||
|
||||
CheckObject(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error)
|
||||
Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav1.ReadResponse, error)
|
||||
ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error)
|
||||
Write(ctx context.Context, in *openfgav1.WriteRequest) error
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
|
||||
@ -142,15 +141,6 @@ func (c *Client) List(ctx context.Context, id claims.AuthInfo, req authz.ListReq
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) CheckObject(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authz.zanzana.client.CheckObject")
|
||||
defer span.End()
|
||||
|
||||
in.StoreId = c.storeID
|
||||
in.AuthorizationModelId = c.modelID
|
||||
return c.openfga.Check(ctx, in)
|
||||
}
|
||||
|
||||
func (c *Client) Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav1.ReadResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authz.zanzana.client.Read")
|
||||
defer span.End()
|
||||
@ -159,16 +149,6 @@ func (c *Client) Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav
|
||||
return c.openfga.Read(ctx, in)
|
||||
}
|
||||
|
||||
func (c *Client) ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authz.zanzana.client.ListObjects")
|
||||
span.SetAttributes(attribute.String("resource.type", in.Type))
|
||||
defer span.End()
|
||||
|
||||
in.StoreId = c.storeID
|
||||
in.AuthorizationModelId = c.modelID
|
||||
return c.openfga.ListObjects(ctx, in)
|
||||
}
|
||||
|
||||
func (c *Client) Write(ctx context.Context, in *openfgav1.WriteRequest) error {
|
||||
in.StoreId = c.storeID
|
||||
in.AuthorizationModelId = c.modelID
|
||||
|
@ -30,18 +30,10 @@ func (nc *NoopClient) List(ctx context.Context, id claims.AuthInfo, req authz.Li
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nc NoopClient) CheckObject(ctx context.Context, in *openfgav1.CheckRequest) (*openfgav1.CheckResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nc NoopClient) Read(ctx context.Context, in *openfgav1.ReadRequest) (*openfgav1.ReadResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nc NoopClient) ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest) (*openfgav1.ListObjectsResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nc NoopClient) Write(ctx context.Context, in *openfgav1.WriteRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ var typedResources = map[string]TypeInfo{
|
||||
NewNamespaceResourceIdent(
|
||||
folderalpha1.FolderResourceInfo.GroupResource().Group,
|
||||
folderalpha1.FolderResourceInfo.GroupResource().Resource,
|
||||
): {Type: "folder2"},
|
||||
): {Type: "folder"},
|
||||
}
|
||||
|
||||
func GetTypeInfo(group, resource string) (TypeInfo, bool) {
|
||||
|
@ -8,13 +8,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
resourceType = "resource"
|
||||
resourceTypeFolder = "folder2"
|
||||
namespaceType = "namespace"
|
||||
TypeResource = "resource"
|
||||
TypeFolder = "folder"
|
||||
TypeNamespace = "namespace"
|
||||
)
|
||||
|
||||
func FolderResourceRelation(relation string) string {
|
||||
return fmt.Sprintf("%s_%s", resourceType, relation)
|
||||
return fmt.Sprintf("%s_%s", TypeResource, relation)
|
||||
}
|
||||
|
||||
func NewTypedIdent(typ string, name string) string {
|
||||
@ -22,15 +22,15 @@ func NewTypedIdent(typ string, name string) string {
|
||||
}
|
||||
|
||||
func NewResourceIdent(group, resource, name string) string {
|
||||
return fmt.Sprintf("%s:%s/%s", resourceType, FormatGroupResource(group, resource), name)
|
||||
return fmt.Sprintf("%s:%s/%s", TypeResource, FormatGroupResource(group, resource), name)
|
||||
}
|
||||
|
||||
func NewFolderIdent(name string) string {
|
||||
return fmt.Sprintf("folder2:%s", name)
|
||||
return fmt.Sprintf("%s:%s", TypeFolder, name)
|
||||
}
|
||||
|
||||
func NewNamespaceResourceIdent(group, resource string) string {
|
||||
return fmt.Sprintf("%s:%s", namespaceType, FormatGroupResource(group, resource))
|
||||
return fmt.Sprintf("%s:%s", TypeNamespace, FormatGroupResource(group, resource))
|
||||
}
|
||||
|
||||
func FormatGroupResource(group, resource string) string {
|
||||
@ -46,7 +46,7 @@ func NewResourceTuple(subject, relation, group, resource, name string) *openfgav
|
||||
Name: "group_filter",
|
||||
Context: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"resource_group": structpb.NewStringValue(FormatGroupResource(group, resource)),
|
||||
"group_resource": structpb.NewStringValue(FormatGroupResource(group, resource)),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -88,7 +88,7 @@ func NewFolderParentTuple(folder, parent string) *openfgav1.TupleKey {
|
||||
}
|
||||
|
||||
func NewFolderTuple(subject, relation, name string) *openfgav1.TupleKey {
|
||||
return NewTypedTuple("folder2", subject, relation, name)
|
||||
return NewTypedTuple(TypeFolder, subject, relation, name)
|
||||
}
|
||||
|
||||
func NewTypedTuple(typ, subject, relation, name string) *openfgav1.TupleKey {
|
||||
|
@ -2,109 +2,60 @@
|
||||
|
||||
Here's some notes about [OpenFGA authorization model](https://openfga.dev/docs/modeling/getting-started) (schema) using to model access control in Grafana.
|
||||
|
||||
## Org-level permissions
|
||||
## Namespace level permissions
|
||||
|
||||
Most of the permissions are exist in org. Users, teams, dashboards, folders and other objects also related to specific org.
|
||||
A relation to a namespace object grant access to all objects of the GroupResource in the entire namespace.
|
||||
They take the form of `{ “user”: “user:1”, relation: “read”, object:”namespace:dashboard.grafana.app/dashboard” }`. This
|
||||
example would grant `user:1` access to all `dashboard.grafana.app/dashboard` in the namespace.
|
||||
|
||||
## Dashboards and folders
|
||||
## Folder level permissions
|
||||
|
||||
Folder hierarchy is stored directly in OpenFGA database. Each dashboard has parent folder and every folder could have sub-folders. Root-level folders do not have parents, but instead, they related to specific org:
|
||||
Folders have a type in our schema, this is different from most of our other resources where we use the generic type for
|
||||
them. This is because we want to store the folder tree relations.
|
||||
|
||||
```text
|
||||
type org
|
||||
relations
|
||||
define instance: [instance]
|
||||
define member: [user]
|
||||
To grant a user access to a specific folder we store `{ “user”: “user:1”, relation: “read”, object:”folder:<name>” }`
|
||||
|
||||
type folder
|
||||
relations
|
||||
define parent: [folder]
|
||||
define org: [org]
|
||||
To grant a user access to sub resources of a folder we store ``{ “user”: “user:1”, relation: “resource_read”, object:”folder:<uid>”}` with additional context.
|
||||
This context holds all GroupResources in a list e.g. `{ "group_resources": ["dashboard.grafana.app/dashboards", "alerting.grafana.app/rules" ] }`.
|
||||
|
||||
type dashboard
|
||||
relations
|
||||
define org: [org]
|
||||
define parent: [folder]
|
||||
```
|
||||
## Resource level permissions
|
||||
|
||||
Therefore, folders tree is stored as tuples like this:
|
||||
Most of our resource should use the generic resource type.
|
||||
|
||||
```text
|
||||
folder:<org_id>-<folder_uid> parent dashboard:<org_id>-<dashboard_uid>
|
||||
folder:<org_id>-<folder_uid> parent folder:<org_id>-<folder_uid>
|
||||
org:<org_id> org folder:<org_id>-<folder_uid>
|
||||
```
|
||||
To grant a user direct access to a specific resource we store `{ “user”: “user:1”, relation: “read”, object:”resource:dashboard.grafana.app/dashboard/<name>” }` with additional context.
|
||||
This context store the GroupResource. `{ "group_resource": "dashboard.grafana.app/dashboards" }`. This is required so we can filter them out for list requests.
|
||||
|
||||
## Managed permissions
|
||||
|
||||
In the RBAC model managed permissions stored as a special "managed" role permissions. OpenFGA model allows to assign permissions directly to users, so it produces following tuples:
|
||||
|
||||
```text
|
||||
user:<user_uid> read folder:<org_id>-<folder_uid>
|
||||
user:<user_uid> read folder:<folder_uid>
|
||||
```
|
||||
|
||||
It's also possible to assign permissions for team members using `#member` relation:
|
||||
|
||||
```text
|
||||
team:<team_uid>#member read folder:<org_id>-<folder_uid>
|
||||
team:<team_uid>#member read folder:<folder_uid>
|
||||
```
|
||||
|
||||
It's important to understand that folder permissions cannot be directly assigned to teams, because it's restricted by schema:
|
||||
|
||||
```text
|
||||
type folder
|
||||
relations
|
||||
define parent: [folder]
|
||||
define org: [org]
|
||||
|
||||
define read: [user, team#member, role#assignee] or read from parent or folder_read from org
|
||||
|
||||
type team
|
||||
relations
|
||||
define org: [org]
|
||||
define admin: [user]
|
||||
define member: [user] or admin
|
||||
```
|
||||
|
||||
Therefore, `team#member` can have `read` relation to folder and user will be automatically granted the same permission if it has `member` relation to specific team.
|
||||
|
||||
## Roles and role assignments
|
||||
|
||||
RBAC authorization model grants permissions to users through roles and role assignments. All permissions are linked to roles and then roles granted to users. To model this in OpenFGA, we use org-level permission and `role` type.
|
||||
RBAC authorization model grants permissions to users through roles and role assignments. All permissions are linked to roles and then roles granted to users. To model this in OpenFGA we use `role` type.
|
||||
|
||||
To understand how RBAC permissions linked to roles, let's take a look at the dashboard read permission as example:
|
||||
To understand how RBAC permissions linked to roles, let's take a look at the folder read permission as example:
|
||||
|
||||
```text
|
||||
type org
|
||||
relations
|
||||
define instance: [instance]
|
||||
define member: [user]
|
||||
|
||||
define folder_read: [role#assignee]
|
||||
|
||||
type role
|
||||
relations
|
||||
define org: [org]
|
||||
define assignee: [user, team#member, role#assignee]
|
||||
|
||||
type folder
|
||||
relations
|
||||
define parent: [folder]
|
||||
define org: [org]
|
||||
|
||||
define read: [user, team#member, role#assignee] or read from parent or folder_read from org
|
||||
define read: [user, team#member, role#assignee] or view or read from parent
|
||||
```
|
||||
|
||||
According to the schema, user can get `read` access to dashboard if it has `read` relation granted directly to the dashboard ot its parent folders, or by having `folder_read from org`. If we take a look at `folder_read` definition in the org type, we could see that this relation could be granted to `role#assignee`. So in order to allow user to read all dahboards in org, following tuples should be added:
|
||||
According to the schema, user can get `read` access to folder if it has `read` relation granted directly to the folder or its parent folders.
|
||||
|
||||
```text
|
||||
role:<org_id>-<role_uid>#assignee folder_read org:<org_uid>
|
||||
user:<user_uid> assignee role:<role_uid>
|
||||
```
|
||||
|
||||
In case of `Admin` basic role, it will be looking like:
|
||||
|
||||
```text
|
||||
role:1-basic_admin#assignee folder_read org:1
|
||||
user:admin assignee role:1-basic_admin
|
||||
```
|
||||
|
@ -1,39 +0,0 @@
|
||||
module core
|
||||
|
||||
type instance
|
||||
|
||||
type user
|
||||
|
||||
type org
|
||||
relations
|
||||
define instance: [instance]
|
||||
define member: [user]
|
||||
|
||||
# team management
|
||||
define team_create: [role#assignee]
|
||||
define team_read: [role#assignee]
|
||||
define team_write: [role#assignee] or team_create
|
||||
define team_delete: [role#assignee] or team_write
|
||||
define team_permissions_write: [role#assignee]
|
||||
define team_permissions_read: [role#assignee] or team_permissions_write
|
||||
|
||||
type role
|
||||
relations
|
||||
define org: [org]
|
||||
define instance: [instance]
|
||||
define assignee: [user, team#member, role#assignee]
|
||||
|
||||
type team
|
||||
relations
|
||||
define org: [org]
|
||||
|
||||
# Action sets
|
||||
define admin: [user]
|
||||
define member: [user] or admin
|
||||
|
||||
define read: [role#assignee] or member or team_read from org
|
||||
define write: [role#assignee] or admin or team_write from org
|
||||
define delete: [role#assignee] or admin or team_delete from org
|
||||
define permissions_read: [role#assignee] or admin or team_permissions_read from org
|
||||
define permissions_write: [role#assignee] or admin or team_permissions_write from org
|
||||
|
@ -1,48 +0,0 @@
|
||||
module dashboard
|
||||
|
||||
extend type org
|
||||
relations
|
||||
|
||||
define dashboard_create: [role#assignee] or fixed_folders_writer or fixed_dashboards_creator
|
||||
define dashboard_read: [role#assignee] or fixed_folders_reader or fixed_folders_writer or fixed_dashboards_reader
|
||||
define dashboard_write: [role#assignee] or fixed_folders_writer or fixed_dashboards_writer
|
||||
define dashboard_delete: [role#assignee] or fixed_folders_writer or fixed_dashboards_writer
|
||||
define dashboard_permissions_read: [role#assignee] or fixed_folders_writer or fixed_dashboards_permissions_reader
|
||||
define dashboard_permissions_write: [role#assignee] or fixed_folders_writer or fixed_dashboards_permissions_writer
|
||||
define dashboard_public_write: [role#assignee] or dashboard_write or fixed_dashboards_public_writer
|
||||
|
||||
define dashboard_annotations_create: [role#assignee]
|
||||
define dashboard_annotations_read: [role#assignee]
|
||||
define dashboard_annotations_write: [role#assignee]
|
||||
define dashboard_annotations_delete: [role#assignee]
|
||||
|
||||
# Fixed roles
|
||||
define fixed_dashboards_creator: [user, team#member, role#assignee] or fixed_dashboards_writer
|
||||
define fixed_dashboards_reader: [user, team#member, role#assignee] or fixed_dashboards_writer
|
||||
define fixed_dashboards_writer: [user, team#member, role#assignee]
|
||||
define fixed_dashboards_insights_reader: [user, team#member, role#assignee]
|
||||
define fixed_dashboards_public_writer: [user, team#member, role#assignee]
|
||||
define fixed_dashboards_permissions_reader: [user, team#member, role#assignee] or fixed_dashboards_permissions_writer
|
||||
define fixed_dashboards_permissions_writer: [user, team#member, role#assignee]
|
||||
|
||||
type dashboard
|
||||
relations
|
||||
define org: [org]
|
||||
|
||||
# Action sets
|
||||
define view: [user, team#member, role#assignee]
|
||||
define edit: [user, team#member, role#assignee]
|
||||
define admin: [user, team#member, role#assignee]
|
||||
|
||||
define create: [user, team#member, role#assignee] or dashboard_create from org
|
||||
define read: [user, team#member, role#assignee] or view or edit or admin or dashboard_read from org
|
||||
define write: [user, team#member, role#assignee] or edit or admin or dashboard_write from org
|
||||
define delete: [user, team#member, role#assignee] or edit or admin or dashboard_delete from org
|
||||
define permissions_read: [user, team#member, role#assignee] or admin or dashboard_permissions_read from org
|
||||
define permissions_write: [user, team#member, role#assignee] or admin or dashboard_permissions_write from org
|
||||
|
||||
define public_write: [user, team#member, role#assignee] or dashboard_public_write from org or write
|
||||
define annotations_create: [user, team#member, role#assignee] or dashboard_annotations_create from org
|
||||
define annotations_read: [user, team#member, role#assignee] or dashboard_annotations_read from org
|
||||
define annotations_write: [user, team#member, role#assignee] or dashboard_annotations_write from org
|
||||
define annotations_delete: [user, team#member, role#assignee] or dashboard_annotations_delete from org
|
@ -1,76 +0,0 @@
|
||||
module folder
|
||||
|
||||
extend type org
|
||||
relations
|
||||
define folder_create: [role#assignee] or fixed_folders_creator
|
||||
define folder_read: [role#assignee] or folder_delete or fixed_folders_reader
|
||||
define folder_write: [role#assignee] or folder_create or fixed_folders_writer
|
||||
define folder_delete: [role#assignee] or folder_write or fixed_folders_writer
|
||||
define folder_permissions_read: [role#assignee] or folder_permissions_write or fixed_folders_permissions_reader
|
||||
define folder_permissions_write: [role#assignee] or fixed_folders_permissions_writer
|
||||
|
||||
define library_panel_create: [role#assignee]
|
||||
define library_panel_read: [role#assignee] or library_panel_write
|
||||
define library_panel_write: [role#assignee] or library_panel_create
|
||||
define library_panel_delete: [role#assignee] or library_panel_create
|
||||
|
||||
define alert_rule_create: [role#assignee]
|
||||
define alert_rule_read: [role#assignee] or alert_rule_write
|
||||
define alert_rule_write: [role#assignee] or alert_rule_create
|
||||
define alert_rule_delete: [role#assignee] or alert_rule_write
|
||||
define alert_silence_create: [role#assignee]
|
||||
define alert_silence_delete: [role#assignee] or alert_silence_write
|
||||
define alert_silence_read: [role#assignee] or alert_silence_write
|
||||
define alert_silence_write: [role#assignee] or alert_silence_create
|
||||
|
||||
# Fixed roles
|
||||
define fixed_folders_creator: [user, team#member, role#assignee] or fixed_folders_writer
|
||||
define fixed_folders_reader: [user, team#member, role#assignee] or fixed_folders_writer
|
||||
define fixed_folders_writer: [user, team#member, role#assignee]
|
||||
define fixed_folders_permissions_reader: [user, team#member, role#assignee] or fixed_folders_permissions_writer
|
||||
define fixed_folders_permissions_writer: [user, team#member, role#assignee]
|
||||
# Read General (root) folder only
|
||||
define fixed_folders_general_reader: [user, team#member, role#assignee]
|
||||
|
||||
type folder
|
||||
relations
|
||||
define parent: [folder]
|
||||
define org: [org]
|
||||
|
||||
# Action sets
|
||||
define view: [user, team#member, role#assignee]
|
||||
define edit: [user, team#member, role#assignee]
|
||||
define admin: [user, team#member, role#assignee]
|
||||
|
||||
define create: [user, team#member, role#assignee] or edit or admin or create from parent or folder_create from org
|
||||
define read: [user, team#member, role#assignee] or view or edit or admin or read from parent or folder_read from org
|
||||
define write: [user, team#member, role#assignee] or edit or admin or write from parent or folder_write from org
|
||||
define delete: [user, team#member, role#assignee] or edit or admin or delete from parent or folder_delete from org
|
||||
define permissions_read: [user, team#member, role#assignee] or admin or permissions_read from parent or folder_permissions_read from org
|
||||
define permissions_write: [user, team#member, role#assignee] or admin or permissions_write from parent or folder_permissions_write from org
|
||||
|
||||
define dashboard_create: [user, team#member, role#assignee] or edit or admin or dashboard_create from parent or dashboard_create from org
|
||||
define dashboard_read: [user, team#member, role#assignee] or view or edit or admin or dashboard_read from parent or dashboard_read from org
|
||||
define dashboard_write: [user, team#member, role#assignee] or edit or admin or dashboard_write from parent or dashboard_write from org
|
||||
define dashboard_delete: [user, team#member, role#assignee] or edit or admin or dashboard_delete from parent or dashboard_delete from org
|
||||
define dashboard_permissions_read: [user, team#member, role#assignee] or admin or dashboard_permissions_read from parent or dashboard_permissions_read from org
|
||||
define dashboard_permissions_write: [user, team#member, role#assignee] or admin or dashboard_permissions_write from parent or dashboard_permissions_write from org
|
||||
define dashboard_public_write: [user, team#member, role#assignee] or dashboard_public_write from parent or dashboard_public_write from org or dashboard_write
|
||||
define dashboard_annotations_create: [user, team#member, role#assignee] or dashboard_annotations_create from parent or dashboard_annotations_create from org
|
||||
define dashboard_annotations_read: [user, team#member, role#assignee] or dashboard_annotations_read from parent or dashboard_annotations_read from org
|
||||
define dashboard_annotations_write: [user, team#member, role#assignee] or dashboard_annotations_write from parent or dashboard_annotations_write from org
|
||||
define dashboard_annotations_delete: [user, team#member, role#assignee] or dashboard_annotations_delete from parent or dashboard_annotations_delete from org
|
||||
|
||||
define library_panel_create: [user, team#member, role#assignee] or edit or admin or library_panel_create from parent or library_panel_create from org
|
||||
define library_panel_read: [user, team#member, role#assignee] or view or edit or admin or library_panel_read from parent or library_panel_read from org or library_panel_write
|
||||
define library_panel_write: [user, team#member, role#assignee] or edit or admin or library_panel_write from parent or library_panel_write from org or library_panel_create
|
||||
define library_panel_delete: [user, team#member, role#assignee] or edit or admin or library_panel_delete from parent or library_panel_delete from org or library_panel_create
|
||||
|
||||
define alert_rule_create: [user, team#member, role#assignee] or edit or admin or alert_rule_create from parent or alert_rule_create from org
|
||||
define alert_rule_read: [user, team#member, role#assignee] or view or edit or admin or alert_rule_read from parent or alert_rule_read from org or alert_rule_write
|
||||
define alert_rule_write: [user, team#member, role#assignee] or edit or admin or alert_rule_write from parent or alert_rule_write from org or alert_rule_create
|
||||
define alert_rule_delete: [user, team#member, role#assignee] or edit or admin or alert_rule_delete from parent or alert_rule_delete from org or alert_rule_write
|
||||
define alert_silence_create: [user, team#member, role#assignee] or edit or admin or alert_silence_create from parent or alert_silence_create from org
|
||||
define alert_silence_read: [user, team#member, role#assignee] or view or edit or admin or alert_silence_read from parent or alert_silence_read from org or alert_silence_write
|
||||
define alert_silence_write: [user, team#member, role#assignee] or edit or admin or alert_silence_write from parent or alert_silence_write from org or alert_silence_create
|
||||
|
@ -7,31 +7,25 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed core.fga
|
||||
//go:embed schema_core.fga
|
||||
coreDSL string
|
||||
//go:embed dashboard.fga
|
||||
dashboardDSL string
|
||||
//go:embed folder.fga
|
||||
//go:embed schema_folder.fga
|
||||
folderDSL string
|
||||
//go:embed resource.fga
|
||||
//go:embed schema_resource.fga
|
||||
resourceDSL string
|
||||
)
|
||||
|
||||
var SchemaModules = []transformer.ModuleFile{
|
||||
{
|
||||
Name: "core.fga",
|
||||
Name: "schema_core.fga",
|
||||
Contents: coreDSL,
|
||||
},
|
||||
{
|
||||
Name: "dashboard.fga",
|
||||
Contents: dashboardDSL,
|
||||
},
|
||||
{
|
||||
Name: "folder.fga",
|
||||
Name: "schema_folder.fga",
|
||||
Contents: folderDSL,
|
||||
},
|
||||
{
|
||||
Name: "resource.fga",
|
||||
Name: "schema_resource.fga",
|
||||
Contents: resourceDSL,
|
||||
},
|
||||
}
|
||||
|
32
pkg/services/authz/zanzana/schema/schema_core.fga
Normal file
32
pkg/services/authz/zanzana/schema/schema_core.fga
Normal file
@ -0,0 +1,32 @@
|
||||
module core
|
||||
|
||||
type namespace
|
||||
relations
|
||||
define view: [user, team#member, role#assignee] or edit
|
||||
define edit: [user, team#member, role#assignee] or admin
|
||||
define admin: [user, team#member, role#assignee]
|
||||
|
||||
define read: [user, team#member, role#assignee] or view
|
||||
define create: [user, team#member, role#assignee] or edit
|
||||
define write: [user, team#member, role#assignee] or edit
|
||||
define delete: [user, team#member, role#assignee] or edit
|
||||
define permissions_read: [user, team#member, role#assignee] or admin
|
||||
define permissions_write: [user, team#member, role#assignee] or admin
|
||||
|
||||
type user
|
||||
|
||||
type role
|
||||
relations
|
||||
define assignee: [user, team#member, role#assignee]
|
||||
|
||||
type team
|
||||
relations
|
||||
# Action sets
|
||||
define admin: [user]
|
||||
define member: [user] or admin
|
||||
|
||||
define read: [role#assignee] or member
|
||||
define write: [role#assignee] or admin
|
||||
define delete: [role#assignee] or admin
|
||||
define permissions_read: [role#assignee] or admin
|
||||
define permissions_write: [role#assignee] or admin
|
21
pkg/services/authz/zanzana/schema/schema_folder.fga
Normal file
21
pkg/services/authz/zanzana/schema/schema_folder.fga
Normal file
@ -0,0 +1,21 @@
|
||||
module folder
|
||||
|
||||
type folder
|
||||
relations
|
||||
define parent: [folder]
|
||||
|
||||
# Action sets
|
||||
define view: [user, team#member, role#assignee] or edit or view from parent
|
||||
define edit: [user, team#member, role#assignee] or admin or edit from parent
|
||||
define admin: [user, team#member, role#assignee] or admin from parent
|
||||
|
||||
define read: [user, team#member, role#assignee] or view or read from parent
|
||||
define create: [user, team#member, role#assignee] or edit or create from parent
|
||||
define write: [user, team#member, role#assignee] or edit or write from parent
|
||||
define delete: [user, team#member, role#assignee] or edit or delete from parent
|
||||
define permissions_read: [user, team#member, role#assignee] or admin or permissions_read from parent
|
||||
define permissions_write: [user, team#member, role#assignee] or admin or permissions_write from parent
|
||||
|
||||
|
||||
|
||||
|
@ -1,34 +1,7 @@
|
||||
module resource
|
||||
|
||||
type namespace
|
||||
extend type folder
|
||||
relations
|
||||
define view: [user, team#member, role#assignee] or edit
|
||||
define edit: [user, team#member, role#assignee] or admin
|
||||
define admin: [user, team#member, role#assignee]
|
||||
|
||||
define read: [user, team#member, role#assignee] or view
|
||||
define create: [user, team#member, role#assignee] or edit
|
||||
define write: [user, team#member, role#assignee] or edit
|
||||
define delete: [user, team#member, role#assignee] or edit
|
||||
define permissions_read: [user, team#member, role#assignee] or admin
|
||||
define permissions_write: [user, team#member, role#assignee] or admin
|
||||
|
||||
type folder2
|
||||
relations
|
||||
define parent: [folder2]
|
||||
|
||||
# Action sets
|
||||
define view: [user, team#member, role#assignee] or edit or view from parent
|
||||
define edit: [user, team#member, role#assignee] or admin or edit from parent
|
||||
define admin: [user, team#member, role#assignee] or admin from parent
|
||||
|
||||
define read: [user, team#member, role#assignee] or view or read from parent
|
||||
define create: [user, team#member, role#assignee] or edit or create from parent
|
||||
define write: [user, team#member, role#assignee] or edit or write from parent
|
||||
define delete: [user, team#member, role#assignee] or edit or delete from parent
|
||||
define permissions_read: [user, team#member, role#assignee] or admin or permissions_read from parent
|
||||
define permissions_write: [user, team#member, role#assignee] or admin or permissions_write from parent
|
||||
|
||||
define resource_view: [user with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_view from parent
|
||||
define resource_edit: [user with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_edit from parent
|
||||
define resource_admin: [user with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin from parent
|
||||
@ -40,6 +13,7 @@ type folder2
|
||||
define resource_permissions_read: [user with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_permissions_read from parent
|
||||
define resource_permissions_write: [user with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_permissions_write from parent
|
||||
|
||||
|
||||
type resource
|
||||
relations
|
||||
define view: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
|
||||
@ -54,8 +28,8 @@ type resource
|
||||
define permissions_write: [user with group_filter, team#member with group_filter, role#assignee with group_filter] or admin
|
||||
|
||||
|
||||
condition group_filter(requested_group: string, resource_group: string) {
|
||||
resource_group == requested_group
|
||||
condition group_filter(requested_group: string, group_resource: string) {
|
||||
requested_group == group_resource
|
||||
}
|
||||
|
||||
condition folder_group_filter(requested_group: string, group_resources: list<string>) {
|
@ -19,7 +19,7 @@ import (
|
||||
const (
|
||||
resourceType = "resource"
|
||||
namespaceType = "namespace"
|
||||
folderTypePrefix = "folder2:"
|
||||
folderTypePrefix = "folder:"
|
||||
)
|
||||
|
||||
var _ authzv1.AuthzServiceServer = (*Server)(nil)
|
||||
|
@ -85,7 +85,7 @@ func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*a
|
||||
folders, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{
|
||||
StoreId: s.storeID,
|
||||
AuthorizationModelId: s.modelID,
|
||||
Type: "folder2",
|
||||
Type: common.TypeFolder,
|
||||
Relation: common.FolderResourceRelation(relation),
|
||||
User: r.GetSubject(),
|
||||
Context: &structpb.Struct{
|
||||
@ -102,7 +102,7 @@ func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*a
|
||||
direct, err := s.openfga.ListObjects(ctx, &openfgav1.ListObjectsRequest{
|
||||
StoreId: s.storeID,
|
||||
AuthorizationModelId: s.modelID,
|
||||
Type: "resource",
|
||||
Type: common.TypeResource,
|
||||
Relation: relation,
|
||||
User: r.GetSubject(),
|
||||
Context: &structpb.Struct{
|
||||
@ -116,7 +116,7 @@ func (s *Server) listGeneric(ctx context.Context, r *authzextv1.ListRequest) (*a
|
||||
}
|
||||
|
||||
return &authzextv1.ListResponse{
|
||||
Folders: folderObject(r.GetGroup(), r.GetResource(), folders.GetObjects()),
|
||||
Folders: folderObject(folders.GetObjects()),
|
||||
Items: directObjects(r.GetGroup(), r.GetResource(), direct.GetObjects()),
|
||||
}, nil
|
||||
}
|
||||
@ -137,7 +137,7 @@ func directObjects(group, resource string, objects []string) []string {
|
||||
return objects
|
||||
}
|
||||
|
||||
func folderObject(group, resource string, objects []string) []string {
|
||||
func folderObject(objects []string) []string {
|
||||
for i := range objects {
|
||||
objects[i] = strings.TrimPrefix(objects[i], folderTypePrefix)
|
||||
}
|
||||
|
@ -5,109 +5,6 @@ import (
|
||||
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
)
|
||||
|
||||
type actionKindTranslation struct {
|
||||
objectType string
|
||||
orgScoped bool
|
||||
translations map[string]string
|
||||
}
|
||||
|
||||
// rbac action to relation translation
|
||||
var folderActions = map[string]string{
|
||||
"folders:create": "create",
|
||||
"folders:read": "read",
|
||||
"folders:write": "write",
|
||||
"folders:delete": "delete",
|
||||
"folders.permissions:read": "permissions_read",
|
||||
"folders.permissions:write": "permissions_write",
|
||||
|
||||
"dashboards:create": "dashboard_create",
|
||||
"dashboards:read": "dashboard_read",
|
||||
"dashboards:write": "dashboard_write",
|
||||
"dashboards:delete": "dashboard_delete",
|
||||
"dashboards.permissions:read": "dashboard_permissions_read",
|
||||
"dashboards.permissions:write": "dashboard_permissions_write",
|
||||
|
||||
"library.panels:create": "library_panel_create",
|
||||
"library.panels:read": "library_panel_read",
|
||||
"library.panels:write": "library_panel_write",
|
||||
"library.panels:delete": "library_panel_delete",
|
||||
|
||||
"alert.rules:create": "alert_rule_create",
|
||||
"alert.rules:read": "alert_rule_read",
|
||||
"alert.rules:write": "alert_rule_write",
|
||||
"alert.rules:delete": "alert_rule_delete",
|
||||
|
||||
"alert.silences:create": "alert_silence_create",
|
||||
"alert.silences:read": "alert_silence_read",
|
||||
"alert.silences:write": "alert_silence_write",
|
||||
}
|
||||
|
||||
var dashboardActions = map[string]string{
|
||||
"dashboards:create": "create",
|
||||
"dashboards:read": "read",
|
||||
"dashboards:write": "write",
|
||||
"dashboards:delete": "delete",
|
||||
"dashboards.permissions:read": "permissions_read",
|
||||
"dashboards.permissions:write": "permissions_write",
|
||||
}
|
||||
|
||||
var orgActions = map[string]string{
|
||||
"folders:create": "folder_create",
|
||||
"folders:read": "folder_read",
|
||||
"folders:write": "folder_write",
|
||||
"folders:delete": "folder_delete",
|
||||
"folders.permissions:read": "folder_permissions_read",
|
||||
"folders.permissions:write": "folder_permissions_write",
|
||||
|
||||
"dashboards:create": "dashboard_create",
|
||||
"dashboards:read": "dashboard_read",
|
||||
"dashboards:write": "dashboard_write",
|
||||
"dashboards:delete": "dashboard_delete",
|
||||
"dashboards.permissions:read": "dashboard_permissions_read",
|
||||
"dashboards.permissions:write": "dashboard_permissions_write",
|
||||
|
||||
"library.panels:create": "library_panel_create",
|
||||
"library.panels:read": "library_panel_read",
|
||||
"library.panels:write": "library_panel_write",
|
||||
"library.panels:delete": "library_panel_delete",
|
||||
|
||||
"alert.rules:create": "alert_rule_create",
|
||||
"alert.rules:read": "alert_rule_read",
|
||||
"alert.rules:write": "alert_rule_write",
|
||||
"alert.rules:delete": "alert_rule_delete",
|
||||
|
||||
"alert.silences:create": "alert_silence_create",
|
||||
"alert.silences:read": "alert_silence_read",
|
||||
"alert.silences:write": "alert_silence_write",
|
||||
}
|
||||
|
||||
// RBAC to OpenFGA translations grouped by kind
|
||||
var actionKindTranslations = map[string]actionKindTranslation{
|
||||
KindOrg: {
|
||||
objectType: TypeOrg,
|
||||
orgScoped: false,
|
||||
translations: orgActions,
|
||||
},
|
||||
KindFolders: {
|
||||
objectType: TypeFolder,
|
||||
orgScoped: true,
|
||||
translations: folderActions,
|
||||
},
|
||||
KindDashboards: {
|
||||
objectType: TypeDashboard,
|
||||
orgScoped: true,
|
||||
translations: dashboardActions,
|
||||
},
|
||||
}
|
||||
|
||||
var basicRolesTranslations = map[string]string{
|
||||
RoleGrafanaAdmin: "basic_grafana_admin",
|
||||
RoleAdmin: "basic_admin",
|
||||
RoleEditor: "basic_editor",
|
||||
RoleViewer: "basic_viewer",
|
||||
RoleNone: "basic_none",
|
||||
}
|
||||
|
||||
type resourceTranslation struct {
|
||||
typ string
|
||||
group string
|
||||
@ -139,7 +36,7 @@ var (
|
||||
|
||||
var resourceTranslations = map[string]resourceTranslation{
|
||||
KindFolders: {
|
||||
typ: TypeFolder2,
|
||||
typ: TypeFolder,
|
||||
group: folderGroup,
|
||||
resource: folderResource,
|
||||
mapping: map[string]actionMappig{
|
||||
|
@ -2,7 +2,6 @@ package zanzana
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
@ -10,14 +9,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
TypeUser string = "user"
|
||||
TypeTeam string = "team"
|
||||
TypeRole string = "role"
|
||||
TypeFolder string = "folder"
|
||||
TypeFolder2 string = "folder2"
|
||||
TypeDashboard string = "dashboard"
|
||||
TypeOrg string = "org"
|
||||
TypeResource string = "resource"
|
||||
TypeUser string = "user"
|
||||
TypeTeam string = "team"
|
||||
TypeRole string = "role"
|
||||
TypeFolder string = "folder"
|
||||
TypeResource string = "resource"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -46,10 +42,9 @@ const (
|
||||
)
|
||||
|
||||
var ResourceRelations = []string{RelationRead, RelationWrite, RelationCreate, RelationDelete, RelationPermissionsRead, RelationPermissionsWrite}
|
||||
var Folder2Relations = append(ResourceRelations, FolderResourceRelationRead, FolderResourceRelationWrite, FolderResourceRelationCreate, FolderResourceRelationDelete, FolderResourceRelationPermissionsRead, FolderResourceRelationPermissionsWrite)
|
||||
var FolderRelations = append(ResourceRelations, FolderResourceRelationRead, FolderResourceRelationWrite, FolderResourceRelationCreate, FolderResourceRelationDelete, FolderResourceRelationPermissionsRead, FolderResourceRelationPermissionsWrite)
|
||||
|
||||
const (
|
||||
KindOrg string = "org"
|
||||
KindDashboards string = "dashboards"
|
||||
KindFolders string = "folders"
|
||||
)
|
||||
@ -78,40 +73,6 @@ func NewTupleEntry(objectType, id, relation string) string {
|
||||
return obj
|
||||
}
|
||||
|
||||
// NewScopedTupleEntry constructs new openfga entry type:id[#relation]
|
||||
// with id prefixed by scope (usually org id)
|
||||
func NewScopedTupleEntry(objectType, id, relation, scope string) string {
|
||||
return NewTupleEntry(objectType, fmt.Sprintf("%s-%s", scope, id), relation)
|
||||
}
|
||||
|
||||
func TranslateToTuple(user string, action, kind, identifier string, orgID int64) (*openfgav1.TupleKey, bool) {
|
||||
typeTranslation, ok := actionKindTranslations[kind]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
relation, ok := typeTranslation.translations[action]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
tuple := &openfgav1.TupleKey{
|
||||
Relation: relation,
|
||||
}
|
||||
|
||||
tuple.User = user
|
||||
tuple.Relation = relation
|
||||
|
||||
// Some uid:s in grafana are not guarantee to be unique across orgs so we need to scope them.
|
||||
if typeTranslation.orgScoped {
|
||||
tuple.Object = NewScopedTupleEntry(typeTranslation.objectType, identifier, "", strconv.FormatInt(orgID, 10))
|
||||
} else {
|
||||
tuple.Object = NewTupleEntry(typeTranslation.objectType, identifier, "")
|
||||
}
|
||||
|
||||
return tuple, true
|
||||
}
|
||||
|
||||
func TranslateToResourceTuple(subject string, action, kind, name string) (*openfgav1.TupleKey, bool) {
|
||||
translation, ok := resourceTranslations[kind]
|
||||
|
||||
@ -128,7 +89,7 @@ func TranslateToResourceTuple(subject string, action, kind, name string) (*openf
|
||||
return common.NewResourceTuple(subject, m.relation, translation.group, translation.resource, name), true
|
||||
}
|
||||
|
||||
if translation.typ == TypeFolder2 {
|
||||
if translation.typ == TypeFolder {
|
||||
if m.group != "" && m.resource != "" {
|
||||
return common.NewFolderResourceTuple(subject, m.relation, m.group, m.resource, name), true
|
||||
}
|
||||
@ -140,7 +101,7 @@ func TranslateToResourceTuple(subject string, action, kind, name string) (*openf
|
||||
}
|
||||
|
||||
func IsFolderResourceTuple(t *openfgav1.TupleKey) bool {
|
||||
return strings.HasPrefix(t.Object, TypeFolder2) && strings.HasPrefix(t.Relation, "resource_")
|
||||
return strings.HasPrefix(t.Object, TypeFolder) && strings.HasPrefix(t.Relation, "resource_")
|
||||
}
|
||||
|
||||
func MergeFolderResourceTuples(a, b *openfgav1.TupleKey) {
|
||||
@ -149,30 +110,6 @@ func MergeFolderResourceTuples(a, b *openfgav1.TupleKey) {
|
||||
va.GetListValue().Values = append(va.GetListValue().Values, vb.GetListValue().Values...)
|
||||
}
|
||||
|
||||
func TranslateToOrgTuple(user string, action string, orgID int64) (*openfgav1.TupleKey, bool) {
|
||||
typeTranslation, ok := actionKindTranslations[KindOrg]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
relation, ok := typeTranslation.translations[action]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
tuple := &openfgav1.TupleKey{
|
||||
Relation: relation,
|
||||
User: user,
|
||||
Object: NewTupleEntry(typeTranslation.objectType, strconv.FormatInt(orgID, 10), ""),
|
||||
}
|
||||
|
||||
return tuple, true
|
||||
}
|
||||
|
||||
func TranslateBasicRole(role string) string {
|
||||
return basicRolesTranslations[role]
|
||||
}
|
||||
|
||||
func TranslateFixedRole(role string) string {
|
||||
role = strings.ReplaceAll(role, ":", "_")
|
||||
role = strings.ReplaceAll(role, ".", "_")
|
||||
|
@ -2,29 +2,13 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
)
|
||||
|
||||
const (
|
||||
// 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
|
||||
defaultQueryLimit = 1000
|
||||
)
|
||||
|
||||
type searchResult struct {
|
||||
runner string
|
||||
result []dashboards.DashboardSearchProjection
|
||||
@ -95,225 +79,7 @@ func (dr *DashboardServiceImpl) findDashboardsZanzanaCompare(ctx context.Context
|
||||
return first.result, first.err
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzana(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
findDashboards := dr.getFindDashboardsFn(query)
|
||||
return findDashboards(ctx, query)
|
||||
}
|
||||
|
||||
type findDashboardsFn func(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error)
|
||||
|
||||
// getFindDashboardsFn makes a decision which search method should be used
|
||||
func (dr *DashboardServiceImpl) getFindDashboardsFn(query dashboards.FindPersistedDashboardsQuery) findDashboardsFn {
|
||||
if query.Limit > 0 && query.Limit < listQueryLimitThreshold && len(query.Title) > 0 {
|
||||
return dr.findDashboardsZanzanaCheck
|
||||
}
|
||||
if len(query.DashboardUIDs) > 0 || len(query.DashboardIds) > 0 {
|
||||
return dr.findDashboardsZanzanaCheck
|
||||
}
|
||||
if len(query.FolderUIDs) > 0 {
|
||||
return dr.findDashboardsZanzanaCheck
|
||||
}
|
||||
if len(query.Title) <= listQueryLengthThreshold {
|
||||
return dr.findDashboardsZanzanaList
|
||||
}
|
||||
return dr.findDashboardsZanzanaCheck
|
||||
}
|
||||
|
||||
// findDashboardsZanzanaCheck implements "Search, then check" strategy. It first performs search query, then filters out results
|
||||
// by checking access to each item.
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzanaCheck(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.findDashboardsZanzanaCheck")
|
||||
defer span.End()
|
||||
|
||||
result := make([]dashboards.DashboardSearchProjection, 0, query.Limit)
|
||||
var page int64 = 1
|
||||
query.SkipAccessControlFilter = true
|
||||
// Remember initial query limit
|
||||
limit := query.Limit
|
||||
// Set limit to default to prevent pagination issues
|
||||
query.Limit = defaultQueryLimit
|
||||
|
||||
for len(result) < int(limit) {
|
||||
query.Page = page
|
||||
findRes, err := dr.dashboardStore.FindDashboards(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remains := limit - int64(len(result))
|
||||
res, err := dr.checkDashboards(ctx, query, findRes, remains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, res...)
|
||||
page++
|
||||
|
||||
// Stop when last page reached
|
||||
if len(findRes) < defaultQueryLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) checkDashboards(ctx context.Context, query dashboards.FindPersistedDashboardsQuery, searchRes []dashboards.DashboardSearchProjection, remains int64) ([]dashboards.DashboardSearchProjection, error) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.checkDashboards")
|
||||
defer span.End()
|
||||
|
||||
if len(searchRes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
orgId := query.OrgId
|
||||
if orgId == 0 && query.SignedInUser.GetOrgID() != 0 {
|
||||
orgId = query.SignedInUser.GetOrgID()
|
||||
} else {
|
||||
return nil, dashboards.ErrUserIsNotSignedInToOrg
|
||||
}
|
||||
|
||||
concurrentRequests := dr.cfg.Zanzana.ConcurrentChecks
|
||||
var wg sync.WaitGroup
|
||||
res := make([]dashboards.DashboardSearchProjection, 0)
|
||||
resToCheck := make(chan dashboards.DashboardSearchProjection, concurrentRequests)
|
||||
allowedResults := make(chan dashboards.DashboardSearchProjection, len(searchRes))
|
||||
|
||||
for i := 0; i < int(concurrentRequests); i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for d := range resToCheck {
|
||||
if int64(len(allowedResults)) >= remains {
|
||||
return
|
||||
}
|
||||
|
||||
objectType := zanzana.TypeDashboard
|
||||
if d.IsFolder {
|
||||
objectType = zanzana.TypeFolder
|
||||
}
|
||||
|
||||
req := accesscontrol.CheckRequest{
|
||||
Namespace: claims.OrgNamespaceFormatter(orgId),
|
||||
User: query.SignedInUser.GetUID(),
|
||||
Relation: "read",
|
||||
Object: zanzana.NewScopedTupleEntry(objectType, d.UID, "", strconv.FormatInt(orgId, 10)),
|
||||
}
|
||||
|
||||
if objectType != zanzana.TypeFolder {
|
||||
// Pass parentn folder for the correct check
|
||||
req.Parent = d.FolderUID
|
||||
req.ObjectType = objectType
|
||||
}
|
||||
|
||||
allowed, err := dr.ac.Check(ctx, req)
|
||||
if err != nil {
|
||||
dr.log.Error("error checking access", "error", err)
|
||||
} else if allowed {
|
||||
allowedResults <- d
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for _, r := range searchRes {
|
||||
resToCheck <- r
|
||||
}
|
||||
close(resToCheck)
|
||||
|
||||
wg.Wait()
|
||||
close(allowedResults)
|
||||
|
||||
for r := range allowedResults {
|
||||
if int64(len(res)) >= remains {
|
||||
break
|
||||
}
|
||||
res = append(res, r)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// findDashboardsZanzanaList implements "List, then search" strategy. It first retrieve a list of resources
|
||||
// with given type available to the user and then passes that list as a filter to the search query.
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzanaList(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
// Always use "search, then check" if dashboard or folder UIDs provided. Otherwise we should make intersection
|
||||
// of user's resources and provided UIDs which might not be correct if ListObjects() request is limited by OpenFGA.
|
||||
if len(query.DashboardUIDs) > 0 || len(query.DashboardIds) > 0 || len(query.FolderUIDs) > 0 {
|
||||
return dr.findDashboardsZanzanaCheck(ctx, query)
|
||||
}
|
||||
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.findDashboardsZanzanaList")
|
||||
defer span.End()
|
||||
|
||||
var result []dashboards.DashboardSearchProjection
|
||||
|
||||
allowedFolders, err := dr.listAllowedResources(ctx, query, zanzana.TypeFolder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(allowedFolders) > 0 {
|
||||
// Find dashboards in folders that user has access to
|
||||
query.SkipAccessControlFilter = true
|
||||
query.FolderUIDs = allowedFolders
|
||||
result, err = dr.dashboardStore.FindDashboards(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// skip if limit reached
|
||||
rest := query.Limit - int64(len(result))
|
||||
if rest <= 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Run second query to find dashboards with direct permission assignments
|
||||
allowedDashboards, err := dr.listAllowedResources(ctx, query, zanzana.TypeDashboard)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(allowedDashboards) > 0 {
|
||||
query.FolderUIDs = []string{}
|
||||
query.DashboardUIDs = allowedDashboards
|
||||
query.Limit = rest
|
||||
dashboardRes, err := dr.dashboardStore.FindDashboards(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, dashboardRes...)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) listAllowedResources(ctx context.Context, query dashboards.FindPersistedDashboardsQuery, resourceType string) ([]string, error) {
|
||||
res, err := dr.ac.ListObjects(ctx, accesscontrol.ListObjectsRequest{
|
||||
User: query.SignedInUser.GetUID(),
|
||||
Type: resourceType,
|
||||
Relation: "read",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgId := query.OrgId
|
||||
if orgId == 0 && query.SignedInUser.GetOrgID() != 0 {
|
||||
orgId = query.SignedInUser.GetOrgID()
|
||||
} else {
|
||||
return nil, dashboards.ErrUserIsNotSignedInToOrg
|
||||
}
|
||||
// dashboard:<orgId>-
|
||||
prefix := fmt.Sprintf("%s:%d-", resourceType, orgId)
|
||||
|
||||
resourceUIDs := make([]string, 0)
|
||||
for _, d := range res {
|
||||
if uid, found := strings.CutPrefix(d, prefix); found {
|
||||
resourceUIDs = append(resourceUIDs, uid)
|
||||
}
|
||||
}
|
||||
|
||||
return resourceUIDs, nil
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzana(_ context.Context, _ dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
// FIXME: Implement using the new schema
|
||||
return []dashboards.DashboardSearchProjection{}, nil
|
||||
}
|
||||
|
@ -1,126 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"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/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, quotaService)
|
||||
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,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
guardianMock := &guardian.FakeDashboardGuardian{
|
||||
CanSaveValue: true,
|
||||
}
|
||||
guardian.MockDashboardGuardian(guardianMock)
|
||||
|
||||
createDashboards(t, service, 100, "test-a")
|
||||
createDashboards(t, service, 100, "test-b")
|
||||
|
||||
// Sync Grafana DB with zanzana (migrate data)
|
||||
zanzanaSyncronizer := dualwrite.NewZanzanaReconciler(zclient, db, nil)
|
||||
err = zanzanaSyncronizer.Sync(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
query := &dashboards.FindPersistedDashboardsQuery{
|
||||
Title: "test-a",
|
||||
Limit: 1000,
|
||||
SignedInUser: &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
UserID: 1,
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
@ -7,7 +7,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
var _ accesscontrol.AccessControl = &recordingAccessControlFake{}
|
||||
|
||||
type recordingAccessControlFake struct {
|
||||
accesscontrol.AccessControl
|
||||
Disabled bool
|
||||
EvaluateRecordings []struct {
|
||||
Permissions map[string][]string
|
||||
@ -26,22 +29,3 @@ func (a *recordingAccessControlFake) Evaluate(_ context.Context, ur identity.Req
|
||||
}
|
||||
return a.Callback(ur, evaluator)
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) WithoutResolvers() accesscontrol.AccessControl {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) Check(ctx context.Context, in accesscontrol.CheckRequest) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) ListObjects(ctx context.Context, in accesscontrol.ListObjectsRequest) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var _ accesscontrol.AccessControl = &recordingAccessControlFake{}
|
||||
|
@ -103,6 +103,7 @@ func (f *fakeAlertInstanceManager) GenerateAlertInstances(orgID int64, alertRule
|
||||
}
|
||||
|
||||
type recordingAccessControlFake struct {
|
||||
ac.AccessControl
|
||||
Disabled bool
|
||||
EvaluateRecordings []struct {
|
||||
User *user.SignedInUser
|
||||
@ -123,28 +124,6 @@ func (a *recordingAccessControlFake) Evaluate(ctx context.Context, ur identity.R
|
||||
return a.Callback(u, evaluator)
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) RegisterScopeAttributeResolver(prefix string, resolver ac.ScopeAttributeResolver) {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) WithoutResolvers() ac.AccessControl {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) IsDisabled() bool {
|
||||
return a.Disabled
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) Check(ctx context.Context, in ac.CheckRequest) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (a *recordingAccessControlFake) ListObjects(ctx context.Context, in ac.ListObjectsRequest) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var _ ac.AccessControl = &recordingAccessControlFake{}
|
||||
|
||||
type fakeRuleAccessControlService struct {
|
||||
|
Loading…
Reference in New Issue
Block a user