Skip to content

Commit 6f9226b

Browse files
authored
Merge pull request #9930 from Turbo87/issue-2736
Fix owner removal for duplicate logins
2 parents 3ba3e40 + 84d52e5 commit 6f9226b

File tree

7 files changed

+112
-32
lines changed

7 files changed

+112
-32
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/models/owner.rs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -79,26 +79,6 @@ impl Owner {
7979
}
8080
}
8181

82-
/// Finds the owner by name. Never recreates a team, to ensure that
83-
/// organizations that were deleted after they were added can still be
84-
/// removed.
85-
///
86-
/// May be a user's GH login or a full team name. This is case
87-
/// sensitive.
88-
pub fn find_by_login(conn: &mut impl Conn, name: &str) -> AppResult<Owner> {
89-
if name.contains(':') {
90-
Team::find_by_login(conn, name)
91-
.optional()?
92-
.map(Owner::Team)
93-
.ok_or_else(|| bad_request(format_args!("could not find team with login `{name}`")))
94-
} else {
95-
User::find_by_login(conn, name)
96-
.optional()?
97-
.map(Owner::User)
98-
.ok_or_else(|| bad_request(format_args!("could not find user with login `{name}`")))
99-
}
100-
}
101-
10282
pub fn kind(&self) -> i32 {
10383
match self {
10484
Owner::User(_) => OwnerKind::User as i32,

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/issues/issue2736.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use crate::models::{CrateOwner, OwnerKind};
2+
use crate::tests::builders::CrateBuilder;
3+
use crate::tests::util::{RequestHelper, TestApp};
4+
use crates_io_database::schema::{crate_owners, users};
5+
use diesel::prelude::*;
6+
use http::StatusCode;
7+
use insta::assert_snapshot;
8+
9+
/// See <https://github.com/rust-lang/crates.io/issues/2736>.
10+
#[tokio::test(flavor = "multi_thread")]
11+
async fn test_issue_2736() -> anyhow::Result<()> {
12+
let (app, _) = TestApp::full().empty();
13+
let mut conn = app.db_conn();
14+
15+
// - A user had a GitHub account named, let's say, `foo`
16+
let foo1 = app.db_new_user("foo");
17+
18+
// - Another user `someone_else` added them as an owner of a crate
19+
let someone_else = app.db_new_user("someone_else");
20+
21+
let krate = CrateBuilder::new("crate1", someone_else.as_model().id).expect_build(&mut conn);
22+
23+
diesel::insert_into(crate_owners::table)
24+
.values(CrateOwner {
25+
crate_id: krate.id,
26+
owner_id: foo1.as_model().id,
27+
created_by: someone_else.as_model().id,
28+
owner_kind: OwnerKind::User,
29+
email_notifications: true,
30+
})
31+
.execute(&mut conn)?;
32+
33+
// - `foo` deleted their GitHub account (but crates.io has no real knowledge of this)
34+
// - `foo` recreated their GitHub account with the same username (because it was still available), but in this situation GitHub assigns them a new ID
35+
// - When `foo` now logs in to crates.io, it's a different account than their old `foo` crates.io account because of the new GitHub ID (and if it wasn't, this would be a security problem)
36+
let foo2 = app.db_new_user("foo");
37+
38+
let github_ids = users::table
39+
.filter(users::gh_login.eq("foo"))
40+
.select(users::gh_id)
41+
.load::<i32>(&mut conn)?;
42+
43+
assert_eq!(github_ids.len(), 2);
44+
assert_ne!(github_ids[0], github_ids[1]);
45+
46+
// - The new `foo` account is NOT an owner of the crate
47+
let owners = krate.owners(&mut conn)?;
48+
assert_eq!(owners.len(), 2);
49+
assert_none!(owners.iter().find(|o| o.id() == foo2.as_model().id));
50+
51+
// Removing an owner, whether it's valid/current or not, should always work (if performed by another valid owner, etc)
52+
let response = someone_else.remove_named_owner("crate1", "foo").await;
53+
assert_eq!(response.status(), StatusCode::OK);
54+
assert_snapshot!(response.text(), @r#"{"msg":"owners successfully removed","ok":true}"#);
55+
56+
let owners = krate.owners(&mut conn)?;
57+
assert_eq!(owners.len(), 1);
58+
assert_eq!(owners[0].id(), someone_else.as_model().id);
59+
60+
// Once that removal works, it should be possible to add the new account as an owner
61+
let response = someone_else.add_named_owner("crate1", "foo").await;
62+
assert_eq!(response.status(), StatusCode::OK);
63+
assert_snapshot!(response.text(), @r#"{"msg":"user foo has been invited to be an owner of crate crate1","ok":true}"#);
64+
65+
Ok(())
66+
}

src/tests/issues/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
mod issue1205;
2+
mod issue2736;

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)