grafana-iam: Implement resourcepermission get (#110256)
* resource permissions get * address review feedback * address comments - read using rp name - narrow by scope and actionsets - update sql tests * align with verb simplification * keep original format to avoid conflicts * add sqltests * cleanup * Remove unecessary errors * Move query template to queries * Use splitN to make sure we have three parts * Revert user permission management for now. We don't need it * Revert error change * group permissions by resource * extract parse scope * Move sql_test * Move & test parseScope * Add tests to getResourcePermission * Linting * Use namespace * Add test to the backend * Ongoing tests * Remove pagination, fix query boolean, insert basic role binding * Linting * Straightened the created and updated times * error handling and uniformization with other backend * Restore comments to avoid later conflicts * Integration testing * switch to function, no need to make it a method * isServiceAccount should default to FALSE instead of TRUE :surprised: * PR feedback * Sort spec permissions * Shouldn't happen but double proofing --------- Co-authored-by: Gabriel Mabille <gabriel.mabille@grafana.com>
This commit is contained in:
co-authored by
Gabriel Mabille
parent
40bf167cb7
commit
abcdf20105
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 <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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
Vendored
Executable
+18
@@ -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
|
||||
Vendored
Executable
+21
@@ -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
|
||||
Vendored
Executable
+18
@@ -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
|
||||
Vendored
Executable
+21
@@ -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
|
||||
Vendored
Executable
+18
@@ -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
|
||||
Vendored
Executable
+21
@@ -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
|
||||
Reference in New Issue
Block a user