Skip to main content

systemprompt_models/auth/enums/
caller.rs

1//! Caller classification and the rate-limit tier it maps to.
2//!
3//! [`UserType`] is the privilege class derived from a permission set;
4//! [`RateLimitTier`] is the throughput band it resolves to; [`TokenType`]
5//! is the bearer-scheme marker. [`UserType::from_permissions`] is the single
6//! source of truth for the permission → type mapping.
7
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::str::FromStr;
11
12use crate::auth::permission::Permission;
13use crate::errors::ParseEnumError;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum UserType {
18    Admin,
19    User,
20    A2a,
21    Mcp,
22    Service,
23    Anon,
24    Unknown,
25}
26
27impl UserType {
28    /// Derives the caller type from a permission set, the single source of
29    /// truth for the permission → type mapping. The precedence is
30    /// privilege-descending (`Admin` wins over `User`, etc.); the hook scopes
31    /// resolve to `Service` so a hook principal is never silently downgraded
32    /// to `Anon`.
33    pub fn from_permissions(permissions: &[Permission]) -> Self {
34        let has = |p: Permission| permissions.contains(&p);
35        if has(Permission::Admin) {
36            Self::Admin
37        } else if has(Permission::User) {
38            Self::User
39        } else if has(Permission::A2a) {
40            Self::A2a
41        } else if has(Permission::Mcp) {
42            Self::Mcp
43        } else if has(Permission::Service)
44            || has(Permission::HookGovern)
45            || has(Permission::HookTrack)
46        {
47            Self::Service
48        } else {
49            Self::Anon
50        }
51    }
52
53    pub const fn as_str(&self) -> &'static str {
54        match self {
55            Self::Admin => "admin",
56            Self::User => "user",
57            Self::A2a => "a2a",
58            Self::Mcp => "mcp",
59            Self::Service => "service",
60            Self::Anon => "anon",
61            Self::Unknown => "unknown",
62        }
63    }
64
65    pub const fn rate_tier(&self) -> RateLimitTier {
66        match self {
67            Self::Admin => RateLimitTier::Admin,
68            Self::User => RateLimitTier::User,
69            Self::A2a => RateLimitTier::A2a,
70            Self::Mcp => RateLimitTier::Mcp,
71            Self::Service => RateLimitTier::Service,
72            Self::Anon | Self::Unknown => RateLimitTier::Anon,
73        }
74    }
75
76    // Human types (Admin/User) are authoritative on the users row, not the JWT:
77    // an Admin-claimed token whose user row is no longer in the admin role gets
78    // downgraded here. Machine types (Service/A2a/Mcp/Anon) are not reflected in
79    // users.roles — they are minted by the OAuth layer and trusted as claimed.
80    #[must_use]
81    pub const fn reconcile_with(self, user_is_admin: bool) -> Self {
82        match self {
83            Self::Admin if !user_is_admin => Self::User,
84            other => other,
85        }
86    }
87}
88
89impl fmt::Display for UserType {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        write!(f, "{}", self.as_str())
92    }
93}
94
95impl FromStr for UserType {
96    type Err = ParseEnumError;
97    fn from_str(s: &str) -> Result<Self, Self::Err> {
98        match s {
99            "admin" => Ok(Self::Admin),
100            "user" => Ok(Self::User),
101            "a2a" => Ok(Self::A2a),
102            "mcp" => Ok(Self::Mcp),
103            "service" => Ok(Self::Service),
104            "anon" => Ok(Self::Anon),
105            "unknown" => Ok(Self::Unknown),
106            _ => Err(ParseEnumError::new("user_type", s)),
107        }
108    }
109}
110
111#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
112pub enum TokenType {
113    #[default]
114    Bearer,
115}
116
117impl TokenType {
118    pub const fn as_str(self) -> &'static str {
119        match self {
120            Self::Bearer => "Bearer",
121        }
122    }
123}
124
125impl fmt::Display for TokenType {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        write!(f, "Bearer")
128    }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
132#[serde(rename_all = "lowercase")]
133pub enum RateLimitTier {
134    Admin,
135    User,
136    A2a,
137    Mcp,
138    Service,
139    Anon,
140}
141
142impl RateLimitTier {
143    pub const fn as_str(&self) -> &'static str {
144        match self {
145            Self::Admin => "admin",
146            Self::User => "user",
147            Self::A2a => "a2a",
148            Self::Mcp => "mcp",
149            Self::Service => "service",
150            Self::Anon => "anon",
151        }
152    }
153}
154
155impl fmt::Display for RateLimitTier {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        write!(f, "{}", self.as_str())
158    }
159}
160
161impl FromStr for RateLimitTier {
162    type Err = ParseEnumError;
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        match s {
165            "admin" => Ok(Self::Admin),
166            "user" => Ok(Self::User),
167            "a2a" => Ok(Self::A2a),
168            "mcp" => Ok(Self::Mcp),
169            "service" => Ok(Self::Service),
170            "anon" => Ok(Self::Anon),
171            _ => Err(ParseEnumError::new("rate_limit_tier", s)),
172        }
173    }
174}