Skip to content

Extract auth module #5530

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -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<i32>,
}

impl AuthenticatedUser {
pub fn user_id(&self) -> i32 {
self.user.id
}

pub fn api_token_id(&self) -> Option<i32> {
self.token_id
}

pub fn user(self) -> User {
self.user
}

/// Disallows token authenticated users
pub fn forbid_api_token_auth(self) -> AppResult<Self> {
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<AuthenticatedUser> {
let conn = req.db_write()?;

let session = req.session();
let user_id_from_session = session.get("user_id").and_then(|s| s.parse::<i32>().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::<InsecurelyGeneratedTokenRevoked>() {
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<AuthenticatedUser>;
}

impl<'a> UserAuthenticationExt for dyn RequestExt + 'a {
/// Obtain `AuthenticatedUser` for the request or return an `Forbidden` error
fn authenticate(&mut self) -> AppResult<AuthenticatedUser> {
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)
}
}
7 changes: 2 additions & 5 deletions src/controllers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,10 +24,6 @@ mod prelude {
use indexmap::IndexMap;
use serde::Serialize;

pub trait UserAuthenticationExt {
fn authenticate(&mut self) -> AppResult<super::util::AuthenticatedUser>;
}

pub trait RequestUtils {
fn redirect(&self, url: String) -> AppResponse;

Expand Down Expand Up @@ -74,7 +71,7 @@ mod prelude {
}

pub mod helpers;
mod util;
pub mod util;

pub mod category;
pub mod crate_owner_invitation;
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/crate_owner_invitation.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
118 changes: 2 additions & 116 deletions src/controllers/util.rs
Original file line number Diff line number Diff line change
@@ -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<i32>,
}

impl AuthenticatedUser {
pub fn user_id(&self) -> i32 {
self.user.id
}

pub fn api_token_id(&self) -> Option<i32> {
self.token_id
}

pub fn user(self) -> User {
self.user
}

/// Disallows token authenticated users
pub fn forbid_api_token_auth(self) -> AppResult<Self> {
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;

Expand All @@ -63,77 +23,3 @@ fn verify_origin(req: &dyn RequestExt) -> AppResult<()> {
}
Ok(())
}

fn authenticate_user(req: &dyn RequestExt) -> AppResult<AuthenticatedUser> {
let conn = req.db_write()?;

let session = req.session();
let user_id_from_session = session.get("user_id").and_then(|s| s.parse::<i32>().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::<InsecurelyGeneratedTokenRevoked>() {
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<AuthenticatedUser> {
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)
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub mod uploaders;
pub mod util;
pub mod worker;

mod auth;
pub mod controllers;
pub mod models;
mod router;
Expand Down