build endpoints
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
module WarpEngine
|
||||
# Token-hitelesítés a publikáló (/build/*) endpointokhoz.
|
||||
# A hitelesítési forrás kizárólagos: :database módban a shared secret nem
|
||||
# érvényes, :env módban a DB-tokenek nem.
|
||||
module UpdateAuthentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_application_token
|
||||
|
||||
# A token kizárólag az X-Update-Secret headerből jöhet — URL-ben a secret
|
||||
# proxy- és access-logokba szivárogna.
|
||||
def update_authorized?(required_scope:)
|
||||
token = request.headers["X-Update-Secret"].presence
|
||||
return false if token.blank?
|
||||
|
||||
case WarpEngine.config.application_token_source
|
||||
when :database then database_token_authorized?(token, required_scope)
|
||||
else env_secret_authorized?(token)
|
||||
end
|
||||
end
|
||||
|
||||
def env_secret_authorized?(token)
|
||||
expected = WarpEngine.config.update_secret
|
||||
# Konfigurálatlan secret esetén az endpoint zárva marad.
|
||||
expected.present? && ActiveSupport::SecurityUtils.secure_compare(token, expected)
|
||||
end
|
||||
|
||||
def database_token_authorized?(token, required_scope)
|
||||
if WarpEngine.config.application_token_owner_class.blank?
|
||||
Rails.logger.error("[#{self.class.name}] application_token_source=:database, de application_token_owner_class nincs beállítva — minden kérés elutasítva")
|
||||
return false
|
||||
end
|
||||
|
||||
record = WarpEngine::ApplicationToken.authenticate(token, required_scope: required_scope)
|
||||
return false if record.nil?
|
||||
|
||||
record.touch_last_used!
|
||||
@current_application_token = record
|
||||
true
|
||||
end
|
||||
|
||||
# Owner-kényszer: csak :database módban (van token) és bekapcsolt
|
||||
# enforce_software_ownership mellett szűr. Owner nélküli software a
|
||||
# backfillig szabad préda — a kényszer bekapcsolása előtt kell backfillelni.
|
||||
def software_ownership_authorized?(name)
|
||||
return true unless WarpEngine.config.enforce_software_ownership
|
||||
|
||||
token = current_application_token
|
||||
return true if token.nil? || token.unrestricted?
|
||||
|
||||
software = WarpEngine::Software.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
|
||||
end
|
||||
|
||||
# Az először publikált (vagy backfill előtti, gazdátlan) software a beküldő
|
||||
# token ownerét kapja. Unrestricted (belső) token nem foglal ownert.
|
||||
def claim_software_ownership(name)
|
||||
token = current_application_token
|
||||
return if token.nil? || token.unrestricted?
|
||||
|
||||
software = WarpEngine::Software.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)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,6 +29,7 @@ module WarpEngine
|
||||
end
|
||||
|
||||
api :GET, "/api/software", "List all software entries with releases"
|
||||
param :owner_id, :number, required: false, desc: "Filter to the softwares of one owner (publisher)"
|
||||
returns code: 200, desc: "Wrapper object with softwares array" do
|
||||
property :softwares, Array, desc: "Array of software entries" do
|
||||
property :ID, Integer, desc: "Software ID"
|
||||
@@ -55,7 +56,7 @@ module WarpEngine
|
||||
end
|
||||
end
|
||||
def index
|
||||
render json: WarpEngine::SoftwareService.new.index
|
||||
render json: WarpEngine::SoftwareService.new.index(owner_id: params[:owner_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
module WarpEngine
|
||||
module Build
|
||||
class PublishController < ApiController
|
||||
include UpdateAuthentication
|
||||
|
||||
resource_description do
|
||||
short "Build release publishing"
|
||||
end
|
||||
|
||||
api :POST, "/build/publish", "Register an uploaded build as a release"
|
||||
header "X-Update-Secret", "Shared secret or application token (update scope)", required: true
|
||||
param :name, String, required: true, desc: "Software name"
|
||||
param :platform, String, required: true, desc: "Platform (tic80, love, ebitengine, c64, godot, bevy, phaser)"
|
||||
param :version, String, required: true, desc: "Version string"
|
||||
returns code: 200, desc: "JSON with the published name/platform/version"
|
||||
error code: 401, desc: "Invalid secret"
|
||||
error code: 403, desc: "Token does not own this software"
|
||||
error code: 400, desc: "Missing or invalid arguments"
|
||||
def create
|
||||
unless update_authorized?(required_scope: WarpEngine::ApplicationToken::UPDATE_SCOPE)
|
||||
return render json: { error: "Unauthorized" }, status: :unauthorized
|
||||
end
|
||||
|
||||
%i[name platform version].each do |key|
|
||||
return render json: { error: "#{key.to_s.capitalize} not provided" }, status: :bad_request if params[key].blank?
|
||||
end
|
||||
|
||||
unless software_ownership_authorized?(params[:name])
|
||||
return render json: { error: "Forbidden" }, status: :forbidden
|
||||
end
|
||||
|
||||
input = WarpEngine::UpdateInputDto.new(
|
||||
platform: params[:platform],
|
||||
name: params[:name],
|
||||
version: params[:version]
|
||||
)
|
||||
|
||||
WarpEngine::UpdateService.new.update(input)
|
||||
claim_software_ownership(params[:name])
|
||||
|
||||
render json: { published: true, name: params[:name], platform: params[:platform], version: params[:version] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
require "digest"
|
||||
|
||||
module WarpEngine
|
||||
module Build
|
||||
class UploadsController < ApiController
|
||||
include UpdateAuthentication
|
||||
|
||||
resource_description do
|
||||
short "Build artifact upload"
|
||||
end
|
||||
|
||||
# A release-fájlnevek kötött konvenciója: <name>-<version>.<ext> vagy
|
||||
# <name>-<version>-<target>.zip — az updater is ezeket keresi.
|
||||
NAME_FORMAT = /\A[A-Za-z0-9._-]+\z/
|
||||
|
||||
api :POST, "/build/upload", "Upload a build artifact into the drop area"
|
||||
header "X-Update-Secret", "Shared secret or application token (upload scope)", required: true
|
||||
param :name, String, required: true, desc: "Software name (filename must be prefixed with <name>-<version>)"
|
||||
param :version, String, required: true, desc: "Version string"
|
||||
param :file, File, required: true, desc: "Artifact file (multipart)"
|
||||
param :sha256, String, required: false, desc: "Expected SHA256 hex digest; on mismatch the upload is rejected"
|
||||
returns code: 200, desc: "JSON with stored file name, size and sha256"
|
||||
error code: 401, desc: "Invalid secret"
|
||||
error code: 403, desc: "Token does not own this software"
|
||||
error code: 400, desc: "Missing or invalid arguments"
|
||||
error code: 413, desc: "File larger than max_upload_size"
|
||||
error code: 422, desc: "SHA256 mismatch"
|
||||
def create
|
||||
unless update_authorized?(required_scope: WarpEngine::ApplicationToken::UPLOAD_SCOPE)
|
||||
return render json: { error: "Unauthorized" }, status: :unauthorized
|
||||
end
|
||||
|
||||
name = params[:name].to_s
|
||||
version = params[:version].to_s
|
||||
file = params[:file]
|
||||
|
||||
return render json: { error: "Invalid name" }, status: :bad_request unless name.match?(NAME_FORMAT)
|
||||
return render json: { error: "Invalid version" }, status: :bad_request unless version.match?(NAME_FORMAT)
|
||||
return render json: { error: "File not provided" }, status: :bad_request unless file.respond_to?(:original_filename)
|
||||
|
||||
unless software_ownership_authorized?(name)
|
||||
return render json: { error: "Forbidden" }, status: :forbidden
|
||||
end
|
||||
|
||||
filename = File.basename(file.original_filename.to_s)
|
||||
unless filename.start_with?("#{name}-#{version}.", "#{name}-#{version}-")
|
||||
return render json: { error: "Filename must be prefixed with #{name}-#{version}" }, status: :bad_request
|
||||
end
|
||||
|
||||
max = WarpEngine.config.max_upload_size
|
||||
if file.size > max
|
||||
return render json: { error: "File too large (max #{max / (1024 * 1024)}MB)" }, status: :payload_too_large
|
||||
end
|
||||
|
||||
digest = Digest::SHA256.file(file.tempfile.path).hexdigest
|
||||
if params[:sha256].present? && !ActiveSupport::SecurityUtils.secure_compare(params[:sha256].downcase, digest)
|
||||
return render json: { error: "SHA256 mismatch" }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
stored = WarpEngine::FileManagerService.new.upload("", file)
|
||||
render json: { file: stored, size: file.size, sha256: digest }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,73 +0,0 @@
|
||||
module WarpEngine
|
||||
class UpdateController < ApiController
|
||||
resource_description do
|
||||
short "Software updater"
|
||||
formats [ "text" ]
|
||||
end
|
||||
|
||||
rescue_from ArgumentError do |e|
|
||||
render plain: e.message, status: :bad_request
|
||||
end
|
||||
|
||||
rescue_from StandardError do |e|
|
||||
Rails.logger.error("[UpdateController] #{e.class}: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
|
||||
render plain: "Internal server error", status: :internal_server_error
|
||||
end
|
||||
|
||||
api :GET, "/update", "Update software version in database"
|
||||
param :secret, String, required: true, desc: "Shared secret or application token (X-Update-Secret header preferred)"
|
||||
param :platform, String, required: false, desc: "Platform (tic80, love, ebitengine, c64, godot, bevy, phaser)"
|
||||
param :name, String, required: false, desc: "Software name"
|
||||
param :version, String, required: true, desc: "Version string"
|
||||
returns code: 200, desc: "Plain text 'Updated'"
|
||||
error code: 401, desc: "Invalid secret"
|
||||
error code: 400, desc: "Version not provided or invalid arguments"
|
||||
error code: 500, desc: "Internal server error"
|
||||
def update
|
||||
return render plain: "Unauthorized", status: :unauthorized unless authorized?
|
||||
return render plain: "Version not provided", status: :bad_request if params[:version].blank?
|
||||
|
||||
input = WarpEngine::UpdateInputDto.new(
|
||||
platform: params[:platform],
|
||||
name: params[:name],
|
||||
version: params[:version]
|
||||
)
|
||||
|
||||
WarpEngine::UpdateService.new.update(input)
|
||||
render plain: "Updated"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# A hitelesítési forrás kizárólagos: :database módban a shared secret nem
|
||||
# érvényes, :env módban a DB-tokenek nem.
|
||||
def authorized?
|
||||
token = request.headers["X-Update-Secret"].presence || params[:secret].presence
|
||||
return false if token.blank?
|
||||
|
||||
case WarpEngine.config.application_token_source
|
||||
when :database then database_token_authorized?(token)
|
||||
else env_secret_authorized?(token)
|
||||
end
|
||||
end
|
||||
|
||||
def env_secret_authorized?(token)
|
||||
expected = WarpEngine.config.update_secret
|
||||
# Konfigurálatlan secret esetén az endpoint zárva marad.
|
||||
expected.present? && ActiveSupport::SecurityUtils.secure_compare(token, expected)
|
||||
end
|
||||
|
||||
def database_token_authorized?(token)
|
||||
if WarpEngine.config.application_token_owner_class.blank?
|
||||
Rails.logger.error("[UpdateController] application_token_source=:database, de application_token_owner_class nincs beállítva — minden kérés elutasítva")
|
||||
return false
|
||||
end
|
||||
|
||||
record = WarpEngine::ApplicationToken.authenticate(token, required_scope: WarpEngine::ApplicationToken::UPDATE_SCOPE)
|
||||
return false if record.nil?
|
||||
|
||||
record.touch_last_used!
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user