M7.1 smoke harness: lift auth to compliance-core + compliance-smoke service (#83)
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
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
This commit was merged in pull request #83.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user