use std::str::FromStr; use russh::server::Auth; use tokio::task; use db::conn::sea_orm::{EntityTrait, QueryFilter, Set}; use db::user::user_ssh_keys::{self, Entity as SshKeyEntity}; use db::user::users::{self, Entity as UserEntity}; use uuid::Uuid; use crate::handler::GitSshHandler; pub struct SshKeyUser { pub user: users::Model, pub key_id: Uuid, } impl GitSshHandler { /// Find the user whose stored SSH key matches the presented public key. /// The workspace stores OpenSSH public key text (content column) and a /// sha256_hex(content bytes) fingerprint; gitssh must parse the stored /// content and compare with the presented key directly. async fn find_user_by_public_key( &self, username: &str, public_key: &ssh_key::PublicKey, ) -> Result, db::conn::SqlxError> { let db = self.service.auth.db.reader(); let Some(user) = UserEntity::find() .filter(users::COLUMN.lower_name.eq(username.to_lowercase())) .one(&db) .await? else { return Ok(None); }; if !user.is_active { return Ok(None); } let keys = SshKeyEntity::find() .filter(user_ssh_keys::COLUMN.user_id.eq(user.uid)) .all(&db) .await?; let presented = public_key.to_string(); for key in keys { let matched = ssh_key::PublicKey::from_str(key.content.trim()) .ok() .map_or_else( || key.content.trim() == presented.trim(), |p| p == *public_key, ); if matched { return Ok(Some(SshKeyUser { user, key_id: key.id, })); } } Ok(None) } fn update_key_last_used(&self, key_id: Uuid) { let db = self.service.auth.db.writer(); let now = time::OffsetDateTime::now_utc(); task::spawn(async move { let _ = SshKeyEntity::update_many() .set(user_ssh_keys::ActiveModel { last_used_at: Set(Some(now)), ..Default::default() }) .filter(user_ssh_keys::COLUMN.id.eq(key_id)) .exec(&db) .await; }); } pub async fn auth_publickey_offered_impl( &mut self, user: &str, public_key: &ssh_key::PublicKey, ) -> Result { if user.is_empty() || public_key.to_string().len() < 32 { return Ok(Auth::reject()); } match self.find_user_by_public_key(user, public_key).await { Ok(Some(_)) => Ok(Auth::Accept), Ok(None) => Ok(Auth::reject()), Err(e) => { tracing::warn!("ssh auth lookup failed: {e}"); Err(russh::Error::NotAuthenticated) } } } pub async fn auth_publickey_impl( &mut self, user: &str, public_key: &ssh_key::PublicKey, ) -> Result { if user.is_empty() || public_key.to_string().len() < 32 { return Ok(Auth::reject()); } match self.find_user_by_public_key(user, public_key).await { Ok(Some(found)) => { let key_id = found.key_id; self.operator = Some(found.user); self.update_key_last_used(key_id); Ok(Auth::Accept) } Ok(None) => Ok(Auth::reject()), Err(e) => { tracing::warn!("ssh auth lookup failed: {e}"); Err(russh::Error::NotAuthenticated) } } } }