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-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"

View File

@ -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'

1
rustfmt.toml Normal file
View File

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

View File

@ -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<Option<models::UserDTO>, 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::<models::UserDTO>(conn)
.optional();
@ -75,9 +72,33 @@ pub fn insert_new_user(
diesel::insert_into(users)
.values(nu.as_ref())
.execute(conn)?;
let user = users
.select((name, created_at))
.filter(name.eq(nu.name.clone()))
.first::<models::UserDTO>(conn);
Ok(user?)
let user =
_get_user_by_name(nu.name.clone()).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 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<DBError> 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(),
}),
}
}

View File

@ -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]>();

View File

@ -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,
}

View File

@ -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))]

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 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<HttpResponse, Error> {
pub async fn login(
id: Identity,
credentials: BasicAuth,
pool: web::Data<DbPool>,
) -> Result<HttpResponse, impl ResponseError> {
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<HttpResponse, Error> {
pub async fn logout(
id: Identity,
_credentials: BasicAuth,
) -> Result<HttpResponse, Error> {
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<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 {
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<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
let res = web::block(move || {
let conn = pool.get()?;
@ -43,19 +45,16 @@ pub async fn get_all_users(pool: web::Data<DbPool>) -> Result<HttpResponse, impl
debug!("{:?}", maybe_users);
if let Some(users) = maybe_users {
if users.is_empty() {
let res = HttpResponse::Ok().json(models::ErrorModel {
status_code: 200,
reason: "No users available".to_string(),
});
let res = HttpResponse::NotFound()
.json(models::ErrorModel::new(40, "No users available"));
// let res = crate::errors::DomainError::new_generic_error("".to_owned());
Ok(res)
} else {
Ok(HttpResponse::Ok().json(users))
}
} else {
let res = HttpResponse::Ok().json(models::ErrorModel {
status_code: 200,
reason: "No users available".to_string(),
});
let res = HttpResponse::NotFound()
.json(models::ErrorModel::new(40, "No users available"));
Ok(res)
}
});