2ae9196
ZhenYi
feat(lib/git): add HTTP handler tree with routes and errors across analysis, archives, blobs, branches, commits, conflicts, content, diffs, hooks, lfs, logs, maintenance, packs, patches, pipelines, refs, remotes, repos, signatures, submodules, tags, trees
2ae9196
use std::str::FromStr;
use models::users::user;
use models::users::user_ssh_keys;
use russh::keys::PublicKey;
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,
}
impl GitSshHandler {
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();
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?;
let presented = public_key.to_string();
for key in keys {
let matched = 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.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> {
if self.over_capacity {
return Ok(Auth::reject());
}
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: &PublicKey,
) -> Result<Auth, russh::Error> {
if self.over_capacity {
return Ok(Auth::reject());
}
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)) => {
if found.user.is_restricted || found.user.prohibit_login {
return Ok(Auth::reject());
}
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)
}
}
}
}