Skip to main content

rustev_remote_http/
principal.rs

1//! The host-endpoint principal reference (spec 020, 3.6, I-3).
2//!
3//! Omitted by default. A host may enable it only for an endpoint it
4//! classifies [`EndpointOperator::HostOperated`], only over
5//! `rustev.remote/2`, with a mapping version and a declared scope. Per
6//! attempt, the host's [`PrincipalSource`] supplies the opaque reference
7//! (it may derive it from `CallContext`'s principal handle); Rustev never
8//! interprets, authenticates, authorizes or expands it. The reference is
9//! routing and isolation context only: not a credential, bearer token,
10//! capability, tenant grant, policy fact, input value or action permission,
11//! and not proof that the endpoint enforced isolation. It is sent in the
12//! identified request field and nowhere else; evidence holds only its
13//! domain-separated digest and mapping version.
14
15use std::fmt;
16use std::sync::Arc;
17
18use rustev_contract::remote::WirePrincipal;
19use serde::Serialize;
20
21/// Who operates the endpoint, as the host classifies it.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum EndpointOperator {
24    /// The default: an endpoint a third party operates. A principal
25    /// reference is never sent to it.
26    #[default]
27    ThirdParty,
28    /// An endpoint the host operates.
29    HostOperated,
30}
31
32/// Supplies the opaque principal reference of one attempt.
33pub trait PrincipalSource: Send + Sync {
34    /// The reference for the attempt whose `CallContext` carries
35    /// `principal_handle`, or `None` when there is none; the attempt is
36    /// then refused before any request byte is sent.
37    fn reference(&self, principal_handle: &[u8]) -> Option<Vec<u8>>;
38}
39
40/// Sends the principal handle itself as the reference. The handle is
41/// already opaque to Rustev; a host that wants another reference supplies
42/// its own [`PrincipalSource`].
43pub struct HandleAsReference;
44
45impl PrincipalSource for HandleAsReference {
46    fn reference(&self, principal_handle: &[u8]) -> Option<Vec<u8>> {
47        (!principal_handle.is_empty()).then(|| principal_handle.to_vec())
48    }
49}
50
51/// The principal wire posture of a client.
52#[derive(Clone, Default)]
53pub enum PrincipalPosture {
54    /// The default: no principal byte is ever sent.
55    #[default]
56    Omitted,
57    /// Send the reference in the identified request field.
58    Sent {
59        mapping_version: String,
60        scope: String,
61        source: Arc<dyn PrincipalSource>,
62    },
63}
64
65impl fmt::Debug for PrincipalPosture {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            PrincipalPosture::Omitted => f.write_str("Omitted"),
69            PrincipalPosture::Sent {
70                mapping_version,
71                scope,
72                ..
73            } => f
74                .debug_struct("Sent")
75                .field("mapping_version", mapping_version)
76                .field("scope", scope)
77                .finish_non_exhaustive(),
78        }
79    }
80}
81
82/// The principal posture as the adapter binding holds it: posture, mapping
83/// version and scope, never a reference. Changing any of them changes the
84/// binding identity; rotating the reference does not.
85#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
86#[serde(rename_all = "snake_case")]
87pub enum PrincipalWire {
88    #[default]
89    Omitted,
90    HostOperated {
91        mapping_version: String,
92        scope: String,
93    },
94}
95
96impl PrincipalWire {
97    pub fn is_omitted(&self) -> bool {
98        matches!(self, PrincipalWire::Omitted)
99    }
100}
101
102/// Why a principal configuration is refused at construction.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum PrincipalConfigError {
105    /// Principal forwarding to an endpoint the host does not operate.
106    ThirdPartyEndpoint,
107    /// The reference is a `rustev.remote/2` member.
108    NeedsProtocolV2,
109    /// A mapping version or scope that is empty, too long or not printable.
110    BadLabel,
111}
112
113impl fmt::Display for PrincipalConfigError {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        f.write_str(match self {
116            PrincipalConfigError::ThirdPartyEndpoint => {
117                "a principal reference is never sent to a third-party endpoint"
118            }
119            PrincipalConfigError::NeedsProtocolV2 => "a principal reference needs rustev.remote/2",
120            PrincipalConfigError::BadLabel => {
121                "the principal mapping version and scope must be 1 to 64 printable ASCII bytes"
122            }
123        })
124    }
125}
126
127impl std::error::Error for PrincipalConfigError {}
128
129impl PrincipalPosture {
130    /// Check the posture against the endpoint and protocol, and give the
131    /// binding's view of it.
132    pub fn validate(
133        &self,
134        operator: EndpointOperator,
135        protocol_major: u32,
136    ) -> Result<PrincipalWire, PrincipalConfigError> {
137        match self {
138            PrincipalPosture::Omitted => Ok(PrincipalWire::Omitted),
139            PrincipalPosture::Sent {
140                mapping_version,
141                scope,
142                ..
143            } => {
144                if operator != EndpointOperator::HostOperated {
145                    return Err(PrincipalConfigError::ThirdPartyEndpoint);
146                }
147                if protocol_major != rustev_contract::remote::PROTOCOL_MAJOR_V2 {
148                    return Err(PrincipalConfigError::NeedsProtocolV2);
149                }
150                // A one-byte reference checks the labels' bounds.
151                if WirePrincipal::encode(&[0], mapping_version, scope).is_none() {
152                    return Err(PrincipalConfigError::BadLabel);
153                }
154                Ok(PrincipalWire::HostOperated {
155                    mapping_version: mapping_version.clone(),
156                    scope: scope.clone(),
157                })
158            }
159        }
160    }
161
162    /// The reference to send for an attempt with `principal_handle`:
163    /// `Ok(None)` when omitted, `Err(())` when the posture requires one and
164    /// the source has none within its bounds (refused before send).
165    #[allow(clippy::result_unit_err)]
166    pub fn for_attempt(&self, principal_handle: &[u8]) -> Result<Option<WirePrincipal>, ()> {
167        match self {
168            PrincipalPosture::Omitted => Ok(None),
169            PrincipalPosture::Sent {
170                mapping_version,
171                scope,
172                source,
173            } => source
174                .reference(principal_handle)
175                .and_then(|r| WirePrincipal::encode(&r, mapping_version, scope))
176                .map(Some)
177                .ok_or(()),
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use rustev_contract::remote::MAX_PRINCIPAL_BYTES;
186
187    fn sent() -> PrincipalPosture {
188        PrincipalPosture::Sent {
189            mapping_version: "synthetic-map/1".into(),
190            scope: "synthetic-tenant-routing".into(),
191            source: Arc::new(HandleAsReference),
192        }
193    }
194
195    #[test]
196    fn only_a_host_operated_endpoint_over_version_2_may_receive_it() {
197        assert_eq!(
198            PrincipalPosture::Omitted.validate(EndpointOperator::ThirdParty, 1),
199            Ok(PrincipalWire::Omitted)
200        );
201        assert_eq!(
202            sent().validate(EndpointOperator::ThirdParty, 2),
203            Err(PrincipalConfigError::ThirdPartyEndpoint)
204        );
205        assert_eq!(
206            sent().validate(EndpointOperator::HostOperated, 1),
207            Err(PrincipalConfigError::NeedsProtocolV2)
208        );
209        assert!(sent().validate(EndpointOperator::HostOperated, 2).is_ok());
210        for (m, s) in [
211            ("", "s"),
212            ("m", ""),
213            ("m m", "s"),
214            (&"x".repeat(65)[..], "s"),
215        ] {
216            let p = PrincipalPosture::Sent {
217                mapping_version: m.into(),
218                scope: s.into(),
219                source: Arc::new(HandleAsReference),
220            };
221            assert_eq!(
222                p.validate(EndpointOperator::HostOperated, 2),
223                Err(PrincipalConfigError::BadLabel)
224            );
225        }
226    }
227
228    #[test]
229    fn a_missing_or_oversized_reference_is_refused_and_nothing_prints_it() {
230        assert_eq!(PrincipalPosture::Omitted.for_attempt(b"h"), Ok(None));
231        assert_eq!(sent().for_attempt(b""), Err(()));
232        assert_eq!(sent().for_attempt(&[7; MAX_PRINCIPAL_BYTES + 1]), Err(()));
233        let p = sent()
234            .for_attempt(&[7; MAX_PRINCIPAL_BYTES])
235            .unwrap()
236            .unwrap();
237        assert_eq!(p.reference_bytes().unwrap(), vec![7; MAX_PRINCIPAL_BYTES]);
238        let w = sent().for_attempt(b"SYNTHETIC-principal").unwrap().unwrap();
239        assert!(!format!("{w:?}").contains(&w.reference));
240        assert!(!format!("{:?}", sent()).contains("SYNTHETIC"));
241    }
242}