diff --git a/src/auth-service/auth.rs b/src/auth-service/auth.rs new file mode 100644 index 0000000..390e7be --- /dev/null +++ b/src/auth-service/auth.rs @@ -0,0 +1,254 @@ +use std::sync::Mutex; + +use crate::{sessions::Sessions, users::Users}; + +use tonic::{Request, Response, Status}; + +use authentication::auth_server::Auth; +use authentication::{ + SignInRequest, SignInResponse, SignOutRequest, SignOutResponse, SignUpRequest, SignUpResponse, + StatusCode, +}; + +pub mod authentication { + tonic::include_proto!("authentication"); +} + +// Re-exporting +pub use authentication::auth_server::AuthServer; +pub use tonic::transport::Server; + +pub struct AuthService { + users_service: Box>, + sessions_service: Box>, +} + +impl AuthService { + pub fn new( + users_service: Box>, + sessions_service: Box>, + ) -> Self { + Self { + users_service, + sessions_service, + } + } +} + +#[tonic::async_trait] +impl Auth for AuthService { + async fn sign_in( + &self, + request: Request, + ) -> Result, Status> { + println!("Got a request: {:?}", request); + + let req = request.into_inner(); + + // Get user's uuid from `users_service`. Panic if the lock is poisoned. + + // Match on `result`. If `result` is `None` return a SignInResponse with a the `status_code` set to `Failure` + // and `user_uuid`/`session_token` set to empty strings. + self.users_service + .lock() + .expect("failed to get lock on users_service") + .get_user_uuid(req.username, req.password) + .map_or_else( + || { + Ok(Response::new(SignInResponse { + status_code: StatusCode::Failure.into(), + user_uuid: String::new(), + session_token: String::new(), + })) + }, + |user_uuid| { + let session_token: String = self + .sessions_service + .lock() + .expect("failed to get lock on sessions_service") + .create_session(&user_uuid); + + Ok(Response::new(SignInResponse { + status_code: StatusCode::Success.into(), + user_uuid, + session_token, + })) + }, + ) + } + + async fn sign_up( + &self, + request: Request, + ) -> Result, Status> { + println!("Got a request: {:?}", request); + + let req = request.into_inner(); + + // let result: Result<(), String> = todo!(); // Create a new user through `users_service`. Panic if the lock is poisoned. + + self.users_service + .lock() + .expect("failed to get lock on users_service") + .create_user(req.username, req.password) + .map_or_else( + |_| { + Ok(Response::new(SignUpResponse { + status_code: StatusCode::Failure.into(), + })) + }, + |()| { + Ok(Response::new(SignUpResponse { + status_code: StatusCode::Success.into(), + })) + }, + ) + } + + async fn sign_out( + &self, + request: Request, + ) -> Result, Status> { + println!("Got a request: {:?}", request); + + let req = request.into_inner(); + + // TODO: Delete session using `sessions_service`. + self.sessions_service + .lock() + .expect("failed to get lock on sessions_service") + .delete_session(&req.session_token); + + let reply: SignOutResponse = SignOutResponse { + status_code: StatusCode::Success.into(), + }; // Create `SignOutResponse` with `status_code` set to `Success` + + Ok(Response::new(reply)) + } +} + +#[cfg(test)] +mod tests { + use crate::{sessions::SessionsImpl, users::UsersImpl}; + + use super::*; + + #[tokio::test] + async fn sign_in_should_fail_if_user_not_found() { + let users_service = Box::new(Mutex::new(UsersImpl::default())); + let sessions_service = Box::new(Mutex::new(SessionsImpl::default())); + + let auth_service = AuthService::new(users_service, sessions_service); + + let request = tonic::Request::new(SignInRequest { + username: "123456".to_owned(), + password: "654321".to_owned(), + }); + + let result = auth_service.sign_in(request).await.unwrap().into_inner(); + + assert_eq!(result.status_code, StatusCode::Failure.into()); + assert_eq!(result.user_uuid.is_empty(), true); + assert_eq!(result.session_token.is_empty(), true); + } + + #[tokio::test] + async fn sign_in_should_fail_if_incorrect_password() { + let mut users_service = UsersImpl::default(); + + let _ = users_service.create_user("123456".to_owned(), "654321".to_owned()); + + let users_service = Box::new(Mutex::new(users_service)); + let sessions_service = Box::new(Mutex::new(SessionsImpl::default())); + + let auth_service = AuthService::new(users_service, sessions_service); + + let request = tonic::Request::new(SignInRequest { + username: "123456".to_owned(), + password: "wrong password".to_owned(), + }); + + let result = auth_service.sign_in(request).await.unwrap().into_inner(); + + assert_eq!(result.status_code, StatusCode::Failure.into()); + assert_eq!(result.user_uuid.is_empty(), true); + assert_eq!(result.session_token.is_empty(), true); + } + + #[tokio::test] + async fn sign_in_should_succeed() { + let mut users_service = UsersImpl::default(); + + let _ = users_service.create_user("123456".to_owned(), "654321".to_owned()); + + let users_service = Box::new(Mutex::new(users_service)); + let sessions_service = Box::new(Mutex::new(SessionsImpl::default())); + + let auth_service = AuthService::new(users_service, sessions_service); + + let request = tonic::Request::new(SignInRequest { + username: "123456".to_owned(), + password: "654321".to_owned(), + }); + + let result = auth_service.sign_in(request).await.unwrap().into_inner(); + + assert_eq!(result.status_code, StatusCode::Success.into()); + assert_eq!(result.user_uuid.is_empty(), false); + assert_eq!(result.session_token.is_empty(), false); + } + + #[tokio::test] + async fn sign_up_should_fail_if_username_exists() { + let mut users_service = UsersImpl::default(); + + let _ = users_service.create_user("123456".to_owned(), "654321".to_owned()); + + let users_service = Box::new(Mutex::new(users_service)); + let sessions_service = Box::new(Mutex::new(SessionsImpl::default())); + + let auth_service = AuthService::new(users_service, sessions_service); + + let request = tonic::Request::new(SignUpRequest { + username: "123456".to_owned(), + password: "654321".to_owned(), + }); + + let result = auth_service.sign_up(request).await.unwrap(); + + assert_eq!(result.into_inner().status_code, StatusCode::Failure.into()); + } + + #[tokio::test] + async fn sign_up_should_succeed() { + let users_service = Box::new(Mutex::new(UsersImpl::default())); + let sessions_service = Box::new(Mutex::new(SessionsImpl::default())); + + let auth_service = AuthService::new(users_service, sessions_service); + + let request = tonic::Request::new(SignUpRequest { + username: "123456".to_owned(), + password: "654321".to_owned(), + }); + + let result = auth_service.sign_up(request).await.unwrap(); + + assert_eq!(result.into_inner().status_code, StatusCode::Success.into()); + } + + #[tokio::test] + async fn sign_out_should_succeed() { + let users_service = Box::new(Mutex::new(UsersImpl::default())); + let sessions_service = Box::new(Mutex::new(SessionsImpl::default())); + + let auth_service = AuthService::new(users_service, sessions_service); + + let request = tonic::Request::new(SignOutRequest { + session_token: "".to_owned(), + }); + + let result = auth_service.sign_out(request).await.unwrap(); + + assert_eq!(result.into_inner().status_code, StatusCode::Success.into()); + } +} diff --git a/src/auth-service/main.rs b/src/auth-service/main.rs index 28d5022..e2f362a 100644 --- a/src/auth-service/main.rs +++ b/src/auth-service/main.rs @@ -1,3 +1,30 @@ -fn main() { - println!("auth service"); +use std::sync::Mutex; + +mod auth; +mod sessions; +mod users; + +use auth::*; +use sessions::{SessionsImpl, Sessions}; +use users::{UsersImpl, Users}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Here we are using ip 0.0.0.0 so the service is listening on all the configured network interfaces. This is needed for Docker to work, which we will add later on. + // See: https://stackoverflow.com/questions/39525820/docker-port-forwarding-not-working + // Port 50051 is the recommended gRPC port. + let addr = "[::0]:50051".parse()?; + + let users_service: Box> = todo!(); // Create user service instance + let sessions_service: Box> = todo!(); //Create session service instance + + let auth_service = AuthService::new(users_service, sessions_service); + + // Instantiate gRPC server + Server::builder() + .add_service(AuthServer::new(auth_service)) + .serve(addr) + .await?; + + Ok(()) } diff --git a/src/auth-service/sessions.rs b/src/auth-service/sessions.rs new file mode 100644 index 0000000..699d99f --- /dev/null +++ b/src/auth-service/sessions.rs @@ -0,0 +1,53 @@ +use std::collections::HashMap; + +use uuid::Uuid; + +pub trait Sessions { + fn create_session(&mut self, user_uuid: &str) -> String; + fn delete_session(&mut self, user_uuid: &str); +} + +#[derive(Default)] +pub struct SessionsImpl { + uuid_to_session: HashMap, +} + +impl Sessions for SessionsImpl { + fn create_session(&mut self, user_uuid: &str) -> String { + let session: String = Uuid::new_v4().to_string(); // Create a new session using Uuid::new_v4(). + + self.uuid_to_session + .insert(user_uuid.to_owned(), session.clone()); + + session + } + + fn delete_session(&mut self, user_uuid: &str) { + self.uuid_to_session.remove(user_uuid); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_create_session() { + let mut session_service = SessionsImpl::default(); + assert_eq!(session_service.uuid_to_session.len(), 0); + let session = session_service.create_session("123456"); + assert_eq!(session_service.uuid_to_session.len(), 1); + assert_eq!( + session_service.uuid_to_session.get("123456").unwrap(), + &session + ); + } + + #[test] + fn should_delete_session() { + let mut session_service = SessionsImpl::default(); + session_service.create_session("123456"); + session_service.delete_session("123456"); + assert_eq!(session_service.uuid_to_session.len(), 0); + } +} diff --git a/src/auth-service/users.rs b/src/auth-service/users.rs new file mode 100644 index 0000000..7e2c09c --- /dev/null +++ b/src/auth-service/users.rs @@ -0,0 +1,152 @@ +use pbkdf2::{ + password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Pbkdf2, +}; +use rand_core::OsRng; +use uuid::Uuid; + +use std::collections::HashMap; + +pub trait Users { + fn create_user(&mut self, username: String, password: String) -> Result<(), String>; + fn get_user_uuid(&self, username: String, password: String) -> Option; + fn delete_user(&mut self, user_uuid: String); +} + +#[derive(Clone)] +pub struct User { + user_uuid: String, + username: String, + password: String, +} + +impl User { + pub fn new(username: String, hashed_password: String) -> Self { + let user_uuid = Uuid::new_v4().to_string(); + Self { + user_uuid, + username, + password: hashed_password, + } + } +} + +#[derive(Default)] +pub struct UsersImpl { + uuid_to_user: HashMap, + username_to_user: HashMap, +} + +impl Users for UsersImpl { + fn create_user(&mut self, username: String, password: String) -> Result<(), String> { + if self.username_to_user.contains_key(&username) { + static USER_ALREADY_EXISTS: &str = "username is already taken"; + return Err(USER_ALREADY_EXISTS.to_owned()); + } + + let salt = SaltString::generate(&mut OsRng); + + let hashed_password = Pbkdf2 + .hash_password(password.as_bytes(), &salt) + .map_err(|e| format!("Failed to hash password.\n{e:?}"))? + .to_string(); + + let user: User = User::new(username, hashed_password); // Create new user with unique uuid and hashed password. + + self.uuid_to_user + .insert(user.user_uuid.clone(), user.clone()); + self.username_to_user.insert(user.username.clone(), user); + + Ok(()) + } + + fn get_user_uuid(&self, username: String, password: String) -> Option { + let user: &User = self.username_to_user.get(&username)?; // Retrieve `User` or return `None` is user can't be found. + + // Get user's password as `PasswordHash` instance. + let hashed_password = user.password.clone(); + let parsed_hash = PasswordHash::new(&hashed_password).ok()?; + + // Verify passed in password matches user's password. + Pbkdf2 + .verify_password(password.as_bytes(), &parsed_hash) + .map_or(None, |_| Some(user.user_uuid.clone())) + } + + fn delete_user(&mut self, user_uuid: String) { + // TODO: Remove user from `username_to_user` and `uuid_to_user`. + if let Some(user) = self.uuid_to_user.get(&user_uuid) { + self.username_to_user.remove(&user.username); + self.uuid_to_user.remove(&user_uuid); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_create_user() { + let mut user_service = UsersImpl::default(); + user_service + .create_user("username".to_owned(), "password".to_owned()) + .expect("should create user"); + + assert_eq!(user_service.uuid_to_user.len(), 1); + assert_eq!(user_service.username_to_user.len(), 1); + } + + #[test] + fn should_fail_creating_user_with_existing_username() { + let mut user_service = UsersImpl::default(); + user_service + .create_user("username".to_owned(), "password".to_owned()) + .expect("should create user"); + + let result = user_service.create_user("username".to_owned(), "password".to_owned()); + + assert!(result.is_err()); + } + + #[test] + fn should_retrieve_user_uuid() { + let mut user_service = UsersImpl::default(); + user_service + .create_user("username".to_owned(), "password".to_owned()) + .expect("should create user"); + + assert!(user_service + .get_user_uuid("username".to_owned(), "password".to_owned()) + .is_some()); + } + + #[test] + fn should_fail_to_retrieve_user_uuid_with_incorrect_password() { + let mut user_service = UsersImpl::default(); + user_service + .create_user("username".to_owned(), "password".to_owned()) + .expect("should create user"); + + assert!(user_service + .get_user_uuid("username".to_owned(), "incorrect password".to_owned()) + .is_none()); + } + + #[test] + fn should_delete_user() { + let mut user_service = UsersImpl::default(); + user_service + .create_user("username".to_owned(), "password".to_owned()) + .expect("should create user"); + + let user_uuid = user_service + .get_user_uuid("username".to_owned(), "password".to_owned()) + .unwrap(); + + user_service.delete_user(user_uuid); + + assert_eq!(user_service.uuid_to_user.len(), 0); + assert_eq!(user_service.username_to_user.len(), 0); + } +} diff --git a/src/health-check-service/main.rs b/src/health-check-service/main.rs index b1ee2c3..8d26482 100644 --- a/src/health-check-service/main.rs +++ b/src/health-check-service/main.rs @@ -1,3 +1,70 @@ -fn main() { - println!("health check service"); +use std::env; + +use authentication::auth_client::AuthClient; +use authentication::{SignInRequest, SignOutRequest, SignUpRequest}; +use tokio::time::{sleep, Duration}; +use tonic::{Request, Response}; +use uuid::Uuid; + +use crate::authentication::{SignInResponse, SignOutResponse, SignUpResponse, StatusCode}; + +pub mod authentication { + tonic::include_proto!("authentication"); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // AUTH_SERVICE_HOST_NAME will be set to 'auth' when running the health check service in Docker + // ::0 is required for Docker to work: https://stackoverflow.com/questions/59179831/docker-app-server-ip-address-127-0-0-1-difference-of-0-0-0-0-ip + let auth_hostname = env::var("AUTH_SERVICE_HOST_NAME").unwrap_or("[::0]".to_owned()); + + // Establish connection when auth service + let mut client = AuthClient::connect(format!("http://{}:50051", auth_hostname)).await?; + + loop { + let username: String = Uuid::new_v4().to_string(); // Create random username using new_v4() + let password: String = Uuid::new_v4().to_string(); // Create random password using new_v4() + + let request: Request = Request::new(SignUpRequest { + username: username.clone(), + password: password.clone(), + }); // Create a new `SignUpRequest`. + + let response: Response = client.sign_up(request).await?; // Make a sign up request. Propagate any errors. + + // Log the response + println!( + "SIGN UP RESPONSE STATUS: {:?}", + StatusCode::from_i32(response.into_inner().status_code) + ); + + // --------------------------------------------- + + let request: Request = Request::new(SignInRequest { username, password }); // Create a new `SignInRequest`. + + // Make a sign in request. Propagate any errors. Convert Response into SignInResponse. + let response: SignInResponse = client.sign_in(request).await?.into_inner(); + + println!( + "SIGN IN RESPONSE STATUS: {:?}", + response.status_code // Log response status_code + ); + + // --------------------------------------------- + + let request: Request = Request::new(SignOutRequest { + session_token: response.session_token, + }); // Create a new `SignOutRequest`. + + let response: Response = client.sign_out(request).await?; // Make a sign out request. Propagate any errors. + + println!( + "SIGN OUT RESPONSE STATUS: {:?}", + response.into_inner().status_code // Log response status_code + ); + + println!("--------------------------------------",); + + sleep(Duration::from_secs(3)).await; + } }