mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Zanzana: Evaluate dashboard and folder permissions (#91539)
* Zanzana: basic folder permissions checks * Fix managed permissions for teams * fix sync batch size * add dashboards actions translations * migrate folder tree * migrate dashboard folders * remove action sets from schema * Adding more dashboard and folder-related permissions * refactor * Correctly translate dashboard permissions in folders * fix dashboard parent permissions
This commit is contained in:
parent
423d198d77
commit
1cc438a56c
@ -114,6 +114,7 @@ func (a *AccessControl) evaluateZanzana(ctx context.Context, user identity.Reque
|
|||||||
return false, errAccessNotImplemented
|
return false, errAccessNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.log.Debug("evaluating zanzana", "user", key.User, "relation", key.Relation, "object", key.Object)
|
||||||
res, err := a.zclient.Check(ctx, &openfgav1.CheckRequest{
|
res, err := a.zclient.Check(ctx, &openfgav1.CheckRequest{
|
||||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||||
User: key.User,
|
User: key.User,
|
||||||
|
@ -3,6 +3,7 @@ package migrator
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||||
@ -33,6 +34,8 @@ func NewZanzanaSynchroniser(client zanzana.Client, store db.DB, collectors ...Tu
|
|||||||
collectors,
|
collectors,
|
||||||
teamMembershipCollector(store),
|
teamMembershipCollector(store),
|
||||||
managedPermissionsCollector(store),
|
managedPermissionsCollector(store),
|
||||||
|
folderTreeCollector(store),
|
||||||
|
dashboardFolderCollector(store),
|
||||||
)
|
)
|
||||||
|
|
||||||
return &ZanzanaSynchroniser{
|
return &ZanzanaSynchroniser{
|
||||||
@ -111,9 +114,9 @@ func managedPermissionsCollector(store db.DB) TupleCollector {
|
|||||||
for _, p := range permissions {
|
for _, p := range permissions {
|
||||||
var subject string
|
var subject string
|
||||||
if len(p.UserUID) > 0 {
|
if len(p.UserUID) > 0 {
|
||||||
subject = zanzana.NewObject(zanzana.TypeUser, p.UserUID)
|
subject = zanzana.NewTupleEntry(zanzana.TypeUser, p.UserUID, "")
|
||||||
} else if len(p.TeamUID) > 0 {
|
} else if len(p.TeamUID) > 0 {
|
||||||
subject = zanzana.NewObject(zanzana.TypeTeam, p.TeamUID)
|
subject = zanzana.NewTupleEntry(zanzana.TypeTeam, p.TeamUID, "member")
|
||||||
} else {
|
} else {
|
||||||
// FIXME(kalleep): Unsuported role binding (org role). We need to have basic roles in place
|
// FIXME(kalleep): Unsuported role binding (org role). We need to have basic roles in place
|
||||||
continue
|
continue
|
||||||
@ -161,8 +164,8 @@ func teamMembershipCollector(store db.DB) TupleCollector {
|
|||||||
|
|
||||||
for _, m := range memberships {
|
for _, m := range memberships {
|
||||||
tuple := &openfgav1.TupleKey{
|
tuple := &openfgav1.TupleKey{
|
||||||
User: zanzana.NewObject(zanzana.TypeUser, m.UserUID),
|
User: zanzana.NewTupleEntry(zanzana.TypeUser, m.UserUID, ""),
|
||||||
Object: zanzana.NewObject(zanzana.TypeTeam, m.TeamUID),
|
Object: zanzana.NewTupleEntry(zanzana.TypeTeam, m.TeamUID, ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin permission is 4 and member 0
|
// Admin permission is 4 and member 0
|
||||||
@ -178,3 +181,75 @@ func teamMembershipCollector(store db.DB) TupleCollector {
|
|||||||
return nil
|
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 {
|
||||||
|
const collectorID = "folder"
|
||||||
|
const query = `
|
||||||
|
SELECT uid, parent_uid, org_id FROM folder WHERE parent_uid IS NOT NULL
|
||||||
|
`
|
||||||
|
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 {
|
||||||
|
tuple := &openfgav1.TupleKey{
|
||||||
|
User: zanzana.NewScopedTupleEntry(zanzana.TypeFolder, f.ParentUID, "", strconv.FormatInt(f.OrgID, 10)),
|
||||||
|
Object: zanzana.NewScopedTupleEntry(zanzana.TypeFolder, f.FolderUID, "", strconv.FormatInt(f.OrgID, 10)),
|
||||||
|
Relation: zanzana.RelationParent,
|
||||||
|
}
|
||||||
|
|
||||||
|
tuples[collectorID] = append(tuples[collectorID], tuple)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dashboardFolderCollector collects information about dashboards parent folders
|
||||||
|
func dashboardFolderCollector(store db.DB) TupleCollector {
|
||||||
|
return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error {
|
||||||
|
const collectorID = "folder"
|
||||||
|
const query = `
|
||||||
|
SELECT org_id, uid, folder_uid, is_folder FROM dashboard WHERE is_folder = 0 AND folder_uid IS NOT NULL
|
||||||
|
`
|
||||||
|
type dashboard struct {
|
||||||
|
OrgID int64 `xorm:"org_id"`
|
||||||
|
UID string `xorm:"uid"`
|
||||||
|
ParentUID string `xorm:"folder_uid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var dashboards []dashboard
|
||||||
|
err := store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
|
return sess.SQL(query).Find(&dashboards)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range dashboards {
|
||||||
|
tuple := &openfgav1.TupleKey{
|
||||||
|
User: zanzana.NewScopedTupleEntry(zanzana.TypeFolder, d.ParentUID, "", strconv.FormatInt(d.OrgID, 10)),
|
||||||
|
Object: zanzana.NewScopedTupleEntry(zanzana.TypeDashboard, d.UID, "", strconv.FormatInt(d.OrgID, 10)),
|
||||||
|
Relation: zanzana.RelationParent,
|
||||||
|
}
|
||||||
|
|
||||||
|
tuples[collectorID] = append(tuples[collectorID], tuple)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -11,6 +11,47 @@ type org
|
|||||||
define member: [user]
|
define member: [user]
|
||||||
define viewer: [user]
|
define viewer: [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
|
||||||
|
|
||||||
|
define folder_create: [role#assignee]
|
||||||
|
define folder_read: [role#assignee] or folder_delete
|
||||||
|
define folder_write: [role#assignee] or folder_create
|
||||||
|
define folder_delete: [role#assignee] or folder_write
|
||||||
|
define folder_permissions_write: [role#assignee]
|
||||||
|
define folder_permissions_read: [role#assignee] or folder_permissions_write
|
||||||
|
|
||||||
|
define dashboard_annotations_create: [role#assignee]
|
||||||
|
define dashboard_annotations_read: [role#assignee]
|
||||||
|
define dashboard_annotations_write: [role#assignee]
|
||||||
|
define dashboard_annotations_delete: [role#assignee]
|
||||||
|
define dashboard_create: [role#assignee]
|
||||||
|
define dashboard_delete: [role#assignee]
|
||||||
|
define dashboard_permissions_read: [role#assignee]
|
||||||
|
define dashboard_permissions_write: [role#assignee]
|
||||||
|
define dashboard_public_write: [role#assignee] or dashboard_write
|
||||||
|
define dashboard_read: [role#assignee]
|
||||||
|
define dashboard_write: [role#assignee]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
type role
|
type role
|
||||||
relations
|
relations
|
||||||
define org: [org]
|
define org: [org]
|
||||||
@ -22,3 +63,65 @@ type team
|
|||||||
define org: [org]
|
define org: [org]
|
||||||
define admin: [user]
|
define admin: [user]
|
||||||
define member: [user] or admin
|
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
|
||||||
|
|
||||||
|
type folder
|
||||||
|
relations
|
||||||
|
define parent: [folder]
|
||||||
|
define org: [org]
|
||||||
|
|
||||||
|
define create: [user, team#member, role#assignee] or create from parent or folder_create from org
|
||||||
|
define read: [user, team#member, role#assignee] or read from parent or folder_read from org
|
||||||
|
define write: [user, team#member, role#assignee] or write from parent or folder_write from org
|
||||||
|
define delete: [user, team#member, role#assignee] or delete from parent or folder_delete from org
|
||||||
|
define permissions_read: [user, team#member, role#assignee] or permissions_read from parent or folder_permissions_read from org
|
||||||
|
define permissions_write: [user, team#member, role#assignee] or permissions_write from parent or folder_permissions_write from org
|
||||||
|
|
||||||
|
define dashboard_create: [user, team#member, role#assignee] or dashboard_create from parent or dashboard_create from org
|
||||||
|
define dashboard_read: [user, team#member, role#assignee] or dashboard_read from parent or dashboard_read from org
|
||||||
|
define dashboard_write: [user, team#member, role#assignee] or dashboard_write from parent or dashboard_write from org
|
||||||
|
define dashboard_delete: [user, team#member, role#assignee] or dashboard_delete from parent or dashboard_delete from org
|
||||||
|
define dashboard_permissions_read: [user, team#member, role#assignee] or dashboard_permissions_read from parent or dashboard_permissions_read from org
|
||||||
|
define dashboard_permissions_write: [user, team#member, role#assignee] 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 library_panel_create from parent or library_panel_create from org
|
||||||
|
define library_panel_read: [user, team#member, role#assignee] 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 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 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 alert_rule_create from parent or alert_rule_create from org
|
||||||
|
define alert_rule_read: [user, team#member, role#assignee] 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 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 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 alert_silence_create from parent or alert_silence_create from org
|
||||||
|
define alert_silence_read: [user, team#member, role#assignee] 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 alert_silence_write from parent or alert_silence_write from org or alert_silence_create
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
type dashboard
|
||||||
|
relations
|
||||||
|
define org: [org]
|
||||||
|
define parent: [folder]
|
||||||
|
|
||||||
|
define read: [user, team#member, role#assignee] or dashboard_read from parent or dashboard_read from org
|
||||||
|
define write: [user, team#member, role#assignee] or dashboard_write from parent or dashboard_write from org
|
||||||
|
define delete: [user, team#member, role#assignee] or dashboard_delete from parent or dashboard_delete from org
|
||||||
|
define create: [user, team#member, role#assignee] or dashboard_create from parent or dashboard_create from org
|
||||||
|
define permissions_read: [user, team#member, role#assignee] or dashboard_permissions_read from parent or dashboard_permissions_read from org
|
||||||
|
define permissions_write: [user, team#member, role#assignee] or dashboard_permissions_write from parent or dashboard_permissions_write from org
|
||||||
|
|
||||||
|
define public_write: [user, team#member, role#assignee] or dashboard_public_write from parent or dashboard_public_write from org or write
|
||||||
|
define annotations_create: [user, team#member, role#assignee] or dashboard_annotations_create from parent or dashboard_annotations_create from org
|
||||||
|
define annotations_read: [user, team#member, role#assignee] or dashboard_annotations_read from parent or dashboard_annotations_read from org
|
||||||
|
define annotations_write: [user, team#member, role#assignee] or dashboard_annotations_write from parent or dashboard_annotations_write from org
|
||||||
|
define annotations_delete: [user, team#member, role#assignee] or dashboard_annotations_delete from parent or dashboard_annotations_delete from org
|
||||||
|
61
pkg/services/authz/zanzana/translations.go
Normal file
61
pkg/services/authz/zanzana/translations.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package zanzana
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
// RBAC to OpenFGA translations grouped by kind
|
||||||
|
var actionKindTranslations = map[string]actionKindTranslation{
|
||||||
|
"folders": {
|
||||||
|
objectType: "folder",
|
||||||
|
orgScoped: true,
|
||||||
|
translations: folderActions,
|
||||||
|
},
|
||||||
|
"dashboards": {
|
||||||
|
objectType: "dashboard",
|
||||||
|
orgScoped: true,
|
||||||
|
translations: dashboardActions,
|
||||||
|
},
|
||||||
|
}
|
@ -8,41 +8,42 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeUser string = "user"
|
TypeUser string = "user"
|
||||||
TypeTeam string = "team"
|
TypeTeam string = "team"
|
||||||
|
TypeFolder string = "folder"
|
||||||
|
TypeDashboard string = "dashboard"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RelationTeamMember string = "member"
|
RelationTeamMember string = "member"
|
||||||
RelationTeamAdmin string = "admin"
|
RelationTeamAdmin string = "admin"
|
||||||
|
RelationParent string = "parent"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewObject(typ, id string) string {
|
// NewTupleEntry constructs new openfga entry type:id[#relation].
|
||||||
return fmt.Sprintf("%s:%s", typ, id)
|
// Relation allows to specify group of users (subjects) related to type:id
|
||||||
|
// (for example, team:devs#member refers to users which are members of team devs)
|
||||||
|
func NewTupleEntry(objectType, id, relation string) string {
|
||||||
|
obj := fmt.Sprintf("%s:%s", objectType, id)
|
||||||
|
if relation != "" {
|
||||||
|
obj = fmt.Sprintf("%s#%s", obj, relation)
|
||||||
|
}
|
||||||
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScopedObject(typ, id, scope string) string {
|
// NewScopedTupleEntry constructs new openfga entry type:id[#relation]
|
||||||
return NewObject(typ, fmt.Sprintf("%s-%s", scope, id))
|
// 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), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// rbac action to relation translation
|
|
||||||
var actionTranslations = map[string]string{}
|
|
||||||
|
|
||||||
type kindTranslation struct {
|
|
||||||
typ string
|
|
||||||
orgScoped bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// all kinds that we can translate into a openFGA object
|
|
||||||
var kindTranslations = map[string]kindTranslation{}
|
|
||||||
|
|
||||||
func TranslateToTuple(user string, action, kind, identifier string, orgID int64) (*openfgav1.TupleKey, bool) {
|
func TranslateToTuple(user string, action, kind, identifier string, orgID int64) (*openfgav1.TupleKey, bool) {
|
||||||
relation, ok := actionTranslations[action]
|
typeTranslation, ok := actionKindTranslations[kind]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
t, ok := kindTranslations[kind]
|
relation, ok := typeTranslation.translations[action]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@ -55,10 +56,10 @@ func TranslateToTuple(user string, action, kind, identifier string, orgID int64)
|
|||||||
tuple.Relation = relation
|
tuple.Relation = relation
|
||||||
|
|
||||||
// Some uid:s in grafana are not guarantee to be unique across orgs so we need to scope them.
|
// Some uid:s in grafana are not guarantee to be unique across orgs so we need to scope them.
|
||||||
if t.orgScoped {
|
if typeTranslation.orgScoped {
|
||||||
tuple.Object = NewScopedObject(t.typ, identifier, strconv.FormatInt(orgID, 10))
|
tuple.Object = NewScopedTupleEntry(typeTranslation.objectType, identifier, "", strconv.FormatInt(orgID, 10))
|
||||||
} else {
|
} else {
|
||||||
tuple.Object = NewObject(t.typ, identifier)
|
tuple.Object = NewTupleEntry(typeTranslation.objectType, identifier, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return tuple, true
|
return tuple, true
|
||||||
|
Loading…
Reference in New Issue
Block a user