Skip to main content

systemprompt_traits/
auth.rs

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