Skip to main content

cratestack_axum/envelope_layer/
principal.rs

1//! [`PrincipalMapper`]: from a verified request to the `VerifiedPrincipal`
2//! the rate limiter and the idempotency layer key on (ADR 0006 ยง12).
3
4use std::borrow::Cow;
5
6use async_trait::async_trait;
7use cratestack_core::{CratestackError, VerifiedSigner};
8use http::Method;
9
10use super::resolver::ResolvedRoute;
11
12/// A request the envelope verified, as the [`PrincipalMapper`] sees it.
13///
14/// It has no public constructor: only the layer makes one, and only after
15/// [`super::ServerEnvelope::open_request`] succeeded. A mapper therefore
16/// cannot be handed an unverified signer, by a caller or by mistake:
17///
18/// ```compile_fail,E0451
19/// use cratestack_axum::envelope_layer::VerifiedRequest;
20/// let _ = VerifiedRequest { signer: todo!(), method: todo!(), route: todo!() };
21/// ```
22///
23/// ```compile_fail,E0624
24/// use cratestack_axum::envelope_layer::VerifiedRequest;
25/// let _ = VerifiedRequest::new(todo!(), todo!(), todo!());
26/// ```
27///
28/// The type itself is public, so a mapper can name it (this one compiles,
29/// which shows the two above fail for the privacy and nothing else):
30///
31/// ```
32/// use cratestack_axum::envelope_layer::VerifiedRequest;
33/// fn mapper(verified: &VerifiedRequest<'_>) -> String {
34///     format!("{:?}", verified.signer().thumbprint())
35/// }
36/// ```
37#[derive(Debug)]
38pub struct VerifiedRequest<'a> {
39    signer: &'a VerifiedSigner,
40    method: &'a Method,
41    route: &'a ResolvedRoute,
42}
43
44impl<'a> VerifiedRequest<'a> {
45    pub(super) fn new(
46        signer: &'a VerifiedSigner,
47        method: &'a Method,
48        route: &'a ResolvedRoute,
49    ) -> Self {
50        Self {
51            signer,
52            method,
53            route,
54        }
55    }
56
57    /// The key that verified the request.
58    pub fn signer(&self) -> &'a VerifiedSigner {
59        self.signer
60    }
61
62    /// The request method.
63    pub fn method(&self) -> &'a Method {
64        self.method
65    }
66
67    /// The op it addressed.
68    pub fn route(&self) -> &'a ResolvedRoute {
69        self.route
70    }
71}
72
73/// Names the principal a verified request is charged to: the value of the
74/// `VerifiedPrincipal` extension the layer inserts, which the rate limiter's
75/// default key (`princ:` bucket, no cardinality budget) and the idempotency
76/// layer's default fingerprint (`princ:` namespace) use.
77///
78/// The default is [`ThumbprintPrincipal`]. Replace it to charge a signer to
79/// something coarser (its device's owner, its tenant), looked up
80/// asynchronously if need be. It only names a principal for rate limiting
81/// and idempotency; turning a signer into an *identity* for authorization
82/// is an `AuthProvider`'s job (the adapter of cratestack#1077), never this
83/// trait's (decision D2).
84///
85/// A synchronous mapper is a closure (second-review nit):
86///
87/// ```
88/// use cratestack_axum::envelope_layer::{EnvelopeLayerBuilder, VerifiedRequest};
89///
90/// fn by_kid(builder: EnvelopeLayerBuilder) -> EnvelopeLayerBuilder {
91///     builder.principal_mapper(|v: &VerifiedRequest<'_>| {
92///         Ok(format!("kid:{:x?}", v.signer().kid()))
93///     })
94/// }
95/// # let _ = by_kid;
96/// ```
97///
98/// (That one is for illustration only: a `kid` is 8 bytes and two keys can
99/// share one, which is why the default uses the full thumbprint.)
100///
101/// Async and fallible (API-review decision, 2026-09-26):
102///
103/// - `Err(CratestackError::Unauthorized(_))` refuses the request with the
104///   layer's coarse, **unsigned** `401` (a revoked device, say): the same
105///   answer as a failed verification, so the peer learns nothing more.
106/// - Any other `Err` (a lookup that failed) is a **sealed** `500`, since
107///   the request did verify; the detail is logged, never sent.
108///
109/// **What the layer enforces whatever this returns:** it is called only
110/// with a [`VerifiedRequest`], after verification. An empty principal is
111/// refused (a sealed `500`), since it would put every signer in one bucket
112/// and one idempotency namespace. A constant principal does the same thing
113/// and cannot be detected: choose a mapper that separates callers.
114#[async_trait]
115pub trait PrincipalMapper: Send + Sync + 'static {
116    /// The principal to insert for this request.
117    async fn principal(&self, verified: &VerifiedRequest<'_>) -> Result<String, CratestackError>;
118}
119
120/// The prefix, `cose:` by default, followed by the lowercase hex of the
121/// verifying key's full RFC 9679 thumbprint, never its 8-byte `kid` (two
122/// keys can share one).
123///
124/// The prefix is configurable (second-review nit) for an envelope that is
125/// not COSE, or to keep two services' principals apart in a shared store:
126///
127/// ```
128/// use cratestack_axum::envelope_layer::ThumbprintPrincipal;
129///
130/// let mapper = ThumbprintPrincipal::with_prefix("device:");
131/// # let _ = mapper;
132/// ```
133///
134/// Pick one no other source of `VerifiedPrincipal` in the application
135/// uses: the rate limiter and the idempotency layer see only the string,
136/// so two sources that can produce the same one share a bucket and a
137/// namespace.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct ThumbprintPrincipal {
140    prefix: Cow<'static, str>,
141}
142
143impl ThumbprintPrincipal {
144    /// The default prefix.
145    pub const DEFAULT_PREFIX: &'static str = "cose:";
146
147    /// `cose:<hex thumbprint>`.
148    pub const fn new() -> Self {
149        Self {
150            prefix: Cow::Borrowed(Self::DEFAULT_PREFIX),
151        }
152    }
153
154    /// `<prefix><hex thumbprint>`.
155    pub fn with_prefix(prefix: impl Into<Cow<'static, str>>) -> Self {
156        Self {
157            prefix: prefix.into(),
158        }
159    }
160
161    /// The prefix in use.
162    pub fn prefix(&self) -> &str {
163        &self.prefix
164    }
165}
166
167impl Default for ThumbprintPrincipal {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173#[async_trait]
174impl PrincipalMapper for ThumbprintPrincipal {
175    async fn principal(&self, verified: &VerifiedRequest<'_>) -> Result<String, CratestackError> {
176        let hex: String = verified
177            .signer
178            .thumbprint()
179            .iter()
180            .map(|byte| format!("{byte:02x}"))
181            .collect();
182        Ok(format!("{}{hex}", self.prefix))
183    }
184}
185
186/// A synchronous mapper is a closure.
187#[async_trait]
188impl<F> PrincipalMapper for F
189where
190    F: Fn(&VerifiedRequest<'_>) -> Result<String, CratestackError> + Send + Sync + 'static,
191{
192    async fn principal(&self, verified: &VerifiedRequest<'_>) -> Result<String, CratestackError> {
193        self(verified)
194    }
195}