Skip to main content

plexus_auth_core/
principal.rs

1//! `Principal` โ€” sealed authenticated-actor identity.
2//!
3//! A `Principal` is an authenticated actor: a user, a service, or anonymous.
4//! Every cross-boundary invocation has exactly one immediate-caller principal
5//! that the framework auto-stamps. Activations receive a `&Principal`; they
6//! cannot construct one.
7//!
8//! Per AUTHZ-0 ยง"The sealed-type pattern" (and the same protections enumerated
9//! in `verified_user.rs`):
10//!
11//! - **No fabrication.** Constructors are crate-private.
12//! - **No backdoor `From` / `Into`.** Orphan rules forbid foreign-trait
13//!   impls for this foreign type from a third crate.
14//! - **No accidental `Default`.** Not derived; a default would be ambiguous
15//!   between anonymous and verified-anonymous.
16//! - **No leaky `Deserialize`.** Not derived; raw JSON cannot fabricate one.
17//! - **No mutation.** Fields are private; only accessors expose data.
18
19use crate::verified_user::VerifiedUser;
20use serde::Serialize;
21
22/// Service-identity claim, paired with `Principal::Service` to identify a
23/// non-user authenticated actor (e.g., another Plexus deployment).
24#[derive(Debug, Clone, Serialize)]
25pub struct ServiceIdentity {
26    service_id: String,
27}
28
29impl ServiceIdentity {
30    /// Mint a `ServiceIdentity`. Crate-private โ€” only the framework's
31    /// verifier code (inside `plexus-auth-core`) is able to produce one.
32    ///
33    /// `dead_code` is allowed for the same reason as
34    /// [`VerifiedUser::new_sealed`]: the verifier-side caller lands in a
35    /// follow-up ticket. The constructor must exist now for the trybuild
36    /// compile-fail asserts to be meaningful.
37    #[allow(dead_code)]
38    pub(crate) fn new_sealed(service_id: String) -> Self {
39        Self { service_id }
40    }
41
42    /// The service identifier (e.g., a SPIFFE ID or workload name).
43    pub fn service_id(&self) -> &str {
44        &self.service_id
45    }
46}
47
48/// An authenticated actor: a user, a service, or anonymous.
49///
50/// Every cross-boundary invocation carries exactly one immediate-caller
51/// `Principal`. The framework stamps it; activations read it; nobody outside
52/// `plexus-auth-core` can construct one.
53///
54/// # Sealing
55///
56/// The discriminants below carry sealed payloads (`VerifiedUser`,
57/// `ServiceIdentity`), and the `Anonymous` variant is constructable only via
58/// the `pub(crate)` `anonymous_sealed` constructor. This means external
59/// crates cannot match-then-rebuild a `Principal::Anonymous` and pass it
60/// off as authentic; the only way to obtain any `Principal` is through the
61/// framework's mint paths inside this crate.
62///
63/// `tests/compile_fail/seal_principal_construct.rs` asserts external
64/// construction is rejected.
65#[derive(Debug, Clone, Serialize)]
66pub enum Principal {
67    /// An end-user principal, carrying the verified token claims.
68    User(VerifiedUser),
69    /// A non-user authenticated principal (e.g., another Plexus service).
70    Service(ServiceIdentity),
71    /// An unauthenticated caller. Methods marked `#[plexus::method(public)]`
72    /// see this; everything else is denied at the perimeter.
73    Anonymous,
74}
75
76impl Principal {
77    /// Mint the anonymous principal. Crate-private โ€” exists so no external
78    /// crate can construct *any* `Principal` variant directly.
79    ///
80    /// `dead_code` is allowed because the framework code that mints
81    /// principals lives in plexus-transport and lands in a follow-up
82    /// ticket. The constructor must exist now for the trybuild
83    /// compile-fail asserts to be meaningful.
84    #[allow(dead_code)]
85    pub(crate) fn anonymous_sealed() -> Self {
86        Self::Anonymous
87    }
88
89    /// Mint a `Principal::User` from a `VerifiedUser`. Crate-private.
90    #[allow(dead_code)]
91    pub(crate) fn user_sealed(verified: VerifiedUser) -> Self {
92        Self::User(verified)
93    }
94
95    /// Mint a `Principal::Service` from a `ServiceIdentity`. Crate-private.
96    #[allow(dead_code)]
97    pub(crate) fn service_sealed(service: ServiceIdentity) -> Self {
98        Self::Service(service)
99    }
100
101    /// Is this principal a verified user?
102    pub fn is_user(&self) -> bool {
103        matches!(self, Self::User(_))
104    }
105
106    /// Is this principal anonymous?
107    pub fn is_anonymous(&self) -> bool {
108        matches!(self, Self::Anonymous)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn principal_anonymous_constructs() {
118        let p = Principal::anonymous_sealed();
119        assert!(p.is_anonymous());
120        assert!(!p.is_user());
121    }
122
123    #[test]
124    fn principal_user_carries_verified() {
125        let v = VerifiedUser::new_sealed(
126            "alice".to_string(),
127            "https://idp.example.com".to_string(),
128            1_700_000_000,
129            1_700_003_600,
130        );
131        let p = Principal::user_sealed(v);
132        assert!(p.is_user());
133        match p {
134            Principal::User(v) => assert_eq!(v.user_id(), "alice"),
135            _ => unreachable!("expected User variant"),
136        }
137    }
138
139    #[test]
140    fn principal_service_carries_identity() {
141        let s = ServiceIdentity::new_sealed("plexus.example".to_string());
142        let p = Principal::service_sealed(s);
143        match p {
144            Principal::Service(s) => assert_eq!(s.service_id(), "plexus.example"),
145            _ => unreachable!("expected Service variant"),
146        }
147    }
148}