use super::types::{AuthMethod, AuthResult, AuthzResult};
use crate::auth::jwt::types::{TeamScopeMarker, TokenType, VerifiedActiveTeam};
use crate::config::models::auth::AuthConfig;
use crate::core::models::user::types::{User, UserRole};
use crate::core::teams::TeamRepository;
use crate::core::types::context::RequestContext;
use crate::storage::StorageLayer;
use crate::storage::database::SeaOrmTeamRepository;
use crate::utils::error::gateway_error::Result;
use std::sync::Arc;
use tracing::{debug, info};
use uuid::Uuid;
pub(super) struct TeamValidationSeal(());
#[derive(Clone)]
pub struct AuthSystem {
pub(super) config: Arc<AuthConfig>,
pub(super) storage: Arc<StorageLayer>,
pub(super) jwt: Arc<crate::auth::jwt::types::JwtHandler>,
pub(super) api_key: Arc<crate::auth::api_key::creation::ApiKeyHandler>,
pub(super) rbac: Arc<crate::auth::rbac::RbacSystem>,
}
impl AuthSystem {
pub async fn new(config: &AuthConfig, storage: Arc<StorageLayer>) -> Result<Self> {
info!("Initializing authentication system");
let config = Arc::new(config.clone());
let jwt = Arc::new(crate::auth::jwt::types::JwtHandler::new(&config).await?);
let hmac_secret = config.api_key_hmac_secret.clone();
let api_key = Arc::new(
crate::auth::api_key::creation::ApiKeyHandler::new(storage.clone(), hmac_secret)
.await?,
);
let rbac = Arc::new(crate::auth::rbac::RbacSystem::new(&config.rbac).await?);
info!("Authentication system initialized successfully");
Ok(Self {
config,
storage,
jwt,
api_key,
rbac,
})
}
pub async fn authenticate(
&self,
auth_method: AuthMethod,
context: RequestContext,
) -> Result<AuthResult> {
debug!("Authenticating request: {:?}", auth_method);
match auth_method {
AuthMethod::Jwt(token) => self.authenticate_jwt(&token, context).await,
AuthMethod::ApiKey(key) => self.authenticate_api_key(&key, context).await,
AuthMethod::Session(session_id) => {
self.authenticate_session(&session_id, context).await
}
AuthMethod::None => Ok(AuthResult {
success: false,
user: None,
api_key: None,
session: None,
error: Some("No authentication provided".to_string()),
context,
}),
}
}
async fn authenticate_jwt(
&self,
token: &str,
mut context: RequestContext,
) -> Result<AuthResult> {
match self.jwt.verify_access_token_with_provenance(token).await {
Ok(access_claims) => {
let claims = access_claims.public;
if !matches!(claims.token_type, TokenType::Access) {
return Ok(AuthResult {
success: false,
user: None,
api_key: None,
session: None,
error: Some("Invalid token type: expected access token".to_string()),
context,
});
}
if let Some(user) = self.storage.db().find_user_by_id(claims.sub).await? {
if user.is_active() {
context.user_id = Some(user.id().to_string());
match access_claims.team_scope_version {
TeamScopeMarker::Absent => context.clear_team_id(),
TeamScopeMarker::Present(1) => {
if let Some(team_id) = claims.team_id {
match self.validate_active_team(user.id(), team_id).await? {
Some(verified) => {
context.set_team_id(verified.team_id());
}
None => {
return Ok(AuthResult {
success: false,
user: None,
api_key: None,
session: None,
error: Some(
"Selected team is not active".to_string(),
),
context,
});
}
}
} else {
context.clear_team_id();
}
}
TeamScopeMarker::Present(_) => {
return Ok(AuthResult {
success: false,
user: None,
api_key: None,
session: None,
error: Some(
"Unsupported access-token team scope version".to_string(),
),
context,
});
}
}
Ok(AuthResult {
success: true,
user: Some(user),
api_key: None,
session: None,
error: None,
context,
})
} else {
Ok(AuthResult {
success: false,
user: None,
api_key: None,
session: None,
error: Some("User account is not active".to_string()),
context,
})
}
} else {
Ok(AuthResult {
success: false,
user: None,
api_key: None,
session: None,
error: Some("User not found".to_string()),
context,
})
}
}
Err(e) => Ok(AuthResult {
success: false,
user: None,
api_key: None,
session: None,
error: Some(format!("Invalid JWT token: {}", e)),
context,
}),
}
}
async fn authenticate_api_key(
&self,
key: &str,
mut context: RequestContext,
) -> Result<AuthResult> {
match self.api_key.verify_key(key).await {
Ok(Some((api_key, user))) => {
context.set_api_key_id(api_key.metadata.id);
if let Some(budget_id) = api_key_budget_id(&api_key) {
context.set_api_key_budget_id(budget_id);
} else {
context.clear_api_key_budget_id();
}
context.user_id = api_key.user_id.map(|id| id.to_string());
if let Some(team_id) = api_key.team_id {
context.set_team_id(team_id);
} else {
context.clear_team_id();
}
Ok(AuthResult {
success: true,
user,
api_key: Some(api_key),
session: None,
error: None,
context,
})
}
Ok(None) => Ok(AuthResult {
success: false,
user: None,
api_key: None,
session: None,
error: Some("Invalid API key".to_string()),
context,
}),
Err(error) => Err(error),
}
}
async fn authenticate_session(
&self,
_session_id: &str,
context: RequestContext,
) -> Result<AuthResult> {
Ok(AuthResult {
success: false,
user: None,
api_key: None,
session: None,
error: Some("Session authentication is not yet implemented".to_string()),
context,
})
}
pub async fn authorize(&self, user: &User, permissions: &[String]) -> Result<AuthzResult> {
debug!(
"Authorizing user {} for permissions: {:?}",
user.username, permissions
);
let user_permissions = self.rbac.get_user_permissions(user).await?;
let allowed = self.rbac.check_permissions(&user_permissions, permissions);
Ok(AuthzResult {
allowed,
required_permissions: permissions.to_vec(),
user_permissions,
reason: if !allowed {
Some("Insufficient permissions".to_string())
} else {
None
},
})
}
pub(super) async fn get_user_permissions(&self, user: &User) -> Result<Vec<String>> {
let permissions = match user.role {
UserRole::Admin => vec![
"read:all".to_string(),
"write:all".to_string(),
"delete:all".to_string(),
"manage:users".to_string(),
"manage:api_keys".to_string(),
"manage:teams".to_string(),
],
UserRole::SuperAdmin => vec![
"read:all".to_string(),
"write:all".to_string(),
"delete:all".to_string(),
"manage:users".to_string(),
"manage:api_keys".to_string(),
"manage:teams".to_string(),
"manage:system".to_string(),
],
UserRole::Manager => vec![
"read:team".to_string(),
"write:team".to_string(),
"manage:team_users".to_string(),
"manage:team_api_keys".to_string(),
],
UserRole::User => vec![
"read:own".to_string(),
"write:own".to_string(),
"use:api".to_string(),
],
UserRole::ApiUser => vec!["use:api".to_string()],
UserRole::Viewer => vec!["read:own".to_string()],
};
Ok(permissions)
}
pub(crate) async fn validate_active_team(
&self,
user_id: Uuid,
team_id: Uuid,
) -> Result<Option<VerifiedActiveTeam>> {
let repository = SeaOrmTeamRepository::new(self.storage.database.clone());
let Some(team) = repository.get(team_id).await? else {
return Ok(None);
};
if team.id() != team_id || !team.is_active() {
return Ok(None);
}
let Some(member) = repository.get_member(team_id, user_id).await? else {
return Ok(None);
};
if member.team_id != team_id || member.user_id != user_id || !member.is_active() {
return Ok(None);
}
Ok(Some(VerifiedActiveTeam::new(
user_id,
team_id,
TeamValidationSeal(()),
)))
}
pub fn config(&self) -> &AuthConfig {
&self.config
}
pub fn jwt(&self) -> &crate::auth::jwt::types::JwtHandler {
&self.jwt
}
pub fn api_key(&self) -> &crate::auth::api_key::creation::ApiKeyHandler {
&self.api_key
}
pub fn rbac(&self) -> &crate::auth::rbac::RbacSystem {
&self.rbac
}
}
fn api_key_budget_id(api_key: &crate::core::models::ApiKey) -> Option<Uuid> {
api_key
.metadata
.extra
.get("__core_keys")
.and_then(|value| value.get("budget_id"))
.and_then(|value| value.as_str())
.and_then(|value| Uuid::parse_str(value).ok())
}