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}