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::{Serialize, Deserialize}; use chrono::serde::ts_seconds; use chrono::prelude::{DateTime, Utc}; use chrono::Duration; // TODO: Maybe just use argon2 crate directly use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm}; use jsonwebtoken::{ encode, decode, Header, Validation, Algorithm as JwtAlgorithm, EncodingKey, DecodingKey, errors::Result as JwtResult, errors::ErrorKind as JwtErrorKind }; use crate::schema::*; use crate::DbConn; use crate::config::Config; use crate::models::errors::ErrorResponse; const BEARER: &'static str = "Bearer "; const AUTH_HEADER: &'static str = "Authentication"; #[derive(Debug, DbEnum, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Role { Admin, ZoneAdmin, } // TODO: Store Uuid instead of string?? // TODO: Store role as Role and not String. #[derive(Debug, Queryable, Identifiable, Insertable)] #[table_name = "user"] pub struct User { pub id: String, pub role: 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, } #[derive(Debug, Deserialize)] pub struct CreateUserRequest { pub username: String, pub password: String, pub email: String, pub role: Option } // pub struct LdapUserAssociation { // user_id: Uuid, // ldap_id: String // } #[derive(Debug, Serialize, Deserialize)] pub struct AuthClaims { pub jti: String, pub sub: String, #[serde(with = "ts_seconds")] pub exp: DateTime, #[serde(with = "ts_seconds")] pub iat: DateTime, } #[derive(Debug, Serialize)] pub struct AuthTokenResponse { pub token: String } #[derive(Debug, Deserialize)] pub struct AuthTokenRequest { pub username: String, pub password: String, } #[derive(Debug)] pub struct UserInfo { pub id: String, pub role: String, pub username: String, } #[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() }; // TODO: Better error handling let config = request.guard::>().await.unwrap(); let conn = request.guard::().await.unwrap(); 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(|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 } } #[derive(Debug)] pub enum UserError { NotFound, UserExists, BadToken, ExpiredToken, MalformedHeader, PermissionDenied, DbError(DieselError), PasswordError(HasherError), } impl From for UserError { fn from(e: DieselError) -> Self { match e { DieselError::NotFound => UserError::NotFound, DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserExists, other => UserError::DbError(other) } } } impl From for UserError { fn from(e: HasherError) -> Self { match e { other => UserError::PasswordError(other) } } } 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(), // TODO: Use role from request role: "zoneadmin".into(), }; let new_localuser = LocalUser { user_id: new_user_id.clone(), username: user_request.username.clone(), password: make_password_with_algorithm(&user_request.password, Algorithm::Argon2), }; let res = UserInfo { id: new_user.id.clone(), role: new_user.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(()) })?; 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)?; if !check_password(&request_password, &client_localuser.password)? { return Err(UserError::NotFound); } Ok(UserInfo { id: client_user.id, role: client_user.role, username: client_localuser.username, }) } pub fn get_user_by_uuid(conn: &diesel::SqliteConnection, request_user_id: String) -> 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)?; Ok(UserInfo { id: client_user.id, role: client_user.role, username: client_localuser.username, }) } } impl AuthClaims { pub fn new(user_info: &UserInfo, token_duration: Duration) -> AuthClaims { let jti = Uuid::new_v4().to_simple().to_string(); let iat = Utc::now(); let exp = iat + token_duration; AuthClaims { jti: jti, sub: user_info.id.clone(), exp: exp, iat: iat, } } pub fn decode(token: &str, secret: &str) -> JwtResult { decode::( token, &DecodingKey::from_secret(secret.as_ref()), &Validation::new(JwtAlgorithm::HS256) ).and_then(|data| Ok(data.claims)) } pub fn encode(self, secret: &str) -> JwtResult { encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_ref())) } }