use crate::biz::casbin::access_control::{ ActionType, ObjectType, ToCasbinAction, POLICY_FIELD_INDEX_ACTION, POLICY_FIELD_INDEX_OBJECT, POLICY_FIELD_INDEX_USER, }; use anyhow::anyhow; use app_error::AppError; use casbin::{CoreApi, Enforcer, MgmtApi}; use dashmap::DashMap; use std::ops::Deref; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{event, trace}; pub struct AFEnforcer { enforcer: RwLock, /// Cache for the result of the policy check. It's a memory cache for faster access. enforcer_result_cache: Arc>, action_cache: Arc>, } impl AFEnforcer { pub fn new( enforcer: Enforcer, enforcer_result_cache: Arc>, action_cache: Arc>, ) -> Self { Self { enforcer: RwLock::new(enforcer), enforcer_result_cache, action_cache, } } pub async fn policies_for_user_with_given_object( &self, uid: &i64, object_type: &ObjectType<'_>, ) -> Vec> { let object_type_id = object_type.to_object_id(); let policies_related_to_object = self .enforcer .read() .await .get_filtered_policy(POLICY_FIELD_INDEX_OBJECT, vec![object_type_id]); policies_related_to_object .into_iter() .filter(|p| p[POLICY_FIELD_INDEX_USER] == uid.to_string()) .collect::>() } /// Update permission for a user. /// /// [`ObjectType::Workspace`] has to be paired with [`ActionType::Role`], /// [`ObjectType::Collab`] has to be paired with [`ActionType::Level`], pub async fn update( &self, uid: &i64, obj: &ObjectType<'_>, act: &ActionType, ) -> Result { validate_obj_action(obj, act)?; let policy = vec![uid.to_string(), obj.to_object_id(), act.to_action()]; let policy_key = PolicyCacheKey::new(&policy); // if the policy is already in the cache, return. Only update the policy if it's not in the cache. if let Some(value) = self.enforcer_result_cache.get(&policy_key) { return Ok(*value); } // only one policy per user per object. So remove the old policy and add the new one. let _remove_policies = self.remove(uid, obj).await?; let object_key = ActionCacheKey::new(uid, obj); let result = self .enforcer .write() .await .add_policy(policy) .await .map_err(|e| AppError::Internal(anyhow!("fail to add policy: {e:?}"))); match &result { Ok(value) => { trace!("[access control]: add policy:{} => {}", policy_key.0, value); self.action_cache.insert(object_key, act.to_action()); self.enforcer_result_cache.insert(policy_key, *value); }, Err(err) => { trace!( "[access control]: fail to add policy:{} => {:?}", policy_key.0, err ); }, } result } /// Returns policies that match the filter. pub async fn remove( &self, uid: &i64, object_type: &ObjectType<'_>, ) -> Result>, AppError> { let policies_for_user_on_object = self .policies_for_user_with_given_object(uid, object_type) .await; // if there are no policies for the user on the object, return early. if policies_for_user_on_object.is_empty() { return Ok(vec![]); } event!( tracing::Level::INFO, "[access control]: remove policy: object={}, user={}, policies={:?}", object_type.to_object_id(), uid, policies_for_user_on_object ); debug_assert!( policies_for_user_on_object.len() == 1, "only one policy per user per object" ); self .enforcer .write() .await .remove_policies(policies_for_user_on_object.clone()) .await .map_err(|e| AppError::Internal(anyhow!("error enforce: {e:?}")))?; let object_key = ActionCacheKey::new(uid, object_type); self.action_cache.remove(&object_key); for policy in &policies_for_user_on_object { self .enforcer_result_cache .remove(&PolicyCacheKey::new(policy)); } Ok(policies_for_user_on_object) } pub async fn enforce(&self, uid: &i64, obj: &ObjectType<'_>, act: A) -> Result where A: ToCasbinAction, { let policy = vec![uid.to_string(), obj.to_object_id(), act.to_action()]; let policy_key = PolicyCacheKey::new(&policy); if let Some(value) = self.enforcer_result_cache.get(&policy_key) { return Ok(*value); } let policies_for_object = self .enforcer .read() .await .get_filtered_policy(POLICY_FIELD_INDEX_OBJECT, vec![obj.to_object_id()]); if policies_for_object.is_empty() { self.enforcer_result_cache.insert(policy_key, true); return Ok(true); } let result = self .enforcer .read() .await .enforce(policy) .map_err(|e| AppError::Internal(anyhow!("error enforce: {e:?}")))?; trace!("[access control]: policy:{} => {}", policy_key.0, result); self.enforcer_result_cache.insert(policy_key, result); Ok(result) } pub async fn get_action(&self, uid: &i64, object_type: &ObjectType<'_>) -> Option { let object_key = ActionCacheKey::new(uid, object_type); if let Some(value) = self.action_cache.get(&object_key) { return Some(value.clone()); } // There should only be one entry per user per object, which is enforced in [AccessControl], so just take one using next. let policies = self .policies_for_user_with_given_object(uid, object_type) .await; let action = policies.first()?[POLICY_FIELD_INDEX_ACTION].clone(); trace!("cache action: {}:{}", object_key.0, action.clone()); self.action_cache.insert(object_key, action.clone()); Some(action) } } #[derive(Debug, Hash, Eq, PartialEq)] pub struct PolicyCacheKey(String); impl PolicyCacheKey { fn new(policy: &[String]) -> Self { Self(policy.join(":")) } } impl Deref for PolicyCacheKey { type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(Debug, Hash, Eq, PartialEq)] pub struct ActionCacheKey(String); impl ActionCacheKey { pub(crate) fn new(uid: &i64, object_type: &ObjectType<'_>) -> Self { Self(format!("{}:{}", uid, object_type.to_object_id())) } } fn validate_obj_action(obj: &ObjectType<'_>, act: &ActionType) -> Result<(), AppError> { match (obj, act) { (ObjectType::Workspace(_), ActionType::Role(_)) | (ObjectType::Collab(_), ActionType::Level(_)) => Ok(()), _ => Err(AppError::Internal(anyhow!( "invalid object type and action type combination: object={:?}, action={:?}", obj, act ))), } }