Skip to content

Commit

Permalink
Grant/revoke role to user at the system level
Browse files Browse the repository at this point in the history
- The PolicyBackend gets those methods to persist these roles associated
  with the user for the system.
- With the change with how the policies are constructed, the reader role
  must be granted to typical users in order for them to be able to read
  published content.
  • Loading branch information
metatoaster committed Sep 27, 2024
1 parent 78a8001 commit 1ae7bbd
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 16 deletions.
23 changes: 23 additions & 0 deletions pmrac/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use pmrcore::{
agent::Agent,
genpolicy::Policy,
role::Role,
user,
workflow::State,
},
platform::ACPlatform
Expand Down Expand Up @@ -179,6 +180,28 @@ impl Platform {
// Agent Policy management

impl Platform {
pub async fn grant_role_to_user(
&self,
user: impl Into<user::User>,
role: Role,
) -> Result<(), Error> {
Ok(self.ac_platform.grant_role_to_user(
&user.into(),
role
).await?)
}

pub async fn revoke_role_from_user(
&self,
user: impl Into<user::User>,
role: Role,
) -> Result<(), Error> {
Ok(self.ac_platform.revoke_role_from_user(
&user.into(),
role,
).await?)
}

pub async fn grant_res_role_to_agent(
&self,
res: &str,
Expand Down
12 changes: 12 additions & 0 deletions pmrac/src/user/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,15 @@ impl From<User<'_>> for Agent {
user.into_inner().into()
}
}

impl From<&User<'_>> for user::User {
fn from(user: &User<'_>) -> Self {
user.clone_inner()
}
}

