Skip to content

Commit 63d45b6

Browse files
committed
Auto merge of #2077 - jtgeibel:defer-user-auth, r=carols10cents
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. r? @carols10cents
2 parents 8f4a4f5 + c06e7c2 commit 63d45b6

18 files changed

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

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 {
9+
user_id: i32,
10+
token_id: Option<i32>,
11+
}
12+
13+
impl AuthenticatedUser {
14+
pub fn user_id(&self) -> i32 {
15+
self.user_id
16+
}
17+
18+
pub fn api_token_id(&self) -> Option<i32> {
19+
self.token_id
20+
}
21+
22+
pub fn find_user(&self, conn: &PgConnection) -> AppResult<User> {
23+
User::find(conn, self.user_id())
24+
.chain_error(|| internal("user_id from cookie or token not found in database"))
25+
}
26+
}
27+
28+
impl<'a> UserAuthenticationExt for dyn Request + 'a {
29+
/// Obtain `AuthenticatedUser` for the request or return an `Unauthorized` error
30+
fn authenticate(&self, conn: &PgConnection) -> AppResult<AuthenticatedUser> {
31+
if let Some(id) = self.extensions().find::<TrustedUserId>() {
32+
// A trusted user_id was provided by a signed cookie (or a test `MockCookieUser`)
33+
Ok(AuthenticatedUser {
34+
user_id: id.0,
35+
token_id: None,
36+
})
37+
} else {
38+
// Otherwise, look for an `Authorization` header on the request
39+
if let Some(headers) = self.headers().find("Authorization") {
40+
ApiToken::find_by_api_token(conn, headers[0])
41+
.map(|token| AuthenticatedUser {
42+
user_id: token.user_id,
43+
token_id: Some(token.id),
44+
})
45+
// Convert a NotFound (or other database error) into Unauthorized
46+
.map_err(|_| Box::new(Unauthorized) as Box<dyn AppError>)
47+
} else {
48+
// Unable to authenticate the user
49+
Err(Box::new(Unauthorized))
50+
}
51+
}
52+
}
53+
}

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
}

0 commit comments

Comments
 (0)