AppFlowy-Cloud/libs/access-control/src/enforcer.rs

226 lines
6.9 KiB
Rust

use crate::access::{
load_group_policies, ObjectType, POLICY_FIELD_INDEX_OBJECT, POLICY_FIELD_INDEX_SUBJECT,
};
use crate::act::ActionVariant;
use crate::metrics::MetricsCalState;
use crate::request::{GroupPolicyRequest, PolicyRequest, WorkspacePolicyRequest};
use anyhow::anyhow;
use app_error::AppError;
use async_trait::async_trait;
use casbin::{CoreApi, Enforcer, MgmtApi};
use std::sync::atomic::Ordering;
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::{event, instrument, trace};
pub const ENFORCER_METRICS_TICK_INTERVAL: Duration = Duration::from_secs(120);
#[async_trait]
pub trait EnforcerGroup {
/// Get the group id of the user.
/// User might belong to multiple groups. So return the highest permission group id.
async fn get_enforce_group_id(&self, uid: &i64) -> Option<String>;
}
pub struct AFEnforcer<T> {
enforcer: RwLock<Enforcer>,
pub(crate) metrics_state: MetricsCalState,
enforce_group: T,
}
impl<T> AFEnforcer<T>
where
T: EnforcerGroup,
{
pub async fn new(mut enforcer: Enforcer, enforce_group: T) -> Result<Self, AppError> {
load_group_policies(&mut enforcer).await?;
Ok(Self {
enforcer: RwLock::new(enforcer),
metrics_state: MetricsCalState::new(),
enforce_group,
})
}
/// Update policy for a user.
/// If the policy is already exist, then it will return Ok(false).
///
/// [`ObjectType::Workspace`] has to be paired with [`ActionType::Role`],
/// [`ObjectType::Collab`] has to be paired with [`ActionType::Level`],
#[instrument(level = "debug", skip_all, err)]
pub async fn update_policy(
&self,
uid: &i64,
obj: ObjectType<'_>,
act: ActionVariant<'_>,
) -> Result<(), AppError> {
validate_obj_action(&obj, &act)?;
let policies = act
.policy_acts()
.into_iter()
.map(|act| vec![uid.to_string(), obj.policy_object(), act.to_string()])
.collect::<Vec<Vec<_>>>();
trace!("[access control]: add policy:{:?}", policies);
self
.enforcer
.write()
.await
.add_policies(policies)
.await
.map_err(|e| AppError::Internal(anyhow!("fail to add policy: {e:?}")))?;
Ok(())
}
/// Returns policies that match the filter.
pub async fn remove_policy(
&self,
uid: &i64,
object_type: &ObjectType<'_>,
) -> Result<(), AppError> {
let mut enforcer = self.enforcer.write().await;
self
.remove_with_enforcer(uid, object_type, &mut enforcer)
.await
}
/// 1. **Workspace Policy**: Initially, it checks if the user has permission at the workspace level. If the user
/// has permission to perform the action on the workspace, the function returns `true` without further checks.
///
/// 2. **Group Policy**: (If applicable) If the workspace policy check fails (`false`), the function will then
/// evaluate group-level policies.
///
/// 3. **Object-Specific Policy**: If both previous checks fail, the function finally evaluates the policy
/// specific to the object itself.
///
/// ## Parameters:
/// - `workspace_id`: The identifier for the workspace containing the object.
/// - `uid`: The user ID of the user attempting the action.
/// - `obj`: The type of object being accessed, encapsulated within an `ObjectType`.
/// - `act`: The action being attempted, encapsulated within an `ActionVariant`.
///
/// ## Returns:
/// - `Ok(true)`: If the user is authorized to perform the action based on any of the evaluated policies.
/// - `Ok(false)`: If none of the policies authorize the user to perform the action.
/// - `Err(AppError)`: If an error occurs during policy enforcement.
///
#[instrument(level = "debug", skip_all)]
pub async fn enforce_policy(
&self,
workspace_id: &str,
uid: &i64,
obj: ObjectType<'_>,
act: ActionVariant<'_>,
) -> Result<bool, AppError> {
self
.metrics_state
.total_read_enforce_result
.fetch_add(1, Ordering::Relaxed);
// 1. First, check workspace-level permissions.
let workspace_policy_request = WorkspacePolicyRequest::new(workspace_id, uid, &obj, &act);
let policy = workspace_policy_request.to_policy();
let mut result = self
.enforcer
.read()
.await
.enforce(policy)
.map_err(|e| AppError::Internal(anyhow!("enforce: {e:?}")))?;
// 2. Fallback to group policy if workspace-level check fails.
if !result {
if let Some(guid) = self.enforce_group.get_enforce_group_id(uid).await {
let policy_request = GroupPolicyRequest::new(&guid, &obj, &act);
result = self
.enforcer
.read()
.await
.enforce(policy_request.to_policy())
.map_err(|e| AppError::Internal(anyhow!("enforce: {e:?}")))?;
}
}
// 3. Finally, enforce object-specific policy if previous checks fail.
if !result {
let policy_request = PolicyRequest::new(*uid, &obj, &act);
let policy = policy_request.to_policy();
result = self
.enforcer
.read()
.await
.enforce(policy)
.map_err(|e| AppError::Internal(anyhow!("enforce: {e:?}")))?;
}
Ok(result)
}
#[inline]
async fn remove_with_enforcer(
&self,
uid: &i64,
object_type: &ObjectType<'_>,
enforcer: &mut Enforcer,
) -> Result<(), AppError> {
let policies_for_user_on_object =
policies_for_subject_with_given_object(uid, object_type, enforcer).await;
// if there are no policies for the user on the object, return early.
if policies_for_user_on_object.is_empty() {
return Ok(());
}
event!(
tracing::Level::INFO,
"[access control]: remove policy:user={}, object={}, policies={:?}",
uid,
object_type.policy_object(),
policies_for_user_on_object
);
enforcer
.remove_policies(policies_for_user_on_object)
.await
.map_err(|e| AppError::Internal(anyhow!("error enforce: {e:?}")))?;
Ok(())
}
}
fn validate_obj_action(obj: &ObjectType<'_>, act: &ActionVariant) -> Result<(), AppError> {
match (obj, act) {
(ObjectType::Workspace(_), ActionVariant::FromRole(_))
| (ObjectType::Collab(_), ActionVariant::FromAccessLevel(_)) => Ok(()),
_ => Err(AppError::Internal(anyhow!(
"invalid object type and action type combination: object={:?}, action={:?}",
obj,
act.to_enforce_act()
))),
}
}
#[inline]
async fn policies_for_subject_with_given_object<T: ToString>(
subject: T,
object_type: &ObjectType<'_>,
enforcer: &Enforcer,
) -> Vec<Vec<String>> {
let subject = subject.to_string();
let object_type_id = object_type.policy_object();
let policies_related_to_object =
enforcer.get_filtered_policy(POLICY_FIELD_INDEX_OBJECT, vec![object_type_id]);
policies_related_to_object
.into_iter()
.filter(|p| p[POLICY_FIELD_INDEX_SUBJECT] == subject)
.collect::<Vec<_>>()
}
pub struct NoEnforceGroup;
#[async_trait]
impl EnforcerGroup for NoEnforceGroup {
async fn get_enforce_group_id(&self, _uid: &i64) -> Option<String> {
None
}
}