use nest_rs_core::AppBuilder;
use nest_rs_core::layer_chain::ResolvedLayer;
use nest_rs_graphql::FallbackOperationGuard;
use nest_rs_http::{GlobalGuardsActive, HttpBootCheck, SelfMountGuardWrap};
use nest_rs_interceptors::InterceptorExt;
use poem::EndpointExt;
use crate::Guard;
use crate::dispatch::{GlobalPoolOperationGuard, denial_to_http_response};
use crate::registry::{GuardSpec, GuardSpecs, PipeSpec, PipeSpecs};
pub trait AppBuilderGuardsExt: Sized {
fn use_guards_global<I>(self, specs: I) -> Self
where
I: IntoIterator<Item = GuardSpec>;
}
impl AppBuilderGuardsExt for AppBuilder {
fn use_guards_global<I>(self, specs: I) -> Self
where
I: IntoIterator<Item = GuardSpec>,
{
let collected: Vec<GuardSpec> = specs.into_iter().collect();
validate_order_by_name(&collected);
let active = !collected.is_empty();
let builder = self
.provide(GuardSpecs(collected))
.provide(FallbackOperationGuard(GlobalPoolOperationGuard::factory))
.provide_meta(SelfMountGuardWrap::new(|container, endpoint| {
let chain = container
.get::<GuardSpecs>()
.map(|specs| specs.resolve_chain(container, "self-mount edge"))
.unwrap_or_default();
InterceptorExt::interceptor(endpoint, GuardsHttpFold { chain })
.map_to_response()
.boxed()
}))
.provide_meta(HttpBootCheck::new(|container| {
let Some(specs) = container.get::<GuardSpecs>() else {
return Ok(());
};
let missing: Vec<&str> = specs
.0
.iter()
.filter(|s| s.resolve(container).is_none())
.map(|s| s.name)
.collect();
if missing.is_empty() {
Ok(())
} else {
Err(format!(
"global guard(s) not resolvable from the container: {} — import the \
module that provides them; an unresolvable global guard would \
silently drop and leave every route unguarded",
missing.join(", "),
))
}
}));
if active {
builder.provide(GlobalGuardsActive)
} else {
builder
}
}
}
pub trait AppBuilderPipesExt: Sized {
fn use_pipes_global<I>(self, specs: I) -> Self
where
I: IntoIterator<Item = PipeSpec>;
}
impl AppBuilderPipesExt for AppBuilder {
fn use_pipes_global<I>(self, specs: I) -> Self
where
I: IntoIterator<Item = PipeSpec>,
{
self.provide(PipeSpecs(specs.into_iter().collect()))
.provide_meta(HttpBootCheck::new(|container| {
let Some(specs) = container.get::<PipeSpecs>() else {
return Ok(());
};
let missing: Vec<&str> = specs
.0
.iter()
.filter(|s| s.resolve(container).is_none())
.map(|s| s.name)
.collect();
if missing.is_empty() {
Ok(())
} else {
Err(format!(
"global pipe(s) not resolvable from the container: {} — import the \
module that provides them; an unresolvable global pipe would \
silently drop its edge validation",
missing.join(", "),
))
}
}))
}
}
struct GuardsHttpFold {
chain: Vec<ResolvedLayer<dyn Guard>>,
}
impl nest_rs_core::Layer for GuardsHttpFold {}
#[async_trait::async_trait]
impl nest_rs_interceptors::Interceptor for GuardsHttpFold {
async fn intercept(
&self,
mut req: poem::Request,
next: nest_rs_interceptors::Next<'_>,
) -> poem::Result<poem::Response> {
for entry in &self.chain {
if let Err(denial) = entry.layer.check_http(&mut req).await {
return Ok(denial_to_http_response(denial));
}
}
next.run(req).await
}
}
fn validate_order_by_name(specs: &[GuardSpec]) {
let mut saw_authz = false;
for s in specs {
let name = s.name.to_ascii_lowercase();
let is_authz = name.contains("authz") || name.contains("ability");
let is_authn = (name.contains("auth") && !is_authz) || name.contains("authn");
if saw_authz && is_authn {
tracing::warn!(
target: "nest_rs::layers",
"global guard order looks reversed — `{}` (looks like authn) follows a guard that looks like authz; authn should precede authz",
s.name,
);
}
if is_authz {
saw_authz = true;
}
}
}