package dualwrite import ( "context" openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/grafana/grafana/pkg/infra/db" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) func teamMembershipCollector(store db.DB) legacyTupleCollector { return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) { query := ` SELECT t.uid as team_uid, u.uid as user_uid, tm.permission FROM team_member tm INNER JOIN team t ON tm.team_id = t.id INNER JOIN ` + store.GetDialect().Quote("user") + ` u ON tm.user_id = u.id WHERE t.org_id = ? ` type membership struct { TeamUID string `xorm:"team_uid"` UserUID string `xorm:"user_uid"` Permission int } var memberships []membership err := store.WithDbSession(ctx, func(sess *db.Session) error { return sess.SQL(query, orgID).Find(&memberships) }) if err != nil { return nil, err } tuples := make(map[string]map[string]*openfgav1.TupleKey) for _, m := range memberships { tuple := &openfgav1.TupleKey{ User: zanzana.NewTupleEntry(zanzana.TypeUser, m.UserUID, ""), Object: zanzana.NewTupleEntry(zanzana.TypeTeam, m.TeamUID, ""), } // Admin permission is 4 and member 0 if m.Permission == 4 { tuple.Relation = zanzana.RelationTeamAdmin } else { tuple.Relation = zanzana.RelationTeamMember } if tuples[tuple.Object] == nil { tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) } tuples[tuple.Object][tuple.String()] = tuple } return tuples, nil } } // folderTreeCollector collects folder tree structure and writes it as relation tuples func folderTreeCollector(folderService folder.Service) legacyTupleCollector { return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) { ctx, span := tracer.Start(ctx, "accesscontrol.migrator.folderTreeCollector") defer span.End() user := &user.SignedInUser{ Login: "folder-tree-collector", OrgRole: "Admin", IsGrafanaAdmin: true, IsServiceAccount: true, Permissions: map[int64]map[string][]string{orgID: {dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}}}, OrgID: orgID, } q := folder.GetFoldersQuery{ OrgID: orgID, SignedInUser: user, } folders, err := folderService.GetFolders(ctx, q) if err != nil { return nil, err } tuples := make(map[string]map[string]*openfgav1.TupleKey) for _, f := range folders { var tuple *openfgav1.TupleKey if f.ParentUID == "" { continue } tuple = &openfgav1.TupleKey{ Object: zanzana.NewTupleEntry(zanzana.TypeFolder, f.UID, ""), Relation: zanzana.RelationParent, User: zanzana.NewTupleEntry(zanzana.TypeFolder, f.ParentUID, ""), } if tuples[tuple.Object] == nil { tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) } tuples[tuple.Object][tuple.String()] = tuple } return tuples, nil } } // managedPermissionsCollector collects managed permissions. // 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, kind string) legacyTupleCollector { return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) { query := ` SELECT u.uid as user_uid, u.is_service_account as is_service_account, t.uid as team_uid, p.action, p.kind, p.identifier, r.org_id, br.role as basic_role_name 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:%' AND r.org_id = ? AND p.kind = ? ` type Permission struct { Action string `xorm:"action"` Kind string Identifier string UserUID string `xorm:"user_uid"` IsServiceAccount bool `xorm:"is_service_account"` TeamUID string `xorm:"team_uid"` BasicRoleName string `xorm:"basic_role_name"` } var permissions []Permission err := store.WithDbSession(ctx, func(sess *db.Session) error { return sess.SQL(query, orgID, kind).Find(&permissions) }) if err != nil { return nil, err } tuples := make(map[string]map[string]*openfgav1.TupleKey) for _, p := range permissions { var subject string if len(p.UserUID) > 0 && p.IsServiceAccount { subject = zanzana.NewTupleEntry(zanzana.TypeServiceAccount, p.UserUID, "") } else 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, zanzana.RelationTeamMember) } else { subject = zanzana.NewTupleEntry(zanzana.TypeRole, zanzana.TranslateBasicRole(p.BasicRoleName), zanzana.RelationAssignee) } tuple, ok := zanzana.TranslateToResourceTuple(subject, p.Action, p.Kind, p.Identifier) if !ok { continue } if tuples[tuple.Object] == nil { tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) } // For resource actions on folders we need to merge the tuples into one with combined // group_resources. if zanzana.IsFolderResourceTuple(tuple) { key := tupleStringWithoutCondition(tuple) if t, ok := tuples[tuple.Object][key]; ok { zanzana.MergeFolderResourceTuples(t, tuple) } else { tuples[tuple.Object][key] = tuple } continue } tuples[tuple.Object][tuple.String()] = tuple } return tuples, nil } } func tupleStringWithoutCondition(tuple *openfgav1.TupleKey) string { c := tuple.Condition tuple.Condition = nil s := tuple.String() tuple.Condition = c return s } // basicRoleBindingsCollector collects role bindings for basic roles func basicRoleBindingsCollector(store db.DB) legacyTupleCollector { return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) { query := ` SELECT ou.org_id, u.uid as user_uid, u.is_service_account as is_service_account, ou.role as org_role FROM org_user ou LEFT JOIN ` + store.GetDialect().Quote("user") + ` u ON u.id = ou.user_id WHERE ou.org_id = ? ` // FIXME: handle service admin role type Binding struct { UserUID string `xorm:"user_uid"` IsServiceAccount bool `xorm:"is_service_account"` OrgRole string `xorm:"org_role"` } var bindings []Binding err := store.WithDbSession(ctx, func(sess *db.Session) error { return sess.SQL(query, orgID).Find(&bindings) }) if err != nil { return nil, err } tuples := make(map[string]map[string]*openfgav1.TupleKey) for _, b := range bindings { userType := zanzana.TypeUser if b.IsServiceAccount { userType = zanzana.TypeServiceAccount } tuple := &openfgav1.TupleKey{ User: zanzana.NewTupleEntry(userType, b.UserUID, ""), Relation: zanzana.RelationAssignee, Object: zanzana.NewTupleEntry(zanzana.TypeRole, zanzana.TranslateBasicRole(b.OrgRole), ""), } if tuples[tuple.Object] == nil { tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) } tuples[tuple.Object][tuple.String()] = tuple } return tuples, nil } } func teamRoleBindingsCollector(store db.DB) legacyTupleCollector { return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) { query := ` SELECT t.uid AS team_uid, r.uid AS role_uid FROM team_role tr INNER JOIN team t ON tr.team_id = t.id INNER JOIN role r ON tr.role_id = r.id WHERE t.org_id = ? AND r.name NOT LIKE 'managed:%' ` type Binding struct { TeamUID string `xorm:"team_uid"` RoleUID string `xorm:"role_uid"` } var bindings []Binding err := store.WithDbSession(ctx, func(sess *db.Session) error { return sess.SQL(query, orgID).Find(&bindings) }) if err != nil { return nil, err } tuples := make(map[string]map[string]*openfgav1.TupleKey) for _, b := range bindings { tuple := &openfgav1.TupleKey{ User: zanzana.NewTupleEntry(zanzana.TypeTeam, b.TeamUID, zanzana.RelationTeamMember), Relation: zanzana.RelationAssignee, Object: zanzana.NewTupleEntry(zanzana.TypeRole, b.RoleUID, ""), } if tuples[tuple.Object] == nil { tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) } tuples[tuple.Object][tuple.String()] = tuple } return tuples, nil } } func userRoleBindingsCollector(store db.DB) legacyTupleCollector { return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) { query := ` SELECT u.uid AS user_uid, u.is_service_account as is_service_account, r.uid AS role_uid FROM user_role ur INNER JOIN ` + store.GetDialect().Quote("user") + ` u ON ur.user_id = u.id INNER JOIN role r ON ur.role_id = r.id WHERE (ur.org_id = 0 OR ur.org_id = ?) AND r.name NOT LIKE 'managed:%' ` type Binding struct { UserUID string `xorm:"user_uid"` IsServiceAccount bool `xorm:"is_service_account"` RoleUID string `xorm:"role_uid"` } var bindings []Binding err := store.WithDbSession(ctx, func(sess *db.Session) error { return sess.SQL(query, orgID).Find(&bindings) }) if err != nil { return nil, err } tuples := make(map[string]map[string]*openfgav1.TupleKey) for _, b := range bindings { userType := zanzana.TypeUser if b.IsServiceAccount { userType = zanzana.TypeServiceAccount } tuple := &openfgav1.TupleKey{ User: zanzana.NewTupleEntry(userType, b.UserUID, ""), Relation: zanzana.RelationAssignee, Object: zanzana.NewTupleEntry(zanzana.TypeRole, b.RoleUID, ""), } if tuples[tuple.Object] == nil { tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) } tuples[tuple.Object][tuple.String()] = tuple } return tuples, nil } } func rolePermissionsCollector(store db.DB) legacyTupleCollector { return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) { var query = ` SELECT r.uid as role_uid, p.action, p.kind, p.identifier 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.org_id = 0 OR r.org_id = ?) AND r.name NOT LIKE 'managed:%' AND r.name NOT LIKE 'fixed:%' ` type Permission struct { 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, orgID).Find(&permissions) }) if err != nil { return nil, err } tuples := make(map[string]map[string]*openfgav1.TupleKey) for _, p := range permissions { tuple, ok := zanzana.TranslateToResourceTuple( zanzana.NewTupleEntry(zanzana.TypeRole, p.RoleUID, zanzana.RelationAssignee), p.Action, p.Kind, p.Identifier, ) if !ok { continue } if tuples[tuple.Object] == nil { tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) } // For resource actions on folders we need to merge the tuples into one with combined // group_resources. if zanzana.IsFolderResourceTuple(tuple) { key := tupleStringWithoutCondition(tuple) if t, ok := tuples[tuple.Object][key]; ok { zanzana.MergeFolderResourceTuples(t, tuple) } else { tuples[tuple.Object][key] = tuple } continue } tuples[tuple.Object][tuple.String()] = tuple } return tuples, nil } } func fixedRolePermissionsCollector(store db.DB) globalTupleCollector { return func(ctx context.Context) (map[string]map[string]*openfgav1.TupleKey, error) { var query = ` SELECT r.uid as role_uid, p.action, p.kind, p.identifier 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.org_id = 0 AND r.name LIKE 'fixed:%' ` type Permission struct { 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 nil, err } tuples := make(map[string]map[string]*openfgav1.TupleKey) for _, p := range permissions { tuple, ok := zanzana.TranslateToResourceTuple( zanzana.NewTupleEntry(zanzana.TypeRole, p.RoleUID, zanzana.RelationAssignee), p.Action, p.Kind, p.Identifier, ) if !ok { continue } if tuples[tuple.Object] == nil { tuples[tuple.Object] = make(map[string]*openfgav1.TupleKey) } tuples[tuple.Object][tuple.String()] = tuple } return tuples, nil } } // basicRoleBindingsCollector collects role bindings for basic roles func anonymousRoleBindingsCollector(cfg *setting.Cfg, store db.DB) legacyTupleCollector { return func(ctx context.Context, orgID int64) (map[string]map[string]*openfgav1.TupleKey, error) { tuples := make(map[string]map[string]*openfgav1.TupleKey) object := zanzana.NewTupleEntry(zanzana.TypeRole, zanzana.TranslateBasicRole(cfg.Anonymous.OrgRole), "") // Object should be set to delete obsolete permissions tuples[object] = make(map[string]*openfgav1.TupleKey) o, err := getOrgByName(ctx, store, cfg.Anonymous.OrgName) if err != nil { return tuples, nil } if o.ID != orgID { return tuples, nil } tuple := &openfgav1.TupleKey{ User: zanzana.NewTupleEntry(zanzana.TypeAnonymous, "0", ""), Relation: zanzana.RelationAssignee, Object: object, } tuples[tuple.Object][tuple.String()] = tuple return tuples, nil } } func zanzanaCollector(relations []string) zanzanaTupleCollector { return func(ctx context.Context, client zanzana.Client, object string, namespace 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) { first, err := client.Read(ctx, &authzextv1.ReadRequest{ Namespace: namespace, TupleKey: &authzextv1.ReadRequestTupleKey{ Object: object, Relation: relation, }, }) if err != nil { return nil, err } c := first.ContinuationToken for c != "" { res, err := client.Read(ctx, &authzextv1.ReadRequest{ ContinuationToken: c, Namespace: namespace, TupleKey: &authzextv1.ReadRequestTupleKey{ Object: object, Relation: relation, }, }) if err != nil { return nil, err } c = res.ContinuationToken first.Tuples = append(first.Tuples, res.Tuples...) } return zanzana.ToOpenFGATuples(first.Tuples), nil } out := make(map[string]*openfgav1.TupleKey) for _, r := range relations { tuples, err := list(r) if err != nil { return nil, err } for _, t := range tuples { if zanzana.IsFolderResourceTuple(t.Key) { out[tupleStringWithoutCondition(t.Key)] = t.Key } else { out[t.Key.String()] = t.Key } } } return out, nil } }