279 lines
9.0 KiB
Go
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
|
|
}
|