Skip to main content

litellm_rs/auth/
system.rs

1//! Core authentication system implementation
2
3use super::types::{AuthMethod, AuthResult, AuthzResult};
4use crate::auth::jwt::types::{TeamScopeMarker, TokenType, VerifiedActiveTeam};
5use crate::config::models::auth::AuthConfig;
6use crate::core::models::user::types::{User, UserRole};
7use crate::core::teams::TeamRepository;
8use crate::core::types::context::RequestContext;
9use crate::storage::StorageLayer;
10use crate::storage::database::SeaOrmTeamRepository;
11use crate::utils::error::gateway_error::Result;
12use std::sync::Arc;
13use tracing::{debug, info};
14use uuid::Uuid;
15
16pub(super) struct TeamValidationSeal(());
17
18/// Main authentication system
19#[derive(Clone)]
20pub struct AuthSystem {
21    /// Authentication configuration
22    pub(super) config: Arc<AuthConfig>,
23    /// Storage layer for user data
24    pub(super) storage: Arc<StorageLayer>,
25    /// JWT handler
26    pub(super) jwt: Arc<crate::auth::jwt::types::JwtHandler>,
27    /// API key handler
28    pub(super) api_key: Arc<crate::auth::api_key::creation::ApiKeyHandler>,
29    /// RBAC system
30    pub(super) rbac: Arc<crate::auth::rbac::RbacSystem>,
31}
32
33impl AuthSystem {
34    /// Create a new authentication system
35    pub async fn new(config: &AuthConfig, storage: Arc<StorageLayer>) -> Result<Self> {
36        info!("Initializing authentication system");
37
38        let config = Arc::new(config.clone());
39
40        // Initialize JWT handler
41        let jwt = Arc::new(crate::auth::jwt::types::JwtHandler::new(&config).await?);
42
43        // Initialize API key handler
44        let hmac_secret = config.api_key_hmac_secret.clone();
45        let api_key = Arc::new(
46            crate::auth::api_key::creation::ApiKeyHandler::new(storage.clone(), hmac_secret)
47                .await?,
48        );
49
50        // Initialize RBAC system
51        let rbac = Arc::new(crate::auth::rbac::RbacSystem::new(&config.rbac).await?);
52
53        info!("Authentication system initialized successfully");
54
55        Ok(Self {
56            config,
57            storage,
58            jwt,
59            api_key,
60            rbac,
61        })
62    }
63
64    /// Authenticate a request
65    pub async fn authenticate(
66        &self,
67        auth_method: AuthMethod,
68        context: RequestContext,
69    ) -> Result<AuthResult> {
70        debug!("Authenticating request: {:?}", auth_method);
71
72        match auth_method {
73            AuthMethod::Jwt(token) => self.authenticate_jwt(&token, context).await,
74            AuthMethod::ApiKey(key) => self.authenticate_api_key(&key, context).await,
75            AuthMethod::Session(session_id) => {
76                self.authenticate_session(&session_id, context).await
77            }
78            AuthMethod::None => Ok(AuthResult {
79                success: false,
80                user: None,
81                api_key: None,
82                session: None,
83                error: Some("No authentication provided".to_string()),
84                context,
85            }),
86        }
87    }
88
89    /// Authenticate using JWT token
90    async fn authenticate_jwt(
91        &self,
92        token: &str,
93        mut context: RequestContext,
94    ) -> Result<AuthResult> {
95        match self.jwt.verify_access_token_with_provenance(token).await {
96            Ok(access_claims) => {
97                let claims = access_claims.public;
98                // Reject non-access tokens (e.g. refresh, password reset)
99                if !matches!(claims.token_type, TokenType::Access) {
100                    return Ok(AuthResult {
101                        success: false,
102                        user: None,
103                        api_key: None,
104                        session: None,
105                        error: Some("Invalid token type: expected access token".to_string()),
106                        context,
107                    });
108                }
109
110                // Get user from database. Storage and conversion failures must remain errors;
111                // only a successful lookup with no row is an authentication rejection.
112                if let Some(user) = self.storage.db().find_user_by_id(claims.sub).await? {
113                    if user.is_active() {
114                        context.user_id = Some(user.id().to_string());
115                        match access_claims.team_scope_version {
116                            TeamScopeMarker::Absent => context.clear_team_id(),
117                            TeamScopeMarker::Present(1) => {
118                                if let Some(team_id) = claims.team_id {
119                                    match self.validate_active_team(user.id(), team_id).await? {
120                                        Some(verified) => {
121                                            context.set_team_id(verified.team_id());
122                                        }
123                                        None => {
124                                            return Ok(AuthResult {
125                                                success: false,
126                                                user: None,
127                                                api_key: None,
128                                                session: None,
129                                                error: Some(
130                                                    "Selected team is not active".to_string(),
131                                                ),
132                                                context,
133                                            });
134                                        }
135                                    }
136                                } else {
137                                    context.clear_team_id();
138                                }
139                            }
140                            TeamScopeMarker::Present(_) => {
141                                return Ok(AuthResult {
142                                    success: false,
143                                    user: None,
144                                    api_key: None,
145                                    session: None,
146                                    error: Some(
147                                        "Unsupported access-token team scope version".to_string(),
148                                    ),
149                                    context,
150                                });
151                            }
152                        }
153
154                        Ok(AuthResult {
155                            success: true,
156                            user: Some(user),
157                            api_key: None,
158                            session: None,
159                            error: None,
160                            context,
161                        })
162                    } else {
163                        Ok(AuthResult {
164                            success: false,
165                            user: None,
166                            api_key: None,
167                            session: None,
168                            error: Some("User account is not active".to_string()),
169                            context,
170                        })
171                    }
172                } else {
173                    Ok(AuthResult {
174                        success: false,
175                        user: None,
176                        api_key: None,
177                        session: None,
178                        error: Some("User not found".to_string()),
179                        context,
180                    })
181                }
182            }
183            Err(e) => Ok(AuthResult {
184                success: false,
185                user: None,
186                api_key: None,
187                session: None,
188                error: Some(format!("Invalid JWT token: {}", e)),
189                context,
190            }),
191        }
192    }
193
194    /// Authenticate using API key
195    async fn authenticate_api_key(
196        &self,
197        key: &str,
198        mut context: RequestContext,
199    ) -> Result<AuthResult> {
200        match self.api_key.verify_key(key).await {
201            Ok(Some((api_key, user))) => {
202                context.set_api_key_id(api_key.metadata.id);
203                if let Some(budget_id) = api_key_budget_id(&api_key) {
204                    context.set_api_key_budget_id(budget_id);
205                } else {
206                    context.clear_api_key_budget_id();
207                }
208                context.user_id = api_key.user_id.map(|id| id.to_string());
209                if let Some(team_id) = api_key.team_id {
210                    context.set_team_id(team_id);
211                } else {
212                    context.clear_team_id();
213                }
214
215                Ok(AuthResult {
216                    success: true,
217                    user,
218                    api_key: Some(api_key),
219                    session: None,
220                    error: None,
221                    context,
222                })
223            }
224            Ok(None) => Ok(AuthResult {
225                success: false,
226                user: None,
227                api_key: None,
228                session: None,
229                error: Some("Invalid API key".to_string()),
230                context,
231            }),
232            Err(error) => Err(error),
233        }
234    }
235
236    /// Authenticate using session
237    async fn authenticate_session(
238        &self,
239        _session_id: &str,
240        context: RequestContext,
241    ) -> Result<AuthResult> {
242        // Sessions not yet implemented — reject to prevent JWT-as-session bypass
243        Ok(AuthResult {
244            success: false,
245            user: None,
246            api_key: None,
247            session: None,
248            error: Some("Session authentication is not yet implemented".to_string()),
249            context,
250        })
251    }
252
253    /// Authorize a user for specific permissions
254    pub async fn authorize(&self, user: &User, permissions: &[String]) -> Result<AuthzResult> {
255        debug!(
256            "Authorizing user {} for permissions: {:?}",
257            user.username, permissions
258        );
259
260        let user_permissions = self.rbac.get_user_permissions(user).await?;
261        let allowed = self.rbac.check_permissions(&user_permissions, permissions);
262
263        Ok(AuthzResult {
264            allowed,
265            required_permissions: permissions.to_vec(),
266            user_permissions,
267            reason: if !allowed {
268                Some("Insufficient permissions".to_string())
269            } else {
270                None
271            },
272        })
273    }
274
275    /// Get user permissions based on role
276    pub(super) async fn get_user_permissions(&self, user: &User) -> Result<Vec<String>> {
277        let permissions = match user.role {
278            UserRole::Admin => vec![
279                "read:all".to_string(),
280                "write:all".to_string(),
281                "delete:all".to_string(),
282                "manage:users".to_string(),
283                "manage:api_keys".to_string(),
284                "manage:teams".to_string(),
285            ],
286            UserRole::SuperAdmin => vec![
287                "read:all".to_string(),
288                "write:all".to_string(),
289                "delete:all".to_string(),
290                "manage:users".to_string(),
291                "manage:api_keys".to_string(),
292                "manage:teams".to_string(),
293                "manage:system".to_string(),
294            ],
295            UserRole::Manager => vec![
296                "read:team".to_string(),
297                "write:team".to_string(),
298                "manage:team_users".to_string(),
299                "manage:team_api_keys".to_string(),
300            ],
301            UserRole::User => vec![
302                "read:own".to_string(),
303                "write:own".to_string(),
304                "use:api".to_string(),
305            ],
306            UserRole::ApiUser => vec!["use:api".to_string()],
307            UserRole::Viewer => vec!["read:own".to_string()],
308        };
309        Ok(permissions)
310    }
311
312    /// Validate an explicit active-team selection against canonical team storage.
313    ///
314    /// Missing, inactive, suspended, or mismatched records are semantic rejection
315    /// (`Ok(None)`); repository failures remain errors for the HTTP boundary to
316    /// map to a generic 5xx.
317    pub(crate) async fn validate_active_team(
318        &self,
319        user_id: Uuid,
320        team_id: Uuid,
321    ) -> Result<Option<VerifiedActiveTeam>> {
322        let repository = SeaOrmTeamRepository::new(self.storage.database.clone());
323        let Some(team) = repository.get(team_id).await? else {
324            return Ok(None);
325        };
326        if team.id() != team_id || !team.is_active() {
327            return Ok(None);
328        }
329
330        let Some(member) = repository.get_member(team_id, user_id).await? else {
331            return Ok(None);
332        };
333        if member.team_id != team_id || member.user_id != user_id || !member.is_active() {
334            return Ok(None);
335        }
336
337        Ok(Some(VerifiedActiveTeam::new(
338            user_id,
339            team_id,
340            TeamValidationSeal(()),
341        )))
342    }
343
344    /// Get authentication configuration
345    pub fn config(&self) -> &AuthConfig {
346        &self.config
347    }
348
349    /// Get JWT handler
350    pub fn jwt(&self) -> &crate::auth::jwt::types::JwtHandler {
351        &self.jwt
352    }
353
354    /// Get API key handler
355    pub fn api_key(&self) -> &crate::auth::api_key::creation::ApiKeyHandler {
356        &self.api_key
357    }
358
359    /// Get RBAC system
360    pub fn rbac(&self) -> &crate::auth::rbac::RbacSystem {
361        &self.rbac
362    }
363}
364
365pub(crate) fn api_key_budget_id(api_key: &crate::core::models::ApiKey) -> Option<Uuid> {
366    api_key
367        .metadata
368        .extra
369        .get("__core_keys")
370        .and_then(|value| value.get("budget_id"))
371        .and_then(|value| value.as_str())
372        .and_then(|value| Uuid::parse_str(value).ok())
373}