Skip to content

Commit 7976674

Browse files
committed
Fix checks and consistency in the API endpoints to manage ownership.
Fixes #1583. The API endpoints to add and remove owners for a crate has multiple issues. * The database interactions are not wrapped in a transaction, making them prone to race conditions. * The lack of a transaction can also result in partially applied operations, e.g. when adding or removing multiple owners at once. * The check whether the last owner was removed only triggers if the _original_ list of owners had a length of exactly one. This is wrong in at least two cases: * Teams do not have permission to manage ownership, so they should not be counted. * If multiple users are removed at once, we can end up with no owners even when there originally was more than one user. This change fixes all mentioned problems, and adds tests for all bullet points except for the first one (which can't be tested with reasonable effort).
1 parent 922a7c9 commit 7976674

File tree

4 files changed

+158
-65
lines changed

4 files changed

+158
-65
lines changed

src/controllers/krate/owners.rs

Lines changed: 60 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -68,70 +68,83 @@ pub fn remove_owners(req: &mut dyn Request) -> CargoResult<Response> {
6868
modify_owners(req, false)
6969
}
7070

71-
fn modify_owners(req: &mut dyn Request, add: bool) -> CargoResult<Response> {
71+
/// Parse the JSON request body of requests to modify the owners of a crate.
72+
/// The format is
73+
///
74+
/// {"owners": ["username", "github:org:team", ...]}
75+
fn parse_owners_request(req: &mut dyn Request) -> CargoResult<Vec<String>> {
7276
let mut body = String::new();
7377
req.body().read_to_string(&mut body)?;
74-
75-
let user = req.user()?;
76-
let conn = req.db_conn()?;
77-
let krate = Crate::by_name(&req.params()["crate_id"]).first::<Crate>(&*conn)?;
78-
let owners = krate.owners(&conn)?;
79-
80-
match user.rights(req.app(), &owners)? {
81-
Rights::Full => {}
82-
// Yes!
83-
Rights::Publish => {
84-
return Err(human("team members don't have permission to modify owners"));
85-
}
86-
Rights::None => {
87-
return Err(human("only owners have permission to modify owners"));
88-
}
89-
}
90-
9178
#[derive(Deserialize)]
9279
struct Request {
9380
// identical, for back-compat (owners preferred)
9481
users: Option<Vec<String>>,
9582
owners: Option<Vec<String>>,
9683
}
97-
9884
let request: Request =
9985
serde_json::from_str(&body).map_err(|_| human("invalid json request"))?;
100-
101-
let logins = request
86+
request
10287
.owners
10388
.or(request.users)
104-
.ok_or_else(|| human("invalid json request"))?;
89+
.ok_or_else(|| human("invalid json request"))
90+
}
91+
92+
fn modify_owners(req: &mut dyn Request, add: bool) -> CargoResult<Response> {
93+
let logins = parse_owners_request(req)?;
94+
let app = req.app();
95+
let user = req.user()?;
96+
let crate_name = &req.params()["crate_id"];
97+
let conn = req.db_conn()?;
10598

106-
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)?;
107102

108-
for login in &logins {
109-
if add {
110-
let login_test = |owner: &Owner| owner.login().to_lowercase() == *login.to_lowercase();
111-
if owners.iter().any(login_test) {
112-
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"));
113108
}
114-
let msg = krate.owner_add(req.app(), &conn, user, login)?;
115-
msgs.push(msg);
116-
} else {
117-
// Removing the team that gives you rights is prevented because
118-
// team members only have Rights::Publish
119-
if owners.len() == 1 {
120-
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"));
121111
}
122-
krate.owner_remove(req.app(), &conn, user, login)?;
123112
}
124-
}
125113

126-
let comma_sep_msg = msgs.join(",");
114+
let comma_sep_msg = if add {
115+
let mut msgs = Vec::new();
116+
for login in &logins {
117+
let login_test =
118+
|owner: &Owner| owner.login().to_lowercase() == *login.to_lowercase();
119+
if owners.iter().any(login_test) {
120+
return Err(human(&format_args!("`{}` is already an owner", login)));
121+
}
122+
let msg = krate.owner_add(app, &conn, user, login)?;
123+
msgs.push(msg);
124+
}
125+
msgs.join(",")
126+
} else {
127+
for login in &logins {
128+
krate.owner_remove(app, &conn, user, login)?;
129+
}
130+
if User::owning(&krate, &conn)?.is_empty() {
131+
return Err(human(
132+
"cannot remove all individual owners of a crate. \
133+
Team member don't have permission to modify owners, so \
134+
at least one individual owner is required.",
135+
));
136+
}
137+
"owners successfully removed".to_owned()
138+
};
127139

128-
#[derive(Serialize)]
129-
struct R {
130-
ok: bool,
131-
msg: String,
132-
}
133-
Ok(req.json(&R {
134-
ok: true,
135-
msg: comma_sep_msg,
136-
}))
140+
#[derive(Serialize)]
141+
struct R {
142+
ok: bool,
143+
msg: String,
144+
}
145+
Ok(req.json(&R {
146+
ok: true,
147+
msg: comma_sep_msg,
148+
}))
149+
})
137150
}

