From 9ebd7b57c2904b48808d82359e07629f12a657a3 Mon Sep 17 00:00:00 2001 From: Mario Date: Fri, 31 May 2024 19:13:35 +0200 Subject: [PATCH] feat: [#448] new authorization service --- src/app.rs | 16 ++- src/common.rs | 7 +- src/services/authorization.rs | 199 ++++++++++++++++++++++++++++++++++ src/services/mod.rs | 1 + 4 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 src/services/authorization.rs diff --git a/src/app.rs b/src/app.rs index 761fbb3a..2272dd4f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,8 +16,8 @@ use crate::services::torrent::{ DbCanonicalInfoHashGroupRepository, DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository, }; -use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository}; -use crate::services::{proxy, settings, torrent}; +use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository, Repository}; +use crate::services::{authorization, proxy, settings, torrent}; use crate::tracker::statistics_importer::StatisticsImporter; use crate::web::api::server::signals::Halted; use crate::web::api::server::v1::auth::Authentication; @@ -74,7 +74,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running // Repositories let category_repository = Arc::new(DbCategoryRepository::new(database.clone())); let tag_repository = Arc::new(DbTagRepository::new(database.clone())); - let user_repository = Arc::new(DbUserRepository::new(database.clone())); + let user_repository: Arc> = Arc::new(Box::new(DbUserRepository::new(database.clone()))); let user_authentication_repository = Arc::new(DbUserAuthenticationRepository::new(database.clone())); let user_profile_repository = Arc::new(DbUserProfileRepository::new(database.clone())); let torrent_repository = Arc::new(DbTorrentRepository::new(database.clone())); @@ -87,15 +87,19 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running let banned_user_list = Arc::new(DbBannedUserList::new(database.clone())); // Services + let authorization_service = Arc::new(authorization::Service::new(user_repository.clone())); let tracker_service = Arc::new(tracker::service::Service::new(configuration.clone(), database.clone()).await); let tracker_statistics_importer = Arc::new(StatisticsImporter::new(configuration.clone(), tracker_service.clone(), database.clone()).await); let mailer_service = Arc::new(mailer::Service::new(configuration.clone()).await); let image_cache_service: Arc = Arc::new(ImageCacheService::new(configuration.clone()).await); - let category_service = Arc::new(category::Service::new(category_repository.clone(), user_repository.clone())); - let tag_service = Arc::new(tag::Service::new(tag_repository.clone(), user_repository.clone())); + let category_service = Arc::new(category::Service::new( + category_repository.clone(), + authorization_service.clone(), + )); + let tag_service = Arc::new(tag::Service::new(tag_repository.clone(), authorization_service.clone())); let proxy_service = Arc::new(proxy::Service::new(image_cache_service.clone(), user_repository.clone())); - let settings_service = Arc::new(settings::Service::new(configuration.clone(), user_repository.clone())); + let settings_service = Arc::new(settings::Service::new(configuration.clone(), authorization_service.clone())); let torrent_index = Arc::new(torrent::Index::new( configuration.clone(), tracker_statistics_importer.clone(), diff --git a/src/common.rs b/src/common.rs index 755a775b..7e08e125 100644 --- a/src/common.rs +++ b/src/common.rs @@ -10,11 +10,12 @@ use crate::services::torrent::{ DbCanonicalInfoHashGroupRepository, DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository, DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository, }; -use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository}; +use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, Repository}; use crate::services::{proxy, settings, torrent}; use crate::tracker::statistics_importer::StatisticsImporter; use crate::web::api::server::v1::auth::Authentication; use crate::{mailer, tracker}; + pub type Username = String; pub struct AppData { @@ -30,7 +31,7 @@ pub struct AppData { // Repositories pub category_repository: Arc, pub tag_repository: Arc, - pub user_repository: Arc, + pub user_repository: Arc>, pub user_authentication_repository: Arc, pub user_profile_repository: Arc, pub torrent_repository: Arc, @@ -66,7 +67,7 @@ impl AppData { // Repositories category_repository: Arc, tag_repository: Arc, - user_repository: Arc, + user_repository: Arc>, user_authentication_repository: Arc, user_profile_repository: Arc, torrent_repository: Arc, diff --git a/src/services/authorization.rs b/src/services/authorization.rs new file mode 100644 index 00000000..f49c0716 --- /dev/null +++ b/src/services/authorization.rs @@ -0,0 +1,199 @@ +//! Authorization service. +use std::sync::Arc; + +use super::user::Repository; +use crate::errors::ServiceError; +use crate::models::user::{UserCompact, UserId}; + +pub enum ACTION { + AddCategory, + DeleteCategory, + GetSettings, + GetSettingsSecret, + AddTag, + DeleteTag, +} + +pub struct Service { + user_repository: Arc>, +} + +impl Service { + #[must_use] + pub fn new(user_repository: Arc>) -> Self { + Self { user_repository } + } + + /// # Errors + /// + /// Will return an error if: + /// + /// - There is not any user with the provided `UserId` (when the user id is some). + /// - The user is not authorized to perform the action. + pub async fn authorize(&self, action: ACTION, maybe_user_id: Option) -> Result<(), ServiceError> { + match action { + ACTION::AddCategory + | ACTION::DeleteCategory + | ACTION::GetSettings + | ACTION::GetSettingsSecret + | ACTION::AddTag + | ACTION::DeleteTag => match maybe_user_id { + Some(user_id) => { + let user = self.get_user(user_id).await?; + + if !user.administrator { + return Err(ServiceError::Unauthorized); + } + + Ok(()) + } + None => Err(ServiceError::Unauthorized), + }, + } + } + + async fn get_user(&self, user_id: UserId) -> Result { + self.user_repository.get_compact(&user_id).await + } +} +#[allow(unused_imports)] +#[cfg(test)] +mod test { + use std::str::FromStr; + use std::sync::Arc; + + use mockall::predicate; + + use crate::databases::database; + use crate::errors::ServiceError; + use crate::models::user::{User, UserCompact}; + use crate::services::authorization::{Service, ACTION}; + use crate::services::user::{MockRepository, Repository}; + use crate::web::api::client::v1::random::string; + + #[tokio::test] + async fn a_guest_user_should_not_be_able_to_add_categories() { + let test_user_id = 1; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(|_| Err(ServiceError::UserNotFound)); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!( + service.authorize(ACTION::AddCategory, Some(test_user_id)).await, + Err(ServiceError::UserNotFound) + ); + } + + #[tokio::test] + async fn a_registered_user_should_not_be_able_to_add_categories() { + let test_user_id = 2; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(move |_| { + Ok(UserCompact { + user_id: test_user_id, + username: "non_admin_user".to_string(), + administrator: false, + }) + }); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!( + service.authorize(ACTION::AddCategory, Some(test_user_id)).await, + Err(ServiceError::Unauthorized) + ); + } + + #[tokio::test] + async fn an_admin_user_should_be_able_to_add_categories() { + let test_user_id = 3; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(move |_| { + Ok(UserCompact { + user_id: test_user_id, + username: "admin_user".to_string(), + administrator: true, + }) + }); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!(service.authorize(ACTION::AddCategory, Some(test_user_id)).await, Ok(())); + } + + #[tokio::test] + async fn a_guest_user_should_not_be_able_to_delete_categories() { + let test_user_id = 4; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(|_| Err(ServiceError::UserNotFound)); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!( + service.authorize(ACTION::DeleteCategory, Some(test_user_id)).await, + Err(ServiceError::UserNotFound) + ); + } + + #[tokio::test] + async fn a_registered_user_should_not_be_able_to_delete_categories() { + let test_user_id = 5; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(move |_| { + Ok(UserCompact { + user_id: test_user_id, + username: "non_admin_user".to_string(), + administrator: false, + }) + }); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!( + service.authorize(ACTION::DeleteCategory, Some(test_user_id)).await, + Err(ServiceError::Unauthorized) + ); + } + + #[tokio::test] + async fn an_admin_user_should_be_able_to_delete_categories() { + let test_user_id = 6; + + let mut mock_repository = MockRepository::new(); + mock_repository + .expect_get_compact() + .with(predicate::eq(test_user_id)) + .times(1) + .returning(move |_| { + Ok(UserCompact { + user_id: test_user_id, + username: "admin_user".to_string(), + administrator: true, + }) + }); + + let service = Service::new(Arc::new(Box::new(mock_repository))); + assert_eq!(service.authorize(ACTION::DeleteCategory, Some(test_user_id)).await, Ok(())); + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs index b2431aec..567c35a9 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,6 +1,7 @@ //! App services. pub mod about; pub mod authentication; +pub mod authorization; pub mod category; pub mod hasher; pub mod proxy;