diff --git a/casbin/policy.csv b/casbin/policy.csv index 88610751..9cf66a8b 100644 --- a/casbin/policy.csv +++ b/casbin/policy.csv @@ -3,4 +3,5 @@ p, true, DeleteCategory p, true, GetSettings p, true, GetSettingsSecret p, true, AddTag -p, true, DeleteTag \ No newline at end of file +p, true, DeleteTag +p, true, DeleteTorrent \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 94d6347c..52debae3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -116,6 +116,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running torrent_announce_url_repository.clone(), torrent_tag_repository.clone(), torrent_listing_generator.clone(), + authorization_service.clone(), )); let registration_service = Arc::new(user::RegistrationService::new( configuration.clone(), diff --git a/src/services/authorization.rs b/src/services/authorization.rs index f49c0716..42fd7ba3 100644 --- a/src/services/authorization.rs +++ b/src/services/authorization.rs @@ -1,10 +1,15 @@ //! Authorization service. use std::sync::Arc; +use casbin::prelude::*; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; + use super::user::Repository; use crate::errors::ServiceError; use crate::models::user::{UserCompact, UserId}; +#[derive(Debug, Clone, Serialize, Deserialize, Hash)] pub enum ACTION { AddCategory, DeleteCategory, @@ -12,188 +17,76 @@ pub enum ACTION { GetSettingsSecret, AddTag, DeleteTag, + DeleteTorrent, + BanUser, } pub struct Service { user_repository: Arc>, + casbin_enforcer: Arc, } impl Service { #[must_use] - pub fn new(user_repository: Arc>) -> Self { - Self { user_repository } + pub fn new(user_repository: Arc>, casbin_enforcer: Arc) -> Self { + Self { + user_repository, + casbin_enforcer, + } } + /// It returns the compact user. + /// /// # Errors /// - /// Will return an error if: + /// It returns an error if there is a database error. + pub async fn get_user(&self, user_id: UserId) -> std::result::Result { + self.user_repository.get_compact(&user_id).await + } + + ///Allows or denies an user to perform an action based on the user's privileges /// - /// - There is not any user with the provided `UserId` (when the user id is some). + /// # Errors + /// + /// Will return an error if: + /// - There is no user_id found in the request + /// - The user_id is not found in the database /// - 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(()) + pub async fn authorize(&self, action: ACTION, maybe_user_id: Option) -> std::result::Result<(), ServiceError> { + match maybe_user_id { + Some(user_id) => { + let user_guard = self.get_user(user_id).await.map_err(|_| ServiceError::UserNotFound); + // the user that wants to access a resource. + let role = user_guard.unwrap().administrator; + + // the user that wants to access a resource. + let sub = role.to_string(); + + let act = action; // the operation that the user performs on the resource. + + let enforcer = self.casbin_enforcer.enforcer.read().await; + /* let enforcer = self.casbin_enforcer.clone(); + let enforcer_lock = enforcer.enforcer.read().await; */ + let authorize = enforcer.enforce((sub, act)).unwrap(); + match authorize { + true => Ok(()), + false => Err(ServiceError::Unauthorized), } - None => Err(ServiceError::Unauthorized), - }, + } + 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, - }) - }); +pub struct CasbinEnforcer { + enforcer: Arc>, //Arc> +} - let service = Service::new(Arc::new(Box::new(mock_repository))); - assert_eq!(service.authorize(ACTION::DeleteCategory, Some(test_user_id)).await, Ok(())); +impl CasbinEnforcer { + pub async fn new() -> Self { + let enforcer = Enforcer::new("casbin/model.conf", "casbin/policy.csv").await.unwrap(); + let enforcer = Arc::new(RwLock::new(enforcer)); + //casbin_enforcer.enable_log(true); + Self { enforcer } } } diff --git a/src/services/torrent.rs b/src/services/torrent.rs index 28aa6223..eb840b7b 100644 --- a/src/services/torrent.rs +++ b/src/services/torrent.rs @@ -5,6 +5,7 @@ use serde_derive::{Deserialize, Serialize}; use tracing::debug; use url::Url; +use super::authorization::{self, ACTION}; use super::category::DbCategoryRepository; use crate::config::{Configuration, TrackerMode}; use crate::databases::database::{Database, Error, Sorting}; @@ -34,6 +35,7 @@ pub struct Index { torrent_announce_url_repository: Arc, torrent_tag_repository: Arc, torrent_listing_generator: Arc, + authorization_service: Arc, } pub struct AddTorrentRequest { @@ -90,6 +92,7 @@ impl Index { torrent_announce_url_repository: Arc, torrent_tag_repository: Arc, torrent_listing_repository: Arc, + authorization_service: Arc, ) -> Self { Self { configuration, @@ -104,6 +107,7 @@ impl Index { torrent_announce_url_repository, torrent_tag_repository, torrent_listing_generator: torrent_listing_repository, + authorization_service, } } @@ -289,13 +293,9 @@ impl Index { /// * Unable to get the torrent listing from it's ID. /// * Unable to delete the torrent from the database. pub async fn delete_torrent(&self, info_hash: &InfoHash, user_id: &UserId) -> Result { - let user = self.user_repository.get_compact(user_id).await?; - - // Only administrator can delete torrents. - // todo: move this to an authorization service. - if !user.administrator { - return Err(ServiceError::Unauthorized); - } + self.authorization_service + .authorize(ACTION::DeleteTorrent, Some(*user_id)) + .await?; let torrent_listing = self.torrent_listing_generator.one_torrent_by_info_hash(info_hash).await?;