Merge pull request #28 from EETagent/session_service

Session service with admins
This commit is contained in:
Sebastian Pravda 2022-11-04 12:55:34 +01:00 committed by GitHub
commit f2fadfdfe4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 464 additions and 294 deletions

View file

@ -0,0 +1,40 @@
use entity::candidate::Model as Admin;
use portfolio_core::sea_orm::prelude::Uuid;
use portfolio_core::services::admin_service::AdminService;
use rocket::http::Status;
use rocket::outcome::Outcome;
use rocket::request::{FromRequest, Request};
use crate::pool::Db;
pub struct AdminAuth(Admin);
impl Into<Admin> for AdminAuth {
fn into(self) -> Admin {
self.0
}
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AdminAuth {
type Error = Option<String>;
async fn from_request(req: &'r Request<'_>) -> Outcome<AdminAuth, (Status, Self::Error), ()> {
let session_id = req.cookies().get("id").unwrap().name_value().1;
let conn = &req.rocket().state::<Db>().unwrap().conn;
let uuid = match Uuid::parse_str(&session_id) {
Ok(uuid) => uuid,
Err(_) => return Outcome::Failure((Status::BadRequest, None)),
};
let session = AdminService::auth(conn, uuid).await;
match session {
Ok(model) => Outcome::Success(AdminAuth(model)),
Err(e) => Outcome::Failure(
(Status::from_code(e.code()).unwrap_or(Status::InternalServerError), None)
),
}
}
}

View file

@ -0,0 +1,38 @@
use entity::candidate::Model as Candidate;
use portfolio_core::sea_orm::prelude::Uuid;
use portfolio_core::services::candidate_service::CandidateService;
use rocket::http::Status;
use rocket::outcome::Outcome;
use rocket::request::{FromRequest, Request};
use crate::pool::Db;
pub struct CandidateAuth(Candidate);
impl Into<Candidate> for CandidateAuth {
fn into(self) -> Candidate {
self.0
}
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for CandidateAuth {
type Error = Option<String>;
async fn from_request(req: &'r Request<'_>) -> Outcome<CandidateAuth, (Status, Self::Error), ()> {
let session_id = req.cookies().get("id").unwrap().name_value().1;
let conn = &req.rocket().state::<Db>().unwrap().conn;
let uuid = match Uuid::parse_str(&session_id) {
Ok(uuid) => uuid,
Err(_) => return Outcome::Failure((Status::BadRequest, None)),
};
let session = CandidateService::auth(conn, uuid).await;
match session {
Ok(model) => Outcome::Success(CandidateAuth(model)),
Err(_) => Outcome::Failure((Status::Unauthorized, None)),
}
}
}

View file

@ -0,0 +1,5 @@
pub mod admin;
pub mod candidate;
pub use admin::*;
pub use candidate::*;

View file

@ -1 +1 @@
pub mod session_auth;
pub mod auth;

View file

@ -1,5 +1,6 @@
use entity::candidate::Model as Candidate;
use portfolio_core::sea_orm::prelude::Uuid;
use portfolio_core::services::admin_service::AdminService;
use portfolio_core::services::candidate_service::CandidateService;
use rocket::http::Status;
use rocket::outcome::Outcome;
@ -7,18 +8,18 @@ use rocket::request::{FromRequest, Request};
use crate::pool::Db;
pub struct SessionAuth(Candidate);
pub struct CandidateAuth(Candidate);
impl Into<Candidate> for SessionAuth {
impl Into<Candidate> for CandidateAuth {
fn into(self) -> Candidate {
self.0
}
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for SessionAuth {
impl<'r> FromRequest<'r> for CandidateAuth {
type Error = Option<String>;
async fn from_request(req: &'r Request<'_>) -> Outcome<SessionAuth, (Status, Self::Error), ()> {
async fn from_request(req: &'r Request<'_>) -> Outcome<CandidateAuth, (Status, Self::Error), ()> {
let session_id = req.cookies().get("id").unwrap().name_value().1;
let conn = &req.rocket().state::<Db>().unwrap().conn;
@ -27,12 +28,44 @@ impl<'r> FromRequest<'r> for SessionAuth {
Err(_) => return Outcome::Failure((Status::BadRequest, None)),
};
let session = CandidateService::auth_user_session(conn, uuid).await;
let session = CandidateService::auth(conn, uuid).await;
match session {
Ok(model) => Outcome::Success(SessionAuth(model)),
Ok(model) => Outcome::Success(CandidateAuth(model)),
Err(_) => Outcome::Failure((Status::Unauthorized, None)),
}
}
}
pub struct AdminAuth(Candidate);
impl Into<Candidate> for AdminAuth {
fn into(self) -> Candidate {
self.0
}
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AdminAuth {
type Error = Option<String>;
async fn from_request(req: &'r Request<'_>) -> Outcome<AdminAuth, (Status, Self::Error), ()> {
let session_id = req.cookies().get("id").unwrap().name_value().1;
let conn = &req.rocket().state::<Db>().unwrap().conn;
let uuid = match Uuid::parse_str(&session_id) {
Ok(uuid) => uuid,
Err(_) => return Outcome::Failure((Status::BadRequest, None)),
};
let session = AdminService::auth(conn, uuid).await;
match session {
Ok(model) => Outcome::Success(AdminAuth(model)),
Err(e) => Outcome::Failure(
(Status::from_code(e.code()).unwrap_or(Status::InternalServerError), None)
),
}
}
}

View file

@ -3,8 +3,7 @@ extern crate rocket;
use std::net::SocketAddr;
use guards::request::session_auth::SessionAuth;
use portfolio_core::error::ServiceError;
use guards::request::auth::{CandidateAuth, AdminAuth};
use portfolio_core::services::candidate_service::CandidateService;
use requests::{LoginRequest, RegisterRequest};
use rocket::http::Status;
@ -12,7 +11,6 @@ use rocket::{Rocket, Build};
use rocket::serde::json::Json;
use rocket::fairing::{self, AdHoc};
use rocket::response::status::Custom;
use portfolio_core::{Mutation};
use migration::{MigratorTrait};
use sea_orm_rocket::{Connection, Database};
@ -30,10 +28,6 @@ pub use entity::candidate::Entity as Candidate;
use portfolio_core::crypto::random_8_char_string;
fn custom_err_from_service_err(service_err: ServiceError) -> Custom<String> {
Custom(Status::from_code(service_err.0.code).unwrap_or_default(), service_err.1.to_string())
}
#[post("/", data = "<post_form>")]
async fn create(conn: Connection<'_, Db>, post_form: Json<RegisterRequest>) -> Result<String, Custom<String>> {
let db = conn.into_inner();
@ -41,39 +35,42 @@ async fn create(conn: Connection<'_, Db>, post_form: Json<RegisterRequest>) -> R
let plain_text_password = random_8_char_string();
Mutation::create_candidate(db, form.application_id, &plain_text_password, form.personal_id_number)
.await
.expect("Could not insert candidate");
let candidate = CandidateService::create(db, form.application_id, &plain_text_password, form.personal_id_number)
.await;
if candidate.is_err() { // TODO cleanup
let e = candidate.err().unwrap();
return Err(Custom(Status::from_code(e.code()).unwrap_or_default(), e.message()));
}
Ok(plain_text_password)
Ok(plain_text_password)
}
#[get("/whoami")]
async fn validate(session: SessionAuth) -> Result<String, Custom<String>> {
async fn validate(session: CandidateAuth) -> Result<String, Custom<String>> {
let candidate: entity::candidate::Model = session.into();
Ok(candidate.application.to_string())
}
#[get("/admin")]
async fn admin(session: AdminAuth) -> Result<String, Custom<String>> {
Ok("Hello admin".to_string())
}
#[post("/login", data = "<login_form>")]
async fn login(conn: Connection<'_, Db>, login_form: Json<LoginRequest>, ip_addr: SocketAddr) -> Result<String, Custom<String>> {
let db = conn.into_inner();
println!("{} {}", login_form.application_id, login_form.password);
let session_token = CandidateService::new_session(db,
login_form.application_id,
login_form.password.to_string(),
ip_addr.ip().to_string()
).await;
let session_token = CandidateService::login(
db,
login_form.application_id,
login_form.password.to_string(),
ip_addr.ip().to_string()
)
.await;
if session_token.is_ok() {
return Ok(
session_token.ok().unwrap()
);
} else {
return Err(
custom_err_from_service_err(session_token.err().unwrap())
)
}
session_token.map_err(|e| Custom(Status::from_code(e.code()).unwrap_or_default(), e.message()))
}
#[get("/hello")]
@ -93,7 +90,7 @@ async fn start() -> Result<(), rocket::Error> {
.attach(Db::init())
.attach(AdHoc::try_on_ignite("Migrations", run_migrations))
//.mount("/", FileServer::from(relative!("/static")))
.mount("/", routes![create, login, hello, validate])
.mount("/", routes![create, login, hello, validate, admin])
.register("/", catchers![])
.launch()
.await

View file

@ -1,27 +1,17 @@
use crate::Mutation;
use std::vec;
use ::entity::candidate;
use sea_orm::{*};
use crate::{crypto::{hash_password, self}};
impl Mutation {
pub async fn create_candidate(
db: &DbConn,
application_id: i32,
plain_text_password: &String,
personal_id_number: String,
hashed_password: String,
encrypted_personal_id_number: String,
pubkey: String,
encrypted_priv_key: String
) -> Result<candidate::Model, DbErr> {
// TODO: unwrap pro testing..
let hashed_password = hash_password(plain_text_password.to_string()).await.unwrap();
let (pubkey, priv_key_plain_text) = crypto::create_identity();
let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text, plain_text_password.to_string()).await.unwrap();
let encrypted_personal_id_number = crypto::encrypt_password_with_recipients(
&personal_id_number, vec![&pubkey]
).await.unwrap();
candidate::ActiveModel {
application: Set(application_id),
personal_identification_number: Set(encrypted_personal_id_number),
@ -35,49 +25,4 @@ impl Mutation {
.insert(db)
.await
}
}
#[cfg(test)]
mod tests {
use sea_orm::{Database, DbConn};
use crate::{Mutation, crypto};
#[cfg(test)]
async fn get_memory_sqlite_connection() -> DbConn {
use entity::candidate;
use sea_orm::{DbBackend, sea_query::TableCreateStatement, ConnectionTrait};
use sea_orm::Schema;
let base_url = "sqlite::memory:";
let db: DbConn = Database::connect(base_url).await.unwrap();
let schema = Schema::new(DbBackend::Sqlite);
let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity);
db.execute(db.get_database_backend().build(&stmt)).await.unwrap();
db
}
#[tokio::test]
async fn test_encrypt_decrypt_private_key_with_passphrase() {
let db = get_memory_sqlite_connection().await;
let plain_text_password = "test".to_string();
let secret_message = "trnka".to_string();
let candidate = Mutation::create_candidate(&db, 5555555, &plain_text_password, "".to_string()).await.unwrap();
let encrypted_message = crypto::encrypt_password_with_recipients(&secret_message, vec![&candidate.public_key]).await.unwrap();
let private_key_plain_text = crypto::decrypt_password(candidate.private_key, plain_text_password).await.unwrap();
let decrypted_message = crypto::decrypt_password_with_private_key(&encrypted_message, &private_key_plain_text).await.unwrap();
assert_eq!(secret_message, decrypted_message);
}
}

View file

@ -1,24 +1,45 @@
pub struct Status {
pub code: u16,
pub enum ServiceError {
InvalidCredentials,
Forbidden,
ExpiredSession,
JwtError,
UserNotFound,
DbError,
UserNotFoundByJwtId,
UserNotFoundBySessionId,
}
pub const INVALID_CREDENTIALS_ERROR: ServiceError = ServiceError(Status { code: 401 },
"Invalid credentials");
pub const EXPIRED_SESSION_ERROR: ServiceError = ServiceError(Status { code: 401 },
"Session expired, please login again");
impl ServiceError {
fn code_and_message(&self) -> (u16, String) {
match self {
ServiceError::InvalidCredentials => (401, "Invalid credentials".to_string()),
ServiceError::Forbidden => (403, "Forbidden".to_string()),
ServiceError::ExpiredSession => (401, "Session expired, please login again".to_string()),
ServiceError::JwtError => (500, "Error while encoding JWT".to_string()),
ServiceError::UserNotFound => (404, "User not found".to_string()),
ServiceError::DbError => (500, "Database error".to_string()),
ServiceError::UserNotFoundByJwtId => (500, "User not found, please contact technical support".to_string()),
ServiceError::UserNotFoundBySessionId => (500, "User not found, please contact technical support".to_string()),
}
}
pub const JWT_ERROR: ServiceError = ServiceError(Status { code: 500 },
"Error while encoding JWT");
pub fn code(&self) -> u16 {
self.code_and_message().0
}
pub const USER_NOT_FOUND_ERROR: ServiceError = ServiceError(Status { code: 404 },
"User not found");
pub fn message(&self) -> String {
self.code_and_message().1
}
}
pub const DB_ERROR: ServiceError = ServiceError(Status { code: 500 },
"Database error");
impl std::fmt::Debug for ServiceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ServiceError {{ code: {}, message: {} }}", self.code(), self.message())
}
}
pub const USER_NOT_FOUND_BY_JWT_ID: ServiceError = ServiceError(Status { code: 500 }, // User got somehow deleted
"User not found, please contact technical support"); // Shouldn't ever happen
pub const USER_NOT_FOUND_BY_SESSION_ID: ServiceError = ServiceError(Status { code: 500 }, // User got somehow deleted
"User not found, please contact technical support"); // Shouldn't ever happen
pub struct ServiceError<'a>(pub Status, pub &'a str);
impl std::fmt::Display for ServiceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ServiceError {{ code: {}, message: {} }}", self.code(), self.message())
}
}

View file

@ -0,0 +1,35 @@
use entity::candidate;
use sea_orm::{DbConn, prelude::Uuid};
use crate::error::ServiceError;
use super::session_service::SessionService;
pub struct AdminService;
impl AdminService {
pub async fn login(
db: &DbConn,
user_id: i32,
password: String,
ip_addr: String
) -> Result<String, ServiceError> {
SessionService::new_session(db, user_id, password, ip_addr).await
}
pub async fn auth(
db: &DbConn,
session_uuid: Uuid,
) -> Result<candidate::Model, ServiceError> {
match SessionService::auth_user_session(db, session_uuid).await {
Ok(user) => {
if user.is_admin {
Ok(user)
} else {
Err(ServiceError::Forbidden)
}
},
Err(e) => Err(e)
}
}
}

View file

@ -1,162 +1,98 @@
use std::cmp::min;
use entity::candidate;
use sea_orm::{DatabaseConnection, prelude::Uuid, ModelTrait};
use sea_orm::{DbConn, prelude::Uuid};
use crate::{crypto::{self}, Query, error::{ServiceError, USER_NOT_FOUND_ERROR, INVALID_CREDENTIALS_ERROR, DB_ERROR, USER_NOT_FOUND_BY_JWT_ID, USER_NOT_FOUND_BY_SESSION_ID, EXPIRED_SESSION_ERROR}, Mutation};
use crate::{Mutation, crypto::{hash_password, self}, error::{ServiceError}};
use super::session_service::SessionService;
pub struct CandidateService;
impl CandidateService {
/// Delete n old sessions for user
async fn delete_old_sessions(db: &DatabaseConnection, user_id: i32, keep_n_recent: usize) -> Result<(), ServiceError> {
let mut sessions = Query::find_sessions_by_user_id(db, user_id).await.unwrap();
sessions.sort_by_key(|s| s.created_at);
pub async fn create(
db: &DbConn,
application_id: i32,
plain_text_password: &String,
personal_id_number: String
) -> Result<candidate::Model, ServiceError>{
// TODO: unwrap pro testing..
let hashed_password = hash_password(plain_text_password.to_string()).await.unwrap();
let (pubkey, priv_key_plain_text) = crypto::create_identity();
let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text, plain_text_password.to_string()).await.unwrap();
for session in sessions.iter().take(sessions.len() - min(sessions.len(), keep_n_recent)) {
Mutation::delete_session(db, session.id).await.unwrap();
}
let encrypted_personal_id_number = crypto::encrypt_password_with_recipients(
&personal_id_number, vec![&pubkey]
).await.unwrap();
Ok(())
Mutation::create_candidate(
db,
application_id,
hashed_password,
encrypted_personal_id_number,
pubkey,
encrypted_priv_key
)
.await
.map_err(|_| ServiceError::DbError)
}
/// Authenticate user by application id and password and generate a new session
pub async fn new_session(db: &DatabaseConnection, user_id: i32, password: String, ip_addr: String) -> Result<String, ServiceError> {
let candidate = match Query::find_candidate_by_id(db, user_id).await {
Ok(candidate) => match candidate {
Some(candidate) => candidate,
None => return Err(USER_NOT_FOUND_ERROR)
},
Err(_) => {return Err(DB_ERROR)}
};
// compare passwords
match crypto::verify_password(password,candidate.code.clone()).await {
Ok(valid) => {
if !valid {
return Err(INVALID_CREDENTIALS_ERROR)
}
},
Err(_) => {return Err(INVALID_CREDENTIALS_ERROR)}
}
// user is authenticated, generate a new session
let random_uuid: Uuid = Uuid::new_v4();
let session = match Mutation::insert_session(db, user_id, random_uuid, ip_addr).await {
Ok(session) => session,
Err(_) => return Err(DB_ERROR)
};
// delete old sessions
CandidateService::delete_old_sessions(db, candidate.application, 3).await.ok(); // TODO move to dotenv
Ok(session.id.to_string())
pub async fn login(
db: &DbConn,
user_id: i32,
password: String,
ip_addr: String
) -> Result<String, ServiceError> {
SessionService::new_session(db, user_id, password, ip_addr).await
}
/// Authenticate user by session id
/// Return user model if session is valid
pub async fn auth_user_session(db: &DatabaseConnection, uuid: Uuid) -> Result<candidate::Model, ServiceError> {
let session = match Query::find_session_by_uuid(db, uuid).await {
Ok(session) => match session {
Some(session) => session,
None => return Err(USER_NOT_FOUND_BY_SESSION_ID)
},
Err(_) => {return Err(DB_ERROR)}
};
let now = chrono::Utc::now().naive_utc();
// check if session is expired
if now > session.expires_at {
// delete session
Mutation::delete_session(db, session.id).await.unwrap();
return Err(EXPIRED_SESSION_ERROR)
}
let candidate = match session.find_related(candidate::Entity).one(db).await {
Ok(candidate) => match candidate {
Some(candidate) => candidate,
None => return Err(USER_NOT_FOUND_BY_JWT_ID)
},
Err(_) => {return Err(DB_ERROR)}
};
Ok(candidate)
pub async fn auth(
db: &DbConn,
session_uuid: Uuid,
) -> Result<candidate::Model, ServiceError> {
SessionService::auth_user_session(db, session_uuid).await
}
}
#[cfg(test)]
mod tests {
use entity::candidate;
use sea_orm::{DbConn, Database, sea_query::TableCreateStatement, DbBackend, Schema, ConnectionTrait, prelude::Uuid};
use sea_orm::{Database, DbConn};
use crate::{crypto, Mutation, services::candidate_service::CandidateService};
use crate::{crypto, services::candidate_service::CandidateService};
#[cfg(test)]
async fn get_memory_sqlite_connection() -> DbConn {
use entity::session;
use entity::candidate;
use sea_orm::{DbBackend, sea_query::TableCreateStatement, ConnectionTrait};
use sea_orm::Schema;
let base_url = "sqlite::memory:";
let db: DbConn = Database::connect(base_url).await.unwrap();
let schema = Schema::new(DbBackend::Sqlite);
let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity);
let stmt2: TableCreateStatement = schema.create_table_from_entity(session::Entity);
db.execute(db.get_database_backend().build(&stmt)).await.unwrap();
db.execute(db.get_database_backend().build(&stmt2)).await.unwrap();
db
}
#[tokio::test]
async fn test_create_candidate() {
const SECRET: &str = "Tajny_kod";
#[tokio::test]
async fn test_encrypt_decrypt_private_key_with_passphrase() {
let db = get_memory_sqlite_connection().await;
let candidate = Mutation::create_candidate(&db, 5555555, &SECRET.to_string(), "".to_string()).await.unwrap();
assert_eq!(candidate.application, 5555555);
assert_ne!(candidate.code, SECRET.to_string());
assert!(crypto::verify_password(SECRET.to_string(), candidate.code).await.ok().unwrap());
}
#[tokio::test]
async fn test_candidate_session_correct_password() {
let db = &get_memory_sqlite_connection().await;
let plain_text_password = "test".to_string();
Mutation::create_candidate(&db, 5555555, &"Tajny_kod".to_string(), "".to_string()).await.unwrap();
let secret_message = "trnka".to_string();
// correct password
let session = CandidateService::new_session(
db,
5555555,
"Tajny_kod".to_string(),
"127.0.0.1".to_string(),
)
.await.ok().unwrap();
// println!("{}", session.err().unwrap().1);
let candidate = CandidateService::create(&db, 5555555, &plain_text_password, "".to_string()).await.ok().unwrap();
assert!(
CandidateService::auth_user_session(db, Uuid::parse_str(&session).unwrap())
.await
.is_ok()
);
}
let encrypted_message = crypto::encrypt_password_with_recipients(&secret_message, vec![&candidate.public_key]).await.unwrap();
#[tokio::test]
async fn test_candidate_session_incorrect_password() {
let db = &get_memory_sqlite_connection().await;
let private_key_plain_text = crypto::decrypt_password(candidate.private_key, plain_text_password).await.unwrap();
let candidate_form = Mutation::create_candidate(&db, 5555555, &"Tajny_kod".to_string(), "".to_string()).await.unwrap();
let decrypted_message = crypto::decrypt_password_with_private_key(&encrypted_message, &private_key_plain_text).await.unwrap();
assert_eq!(secret_message, decrypted_message);
// incorrect password
assert!(
CandidateService::new_session(db, candidate_form.application, "Spatny_kod".to_string(), "127.0.0.1".to_string()).await.is_err()
);
}
}

View file

@ -1 +1,3 @@
pub mod candidate_service;
pub mod session_service;
pub mod candidate_service;
pub mod admin_service;

View file

@ -0,0 +1,163 @@
use std::cmp::min;
use entity::candidate;
use sea_orm::{DatabaseConnection, prelude::Uuid, ModelTrait};
use crate::{crypto::{self}, Query, error::{ServiceError}, Mutation};
// TODO: generics
pub(in crate::services) struct SessionService;
impl SessionService {
/// Delete n old sessions for user
async fn delete_old_sessions(db: &DatabaseConnection, user_id: i32, keep_n_recent: usize) -> Result<(), ServiceError> {
let mut sessions = Query::find_sessions_by_user_id(db, user_id).await.unwrap();
sessions.sort_by_key(|s| s.created_at);
for session in sessions.iter().take(sessions.len() - min(sessions.len(), keep_n_recent)) {
Mutation::delete_session(db, session.id).await.unwrap();
}
Ok(())
}
/// Authenticate user by application id and password and generate a new session
pub async fn new_session(db: &DatabaseConnection, user_id: i32, password: String, ip_addr: String) -> Result<String, ServiceError> {
let candidate = match Query::find_candidate_by_id(db, user_id).await {
Ok(candidate) => match candidate {
Some(candidate) => candidate,
None => return Err(ServiceError::UserNotFound)
},
Err(_) => {return Err(ServiceError::DbError)}
};
// compare passwords
match crypto::verify_password(password,candidate.code.clone()).await {
Ok(valid) => {
if !valid {
return Err(ServiceError::InvalidCredentials)
}
},
Err(_) => {return Err(ServiceError::InvalidCredentials)}
}
// user is authenticated, generate a new session
let random_uuid: Uuid = Uuid::new_v4();
let session = match Mutation::insert_session(db, user_id, random_uuid, ip_addr).await {
Ok(session) => session,
Err(_) => return Err(ServiceError::DbError)
};
// delete old sessions
SessionService::delete_old_sessions(db, candidate.application, 3).await.ok(); // TODO move to dotenv
Ok(session.id.to_string())
}
/// Authenticate user by session id
/// Return user model if session is valid
pub async fn auth_user_session(db: &DatabaseConnection, uuid: Uuid) -> Result<candidate::Model, ServiceError> {
let session = match Query::find_session_by_uuid(db, uuid).await {
Ok(session) => match session {
Some(session) => session,
None => return Err(ServiceError::UserNotFoundBySessionId)
},
Err(_) => {return Err(ServiceError::DbError)}
};
let now = chrono::Utc::now().naive_utc();
// check if session is expired
if now > session.expires_at {
// delete session
Mutation::delete_session(db, session.id).await.unwrap();
return Err(ServiceError::ExpiredSession)
}
let candidate = match session.find_related(candidate::Entity).one(db).await {
Ok(candidate) => match candidate {
Some(candidate) => candidate,
None => return Err(ServiceError::UserNotFoundBySessionId)
},
Err(_) => {return Err(ServiceError::DbError)}
};
Ok(candidate)
}
}
#[cfg(test)]
mod tests {
use entity::{candidate};
use sea_orm::{DbConn, Database, sea_query::TableCreateStatement, DbBackend, Schema, ConnectionTrait, prelude::Uuid};
use crate::{crypto, services::{session_service::SessionService, candidate_service::CandidateService}};
#[cfg(test)]
async fn get_memory_sqlite_connection() -> DbConn {
use entity::session;
let base_url = "sqlite::memory:";
let db: DbConn = Database::connect(base_url).await.unwrap();
let schema = Schema::new(DbBackend::Sqlite);
let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity);
let stmt2: TableCreateStatement = schema.create_table_from_entity(session::Entity);
db.execute(db.get_database_backend().build(&stmt)).await.unwrap();
db.execute(db.get_database_backend().build(&stmt2)).await.unwrap();
db
}
#[tokio::test]
async fn test_create_candidate() {
const SECRET: &str = "Tajny_kod";
let db = get_memory_sqlite_connection().await;
let candidate = CandidateService::create(&db, 5555555, &SECRET.to_string(), "".to_string()).await.ok().unwrap();
assert_eq!(candidate.application, 5555555);
assert_ne!(candidate.code, SECRET.to_string());
assert!(crypto::verify_password(SECRET.to_string(), candidate.code).await.ok().unwrap());
}
#[tokio::test]
async fn test_candidate_session_correct_password() {
let db = &get_memory_sqlite_connection().await;
CandidateService::create(&db, 5555555, &"Tajny_kod".to_string(), "".to_string()).await.ok().unwrap();
// correct password
let session = SessionService::new_session(
db,
5555555,
"Tajny_kod".to_string(),
"127.0.0.1".to_string(),
)
.await.ok().unwrap();
// println!("{}", session.err().unwrap().1);
assert!(
SessionService::auth_user_session(db, Uuid::parse_str(&session).unwrap())
.await
.is_ok()
);
}
#[tokio::test]
async fn test_candidate_session_incorrect_password() {
let db = &get_memory_sqlite_connection().await;
let candidate_form = CandidateService::create(&db, 5555555, &"Tajny_kod".to_string(), "".to_string()).await.ok().unwrap();
// incorrect password
assert!(
SessionService::new_session(db, candidate_form.application, "Spatny_kod".to_string(), "127.0.0.1".to_string()).await.is_err()
);
}
}

View file

@ -22,6 +22,8 @@ pub struct Model {
pub personal_identification_number_hash: Option<String>,
pub public_key: String,
pub private_key: String,
#[sea_orm(default_value = false)]
pub is_admin: bool,
pub created_at: DateTime,
pub updated_at: DateTime,
}

View file

@ -1,9 +1,8 @@
pub use sea_orm_migration::prelude::*;
mod m20221024_111310_create_admin;
mod m20221024_121621_create_candidate;
mod m20221024_124701_create_parent;
mod m20221024_134454_fill_admin;
mod m20221024_134454_insert_sample_admin;
mod m20221025_154422_create_session;
mod m20221027_194728_session_create_user_fk;
mod m20221030_133428_parent_create_candidate_fk;
@ -13,10 +12,9 @@ pub struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20221024_111310_create_admin::Migration),
Box::new(m20221024_121621_create_candidate::Migration),
Box::new(m20221024_124701_create_parent::Migration),
Box::new(m20221024_134454_fill_admin::Migration::default()),
Box::new(m20221024_134454_insert_sample_admin::Migration::default()),
Box::new(m20221025_154422_create_session::Migration),
Box::new(m20221027_194728_session_create_user_fk::Migration),
Box::new(m20221030_133428_parent_create_candidate_fk::Migration),

View file

@ -1,49 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Admin::Table)
.if_not_exists()
.col(
ColumnDef::new(Admin::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Admin::Name).string().not_null())
.col(ColumnDef::new(Admin::PublicKey).string().not_null())
.col(ColumnDef::new(Admin::PrivateKeyHash).text().not_null())
.col(ColumnDef::new(Admin::PasswordHash).string().not_null())
.col(ColumnDef::new(Admin::CreatedAt).date_time().not_null())
.col(ColumnDef::new(Admin::UpdatedAt).date_time().not_null())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Admin::Table).to_owned())
.await
}
}
#[derive(Iden)]
enum Admin {
Table,
Id,
Name,
PublicKey,
PrivateKeyHash,
PasswordHash,
CreatedAt,
UpdatedAt,
}

View file

@ -34,6 +34,7 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(Candidate::PersonalIdentificationNumberHash).text())
.col(ColumnDef::new(Candidate::PublicKey).string().not_null())
.col(ColumnDef::new(Candidate::PrivateKey).string().not_null())
.col(ColumnDef::new(Candidate::IsAdmin).boolean().not_null().default(false))
.col(ColumnDef::new(Candidate::CreatedAt).date_time().not_null())
.col(ColumnDef::new(Candidate::UpdatedAt).date_time().not_null())
.to_owned(),
@ -68,6 +69,7 @@ pub enum Candidate {
PersonalIdentificationNumberHash,
PublicKey,
PrivateKey,
IsAdmin,
CreatedAt,
UpdatedAt,
}

