From e940d84f69e3fd627731d5d3f698d6f838797862 Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Thu, 20 Feb 2025 22:53:27 +0100 Subject: WIP --- ...d5f94b139d88355059390a7a34d14754169f7cc0e8.json | 58 ++++++++++ ...2eecae70925916336adf7c5062e0b36a6229b3f252.json | 47 -------- ...e206ea36137a50e7b8871751c631edb738db2cd197.json | 69 ----------- ...dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f.json | 12 -- ...b01ece0643fc1acf182f201a691619de6c092b8445.json | 80 +++++++++++++ ...43043f3633c46854684625ef5f7a77a79c12c86961.json | 12 ++ server/Cargo.lock | 24 ++-- server/Cargo.toml | 5 +- server/api/src/api_model.rs | 6 +- server/common/Cargo.toml | 1 + server/common/src/git.rs | 14 +++ server/common/src/tests.rs | 2 + server/hook/Cargo.toml | 1 + server/hook/src/githook.rs | 1 + server/migrations/1_initial_eyeballs.sql | 1 + server/src/authorized_keys.rs | 17 ++- server/src/git_root.rs | 128 +++++++++++++++++++-- server/src/main.rs | 58 +++++++++- server/src/tests.rs | 8 ++ server/tests/common/mod.rs | 29 +++-- server/tests/integration_test.rs | 4 +- 21 files changed, 411 insertions(+), 166 deletions(-) create mode 100644 server/.sqlx/query-11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8.json delete mode 100644 server/.sqlx/query-2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252.json delete mode 100644 server/.sqlx/query-51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197.json delete mode 100644 server/.sqlx/query-a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f.json create mode 100644 server/.sqlx/query-dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445.json create mode 100644 server/.sqlx/query-fb92a51da21e3ec04b1f5543043f3633c46854684625ef5f7a77a79c12c86961.json (limited to 'server') diff --git a/server/.sqlx/query-11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8.json b/server/.sqlx/query-11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8.json new file mode 100644 index 0000000..13a9155 --- /dev/null +++ b/server/.sqlx/query-11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8.json @@ -0,0 +1,58 @@ +{ + "db_name": "MySQL", + "query": "SELECT id,remote,remote_key,main_branch FROM projects", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | PRIMARY_KEY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 512 + } + }, + { + "ordinal": 1, + "name": "remote", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 8192 + } + }, + { + "ordinal": 2, + "name": "remote_key", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 32768 + } + }, + { + "ordinal": 3, + "name": "main_branch", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 4096 + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8" +} diff --git a/server/.sqlx/query-2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252.json b/server/.sqlx/query-2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252.json deleted file mode 100644 index 97d107d..0000000 --- a/server/.sqlx/query-2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "db_name": "MySQL", - "query": "SELECT id,remote,main_branch FROM projects", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | PRIMARY_KEY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 512 - } - }, - { - "ordinal": 1, - "name": "remote", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 8192 - } - }, - { - "ordinal": 2, - "name": "main_branch", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 4096 - } - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252" -} diff --git a/server/.sqlx/query-51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197.json b/server/.sqlx/query-51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197.json deleted file mode 100644 index 942c054..0000000 --- a/server/.sqlx/query-51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "db_name": "MySQL", - "query": "SELECT id,title,description,remote,main_branch FROM projects WHERE id=?", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | PRIMARY_KEY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 512 - } - }, - { - "ordinal": 1, - "name": "title", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 4096 - } - }, - { - "ordinal": 2, - "name": "description", - "type_info": { - "type": "Blob", - "flags": "NOT_NULL | BLOB", - "char_set": 224, - "max_size": 67108860 - } - }, - { - "ordinal": 3, - "name": "remote", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 8192 - } - }, - { - "ordinal": 4, - "name": "main_branch", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 4096 - } - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false - ] - }, - "hash": "51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197" -} diff --git a/server/.sqlx/query-a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f.json b/server/.sqlx/query-a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f.json deleted file mode 100644 index 6f761ee..0000000 --- a/server/.sqlx/query-a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "MySQL", - "query": "INSERT INTO projects (id, title, description, remote, main_branch) VALUES (?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f" -} diff --git a/server/.sqlx/query-dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445.json b/server/.sqlx/query-dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445.json new file mode 100644 index 0000000..3379f2a --- /dev/null +++ b/server/.sqlx/query-dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445.json @@ -0,0 +1,80 @@ +{ + "db_name": "MySQL", + "query": "SELECT id,title,description,remote,remote_key,main_branch FROM projects WHERE id=?", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | PRIMARY_KEY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 512 + } + }, + { + "ordinal": 1, + "name": "title", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 4096 + } + }, + { + "ordinal": 2, + "name": "description", + "type_info": { + "type": "Blob", + "flags": "NOT_NULL | BLOB", + "char_set": 224, + "max_size": 67108860 + } + }, + { + "ordinal": 3, + "name": "remote", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 8192 + } + }, + { + "ordinal": 4, + "name": "remote_key", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 32768 + } + }, + { + "ordinal": 5, + "name": "main_branch", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 4096 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445" +} diff --git a/server/.sqlx/query-fb92a51da21e3ec04b1f5543043f3633c46854684625ef5f7a77a79c12c86961.json b/server/.sqlx/query-fb92a51da21e3ec04b1f5543043f3633c46854684625ef5f7a77a79c12c86961.json new file mode 100644 index 0000000..92f794c --- /dev/null +++ b/server/.sqlx/query-fb92a51da21e3ec04b1f5543043f3633c46854684625ef5f7a77a79c12c86961.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "INSERT INTO projects (id, title, description, remote, remote_key, main_branch) VALUES (?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "fb92a51da21e3ec04b1f5543043f3633c46854684625ef5f7a77a79c12c86961" +} diff --git a/server/Cargo.lock b/server/Cargo.lock index e44bc77..9394da3 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -272,9 +272,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.12" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" +checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" dependencies = [ "shlex", ] @@ -582,6 +582,7 @@ dependencies = [ "eyeballs-common", "futures", "ldap3", + "log", "reqwest", "rmp-serde", "rocket", @@ -593,6 +594,7 @@ dependencies = [ "testdir", "time", "tokio", + "url", "utoipa", "utoipa-swagger-ui", ] @@ -610,6 +612,7 @@ name = "eyeballs-common" version = "0.1.0" dependencies = [ "futures", + "log", "pathdiff", "serde", "testdir", @@ -621,6 +624,7 @@ name = "eyeballs-githook" version = "0.1.0" dependencies = [ "eyeballs-common", + "log", "rmp-serde", "serde", "tokio", @@ -1461,9 +1465,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" dependencies = [ "adler2", ] @@ -1618,9 +1622,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "opaque-debug" @@ -3041,9 +3045,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.23" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "serde", @@ -3692,9 +3696,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" +checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" dependencies = [ "memchr", ] diff --git a/server/Cargo.toml b/server/Cargo.toml index b9d0488..f8bab3c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -11,6 +11,7 @@ resolver = "2" [workspace.dependencies] futures = "0.3.31" +log = { version = "0.4.25", features = ["release_max_level_warn"] } rmp-serde = "1.3" serde = { version = "1.0", features = ["derive"] } testdir = "0.9.3" @@ -23,6 +24,7 @@ eyeballs-api = { path = "api" } eyeballs-common = { path = "common" } futures.workspace = true ldap3 = { version = "0.11.5", default-features = false, features = [ "native-tls", "tls", "tls-native", "tokio-native-tls" ] } +log.workspace = true rmp-serde.workspace = true rocket = { version = "0.5.1", features = ["json", "secrets"] } rocket_db_pools = { version = "0.2.0", features = ["sqlx_mysql"] } @@ -30,8 +32,9 @@ serde.workspace = true sqlx = { version = "0.7.0", default-features = false, features = ["macros", "migrate"] } time = "0.3.34" tokio = { workspace = true, features = ["process"] } +url = "2.5.4" utoipa = { workspace = true, features = ["rocket_extras"] } -utoipa-swagger-ui = { version = "9", features = ["rocket", "vendored"], default-features = false } +utoipa-swagger-ui = { version = "9", features = ["debug-embed", "rocket", "vendored"], default-features = false } [dev-dependencies] reqwest = { version = "0.12.12", features = ["cookies", "json"], default-features = false } diff --git a/server/api/src/api_model.rs b/server/api/src/api_model.rs index 3760f9e..d769a98 100644 --- a/server/api/src/api_model.rs +++ b/server/api/src/api_model.rs @@ -178,6 +178,8 @@ pub struct Project { pub description: String, #[schema(example = "ssh://git.example.org/srv/git/")] pub remote: String, + #[schema(example = "b3BlbNNz...AQIDBA==")] + pub remote_key_abbrev: String, #[schema(example = "main")] pub main_branch: String, pub users: Vec, @@ -191,6 +193,8 @@ pub struct ProjectData<'r> { pub description: Option<&'r str>, #[schema(example = "ssh://git.example.org/srv/git/")] pub remote: Option<&'r str>, + #[schema(example = "b3BlbNNz...AQIDBA==")] + pub remote_key: Option, #[schema(example = "main")] pub main_branch: Option<&'r str>, } @@ -244,7 +248,7 @@ pub struct UserKeyData<'r> { #[schema(example = "ssh-rsa")] pub kind: &'r str, #[schema(example = "AAAAfoobar==")] - pub data: &'r str, + pub data: String, #[schema(example = "user@host 1970-01-01")] pub comment: Option<&'r str>, } diff --git a/server/common/Cargo.toml b/server/common/Cargo.toml index aa358b2..fdabffa 100644 --- a/server/common/Cargo.toml +++ b/server/common/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] futures.workspace = true +log.workspace = true pathdiff = "0.2.3" serde.workspace = true tokio = { workspace = true, features = ["fs", "macros", "process", "rt", "sync"] } diff --git a/server/common/src/git.rs b/server/common/src/git.rs index ac0bdb3..02a9c0a 100644 --- a/server/common/src/git.rs +++ b/server/common/src/git.rs @@ -49,6 +49,7 @@ pub struct Repository { project_id: Option, socket: Option, githook: Option, + ssh_config: Option, // Lock for any repo task, 90% of all tasks are readers but there are some writers // where nothing else may be done. @@ -138,6 +139,12 @@ impl RepoData { self.config_set(repo, "eyeballs.socket", relative.to_str().unwrap()) .await?; } + if let Some(ssh_config) = repo.ssh_config() { + let relative = diff_paths(ssh_config, repo.path()).unwrap(); + self.config_set(repo, "core.sshcommand", + format!("ssh -F {}", relative.to_str().unwrap()).as_str()) + .await?; + } // Handled by pre-receive hook, allow fast forwards for reviews that expect it. self.config_set(repo, "receive.denyNonFastForwards", "false") @@ -454,10 +461,12 @@ impl Repository { remote: Option>, project_id: Option>, githook: Option>, + ssh_config: Option>, ) -> Self { let path = path.into(); let project_id = project_id.map(|x| x.into()); let githook = githook.map(|x| x.into()); + let ssh_config = ssh_config.map(|x| x.into()); let socket: Option; if let Some(project_id) = &project_id { socket = Some( @@ -475,6 +484,7 @@ impl Repository { path, socket, githook, + ssh_config, bare, lock: RwLock::new(RepoData::new()), } @@ -500,6 +510,10 @@ impl Repository { self.githook.as_deref() } + pub fn ssh_config(&self) -> Option<&Path> { + self.ssh_config.as_deref() + } + pub fn is_bare(&self) -> bool { self.bare } diff --git a/server/common/src/tests.rs b/server/common/src/tests.rs index 41f44fe..5a02119 100644 --- a/server/common/src/tests.rs +++ b/server/common/src/tests.rs @@ -59,6 +59,7 @@ async fn git_setup(bare: bool) -> git::Repository { None::, None::, None::, + None::, ); assert_eq!(repo.remote(), None); assert_eq!(repo.project_id(), None); @@ -212,6 +213,7 @@ async fn git_fetch(bare: bool) -> git::Repository { Some(remote.as_str()), None::, None::, + None::, ); assert_eq!(repo.remote(), Some(remote.as_str())); repo.setup().await.unwrap(); diff --git a/server/hook/Cargo.toml b/server/hook/Cargo.toml index 2a298b7..29663e4 100644 --- a/server/hook/Cargo.toml +++ b/server/hook/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] eyeballs-common = { path = "../common" } +log.workspace = true rmp-serde.workspace = true serde.workspace = true tokio = { workspace = true, features = ["full"] } diff --git a/server/hook/src/githook.rs b/server/hook/src/githook.rs index a9cb898..3a27e2c 100644 --- a/server/hook/src/githook.rs +++ b/server/hook/src/githook.rs @@ -57,6 +57,7 @@ async fn main() -> Result<(), Box> { None::, None::, None::, + None::, ); while let Some(line) = lines.next_line().await? { diff --git a/server/migrations/1_initial_eyeballs.sql b/server/migrations/1_initial_eyeballs.sql index 728c329..a39202d 100644 --- a/server/migrations/1_initial_eyeballs.sql +++ b/server/migrations/1_initial_eyeballs.sql @@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS projects ( title VARCHAR(1024) NOT NULL, description MEDIUMTEXT NOT NULL DEFAULT '', remote VARCHAR(2048) NOT NULL, + remote_key VARCHAR(8192) NOT NULL, main_branch VARCHAR(1024) NOT NULL ); diff --git a/server/src/authorized_keys.rs b/server/src/authorized_keys.rs index 21651ef..81885b3 100644 --- a/server/src/authorized_keys.rs +++ b/server/src/authorized_keys.rs @@ -1,9 +1,11 @@ use futures::stream::TryStreamExt; +use log::{error, info}; 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::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::sync::Mutex; use tokio::fs; @@ -86,8 +88,18 @@ impl AuthorizedKeys { let tmp = path.with_extension("new"); fs::write(&tmp, content.as_bytes()).await?; + if let Ok(metadata) = fs::metadata(path).await { + // Try to replicate ownership and permissions of original file + fs::set_permissions(&tmp, metadata.permissions()) + .await + .unwrap_or(()); + std::os::unix::fs::chown(&tmp, Some(metadata.uid()), Some(metadata.gid())) + .unwrap_or(()); + } fs::rename(tmp, path).await?; + info!("Updated {path:?}, {} keys", keys.len()); + Ok(()) } } @@ -127,7 +139,10 @@ async fn setup_users(rocket: Rocket) -> fairing::Result { Some(roots) => match Db::fetch(&rocket) { Some(db) => match setup_users_keys(roots, config, db).await { Ok(_) => Ok(rocket), - Err(_) => Err(rocket), + Err(e) => { + error!("{e:?}"); + Err(rocket) + } }, None => Err(rocket), }, diff --git a/server/src/git_root.rs b/server/src/git_root.rs index f818495..1df831c 100644 --- a/server/src/git_root.rs +++ b/server/src/git_root.rs @@ -1,17 +1,22 @@ use futures::{future::TryFutureExt, stream::TryStreamExt}; +use log::{trace, error}; use rmp_serde::{decode, Serializer}; use rocket::fairing::{self, AdHoc}; -use rocket::serde::ser::Serialize; use rocket::serde::Deserialize; +use rocket::serde::ser::Serialize; use rocket::{Build, Rocket}; use rocket_db_pools::{sqlx, Database, Pool}; use std::borrow::Cow; use std::collections::HashMap; +use std::fs::Permissions; use std::ops::Deref; -use std::path::PathBuf; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; +use tokio::fs; use tokio::net::{UnixListener, UnixStream}; use tokio::task; +use url::Url; use crate::api_model; use crate::fs_utils; @@ -43,14 +48,16 @@ impl Roots { db: &Db, project_id: &str, remote: &str, + remote_key: &str, main_branch: &str, - ) -> Result<(), git::Error> { + ) -> Result<(), anyhow::Error> { let project_id = project_id.to_string(); let repo = setup_project_root( config, db, &project_id, remote.to_string(), + remote_key.to_string(), main_branch.to_string(), ) .await?; @@ -115,13 +122,17 @@ async fn git_process_prehook( let mut errors: Vec = Vec::new(); for row in receive { + trace!("prehook {} {} {}", row.old_value, row.new_value, row.reference); + if !valid_branch_name(row.reference.as_str()) { if row.reference.starts_with("refs/heads/") { + error!("{}: Bad branch name", row.reference); errors.push(format!( "{}: Bad branch name", row.reference.strip_prefix("refs/heads/").unwrap() )); } else { + error!("{}: Only branches are allowed", row.reference); errors.push(format!("{}: Only branches are allowed", row.reference)); } continue; @@ -141,11 +152,13 @@ async fn git_process_prehook( { Ok(_) => {} Err(e) => { + error!("{e:?}"); errors.push(e.message); continue; } }, None => { + error!("{branch}: Missing commiter"); errors.push(format!("{branch}: Missing commiter")); continue; } @@ -177,6 +190,7 @@ async fn git_process_prehook( // All branches should be connected to a branch, but in case of errors this might // be relevant. if result.is_ok() { + error!("{branch}: Not allowed to delete branch, delete review instead."); errors.push(format!( "{branch}: Not allowed to delete branch, delete review instead." )); @@ -187,6 +201,7 @@ async fn git_process_prehook( let (state, rewrite) = match result { Ok(data) => data, Err(e) => { + error!("{e:?}"); errors.push(e.message); continue; } @@ -194,10 +209,12 @@ async fn git_process_prehook( match state { api_model::ReviewState::Dropped => { + error!("{branch}: Review is dropped, no pushes allowed"); errors.push(format!("{branch}: Review is dropped, no pushes allowed")); continue; } api_model::ReviewState::Closed => { + error!("{branch}: Review is closed, no pushes allowed"); errors.push(format!("{branch}: Review is closed, no pushes allowed")); continue; } @@ -223,10 +240,12 @@ async fn git_process_prehook( if equal_content { continue; } + error!("{branch}: History rewrite not allowed"); errors.push(format!("{}: History rewrite not allowed as final result does not match. Please check locally with `git diff {} {}`", branch, row.old_value, row.new_value)); } api_model::Rewrite::Rebase => {} api_model::Rewrite::Disabled => { + error!("{}: Non fast-forward not allowed", row.reference); errors.push(format!( "Non fast-forward not allowed for review: {}", row.reference @@ -257,12 +276,15 @@ async fn git_process_posthook( let mut updated: Vec = Vec::new(); for row in receive { + trace!("posthook {} {} {}", row.old_value, row.new_value, row.reference); + let branch = row.reference.strip_prefix("refs/heads/").unwrap(); if row.old_value == git::EMPTY { let commiter = match repo.get_commiter(row.reference.as_str()).await { Ok(user) => user, Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: {e}")); continue; } @@ -288,6 +310,7 @@ async fn git_process_posthook( )); } Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: Error {e}",)); } }; @@ -319,6 +342,7 @@ async fn git_process_posthook( updated.push(id); } Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: Error {e}",)); } } @@ -328,6 +352,7 @@ async fn git_process_posthook( } }, Err(e) => { + error!("{e:?}"); messages.push(format!("{branch}: Error {e}",)); } } @@ -402,38 +427,114 @@ async fn git_socket_listen( } } +fn get_host(url: &str) -> String { + match Url::parse(url) { + Ok(u) => match u.host_str() { + Some(h) => h.to_string(), + None => String::new(), + }, + Err(_) => String::new(), + } +} + +async fn write_private_file, D: AsRef<[u8]>>(path: P, data: D) -> anyhow::Result<()> { + fs::write(&path, data).await?; + let permissions = Permissions::from_mode(0o600); + fs::set_permissions(&path, permissions).await?; + Ok(()) +} + +async fn write_ssh_config(ssh_config: &Path, host: &str, remote_key: &str) -> anyhow::Result<()> { + let host_pattern = if host.is_empty() { + "*" + } else { + host + }; + let basedir = ssh_config.parent().unwrap(); + let identity_file = basedir.join("ssh_identity"); + let known_hosts = basedir.join("ssh_known_hosts"); + + let mut identity_data = String::from("-----BEGIN OPENSSH PRIVATE KEY-----\n"); + { + let mut left = remote_key; + while left.len() > 70 { + let (a, b) = left.split_at(70); + identity_data.push_str(a); + identity_data.push('\n'); + left = b; + } + identity_data.push_str(left); + identity_data.push('\n'); + } + identity_data.push_str("-----END OPENSSH PRIVATE KEY-----\n"); + + let config_data = format!("Host {host_pattern} +IdentityFile ./{} +PasswordAuthentication no +StrictHostKeyChecking accept-new +UpdateHostKeys yes +UserKnownHostsFile ./{} +", identity_file.file_name().unwrap().to_str().unwrap(), known_hosts.file_name().unwrap().to_str().unwrap()); + + let (a, b, c) = tokio::join!( + write_private_file(identity_file, identity_data), + fs::write(known_hosts, "\n"), + fs::write(ssh_config, config_data), + ); + a?; + b?; + c?; + Ok(()) +} + async fn setup_project_root( config: &Config<'_>, db: &Db, project_id: &String, remote: String, + remote_key: String, main_branch: String, -) -> Result, git::Error> { +) -> Result, anyhow::Error> { let mut path = PathBuf::from(config.git_server_root.to_string()); path.push(project_id); + info!("{project_id}: Setup repo at {path:?}"); let githook = PathBuf::from(config.git_hook.to_string()); + let ssh_config = path.join("ssh_config"); let repo = Arc::new(git::Repository::new( path, true, Some(remote), Some(project_id), Some(githook), + Some(ssh_config), )); repo.setup().await?; + write_ssh_config(repo.ssh_config().unwrap(), + get_host(repo.remote().unwrap()).as_str(), + remote_key.as_str()).await?; + if !repo.remote().unwrap().is_empty() && !main_branch.is_empty() { let bg_repo = repo.clone(); - tokio::spawn(async move { bg_repo.fetch(main_branch).await }); + let bg_project_id = project_id.clone(); + tokio::spawn(async move { + match bg_repo.fetch(&main_branch).await { + Ok(()) => {} + Err(e) => { + error!("{bg_project_id}: fetch {main_branch} returned {e:?}"); + } + } + }); } let socket_repo = repo.clone(); let socket_db = db.deref().clone(); + let bg_project_id = project_id.clone(); tokio::spawn(async move { match git_socket_listen(socket_repo, socket_db).await { Ok(()) => {} Err(e) => { - // TODO: Log - print!("git_socket_listen returned {:?}", e) + error!("{bg_project_id}: git_socket_listen returned {e:?}"); } } }); @@ -444,17 +545,17 @@ async fn setup_project_root( async fn setup_projects_roots(roots: &Roots, config: &Config<'_>, db: &Db) -> anyhow::Result<()> { fs_utils::create_dir_allow_existing(PathBuf::from(config.git_server_root.to_string())).await?; - let projects = sqlx::query!("SELECT id,remote,main_branch FROM projects") + let projects = sqlx::query!("SELECT id,remote,remote_key,main_branch FROM projects") .fetch(&**db) - .map_ok(|r| (r.id, r.remote, r.main_branch)) + .map_ok(|r| (r.id, r.remote, r.remote_key, r.main_branch)) .try_collect::>() .await .unwrap(); let mut project_repo: HashMap> = HashMap::new(); - for (id, remote, main_branch) in projects { - let repo = setup_project_root(config, db, &id, remote, main_branch).await?; + for (id, remote, remote_key, main_branch) in projects { + let repo = setup_project_root(config, db, &id, remote, remote_key, main_branch).await?; project_repo.insert(id, repo); } @@ -472,7 +573,10 @@ async fn setup_projects(rocket: Rocket) -> fairing::Result { Some(roots) => match Db::fetch(&rocket) { Some(db) => match setup_projects_roots(roots, config, db).await { Ok(_) => Ok(rocket), - Err(_) => Err(rocket), + Err(e) => { + error!("{e:?}"); + Err(rocket) + }, }, None => Err(rocket), }, diff --git a/server/src/main.rs b/server/src/main.rs index 9bdfeaf..a2a9165 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -36,6 +36,7 @@ struct Db(sqlx::MySqlPool); #[derive(OpenApi)] #[openapi( paths( + healthcheck, projects, project, project_new, @@ -107,6 +108,18 @@ async fn projects( }) } +fn abbrivate_key(key: &str) -> String { + let len = key.len(); + if len <= 24 { + // Realisticly this will only happen for empty keys, + // but make sure to NEVER send the whole key if it is really short. + return String::new(); + } + let start = &key[0..8]; + let end = &key[len - 8..len]; + return format!("{start}...{end}") +} + async fn get_project( db: &mut <::Pool as Pool>::Connection, projectid: &str, @@ -129,7 +142,7 @@ async fn get_project( .unwrap(); let project = sqlx::query!( - "SELECT id,title,description,remote,main_branch FROM projects WHERE id=?", + "SELECT id,title,description,remote,remote_key,main_branch FROM projects WHERE id=?", projectid ) .fetch_one(&mut **db) @@ -138,6 +151,7 @@ async fn get_project( title: r.title, description: r.description, remote: r.remote, + remote_key_abbrev: abbrivate_key(r.remote_key.as_str()), main_branch: r.main_branch, users, }) @@ -166,6 +180,20 @@ async fn project( get_project(&mut conn, projectid).await } +// Remove linebreaks and potential openssh wrapper +fn cleanup_key(key: &str) -> String { + let mut ret = String::with_capacity(key.len()); + let mut lines = key.lines(); + while let Some(line) = lines.next() { + match line { + "-----BEGIN OPENSSH PRIVATE KEY-----" | + "-----END OPENSSH PRIVATE KEY-----" => {}, + _ => ret.push_str(line) + } + } + return ret +} + #[utoipa::path( responses( (status = 200, description = "Project created", body = api_model::Project), @@ -185,6 +213,10 @@ async fn project_new( data: Json>, ) -> Result, Custom> { let remote = data.remote.unwrap_or(""); + let remote_key = match &data.remote_key { + Some(data) => cleanup_key(data.as_str()), + None => String::new(), + }; let main_branch = data.main_branch.unwrap_or("main"); let mut conn = db.get().await.unwrap(); @@ -192,11 +224,12 @@ async fn project_new( let mut tx = conn.begin().await.unwrap(); sqlx::query!( - "INSERT INTO projects (id, title, description, remote, main_branch) VALUES (?, ?, ?, ?, ?)", + "INSERT INTO projects (id, title, description, remote, remote_key, main_branch) VALUES (?, ?, ?, ?, ?, ?)", projectid, data.title.unwrap_or("Unnamed"), data.description.unwrap_or(""), remote, + remote_key, main_branch, ) .execute(&mut *tx) @@ -214,8 +247,8 @@ async fn project_new( } roots_state - .new_project(git_roots_config, db, projectid, remote, main_branch) - .map_err(|e| Custom(Status::InternalServerError, e.message)) + .new_project(git_roots_config, db, projectid, remote, remote_key.as_str(), main_branch) + .map_err(|e| Custom(Status::InternalServerError, format!("{e}"))) .await?; Ok(get_project(&mut conn, projectid).await.unwrap()) @@ -278,6 +311,9 @@ async fn project_update( if let Some(remote) = &data.remote { update_builder.set("remote", remote); } + if let Some(remote_key) = &data.remote_key { + update_builder.set("remote_key", cleanup_key(remote_key)); + } if let Some(main_branch) = &data.main_branch { update_builder.set("main_branch", main_branch); } @@ -1060,6 +1096,19 @@ async fn user_keys( }) } +#[utoipa::path( + responses( + (status = 200, description = "All good"), + ), + security( + (), + ), +)] +#[get("/healthcheck")] +fn healthcheck() -> &'static str { + "" +} + async fn run_migrations(rocket: Rocket) -> fairing::Result { match Db::fetch(&rocket) { Some(db) => match sqlx::migrate!().run(&**db).await { @@ -1082,6 +1131,7 @@ fn rocket_from_config(figment: Figment) -> Rocket { basepath, // Remember to update openapi paths when you add something here. routes![ + healthcheck, projects, project, project_new, diff --git a/server/src/tests.rs b/server/src/tests.rs index 1b42485..cc71bf3 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -202,6 +202,7 @@ async fn new_project(client: &Client) -> api_model::Project { title: Some("foo"), description: Some("bar"), remote: None, + remote_key: None, main_branch: Some("zod"), }), ) @@ -334,6 +335,7 @@ async fn test_project_new() { assert_eq!(project.title, "foo"); assert_eq!(project.description, "bar"); assert_eq!(project.remote, ""); + assert_eq!(project.remote_key_abbrev, ""); assert_eq!(project.main_branch, "zod"); assert_eq!(project.users.len(), 1); let user = project.users.get(0).unwrap(); @@ -367,6 +369,7 @@ async fn test_project_new_duplicate() { title: Some("foo"), description: Some("bar"), remote: None, + remote_key: None, main_branch: Some("zod"), }) .header(&FAKE_IP) @@ -389,6 +392,7 @@ async fn test_project_update() { title: Some("foo"), description: None, remote: None, + remote_key: None, main_branch: Some("fum"), }, )) @@ -402,6 +406,7 @@ async fn test_project_update() { title: None, description: Some("bar"), remote: None, + remote_key: None, main_branch: Some("zod"), }) .header(&FAKE_IP) @@ -413,6 +418,7 @@ async fn test_project_update() { assert_eq!(updated_project.title, project.title); assert_eq!(updated_project.description, "bar"); assert_eq!(updated_project.remote, project.remote); + assert_eq!(updated_project.remote_key_abbrev, project.remote_key_abbrev); assert_eq!(updated_project.main_branch, "zod"); } @@ -428,6 +434,7 @@ async fn test_project_update_unknown() { title: Some("foo"), description: Some("bar"), remote: None, + remote_key: None, main_branch: Some("zod"), }) .header(&FAKE_IP) @@ -660,6 +667,7 @@ async fn test_project_check_maintainer() { title: None, description: Some("fool"), remote: None, + remote_key: None, main_branch: None, }) .header(&FAKE_IP) diff --git a/server/tests/common/mod.rs b/server/tests/common/mod.rs index 0eef90b..b30e4d8 100644 --- a/server/tests/common/mod.rs +++ b/server/tests/common/mod.rs @@ -17,6 +17,7 @@ pub struct DockerComposeContext { test_dir: PathBuf, url: String, remote_git: String, + remote_git_key: PathBuf, } async fn run(cmd: &mut Command, name: &str) -> Result<(), anyhow::Error> { @@ -65,12 +66,12 @@ async fn setup_ssh_file( Ok(()) } -async fn git_clone(base: impl AsRef) -> Result<(), anyhow::Error> { +async fn git_clone(base: impl AsRef, remote: &str) -> Result<(), anyhow::Error> { let mut cmd = Command::new("git"); cmd.arg("clone"); - cmd.arg("ssh://localhost/srv/git/fake.git"); + cmd.arg(remote); - cmd.env("GIT_SSH_COMMAND", "ssh -F ssh_config"); + cmd.env("GIT_SSH_COMMAND", "ssh -v -F ssh_config"); cmd.current_dir(base); run(&mut cmd, "git clone").await @@ -99,6 +100,10 @@ impl DockerComposeContext { self.remote_git.as_str() } + pub fn remote_git_key(&self) -> &Path { + self.remote_git_key.as_ref() + } + pub async fn setup_ssh_key(&self, base: &str, key: &str) -> Result<(), anyhow::Error> { let base_dir = self.test_dir.join(base); fs::create_dir(&base_dir).await?; @@ -111,7 +116,7 @@ impl DockerComposeContext { } pub async fn git_clone(&self, base: &str) -> Result<(), anyhow::Error> { - git_clone(self.test_dir.join(base)).await + git_clone(self.test_dir.join(base), "ssh://localhost/srv/git/fake").await } pub fn git_dir(&self, base: &str) -> PathBuf { @@ -129,11 +134,14 @@ impl AsyncTestContext for DockerComposeContext { Ok(pathstr) => PathBuf::from(pathstr), Err(e) => panic!("CARGO_MANIFEST_DIR not set: {e:?}"), }; + let docker_dir = cargo_dir.join("../docker/integration_test"); + let remote_git_key = docker_dir.join("web/gitkey"); let ctx = DockerComposeContext { - docker_dir: cargo_dir.join("../docker/integration_test"), + docker_dir: docker_dir, test_dir: testdir!(), url: "http://localhost:18000".to_string(), remote_git: "ssh://git@remote_git/srv/git/fake.git".to_string(), + remote_git_key, }; // Build githook, needs to use musl to work with the rockstorm/git-server image @@ -152,7 +160,7 @@ impl AsyncTestContext for DockerComposeContext { } // Start docker compose up - { + if false { let mut cmd = Command::new("docker"); cmd.arg("compose"); cmd.arg("up"); @@ -209,7 +217,9 @@ impl AsyncTestContext for DockerComposeContext { run(&mut cmd, "git-init").await.expect("ssh git-init"); } - git_clone(&mod_path).await.expect("git clone"); + git_clone(&mod_path, "ssh://localhost/srv/git/fake.git") + .await + .expect("git clone"); fs::write(mod_path.join("fake/README"), "Hello fellow fake person!") .await @@ -278,7 +288,7 @@ pub async fn user_key_add( ) -> Result { let data = api_model::UserKeyData { kind, - data, + data: data.to_string(), comment: None, }; let result = client @@ -300,11 +310,14 @@ pub async fn create_project( client: &mut Client, projectid: &str, remote: &str, + remote_key: &Path, ) -> Result { + let remote_key_data = fs::read_to_string(remote_key).await?; let data = api_model::ProjectData { title: None, description: None, remote: Some(remote), + remote_key: Some(remote_key_data), main_branch: None, }; let result = client diff --git a/server/tests/integration_test.rs b/server/tests/integration_test.rs index 242655b..d8dc829 100644 --- a/server/tests/integration_test.rs +++ b/server/tests/integration_test.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::thread::sleep; use std::time::Duration; use test_context::test_context; @@ -76,7 +77,8 @@ async fn test_sanity(ctx: &mut common::DockerComposeContext) { .expect("user02 ssh_config setup"); let remote_git = String::from(ctx.remote_git()); - common::create_project(ctx, &mut client1, "fake", &remote_git) + let remote_git_key = PathBuf::from(ctx.remote_git_key()); + common::create_project(ctx, &mut client1, "fake", &remote_git, &remote_git_key) .await .expect("create fake project"); -- cgit v1.2.3-70-g09d2