feat: adds user,session,auth,health-check-service

This commit is contained in:
itsscb 2024-08-13 08:20:11 +02:00
parent 6ffd94725a
commit 43ab8837ce
5 changed files with 557 additions and 4 deletions

254
src/auth-service/auth.rs Normal file
View File

@ -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<Mutex<dyn Users + Send + Sync>>,
sessions_service: Box<Mutex<dyn Sessions + Send + Sync>>,
}
impl AuthService {
pub fn new(
users_service: Box<Mutex<dyn Users + Send + Sync>>,
sessions_service: Box<Mutex<dyn Sessions + Send + Sync>>,
) -> Self {
Self {
users_service,
sessions_service,
}
}
}
#[tonic::async_trait]
impl Auth for AuthService {
async fn sign_in(
&self,
request: Request<SignInRequest>,
) -> Result<Response<SignInResponse>, 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<SignUpRequest>,
) -> Result<Response<SignUpResponse>, 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<SignOutRequest>,
) -> Result<Response<SignOutResponse>, 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());
}
}

View File

@ -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<dyn std::error::Error>> {
// 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<Mutex<dyn Users + Send + Sync + 'static>> = todo!(); // Create user service instance
let sessions_service: Box<Mutex<dyn Sessions + Send + Sync + 'static>> = 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(())
}

View File

@ -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<String, String>,
}
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);
}
}

152
src/auth-service/users.rs Normal file
View File

@ -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<String>;
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<String, User>,
username_to_user: HashMap<String, User>,
}
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<String> {
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);
}
}

View File

@ -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<dyn std::error::Error>> {
// 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<SignUpRequest> = Request::new(SignUpRequest {
username: username.clone(),
password: password.clone(),
}); // Create a new `SignUpRequest`.
let response: Response<SignUpResponse> = 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<SignInRequest> = Request::new(SignInRequest { username, password }); // Create a new `SignInRequest`.
// Make a sign in request. Propagate any errors. Convert Response<SignInResponse> 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<SignOutRequest> = Request::new(SignOutRequest {
session_token: response.session_token,
}); // Create a new `SignOutRequest`.
let response: Response<SignOutResponse> = 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;
}
}