diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 26d1558c..a89b6ba6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,6 +9,7 @@ on: env: RUBY_VERSION: 3.2.6 NODE_VERSION: 18.17.1 + NODE_OPTIONS: "--experimental-global-webcrypto" DISABLE_SPRING: 1 CI: "true" @@ -39,7 +40,7 @@ jobs: fetch-depth: 1 - name: Install imagemagick and wkhtmltopdf - run: sudo apt install imagemagick wkhtmltopdf + run: sudo apt-get update && sudo apt-get install -y imagemagick wkhtmltopdf - uses: ruby/setup-ruby@v1 with: diff --git a/app/controllers/concerns/decidim/participatory_documents/needs_pdf_document.rb b/app/controllers/concerns/decidim/participatory_documents/needs_pdf_document.rb index 503ca799..fa9c5b53 100644 --- a/app/controllers/concerns/decidim/participatory_documents/needs_pdf_document.rb +++ b/app/controllers/concerns/decidim/participatory_documents/needs_pdf_document.rb @@ -40,6 +40,29 @@ def box_color_as_hsla(document, opacity: 0.95) "hsla(#{h}, #{s}%, 90%, #{opacity})" end + # The PDF is fetched by pdf.js through an ActiveStorage redirect, so when + # the file lives in a remote storage service (S3 and compatible) the + # final blob host must be allowed by the connect-src CSP directive. + def append_storage_host_to_csp + origin = pdf_storage_origin + content_security_policy.append_csp_directive("connect-src", origin) if origin + end + + def pdf_storage_origin + return if document.blank? || !document.file.attached? + + ActiveStorage::Current.url_options ||= { host: request.base_url } + uri = URI.parse(document.file.blob.url) + return if uri.host.blank? + return if uri.host == request.host && uri.port == request.port + + origin = "#{uri.scheme}://#{uri.host}" + origin += ":#{uri.port}" if uri.port != uri.default_port + origin + rescue StandardError + nil + end + def pdf_custom_style return if document.blank? diff --git a/app/controllers/decidim/participatory_documents/admin/documents_controller.rb b/app/controllers/decidim/participatory_documents/admin/documents_controller.rb index a4ffb05b..04071645 100644 --- a/app/controllers/decidim/participatory_documents/admin/documents_controller.rb +++ b/app/controllers/decidim/participatory_documents/admin/documents_controller.rb @@ -15,6 +15,8 @@ class DocumentsController < Admin::ApplicationController redirect_to(documents_path) if document.blank? end + before_action :append_storage_host_to_csp, only: [:pdf_viewer, :edit_pdf] + def index redirect_to(document_suggestions_path(document)) && return if document.present? && document.file.attached? end diff --git a/app/controllers/decidim/participatory_documents/documents_controller.rb b/app/controllers/decidim/participatory_documents/documents_controller.rb index 60667eba..e2660c75 100644 --- a/app/controllers/decidim/participatory_documents/documents_controller.rb +++ b/app/controllers/decidim/participatory_documents/documents_controller.rb @@ -5,6 +5,8 @@ module ParticipatoryDocuments class DocumentsController < Decidim::ParticipatoryDocuments::ApplicationController helper Decidim::LayoutHelper + before_action :append_storage_host_to_csp, only: [:pdf_viewer] + def pdf_viewer render layout: false end diff --git a/spec/controllers/decidim/participatory_documents/admin/documents_controller_spec.rb b/spec/controllers/decidim/participatory_documents/admin/documents_controller_spec.rb new file mode 100644 index 00000000..c69dd042 --- /dev/null +++ b/spec/controllers/decidim/participatory_documents/admin/documents_controller_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module ParticipatoryDocuments + module Admin + describe DocumentsController do + routes { Decidim::ParticipatoryDocuments::AdminEngine.routes } + + let(:organization) { component.organization } + let(:participatory_process) { component.participatory_space } + let(:component) { document.component } + let(:document) { create(:participatory_documents_document, :with_file) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + + before do + request.env["decidim.current_organization"] = organization + request.env["decidim.current_participatory_space"] = participatory_process + request.env["decidim.current_component"] = component + sign_in user + end + + describe "GET pdf_viewer" do + context "when the file is stored in a remote service" do + let(:remote_url) { "https://eu2.contabostorage.com/bucket/file.pdf?X-Amz-Signature=abc" } + + before do + allow(ActiveStorage::Blob.service).to receive(:url).and_return(remote_url) + end + + it "appends the storage host to the connect-src CSP directive" do + get :pdf_viewer + expect(response.headers["Content-Security-Policy"]).to match(%r{connect-src [^;]*https://eu2\.contabostorage\.com}) + end + end + end + + describe "GET edit_pdf" do + context "when the file is stored in a remote service" do + let(:remote_url) { "https://eu2.contabostorage.com/bucket/file.pdf?X-Amz-Signature=abc" } + + before do + allow(ActiveStorage::Blob.service).to receive(:url).and_return(remote_url) + end + + it "appends the storage host to the connect-src CSP directive" do + get :edit_pdf + expect(response.headers["Content-Security-Policy"]).to match(%r{connect-src [^;]*https://eu2\.contabostorage\.com}) + end + end + end + end + end + end +end diff --git a/spec/controllers/decidim/participatory_documents/documents_controller_spec.rb b/spec/controllers/decidim/participatory_documents/documents_controller_spec.rb new file mode 100644 index 00000000..eea114bf --- /dev/null +++ b/spec/controllers/decidim/participatory_documents/documents_controller_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module ParticipatoryDocuments + describe DocumentsController do + routes { Decidim::ParticipatoryDocuments::Engine.routes } + + let(:organization) { component.organization } + let(:participatory_process) { component.participatory_space } + let(:component) { document.component } + let(:document) { create(:participatory_documents_document, :with_file) } + + before do + request.env["decidim.current_organization"] = organization + request.env["decidim.current_participatory_space"] = participatory_process + request.env["decidim.current_component"] = component + end + + describe "GET pdf_viewer" do + it "renders the pdf_viewer template" do + get :pdf_viewer + expect(response).to render_template("pdf_viewer") + end + + context "when the file is stored in a remote service" do + let(:remote_url) { "https://eu2.contabostorage.com/bucket/file.pdf?X-Amz-Signature=abc" } + + before do + allow(ActiveStorage::Blob.service).to receive(:url).and_return(remote_url) + end + + it "appends the storage host to the connect-src CSP directive" do + get :pdf_viewer + expect(response.headers["Content-Security-Policy"]).to match(%r{connect-src [^;]*https://eu2\.contabostorage\.com}) + end + end + + context "when the file is stored in the local service" do + it "does not append the request host to the connect-src CSP directive" do + get :pdf_viewer + csp = response.headers["Content-Security-Policy"] + expect(csp).to match(/connect-src [^;]*'self'/) + expect(csp[/connect-src [^;]*/]).not_to include("test.host") + end + end + + context "when the document has no file attached" do + let(:document) { create(:participatory_documents_document) } + + it "renders without altering the connect-src CSP directive" do + get :pdf_viewer + expect(response.headers["Content-Security-Policy"][/connect-src [^;]*/]).not_to include("test.host") + end + end + end + end + end +end