osproxy_spi/principal.rs
1//! The authenticated client identity passed to the SPI.
2
3use osproxy_core::PrincipalId;
4
5/// The authenticated caller, as seen by the routing/tenancy SPI.
6///
7/// Carries a stable [`PrincipalId`] and a small set of attributes an
8/// implementer may key tenancy decisions on (e.g. a tenant id derived from the
9/// client certificate). It **never** carries the raw credential (token,
10/// certificate bytes): those are consumed by the authenticator and dropped, so
11/// nothing secret reaches the SPI or telemetry (`docs/05` ยง7, NFR-S2).
12///
13/// # Examples
14///
15/// ```
16/// use osproxy_core::PrincipalId;
17/// use osproxy_spi::{Principal, PrincipalAttr};
18///
19/// let p = Principal::new(PrincipalId::from("svc-ingest"))
20/// .with_attr(PrincipalAttr::new("tenant", "acme"));
21/// assert_eq!(p.id().as_str(), "svc-ingest");
22/// assert_eq!(p.attr("tenant"), Some("acme"));
23/// assert_eq!(p.attr("missing"), None);
24/// ```
25#[derive(Clone, PartialEq, Eq, Debug)]
26pub struct Principal {
27 id: PrincipalId,
28 attrs: Vec<PrincipalAttr>,
29}
30
31impl Principal {
32 /// Constructs a principal with no attributes.
33 #[must_use]
34 pub fn new(id: PrincipalId) -> Self {
35 Self {
36 id,
37 attrs: Vec::new(),
38 }
39 }
40
41 /// Adds an attribute (builder style).
42 #[must_use]
43 pub fn with_attr(mut self, attr: PrincipalAttr) -> Self {
44 self.attrs.push(attr);
45 self
46 }
47
48 /// The principal's stable id.
49 #[must_use]
50 pub fn id(&self) -> &PrincipalId {
51 &self.id
52 }
53
54 /// Looks up an attribute value by key, if present.
55 #[must_use]
56 pub fn attr(&self, key: &str) -> Option<&str> {
57 self.attrs
58 .iter()
59 .find(|a| a.key == key)
60 .map(|a| a.value.as_str())
61 }
62}
63
64/// A single named attribute carried by a [`Principal`].
65///
66/// Both key and value are derived identity facts (never secrets), so they are
67/// safe to use in routing and to surface as trace attributes.
68#[derive(Clone, PartialEq, Eq, Debug)]
69pub struct PrincipalAttr {
70 /// The attribute name (e.g. `"tenant"`).
71 pub key: String,
72 /// The attribute value (e.g. `"acme"`).
73 pub value: String,
74}
75
76impl PrincipalAttr {
77 /// Constructs an attribute from a key and value.
78 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
79 Self {
80 key: key.into(),
81 value: value.into(),
82 }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn attributes_are_looked_up_by_key() {
92 let p = Principal::new(PrincipalId::from("u-1"))
93 .with_attr(PrincipalAttr::new("tenant", "acme"))
94 .with_attr(PrincipalAttr::new("region", "eu"));
95 assert_eq!(p.attr("tenant"), Some("acme"));
96 assert_eq!(p.attr("region"), Some("eu"));
97 assert_eq!(p.attr("nope"), None);
98 assert_eq!(p.id().as_str(), "u-1");
99 }
100}