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:
Alexander Zobnin 2024-08-09 13:48:56 +02:00 committed by GitHub
parent 423d198d77
commit 1cc438a56c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 267 additions and 26 deletions

View File

@ -114,6 +114,7 @@ func (a *AccessControl) evaluateZanzana(ctx context.Context, user identity.Reque
return false, errAccessNotImplemented
}
a.log.Debug("evaluating zanzana", "user", key.User, "relation", key.Relation, "object", key.Object)
res, err := a.zclient.Check(ctx, &openfgav1.CheckRequest{
TupleKey: &openfgav1.CheckRequestTupleKey{
User: key.User,

View File

@ -3,6 +3,7 @@ package migrator
import (
"context"
"fmt"
"strconv"
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
@ -33,6 +34,8 @@ func NewZanzanaSynchroniser(client zanzana.Client, store db.DB, collectors ...Tu
collectors,
teamMembershipCollector(store),
managedPermissionsCollector(store),
folderTreeCollector(store),
dashboardFolderCollector(store),
)
return &ZanzanaSynchroniser{
@ -111,9 +114,9 @@ func managedPermissionsCollector(store db.DB) TupleCollector {
for _, p := range permissions {
var subject string
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 {
subject = zanzana.NewObject(zanzana.TypeTeam, p.TeamUID)
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
@ -161,8 +164,8 @@ func teamMembershipCollector(store db.DB) TupleCollector {
for _, m := range memberships {
tuple := &openfgav1.TupleKey{
User: zanzana.NewObject(zanzana.TypeUser, m.UserUID),
Object: zanzana.NewObject(zanzana.TypeTeam, m.TeamUID),
User: zanzana.NewTupleEntry(zanzana.TypeUser, m.UserUID, ""),
Object: zanzana.NewTupleEntry(zanzana.TypeTeam, m.TeamUID, ""),
}
// Admin permission is 4 and member 0
@ -178,3 +181,75 @@ func teamMembershipCollector(store db.DB) TupleCollector {
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
}
}

View File

@ -11,6 +11,47 @@ type org
define member: [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
relations
define org: [org]
@ -22,3 +63,65 @@ type team
define org: [org]
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
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

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

View File

@ -8,41 +8,42 @@ import (
)
const (
TypeUser string = "user"
TypeTeam string = "team"
TypeUser string = "user"
TypeTeam string = "team"
TypeFolder string = "folder"
TypeDashboard string = "dashboard"
)
const (
RelationTeamMember string = "member"
RelationTeamAdmin string = "admin"
RelationParent string = "parent"
)
func NewObject(typ, id string) string {
return fmt.Sprintf("%s:%s", typ, id)
// NewTupleEntry constructs new openfga entry type:id[#relation].
// 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 {
return NewObject(typ, fmt.Sprintf("%s-%s", scope, id))
// 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), "")
}
// 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) {
relation, ok := actionTranslations[action]
typeTranslation, ok := actionKindTranslations[kind]
if !ok {
return nil, false
}
t, ok := kindTranslations[kind]
relation, ok := typeTranslation.translations[action]
if !ok {
return nil, false
}
@ -55,10 +56,10 @@ func TranslateToTuple(user string, action, kind, identifier string, orgID int64)
tuple.Relation = relation
// Some uid:s in grafana are not guarantee to be unique across orgs so we need to scope them.
if t.orgScoped {
tuple.Object = NewScopedObject(t.typ, identifier, strconv.FormatInt(orgID, 10))
if typeTranslation.orgScoped {
tuple.Object = NewScopedTupleEntry(typeTranslation.objectType, identifier, "", strconv.FormatInt(orgID, 10))
} else {
tuple.Object = NewObject(t.typ, identifier)
tuple.Object = NewTupleEntry(typeTranslation.objectType, identifier, "")
}
return tuple, true