diff --git a/src/environmentd/src/bin/environmentd/main.rs b/src/environmentd/src/bin/environmentd/main.rs index b4d72bc4c6d1..2a5908c045b0 100644 --- a/src/environmentd/src/bin/environmentd/main.rs +++ b/src/environmentd/src/bin/environmentd/main.rs @@ -537,6 +537,10 @@ pub struct Args { #[clap(long, env = "HTTP_HOST_NAME")] http_host_name: Option, + /// URL of the Web Console to redirect to from the /internal-console endpoint on the InternalHTTPServer + #[clap(long, env = "INTERNAL_CONSOLE_REDIRECT_URL")] + internal_console_redirect_url: Option, + #[clap(long, env = "DEPLOY_GENERATION")] deploy_generation: Option, @@ -943,6 +947,7 @@ fn run(mut args: Args) -> Result<(), anyhow::Error> { bootstrap_role: args.bootstrap_role, deploy_generation: args.deploy_generation, http_host_name: args.http_host_name, + internal_console_redirect_url: args.internal_console_redirect_url, }) .await })?; diff --git a/src/environmentd/src/http.rs b/src/environmentd/src/http.rs index 5c6dcd5841c0..aac7de0f5834 100644 --- a/src/environmentd/src/http.rs +++ b/src/environmentd/src/http.rs @@ -27,7 +27,7 @@ use axum::error_handling::HandleErrorLayer; use axum::extract::ws::{Message, WebSocket}; use axum::extract::{DefaultBodyLimit, FromRequestParts, Query, State}; use axum::middleware::{self, Next}; -use axum::response::{IntoResponse, Response}; +use axum::response::{IntoResponse, Redirect, Response}; use axum::{routing, Extension, Json, Router}; use futures::future::{FutureExt, Shared, TryFutureExt}; use headers::authorization::{Authorization, Basic, Bearer}; @@ -225,6 +225,7 @@ pub struct InternalHttpConfig { pub active_connection_count: Arc>, pub promote_leader: oneshot::Sender<()>, pub ready_to_promote: oneshot::Receiver<()>, + pub internal_console_redirect_url: Option, } pub struct InternalHttpServer { @@ -350,6 +351,25 @@ pub async fn handle_leader_promote( ) } +/// This route allows User Impersonation by using Teleport to proxy requests to the Internal HTTP Server. +/// Teleport is configured to handle the user auth and then set an auth cookie stored in the user's browser +/// that is tied to the host being proxied (the InternalHTTPServer). This /internal-console route accepts +/// that request and then redirects the user's browser to a Web Console URL, and the Console code can +/// then make further requests to the InternalHTTPServer using the teleport auth cookie now in the user's browser +async fn handle_internal_console_redirect( + internal_console_redirect_url: &Option, +) -> Response { + if let Some(redirect_url) = internal_console_redirect_url { + Redirect::temporary(redirect_url).into_response() + } else { + ( + StatusCode::BAD_REQUEST, + "Redirect URL is not correctly configured".to_string(), + ) + .into_response() + } +} + impl InternalHttpServer { pub fn new( InternalHttpConfig { @@ -358,6 +378,7 @@ impl InternalHttpServer { active_connection_count, promote_leader, ready_to_promote, + internal_console_redirect_url, }: InternalHttpConfig, ) -> InternalHttpServer { let metrics = Metrics::register_into(&metrics_registry, "mz_internal_http"); @@ -408,6 +429,12 @@ impl InternalHttpServer { "/api/catalog/check", routing::get(catalog::handle_catalog_check), ) + .route( + "/api/internal-console", + routing::get(|| async move { + handle_internal_console_redirect(&internal_console_redirect_url).await + }), + ) .layer(Extension(AuthedUser(SYSTEM_USER.clone()))) .layer(Extension(adapter_client_rx.shared())) .layer(Extension(active_connection_count)); diff --git a/src/environmentd/src/lib.rs b/src/environmentd/src/lib.rs index e70c98796fbd..cb5e460ab97b 100644 --- a/src/environmentd/src/lib.rs +++ b/src/environmentd/src/lib.rs @@ -206,6 +206,8 @@ pub struct Config { pub deploy_generation: Option, /// Host name or URL for connecting to the HTTP server of this instance. pub http_host_name: Option, + /// URL of the Web Console to redirect to from the /internal-console endpoint on the InternalHTTPServer + pub internal_console_redirect_url: Option, // === Tracing options. === /// The metrics registry to use. @@ -336,6 +338,7 @@ impl Listeners { active_connection_count: Arc::clone(&active_connection_count), promote_leader: promote_leader_tx, ready_to_promote: ready_to_promote_rx, + internal_console_redirect_url: config.internal_console_redirect_url, }); mz_ore::server::serve(internal_http_conns, internal_http_server) }); diff --git a/src/environmentd/tests/server.rs b/src/environmentd/tests/server.rs index 84cbede35701..7d6fe22a9f8b 100644 --- a/src/environmentd/tests/server.rs +++ b/src/environmentd/tests/server.rs @@ -82,7 +82,7 @@ use std::fmt::Write; use std::io::Write as _; use std::net::Ipv4Addr; use std::process::{Command, Stdio}; -use std::sync::atomic::{AtomicBool, AtomicUsize}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use std::{iter, thread}; @@ -113,7 +113,7 @@ use postgres::SimpleQueryMessage; use postgres_array::Array; use rand::RngCore; use reqwest::blocking::Client; -use reqwest::Url; +use reqwest::{redirect, Url}; use serde::{Deserialize, Serialize}; use tempfile::TempDir; use tokio_postgres::error::SqlState; @@ -2016,6 +2016,46 @@ fn test_concurrent_id_reuse() { client.batch_execute("SELECT 1").unwrap(); } +#[mz_ore::test] +fn test_internal_console_redirect() { + let test_url = + Url::parse("https://test_org.test_region.internal.console.materialize.com").unwrap(); + + let config = util::Config::default() + .unsafe_mode() + .with_internal_console_redirect_url(Some(test_url.to_string())); + let server = util::start_server(config.clone()).unwrap(); + + let redirected = Arc::new(AtomicBool::new(false)); + let cloned = Arc::clone(&redirected); + // Reqwest will default follow redirects, so we want to avoid that and introspect + // the redirect to make sure it's pointing to our test_url + let custom_redirect_policy = redirect::Policy::custom(move |attempt| { + tracing::debug!("Got redirect request to URL: {:?}", attempt.url()); + if attempt.url() == &test_url { + cloned.store(true, Ordering::Relaxed); + } + attempt.stop() + }); + + let res = Client::builder() + .redirect(custom_redirect_policy) + .build() + .unwrap() + .get( + Url::parse(&format!( + "http://{}/api/internal-console", + server.inner.internal_http_local_addr() + )) + .unwrap(), + ) + .send() + .unwrap(); + + assert_eq!(res.status(), StatusCode::TEMPORARY_REDIRECT); + assert_eq!(redirected.load(Ordering::Relaxed), true); +} + #[mz_ore::test] #[cfg_attr(miri, ignore)] // too slow fn test_leader_promotion() { diff --git a/src/environmentd/tests/util.rs b/src/environmentd/tests/util.rs index 1ceef27da0dd..f8bbf30e167a 100644 --- a/src/environmentd/tests/util.rs +++ b/src/environmentd/tests/util.rs @@ -146,6 +146,7 @@ pub struct Config { deploy_generation: Option, system_parameter_defaults: BTreeMap, concurrent_webhook_req_count: Option, + internal_console_redirect_url: Option, } impl Default for Config { @@ -168,6 +169,7 @@ impl Default for Config { deploy_generation: None, system_parameter_defaults: BTreeMap::new(), concurrent_webhook_req_count: None, + internal_console_redirect_url: None, } } } @@ -267,6 +269,14 @@ impl Config { self.concurrent_webhook_req_count = Some(limit); self } + + pub fn with_internal_console_redirect_url( + mut self, + internal_console_redirect_url: Option, + ) -> Self { + self.internal_console_redirect_url = internal_console_redirect_url; + self + } } pub struct Listeners { @@ -463,6 +473,7 @@ impl Listeners { bootstrap_role: config.bootstrap_role, deploy_generation: config.deploy_generation, http_host_name: Some(host_name), + internal_console_redirect_url: config.internal_console_redirect_url, }) .await })?; diff --git a/src/sqllogictest/src/runner.rs b/src/sqllogictest/src/runner.rs index cef9525cd6ca..2a7cac4bd055 100644 --- a/src/sqllogictest/src/runner.rs +++ b/src/sqllogictest/src/runner.rs @@ -1023,6 +1023,7 @@ impl<'a> RunnerInner<'a> { bootstrap_role: Some("materialize".into()), deploy_generation: None, http_host_name: Some(host_name), + internal_console_redirect_url: None, }; // We need to run the server on its own Tokio runtime, which in turn // requires its own thread, so that we can wait for any tasks spawned