From 260f887d227ede018b3e493f4c8c8bbbff516e67 Mon Sep 17 00:00:00 2001 From: Rohan Sircar Date: Tue, 12 May 2020 01:03:11 +0530 Subject: [PATCH] Implemented basic auth logic + other stuff --- Cargo.lock | 45 +++++++++++++++++++++++++ Cargo.toml | 3 ++ rustfmt.toml | 1 + src/actions/users.rs | 41 +++++++++++++++++------ src/errors/domain_error.rs | 52 +++++++++-------------------- src/main.rs | 15 +++++---- src/models/errors.rs | 14 ++++---- src/models/users.rs | 5 ++- src/routes/auth.rs | 67 ++++++++++++++++++++++++++++++++------ src/routes/users.rs | 21 ++++++------ 10 files changed, 180 insertions(+), 84 deletions(-) create mode 100644 rustfmt.toml diff --git a/Cargo.lock b/Cargo.lock index 6370737..d5462b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,7 @@ dependencies = [ "actix-identity", "actix-rt", "actix-service", + "actix-threadpool", "actix-web", "actix-web-httpauth", "bcrypt", @@ -50,7 +51,9 @@ dependencies = [ "chrono", "comp", "custom_error", + "derive-new", "diesel", + "diesel_migrations", "dotenv", "env_logger 0.7.1", "futures", @@ -832,6 +835,17 @@ dependencies = [ "syn", ] +[[package]] +name = "derive-new" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71f31892cd5c62e414316f2963c5689242c43d8e7bbcaaeca97e5e28c95d91d9" +dependencies = [ + "proc-macro2 1.0.12", + "quote 1.0.4", + "syn", +] + [[package]] name = "derive_builder" version = "0.9.0" @@ -892,6 +906,16 @@ dependencies = [ "syn", ] +[[package]] +name = "diesel_migrations" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c" +dependencies = [ + "migrations_internals", + "migrations_macros", +] + [[package]] name = "directories" version = "2.0.2" @@ -1610,6 +1634,27 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +[[package]] +name = "migrations_internals" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860" +dependencies = [ + "diesel", +] + +[[package]] +name = "migrations_macros" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" +dependencies = [ + "migrations_internals", + "proc-macro2 1.0.12", + "quote 1.0.4", + "syn", +] + [[package]] name = "mime" version = "0.3.16" diff --git a/Cargo.toml b/Cargo.toml index 718c43b..d9c8a5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,9 @@ regex = '1.3.7' lazy_static = '1.4.0' lazy-regex = '0.1.2' custom_error = '1.7.1' +derive-new = '0.5.8' +diesel_migrations = '1.4.0' +actix-threadpool = '0.3.1' [dependencies.serde] version = '1.0.106' diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..df99c69 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 80 diff --git a/src/actions/users.rs b/src/actions/users.rs index f9b6cbf..91824e3 100644 --- a/src/actions/users.rs +++ b/src/actions/users.rs @@ -1,8 +1,9 @@ use diesel::prelude::*; +use diesel::sqlite::Sqlite; use crate::errors; use crate::models; -use bcrypt::{hash, DEFAULT_COST}; +use bcrypt::{hash, verify, DEFAULT_COST}; use std::rc::Rc; pub fn find_user_by_uid( @@ -24,11 +25,7 @@ pub fn _find_user_by_name( user_name: String, conn: &SqliteConnection, ) -> Result, errors::DomainError> { - use crate::schema::users::dsl::*; - - let maybe_user = users - .select((name, created_at)) - .filter(name.eq(user_name)) + let maybe_user = _get_user_by_name(user_name) .first::(conn) .optional(); @@ -75,9 +72,33 @@ pub fn insert_new_user( diesel::insert_into(users) .values(nu.as_ref()) .execute(conn)?; - let user = users + let user = + _get_user_by_name(nu.name.clone()).first::(conn)?; + Ok(user) +} + +pub fn verify_password( + user_name: String, + given_password: String, + conn: &SqliteConnection, +) -> Result { + use crate::schema::users::dsl::*; + let password_hash = users + .select(password) + .filter(name.eq(user_name)) + .first::(conn)?; + Ok(verify(given_password.as_str(), password_hash.as_str())?) +} +fn _get_user_by_name( + user_name: String, +) -> crate::schema::users::BoxedQuery< + 'static, + Sqlite, + (diesel::sql_types::Text, diesel::sql_types::Timestamp), +> { + use crate::schema::users::dsl::*; + users .select((name, created_at)) - .filter(name.eq(nu.name.clone())) - .first::(conn); - Ok(user?) + .filter(name.eq(user_name)) + .into_boxed() } diff --git a/src/errors/domain_error.rs b/src/errors/domain_error.rs index 118e3be..8df84f9 100644 --- a/src/errors/domain_error.rs +++ b/src/errors/domain_error.rs @@ -6,24 +6,6 @@ use custom_error::custom_error; use crate::models::errors::*; use r2d2; use std::convert::From; -// use std::error::Error; - -// pub enum DomainError { -// #[display(fmt = "PasswordHashError")] -// PwdHashError, -// #[display(fmt = "Bad Id")] -// IdError, -// #[display(fmt = "Generic Error")] -// GenericError, -// DuplicateValue, -// } - -// impl Error for DomainError { -// fn source(&self) -> Option<&(dyn error::Error + 'static)> { -// // Generic error, underlying cause isn't tracked. -// None -// } -// } // impl From for DomainError { // fn from(error: DBError) -> DomainError { @@ -41,47 +23,43 @@ use std::convert::From; // } // } -custom_error! { pub DomainError +custom_error! { #[derive(new)] pub DomainError PwdHashError {source: BcryptError} = "Failed to has password", DbError {source: diesel::result::Error} = "Database error", DbPoolError {source: r2d2::Error} = "Failed to get connection from pool", + PasswordError {cause: String} = "Failed to validate password - {cause}", GenericError {cause: String} = "Generic Error - Reason: {cause}" } impl ResponseError for DomainError { fn error_response(&self) -> HttpResponse { + let err = self; match self { DomainError::PwdHashError { source } => { HttpResponse::InternalServerError().json(ErrorModel { - status_code: 500, - reason: format!( - "{} {}", - "Unexpected Error - Failed to hash password", source - ), + error_code: 500, + reason: format!("{} {}", err.to_string(), source).as_str(), }) } DomainError::DbError { source } => { HttpResponse::InternalServerError().json(ErrorModel { - status_code: 500, - reason: format!("{} {}", "Unexpected Database Error", source), + error_code: 500, + reason: format!("{} {}", err.to_string(), source).as_str(), }) } DomainError::DbPoolError { source } => { HttpResponse::InternalServerError().json(ErrorModel { - status_code: 500, - reason: format!( - "{} {}", - "Unexpected Error - Failed to get connection from pool", source - ), + error_code: 500, + reason: format!("{} {}", err.to_string(), source).as_str(), }) } + DomainError::PasswordError { cause } => HttpResponse::BadRequest().json(ErrorModel { + error_code: 400, + reason: format!("{} {}, ", err.to_string(), cause.clone()).as_str(), + }), DomainError::GenericError { cause } => HttpResponse::BadRequest().json(ErrorModel { - status_code: 400, - reason: format!( - "{} {}, ", - "Unexpected Database Error - ".to_owned(), - cause.clone() - ), + error_code: 400, + reason: format!("{} {}, ", err.to_string(), cause.clone()).as_str(), }), } } diff --git a/src/main.rs b/src/main.rs index 3ac38c4..ab5c98e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,17 @@ #[macro_use] extern crate diesel; #[macro_use] -extern crate comp; -#[macro_use] -extern crate validator_derive; +extern crate derive_new; +// #[macro_use] +// extern crate comp; +// #[macro_use] +// extern crate validator_derive; extern crate bcrypt; extern crate custom_error; extern crate regex; extern crate validator; -use actix_web::{ - error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder, -}; +use actix_web::{error, get, middleware, post, web, App, Error, HttpResponse, HttpServer}; use yarte::Template; @@ -94,6 +94,9 @@ async fn main() -> std::io::Result<()> { .build(manager) .expect("Failed to create pool."); + diesel_migrations::run_pending_migrations(&pool.get().unwrap()) + .expect("Error running migrations"); + let addr = std::env::var("BIND_ADDRESS").expect("BIND ADDRESS NOT FOUND"); info!("Starting server {}", addr); let private_key = rand::thread_rng().gen::<[u8; 32]>(); diff --git a/src/models/errors.rs b/src/models/errors.rs index 1bf5f77..f423f39 100644 --- a/src/models/errors.rs +++ b/src/models/errors.rs @@ -1,13 +1,13 @@ use serde::Serialize; -#[derive(Debug, Clone, Serialize)] -pub struct JsonErrorModel { +#[derive(Debug, Clone, Serialize, new)] +pub struct JsonErrorModel<'a> { status_code: i16, pub line: String, - pub reason: String, + pub reason: &'a str, } -#[derive(Debug, Clone, Serialize)] -pub struct ErrorModel { - pub status_code: i16, - pub reason: String, +#[derive(Debug, Clone, Serialize, new)] +pub struct ErrorModel<'a> { + pub error_code: i16, + pub reason: &'a str, } diff --git a/src/models/users.rs b/src/models/users.rs index 9c22e32..10599f6 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -2,12 +2,11 @@ use serde::{Deserialize, Serialize}; use crate::schema::users; use crate::utils::regexs; -use chrono; use validator::Validate; use validator_derive::*; use yarte::Template; -#[derive(Debug, Clone, Serialize, Queryable, Identifiable, Deserialize, PartialEq)] +#[derive(Debug, Clone, Queryable, Identifiable, Deserialize)] pub struct User { pub id: i32, pub name: String, @@ -15,7 +14,7 @@ pub struct User { pub created_at: chrono::NaiveDateTime, } -#[derive(Debug, Clone, Serialize, Insertable, Deserialize, Validate)] +#[derive(Debug, Clone, Insertable, Deserialize, Validate)] #[table_name = "users"] pub struct NewUser { #[validate(regex = "regexs::USERNAME_REG", length(min = 4, max = 10))] diff --git a/src/routes/auth.rs b/src/routes/auth.rs index cc7dc98..2af76fe 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,27 +1,53 @@ +use crate::types::DbPool; +use actix_threadpool::BlockingError; +use actix_web::{web, ResponseError}; use actix_web_httpauth::extractors::basic::BasicAuth; +use crate::actions::users; +use crate::errors; use actix_identity::Identity; -use actix_web::{get, post, Error, HttpResponse}; +use actix_web::{get, Error, HttpResponse}; #[get("/login")] -pub async fn login(id: Identity, credentials: BasicAuth) -> Result { +pub async fn login( + id: Identity, + credentials: BasicAuth, + pool: web::Data, +) -> Result { let maybe_identity = id.identity(); let response = if let Some(identity) = maybe_identity { - HttpResponse::Found() + Ok(HttpResponse::Found() .header("location", "/") .content_type("text/plain") - .json(format!("Already logged in as {}", identity)) + .json(format!("Already logged in as {}", identity))) } else { - id.remember(credentials.user_id().to_string()); - HttpResponse::Found().header("location", "/").finish() + let credentials2 = credentials.clone(); + web::block(move || validate_basic_auth(credentials2, &pool)) + .await + .and_then(|valid| { + if valid { + id.remember(credentials.user_id().to_string()); + Ok(HttpResponse::Found().header("location", "/").finish()) + } else { + Err(BlockingError::Error( + errors::DomainError::new_password_error( + "Wrong password or account does not exist" + .to_string(), + ), + )) + } + }) }; - println!("{}", credentials.user_id()); - println!("{:?}", credentials.password()); - Ok(response) + // println!("{}", credentials.user_id()); + // println!("{:?}", credentials.password()); + response } #[get("/logout")] -pub async fn logout(id: Identity, _credentials: BasicAuth) -> Result { +pub async fn logout( + id: Identity, + _credentials: BasicAuth, +) -> Result { let maybe_identity = id.identity(); let response = if let Some(identity) = maybe_identity { info!("Logging out {user}", user = identity); @@ -43,3 +69,24 @@ pub async fn index(id: Identity) -> String { id.identity().unwrap_or_else(|| "Anonymous".to_owned()) ) } + +fn validate_basic_auth( + credentials: BasicAuth, + pool: &web::Data, +) -> Result { + let result = if let Some(password_ref) = credentials.password() { + let conn = pool.get()?; + let password = password_ref.clone().into_owned(); + let valid = users::verify_password( + credentials.user_id().clone().into_owned(), + password, + &conn, + )?; + Ok(valid) + } else { + Err(errors::DomainError::new_password_error( + "No password given".to_owned(), + )) + }; + result +} diff --git a/src/routes/users.rs b/src/routes/users.rs index 11c62ec..ce79894 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -23,8 +23,8 @@ pub async fn get_user( if let Some(user) = maybe_user { Ok(HttpResponse::Ok().json(user)) } else { - let res = - HttpResponse::NotFound().body(format!("No user found with uid: {}", user_uid)); + let res = HttpResponse::NotFound() + .body(format!("No user found with uid: {}", user_uid)); Ok(res) } }); @@ -32,7 +32,9 @@ pub async fn get_user( } #[get("/api/authzd/users/get")] -pub async fn get_all_users(pool: web::Data) -> Result { +pub async fn get_all_users( + pool: web::Data, +) -> Result { // use web::block to offload blocking Diesel code without blocking server thread let res = web::block(move || { let conn = pool.get()?; @@ -43,19 +45,16 @@ pub async fn get_all_users(pool: web::Data) -> Result