Skip to content

Commit 868792c

Browse files
committed
Defer user authentication until requested by endpoint
Many endpoints do not require user authentication, and by moving the logic out of the middleware layer these endpoints now avoid obtaining a database connection from the pool and 1 or 2 queries. Due to lifetime issues a small middlware component remains. Further details are described in the middleware's documentation.
1 parent a34f6c4 commit 868792c

File tree

18 files changed

+213
-166
lines changed

18 files changed

+213
-166
lines changed

src/controllers.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ mod prelude {
1919
pub use crate::util::{cargo_err, AppResult}; // TODO: Remove cargo_err from here
2020

2121
pub use crate::middleware::app::RequestApp;
22-
pub use crate::middleware::current_user::RequestUser;
2322

2423
use std::collections::HashMap;
2524
use std::io;
@@ -28,6 +27,10 @@ mod prelude {
2827
use serde::Serialize;
2928
use url;
3029

30+
pub trait UserAuthenticationExt {
31+
fn authenticate(&self, conn: &PgConnection) -> AppResult<super::util::AuthenticatedUser>;
32+
}
33+
3134
pub trait RequestUtils {
3235
fn redirect(&self, url: String) -> Response;
3336

@@ -77,6 +80,7 @@ mod prelude {
7780
}
7881

7982
pub mod helpers;
83+
mod util;
8084

8185
pub mod category;
8286
pub mod crate_owner_invitation;

src/controllers/crate_owner_invitation.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::views::{EncodableCrateOwnerInvitation, InvitationResponse};
77
/// Handles the `GET /me/crate_owner_invitations` route.
88
pub fn list(req: &mut dyn Request) -> AppResult<Response> {
99
let conn = &*req.db_conn()?;
10-
let user_id = req.user()?.id;
10+
let user_id = req.authenticate(conn)?.user_id();
1111

1212
let crate_owner_invitations = crate_owner_invitations::table
1313
.filter(crate_owner_invitations::invited_user_id.eq(user_id))
@@ -56,7 +56,7 @@ fn accept_invite(
5656
) -> AppResult<Response> {
5757
use diesel::{delete, insert_into};
5858

59-
let user_id = req.user()?.id;
59+
let user_id = req.authenticate(conn)?.user_id();
6060

6161
conn.transaction(|| {
6262
let pending_crate_owner = crate_owner_invitations::table
@@ -95,8 +95,7 @@ fn decline_invite(
9595
) -> AppResult<Response> {
9696
use diesel::delete;
9797

98-
let user_id = req.user()?.id;
99-
98+
let user_id = req.authenticate(conn)?.user_id();
10099
delete(crate_owner_invitations::table.find((user_id, crate_invite.crate_id))).execute(conn)?;
101100

102101
#[derive(Serialize)]

src/controllers/krate/follow.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,12 @@ use crate::models::{Crate, Follow};
88
use crate::schema::*;
99

1010
fn follow_target(req: &dyn Request, conn: &DieselPooledConn<'_>) -> AppResult<Follow> {
11-
let user = req.user()?;
11+
let user_id = req.authenticate(conn)?.user_id();
1212
let crate_name = &req.params()["crate_id"];
1313
let crate_id = Crate::by_name(crate_name)
1414
.select(crates::id)
1515
.first(&**conn)?;
16-
Ok(Follow {
17-
user_id: user.id,
18-
crate_id,
19-
})
16+
Ok(Follow { user_id, crate_id })
2017
}
2118

2219
/// Handles the `PUT /crates/:crate_id/follow` route.

src/controllers/krate/owners.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,10 @@ fn parse_owners_request(req: &mut dyn Request) -> AppResult<Vec<String>> {
9292
fn modify_owners(req: &mut dyn Request, add: bool) -> AppResult<Response> {
9393
let logins = parse_owners_request(req)?;
9494
let app = req.app();
95-
let user = req.user()?;
9695
let crate_name = &req.params()["crate_id"];
96+
9797
let conn = req.db_conn()?;
98+
let user = req.authenticate(&conn)?.find_user(&conn)?;
9899

99100
conn.transaction(|| {
100101
let krate = Crate::by_name(crate_name).first::<Crate>(&*conn)?;
@@ -121,13 +122,13 @@ fn modify_owners(req: &mut dyn Request, add: bool) -> AppResult<Response> {
121122
if owners.iter().any(login_test) {
122123
return Err(cargo_err(&format_args!("`{}` is already an owner", login)));
123124
}
124-
let msg = krate.owner_add(app, &conn, user, login)?;
125+
let msg = krate.owner_add(app, &conn, &user, login)?;
125126
msgs.push(msg);
126127
}
127128
msgs.join(",")
128129
} else {
129130
for login in &logins {
130-
krate.owner_remove(app, &conn, user, login)?;
131+
krate.owner_remove(app, &conn, &user, login)?;
131132
}
132133
if User::owning(&krate, &conn)?.is_empty() {
133134
return Err(cargo_err(

src/controllers/krate/publish.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::controllers::cargo_prelude::*;
88
use crate::git;
99
use crate::models::dependency;
1010
use crate::models::{
11-
insert_version_owner_action, Badge, Category, Keyword, NewCrate, NewVersion, Rights, User,
11+
insert_version_owner_action, Badge, Category, Keyword, NewCrate, NewVersion, Rights,
1212
VersionAction,
1313
};
1414

@@ -40,9 +40,11 @@ pub fn publish(req: &mut dyn Request) -> AppResult<Response> {
4040
// - Then the .crate tarball length is passed to the upload_crate function where the actual
4141
// file is read and uploaded.
4242

43-
let (new_crate, user) = parse_new_headers(req)?;
43+
let new_crate = parse_new_headers(req)?;
4444

4545
let conn = app.diesel_database.get()?;
46+
let ids = req.authenticate(&conn)?;
47+
let user = ids.find_user(&conn)?;
4648

4749
let verified_email_address = user.verified_email(&conn)?;
4850
let verified_email_address = verified_email_address.ok_or_else(|| {
@@ -151,7 +153,7 @@ pub fn publish(req: &mut dyn Request) -> AppResult<Response> {
151153
&conn,
152154
version.id,
153155
user.id,
154-
req.authentication_source()?.api_token_id(),
156+
ids.api_token_id(),
155157
VersionAction::Publish,
156158
)?;
157159

@@ -224,9 +226,8 @@ pub fn publish(req: &mut dyn Request) -> AppResult<Response> {
224226
/// Used by the `krate::new` function.
225227
///
226228
/// This function parses the JSON headers to interpret the data and validates
227-
/// the data during and after the parsing. Returns crate metadata and user
228-
/// information.
229-
fn parse_new_headers(req: &mut dyn Request) -> AppResult<(EncodableCrateUpload, User)> {
229+
/// the data during and after the parsing. Returns crate metadata.
230+
fn parse_new_headers(req: &mut dyn Request) -> AppResult<EncodableCrateUpload> {
230231
// Read the json upload request
231232
let metadata_length = u64::from(read_le_u32(req.body())?);
232233
req.mut_extensions().insert(metadata_length);
@@ -265,6 +266,5 @@ fn parse_new_headers(req: &mut dyn Request) -> AppResult<(EncodableCrateUpload,
265266
)));
266267
}
267268

268-
let user = req.user()?;
269-
Ok((new, user.clone()))
269+
Ok(new)
270270
}

src/controllers/krate/search.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,12 @@ pub fn search(req: &mut dyn Request) -> AppResult<Response> {
132132
),
133133
);
134134
} else if params.get("following").is_some() {
135+
let user_id = req.authenticate(&conn)?.user_id();
135136
query = query.filter(
136137
crates::id.eq_any(
137138
follows::table
138139
.select(follows::crate_id)
139-
.filter(follows::user_id.eq(req.user()?.id)),
140+
.filter(follows::user_id.eq(user_id)),
140141
),
141142
);
142143
}

src/controllers/token.rs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use super::prelude::*;
22

3-
use crate::middleware::current_user::AuthenticationSource;
43
use crate::models::ApiToken;
54
use crate::schema::api_tokens;
65
use crate::util::{bad_request, read_fill, ChainError};
@@ -10,10 +9,13 @@ use serde_json as json;
109

1110
/// Handles the `GET /me/tokens` route.
1211
pub fn list(req: &mut dyn Request) -> AppResult<Response> {
13-
let tokens = ApiToken::belonging_to(req.user()?)
12+
let conn = req.db_conn()?;
13+
let user = req.authenticate(&conn)?.find_user(&conn)?;
14+
15+
let tokens = ApiToken::belonging_to(&user)
1416
.filter(api_tokens::revoked.eq(false))
1517
.order(api_tokens::created_at.desc())
16-
.load(&*req.db_conn()?)?;
18+
.load(&*conn)?;
1719
#[derive(Serialize)]
1820
struct R {
1921
api_tokens: Vec<ApiToken>,
@@ -35,12 +37,6 @@ pub fn new(req: &mut dyn Request) -> AppResult<Response> {
3537
api_token: NewApiToken,
3638
}
3739

38-
if req.authentication_source()? != AuthenticationSource::SessionCookie {
39-
return Err(bad_request(
40-
"cannot use an API token to create a new API token",
41-
));
42-
}
43-
4440
let max_size = 2000;
4541
let length = req
4642
.content_length()
@@ -64,11 +60,18 @@ pub fn new(req: &mut dyn Request) -> AppResult<Response> {
6460
return Err(bad_request("name must have a value"));
6561
}
6662

67-
let user = req.user()?;
6863
let conn = req.db_conn()?;
64+
let ids = req.authenticate(&conn)?;
65+
let user = ids.find_user(&conn)?;
66+
67+
if ids.api_token_id().is_some() {
68+
return Err(bad_request(
69+
"cannot use an API token to create a new API token",
70+
));
71+
}
6972

7073
let max_token_per_user = 500;
71-
let count = ApiToken::belonging_to(user)
74+
let count = ApiToken::belonging_to(&user)
7275
.count()
7376
.get_result::<i64>(&*conn)?;
7477
if count >= max_token_per_user {
@@ -95,9 +98,11 @@ pub fn revoke(req: &mut dyn Request) -> AppResult<Response> {
9598
.parse::<i32>()
9699
.map_err(|e| bad_request(&format!("invalid token id: {:?}", e)))?;
97100

98-
diesel::update(ApiToken::belonging_to(req.user()?).find(id))
101+
let conn = req.db_conn()?;
102+
let user = req.authenticate(&conn)?.find_user(&conn)?;
103+
diesel::update(ApiToken::belonging_to(&user).find(id))
99104
.set(api_tokens::revoked.eq(true))
100-
.execute(&*req.db_conn()?)?;
105+
.execute(&*conn)?;
101106

102107
#[derive(Serialize)]
103108
struct R {}

src/controllers/user/me.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ pub fn me(req: &mut dyn Request) -> AppResult<Response> {
2525
// perhaps adding `req.mut_extensions().insert(user)` to the
2626
// update_user route, however this somehow does not seem to work
2727

28-
let user_id = req.user()?.id;
2928
let conn = req.db_conn()?;
29+
let user_id = req.authenticate(&conn)?.user_id();
3030

3131
let (user, verified, email, verification_sent) = users::table
3232
.find(user_id)
@@ -65,10 +65,10 @@ pub fn me(req: &mut dyn Request) -> AppResult<Response> {
6565
pub fn updates(req: &mut dyn Request) -> AppResult<Response> {
6666
use diesel::dsl::any;
6767

68-
let user = req.user()?;
6968
let conn = req.db_conn()?;
69+
let user = req.authenticate(&conn)?.find_user(&conn)?;
7070

71-
let followed_crates = Follow::belonging_to(user).select(follows::crate_id);
71+
let followed_crates = Follow::belonging_to(&user).select(follows::crate_id);
7272
let data = versions::table
7373
.inner_join(crates::table)
7474
.left_outer_join(users::table)
@@ -119,12 +119,12 @@ pub fn update_user(req: &mut dyn Request) -> AppResult<Response> {
119119
let mut body = String::new();
120120

121121
req.body().read_to_string(&mut body)?;
122-
let user = req.user()?;
123-
let name = &req.params()["user_id"];
122+
let param_user_id = &req.params()["user_id"];
124123
let conn = req.db_conn()?;
124+
let user = req.authenticate(&conn)?.find_user(&conn)?;
125125

126126
// need to check if current user matches user to be updated
127-
if &user.id.to_string() != name {
127+
if &user.id.to_string() != param_user_id {
128128
return Err(bad_request("current user does not match requested user"));
129129
}
130130

@@ -198,17 +198,17 @@ pub fn regenerate_token_and_send(req: &mut dyn Request) -> AppResult<Response> {
198198
use diesel::dsl::sql;
199199
use diesel::update;
200200

201-
let user = req.user()?;
202-
let name = &req.params()["user_id"].parse::<i32>().ok().unwrap();
201+
let param_user_id = req.params()["user_id"].parse::<i32>().ok().unwrap();
203202
let conn = req.db_conn()?;
203+
let user = req.authenticate(&conn)?.find_user(&conn)?;
204204

205205
// need to check if current user matches user to be updated
206-
if &user.id != name {
206+
if user.id != param_user_id {
207207
return Err(bad_request("current user does not match requested user"));
208208
}
209209

210210
conn.transaction(|| {
211-
let email = update(Email::belonging_to(user))
211+
let email = update(Email::belonging_to(&user))
212212
.set(emails::token.eq(sql("DEFAULT")))
213213
.get_result::<Email>(&*conn)
214214
.map_err(|_| bad_request("Email could not be found"))?;
@@ -239,12 +239,12 @@ pub fn update_email_notifications(req: &mut dyn Request) -> AppResult<Response>
239239
.map(|c| (c.id, c.email_notifications))
240240
.collect();
241241

242-
let user = req.user()?;
243242
let conn = req.db_conn()?;
243+
let user_id = req.authenticate(&conn)?.user_id();
244244

245245
// Build inserts from existing crates belonging to the current user
246246
let to_insert = CrateOwner::by_owner_kind(OwnerKind::User)
247-
.filter(owner_id.eq(user.id))
247+
.filter(owner_id.eq(user_id))
248248
.select((crate_id, owner_id, owner_kind, email_notifications))
249249
.load(&*conn)?
250250
.into_iter()

src/controllers/util.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use super::prelude::*;
2+
3+
use crate::middleware::current_user::TrustedUserId;
4+
use crate::models::{ApiToken, User};
5+
use crate::util::errors::{internal, AppError, AppResult, ChainError, Unauthorized};
6+
7+
#[derive(Debug)]
8+
pub struct AuthenticatedUser(i32, Option<i32>);
9+
10+
impl AuthenticatedUser {
11+
pub fn user_id(&self) -> i32 {
12+
self.0
13+
}
14+
15+
pub fn api_token_id(&self) -> Option<i32> {
16+
self.1
17+
}
18+
19+
pub fn find_user(&self, conn: &PgConnection) -> AppResult<User> {
20+
User::find(conn, self.user_id())
21+
.chain_error(|| internal("user_id from cookie or token not found in database"))
22+
}
23+
}
24+
25+
impl<'a> UserAuthenticationExt for dyn Request + 'a {
26+
/// Obtain `CurrentUserIds` for the request or return an `Unauthorized` error
27+
fn authenticate(&self, conn: &PgConnection) -> AppResult<AuthenticatedUser> {
28+
if let Some(id) = self.extensions().find::<TrustedUserId>() {
29+
// A trusted user_id was provided by a signed cookie (or a test `MockCookieUser`)
30+
Ok(AuthenticatedUser(id.0, None))
31+
} else {
32+
// Otherwise, look for an `Authorization` header on the request
33+
if let Some(headers) = self.headers().find("Authorization") {
34+
ApiToken::find_by_api_token(conn, headers[0])
35+
.map(|token| AuthenticatedUser(token.user_id, Some(token.id)))
36+
// Convert a NotFound (or other database error) into Unauthorized
37+
.map_err(|_| Box::new(Unauthorized) as Box<dyn AppError>)
38+
} else {
39+
// Unable to authenticate the user
40+
Err(Box::new(Unauthorized))
41+
}
42+
}
43+
}
44+
}

src/controllers/version.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ pub mod yank;
55

66
use super::prelude::*;
77

8+
use crate::db::DieselPooledConn;
89
use crate::models::{Crate, CrateVersions, Version};
910
use crate::schema::versions;
1011

11-
fn version_and_crate(req: &mut dyn Request) -> AppResult<(Version, Crate)> {
12+
fn version_and_crate(req: &dyn Request) -> AppResult<(DieselPooledConn<'_>, Version, Crate)> {
1213
let crate_name = &req.params()["crate_id"];
1314
let semver = &req.params()["version"];
1415
if semver::Version::parse(semver).is_err() {
1516
return Err(cargo_err(&format_args!("invalid semver: {}", semver)));
1617
};
18+
1719
let conn = req.db_conn()?;
1820
let krate = Crate::by_name(crate_name).first::<Crate>(&*conn)?;
1921
let version = krate
@@ -26,5 +28,6 @@ fn version_and_crate(req: &mut dyn Request) -> AppResult<(Version, Crate)> {
2628
crate_name, semver
2729
))
2830
})?;
29-
Ok((version, krate))
31+
32+
Ok((conn, version, krate))
3033
}

src/controllers/version/downloads.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ fn increment_download_counts(
6969

7070
/// Handles the `GET /crates/:crate_id/:version/downloads` route.
7171
pub fn downloads(req: &mut dyn Request) -> AppResult<Response> {
72-
let (version, _) = version_and_crate(req)?;
73-
let conn = req.db_conn()?;
72+
let (conn, version, _) = version_and_crate(req)?;
7473
let cutoff_end_date = req
7574
.query()
7675
.get("before_date")

0 commit comments

Comments
 (0)