Skip to content

Commit fba2ade

Browse files
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

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}}

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))

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.

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>,

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();

0 commit comments

Comments
 (0)