From 0e0c877609bb6c02093f554432845a4f767e809b Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Aug 2024 15:39:22 +0200 Subject: [PATCH] Zanzana: Model fixed roles as a part of schema (#92364) * model fixed roles for dashboards and folders * Correctly translate fixed role assignments * minor refactor * assign fixed roles to teams * fix linter errors * Migrate general folder permissions for fixed roles * fix dashboards:create permission --- .../accesscontrol/migrator/zanzana.go | 138 ++++++++++++++---- .../authz/zanzana/schema/dashboard.fga | 25 +++- pkg/services/authz/zanzana/schema/folder.fga | 21 ++- pkg/services/authz/zanzana/zanzana.go | 7 + 4 files changed, 148 insertions(+), 43 deletions(-) diff --git a/pkg/services/accesscontrol/migrator/zanzana.go b/pkg/services/accesscontrol/migrator/zanzana.go index e65997ed86e..fe21f0101fe 100644 --- a/pkg/services/accesscontrol/migrator/zanzana.go +++ b/pkg/services/accesscontrol/migrator/zanzana.go @@ -45,6 +45,7 @@ func NewZanzanaSynchroniser(client zanzana.Client, store db.DB, collectors ...Tu basicRoleAssignemtCollector(store), userRoleAssignemtCollector(store), teamRoleAssignemtCollector(store), + fixedRoleTuplesCollector(store), ) return &ZanzanaSynchroniser{ @@ -474,16 +475,18 @@ func userRoleAssignemtCollector(store db.DB) TupleCollector { return func(ctx context.Context, tuples map[string][]*openfgav1.TupleKey) error { const collectorID = "user_role_assignment" const query = ` - SELECT ur.org_id, u.uid AS user_uid, r.uid AS role_uid + 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 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"` + OrgID int64 `xorm:"org_id"` + UserUID string `xorm:"user_uid"` + RoleUID string `xorm:"role_uid"` + RoleName string `xorm:"role_name"` } var assignments []Assignment @@ -495,21 +498,32 @@ func userRoleAssignemtCollector(store db.DB) TupleCollector { } for _, a := range assignments { - var subject string - if a.UserUID != "" && a.RoleUID != "" { - subject = zanzana.NewTupleEntry(zanzana.TypeUser, a.UserUID, "") - } else { + if a.UserUID == "" || a.RoleUID == "" { continue } - tuple := &openfgav1.TupleKey{ - User: subject, - Relation: zanzana.RelationAssignee, - Object: zanzana.NewScopedTupleEntry(zanzana.TypeRole, a.RoleUID, "", strconv.FormatInt(a.OrgID, 10)), - } + 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: 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) + key := fmt.Sprintf("%s-%s", collectorID, zanzana.RelationAssignee) + tuples[key] = append(tuples[key], tuple) + } } return nil @@ -520,16 +534,18 @@ 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 + 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"` + OrgID int64 `xorm:"org_id"` + TeamUID string `xorm:"team_uid"` + RoleUID string `xorm:"role_uid"` + RoleName string `xorm:"role_name"` } var assignments []Assignment @@ -541,21 +557,83 @@ func teamRoleAssignemtCollector(store db.DB) TupleCollector { } for _, a := range assignments { - var subject string - if a.TeamUID != "" && a.RoleUID != "" { - subject = zanzana.NewTupleEntry(zanzana.TypeTeam, a.TeamUID, "member") - } else { + if a.TeamUID == "" || a.RoleUID == "" { continue } - tuple := &openfgav1.TupleKey{ - User: subject, - Relation: zanzana.RelationAssignee, - Object: zanzana.NewScopedTupleEntry(zanzana.TypeRole, a.RoleUID, "", strconv.FormatInt(a.OrgID, 10)), - } + 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: 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) + 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 diff --git a/pkg/services/authz/zanzana/schema/dashboard.fga b/pkg/services/authz/zanzana/schema/dashboard.fga index 6769e3f2bab..ee08372ac7f 100644 --- a/pkg/services/authz/zanzana/schema/dashboard.fga +++ b/pkg/services/authz/zanzana/schema/dashboard.fga @@ -2,17 +2,28 @@ 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] - 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] + + # 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 diff --git a/pkg/services/authz/zanzana/schema/folder.fga b/pkg/services/authz/zanzana/schema/folder.fga index 4b4587b9fb5..a43fc5233c7 100644 --- a/pkg/services/authz/zanzana/schema/folder.fga +++ b/pkg/services/authz/zanzana/schema/folder.fga @@ -2,12 +2,12 @@ module folder extend type org relations - 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 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 @@ -23,6 +23,15 @@ extend type org 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] diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index d59b7616e73..03bfe9f36ed 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -3,6 +3,7 @@ package zanzana import ( "fmt" "strconv" + "strings" openfgav1 "github.com/openfga/api/proto/openfga/v1" ) @@ -111,3 +112,9 @@ func TranslateToOrgTuple(user string, action string, orgID int64) (*openfgav1.Tu func TranslateBasicRole(role string) string { return basicRolesTranslations[role] } + +func TranslateFixedRole(role string) string { + role = strings.ReplaceAll(role, ":", "_") + role = strings.ReplaceAll(role, ".", "_") + return role +}