Skip to main content

systemprompt_traits/
auth.rs

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