swarm-controller: accept the legacy pre-alignment email in the lockdown guard
disable_repo_creation's ownership guard now matches either the aligned {agent}@hyperhive.local email or the legacy {agent}@hive.local one hive-c0re::forge::users::ensure_user_exists used before its own ensure_user_email alignment pass existed. That pass only runs on the forge-host hive, and only once it has a core token and has ticked, so a real pre-existing agent can still carry the old email when this node reads it. Without this, the guard would bail "not this agent's" on a genuine agent during that rollout window (argus, round 2). Verified separately (not a code change): swarm-controller's forge account is a site admin (created with --admin), and Forgejo's convert.toUser/ToUser only hides an account's email when the caller isn't the admin and isn't the account itself (services/convert/user.go), so user_get already returns the real email regardless of hide_email on the target account. No endpoint change needed for that half of the review.
This commit is contained in:
parent
58f50ed506
commit
2fda529ca8
1 changed files with 50 additions and 7 deletions
|
|
@ -408,11 +408,17 @@ impl Client {
|
|||
/// a 422's follow-up `user_get` only confirms the username, not that
|
||||
/// swarm-controller created it — so before that PATCH this reads the
|
||||
/// account once and checks the one thing that *is* comparable: the
|
||||
/// email `create_agent_user` sets at creation (see [`agent_email`]),
|
||||
/// the same marker `hive-c0re`'s legacy `ensure_user_exists` /
|
||||
/// `ensure_user_email` set and converge to. A mismatch means the name
|
||||
/// collided with an account this node didn't create, so it fails the
|
||||
/// node rather than sending the PATCH (or silently skipping) to an
|
||||
/// email. A match against either [`agent_email`] (what
|
||||
/// `create_agent_user` itself sets) or [`legacy_agent_email`] (what
|
||||
/// `hive-c0re::forge::users::ensure_user_exists` used before its own
|
||||
/// `ensure_user_email` alignment pass runs) counts as this agent's
|
||||
/// account — the legacy value has to be accepted too, not just the
|
||||
/// aligned one, because `ensure_user_email` only runs on the forge-host
|
||||
/// hive and only once it has a core token and has ticked at least once,
|
||||
/// so a real, existing legacy agent can still carry the old email when
|
||||
/// this node reads it mid-rollout. A mismatch against both means the
|
||||
/// name collided with an account this node didn't create, so it fails
|
||||
/// the node rather than sending the PATCH (or silently skipping) to an
|
||||
/// account nobody verified is agent-controlled.
|
||||
async fn disable_repo_creation(&self, agent: &str) -> Result<()> {
|
||||
let user = self
|
||||
|
|
@ -421,10 +427,11 @@ impl Client {
|
|||
.await
|
||||
.with_context(|| format!("read forge user {agent} before locking it down"))?;
|
||||
let expected = agent_email(agent);
|
||||
if user.email.as_deref() != Some(expected.as_str()) {
|
||||
let legacy = legacy_agent_email(agent);
|
||||
if !matches!(user.email.as_deref(), Some(e) if e == expected || e == legacy) {
|
||||
anyhow::bail!(
|
||||
"forge account `{agent}` exists but is not this agent's (email {:?}, expected \
|
||||
`{expected}`); refusing to lock it down",
|
||||
`{expected}` or the legacy `{legacy}`); refusing to lock it down",
|
||||
user.email
|
||||
);
|
||||
}
|
||||
|
|
@ -1146,6 +1153,19 @@ fn agent_email(agent: &str) -> String {
|
|||
format!("{agent}@hyperhive.local")
|
||||
}
|
||||
|
||||
/// The pre-alignment email `hive-c0re::forge::users::ensure_user_exists`
|
||||
/// gave an agent account before that crate's own `ensure_user_email`
|
||||
/// existed — see that function's doc comment ("Existing agents were
|
||||
/// created with `{name}@hive.local`; this corrects that"). Accepted by
|
||||
/// [`Client::disable_repo_creation`]'s guard alongside [`agent_email`]
|
||||
/// because `ensure_user_email`'s alignment PATCH only runs on the
|
||||
/// forge-host hive, and only after it has a core token and has ticked at
|
||||
/// least once, so a real legacy agent can still carry this value when this
|
||||
/// node reads it.
|
||||
fn legacy_agent_email(agent: &str) -> String {
|
||||
format!("{agent}@hive.local")
|
||||
}
|
||||
|
||||
/// The `admin_edit_user` body that sets `max_repo_creation = 0` on `agent`
|
||||
/// and nothing else. Same shape as `hive-c0re::forge::users`'
|
||||
/// `sparse_edit_user_option`: `login_name` + `source_id = 0` (local auth,
|
||||
|
|
@ -1606,4 +1626,27 @@ mod tests {
|
|||
let body = lockdown_patch(&seen, "alice").expect("no lockdown PATCH sent");
|
||||
assert_eq!(body["max_repo_creation"], 0);
|
||||
}
|
||||
|
||||
/// argus's round-2 🟡: `hive-c0re`'s `ensure_user_email` alignment pass
|
||||
/// (the thing that rewrites a legacy `{name}@hive.local` account to
|
||||
/// `{name}@hyperhive.local`) only runs on the forge-host hive, and only
|
||||
/// once it has a core token and has ticked — so a real, pre-existing
|
||||
/// agent can still be sitting on the pre-alignment `@hive.local` email
|
||||
/// when this node reads it. The guard must accept that value too, not
|
||||
/// just the aligned one, or a real agent would fail with "not this
|
||||
/// agent's" during the rollout window.
|
||||
#[tokio::test]
|
||||
async fn a_pre_alignment_legacy_email_still_gets_locked_down() {
|
||||
let (client, seen) = stub_forge(
|
||||
(409, r#"{"message":"user already exists"}"#),
|
||||
(200, r#"{"email":"alice@hive.local"}"#),
|
||||
(200, "{}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
client.ensure_agent_user("alice").await.unwrap();
|
||||
|
||||
let body = lockdown_patch(&seen, "alice").expect("no lockdown PATCH sent");
|
||||
assert_eq!(body["max_repo_creation"], 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue