swarm UI: delete linked accounts — R2 fixes
Addresses argus review comment 90297 on PR #4899: - swarm-controller/README.md: list the three DELETE routes (including matrix's ?revoke=true) beside the PUT/GET ones already documented. - LinkedAccounts.tsx: a delete answering 404 means the account is already gone, so treat it as the delete's end state — re-fetch and close the dialog instead of showing an error. - matrix_account.rs: matrix_logout treats a 401 M_UNKNOWN_TOKEN as the token already being revoked and proceeds with the delete; every other logout failure still keeps the account. Adds unit tests and updates docs/swarm/ui.md to match.
This commit is contained in:
parent
bbf931207f
commit
b90be9e65e
4 changed files with 72 additions and 11 deletions
|
|
@ -75,13 +75,16 @@ request:
|
|||
swarm-controller removes every version of the entry from the swarm secret
|
||||
store, and answers 404 when the store holds nothing there. It refuses `main`: the
|
||||
swarm mints that account and would re-mint it. The panel requests the list
|
||||
again after a delete.
|
||||
again after a delete, including a 404 one: the account being already gone is
|
||||
the end state the delete wanted, so the dialog closes without an error.
|
||||
|
||||
The matrix confirmation has a checkbox, off by default, that logs the token
|
||||
out at its homeserver first (`?revoke=true`). If the homeserver doesn't
|
||||
confirm the logout, or the account has no homeserver stored, the account stays
|
||||
in the store and the dialog shows why. Deleting a forge account or GitHub token
|
||||
leaves the token valid at its provider; revoke it there.
|
||||
out at its homeserver first (`?revoke=true`). A 401 `M_UNKNOWN_TOKEN` from
|
||||
that logout means the homeserver already doesn't recognise the token, so the
|
||||
delete proceeds as if it had succeeded. Any other failed logout, or an account
|
||||
with no homeserver stored, leaves the account in the store and the dialog
|
||||
shows why. Deleting a forge account or GitHub token leaves the token valid at
|
||||
its provider; revoke it there.
|
||||
|
||||
The agent isn't told about a delete. Its matrix daemon drops the account when
|
||||
it next lists the store, within two minutes. Its forge and GitHub units never
|
||||
|
|
|
|||
|
|
@ -96,7 +96,9 @@ export function LinkedAccounts({
|
|||
deleteTarget.kind === "matrix" && revoke ? `${url}?revoke=true` : url,
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
if (!r.ok) {
|
||||
// A 404 means the account the dialog is about is already gone — the
|
||||
// end state the delete wanted — so it closes the same as a success.
|
||||
if (!r.ok && r.status !== 404) {
|
||||
setDeleteError(await readApiError(r));
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ The swarm's control plane. The swarm UI and `swarmctl` are its clients.
|
|||
`.../forge-accounts/{label}`, `.../github-account`); distinct from the agent's own swarm-minted
|
||||
accounts above. `GET .../linked-accounts` lists them, plus the agent's own
|
||||
`main` matrix account, by kind, name and host, never with a credential.
|
||||
Each of the three also takes a `DELETE`; matrix's also takes
|
||||
`?revoke=true`, which logs the stored token out at its homeserver first and
|
||||
leaves the account in place if that fails.
|
||||
- **Config PR status** — each agent's open config-repo PR, cached from forge
|
||||
webhooks (`GET /api/config-prs`, `/api/agents/{name}/config-pr`).
|
||||
- **Swarm-wide forge objects and webhooks** →
|
||||
|
|
|
|||
|
|
@ -359,6 +359,10 @@ async fn matrix_password_login(
|
|||
/// POST `<homeserver>/_matrix/client/v3/logout` with `token`, which
|
||||
/// invalidates that token at the homeserver.
|
||||
///
|
||||
/// A 401 `M_UNKNOWN_TOKEN` means the homeserver already does not recognise
|
||||
/// the token — the state a logout wants — so that one answer counts as
|
||||
/// success rather than failure. Every other non-2xx is a failure.
|
||||
///
|
||||
/// The error names the status and the homeserver's `error` text, never the
|
||||
/// token.
|
||||
pub(crate) async fn matrix_logout(homeserver: &str, token: &str) -> Result<(), String> {
|
||||
|
|
@ -377,10 +381,17 @@ pub(crate) async fn matrix_logout(homeserver: &str, token: &str) -> Result<(), S
|
|||
if status.is_success() {
|
||||
return Ok(());
|
||||
}
|
||||
let err = resp
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.ok()
|
||||
let body = resp.json::<serde_json::Value>().await.ok();
|
||||
if status == StatusCode::UNAUTHORIZED
|
||||
&& body
|
||||
.as_ref()
|
||||
.and_then(|j| j.get("errcode"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some("M_UNKNOWN_TOKEN")
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let err = body
|
||||
.and_then(|j| {
|
||||
j.get("error")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
|
|
@ -394,7 +405,7 @@ pub(crate) async fn matrix_logout(homeserver: &str, token: &str) -> Result<(), S
|
|||
mod tests {
|
||||
use super::{
|
||||
PutMatrixAccountRequest, homeserver_or_configured_default, is_reserved_account,
|
||||
password_fields, resolve_credential, token_credential,
|
||||
matrix_logout, password_fields, resolve_credential, token_credential,
|
||||
};
|
||||
|
||||
fn request(mode: &str) -> PutMatrixAccountRequest {
|
||||
|
|
@ -600,4 +611,46 @@ mod tests {
|
|||
"{problem:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A stand-in homeserver on a loopback port, answering every
|
||||
/// `/_matrix/client/v3/logout` with `status` and `body`.
|
||||
async fn stub_homeserver(status: u16, body: &'static str) -> String {
|
||||
let app = axum::Router::new().route(
|
||||
"/_matrix/client/v3/logout",
|
||||
axum::routing::post(move || async move {
|
||||
(
|
||||
axum::http::StatusCode::from_u16(status).unwrap(),
|
||||
[(axum::http::header::CONTENT_TYPE, "application/json")],
|
||||
body,
|
||||
)
|
||||
}),
|
||||
);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
tokio::spawn(async move { axum::serve(listener, app).await });
|
||||
format!("http://{addr}")
|
||||
}
|
||||
|
||||
/// A revoke whose token the homeserver already does not recognise has
|
||||
/// reached logout's end state, not failed it.
|
||||
#[tokio::test]
|
||||
async fn logout_treats_401_m_unknown_token_as_already_revoked() {
|
||||
let homeserver = stub_homeserver(
|
||||
401,
|
||||
r#"{"errcode":"M_UNKNOWN_TOKEN","error":"Access token invalid"}"#,
|
||||
)
|
||||
.await;
|
||||
assert!(matrix_logout(&homeserver, "t0k3n").await.is_ok());
|
||||
}
|
||||
|
||||
/// The control: a 401 with a different errcode is still a failure, so
|
||||
/// the fold above is specific to `M_UNKNOWN_TOKEN` and not any 401.
|
||||
#[tokio::test]
|
||||
async fn logout_fails_on_other_401s() {
|
||||
let homeserver = stub_homeserver(401, r#"{"errcode":"M_FORBIDDEN","error":"nope"}"#).await;
|
||||
let err = matrix_logout(&homeserver, "t0k3n")
|
||||
.await
|
||||
.expect_err("not M_UNKNOWN_TOKEN");
|
||||
assert!(err.contains("401"), "{err}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue