diff --git a/Gemfile b/Gemfile index 4282b857c2..a792ac9229 100644 --- a/Gemfile +++ b/Gemfile @@ -8,16 +8,11 @@ gem "rails", "~> 8.0" gem "after_party" # Post-deployment tasks gem "amazing_print" # Easier console reading gem "authtrail" # Track Devise login activity -gem "azure-storage-blob", require: false +gem "azure-blob", require: false # Active Storage adapter for Azure (maintained replacement for retired azure-storage-blob) gem "blueprinter" # JSON serialization gem "bugsnag" # Error tracking in production gem "caxlsx", "~> 4.5" # Excel spreadsheets - TODO can we remove this version restriction? gem "caxlsx_rails", "~> 0.7.1" # Excel spreadsheets - TODO can we remove this version restriction? -# Ruby 4.0 ships a cgi stdlib with only escape/unescape; azure-storage-common needs the -# full library's CGI.parse to sign blob URLs. Rails 8 no longer pulls cgi in transitively, -# so without this the stripped stdlib wins and every Active Storage read/write 500s. -# Guarded by spec/lib/azure_storage_signing_spec.rb. Remove when azure-storage-blob is dropped. -gem "cgi", "~> 0.5.1" gem "cssbundling-rails", "~> 1.4" # CSS compilation gem "delayed_job_active_record" # Background job processing gem "devise" # Authentication diff --git a/Gemfile.lock b/Gemfile.lock index 7e5b3633a1..f230d691b7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,14 +92,9 @@ GEM dumb_delegator axe-core-rspec (4.12.0) axe-core-api (= 4.12.0) - azure-storage-blob (2.0.3) - azure-storage-common (~> 2.0) - nokogiri (~> 1, >= 1.10.8) - azure-storage-common (2.0.4) - faraday (~> 1.0) - faraday_middleware (~> 1.0, >= 1.0.0.rc1) - net-http-persistent (~> 4.0) - nokogiri (~> 1, >= 1.10.8) + azure-blob (0.8.0) + cgi + rexml base64 (0.3.0) bcrypt (3.1.22) benchmark (0.5.0) @@ -237,8 +232,6 @@ GEM faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.4) - faraday_middleware (1.2.1) - faraday (~> 1.0) ffi (1.17.4) ffi (1.17.4-arm64-darwin) ffi (1.17.4-x86_64-darwin) @@ -342,15 +335,12 @@ GEM benchmark logger mini_mime (1.1.5) - mini_portile2 (2.8.9) minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) multi_xml (0.8.1) bigdecimal (>= 3.1, < 5) multipart-post (2.4.1) - net-http-persistent (4.0.8) - connection_pool (>= 2.2.4, < 4) net-imap (0.6.4.1) date net-protocol @@ -361,9 +351,6 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) - nokogiri (1.19.4) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) nokogiri (1.19.4-arm64-darwin) racc (~> 1.4) nokogiri (1.19.4-x86_64-darwin) @@ -709,7 +696,6 @@ GEM PLATFORMS arm64-darwin - ruby x86_64-darwin x86_64-linux @@ -719,7 +705,7 @@ DEPENDENCIES annotaterb authtrail axe-core-rspec - azure-storage-blob + azure-blob blueprinter brakeman bugsnag @@ -729,7 +715,6 @@ DEPENDENCIES capybara-screenshot caxlsx (~> 4.5) caxlsx_rails (~> 0.7.1) - cgi (~> 0.5.1) cssbundling-rails (~> 1.4) database_cleaner-active_record delayed_job_active_record diff --git a/config/brakeman.ignore b/config/brakeman.ignore index fa9463628f..ae2dbc0410 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -22,8 +22,27 @@ 22 ], "note": "" + }, + { + "warning_type": "Unmaintained Dependency", + "warning_code": 122, + "fingerprint": "21ab0fe00fdd5899ffc405cff75aadb91b805ee996a614f7e27b08a287e9062d", + "check_name": "EOLRails", + "message": "Support for Rails 8.0.5.1 ends on 2026-10-07", + "file": "Gemfile.lock", + "line": 459, + "link": "https://brakemanscanner.org/docs/warning_types/unmaintained_dependency/", + "code": null, + "render_path": null, + "location": null, + "user_input": null, + "confidence": "Weak", + "cwe_id": [ + 1104 + ], + "note": "Rails EOL is tracked separately from this Azure adapter change; upgrading Rails is out of scope here." } ], - "updated": "2023-01-17 23:08:51 -0500", - "brakeman_version": "5.4.0" + "updated": "2026-08-08 00:00:00 -0500", + "brakeman_version": "7.1.2" } diff --git a/config/storage.yml b/config/storage.yml index 7717a63052..d081149eaa 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -23,7 +23,7 @@ local: # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) microsoft: - service: AzureStorage + service: AzureBlob storage_account_name: <%= ENV['STORAGE_ACCOUNT_NAME'] %> storage_access_key: <%= ENV['STORAGE_ACCESS_KEY'] %> container: <%= ENV['STORAGE_CONTAINER'] %> diff --git a/spec/lib/azure_storage_signing_spec.rb b/spec/lib/azure_storage_signing_spec.rb index cb0e44704d..e3dbbefa1d 100644 --- a/spec/lib/azure_storage_signing_spec.rb +++ b/spec/lib/azure_storage_signing_spec.rb @@ -1,39 +1,54 @@ -require "rails_helper" -require "azure/storage/blob" +# frozen_string_literal: true -# Production stores every attachment on Azure (config.active_storage.service = :microsoft), -# and azure-storage-common signs both uploads and download URLs with CGI.parse. Ruby 4.0's -# stdlib cgi ships only escape/unescape, so the Gemfile has to pin the real cgi gem back in -# — Rails 8 stopped pulling it in transitively. When that pin goes missing, every Active -# Storage read and write 500s in production while CI stays green, because the test -# environment uses Disk storage and never touches this code. +require "rails_helper" +require "active_storage/service/azure_blob_service" + +# Production stores every attachment on Azure (config.active_storage.service = :microsoft, +# service: AzureBlob). The azure-blob gem signs both uploads and download URLs with CGI.parse. +# Ruby 4.0's stdlib cgi ships only escape/unescape, so a full cgi gem must resolve — azure-blob +# declares it as a dependency, which pulls it back in (Rails 8 stopped doing so transitively). +# When that goes missing, every Active Storage read and write 500s in production while CI stays +# green, because the test environment uses Disk storage and never touches this code. # -# See https://github.com/rubyforgood/casa/issues/7093 +# This is the regression guard for the retired azure-storage-blob → azure-blob migration. +# See https://github.com/rubyforgood/casa/issues/7094 (and the earlier #7093). RSpec.describe "Azure Storage request signing" do - let(:account_name) { "casaaccount" } - let(:access_key) { Base64.strict_encode64("not-a-real-key") } + let(:service) do + ActiveStorage::Service::AzureBlobService.new( + storage_account_name: "casaaccount", + storage_access_key: Base64.strict_encode64("not-a-real-key"), + container: "casa" + ) + end it "has the full CGI library, not Ruby 4.0's escape-only stdlib" do expect(CGI).to respond_to(:parse) end - # Exercised by CaseCourtReportsController#save_report when it attaches the .docx. - it "signs an upload request" do - signer = Azure::Storage::Common::Core::Auth::SharedKey.new(account_name, access_key) - uri = URI("https://#{account_name}.blob.core.windows.net/casa/report.docx?comp=block&blockid=abc") - - signature = signer.sign(:put, uri, {"Content-Type" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}) - - expect(signature).to start_with("#{account_name}:") + # Exercised by active_storage/blobs/redirect#show when a user downloads a court report. + it "signs a private download URL locally, without hitting Azure" do + url = service.url( + "report.docx", + expires_in: 5.minutes, + filename: ActiveStorage::Filename.new("report.docx"), + disposition: :attachment, + content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ) + + expect(url).to start_with("https://casaaccount.blob.core.windows.net/casa/report.docx") + expect(url).to include("sig=") end - # Exercised by active_storage/blobs/redirect#show when a user downloads the report. - it "signs a download URL" do - generator = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(account_name, access_key) - uri = URI("https://#{account_name}.blob.core.windows.net/casa/report.docx") - - signed_uri = generator.signed_uri(uri, false, service: "b", permissions: "r", expiry: "2050-01-01T00:00:00Z") - - expect(signed_uri.query).to include("sig=") + # Exercised whenever an attachment is uploaded (e.g. CaseCourtReportsController#save_report). + it "signs a direct-upload URL locally, without hitting Azure" do + url = service.url_for_direct_upload( + "report.docx", + expires_in: 5.minutes, + content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + content_length: 1024, + checksum: "md5-checksum" + ) + + expect(url).to include("sig=") end end