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