Skip to main content

soma_domain/
principal.rs

1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5/// A deduplicated, order-stable set of scope strings.
6#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(transparent)]
8pub struct ScopeSet(BTreeSet<String>);
9
10impl ScopeSet {
11    /// Builds a set from the given scopes, dropping empty strings.
12    pub fn new(scopes: impl IntoIterator<Item = String>) -> Self {
13        Self(
14            scopes
15                .into_iter()
16                .filter(|scope| !scope.is_empty())
17                .collect(),
18        )
19    }
20
21    /// Returns true if `scope` is present in the set.
22    pub fn contains(&self, scope: &str) -> bool {
23        self.0.contains(scope)
24    }
25
26    /// Returns the scopes as a sorted `Vec`.
27    pub fn to_vec(&self) -> Vec<String> {
28        self.0.iter().cloned().collect()
29    }
30}
31
32impl<const N: usize> From<[&str; N]> for ScopeSet {
33    fn from(scopes: [&str; N]) -> Self {
34        Self::new(scopes.into_iter().map(ToOwned::to_owned))
35    }
36}
37
38/// An authenticated caller identity and the scopes it holds.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Principal {
41    /// Identifier of the caller (e.g. token subject).
42    pub subject: String,
43    /// Scopes granted to this caller.
44    pub scopes: ScopeSet,
45    /// Token issuer, when known.
46    pub issuer: Option<String>,
47}
48
49impl Principal {
50    /// Builds a principal with the given subject and scopes and no issuer.
51    pub fn new(subject: impl Into<String>, scopes: ScopeSet) -> Self {
52        Self {
53            subject: subject.into(),
54            scopes,
55            issuer: None,
56        }
57    }
58
59    /// Returns a copy of this principal with the issuer set.
60    pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
61        self.issuer = Some(issuer.into());
62        self
63    }
64
65    /// An unauthenticated principal with no scopes.
66    pub fn anonymous() -> Self {
67        Self::new("anonymous", ScopeSet::default())
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::ScopeSet;
74
75    #[test]
76    fn scope_sets_are_deduplicated_and_stable() {
77        let scopes = ScopeSet::new([
78            "soma:write".to_owned(),
79            "soma:read".to_owned(),
80            "soma:write".to_owned(),
81        ]);
82        assert_eq!(
83            scopes.to_vec(),
84            vec!["soma:read".to_owned(), "soma:write".to_owned()]
85        );
86    }
87}