From fc1076f8fc23288607010f88e1894e3820cf07dd Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Tue, 25 Aug 2026 14:49:23 +0200 Subject: [PATCH] refact --- .../app/admin/application_tokens.rb | 4 +++ libs/ruby/warp_engine/app/admin/downloads.rb | 2 +- .../warp_engine/app/admin/external_links.rb | 6 ++++ libs/ruby/warp_engine/app/admin/pipelines.rb | 4 +-- .../warp_engine/app/admin/platform_links.rb | 6 ++++ libs/ruby/warp_engine/app/admin/releases.rb | 6 ++++ libs/ruby/warp_engine/app/admin/softwares.rb | 2 +- .../warp_engine/update_authentication.rb | 4 +-- .../controllers/warp_engine/api_controller.rb | 4 ++- .../concerns/warp_engine/soft_deletable.rb | 22 +++++++++++++++ .../models/warp_engine/application_record.rb | 8 ++++++ .../models/warp_engine/application_token.rb | 11 +++----- .../app/models/warp_engine/device_grant.rb | 4 --- .../app/models/warp_engine/download.rb | 12 ++------ .../app/models/warp_engine/external_link.rb | 12 ++------ .../app/models/warp_engine/pipeline.rb | 17 +++++------ .../app/models/warp_engine/platform_link.rb | 13 ++------- .../app/models/warp_engine/release.rb | 22 +++++++-------- .../app/models/warp_engine/release_asset.rb | 12 ++------ .../app/models/warp_engine/software.rb | 18 ++++-------- .../app/models/warp_engine/software_image.rb | 8 ------ .../warp_engine/access_policy_guard.rb | 16 +++++++++++ .../warp_engine/platforms/metadata_parsing.rb | 13 +++++++++ .../platforms/software_persistence.rb | 8 +++--- .../services/warp_engine/asset_coverage.rb | 6 ++-- .../services/warp_engine/builds_service.rb | 2 +- .../services/warp_engine/download_service.rb | 20 ++++--------- .../warp_engine/file_manager_service.rb | 10 +++++-- .../app/services/warp_engine/file_service.rb | 15 ++++------ .../warp_engine/pipeline_sync_service.rb | 10 +++---- .../warp_engine/platforms/tic80/service.rb | 13 --------- .../warp_engine/secret_sync_service.rb | 28 +++++++++++-------- .../software_highlighted_service.rb | 12 ++++---- .../warp_engine/software_response_builder.rb | 4 +-- libs/ruby/warp_engine/lib/warp_engine.rb | 1 + .../warp_engine/lib/warp_engine/access.rb | 2 +- .../lib/warp_engine/access_denied.rb | 3 ++ .../spec/models/application_token_spec.rb | 4 +-- .../warp_engine/spec/models/pipeline_spec.rb | 14 +++++++--- .../warp_engine/spec/models/software_spec.rb | 11 ++++++-- 40 files changed, 207 insertions(+), 182 deletions(-) create mode 100644 libs/ruby/warp_engine/app/models/concerns/warp_engine/soft_deletable.rb create mode 100644 libs/ruby/warp_engine/app/services/concerns/warp_engine/access_policy_guard.rb create mode 100644 libs/ruby/warp_engine/lib/warp_engine/access_denied.rb diff --git a/libs/ruby/warp_engine/app/admin/application_tokens.rb b/libs/ruby/warp_engine/app/admin/application_tokens.rb index 217a624..d95ee33 100644 --- a/libs/ruby/warp_engine/app/admin/application_tokens.rb +++ b/libs/ruby/warp_engine/app/admin/application_tokens.rb @@ -96,6 +96,10 @@ ActiveAdmin.register WarpEngine::ApplicationToken, as: "Application Token" do end controller do + def scoped_collection + super.kept + end + def create create! do |success, _failure| success.html do diff --git a/libs/ruby/warp_engine/app/admin/downloads.rb b/libs/ruby/warp_engine/app/admin/downloads.rb index b1b9ab9..05b6f26 100644 --- a/libs/ruby/warp_engine/app/admin/downloads.rb +++ b/libs/ruby/warp_engine/app/admin/downloads.rb @@ -100,7 +100,7 @@ ActiveAdmin.register WarpEngine::Download, as: "Download" do controller do def scoped_collection - super.includes(release: :software) + super.kept.includes(release: :software) end end end diff --git a/libs/ruby/warp_engine/app/admin/external_links.rb b/libs/ruby/warp_engine/app/admin/external_links.rb index f535ff0..d78a4ac 100644 --- a/libs/ruby/warp_engine/app/admin/external_links.rb +++ b/libs/ruby/warp_engine/app/admin/external_links.rb @@ -5,6 +5,12 @@ ActiveAdmin.register WarpEngine::ExternalLink, as: "External Link" do belongs_to :software + controller do + def scoped_collection + super.kept + end + end + index do selectable_column id_column diff --git a/libs/ruby/warp_engine/app/admin/pipelines.rb b/libs/ruby/warp_engine/app/admin/pipelines.rb index ce603d5..4364ab3 100644 --- a/libs/ruby/warp_engine/app/admin/pipelines.rb +++ b/libs/ruby/warp_engine/app/admin/pipelines.rb @@ -47,7 +47,7 @@ ActiveAdmin.register WarpEngine::Pipeline, as: "Pipeline" do f.inputs do f.input :platform, as: :select, collection: WarpEngine::PlatformLink::SUPPORTED_PLATFORMS f.input :software_id, as: :select, - collection: WarpEngine::Software.order(:title).map { |s| [ s.title, s.id ] }, + collection: WarpEngine::Software.kept.order(:title).map { |s| [ s.title, s.id ] }, include_blank: "- none -", hint: "One pipeline per software. Picking one that another pipeline already " \ "has moves the link here — that pipeline is left without a software, " \ @@ -124,7 +124,7 @@ ActiveAdmin.register WarpEngine::Pipeline, as: "Pipeline" do controller do def scoped_collection - super.includes(:software) + super.kept.includes(:software) end end end diff --git a/libs/ruby/warp_engine/app/admin/platform_links.rb b/libs/ruby/warp_engine/app/admin/platform_links.rb index b0aa8c2..1b27bf5 100644 --- a/libs/ruby/warp_engine/app/admin/platform_links.rb +++ b/libs/ruby/warp_engine/app/admin/platform_links.rb @@ -14,6 +14,12 @@ ActiveAdmin.register WarpEngine::PlatformLink, as: "Platform Link" do scope("Bevy") { |s| s.where(platform: "bevy") } scope("Phaser") { |s| s.where(platform: "phaser") } + controller do + def scoped_collection + super.kept + end + end + index do selectable_column id_column diff --git a/libs/ruby/warp_engine/app/admin/releases.rb b/libs/ruby/warp_engine/app/admin/releases.rb index eb1ccbe..14724dd 100644 --- a/libs/ruby/warp_engine/app/admin/releases.rb +++ b/libs/ruby/warp_engine/app/admin/releases.rb @@ -17,6 +17,12 @@ ActiveAdmin.register WarpEngine::Release, as: "Release" do filter :version + controller do + def scoped_collection + super.kept + end + end + show do attributes_table do row :id diff --git a/libs/ruby/warp_engine/app/admin/softwares.rb b/libs/ruby/warp_engine/app/admin/softwares.rb index bd23243..1c383fd 100644 --- a/libs/ruby/warp_engine/app/admin/softwares.rb +++ b/libs/ruby/warp_engine/app/admin/softwares.rb @@ -150,7 +150,7 @@ ActiveAdmin.register WarpEngine::Software, as: "Software" do controller do def scoped_collection - super.includes({ releases: :release_assets }, software_images: :image) + super.kept.includes({ releases: :release_assets }, software_images: :image) end def find_resource diff --git a/libs/ruby/warp_engine/app/controllers/concerns/warp_engine/update_authentication.rb b/libs/ruby/warp_engine/app/controllers/concerns/warp_engine/update_authentication.rb index c69bbdb..978732d 100644 --- a/libs/ruby/warp_engine/app/controllers/concerns/warp_engine/update_authentication.rb +++ b/libs/ruby/warp_engine/app/controllers/concerns/warp_engine/update_authentication.rb @@ -43,7 +43,7 @@ module WarpEngine token = current_application_token return true if token.nil? || token.unrestricted? - software = WarpEngine::Software.find_by(name: name) + software = WarpEngine::Software.kept.find_by(name: name) return true if software.nil? || software.owner_id.nil? software.owner_type == token.owner_type && software.owner_id == token.owner_id @@ -53,7 +53,7 @@ module WarpEngine token = current_application_token return if token.nil? || token.unrestricted? - software = WarpEngine::Software.find_by(name: name) + software = WarpEngine::Software.kept.find_by(name: name) return if software.nil? || software.owner_id.present? software.update_columns(owner_type: token.owner_type, owner_id: token.owner_id) diff --git a/libs/ruby/warp_engine/app/controllers/warp_engine/api_controller.rb b/libs/ruby/warp_engine/app/controllers/warp_engine/api_controller.rb index 5f05267..d5601d5 100644 --- a/libs/ruby/warp_engine/app/controllers/warp_engine/api_controller.rb +++ b/libs/ruby/warp_engine/app/controllers/warp_engine/api_controller.rb @@ -1,3 +1,5 @@ +require "warp_engine/access_denied" + module WarpEngine class ApiController < ActionController::API resource_description do @@ -10,7 +12,7 @@ module WarpEngine include WarpEngine::ApiErrorRendering include WarpEngine::SubjectAuthentication - rescue_from WarpEngine::DownloadService::Denied do + rescue_from WarpEngine::AccessDenied do render json: { error: "Forbidden" }, status: :forbidden end diff --git a/libs/ruby/warp_engine/app/models/concerns/warp_engine/soft_deletable.rb b/libs/ruby/warp_engine/app/models/concerns/warp_engine/soft_deletable.rb new file mode 100644 index 0000000..55a88d2 --- /dev/null +++ b/libs/ruby/warp_engine/app/models/concerns/warp_engine/soft_deletable.rb @@ -0,0 +1,22 @@ +module WarpEngine + module SoftDeletable + extend ActiveSupport::Concern + + included do + scope :kept, -> { where(deleted_at: nil) } + scope :discarded, -> { where.not(deleted_at: nil) } + end + + def soft_delete! + update_column(:deleted_at, Time.current) + end + + def restore! + update_column(:deleted_at, nil) + end + + def discarded? + deleted_at.present? + end + end +end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/application_record.rb b/libs/ruby/warp_engine/app/models/warp_engine/application_record.rb index 9c62512..f55fe9d 100644 --- a/libs/ruby/warp_engine/app/models/warp_engine/application_record.rb +++ b/libs/ruby/warp_engine/app/models/warp_engine/application_record.rb @@ -1,5 +1,13 @@ module WarpEngine class ApplicationRecord < ActiveRecord::Base self.abstract_class = true + + def self.ransackable_attributes(auth_object = nil) + column_names + end + + def self.ransackable_associations(auth_object = nil) + reflect_on_all_associations.map(&:name).map(&:to_s) + end end end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/application_token.rb b/libs/ruby/warp_engine/app/models/warp_engine/application_token.rb index e0c7cc2..57e5e1a 100644 --- a/libs/ruby/warp_engine/app/models/warp_engine/application_token.rb +++ b/libs/ruby/warp_engine/app/models/warp_engine/application_token.rb @@ -9,12 +9,13 @@ module WarpEngine CATALOG_SCOPE = "catalog".freeze + include SoftDeletable + attr_reader :plain_token belongs_to :owner, polymorphic: true - default_scope { where(deleted_at: nil) } - scope :active, -> { where("expires_at IS NULL OR expires_at > ?", Time.current) } + scope :active, -> { kept.where("expires_at IS NULL OR expires_at > ?", Time.current) } before_validation :assign_owner_type, on: :create before_validation :generate_token, on: :create @@ -45,7 +46,7 @@ module WarpEngine end def revoke! - update_column(:deleted_at, Time.current) + soft_delete! end def touch_last_used! @@ -60,10 +61,6 @@ module WarpEngine self.scopes = value.to_s.split(",").map(&:strip).reject(&:blank?).uniq end - def self.ransackable_attributes(auth_object = nil) - %w[created_at deleted_at expires_at id last_used_at name owner_id owner_type token_prefix unrestricted updated_at] - end - def self.ransackable_associations(auth_object = nil) [] end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/device_grant.rb b/libs/ruby/warp_engine/app/models/warp_engine/device_grant.rb index b423f50..d814fd3 100644 --- a/libs/ruby/warp_engine/app/models/warp_engine/device_grant.rb +++ b/libs/ruby/warp_engine/app/models/warp_engine/device_grant.rb @@ -46,10 +46,6 @@ module WarpEngine where(expires_at: ...Time.current).where.not(issued_token: nil).update_all(issued_token: nil) end - def self.ransackable_attributes(auth_object = nil) - %w[approved_at client_name created_at denied_at expires_at id subject_id subject_type updated_at user_code] - end - def self.ransackable_associations(auth_object = nil) [] end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/download.rb b/libs/ruby/warp_engine/app/models/warp_engine/download.rb index 0247998..431835a 100644 --- a/libs/ruby/warp_engine/app/models/warp_engine/download.rb +++ b/libs/ruby/warp_engine/app/models/warp_engine/download.rb @@ -1,19 +1,11 @@ module WarpEngine class Download < ApplicationRecord + include SoftDeletable + belongs_to :release, optional: true validates :file_path, presence: true - default_scope { where(deleted_at: nil) } - - def self.ransackable_attributes(auth_object = nil) - %w[created_at deleted_at file_path id ip_address referer release_id updated_at user_agent] - end - - def self.ransackable_associations(auth_object = nil) - %w[release] - end - ActiveSupport.run_load_hooks(:warp_engine_download, self) end end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/external_link.rb b/libs/ruby/warp_engine/app/models/warp_engine/external_link.rb index b582f01..fd316dc 100644 --- a/libs/ruby/warp_engine/app/models/warp_engine/external_link.rb +++ b/libs/ruby/warp_engine/app/models/warp_engine/external_link.rb @@ -2,21 +2,13 @@ module WarpEngine class ExternalLink < ApplicationRecord self.table_name = "external_links" + include SoftDeletable + belongs_to :software validates :label, presence: true validates :url, presence: true - default_scope { where(deleted_at: nil) } - - def self.ransackable_attributes(auth_object = nil) - %w[created_at deleted_at id label software_id updated_at url] - end - - def self.ransackable_associations(auth_object = nil) - %w[software] - end - ActiveSupport.run_load_hooks(:warp_engine_external_link, self) end end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/pipeline.rb b/libs/ruby/warp_engine/app/models/warp_engine/pipeline.rb index 71c25be..3f176a8 100644 --- a/libs/ruby/warp_engine/app/models/warp_engine/pipeline.rb +++ b/libs/ruby/warp_engine/app/models/warp_engine/pipeline.rb @@ -4,12 +4,12 @@ module WarpEngine UNKNOWN_PLATFORM = "unknown".freeze + include SoftDeletable + belongs_to :software, class_name: "WarpEngine::Software", optional: true attr_reader :software_taken_from - default_scope { where(deleted_at: nil) } - before_save :claim_software_from_other_pipelines, if: :will_save_change_to_software_id? validates :woodpecker_repo_id, presence: true, uniqueness: true @@ -18,7 +18,9 @@ module WarpEngine validates :platform, presence: true, inclusion: { in: WarpEngine::PlatformLink::SUPPORTED_PLATFORMS + [ UNKNOWN_PLATFORM ] } - scope :active, -> { where(active: true) } + scope :active, -> { kept.where(active: true) } + + scope :by_remote_repo_id, ->(id) { where(woodpecker_repo_id: id) } def full_name "#{repo_owner}/#{repo_name}" @@ -32,13 +34,8 @@ module WarpEngine self.woodpecker_repo_id = value end - def self.ransackable_attributes(auth_object = nil) - %w[active created_at deleted_at id last_pipeline_at last_pipeline_status - platform repo_name repo_owner software_id woodpecker_repo_id] - end - - def self.ransackable_associations(auth_object = nil) - %w[software] + def self.find_or_initialize_by_remote_repo_id(id) + find_or_initialize_by(woodpecker_repo_id: id) end private diff --git a/libs/ruby/warp_engine/app/models/warp_engine/platform_link.rb b/libs/ruby/warp_engine/app/models/warp_engine/platform_link.rb index 106df81..e53e43f 100644 --- a/libs/ruby/warp_engine/app/models/warp_engine/platform_link.rb +++ b/libs/ruby/warp_engine/app/models/warp_engine/platform_link.rb @@ -4,23 +4,16 @@ module WarpEngine SUPPORTED_PLATFORMS = WarpEngine::Platform::NAMES + include SoftDeletable + validates :name, presence: true validates :url, presence: true validates :platform, presence: true, inclusion: { in: SUPPORTED_PLATFORMS } - default_scope { where(deleted_at: nil) } scope :ordered, -> { order(:position) } - def self.ransackable_attributes(auth_object = nil) - %w[created_at deleted_at id name platform position updated_at url] - end - - def self.ransackable_associations(auth_object = nil) - [] - end - def self.for_platform(platform) - ordered.where(platform: platform).to_a + kept.ordered.where(platform: platform).to_a end ActiveSupport.run_load_hooks(:warp_engine_platform_link, self) diff --git a/libs/ruby/warp_engine/app/models/warp_engine/release.rb b/libs/ruby/warp_engine/app/models/warp_engine/release.rb index 8938887..327c7af 100644 --- a/libs/ruby/warp_engine/app/models/warp_engine/release.rb +++ b/libs/ruby/warp_engine/app/models/warp_engine/release.rb @@ -2,19 +2,25 @@ module WarpEngine class Release < ApplicationRecord self.table_name = "releases" + include SoftDeletable + belongs_to :software - has_many :downloads - has_many :release_assets + has_many :downloads, -> { kept } + has_many :release_assets, -> { kept } accepts_nested_attributes_for :release_assets, allow_destroy: true validates :version, presence: true validates :version, uniqueness: { scope: :software_id } - default_scope { where(deleted_at: nil) } - validate :c64_cannot_be_web_playable + def self.latest_non_dev(releases) + sorted = releases.sort_by { |r| [r.created_at || Time.at(0), r.id] }.reverse + candidates = sorted.reject { |r| r.version.to_s.start_with?("dev-") } + candidates.empty? ? sorted.first : candidates.first + end + def c64_cannot_be_web_playable return unless software&.platform == "c64" @@ -24,14 +30,6 @@ module WarpEngine end end - def self.ransackable_attributes(auth_object = nil) - %w[created_at deleted_at id software_id updated_at version] - end - - def self.ransackable_associations(auth_object = nil) - %w[downloads release_assets software] - end - ActiveSupport.run_load_hooks(:warp_engine_release, self) end end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/release_asset.rb b/libs/ruby/warp_engine/app/models/warp_engine/release_asset.rb index 847b645..29eca9d 100644 --- a/libs/ruby/warp_engine/app/models/warp_engine/release_asset.rb +++ b/libs/ruby/warp_engine/app/models/warp_engine/release_asset.rb @@ -4,6 +4,8 @@ module WarpEngine win_x86 win_x64 linux_x86 linux_x64 linux_arm64 mac_x64 mac_arm64 mac_universal].freeze + include SoftDeletable + belongs_to :release enum :kind, KINDS.index_by(&:itself) @@ -11,21 +13,11 @@ module WarpEngine validates :path, presence: true validates :kind, uniqueness: { scope: :release_id } - default_scope { where(deleted_at: nil) } - def self.for_relative_path(relative) absolute = File.join(WarpEngine.config.file_container_path, relative.to_s) find_by(path: absolute) || where("path LIKE ?", "%/#{sanitize_sql_like(relative.to_s)}").first end - def self.ransackable_attributes(auth_object = nil) - %w[created_at deleted_at id kind path release_id updated_at] - end - - def self.ransackable_associations(auth_object = nil) - %w[release] - end - ActiveSupport.run_load_hooks(:warp_engine_release_asset, self) end end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/software.rb b/libs/ruby/warp_engine/app/models/warp_engine/software.rb index a255071..979d1f9 100644 --- a/libs/ruby/warp_engine/app/models/warp_engine/software.rb +++ b/libs/ruby/warp_engine/app/models/warp_engine/software.rb @@ -4,14 +4,16 @@ module WarpEngine STATUSES = %w[development demo released archived].freeze + include SoftDeletable + belongs_to :owner, polymorphic: true, optional: true has_many :software_images, -> { ordered }, foreign_key: :software_id, dependent: :destroy has_many :images, through: :software_images - has_many :releases, foreign_key: :software_id + has_many :releases, -> { kept }, foreign_key: :software_id has_many :downloads, through: :releases - has_many :external_links, foreign_key: :software_id - has_one :pipeline, foreign_key: :software_id + has_many :external_links, -> { kept }, foreign_key: :software_id + has_one :pipeline, -> { kept }, foreign_key: :software_id accepts_nested_attributes_for :software_images, allow_destroy: true accepts_nested_attributes_for :external_links, allow_destroy: true @@ -22,16 +24,6 @@ module WarpEngine validates :platform, presence: true, inclusion: { in: WarpEngine::Platform::NAMES } validates :status, inclusion: { in: STATUSES }, allow_blank: true - default_scope { where(deleted_at: nil) } - - def self.ransackable_attributes(auth_object = nil) - %w[author created_at desc highlighted id license name owner_id owner_type platform site status story title updated_at] - end - - def self.ransackable_associations(auth_object = nil) - %w[releases external_links software_images images] - end - ActiveSupport.run_load_hooks(:warp_engine_software, self) end end diff --git a/libs/ruby/warp_engine/app/models/warp_engine/software_image.rb b/libs/ruby/warp_engine/app/models/warp_engine/software_image.rb index 0f61851..4b81305 100644 --- a/libs/ruby/warp_engine/app/models/warp_engine/software_image.rb +++ b/libs/ruby/warp_engine/app/models/warp_engine/software_image.rb @@ -15,14 +15,6 @@ module WarpEngine scope :ordered, -> { order(:position) } - def self.ransackable_attributes(auth_object = nil) - %w[created_at id image_id is_default position software_id updated_at] - end - - def self.ransackable_associations(auth_object = nil) - %w[image software] - end - ActiveSupport.run_load_hooks(:warp_engine_software_image, self) private diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/access_policy_guard.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/access_policy_guard.rb new file mode 100644 index 0000000..e2153e9 --- /dev/null +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/access_policy_guard.rb @@ -0,0 +1,16 @@ +module WarpEngine + module AccessPolicyGuard + private + + def authorize_download!(asset:, subject:, request: nil) + grant = WarpEngine.access_policy.authorize_download(asset: asset, subject: subject, request: request) + raise WarpEngine::AccessDenied if grant.nil? + grant + rescue WarpEngine::AccessDenied + raise + rescue StandardError => e + Rails.logger.error("[WarpEngine::AccessPolicy] #{e.class}: #{e.message}") + raise WarpEngine::AccessDenied + end + end +end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/platforms/metadata_parsing.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/platforms/metadata_parsing.rb index 3d32e6c..064a21e 100644 --- a/libs/ruby/warp_engine/app/services/concerns/warp_engine/platforms/metadata_parsing.rb +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/platforms/metadata_parsing.rb @@ -11,6 +11,19 @@ module WarpEngine raw = JSON.parse(File.read(path), symbolize_names: true) raw.slice(*METADATA_KEYS) end + + def parse_lua_metadata(source_path) + metadata = {} + File.foreach(source_path) do |line| + break unless line.start_with?("--") + parts = line[2..].split(":", 2) + next if parts.length != 2 + key = parts[0].strip.downcase.to_sym + value = parts[1].strip + metadata[key] = value + end + metadata.slice(*METADATA_KEYS) + end end end end diff --git a/libs/ruby/warp_engine/app/services/concerns/warp_engine/platforms/software_persistence.rb b/libs/ruby/warp_engine/app/services/concerns/warp_engine/platforms/software_persistence.rb index 8ae1a18..6789d79 100644 --- a/libs/ruby/warp_engine/app/services/concerns/warp_engine/platforms/software_persistence.rb +++ b/libs/ruby/warp_engine/app/services/concerns/warp_engine/platforms/software_persistence.rb @@ -4,7 +4,7 @@ module WarpEngine private def update_or_create_software(attrs) - software = WarpEngine::Software.unscoped.find_or_initialize_by(name: attrs[:name]) + software = WarpEngine::Software.find_or_initialize_by(name: attrs[:name]) software.assign_attributes(attrs.except(:name)) software.deleted_at = nil software.save! @@ -12,14 +12,14 @@ module WarpEngine end def upsert_external_link(software_id, label, url) - link = WarpEngine::ExternalLink.unscoped.find_or_initialize_by(software_id: software_id, label: label) + link = WarpEngine::ExternalLink.find_or_initialize_by(software_id: software_id, label: label) link.url = url link.deleted_at = nil link.save! end def create_release_if_not_exists(attrs) - existing = WarpEngine::Release.unscoped.find_by(software_id: attrs[:software_id], version: attrs[:version]) + existing = WarpEngine::Release.find_by(software_id: attrs[:software_id], version: attrs[:version]) return existing if existing WarpEngine::Release.create!(attrs) @@ -27,7 +27,7 @@ module WarpEngine def sync_release_assets(release, kind_paths) kind_paths.each do |kind, path| - asset = WarpEngine::ReleaseAsset.unscoped.find_or_initialize_by(release_id: release.id, kind: kind) + asset = WarpEngine::ReleaseAsset.find_or_initialize_by(release_id: release.id, kind: kind) asset.path = path asset.deleted_at = nil asset.save! diff --git a/libs/ruby/warp_engine/app/services/warp_engine/asset_coverage.rb b/libs/ruby/warp_engine/app/services/warp_engine/asset_coverage.rb index b8bad94..b2932f5 100644 --- a/libs/ruby/warp_engine/app/services/warp_engine/asset_coverage.rb +++ b/libs/ruby/warp_engine/app/services/warp_engine/asset_coverage.rb @@ -67,7 +67,7 @@ module WarpEngine private def scope - relation = WarpEngine::Software.order(:name).includes(releases: :release_assets) + relation = WarpEngine::Software.kept.order(:name).includes(releases: :release_assets) relation = relation.where(name: name) if name relation = relation.where(platform: platform) if platform relation @@ -113,9 +113,7 @@ module WarpEngine def sorted(all) = all.sort_by { |release| [ release.created_at || Time.at(0), release.id ] }.reverse def latest(all) - candidates = all.reject { |release| release.version.to_s.start_with?("dev-") } - candidates = all if candidates.empty? - sorted(candidates).first + Release.latest_non_dev(all) end end end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/builds_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/builds_service.rb index 65bac81..a9221db 100644 --- a/libs/ruby/warp_engine/app/services/warp_engine/builds_service.rb +++ b/libs/ruby/warp_engine/app/services/warp_engine/builds_service.rb @@ -11,7 +11,7 @@ module WarpEngine end def show(name) - software = WarpEngine::Software.find_by!(name: name) + software = WarpEngine::Software.kept.find_by!(name: name) expected = WarpEngine::Platform.find!(software.platform).expected_kinds releases = software.releases.includes(:release_assets).order(updated_at: :desc) diff --git a/libs/ruby/warp_engine/app/services/warp_engine/download_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/download_service.rb index ca2c122..194a7c7 100644 --- a/libs/ruby/warp_engine/app/services/warp_engine/download_service.rb +++ b/libs/ruby/warp_engine/app/services/warp_engine/download_service.rb @@ -1,14 +1,18 @@ +require "warp_engine/access_denied" + module WarpEngine class DownloadService - class Denied < StandardError; end + Denied = WarpEngine::AccessDenied + + include AccessPolicyGuard def locate(path:, ip:, user_agent:, referer:, subject: nil, request: nil) relative = path.to_s return nil unless storage.file?(relative) asset = find_asset(relative) - grant = authorize!(asset, subject, request) + grant = authorize_download!(asset: asset, subject: subject, request: request) log_download(relative, asset: asset, ip: ip, user_agent: user_agent, referer: referer, subject: subject) @@ -31,18 +35,6 @@ module WarpEngine WarpEngine.storage end - def authorize!(asset, subject, request) - grant = WarpEngine.access_policy.authorize_download(asset: asset, subject: subject, request: request) - raise Denied if grant.nil? - - grant - rescue Denied - raise - rescue StandardError => e - Rails.logger.error("[WarpEngine::AccessPolicy] #{e.class}: #{e.message}") - raise Denied - end - def find_asset(relative) WarpEngine::ReleaseAsset.for_relative_path(relative) end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/file_manager_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/file_manager_service.rb index 1bafd5d..25824bc 100644 --- a/libs/ruby/warp_engine/app/services/warp_engine/file_manager_service.rb +++ b/libs/ruby/warp_engine/app/services/warp_engine/file_manager_service.rb @@ -30,7 +30,7 @@ module WarpEngine safe_name = sanitize_name(uploaded_file.original_filename) target = dir.join(safe_name) - raise ArgumentError, "Path escape" unless target.to_s.start_with?(base_path.to_s) + ensure_within_base!(target) IO.copy_stream(uploaded_file.to_io, target.to_s) target.relative_path_from(base_path).to_s @@ -53,7 +53,7 @@ module WarpEngine safe_name = sanitize_name(new_name) new_full = full.parent.join(safe_name) - raise ArgumentError, "Path escape" unless new_full.to_s.start_with?(base_path.to_s) + ensure_within_base!(new_full) full.rename(new_full) new_full.relative_path_from(base_path).to_s @@ -65,7 +65,7 @@ module WarpEngine safe_name = sanitize_name(folder_name) new_dir = parent.join(safe_name) - raise ArgumentError, "Path escape" unless new_dir.to_s.start_with?(base_path.to_s) + ensure_within_base!(new_dir) new_dir.mkdir new_dir.relative_path_from(base_path).to_s @@ -73,6 +73,10 @@ module WarpEngine private + def ensure_within_base!(path) + raise ArgumentError, "Path escape" unless path.to_s.start_with?(base_path.to_s) + end + def safe_path!(relative_path) cleaned = relative_path.to_s.gsub("..", "").squeeze("/").gsub(%r{^/|/$}, "") full = base_path.join(cleaned) diff --git a/libs/ruby/warp_engine/app/services/warp_engine/file_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/file_service.rb index b2cee17..96a1dcf 100644 --- a/libs/ruby/warp_engine/app/services/warp_engine/file_service.rb +++ b/libs/ruby/warp_engine/app/services/warp_engine/file_service.rb @@ -1,6 +1,10 @@ +require "warp_engine/access_denied" + module WarpEngine class FileService + include AccessPolicyGuard + def show(input, subject: nil) relative = input.path.to_s @@ -21,19 +25,10 @@ module WarpEngine private def authorize!(relative, subject) - return WarpEngine::Access::Grant::OPEN if WarpEngine::AccessPolicy.open? asset = WarpEngine::ReleaseAsset.for_relative_path(relative) - grant = WarpEngine.access_policy.authorize_download(asset: asset, subject: subject, request: nil) - raise WarpEngine::DownloadService::Denied if grant.nil? - - grant - rescue WarpEngine::DownloadService::Denied - raise - rescue StandardError => e - Rails.logger.error("[WarpEngine::AccessPolicy] #{e.class}: #{e.message}") - raise WarpEngine::DownloadService::Denied + authorize_download!(asset: asset, subject: subject, request: nil) end def storage diff --git a/libs/ruby/warp_engine/app/services/warp_engine/pipeline_sync_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/pipeline_sync_service.rb index e357aae..21b72c4 100644 --- a/libs/ruby/warp_engine/app/services/warp_engine/pipeline_sync_service.rb +++ b/libs/ruby/warp_engine/app/services/warp_engine/pipeline_sync_service.rb @@ -10,7 +10,7 @@ module WarpEngine remote_ids = remote_repos.map(&:id) remote_repos.each do |remote| - record = Pipeline.unscoped.find_or_initialize_by(woodpecker_repo_id: remote.id) + record = Pipeline.find_or_initialize_by_remote_repo_id(remote.id) was_new = record.new_record? record.assign_attributes( @@ -27,7 +27,7 @@ module WarpEngine results[was_new ? :created : :updated] << record end - Pipeline.where.not(woodpecker_repo_id: remote_ids).find_each do |orphan| + Pipeline.kept.where.not(woodpecker_repo_id: remote_ids).find_each do |orphan| orphan.update!(active: false) if orphan.active? results[:deactivated] << orphan end @@ -42,7 +42,7 @@ module WarpEngine def deactivate(repo_id) @ci.deactivate_repo(repo_id) - record = Pipeline.find_by!(woodpecker_repo_id: repo_id) + record = Pipeline.kept.find_by!(woodpecker_repo_id: repo_id) record.update!(active: false) end @@ -50,7 +50,7 @@ module WarpEngine def sync_single(repo_id) remote = @ci.repo(repo_id) - record = Pipeline.unscoped.find_or_initialize_by(woodpecker_repo_id: repo_id) + record = Pipeline.find_or_initialize_by_remote_repo_id(repo_id) record.assign_attributes( repo_name: remote.name, repo_owner: remote.owner, active: remote.active?, deleted_at: nil @@ -63,7 +63,7 @@ module WarpEngine def assign_platform(record, repo_name) return unless record.platform.blank? || record.platform == Pipeline::UNKNOWN_PLATFORM - sw = Software.find_by(name: repo_name) + sw = Software.kept.find_by(name: repo_name) record.platform = sw&.platform || Pipeline::UNKNOWN_PLATFORM record.software = sw if sw end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/platforms/tic80/service.rb b/libs/ruby/warp_engine/app/services/warp_engine/platforms/tic80/service.rb index 1de7d31..0522466 100644 --- a/libs/ruby/warp_engine/app/services/warp_engine/platforms/tic80/service.rb +++ b/libs/ruby/warp_engine/app/services/warp_engine/platforms/tic80/service.rb @@ -21,19 +21,6 @@ module WarpEngine def parse_metadata(versioned) parse_lua_metadata(full_path("#{versioned}.lua")) end - - def parse_lua_metadata(source_path) - metadata = {} - File.foreach(source_path) do |line| - break unless line.start_with?("--") - parts = line[2..].split(":", 2) - next if parts.length != 2 - key = parts[0].strip.downcase.to_sym - value = parts[1].strip - metadata[key] = value - end - metadata.slice(*MetadataParsing::METADATA_KEYS) - end end end end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/secret_sync_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/secret_sync_service.rb index 1c7a3da..e94133d 100644 --- a/libs/ruby/warp_engine/app/services/warp_engine/secret_sync_service.rb +++ b/libs/ruby/warp_engine/app/services/warp_engine/secret_sync_service.rb @@ -36,22 +36,28 @@ module WarpEngine pipelines = pipelines_for_token(application_token) return { rotated: false, reason: "no pipelines" } if pipelines.empty? - new_token = ApplicationToken.create!( - name: "#{application_token.name} (rotated #{Date.current})", - owner_id: application_token.owner_id, - owner_type: application_token.owner_type, - scopes: application_token.scopes, - expires_at: application_token.expires_at, - unrestricted: application_token.unrestricted? - ) + new_token = nil + ActiveRecord::Base.transaction do + new_token = ApplicationToken.create!( + name: "#{application_token.name} (rotated #{Date.current})", + owner_id: application_token.owner_id, + owner_type: application_token.owner_type, + scopes: application_token.scopes, + expires_at: application_token.expires_at, + unrestricted: application_token.unrestricted? + ) + application_token.soft_delete! + end result = provision(new_token.plain_token, pipelines: pipelines) if result[:synced].any? - application_token.revoke! { rotated: true, new_token: new_token, sync_result: result } else - new_token.revoke! + ActiveRecord::Base.transaction do + application_token.restore! + new_token.soft_delete! + end { rotated: false, reason: "all pipelines failed", sync_result: result } end end @@ -60,7 +66,7 @@ module WarpEngine if application_token.unrestricted? Pipeline.active.to_a else - software_ids = Software.where( + software_ids = Software.kept.where( owner_type: application_token.owner_type, owner_id: application_token.owner_id ).pluck(:id) diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_highlighted_service.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_highlighted_service.rb index 0947efa..dd13e83 100644 --- a/libs/ruby/warp_engine/app/services/warp_engine/software_highlighted_service.rb +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_highlighted_service.rb @@ -3,13 +3,15 @@ module WarpEngine include SoftwareResponseBuilder def index(subject: nil) - software = visible_scope(subject).includes(:external_links, :software_images) - .where(highlighted: true) - .order(id: :desc) - .first + software = visible_scope(subject) + .includes(releases: :release_assets) + .includes(:external_links, :software_images) + .where(highlighted: true) + .order(id: :desc) + .first return nil unless software - releases = WarpEngine::Release.includes(:release_assets).where(software_id: software.id).to_a + releases = software.releases.to_a build_response(software, releases, download_counts_for(releases.map(&:id)), subject: subject) end end diff --git a/libs/ruby/warp_engine/app/services/warp_engine/software_response_builder.rb b/libs/ruby/warp_engine/app/services/warp_engine/software_response_builder.rb index b5511ae..e187787 100644 --- a/libs/ruby/warp_engine/app/services/warp_engine/software_response_builder.rb +++ b/libs/ruby/warp_engine/app/services/warp_engine/software_response_builder.rb @@ -4,7 +4,7 @@ module WarpEngine def build_response(software, releases, download_counts, subject: nil) sorted = releases.sort_by { |r| r.created_at || Time.at(0) }.reverse - latest = sorted.reject { |r| r.version.to_s.start_with?("dev-") }.first + latest = Release.latest_non_dev(releases) web_playable = latest if latest&.release_assets&.any? { |a| a.kind == "html" } total_downloads = releases.sum { |r| download_counts.fetch(r.id, 0) } @@ -36,7 +36,7 @@ module WarpEngine def download_counts_for(release_ids) return {} if release_ids.empty? - WarpEngine::Download.where(release_id: release_ids).group(:release_id).count + WarpEngine::Download.kept.where(release_id: release_ids).group(:release_id).count end end end diff --git a/libs/ruby/warp_engine/lib/warp_engine.rb b/libs/ruby/warp_engine/lib/warp_engine.rb index b9b5d6b..7d1694f 100644 --- a/libs/ruby/warp_engine/lib/warp_engine.rb +++ b/libs/ruby/warp_engine/lib/warp_engine.rb @@ -8,6 +8,7 @@ require "warp_engine/configuration" require "warp_engine/platform" require "warp_engine/storage" require "warp_engine/access" +require "warp_engine/access_denied" require "warp_engine/images" require "warp_engine/ci" diff --git a/libs/ruby/warp_engine/lib/warp_engine/access.rb b/libs/ruby/warp_engine/lib/warp_engine/access.rb index 06c7a93..121d161 100644 --- a/libs/ruby/warp_engine/lib/warp_engine/access.rb +++ b/libs/ruby/warp_engine/lib/warp_engine/access.rb @@ -4,7 +4,7 @@ module WarpEngine class Open def visible_software_scope(subject: nil) - WarpEngine::Software.all + WarpEngine::Software.kept end def access_for(software:, subject: nil) diff --git a/libs/ruby/warp_engine/lib/warp_engine/access_denied.rb b/libs/ruby/warp_engine/lib/warp_engine/access_denied.rb new file mode 100644 index 0000000..2720b6e --- /dev/null +++ b/libs/ruby/warp_engine/lib/warp_engine/access_denied.rb @@ -0,0 +1,3 @@ +module WarpEngine + class AccessDenied < StandardError; end +end diff --git a/libs/ruby/warp_engine/spec/models/application_token_spec.rb b/libs/ruby/warp_engine/spec/models/application_token_spec.rb index 6419d55..f31b008 100644 --- a/libs/ruby/warp_engine/spec/models/application_token_spec.rb +++ b/libs/ruby/warp_engine/spec/models/application_token_spec.rb @@ -109,8 +109,8 @@ RSpec.describe WarpEngine::ApplicationToken, type: :model do token = create(:application_token) token.revoke! - expect(described_class.find_by(id: token.id)).to be_nil - expect(described_class.unscoped.find(token.id).deleted_at).to be_present + expect(described_class.kept.find_by(id: token.id)).to be_nil + expect(described_class.find(token.id).deleted_at).to be_present end end diff --git a/libs/ruby/warp_engine/spec/models/pipeline_spec.rb b/libs/ruby/warp_engine/spec/models/pipeline_spec.rb index 17f33ca..0fb6110 100644 --- a/libs/ruby/warp_engine/spec/models/pipeline_spec.rb +++ b/libs/ruby/warp_engine/spec/models/pipeline_spec.rb @@ -36,18 +36,24 @@ RSpec.describe WarpEngine::Pipeline do end end - describe "default_scope" do + describe ".kept scope" do it "excludes soft-deleted records" do repo = create(:pipeline) repo.update_column(:deleted_at, Time.current) - expect(described_class.all).not_to include(repo) - expect(described_class.unscoped).to include(repo) + expect(described_class.kept).not_to include(repo) + end + + it "includes soft-deleted records without scope" do + repo = create(:pipeline) + repo.update_column(:deleted_at, Time.current) + + expect(described_class.all).to include(repo) end end describe ".active" do - it "returns only active repos" do + it "returns only active kept repos" do active = create(:pipeline, active: true) inactive = create(:pipeline, active: false) diff --git a/libs/ruby/warp_engine/spec/models/software_spec.rb b/libs/ruby/warp_engine/spec/models/software_spec.rb index 1e35cbc..af2b310 100644 --- a/libs/ruby/warp_engine/spec/models/software_spec.rb +++ b/libs/ruby/warp_engine/spec/models/software_spec.rb @@ -13,12 +13,19 @@ RSpec.describe WarpEngine::Software, type: :model do it { should have_many(:external_links) } it { should have_many(:software_images).dependent(:destroy) } - describe "default scope" do + describe ".kept scope" do it "excludes soft-deleted records" do active = create(:software) create(:software, deleted_at: Time.current) - expect(WarpEngine::Software.all).to eq([active]) + expect(WarpEngine::Software.kept).to eq([active]) + end + + it "includes soft-deleted records without scope" do + active = create(:software) + deleted = create(:software, deleted_at: Time.current) + + expect(WarpEngine::Software.all).to contain_exactly(active, deleted) end end end