From c8b14c1f6134d58af529a46d88f54e623b08da7b Mon Sep 17 00:00:00 2001 From: Viktor Platz Date: Mon, 4 Dec 2023 12:16:07 +0100 Subject: [PATCH 01/41] protobuf --- Ecdar-ProtoBuf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ecdar-ProtoBuf b/Ecdar-ProtoBuf index 6600b15..506b667 160000 --- a/Ecdar-ProtoBuf +++ b/Ecdar-ProtoBuf @@ -1 +1 @@ -Subproject commit 6600b155ef58ca57187c28d7f570bf59bc9e4f6b +Subproject commit 506b667d244223784c5e04b0721d40596c5c5ef4 From d1d7a05b0bf61167b0c3fac6f6bd800878807560 Mon Sep 17 00:00:00 2001 From: sabotack Date: Mon, 4 Dec 2023 13:09:20 +0100 Subject: [PATCH 02/41] Add role check to create_access and fix tests --- src/api/ecdar_api.rs | 23 ++++++++ src/tests/api/access_logic.rs | 105 +++++++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 2 deletions(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index fcc1c6a..b940572 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -480,6 +480,29 @@ impl EcdarApi for ConcreteEcdarApi { &self, request: Request, ) -> Result, Status> { + let message = request.get_ref().clone(); + let uid = request + .uid() + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + // Check if user has access to model with role 'Editor' + let access = self + .contexts + .access_context + .get_access_by_uid_and_model_id(uid, message.model_id) + .await + .map_err(|err| Status::new(Code::Internal, err.to_string()))? + .ok_or_else(|| { + Status::new(Code::PermissionDenied, "User does not have access to model") + })?; + + if access.role != "Editor" { + return Err(Status::new( + Code::PermissionDenied, + "You do not have permission to create access for this model", + )); + } + let access = request.get_ref(); let access = access::Model { diff --git a/src/tests/api/access_logic.rs b/src/tests/api/access_logic.rs index 4515825..d9ffacc 100644 --- a/src/tests/api/access_logic.rs +++ b/src/tests/api/access_logic.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use crate::api::server::server::ecdar_api_server::EcdarApi; use crate::api::server::server::{CreateAccessRequest, DeleteAccessRequest, UpdateAccessRequest}; use crate::entities::access; @@ -6,6 +8,69 @@ use mockall::predicate; use sea_orm::DbErr; use tonic::{Code, Request}; +#[tokio::test] +async fn create_incorrect_role_returns_err() { + let mut mock_services = get_mock_services(); + + mock_services + .access_context_mock + .expect_get_access_by_uid_and_model_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: Default::default(), + role: "Viewer".to_owned(), + user_id: 1, + model_id: 1, + })) + }); + + let mut request = Request::new(CreateAccessRequest { + role: "Viewer".to_string(), + model_id: 1, + user_id: 1, + }); + + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + + let api = get_mock_concrete_ecdar_api(mock_services); + + let res = api.create_access(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); +} + +#[tokio::test] +async fn create_no_access_returns_err() { + let mut mock_services = get_mock_services(); + + mock_services + .access_context_mock + .expect_get_access_by_uid_and_model_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| Ok(None)); + + let mut request = Request::new(CreateAccessRequest { + role: "Editor".to_string(), + model_id: 1, + user_id: 1, + }); + + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + + let api = get_mock_concrete_ecdar_api(mock_services); + + let res = api.create_access(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); +} + #[tokio::test] async fn create_invalid_access_returns_err() { let mut mock_services = get_mock_services(); @@ -23,12 +88,30 @@ async fn create_invalid_access_returns_err() { .with(predicate::eq(access.clone())) .returning(move |_| Err(DbErr::RecordNotInserted)); - let request = Request::new(CreateAccessRequest { + mock_services + .access_context_mock + .expect_get_access_by_uid_and_model_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: Default::default(), + role: "Editor".to_owned(), + user_id: 1, + model_id: 1, + })) + }); + + let mut request = Request::new(CreateAccessRequest { role: "Editor".to_string(), model_id: 1, user_id: 1, }); + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + let api = get_mock_concrete_ecdar_api(mock_services); let res = api.create_access(request).await.unwrap_err(); @@ -47,18 +130,36 @@ async fn create_access_returns_ok() { user_id: 1, }; + mock_services + .access_context_mock + .expect_get_access_by_uid_and_model_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: Default::default(), + role: "Editor".to_owned(), + user_id: 1, + model_id: 1, + })) + }); + mock_services .access_context_mock .expect_create() .with(predicate::eq(access.clone())) .returning(move |_| Ok(access.clone())); - let request = Request::new(CreateAccessRequest { + let mut request = Request::new(CreateAccessRequest { role: "Editor".to_string(), model_id: 1, user_id: 1, }); + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + let api = get_mock_concrete_ecdar_api(mock_services); let res = api.create_access(request).await; From 08719c3ed98fe790a3a5e4eda6dbb9db3b3bc142 Mon Sep 17 00:00:00 2001 From: sabotack Date: Mon, 4 Dec 2023 13:11:29 +0100 Subject: [PATCH 03/41] Move comment --- src/api/ecdar_api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index b940572..9f781f9 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -485,7 +485,6 @@ impl EcdarApi for ConcreteEcdarApi { .uid() .ok_or(Status::internal("Could not get uid from request metadata"))?; - // Check if user has access to model with role 'Editor' let access = self .contexts .access_context @@ -496,6 +495,7 @@ impl EcdarApi for ConcreteEcdarApi { Status::new(Code::PermissionDenied, "User does not have access to model") })?; + // Check if user has access to model with role 'Editor' if access.role != "Editor" { return Err(Status::new( Code::PermissionDenied, From df0b9c857fb20558056ebbe3f2c234ed90352b81 Mon Sep 17 00:00:00 2001 From: Viktor Platz Date: Tue, 5 Dec 2023 09:01:18 +0100 Subject: [PATCH 04/41] add list access info endpoint --- src/api/ecdar_api.rs | 51 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index fcc1c6a..59d552e 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -8,7 +8,7 @@ use super::server::server::{ GetAuthTokenRequest, GetAuthTokenResponse, GetModelRequest, GetModelResponse, ListModelsInfoResponse, Query, QueryRequest, QueryResponse, SimulationStartRequest, SimulationStepRequest, SimulationStepResponse, UpdateAccessRequest, UpdateModelRequest, - UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, + UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, ListAccessInfoRequest, ListAccessInfoResponse, }; use crate::api::context_collection::ContextCollection; use crate::api::{ @@ -212,6 +212,55 @@ impl EcdarApi for ConcreteEcdarApi { })) } + async fn list_access_info( + &self, + request: Request, + ) -> Result, Status> { + let message = request.get_ref().clone(); + + let uid = request + .uid() + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + match self + .contexts + .access_context + .get_access_by_uid_and_model_id(uid, message.model_id) + .await + { + Ok(access) => { + if access.is_none() { + return Err(Status::new( + Code::PermissionDenied, + "User does not have access to model", + )); + } + } + Err(error) => return Err(Status::new(Code::Internal, error.to_string())), + }; + + match self + .contexts + .access_context + .get_access_by_model_id(message.model_id) + .await + { + Ok(access_info_list) => { + if access_info_list.is_empty() { + return Err(Status::new( + Code::NotFound, + "No access found for given user", + )); + } else { + Ok(Response::new(ListAccessInfoResponse { access_info_list })) + } + } + Err(error) => Err(Status::new(Code::Internal, error.to_string())), + } + + + } + async fn create_model( &self, request: Request, From 78ddaec0f5447e15a8233229690408876b8cb1ad Mon Sep 17 00:00:00 2001 From: Viktor Platz Date: Tue, 5 Dec 2023 09:01:51 +0100 Subject: [PATCH 05/41] add FromQueryResult so database can convert --- src/build.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/build.rs b/src/build.rs index bd831e4..e1df5da 100644 --- a/src/build.rs +++ b/src/build.rs @@ -9,6 +9,7 @@ fn main() { "#[derive(serde::Serialize, serde::Deserialize)]", ) .type_attribute("ModelInfo", "#[derive(sea_orm::FromQueryResult)]") + .type_attribute("AccessInfo", "#[derive(sea_orm::FromQueryResult)]") .enum_attribute("rep", "#[derive(serde::Serialize, serde::Deserialize)]") .compile(&["Ecdar-ProtoBuf/services.proto"], &["Ecdar-ProtoBuf/"]) .unwrap(); From cde61b05433bd2e6687985ebd62cb86285dec1e7 Mon Sep 17 00:00:00 2001 From: Viktor Platz Date: Tue, 5 Dec 2023 09:04:12 +0100 Subject: [PATCH 06/41] list access info endpoints tests --- src/tests/api/access_logic.rs | 102 +++++++++++++++++++++++++++++++++- src/tests/api/helpers.rs | 12 ++++ 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/tests/api/access_logic.rs b/src/tests/api/access_logic.rs index 4515825..7ea2238 100644 --- a/src/tests/api/access_logic.rs +++ b/src/tests/api/access_logic.rs @@ -1,10 +1,12 @@ use crate::api::server::server::ecdar_api_server::EcdarApi; -use crate::api::server::server::{CreateAccessRequest, DeleteAccessRequest, UpdateAccessRequest}; +use crate::api::server::server::{CreateAccessRequest, DeleteAccessRequest, UpdateAccessRequest, AccessInfo, ListAccessInfoRequest}; use crate::entities::access; use crate::tests::api::helpers::{get_mock_concrete_ecdar_api, get_mock_services}; use mockall::predicate; use sea_orm::DbErr; -use tonic::{Code, Request}; +use tonic::{metadata, Code, Request}; +use std::str::FromStr; + #[tokio::test] async fn create_invalid_access_returns_err() { @@ -168,3 +170,99 @@ async fn delete_access_returns_ok() { assert!(res.is_ok()); } + +#[tokio::test] +async fn list_access_info_returns_ok() { + let mut mock_services = get_mock_services(); + + let mut request: Request = Request::new(ListAccessInfoRequest { model_id: 1 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let access = AccessInfo { + id: 1, + role: "Editor".to_string(), + model_id: 1, + user_id: 1, + }; + + mock_services + .access_context_mock + .expect_get_access_by_uid_and_model_id() + .returning(move |_,_| Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + model_id: Default::default(), + user_id: Default::default(), + }))); + + mock_services + .access_context_mock + .expect_get_access_by_model_id() + .returning(move |_| Ok(vec![access.clone()])); + + let api = get_mock_concrete_ecdar_api(mock_services); + + let res = api.list_access_info(request).await; + + assert!(res.is_ok()); +} + +#[tokio::test] +async fn list_access_info_returns_not_found() { + let mut mock_services = get_mock_services(); + + let mut request = Request::new(ListAccessInfoRequest { model_id: 1 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + + let access = access::Model { + id: 1, + role: "Editor".to_string(), + model_id: 1, + user_id: 1, + }; + + mock_services + .access_context_mock + .expect_get_access_by_model_id() + .returning(move |_| Ok(vec![])); + + mock_services + .access_context_mock + .expect_get_access_by_uid_and_model_id() + .returning(move |_, _| Ok(Some(access.clone()))); + + let api = get_mock_concrete_ecdar_api(mock_services); + + let res = api.list_access_info(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::NotFound); +} + +#[tokio::test] +async fn list_access_info_returns_no_permission() { + let mut request = Request::new(ListAccessInfoRequest { model_id: 1 }); + + request + .metadata_mut() + .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); + + let mut mock_services = get_mock_services(); + + mock_services + .access_context_mock + .expect_get_access_by_uid_and_model_id() + .returning(move |_,_| Ok(None)); + + let api = get_mock_concrete_ecdar_api(mock_services); + + let res = api.list_access_info(request).await.unwrap_err(); + + assert_eq!(res.code(), Code::PermissionDenied); +} \ No newline at end of file diff --git a/src/tests/api/helpers.rs b/src/tests/api/helpers.rs index 889d139..34cef3d 100644 --- a/src/tests/api/helpers.rs +++ b/src/tests/api/helpers.rs @@ -23,6 +23,7 @@ use mockall::mock; use sea_orm::DbErr; use std::sync::Arc; use tonic::{Request, Response, Status}; +use crate::api::server::server::AccessInfo; pub fn get_mock_concrete_ecdar_api(mock_services: MockServices) -> ConcreteEcdarApi { let contexts = ContextCollection { @@ -88,6 +89,17 @@ mock! { .one(&self.db_context.get_connection()) .await } + + async fn get_access_by_model_id( + &self, + model_id: i32, + ) -> Result, DbErr> { + access::Entity::find() + .filter(access::Column::ModelId.eq(model_id)) + .into_model::() + .all(&self.db_context.get_connection()) + .await + } } } From 10bcc620e68fd01c6220e0f53bff1c36f90ff63f Mon Sep 17 00:00:00 2001 From: Viktor Platz Date: Tue, 5 Dec 2023 09:05:14 +0100 Subject: [PATCH 07/41] get_access_by_model_id crud --- src/database/access_context.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/database/access_context.rs b/src/database/access_context.rs index a1bad6b..466ee17 100644 --- a/src/database/access_context.rs +++ b/src/database/access_context.rs @@ -1,3 +1,4 @@ +use crate::api::server::server::AccessInfo; use crate::database::database_context::DatabaseContextTrait; use crate::database::entity_context::EntityContextTrait; use crate::entities::access; @@ -17,6 +18,11 @@ pub trait AccessContextTrait: EntityContextTrait { uid: i32, model_id: i32, ) -> Result, DbErr>; + + async fn get_access_by_model_id( + &self, + model_id: i32, + ) -> Result, DbErr>; } #[async_trait] @@ -35,6 +41,18 @@ impl AccessContextTrait for AccessContext { .one(&self.db_context.get_connection()) .await } + + async fn get_access_by_model_id( + &self, + model_id: i32, + ) -> Result, DbErr> { + access::Entity::find() + .filter(access::Column::ModelId.eq(model_id)) + .into_model::() + .all(&self.db_context.get_connection()) + .await + } + } impl AccessContext { From 2832c6c663122c98b3eb1b706b314b54b3ae4b38 Mon Sep 17 00:00:00 2001 From: Viktor Platz Date: Tue, 5 Dec 2023 09:06:07 +0100 Subject: [PATCH 08/41] get_access_by_model_id crud tests --- src/tests/database/access_context.rs | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/tests/database/access_context.rs b/src/tests/database/access_context.rs index cc2c13c..0e29d9c 100644 --- a/src/tests/database/access_context.rs +++ b/src/tests/database/access_context.rs @@ -8,6 +8,7 @@ use crate::{ to_active_models, }; use sea_orm::{entity::prelude::*, IntoActiveModel}; +use crate::api::server::server::AccessInfo; async fn seed_db() -> (AccessContext, access::Model, user::Model, model::Model) { let db_context = get_reset_database_context().await; @@ -360,3 +361,33 @@ async fn get_by_uid_and_model_id_test() { assert!(access.unwrap().unwrap() == expected_access); } + +#[tokio::test] +async fn get_access_by_model_id_test_returns_ok() { + let (access_context, expected_access, _, model) = seed_db().await; + + let expected_access_access_info_vector = vec![AccessInfo { + id: expected_access.id.clone(), + model_id: expected_access.model_id.clone(), + user_id: expected_access.user_id.clone(), + role: expected_access.role.clone(), + }]; + + access::Entity::insert(expected_access.clone().into_active_model()) + .exec(&access_context.db_context.get_connection()) + .await + .unwrap(); + + let access = access_context.get_access_by_model_id(model.id).await; + + assert!(access.unwrap() == expected_access_access_info_vector); +} + +#[tokio::test] +async fn get_access_by_model_id_test_returns_empty() { + let (access_context, _, _, model) = seed_db().await; + + let access = access_context.get_access_by_model_id(model.id).await; + + assert!(access.unwrap().is_empty()); +} \ No newline at end of file From 77f9db1aa186b607dc31465ed08541d9214010e6 Mon Sep 17 00:00:00 2001 From: Viktor Platz Date: Tue, 5 Dec 2023 09:09:35 +0100 Subject: [PATCH 09/41] protobufffffffffffffffffff --- Ecdar-ProtoBuf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ecdar-ProtoBuf b/Ecdar-ProtoBuf index 506b667..02e1d60 160000 --- a/Ecdar-ProtoBuf +++ b/Ecdar-ProtoBuf @@ -1 +1 @@ -Subproject commit 506b667d244223784c5e04b0721d40596c5c5ef4 +Subproject commit 02e1d6013ad8bcaba14359df4548119a53a5b83f From 3bbfba36208c572dc5f0d69f267699b926710e32 Mon Sep 17 00:00:00 2001 From: Viktor Platz Date: Tue, 5 Dec 2023 09:12:33 +0100 Subject: [PATCH 10/41] clippy --- src/tests/database/access_context.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/database/access_context.rs b/src/tests/database/access_context.rs index 0e29d9c..3ca1ff4 100644 --- a/src/tests/database/access_context.rs +++ b/src/tests/database/access_context.rs @@ -367,9 +367,9 @@ async fn get_access_by_model_id_test_returns_ok() { let (access_context, expected_access, _, model) = seed_db().await; let expected_access_access_info_vector = vec![AccessInfo { - id: expected_access.id.clone(), - model_id: expected_access.model_id.clone(), - user_id: expected_access.user_id.clone(), + id: expected_access.id, + model_id: expected_access.model_id, + user_id: expected_access.user_id, role: expected_access.role.clone(), }]; From e77f78d6880b4afdb7418a40199eb25df7a56691 Mon Sep 17 00:00:00 2001 From: sabotack Date: Tue, 5 Dec 2023 09:13:28 +0100 Subject: [PATCH 11/41] Implement role checking in create_access --- Ecdar-ProtoBuf | 2 +- src/api/ecdar_api.rs | 75 ++++++++++++++++++++++++++++------- src/tests/api/access_logic.rs | 39 +++++++++++++++--- 3 files changed, 94 insertions(+), 22 deletions(-) diff --git a/Ecdar-ProtoBuf b/Ecdar-ProtoBuf index 6600b15..1cef561 160000 --- a/Ecdar-ProtoBuf +++ b/Ecdar-ProtoBuf @@ -1 +1 @@ -Subproject commit 6600b155ef58ca57187c28d7f570bf59bc9e4f6b +Subproject commit 1cef56103282546bc9fd3425906455603e6bec5f diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index 9f781f9..fd238cc 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -1,4 +1,5 @@ use super::server::server::{ + create_access_request::User, ecdar_api_auth_server::EcdarApiAuth, ecdar_api_server::EcdarApi, ecdar_backend_server::EcdarBackend, @@ -6,9 +7,10 @@ use super::server::server::{ CreateAccessRequest, CreateModelRequest, CreateModelResponse, CreateQueryRequest, CreateUserRequest, DeleteAccessRequest, DeleteModelRequest, DeleteQueryRequest, GetAuthTokenRequest, GetAuthTokenResponse, GetModelRequest, GetModelResponse, - ListModelsInfoResponse, Query, QueryRequest, QueryResponse, SimulationStartRequest, - SimulationStepRequest, SimulationStepResponse, UpdateAccessRequest, UpdateModelRequest, - UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, + ListAccessInfoRequest, ListAccessInfoResponse, ListModelsInfoResponse, Query, QueryRequest, + QueryResponse, SimulationStartRequest, SimulationStepRequest, SimulationStepResponse, + UpdateAccessRequest, UpdateModelRequest, UpdateQueryRequest, UpdateUserRequest, + UserTokenResponse, }; use crate::api::context_collection::ContextCollection; use crate::api::{ @@ -102,6 +104,13 @@ impl ConcreteEcdarApi { #[tonic::async_trait] impl EcdarApi for ConcreteEcdarApi { + async fn list_access_info( + &self, + _request: Request, + ) -> Result, Status> { + todo!() + } + /// Gets a Model and its queries from the database. /// /// If the Model is not in use, it will now be in use by the requestees session, @@ -481,10 +490,12 @@ impl EcdarApi for ConcreteEcdarApi { request: Request, ) -> Result, Status> { let message = request.get_ref().clone(); + let uid = request .uid() .ok_or(Status::internal("Could not get uid from request metadata"))?; + // Check if user (the requester) has access to model let access = self .contexts .access_context @@ -495,7 +506,7 @@ impl EcdarApi for ConcreteEcdarApi { Status::new(Code::PermissionDenied, "User does not have access to model") })?; - // Check if user has access to model with role 'Editor' + // Check if user (the requester) has role 'Editor' if access.role != "Editor" { return Err(Status::new( Code::PermissionDenied, @@ -503,18 +514,27 @@ impl EcdarApi for ConcreteEcdarApi { )); } - let access = request.get_ref(); + if let Some(user) = message.user { + let user_from_db = + create_access_find_user_helper(Arc::clone(&self.contexts.user_context), user) + .await?; - let access = access::Model { - id: Default::default(), - role: access.role.to_string(), - model_id: access.model_id, - user_id: access.user_id, - }; - - match self.contexts.access_context.create(access).await { - Ok(_) => Ok(Response::new(())), - Err(error) => Err(Status::new(Code::Internal, error.to_string())), + let access = access::Model { + id: Default::default(), + role: message.role.to_string(), + model_id: message.model_id, + user_id: user_from_db.id, + }; + + match self.contexts.access_context.create(access).await { + Ok(_) => Ok(Response::new(())), + Err(error) => Err(Status::new(Code::Internal, error.to_string())), + } + } else { + Err(Status::new( + Code::InvalidArgument, + "No user identification provided", + )) } } @@ -726,6 +746,31 @@ impl EcdarApi for ConcreteEcdarApi { } } +async fn create_access_find_user_helper( + user_context: Arc, + user: User, +) -> Result { + match user { + User::UserId(user_id) => Ok(user_context + .get_by_id(user_id) + .await + .map_err(|err| Status::new(Code::Internal, err.to_string()))? + .ok_or_else(|| Status::new(Code::NotFound, "No user found with given id"))?), + + User::Username(username) => Ok(user_context + .get_by_username(username) + .await + .map_err(|err| Status::new(Code::Internal, err.to_string()))? + .ok_or_else(|| Status::new(Code::NotFound, "No user found with given username"))?), + + User::Email(email) => Ok(user_context + .get_by_email(email) + .await + .map_err(|err| Status::new(Code::Internal, err.to_string()))? + .ok_or_else(|| Status::new(Code::NotFound, "No user found with given email"))?), + } +} + async fn get_auth_find_user_helper( user_context: Arc, user_credentials: UserCredentials, diff --git a/src/tests/api/access_logic.rs b/src/tests/api/access_logic.rs index d9ffacc..94ac029 100644 --- a/src/tests/api/access_logic.rs +++ b/src/tests/api/access_logic.rs @@ -1,8 +1,9 @@ use std::str::FromStr; +use crate::api::server::server::create_access_request::User; use crate::api::server::server::ecdar_api_server::EcdarApi; use crate::api::server::server::{CreateAccessRequest, DeleteAccessRequest, UpdateAccessRequest}; -use crate::entities::access; +use crate::entities::{access, user}; use crate::tests::api::helpers::{get_mock_concrete_ecdar_api, get_mock_services}; use mockall::predicate; use sea_orm::DbErr; @@ -28,7 +29,7 @@ async fn create_incorrect_role_returns_err() { let mut request = Request::new(CreateAccessRequest { role: "Viewer".to_string(), model_id: 1, - user_id: 1, + user: Default::default(), }); request.metadata_mut().insert( @@ -56,7 +57,7 @@ async fn create_no_access_returns_err() { let mut request = Request::new(CreateAccessRequest { role: "Editor".to_string(), model_id: 1, - user_id: 1, + user: Default::default(), }); request.metadata_mut().insert( @@ -101,10 +102,23 @@ async fn create_invalid_access_returns_err() { })) }); + mock_services + .user_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(user::Model { + id: 1, + email: Default::default(), + username: "test".to_string(), + password: "test".to_string(), + })) + }); + let mut request = Request::new(CreateAccessRequest { role: "Editor".to_string(), model_id: 1, - user_id: 1, + user: Some(User::UserId(1)), }); request.metadata_mut().insert( @@ -137,7 +151,7 @@ async fn create_access_returns_ok() { .returning(move |_, _| { Ok(Some(access::Model { id: Default::default(), - role: "Editor".to_owned(), + role: "Editor".to_string(), user_id: 1, model_id: 1, })) @@ -149,10 +163,23 @@ async fn create_access_returns_ok() { .with(predicate::eq(access.clone())) .returning(move |_| Ok(access.clone())); + mock_services + .user_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(user::Model { + id: 1, + email: Default::default(), + username: "test".to_string(), + password: "test".to_string(), + })) + }); + let mut request = Request::new(CreateAccessRequest { role: "Editor".to_string(), model_id: 1, - user_id: 1, + user: Some(User::UserId(1)), }); request.metadata_mut().insert( From 1a577f12ee3caff5842914d396c26204cc07bbf6 Mon Sep 17 00:00:00 2001 From: Viktor Platz Date: Tue, 5 Dec 2023 09:16:17 +0100 Subject: [PATCH 12/41] protobuf stuff --- Ecdar-ProtoBuf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ecdar-ProtoBuf b/Ecdar-ProtoBuf index 02e1d60..3d32735 160000 --- a/Ecdar-ProtoBuf +++ b/Ecdar-ProtoBuf @@ -1 +1 @@ -Subproject commit 02e1d6013ad8bcaba14359df4548119a53a5b83f +Subproject commit 3d3273559cdda9efc57455321f90e5fcdb6add91 From f6a4b7d1514af538ce59bf65e656c405f36a3704 Mon Sep 17 00:00:00 2001 From: sabotack Date: Tue, 5 Dec 2023 10:22:52 +0100 Subject: [PATCH 13/41] Implement role checking in update_access --- src/api/ecdar_api.rs | 62 ++++++++++++++++++-- src/tests/api/access_logic.rs | 104 +++++++++++++++++++++++++++++++--- 2 files changed, 155 insertions(+), 11 deletions(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index fd238cc..cf7e04d 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -495,8 +495,8 @@ impl EcdarApi for ConcreteEcdarApi { .uid() .ok_or(Status::internal("Could not get uid from request metadata"))?; - // Check if user (the requester) has access to model - let access = self + // Check if the requester has access to model + let requester_access = self .contexts .access_context .get_access_by_uid_and_model_id(uid, message.model_id) @@ -506,8 +506,8 @@ impl EcdarApi for ConcreteEcdarApi { Status::new(Code::PermissionDenied, "User does not have access to model") })?; - // Check if user (the requester) has role 'Editor' - if access.role != "Editor" { + // Check if the requester has role 'Editor' + if requester_access.role != "Editor" { return Err(Status::new( Code::PermissionDenied, "You do not have permission to create access for this model", @@ -551,6 +551,60 @@ impl EcdarApi for ConcreteEcdarApi { ) -> Result, Status> { let message = request.get_ref().clone(); + let uid = request + .uid() + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + let user_access = self + .contexts + .access_context + .get_by_id(message.id) + .await + .map_err(|err| Status::new(Code::Internal, err.to_string()))? + .ok_or_else(|| { + Status::new( + Code::NotFound, + "No access entity found for user".to_string(), + ) + })?; + + let requester_access = self + .contexts + .access_context + .get_access_by_uid_and_model_id(uid, user_access.model_id) + .await + .map_err(|err| Status::new(Code::Internal, err.to_string()))? + .ok_or_else(|| { + Status::new( + Code::NotFound, + "No access entity found for requester".to_string(), + ) + })?; + + // Check if the requester has role 'Editor' + if requester_access.role != "Editor" { + return Err(Status::new( + Code::PermissionDenied, + "Requester does not have permission to update access for this model", + )); + } + + let model = self + .contexts + .model_context + .get_by_id(user_access.model_id) + .await + .map_err(|err| Status::new(Code::Internal, err.to_string()))? + .ok_or_else(|| Status::new(Code::NotFound, "No model found for access".to_string()))?; + + // Check that the requester is not trying to update the owner's access + if model.owner_id == message.id { + return Err(Status::new( + Code::PermissionDenied, + "Requester does not have permission to update access for this user", + )); + } + let access = access::Model { id: message.id, role: message.role, diff --git a/src/tests/api/access_logic.rs b/src/tests/api/access_logic.rs index 94ac029..53bcab5 100644 --- a/src/tests/api/access_logic.rs +++ b/src/tests/api/access_logic.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use crate::api::server::server::create_access_request::User; use crate::api::server::server::ecdar_api_server::EcdarApi; use crate::api::server::server::{CreateAccessRequest, DeleteAccessRequest, UpdateAccessRequest}; -use crate::entities::{access, user}; +use crate::entities::{access, model, user}; use crate::tests::api::helpers::{get_mock_concrete_ecdar_api, get_mock_services}; use mockall::predicate; use sea_orm::DbErr; @@ -199,7 +199,7 @@ async fn update_invalid_access_returns_err() { let mut mock_services = get_mock_services(); let access = access::Model { - id: 1, + id: 2, role: "Editor".to_string(), model_id: Default::default(), user_id: Default::default(), @@ -211,11 +211,55 @@ async fn update_invalid_access_returns_err() { .with(predicate::eq(access.clone())) .returning(move |_| Err(DbErr::RecordNotUpdated)); - let request = Request::new(UpdateAccessRequest { - id: 1, + mock_services + .access_context_mock + .expect_get_by_id() + .with(predicate::eq(2)) + .returning(move |_| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + model_id: 1, + user_id: 2, + })) + }); + + mock_services + .access_context_mock + .expect_get_access_by_uid_and_model_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + model_id: 1, + user_id: 1, + })) + }); + + mock_services + .model_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(model::Model { + id: 1, + name: "test".to_string(), + owner_id: 1, + components_info: Default::default(), + })) + }); + + let mut request = Request::new(UpdateAccessRequest { + id: 2, role: "Editor".to_string(), }); + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + let api = get_mock_concrete_ecdar_api(mock_services); let res = api.update_access(request).await.unwrap_err(); @@ -228,7 +272,7 @@ async fn update_access_returns_ok() { let mut mock_services = get_mock_services(); let access = access::Model { - id: 1, + id: 2, role: "Editor".to_string(), model_id: Default::default(), user_id: Default::default(), @@ -240,15 +284,61 @@ async fn update_access_returns_ok() { .with(predicate::eq(access.clone())) .returning(move |_| Ok(access.clone())); - let request = Request::new(UpdateAccessRequest { - id: 1, + mock_services + .access_context_mock + .expect_get_by_id() + .with(predicate::eq(2)) + .returning(move |_| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + model_id: 1, + user_id: 2, + })) + }); + + mock_services + .access_context_mock + .expect_get_access_by_uid_and_model_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + model_id: 1, + user_id: 1, + })) + }); + + mock_services + .model_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(model::Model { + id: 1, + name: "test".to_string(), + owner_id: 1, + components_info: Default::default(), + })) + }); + + let mut request = Request::new(UpdateAccessRequest { + id: 2, role: "Editor".to_string(), }); + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); + let api = get_mock_concrete_ecdar_api(mock_services); let res = api.update_access(request).await; + print!("{:?}", res); + assert!(res.is_ok()); } From f87e1c0a9e5b0e5f05dc30f5401fa27138ad6cf4 Mon Sep 17 00:00:00 2001 From: sabotack Date: Tue, 5 Dec 2023 10:48:38 +0100 Subject: [PATCH 14/41] Add editor_role_check_helper method --- src/api/ecdar_api.rs | 91 ++++++++++++++++++----------------- src/tests/api/access_logic.rs | 2 + 2 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index cf7e04d..7815549 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -12,13 +12,15 @@ use super::server::server::{ UpdateAccessRequest, UpdateModelRequest, UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, }; -use crate::api::context_collection::ContextCollection; use crate::api::{ auth::{RequestExt, Token, TokenType}, server::server::Model, }; use crate::database::{session_context::SessionContextTrait, user_context::UserContextTrait}; use crate::entities::{access, in_use, model, query, session, user}; +use crate::{ + api::context_collection::ContextCollection, database::access_context::AccessContextTrait, +}; use chrono::{Duration, Utc}; use regex::Regex; use sea_orm::SqlErr; @@ -495,24 +497,13 @@ impl EcdarApi for ConcreteEcdarApi { .uid() .ok_or(Status::internal("Could not get uid from request metadata"))?; - // Check if the requester has access to model - let requester_access = self - .contexts - .access_context - .get_access_by_uid_and_model_id(uid, message.model_id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new(Code::PermissionDenied, "User does not have access to model") - })?; - - // Check if the requester has role 'Editor' - if requester_access.role != "Editor" { - return Err(Status::new( - Code::PermissionDenied, - "You do not have permission to create access for this model", - )); - } + // Check if the requester has access to model with role 'Editor' + check_editor_role_helper( + Arc::clone(&self.contexts.access_context), + uid, + message.model_id, + ) + .await?; if let Some(user) = message.user { let user_from_db = @@ -568,26 +559,12 @@ impl EcdarApi for ConcreteEcdarApi { ) })?; - let requester_access = self - .contexts - .access_context - .get_access_by_uid_and_model_id(uid, user_access.model_id) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new( - Code::NotFound, - "No access entity found for requester".to_string(), - ) - })?; - - // Check if the requester has role 'Editor' - if requester_access.role != "Editor" { - return Err(Status::new( - Code::PermissionDenied, - "Requester does not have permission to update access for this model", - )); - } + check_editor_role_helper( + Arc::clone(&self.contexts.access_context), + uid, + user_access.model_id, + ) + .await?; let model = self .contexts @@ -626,12 +603,9 @@ impl EcdarApi for ConcreteEcdarApi { &self, request: Request, ) -> Result, Status> { - match self - .contexts - .access_context - .delete(request.get_ref().id) - .await - { + let message = request.get_ref().clone(); + + match self.contexts.access_context.delete(message.id).await { Ok(_) => Ok(Response::new(())), Err(error) => match error { sea_orm::DbErr::RecordNotFound(message) => { @@ -800,6 +774,33 @@ impl EcdarApi for ConcreteEcdarApi { } } +async fn check_editor_role_helper( + access_context: Arc, + user_id: i32, + model_id: i32, +) -> Result<(), Status> { + let access = access_context + .get_access_by_uid_and_model_id(user_id, model_id) + .await + .map_err(|err| Status::new(Code::Internal, err.to_string()))? + .ok_or_else(|| { + Status::new( + Code::PermissionDenied, + "User does not have access to model".to_string(), + ) + })?; + + // Check if the requester has role 'Editor' + if access.role != "Editor" { + return Err(Status::new( + Code::PermissionDenied, + "User does not have 'Editor' role for this model", + )); + } + + Ok(()) +} + async fn create_access_find_user_helper( user_context: Arc, user: User, diff --git a/src/tests/api/access_logic.rs b/src/tests/api/access_logic.rs index 53bcab5..7c4ab30 100644 --- a/src/tests/api/access_logic.rs +++ b/src/tests/api/access_logic.rs @@ -69,6 +69,8 @@ async fn create_no_access_returns_err() { let res = api.create_access(request).await.unwrap_err(); + print!("{:?}", res); + assert_eq!(res.code(), Code::PermissionDenied); } From b79a3d08928dea12db8a63ce6b3efadab357233a Mon Sep 17 00:00:00 2001 From: sabotack Date: Tue, 5 Dec 2023 11:04:16 +0100 Subject: [PATCH 15/41] Implement role checking in delete_access --- src/api/ecdar_api.rs | 40 ++++++++++++++ src/tests/api/access_logic.rs | 98 +++++++++++++++++++++++++++++++++-- 2 files changed, 133 insertions(+), 5 deletions(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index 7815549..fc715e8 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -605,6 +605,46 @@ impl EcdarApi for ConcreteEcdarApi { ) -> Result, Status> { let message = request.get_ref().clone(); + let uid = request + .uid() + .ok_or(Status::internal("Could not get uid from request metadata"))?; + + let user_access = self + .contexts + .access_context + .get_by_id(message.id) + .await + .map_err(|err| Status::new(Code::Internal, err.to_string()))? + .ok_or_else(|| { + Status::new( + Code::NotFound, + "No access entity found for user".to_string(), + ) + })?; + + check_editor_role_helper( + Arc::clone(&self.contexts.access_context), + uid, + user_access.model_id, + ) + .await?; + + let model = self + .contexts + .model_context + .get_by_id(user_access.model_id) + .await + .map_err(|err| Status::new(Code::Internal, err.to_string()))? + .ok_or_else(|| Status::new(Code::NotFound, "No model found for access".to_string()))?; + + // Check that the requester is not trying to delete the owner's access + if model.owner_id == message.id { + return Err(Status::new( + Code::PermissionDenied, + "You cannot delete the access entity for this user", + )); + } + match self.contexts.access_context.delete(message.id).await { Ok(_) => Ok(Response::new(())), Err(error) => match error { diff --git a/src/tests/api/access_logic.rs b/src/tests/api/access_logic.rs index 7c4ab30..d36490e 100644 --- a/src/tests/api/access_logic.rs +++ b/src/tests/api/access_logic.rs @@ -351,10 +351,54 @@ async fn delete_invalid_access_returns_err() { mock_services .access_context_mock .expect_delete() - .with(predicate::eq(1)) + .with(predicate::eq(2)) .returning(move |_| Err(DbErr::RecordNotFound("".to_string()))); - let request = Request::new(DeleteAccessRequest { id: 1 }); + mock_services + .access_context_mock + .expect_get_by_id() + .with(predicate::eq(2)) + .returning(move |_| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + model_id: 1, + user_id: 2, + })) + }); + + mock_services + .access_context_mock + .expect_get_access_by_uid_and_model_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + model_id: 1, + user_id: 1, + })) + }); + + mock_services + .model_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(model::Model { + id: 1, + name: "test".to_string(), + owner_id: 1, + components_info: Default::default(), + })) + }); + + let mut request = Request::new(DeleteAccessRequest { id: 2 }); + + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); let api = get_mock_concrete_ecdar_api(mock_services); @@ -368,7 +412,7 @@ async fn delete_access_returns_ok() { let mut mock_services = get_mock_services(); let access = access::Model { - id: 1, + id: 2, role: "Editor".to_string(), model_id: Default::default(), user_id: Default::default(), @@ -377,10 +421,54 @@ async fn delete_access_returns_ok() { mock_services .access_context_mock .expect_delete() - .with(predicate::eq(1)) + .with(predicate::eq(2)) .returning(move |_| Ok(access.clone())); - let request = Request::new(DeleteAccessRequest { id: 1 }); + mock_services + .access_context_mock + .expect_get_by_id() + .with(predicate::eq(2)) + .returning(move |_| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + model_id: 1, + user_id: 2, + })) + }); + + mock_services + .access_context_mock + .expect_get_access_by_uid_and_model_id() + .with(predicate::eq(1), predicate::eq(1)) + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + model_id: 1, + user_id: 1, + })) + }); + + mock_services + .model_context_mock + .expect_get_by_id() + .with(predicate::eq(1)) + .returning(move |_| { + Ok(Some(model::Model { + id: 1, + name: "test".to_string(), + owner_id: 1, + components_info: Default::default(), + })) + }); + + let mut request = Request::new(DeleteAccessRequest { id: 2 }); + + request.metadata_mut().insert( + "uid", + tonic::metadata::MetadataValue::from_str("1").unwrap(), + ); let api = get_mock_concrete_ecdar_api(mock_services); From 573445eff8e1eba513f22b4073a3bb7f4dde25c5 Mon Sep 17 00:00:00 2001 From: Viktor Platz Date: Tue, 5 Dec 2023 11:40:09 +0100 Subject: [PATCH 16/41] clippy fmt --- src/api/ecdar_api.rs | 11 +++++------ src/database/access_context.rs | 11 ++--------- src/tests/api/access_logic.rs | 29 ++++++++++++++++------------ src/tests/api/helpers.rs | 2 +- src/tests/database/access_context.rs | 4 ++-- 5 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index f0a8b52..68ad1e1 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -7,9 +7,10 @@ use super::server::server::{ CreateAccessRequest, CreateModelRequest, CreateModelResponse, CreateQueryRequest, CreateUserRequest, DeleteAccessRequest, DeleteModelRequest, DeleteQueryRequest, GetAuthTokenRequest, GetAuthTokenResponse, GetModelRequest, GetModelResponse, - ListModelsInfoResponse, Query, QueryRequest, QueryResponse, SimulationStartRequest, - SimulationStepRequest, SimulationStepResponse, UpdateAccessRequest, UpdateModelRequest, - UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, ListAccessInfoRequest, ListAccessInfoResponse, + ListAccessInfoRequest, ListAccessInfoResponse, ListModelsInfoResponse, Query, QueryRequest, + QueryResponse, SimulationStartRequest, SimulationStepRequest, SimulationStepResponse, + UpdateAccessRequest, UpdateModelRequest, UpdateQueryRequest, UpdateUserRequest, + UserTokenResponse, }; use crate::api::{ auth::{RequestExt, Token, TokenType}, @@ -238,7 +239,7 @@ impl EcdarApi for ConcreteEcdarApi { "User does not have access to model", )); } - } + } Err(error) => return Err(Status::new(Code::Internal, error.to_string())), }; @@ -260,8 +261,6 @@ impl EcdarApi for ConcreteEcdarApi { } Err(error) => Err(Status::new(Code::Internal, error.to_string())), } - - } async fn create_model( diff --git a/src/database/access_context.rs b/src/database/access_context.rs index 466ee17..9ab701c 100644 --- a/src/database/access_context.rs +++ b/src/database/access_context.rs @@ -19,10 +19,7 @@ pub trait AccessContextTrait: EntityContextTrait { model_id: i32, ) -> Result, DbErr>; - async fn get_access_by_model_id( - &self, - model_id: i32, - ) -> Result, DbErr>; + async fn get_access_by_model_id(&self, model_id: i32) -> Result, DbErr>; } #[async_trait] @@ -42,17 +39,13 @@ impl AccessContextTrait for AccessContext { .await } - async fn get_access_by_model_id( - &self, - model_id: i32, - ) -> Result, DbErr> { + async fn get_access_by_model_id(&self, model_id: i32) -> Result, DbErr> { access::Entity::find() .filter(access::Column::ModelId.eq(model_id)) .into_model::() .all(&self.db_context.get_connection()) .await } - } impl AccessContext { diff --git a/src/tests/api/access_logic.rs b/src/tests/api/access_logic.rs index 7d9a771..69cdccd 100644 --- a/src/tests/api/access_logic.rs +++ b/src/tests/api/access_logic.rs @@ -2,7 +2,10 @@ use std::str::FromStr; use crate::api::server::server::create_access_request::User; use crate::api::server::server::ecdar_api_server::EcdarApi; -use crate::api::server::server::{CreateAccessRequest, DeleteAccessRequest, UpdateAccessRequest, AccessInfo, ListAccessInfoRequest}; +use crate::api::server::server::{ + AccessInfo, CreateAccessRequest, DeleteAccessRequest, ListAccessInfoRequest, + UpdateAccessRequest, +}; use crate::entities::{access, model, user}; use crate::tests::api::helpers::{get_mock_concrete_ecdar_api, get_mock_services}; use mockall::predicate; @@ -416,7 +419,8 @@ async fn delete_access_returns_ok() { async fn list_access_info_returns_ok() { let mut mock_services = get_mock_services(); - let mut request: Request = Request::new(ListAccessInfoRequest { model_id: 1 }); + let mut request: Request = + Request::new(ListAccessInfoRequest { model_id: 1 }); request .metadata_mut() @@ -432,12 +436,14 @@ async fn list_access_info_returns_ok() { mock_services .access_context_mock .expect_get_access_by_uid_and_model_id() - .returning(move |_,_| Ok(Some(access::Model { - id: 1, - role: "Editor".to_string(), - model_id: Default::default(), - user_id: Default::default(), - }))); + .returning(move |_, _| { + Ok(Some(access::Model { + id: 1, + role: "Editor".to_string(), + model_id: Default::default(), + user_id: Default::default(), + })) + }); mock_services .access_context_mock @@ -461,7 +467,6 @@ async fn list_access_info_returns_not_found() { .metadata_mut() .insert("uid", metadata::MetadataValue::from_str("1").unwrap()); - let access = access::Model { id: 1, role: "Editor".to_string(), @@ -478,7 +483,7 @@ async fn list_access_info_returns_not_found() { .access_context_mock .expect_get_access_by_uid_and_model_id() .returning(move |_, _| Ok(Some(access.clone()))); - + let api = get_mock_concrete_ecdar_api(mock_services); let res = api.list_access_info(request).await.unwrap_err(); @@ -499,11 +504,11 @@ async fn list_access_info_returns_no_permission() { mock_services .access_context_mock .expect_get_access_by_uid_and_model_id() - .returning(move |_,_| Ok(None)); + .returning(move |_, _| Ok(None)); let api = get_mock_concrete_ecdar_api(mock_services); let res = api.list_access_info(request).await.unwrap_err(); assert_eq!(res.code(), Code::PermissionDenied); -} \ No newline at end of file +} diff --git a/src/tests/api/helpers.rs b/src/tests/api/helpers.rs index 34cef3d..2b2dec9 100644 --- a/src/tests/api/helpers.rs +++ b/src/tests/api/helpers.rs @@ -5,6 +5,7 @@ use crate::api::context_collection::ContextCollection; use crate::api::ecdar_api::ConcreteEcdarApi; use crate::api::hashing_context::HashingContextTrait; use crate::api::server::server::ecdar_backend_server::EcdarBackend; +use crate::api::server::server::AccessInfo; use crate::api::server::server::ModelInfo; use crate::api::server::server::{ QueryRequest, QueryResponse, SimulationStartRequest, SimulationStepRequest, @@ -23,7 +24,6 @@ use mockall::mock; use sea_orm::DbErr; use std::sync::Arc; use tonic::{Request, Response, Status}; -use crate::api::server::server::AccessInfo; pub fn get_mock_concrete_ecdar_api(mock_services: MockServices) -> ConcreteEcdarApi { let contexts = ContextCollection { diff --git a/src/tests/database/access_context.rs b/src/tests/database/access_context.rs index 3ca1ff4..8d9940d 100644 --- a/src/tests/database/access_context.rs +++ b/src/tests/database/access_context.rs @@ -1,3 +1,4 @@ +use crate::api::server::server::AccessInfo; use crate::database::access_context::AccessContextTrait; use crate::tests::database::helpers::{ create_accesses, create_models, create_users, get_reset_database_context, @@ -8,7 +9,6 @@ use crate::{ to_active_models, }; use sea_orm::{entity::prelude::*, IntoActiveModel}; -use crate::api::server::server::AccessInfo; async fn seed_db() -> (AccessContext, access::Model, user::Model, model::Model) { let db_context = get_reset_database_context().await; @@ -390,4 +390,4 @@ async fn get_access_by_model_id_test_returns_empty() { let access = access_context.get_access_by_model_id(model.id).await; assert!(access.unwrap().is_empty()); -} \ No newline at end of file +} From 4910565999a6ba2703800c32e95ef278fa8b3be2 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Tue, 5 Dec 2023 14:06:24 +0100 Subject: [PATCH 17/41] Add thiserror crate --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 3881ade..cf7d9da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ regex = "1.10.2" mockall = "0.11.4" bcrypt = "0.15.0" serde_json = "1.0.108" +thiserror = "1.0.50" [build-dependencies] tonic-build = "0.10.2" From e85d910c5be4af8699c3537bb5fd35ab66cfa14b Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Tue, 5 Dec 2023 14:07:03 +0100 Subject: [PATCH 18/41] Add get_by_ids mock --- src/tests/api/helpers.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/api/helpers.rs b/src/tests/api/helpers.rs index 889d139..1cebf17 100644 --- a/src/tests/api/helpers.rs +++ b/src/tests/api/helpers.rs @@ -172,6 +172,7 @@ mock! { impl UserContextTrait for UserContext { async fn get_by_username(&self, username: String) -> Result, DbErr>; async fn get_by_email(&self, email: String) -> Result, DbErr>; + async fn get_by_ids(&self, ids: Vec) -> Result, DbErr>; } } From fead523ef71564aa890b3704b3ef25ce2f992772 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Tue, 5 Dec 2023 14:09:02 +0100 Subject: [PATCH 19/41] Fix tests --- src/tests/api/auth.rs | 2 + src/tests/api/user_logic.rs | 88 ++++++++++++++++++++++++------------- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/tests/api/auth.rs b/src/tests/api/auth.rs index 8df094f..c5b9a67 100644 --- a/src/tests/api/auth.rs +++ b/src/tests/api/auth.rs @@ -130,6 +130,8 @@ mod auth { #[tokio::test] async fn token_from_str_invalid_returns_err() { + env::set_var("ACCESS_TOKEN_HS512_SECRET", "access_secret"); + let token = Token::from_str(TokenType::AccessToken, "invalid_token"); let result = token.validate(); diff --git a/src/tests/api/user_logic.rs b/src/tests/api/user_logic.rs index 0052ea7..092cfb9 100644 --- a/src/tests/api/user_logic.rs +++ b/src/tests/api/user_logic.rs @@ -1,6 +1,6 @@ use crate::api::server::server::ecdar_api_auth_server::EcdarApiAuth; use crate::api::server::server::ecdar_api_server::EcdarApi; -use crate::api::server::server::{CreateUserRequest, UpdateUserRequest}; +use crate::api::server::server::{CreateUserRequest, GetUsersRequest, UpdateUserRequest}; use crate::entities::user; use crate::tests::api::helpers::{get_mock_concrete_ecdar_api, get_mock_services}; use mockall::predicate; @@ -68,13 +68,26 @@ async fn delete_user_existing_user_returns_ok() { async fn create_user_nonexistent_user_returns_ok() { let mut mock_services = get_mock_services(); + let password = "Password123".to_string(); + let user = user::Model { id: Default::default(), email: "anders21@student.aau.dk".to_string(), username: "anders".to_string(), - password: "123".to_string(), + password: password.clone(), }; + let create_user_request = Request::new(CreateUserRequest { + email: "anders21@student.aau.dk".to_string(), + username: "anders".to_string(), + password: password.clone(), + }); + + mock_services + .hashing_context_mock + .expect_hash_password() + .returning(move |_| password.clone()); + mock_services .user_context_mock .expect_create() @@ -83,12 +96,6 @@ async fn create_user_nonexistent_user_returns_ok() { let api = get_mock_concrete_ecdar_api(mock_services); - let create_user_request = Request::new(CreateUserRequest { - email: "anders21@student.aau.dk".to_string(), - username: "anders".to_string(), - password: "123".to_string(), - }); - let create_user_response = api.create_user(create_user_request).await; assert!(create_user_response.is_ok()); } @@ -97,13 +104,26 @@ async fn create_user_nonexistent_user_returns_ok() { async fn create_user_duplicate_email_returns_error() { let mut mock_services = get_mock_services(); + let password = "Password123".to_string(); + let user = user::Model { id: Default::default(), email: "anders21@student.aau.dk".to_string(), username: "anders".to_string(), - password: "".to_string(), + password: password.clone(), }; + let create_user_request = Request::new(CreateUserRequest { + email: "anders21@student.aau.dk".to_string(), + username: "anders".to_string(), + password: password.clone(), + }); + + mock_services + .hashing_context_mock + .expect_hash_password() + .returning(move |_| password.clone()); + mock_services .user_context_mock .expect_create() @@ -112,12 +132,6 @@ async fn create_user_duplicate_email_returns_error() { let api = get_mock_concrete_ecdar_api(mock_services); - let create_user_request = Request::new(CreateUserRequest { - email: "anders21@student.aau.dk".to_string(), - username: "anders".to_string(), - password: "".to_string(), - }); - let res = api.create_user(create_user_request).await; assert_eq!(res.unwrap_err().code(), Code::Internal); //todo!("Needs to be code AlreadyExists when mocked Error is corrected) } @@ -142,13 +156,26 @@ async fn create_user_invalid_email_returns_error() { async fn create_user_duplicate_username_returns_error() { let mut mock_services = get_mock_services(); + let password = "Password123".to_string(); + let user = user::Model { id: Default::default(), email: "anders21@student.aau.dk".to_string(), username: "anders".to_string(), - password: "".to_string(), + password: password.clone(), }; + let create_user_request = Request::new(CreateUserRequest { + email: "anders21@student.aau.dk".to_string(), + username: "anders".to_string(), + password: password.clone(), + }); + + mock_services + .hashing_context_mock + .expect_hash_password() + .returning(move |_| password.clone()); + mock_services .user_context_mock .expect_create() @@ -157,12 +184,6 @@ async fn create_user_duplicate_username_returns_error() { let api = get_mock_concrete_ecdar_api(mock_services); - let create_user_request = Request::new(CreateUserRequest { - email: "anders21@student.aau.dk".to_string(), - username: "anders".to_string(), - password: "".to_string(), - }); - let res = api.create_user(create_user_request).await; assert_eq!(res.unwrap_err().code(), Code::Internal); //todo!("Needs to be code AlreadyExists when mocked Error is corrected) } @@ -184,16 +205,29 @@ async fn create_user_invalid_username_returns_error() { } #[tokio::test] -async fn test_create_user_valid_request_returns_ok() { +async fn create_user_valid_request_returns_ok() { let mut mock_services = get_mock_services(); + let password = "Password123".to_string(); + let user = user::Model { id: Default::default(), email: "newuser@example.com".to_string(), username: "newuser".to_string(), - password: "StrongPassword123".to_string(), + password: password.clone(), }; + let create_user_request = Request::new(CreateUserRequest { + email: "newuser@example.com".to_string(), + username: "newuser".to_string(), + password: password.clone(), + }); + + mock_services + .hashing_context_mock + .expect_hash_password() + .returning(move |_| password.clone()); + mock_services .user_context_mock .expect_create() @@ -202,12 +236,6 @@ async fn test_create_user_valid_request_returns_ok() { let api = get_mock_concrete_ecdar_api(mock_services); - let create_user_request = Request::new(CreateUserRequest { - email: "newuser@example.com".to_string(), - username: "newuser".to_string(), - password: "StrongPassword123".to_string(), - }); - let create_user_response = api.create_user(create_user_request).await; assert!(create_user_response.is_ok()); } From c2d7276f9059054c89c0e4ec78a025203300034d Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Tue, 5 Dec 2023 14:10:22 +0100 Subject: [PATCH 20/41] Add get users endpoint --- src/api/ecdar_api.rs | 36 +++++++++++++++++++++---- src/tests/api/user_logic.rs | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index 664be9e..4a1e203 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -3,12 +3,13 @@ use super::server::server::{ ecdar_api_server::EcdarApi, ecdar_backend_server::EcdarBackend, get_auth_token_request::{user_credentials, UserCredentials}, + get_users_response::UserInfo, CreateAccessRequest, CreateModelRequest, CreateModelResponse, CreateQueryRequest, CreateUserRequest, DeleteAccessRequest, DeleteModelRequest, DeleteQueryRequest, - GetAuthTokenRequest, GetAuthTokenResponse, GetModelRequest, GetModelResponse, - ListModelsInfoResponse, Query, QueryRequest, QueryResponse, SimulationStartRequest, - SimulationStepRequest, SimulationStepResponse, UpdateAccessRequest, UpdateModelRequest, - UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, + GetAuthTokenRequest, GetAuthTokenResponse, GetModelRequest, GetModelResponse, GetUsersRequest, + GetUsersResponse, ListModelsInfoResponse, Query, QueryRequest, QueryResponse, + SimulationStartRequest, SimulationStepRequest, SimulationStepResponse, UpdateAccessRequest, + UpdateModelRequest, UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, }; use crate::api::context_collection::ContextCollection; use crate::api::{ @@ -19,7 +20,7 @@ use crate::database::{session_context::SessionContextTrait, user_context::UserCo use crate::entities::{access, in_use, model, query, session, user}; use chrono::{Duration, Utc}; use regex::Regex; -use sea_orm::SqlErr; +use sea_orm::{DbErr, SqlErr}; use serde_json; use std::sync::Arc; use tonic::{Code, Request, Response, Status}; @@ -533,6 +534,31 @@ impl EcdarApi for ConcreteEcdarApi { Err(error) => Err(Status::new(Code::Internal, error.to_string())), } } + /// Gets users from the database. + /// If no users exits with the given ids, an empty list is returned. + async fn get_users( + &self, + request: Request, + ) -> Result, Status> { + let ids = request.get_ref().ids.clone(); + + let users = self + .contexts + .user_context + .get_by_ids(ids) + .await + .map_err(|err| Status::internal(err.to_string()))?; + + let users_info = users + .into_iter() + .map(|user| UserInfo { + id: user.id, + username: user.username, + }) + .collect::>(); + + Ok(Response::new(GetUsersResponse { users: users_info })) + } /// Creates a query in the database /// # Errors diff --git a/src/tests/api/user_logic.rs b/src/tests/api/user_logic.rs index 092cfb9..07a06c1 100644 --- a/src/tests/api/user_logic.rs +++ b/src/tests/api/user_logic.rs @@ -319,3 +319,56 @@ async fn update_user_non_existant_user_returns_err() { assert_eq!(res.unwrap_err().code(), Code::Internal); } + +#[tokio::test] +async fn get_users_returns_ok() { + let mut mock_services = get_mock_services(); + + let users = vec![ + user::Model { + id: 1, + email: "".to_string(), + username: "".to_string(), + password: "".to_string(), + }, + user::Model { + id: 2, + email: "".to_string(), + username: "".to_string(), + password: "".to_string(), + }, + ]; + + mock_services + .user_context_mock + .expect_get_by_ids() + .returning(move |_| Ok(users.clone())); + + let api = get_mock_concrete_ecdar_api(mock_services); + + let get_users_request = Request::new(GetUsersRequest { ids: vec![1, 2] }); + + let get_users_response = api.get_users(get_users_request).await.unwrap(); + + assert_eq!(get_users_response.get_ref().users.len(), 2); +} + +#[tokio::test] +async fn get_users_returns_empty_array() { + let mut mock_services = get_mock_services(); + + let users: Vec = vec![]; + + mock_services + .user_context_mock + .expect_get_by_ids() + .returning(move |_| Ok(users.clone())); + + let api = get_mock_concrete_ecdar_api(mock_services); + + let get_users_request = Request::new(GetUsersRequest { ids: vec![1, 2] }); + + let get_users_response = api.get_users(get_users_request).await.unwrap(); + + assert_eq!(get_users_response.get_ref().users.len(), 0); +} From cb125bab2eeef16ac23d292c1bf5dcd05b38e79f Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Tue, 5 Dec 2023 14:11:23 +0100 Subject: [PATCH 21/41] Add get_by_ids on user context --- src/database/user_context.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/database/user_context.rs b/src/database/user_context.rs index 6a42197..2a14060 100644 --- a/src/database/user_context.rs +++ b/src/database/user_context.rs @@ -14,6 +14,14 @@ pub struct UserContext { pub trait UserContextTrait: EntityContextTrait { async fn get_by_username(&self, username: String) -> Result, DbErr>; async fn get_by_email(&self, email: String) -> Result, DbErr>; + /// Returns all the user entities with the given ids + /// # Example + /// ``` + /// let context : UserContext = UserContext::new(...); + /// let model : vec = context.get_by_ids(vec![1,2]).unwrap(); + /// assert_eq!(model.len(),2); + /// ``` + async fn get_by_ids(&self, ids: Vec) -> Result, DbErr>; } #[async_trait] @@ -30,6 +38,13 @@ impl UserContextTrait for UserContext { .one(&self.db_context.get_connection()) .await } + + async fn get_by_ids(&self, ids: Vec) -> Result, DbErr> { + user::Entity::find() + .filter(user::Column::Id.is_in(ids)) + .all(&self.db_context.get_connection()) + .await + } } impl UserContext { @@ -145,6 +160,7 @@ impl EntityContextTrait for UserContext { } } } + #[cfg(test)] #[path = "../tests/database/user_context.rs"] mod user_context_tests; From 7923c4e79bff750e19f278bcf543765dd72ed628 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Tue, 5 Dec 2023 14:15:41 +0100 Subject: [PATCH 22/41] Fix Token --- src/api/auth.rs | 109 +++++++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/src/api/auth.rs b/src/api/auth.rs index 3acccbc..2f85bcb 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -9,8 +9,8 @@ use tonic::{metadata, Request, Status}; /// This method is used to validate the access token (not refresh). pub fn validation_interceptor(mut req: Request<()>) -> Result, Status> { - let token = match req.token_string() { - Some(token) => Token::from_str(TokenType::AccessToken, &token), + let token = match req.token_str() { + Some(token) => Token::from_str(TokenType::AccessToken, token), None => return Err(Status::unauthenticated("Token not found")), }; @@ -75,6 +75,7 @@ impl TokenType { /// assert_eq!(token.token_type(), TokenType::AccessToken); /// assert_eq!(token.to_string(), token.as_str()); /// ``` + pub struct Token { token_type: TokenType, token: String, @@ -115,6 +116,41 @@ impl Token { Ok(Token { token_type, token }) } + + /// Creates a new refresh token. + /// + /// # Arguments + /// * `uid` - The user id to create the token for. + /// + /// # Examples + /// ``` + /// use ecdar_api::api::auth::{Token, TokenType}; + /// + /// let refresh_token = Token::refresh("1").unwrap(); + /// + /// assert_eq!(refresh_token.token_type(), TokenType::RefreshToken); + /// ``` + pub fn refresh(uid: &str) -> Result { + Token::new(TokenType::RefreshToken, uid) + } + + /// Creates a new access token. + /// + /// # Arguments + /// * `uid` - The user id to create the token for. + /// + /// # Examples + /// ``` + /// use ecdar_api::api::auth::{Token, TokenType}; + /// + /// let access_token = Token::access("1").unwrap(); + /// + /// assert_eq!(access_token.token_type(), TokenType::AccessToken); + /// ``` + pub fn access(uid: &str) -> Result { + Token::new(TokenType::AccessToken, uid) + } + /// Create a token from a string. /// /// # Arguments @@ -125,7 +161,7 @@ impl Token { /// ``` /// use ecdar_api::api::auth::{Token, TokenType}; /// - /// let token = Token::from_str(TokenType::AccessToken, "token").unwrap(); + /// let token = Token::from_str(TokenType::AccessToken, "token") /// ``` pub fn from_str(token_type: TokenType, token: &str) -> Token { Token { @@ -158,19 +194,12 @@ impl Token { Err(err) => Err(err.into()), } } - - /// Returns the token as a string. - // pub fn to_string(&self) -> String { - // self.token.clone() - // } - /// Extracts the token as a string slice. - /// /// # Examples /// /// ``` /// use ecdar_api::api::auth::{Token, TokenType}; /// - /// let token = Token::new(TokenType::AccessToken, "1").unwrap(); + /// let token = Token::from_str(TokenType::AccessToken, "token"); /// /// assert_eq!(token.as_str(), "token"); /// ``` @@ -199,27 +228,22 @@ impl Display for Token { } } -#[derive(Debug)] +#[derive(Debug, PartialEq, thiserror::Error)] pub enum TokenError { + #[error("Invalid token")] + #[from(jsonwebtoken::errors::ErrorKind::InvalidToken)] InvalidToken, + #[error("Invalid signature")] + #[from(jsonwebtoken::errors::ErrorKind::InvalidSignature)] InvalidSignature, + #[error("Expired signature")] + #[from(jsonwebtoken::errors::ErrorKind::ExpiredSignature)] ExpiredSignature, + #[error("{0}")] Custom(String), } -/// This is used to get the token error as a string. -impl Display for TokenError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TokenError::InvalidToken => write!(f, "Invalid token"), - TokenError::InvalidSignature => write!(f, "Invalid signature"), - TokenError::ExpiredSignature => write!(f, "Expired signature"), - TokenError::Custom(message) => write!(f, "{}", message), - } - } -} - -/// This is used to convert the jsonwebtoken error kind to a [TokenError]. +// This is used to convert the jsonwebtoken error kind to a [TokenError]. impl From for TokenError { fn from(error: jsonwebtoken::errors::ErrorKind) -> Self { match error { @@ -248,39 +272,30 @@ impl From for Status { /// An extension trait for [Request]`s that provides a variety of convenient /// auth related methods. pub trait RequestExt { - fn token_string(&self) -> Option; fn token_str(&self) -> Option<&str>; - + fn token_string(&self) -> Option; fn uid(&self) -> Option; } impl RequestExt for Request { - /// Returns the token string from the request metadata. - fn token_string(&self) -> Option { - self.metadata().get("authorization").map(|token| { - token - .to_str() - .unwrap() - .trim_start_matches("Bearer ") - .to_string() - }) - } /// Returns the token string slice from the request metadata. fn token_str(&self) -> Option<&str> { - match self.metadata().get("authorization") { - Some(token) => Some(token.to_str().unwrap().trim_start_matches("Bearer ")), - None => None, - } + self.metadata() + .get("authorization") + .and_then(|token| token.to_str().ok()) + .map(|token_str| token_str.trim_start_matches("Bearer ")) } + /// Returns the token string from the request metadata. + fn token_string(&self) -> Option { + self.token_str().map(String::from) + } /// Returns the uid from the request metadata. fn uid(&self) -> Option { - let uid = match self.metadata().get("uid").unwrap().to_str() { - Ok(uid) => uid, - Err(_) => return None, - }; - - Some(uid.parse().unwrap()) + self.metadata() + .get("uid") + .and_then(|uid| uid.to_str().ok()) + .and_then(|uid_str| uid_str.parse().ok()) } } From 0afc0c2def7d1b89a9da21f2b83b4002feea3940 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Tue, 5 Dec 2023 14:18:02 +0100 Subject: [PATCH 23/41] Fix path to Model --- src/database/user_context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/user_context.rs b/src/database/user_context.rs index 2a14060..42c2aff 100644 --- a/src/database/user_context.rs +++ b/src/database/user_context.rs @@ -39,7 +39,7 @@ impl UserContextTrait for UserContext { .await } - async fn get_by_ids(&self, ids: Vec) -> Result, DbErr> { + async fn get_by_ids(&self, ids: Vec) -> Result, DbErr> { user::Entity::find() .filter(user::Column::Id.is_in(ids)) .all(&self.db_context.get_connection()) From 01ef8b59bad47044b17ec56b39c9ec9e35643c15 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Tue, 5 Dec 2023 14:24:37 +0100 Subject: [PATCH 24/41] Verify hashed password and refactor --- src/api/ecdar_api.rs | 248 ++++++++++++++++++------------------------- 1 file changed, 105 insertions(+), 143 deletions(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index 4a1e203..cdbe30e 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -32,55 +32,42 @@ pub struct ConcreteEcdarApi { contexts: ContextCollection, } -/// Updates or creates a session in the database for a given user. -/// -/// -/// # Errors -/// This function will return an error if the database context returns an error -/// or if a session is not found when trying to update an existing one. -pub async fn handle_session( +/// Updates the session given by refresh token in the database. +/// Returns the new access and refresh token. +pub async fn update_session( session_context: Arc, - request: &Request, - is_new_session: bool, - access_token: String, refresh_token: String, - uid: String, -) -> Result<(), Status> { - if is_new_session { - let res = session_context - .create(session::Model { - id: Default::default(), - access_token: access_token.clone(), - refresh_token: refresh_token.clone(), - updated_at: Default::default(), - user_id: uid.parse().unwrap(), - }) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))?; - } else { - let mut session = match session_context - .get_by_token(TokenType::RefreshToken, request.token_string().unwrap()) - .await - { - Ok(Some(session)) => session, - Ok(None) => { - return Err(Status::new( - Code::Unauthenticated, - "No session found with given refresh token", - )); - } - Err(err) => return Err(Status::new(Code::Internal, err.to_string())), - }; - - session.access_token = access_token.clone(); - session.refresh_token = refresh_token.clone(); - - session_context - .update(session) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))?; - } - Ok(()) +) -> Result<(Token, Token), Status> { + let session = match session_context + .get_by_token(TokenType::RefreshToken, refresh_token) + .await + { + Ok(Some(session)) => session, + Ok(None) => { + return Err(Status::unauthenticated( + "No session found with given refresh token", + )); + } + Err(err) => return Err(Status::internal(err.to_string())), + }; + + let uid = session.user_id.to_string(); + + let access_token = Token::access(&uid)?; + let refresh_token = Token::refresh(&uid)?; + + session_context + .update(session::Model { + id: session.id, + access_token: access_token.to_string(), + refresh_token: refresh_token.to_string(), + updated_at: Default::default(), + user_id: session.user_id, + }) + .await + .unwrap(); + + Ok((access_token, refresh_token)) } fn is_valid_email(email: &str) -> bool { @@ -358,34 +345,6 @@ impl EcdarApi for ConcreteEcdarApi { } Err(error) => Err(Status::new(Code::Internal, error.to_string())), } - /*let mut model_info_list_vector: Vec = Vec::new(); // Initialize the Vec - - // Get all the models that the user has access to - match self.model_context.get_model_info_by_uid(uid).await { - Ok(model_info_list) => { - for model_info in model_info_list { - let model_info_test = ModelInfo { - model_id: model_info.model_id, - model_name: model_info.model_name, - model_owner_id: model_info.model_owner_id, - user_role_on_model: model_info.user_role_on_model, - }; - model_info_list_vector.push(model_info_test); - } - - if model_info_list_vector.is_empty() { - return Err(Status::new( - Code::NotFound, - "No access found for given user", - )); - } else { - Ok(Response::new(ListModelInfoResponse { - model_info_list: model_info_list_vector, - })) - } - } - Err(error) => Err(Status::new(Code::Internal, error.to_string())), - }*/ } /// Creates an access in the database. @@ -643,28 +602,16 @@ impl EcdarApi for ConcreteEcdarApi { } } -async fn get_auth_find_user_helper( +async fn user_from_user_credentials( user_context: Arc, user_credentials: UserCredentials, -) -> Result { - if let Some(user) = user_credentials.user { - match user { - user_credentials::User::Username(username) => Ok(user_context - .get_by_username(username) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| Status::new(Code::NotFound, "No user found with given username"))?), - - user_credentials::User::Email(email) => Ok(user_context - .get_by_email(email) - .await - .map_err(|err| Status::new(Code::Internal, err.to_string()))? - .ok_or_else(|| { - Status::new(Code::NotFound, "No user found with the given email") - })?), +) -> Result, DbErr> { + match user_credentials.user { + Some(user_credentials::User::Username(username)) => { + Ok(user_context.get_by_username(username).await?) } - } else { - Err(Status::new(Code::InvalidArgument, "No user provided")) + Some(user_credentials::User::Email(email)) => Ok(user_context.get_by_email(email).await?), + None => Ok(None), } } @@ -681,61 +628,67 @@ impl EcdarApiAuth for ConcreteEcdarApi { request: Request, ) -> Result, Status> { let message = request.get_ref().clone(); - let uid: String; - let user_from_db: user::Model; - let is_new_session: bool; - - // Get user from credentials - if let Some(user_credentials) = message.user_credentials { - let input_password = user_credentials.password.clone(); - user_from_db = get_auth_find_user_helper( - Arc::clone(&self.contexts.user_context), - user_credentials, - ) - .await?; - - // Check if password in request matches users password - if input_password != user_from_db.password { - return Err(Status::new(Code::Unauthenticated, "Wrong password")); + + let (access_token, refresh_token) = match message.user_credentials { + None => { + let refresh_token = Token::from_str( + TokenType::RefreshToken, + request + .token_str() + .ok_or(Status::unauthenticated("No refresh token provided"))?, + ); + + refresh_token.validate()?; + + update_session( + self.contexts.session_context.clone(), + refresh_token.to_string(), + ) + .await? } + Some(user_credentials) => { + let input_password = user_credentials.password.clone(); + let user = user_from_user_credentials( + self.contexts.user_context.clone(), + user_credentials, + ) + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::unauthenticated("Wrong username or password"))?; + + // Check if password in request matches users password + if !self + .contexts + .hashing_context + .verify_password(input_password, user.password.as_str()) + { + return Err(Status::unauthenticated("Wrong username or password")); + } - uid = user_from_db.id.to_string(); + let uid = user.id.to_string(); - // Since the user does not have a refresh_token, a new session has to be made - is_new_session = true; + let access_token = Token::access(&uid)?; + let refresh_token = Token::refresh(&uid)?; - // Get user from refresh_token - } else { - let refresh_token = Token::from_str( - TokenType::RefreshToken, - request - .token_str() - .ok_or(Status::unauthenticated("No refresh token provided"))?, - ); - let token_data = refresh_token.validate()?; - uid = token_data.claims.sub; + self.contexts + .session_context + .create(session::Model { + id: Default::default(), + access_token: access_token.to_string(), + refresh_token: refresh_token.to_string(), + updated_at: Default::default(), + user_id: uid.parse().unwrap(), + }) + .await + .map_err(|err| Status::internal(err.to_string()))?; - // Since the user does have a refresh_token, a session already exists - is_new_session = false; - } - // Create new access and refresh token with user id - let access_token = Token::new(TokenType::AccessToken, &uid)?.to_string(); - let refresh_token = Token::new(TokenType::RefreshToken, &uid)?.to_string(); - - // Update or create session in database - handle_session( - self.contexts.session_context.clone(), - &request, - is_new_session, - access_token.clone(), - refresh_token.clone(), - uid, - ) - .await?; + (access_token, refresh_token) + } + }; Ok(Response::new(GetAuthTokenResponse { - access_token, - refresh_token, + access_token: access_token.to_string(), + refresh_token: refresh_token.to_string(), })) } @@ -753,10 +706,15 @@ impl EcdarApiAuth for ConcreteEcdarApi { return Err(Status::new(Code::InvalidArgument, "Invalid email")); } + let hashed_password = self + .contexts + .hashing_context + .hash_password(message.clone().password); + let user = user::Model { id: Default::default(), username: message.clone().username, - password: message.clone().password, + password: hashed_password, email: message.clone().email, }; @@ -834,3 +792,7 @@ mod user_logic_tests; #[cfg(test)] #[path = "../tests/api/session_logic.rs"] mod session_logic_tests; + +#[cfg(test)] +#[path = "../tests/api/get_auth_logic.rs"] +mod get_auth_logic_tests; From c9547f5c31737237e73fd346275b12f1015347f2 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Tue, 5 Dec 2023 14:25:41 +0100 Subject: [PATCH 25/41] Add get auth token tests --- src/tests/api/get_auth_logic.rs | 128 ++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/tests/api/get_auth_logic.rs diff --git a/src/tests/api/get_auth_logic.rs b/src/tests/api/get_auth_logic.rs new file mode 100644 index 0000000..8191c25 --- /dev/null +++ b/src/tests/api/get_auth_logic.rs @@ -0,0 +1,128 @@ +use crate::api::auth::{Token, TokenType}; +use crate::api::server::server::ecdar_api_auth_server::EcdarApiAuth; +use crate::api::server::server::get_auth_token_request::{user_credentials, UserCredentials}; +use crate::api::server::server::GetAuthTokenRequest; +use crate::entities::{session, user}; +use crate::tests::api::helpers::{get_mock_concrete_ecdar_api, get_mock_services}; +use std::env; +use std::str::FromStr; +use tonic::{metadata, Code, Request}; + +#[tokio::test] +async fn get_auth_token_from_credentials_returns_ok() { + let mut mock_services = get_mock_services(); + + let request = GetAuthTokenRequest { + user_credentials: Option::from(UserCredentials { + password: "Password123".to_string(), + user: Option::from(user_credentials::User::Username("Example".to_string())), + }), + }; + + mock_services + .user_context_mock + .expect_get_by_username() + .returning(move |_| { + Ok(Option::from(user::Model { + id: 1, + email: "".to_string(), + username: "Example".to_string(), + password: "".to_string(), + })) + }); + + mock_services + .hashing_context_mock + .expect_verify_password() + .returning(move |_, _| true); + + mock_services + .session_context_mock + .expect_create() + .returning(move |_| { + Ok(session::Model { + id: 0, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id: 1, + }) + }); + + let api = get_mock_concrete_ecdar_api(mock_services); + let response = api.get_auth_token(Request::new(request)).await.unwrap(); + + assert!(!response.get_ref().refresh_token.is_empty()); + assert!(!response.get_ref().access_token.is_empty()); +} + +#[tokio::test] +async fn get_auth_token_from_token_returns_ok() { + env::set_var("REFRESH_TOKEN_HS512_SECRET", "refresh_secret"); + + let mut mock_services = get_mock_services(); + + let mut request = Request::new(GetAuthTokenRequest { + user_credentials: None, + }); + + let refresh_token = Token::new(TokenType::RefreshToken, "1").unwrap(); + + request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str(format!("Bearer {}", refresh_token).as_str()).unwrap(), + ); + + mock_services + .session_context_mock + .expect_get_by_token() + .returning(move |_, _| { + Ok(Option::from(session::Model { + id: 0, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id: 1, + })) + }); + + mock_services + .session_context_mock + .expect_update() + .returning(move |_| { + Ok(session::Model { + id: 0, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id: 1, + }) + }); + + let api = get_mock_concrete_ecdar_api(mock_services); + + let response = api.get_auth_token(request).await.unwrap(); + + assert!(!response.get_ref().refresh_token.is_empty()); + assert!(!response.get_ref().access_token.is_empty()); +} + +#[tokio::test] +async fn get_auth_token_from_invalid_token_returns_err() { + let mock_services = get_mock_services(); + + let mut request = Request::new(GetAuthTokenRequest { + user_credentials: None, + }); + + request.metadata_mut().insert( + "authorization", + metadata::MetadataValue::from_str("invalid token").unwrap(), + ); + + let api = get_mock_concrete_ecdar_api(mock_services); + + let response = api.get_auth_token(request).await; + + assert_eq!(response.unwrap_err().code(), Code::Unauthenticated); +} From cd29928b1871ce6da339796af08e580c4456fe50 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Tue, 5 Dec 2023 14:28:10 +0100 Subject: [PATCH 26/41] Add GetUsers rpc to protobuff --- Ecdar-ProtoBuf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ecdar-ProtoBuf b/Ecdar-ProtoBuf index 6600b15..3d5326c 160000 --- a/Ecdar-ProtoBuf +++ b/Ecdar-ProtoBuf @@ -1 +1 @@ -Subproject commit 6600b155ef58ca57187c28d7f570bf59bc9e4f6b +Subproject commit 3d5326cd4d1ced6b8cd86b3d7d73b728c27ac807 From 8aa7e93db02021ec6e45c02bd7131849b747c4a7 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Tue, 5 Dec 2023 14:46:28 +0100 Subject: [PATCH 27/41] WIP: Fix session_logic test --- src/api/auth.rs | 2 +- src/tests/api/session_logic.rs | 114 +++------------------------------ 2 files changed, 10 insertions(+), 106 deletions(-) diff --git a/src/api/auth.rs b/src/api/auth.rs index 2f85bcb..002b59e 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -75,7 +75,7 @@ impl TokenType { /// assert_eq!(token.token_type(), TokenType::AccessToken); /// assert_eq!(token.to_string(), token.as_str()); /// ``` - +#[derive(Debug)] pub struct Token { token_type: TokenType, token: String, diff --git a/src/tests/api/session_logic.rs b/src/tests/api/session_logic.rs index 4513531..65276f7 100644 --- a/src/tests/api/session_logic.rs +++ b/src/tests/api/session_logic.rs @@ -1,72 +1,15 @@ +use crate::api::auth::TokenType; +use crate::api::ecdar_api::update_session; use crate::api::server::server::GetAuthTokenRequest; -use crate::api::{auth::TokenType, ecdar_api::handle_session}; use crate::entities::session; use crate::tests::api::helpers::get_mock_services; use mockall::predicate; use sea_orm::DbErr; -use std::str::FromStr; use std::sync::Arc; use tonic::{metadata, Code, Request}; #[tokio::test] -async fn handle_session_updated_session_contains_correct_fields_returns_ok() { - let mut mock_services = get_mock_services(); - - let old_session = session::Model { - id: 1, - refresh_token: "old_refresh_token".to_string(), - access_token: "old_access_token".to_string(), - updated_at: Default::default(), - user_id: 1, - }; - - let new_session = session::Model { - id: 1, - refresh_token: "new_refresh_token".to_string(), - access_token: "new_access_token".to_string(), - updated_at: Default::default(), - user_id: 1, - }; - - mock_services - .session_context_mock - .expect_get_by_token() - .with( - predicate::eq(TokenType::RefreshToken), - predicate::eq("old_refresh_token".to_string()), - ) - .returning(move |_, _| Ok(Some(old_session.clone()))); - - mock_services - .session_context_mock - .expect_update() - .with(predicate::eq(new_session.clone())) - .returning(move |_| Ok(new_session.clone())); - - let mut get_auth_token_request = Request::new(GetAuthTokenRequest { - user_credentials: None, - }); - - get_auth_token_request.metadata_mut().insert( - "authorization", - metadata::MetadataValue::from_str("Bearer old_refresh_token").unwrap(), - ); - - let res = handle_session( - Arc::new(mock_services.session_context_mock), - &get_auth_token_request, - false, - "new_access_token".to_string(), - "new_refresh_token".to_string(), - "1".to_string(), - ) - .await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn handle_session_no_session_exists_creates_session_returns_ok() { +async fn update_session_no_session_exists_creates_session_returns_err() { let mut mock_services = get_mock_services(); let session = session::Model { @@ -79,58 +22,19 @@ async fn handle_session_no_session_exists_creates_session_returns_ok() { mock_services .session_context_mock - .expect_create() - .with(predicate::eq(session.clone())) - .returning(move |_| Ok(session.clone())); - - let get_auth_token_request = Request::new(GetAuthTokenRequest { - user_credentials: None, - }); - - let res = handle_session( - Arc::new(mock_services.session_context_mock), - &get_auth_token_request, - true, - "new_access_token".to_string(), - "new_refresh_token".to_string(), - "1".to_string(), - ) - .await; - - assert!(res.is_ok()); -} - -#[tokio::test] -async fn handle_session_no_session_exists_creates_session_returns_err() { - let mut mock_services = get_mock_services(); - - let session = session::Model { - id: Default::default(), - refresh_token: "new_refresh_token".to_string(), - access_token: "new_access_token".to_string(), - updated_at: Default::default(), - user_id: 1, - }; + .expect_get_by_token() + .returning(move |_, _| Ok(None)); mock_services .session_context_mock - .expect_create() - .with(predicate::eq(session.clone())) + .expect_update() .returning(move |_| Err(DbErr::RecordNotInserted)); - let get_auth_token_request = Request::new(GetAuthTokenRequest { - user_credentials: None, - }); - - let res = handle_session( + let res = update_session( Arc::new(mock_services.session_context_mock), - &get_auth_token_request, - true, - "new_access_token".to_string(), - "new_refresh_token".to_string(), - "1".to_string(), + "old_refresh_token".to_string(), ) .await; - assert_eq!(res.unwrap_err().code(), Code::Internal); + assert_eq!(res.unwrap_err().code(), Code::Unauthenticated); } From 285136ae4506a8a1999f0bec6895d19a9a58eaaf Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 08:17:55 +0100 Subject: [PATCH 28/41] Add session_logic tests --- src/tests/api/session_logic.rs | 90 +++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/src/tests/api/session_logic.rs b/src/tests/api/session_logic.rs index 65276f7..e3a58d7 100644 --- a/src/tests/api/session_logic.rs +++ b/src/tests/api/session_logic.rs @@ -1,25 +1,16 @@ -use crate::api::auth::TokenType; use crate::api::ecdar_api::update_session; -use crate::api::server::server::GetAuthTokenRequest; + use crate::entities::session; use crate::tests::api::helpers::get_mock_services; -use mockall::predicate; + use sea_orm::DbErr; use std::sync::Arc; -use tonic::{metadata, Code, Request}; +use tonic::Code; #[tokio::test] async fn update_session_no_session_exists_creates_session_returns_err() { let mut mock_services = get_mock_services(); - let session = session::Model { - id: Default::default(), - refresh_token: "new_refresh_token".to_string(), - access_token: "new_access_token".to_string(), - updated_at: Default::default(), - user_id: 1, - }; - mock_services .session_context_mock .expect_get_by_token() @@ -38,3 +29,78 @@ async fn update_session_no_session_exists_creates_session_returns_err() { assert_eq!(res.unwrap_err().code(), Code::Unauthenticated); } + +#[tokio::test] +async fn update_session_returns_new_tokens_when_session_exists() { + let mut mock_services = get_mock_services(); + let refresh_token = "refresh_token".to_string(); + + mock_services + .session_context_mock + .expect_get_by_token() + .times(1) + .returning(|_, _| { + Ok(Some(session::Model { + id: 0, + access_token: "old_access_token".to_string(), + refresh_token: "old_refresh_token".to_string(), + updated_at: Default::default(), + user_id: 1, + })) + }); + + mock_services + .session_context_mock + .expect_update() + .times(1) + .returning(move |_| { + Ok(session::Model { + id: 0, + refresh_token: "refresh_token".to_string(), + access_token: "access_token".to_string(), + updated_at: Default::default(), + user_id: 1, + }) + }); + + let result = update_session(Arc::new(mock_services.session_context_mock), refresh_token).await; + + assert!(result.is_ok()); + let (access_token, refresh_token) = result.unwrap(); + assert_ne!(access_token.to_string(), "old_access_token"); + assert_ne!(refresh_token.to_string(), "old_refresh_token"); +} + +#[tokio::test] +async fn update_session_returns_error_when_no_session_found() { + let mut mock_services = get_mock_services(); + let refresh_token = "refresh_token".to_string(); + + mock_services + .session_context_mock + .expect_get_by_token() + .times(1) + .returning(|_, _| Ok(None)); + + let result = update_session(Arc::new(mock_services.session_context_mock), refresh_token).await; + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().code(), Code::Unauthenticated); +} + +#[tokio::test] +async fn update_session_returns_error_when_database_error_occurs() { + let mut mock_services = get_mock_services(); + let refresh_token = "refresh_token".to_string(); + + mock_services + .session_context_mock + .expect_get_by_token() + .times(1) + .returning(|_, _| Err(DbErr::RecordNotFound("".to_string()))); + + let result = update_session(Arc::new(mock_services.session_context_mock), refresh_token).await; + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().code(), Code::Internal); +} From 9cf4f538053696ffb845cc548eca011d1f2889c0 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 08:27:43 +0100 Subject: [PATCH 29/41] Add repo-token to Install Protoc step to prevent rate limit --- .github/workflows/check_format.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/check_format.yml b/.github/workflows/check_format.yml index e15a762..83e5ef9 100644 --- a/.github/workflows/check_format.yml +++ b/.github/workflows/check_format.yml @@ -11,6 +11,8 @@ jobs: steps: - name: Install Protoc uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/checkout@v3 with: submodules: 'true' From 71a8a0b757880106a66ae28fb469ab6a44aa57a6 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 08:30:01 +0100 Subject: [PATCH 30/41] Add repo-token to Install Protoc step to prevent rate limit --- .github/workflows/build.yml | 2 ++ .github/workflows/test.yml | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ffb6abd..afc2a04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,8 @@ jobs: submodules: true - name: Install Protoc uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - uses: Swatinem/rust-cache@v2 with: cache-on-failure: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b0f69c1..8aa3ff6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,8 @@ jobs: - name: Install dependencies uses: arduino/setup-protoc@v2 - + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - uses: Swatinem/rust-cache@v2 with: cache-on-failure: true From 6399e239643995e65937274e03e1c1f47dabb4cb Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 08:38:07 +0100 Subject: [PATCH 31/41] Merge main --- Ecdar-ProtoBuf | 2 +- src/api/ecdar_api.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Ecdar-ProtoBuf b/Ecdar-ProtoBuf index 3d5326c..6626d09 160000 --- a/Ecdar-ProtoBuf +++ b/Ecdar-ProtoBuf @@ -1 +1 @@ -Subproject commit 3d5326cd4d1ced6b8cd86b3d7d73b728c27ac807 +Subproject commit 6626d096d9364f6c682a96b082c2f00d25d6419f diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index b719790..915b8ed 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -6,8 +6,8 @@ use super::server::server::{ get_users_response::UserInfo, CreateAccessRequest, CreateModelRequest, CreateModelResponse, CreateQueryRequest, CreateUserRequest, DeleteAccessRequest, DeleteModelRequest, DeleteQueryRequest, - GetAuthTokenRequest, GetAuthTokenResponse, GetModelRequest, GetModelResponse, - ListModelsInfoResponse, Query, QueryRequest, QueryResponse, SendQueryRequest, + GetAuthTokenRequest, GetAuthTokenResponse, GetModelRequest, GetModelResponse, GetUsersRequest, + GetUsersResponse, ListModelsInfoResponse, Query, QueryRequest, QueryResponse, SendQueryRequest, SendQueryResponse, SimulationStartRequest, SimulationStepRequest, SimulationStepResponse, UpdateAccessRequest, UpdateModelRequest, UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, @@ -332,7 +332,7 @@ impl EcdarApi for ConcreteEcdarApi { Ok(None) => { return Err(Status::unauthenticated( "No session found with given access token", - )) + )); } Err(error) => return Err(Status::internal(error.to_string())), }; From 1a7afbe035dac0acef3144eae540ae680cc31c69 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 09:50:28 +0100 Subject: [PATCH 32/41] Add delete_by_token CRUD to session_context --- src/database/session_context.rs | 22 ++++++++++++++ src/tests/api/helpers.rs | 1 + src/tests/database/session_context.rs | 44 +++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/src/database/session_context.rs b/src/database/session_context.rs index 4709fc2..28d111c 100644 --- a/src/database/session_context.rs +++ b/src/database/session_context.rs @@ -2,6 +2,7 @@ use crate::api::auth::TokenType; use crate::database::database_context::DatabaseContextTrait; use crate::database::entity_context::EntityContextTrait; use crate::entities::session; +use crate::entities::session::Model; use chrono::Local; use sea_orm::prelude::async_trait::async_trait; use sea_orm::ActiveValue::{Set, Unchanged}; @@ -19,6 +20,12 @@ pub trait SessionContextTrait: EntityContextTrait { token_type: TokenType, token: String, ) -> Result, DbErr>; + + async fn delete_by_token( + &self, + token_type: TokenType, + token: String, + ) -> Result; } #[async_trait] @@ -43,6 +50,21 @@ impl SessionContextTrait for SessionContext { } } } + + async fn delete_by_token(&self, token_type: TokenType, token: String) -> Result { + let session = self + .get_by_token(token_type, token) + .await? + .ok_or(DbErr::RecordNotFound( + "No session found with the provided access token".into(), + ))?; + + session::Entity::delete_by_id(session.id) + .exec(&self.db_context.get_connection()) + .await?; + + Ok(session) + } } impl SessionContext { diff --git a/src/tests/api/helpers.rs b/src/tests/api/helpers.rs index 7ccc3c7..0d4edb2 100644 --- a/src/tests/api/helpers.rs +++ b/src/tests/api/helpers.rs @@ -146,6 +146,7 @@ mock! { #[async_trait] impl SessionContextTrait for SessionContext { async fn get_by_token(&self, token_type: TokenType, token: String) -> Result, DbErr>; + async fn delete_by_token(&self, token_type: TokenType, token: String) -> Result; } } diff --git a/src/tests/database/session_context.rs b/src/tests/database/session_context.rs index 12d05fd..95d022f 100644 --- a/src/tests/database/session_context.rs +++ b/src/tests/database/session_context.rs @@ -376,3 +376,47 @@ async fn get_by_token_access_test() { assert_eq!(fetched_session.unwrap().access_token, session.access_token); } + +#[tokio::test] +async fn delete_by_token_refresh_test() { + let (session_context, session, _, _) = seed_db().await; + + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + session_context + .delete_by_token(TokenType::RefreshToken, session.refresh_token.clone()) + .await + .unwrap(); + + let fetched_session = session_context + .get_by_token(TokenType::RefreshToken, session.refresh_token.clone()) + .await + .unwrap(); + + assert!(fetched_session.is_none()); +} + +#[tokio::test] +async fn delete_by_token_access_test() { + let (session_context, session, _, _) = seed_db().await; + + session::Entity::insert(session.clone().into_active_model()) + .exec(&session_context.db_context.get_connection()) + .await + .unwrap(); + + session_context + .delete_by_token(TokenType::AccessToken, session.access_token.clone()) + .await + .unwrap(); + + let fetched_session = session_context + .get_by_token(TokenType::AccessToken, session.access_token.clone()) + .await + .unwrap(); + + assert!(fetched_session.is_none()); +} From 0dd067731e5caaee84a59356bbdd0b7a15e24e39 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 11:00:47 +0100 Subject: [PATCH 33/41] fix: Delete session when token is expired --- src/api/ecdar_api.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index 915b8ed..ed8b01f 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -12,6 +12,7 @@ use super::server::server::{ UpdateAccessRequest, UpdateModelRequest, UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, }; +use crate::api::auth::{Claims, TokenError}; use crate::api::context_collection::ContextCollection; use crate::api::{ auth::{RequestExt, Token, TokenType}, @@ -20,6 +21,7 @@ use crate::api::{ use crate::database::{session_context::SessionContextTrait, user_context::UserContextTrait}; use crate::entities::{access, in_use, model, query, session, user}; use chrono::{Duration, Utc}; +use jsonwebtoken::TokenData; use regex::Regex; use sea_orm::{DbErr, SqlErr}; use serde_json; @@ -886,7 +888,21 @@ impl EcdarApiAuth for ConcreteEcdarApi { .ok_or(Status::unauthenticated("No refresh token provided"))?, ); - refresh_token.validate()?; + // Validate refresh token + match refresh_token.validate() { + Ok(_) => (), + Err(TokenError::ExpiredSignature) => { + // Delete session if expired + let _ = self + .contexts + .session_context + .delete_by_token(TokenType::RefreshToken, refresh_token.to_string()) + .await; + + return Err(Status::from(TokenError::ExpiredSignature)); + } + Err(err) => return Err(Status::from(err)), + } update_session( self.contexts.session_context.clone(), From a1c30db1c241afd012a4db3b6376c6e0be942619 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 11:01:09 +0100 Subject: [PATCH 34/41] Fix TokenError --- src/api/auth.rs | 17 +++++++---------- src/tests/api/auth.rs | 5 +++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/api/auth.rs b/src/api/auth.rs index 002b59e..9e2c925 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -231,38 +231,35 @@ impl Display for Token { #[derive(Debug, PartialEq, thiserror::Error)] pub enum TokenError { #[error("Invalid token")] - #[from(jsonwebtoken::errors::ErrorKind::InvalidToken)] InvalidToken, #[error("Invalid signature")] - #[from(jsonwebtoken::errors::ErrorKind::InvalidSignature)] InvalidSignature, #[error("Expired signature")] - #[from(jsonwebtoken::errors::ErrorKind::ExpiredSignature)] ExpiredSignature, #[error("{0}")] - Custom(String), + Unknown(String), } -// This is used to convert the jsonwebtoken error kind to a [TokenError]. +/// This is used to convert a [jsonwebtoken::errors::ErrorKind] to a [TokenError]. impl From for TokenError { - fn from(error: jsonwebtoken::errors::ErrorKind) -> Self { - match error { + fn from(error_kind: jsonwebtoken::errors::ErrorKind) -> Self { + match error_kind { jsonwebtoken::errors::ErrorKind::InvalidToken => TokenError::InvalidToken, jsonwebtoken::errors::ErrorKind::InvalidSignature => TokenError::InvalidSignature, jsonwebtoken::errors::ErrorKind::ExpiredSignature => TokenError::ExpiredSignature, - _ => TokenError::Custom("Failed to validate token".to_string()), + _ => TokenError::Unknown("Unknown token error".to_string()), } } } -/// This is used to convert the jsonwebtoken error to a [TokenError]. +/// This is used to convert a [jsonwebtoken::errors::Error] to a [TokenError]. impl From for TokenError { fn from(error: jsonwebtoken::errors::Error) -> Self { TokenError::from(error.kind().clone()) } } -/// This is used to convert the [TokenError] to a [Status]. +/// This is used to convert a [TokenError] to a [Status]. impl From for Status { fn from(error: TokenError) -> Self { Status::unauthenticated(error.to_string()) diff --git a/src/tests/api/auth.rs b/src/tests/api/auth.rs index c5b9a67..ad57a23 100644 --- a/src/tests/api/auth.rs +++ b/src/tests/api/auth.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod auth { - use crate::api::auth::{RequestExt, Token, TokenType}; + use crate::api::auth::{RequestExt, Token, TokenError, TokenType}; use std::{env, str::FromStr}; use tonic::{metadata::MetadataValue, Request}; @@ -73,7 +73,8 @@ mod auth { let result_access = Token::from_str(TokenType::AccessToken, "invalid_token").validate(); let result_refresh = Token::from_str(TokenType::RefreshToken, "invalid_token").validate(); - assert!(result_access.is_err() && result_refresh.is_err()); + assert_eq!(result_access.unwrap_err(), TokenError::InvalidToken); + assert_eq!(result_refresh.unwrap_err(), TokenError::InvalidToken); } #[tokio::test] From 2c2d70b58d8b591b155f035ffe3ccb1edb138fb5 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 11:05:03 +0100 Subject: [PATCH 35/41] Organize functions --- src/api/ecdar_api.rs | 109 ++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index ed8b01f..1678f15 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -35,56 +35,6 @@ pub struct ConcreteEcdarApi { contexts: ContextCollection, } -/// Updates the session given by refresh token in the database. -/// Returns the new access and refresh token. -pub async fn update_session( - session_context: Arc, - refresh_token: String, -) -> Result<(Token, Token), Status> { - let session = match session_context - .get_by_token(TokenType::RefreshToken, refresh_token) - .await - { - Ok(Some(session)) => session, - Ok(None) => { - return Err(Status::unauthenticated( - "No session found with given refresh token", - )); - } - Err(err) => return Err(Status::internal(err.to_string())), - }; - - let uid = session.user_id.to_string(); - - let access_token = Token::access(&uid)?; - let refresh_token = Token::refresh(&uid)?; - - session_context - .update(session::Model { - id: session.id, - access_token: access_token.to_string(), - refresh_token: refresh_token.to_string(), - updated_at: Default::default(), - user_id: session.user_id, - }) - .await - .unwrap(); - - Ok((access_token, refresh_token)) -} - -fn is_valid_email(email: &str) -> bool { - Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") - .unwrap() - .is_match(email) -} - -fn is_valid_username(username: &str) -> bool { - Regex::new(r"^[a-zA-Z0-9_]{3,32}$") - .unwrap() - .is_match(username) -} - impl ConcreteEcdarApi { pub fn new(contexts: ContextCollection) -> Self { ConcreteEcdarApi { contexts } @@ -865,6 +815,57 @@ async fn user_from_user_credentials( } } +/// Updates the session given by refresh token in the database. +/// Returns the new access and refresh token. +pub async fn update_session( + session_context: Arc, + refresh_token: String, +) -> Result<(Token, Token), Status> { + let session = match session_context + .get_by_token(TokenType::RefreshToken, refresh_token) + .await + { + Ok(Some(session)) => session, + Ok(None) => { + return Err(Status::unauthenticated( + "No session found with given refresh token", + )); + } + Err(err) => return Err(Status::internal(err.to_string())), + }; + + let uid = session.user_id.to_string(); + + let access_token = Token::access(&uid)?; + let refresh_token = Token::refresh(&uid)?; + + session_context + .update(session::Model { + id: session.id, + access_token: access_token.to_string(), + refresh_token: refresh_token.to_string(), + updated_at: Default::default(), + user_id: session.user_id, + }) + .await + .unwrap(); + + Ok((access_token, refresh_token)) +} + +fn is_valid_email(email: &str) -> bool { + Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") + .unwrap() + .is_match(email) +} + +fn is_valid_username(username: &str) -> bool { + Regex::new(r"^[a-zA-Z0-9_]{3,32}$") + .unwrap() + .is_match(username) +} + + #[tonic::async_trait] impl EcdarApiAuth for ConcreteEcdarApi { /// This method is used to get a new access and refresh token for a user. @@ -908,7 +909,7 @@ impl EcdarApiAuth for ConcreteEcdarApi { self.contexts.session_context.clone(), refresh_token.to_string(), ) - .await? + .await? } Some(user_credentials) => { let input_password = user_credentials.password.clone(); @@ -916,9 +917,9 @@ impl EcdarApiAuth for ConcreteEcdarApi { self.contexts.user_context.clone(), user_credentials, ) - .await - .map_err(|err| Status::internal(err.to_string()))? - .ok_or_else(|| Status::unauthenticated("Wrong username or password"))?; + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::unauthenticated("Wrong username or password"))?; // Check if password in request matches users password if !self From bbffbec1d3ba6397511640320dc80ee43b3f43e3 Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 11:52:30 +0100 Subject: [PATCH 36/41] Add comments to functions --- src/api/ecdar_api.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index 1678f15..fe40fdf 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -816,7 +816,7 @@ async fn user_from_user_credentials( } /// Updates the session given by refresh token in the database. -/// Returns the new access and refresh token. +/// Returns the new access and refresh token i.e. a tuple `(Token, Token)` where the 0th element is the access token and the 1st element refresh token. pub async fn update_session( session_context: Arc, refresh_token: String, @@ -853,12 +853,14 @@ pub async fn update_session( Ok((access_token, refresh_token)) } +/// Returns true if the given email is a valid format. fn is_valid_email(email: &str) -> bool { Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") .unwrap() .is_match(email) } +/// Returns true if the given username is a valid format, i.e. only contains letters and numbers and a length from 3 to 32. fn is_valid_username(username: &str) -> bool { Regex::new(r"^[a-zA-Z0-9_]{3,32}$") .unwrap() From 1429c5410b5e21a4ca807de1da8d6aa88b33101f Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 11:53:55 +0100 Subject: [PATCH 37/41] fmt --- src/api/ecdar_api.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index fe40fdf..0af9ee1 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -12,7 +12,7 @@ use super::server::server::{ UpdateAccessRequest, UpdateModelRequest, UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, }; -use crate::api::auth::{Claims, TokenError}; +use crate::api::auth::TokenError; use crate::api::context_collection::ContextCollection; use crate::api::{ auth::{RequestExt, Token, TokenType}, @@ -21,7 +21,7 @@ use crate::api::{ use crate::database::{session_context::SessionContextTrait, user_context::UserContextTrait}; use crate::entities::{access, in_use, model, query, session, user}; use chrono::{Duration, Utc}; -use jsonwebtoken::TokenData; + use regex::Regex; use sea_orm::{DbErr, SqlErr}; use serde_json; @@ -867,7 +867,6 @@ fn is_valid_username(username: &str) -> bool { .is_match(username) } - #[tonic::async_trait] impl EcdarApiAuth for ConcreteEcdarApi { /// This method is used to get a new access and refresh token for a user. @@ -911,7 +910,7 @@ impl EcdarApiAuth for ConcreteEcdarApi { self.contexts.session_context.clone(), refresh_token.to_string(), ) - .await? + .await? } Some(user_credentials) => { let input_password = user_credentials.password.clone(); @@ -919,9 +918,9 @@ impl EcdarApiAuth for ConcreteEcdarApi { self.contexts.user_context.clone(), user_credentials, ) - .await - .map_err(|err| Status::internal(err.to_string()))? - .ok_or_else(|| Status::unauthenticated("Wrong username or password"))?; + .await + .map_err(|err| Status::internal(err.to_string()))? + .ok_or_else(|| Status::unauthenticated("Wrong username or password"))?; // Check if password in request matches users password if !self From 6af2fc86e1909994a2dcc3525029fcda70fd3550 Mon Sep 17 00:00:00 2001 From: sabotack Date: Wed, 6 Dec 2023 12:01:31 +0100 Subject: [PATCH 38/41] cargo fmt --- src/api/ecdar_api.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index c4183fd..484ec25 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -7,9 +7,10 @@ use super::server::server::{ CreateAccessRequest, CreateModelRequest, CreateModelResponse, CreateQueryRequest, CreateUserRequest, DeleteAccessRequest, DeleteModelRequest, DeleteQueryRequest, GetAuthTokenRequest, GetAuthTokenResponse, GetModelRequest, GetModelResponse, - ListModelsInfoResponse, Query, QueryRequest, QueryResponse, SimulationStartRequest, + ListAccessInfoRequest, ListAccessInfoResponse, ListModelsInfoResponse, Query, QueryRequest, + QueryResponse, SendQueryRequest, SendQueryResponse, SimulationStartRequest, SimulationStepRequest, SimulationStepResponse, UpdateAccessRequest, UpdateModelRequest, - UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, ListAccessInfoRequest, ListAccessInfoResponse, SendQueryRequest, SendQueryResponse, + UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, }; use crate::api::{ auth::{RequestExt, Token, TokenType}, From 37eef9a8a8ed6388358a39c45408dca7b939030d Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 13:48:56 +0100 Subject: [PATCH 39/41] Fix imports --- src/api/ecdar_api.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/api/ecdar_api.rs b/src/api/ecdar_api.rs index 29fa7bc..ff02ede 100644 --- a/src/api/ecdar_api.rs +++ b/src/api/ecdar_api.rs @@ -3,19 +3,17 @@ use super::server::server::{ ecdar_api_server::EcdarApi, ecdar_backend_server::EcdarBackend, get_auth_token_request::{user_credentials, UserCredentials}, - get_users_response::UserInfo, - CreateAccessRequest, CreateAccessRequest, CreateProjectRequest, CreateProjectResponse, - CreateQueryRequest, CreateQueryRequest, CreateUserRequest, CreateUserRequest, - DeleteAccessRequest, DeleteAccessRequest, DeleteProjectRequest, DeleteQueryRequest, - DeleteQueryRequest, GetAuthTokenRequest, GetAuthTokenRequest, GetAuthTokenResponse, - GetAuthTokenResponse, GetProjectRequest, GetProjectResponse, GetUsersRequest, GetUsersResponse, - ListProjectsInfoResponse, Query, Query, QueryRequest, QueryRequest, QueryResponse, - QueryResponse, SendQueryRequest, SendQueryRequest, SendQueryResponse, SimulationStartRequest, + CreateAccessRequest, CreateProjectRequest, CreateProjectResponse, CreateQueryRequest, + CreateUserRequest, DeleteAccessRequest, DeleteProjectRequest, DeleteQueryRequest, + GetAuthTokenRequest, GetAuthTokenResponse, GetProjectRequest, GetProjectResponse, + GetUsersRequest, GetUsersResponse, ListProjectsInfoResponse, Query, QueryRequest, + QueryResponse, SendQueryRequest, SendQueryResponse, SimulationStartRequest, SimulationStepRequest, SimulationStepResponse, UpdateAccessRequest, UpdateProjectRequest, UpdateQueryRequest, UpdateUserRequest, UserTokenResponse, }; use crate::api::auth::TokenError; use crate::api::context_collection::ContextCollection; +use crate::api::server::server::get_users_response::UserInfo; use crate::api::{ auth::{RequestExt, Token, TokenType}, server::server::Project, @@ -23,7 +21,6 @@ use crate::api::{ use crate::database::{session_context::SessionContextTrait, user_context::UserContextTrait}; use crate::entities::{access, in_use, project, query, session, user}; use chrono::{Duration, Utc}; - use regex::Regex; use sea_orm::{DbErr, SqlErr}; use serde_json; From 967f0b9fca80b006ba19d04128c4fcb1372f504c Mon Sep 17 00:00:00 2001 From: Mads Risager Date: Wed, 6 Dec 2023 14:01:31 +0100 Subject: [PATCH 40/41] Update submod --- Ecdar-ProtoBuf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ecdar-ProtoBuf b/Ecdar-ProtoBuf index c7e1155..285004c 160000 --- a/Ecdar-ProtoBuf +++ b/Ecdar-ProtoBuf @@ -1 +1 @@ -Subproject commit c7e115515797e9e6f39990f04e4fb6d41d73a2bd +Subproject commit 285004c6a2b310f82a1a6c045933246046499e98 From 47808cd91f97ebb26a4ba2693579c3a9d0650bd4 Mon Sep 17 00:00:00 2001 From: williamwoldum Date: Wed, 6 Dec 2023 15:23:34 +0100 Subject: [PATCH 41/41] Fixed import typo --- src/database/session_context.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/database/session_context.rs b/src/database/session_context.rs index 28d111c..5ef3f01 100644 --- a/src/database/session_context.rs +++ b/src/database/session_context.rs @@ -2,7 +2,6 @@ use crate::api::auth::TokenType; use crate::database::database_context::DatabaseContextTrait; use crate::database::entity_context::EntityContextTrait; use crate::entities::session; -use crate::entities::session::Model; use chrono::Local; use sea_orm::prelude::async_trait::async_trait; use sea_orm::ActiveValue::{Set, Unchanged}; @@ -51,7 +50,11 @@ impl SessionContextTrait for SessionContext { } } - async fn delete_by_token(&self, token_type: TokenType, token: String) -> Result { + async fn delete_by_token( + &self, + token_type: TokenType, + token: String, + ) -> Result { let session = self .get_by_token(token_type, token) .await?