Skip to main content

cratestack_axum/envelope_layer/
builder.rs

1//! [`EnvelopeLayerBuilder`]: nothing is checked until `build` (in
2//! `build.rs`), and the calls commute: the order they are made in never
3//! changes the layer.
4
5use std::sync::Arc;
6
7use cratestack_core::{AcceptedContracts, RouteTransportDescriptor};
8
9use super::mode::{EnvelopeMode, EnvelopePolicy};
10use super::principal::PrincipalMapper;
11use super::resolver::BindingResolver;
12use super::seal_policy::ResponseSealPolicy;
13use super::server_envelope::ServerEnvelope;
14
15pub(super) enum Transport {
16    None,
17    Rest(&'static [RouteTransportDescriptor]),
18    Rpc,
19    Custom(Box<dyn BindingResolver>),
20}
21
22/// Configuration for an [`super::EnvelopeLayer`]. Nothing is checked until
23/// [`build`](Self::build), and the calls may come in any order.
24pub struct EnvelopeLayerBuilder {
25    pub(super) envelope: Arc<dyn ServerEnvelope>,
26    pub(super) audience: String,
27    pub(super) contracts: AcceptedContracts,
28    pub(super) max_contract_trials: usize,
29    pub(super) policy: Option<Box<dyn EnvelopePolicy>>,
30    pub(super) transport: Transport,
31    /// The prefix given to `rest(..)` / `rpc(..)`.
32    pub(super) transport_prefix: Option<String>,
33    /// The prefix given to `mount_prefix(..)`, which wins over the
34    /// transport's in any order (second-review nit).
35    pub(super) mount_prefix: Option<String>,
36    pub(super) unresolved_mode: Option<EnvelopeMode>,
37    pub(super) allow_unresolved: Vec<String>,
38    pub(super) principal: Box<dyn PrincipalMapper>,
39    pub(super) seal_policy: Box<dyn ResponseSealPolicy>,
40    pub(super) max_body_bytes: usize,
41    /// `payload_media_types(..)`; CBOR alone by default.
42    pub(super) payload_request: Vec<String>,
43    pub(super) payload_response: Vec<String>,
44}
45
46impl EnvelopeLayerBuilder {
47    /// Which mode each op runs in: an [`super::EnvelopeMode`] for all of
48    /// them, or a closure per op. Required (decision D9).
49    pub fn policy(mut self, policy: impl EnvelopePolicy) -> Self {
50        self.policy = Some(Box::new(policy));
51        self
52    }
53
54    /// A REST router mounted at `prefix` (`""` at the root), over the
55    /// generated `cratestack_schema::axum::ROUTE_TRANSPORTS`, which must not
56    /// be empty.
57    pub fn rest(mut self, prefix: &str, routes: &'static [RouteTransportDescriptor]) -> Self {
58        self.transport = Transport::Rest(routes);
59        self.transport_prefix = Some(prefix.to_owned());
60        self
61    }
62
63    /// A `transport rpc` router mounted at `prefix` (`""` at the root).
64    pub fn rpc(mut self, prefix: &str) -> Self {
65        self.transport = Transport::Rpc;
66        self.transport_prefix = Some(prefix.to_owned());
67        self
68    }
69
70    /// A custom [`BindingResolver`], instead of [`rest`](Self::rest) or
71    /// [`rpc`](Self::rpc).
72    pub fn binding_resolver(mut self, resolver: impl BindingResolver) -> Self {
73        self.transport = Transport::Custom(Box::new(resolver));
74        self
75    }
76
77    /// Where the router is mounted (`"/api"` for `Router::nest("/api",
78    /// router)`). It wins over the prefix given to [`rest`](Self::rest) or
79    /// [`rpc`](Self::rpc), whichever order the calls come in: the
80    /// generated `envelope_layer` passes the root to those, so this is how
81    /// its layer is told about a mount. Also what
82    /// [`allow_unresolved`](Self::allow_unresolved) templates are relative
83    /// to.
84    pub fn mount_prefix(mut self, prefix: &str) -> Self {
85        self.mount_prefix = Some(prefix.to_owned());
86        self
87    }
88
89    /// Route templates (as given to `Router::route`, relative to the mount
90    /// prefix, e.g. `"/health"`) that the router may match without the
91    /// resolver knowing them: hand-written routes merged into the generated
92    /// router before the layer. Plain traffic to them passes through
93    /// unsigned. Any other matched route the resolver cannot bind fails
94    /// closed with a `500` when the policy's `unresolved_mode` is
95    /// `Required` (decision S2), which is what a wrong or missing mount
96    /// prefix looks like. A COSE body to an allow-listed route is still
97    /// refused (`415`).
98    pub fn allow_unresolved<I, T>(mut self, templates: I) -> Self
99    where
100        I: IntoIterator<Item = T>,
101        T: Into<String>,
102    {
103        self.allow_unresolved
104            .extend(templates.into_iter().map(Into::into));
105        self
106    }
107
108    /// Replace the policy's
109    /// [`unresolved_mode`](super::EnvelopePolicy::unresolved_mode) with
110    /// `mode`, whatever the policy answers (second-review nit). An explicit
111    /// opt-in, mostly for a closure policy, whose `unresolved_mode` is
112    /// always `Required`: with `Optional` or `Off` here, a matched route the
113    /// resolver cannot bind passes through plain (warned about once per
114    /// process) instead of failing closed with the `500`, and so does
115    /// every route when the mount prefix is wrong. It also lets through a
116    /// method the schema does not generate on a generated path (normally
117    /// the layer's own `405`), so a hand-written handler for that method
118    /// runs unsigned. Prefer listing
119    /// hand-written routes with [`allow_unresolved`](Self::allow_unresolved),
120    /// which keeps a misconfiguration loud. The ops' own modes are
121    /// unchanged; a `/rpc/batch` whose frames cannot be read uses this
122    /// mode too.
123    pub fn unresolved_mode(mut self, mode: EnvelopeMode) -> Self {
124        self.unresolved_mode = Some(mode);
125        self
126    }
127
128    /// Replace [`super::ThumbprintPrincipal`] (`cose:<hex thumbprint>`;
129    /// `ThumbprintPrincipal::with_prefix(..)` keeps the thumbprint and
130    /// changes the prefix).
131    pub fn principal_mapper(mut self, mapper: impl PrincipalMapper) -> Self {
132        self.principal = Box::new(mapper);
133        self
134    }
135
136    /// Replace [`super::AcceptNamesEnvelope`] (decision D10).
137    pub fn response_seal_policy(mut self, policy: impl ResponseSealPolicy) -> Self {
138        self.seal_policy = Box::new(policy);
139        self
140    }
141
142    /// How many accepted digests the layer tries for a request that names
143    /// none (no `Cratestack-Contract` header: a hand-built client), newest
144    /// first. Default [`super::DEFAULT_MAX_CONTRACT_TRIALS`]; at least 1.
145    /// A request that does name one is verified under it alone, so an
146    /// up-to-date generated client costs one verification however long an
147    /// op's accepted list grows, and a forged request without the header
148    /// costs one parse, one key resolution and at most this many signature
149    /// verifications (the COSE envelope's `open_request_any`; a custom
150    /// [`ServerEnvelope`](super::ServerEnvelope) that keeps the default
151    /// pays the parse and the resolution per trial). A failed trial records
152    /// no nonce.
153    pub fn max_contract_trials(mut self, trials: usize) -> Self {
154        self.max_contract_trials = trials;
155        self
156    }
157
158    /// The largest request body the layer buffers. Default
159    /// [`super::DEFAULT_MAX_BODY_BYTES`]; keep it a little above the
160    /// router's own body limit, which applies to the payload after opening.
161    pub fn max_body_bytes(mut self, limit: usize) -> Self {
162        self.max_body_bytes = limit;
163        self
164    }
165}