Skip to content

Commit 055df9b

Browse files
committed
Wrap owner modification endpoints in database transaction.
1 parent a009846 commit 055df9b

File tree

3 files changed

+121
-54
lines changed

3 files changed

+121
-54
lines changed

src/controllers/krate/owners.rs

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -91,51 +91,57 @@ fn parse_owners_request(req: &mut dyn Request) -> CargoResult<Vec<String>> {
9191

9292
fn modify_owners(req: &mut dyn Request, add: bool) -> CargoResult<Response> {
9393
let logins = parse_owners_request(req)?;
94+
let app = req.app();
9495
let user = req.user()?;
96+
let crate_name = &req.params()["crate_id"];
9597
let conn = req.db_conn()?;
96-
let krate = Crate::by_name(&req.params()["crate_id"]).first::<Crate>(&*conn)?;
97-
let owners = krate.owners(&conn)?;
98-
99-
match user.rights(req.app(), &owners)? {
100-
Rights::Full => {}
101-
// Yes!
102-
Rights::Publish => {
103-
return Err(human("team members don't have permission to modify owners"));
104-
}
105-
Rights::None => {
106-
return Err(human("only owners have permission to modify owners"));
107-
}
108-
}
10998

110-
let mut msgs = Vec::new();
99+
conn.transaction(|| {
100+
let krate = Crate::by_name(crate_name).first::<Crate>(&*conn)?;
101+
let owners = krate.owners(&conn)?;
111102

112-
for login in &logins {
113-
if add {
114-
let login_test = |owner: &Owner| owner.login().to_lowercase() == *login.to_lowercase();
115-
if owners.iter().any(login_test) {
116-
return Err(human(&format_args!("`{}` is already an owner", login)));
103+
match user.rights(app, &owners)? {
104+
Rights::Full => {}
105+
// Yes!
106+
Rights::Publish => {
107+
return Err(human("team members don't have permission to modify owners"));
117108
}
118-
let msg = krate.owner_add(req.app(), &conn, user, login)?;
119-
msgs.push(msg);
120-
} else {
121-
// Removing the team that gives you rights is prevented because
122-
// team members only have Rights::Publish
123-
if owners.len() == 1 {
124-
return Err(human("cannot remove the sole owner of a crate"));
109+
Rights::None => {
110+
return Err(human("only owners have permission to modify owners"));
111+
}
112+
}
113+
114+
let mut msgs = Vec::new();
115+
116+
for login in &logins {
117+
if add {
118+
let login_test =
119+
|owner: &Owner| owner.login().to_lowercase() == *login.to_lowercase();
120+
if owners.iter().any(login_test) {
121+
return Err(human(&format_args!("`{}` is already an owner", login)));
122+
}
123+
let msg = krate.owner_add(req.app(), &conn, user, login)?;
124+
msgs.push(msg);
125+
} else {
126+
// Removing the team that gives you rights is prevented because
127+
// team members only have Rights::Publish
128+
if owners.len() == 1 {
129+
return Err(human("cannot remove the sole owner of a crate"));
130+
}
131+
krate.owner_remove(req.app(), &conn, user, login)?;
125132
}
126-
krate.owner_remove(req.app(), &conn, user, login)?;
127133
}
128-
}
129134

130-
let comma_sep_msg = msgs.join(",");
135+
let comma_sep_msg = msgs.join(",");
131136

132-
#[derive(Serialize)]
133-
struct R {
134-
ok: bool,
135-
msg: String,
136-
}
137-
Ok(req.json(&R {
138-
ok: true,
139-
msg: comma_sep_msg,
140-
}))
137+
#[derive(Serialize)]
138+
struct R {
139+
ok: bool,
140+
msg: String,
141+
}
142+
Ok(req.json(&R {
143+
ok: true,
144+
msg: comma_sep_msg,
145+
}))
146+
})
141147
}

src/tests/owners.rs

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::{
22
add_team_to_crate,
33
builders::{CrateBuilder, PublishBuilder},
44
new_team,
5-
util::RequestHelper,
5+
util::{MockCookieUser, MockTokenUser, RequestHelper},
66
TestApp,
77
};
88
use cargo_registry::{
@@ -26,7 +26,7 @@ struct InvitationListResponse {
2626
}
2727

2828
// Implementing locally for now, unless these are needed elsewhere
29-
impl crate::util::MockCookieUser {
29+
impl MockCookieUser {
3030
/// As the currently logged in user, accept an invitation to become an owner of the named
3131
/// crate.
3232
fn accept_ownership_invitation(&self, krate_name: &str, krate_id: i32) {
@@ -111,43 +111,89 @@ fn new_crate_owner() {
111111
.good();
112112
}
113113

114+
fn create_and_add_owner(
115+
app: &TestApp,
116+
token: &MockTokenUser,
117+
username: &str,
118+
krate: &Crate,
119+
) -> MockCookieUser {
120+
let user = app.db_new_user(username);
121+
token.add_user_owner(&krate.name, user.as_model());
122+
user.accept_ownership_invitation(&krate.name, krate.id);
123+
user
124+
}
125+
114126
// Ensures that so long as at least one owner remains associated with the crate,
115127
// a user can still remove their own login as an owner
116128
#[test]
117129
fn owners_can_remove_self() {
118130
let (app, _, user, token) = TestApp::init().with_token();
131+
let username = &user.as_model().gh_login;
119132

120133
let krate = app
121134
.db(|conn| CrateBuilder::new("owners_selfremove", user.as_model().id).expect_build(conn));
122135

123136
// Deleting yourself when you're the only owner isn't allowed.
124137
let json = token
125-
.remove_named_owner("owners_selfremove", &user.as_model().gh_login)
138+
.remove_named_owner("owners_selfremove", username)
126139
.bad_with_status(200);
127140
assert!(json.errors[0]
128141
.detail
129142
.contains("cannot remove the sole owner of a crate"));
130143

131-
let user2 = app.db_new_user("secondowner");
132-
token.add_user_owner("owners_selfremove", user2.as_model());
133-
user2.accept_ownership_invitation("owners_selfremove", krate.id);
144+
create_and_add_owner(&app, &token, "secondowner", &krate);
134145

135146
// Deleting yourself when there are other owners is allowed.
136147
let json = token
137-
.remove_named_owner("owners_selfremove", &user.as_model().gh_login)
148+
.remove_named_owner("owners_selfremove", username)
138149
.good();
139150
assert!(json.ok);
140151

141152
// After you delete yourself, you no longer have permisions to manage the crate.
142153
let json = token
143-
.remove_named_owner("owners_selfremove", &user2.as_model().gh_login)
154+
.remove_named_owner("owners_selfremove", username)
144155
.bad_with_status(200);
145-
146156
assert!(json.errors[0]
147157
.detail
148158
.contains("only owners have permission to modify owners",));
149159
}
150160

161+
// Verify consistency when adidng or removing multiple owners in a single request.
162+
#[test]
163+
fn modify_multiple_owners() {
164+
let (app, _, user, token) = TestApp::init().with_token();
165+
let username = &user.as_model().gh_login;
166+
167+
let krate =
168+
app.db(|conn| CrateBuilder::new("owners_multiple", user.as_model().id).expect_build(conn));
169+
170+
let user2 = create_and_add_owner(&app, &token, "user2", &krate);
171+
let user3 = create_and_add_owner(&app, &token, "user3", &krate);
172+
173+
// Deleting two owners at once is allowed.
174+
let json = token
175+
.remove_named_owners("owners_multiple", &["user2", "user3"])
176+
.good();
177+
assert!(json.ok);
178+
assert_eq!(app.db(|conn| krate.owners(&conn).unwrap()).len(), 1);
179+
180+
// Adding multiple users fails if one of them already is an owner.
181+
let json = token
182+
.add_named_owners("owners_multiple", &["user2", username])
183+
.bad_with_status(200);
184+
assert!(&json.errors[0].detail.contains("is already an owner"));
185+
assert_eq!(app.db(|conn| krate.owners(&conn).unwrap()).len(), 1);
186+
187+
// Adding multiple users at once succeeds.
188+
let json = token
189+
.add_named_owners("owners_multiple", &["user2", "user3"])
190+
.good();
191+
assert!(json.ok);
192+
user2.accept_ownership_invitation(&krate.name, krate.id);
193+
user3.accept_ownership_invitation(&krate.name, krate.id);
194+
assert_eq!(app.db(|conn| krate.owners(&conn).unwrap()).len(), 3);
195+
}
196+
151197
/* Testing the crate ownership between two crates and one team.
152198
Given two crates, one crate owned by both a team and a user,
153199
one only owned by a user, check that the CrateList returned

src/tests/util.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -497,18 +497,33 @@ impl MockTokenUser {
497497
&self.token
498498
}
499499

500-
/// Add to the specified crate the specified owner.
500+
/// Add to the specified crate the specified owners.
501+
pub fn add_named_owners(&self, krate_name: &str, owners: &[&str]) -> Response<OkBool> {
502+
self.modify_owners(krate_name, owners, Self::put)
503+
}
504+
505+
/// Add a single owner to the specified crate.
501506
pub fn add_named_owner(&self, krate_name: &str, owner: &str) -> Response<OkBool> {
502-
let url = format!("/api/v1/crates/{}/owners", krate_name);
503-
let body = format!("{{\"users\":[\"{}\"]}}", owner);
504-
self.put(&url, body.as_bytes())
507+
self.add_named_owners(krate_name, &[owner])
508+
}
509+
510+
/// Remove from the specified crate the specified owners.
511+
pub fn remove_named_owners(&self, krate_name: &str, owners: &[&str]) -> Response<OkBool> {
512+
self.modify_owners(krate_name, owners, Self::delete_with_body)
505513
}
506514

507-
/// Remove from the specified crate the specified owner.
515+
/// Remove a single owner to the specified crate.
508516
pub fn remove_named_owner(&self, krate_name: &str, owner: &str) -> Response<OkBool> {
517+
self.remove_named_owners(krate_name, &[owner])
518+
}
519+
520+
fn modify_owners<F>(&self, krate_name: &str, owners: &[&str], method: F) -> Response<OkBool>
521+
where
522+
F: Fn(&MockTokenUser, &str, &[u8]) -> Response<OkBool>,
523+
{
509524
let url = format!("/api/v1/crates/{}/owners", krate_name);
510-
let body = format!("{{\"users\":[\"{}\"]}}", owner);
511-
self.delete_with_body(&url, body.as_bytes())
525+
let body = format!("{{\"owners\":[\"{}\"]}}", owners.join("\", \""));
526+
method(&self, &url, body.as_bytes())
512527
}
513528

514529
/// Add a user as an owner for a crate.

0 commit comments

Comments
 (0)