diff --git a/pkg/registry/apis/iam/resourcepermission/mapper.go b/pkg/registry/apis/iam/resourcepermission/mapper.go new file mode 100644 index 00000000000..acb5f889e8b --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/mapper.go @@ -0,0 +1,30 @@ +package resourcepermission + +type Mapper interface { + ActionSets() []string + Scope(name string) string +} + +type mapper struct { + resource string + actionSets []string +} + +func NewMapper(resource string, levels []string) Mapper { + sets := make([]string, 0, len(levels)) + for _, level := range levels { + sets = append(sets, resource+":"+level) + } + return mapper{ + resource: resource, + actionSets: sets, + } +} + +func (m mapper) ActionSets() []string { + return m.actionSets +} + +func (m mapper) Scope(name string) string { + return m.resource + ":uid:" + name +} diff --git a/pkg/registry/apis/iam/resourcepermission/models.go b/pkg/registry/apis/iam/resourcepermission/models.go index 7a70d9893a7..82e06fe1ece 100644 --- a/pkg/registry/apis/iam/resourcepermission/models.go +++ b/pkg/registry/apis/iam/resourcepermission/models.go @@ -3,9 +3,159 @@ package resourcepermission import ( "errors" "fmt" + "sort" + "strings" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" ) var ( - errNotImplemented = errors.New("not supported by this storage backend") - errEmptyName = fmt.Errorf("name cannot be empty") + errDatabaseHelper = errors.New("failed to get database") + errNotImplemented = errors.New("not supported by this storage backend") + errEmptyName = errors.New("name cannot be empty") + errUnknownGroupResource = errors.New("unknown group/resource") + errNotFound = errors.New("not found") + errInvalidName = errors.New("invalid name") + errInvalidScope = errors.New("invalid scope") + errInvalidNamespace = errors.New("invalid namespace") + + defaultLevels = []string{"view", "edit", "admin"} ) + +type ListResourcePermissionsQuery struct { + Scope string + OrgID int64 + ActionSets []string + // TODO Pagination common.Pagination +} + +type flatResourcePermission struct { + ID int64 `xorm:"id"` + Action string `xorm:"action"` + Scope string `xorm:"scope"` + Created time.Time `xorm:"created"` + Updated time.Time `xorm:"updated"` + RoleName string `xorm:"role_name"` + SubjectUID string `xorm:"subject_uid"` + SubjectType string `xorm:"subject_type"` // 'user', 'team', or 'builtin_role' + 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) { + if len(permsByResource) == 0 { + return nil, nil + } + + resourcePermissions := make([]v0alpha1.ResourcePermission, 0, len(permsByResource)) + for resource, perms := range permsByResource { + specs := make([]v0alpha1.ResourcePermissionspecPermission, 0, len(perms)) + + var ( + created = time.Now() + updated = time.Now() + permissionKind v0alpha1.ResourcePermissionSpecPermissionKind + ) + for i := range perms { + // Find the most recent updated time + if i == 0 || perms[i].Updated.After(updated) { + updated = perms[i].Updated + } + // Find the oldest created time + if i == 0 || perms[i].Created.Before(created) { + created = perms[i].Created + } + perm := perms[i] + switch perm.SubjectType { + case "user": + if perm.IsServiceAccount { + permissionKind = v0alpha1.ResourcePermissionSpecPermissionKindServiceAccount + } else { + permissionKind = v0alpha1.ResourcePermissionSpecPermissionKindUser + } + case "team": + permissionKind = v0alpha1.ResourcePermissionSpecPermissionKindTeam + case "builtin_role": + permissionKind = v0alpha1.ResourcePermissionSpecPermissionKindBasicRole + default: + return nil, errors.New("unknown subject type: " + perm.SubjectType) + } + + actionParts := strings.SplitN(perm.Action, ":", 2) + if len(actionParts) < 2 || actionParts[1] == "" { + return nil, fmt.Errorf("invalid action format: %s", perm.Action) + } + verb := actionParts[1] + specs = append(specs, v0alpha1.ResourcePermissionspecPermission{ + Kind: permissionKind, + Name: perm.SubjectUID, + Verb: verb, + }) + } + + sort.Slice(specs, func(i, j int) bool { + if specs[i].Kind != specs[j].Kind { + return specs[i].Kind < specs[j].Kind + } + if specs[i].Name != specs[j].Name { + return specs[i].Name < specs[j].Name + } + return specs[i].Verb < specs[j].Verb + }) + + r := v0alpha1.ResourcePermission{ + TypeMeta: v0alpha1.ResourcePermissionInfo.TypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + Name: resource.string(), + ResourceVersion: fmt.Sprint(updated.UnixMilli()), + CreationTimestamp: metav1.NewTime(created.UTC()), + }, + Spec: v0alpha1.ResourcePermissionSpec{ + Resource: resource.v0alpha1(), + Permissions: specs, + }, + } + r.SetUpdateTimestamp(updated.UTC()) + resourcePermissions = append(resourcePermissions, r) + } + + return resourcePermissions, nil +} + +type groupResourceName struct { + Group string + Resource string + Name string +} + +func (g *groupResourceName) string() string { + return g.Group + "-" + g.Resource + "-" + g.Name +} + +func (g *groupResourceName) v0alpha1() v0alpha1.ResourcePermissionspecResource { + return v0alpha1.ResourcePermissionspecResource{ + ApiGroup: g.Group, + Resource: g.Resource, + Name: g.Name, + } +} + +// parseScope parses a scope string (e.g. folders:uid:1) into a groupResourceName (e.g. {folder.grafana.app, folders, fold1}). +func (s *ResourcePermSqlBackend) parseScope(scope string) (*groupResourceName, error) { + parts := strings.SplitN(scope, ":", 3) + if len(parts) != 3 { + return nil, fmt.Errorf("%w: %s", errInvalidScope, scope) + } + gr, ok := s.reverseMappers[parts[0]] + if !ok { + return nil, fmt.Errorf("%w: %s", errUnknownGroupResource, parts[0]) + } + return &groupResourceName{ + Group: gr.Group, + Resource: gr.Resource, + Name: parts[2], + }, nil +} diff --git a/pkg/registry/apis/iam/resourcepermission/models_test.go b/pkg/registry/apis/iam/resourcepermission/models_test.go new file mode 100644 index 00000000000..316252dfe47 --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/models_test.go @@ -0,0 +1,155 @@ +package resourcepermission + +import ( + "context" + "testing" + "time" + + v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/stretchr/testify/require" +) + +// setupBackendNoDB sets up a ResourcePermSqlBackend with a no-op dbProvider for tests that do not require DB access. +func setupBackendNoDB(t *testing.T) *ResourcePermSqlBackend { + noProvider := func(ctx context.Context) (*legacysql.LegacyDatabaseHelper, error) { + return nil, nil + } + return ProvideStorageBackend(noProvider) +} + +func TestToV0ResourcePermissions(t *testing.T) { + t.Run("empty permissions", func(t *testing.T) { + result, err := toV0ResourcePermissions(map[groupResourceName][]flatResourcePermission{}) + 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{ + { + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "test123", + }: { + { + ID: 1, + Action: "dashboards:admin", + Scope: "dashboards:uid:test123", + Created: now, + Updated: now, + RoleName: "managed:role1", + SubjectUID: "testuser", + SubjectType: "user", + IsServiceAccount: false, + }, + { + ID: 2, + Action: "dashboards:edit", + Scope: "dashboards:uid:test123", + Created: now, + Updated: now, + RoleName: "managed:role1", + SubjectUID: "sa1", + SubjectType: "user", + IsServiceAccount: true, + }, + { + ID: 3, + Action: "dashboards:view", + Scope: "dashboards:uid:test123", + Created: now, + Updated: now, + RoleName: "managed:role1", + SubjectUID: "testteam", + SubjectType: "team", + IsServiceAccount: false, + }, + { + ID: 4, + Action: "dashboards:view", + Scope: "dashboards:uid:test123", + Created: now, + Updated: now, + RoleName: "managed:role1", + SubjectUID: "Viewer", + SubjectType: "builtin_role", + IsServiceAccount: false, + }, + }, + } + + result, err := toV0ResourcePermissions(permissions) + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, result, 1) + require.Len(t, result[0].Spec.Permissions, 4) + + perm := result[0].Spec.Permissions[0] + require.Equal(t, v0alpha1.ResourcePermissionSpecPermissionKindBasicRole, perm.Kind) + require.Equal(t, "Viewer", perm.Name) + require.Equal(t, "view", perm.Verb) + perm = result[0].Spec.Permissions[1] + require.Equal(t, v0alpha1.ResourcePermissionSpecPermissionKindServiceAccount, perm.Kind) + require.Equal(t, "sa1", perm.Name) + require.Equal(t, "edit", perm.Verb) + perm = result[0].Spec.Permissions[2] + require.Equal(t, v0alpha1.ResourcePermissionSpecPermissionKindTeam, perm.Kind) + require.Equal(t, "testteam", perm.Name) + require.Equal(t, "view", perm.Verb) + perm = result[0].Spec.Permissions[3] + require.Equal(t, v0alpha1.ResourcePermissionSpecPermissionKindUser, perm.Kind) + require.Equal(t, "testuser", perm.Name) + require.Equal(t, "admin", perm.Verb) + }) +} + +func TestParseScope(t *testing.T) { + backend := setupBackendNoDB(t) + + tests := []struct { + name string + scope string + expected *groupResourceName + expectError error + }{ + { + name: "valid scope", + scope: "dashboards:uid:dash1", + expected: &groupResourceName{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "dash1", + }, + }, + { + name: "invalid scope format", + scope: "dashboards:someotherformat", + expected: nil, + expectError: errInvalidScope, + }, + { + name: "unknown group resource", + scope: "unknown:uid:u1", + expected: nil, + expectError: errUnknownGroupResource, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := backend.parseScope(tt.scope) + if tt.expectError != nil { + require.ErrorIs(t, err, tt.expectError) + return + } + + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, tt.expected.Group, result.Group) + require.Equal(t, tt.expected.Resource, result.Resource) + require.Equal(t, tt.expected.Name, result.Name) + }) + } +} diff --git a/pkg/registry/apis/iam/resourcepermission/queries/resource_permission_query.sql b/pkg/registry/apis/iam/resourcepermission/queries/resource_permission_query.sql new file mode 100644 index 00000000000..9fc9b6f555c --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/queries/resource_permission_query.sql @@ -0,0 +1,27 @@ +SELECT + p.id, p.action, p.scope, p.created, p.updated, + r.name as role_name, + COALESCE(u.uid, t.uid, br.role) as subject_uid, + CASE WHEN u.uid IS NOT NULL THEN 'user' + WHEN t.uid IS NOT NULL THEN 'team' + ELSE 'builtin_role' END as subject_type, + COALESCE(u.is_service_account, {{ .Arg false }}) as is_service_account +FROM {{ .Ident .PermissionTable }} p +INNER JOIN {{ .Ident .RoleTable }} r ON p.role_id = r.id +LEFT JOIN {{ .Ident .UserRoleTable }} ur ON r.id = ur.role_id AND ur.org_id = r.org_id +LEFT JOIN {{ .Ident .UserTable }} u ON ur.user_id = u.id +LEFT JOIN {{ .Ident .TeamRoleTable }} tr ON r.id = tr.role_id AND tr.org_id = r.org_id +LEFT JOIN {{ .Ident .TeamTable }} t ON tr.team_id = t.id +LEFT JOIN {{ .Ident .BuiltinRoleTable }} br ON r.id = br.role_id +WHERE r.name LIKE {{ .Arg .ManagedRolePattern }} +{{ if .Query.ActionSets }} +AND p.action IN ({{ .ArgList .Query.ActionSets }}) +{{ end }} +AND (u.uid IS NOT NULL OR t.uid IS NOT NULL OR br.role IS NOT NULL) +{{ if .Query.OrgID }} +AND COALESCE(ur.org_id, tr.org_id, r.org_id) = {{ .Arg .Query.OrgID }} +{{ end }} +{{ if .Query.Scope }} +AND p.scope = {{ .Arg .Query.Scope }} +{{ end }} +ORDER BY p.id diff --git a/pkg/registry/apis/iam/resourcepermission/sql.go b/pkg/registry/apis/iam/resourcepermission/sql.go index c0790370280..c8ff7e5126f 100644 --- a/pkg/registry/apis/iam/resourcepermission/sql.go +++ b/pkg/registry/apis/iam/resourcepermission/sql.go @@ -1,8 +1,98 @@ package resourcepermission +import ( + "context" + "fmt" + "strings" + + "github.com/grafana/authlib/types" + v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "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) { + rawQuery, args, err := buildListResourcePermissionsQueryFromTemplate(sql, query) + if err != nil { + return nil, err + } + + rows, err := sql.DB.GetSqlxSession().Query(ctx, rawQuery, args...) + if err != nil { + if rows != nil { + _ = rows.Close() + } + return nil, fmt.Errorf("querying resource permissions: %w", err) + } + defer func() { + _ = rows.Close() + }() + + permissions := make(map[groupResourceName][]flatResourcePermission) + for rows.Next() { + var perm flatResourcePermission + if err := rows.Scan( + &perm.ID, &perm.Action, &perm.Scope, &perm.Created, &perm.Updated, &perm.RoleName, + &perm.SubjectUID, &perm.SubjectType, &perm.IsServiceAccount, + ); err != nil { + return nil, fmt.Errorf("scanning resource permission: %w", err) + } + + key, err := s.parseScope(perm.Scope) + if err != nil { + s.logger.Warn("skipping", "scope", perm.Scope, "err", err) + continue + } + + permissions[*key] = append(permissions[*key], perm) + } + + return permissions, nil +} + +// getResourcePermission retrieves a single ResourcePermission by its name in the format -- (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) + } + + resourceQuery := &ListResourcePermissionsQuery{ + Scope: mapper.Scope(uid), + OrgID: ns.OrgID, + ActionSets: mapper.ActionSets(), + } + + permsByResource, err := s.getResourcePermissions(ctx, sql, resourceQuery) + if err != nil { + return nil, err + } + + if len(permsByResource) == 0 { + return nil, fmt.Errorf("resource permission %q: %w", resourceQuery.Scope, errNotFound) + } + + resourcePermission, err := toV0ResourcePermissions(permsByResource) + if err != nil { + return nil, err + } + if resourcePermission == nil { + return nil, fmt.Errorf("resource permission %q: %w", resourceQuery.Scope, errNotFound) + } + + return &resourcePermission[0], nil +} // Create diff --git a/pkg/registry/apis/iam/resourcepermission/sql_test.go b/pkg/registry/apis/iam/resourcepermission/sql_test.go new file mode 100644 index 00000000000..53994954588 --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/sql_test.go @@ -0,0 +1,203 @@ +package resourcepermission + +import ( + "context" + "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/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/tests/testsuite" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func setupBackend(t *testing.T) *ResourcePermSqlBackend { + store := db.InitTestDB(t) + + sqlHelper := &legacysql.LegacyDatabaseHelper{ + DB: store, + Table: func(name string) string { return name }, + } + + dbProvider := func(ctx context.Context) (*legacysql.LegacyDatabaseHelper, error) { + return sqlHelper, nil + } + return ProvideStorageBackend(dbProvider) +} + +func setupTestRoles(t *testing.T, store db.DB) { + sess := store.GetSqlxSession() + + _, err := sess.Exec(context.Background(), + `INSERT INTO role (id, version, org_id, uid, name, display_name, description, group_name, hidden, created, updated) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?), + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + // Managed roles + 1, 0, 1, "managed_users_1_permissions_org1", "managed:users:1:permissions", "", "", "", false, "2025-09-02", "2025-09-02", // Org-1 - User 1 + 2, 0, 2, "managed_users_2_permissions_org2", "managed:users:2:permissions", "", "", "", false, "2025-09-02", "2025-09-02", // Org-2 - User 2 + 3, 0, 1, "managed_users_3_permissions_org1", "managed:users:3:permissions", "", "", "", false, "2025-09-02", "2025-09-02", // Org-1 - Service Account + 4, 0, 1, "managed_builtins_editor_permissions_org1", "managed:builtins:editor:permissions", "", "", "", false, "2025-09-02", "2025-09-02", // Org-1 - Builtin Editor + 5, 0, 1, "managed_teams_1_permissions_org1", "managed:teams:1:permissions", "", "", "", false, "2025-09-02", "2025-09-02", // Org-1 - Team 1 + ) + require.NoError(t, err) + + // Permissions + _, err = sess.Exec(context.Background(), + `INSERT INTO permission (role_id, action, scope, created, updated) + VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)`, + // Permissions for managed:users:1:permissions + 1, "folders:view", "folders:uid:fold1", "2025-09-02", "2025-09-02", + 1, "dashboards:edit", "dashboards:uid:dash1", "2025-09-02", "2025-09-02", + // Permissions for managed:users:2:permissions + 2, "folders:edit", "folders:uid:fold1", "2025-09-02", "2025-09-02", + // Permissions for managed:users:3:permissions (service account) + 3, "dashboards:view", "dashboards:uid:dash1", "2025-09-02", "2025-09-02", + // Permissions for managed:builtins:editor:permissions + 4, "dashboards:edit", "dashboards:uid:dash1", "2025-09-02", "2025-09-02", + // Permissions for managed:teams:1:permissions + 5, "dashboards:admin", "dashboards:uid:dash1", "2025-09-02", "2025-09-03", // Recently updated + ) + require.NoError(t, err) + + _, err = sess.Exec(context.Background(), + `INSERT INTO`+store.GetDialect().Quote("user")+`(id, org_id, uid, login, email, is_admin, is_service_account, created, updated, version) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + // Users + 1, 1, "user-1", "user-1", "user-1@example.com", false, false, "2025-09-02 00:00:00", "2025-09-02 00:00:00", 0, + 2, 1, "user-2", "user-2", "user-2@example.com", false, false, "2025-09-02 00:00:00", "2025-09-02 00:00:00", 0, + 3, 1, "sa-1", "sa-1", "", false, true, "2025-09-02 00:00:00", "2025-09-02 00:00:00", 0, + ) + require.NoError(t, err) + + _, err = sess.Exec(context.Background(), + `INSERT INTO team (id, org_id, uid, name, created, updated) + VALUES (?, ?, ?, ?, ?, ?)`, + // Teams + 1, 1, "team-1", "team-1", "2025-09-02 00:00:00", "2025-09-02 00:00:00", + ) + require.NoError(t, err) + + // User role bindings + _, err = sess.Exec(context.Background(), + `INSERT INTO user_role (org_id, user_id, role_id, created) + VALUES (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)`, + 1, 1, 1, "2025-09-02 00:00:00", // User-1 -> managed:users:1:permissions + 2, 2, 2, "2025-09-02 00:00:00", // User-2 -> managed:users:2:permissions + 1, 3, 3, "2025-09-02 00:00:00", // ServiceAccount -> managed:users:3:permissions + ) + require.NoError(t, err) + + // Team role bindings + _, err = sess.Exec(context.Background(), + `INSERT INTO team_role (org_id, team_id, role_id, created) + VALUES (?, ?, ?, ?)`, + 1, 1, 5, "2025-09-02 00:00:00", // Team-1 -> managed:teams:1:permissions + ) + require.NoError(t, err) + + // basic role bindings + _, err = sess.Exec(context.Background(), + `INSERT INTO builtin_role (org_id, role, role_id, created, updated) + VALUES (?, ?, ?, ?, ?)`, + 1, "Editor", 4, "2025-09-02 00:00:00", "2025-09-02 00:00:00", // Builtin Editor -> managed:builtins:editor:permissions + ) + require.NoError(t, err) +} + +func TestResourcePermSqlBackend_getResourcePermission(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + backend := setupBackend(t) + sql, err := backend.dbProvider(context.Background()) + require.NoError(t, err) + setupTestRoles(t, sql.DB) + + created := time.Date(2025, 9, 2, 0, 0, 0, 0, time.UTC) + + tests := []struct { + name string + resource string + orgID int64 + want v0alpha1.ResourcePermission + err error + }{ + { + name: "should return only org-1 permissions for fold1", + resource: "folder.grafana.app-folders-fold1", + orgID: 1, + want: v0alpha1.ResourcePermission{ + ObjectMeta: metaV1.ObjectMeta{ + Name: "folder.grafana.app-folders-fold1", + CreationTimestamp: metaV1.Time{Time: created}, + ResourceVersion: fmt.Sprint(created.UnixMilli()), + }, + TypeMeta: v0alpha1.ResourcePermissionInfo.TypeMeta(), + Spec: v0alpha1.ResourcePermissionSpec{ + Resource: v0alpha1.ResourcePermissionspecResource{ + ApiGroup: "folder.grafana.app", + Resource: "folders", + Name: "fold1", + }, + Permissions: []v0alpha1.ResourcePermissionspecPermission{ + { + Kind: v0alpha1.ResourcePermissionSpecPermissionKindUser, + Name: "user-1", + Verb: "view", + }, + }, + }, + }, + err: nil, + }, + { + name: "should return empty for org-2", + orgID: 2, + resource: "dashboard.grafana.app-dashboards-dash1", + err: errNotFound, + }, + { + name: "should return an error for unknown resource type", + orgID: 1, + resource: "unknown.grafana.app-unknown-u1", + err: errUnknownGroupResource, + }, + { + name: "should return an error for invalid resource name", + orgID: 1, + resource: "invalid.grafana.app-invalid", + err: errInvalidName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ns := types.NamespaceInfo{ + OrgID: tt.orgID, + } + got, err := backend.getResourcePermission(context.Background(), sql, ns, tt.resource) + if tt.err != nil { + require.Error(t, err) + require.ErrorIs(t, err, tt.err) + return + } + require.NoError(t, err) + + require.Equal(t, tt.want.Name, got.Name) + require.Equal(t, tt.want.CreationTimestamp, got.CreationTimestamp) + require.Equal(t, tt.want.ResourceVersion, got.ResourceVersion) + require.NotZero(t, got.GetUpdateTimestamp()) + require.Equal(t, tt.want.TypeMeta, got.TypeMeta) + require.Equal(t, tt.want.Spec, got.Spec) + }) + } +} diff --git a/pkg/registry/apis/iam/resourcepermission/storage_backend.go b/pkg/registry/apis/iam/resourcepermission/storage_backend.go index 32f83e9b1db..dfea11ca847 100644 --- a/pkg/registry/apis/iam/resourcepermission/storage_backend.go +++ b/pkg/registry/apis/iam/resourcepermission/storage_backend.go @@ -2,12 +2,14 @@ package resourcepermission import ( "context" + "encoding/json" + "errors" "fmt" "iter" - "net/http" "sync" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" "github.com/grafana/authlib/types" "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" @@ -25,6 +27,9 @@ type ResourcePermSqlBackend struct { dbProvider legacysql.LegacyDatabaseProvider logger log.Logger + mappers map[schema.GroupResource]Mapper // group/resource -> rbac mapper + reverseMappers map[string]schema.GroupResource // rbac kind -> group/resource + subscribers []chan *resource.WrittenEvent mutex sync.Mutex } @@ -34,6 +39,15 @@ func ProvideStorageBackend(dbProvider legacysql.LegacyDatabaseProvider) *Resourc dbProvider: dbProvider, logger: log.New("resourceperm_storage_backend"), + mappers: map[schema.GroupResource]Mapper{ + {Group: "folder.grafana.app", Resource: "folders"}: NewMapper("folders", defaultLevels), + {Group: "dashboard.grafana.app", Resource: "dashboards"}: NewMapper("dashboards", defaultLevels), + }, + reverseMappers: map[string]schema.GroupResource{ + "folders": {Group: "folder.grafana.app", Resource: "folders"}, + "dashboards": {Group: "dashboard.grafana.app", Resource: "dashboards"}, + }, + subscribers: make([]chan *resource.WrittenEvent, 0), mutex: sync.Mutex{}, } @@ -57,11 +71,55 @@ func (s *ResourcePermSqlBackend) ListModifiedSince(ctx context.Context, key reso } } -func (s *ResourcePermSqlBackend) ReadResource(_ context.Context, req *resourcepb.ReadRequest) *resource.BackendReadResponse { - return &resource.BackendReadResponse{ - Key: req.GetKey(), - Error: &resourcepb.ErrorResult{Code: http.StatusForbidden, Message: errNotImplemented.Error()}, +func (s *ResourcePermSqlBackend) ReadResource(ctx context.Context, req *resourcepb.ReadRequest) *resource.BackendReadResponse { + rsp := &resource.BackendReadResponse{Key: req.GetKey()} + + ns, err := types.ParseNamespace(req.Key.Namespace) + if err != nil { + rsp.Error = resource.AsErrorResult(err) + return rsp } + if ns.OrgID <= 0 { + rsp.Error = resource.AsErrorResult(apierrors.NewBadRequest(errInvalidNamespace.Error())) + return rsp + } + + if req.ResourceVersion > 0 { + rsp.Error = resource.AsErrorResult(apierrors.NewBadRequest("resourceVersion is not supported")) + return rsp + } + + dbHelper, err := s.dbProvider(ctx) + if err != nil { + // Hide the error from the user, but log it + logger := s.logger.FromContext(ctx) + logger.Error("Failed to get database helper", "error", err) + rsp.Error = resource.AsErrorResult(errDatabaseHelper) + return rsp + } + + resourcePermission, err := s.getResourcePermission(ctx, dbHelper, ns, req.Key.Name) + if err != nil { + if errors.Is(err, errNotFound) { + rsp.Error = resource.AsErrorResult( + apierrors.NewNotFound(v0alpha1.ResourcePermissionInfo.GroupResource(), req.Key.Name), + ) + } else if errors.Is(err, errUnknownGroupResource) || errors.Is(err, errInvalidName) { + rsp.Error = resource.AsErrorResult(apierrors.NewBadRequest(err.Error())) + } else { + rsp.Error = resource.AsErrorResult(err) + } + return rsp + } + + rsp.ResourceVersion = resourcePermission.GetUpdateTimestamp().UnixMilli() + rsp.Value, err = json.Marshal(resourcePermission) + if err != nil { + rsp.Error = resource.AsErrorResult(err) + return rsp + } + + return rsp } func (s *ResourcePermSqlBackend) WatchWriteEvents(ctx context.Context) (<-chan *resource.WrittenEvent, error) { diff --git a/pkg/registry/apis/iam/resourcepermission/storage_backend_test.go b/pkg/registry/apis/iam/resourcepermission/storage_backend_test.go new file mode 100644 index 00000000000..21502290d6d --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/storage_backend_test.go @@ -0,0 +1,110 @@ +package resourcepermission + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" + + v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" +) + +func TestIntegration_ResourcePermSqlBackend_ReadResource(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + backend := setupBackend(t) + sql, err := backend.dbProvider(context.Background()) + require.NoError(t, err) + setupTestRoles(t, sql.DB) + + updated1 := time.Date(2025, 9, 2, 0, 0, 0, 0, time.UTC) + updated2 := time.Date(2025, 9, 3, 0, 0, 0, 0, time.UTC) // managed role for team 1 has a later updated permission + + t.Run("ReadResource - Invalid namespace", func(t *testing.T) { + resp := backend.ReadResource(context.Background(), &resourcepb.ReadRequest{ + Key: &resourcepb.ResourceKey{Name: "folder.grafana.app-folders-fold1", Namespace: "invalid"}, + }) + require.NotNil(t, resp) + require.NotNil(t, resp.Error) + require.Contains(t, resp.Error.Message, errInvalidNamespace.Error()) + require.Equal(t, int32(400), resp.Error.Code) + }) + + t.Run("ReadResource - Get fold1 resource permissions", func(t *testing.T) { + resp := backend.ReadResource(context.Background(), &resourcepb.ReadRequest{ + Key: &resourcepb.ResourceKey{Name: "folder.grafana.app-folders-fold1", Namespace: "default"}, + }) + + require.NotNil(t, resp) + require.Nil(t, resp.Error) + require.NotNil(t, resp.Value) + require.Equal(t, updated1.UnixMilli(), resp.ResourceVersion) + + var permission v0alpha1.ResourcePermission + err := json.Unmarshal(resp.Value, &permission) + require.NoError(t, err) + require.Equal(t, "folder.grafana.app-folders-fold1", permission.Name) + require.Len(t, permission.Spec.Permissions, 1) + require.Equal(t, "user-1", permission.Spec.Permissions[0].Name) + require.Equal(t, v0alpha1.ResourcePermissionSpecPermissionKindUser, permission.Spec.Permissions[0].Kind) + require.Equal(t, "view", permission.Spec.Permissions[0].Verb) + }) + + t.Run("ReadResource - Get fold1 in org-2 resource permissions", func(t *testing.T) { + resp := backend.ReadResource(context.Background(), &resourcepb.ReadRequest{ + Key: &resourcepb.ResourceKey{Name: "folder.grafana.app-folders-fold1", Namespace: "org-2"}, + }) + + require.NotNil(t, resp) + require.Nil(t, resp.Error) + require.NotNil(t, resp.Value) + require.Equal(t, updated1.UnixMilli(), resp.ResourceVersion) + + var permission v0alpha1.ResourcePermission + err := json.Unmarshal(resp.Value, &permission) + require.NoError(t, err) + require.Equal(t, "folder.grafana.app-folders-fold1", permission.Name) + require.Len(t, permission.Spec.Permissions, 1) + require.Equal(t, "user-2", permission.Spec.Permissions[0].Name) + require.Equal(t, v0alpha1.ResourcePermissionSpecPermissionKindUser, permission.Spec.Permissions[0].Kind) + require.Equal(t, "edit", permission.Spec.Permissions[0].Verb) + }) + + t.Run("ReadResource - Get dash1 resource permissions", func(t *testing.T) { + resp := backend.ReadResource(context.Background(), &resourcepb.ReadRequest{ + Key: &resourcepb.ResourceKey{Name: "dashboard.grafana.app-dashboards-dash1", Namespace: "default"}, + }) + + require.NotNil(t, resp) + require.Nil(t, resp.Error) + require.NotNil(t, resp.Value) + require.Equal(t, updated2.UnixMilli(), resp.ResourceVersion) + + var permission v0alpha1.ResourcePermission + err := json.Unmarshal(resp.Value, &permission) + require.NoError(t, err) + require.Equal(t, "dashboard.grafana.app-dashboards-dash1", permission.Name) + require.Len(t, permission.Spec.Permissions, 4) + + require.Equal(t, "Editor", permission.Spec.Permissions[0].Name) + require.Equal(t, v0alpha1.ResourcePermissionSpecPermissionKindBasicRole, permission.Spec.Permissions[0].Kind) + require.Equal(t, "edit", permission.Spec.Permissions[0].Verb) + + require.Equal(t, "sa-1", permission.Spec.Permissions[1].Name) + require.Equal(t, v0alpha1.ResourcePermissionSpecPermissionKindServiceAccount, permission.Spec.Permissions[1].Kind) + require.Equal(t, "view", permission.Spec.Permissions[1].Verb) + + require.Equal(t, "team-1", permission.Spec.Permissions[2].Name) + require.Equal(t, v0alpha1.ResourcePermissionSpecPermissionKindTeam, permission.Spec.Permissions[2].Kind) + require.Equal(t, "admin", permission.Spec.Permissions[2].Verb) + + require.Equal(t, "user-1", permission.Spec.Permissions[3].Name) + require.Equal(t, v0alpha1.ResourcePermissionSpecPermissionKindUser, permission.Spec.Permissions[3].Kind) + require.Equal(t, "edit", permission.Spec.Permissions[3].Verb) + }) +} diff --git a/pkg/registry/apis/iam/resourcepermission/templates.go b/pkg/registry/apis/iam/resourcepermission/templates.go index c0790370280..d36ce0abb09 100644 --- a/pkg/registry/apis/iam/resourcepermission/templates.go +++ b/pkg/registry/apis/iam/resourcepermission/templates.go @@ -1,8 +1,79 @@ package resourcepermission +import ( + "embed" + "fmt" + "text/template" + + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" +) + +// Templates setup. +var ( + //go:embed queries/*.sql + sqlTemplatesFS embed.FS + + sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `queries/*.sql`)) + + resourcePermissionsQueryTplt = mustTemplate("resource_permission_query.sql") +) + +func mustTemplate(filename string) *template.Template { + if t := sqlTemplates.Lookup(filename); t != nil { + return t + } + panic(fmt.Sprintf("template file not found: %s", filename)) +} + // List -// Get +type listResourcePermissionsQueryTemplate struct { + sqltemplate.SQLTemplate + Query *ListResourcePermissionsQuery + PermissionTable string + RoleTable string + UserTable string + TeamTable string + BuiltinRoleTable string + UserRoleTable string + TeamRoleTable string + ManagedRolePattern string +} + +// BooleanStr provides the BooleanStr functionality that the template is trying to access +func (r listResourcePermissionsQueryTemplate) BooleanStr(value bool) string { + if value { + return "1" + } + return "0" +} + +func (r listResourcePermissionsQueryTemplate) Validate() error { + return nil +} + +func buildListResourcePermissionsQueryFromTemplate(sql *legacysql.LegacyDatabaseHelper, query *ListResourcePermissionsQuery) (string, []interface{}, error) { + req := listResourcePermissionsQueryTemplate{ + SQLTemplate: sqltemplate.New(sql.DialectForDriver()), + Query: query, + PermissionTable: sql.Table("permission"), + RoleTable: sql.Table("role"), + UserTable: sql.Table("user"), + TeamTable: sql.Table("team"), + BuiltinRoleTable: sql.Table("builtin_role"), + UserRoleTable: sql.Table("user_role"), + TeamRoleTable: sql.Table("team_role"), + ManagedRolePattern: "managed:%", + } + + rawQuery, err := sqltemplate.Execute(resourcePermissionsQueryTplt, req) + if err != nil { + return "", nil, fmt.Errorf("execute template %q: %w", resourcePermissionsQueryTplt.Name(), err) + } + + return rawQuery, req.GetArgs(), nil +} // Create diff --git a/pkg/registry/apis/iam/resourcepermission/templates_test.go b/pkg/registry/apis/iam/resourcepermission/templates_test.go new file mode 100644 index 00000000000..571231773d3 --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/templates_test.go @@ -0,0 +1,56 @@ +package resourcepermission + +import ( + "testing" + "text/template" + + "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) { + nodb := &legacysql.LegacyDatabaseHelper{ + Table: func(n string) string { + return "grafana." + n + }, + } + + getListResourcePermissionsQuery := func(q *ListResourcePermissionsQuery) sqltemplate.SQLTemplate { + v := listResourcePermissionsQueryTemplate{ + SQLTemplate: sqltemplate.New(nodb.DialectForDriver()), + Query: q, + PermissionTable: nodb.Table("permission"), + RoleTable: nodb.Table("role"), + UserTable: nodb.Table("user"), + TeamTable: nodb.Table("team"), + BuiltinRoleTable: nodb.Table("builtin_role"), + UserRoleTable: nodb.Table("user_role"), + TeamRoleTable: nodb.Table("team_role"), + ManagedRolePattern: "managed:%", + } + v.SQLTemplate = mocks.NewTestingSQLTemplate() + return &v + } + + mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ + RootDir: "testdata", + SQLTemplatesFS: sqlTemplatesFS, + Templates: map[*template.Template][]mocks.TemplateTestCase{ + resourcePermissionsQueryTplt: { + { + Name: "basic_query", + Data: getListResourcePermissionsQuery(&ListResourcePermissionsQuery{}), + }, + { + Name: "with_all_fields", + Data: getListResourcePermissionsQuery(&ListResourcePermissionsQuery{ + Scope: "123", + OrgID: 3, + ActionSets: []string{"folders:admin", "folders:edit", "folders:view"}, + }), + }, + }, + }, + }) +} diff --git a/pkg/registry/apis/iam/resourcepermission/testdata/mysql--resource_permission_query-basic_query.sql b/pkg/registry/apis/iam/resourcepermission/testdata/mysql--resource_permission_query-basic_query.sql new file mode 100755 index 00000000000..da6ac22085b --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/testdata/mysql--resource_permission_query-basic_query.sql @@ -0,0 +1,18 @@ +SELECT + p.id, p.action, p.scope, p.created, p.updated, + r.name as role_name, + COALESCE(u.uid, t.uid, br.role) as subject_uid, + CASE WHEN u.uid IS NOT NULL THEN 'user' + WHEN t.uid IS NOT NULL THEN 'team' + ELSE 'builtin_role' END as subject_type, + COALESCE(u.is_service_account, FALSE) as is_service_account +FROM `grafana`.`permission` p +INNER JOIN `grafana`.`role` r ON p.role_id = r.id +LEFT JOIN `grafana`.`user_role` ur ON r.id = ur.role_id AND ur.org_id = r.org_id +LEFT JOIN `grafana`.`user` u ON ur.user_id = u.id +LEFT JOIN `grafana`.`team_role` tr ON r.id = tr.role_id AND tr.org_id = r.org_id +LEFT JOIN `grafana`.`team` t ON tr.team_id = t.id +LEFT JOIN `grafana`.`builtin_role` br ON r.id = br.role_id +WHERE r.name LIKE 'managed:%' +AND (u.uid IS NOT NULL OR t.uid IS NOT NULL OR br.role IS NOT NULL) +ORDER BY p.id diff --git a/pkg/registry/apis/iam/resourcepermission/testdata/mysql--resource_permission_query-with_all_fields.sql b/pkg/registry/apis/iam/resourcepermission/testdata/mysql--resource_permission_query-with_all_fields.sql new file mode 100755 index 00000000000..134db52f15e --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/testdata/mysql--resource_permission_query-with_all_fields.sql @@ -0,0 +1,21 @@ +SELECT + p.id, p.action, p.scope, p.created, p.updated, + r.name as role_name, + COALESCE(u.uid, t.uid, br.role) as subject_uid, + CASE WHEN u.uid IS NOT NULL THEN 'user' + WHEN t.uid IS NOT NULL THEN 'team' + ELSE 'builtin_role' END as subject_type, + COALESCE(u.is_service_account, FALSE) as is_service_account +FROM `grafana`.`permission` p +INNER JOIN `grafana`.`role` r ON p.role_id = r.id +LEFT JOIN `grafana`.`user_role` ur ON r.id = ur.role_id AND ur.org_id = r.org_id +LEFT JOIN `grafana`.`user` u ON ur.user_id = u.id +LEFT JOIN `grafana`.`team_role` tr ON r.id = tr.role_id AND tr.org_id = r.org_id +LEFT JOIN `grafana`.`team` t ON tr.team_id = t.id +LEFT JOIN `grafana`.`builtin_role` br ON r.id = br.role_id +WHERE r.name LIKE 'managed:%' +AND p.action IN ('folders:admin', 'folders:edit', 'folders:view') +AND (u.uid IS NOT NULL OR t.uid IS NOT NULL OR br.role IS NOT NULL) +AND COALESCE(ur.org_id, tr.org_id, r.org_id) = 3 +AND p.scope = '123' +ORDER BY p.id diff --git a/pkg/registry/apis/iam/resourcepermission/testdata/postgres--resource_permission_query-basic_query.sql b/pkg/registry/apis/iam/resourcepermission/testdata/postgres--resource_permission_query-basic_query.sql new file mode 100755 index 00000000000..2dd6d343f94 --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/testdata/postgres--resource_permission_query-basic_query.sql @@ -0,0 +1,18 @@ +SELECT + p.id, p.action, p.scope, p.created, p.updated, + r.name as role_name, + COALESCE(u.uid, t.uid, br.role) as subject_uid, + CASE WHEN u.uid IS NOT NULL THEN 'user' + WHEN t.uid IS NOT NULL THEN 'team' + ELSE 'builtin_role' END as subject_type, + COALESCE(u.is_service_account, FALSE) as is_service_account +FROM "grafana"."permission" p +INNER JOIN "grafana"."role" r ON p.role_id = r.id +LEFT JOIN "grafana"."user_role" ur ON r.id = ur.role_id AND ur.org_id = r.org_id +LEFT JOIN "grafana"."user" u ON ur.user_id = u.id +LEFT JOIN "grafana"."team_role" tr ON r.id = tr.role_id AND tr.org_id = r.org_id +LEFT JOIN "grafana"."team" t ON tr.team_id = t.id +LEFT JOIN "grafana"."builtin_role" br ON r.id = br.role_id +WHERE r.name LIKE 'managed:%' +AND (u.uid IS NOT NULL OR t.uid IS NOT NULL OR br.role IS NOT NULL) +ORDER BY p.id diff --git a/pkg/registry/apis/iam/resourcepermission/testdata/postgres--resource_permission_query-with_all_fields.sql b/pkg/registry/apis/iam/resourcepermission/testdata/postgres--resource_permission_query-with_all_fields.sql new file mode 100755 index 00000000000..2562573bfcb --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/testdata/postgres--resource_permission_query-with_all_fields.sql @@ -0,0 +1,21 @@ +SELECT + p.id, p.action, p.scope, p.created, p.updated, + r.name as role_name, + COALESCE(u.uid, t.uid, br.role) as subject_uid, + CASE WHEN u.uid IS NOT NULL THEN 'user' + WHEN t.uid IS NOT NULL THEN 'team' + ELSE 'builtin_role' END as subject_type, + COALESCE(u.is_service_account, FALSE) as is_service_account +FROM "grafana"."permission" p +INNER JOIN "grafana"."role" r ON p.role_id = r.id +LEFT JOIN "grafana"."user_role" ur ON r.id = ur.role_id AND ur.org_id = r.org_id +LEFT JOIN "grafana"."user" u ON ur.user_id = u.id +LEFT JOIN "grafana"."team_role" tr ON r.id = tr.role_id AND tr.org_id = r.org_id +LEFT JOIN "grafana"."team" t ON tr.team_id = t.id +LEFT JOIN "grafana"."builtin_role" br ON r.id = br.role_id +WHERE r.name LIKE 'managed:%' +AND p.action IN ('folders:admin', 'folders:edit', 'folders:view') +AND (u.uid IS NOT NULL OR t.uid IS NOT NULL OR br.role IS NOT NULL) +AND COALESCE(ur.org_id, tr.org_id, r.org_id) = 3 +AND p.scope = '123' +ORDER BY p.id diff --git a/pkg/registry/apis/iam/resourcepermission/testdata/sqlite--resource_permission_query-basic_query.sql b/pkg/registry/apis/iam/resourcepermission/testdata/sqlite--resource_permission_query-basic_query.sql new file mode 100755 index 00000000000..2dd6d343f94 --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/testdata/sqlite--resource_permission_query-basic_query.sql @@ -0,0 +1,18 @@ +SELECT + p.id, p.action, p.scope, p.created, p.updated, + r.name as role_name, + COALESCE(u.uid, t.uid, br.role) as subject_uid, + CASE WHEN u.uid IS NOT NULL THEN 'user' + WHEN t.uid IS NOT NULL THEN 'team' + ELSE 'builtin_role' END as subject_type, + COALESCE(u.is_service_account, FALSE) as is_service_account +FROM "grafana"."permission" p +INNER JOIN "grafana"."role" r ON p.role_id = r.id +LEFT JOIN "grafana"."user_role" ur ON r.id = ur.role_id AND ur.org_id = r.org_id +LEFT JOIN "grafana"."user" u ON ur.user_id = u.id +LEFT JOIN "grafana"."team_role" tr ON r.id = tr.role_id AND tr.org_id = r.org_id +LEFT JOIN "grafana"."team" t ON tr.team_id = t.id +LEFT JOIN "grafana"."builtin_role" br ON r.id = br.role_id +WHERE r.name LIKE 'managed:%' +AND (u.uid IS NOT NULL OR t.uid IS NOT NULL OR br.role IS NOT NULL) +ORDER BY p.id diff --git a/pkg/registry/apis/iam/resourcepermission/testdata/sqlite--resource_permission_query-with_all_fields.sql b/pkg/registry/apis/iam/resourcepermission/testdata/sqlite--resource_permission_query-with_all_fields.sql new file mode 100755 index 00000000000..2562573bfcb --- /dev/null +++ b/pkg/registry/apis/iam/resourcepermission/testdata/sqlite--resource_permission_query-with_all_fields.sql @@ -0,0 +1,21 @@ +SELECT + p.id, p.action, p.scope, p.created, p.updated, + r.name as role_name, + COALESCE(u.uid, t.uid, br.role) as subject_uid, + CASE WHEN u.uid IS NOT NULL THEN 'user' + WHEN t.uid IS NOT NULL THEN 'team' + ELSE 'builtin_role' END as subject_type, + COALESCE(u.is_service_account, FALSE) as is_service_account +FROM "grafana"."permission" p +INNER JOIN "grafana"."role" r ON p.role_id = r.id +LEFT JOIN "grafana"."user_role" ur ON r.id = ur.role_id AND ur.org_id = r.org_id +LEFT JOIN "grafana"."user" u ON ur.user_id = u.id +LEFT JOIN "grafana"."team_role" tr ON r.id = tr.role_id AND tr.org_id = r.org_id +LEFT JOIN "grafana"."team" t ON tr.team_id = t.id +LEFT JOIN "grafana"."builtin_role" br ON r.id = br.role_id +WHERE r.name LIKE 'managed:%' +AND p.action IN ('folders:admin', 'folders:edit', 'folders:view') +AND (u.uid IS NOT NULL OR t.uid IS NOT NULL OR br.role IS NOT NULL) +AND COALESCE(ur.org_id, tr.org_id, r.org_id) = 3 +AND p.scope = '123' +ORDER BY p.id