From c1a5c25fd57446f9cf5ea1fd92a15f26e8eed4ec Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 7 Aug 2024 11:12:27 +0100 Subject: [PATCH] feat: [#702] allow overwriting casbin configuration This is an unsatble feature. You can overwrite casbin configuration to change permissions for roles: guest, registered and admin. You can do it by adding this toml file config section: ```toml [unstable.auth.casbin] model = """ [request_definition] r = role, action [policy_definition] p = role, action [policy_effect] e = some(where (p.eft == allow)) [matchers] m = r.role == p.role && r.action == p.action """ policy = """ admin, GetAboutPage admin, GetLicensePage admin, AddCategory admin, DeleteCategory admin, GetCategories admin, GetImageByUrl admin, GetSettings admin, GetSettingsSecret admin, GetPublicSettings admin, AddTag admin, DeleteTag admin, GetTags admin, AddTorrent admin, GetTorrent admin, DeleteTorrent admin, GetTorrentInfo admin, GenerateTorrentInfoListing admin, GetCanonicalInfoHash admin, ChangePassword admin, BanUser registered, GetAboutPage registered, GetLicensePage registered, GetCategories registered, GetImageByUrl registered, GetPublicSettings registered, GetTags registered, AddTorrent registered, GetTorrent registered, GetTorrentInfo registered, GenerateTorrentInfoListing registered, GetCanonicalInfoHash registered, ChangePassword guest, GetAboutPage guest, GetLicensePage guest, GetCategories guest, GetPublicSettings guest, GetTags guest, GetTorrent guest, GetTorrentInfo guest, GenerateTorrentInfoListing guest, GetCanonicalInfoHash """ ``` For example, if you wnat to force users to login to see the torrent list you can remove the following line from the policy: ``` guest, GenerateTorrentInfoListing ``` NOTICE: This is an unstable feature. It will panic with wrong casbin configuration, invalid roles, etcetera. --- project-words.txt | 2 + src/app.rs | 17 +++++++- src/config/v2/mod.rs | 11 +++++ src/config/v2/unstable.rs | 55 ++++++++++++++++++++++++ src/services/authorization.rs | 78 +++++++++++++++++++++++++++-------- 5 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 src/config/v2/unstable.rs diff --git a/project-words.txt b/project-words.txt index c6dbbf95..01908fce 100644 --- a/project-words.txt +++ b/project-words.txt @@ -9,6 +9,7 @@ binascii btih buildx camino +Casbin chrono clippy codecov @@ -51,6 +52,7 @@ luckythelab mailcatcher mandelbrotset metainfo +Mgmt migth nanos NCCA diff --git a/src/app.rs b/src/app.rs index ab9a2066..bba7dac2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,6 +11,7 @@ use crate::config::validator::Validator; use crate::config::Configuration; use crate::databases::database; use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebToken, Service}; +use crate::services::authorization::{CasbinConfiguration, CasbinEnforcer}; use crate::services::category::{self, DbCategoryRepository}; use crate::services::tag::{self, DbTagRepository}; use crate::services::torrent::{ @@ -62,6 +63,8 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running // From [net] config let config_bind_address = settings.net.bind_address; let opt_net_tsl = settings.net.tsl.clone(); + // Unstable config + let unstable = settings.unstable.clone(); // IMPORTANT: drop settings before starting server to avoid read locks that // leads to requests hanging. @@ -87,7 +90,19 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running let torrent_tag_repository = Arc::new(DbTorrentTagRepository::new(database.clone())); let torrent_listing_generator = Arc::new(DbTorrentListingGenerator::new(database.clone())); let banned_user_list = Arc::new(DbBannedUserList::new(database.clone())); - let casbin_enforcer = Arc::new(authorization::CasbinEnforcer::new().await); + let casbin_enforcer = Arc::new( + if let Some(casbin) = unstable + .as_ref() + .and_then(|u| u.auth.as_ref()) + .and_then(|auth| auth.casbin.as_ref()) + { + println!("loading custom"); + CasbinEnforcer::with_configuration(CasbinConfiguration::new(&casbin.model, &casbin.policy)).await + } else { + println!("loading default"); + CasbinEnforcer::with_default_configuration().await + }, + ); // Services let authorization_service = Arc::new(authorization::Service::new(user_repository.clone(), casbin_enforcer.clone())); diff --git a/src/config/v2/mod.rs b/src/config/v2/mod.rs index e5519f56..cf8d185c 100644 --- a/src/config/v2/mod.rs +++ b/src/config/v2/mod.rs @@ -8,11 +8,13 @@ pub mod net; pub mod registration; pub mod tracker; pub mod tracker_statistics_importer; +pub mod unstable; pub mod website; use logging::Logging; use registration::Registration; use serde::{Deserialize, Serialize}; +use unstable::Unstable; use self::api::Api; use self::auth::{Auth, ClaimTokenPepper}; @@ -76,6 +78,10 @@ pub struct Settings { /// The tracker statistics importer job configuration. #[serde(default = "Settings::default_tracker_statistics_importer")] pub tracker_statistics_importer: TrackerStatisticsImporter, + + /// The unstable configuration. + #[serde(default = "Settings::default_unstable")] + pub unstable: Option, } impl Default for Settings { @@ -93,6 +99,7 @@ impl Default for Settings { api: Self::default_api(), registration: Self::default_registration(), tracker_statistics_importer: Self::default_tracker_statistics_importer(), + unstable: Self::default_unstable(), } } } @@ -174,6 +181,10 @@ impl Settings { fn default_tracker_statistics_importer() -> TrackerStatisticsImporter { TrackerStatisticsImporter::default() } + + fn default_unstable() -> Option { + None + } } impl Validator for Settings { diff --git a/src/config/v2/unstable.rs b/src/config/v2/unstable.rs new file mode 100644 index 00000000..437c033a --- /dev/null +++ b/src/config/v2/unstable.rs @@ -0,0 +1,55 @@ +use serde::{Deserialize, Serialize}; + +/// Unstable configuration options. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Unstable { + /// The casbin configuration used for authorization. + #[serde(default = "Unstable::default_auth")] + pub auth: Option, +} + +impl Default for Unstable { + fn default() -> Self { + Self { + auth: Self::default_auth(), + } + } +} + +impl Unstable { + fn default_auth() -> Option { + None + } +} + +/// Unstable auth configuration options. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Auth { + /// The casbin configuration used for authorization. + #[serde(default = "Auth::default_casbin")] + pub casbin: Option, +} + +impl Default for Auth { + fn default() -> Self { + Self { + casbin: Self::default_casbin(), + } + } +} + +impl Auth { + fn default_casbin() -> Option { + None + } +} + +/// Authentication options. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Casbin { + /// The model. See . + pub model: String, + + /// The policy. See . + pub policy: String, +} diff --git a/src/services/authorization.rs b/src/services/authorization.rs index 51d2a0d7..dcca38f8 100644 --- a/src/services/authorization.rs +++ b/src/services/authorization.rs @@ -132,40 +132,84 @@ pub struct CasbinEnforcer { impl CasbinEnforcer { /// # Panics /// - /// It panics if the policy and/or model file cannot be loaded - pub async fn new() -> Self { - let casbin_configuration = CasbinConfiguration::new(); + /// Will panic if: + /// + /// - The enforcer can't be created. + /// - The policies can't be loaded. + pub async fn with_default_configuration() -> Self { + let casbin_configuration = CasbinConfiguration::default(); - let model = DefaultModel::from_str(&casbin_configuration.model) + let mut enforcer = Enforcer::new(casbin_configuration.default_model().await, ()) .await - .expect("Error loading the model"); + .expect("Error creating the enforcer"); - // Converts the policy from a string type to a vector - let policy = casbin_configuration - .policy - .lines() - .filter(|line| !line.trim().is_empty()) - .map(|line| line.split(',').map(|s| s.trim().to_owned()).collect::>()) - .collect(); + enforcer + .add_policies(casbin_configuration.policy_lines()) + .await + .expect("Error loading the policy"); + + let enforcer = Arc::new(RwLock::new(enforcer)); + + Self { enforcer } + } - let mut enforcer = Enforcer::new(model, ()).await.expect("Error creating the enforcer"); + /// # Panics + /// + /// Will panic if: + /// + /// - The enforcer can't be created. + /// - The policies can't be loaded. + pub async fn with_configuration(casbin_configuration: CasbinConfiguration) -> Self { + let mut enforcer = Enforcer::new(casbin_configuration.default_model().await, ()) + .await + .expect("Error creating the enforcer"); - enforcer.add_policies(policy).await.expect("Error loading the policy"); + enforcer + .add_policies(casbin_configuration.policy_lines()) + .await + .expect("Error loading the policy"); let enforcer = Arc::new(RwLock::new(enforcer)); Self { enforcer } } } + #[allow(dead_code)] -struct CasbinConfiguration { +pub struct CasbinConfiguration { model: String, policy: String, } impl CasbinConfiguration { - pub fn new() -> Self { - CasbinConfiguration { + #[must_use] + pub fn new(model: &str, policy: &str) -> Self { + Self { + model: model.to_owned(), + policy: policy.to_owned(), + } + } + + /// # Panics + /// + /// It panics if the model cannot be loaded. + async fn default_model(&self) -> DefaultModel { + DefaultModel::from_str(&self.model).await.expect("Error loading the model") + } + + /// Converts the policy from a string type to a vector. + fn policy_lines(&self) -> Vec> { + self.policy + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| line.split(',').map(|s| s.trim().to_owned()).collect::>()) + .collect() + } +} + +impl Default for CasbinConfiguration { + fn default() -> Self { + Self { model: String::from( " [request_definition]