Files
Ieva 2b86de8b7f Resource Permissions: Move validator higher up (#111557)
* move resource permission create and update validator higher up the chain

* undo unwanted change
2025-09-26 11:30:28 +00:00

531 lines
18 KiB
Go

package resourcepermission
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore/session"
"github.com/grafana/grafana/pkg/storage/legacysql"
)
// List
func (s *ResourcePermSqlBackend) newRoleIterator(ctx context.Context, dbHelper *legacysql.LegacyDatabaseHelper, ns types.NamespaceInfo, pagination *common.Pagination) (*listIterator, error) {
var (
scope string
actionSets = make([]string, 0, 3*len(s.mappers))
scopePatterns = make([]string, 0, len(s.mappers))
assignments = make([]rbacAssignment, 0, 8)
scopes = make([]string, 0, 8)
)
for _, mapper := range s.mappers {
actionSets = append(actionSets, mapper.ActionSets()...)
}
for _, mapper := range s.mappers {
scopePatterns = append(scopePatterns, mapper.ScopePattern())
}
// Run in a transaction to ensure a consistent view of the data
err := dbHelper.DB.GetSqlxSession().WithTransaction(ctx, func(tx *session.SessionTx) error {
// Get page
rawPageQuery, pageArgs, err := buildPageQueryFromTemplate(dbHelper, &PageQuery{
ScopePatterns: scopePatterns,
OrgID: ns.OrgID,
Pagination: *pagination,
})
if err != nil {
return err
}
rows, err := tx.Query(ctx, rawPageQuery, pageArgs...)
if err != nil {
if rows != nil {
_ = rows.Close()
}
return fmt.Errorf("querying resource permissions: %w", err)
}
defer func() {
_ = rows.Close()
}()
for rows.Next() {
if err := rows.Scan(&scope); err != nil {
return fmt.Errorf("scanning resource permission: %w", err)
}
scopes = append(scopes, scope)
}
if len(scopes) == 0 {
// No results
return nil
}
// Get assignments for the page
assignments, err = s.getRbacAssignmentsWithTx(ctx, dbHelper, tx, &ListResourcePermissionsQuery{
Scopes: scopes,
OrgID: ns.OrgID,
ActionSets: actionSets,
})
return err
})
if err != nil {
return nil, err
}
if len(assignments) == 0 {
// No results
return &listIterator{}, nil
}
v0ResourcePermissions, err := s.toV0ResourcePermissions(assignments, ns.Value)
if err != nil {
return nil, err
}
return &listIterator{
resourcePermissions: v0ResourcePermissions,
initOffset: pagination.Continue,
}, nil
}
func (s *ResourcePermSqlBackend) latestUpdate(ctx context.Context, dbHelper *legacysql.LegacyDatabaseHelper, ns types.NamespaceInfo) int64 {
scopePatterns := make([]string, 0, len(s.mappers)*3)
for _, mapper := range s.mappers {
scopePatterns = append(scopePatterns, mapper.ScopePattern())
}
query, args, err := buildLatestUpdateQueryFromTemplate(dbHelper, ns.OrgID, scopePatterns)
if err != nil {
s.logger.FromContext(ctx).Warn("Failed to build latest update query", "error", err)
return timeNow().UnixMilli()
}
var maxUpdated time.Time
err = dbHelper.DB.GetSqlxSession().Get(ctx, &maxUpdated, query, args...)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
s.logger.FromContext(ctx).Warn("Failed to get latest update for roles", "error", err)
}
return timeNow().UnixMilli()
}
return maxUpdated.UnixMilli()
}
// getRbacAssignmentsWithTx queries resource permissions based on the provided ListResourcePermissionsQuery and groups them by resource (e.g. {folder.grafana.app, folders, fold1})
func (s *ResourcePermSqlBackend) getRbacAssignmentsWithTx(ctx context.Context, sql *legacysql.LegacyDatabaseHelper, tx *session.SessionTx, query *ListResourcePermissionsQuery) ([]rbacAssignment, error) {
rawQuery, args, err := buildListResourcePermissionsQueryFromTemplate(sql, query)
if err != nil {
return nil, err
}
rows, err := tx.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([]rbacAssignment, 0, 8)
for rows.Next() {
var perm rbacAssignment
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)
}
permissions = append(permissions, 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, tx *session.SessionTx, ns types.NamespaceInfo, name string) (*v0alpha1.ResourcePermission, error) {
grn, err := splitResourceName(name)
if err != nil {
return nil, apierrors.NewInternalError(err)
}
mapper, err := s.getResourceMapper(grn.Group, grn.Resource)
if err != nil {
return nil, apierrors.NewInternalError(err)
}
resourceQuery := &ListResourcePermissionsQuery{
Scopes: []string{mapper.Scope(grn.Name)},
OrgID: ns.OrgID,
ActionSets: mapper.ActionSets(),
}
assignments, err := s.getRbacAssignmentsWithTx(ctx, sql, tx, resourceQuery)
if err != nil {
return nil, apierrors.NewInternalError(err)
}
if len(assignments) == 0 {
return nil, apierrors.NewNotFound(v0alpha1.ResourcePermissionInfo.GroupResource(), name)
}
resourcePermission, err := s.toV0ResourcePermissions(assignments, ns.Value)
if err != nil {
return nil, apierrors.NewInternalError(err)
}
if resourcePermission == nil {
return nil, apierrors.NewNotFound(v0alpha1.ResourcePermissionInfo.GroupResource(), name)
}
return &resourcePermission[0], nil
}
// Create
// createAndAssignManagedRole creates a new managed role and assigns it to the given user/team/service account/basic role
func (s *ResourcePermSqlBackend) createAndAssignManagedRole(ctx context.Context, tx *session.SessionTx, dbHelper *legacysql.LegacyDatabaseHelper, orgID int64, assignment rbacAssignmentCreate) (int64, error) {
// Create the managed role
roleUID := accesscontrol.PrefixedRoleUID(fmt.Sprintf("%s:org:%v", assignment.RoleName, orgID))
insertRoleQuery, args, err := buildInsertRoleQuery(dbHelper, orgID, roleUID, assignment.RoleName)
if err != nil {
return 0, err
}
_, err = tx.Exec(ctx, insertRoleQuery, args...)
if err != nil {
s.logger.Error("could not insert new role", "orgID", orgID, "roleName", assignment.RoleName, "error", err.Error())
return 0, fmt.Errorf("could not insert new role")
}
var roleID int64
idQuery := fmt.Sprintf("SELECT id FROM %s WHERE org_id = ? AND name = ?", dbHelper.Table("role"))
err = tx.Get(ctx, &roleID, idQuery, orgID, assignment.RoleName)
if err != nil {
s.logger.Error("could not retrieve id of created role", "orgID", orgID, "roleName", assignment.RoleName, "error", err.Error())
return 0, fmt.Errorf("could not retrieve id of created role")
}
assignQuery, args, err := buildInsertAssignmentQuery(dbHelper, orgID, roleID, assignment)
if err != nil {
return 0, err
}
_, err = tx.Exec(ctx, assignQuery, args...)
if err != nil {
s.logger.Error("could not insert role assignment", "orgID", orgID, "roleName", assignment.RoleName, "subjectID", assignment.SubjectID, "error", err.Error())
return 0, fmt.Errorf("could not insert role assignment")
}
return roleID, nil
}
// storeRbacAssignment ensures that a role exists for the given assignment, creates and assigns it if it doesn't
// and then ensures that the role has the correct permission for the given scope
func (s *ResourcePermSqlBackend) storeRbacAssignment(ctx context.Context, dbHelper *legacysql.LegacyDatabaseHelper, tx *session.SessionTx, orgID int64, assignment rbacAssignmentCreate) error {
// Check if role already exists
var roleID int64
query := fmt.Sprintf("SELECT id FROM %s WHERE org_id = ? AND name = ?", dbHelper.Table("role"))
err := tx.Get(ctx, &roleID, query, orgID, assignment.RoleName)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
s.logger.Error("could not check for existing role", "orgID", orgID, "roleName", assignment.RoleName, "error", err.Error())
return fmt.Errorf("could not check for existing role")
}
// Role doesn't exist, create it
if roleID == 0 {
roleID, err = s.createAndAssignManagedRole(ctx, tx, dbHelper, orgID, assignment)
if err != nil {
return err
}
}
// Add the new permission
insertPermQuery, args, err := buildInsertPermissionQuery(dbHelper, roleID, assignment.permission())
if err != nil {
return err
}
_, err = tx.Exec(ctx, insertPermQuery, args...)
if err != nil {
s.logger.Error("could not insert role permission", "roleID", roleID, "scope", assignment.Scope, "error", err.Error())
return fmt.Errorf("could not insert role permission")
}
return nil
}
// buildRbacAssignments builds the list of assignments (role assignments and permissions) for a given ResourcePermission spec
// It resolves user/team/service account UIDs to internal IDs for the role name and assignee subjectID
func (s *ResourcePermSqlBackend) buildRbacAssignments(ctx context.Context, ns types.NamespaceInfo, mapper Mapper, v0ResourcePerm []v0alpha1.ResourcePermissionspecPermission, rbacScope string) ([]rbacAssignmentCreate, error) {
assignments := make([]rbacAssignmentCreate, 0, len(v0ResourcePerm))
for _, perm := range v0ResourcePerm {
rbacActionSet, err := mapper.ActionSet(perm.Verb)
if err != nil {
return nil, err
}
switch perm.Kind {
case v0alpha1.ResourcePermissionSpecPermissionKindUser:
userID, err := s.identityStore.GetUserInternalID(ctx, ns, legacy.GetUserInternalIDQuery{
UID: perm.Name,
OrgID: ns.OrgID,
})
if err != nil && !strings.Contains(err.Error(), "not found") {
return nil, fmt.Errorf("resolving user %q to internal ID: %w", perm.Name, err)
}
if userID == nil {
return nil, fmt.Errorf("user %q not found: %w", perm.Name, errInvalidSpec)
}
assignments = append(assignments, rbacAssignmentCreate{
RoleName: fmt.Sprintf("managed:users:%d:permissions", userID.ID),
AssignmentTable: "user_role",
AssignmentColumn: "user_id",
SubjectID: fmt.Sprintf("%d", userID.ID),
Action: rbacActionSet,
Scope: rbacScope,
})
case v0alpha1.ResourcePermissionSpecPermissionKindTeam:
teamID, err := s.identityStore.GetTeamInternalID(ctx, ns, legacy.GetTeamInternalIDQuery{
UID: perm.Name,
OrgID: ns.OrgID,
})
if err != nil && !strings.Contains(err.Error(), "not found") {
return nil, fmt.Errorf("resolving team %q to internal ID: %w", perm.Name, err)
}
if teamID == nil {
return nil, fmt.Errorf("team %q not found: %w", perm.Name, errInvalidSpec)
}
assignments = append(assignments, rbacAssignmentCreate{
RoleName: fmt.Sprintf("managed:teams:%d:permissions", teamID.ID),
AssignmentTable: "team_role",
AssignmentColumn: "team_id",
SubjectID: fmt.Sprintf("%d", teamID.ID),
Action: rbacActionSet,
Scope: rbacScope,
})
case v0alpha1.ResourcePermissionSpecPermissionKindServiceAccount:
saID, err := s.identityStore.GetServiceAccountInternalID(ctx, ns, legacy.GetServiceAccountInternalIDQuery{
UID: perm.Name,
OrgID: ns.OrgID,
})
if err != nil && !strings.Contains(err.Error(), "not found") {
return nil, fmt.Errorf("resolving service account %q to internal ID: %w", perm.Name, err)
}
if saID == nil {
return nil, fmt.Errorf("service account %q not found: %w", perm.Name, errInvalidSpec)
}
assignments = append(assignments, rbacAssignmentCreate{
RoleName: fmt.Sprintf("managed:users:%d:permissions", saID.ID),
AssignmentTable: "user_role",
AssignmentColumn: "user_id",
SubjectID: fmt.Sprintf("%d", saID.ID),
Action: rbacActionSet,
Scope: rbacScope,
})
case v0alpha1.ResourcePermissionSpecPermissionKindBasicRole:
if !allowedBasicRoles[perm.Name] {
return nil, fmt.Errorf("invalid basic role %q: %w", perm.Name, errInvalidSpec)
}
assignments = append(assignments, rbacAssignmentCreate{
RoleName: fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(perm.Name)),
AssignmentTable: "builtin_role",
AssignmentColumn: "role",
SubjectID: perm.Name,
Action: rbacActionSet,
Scope: rbacScope,
})
default:
return nil, fmt.Errorf("unknown permission kind: %q: %w", perm.Kind, errInvalidSpec)
}
}
return assignments, nil
}
// existsResourcePermission checks if a resource permission for the given scope already exists in the given organization
func (s *ResourcePermSqlBackend) existsResourcePermission(ctx context.Context, tx *session.SessionTx, dbHelper *legacysql.LegacyDatabaseHelper, orgID int64, scope string) error {
idQuery := fmt.Sprintf(
`SELECT r.id FROM %s AS r INNER JOIN %s AS p ON p.role_id = r.id WHERE r.org_id = ? AND r.name LIKE ? AND p.scope = ? LIMIT 1`,
dbHelper.Table("role"), dbHelper.Table("permission"),
)
roleID := int64(0)
err := tx.Get(ctx, &roleID, idQuery, orgID, "managed:%", scope)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
s.logger.Error("could not check for existing resource permission", "orgID", orgID, "scope", scope, "error", err.Error())
return fmt.Errorf("could not check for existing resource permission")
}
if roleID != 0 {
return errConflict
}
return nil
}
func (s *ResourcePermSqlBackend) createResourcePermission(
ctx context.Context, dbHelper *legacysql.LegacyDatabaseHelper, ns types.NamespaceInfo, mapper Mapper, grn *groupResourceName, v0ResourcePerm *v0alpha1.ResourcePermission,
) (int64, error) {
assignments, err := s.buildRbacAssignments(ctx, ns, mapper, v0ResourcePerm.Spec.Permissions, mapper.Scope(grn.Name))
if err != nil {
return 0, err
}
err = dbHelper.DB.GetSqlxSession().WithTransaction(ctx, func(tx *session.SessionTx) error {
// Check if a resource permission for the same resource already exists
if err = s.existsResourcePermission(ctx, tx, dbHelper, ns.OrgID, mapper.Scope(grn.Name)); err != nil {
return err
}
for _, assignment := range assignments {
if err := s.storeRbacAssignment(ctx, dbHelper, tx, ns.OrgID, assignment); err != nil {
return err
}
}
return nil
})
if err != nil {
return 0, err
}
// Return a timestamp as resource version
return timeNow().UnixMilli(), nil
}
func (s *ResourcePermSqlBackend) updateResourcePermission(ctx context.Context, dbHelper *legacysql.LegacyDatabaseHelper, ns types.NamespaceInfo, mapper Mapper, grn *groupResourceName, v0ResourcePerm *v0alpha1.ResourcePermission) (int64, error) {
err := dbHelper.DB.GetSqlxSession().WithTransaction(ctx, func(tx *session.SessionTx) error {
currentPerms, err := s.getResourcePermission(ctx, dbHelper, tx, ns, grn.string())
if err != nil {
if apierrors.IsNotFound(err) {
return apierrors.NewNotFound(v0alpha1.ResourcePermissionInfo.GroupResource(), grn.string())
}
s.logger.Error("could not get resource permissions", "orgID", ns.OrgID, "scope", grn.Name, "error", err.Error())
return fmt.Errorf("could not get the existing resource permissions for resource %s", grn.Name)
}
permissionsToAdd, permissionsToRemove := diffPermissions(currentPerms.Spec.Permissions, v0ResourcePerm.Spec.Permissions)
if len(permissionsToRemove) > 0 {
permsToRemove, err := s.buildRbacAssignments(ctx, ns, mapper, permissionsToRemove, mapper.Scope(grn.Name))
if err != nil {
return err
}
for _, perm := range permsToRemove {
resourceQuery := &DeleteResourcePermissionsQuery{
Scope: perm.Scope,
OrgID: ns.OrgID,
RoleName: perm.RoleName,
}
removePermQuery, args, err := buildDeleteResourcePermissionsQueryFromTemplate(dbHelper, resourceQuery)
if err != nil {
return err
}
_, err = tx.Exec(ctx, removePermQuery, args...)
if err != nil {
s.logger.Error("could not remove role permission", "scope", perm.Scope, "role", perm.RoleName, "error", err.Error())
return fmt.Errorf("could not remove role permission")
}
}
}
if len(permissionsToAdd) > 0 {
permsToAdd, err := s.buildRbacAssignments(ctx, ns, mapper, permissionsToAdd, mapper.Scope(grn.Name))
if err != nil {
return err
}
for _, assignment := range permsToAdd {
if err := s.storeRbacAssignment(ctx, dbHelper, tx, ns.OrgID, assignment); err != nil {
return err
}
}
}
return nil
})
if err != nil {
return 0, err
}
// Return a timestamp as resource version
return timeNow().UnixMilli(), nil
}
func diffPermissions(currentPermissions, desiredPermissions []v0alpha1.ResourcePermissionspecPermission) (permissionsToAdd, permissionsToRemove []v0alpha1.ResourcePermissionspecPermission) {
for _, desired := range desiredPermissions {
found := false
for _, existing := range currentPermissions {
if desired.Name == existing.Name && desired.Kind == existing.Kind && desired.Verb == existing.Verb {
found = true
break
}
}
if !found {
permissionsToAdd = append(permissionsToAdd, desired)
}
}
// Compile a list of permissions to remove
for _, existing := range currentPermissions {
found := false
for _, desired := range desiredPermissions {
if desired.Name == existing.Name && desired.Kind == existing.Kind && desired.Verb == existing.Verb {
found = true
break
}
}
if !found {
permissionsToRemove = append(permissionsToRemove, existing)
}
}
return permissionsToAdd, permissionsToRemove
}
// deleteResourcePermission deletes resource permissions for a single ResourcePermission resource referenced by its name in the format <group>-<resource>-<name> (e.g. dashboard.grafana.app-dashboards-ad5rwqs)
func (s *ResourcePermSqlBackend) deleteResourcePermission(ctx context.Context, sql *legacysql.LegacyDatabaseHelper, ns types.NamespaceInfo, name string) error {
grn, err := splitResourceName(name)
if err != nil {
return err
}
mapper, err := s.getResourceMapper(grn.Group, grn.Resource)
if err != nil {
return err
}
scope := mapper.Scope(grn.Name)
resourceQuery := &DeleteResourcePermissionsQuery{
Scope: scope,
OrgID: ns.OrgID,
}
rawQuery, args, err := buildDeleteResourcePermissionsQueryFromTemplate(sql, resourceQuery)
if err != nil {
return err
}
_, err = sql.DB.GetSqlxSession().Exec(ctx, rawQuery, args...)
if err != nil {
s.logger.Error("could not delete resource permissions", "scope", scope, "orgID", ns.OrgID, err.Error())
return fmt.Errorf("could not delete resource permission")
}
return nil
}