Skip to main content

rust_web_server/authz/
identity.rs

1use std::cell::RefCell;
2use std::sync::Arc;
3
4/// The authenticated caller of the current request: an object (a type and
5/// an id) — never a set like `group:eng#member`.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct Identity {
8    kind: Arc<str>,
9    id: Arc<str>,
10    subject: Arc<str>,
11    source: &'static str,
12    claims: Option<Arc<str>>,
13}
14
15impl Identity {
16    /// A caller you name in code: `"service:billing"` (a type, `:`, an id)
17    /// or a bare id, which gets the type `user`. `source` says who vouched
18    /// (`"jwt"`, `"oidc"`, `"saml"`, `"basic"`, `"forward"`, `"mcp"`, or your
19    /// own). For a value that came from outside, use [`Identity::user`].
20    pub fn new(subject: impl AsRef<str>, source: &'static str) -> Identity {
21        let subject = subject.as_ref();
22        let (kind, id) = subject.split_once(':').unwrap_or(("user", subject));
23        Identity::typed(kind, id, source)
24    }
25
26    fn typed(kind: &str, id: &str, source: &'static str) -> Identity {
27        Identity { kind: Arc::from(kind), id: Arc::from(id), subject: Arc::from(format!("{kind}:{id}")), source, claims: None }
28    }
29
30    /// The user whose id is `id` exactly — what authenticators use for a
31    /// value from a token, a header or a login. A `:` stays part of the id
32    /// (`user:a:b`). `None` when `id` can't name an object: empty, over 256
33    /// characters, or holding `#`, `@`, a space or a control character —
34    /// so a token whose `sub` is `group:eng#member` can't pass as that group.
35    pub fn user(id: &str, source: &'static str) -> Option<Identity> {
36        crate::authz::rebac::ObjectRef::parse(&format!("user:{id}")).ok()?;
37        Some(Identity::typed("user", id, source))
38    }
39
40    /// The same, carrying the token's or assertion's claims (a JSON object).
41    pub fn with_claims(mut self, claims: impl AsRef<str>) -> Identity {
42        self.claims = Some(Arc::from(claims.as_ref()));
43        self
44    }
45
46    /// An identity from a JSON claims object: its `claim` (`"sub"` usually) as
47    /// the subject id. `None` when the claim is missing or not a string.
48    pub fn from_claims(claims: &str, claim: &str, source: &'static str) -> Option<Identity> {
49        Some(Identity::user(&claim_value(claims, claim)?, source)?.with_claims(claims))
50    }
51
52    /// The caller of the request being served on this thread or task.
53    pub fn current() -> Option<Identity> {
54        #[cfg(feature = "http2")]
55        if let Ok(Some(identity)) = TASK.try_with(|i| i.clone()) {
56            return Some(identity);
57        }
58        CURRENT.with(|c| c.borrow().clone())
59    }
60
61    /// `type:id`, e.g. `user:alice`.
62    pub fn subject(&self) -> &str {
63        &self.subject
64    }
65
66    /// The type part of the subject (`user`).
67    pub fn subject_type(&self) -> &str {
68        &self.kind
69    }
70
71    /// The id part of the subject (`alice`).
72    pub fn subject_id(&self) -> &str {
73        &self.id
74    }
75
76    /// The caller as a tuple subject: an object, never a set.
77    pub fn as_subject(&self) -> crate::authz::Subject {
78        crate::authz::Subject { object: crate::authz::ObjectRef { kind: self.kind.to_string(), id: self.id.to_string() }, relation: None }
79    }
80
81    /// Which authenticator vouched for it.
82    pub fn source(&self) -> &'static str {
83        self.source
84    }
85
86    /// The raw claims JSON, when the authenticator had any.
87    pub fn claims(&self) -> Option<&str> {
88        self.claims.as_deref()
89    }
90
91    /// One claim as a string (top-level members only).
92    pub fn claim(&self, name: &str) -> Option<String> {
93        claim_value(self.claims.as_deref()?, name)
94    }
95}
96
97/// A top-level string member of a JSON object.
98pub(crate) fn claim_value(claims: &str, name: &str) -> Option<String> {
99    let members = crate::mcp::json_rpc::object_members(claims)?;
100    let (_, raw) = members.into_iter().find(|(key, _)| key == name)?;
101    raw.trim().starts_with('"').then(|| crate::mcp::json_rpc::extract_str(&format!("{{\"v\":{raw}}}"), "v")).flatten()
102}
103
104thread_local! {
105    static CURRENT: RefCell<Option<Identity>> = const { RefCell::new(None) };
106}
107
108#[cfg(feature = "http2")]
109tokio::task_local! {
110    static TASK: Option<Identity>;
111}
112
113/// Run `f` as `identity` (what [`Identity::current`] returns inside it); the
114/// previous one comes back afterwards, also on panic. Authenticators call
115/// this around the rest of the request.
116pub fn with_identity<R>(identity: Identity, f: impl FnOnce() -> R) -> R {
117    let _guard = enter(identity);
118    f()
119}
120
121/// This thread runs as `identity` until the guard is dropped.
122pub(crate) fn enter(identity: Identity) -> IdentityGuard {
123    IdentityGuard(CURRENT.with(|c| c.borrow_mut().replace(identity)))
124}
125
126/// Puts the previous identity back when dropped.
127pub(crate) struct IdentityGuard(Option<Identity>);
128
129impl Drop for IdentityGuard {
130    fn drop(&mut self) {
131        let previous = self.0.take();
132        CURRENT.with(|c| *c.borrow_mut() = previous);
133    }
134}
135
136/// [`with_identity`] for async code: `future` runs as `identity`.
137#[cfg(feature = "http2")]
138pub async fn scope_identity<F: std::future::Future>(identity: Identity, future: F) -> F::Output {
139    TASK.scope(Some(identity), future).await
140}