dashboard: return problem details directly from client-error handlers

This commit is contained in:
damocles 2026-06-22 15:07:57 +02:00 committed by mara
commit 65ad994c85
8 changed files with 102 additions and 100 deletions

View file

@ -15,7 +15,7 @@ use serde::Deserialize;
use problem_details::ProblemDetails;
use super::{error_response, strip_container_prefix, validate_agent_name};
use super::{error_problem, strip_container_prefix, validate_agent_name};
use crate::lifecycle;
#[derive(Deserialize)]
@ -36,13 +36,14 @@ pub(super) struct JournalQuery {
pub(super) async fn get_journal(
AxumPath(name): AxumPath<String>,
axum::extract::Query(q): axum::extract::Query<JournalQuery>,
) -> Response {
) -> Result<Response, ProblemDetails> {
// Defense-in-depth format check so weird chars never reach the
// shellout below — the `lifecycle::list()` existence check would
// catch them anyway, but rejecting at the boundary keeps the
// failure mode crisp.
if let Some(reason) = validate_agent_name(&name) {
return (StatusCode::BAD_REQUEST, format!("bad agent name: {reason}")).into_response();
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("bad agent name: {reason}")));
}
// Validate the container name against the list of managed
// containers so we don't shell out with arbitrary input.
@ -50,9 +51,8 @@ pub(super) async fn get_journal(
let prefixed = format!("{}{container}", lifecycle::AGENT_PREFIX);
let live = lifecycle::list().await.unwrap_or_default();
if !live.iter().any(|c| c == &prefixed) {
return ProblemDetails::from_status_code(StatusCode::NOT_FOUND)
.with_detail(format!("journal: no managed container {prefixed:?}"))
.into_response();
return Err(ProblemDetails::from_status_code(StatusCode::NOT_FOUND)
.with_detail(format!("journal: no managed container {prefixed:?}")));
}
let lines = q.lines.unwrap_or(500).min(5000);
let unit = match q.unit.as_deref().filter(|s| !s.is_empty()) {
@ -65,9 +65,8 @@ pub(super) async fn get_journal(
format!("{u}.service")
};
if !allowed.contains(&unit.as_str()) {
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("journal: unknown unit {unit:?}"))
.into_response();
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("journal: unknown unit {unit:?}")));
}
Some(unit)
}
@ -92,9 +91,9 @@ pub(super) async fn get_journal(
body.push_str("\n--- stderr ---\n");
body.push_str(&stderr);
}
([("content-type", "text/plain; charset=utf-8")], body).into_response()
Ok(([("content-type", "text/plain; charset=utf-8")], body).into_response())
}
Err(e) => error_response(&format!("journal read: {e:#}")),
Err(e) => Err(error_problem(&format!("journal read: {e:#}"))),
}
}
@ -114,7 +113,7 @@ pub(super) struct JournalHostQuery {
/// dashboard binding to a host-only port.
pub(super) async fn get_journal_host(
axum::extract::Query(q): axum::extract::Query<JournalHostQuery>,
) -> Response {
) -> Result<Response, ProblemDetails> {
let lines = q.lines.unwrap_or(500).min(5000);
let allowed = ["hive-c0re.service"];
let mut cmd = tokio::process::Command::new("journalctl");
@ -127,9 +126,8 @@ pub(super) async fn get_journal_host(
format!("{u}.service")
};
if !allowed.contains(&unit.as_str()) {
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("journal-host: unknown unit {unit:?}"))
.into_response();
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("journal-host: unknown unit {unit:?}")));
}
cmd.args(["-u", &unit]);
}
@ -140,8 +138,8 @@ pub(super) async fn get_journal_host(
body.push_str("\n--- stderr ---\n");
body.push_str(&String::from_utf8_lossy(&out.stderr));
}
([("content-type", "text/plain; charset=utf-8")], body).into_response()
Ok(([("content-type", "text/plain; charset=utf-8")], body).into_response())
}
Err(e) => error_response(&format!("journalctl spawn: {e}")),
Err(e) => Err(error_problem(&format!("journalctl spawn: {e}"))),
}
}