Implemented basic auth logic + other stuff

This commit is contained in:
Rohan Sircar 2020-05-12 01:03:11 +05:30
parent cb26393fcd
commit 260f887d22
10 changed files with 181 additions and 85 deletions

45
Cargo.lock generated
View File

@ -43,6 +43,7 @@ dependencies = [
"actix-identity", "actix-identity",
"actix-rt", "actix-rt",
"actix-service", "actix-service",
"actix-threadpool",
"actix-web", "actix-web",
"actix-web-httpauth", "actix-web-httpauth",
"bcrypt", "bcrypt",
@ -50,7 +51,9 @@ dependencies = [
"chrono", "chrono",
"comp", "comp",
"custom_error", "custom_error",
"derive-new",
"diesel", "diesel",
"diesel_migrations",
"dotenv", "dotenv",
"env_logger 0.7.1", "env_logger 0.7.1",
"futures", "futures",
@ -832,6 +835,17 @@ dependencies = [
"syn", "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]] [[package]]
name = "derive_builder" name = "derive_builder"
version = "0.9.0" version = "0.9.0"
@ -892,6 +906,16 @@ dependencies = [
"syn", "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]] [[package]]
name = "directories" name = "directories"
version = "2.0.2" version = "2.0.2"
@ -1610,6 +1634,27 @@ version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 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]] [[package]]
name = "mime" name = "mime"
version = "0.3.16" version = "0.3.16"

View File

@ -33,6 +33,9 @@ regex = '1.3.7'
lazy_static = '1.4.0' lazy_static = '1.4.0'
lazy-regex = '0.1.2' lazy-regex = '0.1.2'
custom_error = '1.7.1' custom_error = '1.7.1'
derive-new = '0.5.8'
diesel_migrations = '1.4.0'
actix-threadpool = '0.3.1'
[dependencies.serde] [dependencies.serde]
version = '1.0.106' version = '1.0.106'

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
max_width = 80

View File

@ -1,8 +1,9 @@
use diesel::prelude::*; use diesel::prelude::*;
use diesel::sqlite::Sqlite;
use crate::errors; use crate::errors;
use crate::models; use crate::models;
use bcrypt::{hash, DEFAULT_COST}; use bcrypt::{hash, verify, DEFAULT_COST};
use std::rc::Rc; use std::rc::Rc;
pub fn find_user_by_uid( pub fn find_user_by_uid(
@ -24,11 +25,7 @@ pub fn _find_user_by_name(
user_name: String, user_name: String,
conn: &SqliteConnection, conn: &SqliteConnection,
) -> Result<Option<models::UserDTO>, errors::DomainError> { ) -> Result<Option<models::UserDTO>, errors::DomainError> {
use crate::schema::users::dsl::*; let maybe_user = _get_user_by_name(user_name)
let maybe_user = users
.select((name, created_at))
.filter(name.eq(user_name))
.first::<models::UserDTO>(conn) .first::<models::UserDTO>(conn)
.optional(); .optional();
@ -75,9 +72,33 @@ pub fn insert_new_user(
diesel::insert_into(users) diesel::insert_into(users)
.values(nu.as_ref()) .values(nu.as_ref())
.execute(conn)?; .execute(conn)?;
let user = users let user =
.select((name, created_at)) _get_user_by_name(nu.name.clone()).first::<models::UserDTO>(conn)?;
.filter(name.eq(nu.name.clone())) Ok(user)
.first::<models::UserDTO>(conn); }
Ok(user?)
pub fn verify_password(
user_name: String,
given_password: String,
conn: &SqliteConnection,
) -> Result<bool, errors::DomainError> {
use crate::schema::users::dsl::*;
let password_hash = users
.select(password)
.filter(name.eq(user_name))
.first::<String>(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(user_name))
.into_boxed()
} }

View File

@ -6,24 +6,6 @@ use custom_error::custom_error;
use crate::models::errors::*; use crate::models::errors::*;
use r2d2; use r2d2;
use std::convert::From; 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<DBError> for DomainError { // impl From<DBError> for DomainError {
// fn from(error: DBError) -> 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", PwdHashError {source: BcryptError} = "Failed to has password",
DbError {source: diesel::result::Error} = "Database error", DbError {source: diesel::result::Error} = "Database error",
DbPoolError {source: r2d2::Error} = "Failed to get connection from pool", 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}" GenericError {cause: String} = "Generic Error - Reason: {cause}"
} }
impl ResponseError for DomainError { impl ResponseError for DomainError {
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
let err = self;
match self { match self {
DomainError::PwdHashError { source } => { DomainError::PwdHashError { source } => {
HttpResponse::InternalServerError().json(ErrorModel { HttpResponse::InternalServerError().json(ErrorModel {
status_code: 500, error_code: 500,
reason: format!( reason: format!("{} {}", err.to_string(), source).as_str(),
"{} {}",
"Unexpected Error - Failed to hash password", source
),
}) })
} }
DomainError::DbError { source } => { DomainError::DbError { source } => {
HttpResponse::InternalServerError().json(ErrorModel { HttpResponse::InternalServerError().json(ErrorModel {
status_code: 500, error_code: 500,
reason: format!("{} {}", "Unexpected Database Error", source), reason: format!("{} {}", err.to_string(), source).as_str(),
}) })
} }
DomainError::DbPoolError { source } => { DomainError::DbPoolError { source } => {
HttpResponse::InternalServerError().json(ErrorModel { HttpResponse::InternalServerError().json(ErrorModel {
status_code: 500, error_code: 500,
reason: format!( reason: format!("{} {}", err.to_string(), source).as_str(),
"{} {}",
"Unexpected Error - Failed to get connection from pool", source
),
}) })
} }
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 { DomainError::GenericError { cause } => HttpResponse::BadRequest().json(ErrorModel {
status_code: 400, error_code: 400,
reason: format!( reason: format!("{} {}, ", err.to_string(), cause.clone()).as_str(),
"{} {}, ",
"Unexpected Database Error - ".to_owned(),
cause.clone()
),
}), }),
} }
} }

View File

@ -1,17 +1,17 @@
#[macro_use] #[macro_use]
extern crate diesel; extern crate diesel;
#[macro_use] #[macro_use]
extern crate comp; extern crate derive_new;
#[macro_use] // #[macro_use]
extern crate validator_derive; // extern crate comp;
// #[macro_use]
// extern crate validator_derive;
extern crate bcrypt; extern crate bcrypt;
extern crate custom_error; extern crate custom_error;
extern crate regex; extern crate regex;
extern crate validator; extern crate validator;
use actix_web::{ use actix_web::{error, get, middleware, post, web, App, Error, HttpResponse, HttpServer};
error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
};
use yarte::Template; use yarte::Template;
@ -94,6 +94,9 @@ async fn main() -> std::io::Result<()> {
.build(manager) .build(manager)
.expect("Failed to create pool."); .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"); let addr = std::env::var("BIND_ADDRESS").expect("BIND ADDRESS NOT FOUND");
info!("Starting server {}", addr); info!("Starting server {}", addr);
let private_key = rand::thread_rng().gen::<[u8; 32]>(); let private_key = rand::thread_rng().gen::<[u8; 32]>();

View File

@ -1,13 +1,13 @@
use serde::Serialize; use serde::Serialize;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize, new)]
pub struct JsonErrorModel { pub struct JsonErrorModel<'a> {
status_code: i16, status_code: i16,
pub line: String, pub line: String,
pub reason: String, pub reason: &'a str,
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize, new)]
pub struct ErrorModel { pub struct ErrorModel<'a> {
pub status_code: i16, pub error_code: i16,
pub reason: String, pub reason: &'a str,
} }

