Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fba2ade

Browse files
committedOct 6, 2017
Merge #1108
1108: Require owner approval r=carols10cents This PR addresses issue #924, adding owners with their consent, deployable chunk 4. It implements the Crates functionality necessary for Cargo to invite a user to be an owner instead of automatically adding any user to be an owner of any crate. These changes correlate with the Cargo changes made in [this PR](rust-lang/cargo#4551). Function `owner_add` now creates an owner invitation instead of adding a user to be an owner, and returns a `boolean` okay value with a `String` message indicating that the user was invited to be an owner of the crate. This `boolean` okay value is not explicitly used for anything but is necessary to support older versions of Cargo, without which older versions will fail to decode only a `String` response.
2 parents 696e6bb + 0264e76 commit fba2ade

File tree

8 files changed

+237
-97
lines changed

8 files changed

+237
-97
lines changed
 

Diff for: ‎app/templates/application.hbs

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
{{#rl-dropdown tagName="ul" class="dropdown current-user-links" closeOnChildClick="a:link"}}
6666
<li>{{#link-to 'dashboard'}}Dashboard{{/link-to}}</li>
6767
<li>{{#link-to 'me'}}Account Settings{{/link-to}}</li>
68+
<li>{{#link-to 'me.pending-invites'}}Owner Invites{{/link-to}}</li>
6869
<li class='last'>{{#link-to 'logout'}}Sign Out{{/link-to}}</li>
6970
{{/rl-dropdown}}
7071
{{/rl-dropdown-container}}

Diff for: ‎src/crate_owner_invitation.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,10 @@ fn accept_invite(
121121
conn: &PgConnection,
122122
crate_invite: InvitationResponse,
123123
) -> CargoResult<Response> {
124-
let user_id = req.user()?.id;
125124
use diesel::{delete, insert};
125+
use diesel::pg::upsert::{do_update, OnConflictExtension};
126+
127+
let user_id = req.user()?.id;
126128
let pending_crate_owner = crate_owner_invitations::table
127129
.filter(crate_owner_invitations::crate_id.eq(crate_invite.crate_id))
128130
.filter(crate_owner_invitations::invited_user_id.eq(user_id))
@@ -136,7 +138,11 @@ fn accept_invite(
136138
};
137139

138140
conn.transaction(|| {
139-
insert(&owner).into(crate_owners::table).execute(conn)?;
141+
insert(&owner.on_conflict(
142+
crate_owners::table.primary_key(),
143+
do_update().set(crate_owners::deleted.eq(false)),
144+
)).into(crate_owners::table)
145+
.execute(conn)?;
140146
delete(
141147
crate_owner_invitations::table
142148
.filter(crate_owner_invitations::crate_id.eq(crate_invite.crate_id))

Diff for: ‎src/krate/mod.rs

+59-17
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use download::{EncodableVersionDownload, VersionDownload};
2727
use git;
2828
use keyword::{CrateKeyword, EncodableKeyword};
2929
use owner::{rights, CrateOwner, EncodableOwner, Owner, OwnerKind, Rights, Team};
30+
use crate_owner_invitation::NewCrateOwnerInvitation;
3031
use pagination::Paginate;
3132
use render;
3233
use schema::*;
@@ -460,22 +461,52 @@ impl Crate {
460461
conn: &PgConnection,
461462
req_user: &User,
462463
login: &str,
463-
) -> CargoResult<()> {
464+
) -> CargoResult<String> {
465+
use diesel::insert;
466+
464467
let owner = Owner::find_or_create_by_login(app, conn, req_user, login)?;
465468

466-
let crate_owner = CrateOwner {
467-
crate_id: self.id,
468-
owner_id: owner.id(),
469-
created_by: req_user.id,
470-
owner_kind: owner.kind() as i32,
471-
};
472-
diesel::insert(&crate_owner.on_conflict(
473-
crate_owners::table.primary_key(),
474-
do_update().set(crate_owners::deleted.eq(false)),
475-
)).into(crate_owners::table)
476-
.execute(conn)?;
469+
match owner {
470+
// Users are invited and must accept before being added
471+
owner @ Owner::User(_) => {
472+
let owner_invitation = NewCrateOwnerInvitation {
473+
invited_user_id: owner.id(),
474+
invited_by_user_id: req_user.id,
475+
crate_id: self.id,
476+
};
477477

478-
Ok(())
478+
diesel::insert(&owner_invitation.on_conflict_do_nothing())
479+
.into(crate_owner_invitations::table)
480+
.execute(conn)?;
481+
482+
Ok(format!(
483+
"user {} has been invited to be an owner of crate {}",
484+
owner.login(),
485+
self.name
486+
))
487+
}
488+
// Teams are added as owners immediately
489+
owner @ Owner::Team(_) => {
490+
let crate_owner = CrateOwner {
491+
crate_id: self.id,
492+
owner_id: owner.id(),
493+
created_by: req_user.id,
494+
owner_kind: OwnerKind::Team as i32,
495+
};
496+
497+
insert(&crate_owner.on_conflict(
498+
crate_owners::table.primary_key(),
499+
do_update().set(crate_owners::deleted.eq(false)),
500+
)).into(crate_owners::table)
501+
.execute(conn)?;
502+
503+
Ok(format!(
504+
"team {} has been added as an owner of crate {}",
505+
owner.login(),
506+
self.name
507+
))
508+
}
509+
}
479510
}
480511

481512
pub fn owner_remove(
@@ -924,8 +955,10 @@ pub fn new(req: &mut Request) -> CargoResult<Response> {
924955
let owners = krate.owners(&conn)?;
925956
if rights(req.app(), &owners, &user)? < Rights::Publish {
926957
return Err(human(
927-
"crate name has already been claimed by \
928-
another user",
958+
"this crate exists but you don't seem to be an owner. \
959+
If you believe this is a mistake, perhaps you need \
960+
to accept an invitation to be an owner before \
961+
publishing.",
929962
));
930963
}
931964

@@ -1373,12 +1406,15 @@ fn modify_owners(req: &mut Request, add: bool) -> CargoResult<Response> {
13731406
.or(request.users)
13741407
.ok_or_else(|| human("invalid json request"))?;
13751408

1409+
let mut msgs = Vec::new();
1410+
13761411
for login in &logins {
13771412
if add {
13781413
if owners.iter().any(|owner| owner.login() == *login) {
13791414
return Err(human(&format_args!("`{}` is already an owner", login)));
13801415
}
1381-
krate.owner_add(req.app(), &conn, user, login)?;
1416+
let msg = krate.owner_add(req.app(), &conn, user, login)?;
1417+
msgs.push(msg);
13821418
} else {
13831419
// Removing the team that gives you rights is prevented because
13841420
// team members only have Rights::Publish
@@ -1389,11 +1425,17 @@ fn modify_owners(req: &mut Request, add: bool) -> CargoResult<Response> {
13891425
}
13901426
}
13911427

1428+
let comma_sep_msg = msgs.join(",");
1429+
13921430
#[derive(Serialize)]
13931431
struct R {
13941432
ok: bool,
1433+
msg: String,
13951434
}
1396-
Ok(req.json(&R { ok: true }))
1435+
Ok(req.json(&R {
1436+
ok: true,
1437+
msg: comma_sep_msg,
1438+
}))
13971439
}
13981440

13991441
/// Handles the `GET /crates/:crate_id/reverse_dependencies` route.

Diff for: ‎src/tests/all.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ mod token;
100100
mod user;
101101
mod version;
102102

103-
#[derive(Deserialize)]
103+
#[derive(Deserialize, Debug)]
104104
struct GoodCrate {
105105
#[serde(rename = "crate")] krate: EncodableCrate,
106106
warnings: Warnings,
@@ -110,7 +110,7 @@ struct CrateList {
110110
crates: Vec<EncodableCrate>,
111111
meta: CrateMeta,
112112
}
113-
#[derive(Deserialize)]
113+
#[derive(Deserialize, Debug)]
114114
struct Warnings {
115115
invalid_categories: Vec<String>,
116116
invalid_badges: Vec<String>,

Diff for: ‎src/tests/krate.rs

+53-1
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,9 @@ fn new_krate_wrong_user() {
699699

700700
let json = bad_resp!(middle.call(&mut req));
701701
assert!(
702-
json.errors[0].detail.contains("another user"),
702+
json.errors[0]
703+
.detail
704+
.contains("this crate exists but you don't seem to be an owner.",),
703705
"{:?}",
704706
json.errors
705707
);
@@ -2003,6 +2005,56 @@ fn block_blacklisted_documentation_url() {
20032005
assert_eq!(json.krate.documentation, None);
20042006
}
20052007

2008+
// This is testing Cargo functionality! ! !
2009+
// specifically functions modify_owners and add_owners
2010+
// which call the `PUT /crates/:crate_id/owners` route
2011+
#[test]
2012+
fn test_cargo_invite_owners() {
2013+
let (_b, app, middle) = ::app();
2014+
let mut req = ::req(app.clone(), Method::Get, "/");
2015+
2016+
let new_user = {
2017+
let conn = app.diesel_database.get().unwrap();
2018+
let owner = ::new_user("avocado").create_or_update(&conn).unwrap();
2019+
::sign_in_as(&mut req, &owner);
2020+
::CrateBuilder::new("guacamole", owner.id).expect_build(&conn);
2021+
::new_user("cilantro").create_or_update(&conn).unwrap()
2022+
};
2023+
2024+
#[derive(Serialize)]
2025+
struct OwnerReq {
2026+
owners: Option<Vec<String>>,
2027+
}
2028+
#[derive(Deserialize, Debug)]
2029+
struct OwnerResp {
2030+
ok: bool,
2031+
msg: String,
2032+
}
2033+
2034+
let body = serde_json::to_string(&OwnerReq {
2035+
owners: Some(vec![new_user.gh_login]),
2036+
});
2037+
let mut response = ok_resp!(
2038+
middle.call(
2039+
req.with_path("/api/v1/crates/guacamole/owners")
2040+
.with_method(Method::Put)
2041+
.with_body(body.unwrap().as_bytes()),
2042+
)
2043+
);
2044+
2045+
let r = ::json::<OwnerResp>(&mut response);
2046+
// this ok:true field is what old versions of Cargo
2047+
// need - do not remove unless you're cool with
2048+
// dropping support for old versions
2049+
assert!(r.ok);
2050+
// msg field is what is sent and used in updated
2051+
// version of cargo
2052+
assert_eq!(
2053+
r.msg,
2054+
"user cilantro has been invited to be an owner of crate guacamole"
2055+
)
2056+
}
2057+
20062058
// #[test]
20072059
// fn new_crate_bad_tarball() {
20082060
// let (_b, app, middle) = ::app();

Diff for: ‎src/tests/owners.rs

+101-66
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use cargo_registry::user::EncodablePublicUser;
55
use cargo_registry::crate_owner_invitation::{EncodableCrateOwnerInvitation, InvitationResponse,
66
NewCrateOwnerInvitation};
77
use cargo_registry::schema::crate_owner_invitations;
8+
use cargo_registry::krate::Crate;
89

910
use conduit::{Handler, Method};
1011
use diesel;
@@ -49,14 +50,52 @@ fn new_crate_owner() {
4950
)
5051
);
5152
assert!(::json::<O>(&mut response).ok);
52-
bad_resp!(
53+
54+
let krate_id = {
55+
let conn = app.diesel_database.get().unwrap();
56+
Crate::by_name("foo_owner")
57+
.first::<Crate>(&*conn)
58+
.unwrap()
59+
.id
60+
};
61+
62+
let body = json!({
63+
"crate_owner_invite": {
64+
"invited_by_username": "foo",
65+
"crate_name": "foo_owner",
66+
"crate_id": krate_id,
67+
"created_at": "",
68+
"accepted": true
69+
}
70+
});
71+
72+
::logout(&mut req);
73+
::sign_in_as(&mut req, &u2);
74+
75+
// accept invitation for user to be added as owner
76+
let mut response = ok_resp!(
5377
middle.call(
54-
req.with_path("/api/v1/crates/foo_owner/owners")
78+
req.with_path(&format!("/api/v1/me/crate_owner_invitations/{}", krate_id))
5579
.with_method(Method::Put)
56-
.with_body(body.as_bytes()),
80+
.with_body(body.to_string().as_bytes()),
5781
)
5882
);
5983

84+
#[derive(Deserialize)]
85+
struct CrateOwnerInvitation {
86+
crate_owner_invitation: InvitationResponse,
87+
}
88+
89+
#[derive(Deserialize)]
90+
struct InvitationResponse {
91+
crate_id: i32,
92+
accepted: bool,
93+
}
94+
95+
let crate_owner_invite = ::json::<CrateOwnerInvitation>(&mut response);
96+
assert!(crate_owner_invite.crate_owner_invitation.accepted);
97+
assert_eq!(crate_owner_invite.crate_owner_invitation.crate_id, krate_id);
98+
6099
// Make sure this shows up as one of their crates.
61100
let query = format!("user_id={}", u2.id);
62101
let mut response = ok_resp!(
@@ -100,13 +139,14 @@ fn owners_can_remove_self() {
100139
Method::Get,
101140
"/api/v1/crates/owners_selfremove/owners",
102141
);
103-
{
142+
let (first_owner, second_owner) = {
104143
let conn = app.diesel_database.get().unwrap();
105-
::new_user("secondowner").create_or_update(&conn).unwrap();
106144
let user = ::new_user("firstowner").create_or_update(&conn).unwrap();
145+
let user_two = ::new_user("secondowner").create_or_update(&conn).unwrap();
107146
::sign_in_as(&mut req, &user);
108147
::CrateBuilder::new("owners_selfremove", user.id).expect_build(&conn);
109-
}
148+
(user, user_two)
149+
};
110150

111151
let mut response = ok_resp!(middle.call(&mut req));
112152
let r: R = ::json(&mut response);
@@ -128,80 +168,75 @@ fn owners_can_remove_self() {
128168
ok_resp!(middle.call(req.with_method(Method::Put,).with_body(body.as_bytes(),),));
129169
assert!(::json::<O>(&mut response).ok);
130170

131-
// Deleting yourself when there are other owners is allowed.
132-
let body = r#"{"users":["firstowner"]}"#;
133-
let mut response =
134-
ok_resp!(middle.call(req.with_method(Method::Delete,).with_body(body.as_bytes(),),));
135-
assert!(::json::<O>(&mut response).ok);
171+
// Need to accept owner invitation to add secondowner as owner
172+
let krate_id = {
173+
let conn = app.diesel_database.get().unwrap();
174+
Crate::by_name("owners_selfremove")
175+
.first::<Crate>(&*conn)
176+
.unwrap()
177+
.id
178+
};
136179

137-
// After you delete yourself, you no longer have permisions to manage the crate.
138-
let body = r#"{"users":["secondowner"]}"#;
139-
let mut response =
140-
ok_resp!(middle.call(req.with_method(Method::Delete,).with_body(body.as_bytes(),),));
141-
let json = ::json::<::Bad>(&mut response);
142-
assert!(
143-
json.errors[0]
144-
.detail
145-
.contains("only owners have permission to modify owners",)
180+
let body = json!({
181+
"crate_owner_invite": {
182+
"invited_by_username": "foo",
183+
"crate_name": "foo_owner",
184+
"crate_id": krate_id,
185+
"created_at": "",
186+
"accepted": true
187+
}
188+
});
189+
190+
::logout(&mut req);
191+
::sign_in_as(&mut req, &second_owner);
192+
193+
let mut response = ok_resp!(
194+
middle.call(
195+
req.with_path(&format!("/api/v1/me/crate_owner_invitations/{}", krate_id))
196+
.with_method(Method::Put)
197+
.with_body(body.to_string().as_bytes())
198+
)
146199
);
147-
}
148200

149-
#[test]
150-
fn owners() {
151201
#[derive(Deserialize)]
152-
struct R {
153-
users: Vec<EncodablePublicUser>,
154-
}
155-
#[derive(Deserialize)]
156-
struct O {
157-
ok: bool,
202+
struct CrateOwnerInvitation {
203+
crate_owner_invitation: InvitationResponse,
158204
}
159205

160-
let (_b, app, middle) = ::app();
161-
let mut req = ::req(app.clone(), Method::Get, "/api/v1/crates/foo_owners/owners");
162-
{
163-
let conn = app.diesel_database.get().unwrap();
164-
::new_user("foobar").create_or_update(&conn).unwrap();
165-
let user = ::new_user("foo").create_or_update(&conn).unwrap();
166-
::sign_in_as(&mut req, &user);
167-
::CrateBuilder::new("foo_owners", user.id).expect_build(&conn);
206+
#[derive(Deserialize)]
207+
struct InvitationResponse {
208+
crate_id: i32,
209+
accepted: bool,
168210
}
169211

170-
let mut response = ok_resp!(middle.call(&mut req));
171-
let r: R = ::json(&mut response);
172-
assert_eq!(r.users.len(), 1);
212+
let crate_owner_invite = ::json::<CrateOwnerInvitation>(&mut response);
213+
assert!(crate_owner_invite.crate_owner_invitation.accepted);
214+
assert_eq!(crate_owner_invite.crate_owner_invitation.crate_id, krate_id);
173215

174-
let mut response = ok_resp!(middle.call(req.with_method(Method::Get)));
175-
let r: R = ::json(&mut response);
176-
assert_eq!(r.users.len(), 1);
216+
::logout(&mut req);
217+
::sign_in_as(&mut req, &first_owner);
177218

178-
let body = r#"{"users":["foobar"]}"#;
179-
let mut response =
180-
ok_resp!(middle.call(req.with_method(Method::Put,).with_body(body.as_bytes(),),));
181-
assert!(::json::<O>(&mut response).ok);
182-
183-
let mut response = ok_resp!(middle.call(req.with_method(Method::Get)));
184-
let r: R = ::json(&mut response);
185-
assert_eq!(r.users.len(), 2);
186-
187-
let body = r#"{"users":["foobar"]}"#;
188-
let mut response =
189-
ok_resp!(middle.call(req.with_method(Method::Delete,).with_body(body.as_bytes(),),));
219+
// Deleting yourself when there are other owners is allowed.
220+
let body = r#"{"users":["firstowner"]}"#;
221+
let mut response = ok_resp!(
222+
middle.call(
223+
req.with_path("/api/v1/crates/owners_selfremove/owners")
224+
.with_method(Method::Delete)
225+
.with_body(body.as_bytes())
226+
)
227+
);
190228
assert!(::json::<O>(&mut response).ok);
191229

192-
let mut response = ok_resp!(middle.call(req.with_method(Method::Get)));
193-
let r: R = ::json(&mut response);
194-
assert_eq!(r.users.len(), 1);
195-
196-
let body = r#"{"users":["foo"]}"#;
230+
// After you delete yourself, you no longer have permisions to manage the crate.
231+
let body = r#"{"users":["secondowner"]}"#;
197232
let mut response =
198233
ok_resp!(middle.call(req.with_method(Method::Delete,).with_body(body.as_bytes(),),));
199-
::json::<::Bad>(&mut response);
200-
201-
let body = r#"{"users":["foobar"]}"#;
202-
let mut response =
203-
ok_resp!(middle.call(req.with_method(Method::Put,).with_body(body.as_bytes(),),));
204-
assert!(::json::<O>(&mut response).ok);
234+
let json = ::json::<::Bad>(&mut response);
235+
assert!(
236+
json.errors[0]
237+
.detail
238+
.contains("only owners have permission to modify owners",)
239+
);
205240
}
206241

207242
/* Testing the crate ownership between two crates and one team.

Diff for: ‎src/tests/team.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ fn remove_team_as_named_owner() {
219219
)
220220
);
221221
assert!(
222-
json.errors[0].detail.contains("another user"),
222+
json.errors[0]
223+
.detail
224+
.contains("this crate exists but you don't seem to be an owner.",),
223225
"{:?}",
224226
json.errors
225227
);
@@ -314,7 +316,9 @@ fn publish_not_owned() {
314316
)
315317
);
316318
assert!(
317-
json.errors[0].detail.contains("another user"),
319+
json.errors[0]
320+
.detail
321+
.contains("this crate exists but you don't seem to be an owner.",),
318322
"{:?}",
319323
json.errors
320324
);

Diff for: ‎src/tests/user.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -544,8 +544,8 @@ fn test_insert_into_email_table() {
544544
let conn = app.diesel_database.get().unwrap();
545545
let user = NewUser {
546546
gh_id: 1,
547-
email: Some("potato@example.com"),
548-
..::new_user("potato")
547+
email: Some("apple@example.com"),
548+
..::new_user("apple")
549549
};
550550

551551
let user = user.create_or_update(&conn).unwrap();
@@ -554,8 +554,8 @@ fn test_insert_into_email_table() {
554554

555555
let mut response = ok_resp!(middle.call(req.with_path("/api/v1/me").with_method(Method::Get),));
556556
let r = ::json::<R>(&mut response);
557-
assert_eq!(r.user.email.unwrap(), "potato@example.com");
558-
assert_eq!(r.user.login, "potato");
557+
assert_eq!(r.user.email.unwrap(), "apple@example.com");
558+
assert_eq!(r.user.login, "apple");
559559

560560
::logout(&mut req);
561561

@@ -565,7 +565,7 @@ fn test_insert_into_email_table() {
565565
let user = NewUser {
566566
gh_id: 1,
567567
email: Some("banana@example.com"),
568-
..::new_user("potato")
568+
..::new_user("apple")
569569
};
570570

571571
let user = user.create_or_update(&conn).unwrap();
@@ -574,8 +574,8 @@ fn test_insert_into_email_table() {
574574

575575
let mut response = ok_resp!(middle.call(req.with_path("/api/v1/me").with_method(Method::Get),));
576576
let r = ::json::<R>(&mut response);
577-
assert_eq!(r.user.email.unwrap(), "potato@example.com");
578-
assert_eq!(r.user.login, "potato");
577+
assert_eq!(r.user.email.unwrap(), "apple@example.com");
578+
assert_eq!(r.user.login, "apple");
579579
}
580580

581581
/* Given a new user, check that when an email is added,

0 commit comments

Comments
 (0)
Please sign in to comment.