diff --git a/pmrac/Cargo.toml b/pmrac/Cargo.toml index 7b48fef..c25b857 100644 --- a/pmrac/Cargo.toml +++ b/pmrac/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" argon2 = { version = "0.5.3", features = [ "std" ] } casbin = { version = "2.5.0" } log = { version = "0.4" } +pmrrbac = { version = "0.0.1" } pmrcore = { version = "0.0.1", features = [ "display" ] } thiserror = "1.0" tokio = { version = "1.35", features = [ "fs", "io-util", "macros", "rt" ] } diff --git a/pmrac/src/error.rs b/pmrac/src/error.rs index d5ac7b1..951de10 100644 --- a/pmrac/src/error.rs +++ b/pmrac/src/error.rs @@ -11,6 +11,8 @@ pub enum Error { Authentication(#[from] AuthenticationError), #[error("Misconfiguration Password")] Misconfiguration, + #[error(transparent)] + Rbac(#[from] pmrrbac::error::Error), } #[non_exhaustive] diff --git a/pmrac/src/platform.rs b/pmrac/src/platform.rs index 04e4e2f..8c832a0 100644 --- a/pmrac/src/platform.rs +++ b/pmrac/src/platform.rs @@ -7,6 +7,7 @@ use pmrcore::{ }, platform::ACPlatform }; +use pmrrbac::Builder as PmrRbacBuilder; use std::sync::Arc; use crate::{ @@ -25,16 +26,21 @@ pub struct Builder { ac_platform: Option>, // automatically purges all but the most recent passwords password_autopurge: bool, + pmrrbac_builder: PmrRbacBuilder, } pub struct Platform { ac_platform: Arc, password_autopurge: bool, + pmrrbac_builder: PmrRbacBuilder, } impl Builder { pub fn new() -> Self { - Self::default() + Self { + pmrrbac_builder: PmrRbacBuilder::new(), + .. Default::default() + } } pub fn ac_platform(mut self, val: impl ACPlatform + 'static) -> Self { @@ -47,10 +53,16 @@ impl Builder { self } + pub fn pmrrbac_builder(mut self, val: PmrRbacBuilder) -> Self { + self.pmrrbac_builder = val; + self + } + pub fn build(self) -> Platform { Platform { ac_platform: self.ac_platform.expect("missing required argument ac_platform"), password_autopurge: self.password_autopurge, + pmrrbac_builder: self.pmrrbac_builder } } } @@ -59,11 +71,13 @@ impl Platform { pub fn new( ac_platform: impl ACPlatform + 'static, password_autopurge: bool, + pmrrbac_builder: PmrRbacBuilder, ) -> Self { let ac_platform = Arc::new(ac_platform); Self { ac_platform, password_autopurge, + pmrrbac_builder, } } } @@ -245,3 +259,27 @@ impl Platform { ).await?) } } + +// Enforcement + +impl Platform { + pub async fn enforce( + &self, + agent: impl Into, + res: impl AsRef + ToString, + endpoint_group: impl AsRef, + http_method: &str, + ) -> Result { + Ok(self.pmrrbac_builder + .build_with_resource_policy( + self.generate_policy_for_res(res.to_string()).await?, + ) + .await? + .enforce( + >>::into(agent.into()), + res, + endpoint_group, + http_method, + )?) + } +} diff --git a/pmrac/tests/test_platform.rs b/pmrac/tests/test_platform.rs index f045a9f..a8f2447 100644 --- a/pmrac/tests/test_platform.rs +++ b/pmrac/tests/test_platform.rs @@ -3,19 +3,23 @@ use pmrcore::ac::{ workflow::State, }; use pmrac::{ - Platform, error::{ AuthenticationError, Error, PasswordError, }, password::Password, + platform::Builder, }; +use pmrrbac::Builder as PmrRbacBuilder; -use test_pmr::ac::create_sqlite_platform; +use test_pmr::ac::{ + create_sqlite_backend, + create_sqlite_platform, +}; async fn basic_lifecycle(purge: bool) -> anyhow::Result<()> { - let platform: Platform = create_sqlite_platform(purge).await?; + let platform = create_sqlite_platform(purge).await?; let new_user = platform.create_user("admin").await?; let admin = platform.get_user(new_user.id()).await?; assert_eq!(admin.id(), new_user.id()); @@ -102,7 +106,7 @@ async fn basic_lifecycle_no_autopurge() -> anyhow::Result<()> { } async fn error_handling(purge: bool) -> anyhow::Result<()> { - let platform: Platform = create_sqlite_platform(purge).await?; + let platform = create_sqlite_platform(purge).await?; let new_user = platform.create_user("admin").await?; let admin = platform.get_user(new_user.id()).await?; @@ -132,7 +136,7 @@ async fn error_handling_no_autopurge() -> anyhow::Result<()> { #[async_std::test] async fn policy() -> anyhow::Result<()> { - let platform: Platform = create_sqlite_platform(true).await?; + let platform = create_sqlite_platform(true).await?; let user = platform.create_user("admin").await?; let state = State::Private; let role = Role::Manager; @@ -147,7 +151,7 @@ async fn policy() -> anyhow::Result<()> { #[async_std::test] async fn resource_wf_state() -> anyhow::Result<()> { - let platform: Platform = create_sqlite_platform(true).await?; + let platform = create_sqlite_platform(true).await?; let admin = platform.create_user("admin").await?; let user = platform.create_user("test_user").await?; @@ -227,3 +231,70 @@ async fn resource_wf_state() -> anyhow::Result<()> { Ok(()) } + +#[async_std::test] +async fn policy_enforcement() -> anyhow::Result<()> { + let platform = Builder::new() + .ac_platform(create_sqlite_backend().await?) + .pmrrbac_builder(PmrRbacBuilder::new_limited()) + .build(); + platform.assign_policy_to_wf_state(State::Private, Role::Owner, "edit", "GET").await?; + platform.assign_policy_to_wf_state(State::Private, Role::Owner, "edit", "POST").await?; + platform.assign_policy_to_wf_state(State::Pending, Role::Reviewer, "", "GET").await?; + platform.assign_policy_to_wf_state(State::Pending, Role::Reviewer, "", "POST").await?; + platform.assign_policy_to_wf_state(State::Pending, Role::Reviewer, "edit", "GET").await?; + platform.assign_policy_to_wf_state(State::Pending, Role::Reviewer, "edit", "POST").await?; + platform.assign_policy_to_wf_state(State::Published, Role::Owner, "edit", "GET").await?; + platform.assign_policy_to_wf_state(State::Published, Role::Reader, "", "GET").await?; + + let admin = platform.create_user("admin").await?; + admin.reset_password("admin", "admin").await?; + platform.grant_role_to_agent("/*", admin, Role::Manager).await?; + + let reviewer = platform.create_user("reviewer").await?; + reviewer.reset_password("reviewer", "reviewer").await?; + // this makes the reviewer being able to review globally + // platform.grant_role_to_agent("/*", &reviewer, Role::Reviewer).await?; + // we need something actually + // Or is this something that can be expressed with casbin as part of the base model/policy? + // platform.enable_role_at_state_for_resource(Role::Reviewer, State::Pending, "/*").await?; + platform.grant_role_to_agent("/profile/reviewer", reviewer, Role::Owner).await?; + platform.set_wf_state_for_res("/profile/reviewer", State::Private).await?; + + let user = platform.create_user("user").await?; + user.reset_password("user", "user").await?; + platform.grant_role_to_agent("/profile/user", user, Role::Owner).await?; + platform.set_wf_state_for_res("/profile/user", State::Private).await?; + + let admin = platform.authenticate_user("admin", "admin").await?; + let reviewer = platform.authenticate_user("reviewer", "reviewer").await?; + let user = platform.authenticate_user("user", "user").await?; + + assert!(platform.enforce(&admin, "/profile/user", "", "GET").await?); + assert!(platform.enforce(&user, "/profile/user", "", "GET").await?); + assert!(!platform.enforce(&reviewer, "/profile/user", "", "GET").await?); + + // create content owned by user + platform.grant_role_to_agent("/news/post/1", &user, Role::Owner).await?; + + // editable by the user while private + platform.set_wf_state_for_res("/news/post/1", State::Private).await?; + assert!(platform.enforce(&admin, "/news/post/1", "edit", "POST").await?); + assert!(platform.enforce(&user, "/news/post/1", "edit", "POST").await?); + assert!(!platform.enforce(&reviewer, "/news/post/1", "edit", "POST").await?); + + platform.set_wf_state_for_res("/news/post/1", State::Pending).await?; + // TODO need to figure out the API for this, rather than doing the wildcard as + // that doesn't work. for now, we need to assign the exact reviewer at this exact + // moment, rather than the role in a more general way + // That said, this address the use case for assigning _specific_ reviewer for the + // task and they will have the rights required + platform.grant_role_to_agent("/news/post/1", &reviewer, Role::Reviewer).await?; + + assert!(platform.enforce(&admin, "/news/post/1", "edit", "POST").await?); + assert!(!platform.enforce(&user, "/news/post/1", "edit", "POST").await?); + assert!(platform.enforce(&reviewer, "/news/post/1", "edit", "POST").await?); + assert!(!platform.enforce(&reviewer, "/news/post/1", "grant", "POST").await?); + + Ok(()) +} diff --git a/pmrcore/src/ac/agent/impls.rs b/pmrcore/src/ac/agent/impls.rs index e02b976..ed80ce9 100644 --- a/pmrcore/src/ac/agent/impls.rs +++ b/pmrcore/src/ac/agent/impls.rs @@ -8,10 +8,19 @@ impl From for Agent { } impl From<&Agent> for Option { - fn from(agent: &Agent) -> Option { + fn from(agent: &Agent) -> Self { match agent { Agent::Anonymous => None, Agent::User(User { id, .. }) => Some(*id), } } } + +impl From for Option { + fn from(agent: Agent) -> Self { + match agent { + Agent::Anonymous => None, + Agent::User(User { name, .. }) => Some(name), + } + } +}