Skip to main content

mz_environmentd/http/
console.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apach
9
10//! HTTP endpoints for the web console.
11
12use std::collections::BTreeMap;
13use std::sync::{Arc, LazyLock};
14
15use axum::Extension;
16use axum::Json;
17use axum::body::Body;
18use axum::http::{Request, StatusCode};
19use axum::response::{IntoResponse, Response};
20use http::HeaderValue;
21use http::header::HOST;
22use hyper::Uri;
23use hyper_tls::HttpsConnector;
24use hyper_util::client::legacy::Client;
25use hyper_util::client::legacy::connect::HttpConnector;
26use hyper_util::rt::TokioExecutor;
27use mz_adapter_types::dyncfgs::{CONSOLE_OIDC_CLIENT_ID, CONSOLE_OIDC_SCOPES, OIDC_ISSUER};
28
29use crate::http::Delayed;
30
31pub(crate) struct ConsoleProxyConfig {
32    /// Hyper http client, supports https.
33    client: Client<HttpsConnector<HttpConnector>, Body>,
34
35    /// URL of upstream console to proxy to (e.g. <https://console.materialize.com>).
36    url: String,
37
38    /// Route this is being served from (e.g. /internal-console).
39    route_prefix: String,
40}
41
42impl ConsoleProxyConfig {
43    pub(crate) fn new(proxy_url: Option<String>, route_prefix: String) -> Self {
44        let mut url = proxy_url.unwrap_or_else(|| "https://console.materialize.com".to_string());
45        if let Some(new) = url.strip_suffix('/') {
46            url = new.to_string();
47        }
48        Self {
49            client: Client::builder(TokioExecutor::new()).build(HttpsConnector::new()),
50            url,
51            route_prefix,
52        }
53    }
54}
55
56/// OIDC configuration values needed by the Console to initiate OIDC login.
57static CONSOLE_CONFIG_VAR_NAMES: LazyLock<[&'static str; 3]> = LazyLock::new(|| {
58    [
59        OIDC_ISSUER.name(),
60        CONSOLE_OIDC_CLIENT_ID.name(),
61        CONSOLE_OIDC_SCOPES.name(),
62    ]
63});
64
65/// Returns system variable values the web console needs from
66/// environmentd. This endpoint requires no authentication.
67pub async fn handle_console_config(
68    Extension(adapter_client_rx): Extension<Delayed<mz_adapter::Client>>,
69) -> Result<Response, (StatusCode, String)> {
70    let adapter_client = adapter_client_rx.await.map_err(|_| {
71        (
72            StatusCode::INTERNAL_SERVER_ERROR,
73            "Adapter client unavailable".to_string(),
74        )
75    })?;
76
77    let system_vars = adapter_client.get_system_vars().await;
78    let mut config: BTreeMap<&str, String> = BTreeMap::new();
79    for var_name in CONSOLE_CONFIG_VAR_NAMES.iter() {
80        let value = system_vars.get(var_name).map(|v| v.value()).map_err(|_| {
81            (
82                StatusCode::INTERNAL_SERVER_ERROR,
83                format!("failed to retrieve system variable {var_name}"),
84            )
85        })?;
86        config.insert(var_name, value);
87    }
88
89    Ok((StatusCode::OK, Json(config)).into_response())
90}
91
92/// The User Impersonation feature uses a Teleport proxy in front of the
93/// Internal HTTP Server, however Teleport has issues with CORS that prevent
94/// making requests to that Teleport-proxied app from our production console URLs.
95/// To avoid CORS and serve the Console from the same host as the Teleport app,
96/// this route proxies the upstream Console to handle requests for
97/// HTML, JS, and CSS static files.
98pub(crate) async fn handle_internal_console(
99    console_config: Extension<Arc<ConsoleProxyConfig>>,
100    mut req: Request<Body>,
101) -> Result<Response, StatusCode> {
102    let path = req.uri().path();
103    let mut path_query = req
104        .uri()
105        .path_and_query()
106        .map(|v| v.as_str())
107        .unwrap_or(path);
108    if let Some(stripped_path_query) = path_query.strip_prefix(&console_config.route_prefix) {
109        path_query = stripped_path_query;
110    }
111
112    let uri = Uri::try_from(format!("{}{}", &console_config.url, path_query)).unwrap();
113    let host = uri.host().unwrap().to_string();
114    // Preserve the request, but update the URI to point upstream.
115    *req.uri_mut() = uri;
116
117    // If vercel sees the request being served from a different host it tries to redirect to it's own.
118    req.headers_mut()
119        .insert(HOST, HeaderValue::from_str(&host).unwrap());
120
121    // Call this request against the upstream, return response directly.
122    Ok(console_config
123        .client
124        .request(req)
125        .await
126        .map_err(|err| {
127            tracing::warn!("Error retrieving console url: {}", err);
128            StatusCode::BAD_REQUEST
129        })?
130        .into_response())
131}