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 )
449 .route(
450 "/internal-console/",
451 routing::get(console::handle_internal_console),
452 )
453 .route("/clusters", routing::get(cluster::handle_clusters))
455 .route(
456 "/api/cluster/{:cluster_id}/replica/{:replica_id}/process/{:process}/",
457 routing::any(cluster::handle_cluster_proxy_root),
458 )
459 .route(
460 "/api/cluster/{:cluster_id}/replica/{:replica_id}/process/{:process}/{*path}",
461 routing::any(cluster::handle_cluster_proxy),
462 )
463 .layer(Extension(console_config))
464 .layer(Extension(Arc::clone(&cluster_proxy_config)))
465 .authorize(internal_roles);
466 base_router = base_router.merge(internal_group);
467
468 let leader_router = Router::new()
469 .route("/api/leader/status", routing::get(handle_leader_status))
470 .route("/api/leader/promote", routing::post(handle_leader_promote))
471 .route(
472 "/api/leader/skip-catchup",
473 routing::post(handle_leader_skip_catchup),
474 )
475 .authorize(internal_roles)
476 .layer(auth_middleware.clone())
477 .with_state(internal_route_config.deployment_state_handle.clone());
478 router = router.merge(leader_router);
479 }
480
481 if let RouteGroup::Enabled(metrics_roles) = routes_enabled.metrics {
482 let metrics_registry_for_handler = metrics_registry.clone();
486 let metrics_router = Router::new()
487 .route(
488 "/metrics",
489 routing::get(move |headers: HeaderMap| async move {
490 mz_http_util::handle_prometheus(&metrics_registry_for_handler, headers)
491 .await
492 }),
493 )
494 .route(
495 "/metrics/mz_usage",
496 routing::get(|client: AuthedClient, headers: HeaderMap| async move {
497 let registry = sql::handle_promsql(client, USAGE_METRIC_QUERIES).await;
498 mz_http_util::handle_prometheus(®istry, headers).await
499 }),
500 )
501 .route(
502 "/metrics/mz_frontier",
503 routing::get(|client: AuthedClient, headers: HeaderMap| async move {
504 let registry = sql::handle_promsql(client, FRONTIER_METRIC_QUERIES).await;
505 mz_http_util::handle_prometheus(®istry, headers).await
506 }),
507 )
508 .route(
509 "/metrics/mz_compute",
510 routing::get(|client: AuthedClient, headers: HeaderMap| async move {
511 let registry = sql::handle_promsql(client, COMPUTE_METRIC_QUERIES).await;
512 mz_http_util::handle_prometheus(®istry, headers).await
513 }),
514 )
515 .route(
516 "/metrics/mz_storage",
517 routing::get(|client: AuthedClient, headers: HeaderMap| async move {
518 let registry = sql::handle_promsql(client, STORAGE_METRIC_QUERIES).await;
519 mz_http_util::handle_prometheus(®istry, headers).await
520 }),
521 )
522 .route(
523 "/api/livez",
524 routing::get(mz_http_util::handle_liveness_check),
525 )
526 .route("/api/readyz", routing::get(probe::handle_ready))
527 .authorize(metrics_roles)
528 .layer(auth_middleware.clone())
529 .layer(Extension(adapter_client_rx.clone()))
530 .layer(Extension(active_connection_counter.clone()))
531 .layer(Extension(HelmChartVersion(helm_chart_version.clone())));
532 router = router.merge(metrics_router);
533 }
534
535 if routes_enabled.console_config.is_enabled() {
536 let console_config_router = Router::new()
537 .route(
538 "/api/console/config",
539 routing::get(console::handle_console_config),
540 )
541 .layer(Extension(adapter_client_rx.clone()))
542 .layer(Extension(active_connection_counter.clone()));
543 router = router.merge(console_config_router);
544 }
545
546 if routes_enabled.mcp_agent.is_enabled() || routes_enabled.mcp_developer.is_enabled() {
549 use tracing::info;
550
551 let oauth_metadata_router = Router::new()
563 .route(
564 oauth_metadata::PROTECTED_RESOURCE_METADATA_PATH,
565 routing::get(oauth_metadata::handle_protected_resource_metadata),
566 )
567 .route(
568 oauth_metadata::PROTECTED_RESOURCE_METADATA_PATH_AGENT,
569 routing::get(oauth_metadata::handle_protected_resource_metadata),
570 )
571 .route(
572 oauth_metadata::PROTECTED_RESOURCE_METADATA_PATH_DEVELOPER,
573 routing::get(oauth_metadata::handle_protected_resource_metadata),
574 )
575 .layer(Extension(adapter_client_rx.clone()))
576 .layer(Extension(oauth_metadata::McpOAuthConfig {
577 http_host_name: http_host_name.clone(),
578 discovery: Arc::clone(&oauth_discovery),
579 }))
580 .layer(Extension(oauth_metadata_metrics.clone()));
581 router = router.merge(oauth_metadata_router);
582
583 let mut mcp_router = Router::new();
584
585 if let RouteGroup::Enabled(mcp_agent_roles) = routes_enabled.mcp_agent {
586 info!("Enabling MCP agent endpoint: /api/mcp/agent");
587 let agent_router = Router::new()
588 .route(
589 "/api/mcp/agent",
590 routing::post(mcp::handle_mcp_agent)
591 .get(mcp::handle_mcp_method_not_allowed),
592 )
593 .authorize(mcp_agent_roles);
594 mcp_router = mcp_router.merge(agent_router);
595 }
596
597 if let RouteGroup::Enabled(mcp_developer_roles) = routes_enabled.mcp_developer {
598 info!("Enabling MCP developer endpoint: /api/mcp/developer");
599 let developer_router = Router::new()
600 .route(
601 "/api/mcp/developer",
602 routing::post(mcp::handle_mcp_developer)
603 .get(mcp::handle_mcp_method_not_allowed),
604 )
605 .authorize(mcp_developer_roles);
606 mcp_router = mcp_router.merge(developer_router);
607 }
608
609 let mcp_allowed_origins = Arc::new(allowed_origin_list.clone());
616 mcp_router = mcp_router
617 .layer(auth_middleware.clone())
618 .layer(Extension(oauth_metadata::McpOAuthConfig {
619 http_host_name: http_host_name.clone(),
620 discovery: Arc::clone(&oauth_discovery),
621 }))
622 .layer(Extension(adapter_client_rx.clone()))
623 .layer(Extension(active_connection_counter.clone()))
624 .layer(Extension(HelmChartVersion(helm_chart_version.clone())))
625 .layer(Extension(mcp_allowed_origins))
626 .layer(Extension(mcp_metrics))
627 .layer(
628 CorsLayer::new()
629 .allow_methods(Method::POST)
630 .allow_origin(allowed_origin.clone())
631 .allow_headers([AUTHORIZATION, CONTENT_TYPE]),
632 );
633 if let listeners::AuthenticatorKind::None = authenticator_kind {
637 mcp_router = mcp_router.layer(middleware::from_fn(x_materialize_user_header_auth));
638 }
639 router = router.merge(mcp_router);
640 }
641
642 base_router = base_router
643 .layer(auth_middleware.clone())
644 .layer(Extension(adapter_client_rx.clone()))
645 .layer(Extension(active_connection_counter.clone()))
646 .layer(Extension(HelmChartVersion(helm_chart_version)))
647 .layer(
648 CorsLayer::new()
649 .allow_credentials(false)
650 .allow_headers([
651 AUTHORIZATION,
652 CONTENT_TYPE,
653 HeaderName::from_static("x-materialize-version"),
654 ])
655 .allow_methods(Any)
656 .allow_origin(allowed_origin)
657 .expose_headers(Any)
658 .max_age(Duration::from_secs(60) * 60),
659 );
660
661 match authenticator_kind {
662 listeners::AuthenticatorKind::Password | listeners::AuthenticatorKind::Oidc => {
663 base_router = base_router.layer(session_layer.clone());
664
665 let login_router = Router::new()
666 .route("/api/login", routing::post(handle_login))
667 .route("/api/logout", routing::post(handle_logout))
668 .layer(Extension(adapter_client_rx));
669 router = router.merge(login_router).layer(session_layer);
670 }
671 listeners::AuthenticatorKind::None => {
672 base_router =
673 base_router.layer(middleware::from_fn(x_materialize_user_header_auth));
674 }
675 _ => {}
676 }
677
678 router = router
679 .merge(base_router)
680 .apply_default_layers(source, metrics);
681
682 HttpServer { tls, router }
683 }
684}
685
686impl Server for HttpServer {
687 const NAME: &'static str = "http";
688
689 fn handle_connection(
690 &self,
691 conn: Connection,
692 _tokio_metrics_intervals: impl Iterator<Item = TaskMetrics> + Send + 'static,
693 ) -> ConnectionHandler {
694 let router = self.router.clone();
695 let tls_context = self.tls.clone();
696 let mut conn = TokioIo::new(conn);
697
698 Box::pin(async {
699 let direct_peer_addr = conn.inner().peer_addr().context("fetching peer addr")?;
700 let peer_addr = conn
701 .inner_mut()
702 .take_proxy_header_address()
703 .await
704 .map(|a| a.source)
705 .unwrap_or(direct_peer_addr);
706
707 let (conn, conn_protocol) = match tls_context {
708 Some(tls_context) => {
709 let mut ssl_stream = SslStream::new(Ssl::new(&tls_context.get())?, conn)?;
710 if let Err(e) = Pin::new(&mut ssl_stream).accept().await {
711 let _ = ssl_stream.get_mut().inner_mut().shutdown().await;
712 return Err(e.into());
713 }
714 (MaybeHttpsStream::Https(ssl_stream), ConnProtocol::Https)
715 }
716 _ => (MaybeHttpsStream::Http(conn), ConnProtocol::Http),
717 };
718 let mut make_tower_svc = router
719 .layer(Extension(conn_protocol))
720 .into_make_service_with_connect_info::<SocketAddr>();
721 let tower_svc = make_tower_svc.call(peer_addr).await.unwrap();
722 let hyper_svc = hyper::service::service_fn(|req| tower_svc.clone().call(req));
723 let http = hyper::server::conn::http1::Builder::new();
724 http.serve_connection(conn, hyper_svc)
725 .with_upgrades()
726 .err_into()
727 .await
728 })
729 }
730}
731
732pub async fn handle_leader_status(
733 State(deployment_state_handle): State<DeploymentStateHandle>,
734) -> impl IntoResponse {
735 let status = deployment_state_handle.status();
736 (StatusCode::OK, Json(json!({ "status": status })))
737}
738
739pub async fn handle_leader_promote(
740 State(deployment_state_handle): State<DeploymentStateHandle>,
741) -> impl IntoResponse {
742 match deployment_state_handle.try_promote() {
743 Ok(()) => {
744 let status = StatusCode::OK;
747 let body = Json(json!({
748 "result": "Success",
749 }));
750 (status, body)
751 }
752 Err(()) => {
753 let status = StatusCode::BAD_REQUEST;
756 let body = Json(json!({
757 "result": {"Failure": {"message": "cannot promote leader while initializing"}},
758 }));
759 (status, body)
760 }
761 }
762}
763
764pub async fn handle_leader_skip_catchup(
765 State(deployment_state_handle): State<DeploymentStateHandle>,
766) -> impl IntoResponse {
767 match deployment_state_handle.try_skip_catchup() {
768 Ok(()) => StatusCode::NO_CONTENT.into_response(),
769 Err(()) => {
770 let status = StatusCode::BAD_REQUEST;
771 let body = Json(json!({
772 "message": "cannot skip catchup in this phase of initialization; try again later",
773 }));
774 (status, body).into_response()
775 }
776 }
777}
778
779async fn x_materialize_user_header_auth(mut req: Request, next: Next) -> impl IntoResponse {
780 if let Some(username) = req.headers().get("x-materialize-user").map(|h| h.to_str()) {
782 let username = match username {
783 Ok(name @ (SUPPORT_USER_NAME | SYSTEM_USER_NAME)) => name.to_string(),
784 _ => {
785 return Err(AuthError::MismatchedUser(format!(
786 "user specified in x-materialize-user must be {SUPPORT_USER_NAME} or {SYSTEM_USER_NAME}"
787 )));
788 }
789 };
790 req.extensions_mut().insert(AuthedUser {
794 name: username,
795 external_metadata_rx: None,
796 authenticated: Authenticated,
797 authenticator_kind: mz_auth::AuthenticatorKind::None,
798 groups: None,
799 });
800 }
801 Ok(next.run(req).await)
802}
803
804pub(crate) type Delayed<T> = Shared<oneshot::Receiver<T>>;
805
806async fn group_claim_for(adapter_client_rx: &Delayed<Client>) -> String {
810 let client = adapter_client_rx
811 .clone()
812 .await
813 .expect("adapter client receiver dropped");
814 OIDC_GROUP_CLAIM.get(client.get_system_vars().await.dyncfgs())
815}
816
817#[derive(Clone)]
818enum ConnProtocol {
819 Http,
820 Https,
821}
822
823#[derive(Clone, Copy)]
826struct RouteAllowedRoles(AllowedRoles);
827
828trait AuthzRouterExt {
830 fn authorize(self, roles: AllowedRoles) -> Self;
831}
832
833impl<S> AuthzRouterExt for Router<S>
834where
835 S: Clone + Send + Sync + 'static,
836{
837 fn authorize(self, roles: AllowedRoles) -> Router<S> {
838 self.layer(middleware::from_fn(http_authz))
841 .layer(Extension(RouteAllowedRoles(roles)))
842 }
843}
844
845#[derive(Clone, Debug)]
846pub struct AuthedUser {
847 name: String,
848 external_metadata_rx: Option<watch::Receiver<ExternalUserMetadata>>,
849 authenticated: Authenticated,
850 authenticator_kind: mz_auth::AuthenticatorKind,
851 groups: Option<Vec<String>>,
853}
854
855pub struct AuthedClient {
856 pub client: SessionClient,
857 pub connection_guard: Option<ConnectionHandle>,
858}
859
860impl AuthedClient {
861 async fn new<F>(
862 adapter_client: &Client,
863 user: AuthedUser,
864 peer_addr: IpAddr,
865 active_connection_counter: ConnectionCounter,
866 helm_chart_version: Option<String>,
867 session_config: F,
868 options: BTreeMap<String, String>,
869 now: NowFn,
870 ) -> Result<Self, AdapterError>
871 where
872 F: FnOnce(&mut AdapterSession),
873 {
874 let conn_id = adapter_client.new_conn_id()?;
875 let mut session = adapter_client.new_session(
876 AdapterSessionConfig {
877 conn_id,
878 uuid: epoch_to_uuid_v7(&(now)()),
879 user: user.name,
880 client_ip: Some(peer_addr),
881 external_metadata_rx: user.external_metadata_rx,
882 helm_chart_version,
883 authenticator_kind: user.authenticator_kind,
884 groups: user.groups,
885 },
886 user.authenticated,
887 );
888 let connection_guard = active_connection_counter.allocate_connection(session.user())?;
889
890 session_config(&mut session);
891 let system_vars = adapter_client.get_system_vars().await;
892 for (key, val) in options {
893 const LOCAL: bool = false;
894 if let Err(err) =
895 session
896 .vars_mut()
897 .set(&system_vars, &key, VarInput::Flat(&val), LOCAL)
898 {
899 session.add_notice(AdapterNotice::BadStartupSetting {
900 name: key.to_string(),
901 reason: err.to_string(),
902 })
903 }
904 }
905 let adapter_client = adapter_client.startup(session).await?;
906 Ok(AuthedClient {
907 client: adapter_client,
908 connection_guard,
909 })
910 }
911}
912
913impl<S> FromRequestParts<S> for AuthedClient
914where
915 S: Send + Sync,
916{
917 type Rejection = Response;
918
919 async fn from_request_parts(
920 req: &mut http::request::Parts,
921 state: &S,
922 ) -> Result<Self, Self::Rejection> {
923 #[derive(Debug, Default, Deserialize)]
924 struct Params {
925 #[serde(default)]
926 options: String,
927 }
928 let params: Query<Params> = Query::from_request_parts(req, state)
929 .await
930 .unwrap_or_default();
931
932 let peer_addr = req
933 .extensions
934 .get::<ConnectInfo<SocketAddr>>()
935 .expect("ConnectInfo extension guaranteed to exist")
936 .0
937 .ip();
938
939 let user = req.extensions.get::<AuthedUser>().unwrap();
940 let adapter_client = req
941 .extensions
942 .get::<Delayed<mz_adapter::Client>>()
943 .unwrap()
944 .clone();
945 let adapter_client = adapter_client.await.map_err(|_| {
946 (StatusCode::INTERNAL_SERVER_ERROR, "adapter client missing").into_response()
947 })?;
948 let active_connection_counter = req.extensions.get::<ConnectionCounter>().unwrap();
949 let helm_chart_version = req
950 .extensions
951 .get::<HelmChartVersion>()
952 .map(|h| h.0.clone())
953 .unwrap_or(None);
954
955 let options = if params.options.is_empty() {
956 BTreeMap::<String, String>::default()
959 } else {
960 match serde_json::from_str(¶ms.options) {
961 Ok(options) => options,
962 Err(_e) => {
963 let code = StatusCode::BAD_REQUEST;
965 let msg = format!("Failed to deserialize {} map", "options".quoted());
966 return Err((code, msg).into_response());
967 }
968 }
969 };
970
971 let client = AuthedClient::new(
972 &adapter_client,
973 user.clone(),
974 peer_addr,
975 active_connection_counter.clone(),
976 helm_chart_version,
977 |session| {
978 session
979 .vars_mut()
980 .set_default(WELCOME_MESSAGE.name(), VarInput::Flat(&false.format()))
981 .expect("known to exist")
982 },
983 options,
984 SYSTEM_TIME.clone(),
985 )
986 .await
987 .map_err(|e| {
988 let status = match e {
989 AdapterError::UserSessionsDisallowed | AdapterError::NetworkPolicyDenied(_) => {
990 StatusCode::FORBIDDEN
991 }
992 _ => StatusCode::INTERNAL_SERVER_ERROR,
993 };
994 (status, Json(SqlError::from(e))).into_response()
995 })?;
996
997 Ok(client)
998 }
999}
1000
1001#[derive(Debug, Clone, Default)]
1010pub(crate) struct WwwAuthenticateChallenges {
1011 pub include_basic: bool,
1013 pub bearer_resource_metadata: Option<String>,
1018 pub bearer_scope: Option<&'static str>,
1023}
1024
1025#[derive(Debug, Error)]
1026pub(crate) enum AuthError {
1027 #[error("role dissallowed")]
1028 RoleDisallowed(String),
1029 #[error("{0}")]
1030 Frontegg(#[from] FronteggError),
1031 #[error("missing authorization header")]
1032 MissingHttpAuthentication {
1033 challenges: WwwAuthenticateChallenges,
1034 },
1035 #[error("{0}")]
1036 MismatchedUser(String),
1037 #[error("session expired")]
1038 SessionExpired,
1039 #[error("failed to update session")]
1040 FailedToUpdateSession,
1041 #[error("invalid credentials")]
1042 InvalidCredentials,
1043 #[error("{0}")]
1045 OidcFailed(String),
1046}
1047
1048impl IntoResponse for AuthError {
1049 fn into_response(self) -> Response {
1050 warn!("HTTP request failed authentication: {}", self);
1051 let mut headers = HeaderMap::new();
1052 let body = match &self {
1057 AuthError::MissingHttpAuthentication { challenges } => {
1063 if let Some(resource_metadata) = &challenges.bearer_resource_metadata {
1064 let value = match &challenges.bearer_scope {
1070 Some(scope) => format!(
1071 "Bearer scope=\"{scope}\", resource_metadata=\"{resource_metadata}\"",
1072 ),
1073 None => format!("Bearer resource_metadata=\"{resource_metadata}\""),
1074 };
1075 match HeaderValue::from_str(&value) {
1076 Ok(v) => {
1077 headers.append(http::header::WWW_AUTHENTICATE, v);
1078 }
1079 Err(e) => {
1080 warn!(
1081 "skipping Bearer WWW-Authenticate challenge: invalid header \
1082 value derived from resource_metadata={resource_metadata:?}: {e}",
1083 );
1084 }
1085 }
1086 }
1087 if challenges.include_basic {
1088 headers.append(
1089 http::header::WWW_AUTHENTICATE,
1090 HeaderValue::from_static("Basic realm=Materialize"),
1091 );
1092 }
1093 "unauthorized".to_string()
1094 }
1095 AuthError::OidcFailed(message) => message.clone(),
1096 _ => "unauthorized".to_string(),
1097 };
1098 (StatusCode::UNAUTHORIZED, headers, body).into_response()
1099 }
1100}
1101
1102pub async fn handle_login(
1104 session: Option<Extension<TowerSession>>,
1105 Extension(adapter_client_rx): Extension<Delayed<Client>>,
1106 Json(LoginCredentials { username, password }): Json<LoginCredentials>,
1107) -> impl IntoResponse {
1108 let Ok(adapter_client) = adapter_client_rx.clone().await else {
1113 return StatusCode::INTERNAL_SERVER_ERROR;
1114 };
1115 let authenticated = match adapter_client.authenticate(&username, &password).await {
1116 Ok(authenticated) => authenticated,
1117 Err(err) => {
1118 warn!(?err, "HTTP login failed authentication");
1119 return StatusCode::UNAUTHORIZED;
1120 }
1121 };
1122 let session_data = TowerSessionData {
1124 username,
1125 created_at: SystemTime::now(),
1126 last_activity: SystemTime::now(),
1127 authenticated,
1128 authenticator_kind: mz_auth::AuthenticatorKind::Password,
1129 };
1130 let session = session.and_then(|Extension(session)| Some(session));
1132 let Some(session) = session else {
1133 return StatusCode::INTERNAL_SERVER_ERROR;
1134 };
1135 match session.insert("data", &session_data).await {
1136 Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
1137 Ok(_) => StatusCode::OK,
1138 }
1139}
1140
1141pub async fn handle_logout(session: Option<Extension<TowerSession>>) -> impl IntoResponse {
1143 let session = session.and_then(|Extension(session)| Some(session));
1144 let Some(session) = session else {
1145 return StatusCode::INTERNAL_SERVER_ERROR;
1146 };
1147 match session.delete().await {
1149 Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
1150 Ok(_) => StatusCode::OK,
1151 }
1152}
1153
1154async fn http_auth(
1156 mut req: Request,
1157 next: Next,
1158 tls_enabled: bool,
1159 authenticator_kind: listeners::AuthenticatorKind,
1160 frontegg: Option<mz_frontegg_auth::Authenticator>,
1161 oidc_rx: Delayed<mz_authenticator::GenericOidcAuthenticator>,
1162 adapter_client_rx: Delayed<Client>,
1163) -> Result<impl IntoResponse, AuthError> {
1164 let creds = if let Some(basic) = req.headers().typed_get::<Authorization<Basic>>() {
1165 Some(Credentials::Password {
1166 username: basic.username().to_owned(),
1167 password: Password(basic.password().to_owned()),
1168 })
1169 } else if let Some(bearer) = req.headers().typed_get::<Authorization<Bearer>>() {
1170 Some(Credentials::Token {
1171 token: bearer.token().to_owned(),
1172 })
1173 } else {
1174 None
1175 };
1176
1177 if creds.is_none()
1181 && let Some((session, session_data)) =
1182 maybe_get_authenticated_session(req.extensions().get::<TowerSession>()).await
1183 {
1184 let user = ensure_session_unexpired(session, session_data).await?;
1185 req.extensions_mut().insert(user);
1188 return Ok(next.run(req).await);
1189 }
1190
1191 let conn_protocol = req.extensions().get::<ConnProtocol>().unwrap();
1195 match (tls_enabled, &conn_protocol) {
1196 (false, ConnProtocol::Http) => {}
1197 (false, ConnProtocol::Https { .. }) => unreachable!(),
1198 (true, ConnProtocol::Http) => {
1199 let mut parts = req.uri().clone().into_parts();
1200 parts.scheme = Some(Scheme::HTTPS);
1201 return Ok(Redirect::permanent(
1202 &Uri::from_parts(parts)
1203 .expect("it was already a URI, just changed the scheme")
1204 .to_string(),
1205 )
1206 .into_response());
1207 }
1208 (true, ConnProtocol::Https { .. }) => {}
1209 }
1210 if req.extensions().get::<AuthedUser>().is_some() {
1212 return Ok(next.run(req).await);
1213 }
1214
1215 let path = req.uri().path();
1216 let oauth_config = req
1224 .extensions()
1225 .get::<oauth_metadata::McpOAuthConfig>()
1226 .cloned();
1227 let include_basic = path == "/"
1228 || PROFILING_API_ENDPOINTS
1229 .iter()
1230 .any(|prefix| path.starts_with(prefix))
1231 || oauth_config.is_some();
1232 let (bearer_resource_metadata, bearer_scope) = if let Some(config) = &oauth_config
1233 && config.discovery.is_enabled()
1234 {
1235 (
1236 oauth_metadata::metadata_url(&req, config.http_host_name.as_deref()),
1237 Some(config.scope()),
1238 )
1239 } else {
1240 (None, None)
1241 };
1242 let challenges = WwwAuthenticateChallenges {
1243 include_basic,
1244 bearer_resource_metadata,
1245 bearer_scope,
1246 };
1247 let authenticator = get_authenticator(
1248 authenticator_kind,
1249 creds.as_ref(),
1250 frontegg,
1251 &oidc_rx,
1252 &adapter_client_rx,
1253 )
1254 .await;
1255
1256 let group_claim = if matches!(authenticator, Authenticator::Frontegg(_)) {
1262 Some(group_claim_for(&adapter_client_rx).await)
1263 } else {
1264 None
1265 };
1266 let user = auth(&authenticator, creds, &challenges, group_claim.as_deref()).await?;
1267
1268 req.extensions_mut().insert(user);
1271
1272 Ok(next.run(req).await)
1274}
1275
1276async fn http_authz(req: Request, next: Next) -> Result<impl IntoResponse, AuthError> {
1285 match (
1286 req.extensions().get::<AuthedUser>(),
1287 req.extensions().get::<RouteAllowedRoles>().copied(),
1288 ) {
1289 (Some(user), Some(RouteAllowedRoles(allowed_roles))) => {
1290 check_role_allowed(&user.name, allowed_roles)?;
1291 Ok(next.run(req).await)
1292 }
1293 _ => {
1294 warn!("http_authz missing AuthedUser or RouteAllowedRoles extension; denying request");
1295 Err(AuthError::RoleDisallowed("<unknown>".to_string()))
1296 }
1297 }
1298}
1299
1300async fn init_ws(
1301 WsState {
1302 frontegg,
1303 oidc_rx,
1304 authenticator_kind,
1305 adapter_client_rx,
1306 active_connection_counter,
1307 helm_chart_version,
1308 allowed_roles,
1309 }: WsState,
1310 existing_user: Option<ExistingUser>,
1311 peer_addr: IpAddr,
1312 ws: &mut WebSocket,
1313) -> Result<AuthedClient, anyhow::Error> {
1314 let ws_auth: WebSocketAuth = loop {
1317 let init_msg = ws.recv().await.ok_or_else(|| anyhow::anyhow!("closed"))??;
1318 match init_msg {
1319 Message::Text(data) => break serde_json::from_str(&data)?,
1320 Message::Binary(data) => break serde_json::from_slice(&data)?,
1321 Message::Ping(_) => {
1323 continue;
1324 }
1325 Message::Pong(_) => {
1326 continue;
1327 }
1328 Message::Close(_) => {
1329 anyhow::bail!("closed");
1330 }
1331 }
1332 };
1333
1334 let (creds, options) = match ws_auth {
1337 WebSocketAuth::Basic {
1338 user,
1339 password,
1340 options,
1341 } => {
1342 let creds = Credentials::Password {
1343 username: user,
1344 password,
1345 };
1346 (Some(creds), options)
1347 }
1348 WebSocketAuth::Bearer { token, options } => {
1349 let creds = Credentials::Token { token };
1350 (Some(creds), options)
1351 }
1352 WebSocketAuth::OptionsOnly { options } => (None, options),
1353 };
1354
1355 let user = match (existing_user, creds) {
1356 (Some(ExistingUser::XMaterializeUserHeader(_)), Some(_creds)) => {
1357 warn!("Unexpected bearer or basic auth provided when using user header");
1358 anyhow::bail!("unexpected")
1359 }
1360 (Some(ExistingUser::Session(user)), None) => user,
1361 (Some(ExistingUser::XMaterializeUserHeader(user)), None) => user,
1362 (_, Some(creds)) => {
1363 let authenticator = get_authenticator(
1364 authenticator_kind,
1365 Some(&creds),
1366 frontegg,
1367 &oidc_rx,
1368 &adapter_client_rx,
1369 )
1370 .await;
1371 let no_challenges = WwwAuthenticateChallenges::default();
1375 let group_claim = if matches!(authenticator, Authenticator::Frontegg(_)) {
1378 Some(group_claim_for(&adapter_client_rx).await)
1379 } else {
1380 None
1381 };
1382 let user = auth(
1383 &authenticator,
1384 Some(creds),
1385 &no_challenges,
1386 group_claim.as_deref(),
1387 )
1388 .await?;
1389 user
1390 }
1391 (None, None) => anyhow::bail!("expected auth information"),
1392 };
1393
1394 check_role_allowed(&user.name, allowed_roles)?;
1399
1400 let client = AuthedClient::new(
1401 &adapter_client_rx.clone().await?,
1402 user,
1403 peer_addr,
1404 active_connection_counter.clone(),
1405 helm_chart_version.clone(),
1406 |_session| (),
1407 options,
1408 SYSTEM_TIME.clone(),
1409 )
1410 .await?;
1411
1412 Ok(client)
1413}
1414
1415enum Credentials {
1416 Password {
1417 username: String,
1418 password: Password,
1419 },
1420 Token {
1421 token: String,
1422 },
1423}
1424
1425async fn get_authenticator(
1426 kind: listeners::AuthenticatorKind,
1427 creds: Option<&Credentials>,
1428 frontegg: Option<mz_frontegg_auth::Authenticator>,
1429 oidc_rx: &Delayed<mz_authenticator::GenericOidcAuthenticator>,
1430 adapter_client_rx: &Delayed<Client>,
1431) -> Authenticator {
1432 match kind {
1433 listeners::AuthenticatorKind::Frontegg => Authenticator::Frontegg(frontegg.expect(
1434 "Frontegg authenticator should exist with listeners::AuthenticatorKind::Frontegg",
1435 )),
1436 listeners::AuthenticatorKind::Password | listeners::AuthenticatorKind::Sasl => {
1437 let client = adapter_client_rx.clone().await.expect("sender not dropped");
1438 Authenticator::Password(client)
1439 }
1440 listeners::AuthenticatorKind::Oidc => match creds {
1441 Some(Credentials::Password { .. }) => {
1443 let client = adapter_client_rx.clone().await.expect("sender not dropped");
1444 Authenticator::Password(client)
1445 }
1446 _ => Authenticator::Oidc(oidc_rx.clone().await.expect("sender not dropped")),
1447 },
1448 listeners::AuthenticatorKind::None => Authenticator::None,
1449 }
1450}
1451
1452pub(crate) async fn maybe_get_authenticated_session(
1456 session: Option<&TowerSession>,
1457) -> Option<(&TowerSession, TowerSessionData)> {
1458 if let Some(session) = session {
1459 if let Ok(Some(session_data)) = session.get::<TowerSessionData>("data").await {
1460 return Some((session, session_data));
1461 }
1462 }
1463 None
1464}
1465
1466pub(crate) async fn ensure_session_unexpired(
1469 session: &TowerSession,
1470 session_data: TowerSessionData,
1471) -> Result<AuthedUser, AuthError> {
1472 if session_data
1473 .last_activity
1474 .elapsed()
1475 .unwrap_or(Duration::MAX)
1476 > SESSION_DURATION
1477 {
1478 let _ = session.delete().await;
1479 return Err(AuthError::SessionExpired);
1480 }
1481 let mut updated_data = session_data.clone();
1482 updated_data.last_activity = SystemTime::now();
1483 session
1484 .insert("data", &updated_data)
1485 .await
1486 .map_err(|_| AuthError::FailedToUpdateSession)?;
1487
1488 Ok(AuthedUser {
1489 name: session_data.username,
1490 external_metadata_rx: None,
1491 authenticated: session_data.authenticated,
1492 authenticator_kind: session_data.authenticator_kind,
1493 groups: None,
1494 })
1495}
1496
1497async fn auth(
1498 authenticator: &Authenticator,
1499 creds: Option<Credentials>,
1500 challenges: &WwwAuthenticateChallenges,
1501 group_claim: Option<&str>,
1502) -> Result<AuthedUser, AuthError> {
1503 let (name, external_metadata_rx, authenticated, groups) = match authenticator {
1504 Authenticator::Frontegg(frontegg) => match creds {
1505 Some(Credentials::Password { username, password }) => {
1506 let (auth_session, authenticated) = frontegg
1507 .authenticate(&username, password.as_str(), group_claim)
1508 .await?;
1509 let name = auth_session.user().into();
1510 let groups = auth_session.groups();
1511 let external_metadata_rx = Some(auth_session.external_metadata_rx());
1512 (name, external_metadata_rx, authenticated, groups)
1513 }
1514 Some(Credentials::Token { token }) => {
1515 let (claims, authenticated) =
1516 frontegg.validate_access_token(&token, None, group_claim)?;
1517 let (_, external_metadata_rx) = watch::channel(ExternalUserMetadata {
1518 user_id: claims.user_id,
1519 admin: claims.is_admin,
1520 });
1521 (
1522 claims.user,
1523 Some(external_metadata_rx),
1524 authenticated,
1525 claims.groups,
1526 )
1527 }
1528 None => {
1529 return Err(AuthError::MissingHttpAuthentication {
1530 challenges: challenges.clone(),
1531 });
1532 }
1533 },
1534 Authenticator::Password(adapter_client) => match creds {
1535 Some(Credentials::Password { username, password }) => {
1536 let authenticated = adapter_client
1537 .authenticate(&username, &password)
1538 .await
1539 .map_err(|_| AuthError::InvalidCredentials)?;
1540 (username, None, authenticated, None)
1541 }
1542 _ => {
1543 return Err(AuthError::MissingHttpAuthentication {
1544 challenges: challenges.clone(),
1545 });
1546 }
1547 },
1548 Authenticator::Sasl(_) => {
1549 return Err(AuthError::MissingHttpAuthentication {
1553 challenges: challenges.clone(),
1554 });
1555 }
1556 Authenticator::Oidc(oidc) => match creds {
1557 Some(Credentials::Token { token }) => {
1558 let (mut claims, authenticated) = oidc
1559 .authenticate(&token, None)
1560 .await
1561 .map_err(|e| AuthError::OidcFailed(e.to_string()))?;
1562 let name = std::mem::take(&mut claims.user);
1563 let groups = claims.groups.take();
1564 (name, None, authenticated, groups)
1565 }
1566 _ => {
1567 return Err(AuthError::MissingHttpAuthentication {
1568 challenges: challenges.clone(),
1569 });
1570 }
1571 },
1572 Authenticator::None => {
1573 let name = match creds {
1577 Some(Credentials::Password { username, .. }) => username,
1578 _ => HTTP_DEFAULT_USER.name.to_owned(),
1579 };
1580 (name, None, Authenticated, None)
1581 }
1582 };
1583
1584 Ok(AuthedUser {
1585 name,
1586 external_metadata_rx,
1587 authenticated,
1588 authenticator_kind: authenticator.kind(),
1589 groups,
1590 })
1591}
1592
1593fn check_role_allowed(name: &str, allowed_roles: AllowedRoles) -> Result<(), AuthError> {
1595 let is_internal_user = INTERNAL_USER_NAMES.contains(name);
1596 let is_reserved_user = mz_adapter::catalog::is_reserved_role_name(name);
1598 let role_allowed = match allowed_roles {
1599 AllowedRoles::Normal => !is_reserved_user,
1600 AllowedRoles::Internal => is_internal_user,
1601 AllowedRoles::NormalAndInternal => !is_reserved_user || is_internal_user,
1602 };
1603 if role_allowed {
1604 Ok(())
1605 } else {
1606 Err(AuthError::RoleDisallowed(name.to_owned()))
1607 }
1608}
1609
1610trait DefaultLayers {
1613 fn apply_default_layers(self, source: &'static str, metrics: Metrics) -> Self;
1614}
1615
1616impl DefaultLayers for Router {
1617 fn apply_default_layers(self, source: &'static str, metrics: Metrics) -> Self {
1618 self.layer(DefaultBodyLimit::max(MAX_REQUEST_SIZE))
1619 .layer(metrics::PrometheusLayer::new(source, metrics))
1620 }
1621}
1622
1623async fn handle_load_error(error: tower::BoxError) -> impl IntoResponse {
1630 if error.is::<tower::load_shed::error::Overloaded>() {
1631 return (
1632 StatusCode::TOO_MANY_REQUESTS,
1633 Cow::from("too many requests, try again later"),
1634 );
1635 }
1636
1637 (
1640 StatusCode::INTERNAL_SERVER_ERROR,
1641 Cow::from(format!("Unhandled internal error: {}", error)),
1642 )
1643}
1644
1645#[derive(Debug, Deserialize, Serialize, PartialEq)]
1646pub struct LoginCredentials {
1647 username: String,
1648 password: Password,
1649}
1650
1651#[derive(Debug, Clone, Serialize, Deserialize)]
1652pub struct TowerSessionData {
1653 username: String,
1654 created_at: SystemTime,
1655 last_activity: SystemTime,
1656 authenticated: Authenticated,
1657 authenticator_kind: mz_auth::AuthenticatorKind,
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662 use super::{AllowedRoles, check_role_allowed};
1663
1664 #[mz_ore::test]
1665 fn test_check_role_allowed() {
1666 assert!(check_role_allowed("mz_system", AllowedRoles::Internal).is_ok());
1668 assert!(check_role_allowed("mz_system", AllowedRoles::NormalAndInternal).is_ok());
1669 assert!(check_role_allowed("mz_system", AllowedRoles::Normal).is_err());
1670
1671 assert!(check_role_allowed("mz_support", AllowedRoles::Internal).is_ok());
1673 assert!(check_role_allowed("mz_support", AllowedRoles::NormalAndInternal).is_ok());
1674 assert!(check_role_allowed("mz_support", AllowedRoles::Normal).is_err());
1675
1676 assert!(check_role_allowed("mz_analytics", AllowedRoles::Internal).is_ok());
1678 assert!(check_role_allowed("mz_analytics", AllowedRoles::NormalAndInternal).is_ok());
1679 assert!(check_role_allowed("mz_analytics", AllowedRoles::Normal).is_err());
1680
1681 assert!(check_role_allowed("materialize", AllowedRoles::Internal).is_err());
1683 assert!(check_role_allowed("materialize", AllowedRoles::NormalAndInternal).is_ok());
1684 assert!(check_role_allowed("materialize", AllowedRoles::Normal).is_ok());
1685
1686 assert!(check_role_allowed("anonymous_http_user", AllowedRoles::Internal).is_err());
1688 assert!(check_role_allowed("anonymous_http_user", AllowedRoles::NormalAndInternal).is_ok());
1689 assert!(check_role_allowed("anonymous_http_user", AllowedRoles::Normal).is_ok());
1690
1691 assert!(check_role_allowed("alex", AllowedRoles::Internal).is_err());
1693 assert!(check_role_allowed("alex", AllowedRoles::NormalAndInternal).is_ok());
1694 assert!(check_role_allowed("alex", AllowedRoles::Normal).is_ok());
1695
1696 assert!(check_role_allowed("external_asdf", AllowedRoles::Internal).is_err());
1698 assert!(check_role_allowed("external_asdf", AllowedRoles::NormalAndInternal).is_err());
1699 assert!(check_role_allowed("external_asdf", AllowedRoles::Normal).is_err());
1700
1701 assert!(check_role_allowed("pg_somebody", AllowedRoles::Internal).is_err());
1703 assert!(check_role_allowed("pg_somebody", AllowedRoles::NormalAndInternal).is_err());
1704 assert!(check_role_allowed("pg_somebody", AllowedRoles::Normal).is_err());
1705
1706 assert!(check_role_allowed("mz_unknown", AllowedRoles::Internal).is_err());
1708 assert!(check_role_allowed("mz_unknown", AllowedRoles::NormalAndInternal).is_err());
1709 assert!(check_role_allowed("mz_unknown", AllowedRoles::Normal).is_err());
1710
1711 assert!(check_role_allowed("PUBLIC", AllowedRoles::Internal).is_err());
1713 assert!(check_role_allowed("PUBLIC", AllowedRoles::NormalAndInternal).is_err());
1714 assert!(check_role_allowed("PUBLIC", AllowedRoles::Normal).is_err());
1715 }
1716}