Skip to main content

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}