Files

279 lines
9.0 KiB
Go

package resourcepermission
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/authlib/types"
v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
idStore "github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/accesscontrol"
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
)
var (
timeNow = func() time.Time { return time.Now() }
errDatabaseHelper = errors.New("failed to get database")
errNotImplemented = errors.New("not supported by this storage backend")
errNameMismatch = errors.New("name mismatch")
errNamespaceMismatch = errors.New("namespace mismatch")
errUnknownGroupResource = errors.New("unknown group/resource")
errNotFound = errors.New("not found")
errConflict = errors.New("conflict")
errInvalidSpec = errors.New("invalid spec")
errInvalidName = errors.New("invalid name")
errInvalidScope = errors.New("invalid scope")
errInvalidNamespace = errors.New("invalid namespace")
defaultLevels = []string{"view", "edit", "admin"}
allowedBasicRoles = map[string]bool{"Viewer": true, "Editor": true, "Admin": true}
)
type IdentityStore interface {
GetServiceAccountInternalID(ctx context.Context, ns types.NamespaceInfo, query idStore.GetServiceAccountInternalIDQuery) (*idStore.GetServiceAccountInternalIDResult, error)
GetTeamInternalID(ctx context.Context, ns types.NamespaceInfo, query idStore.GetTeamInternalIDQuery) (*idStore.GetTeamInternalIDResult, error)
GetUserInternalID(ctx context.Context, ns types.NamespaceInfo, query idStore.GetUserInternalIDQuery) (*idStore.GetUserInternalIDResult, error)
}
type PageQuery struct {
ScopePatterns []string
OrgID int64
Pagination common.Pagination
}
type ListResourcePermissionsQuery struct {
Scopes []string
OrgID int64
ActionSets []string
}
type DeleteResourcePermissionsQuery struct {
Scope string
OrgID int64
RoleName string
}
type rbacAssignmentCreate struct {
Action string // e.g. "dashboards:edit"
Scope string // e.g. "folders:uid:1"
RoleName string // e.g. "managed:users:1:permissions
SubjectID any // int64 for user/team, string for builtin_role
AssignmentTable string // "user_role", "team_role", or "builtin_role"
AssignmentColumn string // "user_id", "team_id", or "role"
}
func (g *rbacAssignmentCreate) permission() accesscontrol.Permission {
p := accesscontrol.Permission{
Action: g.Action,
Scope: g.Scope,
}
p.Kind, p.Attribute, p.Identifier = accesscontrol.SplitScope(p.Scope)
return p
}
type rbacAssignment struct {
ID int64 `xorm:"id"`
Action string `xorm:"action"`
Scope string `xorm:"scope"`
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"`
}
// newV0ResourcePermission creates a new v0alpha1.ResourcePermission from the given groupResourceName and permission specs.
// Specs are sorted for consistency, created and updated are used for the metadata timestamps and resourceVersion is set to the updated timestamp in milliseconds.
func newV0ResourcePermission(grn *groupResourceName, specs []v0alpha1.ResourcePermissionspecPermission, created, updated time.Time, namespace string) v0alpha1.ResourcePermission {
// Sort specs for consistency
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: grn.string(),
Namespace: namespace,
ResourceVersion: fmt.Sprint(updated.UnixMilli()),
CreationTimestamp: metav1.NewTime(created.UTC()),
},
Spec: v0alpha1.ResourcePermissionSpec{
Resource: grn.v0alpha1(),
Permissions: specs,
},
}
r.SetUpdateTimestamp(updated.UTC())
r.UID = gapiutil.CalculateClusterWideUID(&r)
return r
}
// toV0ResourcePermissions translates a list of rbacAssignments into a list of v0alpha1.ResourcePermissions.
// it is assumed that assignments are sorted by scope
func (s *ResourcePermSqlBackend) toV0ResourcePermissions(assignments []rbacAssignment, namespace string) ([]v0alpha1.ResourcePermission, error) {
if len(assignments) == 0 {
return nil, nil
}
var (
created = assignments[0].Created
updated = assignments[0].Updated
permissionKind v0alpha1.ResourcePermissionSpecPermissionKind
resourcePermissions = make([]v0alpha1.ResourcePermission, 0, 8)
specs = make([]v0alpha1.ResourcePermissionspecPermission, 0, 4)
)
grn, err := s.parseScope(assignments[0].Scope)
if err != nil {
return nil, err
}
for _, assign := range assignments {
// Ensure all assignments belong to the same resource
parsedGrn, err := s.parseScope(assign.Scope)
if err != nil {
return nil, err
}
// If it's a new resource, flush the current specs to a ResourcePermission and start a new one
if *parsedGrn != *grn {
resourcePermissions = append(
resourcePermissions,
newV0ResourcePermission(grn, specs, created, updated, namespace),
)
// Reset for the new resource
grn = parsedGrn
specs = make([]v0alpha1.ResourcePermissionspecPermission, 0, 4)
created = assign.Created
updated = assign.Updated
}
// Find the most recent updated time
if assign.Updated.After(updated) {
updated = assign.Updated
}
// Find the oldest created time
if assign.Created.Before(created) {
created = assign.Created
}
// Determine permission kind
switch assign.SubjectType {
case "user":
if assign.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: " + assign.SubjectType)
}
// Determine verb from action
actionParts := strings.SplitN(assign.Action, ":", 2)
if len(actionParts) < 2 || actionParts[1] == "" {
return nil, fmt.Errorf("invalid action format: %s", assign.Action)
}
verb := actionParts[1]
// Append the translated permission spec
specs = append(specs, v0alpha1.ResourcePermissionspecPermission{
Kind: permissionKind,
Name: assign.SubjectUID,
Verb: verb,
})
}
// Flush the final resource
resourcePermissions = append(
resourcePermissions,
newV0ResourcePermission(grn, specs, created, updated, namespace),
)
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
}
// splitResourceName splits a resource name in the format <group>-<resource>-<name> (e.g. dashboard.grafana.app-dashboards-ad5rwqs) into its components
func splitResourceName(resourceName string) (*groupResourceName, error) {
// e.g. dashboard.grafana.app-dashboards-ad5rwqs
parts := strings.SplitN(resourceName, "-", 3)
if len(parts) != 3 {
return nil, fmt.Errorf("%w: %s", errInvalidName, resourceName)
}
group, resourceType, uid := parts[0], parts[1], parts[2]
return &groupResourceName{
Group: group,
Resource: resourceType,
Name: uid,
}, nil
}
// getResourceMapper returns the Mapper of the given group and resource to access levels and scope prefix for that resource.
func (s *ResourcePermSqlBackend) getResourceMapper(group, resource string) (Mapper, error) {
mapper, ok := s.mappers[schema.GroupResource{Group: group, Resource: resource}]
if !ok {
return nil, fmt.Errorf("%w: %s/%s", errUnknownGroupResource, group, resource)
}
return mapper, nil
}