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