feat(dashboard): batch POST /api/permissions for save-all perms (#1719)
This commit is contained in:
parent
75dbcc2aa2
commit
8e24814efe
5 changed files with 163 additions and 0 deletions
|
|
@ -223,6 +223,86 @@ pub(super) async fn post_capabilities(
|
|||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
/// One agent's slice of a batch permission change. Sparse: an omitted
|
||||
/// field leaves that perm-type untouched, an empty array clears it, a
|
||||
/// populated array fully replaces it (same replace semantics as the
|
||||
/// per-agent endpoints).
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct PermChangeBody {
|
||||
agent: String,
|
||||
#[serde(default)]
|
||||
tool_groups: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
capabilities: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct BatchPermsBody {
|
||||
changes: Vec<PermChangeBody>,
|
||||
}
|
||||
|
||||
/// A validated, non-empty change staged for enqueue:
|
||||
/// `(logical agent, new groups?, new caps?)`.
|
||||
type StagedPerm = (String, Option<Vec<String>>, Option<Vec<String>>);
|
||||
|
||||
/// Batch permission apply — `POST /api/permissions`. The save-all
|
||||
/// permissions UI sends only the perm-types that actually changed per
|
||||
/// agent; each affected agent gets ONE combined `PermChange`, so the
|
||||
/// dedup key collapses to `(kind, agent)` and an agent whose caps AND
|
||||
/// groups both changed rebuilds once, not twice. The whole batch is
|
||||
/// atomic: every change is validated up front and on any validation
|
||||
/// error nothing is written or enqueued.
|
||||
pub(super) async fn post_permissions(
|
||||
State(state): State<AppState>,
|
||||
axum::Json(body): axum::Json<BatchPermsBody>,
|
||||
) -> Response {
|
||||
let known_caps: Vec<&str> = hive_sh4re::Capability::ALL
|
||||
.iter()
|
||||
.map(|c| c.as_str())
|
||||
.collect();
|
||||
// Phase 1 — validate everything before touching any file or the
|
||||
// queue, so a bad entry fails the whole POST with zero side effects.
|
||||
// No-op rows (both fields omitted) are skipped, not errors.
|
||||
let mut staged: Vec<StagedPerm> = Vec::new();
|
||||
for change in &body.changes {
|
||||
let logical = strip_container_prefix(&change.agent);
|
||||
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
||||
return reject;
|
||||
}
|
||||
if let Some(groups) = &change.tool_groups
|
||||
&& let Err(e) = crate::tool_groups::validate_groups(groups)
|
||||
{
|
||||
return error_response(&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 error_response(&format!("unknown capability for {logical}: {cap}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if change.tool_groups.is_some() || change.capabilities.is_some() {
|
||||
staged.push((
|
||||
logical,
|
||||
change.tool_groups.clone(),
|
||||
change.capabilities.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// Phase 2 — enqueue one combined PermChange per affected agent.
|
||||
for (logical, groups, caps) in staged {
|
||||
state.coord.rebuild_queue.enqueue_with_perm(
|
||||
logical.clone(),
|
||||
crate::rebuild_queue::QueueSource::Manual,
|
||||
"batch permission change via permissions UI".to_owned(),
|
||||
crate::rebuild_queue::PermPayload::Combined { groups, caps },
|
||||
);
|
||||
tracing::info!(agent = %logical, "operator: batch perm change via dashboard");
|
||||
}
|
||||
state.coord.emit_rebuild_queue_snapshot();
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::roster_and_effective;
|
||||
|
|
|
|||
Loading…
Reference in a new issue