diff --git a/pkg/services/accesscontrol/acimpl/accesscontrol.go b/pkg/services/accesscontrol/acimpl/accesscontrol.go index a1dd9bbcc95..48bc7bd3ca2 100644 --- a/pkg/services/accesscontrol/acimpl/accesscontrol.go +++ b/pkg/services/accesscontrol/acimpl/accesscontrol.go @@ -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, diff --git a/pkg/services/accesscontrol/migrator/zanzana.go b/pkg/services/accesscontrol/migrator/zanzana.go index 426485bde23..862cb1caf74 100644 --- a/pkg/services/accesscontrol/migrator/zanzana.go +++ b/pkg/services/accesscontrol/migrator/zanzana.go @@ -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 + } +} diff --git a/pkg/services/authz/zanzana/schema/schema.fga b/pkg/services/authz/zanzana/schema/schema.fga index 0c5bd688d88..d4d8a5ba8df 100644 --- a/pkg/services/authz/zanzana/schema/schema.fga +++ b/pkg/services/authz/zanzana/schema/schema.fga @@ -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 diff --git a/pkg/services/authz/zanzana/translations.go b/pkg/services/authz/zanzana/translations.go new file mode 100644 index 00000000000..8e52ea2c69c --- /dev/null +++ b/pkg/services/authz/zanzana/translations.go @@ -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, + }, +} diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index 3062511a031..940c9c3691f 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -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