View file

@ -1,5 +1,5 @@
use chrono::Local;
use entity::admin;
use entity::{candidate};
use sea_orm_migration::{
prelude::*,
sea_orm::{ActiveModelTrait, Set},
@ -7,20 +7,22 @@ use sea_orm_migration::{
#[derive(DeriveMigrationName)]
pub struct Migration {
admin: admin::ActiveModel,
candidate: candidate::ActiveModel,
}
impl Default for Migration {
fn default() -> Self {
Self {
admin: admin::ActiveModel {
id: Set(1),
name: Set("Administrátor Pepa".to_owned()),
candidate: candidate::ActiveModel {
application: Set(1),
name: Set(Some("Administrátor Pepa".to_owned())),
public_key: Set("lorem ipsum".to_owned()),
private_key_hash: Set("lorem ipsum".to_owned()),
password_hash: Set("lorem ipsum".to_owned()),
private_key: Set("lorem ipsum".to_owned()),
code: Set("$argon2id$v=19$m=4096,t=3,p=1$V2M1eENXcnJvenhqTVF1Yw$xwriCZexpzF7Qtj9lwq0Sw".to_owned()),
personal_identification_number: Set("ADMIN".to_owned()),
created_at: Set(Local::now().naive_local()),
updated_at: Set(Local::now().naive_local()),
is_admin: Set(true),
..Default::default()
},
}
@ -32,7 +34,7 @@ impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
self.admin.to_owned().insert(db).await?;
self.candidate.to_owned().insert(db).await?;
Ok(())
}
@ -40,7 +42,7 @@ impl MigrationTrait for Migration {
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
self.admin.to_owned().delete(db).await?;
self.candidate.to_owned().delete(db).await?;
Ok(())
}