summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-02-20 22:53:27 +0100
committerJoel Klinghed <the_jk@spawned.biz>2025-02-20 22:53:27 +0100
commite940d84f69e3fd627731d5d3f698d6f838797862 (patch)
tree779eefcde993e22c0a69c18a3cb6e1cb9d17aad3
parentbf025b4977543a371df9dbdddfe9cc2f02f2a8d0 (diff)
WIPWIP
-rw-r--r--docker/integration_test/docker-compose.yaml3
-rw-r--r--docker/integration_test/web/Dockerfile21
-rw-r--r--server/.sqlx/query-11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8.json (renamed from server/.sqlx/query-2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252.json)15
-rw-r--r--server/.sqlx/query-dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445.json (renamed from server/.sqlx/query-51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197.json)15
-rw-r--r--server/.sqlx/query-fb92a51da21e3ec04b1f5543043f3633c46854684625ef5f7a77a79c12c86961.json (renamed from server/.sqlx/query-a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f.json)6
-rw-r--r--server/Cargo.lock24
-rw-r--r--server/Cargo.toml5
-rw-r--r--server/api/src/api_model.rs6
-rw-r--r--server/common/Cargo.toml1
-rw-r--r--server/common/src/git.rs14
-rw-r--r--server/common/src/tests.rs2
-rw-r--r--server/hook/Cargo.toml1
-rw-r--r--server/hook/src/githook.rs1
-rw-r--r--server/migrations/1_initial_eyeballs.sql1
-rw-r--r--server/src/authorized_keys.rs17
-rw-r--r--server/src/git_root.rs128
-rw-r--r--server/src/main.rs58
-rw-r--r--server/src/tests.rs8
-rw-r--r--server/tests/common/mod.rs29
-rw-r--r--server/tests/integration_test.rs4
20 files changed, 312 insertions, 47 deletions
diff --git a/docker/integration_test/docker-compose.yaml b/docker/integration_test/docker-compose.yaml
index 628973f..442d2ce 100644
--- a/docker/integration_test/docker-compose.yaml
+++ b/docker/integration_test/docker-compose.yaml
@@ -40,7 +40,8 @@ services:
environment:
- SSH_AUTH_METHODS=publickey
depends_on:
- - web
+ web:
+ condition: service_healthy
ports:
- '10022:22'
volumes:
diff --git a/docker/integration_test/web/Dockerfile b/docker/integration_test/web/Dockerfile
index 2ba7a5d..5056fa1 100644
--- a/docker/integration_test/web/Dockerfile
+++ b/docker/integration_test/web/Dockerfile
@@ -1,6 +1,6 @@
FROM archlinux:base
-RUN pacman -Suy --noconfirm && pacman -S openssl git --noconfirm
+RUN pacman -Suy --noconfirm && pacman -S openssh openssl git --noconfirm
# Docker still have this really stupied idea that all files must be relative "context",
# so context is set to ../.. relative the docker-compose.yaml
@@ -8,8 +8,27 @@ COPY server/target/x86_64-unknown-linux-musl/debug/eyeballs-githook /app/eyeball
COPY server/target/debug/eyeballs /app/eyeballs
COPY docker/integration_test/web/setup.sh /app/setup.sh
+RUN mkdir -p /git/auth /git/repos
+
+# git image runs as default git user, with uid 1000 gid 1000.
+# we need the same, but it can't be named git (as package git installs a git user)
+# so add another user with 1000 gid 1000 and make sure that shared files
+# (/git/auth and /git/repos) are owned by that user and not root.
+RUN useradd --no-create-home --uid 1000 --user-group -s /usr/bin/nologin alf
+
+RUN chown alf:alf /app
+RUN chown alf:alf /git/auth
+RUN chown alf:alf /git/repos
+VOLUME /git/auth
+VOLUME /git/repos
+
+USER alf:alf
+
RUN mkdir -p -m 0700 /app/.ssh
COPY docker/integration_test/web/gitkey /app/.ssh/id_rsa
WORKDIR /app
ENTRYPOINT /app/setup.sh
+
+HEALTHCHECK --start-interval=1s --timeout=1s CMD curl -f http://localhost:8000/api/v1/healthcheck
+
diff --git a/server/.sqlx/query-2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252.json b/server/.sqlx/query-11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8.json
index 97d107d..13a9155 100644
--- a/server/.sqlx/query-2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252.json
+++ b/server/.sqlx/query-11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8.json
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
- "query": "SELECT id,remote,main_branch FROM projects",
+ "query": "SELECT id,remote,remote_key,main_branch FROM projects",
"describe": {
"columns": [
{
@@ -25,6 +25,16 @@
},
{
"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",
@@ -40,8 +50,9 @@
"nullable": [
false,
false,
+ false,
false
]
},
- "hash": "2bc668a035fccffc3906fc2eecae70925916336adf7c5062e0b36a6229b3f252"
+ "hash": "11b635f329146d1361a207d5f94b139d88355059390a7a34d14754169f7cc0e8"
}
diff --git a/server/.sqlx/query-51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197.json b/server/.sqlx/query-dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445.json
index 942c054..3379f2a 100644
--- a/server/.sqlx/query-51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197.json
+++ b/server/.sqlx/query-dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445.json
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
- "query": "SELECT id,title,description,remote,main_branch FROM projects WHERE id=?",
+ "query": "SELECT id,title,description,remote,remote_key,main_branch FROM projects WHERE id=?",
"describe": {
"columns": [
{
@@ -45,6 +45,16 @@
},
{
"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",
@@ -62,8 +72,9 @@
false,
false,
false,
+ false,
false
]
},
- "hash": "51f58915888d2523f6de00e206ea36137a50e7b8871751c631edb738db2cd197"
+ "hash": "dddecd7c473f1f076e4ac3b01ece0643fc1acf182f201a691619de6c092b8445"
}
diff --git a/server/.sqlx/query-a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f.json b/server/.sqlx/query-fb92a51da21e3ec04b1f5543043f3633c46854684625ef5f7a77a79c12c86961.json
index 6f761ee..92f794c 100644
--- a/server/.sqlx/query-a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f.json
+++ b/server/.sqlx/query-fb92a51da21e3ec04b1f5543043f3633c46854684625ef5f7a77a79c12c86961.json
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
- "query": "INSERT INTO projects (id, title, description, remote, main_branch) VALUES (?, ?, ?, ?, ?)",
+ "query": "INSERT INTO projects (id, title, description, remote, remote_key, main_branch) VALUES (?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
- "Right": 5
+ "Right": 6
},
"nullable": []
},
- "hash": "a3929af470685bfea9b913dd94f4749fcf1f80cc0389cf3f84f0864a4bfece6f"
+ "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<ProjectUserEntry>,
@@ -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<String>,
#[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<String>,
socket: Option<PathBuf>,
githook: Option<PathBuf>,
+ ssh_config: Option<PathBuf>,
// 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<impl Into<String>>,
project_id: Option<impl Into<String>>,
githook: Option<impl Into<PathBuf>>,
+ ssh_config: Option<impl Into<PathBuf>>,
) -> 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<PathBuf>;
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::<String>,
None::<String>,
None::<PathBuf>,
+ None::<PathBuf>,
);
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::<String>,
None::<PathBuf>,
+ None::<PathBuf>,
);
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<dyn Error>> {
None::<String>,
None::<String>,
None::<PathBuf>,
+ None::<PathBuf>,
);
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<Build>) -> 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<String> = 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<u64> = 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<P: AsRef<Path>, 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<Arc<git::Repository>, git::Error> {
+) -> Result<Arc<git::Repository>, 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::<Vec<_>>()
.await
.unwrap();
let mut project_repo: HashMap<String, Arc<git::Repository>> = 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<Build>) -> 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 <<Db as Database>::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<api_model::ProjectData<'_>>,
) -> Result<Json<api_model::Project>, Custom<String>> {
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<Build>) -> 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<Build> {
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<Path>) -> Result<(), anyhow::Error> {
+async fn git_clone(base: impl AsRef<Path>, 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<api_model::UserKey, anyhow::Error> {
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<api_model::Project, anyhow::Error> {
+ 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");