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:
mohammad-hamid
2025-09-04 17:14:15 +02:00
committed by GitHub
co-authored by Gabriel Mabille
parent 40bf167cb7
commit abcdf20105
16 changed files with 1075 additions and 8 deletions
@@ -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
}