Skip to main content

cratestack_axum/envelope_layer/
resolver.rs

1//! [`BindingResolver`]: which generated op a request addresses, as the
2//! `route` and `path_params` of its binding (ADR 0006 ยง4). The built-in
3//! resolvers are in `resolver_rest` and `resolver_rpc`.
4
5use std::borrow::Cow;
6
7use http::Method;
8
9use super::resolver_payload::PayloadTypes;
10
11/// What a resolver sees of a request: the method, the raw path, the route
12/// template axum matched (`None` when no route matched, or under
13/// `nest_service`), and the matched path parameters, percent-decoded, in the
14/// order axum reports them (a parameterised mount prefix's first).
15#[derive(Debug, Clone, Copy)]
16pub struct RouteRequest<'a> {
17    method: &'a Method,
18    path: &'a str,
19    matched_path: Option<&'a str>,
20    path_params: &'a [(String, String)],
21}
22
23impl<'a> RouteRequest<'a> {
24    /// A request as the layer describes it. Public so a custom resolver can
25    /// be unit-tested; the layer builds its own from the request.
26    pub fn new(
27        method: &'a Method,
28        path: &'a str,
29        matched_path: Option<&'a str>,
30        path_params: &'a [(String, String)],
31    ) -> Self {
32        Self {
33            method,
34            path,
35            matched_path,
36            path_params,
37        }
38    }
39
40    /// The request method.
41    pub fn method(&self) -> &'a Method {
42        self.method
43    }
44
45    /// The raw request path, mount prefix included.
46    pub fn path(&self) -> &'a str {
47        self.path
48    }
49
50    /// The route template axum matched, mount prefix included.
51    pub fn matched_path(&self) -> Option<&'a str> {
52        self.matched_path
53    }
54
55    /// `(name, value)` pairs, decoded.
56    pub fn path_params(&self) -> &'a [(String, String)] {
57        self.path_params
58    }
59}
60
61/// A resolved op: the binding's `route` and `path_params`.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct ResolvedRoute {
64    route: Cow<'static, str>,
65    path_params: Vec<String>,
66    contract_key: Option<Cow<'static, str>>,
67    pub(super) payload_types: Option<PayloadTypes>,
68}
69
70/// The route an RPC subscription is bound under: `subscribe/<op id>`.
71const SUBSCRIBE_ROUTE_PREFIX: &str = "subscribe/";
72
73impl ResolvedRoute {
74    /// `route` as the client binds it: the RPC op id (`batch` for
75    /// `/rpc/batch`, `subscribe/<op id>` for a subscription), or the REST
76    /// route template as the schema declares it, without the mount prefix.
77    /// `path_params` in the order they are bound.
78    pub fn new(route: impl Into<Cow<'static, str>>, path_params: Vec<String>) -> Self {
79        Self {
80            route: route.into(),
81            path_params,
82            contract_key: None,
83            payload_types: None,
84        }
85    }
86
87    /// The op whose contract digest this route is bound under, when the
88    /// route a custom resolver binds (a versioned `/v1/...` path, say) is
89    /// not itself the op's key: the RPC `op_id`, or `"<METHOD> <template>"`
90    /// on REST, as the generated `ACCEPTED_CONTRACTS` spells it. Without
91    /// it the layer looks the route up as given (the op id, or the method
92    /// and template), and a route with no row is a `500` for a signed
93    /// request: the layer cannot tell which shape the request was
94    /// signed under.
95    #[must_use]
96    pub fn with_contract_key(mut self, key: impl Into<Cow<'static, str>>) -> Self {
97        self.contract_key = Some(key.into());
98        self
99    }
100
101    /// The key set by [`with_contract_key`](Self::with_contract_key).
102    pub fn contract_key(&self) -> Option<&str> {
103        self.contract_key.as_deref()
104    }
105
106    /// The route.
107    pub fn route(&self) -> &str {
108        &self.route
109    }
110
111    /// The path parameter values.
112    pub fn path_params(&self) -> &[String] {
113        &self.path_params
114    }
115
116    /// The op an [`super::EnvelopePolicy`] is asked about: the route, or for
117    /// a subscription the bare op id after `subscribe/` (decision B1).
118    pub fn op(&self) -> &str {
119        self.route
120            .strip_prefix(SUBSCRIBE_ROUTE_PREFIX)
121            .unwrap_or(&self.route)
122    }
123
124    /// Bound as `subscribe/<op id>`: an RPC subscription, which streams and
125    /// so can never answer a signed request (the layer refuses it with a
126    /// sealed `406` before the handler runs).
127    pub fn is_subscription(&self) -> bool {
128        self.route.starts_with(SUBSCRIBE_ROUTE_PREFIX)
129    }
130
131    /// Bound as `batch`: the whole `/rpc/batch` call (decision D11), whose
132    /// frames the layer reads to apply the policy to each (B1). No REST
133    /// template (they start with `/`) and no unary op id (the RPC resolver
134    /// never binds one without a `.`, B2) can be `batch`.
135    pub fn is_batch(&self) -> bool {
136        self.route == "batch"
137    }
138}
139
140/// A [`BindingResolver`]'s answer.
141#[derive(Debug, Clone, PartialEq, Eq)]
142#[non_exhaustive]
143pub enum Resolution {
144    /// A generated op: the request is opened, verified, or refused
145    /// according to the policy.
146    Op(ResolvedRoute),
147    /// A route the resolver recognises and knows is not an op, such as an
148    /// RPC op id that no generated op can have (`/rpc/%62atch`, decision
149    /// B2). A plain request passes through (the router answers it, a
150    /// `404`), a COSE one is refused with the unsigned `415`.
151    NotAnOp,
152    /// A generated route, but not for this method: the router answers
153    /// `405` with these methods in `Allow`. Passes through, unless the
154    /// policy's `unresolved_mode` is `Required` and the route is not
155    /// allow-listed: then the layer answers the `405` itself, so a
156    /// hand-written handler for another method on a generated path cannot
157    /// run unsigned (decision S2).
158    MethodNotAllowed(Vec<Method>),
159    /// A route the resolver does not know. With no matched route (a `404`)
160    /// it passes through; with one, under a `Required` `unresolved_mode`
161    /// and not allow-listed, the layer fails closed with a `500` (S2).
162    Unresolved,
163}
164
165/// Resolves the op a request addresses.
166///
167/// Use [`super::RestBindingResolver`] or [`super::RpcBindingResolver`]
168/// unless the router is mounted in a way they cannot see through.
169///
170/// **What the layer enforces whatever this returns:** it is called exactly
171/// once per request, and the request and the response are bound with the
172/// same [`ResolvedRoute`], so a resolver cannot make the two disagree. A
173/// wrong answer can only make verification fail (the client bound something
174/// else), fail closed under `Required` ([`Resolution::Unresolved`]), or, by
175/// returning [`Resolution::NotAnOp`] for a real op, let *plain* traffic to
176/// it through; it can never make an unverified envelope pass.
177pub trait BindingResolver: Send + Sync + 'static {
178    /// The op `request` addresses, if it is one.
179    fn resolve(&self, request: &RouteRequest<'_>) -> Resolution;
180}