1#![allow(clippy::unused_async)]
55
56use std::borrow::Cow;
57use std::collections::BTreeMap;
58use std::fmt::Debug;
59use std::net::{IpAddr, SocketAddr};
60use std::pin::Pin;
61use std::sync::Arc;
62use std::time::{Duration, SystemTime};
63
64use anyhow::Context;
65use axum::error_handling::HandleErrorLayer;
66use axum::extract::ws::{Message, WebSocket};
67use axum::extract::{ConnectInfo, DefaultBodyLimit, FromRequestParts, Query, Request, State};
68use axum::middleware::{self, Next};
69use axum::response::{IntoResponse, Redirect, Response};
70use axum::{Extension, Json, Router, routing};
71use futures::future::{Shared, TryFutureExt};
72use headers::authorization::{Authorization, Basic, Bearer};
73use headers::{HeaderMapExt, HeaderName};
74use http::header::{AUTHORIZATION, CONTENT_TYPE};
75use http::uri::Scheme;
76use http::{HeaderMap, HeaderValue, Method, StatusCode, Uri};
77use hyper_openssl::SslStream;
78use hyper_openssl::client::legacy::MaybeHttpsStream;
79use hyper_util::rt::TokioIo;
80use mz_adapter::session::{Session as AdapterSession, SessionConfig as AdapterSessionConfig};
81use mz_adapter::{AdapterError, AdapterNotice, Client, SessionClient, WebhookAppenderCache};
82use mz_adapter_types::dyncfgs::OIDC_GROUP_CLAIM;
83use mz_auth::Authenticated;
84use mz_auth::password::Password;
85use mz_authenticator::Authenticator;
86use mz_controller::ReplicaHttpLocator;
87use mz_dyncfg::ConfigSet;
88use mz_frontegg_auth::Error as FronteggError;
89use mz_http_util::DynamicFilterTarget;
90use mz_ore::cast::u64_to_usize;
91use mz_ore::metrics::MetricsRegistry;
92use mz_ore::now::{NowFn, SYSTEM_TIME, epoch_to_uuid_v7};
93use mz_ore::str::StrExt;
94use mz_pgwire_common::{ConnectionCounter, ConnectionHandle};
95use mz_repr::user::ExternalUserMetadata;
96use mz_server_core::listeners::{self, AllowedRoles, HttpRoutesEnabled, RouteGroup};
97use mz_server_core::{Connection, ConnectionHandler, ReloadingSslContext, Server};
98use mz_sql::session::metadata::SessionMetadata;
99use mz_sql::session::user::{
100 HTTP_DEFAULT_USER, INTERNAL_USER_NAMES, SUPPORT_USER_NAME, SYSTEM_USER_NAME,
101};
102use mz_sql::session::vars::{Value, Var, VarInput, WELCOME_MESSAGE};
103use openssl::ssl::Ssl;
104use prometheus::{
105 COMPUTE_METRIC_QUERIES, FRONTIER_METRIC_QUERIES, STORAGE_METRIC_QUERIES, USAGE_METRIC_QUERIES,
106};
107use serde::{Deserialize, Serialize};
108use serde_json::json;
109use thiserror::Error;
110use tokio::io::AsyncWriteExt;
111use tokio::sync::oneshot::Receiver;
112use tokio::sync::{oneshot, watch};
113use tokio_metrics::TaskMetrics;
114use tower::limit::GlobalConcurrencyLimitLayer;
115use tower::{Service, ServiceBuilder};
116use tower_http::cors::{AllowOrigin, Any, CorsLayer};
117use tower_sessions::{
118 MemoryStore as TowerSessionMemoryStore, Session as TowerSession,
119 SessionManagerLayer as TowerSessionManagerLayer,
120};
121use tracing::warn;
122
123use crate::BUILD_INFO;
124use crate::deployment::state::DeploymentStateHandle;
125use crate::http::sql::{ExistingUser, SqlError};
126
127mod catalog;
128mod cluster;
129mod console;
130mod mcp;
131pub mod mcp_metrics;
132mod memory;
133mod metrics;
134mod metrics_public;
135mod metrics_viz;
136pub(crate) mod oauth_metadata;
137mod probe;
138mod prometheus;
139mod root;
140mod sql;
141mod webhook;
142
143pub use metrics::Metrics;
144pub use sql::{SqlResponse, WebSocketAuth, WebSocketResponse};
145
146pub const MAX_REQUEST_SIZE: usize = u64_to_usize(5 * bytesize::MIB);
148
149const SESSION_DURATION: Duration = Duration::from_secs(8 * 3600); const PROFILING_API_ENDPOINTS: &[&str] = &["/memory", "/hierarchical-memory", "/prof/"];
152
153#[derive(Debug)]
154pub struct HttpConfig {
155 pub source: &'static str,
156 pub tls: Option<ReloadingSslContext>,
157 pub authenticator_kind: listeners::AuthenticatorKind,
158 pub frontegg: Option<mz_frontegg_auth::Authenticator>,
159 pub oidc_rx: Delayed<mz_authenticator::GenericOidcAuthenticator>,
160 pub adapter_client_rx: Shared<Receiver<Client>>,
161 pub allowed_origin: AllowOrigin,
162 pub allowed_origin_list: Vec<HeaderValue>,
165 pub active_connection_counter: ConnectionCounter,
166 pub helm_chart_version: Option<String>,
167 pub http_host_name: Option<String>,
180 pub frontegg_oauth_issuer_url: Option<String>,
181 pub concurrent_webhook_req: Arc<tokio::sync::Semaphore>,
182 pub dyncfgs: Arc<ConfigSet>,
183 pub metrics: Metrics,
184 pub metrics_registry: MetricsRegistry,
185 pub mcp_metrics: mcp_metrics::McpMetrics,
186 pub oauth_metadata_metrics: oauth_metadata::OauthMetadataMetrics,
187 pub internal_route_config: Arc<InternalRouteConfig>,
188 pub routes_enabled: HttpRoutesEnabled,
189 pub replica_http_locator: Arc<ReplicaHttpLocator>,
191}
192
193#[derive(Debug, Clone)]
194pub struct InternalRouteConfig {
195 pub deployment_state_handle: DeploymentStateHandle,
196 pub internal_console_redirect_url: Option<String>,
197}
198
199#[derive(Clone)]
200pub struct WsState {
201 frontegg: Option<mz_frontegg_auth::Authenticator>,
202 oidc_rx: Delayed<mz_authenticator::GenericOidcAuthenticator>,
203 authenticator_kind: listeners::AuthenticatorKind,
204 adapter_client_rx: Delayed<mz_adapter::Client>,
205 active_connection_counter: ConnectionCounter,
206 helm_chart_version: Option<String>,
207 allowed_roles: AllowedRoles,
208}
209
210#[derive(Clone)]
211pub struct WebhookState {
212 adapter_client_rx: Delayed<mz_adapter::Client>,
213 webhook_cache: WebhookAppenderCache,
214 dyncfgs: Arc<ConfigSet>,
215}
216
217#[derive(Clone, Debug)]
218struct HelmChartVersion(Option<String>);
219
220#[derive(Debug)]
221pub struct HttpServer {
222 tls: Option<ReloadingSslContext>,
223 router: Router,
224}
225
226impl HttpServer {
227 pub fn new(
228 HttpConfig {
229 source,
230 tls,
231 authenticator_kind,
232 frontegg,
233 oidc_rx,
234 adapter_client_rx,
235 allowed_origin,
236 allowed_origin_list,
237 active_connection_counter,
238 helm_chart_version,
239 http_host_name,
240 frontegg_oauth_issuer_url,
241 concurrent_webhook_req,
242 dyncfgs,
243 metrics,
244 metrics_registry,
245 mcp_metrics,
246 oauth_metadata_metrics,
247 internal_route_config,
248 routes_enabled,
249 replica_http_locator,
250 }: HttpConfig,
251 ) -> HttpServer {
252 let tls_enabled = tls.is_some();
253 let webhook_cache = WebhookAppenderCache::new();
254
255 let oauth_discovery = Arc::new(oauth_metadata::McpOAuthDiscovery::for_authenticator(
259 authenticator_kind,
260 frontegg_oauth_issuer_url.as_deref(),
261 ));
262
263 let session_store = TowerSessionMemoryStore::default();
265 let session_layer = TowerSessionManagerLayer::new(session_store)
266 .with_secure(tls_enabled) .with_same_site(tower_sessions::cookie::SameSite::Strict) .with_http_only(true) .with_name("mz_session") .with_path("/"); let frontegg_middleware = frontegg.clone();
273 let oidc_middleware_rx = oidc_rx.clone();
274 let adapter_client_middleware_rx = adapter_client_rx.clone();
275 let auth_middleware = middleware::from_fn(move |req, next| {
276 let frontegg = frontegg_middleware.clone();
277 let oidc_rx = oidc_middleware_rx.clone();
278 let adapter_client_rx = adapter_client_middleware_rx.clone();
279 async move {
280 http_auth(
281 req,
282 next,
283 tls_enabled,
284 authenticator_kind,
285 frontegg,
286 oidc_rx,
287 adapter_client_rx,
288 )
289 .await
290 }
291 });
292 let mut router = Router::new();
293 let mut base_router = Router::new();
294 let cluster_proxy_config = Arc::new(cluster::ClusterProxyConfig::new(Arc::clone(
295 &replica_http_locator,
296 )));
297 if let RouteGroup::Enabled(base_roles) = routes_enabled.base {
298 let base_group = Router::new()
299 .route(
300 "/",
301 routing::get(move || async move { root::handle_home(routes_enabled).await }),
302 )
303 .route("/api/sql", routing::post(sql::handle_sql))
304 .route("/memory", routing::get(memory::handle_memory))
305 .route(
306 "/hierarchical-memory",
307 routing::get(memory::handle_hierarchical_memory),
308 )
309 .route(
310 "/metrics-viz",
311 routing::get(metrics_viz::handle_metrics_viz),
312 )
313 .route("/static/{*path}", routing::get(root::handle_static))
314 .route(
315 "/metrics/public",
316 routing::get(metrics_public::handle_public_metrics),
317 )
318 .layer(Extension(metrics_registry.clone()))
319 .layer(Extension(Arc::clone(&cluster_proxy_config)))
320 .authorize(base_roles);
321 base_router = base_router.merge(base_group);
322
323 let mut ws_router = Router::new()
324 .route("/api/experimental/sql", routing::get(sql::handle_sql_ws))
325 .with_state(WsState {
326 frontegg,
327 oidc_rx: oidc_rx.clone(),
328 authenticator_kind,
329 adapter_client_rx: adapter_client_rx.clone(),
330 active_connection_counter: active_connection_counter.clone(),
331 helm_chart_version: helm_chart_version.clone(),
332 allowed_roles: base_roles,
335 });
336 if let listeners::AuthenticatorKind::None = authenticator_kind {
337 ws_router = ws_router.layer(middleware::from_fn(x_materialize_user_header_auth));
338 }
339 router = router.merge(ws_router);
340 }
341 if let RouteGroup::Enabled(profiling_roles) = routes_enabled.profiling {
342 let profiling_group = Router::new()
343 .nest("/prof/", mz_prof_http::router(&BUILD_INFO))
344 .authorize(profiling_roles);
345 base_router = base_router.merge(profiling_group);
346 }
347
348 if routes_enabled.webhook.is_enabled() {
349 let webhook_router = Router::new()
350 .route(
351 "/api/webhook/{:database}/{:schema}/{:id}",
352 routing::post(webhook::handle_webhook),
353 )
354 .with_state(WebhookState {
355 adapter_client_rx: adapter_client_rx.clone(),
356 webhook_cache,
357 dyncfgs,
358 })
359 .layer(
360 tower_http::decompression::RequestDecompressionLayer::new()
361 .gzip(true)
362 .deflate(true)
363 .br(true)
364 .zstd(true),
365 )
366 .layer(DefaultBodyLimit::disable())
371 .layer(
372 CorsLayer::new()
373 .allow_methods(Method::POST)
374 .allow_origin(AllowOrigin::mirror_request())
375 .allow_headers(Any),
376 )
377 .layer(
378 ServiceBuilder::new()
379 .layer(HandleErrorLayer::new(handle_load_error))
380 .load_shed()
381 .layer(GlobalConcurrencyLimitLayer::with_semaphore(
382 concurrent_webhook_req,
383 )),
384 );
385 router = router.merge(webhook_router);
386 }
387
388 if let RouteGroup::Enabled(internal_roles) = routes_enabled.internal {
389 let console_config = Arc::new(console::ConsoleProxyConfig::new(
390 internal_route_config.internal_console_redirect_url.clone(),
391 "/internal-console".to_string(),
392 ));
393 let internal_group = Router::new()
394 .route(
395 "/api/opentelemetry/config",
396 routing::put({
397 move |_: axum::Json<DynamicFilterTarget>| async {
398 (
399 StatusCode::BAD_REQUEST,
400 "This endpoint has been replaced. \
401 Use the `opentelemetry_filter` system variable."
402 .to_string(),
403 )
404 }
405 }),
406 )
407 .route(
408 "/api/stderr/config",
409 routing::put({
410 move |_: axum::Json<DynamicFilterTarget>| async {
411 (
412 StatusCode::BAD_REQUEST,
413 "This endpoint has been replaced. \
414 Use the `log_filter` system variable."
415 .to_string(),
416 )
417 }
418 }),
419 )
420 .route("/api/tracing", routing::get(mz_http_util::handle_tracing))
421 .route(
422 "/api/catalog/dump",
423 routing::get(catalog::handle_catalog_dump),
424 )
425 .route(
426 "/api/catalog/check",
427 routing::get(catalog::handle_catalog_check),
428 )
429 .route(
430 "/api/catalog/inject-audit-events",
431 routing::post(catalog::handle_inject_audit_events),
432 )
433 .route(
434 "/api/coordinator/check",
435 routing::get(catalog::handle_coordinator_check),
436 )
437 .route(
438 "/api/coordinator/dump",
439 routing::get(catalog::handle_coordinator_dump),
440 )
441 .route(
442 "/internal-console",
443 routing::get(|| async { Redirect::temporary("/internal-console/") }),
444 )
445 .route(
446 "/internal-console/{*path}",
447 routing::get(console::handle_internal_console)
448 .post(console::handle_internal_console),
449 )
450 .route(
451 "/internal-console/",
452 routing::get(console::handle_internal_console)
453 .post(console::handle_internal_console),
454 )
455 .route("/clusters", routing::get(cluster::handle_clusters))
457 .route(
458 "/api/cluster/{:cluster_id}/replica/{:replica_id}/process/{:process}/",
459 routing::any(cluster::handle_cluster_proxy_root),
460 )
461 .route(
462 "/api/cluster/{:cluster_id}/replica/{:replica_id}/process/{:process}/{*path}",
463 routing::any(cluster::handle_cluster_proxy),
464 )
465 .layer(Extension(console_config))
466 .layer(Extension(Arc::clone(&cluster_proxy_config)))
467 .authorize(internal_roles);
468 base_router = base_router.merge(internal_group);
469
470 let leader_router = Router::new()
471 .route("/api/leader/status", routing::get(handle_leader_status))
472 .route("/api/leader/promote", routing::post(handle_leader_promote))
473 .route(
474 "/api/leader/skip-catchup",
475 routing::post(handle_leader_skip_catchup),
476 )
477 .authorize(internal_roles)
478 .layer(auth_middleware.clone())
479 .with_state(internal_route_config.deployment_state_handle.clone());
480 router = router.merge(leader_router);
481 }
482
483 if let RouteGroup::Enabled(metrics_roles) = routes_enabled.metrics {
484 let metrics_registry_for_handler = metrics_registry.clone();
488 let metrics_router = Router::new()
489 .route(
490 "/metrics",
491 routing::get(move |headers: HeaderMap| async move {
492 mz_http_util::handle_prometheus(&metrics_registry_for_handler, headers)
493 .await
494 }),
495 )
496 .route(
497 "/metrics/mz_usage",
498 routing::get(|client: AuthedClient, headers: HeaderMap| async move {
499 let registry = sql::handle_promsql(client, USAGE_METRIC_QUERIES).await;
500 mz_http_util::handle_prometheus(®istry, headers).await
501 }),
502 )
503 .route(
504 "/metrics/mz_frontier",
505 routing::get(|client: AuthedClient, headers: HeaderMap| async move {
506 let registry = sql::handle_promsql(client, FRONTIER_METRIC_QUERIES).await;
507 mz_http_util::handle_prometheus(®istry, headers).await
508 }),
509 )
510 .route(
511 "/metrics/mz_compute",
512 routing::get(|client: AuthedClient, headers: HeaderMap| async move {
513 let registry = sql::handle_promsql(client, COMPUTE_METRIC_QUERIES).await;
514 mz_http_util::handle_prometheus(®istry, headers).await
515 }),
516 )
517 .route(
518 "/metrics/mz_storage",
519 routing::get(|client: AuthedClient, headers: HeaderMap| async move {
520 let registry = sql::handle_promsql(client, STORAGE_METRIC_QUERIES).await;
521 mz_http_util::handle_prometheus(®istry, headers).await
522 }),
523 )
524 .route(
525 "/api/livez",
526 routing::get(mz_http_util::handle_liveness_check),
527 )
528 .route("/api/readyz", routing::get(probe::handle_ready))
529 .authorize(metrics_roles)
530 .layer(auth_middleware.clone())
531 .layer(Extension(adapter_client_rx.clone()))
532 .layer(Extension(active_connection_counter.clone()))
533 .layer(Extension(HelmChartVersion(helm_chart_version.clone())));
534 router = router.merge(metrics_router);
535 }
536
537 if routes_enabled.console_config.is_enabled() {
538 let console_config_router = Router::new()
539 .route(
540 "/api/console/config",
541 routing::get(console::handle_console_config),
542 )
543 .layer(Extension(adapter_client_rx.clone()))
544 .layer(Extension(active_connection_counter.clone()));
545 router = router.merge(console_config_router);
546 }
547
548 if routes_enabled.mcp_agent.is_enabled() || routes_enabled.mcp_developer.is_enabled() {
551 use tracing::info;
552
553 let oauth_metadata_router = Router::new()
565 .route(
566 oauth_metadata::PROTECTED_RESOURCE_METADATA_PATH,
567 routing::get(oauth_metadata::handle_protected_resource_metadata),
568 )
569 .route(
570 oauth_metadata::PROTECTED_RESOURCE_METADATA_PATH_AGENT,
571 routing::get(oauth_metadata::handle_protected_resource_metadata),
572 )
573 .route(
574 oauth_metadata::PROTECTED_RESOURCE_METADATA_PATH_DEVELOPER,
575 routing::get(oauth_metadata::handle_protected_resource_metadata),
576 )
577 .layer(Extension(adapter_client_rx.clone()))
578 .layer(Extension(oauth_metadata::McpOAuthConfig {
579 http_host_name: http_host_name.clone(),
580 discovery: Arc::clone(&oauth_discovery),
581 }))
582 .layer(Extension(oauth_metadata_metrics.clone()));
583 router = router.merge(oauth_metadata_router);
584
585 let mut mcp_router = Router::new();
586
587 if let RouteGroup::Enabled(mcp_agent_roles) = routes_enabled.mcp_agent {
588 info!("Enabling MCP agent endpoint: /api/mcp/agent");
589 let agent_router = Router::new()
590 .route(
591 "/api/mcp/agent",
592 routing::post(mcp::handle_mcp_agent)
593 .get(mcp::handle_mcp_method_not_allowed),
594 )
595 .authorize(mcp_agent_roles);
596 mcp_router = mcp_router.merge(agent_router);
597 }
598
599 if let RouteGroup::Enabled(mcp_developer_roles) = routes_enabled.mcp_developer {
600 info!("Enabling MCP developer endpoint: /api/mcp/developer");
601 let developer_router = Router::new()
602 .route(
603 "/api/mcp/developer",
604 routing::post(mcp::handle_mcp_developer)
605 .get(mcp::handle_mcp_method_not_allowed),
606 )
607 .authorize(mcp_developer_roles);
608 mcp_router = mcp_router.merge(developer_router);
609 }
610
611 let mcp_allowed_origins = Arc::new(allowed_origin_list.clone());
618 mcp_router = mcp_router
619 .layer(auth_middleware.clone())
620 .layer(Extension(oauth_metadata::McpOAuthConfig {
621 http_host_name: http_host_name.clone(),
622 discovery: Arc::clone(&oauth_discovery),
623 }))
624 .layer(Extension(adapter_client_rx.clone()))
625 .layer(Extension(active_connection_counter.clone()))
626 .layer(Extension(HelmChartVersion(helm_chart_version.clone())))
627 .layer(Extension(mcp_allowed_origins))
628 .layer(Extension(mcp_metrics))
629 .layer(
630 CorsLayer::new()
631 .allow_methods(Method::POST)
632 .allow_origin(allowed_origin.clone())
633 .allow_headers([AUTHORIZATION, CONTENT_TYPE]),
634 );
635 if let listeners::AuthenticatorKind::None = authenticator_kind {
639 mcp_router = mcp_router.layer(middleware::from_fn(x_materialize_user_header_auth));
640 }
641 router = router.merge(mcp_router);
642 }
643
644 base_router = base_router
645 .layer(auth_middleware.clone())
646 .layer(Extension(adapter_client_rx.clone()))
647 .layer(Extension(active_connection_counter.clone()))
648 .layer(Extension(HelmChartVersion(helm_chart_version)))
649 .layer(
650 CorsLayer::new()
651 .allow_credentials(false)
652 .allow_headers([
653 AUTHORIZATION,
654 CONTENT_TYPE,
655 HeaderName::from_static("x-materialize-version"),
656 ])
657 .allow_methods(Any)
658 .allow_origin(allowed_origin)
659 .expose_headers(Any)
660 .max_age(Duration::from_secs(60) * 60),
661 );
662
663 match authenticator_kind {
664 listeners::AuthenticatorKind::Password | listeners::AuthenticatorKind::Oidc => {
665 base_router = base_router.layer(session_layer.clone());
666
667 let login_router = Router::new()
668 .route("/api/login", routing::post(handle_login))
669 .route("/api/logout", routing::post(handle_logout))
670 .layer(Extension(adapter_client_rx));
671 router = router.merge(login_router).layer(session_layer);
672 }
673 listeners::AuthenticatorKind::None => {
674 base_router =
675 base_router.layer(middleware::from_fn(x_materialize_user_header_auth));
676 }
677 _ => {}
678 }
679
680 router = router
681 .merge(base_router)
682 .apply_default_layers(source, metrics);
683
684 HttpServer { tls, router }
685 }
686}
687
688impl Server for HttpServer {
689 const NAME: &'static str = "http";
690
691 fn handle_connection(
692 &self,
693 conn: Connection,
694 _tokio_metrics_intervals: impl Iterator<Item = TaskMetrics> + Send + 'static,
695 ) -> ConnectionHandler {
696 let router = self.router.clone();
697 let tls_context = self.tls.clone();
698 let mut conn = TokioIo::new(conn);
699
700 Box::pin(async {
701 let direct_peer_addr = conn.inner().peer_addr().context("fetching peer addr")?;
702 let peer_addr = conn
703 .inner_mut()
704 .take_proxy_header_address()
705 .await
706 .map(|a| a.source)
707 .unwrap_or(direct_peer_addr);
708
709 let (conn, conn_protocol) = match tls_context {
710 Some(tls_context) => {
711 let mut ssl_stream = SslStream::new(Ssl::new(&tls_context.get())?, conn)?;
712 if let Err(e) = Pin::new(&mut ssl_stream).accept().await {
713 let _ = ssl_stream.get_mut().inner_mut().shutdown().await;
714 return Err(e.into());
715 }
716 (MaybeHttpsStream::Https(ssl_stream), ConnProtocol::Https)
717 }
718 _ => (MaybeHttpsStream::Http(conn), ConnProtocol::Http),
719 };
720 let mut make_tower_svc = router
721 .layer(Extension(conn_protocol))
722 .into_make_service_with_connect_info::<SocketAddr>();
723 let tower_svc = make_tower_svc.call(peer_addr).await.unwrap();
724 let hyper_svc = hyper::service::service_fn(|req| tower_svc.clone().call(req));
725 let http = hyper::server::conn::http1::Builder::new();
726 http.serve_connection(conn, hyper_svc)
727 .with_upgrades()
728 .err_into()
729 .await
730 })
731 }
732}
733
734pub async fn handle_leader_status(
735 State(deployment_state_handle): State<DeploymentStateHandle>,
736) -> impl IntoResponse {
737 let status = deployment_state_handle.status();
738 (StatusCode::OK, Json(json!({ "status": status })))
739}
740
741pub async fn handle_leader_promote(
742 State(deployment_state_handle): State<DeploymentStateHandle>,
743) -> impl IntoResponse {
744 match deployment_state_handle.try_promote() {
745 Ok(()) => {
746 let status = StatusCode::OK;
749 let body = Json(json!({
750 "result": "Success",
751 }));
752 (status, body)
753 }
754 Err(()) => {
755 let status = StatusCode::BAD_REQUEST;
758 let body = Json(json!({
759 "result": {"Failure": {"message": "cannot promote leader while initializing"}},
760 }));
761 (status, body)
762 }
763 }
764}
765
766pub async fn handle_leader_skip_catchup(
767 State(deployment_state_handle): State<DeploymentStateHandle>,
768) -> impl IntoResponse {
769 match deployment_state_handle.try_skip_catchup() {
770 Ok(()) => StatusCode::NO_CONTENT.into_response(),
771 Err(()) => {
772 let status = StatusCode::BAD_REQUEST;
773 let body = Json(json!({
774 "message": "cannot skip catchup in this phase of initialization; try again later",
775 }));
776 (status, body).into_response()
777 }
778 }
779}
780
781async fn x_materialize_user_header_auth(mut req: Request, next: Next) -> impl IntoResponse {
782 if let Some(username) = req.headers().get("x-materialize-user").map(|h| h.to_str()) {
784 let username = match username {
785 Ok(name @ (SUPPORT_USER_NAME | SYSTEM_USER_NAME)) => name.to_string(),
786 _ => {
787 return Err(AuthError::MismatchedUser(format!(
788 "user specified in x-materialize-user must be {SUPPORT_USER_NAME} or {SYSTEM_USER_NAME}"
789 )));
790 }
791 };
792 req.extensions_mut().insert(AuthedUser {
796 name: username,
797 external_metadata_rx: None,
798 authenticated: Authenticated,
799 authenticator_kind: mz_auth::AuthenticatorKind::None,
800 groups: None,
801 });
802 }
803 Ok(next.run(req).await)
804}
805
806pub(crate) type Delayed<T> = Shared<oneshot::Receiver<T>>;
807
808async fn group_claim_for(adapter_client_rx: &Delayed<Client>) -> String {
812 let client = adapter_client_rx
813 .clone()
814 .await
815 .expect("adapter client receiver dropped");
816 OIDC_GROUP_CLAIM.get(client.get_system_vars().await.dyncfgs())
817}
818
819#[derive(Clone)]
820enum ConnProtocol {
821 Http,
822 Https,
823}
824
825#[derive(Clone, Copy)]
828struct RouteAllowedRoles(AllowedRoles);
829
830trait AuthzRouterExt {
832 fn authorize(self, roles: AllowedRoles) -> Self;
833}
834
835impl<S> AuthzRouterExt for Router<S>
836where
837 S: Clone + Send + Sync + 'static,
838{
839 fn authorize(self, roles: AllowedRoles) -> Router<S> {
840 self.layer(middleware::from_fn(http_authz))
843 .layer(Extension(RouteAllowedRoles(roles)))
844 }
845}
846
847#[derive(Clone, Debug)]
848pub struct AuthedUser {
849 name: String,
850 external_metadata_rx: Option<watch::Receiver<ExternalUserMetadata>>,
851 authenticated: Authenticated,
852 authenticator_kind: mz_auth::AuthenticatorKind,
853 groups: Option<Vec<String>>,
855}
856
857pub struct AuthedClient {
858 pub client: SessionClient,
859 pub connection_guard: Option<ConnectionHandle>,
860}
861
862impl AuthedClient {
863 async fn new<F>(
864 adapter_client: &Client,
865 user: AuthedUser,
866 peer_addr: IpAddr,
867 active_connection_counter: ConnectionCounter,
868 helm_chart_version: Option<String>,
869 session_config: F,
870 options: BTreeMap<String, String>,
871 now: NowFn,
872 ) -> Result<Self, AdapterError>
873 where
874 F: FnOnce(&mut AdapterSession),
875 {
876 let conn_id = adapter_client.new_conn_id()?;
877 let mut session = adapter_client.new_session(
878 AdapterSessionConfig {
879 conn_id,
880 uuid: epoch_to_uuid_v7(&(now)()),
881 user: user.name,
882 client_ip: Some(peer_addr),
883 external_metadata_rx: user.external_metadata_rx,
884 helm_chart_version,
885 authenticator_kind: user.authenticator_kind,
886 groups: user.groups,
887 },
888 user.authenticated,
889 );
890 let connection_guard = active_connection_counter.allocate_connection(session.user())?;
891
892 session_config(&mut session);
893 let system_vars = adapter_client.get_system_vars().await;
894 for (key, val) in options {
895 const LOCAL: bool = false;
896 if let Err(err) =
897 session
898 .vars_mut()
899 .set(&system_vars, &key, VarInput::Flat(&val), LOCAL)
900 {
901 session.add_notice(AdapterNotice::BadStartupSetting {
902 name: key.to_string(),
903 reason: err.to_string(),
904 })
905 }
906 }
907 let adapter_client = adapter_client.startup(session).await?;
908 Ok(AuthedClient {
909 client: adapter_client,
910 connection_guard,
911 })
912 }
913}
914
915impl<S> FromRequestParts<S> for AuthedClient
916where
917 S: Send + Sync,
918{
919 type Rejection = Response;
920
921 async fn from_request_parts(
922 req: &mut http::request::Parts,
923 state: &S,
924 ) -> Result<Self, Self::Rejection> {
925 #[derive(Debug, Default, Deserialize)]
926 struct Params {
927 #[serde(default)]
928 options: String,
929 }
930 let params: Query<Params> = Query::from_request_parts(req, state)
931 .await
932 .unwrap_or_default();
933
934 let peer_addr = req
935 .extensions
936 .get::<ConnectInfo<SocketAddr>>()
937 .expect("ConnectInfo extension guaranteed to exist")
938 .0
939 .ip();
940
941 let user = req.extensions.get::<AuthedUser>().unwrap();
942 let adapter_client = req
943 .extensions
944 .get::<Delayed<mz_adapter::Client>>()
945 .unwrap()
946 .clone();
947 let adapter_client = adapter_client.await.map_err(|_| {
948 (StatusCode::INTERNAL_SERVER_ERROR, "adapter client missing").into_response()
949 })?;
950 let active_connection_counter = req.extensions.get::<ConnectionCounter>().unwrap();
951 let helm_chart_version = req
952 .extensions
953 .get::<HelmChartVersion>()
954 .map(|h| h.0.clone())
955 .unwrap_or(None);
956
957 let options = if params.options.is_empty() {
958 BTreeMap::<String, String>::default()
961 } else {
962 match serde_json::from_str(¶ms.options) {
963 Ok(options) => options,
964 Err(_e) => {
965 let code = StatusCode::BAD_REQUEST;
967 let msg = format!("Failed to deserialize {} map", "options".quoted());
968 return Err((code, msg).into_response());
969 }
970 }
971 };
972
973 let client = AuthedClient::new(
974 &adapter_client,
975 user.clone(),
976 peer_addr,
977 active_connection_counter.clone(),
978 helm_chart_version,
979 |session| {
980 session
981 .vars_mut()
982 .set_default(WELCOME_MESSAGE.name(), VarInput::Flat(&false.format()))
983 .expect("known to exist")
984 },
985 options,
986 SYSTEM_TIME.clone(),
987 )
988 .await
989 .map_err(|e| {
990 let status = match e {
991 AdapterError::UserSessionsDisallowed | AdapterError::NetworkPolicyDenied(_) => {
992 StatusCode::FORBIDDEN
993 }
994 _ => StatusCode::INTERNAL_SERVER_ERROR,
995 };
996 (status, Json(SqlError::from(e))).into_response()
997 })?;
998
999 Ok(client)
1000 }
1001}
1002
1003#[derive(Debug, Clone, Default)]
1012pub(crate) struct WwwAuthenticateChallenges {
1013 pub include_basic: bool,
1015 pub bearer_resource_metadata: Option<String>,
1020 pub bearer_scope: Option<&'static str>,
1025}
1026
1027#[derive(Debug, Error)]
1028pub(crate) enum AuthError {
1029 #[error("role dissallowed")]
1030 RoleDisallowed(String),
1031 #[error("{0}")]
1032 Frontegg(#[from] FronteggError),
1033 #[error("missing authorization header")]
1034 MissingHttpAuthentication {
1035 challenges: WwwAuthenticateChallenges,
1036 },
1037 #[error("{0}")]
1038 MismatchedUser(String),
1039 #[error("session expired")]
1040 SessionExpired,
1041 #[error("failed to update session")]
1042 FailedToUpdateSession,
1043 #[error("invalid credentials")]
1044 InvalidCredentials,
1045 #[error("{0}")]
1047 OidcFailed(String),
1048}
1049
1050impl IntoResponse for AuthError {
1051 fn into_response(self) -> Response {
1052 warn!("HTTP request failed authentication: {}", self);
1053 let mut headers = HeaderMap::new();
1054 let body = match &self {
1059 AuthError::MissingHttpAuthentication { challenges } => {
1065 if let Some(resource_metadata) = &challenges.bearer_resource_metadata {
1066 let value = match &challenges.bearer_scope {
1072 Some(scope) => format!(
1073 "Bearer scope=\"{scope}\", resource_metadata=\"{resource_metadata}\"",
1074 ),
1075 None => format!("Bearer resource_metadata=\"{resource_metadata}\""),
1076 };
1077 match HeaderValue::from_str(&value) {
1078 Ok(v) => {
1079 headers.append(http::header::WWW_AUTHENTICATE, v);
1080 }
1081 Err(e) => {
1082 warn!(
1083 "skipping Bearer WWW-Authenticate challenge: invalid header \
1084 value derived from resource_metadata={resource_metadata:?}: {e}",
1085 );
1086 }
1087 }
1088 }
1089 if challenges.include_basic {
1090 headers.append(
1091 http::header::WWW_AUTHENTICATE,
1092 HeaderValue::from_static("Basic realm=Materialize"),
1093 );
1094 }
1095 "unauthorized".to_string()
1096 }
1097 AuthError::OidcFailed(message) => message.clone(),
1098 _ => "unauthorized".to_string(),
1099 };
1100 (StatusCode::UNAUTHORIZED, headers, body).into_response()
1101 }
1102}
1103
1104pub async fn handle_login(
1106 session: Option<Extension<TowerSession>>,
1107 Extension(adapter_client_rx): Extension<Delayed<Client>>,
1108 Json(LoginCredentials { username, password }): Json<LoginCredentials>,
1109) -> impl IntoResponse {
1110 let Ok(adapter_client) = adapter_client_rx.clone().await else {
1115 return StatusCode::INTERNAL_SERVER_ERROR;
1116 };
1117 let authenticated = match adapter_client.authenticate(&username, &password).await {
1118 Ok(authenticated) => authenticated,
1119 Err(err) => {
1120 warn!(?err, "HTTP login failed authentication");
1121 return StatusCode::UNAUTHORIZED;
1122 }
1123 };
1124 let session_data = TowerSessionData {
1126 username,
1127 created_at: SystemTime::now(),
1128 last_activity: SystemTime::now(),
1129 authenticated,
1130 authenticator_kind: mz_auth::AuthenticatorKind::Password,
1131 };
1132 let session = session.and_then(|Extension(session)| Some(session));
1134 let Some(session) = session else {
1135 return StatusCode::INTERNAL_SERVER_ERROR;
1136 };
1137 match session.insert("data", &session_data).await {
1138 Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
1139 Ok(_) => StatusCode::OK,
1140 }
1141}
1142
1143pub async fn handle_logout(session: Option<Extension<TowerSession>>) -> impl IntoResponse {
1145 let session = session.and_then(|Extension(session)| Some(session));
1146 let Some(session) = session else {
1147 return StatusCode::INTERNAL_SERVER_ERROR;
1148 };
1149 match session.delete().await {
1151 Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
1152 Ok(_) => StatusCode::OK,
1153 }
1154}
1155
1156async fn http_auth(
1158 mut req: Request,
1159 next: Next,
1160 tls_enabled: bool,
1161 authenticator_kind: listeners::AuthenticatorKind,
1162 frontegg: Option<mz_frontegg_auth::Authenticator>,
1163 oidc_rx: Delayed<mz_authenticator::GenericOidcAuthenticator>,
1164 adapter_client_rx: Delayed<Client>,
1165) -> Result<impl IntoResponse, AuthError> {
1166 let creds = if let Some(basic) = req.headers().typed_get::<Authorization<Basic>>() {
1167 Some(Credentials::Password {
1168 username: basic.username().to_owned(),
1169 password: Password(basic.password().to_owned()),
1170 })
1171 } else if let Some(bearer) = req.headers().typed_get::<Authorization<Bearer>>() {
1172 Some(Credentials::Token {
1173 token: bearer.token().to_owned(),
1174 })
1175 } else {
1176 None
1177 };
1178
1179 if creds.is_none()
1183 && let Some((session, session_data)) =
1184 maybe_get_authenticated_session(req.extensions().get::<TowerSession>()).await
1185 {
1186 let user = ensure_session_unexpired(session, session_data).await?;
1187 req.extensions_mut().insert(user);
1190 return Ok(next.run(req).await);
1191 }
1192
1193 let conn_protocol = req.extensions().get::<ConnProtocol>().unwrap();
1197 match (tls_enabled, &conn_protocol) {
1198 (false, ConnProtocol::Http) => {}
1199 (false, ConnProtocol::Https { .. }) => unreachable!(),
1200 (true, ConnProtocol::Http) => {
1201 let mut parts = req.uri().clone().into_parts();
1202 parts.scheme = Some(Scheme::HTTPS);
1203 return Ok(Redirect::permanent(
1204 &Uri::from_parts(parts)
1205 .expect("it was already a URI, just changed the scheme")
1206 .to_string(),
1207 )
1208 .into_response());
1209 }
1210 (true, ConnProtocol::Https { .. }) => {}
1211 }
1212 if req.extensions().get::<AuthedUser>().is_some() {
1214 return Ok(next.run(req).await);
1215 }
1216
1217 let path = req.uri().path();
1218 let oauth_config = req
1226 .extensions()
1227 .get::<oauth_metadata::McpOAuthConfig>()
1228 .cloned();
1229 let include_basic = path == "/"
1230 || PROFILING_API_ENDPOINTS
1231 .iter()
1232 .any(|prefix| path.starts_with(prefix))
1233 || oauth_config.is_some();
1234 let (bearer_resource_metadata, bearer_scope) = if let Some(config) = &oauth_config
1235 && config.discovery.is_enabled()
1236 {
1237 (
1238 oauth_metadata::metadata_url(&req, config.http_host_name.as_deref()),
1239 Some(config.scope()),
1240 )
1241 } else {
1242 (None, None)
1243 };
1244 let challenges = WwwAuthenticateChallenges {
1245 include_basic,
1246 bearer_resource_metadata,
1247 bearer_scope,
1248 };
1249 let authenticator = get_authenticator(
1250 authenticator_kind,
1251 creds.as_ref(),
1252 frontegg,
1253 &oidc_rx,
1254 &adapter_client_rx,
1255 )
1256 .await;
1257
1258 let group_claim = if matches!(authenticator, Authenticator::Frontegg(_)) {
1264 Some(group_claim_for(&adapter_client_rx).await)
1265 } else {
1266 None
1267 };
1268 let user = auth(&authenticator, creds, &challenges, group_claim.as_deref()).await?;
1269
1270 req.extensions_mut().insert(user);
1273
1274 Ok(next.run(req).await)
1276}
1277
1278async fn http_authz(req: Request, next: Next) -> Result<impl IntoResponse, AuthError> {
1287 match (
1288 req.extensions().get::<AuthedUser>(),
1289 req.extensions().get::<RouteAllowedRoles>().copied(),
1290 ) {
1291 (Some(user), Some(RouteAllowedRoles(allowed_roles))) => {
1292 check_role_allowed(&user.name, allowed_roles)?;
1293 Ok(next.run(req).await)
1294 }
1295 _ => {
1296 warn!("http_authz missing AuthedUser or RouteAllowedRoles extension; denying request");
1297 Err(AuthError::RoleDisallowed("<unknown>".to_string()))
1298 }
1299 }
1300}
1301
1302async fn init_ws(
1303 WsState {
1304 frontegg,
1305 oidc_rx,
1306 authenticator_kind,
1307 adapter_client_rx,
1308 active_connection_counter,
1309 helm_chart_version,
1310 allowed_roles,
1311 }: WsState,
1312 existing_user: Option<ExistingUser>,
1313 peer_addr: IpAddr,
1314 ws: &mut WebSocket,
1315) -> Result<AuthedClient, anyhow::Error> {
1316 let ws_auth: WebSocketAuth = loop {
1319 let init_msg = ws.recv().await.ok_or_else(|| anyhow::anyhow!("closed"))??;
1320 match init_msg {
1321 Message::Text(data) => break serde_json::from_str(&data)?,
1322 Message::Binary(data) => break serde_json::from_slice(&data)?,
1323 Message::Ping(_) => {
1325 continue;
1326 }
1327 Message::Pong(_) => {
1328 continue;
1329 }
1330 Message::Close(_) => {
1331 anyhow::bail!("closed");
1332 }
1333 }
1334 };
1335
1336 let (creds, options) = match ws_auth {
1339 WebSocketAuth::Basic {
1340 user,
1341 password,
1342 options,
1343 } => {
1344 let creds = Credentials::Password {
1345 username: user,
1346 password,
1347 };
1348 (Some(creds), options)
1349 }
1350 WebSocketAuth::Bearer { token, options } => {
1351 let creds = Credentials::Token { token };
1352 (Some(creds), options)
1353 }
1354 WebSocketAuth::OptionsOnly { options } => (None, options),
1355 };
1356
1357 let user = match (existing_user, creds) {
1358 (Some(ExistingUser::XMaterializeUserHeader(_)), Some(_creds)) => {
1359 warn!("Unexpected bearer or basic auth provided when using user header");
1360 anyhow::bail!("unexpected")
1361 }
1362 (Some(ExistingUser::Session(user)), None) => user,
1363 (Some(ExistingUser::XMaterializeUserHeader(user)), None) => user,
1364 (_, Some(creds)) => {
1365 let authenticator = get_authenticator(
1366 authenticator_kind,
1367 Some(&creds),
1368 frontegg,
1369 &oidc_rx,
1370 &adapter_client_rx,
1371 )
1372 .await;
1373 let no_challenges = WwwAuthenticateChallenges::default();
1377 let group_claim = if matches!(authenticator, Authenticator::Frontegg(_)) {
1380 Some(group_claim_for(&adapter_client_rx).await)
1381 } else {
1382 None
1383 };
1384 let user = auth(
1385 &authenticator,
1386 Some(creds),
1387 &no_challenges,
1388 group_claim.as_deref(),
1389 )
1390 .await?;
1391 user
1392 }
1393 (None, None) => anyhow::bail!("expected auth information"),
1394 };
1395
1396 check_role_allowed(&user.name, allowed_roles)?;
1401
1402 let client = AuthedClient::new(
1403 &adapter_client_rx.clone().await?,
1404 user,
1405 peer_addr,
1406 active_connection_counter.clone(),
1407 helm_chart_version.clone(),
1408 |_session| (),
1409 options,
1410 SYSTEM_TIME.clone(),
1411 )
1412 .await?;
1413
1414 Ok(client)
1415}
1416
1417enum Credentials {
1418 Password {
1419 username: String,
1420 password: Password,
1421 },
1422 Token {
1423 token: String,
1424 },
1425}
1426
1427async fn get_authenticator(
1428 kind: listeners::AuthenticatorKind,
1429 creds: Option<&Credentials>,
1430 frontegg: Option<mz_frontegg_auth::Authenticator>,
1431 oidc_rx: &Delayed<mz_authenticator::GenericOidcAuthenticator>,
1432 adapter_client_rx: &Delayed<Client>,
1433) -> Authenticator {
1434 match kind {
1435 listeners::AuthenticatorKind::Frontegg => Authenticator::Frontegg(frontegg.expect(
1436 "Frontegg authenticator should exist with listeners::AuthenticatorKind::Frontegg",
1437 )),
1438 listeners::AuthenticatorKind::Password | listeners::AuthenticatorKind::Sasl => {
1439 let client = adapter_client_rx.clone().await.expect("sender not dropped");
1440 Authenticator::Password(client)
1441 }
1442 listeners::AuthenticatorKind::Oidc => match creds {
1443 Some(Credentials::Password { .. }) => {
1445 let client = adapter_client_rx.clone().await.expect("sender not dropped");
1446 Authenticator::Password(client)
1447 }
1448 _ => Authenticator::Oidc(oidc_rx.clone().await.expect("sender not dropped")),
1449 },
1450 listeners::AuthenticatorKind::None => Authenticator::None,
1451 }
1452}
1453
1454pub(crate) async fn maybe_get_authenticated_session(
1458 session: Option<&TowerSession>,
1459) -> Option<(&TowerSession, TowerSessionData)> {
1460 if let Some(session) = session {
1461 if let Ok(Some(session_data)) = session.get::<TowerSessionData>("data").await {
1462 return Some((session, session_data));
1463 }
1464 }
1465 None
1466}
1467
1468pub(crate) async fn ensure_session_unexpired(
1471 session: &TowerSession,
1472 session_data: TowerSessionData,
1473) -> Result<AuthedUser, AuthError> {
1474 if session_data
1475 .last_activity
1476 .elapsed()
1477 .unwrap_or(Duration::MAX)
1478 > SESSION_DURATION
1479 {
1480 let _ = session.delete().await;
1481 return Err(AuthError::SessionExpired);
1482 }
1483 let mut updated_data = session_data.clone();
1484 updated_data.last_activity = SystemTime::now();
1485 session
1486 .insert("data", &updated_data)
1487 .await
1488 .map_err(|_| AuthError::FailedToUpdateSession)?;
1489
1490 Ok(AuthedUser {
1491 name: session_data.username,
1492 external_metadata_rx: None,
1493 authenticated: session_data.authenticated,
1494 authenticator_kind: session_data.authenticator_kind,
1495 groups: None,
1496 })
1497}
1498
1499async fn auth(
1500 authenticator: &Authenticator,
1501 creds: Option<Credentials>,
1502 challenges: &WwwAuthenticateChallenges,
1503 group_claim: Option<&str>,
1504) -> Result<AuthedUser, AuthError> {
1505 let (name, external_metadata_rx, authenticated, groups) = match authenticator {
1506 Authenticator::Frontegg(frontegg) => match creds {
1507 Some(Credentials::Password { username, password }) => {
1508 let (auth_session, authenticated) = frontegg
1509 .authenticate(&username, password.as_str(), group_claim)
1510 .await?;
1511 let name = auth_session.user().into();
1512 let groups = auth_session.groups();
1513 let external_metadata_rx = Some(auth_session.external_metadata_rx());
1514 (name, external_metadata_rx, authenticated, groups)
1515 }
1516 Some(Credentials::Token { token }) => {
1517 let (claims, authenticated) =
1518 frontegg.validate_access_token(&token, None, group_claim)?;
1519 let (_, external_metadata_rx) = watch::channel(ExternalUserMetadata {
1520 user_id: claims.user_id,
1521 admin: claims.is_admin,
1522 });
1523 (
1524 claims.user,
1525 Some(external_metadata_rx),
1526 authenticated,
1527 claims.groups,
1528 )
1529 }
1530 None => {
1531 return Err(AuthError::MissingHttpAuthentication {
1532 challenges: challenges.clone(),
1533 });
1534 }
1535 },
1536 Authenticator::Password(adapter_client) => match creds {
1537 Some(Credentials::Password { username, password }) => {
1538 let authenticated = adapter_client
1539 .authenticate(&username, &password)
1540 .await
1541 .map_err(|_| AuthError::InvalidCredentials)?;
1542 (username, None, authenticated, None)
1543 }
1544 _ => {
1545 return Err(AuthError::MissingHttpAuthentication {
1546 challenges: challenges.clone(),
1547 });
1548 }
1549 },
1550 Authenticator::Sasl(_) => {
1551 return Err(AuthError::MissingHttpAuthentication {
1555 challenges: challenges.clone(),
1556 });
1557 }
1558 Authenticator::Oidc(oidc) => match creds {
1559 Some(Credentials::Token { token }) => {
1560 let (mut claims, authenticated) = oidc
1561 .authenticate(&token, None)
1562 .await
1563 .map_err(|e| AuthError::OidcFailed(e.to_string()))?;
1564 let name = std::mem::take(&mut claims.user);
1565 let groups = claims.groups.take();
1566 (name, None, authenticated, groups)
1567 }
1568 _ => {
1569 return Err(AuthError::MissingHttpAuthentication {
1570 challenges: challenges.clone(),
1571 });
1572 }
1573 },
1574 Authenticator::None => {
1575 let name = match creds {
1579 Some(Credentials::Password { username, .. }) => username,
1580 _ => HTTP_DEFAULT_USER.name.to_owned(),
1581 };
1582 (name, None, Authenticated, None)
1583 }
1584 };
1585
1586 Ok(AuthedUser {
1587 name,
1588 external_metadata_rx,
1589 authenticated,
1590 authenticator_kind: authenticator.kind(),
1591 groups,
1592 })
1593}
1594
1595fn check_role_allowed(name: &str, allowed_roles: AllowedRoles) -> Result<(), AuthError> {
1597 let is_internal_user = INTERNAL_USER_NAMES.contains(name);
1598 let is_reserved_user = mz_adapter::catalog::is_reserved_role_name(name);
1600 let role_allowed = match allowed_roles {
1601 AllowedRoles::Normal => !is_reserved_user,
1602 AllowedRoles::Internal => is_internal_user,
1603 AllowedRoles::NormalAndInternal => !is_reserved_user || is_internal_user,
1604 };
1605 if role_allowed {
1606 Ok(())
1607 } else {
1608 Err(AuthError::RoleDisallowed(name.to_owned()))
1609 }
1610}
1611
1612trait DefaultLayers {
1615 fn apply_default_layers(self, source: &'static str, metrics: Metrics) -> Self;
1616}
1617
1618impl DefaultLayers for Router {
1619 fn apply_default_layers(self, source: &'static str, metrics: Metrics) -> Self {
1620 self.layer(DefaultBodyLimit::max(MAX_REQUEST_SIZE))
1621 .layer(metrics::PrometheusLayer::new(source, metrics))
1622 }
1623}
1624
1625async fn handle_load_error(error: tower::BoxError) -> impl IntoResponse {
1632 if error.is::<tower::load_shed::error::Overloaded>() {
1633 return (
1634 StatusCode::TOO_MANY_REQUESTS,
1635 Cow::from("too many requests, try again later"),
1636 );
1637 }
1638
1639 (
1642 StatusCode::INTERNAL_SERVER_ERROR,
1643 Cow::from(format!("Unhandled internal error: {}", error)),
1644 )
1645}
1646
1647#[derive(Debug, Deserialize, Serialize, PartialEq)]
1648pub struct LoginCredentials {
1649 username: String,
1650 password: Password,
1651}
1652
1653#[derive(Debug, Clone, Serialize, Deserialize)]
1654pub struct TowerSessionData {
1655 username: String,
1656 created_at: SystemTime,
1657 last_activity: SystemTime,
1658 authenticated: Authenticated,
1659 authenticator_kind: mz_auth::AuthenticatorKind,
1660}
1661
1662#[cfg(test)]
1663mod tests {
1664 use super::{AllowedRoles, check_role_allowed};
1665
1666 #[mz_ore::test]
1667 fn test_check_role_allowed() {
1668 assert!(check_role_allowed("mz_system", AllowedRoles::Internal).is_ok());
1670 assert!(check_role_allowed("mz_system", AllowedRoles::NormalAndInternal).is_ok());
1671 assert!(check_role_allowed("mz_system", AllowedRoles::Normal).is_err());
1672
1673 assert!(check_role_allowed("mz_support", AllowedRoles::Internal).is_ok());
1675 assert!(check_role_allowed("mz_support", AllowedRoles::NormalAndInternal).is_ok());
1676 assert!(check_role_allowed("mz_support", AllowedRoles::Normal).is_err());
1677
1678 assert!(check_role_allowed("mz_analytics", AllowedRoles::Internal).is_ok());
1680 assert!(check_role_allowed("mz_analytics", AllowedRoles::NormalAndInternal).is_ok());
1681 assert!(check_role_allowed("mz_analytics", AllowedRoles::Normal).is_err());
1682
1683 assert!(check_role_allowed("materialize", AllowedRoles::Internal).is_err());
1685 assert!(check_role_allowed("materialize", AllowedRoles::NormalAndInternal).is_ok());
1686 assert!(check_role_allowed("materialize", AllowedRoles::Normal).is_ok());
1687
1688 assert!(check_role_allowed("anonymous_http_user", AllowedRoles::Internal).is_err());
1690 assert!(check_role_allowed("anonymous_http_user", AllowedRoles::NormalAndInternal).is_ok());
1691 assert!(check_role_allowed("anonymous_http_user", AllowedRoles::Normal).is_ok());
1692
1693 assert!(check_role_allowed("alex", AllowedRoles::Internal).is_err());
1695 assert!(check_role_allowed("alex", AllowedRoles::NormalAndInternal).is_ok());
1696 assert!(check_role_allowed("alex", AllowedRoles::Normal).is_ok());
1697
1698 assert!(check_role_allowed("external_asdf", AllowedRoles::Internal).is_err());
1700 assert!(check_role_allowed("external_asdf", AllowedRoles::NormalAndInternal).is_err());
1701 assert!(check_role_allowed("external_asdf", AllowedRoles::Normal).is_err());
1702
1703 assert!(check_role_allowed("pg_somebody", AllowedRoles::Internal).is_err());
1705 assert!(check_role_allowed("pg_somebody", AllowedRoles::NormalAndInternal).is_err());
1706 assert!(check_role_allowed("pg_somebody", AllowedRoles::Normal).is_err());
1707
1708 assert!(check_role_allowed("mz_unknown", AllowedRoles::Internal).is_err());
1710 assert!(check_role_allowed("mz_unknown", AllowedRoles::NormalAndInternal).is_err());
1711 assert!(check_role_allowed("mz_unknown", AllowedRoles::Normal).is_err());
1712
1713 assert!(check_role_allowed("PUBLIC", AllowedRoles::Internal).is_err());
1715 assert!(check_role_allowed("PUBLIC", AllowedRoles::NormalAndInternal).is_err());
1716 assert!(check_role_allowed("PUBLIC", AllowedRoles::Normal).is_err());
1717 }
1718}