agent permissions: a destroyed agent's key goes with it
Declaring an agent Destroyed now deletes its `agent-permissions` key, next to the queue-credential revoke and with the same failure handling: logged at error, the destroy goes ahead, and declaring it destroyed again re-runs it. Agent names are reusable after a destroy, and a create left on the default stores no key, so the old key handed a new agent of the same name the destroyed agent's grant (argus on #5293). Also: migrate.rs's create-time-setup note named ruth's tool-group seed in `ensure_root_agent`, which this PR deletes; it now says she holds the code-constant default when the swarm sets nothing. Part of #5170.
This commit is contained in:
parent
a937a8ba9a
commit
4f9e9bd680
3 changed files with 73 additions and 7 deletions
|
|
@ -206,6 +206,35 @@ pub(crate) async fn delete_agent_permissions(
|
|||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// Delete a destroyed agent's key. A name is free again once its agent is
|
||||
/// destroyed, and a create left on the default stores nothing, so a key left
|
||||
/// here would hand the next agent of that name this one's grant.
|
||||
///
|
||||
/// Like the credential revoke it runs beside, a failure is logged and does
|
||||
/// not stop the destroy; declaring the agent destroyed again re-runs it.
|
||||
pub(crate) async fn clear_on_destroy(state: &AppState, hive: &str, agent: &str) {
|
||||
let cleared = async {
|
||||
let client = queue(state).map_err(|(_, why)| anyhow::anyhow!(why))?;
|
||||
agent_permissions::open_or_create(&client)
|
||||
.await?
|
||||
.delete(agent)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await;
|
||||
match cleared {
|
||||
Ok(()) => tracing::info!(hive, agent, "destroyed agent's permissions cleared"),
|
||||
Err(e) => tracing::error!(
|
||||
hive,
|
||||
agent,
|
||||
error = %format!("{e:#}"),
|
||||
"clearing this agent's permissions FAILED; it was destroyed anyway, so a new \
|
||||
agent of the same name created on the default gets its grant. Declaring the \
|
||||
agent destroyed again re-runs this."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every permission name, and the set an agent the swarm sets nothing for
|
||||
/// holds — so a client lists them without a copy of its own.
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
|
|
@ -658,6 +687,41 @@ mod tests {
|
|||
assert_eq!(published(&client, "damocles").await, None);
|
||||
}
|
||||
|
||||
/// Destroying an agent deletes its key, so a new agent of the same name
|
||||
/// created on the default gets the default, not the old grant.
|
||||
#[tokio::test]
|
||||
async fn a_recreated_agent_does_not_inherit_the_destroyed_ones_grant() {
|
||||
let nats = TestNats::start();
|
||||
let client = nats.client().await;
|
||||
let state = crate::agent_limits::tests::declared(&client).await;
|
||||
let _ = put(&state, "atlas", granted()).await.expect("set");
|
||||
|
||||
let _ = crate::set_agent_state(
|
||||
State(state.clone()),
|
||||
Path(("pr1ma".to_owned(), "atlas".to_owned())),
|
||||
axum::Json(crate::SetAgentStateRequest {
|
||||
state: swarm_queue_client::wanted::AgentState::Destroyed,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("destroyed");
|
||||
let _ = crate::create_agent(
|
||||
State(state.clone()),
|
||||
axum::Json(crate::CreateAgentRequest {
|
||||
name: "atlas".to_owned(),
|
||||
hive: "pr1ma".to_owned(),
|
||||
limits: None,
|
||||
inference: None,
|
||||
permissions: None,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("recreated");
|
||||
|
||||
assert_eq!(published(&client, "atlas").await, None);
|
||||
assert_eq!(get(&state, "atlas").await, Permissions::default());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_catalogue_lists_every_permission_and_the_default() {
|
||||
let json = serde_json::to_value(super::get_permission_catalogue().await.0).unwrap();
|
||||
|
|
|
|||
|
|
@ -1410,11 +1410,12 @@ async fn set_agent_state(
|
|||
|
||||
// After the declaration and never before it: the write above is the
|
||||
// destroy order, and until it lands the agent is not being torn down, so
|
||||
// its credential has to keep working. Sequenced rather than spawned so
|
||||
// the revocation attempt is finished — and its outcome logged — by the
|
||||
// time the operator's call returns.
|
||||
// its credential and permissions have to keep working. Sequenced rather
|
||||
// than spawned so both attempts are finished — and their outcomes
|
||||
// logged — by the time the operator's call returns.
|
||||
if revokes_queue_credential(req.state) {
|
||||
revoke_or_complain(&hive, &agent).await;
|
||||
agent_permissions::clear_on_destroy(&state, &hive, &agent).await;
|
||||
}
|
||||
Ok(Json(render(&declaration)))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue