Skip to content

Commit

Permalink
Implement /internal-console redirect route on envd internal http server
Browse files Browse the repository at this point in the history
  • Loading branch information
rjobanp committed Sep 25, 2023
1 parent 630c7c9 commit b03541f
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 3 deletions.
5 changes: 5 additions & 0 deletions src/environmentd/src/bin/environmentd/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,10 @@ pub struct Args {
#[clap(long, env = "HTTP_HOST_NAME")]
http_host_name: Option<String>,

/// 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<String>,

#[clap(long, env = "DEPLOY_GENERATION")]
deploy_generation: Option<u64>,

Expand Down Expand Up @@ -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
})?;
Expand Down
29 changes: 28 additions & 1 deletion src/environmentd/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -225,6 +225,7 @@ pub struct InternalHttpConfig {
pub active_connection_count: Arc<Mutex<ConnectionCounter>>,
pub promote_leader: oneshot::Sender<()>,
pub ready_to_promote: oneshot::Receiver<()>,
pub internal_console_redirect_url: Option<String>,
}

pub struct InternalHttpServer {
Expand Down Expand Up @@ -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<String>,
) -> 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 {
Expand All @@ -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");
Expand Down Expand Up @@ -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));
Expand Down
3 changes: 3 additions & 0 deletions src/environmentd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ pub struct Config {
pub deploy_generation: Option<u64>,
/// Host name or URL for connecting to the HTTP server of this instance.
pub http_host_name: Option<String>,
/// URL of the Web Console to redirect to from the /internal-console endpoint on the InternalHTTPServer
pub internal_console_redirect_url: Option<String>,

// === Tracing options. ===
/// The metrics registry to use.
Expand Down Expand Up @@ -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)
});
Expand Down
44 changes: 42 additions & 2 deletions src/environmentd/tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
11 changes: 11 additions & 0 deletions src/environmentd/tests/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ pub struct Config {
deploy_generation: Option<u64>,
system_parameter_defaults: BTreeMap<String, String>,
concurrent_webhook_req_count: Option<usize>,
internal_console_redirect_url: Option<String>,
}

impl Default for Config {
Expand All @@ -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,
}
}
}
Expand Down Expand Up @@ -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<String>,
) -> Self {
self.internal_console_redirect_url = internal_console_redirect_url;
self
}
}

pub struct Listeners {
Expand Down Expand Up @@ -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
})?;
Expand Down
1 change: 1 addition & 0 deletions src/sqllogictest/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit b03541f

Please sign in to comment.