Skip to main content

systemprompt_traits/
auth.rs

1//! Authentication and role-management provider traits.
2//!
3//! These traits are dispatched as trait objects (`dyn _`), so they use
4//! `#[async_trait]`; native `async fn` in traits is not yet `dyn`-compatible.
5
6use async_trait::async_trait;
7use std::sync::Arc;
8use systemprompt_identifiers::UserId;
9
10pub type AuthResult<T> = Result<T, AuthProviderError>;
11
12#[derive(Debug, thiserror::Error)]
13#[non_exhaustive]
14pub enum AuthProviderError {
15    #[error("Invalid credentials")]
16    InvalidCredentials,
17
18    #[error("User not found")]
19    UserNotFound,
20
21    #[error("Invalid token")]
22    InvalidToken,
23
24    #[error("Token expired")]
25    TokenExpired,
26
27    #[error("Insufficient permissions")]
28    InsufficientPermissions,
29
30    #[error("Internal error: {0}")]
31    Internal(String),
32}
33
34#[derive(Debug, Clone)]
35pub struct AuthUser {
36    pub id: UserId,
37    pub name: String,
38    pub email: String,
39    pub roles: Vec<String>,
40    pub is_active: bool,
41}
42
43/// Federated-identity claim payload passed to
44/// [`UserProvider::find_or_create_federated`].
45///
46/// Carries only the OIDC fields needed to seed a freshly federated user — the
47/// trait stays free of any concrete JWT type so it can live in
48/// `systemprompt-traits` without taking a dependency on `systemprompt-models`.
49#[derive(Debug, Clone, Default)]
50pub struct FederatedIdentityClaims {
51    pub email: Option<String>,
52    /// Whether the upstream `IdP` has asserted `email_verified=true` for this
53    /// subject. When `false`, callers must refuse to link the federated
54    /// identity to a local account that owns the same email — a hostile
55    /// upstream could otherwise claim arbitrary accounts.
56    pub email_verified: bool,
57    pub name: Option<String>,
58    pub preferred_username: Option<String>,
59    pub roles: Vec<String>,
60}
61
62#[async_trait]
63pub trait UserProvider: Send + Sync {
64    async fn find_by_id(&self, id: &UserId) -> AuthResult<Option<AuthUser>>;
65    async fn find_by_email(&self, email: &str) -> AuthResult<Option<AuthUser>>;
66    async fn find_by_name(&self, name: &str) -> AuthResult<Option<AuthUser>>;
67    async fn create_user(
68        &self,
69        name: &str,
70        email: &str,
71        full_name: Option<&str>,
72    ) -> AuthResult<AuthUser>;
73    async fn create_anonymous(&self, fingerprint: &str) -> AuthResult<AuthUser>;
74    async fn assign_roles(&self, user_id: &UserId, roles: &[String]) -> AuthResult<()>;
75
76    /// Resolve an externally-issued identity (`issuer`, `external_sub`) to a
77    /// stable local `UserId`. On first touch creates a new `users` row plus a
78    /// `federated_identities` mapping; subsequent calls advance `last_seen_at`
79    /// and return the existing id. Implementations MUST perform both writes
80    /// in a single transaction.
81    async fn find_or_create_federated(
82        &self,
83        issuer: &str,
84        external_sub: &str,
85        claims: &FederatedIdentityClaims,
86    ) -> AuthResult<UserId>;
87}
88
89#[async_trait]
90pub trait RoleProvider: Send + Sync {
91    async fn get_roles(&self, user_id: &UserId) -> AuthResult<Vec<String>>;
92    async fn assign_role(&self, user_id: &UserId, role: &str) -> AuthResult<()>;
93    async fn revoke_role(&self, user_id: &UserId, role: &str) -> AuthResult<()>;
94    async fn list_users_by_role(&self, role: &str) -> AuthResult<Vec<AuthUser>>;
95}
96
97pub type DynUserProvider = Arc<dyn UserProvider>;
98pub type DynRoleProvider = Arc<dyn RoleProvider>;