Files
compliance-scanner-agent/compliance-core/src/tenant_ctx.rs
T
sharang 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
M7.1 smoke harness: lift auth to compliance-core + compliance-smoke service (#83)
2026-06-04 14:38:35 +00:00

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());
}
}