diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 00000000000..cbe9238d2a7 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,121 @@ +use crate::controllers; +use crate::db::RequestTransaction; +use crate::middleware::log_request; +use crate::models::{ApiToken, User}; +use crate::util::errors::{ + account_locked, forbidden, internal, AppError, AppResult, InsecurelyGeneratedTokenRevoked, +}; +use chrono::Utc; +use conduit::RequestExt; +use conduit_cookie::RequestSession; +use http::header; + +#[derive(Debug)] +pub struct AuthenticatedUser { + user: User, + token_id: Option, +} + +impl AuthenticatedUser { + pub fn user_id(&self) -> i32 { + self.user.id + } + + pub fn api_token_id(&self) -> Option { + self.token_id + } + + pub fn user(self) -> User { + self.user + } + + /// Disallows token authenticated users + pub fn forbid_api_token_auth(self) -> AppResult { + if self.token_id.is_none() { + Ok(self) + } else { + Err( + internal("API Token authentication was explicitly disallowed for this API") + .chain(forbidden()), + ) + } + } +} + +fn authenticate_user(req: &dyn RequestExt) -> AppResult { + let conn = req.db_write()?; + + let session = req.session(); + let user_id_from_session = session.get("user_id").and_then(|s| s.parse::().ok()); + + if let Some(id) = user_id_from_session { + let user = User::find(&conn, id) + .map_err(|err| err.chain(internal("user_id from cookie not found in database")))?; + + return Ok(AuthenticatedUser { + user, + token_id: None, + }); + } + + // Otherwise, look for an `Authorization` header on the request + let maybe_authorization = req + .headers() + .get(header::AUTHORIZATION) + .and_then(|h| h.to_str().ok()); + + if let Some(header_value) = maybe_authorization { + let token = ApiToken::find_by_api_token(&conn, header_value).map_err(|e| { + if e.is::() { + e + } else { + e.chain(internal("invalid token")).chain(forbidden()) + } + })?; + + let user = User::find(&conn, token.user_id) + .map_err(|err| err.chain(internal("user_id from token not found in database")))?; + + return Ok(AuthenticatedUser { + user, + token_id: Some(token.id), + }); + } + + // Unable to authenticate the user + return Err(internal("no cookie session or auth header found").chain(forbidden())); +} + +pub trait UserAuthenticationExt { + fn authenticate(&mut self) -> AppResult; +} + +impl<'a> UserAuthenticationExt for dyn RequestExt + 'a { + /// Obtain `AuthenticatedUser` for the request or return an `Forbidden` error + fn authenticate(&mut self) -> AppResult { + controllers::util::verify_origin(self)?; + + let authenticated_user = authenticate_user(self)?; + + if let Some(reason) = &authenticated_user.user.account_lock_reason { + let still_locked = if let Some(until) = authenticated_user.user.account_lock_until { + until > Utc::now().naive_utc() + } else { + true + }; + if still_locked { + return Err(account_locked( + reason, + authenticated_user.user.account_lock_until, + )); + } + } + + log_request::add_custom_metadata("uid", authenticated_user.user_id()); + if let Some(id) = authenticated_user.api_token_id() { + log_request::add_custom_metadata("tokenid", id); + } + + Ok(authenticated_user) + } +} diff --git a/src/controllers.rs b/src/controllers.rs index 316f4ba19cc..612a4531026 100644 --- a/src/controllers.rs +++ b/src/controllers.rs @@ -15,6 +15,7 @@ mod prelude { pub use conduit::{header, RequestExt, StatusCode}; pub use conduit_router::RequestParams; + pub use crate::auth::UserAuthenticationExt; pub use crate::db::RequestTransaction; pub use crate::middleware::app::RequestApp; pub use crate::util::errors::{cargo_err, AppError, AppResult}; // TODO: Remove cargo_err from here @@ -23,10 +24,6 @@ mod prelude { use indexmap::IndexMap; use serde::Serialize; - pub trait UserAuthenticationExt { - fn authenticate(&mut self) -> AppResult; - } - pub trait RequestUtils { fn redirect(&self, url: String) -> AppResponse; @@ -74,7 +71,7 @@ mod prelude { } pub mod helpers; -mod util; +pub mod util; pub mod category; pub mod crate_owner_invitation; diff --git a/src/controllers/crate_owner_invitation.rs b/src/controllers/crate_owner_invitation.rs index 35ec3642fc0..6173c9d11f8 100644 --- a/src/controllers/crate_owner_invitation.rs +++ b/src/controllers/crate_owner_invitation.rs @@ -1,7 +1,7 @@ use super::frontend_prelude::*; +use crate::auth::AuthenticatedUser; use crate::controllers::helpers::pagination::{Page, PaginationOptions}; -use crate::controllers::util::AuthenticatedUser; use crate::models::{Crate, CrateOwnerInvitation, Rights, User}; use crate::schema::{crate_owner_invitations, crates, users}; use crate::util::errors::{forbidden, internal}; diff --git a/src/controllers/util.rs b/src/controllers/util.rs index ca0074c0bb1..3cf4d42847a 100644 --- a/src/controllers/util.rs +++ b/src/controllers/util.rs @@ -1,52 +1,12 @@ -use chrono::Utc; -use conduit_cookie::RequestSession; - use super::prelude::*; - -use crate::middleware::log_request; -use crate::models::{ApiToken, User}; -use crate::util::errors::{ - account_locked, forbidden, internal, AppError, AppResult, InsecurelyGeneratedTokenRevoked, -}; - -#[derive(Debug)] -pub struct AuthenticatedUser { - user: User, - token_id: Option, -} - -impl AuthenticatedUser { - pub fn user_id(&self) -> i32 { - self.user.id - } - - pub fn api_token_id(&self) -> Option { - self.token_id - } - - pub fn user(self) -> User { - self.user - } - - /// Disallows token authenticated users - pub fn forbid_api_token_auth(self) -> AppResult { - if self.token_id.is_none() { - Ok(self) - } else { - Err( - internal("API Token authentication was explicitly disallowed for this API") - .chain(forbidden()), - ) - } - } -} +use crate::util::errors::{forbidden, internal, AppError, AppResult}; /// The Origin header (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin) /// is sent with CORS requests and POST requests, and indicates where the request comes from. /// We don't want to accept authenticated requests that originated from other sites, so this /// function returns an error if the Origin header doesn't match what we expect "this site" to /// be: https://crates.io in production, or http://localhost:port/ in development. -fn verify_origin(req: &dyn RequestExt) -> AppResult<()> { +pub fn verify_origin(req: &dyn RequestExt) -> AppResult<()> { let headers = req.headers(); let allowed_origins = &req.app().config.allowed_origins; @@ -63,77 +23,3 @@ fn verify_origin(req: &dyn RequestExt) -> AppResult<()> { } Ok(()) } - -fn authenticate_user(req: &dyn RequestExt) -> AppResult { - let conn = req.db_write()?; - - let session = req.session(); - let user_id_from_session = session.get("user_id").and_then(|s| s.parse::().ok()); - - if let Some(id) = user_id_from_session { - let user = User::find(&conn, id) - .map_err(|err| err.chain(internal("user_id from cookie not found in database")))?; - - return Ok(AuthenticatedUser { - user, - token_id: None, - }); - } - - // Otherwise, look for an `Authorization` header on the request - let maybe_authorization = req - .headers() - .get(header::AUTHORIZATION) - .and_then(|h| h.to_str().ok()); - - if let Some(header_value) = maybe_authorization { - let token = ApiToken::find_by_api_token(&conn, header_value).map_err(|e| { - if e.is::() { - e - } else { - e.chain(internal("invalid token")).chain(forbidden()) - } - })?; - - let user = User::find(&conn, token.user_id) - .map_err(|err| err.chain(internal("user_id from token not found in database")))?; - - return Ok(AuthenticatedUser { - user, - token_id: Some(token.id), - }); - } - - // Unable to authenticate the user - return Err(internal("no cookie session or auth header found").chain(forbidden())); -} - -impl<'a> UserAuthenticationExt for dyn RequestExt + 'a { - /// Obtain `AuthenticatedUser` for the request or return an `Forbidden` error - fn authenticate(&mut self) -> AppResult { - verify_origin(self)?; - - let authenticated_user = authenticate_user(self)?; - - if let Some(reason) = &authenticated_user.user.account_lock_reason { - let still_locked = if let Some(until) = authenticated_user.user.account_lock_until { - until > Utc::now().naive_utc() - } else { - true - }; - if still_locked { - return Err(account_locked( - reason, - authenticated_user.user.account_lock_until, - )); - } - } - - log_request::add_custom_metadata("uid", authenticated_user.user_id()); - if let Some(id) = authenticated_user.api_token_id() { - log_request::add_custom_metadata("tokenid", id); - } - - Ok(authenticated_user) - } -} diff --git a/src/lib.rs b/src/lib.rs index 7912ed4b221..050c3944362 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,7 @@ pub mod uploaders; pub mod util; pub mod worker; +mod auth; pub mod controllers; pub mod models; mod router;