Skip to content

Commit 6d897d3

Browse files
committed
Fix owner removal for duplicate logins
When users or teams on GitHub are deleted and recreated they might exist multiple times within our own database with the same `login`. When owners are deleted, only the newest matching `login` is considered for deletion though, which makes it impossible to delete older users/teams. This commit fixes the problem by deleting **all** owners with a matching `login` instead. Unfortunately, `diesel` does not support `UPDATE` queries with `JOINs`, so this has to be a "raw" SQL query with bindings instead…
1 parent 8574435 commit 6d897d3

File tree

4 files changed

+45
-12
lines changed

4 files changed

+45
-12
lines changed

src/models/krate.rs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use chrono::NaiveDateTime;
22
use diesel::associations::Identifiable;
33
use diesel::dsl;
44
use diesel::pg::Pg;
5-
use diesel::sql_types::{Bool, Text};
5+
use diesel::sql_types::{Bool, Integer, Text};
66
use diesel_async::AsyncPgConnection;
77
use secrecy::SecretString;
88
use thiserror::Error;
@@ -18,7 +18,7 @@ use crate::schema::*;
1818
use crate::sql::canon_crate_name;
1919
use crate::util::diesel::prelude::*;
2020
use crate::util::diesel::Conn;
21-
use crate::util::errors::{version_not_found, AppResult};
21+
use crate::util::errors::{bad_request, version_not_found, AppResult};
2222
use crate::{app::App, util::errors::BoxedAppError};
2323

2424
use super::Team;
@@ -457,12 +457,44 @@ impl Crate {
457457
pub fn owner_remove(&self, conn: &mut impl Conn, login: &str) -> AppResult<()> {
458458
use diesel::RunQueryDsl;
459459

460-
let owner = Owner::find_by_login(conn, login)?;
460+
let query = diesel::sql_query(
461+
r#"WITH crate_owners_with_login AS (
462+
SELECT
463+
crate_owners.*,
464+
CASE WHEN crate_owners.owner_kind = 1 THEN
465+
teams.login
466+
ELSE
467+
users.gh_login
468+
END AS login
469+
FROM crate_owners
470+
LEFT JOIN teams
471+
ON crate_owners.owner_id = teams.id
472+
AND crate_owners.owner_kind = 1
473+
LEFT JOIN users
474+
ON crate_owners.owner_id = users.id
475+
AND crate_owners.owner_kind = 0
476+
WHERE crate_owners.crate_id = $1
477+
AND crate_owners.deleted = false
478+
)
479+
UPDATE crate_owners
480+
SET deleted = true
481+
FROM crate_owners_with_login
482+
WHERE crate_owners.crate_id = crate_owners_with_login.crate_id
483+
AND crate_owners.owner_id = crate_owners_with_login.owner_id
484+
AND crate_owners.owner_kind = crate_owners_with_login.owner_kind
485+
AND lower(crate_owners_with_login.login) = lower($2);"#,
486+
);
461487

462-
let target = crate_owners::table.find((self.id(), owner.id(), owner.kind()));
463-
diesel::update(target)
464-
.set(crate_owners::deleted.eq(true))
488+
let num_updated_rows = query
489+
.bind::<Integer, _>(self.id)
490+
.bind::<Text, _>(login)
465491
.execute(conn)?;
492+
493+
if num_updated_rows == 0 {
494+
let error = format!("could not find owner with login `{login}`");
495+
return Err(bad_request(error));
496+
}
497+
466498
Ok(())
467499
}
468500

src/tests/issues/issue1205.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ async fn test_issue_1205() -> anyhow::Result<()> {
4141
.remove_named_owner(CRATE_NAME, "github:rustaudio:owners")
4242
.await;
4343
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
44-
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find team with login `github:rustaudio:owners`"}]}"#);
44+
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with login `github:rustaudio:owners`"}]}"#);
4545

4646
Ok(())
4747
}

src/tests/routes/crates/owners/remove.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async fn test_unknown_user() {
5858

5959
let response = cookie.remove_named_owner("foo", "unknown").await;
6060
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
61-
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find user with login `unknown`"}]}"#);
61+
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with login `unknown`"}]}"#);
6262
}
6363

6464
#[tokio::test(flavor = "multi_thread")]
@@ -72,7 +72,7 @@ async fn test_unknown_team() {
7272
.remove_named_owner("foo", "github:unknown:unknown")
7373
.await;
7474
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
75-
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find team with login `github:unknown:unknown`"}]}"#);
75+
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with login `github:unknown:unknown`"}]}"#);
7676
}
7777

7878
#[tokio::test(flavor = "multi_thread")]

src/tests/team.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,13 +270,14 @@ async fn remove_nonexistent_team() {
270270
.execute(&mut conn)
271271
.expect("couldn't insert nonexistent team");
272272

273-
token
273+
let response = token
274274
.remove_named_owner(
275275
"foo_remove_nonexistent",
276276
"github:test-org:this-does-not-exist",
277277
)
278-
.await
279-
.good();
278+
.await;
279+
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
280+
assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"could not find owner with login `github:test-org:this-does-not-exist`"}]}"#);
280281
}
281282

282283
/// Test trying to publish a crate we don't own

0 commit comments

Comments
 (0)