116293519d
CI / Check (push) Has been cancelled
CI / Detect Changes (push) Has been cancelled
CI / Deploy Agent (push) Has been cancelled
CI / Deploy Dashboard (push) Has been cancelled
CI / Deploy Docs (push) Has been cancelled
CI / Deploy MCP (push) Has been cancelled
96 lines
2.6 KiB
Rust
96 lines
2.6 KiB
Rust
//! Axum extractor for the per-request `TenantContext`.
|
|
//!
|
|
//! Handlers consume it as a normal extractor argument:
|
|
//!
|
|
//! ```ignore
|
|
//! async fn list_findings(TenantCtx(ctx): TenantCtx) -> Json<...> {
|
|
//! let filter = compliance_core::db::tenant_filter(&ctx);
|
|
//! ...
|
|
//! }
|
|
//! ```
|
|
//!
|
|
//! The middleware ([`crate::auth::require_jwt_auth`]) is responsible for
|
|
//! inserting the context into the request extensions. If it's missing on
|
|
//! a route that uses this extractor, that's a bug in the wiring — we
|
|
//! return 401 so the caller sees an auth failure rather than a 500.
|
|
|
|
use axum::{
|
|
extract::FromRequestParts,
|
|
http::{request::Parts, StatusCode},
|
|
response::{IntoResponse, Response},
|
|
};
|
|
|
|
use crate::TenantContext;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct TenantCtx(pub TenantContext);
|
|
|
|
#[derive(Debug)]
|
|
pub struct TenantCtxRejection;
|
|
|
|
impl IntoResponse for TenantCtxRejection {
|
|
fn into_response(self) -> Response {
|
|
(
|
|
StatusCode::UNAUTHORIZED,
|
|
"Missing tenant context — request was not authenticated",
|
|
)
|
|
.into_response()
|
|
}
|
|
}
|
|
|
|
impl<S> FromRequestParts<S> for TenantCtx
|
|
where
|
|
S: Send + Sync,
|
|
{
|
|
type Rejection = TenantCtxRejection;
|
|
|
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
|
parts
|
|
.extensions
|
|
.get::<TenantContext>()
|
|
.cloned()
|
|
.map(TenantCtx)
|
|
.ok_or(TenantCtxRejection)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::expect_used, clippy::unwrap_used)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::TenantStatus;
|
|
use axum::http::Request;
|
|
|
|
fn ctx() -> TenantContext {
|
|
TenantContext {
|
|
tenant_id: "t-1".to_string(),
|
|
tenant_slug: "acme".to_string(),
|
|
org_roles: vec![],
|
|
products: vec![],
|
|
plan: "starter".to_string(),
|
|
status: TenantStatus::Active,
|
|
user_id: "u-1".to_string(),
|
|
user_name: None,
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn extracts_context_when_present() {
|
|
let mut req = Request::new(());
|
|
req.extensions_mut().insert(ctx());
|
|
let (mut parts, _) = req.into_parts();
|
|
let TenantCtx(found) = TenantCtx::from_request_parts(&mut parts, &())
|
|
.await
|
|
.expect("extractor should succeed");
|
|
assert_eq!(found.tenant_id, "t-1");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn rejects_when_missing() {
|
|
let req: Request<()> = Request::new(());
|
|
let (mut parts, _) = req.into_parts();
|
|
let err = TenantCtx::from_request_parts(&mut parts, &()).await;
|
|
assert!(err.is_err());
|
|
}
|
|
}
|