From 2bf88cfe7f4e96cbb54702150d99cafc9d9a21ce Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 13 Apr 2026 07:33:35 -0400 Subject: [PATCH] fix(auth): restore local dev-token client coverage Propagate the local dev token through worker subprocesses, share the same authenticated local-server helper across CLI integration tests, and clean up the async token wait path so fmt, clippy, and full tests pass again after the dev-token auth rollout. --- .../fabro-cli/src/commands/server/record.rs | 8 +-- .../fabro-cli/src/commands/server/start.rs | 3 +- lib/crates/fabro-cli/src/server_client.rs | 51 ++++++++++--------- lib/crates/fabro-cli/tests/it/cmd/attach.rs | 36 ++++--------- lib/crates/fabro-cli/tests/it/cmd/inspect.rs | 12 ++--- lib/crates/fabro-cli/tests/it/cmd/pr_view.rs | 33 ++---------- lib/crates/fabro-cli/tests/it/cmd/run.rs | 8 ++- lib/crates/fabro-cli/tests/it/cmd/runner.rs | 44 ++++++---------- lib/crates/fabro-cli/tests/it/cmd/support.rs | 36 ++++++++++++- lib/crates/fabro-cli/tests/it/scenario/mod.rs | 37 +------------- lib/crates/fabro-cli/tests/it/workflow/mod.rs | 36 +------------ lib/crates/fabro-server/src/jwt_auth.rs | 16 +++--- lib/crates/fabro-server/src/server.rs | 5 +- lib/crates/fabro-server/src/web_auth.rs | 23 +++++---- lib/crates/fabro-util/src/dev_token.rs | 8 ++- lib/crates/fabro-util/src/home.rs | 8 ++- 16 files changed, 148 insertions(+), 216 deletions(-) diff --git a/lib/crates/fabro-cli/src/commands/server/record.rs b/lib/crates/fabro-cli/src/commands/server/record.rs index ee5e0a822..c98b886f9 100644 --- a/lib/crates/fabro-cli/src/commands/server/record.rs +++ b/lib/crates/fabro-cli/src/commands/server/record.rs @@ -9,12 +9,12 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct ServerRecord { - pub pid: u32, - pub bind: Bind, - pub log_path: PathBuf, + pub pid: u32, + pub bind: Bind, + pub log_path: PathBuf, #[serde(skip_serializing_if = "Option::is_none")] pub dev_token_path: Option, - pub started_at: DateTime, + pub started_at: DateTime, } #[derive(Debug, Clone)] diff --git a/lib/crates/fabro-cli/src/commands/server/start.rs b/lib/crates/fabro-cli/src/commands/server/start.rs index 5188248b3..3eb373b12 100644 --- a/lib/crates/fabro-cli/src/commands/server/start.rs +++ b/lib/crates/fabro-cli/src/commands/server/start.rs @@ -8,10 +8,9 @@ use fabro_config::user::default_socket_path; use fabro_server::bind::{Bind, BindRequest}; use fabro_server::serve; use fabro_server::serve::{DEFAULT_TCP_PORT, ServeArgs}; -use fabro_util::Home; -use fabro_util::dev_token; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; +use fabro_util::{Home, dev_token}; use tokio::process::Command as TokioCommand; use tokio::time; diff --git a/lib/crates/fabro-cli/src/server_client.rs b/lib/crates/fabro-cli/src/server_client.rs index 877c4c1d8..f6b377283 100644 --- a/lib/crates/fabro-cli/src/server_client.rs +++ b/lib/crates/fabro-cli/src/server_client.rs @@ -8,12 +8,14 @@ use anyhow::{Context as _, Result, anyhow, bail}; use bytes::Bytes; use fabro_api::types; use fabro_config::Storage; -use fabro_http::header::{CONTENT_LENGTH, CONTENT_TYPE}; +use fabro_http::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE}; use fabro_http::multipart::{Form, Part}; use fabro_server::bind::Bind; use fabro_store::{EventEnvelope, RunSummary, StageId}; use fabro_types::settings::SettingsLayer; use fabro_types::{RunBlobId, RunEvent, RunId}; +use fabro_util::Home; +use fabro_util::dev_token::validate_dev_token_format; use fabro_workflow::artifact_snapshot::CapturedArtifactInfo; use futures::StreamExt; use serde::Serialize; @@ -23,12 +25,9 @@ use tokio::time::sleep; use tokio_util::io::ReaderStream; use crate::args::ServerTargetArgs; -use crate::commands::server::start; -use crate::commands::server::record; +use crate::commands::server::{record, start}; use crate::user_config::cli_http_client_builder; use crate::{sse, user_config}; -use fabro_util::Home; -use fabro_util::dev_token::validate_dev_token_format; #[derive(Clone)] pub(crate) struct ServerStoreClient { @@ -123,7 +122,7 @@ async fn connect_api_client_bundle(storage_dir: &Path) -> Result connect_unix_socket_api_client_bundle(&path, Some(storage_dir)).await, Bind::Tcp(addr) => { - let token = wait_for_local_dev_token(storage_dir)?; + let token = wait_for_local_dev_token(storage_dir).await?; let builder = cli_http_client_builder().no_proxy(); let http_client = apply_bearer_token_auth(builder, &token)?.build()?; let base_url = format!("http://{addr}"); @@ -226,14 +225,14 @@ fn load_dev_token_if_available(storage_dir: Option<&Path>) -> Option { read_dev_token_file(&Home::from_env().dev_token_path()) } -fn wait_for_local_dev_token(storage_dir: &Path) -> Result { +async fn wait_for_local_dev_token(storage_dir: &Path) -> Result { let deadline = std::time::Instant::now() + Duration::from_secs(5); while std::time::Instant::now() < deadline { if let Some(token) = load_dev_token_if_available(Some(storage_dir)) { return Ok(token); } - std::thread::sleep(Duration::from_millis(50)); + sleep(Duration::from_millis(50)).await; } bail!( @@ -248,7 +247,7 @@ fn apply_bearer_token_auth( ) -> Result { let mut headers = fabro_http::HeaderMap::new(); headers.insert( - fabro_http::header::AUTHORIZATION, + AUTHORIZATION, fabro_http::HeaderValue::from_str(&format!("Bearer {token}")) .context("invalid dev token header value")?, ); @@ -287,10 +286,13 @@ async fn try_connect_unix_socket_api_client_bundle( check_server_ready(&probe_client).await?; let http_client = if let Some(storage_dir) = storage_dir { - let token = wait_for_local_dev_token(storage_dir)?; - apply_bearer_token_auth(cli_http_client_builder().unix_socket(path).no_proxy(), &token)? - .build() - .context("Failed to build Unix-socket HTTP client for fabro server")? + let token = wait_for_local_dev_token(storage_dir).await?; + apply_bearer_token_auth( + cli_http_client_builder().unix_socket(path).no_proxy(), + &token, + )? + .build() + .context("Failed to build Unix-socket HTTP client for fabro server")? } else { apply_dev_token_auth(cli_http_client_builder().unix_socket(path).no_proxy(), None)? .build() @@ -311,10 +313,13 @@ async fn connect_unix_socket_api_client_bundle( wait_for_server_ready(&probe_client).await?; let http_client = if let Some(storage_dir) = storage_dir { - let token = wait_for_local_dev_token(storage_dir)?; - apply_bearer_token_auth(cli_http_client_builder().unix_socket(path).no_proxy(), &token)? - .build() - .context("Failed to build Unix-socket HTTP client for fabro server")? + let token = wait_for_local_dev_token(storage_dir).await?; + apply_bearer_token_auth( + cli_http_client_builder().unix_socket(path).no_proxy(), + &token, + )? + .build() + .context("Failed to build Unix-socket HTTP client for fabro server")? } else { apply_dev_token_auth(cli_http_client_builder().unix_socket(path).no_proxy(), None)? .build() @@ -1023,9 +1028,7 @@ mod tests { assert_eq!( token.as_deref(), - Some( - "fabro_dev_cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" - ) + Some("fabro_dev_cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd") ); } @@ -1060,11 +1063,11 @@ mod tests { .server_state() .record_path(); record::write_server_record(&record_path, &record::ServerRecord { - pid: std::process::id(), - bind: fabro_server::bind::Bind::Unix(temp_home.path().join("fabro.sock")), - log_path: storage.path().join("server.log"), + pid: std::process::id(), + bind: fabro_server::bind::Bind::Unix(temp_home.path().join("fabro.sock")), + log_path: storage.path().join("server.log"), dev_token_path: Some(token_path), - started_at: chrono::Utc::now(), + started_at: chrono::Utc::now(), }) .unwrap(); diff --git a/lib/crates/fabro-cli/tests/it/cmd/attach.rs b/lib/crates/fabro-cli/tests/it/cmd/attach.rs index 7389394ea..795ff19a2 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/attach.rs @@ -1,5 +1,4 @@ use std::io::{BufRead, BufReader, Read}; -use std::path::Path; use std::process::{Output, Stdio}; use std::sync::mpsc; use std::time::{Duration, Instant}; @@ -8,34 +7,12 @@ use fabro_test::{apply_filters, fabro_snapshot, test_context}; use serde_json::Value; use super::support::{ - output_stdout, resolve_run, server_target, wait_for_status, write_gated_workflow, + output_stdout, resolve_run, server_endpoint, wait_for_status, write_gated_workflow, }; use crate::support::{example_fixture, fabro_json_snapshot, run_output_filters, unique_run_id}; const SHARED_DAEMON_TIMEOUT: Duration = Duration::from_secs(30); -fn server_endpoint(storage_dir: &Path) -> (fabro_http::HttpClient, String) { - let target = server_target(storage_dir); - if target.starts_with('/') { - ( - fabro_http::HttpClientBuilder::new() - .unix_socket(target) - .no_proxy() - .build() - .expect("test Unix-socket HTTP client should build"), - "http://fabro".to_string(), - ) - } else { - ( - fabro_http::HttpClientBuilder::new() - .no_proxy() - .build() - .expect("test TCP HTTP client should build"), - target, - ) - } -} - async fn wait_for_server_question( client: &fabro_http::HttpClient, base_url: &str, @@ -465,6 +442,11 @@ fn attach_json_errors_without_prompting_for_human_input() { fabro_json_snapshot!(context, &progress, @r#" [ { + "actor": { + "display": "dev", + "id": "dev", + "kind": "user" + }, "event": "run.created", "id": "[EVENT_ID]", "properties": { @@ -580,7 +562,8 @@ fn attach_json_errors_without_prompting_for_human_input() { "version": "0.176.2" }, "subject": { - "auth_method": "disabled" + "auth_method": "dev_token", + "login": "dev" } }, "run_dir": "[RUN_DIR]", @@ -847,7 +830,8 @@ fn attach_json_errors_without_prompting_for_human_input() { tokio::runtime::Runtime::new() .expect("test runtime should build") .block_on(async { - let (client, base_url) = server_endpoint(&context.storage_dir); + let (client, base_url) = + server_endpoint(&context.storage_dir).expect("server endpoint should exist"); let question = wait_for_server_question(&client, &base_url, &run_id).await; let question_id = question["id"] .as_str() diff --git a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs index f5c4a2860..d35ec3082 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/inspect.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/inspect.rs @@ -40,7 +40,7 @@ fn inspect_created_run_shows_run_record_without_start_or_conclusion() { let run = setup_created_fast_dry_run(&context); let output = run_success(&context, &["inspect", &run.run_id]); - assert_snapshot!(serde_json::to_string_pretty(&compact_inspect(&output)).unwrap(), @r###" + assert_snapshot!(serde_json::to_string_pretty(&compact_inspect(&output)).unwrap(), @r#" [ { "run_id": "[ULID]", @@ -55,7 +55,7 @@ fn inspect_created_run_shows_run_record_without_start_or_conclusion() { "server_version": "[VERSION]", "client_name": "fabro-cli", "client_version": "[VERSION]", - "subject_auth_method": "disabled" + "subject_auth_method": "dev_token" } }, "start_record": null, @@ -64,7 +64,7 @@ fn inspect_created_run_shows_run_record_without_start_or_conclusion() { "sandbox": null } ] - "###); + "#); } #[test] @@ -88,7 +88,7 @@ fn inspect_completed_run_shows_run_start_conclusion_checkpoint() { "server_version": "[VERSION]", "client_name": "fabro-cli", "client_version": "[VERSION]", - "subject_auth_method": "disabled" + "subject_auth_method": "dev_token" } }, "start_record": { @@ -154,7 +154,7 @@ fn inspect_completed_run_reads_store_without_disk_metadata_files() { "server_version": "[VERSION]", "client_name": "fabro-cli", "client_version": "[VERSION]", - "subject_auth_method": "disabled" + "subject_auth_method": "dev_token" } }, "start_record": { @@ -205,7 +205,7 @@ fn inspect_git_backed_run_exposes_checkpoint_and_sandbox_state() { "server_version": "[VERSION]", "client_name": "fabro-cli", "client_version": "[VERSION]", - "subject_auth_method": "disabled" + "subject_auth_method": "dev_token" } }, "start_record": { diff --git a/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs b/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs index 81267fbef..eb168643a 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/pr_view.rs @@ -1,17 +1,10 @@ #![allow(clippy::absolute_paths)] -use fabro_config::Storage; -use fabro_server::bind::Bind; use fabro_test::{fabro_snapshot, test_context}; use fabro_types::run_event::PullRequestCreatedProps; use fabro_types::{EventBody, RunEvent, RunId}; -use super::support::setup_completed_fast_dry_run; - -#[derive(Debug, serde::Deserialize)] -struct TestServerRecord { - bind: Bind, -} +use super::support::{server_endpoint, setup_completed_fast_dry_run}; #[test] fn help() { @@ -65,28 +58,8 @@ fn pr_view_reads_pull_request_from_store_without_pull_request_json() { let runtime = tokio::runtime::Runtime::new().unwrap(); runtime.block_on(async { - let record_path = Storage::new(&context.storage_dir) - .server_state() - .record_path(); - let record: TestServerRecord = - serde_json::from_str(&std::fs::read_to_string(record_path).unwrap()).unwrap(); - let (client, base_url) = match record.bind { - Bind::Unix(path) => ( - fabro_http::HttpClientBuilder::new() - .unix_socket(path) - .no_proxy() - .build() - .unwrap(), - "http://fabro".to_string(), - ), - Bind::Tcp(addr) => ( - fabro_http::HttpClientBuilder::new() - .no_proxy() - .build() - .unwrap(), - format!("http://{addr}"), - ), - }; + let (client, base_url) = + server_endpoint(&context.storage_dir).expect("server endpoint should exist"); let event = RunEvent { id: ulid::Ulid::new().to_string(), ts: chrono::Utc::now(), diff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/crates/fabro-cli/tests/it/cmd/run.rs index 137e2937d..9005ab0a6 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/run.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/run.rs @@ -812,6 +812,11 @@ fn json_run_implies_auto_approve_for_human_gates() { fabro_json_snapshot!(context, &progress, @r#" [ { + "actor": { + "display": "dev", + "id": "dev", + "kind": "user" + }, "event": "run.created", "id": "[EVENT_ID]", "properties": { @@ -927,7 +932,8 @@ fn json_run_implies_auto_approve_for_human_gates() { "version": "0.176.2" }, "subject": { - "auth_method": "disabled" + "auth_method": "dev_token", + "login": "dev" } }, "run_dir": "[RUN_DIR]", diff --git a/lib/crates/fabro-cli/tests/it/cmd/runner.rs b/lib/crates/fabro-cli/tests/it/cmd/runner.rs index c0f5a0270..1da64fcee 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/runner.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/runner.rs @@ -13,8 +13,8 @@ use fabro_types::{EventBody, RunEvent, StatusReason}; use httpmock::MockServer; use super::support::{ - output_stderr, run_events, run_state, server_target, wait_for_event_names, wait_for_status, - write_gated_workflow, + local_dev_token, output_stderr, run_events, run_state, server_endpoint, server_target, + wait_for_event_names, wait_for_status, write_gated_workflow, }; use crate::support::{fabro_json_snapshot, unique_run_id}; @@ -55,6 +55,9 @@ fn spawn_worker_process( .env("FABRO_HTTP_PROXY_POLICY", "disabled"); cmd.env("FABRO_SERVER_MAX_CONCURRENT_RUNS", "64"); cmd.env("FABRO_TEST_IN_MEMORY_STORE", "1"); + if let Some(token) = local_dev_token(&context.storage_dir) { + cmd.env("FABRO_DEV_TOKEN", token); + } cmd.args([ "__run-worker", "--server", @@ -108,26 +111,12 @@ fn child_output(mut child: Child, status: ExitStatus) -> Output { } } -fn server_endpoint(storage_dir: &std::path::Path) -> (fabro_http::HttpClient, String) { - let target = server_target(storage_dir); - if target.starts_with('/') { - ( - fabro_http::HttpClientBuilder::new() - .unix_socket(target) - .no_proxy() - .build() - .expect("test Unix-socket HTTP client should build"), - "http://fabro".to_string(), - ) - } else { - ( - fabro_http::HttpClientBuilder::new() - .no_proxy() - .build() - .expect("test TCP HTTP client should build"), - target, - ) +fn worker_command(context: &fabro_test::TestContext) -> assert_cmd::Command { + let mut cmd = context.command(); + if let Some(token) = local_dev_token(&context.storage_dir) { + cmd.env("FABRO_DEV_TOKEN", token); } + cmd } async fn wait_for_server_question( @@ -225,8 +214,7 @@ digraph CachedGraph { let server = server_target(&context.storage_dir); std::fs::remove_file(&workflow_path).unwrap(); - let output = context - .command() + let output = worker_command(&context) .args([ "__run-worker", "--server", @@ -306,7 +294,7 @@ digraph GitHubApp { context.write_home(".fabro/settings.toml", "_version = 1\n"); let server = server_target(&context.storage_dir); - let mut cmd = context.command(); + let mut cmd = worker_command(&context); cmd.env("GITHUB_APP_PRIVATE_KEY", "%%%not-base64%%%"); cmd.args([ "__run-worker", @@ -356,8 +344,7 @@ digraph DetachedStoreOnly { let run_dir = context.find_run_dir(&run_id); let server = server_target(&context.storage_dir); - let output = context - .command() + let output = worker_command(&context) .args([ "__run-worker", "--server", @@ -441,7 +428,7 @@ digraph Test { } "#); - let mut cmd = context.command(); + let mut cmd = worker_command(&context); cmd.args([ "__run-worker", "--server", @@ -601,7 +588,8 @@ fn detached_run_answers_pending_question_without_interview_scratch_files() { let run_dir = context.find_run_dir(&run_id); let runtime = tokio::runtime::Runtime::new().expect("test runtime should build"); let question_id = runtime.block_on(async { - let (client, base_url) = server_endpoint(&context.storage_dir); + let (client, base_url) = + server_endpoint(&context.storage_dir).expect("server endpoint should exist"); let question = wait_for_server_question(&client, &base_url, &run_id).await; let question_id = question["id"] .as_str() diff --git a/lib/crates/fabro-cli/tests/it/cmd/support.rs b/lib/crates/fabro-cli/tests/it/cmd/support.rs index e9b023e83..aadde31a1 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/support.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/support.rs @@ -654,19 +654,50 @@ fn block_on(future: impl std::future::Future) -> T { #[derive(Debug, serde::Deserialize)] struct TestServerRecord { - bind: Bind, + bind: Bind, + #[serde(default)] + dev_token_path: Option, } -fn server_endpoint(storage_dir: &Path) -> Option<(fabro_http::HttpClient, String)> { +fn read_dev_token(path: &Path) -> Option { + std::fs::read_to_string(path) + .ok() + .map(|token| token.trim().to_string()) + .filter(|token| !token.is_empty()) +} + +pub(crate) fn local_dev_token(storage_dir: &Path) -> Option { + let server_state = Storage::new(storage_dir).server_state(); + + read_dev_token(&server_state.dev_token_path()).or_else(|| { + std::fs::read_to_string(server_state.record_path()) + .ok() + .and_then(|content| serde_json::from_str::(&content).ok()) + .and_then(|record| record.dev_token_path) + .as_deref() + .and_then(read_dev_token) + }) +} + +pub(crate) fn server_endpoint(storage_dir: &Path) -> Option<(fabro_http::HttpClient, String)> { let record_path = Storage::new(storage_dir).server_state().record_path(); let record = std::fs::read_to_string(record_path) .ok() .and_then(|content| serde_json::from_str::(&content).ok())?; + let mut headers = fabro_http::HeaderMap::new(); + if let Some(token) = local_dev_token(storage_dir) { + headers.insert( + fabro_http::header::AUTHORIZATION, + fabro_http::HeaderValue::from_str(&format!("Bearer {token}")) + .expect("local dev token should build an authorization header"), + ); + } match record.bind { Bind::Unix(path) if path.exists() => Some(( fabro_http::HttpClientBuilder::new() .unix_socket(path) .no_proxy() + .default_headers(headers.clone()) .build() .expect("test Unix-socket HTTP client should build"), "http://fabro".to_string(), @@ -675,6 +706,7 @@ fn server_endpoint(storage_dir: &Path) -> Option<(fabro_http::HttpClient, String Bind::Tcp(addr) => Some(( fabro_http::HttpClientBuilder::new() .no_proxy() + .default_headers(headers) .build() .expect("test TCP HTTP client should build"), format!("http://{addr}"), diff --git a/lib/crates/fabro-cli/tests/it/scenario/mod.rs b/lib/crates/fabro-cli/tests/it/scenario/mod.rs index f54e1bd8c..ebaf1c0f7 100644 --- a/lib/crates/fabro-cli/tests/it/scenario/mod.rs +++ b/lib/crates/fabro-cli/tests/it/scenario/mod.rs @@ -10,10 +10,7 @@ mod smoke; use std::path::{Path, PathBuf}; use std::time::Duration; -use fabro_config::Storage; -use fabro_server::bind::Bind; - -use crate::cmd::support::RunProjection; +use crate::cmd::support::{RunProjection, server_endpoint}; pub(super) fn fixture(name: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/it/workflow/fixtures") @@ -36,41 +33,11 @@ fn infer_run_id(run_dir: &Path) -> String { .expect("run dir should contain resolvable run id") } -#[derive(Debug, serde::Deserialize)] -struct TestServerRecord { - bind: Bind, -} - -fn server_endpoint(storage_dir: &Path) -> (fabro_http::HttpClient, String) { - let record_path = Storage::new(storage_dir).server_state().record_path(); - let record: TestServerRecord = serde_json::from_str( - &std::fs::read_to_string(record_path).expect("server record should exist"), - ) - .expect("server record should parse"); - match record.bind { - Bind::Unix(path) => ( - fabro_http::HttpClientBuilder::new() - .unix_socket(path) - .no_proxy() - .build() - .expect("test Unix-socket HTTP client should build"), - "http://fabro".to_string(), - ), - Bind::Tcp(addr) => ( - fabro_http::HttpClientBuilder::new() - .no_proxy() - .build() - .expect("test TCP HTTP client should build"), - format!("http://{addr}"), - ), - } -} - async fn get_server_json_for_storage( storage_dir: &Path, path: &str, ) -> T { - let (client, base_url) = server_endpoint(storage_dir); + let (client, base_url) = server_endpoint(storage_dir).expect("server endpoint should exist"); let response = client .get(format!("{base_url}{path}")) .send() diff --git a/lib/crates/fabro-cli/tests/it/workflow/mod.rs b/lib/crates/fabro-cli/tests/it/workflow/mod.rs index 185c35a41..efbcc79e1 100644 --- a/lib/crates/fabro-cli/tests/it/workflow/mod.rs +++ b/lib/crates/fabro-cli/tests/it/workflow/mod.rs @@ -13,13 +13,11 @@ mod real_cli; use std::path::{Path, PathBuf}; use std::time::Duration; -use fabro_config::Storage; -use fabro_server::bind::Bind; use fabro_store::EventEnvelope; use fabro_test::TestContext; use serde_json::Value; -use crate::cmd::support::RunProjection; +use crate::cmd::support::{RunProjection, server_endpoint}; pub(super) fn fixture(name: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) @@ -105,41 +103,11 @@ fn block_on(future: impl std::future::Future) -> T { .block_on(future) } -#[derive(Debug, serde::Deserialize)] -struct TestServerRecord { - bind: Bind, -} - -fn server_endpoint(storage_dir: &Path) -> (fabro_http::HttpClient, String) { - let record_path = Storage::new(storage_dir).server_state().record_path(); - let record: TestServerRecord = serde_json::from_str( - &std::fs::read_to_string(record_path).expect("server record should exist"), - ) - .expect("server record should parse"); - match record.bind { - Bind::Unix(path) => ( - fabro_http::HttpClientBuilder::new() - .unix_socket(path) - .no_proxy() - .build() - .expect("test Unix-socket HTTP client should build"), - "http://fabro".to_string(), - ), - Bind::Tcp(addr) => ( - fabro_http::HttpClientBuilder::new() - .no_proxy() - .build() - .expect("test TCP HTTP client should build"), - format!("http://{addr}"), - ), - } -} - async fn get_server_json_for_storage( storage_dir: &Path, path: &str, ) -> T { - let (client, base_url) = server_endpoint(storage_dir); + let (client, base_url) = server_endpoint(storage_dir).expect("server endpoint should exist"); let response = client .get(format!("{base_url}{path}")) .send() diff --git a/lib/crates/fabro-server/src/jwt_auth.rs b/lib/crates/fabro-server/src/jwt_auth.rs index f75455990..eb5ab8575 100644 --- a/lib/crates/fabro-server/src/jwt_auth.rs +++ b/lib/crates/fabro-server/src/jwt_auth.rs @@ -7,6 +7,7 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use cookie::Key; use fabro_types::RunAuthMethod; use fabro_types::settings::{ServerListenSettings, ServerSettings as ResolvedServerSettings}; +use fabro_util::dev_token::validate_dev_token_format; use hmac::{Hmac, Mac}; use jsonwebtoken::{Algorithm, DecodingKey, Validation}; use rustls_pki_types::CertificateDer; @@ -15,7 +16,6 @@ use sha2::Sha256; use crate::error::ApiError; use crate::web_auth::SessionCookie; -use fabro_util::dev_token::validate_dev_token_format; type HmacSha256 = Hmac; const DEV_TOKEN_COMPARE_KEY: &[u8] = b"fabro-dev-token-compare-key"; @@ -1077,14 +1077,14 @@ enabled = true .body(Body::empty()) .unwrap(); req.extensions_mut().insert(SessionCookie { - login: "brynary".to_string(), - provider: "github".to_string(), - name: "Brynary".to_string(), - email: "b@example.com".to_string(), - avatar_url: "https://example.com/avatar.png".to_string(), - user_url: "https://github.com/brynary".to_string(), + login: "brynary".to_string(), + provider: "github".to_string(), + name: "Brynary".to_string(), + email: "b@example.com".to_string(), + avatar_url: "https://example.com/avatar.png".to_string(), + user_url: "https://github.com/brynary".to_string(), provider_id: Some(1), - exp: 9_999_999_999, + exp: 9_999_999_999, }); let response = app.oneshot(req).await.unwrap(); diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index b96660313..f209b8889 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -617,7 +617,7 @@ impl AppState { pub(crate) fn session_key(&self) -> Option { self.session_key_override.clone().or_else(|| { self.server_secret("SESSION_SECRET") - .map(|value| Key::derive_from(value.as_bytes())) + .map(|value| Key::derive_from(value.as_bytes())) }) } @@ -3172,6 +3172,9 @@ fn worker_command( .stderr(Stdio::piped()); cmd.env_remove("FABRO_JSON"); + if let Some(token) = std::env::var_os("FABRO_DEV_TOKEN") { + cmd.env("FABRO_DEV_TOKEN", token); + } #[cfg(unix)] fabro_proc::pre_exec_setpgid(cmd.as_std_mut()); diff --git a/lib/crates/fabro-server/src/web_auth.rs b/lib/crates/fabro-server/src/web_auth.rs index 191c3884c..47803e4e9 100644 --- a/lib/crates/fabro-server/src/web_auth.rs +++ b/lib/crates/fabro-server/src/web_auth.rs @@ -12,6 +12,7 @@ use cookie::time::Duration; use cookie::{Cookie, CookieJar, Expiration, Key, SameSite}; use fabro_config::Storage; use fabro_types::settings::{InterpString, SettingsLayer}; +use fabro_util::dev_token::validate_dev_token_format; use serde::{Deserialize, Serialize}; use serde_json::json; use tracing::{debug, error, info, warn}; @@ -19,21 +20,20 @@ use tracing::{debug, error, info, warn}; use crate::jwt_auth::{AuthMode, AuthStrategy, dev_token_matches}; use crate::server::AppState; use crate::server_secrets::ServerSecrets; -use fabro_util::dev_token::validate_dev_token_format; pub const SESSION_COOKIE_NAME: &str = "__fabro_session"; const OAUTH_STATE_COOKIE_NAME: &str = "fabro_oauth_state"; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SessionCookie { - pub login: String, - pub provider: String, - pub name: String, - pub email: String, - pub avatar_url: String, - pub user_url: String, + pub login: String, + pub provider: String, + pub name: String, + pub email: String, + pub avatar_url: String, + pub user_url: String, pub provider_id: Option, - pub exp: i64, + pub exp: i64, } #[derive(Deserialize)] @@ -224,8 +224,7 @@ async fn login_dev_token( return json_response(StatusCode::UNAUTHORIZED, json!({"error": "Unauthorized"})); }; - if !validate_dev_token_format(&payload.token) || !dev_token_matches(&payload.token, &expected) - { + if !validate_dev_token_format(&payload.token) || !dev_token_matches(&payload.token, &expected) { return json_response(StatusCode::UNAUTHORIZED, json!({"error": "Unauthorized"})); } @@ -889,7 +888,9 @@ mod tests { use serde_json::{Value, json}; use tower::ServiceExt; - use super::{GitHubManifestConversion, api_routes, merge_settings_keys, read_private_session, routes}; + use super::{ + GitHubManifestConversion, api_routes, merge_settings_keys, read_private_session, routes, + }; use crate::jwt_auth::{AuthMode, AuthStrategy}; use crate::server; diff --git a/lib/crates/fabro-util/src/dev_token.rs b/lib/crates/fabro-util/src/dev_token.rs index dbca567bb..97b5d44e9 100644 --- a/lib/crates/fabro-util/src/dev_token.rs +++ b/lib/crates/fabro-util/src/dev_token.rs @@ -55,7 +55,9 @@ pub fn load_or_create_dev_token(path: &Path) -> Result { let token = generate_dev_token(); let temp_path = path.with_file_name(format!( ".{}.tmp-{:x}", - path.file_name().and_then(|name| name.to_str()).unwrap_or("dev-token"), + path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or("dev-token"), rand::random::() )); write_private_token_file(&temp_path, &token)?; @@ -75,7 +77,9 @@ pub fn write_dev_token(path: &Path, token: &str) -> Result<()> { let temp_path = path.with_file_name(format!( ".{}.tmp-{:x}", - path.file_name().and_then(|name| name.to_str()).unwrap_or("dev-token"), + path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or("dev-token"), rand::random::() )); write_private_token_file(&temp_path, token)?; diff --git a/lib/crates/fabro-util/src/home.rs b/lib/crates/fabro-util/src/home.rs index b3f3d40db..a350411ce 100644 --- a/lib/crates/fabro-util/src/home.rs +++ b/lib/crates/fabro-util/src/home.rs @@ -84,9 +84,10 @@ impl Home { #[cfg(test)] mod tests { - use super::Home; use std::sync::{LazyLock, Mutex}; + use super::Home; + static ENV_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); #[test] @@ -143,6 +144,9 @@ mod tests { std::env::remove_var("HOME"); - assert_eq!(home.root(), std::path::Path::new("/tmp/fabro-home-env/.fabro")); + assert_eq!( + home.root(), + std::path::Path::new("/tmp/fabro-home-env/.fabro") + ); } }