fc95f38
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<Option<SshKeyUser>, 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<Auth, russh::Error> {
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<Auth, russh::Error> {
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)
}
}
}
}