Skip to content

Commit

Permalink
feat: [#615] added authorization for delete torrent action
Browse files Browse the repository at this point in the history
  • Loading branch information
mario-nt committed Jul 3, 2024
1 parent e00b0f6 commit 58a33e7
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 171 deletions.
3 changes: 2 additions & 1 deletion casbin/policy.csv
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ p, true, DeleteCategory
p, true, GetSettings
p, true, GetSettingsSecret
p, true, AddTag
p, true, DeleteTag
p, true, DeleteTag
p, true, DeleteTorrent
1 change: 1 addition & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
219 changes: 56 additions & 163 deletions src/services/authorization.rs
Original file line number Diff line number Diff line change
@@ -1,199 +1,92 @@
//! 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,
GetSettings,
GetSettingsSecret,
AddTag,
DeleteTag,
DeleteTorrent,
BanUser,
}

pub struct Service {
user_repository: Arc<Box<dyn Repository>>,
casbin_enforcer: Arc<CasbinEnforcer>,
}

impl Service {
#[must_use]
pub fn new(user_repository: Arc<Box<dyn Repository>>) -> Self {
Self { user_repository }
pub fn new(user_repository: Arc<Box<dyn Repository>>, casbin_enforcer: Arc<CasbinEnforcer>) -> 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<UserCompact, ServiceError> {
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<UserId>) -> 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<UserId>) -> 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<UserCompact, ServiceError> {
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<RwLock<Enforcer>>, //Arc<tokio::sync::RwLock<casbin::Enforcer>>
}

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 }
}
}
14 changes: 7 additions & 7 deletions src/services/torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -34,6 +35,7 @@ pub struct Index {
torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
torrent_tag_repository: Arc<DbTorrentTagRepository>,
torrent_listing_generator: Arc<DbTorrentListingGenerator>,
authorization_service: Arc<authorization::Service>,
}

pub struct AddTorrentRequest {
Expand Down Expand Up @@ -90,6 +92,7 @@ impl Index {
torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
torrent_tag_repository: Arc<DbTorrentTagRepository>,
torrent_listing_repository: Arc<DbTorrentListingGenerator>,
authorization_service: Arc<authorization::Service>,
) -> Self {
Self {
configuration,
Expand All @@ -104,6 +107,7 @@ impl Index {
torrent_announce_url_repository,
torrent_tag_repository,
torrent_listing_generator: torrent_listing_repository,
authorization_service,
}
}

Expand Down Expand Up @@ -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<DeletedTorrentResponse, ServiceError> {
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?;

Expand Down

0 comments on commit 58a33e7

Please sign in to comment.