Skip to content

Commit d0b9bcc

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 8f4a4f5 commit d0b9bcc

18 files changed

+213
-166
lines changed

src/controllers.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ mod prelude {
1919
pub use crate::util::errors::{cargo_err, AppError, AppResult, ChainError}; // 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

+3-4
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))
@@ -41,7 +41,7 @@ pub fn handle_invite(req: &mut dyn Request) -> AppResult<Response> {
4141
serde_json::from_str(&body).map_err(|_| bad_request("invalid json request"))?;
4242

4343
let crate_invite = crate_invite.crate_owner_invite;
44-
let user_id = req.user()?.id;
44+
let user_id = req.authenticate(conn)?.user_id();
4545

4646
if crate_invite.accepted {
4747
accept_invite(req, conn, crate_invite, user_id)
@@ -116,8 +116,7 @@ fn decline_invite(
116116
) -> AppResult<Response> {
117117
use diesel::delete;
118118

119-
let user_id = req.user()?.id;
120-
119+
let user_id = req.authenticate(conn)?.user_id();
121120
delete(crate_owner_invitations::table.find((user_id, crate_invite.crate_id))).execute(conn)?;
122121

123122
#[derive(Serialize)]

src/controllers/krate/follow.rs

+2-5
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

+4-3
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

+8-8
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

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

42-
let (new_crate, user) = parse_new_headers(req)?;
42+
let new_crate = parse_new_headers(req)?;
4343

4444
let conn = app.diesel_database.get()?;
45+
let ids = req.authenticate(&conn)?;
46+
let user = ids.find_user(&conn)?;
4547

4648
let verified_email_address = user.verified_email(&conn)?;
4749
let verified_email_address = verified_email_address.ok_or_else(|| {
@@ -150,7 +152,7 @@ pub fn publish(req: &mut dyn Request) -> AppResult<Response> {
150152
&conn,
151153
version.id,
152154
user.id,
153-
req.authentication_source()?.api_token_id(),
155+
ids.api_token_id(),
154156
VersionAction::Publish,
155157
)?;
156158

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

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

src/controllers/krate/search.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,12 @@ pub fn search(req: &mut dyn Request) -> AppResult<Response> {
154154
),
155155
);
156156
} else if params.get("following").is_some() {
157+
let user_id = req.authenticate(&conn)?.user_id();
157158
query = query.filter(
158159
crates::id.eq_any(
159160
follows::table
160161
.select(follows::crate_id)
161-
.filter(follows::user_id.eq(req.user()?.id)),
162+
.filter(follows::user_id.eq(user_id)),
162163
),
163164
);
164165
}

src/controllers/token.rs

+18-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use super::frontend_prelude::*;
22

3-
use crate::middleware::current_user::AuthenticationSource;
43
use crate::models::ApiToken;
54
use crate::schema::api_tokens;
65
use crate::util::read_fill;
@@ -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

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

27-
let user_id = req.user()?.id;
2827
let conn = req.db_conn()?;
28+
let user_id = req.authenticate(&conn)?.user_id();
2929

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

67-
let user = req.user()?;
6867
let conn = req.db_conn()?;
68+
let user = req.authenticate(&conn)?.find_user(&conn)?;
6969

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

120120
req.body().read_to_string(&mut body)?;
121-
let user = req.user()?;
122-
let name = &req.params()["user_id"];
121+
let param_user_id = &req.params()["user_id"];
123122
let conn = req.db_conn()?;
123+
let user = req.authenticate(&conn)?.find_user(&conn)?;
124124

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

@@ -195,19 +195,19 @@ pub fn regenerate_token_and_send(req: &mut dyn Request) -> AppResult<Response> {
195195
use diesel::dsl::sql;
196196
use diesel::update;
197197

198-
let user = req.user()?;
199-
let name = &req.params()["user_id"]
198+
let param_user_id = req.params()["user_id"]
200199
.parse::<i32>()
201200
.chain_error(|| bad_request("invalid user_id"))?;
202201
let conn = req.db_conn()?;
202+
let user = req.authenticate(&conn)?.find_user(&conn)?;
203203

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

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

241-
let user = req.user()?;
242241
let conn = req.db_conn()?;
242+
let user_id = req.authenticate(&conn)?.user_id();
243243

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

src/controllers/util.rs

+44
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

+5-2
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

+1-2
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)