View File

@ -2,12 +2,11 @@ use serde::{Deserialize, Serialize};
use crate::schema::users; use crate::schema::users;
use crate::utils::regexs; use crate::utils::regexs;
use chrono;
use validator::Validate; use validator::Validate;
use validator_derive::*; use validator_derive::*;
use yarte::Template; use yarte::Template;
#[derive(Debug, Clone, Serialize, Queryable, Identifiable, Deserialize, PartialEq)] #[derive(Debug, Clone, Queryable, Identifiable, Deserialize)]
pub struct User { pub struct User {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
@ -15,7 +14,7 @@ pub struct User {
pub created_at: chrono::NaiveDateTime, pub created_at: chrono::NaiveDateTime,
} }
#[derive(Debug, Clone, Serialize, Insertable, Deserialize, Validate)] #[derive(Debug, Clone, Insertable, Deserialize, Validate)]
#[table_name = "users"] #[table_name = "users"]
pub struct NewUser { pub struct NewUser {
#[validate(regex = "regexs::USERNAME_REG", length(min = 4, max = 10))] #[validate(regex = "regexs::USERNAME_REG", length(min = 4, max = 10))]

View File

@ -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 actix_web_httpauth::extractors::basic::BasicAuth;
use crate::actions::users;
use crate::errors;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{get, post, Error, HttpResponse}; use actix_web::{get, Error, HttpResponse};
#[get("/login")] #[get("/login")]
pub async fn login(id: Identity, credentials: BasicAuth) -> Result<HttpResponse, Error> { pub async fn login(
id: Identity,
credentials: BasicAuth,
pool: web::Data<DbPool>,
) -> Result<HttpResponse, impl ResponseError> {
let maybe_identity = id.identity(); let maybe_identity = id.identity();
let response = if let Some(identity) = maybe_identity { let response = if let Some(identity) = maybe_identity {
HttpResponse::Found() Ok(HttpResponse::Found()
.header("location", "/") .header("location", "/")
.content_type("text/plain") .content_type("text/plain")
.json(format!("Already logged in as {}", identity)) .json(format!("Already logged in as {}", identity)))
} else { } else {
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()); id.remember(credentials.user_id().to_string());
HttpResponse::Found().header("location", "/").finish() 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.user_id());
println!("{:?}", credentials.password()); // println!("{:?}", credentials.password());
Ok(response) response
} }
#[get("/logout")] #[get("/logout")]
pub async fn logout(id: Identity, _credentials: BasicAuth) -> Result<HttpResponse, Error> { pub async fn logout(
id: Identity,
_credentials: BasicAuth,
) -> Result<HttpResponse, Error> {
let maybe_identity = id.identity(); let maybe_identity = id.identity();
let response = if let Some(identity) = maybe_identity { let response = if let Some(identity) = maybe_identity {
info!("Logging out {user}", user = 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()) id.identity().unwrap_or_else(|| "Anonymous".to_owned())
) )
} }
fn validate_basic_auth(
credentials: BasicAuth,
pool: &web::Data<DbPool>,
) -> Result<bool, errors::DomainError> {
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
}

View File

@ -23,8 +23,8 @@ pub async fn get_user(
if let Some(user) = maybe_user { if let Some(user) = maybe_user {
Ok(HttpResponse::Ok().json(user)) Ok(HttpResponse::Ok().json(user))
} else { } else {
let res = let res = HttpResponse::NotFound()
HttpResponse::NotFound().body(format!("No user found with uid: {}", user_uid)); .body(format!("No user found with uid: {}", user_uid));
Ok(res) Ok(res)
} }
}); });
@ -32,7 +32,9 @@ pub async fn get_user(
} }
#[get("/api/authzd/users/get")] #[get("/api/authzd/users/get")]
pub async fn get_all_users(pool: web::Data<DbPool>) -> Result<HttpResponse, impl ResponseError> { pub async fn get_all_users(
pool: web::Data<DbPool>,
) -> Result<HttpResponse, impl ResponseError> {
// use web::block to offload blocking Diesel code without blocking server thread // use web::block to offload blocking Diesel code without blocking server thread
let res = web::block(move || { let res = web::block(move || {
let conn = pool.get()?; let conn = pool.get()?;
@ -43,19 +45,16 @@ pub async fn get_all_users(pool: web::Data<DbPool>) -> Result<HttpResponse, impl
debug!("{:?}", maybe_users); debug!("{:?}", maybe_users);
if let Some(users) = maybe_users { if let Some(users) = maybe_users {
if users.is_empty() { if users.is_empty() {
let res = HttpResponse::Ok().json(models::ErrorModel { let res = HttpResponse::NotFound()
status_code: 200, .json(models::ErrorModel::new(40, "No users available"));
reason: "No users available".to_string(), // let res = crate::errors::DomainError::new_generic_error("".to_owned());
});
Ok(res) Ok(res)
} else { } else {
Ok(HttpResponse::Ok().json(users)) Ok(HttpResponse::Ok().json(users))
} }
} else { } else {
let res = HttpResponse::Ok().json(models::ErrorModel { let res = HttpResponse::NotFound()
status_code: 200, .json(models::ErrorModel::new(40, "No users available"));
reason: "No users available".to_string(),
});
Ok(res) Ok(res)
} }
}); });