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:
Karl Persson 2024-11-08 09:30:41 +01:00 committed by GitHub
parent 9b0644e5c8
commit f0a5b444e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 131 additions and 1545 deletions

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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
```

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
},
}

View 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

View 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

View File

@ -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>) {

View File

@ -19,7 +19,7 @@ import (
const (
resourceType = "resource"
namespaceType = "namespace"
folderTypePrefix = "folder2:"
folderTypePrefix = "folder:"
)
var _ authzv1.AuthzServiceServer = (*Server)(nil)

View File

@ -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)
}

View File

@ -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{

View File

@ -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, ".", "_")

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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{}

View File

@ -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 {