summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/.sqlx/query-d3caae95146dcdca80546cec471c0a3a51f9456fac0b6db299803bc374fce458.json58
-rw-r--r--server/Rocket.toml1
-rw-r--r--server/src/authorized_keys.rs150
-rw-r--r--server/src/main.rs31
-rw-r--r--server/src/tests.rs28
5 files changed, 262 insertions, 6 deletions
diff --git a/server/.sqlx/query-d3caae95146dcdca80546cec471c0a3a51f9456fac0b6db299803bc374fce458.json b/server/.sqlx/query-d3caae95146dcdca80546cec471c0a3a51f9456fac0b6db299803bc374fce458.json
new file mode 100644
index 0000000..001f82c
--- /dev/null
+++ b/server/.sqlx/query-d3caae95146dcdca80546cec471c0a3a51f9456fac0b6db299803bc374fce458.json
@@ -0,0 +1,58 @@
+{
+ "db_name": "MySQL",
+ "query": "SELECT id,user,kind,data FROM user_keys ORDER BY id",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": {
+ "type": "LongLong",
+ "flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
+ "char_set": 63,
+ "max_size": 20
+ }
+ },
+ {
+ "ordinal": 1,
+ "name": "user",
+ "type_info": {
+ "type": "VarString",
+ "flags": "NOT_NULL | MULTIPLE_KEY | NO_DEFAULT_VALUE",
+ "char_set": 224,
+ "max_size": 512
+ }
+ },
+ {
+ "ordinal": 2,
+ "name": "kind",
+ "type_info": {
+ "type": "VarString",
+ "flags": "NOT_NULL | NO_DEFAULT_VALUE",
+ "char_set": 224,
+ "max_size": 512
+ }
+ },
+ {
+ "ordinal": 3,
+ "name": "data",
+ "type_info": {
+ "type": "VarString",
+ "flags": "NOT_NULL | NO_DEFAULT_VALUE",
+ "char_set": 224,
+ "max_size": 32768
+ }
+ }
+ ],
+ "parameters": {
+ "Right": 0
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "d3caae95146dcdca80546cec471c0a3a51f9456fac0b6db299803bc374fce458"
+}
diff --git a/server/Rocket.toml b/server/Rocket.toml
index d0d29bf..0abe363 100644
--- a/server/Rocket.toml
+++ b/server/Rocket.toml
@@ -5,6 +5,7 @@ ldap_url = "ldap://localhost:1389"
ldap_users = "ou=users,dc=example,dc=org"
ldap_filter = "(objectClass=posixAccount)"
git_server_root = "git_server"
+authorized_keys = "authorized_keys"
[default.databases.eyeballs]
# root is needed for tests
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;