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 -- (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 -- (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 }