diff --git a/pkg/services/accesscontrol/dualwrite/collectors.go b/pkg/services/accesscontrol/dualwrite/collectors.go index 3607675f25c..a48219bab0a 100644 --- a/pkg/services/accesscontrol/dualwrite/collectors.go +++ b/pkg/services/accesscontrol/dualwrite/collectors.go @@ -17,7 +17,7 @@ func teamMembershipCollector(store db.DB) legacyTupleCollector { 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 org_id = ? + WHERE t.org_id = ? ` type membership struct { @@ -109,7 +109,7 @@ func folderTreeCollector(store db.DB) legacyTupleCollector { } } -// managedPermissionsCollector collects managed permissions into provided tuple map. +// 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 { @@ -195,6 +195,99 @@ func tupleStringWithoutCondition(tuple *openfgav1.TupleKey) string { return s } +// basicRolePermissionsCollector collects permissions for basic roles +func basicRolePermissionsCollector(store db.DB) legacyTupleCollector { + return func(ctx context.Context, _ int64) (map[string]map[string]*openfgav1.TupleKey, error) { + const 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.name LIKE 'basic:%' + ` + 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 { + subject := zanzana.NewTupleEntry(zanzana.TypeRole, p.RoleUID, "assignee") + + 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) + } + + tuples[tuple.Object][tuple.String()] = tuple + } + + return tuples, nil + } +} + +// basicRoleBindingsCollects 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, 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 = ? + AND NOT u.is_service_account + ` + // FIXME: handle service admin role + type Binding struct { + UserUID string `xorm:"user_uid"` + 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 { + subject := zanzana.NewTupleEntry(zanzana.TypeUser, b.UserUID, "") + + tuple := &openfgav1.TupleKey{ + User: subject, + 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 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 diff --git a/pkg/services/accesscontrol/dualwrite/reconciler.go b/pkg/services/accesscontrol/dualwrite/reconciler.go index d0e4594933f..82364c8c259 100644 --- a/pkg/services/accesscontrol/dualwrite/reconciler.go +++ b/pkg/services/accesscontrol/dualwrite/reconciler.go @@ -64,6 +64,18 @@ func NewZanzanaReconciler(cfg *setting.Cfg, client zanzana.Client, store db.DB, zanzanaCollector(zanzana.ResourceRelations), client, ), + newResourceReconciler( + "basic role permissions", + basicRolePermissionsCollector(store), + zanzanaCollector(zanzana.FolderRelations), + client, + ), + newResourceReconciler( + "basic role bindings", + basicRoleBindingsCollector(store), + zanzanaCollector([]string{zanzana.RelationAssignee}), + client, + ), }, } } diff --git a/pkg/services/authz/zanzana/translations.go b/pkg/services/authz/zanzana/translations.go index 150f9e25bc1..8432614c7db 100644 --- a/pkg/services/authz/zanzana/translations.go +++ b/pkg/services/authz/zanzana/translations.go @@ -5,6 +5,22 @@ import ( folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" ) +const ( + roleGrafanaAdmin = "Grafana Admin" + roleAdmin = "Admin" + roleEditor = "Editor" + roleViewer = "Viewer" + roleNone = "None" +) + +var basicRolesTranslations = map[string]string{ + roleGrafanaAdmin: "basic_grafana_admin", + roleAdmin: "basic_admin", + roleEditor: "basic_editor", + roleViewer: "basic_viewer", + roleNone: "basic_none", +} + type resourceTranslation struct { typ string group string diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index ac951b79ba4..2001d248780 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -73,19 +73,6 @@ const ( KindFolders string = "folders" ) -const ( - RoleGrafanaAdmin = "Grafana Admin" - RoleAdmin = "Admin" - RoleEditor = "Editor" - RoleViewer = "Viewer" - RoleNone = "None" - - BasicRolePrefix = "basic:" - BasicRoleUIDPrefix = "basic_" - - GlobalOrgID = 0 -) - var ( ToAuthzExtTupleKey = common.ToAuthzExtTupleKey ToAuthzExtTupleKeys = common.ToAuthzExtTupleKeys @@ -98,11 +85,11 @@ var ( ToOpenFGATupleKeyWithoutCondition = common.ToOpenFGATupleKeyWithoutCondition ) -// NewTupleEntry constructs new openfga entry type:id[#relation]. -// Relation allows to specify group of users (subjects) related to type:id +// NewTupleEntry constructs new openfga entry type:name[#relation]. +// Relation allows to specify group of users (subjects) related to type:name // (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) +func NewTupleEntry(objectType, name, relation string) string { + obj := fmt.Sprintf("%s:%s", objectType, name) if relation != "" { obj = fmt.Sprintf("%s#%s", obj, relation) } @@ -121,6 +108,10 @@ func TranslateToResourceTuple(subject string, action, kind, name string) (*openf return nil, false } + if name == "*" { + return common.NewNamespaceResourceTuple(subject, m.relation, translation.group, translation.resource), true + } + if translation.typ == TypeResource { return common.NewResourceTuple(subject, m.relation, translation.group, translation.resource, name), true } @@ -174,3 +165,7 @@ func TranslateToCheckRequest(namespace, action, kind, folder, name string) (*aut return req, true } + +func TranslateBasicRole(name string) string { + return basicRolesTranslations[name] +}