grafana-iam: Implement resourcepermission creation (#110246)

* Extract from #108753

Co-Authored-By: mohammad-hamid <mohammad.hamid@grafana.com>

* Tackle create

Co-Authored-By: mohammad-hamid <mohammad.hamid@grafana.com>

* WIP use identity store to resolve role names

* WIP

* create role

* Remove unecessary comments

* comments

* sql templates

* test role insert tplt

* Add tests 😅

* Test permission insert template

* Test permission delete template

* Test assignment_insert template

* Manually test insertion

* Remove delete permissions. This is a create case we don't have permissions for that resource

* generate name handled by the apiserver library

* Remove comment and conversion

* Small renaming nits

* changes from main

* Add storage backend tests

* Add test to sql

* Test role contains a unique permission

* linting

* Account for pr feedback

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Reuse mappers

* Move function to models

* Add check between name and spec resource

* Check if the resource does not already exist

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* fix query

* Check basic roles

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Account for error

* Make struct names consistent

* Nit. I prefer createAndAssignManagedRole

* Remove notifyign

* log errors instead of returning them

* Fix exist query join

* Test errors

* Remove dup

---------

Co-authored-by: mohammad-hamid <mohammad.hamid@grafana.com>
Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
Gabriel MABILLE
2025-09-05 14:22:25 +02:00
committed by GitHub
co-authored by mohammad-hamid Ieva
parent 726c7ba71b
commit 801fde02a7
25 changed files with 1118 additions and 37 deletions
@@ -1,8 +1,15 @@
package resourcepermission
import (
"fmt"
"slices"
"strings"
)
type Mapper interface {
ActionSets() []string
Scope(name string) string
ActionSet(level string) (string, error)
}
type mapper struct {
@@ -28,3 +35,11 @@ func (m mapper) ActionSets() []string {
func (m mapper) Scope(name string) string {
return m.resource + ":uid:" + name
}
func (m mapper) ActionSet(level string) (string, error) {
actionSet := m.resource + ":" + strings.ToLower(level)
if !slices.Contains(m.actionSets, actionSet) {
return "", fmt.Errorf("invalid level (%s): %w", level, errInvalidSpec)
}
return actionSet, nil
}
@@ -1,6 +1,7 @@
package resourcepermission
import (
"context"
"errors"
"fmt"
"sort"
@@ -8,23 +9,40 @@ import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/authlib/types"
v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
idStore "github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
var (
timeNow = func() time.Time { return time.Now() }
errDatabaseHelper = errors.New("failed to get database")
errNotImplemented = errors.New("not supported by this storage backend")
errEmptyName = errors.New("name cannot be empty")
errNameMismatch = errors.New("name mismatch")
errNamespaceMismatch = errors.New("namespace mismatch")
errUnknownGroupResource = errors.New("unknown group/resource")
errNotFound = errors.New("not found")
errConflict = errors.New("conflict")
errInvalidSpec = errors.New("invalid spec")
errInvalidName = errors.New("invalid name")
errInvalidScope = errors.New("invalid scope")
errInvalidNamespace = errors.New("invalid namespace")
defaultLevels = []string{"view", "edit", "admin"}
defaultLevels = []string{"view", "edit", "admin"}
allowedBasicRoles = map[string]bool{"Viewer": true, "Editor": true, "Admin": true}
)
type IdentityStore interface {
GetServiceAccountInternalID(ctx context.Context, ns types.NamespaceInfo, query idStore.GetServiceAccountInternalIDQuery) (*idStore.GetServiceAccountInternalIDResult, error)
GetTeamInternalID(ctx context.Context, ns types.NamespaceInfo, query idStore.GetTeamInternalIDQuery) (*idStore.GetTeamInternalIDResult, error)
GetUserInternalID(ctx context.Context, ns types.NamespaceInfo, query idStore.GetUserInternalIDQuery) (*idStore.GetUserInternalIDResult, error)
}
type ListResourcePermissionsQuery struct {
Scope string
OrgID int64
@@ -32,7 +50,25 @@ type ListResourcePermissionsQuery struct {
// TODO Pagination common.Pagination
}
type flatResourcePermission struct {
type rbacAssignmentCreate struct {
Action string // e.g. "dashboards:edit"
Scope string // e.g. "folders:uid:1"
RoleName string // e.g. "managed:users:1:permissions
SubjectID any // int64 for user/team, string for builtin_role
AssignmentTable string // "user_role", "team_role", or "builtin_role"
AssignmentColumn string // "user_id", "team_id", or "role"
}
func (g *rbacAssignmentCreate) permission() accesscontrol.Permission {
p := accesscontrol.Permission{
Action: g.Action,
Scope: g.Scope,
}
p.Kind, p.Attribute, p.Identifier = accesscontrol.SplitScope(p.Scope)
return p
}
type rbacAssignment struct {
ID int64 `xorm:"id"`
Action string `xorm:"action"`
Scope string `xorm:"scope"`
@@ -44,8 +80,8 @@ type flatResourcePermission struct {
IsServiceAccount bool `xorm:"is_service_account"`
}
// toV0ResourcePermissions converts flatResourcePermission grouped by resource (e.g. {folder.grafana.app, folders, fold1}) to a list of v0alpha1.ResourcePermission
func toV0ResourcePermissions(permsByResource map[groupResourceName][]flatResourcePermission) ([]v0alpha1.ResourcePermission, error) {
// toV0ResourcePermissions converts rbacAssignment grouped by resource (e.g. {folder.grafana.app, folders, fold1}) to a list of v0alpha1.ResourcePermission
func toV0ResourcePermissions(permsByResource map[groupResourceName][]rbacAssignment) ([]v0alpha1.ResourcePermission, error) {
if len(permsByResource) == 0 {
return nil, nil
}
@@ -159,3 +195,24 @@ func (s *ResourcePermSqlBackend) parseScope(scope string) (*groupResourceName, e
Name: parts[2],
}, nil
}
// splitResourceName splits a resource name in the format <group>-<resource>-<name> (e.g. dashboard.grafana.app-dashboards-ad5rwqs) into its components
func (s *ResourcePermSqlBackend) splitResourceName(resourceName string) (Mapper, *groupResourceName, error) {
// e.g. dashboard.grafana.app-dashboards-ad5rwqs
parts := strings.SplitN(resourceName, "-", 3)
if len(parts) != 3 {
return nil, nil, fmt.Errorf("%w: %s", errInvalidName, resourceName)
}
group, resourceType, uid := parts[0], parts[1], parts[2]
mapper, ok := s.mappers[schema.GroupResource{Group: group, Resource: resourceType}]
if !ok {
return nil, nil, fmt.Errorf("%w: %s/%s", errUnknownGroupResource, group, resourceType)
}
return mapper, &groupResourceName{
Group: group,
Resource: resourceType,
Name: uid,
}, nil
}
@@ -20,14 +20,14 @@ func setupBackendNoDB(t *testing.T) *ResourcePermSqlBackend {
func TestToV0ResourcePermissions(t *testing.T) {
t.Run("empty permissions", func(t *testing.T) {
result, err := toV0ResourcePermissions(map[groupResourceName][]flatResourcePermission{})
result, err := toV0ResourcePermissions(map[groupResourceName][]rbacAssignment{})
require.NoError(t, err)
require.Nil(t, result)
})
now := time.Now()
t.Run("multiple permission are sorted by kind, name, verb", func(t *testing.T) {
permissions := map[groupResourceName][]flatResourcePermission{
permissions := map[groupResourceName][]rbacAssignment{
{
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -0,0 +1,8 @@
INSERT INTO {{ .Ident .AssignmentTable }} (org_id, {{ .Ident .AssignmentColumn }}, role_id, created{{ if eq .AssignmentColumn "role" }}, updated{{ end }})
VALUES (
{{ .Arg .OrgID }},
{{ .Arg .SubjectID }},
{{ .Arg .RoleID }},
{{ .Arg .Now }}{{ if eq .AssignmentColumn "role" }},
{{ .Arg .Now }}{{ end }}
)
@@ -0,0 +1,11 @@
INSERT INTO {{ .Ident .PermissionTable }} (role_id, action, scope, created, updated, kind, attribute, identifier)
VALUES (
{{ .Arg $.RoleID }},
{{ .Arg $.Permission.Action }},
{{ .Arg $.Permission.Scope }},
{{ .Arg $.Now }},
{{ .Arg $.Now }},
{{ .Arg $.Permission.Kind }},
{{ .Arg $.Permission.Attribute }},
{{ .Arg $.Permission.Identifier }}
)
@@ -0,0 +1,9 @@
INSERT INTO {{ .Ident .RoleTable }} (version, org_id, uid, name, created, updated)
VALUES (
0,
{{ .Arg .OrgID }},
{{ .Arg .UID }},
{{ .Arg .Name }},
{{ .Arg .Now }},
{{ .Arg .Now }}
)
+237 -16
View File
@@ -2,20 +2,25 @@ package resourcepermission
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"github.com/grafana/authlib/types"
v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore/session"
"github.com/grafana/grafana/pkg/storage/legacysql"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// List
// Get
// getResourcePermissions queries resource permissions based on the provided ListResourcePermissionsQuery and groups them by resource (e.g. {folder.grafana.app, folders, fold1})
func (s *ResourcePermSqlBackend) getResourcePermissions(ctx context.Context, sql *legacysql.LegacyDatabaseHelper, query *ListResourcePermissionsQuery) (map[groupResourceName][]flatResourcePermission, error) {
func (s *ResourcePermSqlBackend) getResourcePermissions(ctx context.Context, sql *legacysql.LegacyDatabaseHelper, query *ListResourcePermissionsQuery) (map[groupResourceName][]rbacAssignment, error) {
rawQuery, args, err := buildListResourcePermissionsQueryFromTemplate(sql, query)
if err != nil {
return nil, err
@@ -32,9 +37,9 @@ func (s *ResourcePermSqlBackend) getResourcePermissions(ctx context.Context, sql
_ = rows.Close()
}()
permissions := make(map[groupResourceName][]flatResourcePermission)
permissions := make(map[groupResourceName][]rbacAssignment)
for rows.Next() {
var perm flatResourcePermission
var perm rbacAssignment
if err := rows.Scan(
&perm.ID, &perm.Action, &perm.Scope, &perm.Created, &perm.Updated, &perm.RoleName,
&perm.SubjectUID, &perm.SubjectType, &perm.IsServiceAccount,
@@ -56,20 +61,13 @@ func (s *ResourcePermSqlBackend) getResourcePermissions(ctx context.Context, sql
// getResourcePermission retrieves a single ResourcePermission by its name in the format <group>-<resource>-<name> (e.g. dashboard.grafana.app-dashboards-ad5rwqs)
func (s *ResourcePermSqlBackend) getResourcePermission(ctx context.Context, sql *legacysql.LegacyDatabaseHelper, ns types.NamespaceInfo, name string) (*v0alpha1.ResourcePermission, error) {
// e.g. dashboard.grafana.app-dashboards-ad5rwqs
parts := strings.SplitN(name, "-", 3)
if len(parts) != 3 {
return nil, fmt.Errorf("%w: %s", errInvalidName, name)
}
group, resourceType, uid := parts[0], parts[1], parts[2]
mapper, ok := s.mappers[schema.GroupResource{Group: group, Resource: resourceType}]
if !ok {
return nil, fmt.Errorf("%w: %s/%s", errUnknownGroupResource, group, resourceType)
mapper, grn, err := s.splitResourceName(name)
if err != nil {
return nil, err
}
resourceQuery := &ListResourcePermissionsQuery{
Scope: mapper.Scope(uid),
Scope: mapper.Scope(grn.Name),
OrgID: ns.OrgID,
ActionSets: mapper.ActionSets(),
}
@@ -96,6 +94,229 @@ func (s *ResourcePermSqlBackend) getResourcePermission(ctx context.Context, sql
// Create
// createAndAssignManagedRole creates a new managed role and assigns it to the given user/team/service account/basic role
func (s *ResourcePermSqlBackend) createAndAssignManagedRole(ctx context.Context, tx *session.SessionTx, dbHelper *legacysql.LegacyDatabaseHelper, orgID int64, assignment rbacAssignmentCreate) (int64, error) {
// Create the managed role
roleUID := accesscontrol.PrefixedRoleUID(fmt.Sprintf("%s:org:%v", assignment.RoleName, orgID))
insertRoleQuery, args, err := buildInsertRoleQuery(dbHelper, orgID, roleUID, assignment.RoleName)
if err != nil {
return 0, err
}
_, err = tx.Exec(ctx, insertRoleQuery, args...)
if err != nil {
s.logger.Error("could not insert new role", "orgID", orgID, "roleName", assignment.RoleName, "error", err.Error())
return 0, fmt.Errorf("could not insert new role")
}
var roleID int64
idQuery := fmt.Sprintf("SELECT id FROM %s WHERE org_id = ? AND name = ?", dbHelper.Table("role"))
err = tx.Get(ctx, &roleID, idQuery, orgID, assignment.RoleName)
if err != nil {
s.logger.Error("could not retrieve id of created role", "orgID", orgID, "roleName", assignment.RoleName, "error", err.Error())
return 0, fmt.Errorf("could not retrieve id of created role")
}
assignQuery, args, err := buildInsertAssignmentQuery(dbHelper, orgID, roleID, assignment)
if err != nil {
return 0, err
}
_, err = tx.Exec(ctx, assignQuery, args...)
if err != nil {
s.logger.Error("could not insert role assignment", "orgID", orgID, "roleName", assignment.RoleName, "subjectID", assignment.SubjectID, "error", err.Error())
return 0, fmt.Errorf("could not insert role assignment")
}
return roleID, nil
}
// storeRbacAssignment ensures that a role exists for the given assignment, creates and assigns it if it doesn't
// and then ensures that the role has the correct permission for the given scope
func (s *ResourcePermSqlBackend) storeRbacAssignment(ctx context.Context, dbHelper *legacysql.LegacyDatabaseHelper, tx *session.SessionTx, orgID int64, assignment rbacAssignmentCreate) error {
// Check if role already exists
var roleID int64
query := fmt.Sprintf("SELECT id FROM %s WHERE org_id = ? AND name = ?", dbHelper.Table("role"))
err := tx.Get(ctx, &roleID, query, orgID, assignment.RoleName)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
s.logger.Error("could not check for existing role", "orgID", orgID, "roleName", assignment.RoleName, "error", err.Error())
return fmt.Errorf("could not check for existing role")
}
// Role doesn't exist, create it
if roleID == 0 {
roleID, err = s.createAndAssignManagedRole(ctx, tx, dbHelper, orgID, assignment)
if err != nil {
return err
}
}
// Add the new permission
insertPermQuery, args, err := buildInsertPermissionQuery(dbHelper, roleID, assignment.permission())
if err != nil {
return err
}
_, err = tx.Exec(ctx, insertPermQuery, args...)
if err != nil {
s.logger.Error("could not insert role permission", "roleID", roleID, "scope", assignment.Scope, "error", err.Error())
return fmt.Errorf("could not insert role permission")
}
return nil
}
// buildRbacAssignments builds the list of assignments (role assignments and permissions) for a given ResourcePermission spec
// It resolves user/team/service account UIDs to internal IDs for the role name and assignee subjectID
func (s *ResourcePermSqlBackend) buildRbacAssignments(ctx context.Context, ns types.NamespaceInfo, mapper Mapper, v0ResourcePerm *v0alpha1.ResourcePermission, rbacScope string) ([]rbacAssignmentCreate, error) {
assignments := make([]rbacAssignmentCreate, 0, len(v0ResourcePerm.Spec.Permissions))
for _, perm := range v0ResourcePerm.Spec.Permissions {
rbacActionSet, err := mapper.ActionSet(perm.Verb)
if err != nil {
return nil, err
}
switch perm.Kind {
case v0alpha1.ResourcePermissionSpecPermissionKindUser:
userID, err := s.identityStore.GetUserInternalID(ctx, ns, legacy.GetUserInternalIDQuery{
UID: perm.Name,
OrgID: ns.OrgID,
})
if err != nil && !strings.Contains(err.Error(), "not found") {
return nil, fmt.Errorf("resolving user %q to internal ID: %w", perm.Name, err)
}
if userID == nil {
return nil, fmt.Errorf("user %q not found: %w", perm.Name, errInvalidSpec)
}
assignments = append(assignments, rbacAssignmentCreate{
RoleName: fmt.Sprintf("managed:users:%d:permissions", userID.ID),
AssignmentTable: "user_role",
AssignmentColumn: "user_id",
SubjectID: fmt.Sprintf("%d", userID.ID),
Action: rbacActionSet,
Scope: rbacScope,
})
case v0alpha1.ResourcePermissionSpecPermissionKindTeam:
teamID, err := s.identityStore.GetTeamInternalID(ctx, ns, legacy.GetTeamInternalIDQuery{
UID: perm.Name,
OrgID: ns.OrgID,
})
if err != nil && !strings.Contains(err.Error(), "not found") {
return nil, fmt.Errorf("resolving team %q to internal ID: %w", perm.Name, err)
}
if teamID == nil {
return nil, fmt.Errorf("team %q not found: %w", perm.Name, errInvalidSpec)
}
assignments = append(assignments, rbacAssignmentCreate{
RoleName: fmt.Sprintf("managed:teams:%d:permissions", teamID.ID),
AssignmentTable: "team_role",
AssignmentColumn: "team_id",
SubjectID: fmt.Sprintf("%d", teamID.ID),
Action: rbacActionSet,
Scope: rbacScope,
})
case v0alpha1.ResourcePermissionSpecPermissionKindServiceAccount:
saID, err := s.identityStore.GetServiceAccountInternalID(ctx, ns, legacy.GetServiceAccountInternalIDQuery{
UID: perm.Name,
OrgID: ns.OrgID,
})
if err != nil && !strings.Contains(err.Error(), "not found") {
return nil, fmt.Errorf("resolving service account %q to internal ID: %w", perm.Name, err)
}
if saID == nil {
return nil, fmt.Errorf("service account %q not found: %w", perm.Name, errInvalidSpec)
}
assignments = append(assignments, rbacAssignmentCreate{
RoleName: fmt.Sprintf("managed:users:%d:permissions", saID.ID),
AssignmentTable: "user_role",
AssignmentColumn: "user_id",
SubjectID: fmt.Sprintf("%d", saID.ID),
Action: rbacActionSet,
Scope: rbacScope,
})
case v0alpha1.ResourcePermissionSpecPermissionKindBasicRole:
if !allowedBasicRoles[perm.Name] {
return nil, fmt.Errorf("invalid basic role %q: %w", perm.Name, errInvalidSpec)
}
assignments = append(assignments, rbacAssignmentCreate{
RoleName: fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(perm.Name)),
AssignmentTable: "builtin_role",
AssignmentColumn: "role",
SubjectID: perm.Name,
Action: rbacActionSet,
Scope: rbacScope,
})
default:
return nil, fmt.Errorf("unknown permission kind: %q: %w", perm.Kind, errInvalidSpec)
}
}
return assignments, nil
}
// existsResourcePermission checks if a resource permission for the given scope already exists in the given organization
func (s *ResourcePermSqlBackend) existsResourcePermission(ctx context.Context, tx *session.SessionTx, dbHelper *legacysql.LegacyDatabaseHelper, orgID int64, scope string) error {
idQuery := fmt.Sprintf(
`SELECT r.id FROM %s AS r INNER JOIN %s AS p ON p.role_id = r.id WHERE r.org_id = ? AND r.name LIKE ? AND p.scope = ? LIMIT 1`,
dbHelper.Table("role"), dbHelper.Table("permission"),
)
roleID := int64(0)
err := tx.Get(ctx, &roleID, idQuery, orgID, "managed:%", scope)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
s.logger.Error("could not check for existing resource permission", "orgID", orgID, "scope", scope, "error", err.Error())
return fmt.Errorf("could not check for existing resource permission")
}
if roleID != 0 {
return errConflict
}
return nil
}
func (s *ResourcePermSqlBackend) createResourcePermission(
ctx context.Context, dbHelper *legacysql.LegacyDatabaseHelper, ns types.NamespaceInfo, mapper Mapper, grn *groupResourceName, v0ResourcePerm *v0alpha1.ResourcePermission,
) (int64, error) {
if v0ResourcePerm == nil {
return 0, fmt.Errorf("resource permission cannot be nil")
}
if len(v0ResourcePerm.Spec.Permissions) == 0 {
return 0, fmt.Errorf("resource permission must have at least one permission: %w", errInvalidSpec)
}
// Validate that the group/resource/name in the name matches the spec
if grn.Group != v0ResourcePerm.Spec.Resource.ApiGroup ||
grn.Resource != v0ResourcePerm.Spec.Resource.Resource ||
grn.Name != v0ResourcePerm.Spec.Resource.Name {
return 0, fmt.Errorf("resource permission name does not match spec: %w", errInvalidSpec)
}
assignments, err := s.buildRbacAssignments(ctx, ns, mapper, v0ResourcePerm, mapper.Scope(grn.Name))
if err != nil {
return 0, err
}
err = dbHelper.DB.GetSqlxSession().WithTransaction(ctx, func(tx *session.SessionTx) error {
// Check if a resource permission for the same resource already exists
if err = s.existsResourcePermission(ctx, tx, dbHelper, ns.OrgID, mapper.Scope(grn.Name)); err != nil {
return err
}
for _, assignment := range assignments {
if err := s.storeRbacAssignment(ctx, dbHelper, tx, ns.OrgID, assignment); err != nil {
return err
}
}
return nil
})
if err != nil {
return 0, err
}
// Return a timestamp as resource version
return timeNow().UnixMilli(), nil
}
// Update
// Delete
@@ -2,18 +2,22 @@ package resourcepermission
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
@@ -31,6 +35,7 @@ func setupBackend(t *testing.T) *ResourcePermSqlBackend {
dbProvider := func(ctx context.Context) (*legacysql.LegacyDatabaseHelper, error) {
return sqlHelper, nil
}
return ProvideStorageBackend(dbProvider)
}
@@ -113,7 +118,7 @@ func setupTestRoles(t *testing.T, store db.DB) {
require.NoError(t, err)
}
func TestResourcePermSqlBackend_getResourcePermission(t *testing.T) {
func TestIntegration_ResourcePermSqlBackend_getResourcePermission(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
@@ -137,9 +142,9 @@ func TestResourcePermSqlBackend_getResourcePermission(t *testing.T) {
resource: "folder.grafana.app-folders-fold1",
orgID: 1,
want: v0alpha1.ResourcePermission{
ObjectMeta: metaV1.ObjectMeta{
ObjectMeta: metav1.ObjectMeta{
Name: "folder.grafana.app-folders-fold1",
CreationTimestamp: metaV1.Time{Time: created},
CreationTimestamp: metav1.Time{Time: created},
ResourceVersion: fmt.Sprint(created.UnixMilli()),
},
TypeMeta: v0alpha1.ResourcePermissionInfo.TypeMeta(),
@@ -201,3 +206,169 @@ func TestResourcePermSqlBackend_getResourcePermission(t *testing.T) {
})
}
}
func TestIntegration_ResourcePermSqlBackend_CreateResourcePermission(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
store := db.InitTestDB(t)
timeNow = func() time.Time {
return time.Date(2025, 8, 28, 17, 13, 0, 0, time.UTC)
}
t.Run("should create resource permission", func(t *testing.T) {
resourcePerm := &v0alpha1.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Name: "folder.grafana.app-folders-fold1",
Namespace: "default",
},
Spec: v0alpha1.ResourcePermissionSpec{
Resource: v0alpha1.ResourcePermissionspecResource{
ApiGroup: "folder.grafana.app",
Resource: "folders",
Name: "fold1",
},
Permissions: []v0alpha1.ResourcePermissionspecPermission{
{
Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole,
Name: "Viewer",
Verb: "admin",
},
{
Kind: v0alpha1.ResourcePermissionSpecPermissionKindUser,
Name: "captain",
Verb: "edit",
},
{
Kind: v0alpha1.ResourcePermissionSpecPermissionKindServiceAccount,
Name: "robot",
Verb: "view",
},
{
Kind: v0alpha1.ResourcePermissionSpecPermissionKindTeam,
Name: "devs",
Verb: "admin",
},
},
},
}
ctx := context.Background()
backend := setupBackend(t)
sqlHelper, _ := backend.dbProvider(ctx)
backend.identityStore = NewFakeIdentityStore(t)
mapper, grn, err := backend.splitResourceName(resourcePerm.Name)
require.NoError(t, err)
rv, err := backend.createResourcePermission(ctx, sqlHelper, types.NamespaceInfo{Value: "default", OrgID: 1}, mapper, grn, resourcePerm)
require.NoError(t, err)
require.Equal(t, timeNow().UnixMilli(), rv)
var (
assigned int
roleID int64
permission accesscontrol.Permission
sess = store.GetSqlxSession()
)
// Check that the roles were created and assigned
err = sess.Get(ctx, &roleID, "SELECT id FROM role WHERE org_id = ? AND name = ?", 1, "managed:users:101:permissions")
require.NoError(t, err)
require.NotZero(t, roleID)
err = sess.Get(ctx, &assigned, "SELECT 1 FROM user_role WHERE org_id = ? AND role_id = ? AND user_id = ?", 1, roleID, "101")
require.NoError(t, err)
require.Equal(t, 1, assigned)
err = sess.Get(ctx, &permission, "SELECT action, scope FROM permission WHERE role_id = ?", roleID)
require.NoError(t, err)
require.Equal(t, "folders:uid:fold1", permission.Scope)
require.Equal(t, "folders:edit", permission.Action)
err = sess.Get(ctx, &roleID, "SELECT id FROM role WHERE org_id = ? AND name = ?", 1, "managed:users:201:permissions")
require.NoError(t, err)
require.NotZero(t, roleID)
err = sess.Get(ctx, &assigned, "SELECT 1 FROM user_role WHERE org_id = ? AND role_id = ? AND user_id = ?", 1, roleID, "201")
require.NoError(t, err)
require.Equal(t, 1, assigned)
err = sess.Get(ctx, &permission, "SELECT action, scope FROM permission WHERE role_id = ?", roleID)
require.NoError(t, err)
require.Equal(t, "folders:uid:fold1", permission.Scope)
require.Equal(t, "folders:view", permission.Action)
err = sess.Get(ctx, &roleID, "SELECT id FROM role WHERE org_id = ? AND name = ?", 1, "managed:teams:301:permissions")
require.NoError(t, err)
require.NotZero(t, roleID)
err = sess.Get(ctx, &assigned, "SELECT 1 FROM team_role WHERE org_id = ? AND role_id = ? AND team_id = ?", 1, roleID, "301")
require.NoError(t, err)
require.Equal(t, 1, assigned)
err = sess.Get(ctx, &permission, "SELECT action, scope FROM permission WHERE role_id = ?", roleID)
require.NoError(t, err)
require.Equal(t, "folders:uid:fold1", permission.Scope)
require.Equal(t, "folders:admin", permission.Action)
err = sess.Get(ctx, &roleID, "SELECT id FROM role WHERE org_id = ? AND name = ?", 1, "managed:builtins:viewer:permissions")
require.NoError(t, err)
require.NotZero(t, roleID)
err = sess.Get(ctx, &assigned, "SELECT 1 FROM builtin_role WHERE org_id = ? AND role = ?", 1, "Viewer")
require.NoError(t, err)
require.Equal(t, 1, assigned)
err = sess.Get(ctx, &permission, "SELECT action, scope FROM permission WHERE role_id = ?", roleID)
require.NoError(t, err)
require.Equal(t, "folders:uid:fold1", permission.Scope)
require.Equal(t, "folders:admin", permission.Action)
})
}
type fakeIdentityStore struct {
t *testing.T
users map[string]int64
serviceAccounts map[string]int64
teams map[string]int64
expectedNs types.NamespaceInfo
}
func NewFakeIdentityStore(t *testing.T) *fakeIdentityStore {
return &fakeIdentityStore{
t: t,
users: map[string]int64{"captain": 101},
serviceAccounts: map[string]int64{"robot": 201},
teams: map[string]int64{"devs": 301},
expectedNs: types.NamespaceInfo{Value: "default"},
}
}
// GetServiceAccountInternalID implements legacy.LegacyIdentityStore.
func (f *fakeIdentityStore) GetServiceAccountInternalID(ctx context.Context, ns types.NamespaceInfo, query legacy.GetServiceAccountInternalIDQuery) (*legacy.GetServiceAccountInternalIDResult, error) {
require.Equal(f.t, f.expectedNs.Value, ns.Value)
id, ok := f.serviceAccounts[query.UID]
if !ok {
return nil, errors.New("not found")
}
return &legacy.GetServiceAccountInternalIDResult{ID: id}, nil
}
// GetTeamInternalID implements legacy.LegacyIdentityStore.
func (f *fakeIdentityStore) GetTeamInternalID(ctx context.Context, ns types.NamespaceInfo, query legacy.GetTeamInternalIDQuery) (*legacy.GetTeamInternalIDResult, error) {
require.Equal(f.t, f.expectedNs.Value, ns.Value)
id, ok := f.teams[query.UID]
if !ok {
return nil, errors.New("not found")
}
return &legacy.GetTeamInternalIDResult{ID: id}, nil
}
// GetUserInternalID implements legacy.LegacyIdentityStore.
func (f *fakeIdentityStore) GetUserInternalID(ctx context.Context, ns types.NamespaceInfo, query legacy.GetUserInternalIDQuery) (*legacy.GetUserInternalIDResult, error) {
require.Equal(f.t, f.expectedNs.Value, ns.Value)
id, ok := f.users[query.UID]
if !ok {
return nil, errors.New("not found")
}
return &legacy.GetUserInternalIDResult{ID: id}, nil
}
@@ -14,6 +14,7 @@ import (
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/infra/log"
idStore "github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
@@ -24,8 +25,9 @@ var (
)
type ResourcePermSqlBackend struct {
dbProvider legacysql.LegacyDatabaseProvider
logger log.Logger
dbProvider legacysql.LegacyDatabaseProvider
identityStore IdentityStore
logger log.Logger
mappers map[schema.GroupResource]Mapper // group/resource -> rbac mapper
reverseMappers map[string]schema.GroupResource // rbac kind -> group/resource
@@ -36,8 +38,9 @@ type ResourcePermSqlBackend struct {
func ProvideStorageBackend(dbProvider legacysql.LegacyDatabaseProvider) *ResourcePermSqlBackend {
return &ResourcePermSqlBackend{
dbProvider: dbProvider,
logger: log.New("resourceperm_storage_backend"),
dbProvider: dbProvider,
identityStore: idStore.NewLegacySQLStores(dbProvider),
logger: log.New("resourceperm_storage_backend"),
mappers: map[schema.GroupResource]Mapper{
{Group: "folder.grafana.app", Resource: "folders"}: NewMapper("folders", defaultLevels),
@@ -136,11 +139,24 @@ func isValidKey(key *resourcepb.ResourceKey, requireName bool) error {
return fmt.Errorf("expecting resource (%s != %s)", key.Resource, gr.Resource)
}
if requireName && key.Name == "" {
return fmt.Errorf("expecting name (uid): %w", errEmptyName)
return fmt.Errorf("expecting name (uid): %w", errInvalidName)
}
return nil
}
func getResourcePermissionFromEvent(event resource.WriteEvent) (*v0alpha1.ResourcePermission, error) {
obj, ok := event.Object.GetRuntimeObject()
if ok && obj != nil {
resourcePermission, ok := obj.(*v0alpha1.ResourcePermission)
if ok {
return resourcePermission, nil
}
}
resourcePermission := &v0alpha1.ResourcePermission{}
err := json.Unmarshal(event.Value, resourcePermission)
return resourcePermission, err
}
func (s *ResourcePermSqlBackend) WriteEvent(ctx context.Context, event resource.WriteEvent) (rv int64, err error) {
ns, err := types.ParseNamespace(event.Key.Namespace)
if err != nil {
@@ -154,8 +170,54 @@ func (s *ResourcePermSqlBackend) WriteEvent(ctx context.Context, event resource.
return 0, apierrors.NewBadRequest(fmt.Sprintf("invalid key %q: %v", event.Key, err.Error()))
}
mapper, grn, err := s.splitResourceName(event.Key.Name)
if err != nil {
return 0, apierrors.NewBadRequest(fmt.Sprintf("invalid resource name %q: %v", event.Key.Name, err.Error()))
}
if grn.Name == "" {
return 0, fmt.Errorf("resource name cannot be empty: %w", errInvalidName)
}
switch event.Type {
case resourcepb.WatchEvent_ADDED:
{
var v0resourceperm *v0alpha1.ResourcePermission
v0resourceperm, err = getResourcePermissionFromEvent(event)
if err != nil {
return 0, err
}
if v0resourceperm.Name != event.Key.Name {
return 0, apierrors.NewBadRequest(
fmt.Sprintf("resource permission name %q != %q: %v", event.Key.Name, v0resourceperm.Name, errNameMismatch.Error()),
)
}
if v0resourceperm.Namespace != ns.Value {
return 0, apierrors.NewBadRequest(
fmt.Sprintf("namespace %q != %q: %v", ns.Value, v0resourceperm.Namespace, errNamespaceMismatch.Error()),
)
}
dbHelper, err := s.dbProvider(ctx)
if err != nil {
return 0, err
}
rv, err = s.createResourcePermission(ctx, dbHelper, ns, mapper, grn, v0resourceperm)
if err != nil {
if errors.Is(err, errInvalidSpec) || errors.Is(err, errInvalidName) {
return 0, apierrors.NewBadRequest(err.Error())
}
if errors.Is(err, errConflict) {
return 0, apierrors.NewConflict(v0alpha1.ResourcePermissionInfo.GroupResource(), event.Key.Name, err)
}
return 0, err
}
}
default:
return 0, fmt.Errorf("unsupported event type: %v", event.Type)
}
return rv, nil
}
@@ -7,8 +7,13 @@ import (
"time"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
@@ -108,3 +113,219 @@ func TestIntegration_ResourcePermSqlBackend_ReadResource(t *testing.T) {
require.Equal(t, "edit", permission.Spec.Permissions[3].Verb)
})
}
func TestWriteEvent_Add(t *testing.T) {
store := db.InitTestDB(t)
timeNow = func() time.Time {
return time.Date(2025, 8, 28, 17, 13, 0, 0, time.UTC)
}
sqlHelper := &legacysql.LegacyDatabaseHelper{
DB: store,
Table: func(name string) string { return name },
}
dbProvider := func(ctx context.Context) (*legacysql.LegacyDatabaseHelper, error) {
return sqlHelper, nil
}
t.Run("should error with invalid namespace", func(t *testing.T) {
backend := ProvideStorageBackend(dbProvider)
rv, err := backend.WriteEvent(context.Background(), resource.WriteEvent{
Type: resourcepb.WatchEvent_ADDED,
Key: &resourcepb.ResourceKey{Name: "folder.grafana.app-folders-fold1", Namespace: "invalid"},
})
require.Zero(t, rv)
require.NotNil(t, err)
require.Contains(t, err.Error(), "requires a valid namespace")
})
t.Run("should error if there is no permission", func(t *testing.T) {
backend := ProvideStorageBackend(dbProvider)
resourcePerm, err := utils.MetaAccessor(&v0alpha1.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Name: "folder.grafana.app-folders-fold1",
Namespace: "default",
},
Spec: v0alpha1.ResourcePermissionSpec{
Resource: v0alpha1.ResourcePermissionspecResource{
ApiGroup: "folder.grafana.app",
Resource: "folders",
Name: "fold1",
},
},
})
require.NoError(t, err)
gr := v0alpha1.ResourcePermissionInfo.GroupResource()
rv, err := backend.WriteEvent(context.Background(), resource.WriteEvent{
Type: resourcepb.WatchEvent_ADDED,
Key: &resourcepb.ResourceKey{Group: gr.Group, Resource: gr.Resource, Name: "folder.grafana.app-folders-fold1", Namespace: "default"},
Object: resourcePerm,
})
require.Zero(t, rv)
require.NotNil(t, err)
require.Contains(t, err.Error(), errInvalidSpec.Error())
})
t.Run("should error if name and spec do not match", func(t *testing.T) {
backend := ProvideStorageBackend(dbProvider)
resourcePerm, err := utils.MetaAccessor(&v0alpha1.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Name: "folder.grafana.app-folders-fold1",
Namespace: "default",
},
Spec: v0alpha1.ResourcePermissionSpec{
Resource: v0alpha1.ResourcePermissionspecResource{
ApiGroup: "folder.grafana.app",
Resource: "folders",
Name: "fold2",
},
Permissions: []v0alpha1.ResourcePermissionspecPermission{
{
Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole,
Name: "Viewer",
Verb: "Admin",
},
},
},
})
require.NoError(t, err)
gr := v0alpha1.ResourcePermissionInfo.GroupResource()
rv, err := backend.WriteEvent(context.Background(), resource.WriteEvent{
Type: resourcepb.WatchEvent_ADDED,
Key: &resourcepb.ResourceKey{Group: gr.Group, Resource: gr.Resource, Name: "folder.grafana.app-folders-fold1", Namespace: "default"},
Object: resourcePerm,
})
require.Zero(t, rv)
require.NotNil(t, err)
require.Contains(t, err.Error(), errInvalidSpec.Error())
})
t.Run("should error if resource name is empty", func(t *testing.T) {
backend := ProvideStorageBackend(dbProvider)
resourcePerm, err := utils.MetaAccessor(&v0alpha1.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Name: "folder.grafana.app-folders-",
Namespace: "default",
},
Spec: v0alpha1.ResourcePermissionSpec{
Resource: v0alpha1.ResourcePermissionspecResource{
ApiGroup: "folder.grafana.app",
Resource: "folders",
Name: "",
},
Permissions: []v0alpha1.ResourcePermissionspecPermission{
{
Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole,
Name: "Viewer",
Verb: "Admin",
},
},
},
})
require.NoError(t, err)
gr := v0alpha1.ResourcePermissionInfo.GroupResource()
rv, err := backend.WriteEvent(context.Background(), resource.WriteEvent{
Type: resourcepb.WatchEvent_ADDED,
Key: &resourcepb.ResourceKey{Group: gr.Group, Resource: gr.Resource, Name: "folder.grafana.app-folders-", Namespace: "default"},
Object: resourcePerm,
})
require.Zero(t, rv)
require.NotNil(t, err)
require.Contains(t, err.Error(), errInvalidName.Error())
})
t.Run("should error if the resource is unknown", func(t *testing.T) {
backend := ProvideStorageBackend(dbProvider)
resourcePerm, err := utils.MetaAccessor(&v0alpha1.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Name: "unknown.grafana.app-unknown-ukn1",
Namespace: "default",
},
Spec: v0alpha1.ResourcePermissionSpec{
Resource: v0alpha1.ResourcePermissionspecResource{
ApiGroup: "unknown.grafana.app",
Resource: "unknown",
Name: "ukn1",
},
Permissions: []v0alpha1.ResourcePermissionspecPermission{
{
Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole,
Name: "Viewer",
Verb: "Admin",
},
},
},
})
require.NoError(t, err)
gr := v0alpha1.ResourcePermissionInfo.GroupResource()
rv, err := backend.WriteEvent(context.Background(), resource.WriteEvent{
Type: resourcepb.WatchEvent_ADDED,
Key: &resourcepb.ResourceKey{Group: gr.Group, Resource: gr.Resource, Name: "unknown.grafana.app-unknown-ukn1", Namespace: "default"},
Object: resourcePerm,
})
require.Zero(t, rv)
require.NotNil(t, err)
require.Contains(t, err.Error(), errUnknownGroupResource.Error())
})
t.Run("should work with valid resource permission", func(t *testing.T) {
backend := ProvideStorageBackend(dbProvider)
backend.identityStore = NewFakeIdentityStore(t)
resourcePerm, err := utils.MetaAccessor(&v0alpha1.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{
Name: "folder.grafana.app-folders-fold1",
Namespace: "default",
},
Spec: v0alpha1.ResourcePermissionSpec{
Resource: v0alpha1.ResourcePermissionspecResource{
ApiGroup: "folder.grafana.app",
Resource: "folders",
Name: "fold1",
},
Permissions: []v0alpha1.ResourcePermissionspecPermission{
{
Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole,
Name: "Viewer",
Verb: "Admin",
},
},
},
})
require.NoError(t, err)
gr := v0alpha1.ResourcePermissionInfo.GroupResource()
rv, err := backend.WriteEvent(context.Background(), resource.WriteEvent{
Type: resourcepb.WatchEvent_ADDED,
Key: &resourcepb.ResourceKey{Group: gr.Group, Resource: gr.Resource, Name: "folder.grafana.app-folders-fold1", Namespace: "default"},
Object: resourcePerm,
})
require.NoError(t, err)
require.Equal(t, timeNow().UnixMilli(), rv)
t.Run("Should error on duplicate add", func(t *testing.T) {
rv, err := backend.WriteEvent(context.Background(), resource.WriteEvent{
Type: resourcepb.WatchEvent_ADDED,
Key: &resourcepb.ResourceKey{Group: gr.Group, Resource: gr.Resource, Name: "folder.grafana.app-folders-fold1", Namespace: "default"},
Object: resourcePerm,
})
require.NotNil(t, err)
require.Contains(t, err.Error(), errConflict.Error())
require.Zero(t, rv)
})
})
}
@@ -4,7 +4,9 @@ import (
"embed"
"fmt"
"text/template"
"time"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
@@ -16,6 +18,9 @@ var (
sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `queries/*.sql`))
roleInsertTplt = mustTemplate("role_insert.sql")
assignmentInsertTplt = mustTemplate("assignment_insert.sql")
permissionInsertTplt = mustTemplate("permission_insert.sql")
resourcePermissionsQueryTplt = mustTemplate("resource_permission_query.sql")
)
@@ -77,6 +82,99 @@ func buildListResourcePermissionsQueryFromTemplate(sql *legacysql.LegacyDatabase
// Create
type insertRoleTemplate struct {
sqltemplate.SQLTemplate
RoleTable string
OrgID int64
UID string
Name string
Now string
}
func (t insertRoleTemplate) Validate() error {
return nil
}
func buildInsertRoleQuery(dbHelper *legacysql.LegacyDatabaseHelper, orgID int64, uid string, name string) (string, []any, error) {
req := insertRoleTemplate{
SQLTemplate: sqltemplate.New(dbHelper.DialectForDriver()),
RoleTable: dbHelper.Table("role"),
OrgID: orgID,
UID: uid,
Name: name,
Now: timeNow().Format(time.DateTime),
}
rawQuery, err := sqltemplate.Execute(roleInsertTplt, req)
if err != nil {
return "", nil, fmt.Errorf("rendering sql template: %w", err)
}
return rawQuery, req.GetArgs(), nil
}
type insertAssignmentTemplate struct {
sqltemplate.SQLTemplate
AssignmentTable string
AssignmentColumn string
RoleID int64
OrgID int64
SubjectID any // int64 or string
Now string
}
func (t insertAssignmentTemplate) Validate() error {
if t.AssignmentTable == "" {
return fmt.Errorf("assignment table is required")
}
if t.AssignmentColumn == "" {
return fmt.Errorf("assignment column is required")
}
return nil
}
func buildInsertAssignmentQuery(dbHelper *legacysql.LegacyDatabaseHelper, orgID int64, roleID int64, assignment rbacAssignmentCreate) (string, []any, error) {
req := insertAssignmentTemplate{
SQLTemplate: sqltemplate.New(dbHelper.DialectForDriver()),
AssignmentTable: dbHelper.Table(assignment.AssignmentTable),
AssignmentColumn: assignment.AssignmentColumn,
RoleID: roleID,
OrgID: orgID,
SubjectID: assignment.SubjectID,
Now: timeNow().Format(time.DateTime),
}
rawQuery, err := sqltemplate.Execute(assignmentInsertTplt, req)
if err != nil {
return "", nil, fmt.Errorf("rendering sql template: %w", err)
}
return rawQuery, req.GetArgs(), nil
}
type insertPermissionTemplate struct {
sqltemplate.SQLTemplate
PermissionTable string
RoleID int64
Permission accesscontrol.Permission
Now string
}
func (t insertPermissionTemplate) Validate() error {
return nil
}
func buildInsertPermissionQuery(dbHelper *legacysql.LegacyDatabaseHelper, roleID int64, permission accesscontrol.Permission) (string, []any, error) {
req := insertPermissionTemplate{
SQLTemplate: sqltemplate.New(dbHelper.DialectForDriver()),
PermissionTable: dbHelper.Table("permission"),
RoleID: roleID,
Permission: permission,
Now: timeNow().Format(time.DateTime),
}
rawQuery, err := sqltemplate.Execute(permissionInsertTplt, req)
if err != nil {
return "", nil, fmt.Errorf("rendering sql template: %w", err)
}
return rawQuery, req.GetArgs(), nil
}
// Update
// Delete
@@ -4,18 +4,58 @@ import (
"testing"
"text/template"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/storage/legacysql"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks"
)
func TestListResourcePermissionsQuery(t *testing.T) {
func TestTemplates(t *testing.T) {
nodb := &legacysql.LegacyDatabaseHelper{
Table: func(n string) string {
return "grafana." + n
},
}
getInsertRole := func(orgID int64, name string, displayName string) sqltemplate.SQLTemplate {
v := insertRoleTemplate{
SQLTemplate: sqltemplate.New(nodb.DialectForDriver()),
RoleTable: nodb.Table("role"),
OrgID: orgID,
UID: name,
Name: displayName,
Now: "2025-08-27 21:35:00",
}
v.SQLTemplate = mocks.NewTestingSQLTemplate()
return &v
}
getInsertPermission := func(roleID int64, permission accesscontrol.Permission) sqltemplate.SQLTemplate {
v := insertPermissionTemplate{
SQLTemplate: sqltemplate.New(nodb.DialectForDriver()),
PermissionTable: nodb.Table("permission"),
RoleID: roleID,
Permission: permission,
Now: "2025-08-27 21:35:00",
}
v.SQLTemplate = mocks.NewTestingSQLTemplate()
return &v
}
getInsertAssignment := func(orgID int64, roleID int64, assignment rbacAssignmentCreate) sqltemplate.SQLTemplate {
v := insertAssignmentTemplate{
SQLTemplate: sqltemplate.New(nodb.DialectForDriver()),
AssignmentTable: nodb.Table(assignment.AssignmentTable),
AssignmentColumn: assignment.AssignmentColumn,
RoleID: roleID,
OrgID: orgID,
SubjectID: assignment.SubjectID,
Now: "2025-08-27 21:35:00",
}
v.SQLTemplate = mocks.NewTestingSQLTemplate()
return &v
}
getListResourcePermissionsQuery := func(q *ListResourcePermissionsQuery) sqltemplate.SQLTemplate {
v := listResourcePermissionsQueryTemplate{
SQLTemplate: sqltemplate.New(nodb.DialectForDriver()),
@@ -37,6 +77,50 @@ func TestListResourcePermissionsQuery(t *testing.T) {
RootDir: "testdata",
SQLTemplatesFS: sqlTemplatesFS,
Templates: map[*template.Template][]mocks.TemplateTestCase{
roleInsertTplt: {
{
Name: "insert_role",
Data: getInsertRole(
8,
accesscontrol.PrefixedRoleUID("managed:builtins:editor:1:permissions"+":org:8"),
"managed:builtins:editor:1:permissions",
),
},
},
permissionInsertTplt: {
{
Name: "insert_permission",
Data: getInsertPermission(23, accesscontrol.Permission{
Action: "dashboards:view",
Scope: "dashboard:uid:dash1",
Kind: "dashboard",
Attribute: "uid",
Identifier: "dash1",
}),
},
},
assignmentInsertTplt: {
{
Name: "insert user assignment",
Data: getInsertAssignment(8, 23, rbacAssignmentCreate{
SubjectID: 5,
AssignmentTable: "user_role",
AssignmentColumn: "user_id",
Action: "dashboards:edit",
Scope: "dashboard:uid:dash1",
}),
},
{
Name: "insert basic role assignment",
Data: getInsertAssignment(74, 96, rbacAssignmentCreate{
SubjectID: "Viewer",
AssignmentTable: "builtin_role",
AssignmentColumn: "role",
Action: "dashboards:admin",
Scope: "dashboard:uid:dash2",
}),
},
},
resourcePermissionsQueryTplt: {
{
Name: "basic_query",
@@ -0,0 +1,19 @@
apiVersion: iam.grafana.app/v0alpha1
kind: ResourcePermission
metadata:
name: dashboard.grafana.app-dashboards-ad5rwqs
spec:
resource:
apiGroup: dashboard.grafana.app
resource: dashboards
name: ad5rwqs
permissions:
- kind: User
name: few8wwtp1si68e
verbs:
- edit
- kind: BasicRole
name: Editor
verbs:
- view
@@ -0,0 +1,8 @@
INSERT INTO `grafana`.`builtin_role` (org_id, `role`, role_id, created, updated)
VALUES (
74,
'Viewer',
96,
'2025-08-27 21:35:00',
'2025-08-27 21:35:00'
)
@@ -0,0 +1,7 @@
INSERT INTO `grafana`.`user_role` (org_id, `user_id`, role_id, created)
VALUES (
8,
5,
23,
'2025-08-27 21:35:00'
)
@@ -0,0 +1,11 @@
INSERT INTO `grafana`.`permission` (role_id, action, scope, created, updated, kind, attribute, identifier)
VALUES (
23,
'dashboards:view',
'dashboard:uid:dash1',
'2025-08-27 21:35:00',
'2025-08-27 21:35:00',
'dashboard',
'uid',
'dash1'
)
@@ -0,0 +1,9 @@
INSERT INTO `grafana`.`role` (version, org_id, uid, name, created, updated)
VALUES (
0,
8,
'managed_UEjD8KEQ8e2wzcugTRFOM2179aY',
'managed:builtins:editor:1:permissions',
'2025-08-27 21:35:00',
'2025-08-27 21:35:00'
)
@@ -0,0 +1,8 @@
INSERT INTO "grafana"."builtin_role" (org_id, "role", role_id, created, updated)
VALUES (
74,
'Viewer',
96,
'2025-08-27 21:35:00',
'2025-08-27 21:35:00'
)
@@ -0,0 +1,7 @@
INSERT INTO "grafana"."user_role" (org_id, "user_id", role_id, created)
VALUES (
8,
5,
23,
'2025-08-27 21:35:00'
)
@@ -0,0 +1,11 @@
INSERT INTO "grafana"."permission" (role_id, action, scope, created, updated, kind, attribute, identifier)
VALUES (
23,
'dashboards:view',
'dashboard:uid:dash1',
'2025-08-27 21:35:00',
'2025-08-27 21:35:00',
'dashboard',
'uid',
'dash1'
)
@@ -0,0 +1,9 @@
INSERT INTO "grafana"."role" (version, org_id, uid, name, created, updated)
VALUES (
0,
8,
'managed_UEjD8KEQ8e2wzcugTRFOM2179aY',
'managed:builtins:editor:1:permissions',
'2025-08-27 21:35:00',
'2025-08-27 21:35:00'
)
@@ -0,0 +1,8 @@
INSERT INTO "grafana"."builtin_role" (org_id, "role", role_id, created, updated)
VALUES (
74,
'Viewer',
96,
'2025-08-27 21:35:00',
'2025-08-27 21:35:00'
)
@@ -0,0 +1,7 @@
INSERT INTO "grafana"."user_role" (org_id, "user_id", role_id, created)
VALUES (
8,
5,
23,
'2025-08-27 21:35:00'
)
@@ -0,0 +1,11 @@
INSERT INTO "grafana"."permission" (role_id, action, scope, created, updated, kind, attribute, identifier)
VALUES (
23,
'dashboards:view',
'dashboard:uid:dash1',
'2025-08-27 21:35:00',
'2025-08-27 21:35:00',
'dashboard',
'uid',
'dash1'
)
@@ -0,0 +1,9 @@
INSERT INTO "grafana"."role" (version, org_id, uid, name, created, updated)
VALUES (
0,
8,
'managed_UEjD8KEQ8e2wzcugTRFOM2179aY',
'managed:builtins:editor:1:permissions',
'2025-08-27 21:35:00',
'2025-08-27 21:35:00'
)