swarm-claude-login: refuse a step whose agent name is not an identifier
The minter parses `Request.agent` with `Ident::parse`, as the controller does, before it makes a scratch directory, runs `claude auth login` or touches the store, and answers `Failed` otherwise. The name becomes a directory name and a store path in the one service that writes every agent's credentials, so it is checked at this service's own boundary. The doc states that the `operatorsOnly` authelia rule, not the controller, keeps the routes to operators, and that a stale version record waits for a relogin. The env-strip comment says it is defence in depth. Refs #5268
This commit is contained in:
parent
d5de1e4943
commit
693e6e738b
5 changed files with 104 additions and 3 deletions
|
|
@ -124,6 +124,16 @@ async fn handle(payload: &[u8], logins: &Arc<login::Logins>) -> Reply {
|
|||
};
|
||||
}
|
||||
};
|
||||
// Before anything else: the name becomes a scratch directory's name and
|
||||
// a store path, and this service holds write on every agent's leaf.
|
||||
let (Request::Start { agent, .. } | Request::Code { agent, .. } | Request::Cancel { agent }) =
|
||||
&request;
|
||||
if let Err(reason) = hive_types::Ident::parse(agent) {
|
||||
tracing::warn!(agent = ?agent, reason, "a login step for an invalid agent name; refused");
|
||||
return Reply::Failed {
|
||||
error: format!("{agent:?} is not an agent name: {reason}"),
|
||||
};
|
||||
}
|
||||
let (agent, reply) = match request {
|
||||
Request::Start { agent, operator } => {
|
||||
tracing::info!(%agent, %operator, "starting a claude login");
|
||||
|
|
@ -190,3 +200,73 @@ async fn client_secret(client_id: &str) -> Result<String> {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use swarm_queue_client::claude_login::{Reply, Request};
|
||||
|
||||
use super::{handle, login};
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_step_for_an_invalid_agent_name_is_refused_before_anything_is_made() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"swarm-claude-login-test-hostile-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let scratch = root.join("scratch");
|
||||
std::fs::create_dir_all(&scratch).expect("temp dir");
|
||||
let logins = Arc::new(login::Logins::new(
|
||||
login::tests::stubs().join("claude"),
|
||||
scratch.clone(),
|
||||
Duration::from_secs(10),
|
||||
));
|
||||
for agent in ["../escaped", "..", "", "a/b", "Atlas"] {
|
||||
for request in [
|
||||
Request::Start {
|
||||
agent: agent.to_owned(),
|
||||
operator: "op".to_owned(),
|
||||
},
|
||||
Request::Code {
|
||||
agent: agent.to_owned(),
|
||||
code: "c".to_owned(),
|
||||
},
|
||||
Request::Cancel {
|
||||
agent: agent.to_owned(),
|
||||
},
|
||||
] {
|
||||
let payload = serde_json::to_vec(&request).expect("encodes");
|
||||
let reply = handle(&payload, &logins).await;
|
||||
assert!(
|
||||
matches!(reply, Reply::Failed { .. }),
|
||||
"{agent:?}: {reply:?}"
|
||||
);
|
||||
}
|
||||
let made: Vec<_> = std::fs::read_dir(&root)
|
||||
.expect("root")
|
||||
.map(|e| e.expect("entry").file_name())
|
||||
.collect();
|
||||
assert_eq!(made, vec!["scratch"], "{agent:?} made a directory");
|
||||
assert_eq!(
|
||||
std::fs::read_dir(&scratch).expect("scratch").count(),
|
||||
0,
|
||||
"{agent:?}"
|
||||
);
|
||||
}
|
||||
// The control: a valid name starts.
|
||||
let payload = serde_json::to_vec(&Request::Start {
|
||||
agent: "atlas".to_owned(),
|
||||
operator: "op".to_owned(),
|
||||
})
|
||||
.expect("encodes");
|
||||
assert!(matches!(
|
||||
handle(&payload, &logins).await,
|
||||
Reply::Started { .. }
|
||||
));
|
||||
drop(logins);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue