Skip to content
Clone
39d7e6b
188 lines 6.7 KB Rust
use std::str::FromStr;
use models::users::user;
use models::users::user_ssh_keys;
use russh::keys::PublicKey;
use russh::keys::ssh_key::HashAlg;
use russh::server::Auth;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set};
use time::OffsetDateTime;
use tokio::task;
use uuid::Uuid;
use crate::ssh::handler::GitSshHandler;
struct SshKeyUser {
user: user::Model,
key_id: Uuid,
}
fn key_content_matches(content: &str, public_key: &PublicKey) -> bool {
match PublicKey::from_str(content.trim()) {
// Compare fingerprints: OpenSSH may offer RSA keys under rsa-sha2-*
// algorithm names, so direct equality against a stored ssh-rsa line
// can fail even for the same key material.
Ok(p) => p.fingerprint(HashAlg::Sha256) == public_key.fingerprint(HashAlg::Sha256),
Err(_) => content.trim() == public_key.to_string().trim(),
}
}
impl GitSshHandler {
/// Resolve the account owning `public_key`. The SSH username is tried
/// first for exact-name accounts, then authentication falls back to a
/// global key match so virtual accounts (e.g. `git@host`, matching the
/// `ssh_clone_user` in clone URLs) identify users by key like GitHub.
async fn find_user_by_public_key(
&self,
username: &str,
public_key: &PublicKey,
) -> Result<Option<SshKeyUser>, sea_orm::DbErr> {
let db = self.service.smart.git.db_read();
if let Some(found) = self.find_user_key_by_name(db, username, public_key).await? {
return Ok(Some(found));
}
let keys = user_ssh_keys::Entity::find().all(db).await?;
for key in keys {
if !key_content_matches(key.content.as_str(), public_key) {
continue;
}
let Some(user) = user::Entity::find_by_id(key.user_id).one(db).await? else {
continue;
};
if !user.is_active {
continue;
}
return Ok(Some(SshKeyUser {
user,
key_id: key.id,
}));
}
Ok(None)
}
async fn find_user_key_by_name(
&self,
db: &sea_orm::DatabaseConnection,
username: &str,
public_key: &PublicKey,
) -> Result<Option<SshKeyUser>, sea_orm::DbErr> {
let Some(user) = user::Entity::find()
.filter(user::Column::LowerName.eq(username.to_lowercase()))
.one(db)
.await?
else {
return Ok(None);
};
if !user.is_active {
return Ok(None);
}
let keys = user_ssh_keys::Entity::find()
.filter(user_ssh_keys::Column::UserId.eq(user.uid))
.all(db)
.await?;
for key in keys {
if key_content_matches(key.content.as_str(), public_key) {
return Ok(Some(SshKeyUser {
user,
key_id: key.id,
}));
}
}
Ok(None)
}
fn update_key_last_used(&self, key_id: Uuid) {
let db = self.service.smart.git.state.database.clone();
let now = OffsetDateTime::now_utc();
task::spawn(async move {
let _ = user_ssh_keys::Entity::update_many()
.set(user_ssh_keys::ActiveModel {
id: Set(key_id),
last_used_at: Set(Some(now)),
..Default::default()
})
.filter(user_ssh_keys::Column::Id.eq(key_id))
.exec(db.write())
.await;
});
}
pub async fn auth_publickey_offered_impl(
&mut self,
user: &str,
public_key: &PublicKey,
) -> Result<Auth, russh::Error> {
let fingerprint = public_key.fingerprint(HashAlg::Sha256).to_string();
if self.over_capacity {
tracing::debug!(user, %fingerprint, "publickey offer rejected: server over capacity");
return Ok(Auth::reject());
}
if user.is_empty() || public_key.to_string().len() < 32 {
tracing::debug!(user, %fingerprint, "publickey offer rejected: malformed request");
return Ok(Auth::reject());
}
match self.find_user_by_public_key(user, public_key).await {
Ok(Some(_)) => {
tracing::debug!(user, %fingerprint, "publickey offer matched a known key");
Ok(Auth::Accept)
}
Ok(None) => {
tracing::info!(user, %fingerprint, client = ?self.client_addr, "publickey offer rejected: key not registered");
Ok(Auth::reject())
}
Err(e) => {
tracing::warn!(user, %fingerprint, "ssh auth lookup failed: {e}");
Err(russh::Error::NotAuthenticated)
}
}
}
pub async fn auth_publickey_impl(
&mut self,
user: &str,
public_key: &PublicKey,
) -> Result<Auth, russh::Error> {
let fingerprint = public_key.fingerprint(HashAlg::Sha256).to_string();
if self.over_capacity {
tracing::debug!(user, %fingerprint, "publickey auth rejected: server over capacity");
return Ok(Auth::reject());
}
if user.is_empty() || public_key.to_string().len() < 32 {
tracing::debug!(user, %fingerprint, "publickey auth rejected: malformed request");
return Ok(Auth::reject());
}
match self.find_user_by_public_key(user, public_key).await {
Ok(Some(found)) => {
if found.user.is_restricted || found.user.prohibit_login {
tracing::warn!(
user,
user_id = %found.user.uid,
%fingerprint,
"publickey auth rejected: user restricted or login prohibited"
);
return Ok(Auth::reject());
}
let key_id = found.key_id;
tracing::info!(
user,
user_id = %found.user.uid,
key_id = %key_id,
%fingerprint,
client = ?self.client_addr,
"ssh publickey auth accepted"
);
self.operator = Some(found.user);
self.update_key_last_used(key_id);
Ok(Auth::Accept)
}
Ok(None) => {
tracing::info!(user, %fingerprint, client = ?self.client_addr, "publickey auth rejected: key not registered");
Ok(Auth::reject())
}
Err(e) => {
tracing::warn!(user, %fingerprint, "ssh auth lookup failed: {e}");
Err(russh::Error::NotAuthenticated)
}
}
}
}