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}