From 464af1625349d557f688da9f845471ef8b80a5f9 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 31 Jul 2026 18:14:30 +0200 Subject: Gate live-content changes on restricted surfaces publish_draft!, trash!, destroy_from_trash!, attach_asset! and Asset#destroy_witnessed! now refuse unless the acting user holds redaktion, and only when the subject is on a restricted surface: the front page, the updates tree that feeds ~100k subscribers, or disclosure. Drafting, autosaving, tagging and creating stay free everywhere for everyone. Enforcement is in the models rather than the controllers, since attach_asset! and the rest are reachable from rake tasks and internal paths. It follows the errors.add-plus-bare-raise pattern the rest of Node already uses, so every existing RecordInvalid rescue reports it with a localised message; only assets_controller#destroy needed a rescue added. A nil user is treated as a system context and bypasses the gate. The default nil on three of those verbs is what makes that reachable, and removing those defaults once every call site passes a user is the next tightening. --- app/models/asset.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'app/models/asset.rb') diff --git a/app/models/asset.rb b/app/models/asset.rb index 8cec4371..b256b929 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -41,6 +41,13 @@ class Asset < ApplicationRecord :ids => page_ids).distinct end + # An asset's reach is the reach of the pages carrying it: destroying one + # removes it from every live page at once, so a single restricted + # attachment makes the destruction a restricted act. + def restricted? + attached_nodes.any?(&:restricted?) + end + # Witnessed destruction. Destroying an asset is a public-facing act # even when unattached. The original and its variants are publicly # reachable under /system/uploads, so an entry is always written, @@ -49,6 +56,11 @@ class Asset < ApplicationRecord # participates as the first non-Node subject (its participant row # dangles after destroy, by design, the name lives on in metadata). def destroy_witnessed! user: + if user && !user.may_change_live?(self) + errors.add(:base, :not_permitted) + raise ActiveRecord::RecordInvalid.new(self) + end + ActiveRecord::Base.transaction do affected = attached_nodes.to_a headline_losses = affected.select do |node| -- cgit v1.3