use uuid::Uuid; use diesel::prelude::*; use diesel::result::Error as DieselError; use diesel_derive_enum::DbEnum; use rocket::{State, request::{FromRequest, Request, Outcome}}; use serde::{Deserialize}; // TODO: Maybe just use argon2 crate directly use djangohashers::{make_password_with_algorithm, check_password, Algorithm}; use jsonwebtoken::{ errors::ErrorKind as JwtErrorKind }; use crate::schema::*; use crate::DbConn; use crate::config::Config; use crate::models::errors::{UserError, ErrorResponse, make_500}; use crate::models::zone::Zone; use crate::models::auth::AuthClaims; const BEARER: &str = "Bearer "; const AUTH_HEADER: &str = "Authorization"; #[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 auth_header = match request.headers().get_one(AUTH_HEADER) { None => return Outcome::Forward(()), Some(auth_header) => auth_header, }; let token = if auth_header.starts_with(BEARER) { auth_header.trim_start_matches(BEARER) } else { return ErrorResponse::from(UserError::MalformedHeader).into() }; let config = try_outcome!(request.guard::>().await.map_failure(make_500)); let conn = try_outcome!(request.guard::().await.map_failure(make_500)); let token_data = AuthClaims::decode( token, &config.web_app.secret ).map_err(|e| match e.into_kind() { JwtErrorKind::ExpiredSignature => UserError::ExpiredToken, _ => UserError::BadToken, }); let token_data = match token_data { Err(e) => return ErrorResponse::from(e).into(), Ok(data) => data }; let user_id = token_data.sub; conn.run(move |c| { match LocalUser::get_user_by_uuid(c, &user_id) { Err(UserError::NotFound) => ErrorResponse::from(UserError::NotFound).into(), Err(e) => ErrorResponse::from(e).into(), Ok(d) => Outcome::Success(d), } }).await } } impl LocalUser { 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: make_password_with_algorithm(&user_request.password, Algorithm::Argon2), // TODO: Use role from request role: 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 !check_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, }) } }