From 24d4cca670fae4d1a21fb8285f9667ace9dea402 Mon Sep 17 00:00:00 2001 From: Mari Date: Sat, 4 Oct 2025 08:32:55 +0200 Subject: [PATCH] add `/{username}.keys` route to list user ssh keys --- src/routes/user/mod.rs | 3 ++ src/routes/user/user_keys.rs | 24 ++++++++++++++++ src/ssh.rs | 22 +++++++++++++- src/user.rs | 56 ++++++++++++++++++++++++++++++++++-- 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 src/routes/user/user_keys.rs diff --git a/src/routes/user/mod.rs b/src/routes/user/mod.rs index f3c9d57..1f63ccc 100644 --- a/src/routes/user/mod.rs +++ b/src/routes/user/mod.rs @@ -4,6 +4,7 @@ mod api; mod avatar; mod sso; mod user_create; +mod user_keys; mod user_login; mod user_logout; mod user_verify; @@ -25,4 +26,6 @@ pub(crate) fn init(config: &mut ServiceConfig) { config.service(sso::initiate_sso); config.service(sso::sso_callback); + + config.service(user_keys::get_keys); } diff --git a/src/routes/user/user_keys.rs b/src/routes/user/user_keys.rs new file mode 100644 index 0000000..11a8d02 --- /dev/null +++ b/src/routes/user/user_keys.rs @@ -0,0 +1,24 @@ +use crate::ssh::SshKey; +use crate::user::User; +use actix_web::{web, Responder}; +use gitarena_macros::route; +use itertools::Itertools; +use sqlx::PgPool; + +#[route("/{user}.keys", method = "GET", err = "text")] +pub(crate) async fn get_keys( + user: User, + db_pool: web::Data, +) -> anyhow::Result { + let mut transaction = db_pool.begin().await?; + + let result = match SshKey::all_from_user(&user, &mut transaction).await { + Some(keys) if keys.is_empty() => String::new(), + Some(keys) => keys.into_iter().map(|key| key.as_string()).join("\n"), + _ => String::new(), + }; + + transaction.commit().await?; + + Ok(result) +} diff --git a/src/ssh.rs b/src/ssh.rs index 516d4ce..0ca4d6d 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -1,8 +1,9 @@ +use crate::user::User; use chrono::{DateTime, Utc}; use derive_more::Display; use gitarena_common::database::models::KeyType; use serde::Serialize; -use sqlx::FromRow; +use sqlx::{Executor, FromRow, Postgres}; #[derive(FromRow, Display, Debug, Serialize)] #[display(fmt = "{}", title)] @@ -16,3 +17,22 @@ pub(crate) struct SshKey { pub(crate) created_at: DateTime, pub(crate) expires_at: Option>, } + +impl SshKey { + pub(crate) async fn all_from_user<'e, E>(user: &User, executor: E) -> Option> + where + E: Executor<'e, Database = Postgres>, + { + let keys = sqlx::query_as::<_, SshKey>("select * from ssh_keys where owner = $1") + .bind(user.id) + .fetch_all(executor) + .await + .ok(); + + keys + } + + pub(crate) fn as_string(&self) -> String { + format!("{} {}", &self.algorithm, base64::encode(&self.key)) + } +} diff --git a/src/user.rs b/src/user.rs index 34c1ead..f16cdb5 100644 --- a/src/user.rs +++ b/src/user.rs @@ -17,6 +17,7 @@ use futures::Future; use ipnetwork::IpNetwork; use serde::Serialize; use sqlx::{Executor, FromRow, PgPool, Postgres}; +use tracing_unwrap::OptionExt; #[derive(FromRow, Display, Debug, Serialize)] #[display(fmt = "{}", username)] @@ -88,6 +89,57 @@ impl TryFrom for User { } } +impl FromRequest for User { + type Error = GitArenaError; + type Future = Pin>>>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + let match_info = req.match_info(); + + // If this method gets called from a handler that does not have username or repository in the match info + // it is safe to assume the programmer made a mistake, thus .expect_or_log is OK + let username = match_info + .get("user") + .or_else(|| match_info.get("username")) + .expect_or_log("from_request called on User despite not having user/username argument") + .to_owned(); + + match req.app_data::>() { + Some(db_pool) => { + // Data is just a wrapper around `Arc

` so .clone() is cheap + let db_pool = db_pool.clone(); + + Box::pin(async move { + extract_user_from_request(db_pool, username.as_str()) + .await + .map_err(|err| GitArenaError { + source: Arc::new(err), + display_type: ErrorDisplayType::Html, // TODO: Check whenever route is err = "html|json|git" etc... + }) + }) + } + None => Box::pin(async { + Err(GitArenaError { + source: Arc::new(anyhow!("No PgPool in application data")), + display_type: ErrorDisplayType::Html, // TODO: Check whenever route is err = "html|json|git" etc... + }) + }), + } + } +} + +async fn extract_user_from_request(db_pool: Data, username: &str) -> Result { + let mut transaction = db_pool.begin().await?; + + let user = User::find_using_name(username, &mut transaction) + .await + .ok_or_else(|| err!(NOT_FOUND, "Repository not found"))?; + + transaction.commit().await?; + + Ok(user) +} + #[derive(Debug, Display)] pub(crate) enum WebUser { Anonymous, @@ -129,7 +181,7 @@ impl FromRequest for WebUser { let db_pool = db_pool.clone(); Box::pin(async move { - extract_from_request(db_pool, id_future, ip_network, user_agent) + extract_webuser_from_request(db_pool, id_future, ip_network, user_agent) .await .map_err(|err| GitArenaError { source: Arc::new(err), @@ -147,7 +199,7 @@ impl FromRequest for WebUser { } } -async fn extract_from_request>>( +async fn extract_webuser_from_request>>( db_pool: Data, id_future: F, ip_network: IpNetwork,