Skip to content

Commit

Permalink
Integrate the pmrrbac enforcer into pmrac
Browse files Browse the repository at this point in the history
- This finally identified some holes that was thought to exist with how
  the policies are generated where there should be way to conditionally
  enable a role given a state.
  • Loading branch information
metatoaster committed Sep 25, 2024
1 parent c865153 commit 054deb0
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 8 deletions.
1 change: 1 addition & 0 deletions pmrac/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" ] }
Expand Down
2 changes: 2 additions & 0 deletions pmrac/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub enum Error {
Authentication(#[from] AuthenticationError),
#[error("Misconfiguration Password")]
Misconfiguration,
#[error(transparent)]
Rbac(#[from] pmrrbac::error::Error),
}

#[non_exhaustive]
Expand Down
40 changes: 39 additions & 1 deletion pmrac/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use pmrcore::{
},
platform::ACPlatform
};
use pmrrbac::Builder as PmrRbacBuilder;
use std::sync::Arc;

use crate::{
Expand All @@ -25,16 +26,21 @@ pub struct Builder {
ac_platform: Option<Arc<dyn ACPlatform>>,
// automatically purges all but the most recent passwords
password_autopurge: bool,
pmrrbac_builder: PmrRbacBuilder,
}

pub struct Platform {
ac_platform: Arc<dyn ACPlatform>,
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 {
Expand All @@ -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
}
}
}
Expand All @@ -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,
}
}
}
Expand Down Expand Up @@ -245,3 +259,27 @@ impl Platform {
).await?)
}
}

// Enforcement

impl Platform {
pub async fn enforce(
&self,
agent: impl Into<Agent>,
res: impl AsRef<str> + ToString,
endpoint_group: impl AsRef<str>,
http_method: &str,
) -> Result<bool, Error> {
Ok(self.pmrrbac_builder
.build_with_resource_policy(
self.generate_policy_for_res(res.to_string()).await?,
)
.await?
.enforce(
<Agent as Into<Option<String>>>::into(agent.into()),
res,
endpoint_group,
http_method,
)?)
}
}
83 changes: 77 additions & 6 deletions pmrac/tests/test_platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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?;

Expand Down Expand Up @@ -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;
Expand All @@ -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?;

Expand Down Expand Up @@ -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(())
}
11 changes: 10 additions & 1 deletion pmrcore/src/ac/agent/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@ impl From<User> for Agent {
}

impl From<&Agent> for Option<i64> {
fn from(agent: &Agent) -> Option<i64> {
fn from(agent: &Agent) -> Self {
match agent {
Agent::Anonymous => None,
Agent::User(User { id, .. }) => Some(*id),
}
}
}

impl From<Agent> for Option<String> {
fn from(agent: Agent) -> Self {
match agent {
Agent::Anonymous => None,
Agent::User(User { name, .. }) => Some(name),
}
}
}

0 comments on commit 054deb0

Please sign in to comment.