Skip to main content

systemprompt_models/auth/
enums.rs

1//! Authentication and authorization enumerations.
2//!
3//! Defines the closed sets the platform reasons over: [`JwtAudience`],
4//! [`UserType`], [`TokenType`], [`RateLimitTier`], [`UserRole`], and
5//! [`UserStatus`]. [`UserType::from_permissions`] is the single source of
6//! truth for the permission-to-type mapping.
7
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::str::FromStr;
11
12use super::permission::Permission;
13use crate::errors::ParseEnumError;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
16#[serde(rename_all = "lowercase")]
17pub enum JwtAudience {
18    Web,
19    Api,
20    A2a,
21    Mcp,
22    Internal,
23    Bridge,
24    Hook,
25    #[serde(untagged)]
26    Resource(String),
27}
28
29impl JwtAudience {
30    pub fn as_str(&self) -> &str {
31        match self {
32            Self::Web => "web",
33            Self::Api => "api",
34            Self::A2a => "a2a",
35            Self::Mcp => "mcp",
36            Self::Internal => "internal",
37            Self::Bridge => "bridge",
38            Self::Hook => "hook",
39            Self::Resource(s) => s.as_str(),
40        }
41    }
42
43    pub fn standard() -> Vec<Self> {
44        vec![Self::Web, Self::Api, Self::A2a, Self::Mcp]
45    }
46
47    pub fn service() -> Vec<Self> {
48        vec![Self::Api, Self::Mcp, Self::A2a, Self::Internal]
49    }
50}
51
52impl fmt::Display for JwtAudience {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        write!(f, "{}", self.as_str())
55    }
56}
57
58impl FromStr for JwtAudience {
59    type Err = ParseEnumError;
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        match s {
62            "web" => Ok(Self::Web),
63            "api" => Ok(Self::Api),
64            "a2a" => Ok(Self::A2a),
65            "mcp" => Ok(Self::Mcp),
66            "internal" => Ok(Self::Internal),
67            "bridge" => Ok(Self::Bridge),
68            "hook" => Ok(Self::Hook),
69            _ => Ok(Self::Resource(s.to_owned())),
70        }
71    }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
75#[serde(rename_all = "lowercase")]
76pub enum UserType {
77    Admin,
78    User,
79    A2a,
80    Mcp,
81    Service,
82    Anon,
83    Unknown,
84}
85
86impl UserType {
87    /// Derives the caller type from a permission set, the single source of
88    /// truth for the permission → type mapping. The precedence is
89    /// privilege-descending (`Admin` wins over `User`, etc.); the hook scopes
90    /// resolve to `Service` so a hook principal is never silently downgraded
91    /// to `Anon`.
92    pub fn from_permissions(permissions: &[Permission]) -> Self {
93        let has = |p: Permission| permissions.contains(&p);
94        if has(Permission::Admin) {
95            Self::Admin
96        } else if has(Permission::User) {
97            Self::User
98        } else if has(Permission::A2a) {
99            Self::A2a
100        } else if has(Permission::Mcp) {
101            Self::Mcp
102        } else if has(Permission::Service)
103            || has(Permission::HookGovern)
104            || has(Permission::HookTrack)
105        {
106            Self::Service
107        } else {
108            Self::Anon
109        }
110    }
111
112    pub const fn as_str(&self) -> &'static str {
113        match self {
114            Self::Admin => "admin",
115            Self::User => "user",
116            Self::A2a => "a2a",
117            Self::Mcp => "mcp",
118            Self::Service => "service",
119            Self::Anon => "anon",
120            Self::Unknown => "unknown",
121        }
122    }
123
124    pub const fn rate_tier(&self) -> RateLimitTier {
125        match self {
126            Self::Admin => RateLimitTier::Admin,
127            Self::User => RateLimitTier::User,
128            Self::A2a => RateLimitTier::A2a,
129            Self::Mcp => RateLimitTier::Mcp,
130            Self::Service => RateLimitTier::Service,
131            Self::Anon | Self::Unknown => RateLimitTier::Anon,
132        }
133    }
134
135    // Human types (Admin/User) are authoritative on the users row, not the JWT:
136    // an Admin-claimed token whose user row is no longer in the admin role gets
137    // downgraded here. Machine types (Service/A2a/Mcp/Anon) are not reflected in
138    // users.roles — they are minted by the OAuth layer and trusted as claimed.
139    #[must_use]
140    pub const fn reconcile_with(self, user_is_admin: bool) -> Self {
141        match self {
142            Self::Admin if !user_is_admin => Self::User,
143            other => other,
144        }
145    }
146}
147
148impl fmt::Display for UserType {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "{}", self.as_str())
151    }
152}
153
154impl FromStr for UserType {
155    type Err = ParseEnumError;
156    fn from_str(s: &str) -> Result<Self, Self::Err> {
157        match s {
158            "admin" => Ok(Self::Admin),
159            "user" => Ok(Self::User),
160            "a2a" => Ok(Self::A2a),
161            "mcp" => Ok(Self::Mcp),
162            "service" => Ok(Self::Service),
163            "anon" => Ok(Self::Anon),
164            "unknown" => Ok(Self::Unknown),
165            _ => Err(ParseEnumError::new("user_type", s)),
166        }
167    }
168}
169
170#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
171pub enum TokenType {
172    #[default]
173    Bearer,
174}
175
176impl TokenType {
177    pub const fn as_str(self) -> &'static str {
178        match self {
179            Self::Bearer => "Bearer",
180        }
181    }
182}
183
184impl fmt::Display for TokenType {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(f, "Bearer")
187    }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
191#[serde(rename_all = "lowercase")]
192pub enum RateLimitTier {
193    Admin,
194    User,
195    A2a,
196    Mcp,
197    Service,
198    Anon,
199}
200
201impl RateLimitTier {
202    pub const fn as_str(&self) -> &'static str {
203        match self {
204            Self::Admin => "admin",
205            Self::User => "user",
206            Self::A2a => "a2a",
207            Self::Mcp => "mcp",
208            Self::Service => "service",
209            Self::Anon => "anon",
210        }
211    }
212}
213
214impl fmt::Display for RateLimitTier {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        write!(f, "{}", self.as_str())
217    }
218}
219
220impl FromStr for RateLimitTier {
221    type Err = ParseEnumError;
222    fn from_str(s: &str) -> Result<Self, Self::Err> {
223        match s {
224            "admin" => Ok(Self::Admin),
225            "user" => Ok(Self::User),
226            "a2a" => Ok(Self::A2a),
227            "mcp" => Ok(Self::Mcp),
228            "service" => Ok(Self::Service),
229            "anon" => Ok(Self::Anon),
230            _ => Err(ParseEnumError::new("rate_limit_tier", s)),
231        }
232    }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
236#[serde(rename_all = "lowercase")]
237pub enum UserRole {
238    Admin,
239    User,
240    Anonymous,
241}
242
243impl UserRole {
244    pub const fn as_str(&self) -> &'static str {
245        match self {
246            Self::Admin => "admin",
247            Self::User => "user",
248            Self::Anonymous => "anonymous",
249        }
250    }
251}
252
253impl fmt::Display for UserRole {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        write!(f, "{}", self.as_str())
256    }
257}
258
259impl FromStr for UserRole {
260    type Err = ParseEnumError;
261    fn from_str(s: &str) -> Result<Self, Self::Err> {
262        match s {
263            "admin" => Ok(Self::Admin),
264            "user" => Ok(Self::User),
265            "anonymous" => Ok(Self::Anonymous),
266            _ => Err(ParseEnumError::new("user_role", s)),
267        }
268    }
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
272#[serde(rename_all = "lowercase")]
273pub enum UserStatus {
274    Active,
275    Inactive,
276    Suspended,
277    Pending,
278    Deleted,
279    Temporary,
280}
281
282impl UserStatus {
283    pub const fn as_str(&self) -> &'static str {
284        match self {
285            Self::Active => "active",
286            Self::Inactive => "inactive",
287            Self::Suspended => "suspended",
288            Self::Pending => "pending",
289            Self::Deleted => "deleted",
290            Self::Temporary => "temporary",
291        }
292    }
293
294    pub const fn is_active(&self) -> bool {
295        matches!(self, Self::Active)
296    }
297}
298
299impl fmt::Display for UserStatus {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        write!(f, "{}", self.as_str())
302    }
303}
304
305impl FromStr for UserStatus {
306    type Err = ParseEnumError;
307    fn from_str(s: &str) -> Result<Self, Self::Err> {
308        match s {
309            "active" => Ok(Self::Active),
310            "inactive" => Ok(Self::Inactive),
311            "suspended" => Ok(Self::Suspended),
312            "pending" => Ok(Self::Pending),
313            "deleted" => Ok(Self::Deleted),
314            "temporary" => Ok(Self::Temporary),
315            _ => Err(ParseEnumError::new("user_status", s)),
316        }
317    }
318}