use crate::error::GitError; use base64::{Engine as _, engine::general_purpose}; use db::database::AppDatabase; use models::projects::MemberRole; use models::projects::{project, project_history_name, project_members}; use models::repos::{repo, repo_history_name}; use models::users::{user, user_ssh_key}; use sea_orm::sqlx::types::chrono; use sea_orm::*; use sha2::{Digest, Sha256}; use slog::{Logger, error, info, warn}; /// SSH authentication service optimized for performance pub struct SshAuthService { db: AppDatabase, logger: Logger, } impl SshAuthService { pub fn new(db: AppDatabase, logger: Logger) -> Self { Self { db, logger } } pub async fn find_repo( &self, namespace: &str, repo_name: &str, ) -> Result { let namespace = self.find_namespace(namespace).await?; self.find_repository_by_name_and_project(repo_name, namespace.id) .await } async fn find_namespace(&self, namespace: &str) -> Result { if let Some(project) = project::Entity::find() .filter(project::Column::Name.eq(namespace)) .one(self.db.reader()) .await .map_err(|e| GitError::Internal(e.to_string()))? { return Ok(project); } if let Some(history) = project_history_name::Entity::find() .filter(project_history_name::Column::HistoryName.eq(namespace)) .one(self.db.reader()) .await .map_err(|e| GitError::Internal(e.to_string()))? { if let Some(project) = project::Entity::find() .filter(project::Column::Id.eq(history.project_uid)) .one(self.db.reader()) .await .map_err(|e| GitError::Internal(e.to_string()))? { return Ok(project); } } Err(GitError::NotFound("Project not found".to_string())) } async fn find_repository_by_name_and_project( &self, repo_name: &str, project_id: uuid::Uuid, ) -> Result { if let Some(repo) = repo::Entity::find() .filter(repo::Column::RepoName.eq(repo_name)) .filter(repo::Column::Project.eq(project_id)) .one(self.db.reader()) .await .map_err(|e| GitError::Internal(e.to_string()))? { return Ok(repo); } if let Some(history) = repo_history_name::Entity::find() .filter(repo_history_name::Column::Name.eq(repo_name)) .filter(repo_history_name::Column::Project.eq(project_id)) .one(self.db.reader()) .await .map_err(|e| GitError::Internal(e.to_string()))? { if let Some(repo) = repo::Entity::find() .filter(repo::Column::Id.eq(history.repo)) .filter(repo::Column::Project.eq(project_id)) .one(self.db.reader()) .await .map_err(|e| GitError::Internal(e.to_string()))? { return Ok(repo); } } Err(GitError::NotFound("Repository not found".to_string())) } pub async fn find_user_by_public_key( &self, public_key_str: &str, ) -> Result, DbErr> { let fingerprint = match self.generate_fingerprint_from_public_key(public_key_str) { Ok(fp) => fp, Err(e) => { error!(self.logger, "Failed to generate fingerprint"; "error" => %e); return Ok(None); } }; let fingerprint_preview = if fingerprint.len() > 16 { format!("{}...", &fingerprint[..16]) } else { fingerprint.clone() }; info!(self.logger, "Looking up user with SSH key"; "fingerprint" => %fingerprint_preview); let ssh_key = user_ssh_key::Entity::find() .filter(user_ssh_key::Column::Fingerprint.eq(&fingerprint)) .filter(user_ssh_key::Column::IsRevoked.eq(false)) .one(self.db.reader()) .await?; let ssh_key = match ssh_key { Some(key) => key, None => { warn!(self.logger, "No SSH key found"; "fingerprint" => %fingerprint); return Ok(None); } }; if self.is_key_expired(&ssh_key) { warn!(self.logger, "SSH key expired"; "key_id" => ssh_key.id, "expires_at" => ?ssh_key.expires_at); return Ok(None); } let user_model = user::Entity::find() .filter(user::Column::Uid.eq(ssh_key.user)) .one(self.db.reader()) .await?; if let Some(ref user) = user_model { info!(self.logger, "User authenticated"; "user" => %user.username, "key" => %ssh_key.title); self.update_key_last_used_async(ssh_key.id); } Ok(user_model) } fn is_key_expired(&self, ssh_key: &user_ssh_key::Model) -> bool { if let Some(expires_at) = ssh_key.expires_at { let now = chrono::Utc::now(); now >= expires_at } else { false } } fn update_key_last_used_async(&self, key_id: i64) { let db_clone = self.db.clone(); let logger = self.logger.clone(); tokio::spawn(async move { if let Err(e) = Self::update_key_last_used_sync(db_clone, &logger, key_id).await { warn!(&logger, "Failed to update key last_used"; "key_id" => key_id, "error" => %e); } }); } async fn update_key_last_used_sync( db: AppDatabase, logger: &Logger, key_id: i64, ) -> Result<(), DbErr> { let key = user_ssh_key::Entity::find_by_id(key_id) .one(db.reader()) .await?; if let Some(key) = key { let now = chrono::Utc::now(); let mut active_key: user_ssh_key::ActiveModel = key.into(); active_key.last_used_at = Set(Some(now)); active_key.updated_at = Set(now); active_key.update(db.writer()).await?; info!(logger, "Updated key last_used"; "key_id" => key_id); } Ok(()) } pub async fn check_repo_permission( &self, user: &user::Model, repo: &repo::Model, is_write: bool, ) -> bool { if repo.created_by == user.uid { info!(self.logger, "User is repo owner"; "user" => %user.username, "repo" => %repo.repo_name); return true; } if !is_write && !repo.is_private { info!(self.logger, "Public repo allows read"; "repo" => %repo.repo_name); return true; } if self .check_collaborator_permission(user, repo, is_write) .await .unwrap_or(false) { info!(self.logger, "User has collaborator access"; "user" => %user.username, "repo" => %repo.repo_name); return true; } let project_id = repo.project; if self .check_project_member_permission(user, project_id, is_write) .await .unwrap_or(false) { info!(self.logger, "User has project member access"; "user" => %user.username, "repo" => %repo.repo_name); return true; } warn!(self.logger, "Access denied"; "user" => %user.username, "repo" => %repo.repo_name, "write" => is_write); false } async fn check_collaborator_permission( &self, user: &user::Model, repo: &repo::Model, is_write: bool, ) -> Result { use models::repos::repo_collaborator; let collaborator = repo_collaborator::Entity::find() .filter(repo_collaborator::Column::Repo.eq(repo.id)) .filter(repo_collaborator::Column::User.eq(user.uid)) .one(self.db.reader()) .await?; if let Some(collab) = collaborator { let roles: Vec<&str> = collab.scope.split_whitespace().collect(); if roles.contains(&"admin") || roles.contains(&"write") { return Ok(true); } if roles.contains(&"read") && !is_write { return Ok(true); } warn!(self.logger, "Collaborator has no valid roles"; "scope" => %collab.scope); Ok(false) } else { Ok(false) } } async fn check_project_member_permission( &self, user: &user::Model, project_id: uuid::Uuid, is_write: bool, ) -> Result { let member = project_members::Entity::find() .filter(project_members::Column::Project.eq(project_id)) .filter(project_members::Column::User.eq(user.uid)) .one(self.db.reader()) .await?; if let Some(member) = member { match member.scope_role() { Ok(MemberRole::Admin) | Ok(MemberRole::Owner) => Ok(true), Ok(MemberRole::Member) => Ok(!is_write), Err(_) => Ok(false), } } else { Ok(false) } } fn generate_fingerprint_from_public_key(&self, public_key_str: &str) -> Result { // Performance: avoid allocating Vec, use split_once for efficiency let key_data_base64 = public_key_str .split_whitespace() .nth(1) .ok_or("Invalid SSH key format")?; let key_data = general_purpose::STANDARD .decode(key_data_base64) .map_err(|e| format!("Base64 decode error: {}", e))?; // Performance: SHA256 is already optimized, compute hash directly let mut hasher = Sha256::new(); hasher.update(&key_data); let hash = hasher.finalize(); // Performance: pre-allocate string capacity to avoid reallocation let mut fingerprint = String::with_capacity(51); // "SHA256:" (7) + base64 (44) fingerprint.push_str("SHA256:"); fingerprint.push_str(&general_purpose::STANDARD_NO_PAD.encode(&hash)); Ok(fingerprint) } }