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

@ -114,17 +114,19 @@ pub(super) async fn post_tool_groups(
State(state): State<AppState>,
AxumPath(name): AxumPath<String>,
axum::Json(body): axum::Json<SetToolGroupsBody>,
) -> Response {
) -> Result<Response, ProblemDetails> {
let logical = strip_container_prefix(&name);
// `guard_agent_name` yields a ready-made rejection `Response`; pass it
// through as `Ok` (axum sends it verbatim) rather than re-deriving a
// `ProblemDetails` — the guard is shared with `-> Response` handlers.
if let Some(reject) = guard_agent_name(&state, &logical).await {
return reject;
return Ok(reject);
}
// Validate group names before queuing — fail fast so the operator
// sees the error immediately rather than waiting for the worker.
if let Err(e) = crate::tool_groups::validate_groups(&body.groups) {
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("invalid tool-groups for {logical}: {e}"))
.into_response();
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("invalid tool-groups for {logical}: {e}")));
}
// Enqueue a PermChange so the JSON file write is serialised through
// the FIFO worker. Prevents concurrent batch-apply actions for
@ -139,7 +141,7 @@ pub(super) async fn post_tool_groups(
);
state.coord.emit_rebuild_queue_snapshot();
tracing::info!(agent = %logical, groups = ?body.groups, "operator: set tool-groups via dashboard");
(StatusCode::OK, "ok").into_response()
Ok((StatusCode::OK, "ok").into_response())
}
#[derive(Serialize)]
@ -197,10 +199,10 @@ pub(super) async fn post_capabilities(
State(state): State<AppState>,
AxumPath(name): AxumPath<String>,
axum::Json(body): axum::Json<SetCapabilitiesBody>,
) -> Response {
) -> Result<Response, ProblemDetails> {
let logical = strip_container_prefix(&name);
if let Some(reject) = guard_agent_name(&state, &logical).await {
return reject;
return Ok(reject);
}
let known: Vec<&str> = hive_sh4re::Capability::ALL
.iter()
@ -208,9 +210,8 @@ pub(super) async fn post_capabilities(
.collect();
for cap in &body.caps {
if !known.contains(&cap.as_str()) {
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("unknown capability: {cap}"))
.into_response();
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("unknown capability: {cap}")));
}
}
// Enqueue a PermChange so the JSON file write is serialised through
@ -226,7 +227,7 @@ pub(super) async fn post_capabilities(
);
state.coord.emit_rebuild_queue_snapshot();
tracing::info!(agent = %logical, caps = ?body.caps, "operator: set capabilities via dashboard");
(StatusCode::OK, "ok").into_response()
Ok((StatusCode::OK, "ok").into_response())
}
/// One agent's slice of a batch permission change. Sparse: an omitted
@ -261,7 +262,7 @@ type StagedPerm = (String, Option<Vec<String>>, Option<Vec<String>>);
pub(super) async fn post_permissions(
State(state): State<AppState>,
axum::Json(body): axum::Json<BatchPermsBody>,
) -> Response {
) -> Result<Response, ProblemDetails> {
let known_caps: Vec<&str> = hive_sh4re::Capability::ALL
.iter()
.map(|c| c.as_str())
@ -273,21 +274,19 @@ pub(super) async fn post_permissions(
for change in &body.changes {
let logical = strip_container_prefix(&change.agent);
if let Some(reject) = guard_agent_name(&state, &logical).await {
return reject;
return Ok(reject);
}
if let Some(groups) = &change.tool_groups
&& let Err(e) = crate::tool_groups::validate_groups(groups)
{
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("invalid tool-groups for {logical}: {e}"))
.into_response();
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("invalid tool-groups for {logical}: {e}")));
}
if let Some(caps) = &change.capabilities {
for cap in caps {
if !known_caps.contains(&cap.as_str()) {
return ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("unknown capability for {logical}: {cap}"))
.into_response();
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(format!("unknown capability for {logical}: {cap}")));
}
}
}
@ -310,7 +309,7 @@ pub(super) async fn post_permissions(
tracing::info!(agent = %logical, "operator: batch perm change via dashboard");
}
state.coord.emit_rebuild_queue_snapshot();
(StatusCode::OK, "ok").into_response()
Ok((StatusCode::OK, "ok").into_response())
}
#[cfg(test)]