impl From<User<'_>> for user::User {
fn from(user: User<'_>) -> Self {
user.into_inner()
}
}
51 changes: 37 additions & 14 deletions pmrac/tests/test_platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,22 @@ async fn resource_wf_state() -> anyhow::Result<()> {
let admin = platform.create_user("admin").await?;
let user = platform.create_user("test_user").await?;

platform.grant_role_to_user(
&admin,
Role::Reader,
).await?;
platform.grant_res_role_to_agent(
"/*",
admin,
Role::Manager,
).await?;
platform.grant_role_to_user(
&user,
Role::Reader,
).await?;
platform.grant_res_role_to_agent(
"/item/1",
user,
&user,
Role::Owner,
).await?;
platform.assign_policy_to_wf_state(
Expand Down Expand Up @@ -217,12 +225,13 @@ async fn resource_wf_state() -> anyhow::Result<()> {
"/item/1",
State::Published,
).await?;
let mut policy = platform.generate_policy_for_agent_res(&Agent::Anonymous, "/item/1".into()).await?;
let mut policy = platform.generate_policy_for_agent_res(&user.into(), "/item/1".into()).await?;
policy.res_grants.sort_unstable();
policy.role_permits.sort_unstable();
assert_eq!(policy, serde_json::from_str(r#"{
"resource": "/item/1",
"user_roles": [
{"user": "test_user", "role": "Reader"}
],
"res_grants": [
{"res": "/*", "agent": "admin", "role": "Manager"},
Expand Down Expand Up @@ -252,29 +261,37 @@ async fn policy_enforcement() -> anyhow::Result<()> {
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?;

// there is a welcome page for the site that should be readable by all
platform.set_wf_state_for_res("/welcome", State::Published).await?;

let admin = platform.create_user("admin").await?;
admin.reset_password("admin", "admin").await?;
platform.grant_res_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_res_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?;
// this enables the reviewer being able to review resources under pending state
platform.grant_role_to_user(&reviewer, Role::Reviewer).await?;
platform.grant_role_to_user(&reviewer, Role::Reader).await?;
platform.grant_res_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_user(&user, Role::Reader).await?;
platform.grant_res_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?;

// since the anonymous_reader isn't enabled for the rbac enforcer...
assert!(!platform.enforce(Agent::Anonymous, "/welcome", "", "GET").await?);
assert!(platform.enforce(&admin, "/welcome", "", "GET").await?);
assert!(platform.enforce(&reviewer, "/welcome", "", "GET").await?);
assert!(platform.enforce(&user, "/welcome", "", "GET").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?);
Expand All @@ -289,17 +306,23 @@ async fn policy_enforcement() -> anyhow::Result<()> {
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_res_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?);
assert!(!platform.enforce(&reviewer, "/news/post/2", "edit", "POST").await?);

// Reviewer role can be granted for one specific resource, to address the use
// case of requring explicit assignments of items for review to specific reviewer.
let restricted_reviewer = platform.create_user("restricted_reviewer").await?;
platform.grant_res_role_to_agent("/news/post/2", &restricted_reviewer, Role::Reviewer).await?;
assert!(!platform.enforce(&restricted_reviewer, "/news/post/2", "edit", "POST").await?);
platform.set_wf_state_for_res("/news/post/2", State::Pending).await?;
assert!(platform.enforce(&restricted_reviewer, "/news/post/2", "edit", "POST").await?);
assert!(!platform.enforce(&restricted_reviewer, "/news/post/1", "edit", "POST").await?);
// since they were never granted the general reader role, they won't be able to read
// the welcome page either...
assert!(!platform.enforce(&restricted_reviewer, "/welcome", "", "GET").await?);

Ok(())
}
10 changes: 10 additions & 0 deletions pmrcore/src/ac/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ pub trait UserBackend {

#[async_trait]
pub trait PolicyBackend {
async fn grant_role_to_user(
&self,
user: &User,
role: Role,
) -> Result<(), BackendError>;
async fn revoke_role_from_user(
&self,
user: &User,
role: Role,
) -> Result<(), BackendError>;
async fn grant_res_role_to_agent(
&self,
res: &str,
Expand Down
72 changes: 71 additions & 1 deletion pmrmodel/src/model/db/sqlite/ac/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use pmrcore::{
agent::Agent,
role::Role,
traits::PolicyBackend,
user::User,
workflow::State,
},
error::BackendError,
Expand All @@ -13,6 +14,51 @@ use crate::{
backend::db::SqliteBackend,
};

async fn grant_role_to_user_sqlite(
backend: &SqliteBackend,
user: &User,
role: Role,
) -> Result<(), BackendError> {
let role_str = <&'static str>::from(role);
sqlx::query!(
r#"
INSERT INTO user_role (
user_id,
role
)
VALUES ( ?1, ?2 )
"#,
user.id,
role_str,
)
.execute(&*backend.pool)
.await?
.last_insert_rowid();
Ok(())
}

async fn revoke_role_from_user_sqlite(
backend: &SqliteBackend,
user: &User,
role: Role,
) -> Result<(), BackendError> {
let role_str = <&'static str>::from(role);
sqlx::query!(
r#"
DELETE FROM
user_role
WHERE
user_id = ?1 AND
role = ?2
"#,
user.id,
role_str,
)
.execute(&*backend.pool)
.await?;
Ok(())
}

async fn grant_res_role_to_agent_sqlite(
backend: &SqliteBackend,
res: &str,
Expand Down Expand Up @@ -46,7 +92,7 @@ async fn revoke_res_role_from_agent_sqlite(
agent: &Agent,
role: Role,
) -> Result<(), BackendError> {
let role_str = role.to_string();
let role_str = <&'static str>::from(role);
let user_id: Option<i64> = agent.into();
sqlx::query!(
r#"
Expand Down Expand Up @@ -127,6 +173,30 @@ WHERE

#[async_trait]
impl PolicyBackend for SqliteBackend {
async fn grant_role_to_user(
&self,
user: &User,
role: Role,
) -> Result<(), BackendError> {
grant_role_to_user_sqlite(
&self,
user,
role,
).await
}

async fn revoke_role_from_user(
&self,
user: &User,
role: Role,
) -> Result<(), BackendError> {
revoke_role_from_user_sqlite(
&self,
user,
role,
).await
}

async fn grant_res_role_to_agent(
&self,
res: &str,
Expand Down
5 changes: 4 additions & 1 deletion pmrmodel/src/model/db/sqlite/ac/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ pub(crate) mod testing {
let user = UserBackend::get_user_by_id(&backend, user_id).await?;
let state = State::Published;
let role = Role::Reader;
let agent: Agent = user.into();
let agent: Agent = user.clone().into();
PolicyBackend::grant_role_to_user(&backend, &user, role).await?;
PolicyBackend::grant_res_role_to_agent(&backend, "/", &agent, role).await?;
PolicyBackend::assign_policy_to_wf_state(&backend, state, role, "", "GET").await?;
ResourceBackend::set_wf_state_for_res(&backend, "/", state).await?;
Expand All @@ -241,6 +242,7 @@ pub(crate) mod testing {
assert_eq!(policy, serde_json::from_str(r#"{
"resource": "/",
"user_roles": [
{"user": "test_user", "role": "Reader"}
],
"res_grants": [
{"res": "/", "agent": "test_user", "role": "Reader"}
Expand All @@ -250,6 +252,7 @@ pub(crate) mod testing {
]
}"#)?);

PolicyBackend::revoke_role_from_user(&backend, &user, role).await?;
PolicyBackend::revoke_res_role_from_agent(&backend, "/", &agent, role).await?;
PolicyBackend::remove_policy_from_wf_state(&backend, state, role, "", "GET").await?;
let policy = ResourceBackend::generate_policy_for_agent_res(
Expand Down

0 comments on commit 1ae7bbd

Please sign in to comment.