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 router = router.merge(mcp_router);
634 }
635
636 base_router = base_router
637 .layer(auth_middleware.clone())
638 .layer(Extension(adapter_client_rx.clone()))
639 .layer(Extension(active_connection_counter.clone()))
640 .layer(Extension(HelmChartVersion(helm_chart_version)))
641 .layer(
642 CorsLayer::new()
643 .allow_credentials(false)
644 .allow_headers([
645 AUTHORIZATION,
646 CONTENT_TYPE,
647 HeaderName::from_static("x-materialize-version"),
648 ])
649 .allow_methods(Any)
650 .allow_origin(allowed_origin)
651 .expose_headers(Any)
652 .max_age(Duration::from_secs(60) * 60),
653 );
654
655 match authenticator_kind {
656 listeners::AuthenticatorKind::Password | listeners::AuthenticatorKind::Oidc => {
657 base_router = base_router.layer(session_layer.clone());
658
659 let login_router = Router::new()
660 .route("/api/login", routing::post(handle_login))
661 .route("/api/logout", routing::post(handle_logout))
662 .layer(Extension(adapter_client_rx));
663 router = router.merge(login_router).layer(session_layer);
664 }
665 listeners::AuthenticatorKind::None => {
666 base_router =
667 base_router.layer(middleware::from_fn(x_materialize_user_header_auth));
668 }
669 _ => {}
670 }
671
672 router = router
673 .merge(base_router)
674 .apply_default_layers(source, metrics);
675
676 HttpServer { tls, router }
677 }
678}
679
680impl Server for HttpServer {
681 const NAME: &'static str = "http";
682
683 fn handle_connection(
684 &self,
685 conn: Connection,
686 _tokio_metrics_intervals: impl Iterator<Item = TaskMetrics> + Send + 'static,
687 ) -> ConnectionHandler {
688 let router = self.router.clone();
689 let tls_context = self.tls.clone();
690 let mut conn = TokioIo::new(conn);
691
692 Box::pin(async {
693 let direct_peer_addr = conn.inner().peer_addr().context("fetching peer addr")?;
694 let peer_addr = conn
695 .inner_mut()
696 .take_proxy_header_address()
697 .await
698 .map(|a| a.source)
699 .unwrap_or(direct_peer_addr);
700
701 let (conn, conn_protocol) = match tls_context {
702 Some(tls_context) => {
703 let mut ssl_stream = SslStream::new(Ssl::new(&tls_context.get())?, conn)?;
704 if let Err(e) = Pin::new(&mut ssl_stream).accept().await {
705 let _ = ssl_stream.get_mut().inner_mut().shutdown().await;
706 return Err(e.into());
707 }
708 (MaybeHttpsStream::Https(ssl_stream), ConnProtocol::Https)
709 }
710 _ => (MaybeHttpsStream::Http(conn), ConnProtocol::Http),
711 };
712 let mut make_tower_svc = router
713 .layer(Extension(conn_protocol))
714 .into_make_service_with_connect_info::<SocketAddr>();
715 let tower_svc = make_tower_svc.call(peer_addr).await.unwrap();
716 let hyper_svc = hyper::service::service_fn(|req| tower_svc.clone().call(req));
717 let http = hyper::server::conn::http1::Builder::new();
718 http.serve_connection(conn, hyper_svc)
719 .with_upgrades()
720 .err_into()
721 .await
722 })
723 }
724}
725
726pub async fn handle_leader_status(
727 State(deployment_state_handle): State<DeploymentStateHandle>,
728) -> impl IntoResponse {
729 let status = deployment_state_handle.status();
730 (StatusCode::OK, Json(json!({ "status": status })))
731}
732
733pub async fn handle_leader_promote(
734 State(deployment_state_handle): State<DeploymentStateHandle>,
735) -> impl IntoResponse {
736 match deployment_state_handle.try_promote() {
737 Ok(()) => {
738 let status = StatusCode::OK;
741 let body = Json(json!({
742 "result": "Success",
743 }));
744 (status, body)
745 }
746 Err(()) => {
747 let status = StatusCode::BAD_REQUEST;
750 let body = Json(json!({
751 "result": {"Failure": {"message": "cannot promote leader while initializing"}},
752 }));
753 (status, body)
754 }
755 }
756}
757
758pub async fn handle_leader_skip_catchup(
759 State(deployment_state_handle): State<DeploymentStateHandle>,
760) -> impl IntoResponse {
761 match deployment_state_handle.try_skip_catchup() {
762 Ok(()) => StatusCode::NO_CONTENT.into_response(),
763 Err(()) => {
764 let status = StatusCode::BAD_REQUEST;
765 let body = Json(json!({
766 "message": "cannot skip catchup in this phase of initialization; try again later",
767 }));
768 (status, body).into_response()
769 }
770 }
771}
772
773async fn x_materialize_user_header_auth(mut req: Request, next: Next) -> impl IntoResponse {
774 if let Some(username) = req.headers().get("x-materialize-user").map(|h| h.to_str()) {
776 let username = match username {
777 Ok(name @ (SUPPORT_USER_NAME | SYSTEM_USER_NAME)) => name.to_string(),
778 _ => {
779 return Err(AuthError::MismatchedUser(format!(
780 "user specified in x-materialize-user must be {SUPPORT_USER_NAME} or {SYSTEM_USER_NAME}"
781 )));
782 }
783 };
784 req.extensions_mut().insert(AuthedUser {
788 name: username,
789 external_metadata_rx: None,
790 authenticated: Authenticated,
791 authenticator_kind: mz_auth::AuthenticatorKind::None,
792 groups: None,
793 });
794 }
795 Ok(next.run(req).await)
796}
797
798pub(crate) type Delayed<T> = Shared<oneshot::Receiver<T>>;
799
800async fn group_claim_for(adapter_client_rx: &Delayed<Client>) -> String {
804 let client = adapter_client_rx
805 .clone()
806 .await
807 .expect("adapter client receiver dropped");
808 OIDC_GROUP_CLAIM.get(client.get_system_vars().await.dyncfgs())
809}
810
811#[derive(Clone)]
812enum ConnProtocol {
813 Http,
814 Https,
815}
816
817#[derive(Clone, Copy)]
820struct RouteAllowedRoles(AllowedRoles);
821
822trait AuthzRouterExt {
824 fn authorize(self, roles: AllowedRoles) -> Self;
825}
826
827impl<S> AuthzRouterExt for Router<S>
828where
829 S: Clone + Send + Sync + 'static,
830{
831 fn authorize(self, roles: AllowedRoles) -> Router<S> {
832 self.layer(middleware::from_fn(http_authz))
835 .layer(Extension(RouteAllowedRoles(roles)))
836 }
837}
838
839#[derive(Clone, Debug)]
840pub struct AuthedUser {
841 name: String,
842 external_metadata_rx: Option<watch::Receiver<ExternalUserMetadata>>,
843 authenticated: Authenticated,
844 authenticator_kind: mz_auth::AuthenticatorKind,
845 groups: Option<Vec<String>>,
847}
848
849pub struct AuthedClient {
850 pub client: SessionClient,
851 pub connection_guard: Option<ConnectionHandle>,
852}
853
854impl AuthedClient {
855 async fn new<F>(
856 adapter_client: &Client,
857 user: AuthedUser,
858 peer_addr: IpAddr,
859 active_connection_counter: ConnectionCounter,
860 helm_chart_version: Option<String>,
861 session_config: F,
862 options: BTreeMap<String, String>,
863 now: NowFn,
864 ) -> Result<Self, AdapterError>
865 where
866 F: FnOnce(&mut AdapterSession),
867 {
868 let conn_id = adapter_client.new_conn_id()?;
869 let mut session = adapter_client.new_session(
870 AdapterSessionConfig {
871 conn_id,
872 uuid: epoch_to_uuid_v7(&(now)()),
873 user: user.name,
874 client_ip: Some(peer_addr),
875 external_metadata_rx: user.external_metadata_rx,
876 helm_chart_version,
877 authenticator_kind: user.authenticator_kind,
878 groups: user.groups,
879 },
880 user.authenticated,
881 );
882 let connection_guard = active_connection_counter.allocate_connection(session.user())?;
883
884 session_config(&mut session);
885 let system_vars = adapter_client.get_system_vars().await;
886 for (key, val) in options {
887 const LOCAL: bool = false;
888 if let Err(err) =
889 session
890 .vars_mut()
891 .set(&system_vars, &key, VarInput::Flat(&val), LOCAL)
892 {
893 session.add_notice(AdapterNotice::BadStartupSetting {
894 name: key.to_string(),
895 reason: err.to_string(),
896 })
897 }
898 }
899 let adapter_client = adapter_client.startup(session).await?;
900 Ok(AuthedClient {
901 client: adapter_client,
902 connection_guard,
903 })
904 }
905}
906
907impl<S> FromRequestParts<S> for AuthedClient
908where
909 S: Send + Sync,
910{
911 type Rejection = Response;
912
913 async fn from_request_parts(
914 req: &mut http::request::Parts,
915 state: &S,
916 ) -> Result<Self, Self::Rejection> {
917 #[derive(Debug, Default, Deserialize)]
918 struct Params {
919 #[serde(default)]
920 options: String,
921 }
922 let params: Query<Params> = Query::from_request_parts(req, state)
923 .await
924 .unwrap_or_default();
925
926 let peer_addr = req
927 .extensions
928 .get::<ConnectInfo<SocketAddr>>()
929 .expect("ConnectInfo extension guaranteed to exist")
930 .0
931 .ip();
932
933 let user = req.extensions.get::<AuthedUser>().unwrap();
934 let adapter_client = req
935 .extensions
936 .get::<Delayed<mz_adapter::Client>>()
937 .unwrap()
938 .clone();
939 let adapter_client = adapter_client.await.map_err(|_| {
940 (StatusCode::INTERNAL_SERVER_ERROR, "adapter client missing").into_response()
941 })?;
942 let active_connection_counter = req.extensions.get::<ConnectionCounter>().unwrap();
943 let helm_chart_version = req
944 .extensions
945 .get::<HelmChartVersion>()
946 .map(|h| h.0.clone())
947 .unwrap_or(None);
948
949 let options = if params.options.is_empty() {
950 BTreeMap::<String, String>::default()
953 } else {
954 match serde_json::from_str(¶ms.options) {
955 Ok(options) => options,
956 Err(_e) => {
957 let code = StatusCode::BAD_REQUEST;
959 let msg = format!("Failed to deserialize {} map", "options".quoted());
960 return Err((code, msg).into_response());
961 }
962 }
963 };
964
965 let client = AuthedClient::new(
966 &adapter_client,
967 user.clone(),
968 peer_addr,
969 active_connection_counter.clone(),
970 helm_chart_version,
971 |session| {
972 session
973 .vars_mut()
974 .set_default(WELCOME_MESSAGE.name(), VarInput::Flat(&false.format()))
975 .expect("known to exist")
976 },
977 options,
978 SYSTEM_TIME.clone(),
979 )
980 .await
981 .map_err(|e| {
982 let status = match e {
983 AdapterError::UserSessionsDisallowed | AdapterError::NetworkPolicyDenied(_) => {
984 StatusCode::FORBIDDEN
985 }
986 _ => StatusCode::INTERNAL_SERVER_ERROR,
987 };
988 (status, Json(SqlError::from(e))).into_response()
989 })?;
990
991 Ok(client)
992 }
993}
994
995#[derive(Debug, Clone, Default)]
1004pub(crate) struct WwwAuthenticateChallenges {
1005 pub include_basic: bool,
1007 pub bearer_resource_metadata: Option<String>,
1012 pub bearer_scope: Option<&'static str>,
1017}
1018
1019#[derive(Debug, Error)]
1020pub(crate) enum AuthError {
1021 #[error("role dissallowed")]
1022 RoleDisallowed(String),
1023 #[error("{0}")]
1024 Frontegg(#[from] FronteggError),
1025 #[error("missing authorization header")]
1026 MissingHttpAuthentication {
1027 challenges: WwwAuthenticateChallenges,
1028 },
1029 #[error("{0}")]
1030 MismatchedUser(String),
1031 #[error("session expired")]
1032 SessionExpired,
1033 #[error("failed to update session")]
1034 FailedToUpdateSession,
1035 #[error("invalid credentials")]
1036 InvalidCredentials,
1037 #[error("{0}")]
1039 OidcFailed(String),
1040}
1041
1042impl IntoResponse for AuthError {
1043 fn into_response(self) -> Response {
1044 warn!("HTTP request failed authentication: {}", self);
1045 let mut headers = HeaderMap::new();
1046 let body = match &self {
1051 AuthError::MissingHttpAuthentication { challenges } => {
1057 if let Some(resource_metadata) = &challenges.bearer_resource_metadata {
1058 let value = match &challenges.bearer_scope {
1064 Some(scope) => format!(
1065 "Bearer scope=\"{scope}\", resource_metadata=\"{resource_metadata}\"",
1066 ),
1067 None => format!("Bearer resource_metadata=\"{resource_metadata}\""),
1068 };
1069 match HeaderValue::from_str(&value) {
1070 Ok(v) => {
1071 headers.append(http::header::WWW_AUTHENTICATE, v);
1072 }
1073 Err(e) => {
1074 warn!(
1075 "skipping Bearer WWW-Authenticate challenge: invalid header \
1076 value derived from resource_metadata={resource_metadata:?}: {e}",
1077 );
1078 }
1079 }
1080 }
1081 if challenges.include_basic {
1082 headers.append(
1083 http::header::WWW_AUTHENTICATE,
1084 HeaderValue::from_static("Basic realm=Materialize"),
1085 );
1086 }
1087 "unauthorized".to_string()
1088 }
1089 AuthError::OidcFailed(message) => message.clone(),
1090 _ => "unauthorized".to_string(),
1091 };
1092 (StatusCode::UNAUTHORIZED, headers, body).into_response()
1093 }
1094}
1095
1096pub async fn handle_login(
1098 session: Option<Extension<TowerSession>>,
1099 Extension(adapter_client_rx): Extension<Delayed<Client>>,
1100 Json(LoginCredentials { username, password }): Json<LoginCredentials>,
1101) -> impl IntoResponse {
1102 let Ok(adapter_client) = adapter_client_rx.clone().await else {
1107 return StatusCode::INTERNAL_SERVER_ERROR;
1108 };
1109 let authenticated = match adapter_client.authenticate(&username, &password).await {
1110 Ok(authenticated) => authenticated,
1111 Err(err) => {
1112 warn!(?err, "HTTP login failed authentication");
1113 return StatusCode::UNAUTHORIZED;
1114 }
1115 };
1116 let session_data = TowerSessionData {
1118 username,
1119 created_at: SystemTime::now(),
1120 last_activity: SystemTime::now(),
1121 authenticated,
1122 authenticator_kind: mz_auth::AuthenticatorKind::Password,
1123 };
1124 let session = session.and_then(|Extension(session)| Some(session));
1126 let Some(session) = session else {
1127 return StatusCode::INTERNAL_SERVER_ERROR;
1128 };
1129 match session.insert("data", &session_data).await {
1130 Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
1131 Ok(_) => StatusCode::OK,
1132 }
1133}
1134
1135pub async fn handle_logout(session: Option<Extension<TowerSession>>) -> impl IntoResponse {
1137 let session = session.and_then(|Extension(session)| Some(session));
1138 let Some(session) = session else {
1139 return StatusCode::INTERNAL_SERVER_ERROR;
1140 };
1141 match session.delete().await {
1143 Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
1144 Ok(_) => StatusCode::OK,
1145 }
1146}
1147
1148async fn http_auth(
1150 mut req: Request,
1151 next: Next,
1152 tls_enabled: bool,
1153 authenticator_kind: listeners::AuthenticatorKind,
1154 frontegg: Option<mz_frontegg_auth::Authenticator>,
1155 oidc_rx: Delayed<mz_authenticator::GenericOidcAuthenticator>,
1156 adapter_client_rx: Delayed<Client>,
1157) -> Result<impl IntoResponse, AuthError> {
1158 let creds = if let Some(basic) = req.headers().typed_get::<Authorization<Basic>>() {
1159 Some(Credentials::Password {
1160 username: basic.username().to_owned(),
1161 password: Password(basic.password().to_owned()),
1162 })
1163 } else if let Some(bearer) = req.headers().typed_get::<Authorization<Bearer>>() {
1164 Some(Credentials::Token {
1165 token: bearer.token().to_owned(),
1166 })
1167 } else {
1168 None
1169 };
1170
1171 if creds.is_none()
1175 && let Some((session, session_data)) =
1176 maybe_get_authenticated_session(req.extensions().get::<TowerSession>()).await
1177 {
1178 let user = ensure_session_unexpired(session, session_data).await?;
1179 req.extensions_mut().insert(user);
1182 return Ok(next.run(req).await);
1183 }
1184
1185 let conn_protocol = req.extensions().get::<ConnProtocol>().unwrap();
1189 match (tls_enabled, &conn_protocol) {
1190 (false, ConnProtocol::Http) => {}
1191 (false, ConnProtocol::Https { .. }) => unreachable!(),
1192 (true, ConnProtocol::Http) => {
1193 let mut parts = req.uri().clone().into_parts();
1194 parts.scheme = Some(Scheme::HTTPS);
1195 return Ok(Redirect::permanent(
1196 &Uri::from_parts(parts)
1197 .expect("it was already a URI, just changed the scheme")
1198 .to_string(),
1199 )
1200 .into_response());
1201 }
1202 (true, ConnProtocol::Https { .. }) => {}
1203 }
1204 if req.extensions().get::<AuthedUser>().is_some() {
1206 return Ok(next.run(req).await);
1207 }
1208
1209 let path = req.uri().path();
1210 let oauth_config = req
1218 .extensions()
1219 .get::<oauth_metadata::McpOAuthConfig>()
1220 .cloned();
1221 let include_basic = path == "/"
1222 || PROFILING_API_ENDPOINTS
1223 .iter()
1224 .any(|prefix| path.starts_with(prefix))
1225 || oauth_config.is_some();
1226 let (bearer_resource_metadata, bearer_scope) = if let Some(config) = &oauth_config
1227 && config.discovery.is_enabled()
1228 {
1229 (
1230 oauth_metadata::metadata_url(&req, config.http_host_name.as_deref()),
1231 Some(config.scope()),
1232 )
1233 } else {
1234 (None, None)
1235 };
1236 let challenges = WwwAuthenticateChallenges {
1237 include_basic,
1238 bearer_resource_metadata,
1239 bearer_scope,
1240 };
1241 let authenticator = get_authenticator(
1242 authenticator_kind,
1243 creds.as_ref(),
1244 frontegg,
1245 &oidc_rx,
1246 &adapter_client_rx,
1247 )
1248 .await;
1249
1250 let group_claim = if matches!(authenticator, Authenticator::Frontegg(_)) {
1256 Some(group_claim_for(&adapter_client_rx).await)
1257 } else {
1258 None
1259 };
1260 let user = auth(&authenticator, creds, &challenges, group_claim.as_deref()).await?;
1261
1262 req.extensions_mut().insert(user);
1265
1266 Ok(next.run(req).await)
1268}
1269
1270async fn http_authz(req: Request, next: Next) -> Result<impl IntoResponse, AuthError> {
1279 match (
1280 req.extensions().get::<AuthedUser>(),
1281 req.extensions().get::<RouteAllowedRoles>().copied(),
1282 ) {
1283 (Some(user), Some(RouteAllowedRoles(allowed_roles))) => {
1284 check_role_allowed(&user.name, allowed_roles)?;
1285 Ok(next.run(req).await)
1286 }
1287 _ => {
1288 warn!("http_authz missing AuthedUser or RouteAllowedRoles extension; denying request");
1289 Err(AuthError::RoleDisallowed("<unknown>".to_string()))
1290 }
1291 }
1292}
1293
1294async fn init_ws(
1295 WsState {
1296 frontegg,
1297 oidc_rx,
1298 authenticator_kind,
1299 adapter_client_rx,
1300 active_connection_counter,
1301 helm_chart_version,
1302 allowed_roles,
1303 }: WsState,
1304 existing_user: Option<ExistingUser>,
1305 peer_addr: IpAddr,
1306 ws: &mut WebSocket,
1307) -> Result<AuthedClient, anyhow::Error> {
1308 let ws_auth: WebSocketAuth = loop {
1311 let init_msg = ws.recv().await.ok_or_else(|| anyhow::anyhow!("closed"))??;
1312 match init_msg {
1313 Message::Text(data) => break serde_json::from_str(&data)?,
1314 Message::Binary(data) => break serde_json::from_slice(&data)?,
1315 Message::Ping(_) => {
1317 continue;
1318 }
1319 Message::Pong(_) => {
1320 continue;
1321 }
1322 Message::Close(_) => {
1323 anyhow::bail!("closed");
1324 }
1325 }
1326 };
1327
1328 let (creds, options) = match ws_auth {
1331 WebSocketAuth::Basic {
1332 user,
1333 password,
1334 options,
1335 } => {
1336 let creds = Credentials::Password {
1337 username: user,
1338 password,
1339 };
1340 (Some(creds), options)
1341 }
1342 WebSocketAuth::Bearer { token, options } => {
1343 let creds = Credentials::Token { token };
1344 (Some(creds), options)
1345 }
1346 WebSocketAuth::OptionsOnly { options } => (None, options),
1347 };
1348
1349 let user = match (existing_user, creds) {
1350 (Some(ExistingUser::XMaterializeUserHeader(_)), Some(_creds)) => {
1351 warn!("Unexpected bearer or basic auth provided when using user header");
1352 anyhow::bail!("unexpected")
1353 }
1354 (Some(ExistingUser::Session(user)), None) => user,
1355 (Some(ExistingUser::XMaterializeUserHeader(user)), None) => user,
1356 (_, Some(creds)) => {
1357 let authenticator = get_authenticator(
1358 authenticator_kind,
1359 Some(&creds),
1360 frontegg,
1361 &oidc_rx,
1362 &adapter_client_rx,
1363 )
1364 .await;
1365 let no_challenges = WwwAuthenticateChallenges::default();
1369 let group_claim = if matches!(authenticator, Authenticator::Frontegg(_)) {
1372 Some(group_claim_for(&adapter_client_rx).await)
1373 } else {
1374 None
1375 };
1376 let user = auth(
1377 &authenticator,
1378 Some(creds),
1379 &no_challenges,
1380 group_claim.as_deref(),
1381 )
1382 .await?;
1383 user
1384 }
1385 (None, None) => anyhow::bail!("expected auth information"),
1386 };
1387
1388 check_role_allowed(&user.name, allowed_roles)?;
1393
1394 let client = AuthedClient::new(
1395 &adapter_client_rx.clone().await?,
1396 user,
1397 peer_addr,
1398 active_connection_counter.clone(),
1399 helm_chart_version.clone(),
1400 |_session| (),
1401 options,
1402 SYSTEM_TIME.clone(),
1403 )
1404 .await?;
1405
1406 Ok(client)
1407}
1408
1409enum Credentials {
1410 Password {
1411 username: String,
1412 password: Password,
1413 },
1414 Token {
1415 token: String,
1416 },
1417}
1418
1419async fn get_authenticator(
1420 kind: listeners::AuthenticatorKind,
1421 creds: Option<&Credentials>,
1422 frontegg: Option<mz_frontegg_auth::Authenticator>,
1423 oidc_rx: &Delayed<mz_authenticator::GenericOidcAuthenticator>,
1424 adapter_client_rx: &Delayed<Client>,
1425) -> Authenticator {
1426 match kind {
1427 listeners::AuthenticatorKind::Frontegg => Authenticator::Frontegg(frontegg.expect(
1428 "Frontegg authenticator should exist with listeners::AuthenticatorKind::Frontegg",
1429 )),
1430 listeners::AuthenticatorKind::Password | listeners::AuthenticatorKind::Sasl => {
1431 let client = adapter_client_rx.clone().await.expect("sender not dropped");
1432 Authenticator::Password(client)
1433 }
1434 listeners::AuthenticatorKind::Oidc => match creds {
1435 Some(Credentials::Password { .. }) => {
1437 let client = adapter_client_rx.clone().await.expect("sender not dropped");
1438 Authenticator::Password(client)
1439 }
1440 _ => Authenticator::Oidc(oidc_rx.clone().await.expect("sender not dropped")),
1441 },
1442 listeners::AuthenticatorKind::None => Authenticator::None,
1443 }
1444}
1445
1446pub(crate) async fn maybe_get_authenticated_session(
1450 session: Option<&TowerSession>,
1451) -> Option<(&TowerSession, TowerSessionData)> {
1452 if let Some(session) = session {
1453 if let Ok(Some(session_data)) = session.get::<TowerSessionData>("data").await {
1454 return Some((session, session_data));
1455 }
1456 }
1457 None
1458}
1459
1460pub(crate) async fn ensure_session_unexpired(
1463 session: &TowerSession,
1464 session_data: TowerSessionData,
1465) -> Result<AuthedUser, AuthError> {
1466 if session_data
1467 .last_activity
1468 .elapsed()
1469 .unwrap_or(Duration::MAX)
1470 > SESSION_DURATION
1471 {
1472 let _ = session.delete().await;
1473 return Err(AuthError::SessionExpired);
1474 }
1475 let mut updated_data = session_data.clone();
1476 updated_data.last_activity = SystemTime::now();
1477 session
1478 .insert("data", &updated_data)
1479 .await
1480 .map_err(|_| AuthError::FailedToUpdateSession)?;
1481
1482 Ok(AuthedUser {
1483 name: session_data.username,
1484 external_metadata_rx: None,
1485 authenticated: session_data.authenticated,
1486 authenticator_kind: session_data.authenticator_kind,
1487 groups: None,
1488 })
1489}
1490
1491async fn auth(
1492 authenticator: &Authenticator,
1493 creds: Option<Credentials>,
1494 challenges: &WwwAuthenticateChallenges,
1495 group_claim: Option<&str>,
1496) -> Result<AuthedUser, AuthError> {
1497 let (name, external_metadata_rx, authenticated, groups) = match authenticator {
1498 Authenticator::Frontegg(frontegg) => match creds {
1499 Some(Credentials::Password { username, password }) => {
1500 let (auth_session, authenticated) = frontegg
1501 .authenticate(&username, password.as_str(), group_claim)
1502 .await?;
1503 let name = auth_session.user().into();
1504 let groups = auth_session.groups();
1505 let external_metadata_rx = Some(auth_session.external_metadata_rx());
1506 (name, external_metadata_rx, authenticated, groups)
1507 }
1508 Some(Credentials::Token { token }) => {
1509 let (claims, authenticated) =
1510 frontegg.validate_access_token(&token, None, group_claim)?;
1511 let (_, external_metadata_rx) = watch::channel(ExternalUserMetadata {
1512 user_id: claims.user_id,
1513 admin: claims.is_admin,
1514 });
1515 (
1516 claims.user,
1517 Some(external_metadata_rx),
1518 authenticated,
1519 claims.groups,
1520 )
1521 }
1522 None => {
1523 return Err(AuthError::MissingHttpAuthentication {
1524 challenges: challenges.clone(),
1525 });
1526 }
1527 },
1528 Authenticator::Password(adapter_client) => match creds {
1529 Some(Credentials::Password { username, password }) => {
1530 let authenticated = adapter_client
1531 .authenticate(&username, &password)
1532 .await
1533 .map_err(|_| AuthError::InvalidCredentials)?;
1534 (username, None, authenticated, None)
1535 }
1536 _ => {
1537 return Err(AuthError::MissingHttpAuthentication {
1538 challenges: challenges.clone(),
1539 });
1540 }
1541 },
1542 Authenticator::Sasl(_) => {
1543 return Err(AuthError::MissingHttpAuthentication {
1547 challenges: challenges.clone(),
1548 });
1549 }
1550 Authenticator::Oidc(oidc) => match creds {
1551 Some(Credentials::Token { token }) => {
1552 let (mut claims, authenticated) = oidc
1553 .authenticate(&token, None)
1554 .await
1555 .map_err(|e| AuthError::OidcFailed(e.to_string()))?;
1556 let name = std::mem::take(&mut claims.user);
1557 let groups = claims.groups.take();
1558 (name, None, authenticated, groups)
1559 }
1560 _ => {
1561 return Err(AuthError::MissingHttpAuthentication {
1562 challenges: challenges.clone(),
1563 });
1564 }
1565 },
1566 Authenticator::None => {
1567 let name = match creds {
1571 Some(Credentials::Password { username, .. }) => username,
1572 _ => HTTP_DEFAULT_USER.name.to_owned(),
1573 };
1574 (name, None, Authenticated, None)
1575 }
1576 };
1577
1578 Ok(AuthedUser {
1579 name,
1580 external_metadata_rx,
1581 authenticated,
1582 authenticator_kind: authenticator.kind(),
1583 groups,
1584 })
1585}
1586
1587fn check_role_allowed(name: &str, allowed_roles: AllowedRoles) -> Result<(), AuthError> {
1589 let is_internal_user = INTERNAL_USER_NAMES.contains(name);
1590 let is_reserved_user = mz_adapter::catalog::is_reserved_role_name(name);
1592 let role_allowed = match allowed_roles {
1593 AllowedRoles::Normal => !is_reserved_user,
1594 AllowedRoles::Internal => is_internal_user,
1595 AllowedRoles::NormalAndInternal => !is_reserved_user || is_internal_user,
1596 };
1597 if role_allowed {
1598 Ok(())
1599 } else {
1600 Err(AuthError::RoleDisallowed(name.to_owned()))
1601 }
1602}
1603
1604trait DefaultLayers {
1607 fn apply_default_layers(self, source: &'static str, metrics: Metrics) -> Self;
1608}
1609
1610impl DefaultLayers for Router {
1611 fn apply_default_layers(self, source: &'static str, metrics: Metrics) -> Self {
1612 self.layer(DefaultBodyLimit::max(MAX_REQUEST_SIZE))
1613 .layer(metrics::PrometheusLayer::new(source, metrics))
1614 }
1615}
1616
1617async fn handle_load_error(error: tower::BoxError) -> impl IntoResponse {
1624 if error.is::<tower::load_shed::error::Overloaded>() {
1625 return (
1626 StatusCode::TOO_MANY_REQUESTS,
1627 Cow::from("too many requests, try again later"),
1628 );
1629 }
1630
1631 (
1634 StatusCode::INTERNAL_SERVER_ERROR,
1635 Cow::from(format!("Unhandled internal error: {}", error)),
1636 )
1637}
1638
1639#[derive(Debug, Deserialize, Serialize, PartialEq)]
1640pub struct LoginCredentials {
1641 username: String,
1642 password: Password,
1643}
1644
1645#[derive(Debug, Clone, Serialize, Deserialize)]
1646pub struct TowerSessionData {
1647 username: String,
1648 created_at: SystemTime,
1649 last_activity: SystemTime,
1650 authenticated: Authenticated,
1651 authenticator_kind: mz_auth::AuthenticatorKind,
1652}
1653
1654#[cfg(test)]
1655mod tests {
1656 use super::{AllowedRoles, check_role_allowed};
1657
1658 #[mz_ore::test]
1659 fn test_check_role_allowed() {
1660 assert!(check_role_allowed("mz_system", AllowedRoles::Internal).is_ok());
1662 assert!(check_role_allowed("mz_system", AllowedRoles::NormalAndInternal).is_ok());
1663 assert!(check_role_allowed("mz_system", AllowedRoles::Normal).is_err());
1664
1665 assert!(check_role_allowed("mz_support", AllowedRoles::Internal).is_ok());
1667 assert!(check_role_allowed("mz_support", AllowedRoles::NormalAndInternal).is_ok());
1668 assert!(check_role_allowed("mz_support", AllowedRoles::Normal).is_err());
1669
1670 assert!(check_role_allowed("mz_analytics", AllowedRoles::Internal).is_ok());
1672 assert!(check_role_allowed("mz_analytics", AllowedRoles::NormalAndInternal).is_ok());
1673 assert!(check_role_allowed("mz_analytics", AllowedRoles::Normal).is_err());
1674
1675 assert!(check_role_allowed("materialize", AllowedRoles::Internal).is_err());
1677 assert!(check_role_allowed("materialize", AllowedRoles::NormalAndInternal).is_ok());
1678 assert!(check_role_allowed("materialize", AllowedRoles::Normal).is_ok());
1679
1680 assert!(check_role_allowed("anonymous_http_user", AllowedRoles::Internal).is_err());
1682 assert!(check_role_allowed("anonymous_http_user", AllowedRoles::NormalAndInternal).is_ok());
1683 assert!(check_role_allowed("anonymous_http_user", AllowedRoles::Normal).is_ok());
1684
1685 assert!(check_role_allowed("alex", AllowedRoles::Internal).is_err());
1687 assert!(check_role_allowed("alex", AllowedRoles::NormalAndInternal).is_ok());
1688 assert!(check_role_allowed("alex", AllowedRoles::Normal).is_ok());
1689
1690 assert!(check_role_allowed("external_asdf", AllowedRoles::Internal).is_err());
1692 assert!(check_role_allowed("external_asdf", AllowedRoles::NormalAndInternal).is_err());
1693 assert!(check_role_allowed("external_asdf", AllowedRoles::Normal).is_err());
1694
1695 assert!(check_role_allowed("pg_somebody", AllowedRoles::Internal).is_err());
1697 assert!(check_role_allowed("pg_somebody", AllowedRoles::NormalAndInternal).is_err());
1698 assert!(check_role_allowed("pg_somebody", AllowedRoles::Normal).is_err());
1699
1700 assert!(check_role_allowed("mz_unknown", AllowedRoles::Internal).is_err());
1702 assert!(check_role_allowed("mz_unknown", AllowedRoles::NormalAndInternal).is_err());
1703 assert!(check_role_allowed("mz_unknown", AllowedRoles::Normal).is_err());
1704
1705 assert!(check_role_allowed("PUBLIC", AllowedRoles::Internal).is_err());
1707 assert!(check_role_allowed("PUBLIC", AllowedRoles::NormalAndInternal).is_err());
1708 assert!(check_role_allowed("PUBLIC", AllowedRoles::Normal).is_err());
1709 }
1710}