diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ec5b3e..abed472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ ### Fixed +- Serve the Prometheus and Alertmanager pages of the admin UI to a browser + arriving from another subdomain. The pages share a controller with the + proxies, so the proxy's Fetch-Metadata gate refused them as same-site, + which is how the browser reports a navigation from the app subdomain to a + site subdomain. The header links there from every app page, so the + Prometheus and Alertmanager pages answered 403. The gate now applies to the + `proxy` action only; the pages forward nothing and stay on the session path. - Report a service down on the status page, and open an automatic incident, only when more than half of the sites report its uptime probes down. Live status previously ran the site down fraction through the daily-uptime diff --git a/app/controllers/concerns/upright/proxy_authentication.rb b/app/controllers/concerns/upright/proxy_authentication.rb index c3c9cf9..abfa407 100644 --- a/app/controllers/concerns/upright/proxy_authentication.rb +++ b/app/controllers/concerns/upright/proxy_authentication.rb @@ -19,8 +19,12 @@ module Upright::ProxyAuthentication # embedded same-origin UI can still use them on the session path. TOKEN_DENIED_PREFIXES = %w[ /-/ ] + # The `show` action is the admin page that frames the upstream UI. It forwards + # nothing, so it stays on the ordinary session path: the gate below would + # otherwise refuse it whenever the browser arrives from another subdomain, + # which is what the header's Prometheus link does from the app subdomain. included do - prepend_before_action :block_cross_site_session_requests + prepend_before_action :block_cross_site_session_requests, except: :show end private diff --git a/test/integration/alertmanager_proxy_controller_test.rb b/test/integration/alertmanager_proxy_controller_test.rb index 61a9163..323f8b5 100644 --- a/test/integration/alertmanager_proxy_controller_test.rb +++ b/test/integration/alertmanager_proxy_controller_test.rb @@ -6,6 +6,39 @@ class AlertmanagerProxyControllerTest < ActionDispatch::IntegrationTest on_subdomain :app end + test "the framed page is served to a session arriving from another subdomain" do + # The header links here from the app subdomain, which the browser reports + # as same-site. The page only frames the UI, so the proxy gate does not + # apply to it. + sign_in + on_subdomain :ams + + get "/framed/alertmanager", headers: { "Sec-Fetch-Site" => "same-site", "Sec-Fetch-Mode" => "navigate" } + + assert_response :success + assert_select "iframe.service-frame" + end + + test "the framed page still requires a session" do + on_subdomain :ams + + get "/framed/alertmanager", headers: { "Sec-Fetch-Site" => "same-site", "Sec-Fetch-Mode" => "navigate" } + + assert_response :redirect + assert response.location.end_with?("/session/new") + end + + test "the proxy itself is still refused for a same-site request" do + stub = stub_request(:get, "http://localhost:9093/") + sign_in + on_subdomain :ams + + get "/alertmanager", headers: { "Sec-Fetch-Site" => "same-site", "Sec-Fetch-Mode" => "navigate" } + + assert_response :forbidden + assert_not_requested stub + end + test "proxies requests when authenticated" do stub_request(:get, "http://localhost:9093/") .to_return(status: 200, body: "Alertmanager UI") diff --git a/test/integration/prometheus_proxy_controller_test.rb b/test/integration/prometheus_proxy_controller_test.rb index 9667e5e..652993d 100644 --- a/test/integration/prometheus_proxy_controller_test.rb +++ b/test/integration/prometheus_proxy_controller_test.rb @@ -7,6 +7,39 @@ class PrometheusProxyControllerTest < ActionDispatch::IntegrationTest ENV["PROMETHEUS_OTLP_TOKEN"] = "test-token" end + test "the framed page is served to a session arriving from another subdomain" do + # The header links here from the app subdomain, which the browser reports + # as same-site. The page only frames the UI, so the proxy gate does not + # apply to it. + sign_in + on_subdomain :ams + + get "/framed/prometheus", headers: { "Sec-Fetch-Site" => "same-site", "Sec-Fetch-Mode" => "navigate" } + + assert_response :success + assert_select "iframe.service-frame" + end + + test "the framed page still requires a session" do + on_subdomain :ams + + get "/framed/prometheus", headers: { "Sec-Fetch-Site" => "same-site", "Sec-Fetch-Mode" => "navigate" } + + assert_response :redirect + assert response.location.end_with?("/session/new") + end + + test "the proxy itself is still refused for a same-site request" do + stub = stub_request(:get, "http://localhost:9090/graph") + sign_in + on_subdomain :ams + + get "/prometheus/graph", headers: { "Sec-Fetch-Site" => "same-site", "Sec-Fetch-Mode" => "navigate" } + + assert_response :forbidden + assert_not_requested stub + end + test "proxies requests when authenticated" do stub_request(:get, "http://localhost:9090/graph").to_return(status: 200, body: "Prometheus UI") sign_in