From 531e9597de520c758da34f9785ce6aa618cf19fa Mon Sep 17 00:00:00 2001 From: Christian Sutter Date: Fri, 22 Nov 2024 11:26:19 +0000 Subject: [PATCH 1/3] Add rack-cors gem --- Gemfile | 1 + Gemfile.lock | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index 2b56b71bf..cb7b6bfbb 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ gem "govuk_ab_testing" gem "govuk_app_config" gem "govuk_publishing_components" gem "govuk_web_banners" +gem "rack-cors" gem "rest-client" gem "slimmer" gem "sprockets-rails" diff --git a/Gemfile.lock b/Gemfile.lock index 9718f7f80..8a6aca9d9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -498,6 +498,8 @@ GEM nio4r (~> 2.0) racc (1.8.1) rack (3.1.8) + rack-cors (2.0.2) + rack (>= 2.0.0) rack-proxy (0.7.7) rack rack-session (2.0.0) @@ -695,6 +697,7 @@ DEPENDENCIES launchy listen pry-byebug + rack-cors rails (= 7.2.2) rails-controller-testing rest-client From c5bf11c9ff4855f40988ffe7c9d5e5faac9a81cf Mon Sep 17 00:00:00 2001 From: Christian Sutter Date: Fri, 22 Nov 2024 11:33:40 +0000 Subject: [PATCH 2/3] Add CORS configuration for autocomplete API Allow the autocomplete API to be accessed from any GOV.UK domain, including non-production ones. This lets us use the API in local development "live" stacks as well as the GOV.UK Publishing Components guide. --- config/initializers/cors.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 config/initializers/cors.rb diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 000000000..1ccfb542a --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.middleware.insert_before 0, Rack::Cors do + # Allow the autocomplete API to be accessed from any GOV.UK domain, including non-production ones. + # This lets us use the API in local development "live" stacks as well as the GOV.UK Publishing + # Components guide. + allow do + origins GovukContentSecurityPolicy::GOVUK_DOMAINS + + resource "/api/autocomplete.json", + headers: :any, + methods: %i[get] + end +end From cf3002f57dbac18a85a58276cfcd5c8fd23a9ee9 Mon Sep 17 00:00:00 2001 From: Kevin Dew Date: Wed, 11 Dec 2024 22:42:20 +0000 Subject: [PATCH 3/3] Refine CORS configuration and add specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes a couple of bugs in the previous implementation of this. Using GovukContentSecurityPolicy::GOVUK_DOMAINS didn't work because it makes use of wildcard origins, which aren't supported by rack-cors. I chose to put together a simple regex as an alternative. It also had an incorrect path for the resource - I had a wildcard asterisk so it can handle with and without format as the Rails path is without a format, yet in our apps we've configured a .json format [1]. Since the original was written it was also discovered that this configuration would be needed for more than just development environments and would actually be needed in production. The situation where this is needed is CSV previews [2] which are GOV.UK pages with the layout_super_navigation_header component hosted on the assets.publishing.service.gov.uk. In order to demonstrate CORS taking effect requests need to be provided with an origin header e.g: ``` ➜ ~ curl -Is -H "Origin: https://www.gov.uk" \ http://127.0.0.1:3062/api/search/autocomplete.json\?q\=test | grep access-control access-control-allow-origin: https://www.gov.uk access-control-allow-methods: GET access-control-expose-headers: access-control-max-age: 7200 ``` An absence of any access-control-* headers indicates a CORS fail and in a browser a request will be blocked e.g: ``` ➜ ~ curl -Is -H "Origin: https://example.com" \ http://127.0.0.1:3062/api/search/autocomplete.json\?q\=test | grep access-control ``` I've wrote request specs that demonstrate these behaviours. [1]: https://github.com/alphagov/govuk_publishing_components/blob/171e814b327bcfa0f2437fff0514ff086e31c96b/app/views/govuk_publishing_components/components/_layout_super_navigation_header.html.erb#L334 [2]: https://assets.publishing.service.gov.uk/media/663ca4da8603389a07a6d2f8/Malpractice_in_VTQ_-_Example_CSV_File.csv/preview --- config/initializers/cors.rb | 11 +++++----- spec/requests/api/autocomplete_spec.rb | 30 +++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index 1ccfb542a..eaad6c841 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -1,13 +1,14 @@ # Be sure to restart your server when you modify this file. Rails.application.config.middleware.insert_before 0, Rack::Cors do - # Allow the autocomplete API to be accessed from any GOV.UK domain, including non-production ones. - # This lets us use the API in local development "live" stacks as well as the GOV.UK Publishing - # Components guide. + # Allow the autocomplete API to be accessed from any GOV.UK domain, including + # non-production ones. This enables autocomplete on CSV preview GOV.UK pages, + # which are hosted on assets.publishing.service.gov.uk. + # This also allows for local development usage. allow do - origins GovukContentSecurityPolicy::GOVUK_DOMAINS + origins %r{(www|dev|publishing\.service)\.gov\.uk\z} - resource "/api/autocomplete.json", + resource "/api/search/autocomplete*", headers: :any, methods: %i[get] end diff --git a/spec/requests/api/autocomplete_spec.rb b/spec/requests/api/autocomplete_spec.rb index ada6cd347..0faa06210 100644 --- a/spec/requests/api/autocomplete_spec.rb +++ b/spec/requests/api/autocomplete_spec.rb @@ -5,13 +5,14 @@ let(:suggestions) { %w[blue grey red] } let(:autocomplete_response) { instance_double(GdsApi::Response, to_hash: { suggestions: }) } + let(:params) { { q: "loving him was" } } before do allow(Services).to receive(:search_api_v2).and_return(search_api_v2) end it "returns suggestions from Search API v2" do - get "/api/search/autocomplete?q=loving+him+was" + get "/api/search/autocomplete", params: params expect(search_api_v2).to have_received(:autocomplete).with("loving him was") expect(response).to be_successful @@ -23,4 +24,31 @@ expect(response).to have_http_status(:bad_request) end + + describe "CORS headers" do + %w[https://www.gov.uk http://example.dev.gov.uk https://example.publishing.service.gov.uk].each do |allowed_host| + it "returns CORS headers for #{allowed_host}" do + get "/api/search/autocomplete", params:, headers: { Origin: allowed_host } + + expect(response.headers.to_h).to include({ + "access-control-allow-origin" => allowed_host, + "access-control-allow-methods" => "GET", + }) + end + end + + it "returns CORS headers when there is a format extension on the path" do + get "/api/search/autocomplete.json", params:, headers: { Origin: "https://www.gov.uk" } + + expect(response.headers) + .to include("access-control-allow-origin", "access-control-allow-methods") + end + + it "doesn't return CORS headers for an unsupported hosts" do + get "/api/search/autocomplete", params:, headers: { Origin: "https://www.gov.uk.non-govuk.com" } + + expect(response.headers) + .not_to include("access-control-allow-origin", "access-control-allow-methods") + end + end end