Skip to main content

systemprompt_models/auth/
types.rs

1//! Authenticated-principal and OAuth request types.
2//!
3//! [`AuthenticatedUser`] is the resolved principal carried through a request;
4//! [`AuthError`] is the crate's authentication/OAuth error enum. [`PkceMethod`]
5//! and [`ResponseType`] model the OAuth authorization-request parameters.
6
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use systemprompt_identifiers::ClientId;
10use uuid::Uuid;
11
12use super::enums::UserType;
13use super::permission::Permission;
14
15pub const BEARER_PREFIX: &str = "Bearer ";
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct AuthenticatedUser {
19    pub id: Uuid,
20    pub username: String,
21    pub email: String,
22    pub permissions: Vec<Permission>,
23    #[serde(default)]
24    pub roles: Vec<String>,
25    /// Opaque ABAC attribute bag forwarded into `JwtClaims.attributes` and
26    /// onward to `AuthzRequest.attributes`. Tenant-defined, namespaced
27    /// keys; core never interprets values.
28    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
29    pub attributes: BTreeMap<String, serde_json::Value>,
30}
31
32impl AuthenticatedUser {
33    pub const fn new(
34        id: Uuid,
35        username: String,
36        email: String,
37        permissions: Vec<Permission>,
38    ) -> Self {
39        Self {
40            id,
41            username,
42            email,
43            permissions,
44            roles: Vec::new(),
45            attributes: BTreeMap::new(),
46        }
47    }
48
49    pub const fn new_with_roles(
50        id: Uuid,
51        username: String,
52        email: String,
53        permissions: Vec<Permission>,
54        roles: Vec<String>,
55    ) -> Self {
56        Self {
57            id,
58            username,
59            email,
60            permissions,
61            roles,
62            attributes: BTreeMap::new(),
63        }
64    }
65
66    #[must_use]
67    pub fn with_attributes(mut self, attributes: BTreeMap<String, serde_json::Value>) -> Self {
68        self.attributes = attributes;
69        self
70    }
71
72    #[must_use]
73    pub const fn attributes(&self) -> &BTreeMap<String, serde_json::Value> {
74        &self.attributes
75    }
76
77    pub fn has_permission(&self, permission: Permission) -> bool {
78        self.permissions.contains(&permission)
79            || self.permissions.iter().any(|p| p.implies(&permission))
80    }
81
82    pub fn is_admin(&self) -> bool {
83        self.has_permission(Permission::Admin)
84    }
85
86    pub fn permissions(&self) -> &[Permission] {
87        &self.permissions
88    }
89
90    pub fn has_role(&self, role: &str) -> bool {
91        self.roles.iter().any(|r| r == role)
92    }
93
94    pub fn roles(&self) -> &[String] {
95        &self.roles
96    }
97
98    pub fn user_type(&self) -> UserType {
99        UserType::from_permissions(&self.permissions)
100    }
101}
102
103#[derive(Debug, thiserror::Error)]
104pub enum AuthError {
105    #[error("Invalid token format")]
106    InvalidTokenFormat,
107
108    #[error("Token expired")]
109    TokenExpired,
110
111    #[error("Token signature invalid")]
112    InvalidSignature,
113
114    #[error("User not found")]
115    UserNotFound,
116
117    #[error("Insufficient permissions")]
118    InsufficientPermissions,
119
120    #[error("Authentication failed: {message}")]
121    AuthenticationFailed { message: String },
122
123    #[error("Invalid OAuth request: {reason}")]
124    InvalidRequest { reason: String },
125
126    #[error("CSRF token (state) is required")]
127    MissingState,
128
129    #[error("Redirect URI is required and must be registered")]
130    InvalidRedirectUri,
131
132    #[error("PKCE code_challenge is required")]
133    MissingCodeChallenge,
134
135    #[error("PKCE method '{method}' not allowed (must be S256)")]
136    WeakPkceMethod { method: String },
137
138    #[error("Client ID {client_id} not found")]
139    ClientNotFound { client_id: ClientId },
140
141    #[error("Scope '{scope}' is invalid")]
142    InvalidScope { scope: String },
143
144    #[error("Token revocation requires authenticated user")]
145    UnauthenticatedRevocation,
146
147    #[error("WebAuthn RP ID could not be determined")]
148    InvalidRpId,
149
150    #[error("Client registration validation failed: {reason}")]
151    RegistrationFailed { reason: String },
152
153    #[error("Internal error: {0}")]
154    Internal(String),
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum PkceMethod {
159    S256,
160}
161
162impl std::str::FromStr for PkceMethod {
163    type Err = AuthError;
164
165    fn from_str(s: &str) -> Result<Self, Self::Err> {
166        match s {
167            "S256" => Ok(Self::S256),
168            "plain" => Err(AuthError::WeakPkceMethod {
169                method: s.to_owned(),
170            }),
171            _ => Err(AuthError::InvalidRequest {
172                reason: format!("Unknown PKCE method: {s}"),
173            }),
174        }
175    }
176}
177
178impl std::fmt::Display for PkceMethod {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        match self {
181            Self::S256 => write!(f, "S256"),
182        }
183    }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum ResponseType {
188    Code,
189    Token,
190}
191
192impl std::str::FromStr for ResponseType {
193    type Err = AuthError;
194
195    fn from_str(s: &str) -> Result<Self, Self::Err> {
196        match s {
197            "code" => Ok(Self::Code),
198            "token" => Ok(Self::Token),
199            _ => Err(AuthError::InvalidRequest {
200                reason: format!("Unknown response type: {s}"),
201            }),
202        }
203    }
204}
205
206impl std::fmt::Display for ResponseType {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        match self {
209            Self::Code => write!(f, "code"),
210            Self::Token => write!(f, "token"),
211        }
212    }
213}