use uuid::Uuid; use diesel::prelude::*; use diesel::result::Error as DieselError; use diesel_derive_enum::DbEnum; use rocket::request::{FromRequest, Request, Outcome}; use rocket::outcome::try_outcome; use serde::{Deserialize}; use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use argon2::password_hash::errors::Error as PasswordHashError; use rand::rngs::OsRng; use argon2::{Algorithm, Argon2, Version, Params}; use crate::schema::*; use crate::DbConn; use crate::models::errors::{UserError, ErrorResponse, make_500}; use crate::models::zone::Zone; use crate::models::session::Session; #[derive(Debug, DbEnum, Deserialize, Clone)] #[serde(rename_all = "lowercase")] pub enum Role { #[db_rename = "admin"] Admin, #[db_rename = "zoneadmin"] ZoneAdmin, } // TODO: Store Uuid instead of string?? #[derive(Debug, Queryable, Identifiable, Insertable)] #[table_name = "user"] pub struct User { pub id: String, } #[derive(Debug, Queryable, Identifiable, Insertable)] #[table_name = "localuser"] #[primary_key(user_id)] pub struct LocalUser { pub user_id: String, pub username: String, pub password: String, pub role: Role, } #[derive(Debug, Queryable, Identifiable, Insertable)] #[table_name = "user_zone"] #[primary_key(user_id, zone_id)] pub struct UserZone { pub user_id: String, pub zone_id: String, } #[derive(Debug, Deserialize)] pub struct CreateUserRequest { pub username: String, pub password: String, pub email: String, pub role: Option } #[derive(Debug)] pub struct UserInfo { pub id: String, pub role: Role, pub username: String, } impl UserInfo { pub fn is_admin(&self) -> bool { matches!(self.role, Role::Admin) } pub fn check_admin(&self) -> Result<(), UserError> { if self.is_admin() { Ok(()) } else { Err(UserError::PermissionDenied) } } pub fn get_zone(&self, conn: &diesel::SqliteConnection, zone_name: &str) -> Result { use crate::schema::user_zone::dsl::*; use crate::schema::zone::dsl::*; let (res_zone, _): (Zone, UserZone) = zone.inner_join(user_zone) .filter(name.eq(zone_name)) .filter(user_id.eq(&self.id)) .get_result(conn) .map_err(|e| match e { DieselError::NotFound => UserError::ZoneNotFound, other => UserError::DbError(other) })?; Ok(res_zone) } pub fn get_zones(&self, conn: &diesel::SqliteConnection) -> Result, UserError> { use crate::schema::user_zone::dsl::*; use crate::schema::zone::dsl::*; let res: Vec<(Zone, UserZone)> = zone.inner_join(user_zone) .filter(user_id.eq(&self.id)) .get_results(conn) .map_err(UserError::DbError)?; Ok(res.into_iter().map(|(z, _)| z).collect()) } } #[rocket::async_trait] impl<'r> FromRequest<'r> for UserInfo { type Error = ErrorResponse; async fn from_request(request: &'r Request<'_>) -> Outcome { let session = try_outcome!(request.guard::().await); let conn = try_outcome!(request.guard::().await.map_failure(make_500)); conn.run(move |c| { match LocalUser::get_user_by_uuid(c, &session.user_id) { Err(e) => ErrorResponse::from(e).into(), Ok(d) => Outcome::Success(d), } }).await } } impl LocalUser { pub fn hash_password(password: &str) -> String { let salt = SaltString::generate(&mut OsRng); // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html let argon2 = Argon2::new( Algorithm::Argon2id, Version::V0x13, // v19 Params::new(15000, 2, 1, None).expect("password param error"), ); argon2.hash_password(password.as_bytes(), &salt).expect("password hash failed").to_string() } pub fn verify_password(password: &str, password_hash: &str) -> Result { let parsed_hash = PasswordHash::new(&password_hash)?; Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok()) } pub fn create_user(conn: &diesel::SqliteConnection, user_request: CreateUserRequest) -> Result { use crate::schema::localuser::dsl::*; use crate::schema::user::dsl::*; let new_user_id = Uuid::new_v4().to_simple().to_string(); let new_user = User { id: new_user_id.clone(), }; let new_localuser = LocalUser { user_id: new_user_id, username: user_request.username.clone(), password: LocalUser::hash_password(&user_request.password), role: if let Some(user_role) = user_request.role { user_role } else { Role::ZoneAdmin }, }; let res = UserInfo { id: new_user.id.clone(), role: new_localuser.role.clone(), username: new_localuser.username.clone(), }; conn.immediate_transaction(|| -> diesel::QueryResult<()> { diesel::insert_into(user) .values(new_user) .execute(conn)?; diesel::insert_into(localuser) .values(new_localuser) .execute(conn)?; Ok(()) }).map_err(|e| match e { DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserConflict, other => UserError::DbError(other) })?; Ok(res) } pub fn get_user_by_creds( conn: &diesel::SqliteConnection, request_username: &str, request_password: &str ) -> Result { use crate::schema::localuser::dsl::*; use crate::schema::user::dsl::*; let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser) .filter(username.eq(request_username)) .get_result(conn) .map_err(|e| match e { DieselError::NotFound => UserError::BadCreds, other => UserError::DbError(other) })?; if !LocalUser::verify_password(&request_password, &client_localuser.password)? { return Err(UserError::BadCreds); } Ok(UserInfo { id: client_user.id, role: client_localuser.role, username: client_localuser.username, }) } pub fn get_user_by_uuid(conn: &diesel::SqliteConnection, request_user_id: &str) -> Result { use crate::schema::localuser::dsl::*; use crate::schema::user::dsl::*; let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser) .filter(id.eq(request_user_id)) .get_result(conn) .map_err(|e| match e { DieselError::NotFound => UserError::NotFound, other => UserError::DbError(other) })?; Ok(UserInfo { id: client_user.id, role: client_localuser.role, username: client_localuser.username, }) } }