src/tests/owners.rs

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::{
22
add_team_to_crate, app,
33
builders::{CrateBuilder, PublishBuilder},
44
new_team, new_user, req, sign_in_as,
5-
util::RequestHelper,
5+
util::{MockCookieUser, MockTokenUser, RequestHelper},
66
TestApp,
77
};
88
use cargo_registry::{
@@ -30,7 +30,7 @@ struct InvitationListResponse {
3030
}
3131

3232
// Implementing locally for now, unless these are needed elsewhere
33-
impl crate::util::MockCookieUser {
33+
impl MockCookieUser {
3434
/// As the currently logged in user, accept an invitation to become an owner of the named
3535
/// crate.
3636
fn accept_ownership_invitation(&self, krate_name: &str, krate_id: i32) {
@@ -85,43 +85,98 @@ fn new_crate_owner() {
8585
.good();
8686
}
8787

88+
fn create_and_add_owner(
89+
app: &TestApp,
90+
token: &MockTokenUser,
91+
username: &str,
92+
krate: &Crate,
93+
) -> MockCookieUser {
94+
let user = app.db_new_user(username);
95+
token.add_user_owner(&krate.name, user.as_model());
96+
user.accept_ownership_invitation(&krate.name, krate.id);
97+
user
98+
}
99+
88100
// Ensures that so long as at least one owner remains associated with the crate,
89101
// a user can still remove their own login as an owner
90102
#[test]
91103
fn owners_can_remove_self() {
92104
let (app, _, user, token) = TestApp::init().with_token();
105+
let username = &user.as_model().gh_login;
93106

94107
let krate = app
95108
.db(|conn| CrateBuilder::new("owners_selfremove", user.as_model().id).expect_build(conn));
96109

97110
// Deleting yourself when you're the only owner isn't allowed.
98111
let json = token
99-
.remove_named_owner("owners_selfremove", &user.as_model().gh_login)
112+
.remove_named_owner("owners_selfremove", username)
100113
.bad_with_status(200);
101114
assert!(json.errors[0]
102115
.detail
103-
.contains("cannot remove the sole owner of a crate"));
116+
.contains("cannot remove all individual owners of a crate"));
104117

105-
let user2 = app.db_new_user("secondowner");
106-
token.add_user_owner("owners_selfremove", user2.as_model());
107-
user2.accept_ownership_invitation("owners_selfremove", krate.id);
118+
create_and_add_owner(&app, &token, "secondowner", &krate);
108119

109120
// Deleting yourself when there are other owners is allowed.
110121
let json = token
111-
.remove_named_owner("owners_selfremove", &user.as_model().gh_login)
122+
.remove_named_owner("owners_selfremove", username)
112123
.good();
113124
assert!(json.ok);
114125

115126
// After you delete yourself, you no longer have permisions to manage the crate.
116127
let json = token
117-
.remove_named_owner("owners_selfremove", &user2.as_model().gh_login)
128+
.remove_named_owner("owners_selfremove", username)
118129
.bad_with_status(200);
119-
120130
assert!(json.errors[0]
121131
.detail
122132
.contains("only owners have permission to modify owners",));
123133
}
124134

135+
// Verify consistency when adidng or removing multiple owners in a single request.
136+
#[test]
137+
fn modify_multiple_owners() {
138+
let (app, _, user, token) = TestApp::init().with_token();
139+
let username = &user.as_model().gh_login;
140+
141+
let krate =
142+
app.db(|conn| CrateBuilder::new("owners_multiple", user.as_model().id).expect_build(conn));
143+
144+
let user2 = create_and_add_owner(&app, &token, "user2", &krate);
145+
let user3 = create_and_add_owner(&app, &token, "user3", &krate);
146+
147+
// Deleting all owners is not allowed.
148+
let json = token
149+
.remove_named_owners("owners_multiple", &[username, "user2", "user3"])
150+
.bad_with_status(200);
151+
assert!(&json.errors[0]
152+
.detail
153+
.contains("cannot remove all individual owners of a crate"));
154+
assert_eq!(app.db(|conn| krate.owners(&conn).unwrap()).len(), 3);
155+
156+
// Deleting two owners at once is allowed.
157+
let json = token
158+
.remove_named_owners("owners_multiple", &["user2", "user3"])
159+
.good();
160+
assert!(json.ok);
161+
assert_eq!(app.db(|conn| krate.owners(&conn).unwrap()).len(), 1);
162+
163+
// Adding multiple users fails if one of them already is an owner.
164+
let json = token
165+
.add_named_owners("owners_multiple", &["user2", username])
166+
.bad_with_status(200);
167+
assert!(&json.errors[0].detail.contains("is already an owner"));
168+
assert_eq!(app.db(|conn| krate.owners(&conn).unwrap()).len(), 1);
169+
170+
// Adding multiple users at once succeeds.
171+
let json = token
172+
.add_named_owners("owners_multiple", &["user2", "user3"])
173+
.good();
174+
assert!(json.ok);
175+
user2.accept_ownership_invitation(&krate.name, krate.id);
176+
user3.accept_ownership_invitation(&krate.name, krate.id);
177+
assert_eq!(app.db(|conn| krate.owners(&conn).unwrap()).len(), 3);
178+
}
179+
125180
/* Testing the crate ownership between two crates and one team.
126181
Given two crates, one crate owned by both a team and a user,
127182
one only owned by a user, check that the CrateList returned

src/tests/team.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ fn add_team_as_non_member() {
184184
#[test]
185185
fn remove_team_as_named_owner() {
186186
let (app, _) = TestApp::with_proxy().empty();
187-
let user_on_both_teams = app.db_new_user(mock_user_on_both_teams().gh_login);
187+
let username = mock_user_on_both_teams().gh_login;
188+
let user_on_both_teams = app.db_new_user(username);
188189
let token_on_both_teams = user_on_both_teams.db_new_token("arbitrary token name");
189190

190191
app.db(|conn| {
@@ -195,6 +196,15 @@ fn remove_team_as_named_owner() {
195196
.add_named_owner("foo_remove_team", "github:crates-test-org:core")
196197
.good();
197198

199+
// Removing the individual owner is not allowed, since team members don't
200+
// have permission to manage ownership
201+
let json = token_on_both_teams
202+
.remove_named_owner("foo_remove_team", username)
203+
.bad_with_status(200);
204+
assert!(json.errors[0]
205+
.detail
206+
.contains("cannot remove all individual owners of a crate"));
207+
198208
token_on_both_teams
199209
.remove_named_owner("foo_remove_team", "github:crates-test-org:core")
200210
.good();

src/tests/util.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -472,18 +472,33 @@ impl MockTokenUser {
472472
&self.token
473473
}
474474

475-
/// Add to the specified crate the specified owner.
475+
/// Add to the specified crate the specified owners.
476+
pub fn add_named_owners(&self, krate_name: &str, owners: &[&str]) -> Response<OkBool> {
477+
self.modify_owners(krate_name, owners, Self::put)
478+
}
479+
480+
/// Add a single owner to the specified crate.
476481
pub fn add_named_owner(&self, krate_name: &str, owner: &str) -> Response<OkBool> {
477-
let url = format!("/api/v1/crates/{}/owners", krate_name);
478-
let body = format!("{{\"users\":[\"{}\"]}}", owner);
479-
self.put(&url, body.as_bytes())
482+
self.add_named_owners(krate_name, &[owner])
483+
}
484+
485+
/// Remove from the specified crate the specified owners.
486+
pub fn remove_named_owners(&self, krate_name: &str, owners: &[&str]) -> Response<OkBool> {
487+
self.modify_owners(krate_name, owners, Self::delete_with_body)
480488
}
481489

482-
/// Remove from the specified crate the specified owner.
490+
/// Remove a single owner to the specified crate.
483491
pub fn remove_named_owner(&self, krate_name: &str, owner: &str) -> Response<OkBool> {
492+
self.remove_named_owners(krate_name, &[owner])
493+
}
494+
495+
fn modify_owners<F>(&self, krate_name: &str, owners: &[&str], method: F) -> Response<OkBool>
496+
where
497+
F: Fn(&MockTokenUser, &str, &[u8]) -> Response<OkBool>,
498+
{
484499
let url = format!("/api/v1/crates/{}/owners", krate_name);
485-
let body = format!("{{\"users\":[\"{}\"]}}", owner);
486-
self.delete_with_body(&url, body.as_bytes())
500+
let body = format!("{{\"owners\":[\"{}\"]}}", owners.join("\", \""));
501+
method(&self, &url, body.as_bytes())
487502
}
488503

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

0 commit comments

Comments
 (0)