nest_rs_guards/dispatch/operation_guard.rs
1//! [`GlobalPoolOperationGuard`] — the fallback `GraphqlOperationGuard`.
2//!
3//! `/graphql` is `EdgePosture::Exempt`: no guard runs at the HTTP edge, the
4//! per-operation seam is the only gate. An app normally registers its authz
5//! bridge there (`AppGraphqlGuard as dyn GraphqlOperationGuard`); when it
6//! does not, this fallback folds the **global guard pool** in-band so a
7//! forgotten bridge module never leaves GraphQL operations unguarded —
8//! the fail-secure net, not the full authz integration (it installs no
9//! ambient `Ability`; row scoping and masking still require the bridge).
10//!
11//! The GraphQL endpoint carries the [`Public`](nest_rs_core::Public) marker
12//! as request data, so an `AuthGuard` in the pool admits anonymous callers
13//! (resolver-level gates still apply) while a present bearer is verified —
14//! exactly once, here.
15
16use std::sync::Arc;
17
18use nest_rs_core::Container;
19use nest_rs_core::layer_chain::ResolvedLayer;
20use nest_rs_graphql::{BoxFuture, GraphqlOperationGuard};
21use poem::{Request, Response};
22
23use crate::Guard;
24use crate::dispatch::denial_convert::denial_to_http_response;
25use crate::registry::GuardSpecs;
26
27pub struct GlobalPoolOperationGuard {
28 chain: Vec<ResolvedLayer<dyn Guard>>,
29}
30
31impl GlobalPoolOperationGuard {
32 /// Resolve the global pool eagerly — the container is final at mount.
33 pub fn from_container(container: &Container) -> Self {
34 let chain = container
35 .get::<GuardSpecs>()
36 .map(|specs| specs.resolve_chain(container, "POST /graphql (operation)"))
37 .unwrap_or_default();
38 Self { chain }
39 }
40
41 /// The factory `use_guards_global` seeds as
42 /// [`FallbackOperationGuard`](nest_rs_graphql::FallbackOperationGuard).
43 pub fn factory(container: &Container) -> Arc<dyn GraphqlOperationGuard> {
44 Arc::new(Self::from_container(container))
45 }
46}
47
48impl GraphqlOperationGuard for GlobalPoolOperationGuard {
49 fn before<'a>(&'a self, req: &'a mut Request) -> BoxFuture<'a, Result<(), Response>> {
50 Box::pin(async move {
51 for entry in &self.chain {
52 if let Err(denial) = entry.layer.check_http(req).await {
53 tracing::warn!(
54 target: "nest_rs::layers",
55 guard = entry.name,
56 reason = denial.message(),
57 "graphql operation denied by the global guard pool",
58 );
59 return Err(denial_to_http_response(denial));
60 }
61 }
62 Ok(())
63 })
64 }
65
66 fn around<'a>(
67 &'a self,
68 _req: &'a Request,
69 inner: BoxFuture<'a, Response>,
70 ) -> BoxFuture<'a, Response> {
71 // Nothing ambient to install — that is the authz bridge's job.
72 inner
73 }
74}