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}