diff options
Diffstat (limited to 'server/src')
| -rw-r--r-- | server/src/authorized_keys.rs | 150 | ||||
| -rw-r--r-- | server/src/main.rs | 31 | ||||
| -rw-r--r-- | server/src/tests.rs | 28 |
3 files changed, 203 insertions, 6 deletions
diff --git a/server/src/authorized_keys.rs b/server/src/authorized_keys.rs new file mode 100644 index 0000000..21651ef --- /dev/null +++ b/server/src/authorized_keys.rs @@ -0,0 +1,150 @@ +use futures::stream::TryStreamExt; +use rocket::fairing::{self, AdHoc}; +use rocket::serde::Deserialize; +use rocket::{Build, Rocket}; +use rocket_db_pools::{sqlx, Database}; +use std::borrow::Cow; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use tokio::fs; +use tokio::sync::Semaphore; + +use crate::Db; + +#[derive(Debug, Deserialize)] +pub struct Config<'a> { + authorized_keys: Cow<'a, str>, +} + +#[derive(Clone)] +struct Key { + id: u64, + user: String, + kind: String, + data: String, +} + +struct AuthorizedKeysData { + keys: Vec<Key>, +} + +pub struct AuthorizedKeys { + data: Mutex<AuthorizedKeysData>, + update_lock: Semaphore, +} + +impl AuthorizedKeys { + pub async fn new_user_key( + &self, + config: &Config<'_>, + id: u64, + user: &str, + kind: &str, + data: &str, + ) { + let key = Key { + id, + user: user.to_string(), + kind: kind.to_string(), + data: data.to_string(), + }; + + let path = PathBuf::from(config.authorized_keys.to_string()); + + let keys = { + let mut data = self.data.lock().unwrap(); + data.keys.push(key); + data.keys.clone() + }; + + self.update(path.as_path(), &keys).await.unwrap(); + } + + pub async fn del_user_key(&self, config: &Config<'_>, id: u64, user: &str) { + let path = PathBuf::from(config.authorized_keys.to_string()); + + let keys = { + let mut data = self.data.lock().unwrap(); + + if let Some(index) = data.keys.iter().position(|x| x.id == id && x.user == user) { + data.keys.remove(index); + } + data.keys.clone() + }; + + self.update(path.as_path(), &keys).await.unwrap(); + } + + async fn update(&self, path: &Path, keys: &Vec<Key>) -> std::io::Result<()> { + let mut content = String::new(); + + for key in keys { + content.push_str(format!("{} {} {}\n", key.kind, key.data, key.user).as_str()) + } + + let _one_at_a_time = self.update_lock.acquire().await; + + let tmp = path.with_extension("new"); + fs::write(&tmp, content.as_bytes()).await?; + fs::rename(tmp, path).await?; + + Ok(()) + } +} + +async fn setup_users_keys( + authorized_keys: &AuthorizedKeys, + config: &Config<'_>, + db: &Db, +) -> anyhow::Result<()> { + let keys = sqlx::query!("SELECT id,user,kind,data FROM user_keys ORDER BY id") + .fetch(&**db) + .map_ok(|r| Key { + id: r.id, + user: r.user, + kind: r.kind, + data: r.data, + }) + .try_collect::<Vec<_>>() + .await + .unwrap(); + + let path = PathBuf::from(config.authorized_keys.to_string()); + + { + let mut data = authorized_keys.data.lock().unwrap(); + data.keys = keys.clone(); + } + + authorized_keys.update(path.as_path(), &keys).await?; + + Ok(()) +} + +async fn setup_users(rocket: Rocket<Build>) -> fairing::Result { + match rocket.state::<Config>() { + Some(config) => match rocket.state::<AuthorizedKeys>() { + Some(roots) => match Db::fetch(&rocket) { + Some(db) => match setup_users_keys(roots, config, db).await { + Ok(_) => Ok(rocket), + Err(_) => Err(rocket), + }, + None => Err(rocket), + }, + None => Err(rocket), + }, + None => Err(rocket), + } +} + +pub fn stage() -> AdHoc { + AdHoc::on_ignite("Authorized Keys Stage", |rocket| async { + rocket + .manage(AuthorizedKeys { + data: Mutex::new(AuthorizedKeysData { keys: Vec::new() }), + update_lock: Semaphore::new(1), + }) + .attach(AdHoc::config::<Config>()) + .attach(AdHoc::try_on_ignite("Users setup", setup_users)) + }) +} diff --git a/server/src/main.rs b/server/src/main.rs index 298a418..3d6d0e6 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -19,6 +19,7 @@ mod tests; mod api_model; mod auth; +mod authorized_keys; mod db_utils; mod fs_utils; mod git; @@ -669,7 +670,7 @@ async fn users( #[utoipa::path( responses( (status = 200, description = "Key added to current user", body = api_model::UserKey), - (status = 400, description = "Key too large"), + (status = 400, description = "Key too large or invalid"), ), security( ("session" = []), @@ -678,12 +679,17 @@ async fn users( #[post("/user/keys/add", data = "<data>")] async fn user_key_add( mut db: Connection<Db>, + authorized_keys_config: &State<authorized_keys::Config<'_>>, + authorized_keys_state: &State<authorized_keys::AuthorizedKeys>, session: auth::Session, data: Json<api_model::UserKeyData<'_>>, ) -> Result<Json<api_model::UserKey>, Custom<&'static str>> { if data.data.len() > 8192 { return Err(Custom(Status::BadRequest, "Key is too large")); } + if data.kind.contains(' ') || data.data.contains(' ') { + return Err(Custom(Status::BadRequest, "Invalid kind or data")); + } let comment = data.comment.unwrap_or(""); let result = sqlx::query!( @@ -697,12 +703,24 @@ async fn user_key_add( .await .unwrap(); - Ok(Json(api_model::UserKey { + let key = api_model::UserKey { id: result.last_insert_id(), kind: data.kind.to_string(), data: data.data.to_string(), comment: comment.to_string(), - })) + }; + + authorized_keys_state + .new_user_key( + authorized_keys_config, + key.id, + session.user_id.as_str(), + key.kind.as_str(), + key.data.as_str(), + ) + .await; + + Ok(Json(key)) } #[utoipa::path( @@ -750,6 +768,8 @@ async fn user_key_get( #[delete("/user/keys/<id>")] async fn user_key_del( mut db: Connection<Db>, + authorized_keys_config: &State<authorized_keys::Config<'_>>, + authorized_keys_state: &State<authorized_keys::AuthorizedKeys>, session: auth::Session, id: u64, ) -> Result<&'static str, Custom<&'static str>> { @@ -765,6 +785,10 @@ async fn user_key_del( return Err(Custom(Status::NotFound, "No such key for current user")); } + authorized_keys_state + .del_user_key(authorized_keys_config, id, session.user_id.as_str()) + .await; + Ok("") } @@ -863,6 +887,7 @@ fn rocket_from_config(figment: Figment) -> Rocket<Build> { ) .attach(auth::stage(basepath)) .attach(git_root::stage()) + .attach(authorized_keys::stage()) } #[rocket::main] diff --git a/server/src/tests.rs b/server/src/tests.rs index 4567c49..d59dcbe 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -116,10 +116,12 @@ async fn async_client_with_private_database(test_name: String) -> Client { }; let git_root = testdir!(); + let authorized_keys = git_root.join("authorized_keys"); let figment = base_figment .merge(("databases", map!["eyeballs" => db_config])) - .merge(("git_server_root", git_root)); + .merge(("git_server_root", git_root)) + .merge(("authorized_keys", authorized_keys)); Client::tracked(crate::rocket_from_config(figment)) .await @@ -883,8 +885,8 @@ async fn test_user_keys_add() { let key4 = get_user_key_from(client.post("/api/v1/user/keys/add").json( &api_model::UserKeyData { - kind: "another kind", - data: "more data", + kind: "another-kind", + data: "more.data", comment: Some("comment"), }, )) @@ -901,6 +903,26 @@ async fn test_user_keys_add() { } #[rocket::async_test] +async fn test_user_keys_add_invalid() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + login(&client).await; + + let result = client + .post("/api/v1/user/keys/add") + .json(&api_model::UserKeyData { + kind: "kind with space", + data: "data with space", + comment: None, + }) + .header(&FAKE_IP) + .dispatch() + .await; + + assert_eq!(result.status(), Status::BadRequest); +} + +#[rocket::async_test] async fn test_user_keys_del() { let client = async_client_with_private_database(function_name!().to_string()).await; |
