feat: adds user,session,auth,health-check-service
This commit is contained in:
parent
6ffd94725a
commit
43ab8837ce
254
src/auth-service/auth.rs
Normal file
254
src/auth-service/auth.rs
Normal 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());
|
||||
}
|
||||
}
|
@ -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(())
|
||||
}
|
||||
|
53
src/auth-service/sessions.rs
Normal file
53
src/auth-service/sessions.rs
Normal 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
152
src/auth-service/users.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user