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//!
6//! Copyright (c) systemprompt.io — Business Source License 1.1.
7//! See <https://systemprompt.io> for licensing details.
8
9use async_trait::async_trait;
10use std::sync::Arc;
11use systemprompt_identifiers::UserId;
12
13pub type AuthResult<T> = Result<T, AuthProviderError>;
14
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum AuthProviderError {
18    #[error("Invalid credentials")]
19    InvalidCredentials,
20
21    #[error("User not found")]
22    UserNotFound,
23
24    #[error("Invalid token")]
25    InvalidToken,
26
27    #[error("Token expired")]
28    TokenExpired,
29
30    #[error("Insufficient permissions")]
31    InsufficientPermissions,
32
33    #[error("Internal error: {0}")]
34    Internal(String),
35}
36
37#[derive(Debug, Clone)]
38pub struct AuthUser {
39    pub id: UserId,
40    pub name: String,
41    pub email: String,
42    pub roles: Vec<String>,
43    pub is_active: bool,
44}
45
46/// Federated-identity claim payload passed to
47/// [`UserProvider::find_or_create_federated`].
48///
49/// Carries only the OIDC fields needed to seed a freshly federated user — the
50/// trait stays free of any concrete JWT type so it can live in
51/// `systemprompt-traits` without taking a dependency on `systemprompt-models`.
52#[derive(Debug, Clone, Default)]
53pub struct FederatedIdentityClaims {
54    pub email: Option<String>,
55    pub email_verified: bool,
56    pub name: Option<String>,
57    pub preferred_username: Option<String>,
58    pub roles: Vec<String>,
59}
60
61/// Whether an inbound chat-platform sender resolves to a linkable identity.
62///
63/// This is the identity-linking rule, stated once: a sender is `Linked` only
64/// when the platform verified the claims (for Slack, a workspace profile with
65/// a confirmed email — an unconfirmed address would let anyone who can set it
66/// claim the account that owns it). Anything less is `Unlinked`, whose empty
67/// claims land the sender on a fresh, role-less first-touch user that no rule
68/// grants anything to — never on an existing account.
69#[derive(Debug, Clone, Default)]
70pub enum SenderIdentity {
71    Linked(FederatedIdentityClaims),
72    #[default]
73    Unlinked,
74}
75
76impl SenderIdentity {
77    #[must_use]
78    pub fn claims(&self) -> FederatedIdentityClaims {
79        match self {
80            Self::Linked(claims) => claims.clone(),
81            Self::Unlinked => FederatedIdentityClaims::default(),
82        }
83    }
84}
85
86#[async_trait]
87pub trait UserProvider: Send + Sync {
88    async fn find_by_id(&self, id: &UserId) -> AuthResult<Option<AuthUser>>;
89    async fn find_by_email(&self, email: &str) -> AuthResult<Option<AuthUser>>;
90    async fn find_by_name(&self, name: &str) -> AuthResult<Option<AuthUser>>;
91    async fn create_user(
92        &self,
93        name: &str,
94        email: &str,
95        full_name: Option<&str>,
96    ) -> AuthResult<AuthUser>;
97    async fn create_anonymous(&self, fingerprint: &str) -> AuthResult<AuthUser>;
98    async fn assign_roles(&self, user_id: &UserId, roles: &[String]) -> AuthResult<()>;
99
100    async fn find_or_create_federated(
101        &self,
102        issuer: &str,
103        external_sub: &str,
104        claims: &FederatedIdentityClaims,
105    ) -> AuthResult<UserId>;
106
107    async fn promote_anonymous(&self, source: &UserId, target: &UserId) -> AuthResult<u64>;
108}
109
110#[async_trait]
111pub trait RoleProvider: Send + Sync {
112    async fn get_roles(&self, user_id: &UserId) -> AuthResult<Vec<String>>;
113    async fn assign_role(&self, user_id: &UserId, role: &str) -> AuthResult<()>;
114    async fn revoke_role(&self, user_id: &UserId, role: &str) -> AuthResult<()>;
115    async fn list_users_by_role(&self, role: &str) -> AuthResult<Vec<AuthUser>>;
116}
117
118pub type DynUserProvider = Arc<dyn UserProvider>;
119pub type DynRoleProvider = Arc<dyn RoleProvider>;