Skip to main content

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}