349 lines
11 KiB
Rust
349 lines
11 KiB
Rust
use crate::biz::collab::member_listener::{CollabMemberAction, CollabMemberNotification};
|
|
use crate::biz::workspace::access_control::WorkspaceAccessControl;
|
|
use crate::middleware::access_control_mw::{AccessResource, HttpAccessControlService};
|
|
use actix_router::{Path, Url};
|
|
use actix_web::http::Method;
|
|
use app_error::AppError;
|
|
use async_trait::async_trait;
|
|
use database::collab::CollabStorageAccessControl;
|
|
use database::user::select_uid_from_uuid;
|
|
use database_entity::dto::{AFAccessLevel, AFRole};
|
|
use realtime::collaborate::{CollabAccessControl, CollabUserId};
|
|
use sqlx::PgPool;
|
|
use std::collections::hash_map::Entry;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use tokio::sync::{broadcast, RwLock};
|
|
use tracing::{instrument, warn};
|
|
use uuid::Uuid;
|
|
|
|
/// Represents the access level of a collaboration object identified by its OID.
|
|
/// - Key: OID of the collaboration object.
|
|
/// - Value: The user's role within the collaboration.
|
|
///
|
|
type CollabMemberStatusByOid = HashMap<String, MemberStatus>;
|
|
|
|
/// Represents the access levels of various collaboration objects for a user.
|
|
/// - Key: User's UID.
|
|
/// - Value: A mapping between the collaboration object's OID and the user's access level within that collaboration.
|
|
///
|
|
/// uid -> oid -> access level of the user in the collab
|
|
///
|
|
type MemberStatusByUid = HashMap<i64, CollabMemberStatusByOid>;
|
|
|
|
/// Used to cache the access level of a user for collaboration objects.
|
|
/// The cache will be updated after the user's access level for a collaboration object is changed.
|
|
/// The change is broadcasted by the `CollabMemberListener` or set by the [CollabAccessControlImpl::update_member] method.
|
|
///
|
|
/// TODO(nathan): broadcast the member access level changes to all connected devices
|
|
///
|
|
pub struct CollabAccessControlImpl {
|
|
pg_pool: PgPool,
|
|
member_status_by_uid: Arc<RwLock<MemberStatusByUid>>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum MemberStatus {
|
|
/// Mark the user is not the member of the collab.
|
|
/// it don't need to query the database to get the access level of the user in the collab
|
|
/// when the user is not the member of the collab
|
|
Deleted,
|
|
/// The user is the member of the collab
|
|
Valid(AFAccessLevel),
|
|
}
|
|
|
|
impl CollabAccessControlImpl {
|
|
pub fn new(pg_pool: PgPool, listener: broadcast::Receiver<CollabMemberNotification>) -> Self {
|
|
let member_status_by_uid = Arc::new(RwLock::new(HashMap::new()));
|
|
|
|
// Listen to the changes of the collab member and update the memory cache
|
|
spawn_listen_on_collab_member_change(listener, pg_pool.clone(), member_status_by_uid.clone());
|
|
Self {
|
|
pg_pool,
|
|
member_status_by_uid,
|
|
}
|
|
}
|
|
|
|
/// The member's access level may be altered by PostgreSQL notifications. However, there are instances
|
|
/// where these notifications aren't received promptly, leading to potential inconsistencies in the user's access level.
|
|
/// Therefore, it's essential to update the user's access level in the cache whenever there's a change.
|
|
pub async fn update_member(&self, uid: &i64, oid: &str, access_level: AFAccessLevel) {
|
|
cache_collab_member_status(uid, oid, access_level, &self.member_status_by_uid).await;
|
|
}
|
|
|
|
pub async fn remove_member(&self, uid: &i64, oid: &str) {
|
|
if let Some(inner_map) = self.member_status_by_uid.write().await.get_mut(uid) {
|
|
if let Entry::Occupied(mut entry) = inner_map.entry(oid.to_string()) {
|
|
entry.insert(MemberStatus::Deleted);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[instrument(level = "debug", skip(self))]
|
|
async fn get_user_collab_access_level(
|
|
&self,
|
|
uid: &i64,
|
|
oid: &str,
|
|
) -> Result<AFAccessLevel, AppError> {
|
|
let member_status = self
|
|
.member_status_by_uid
|
|
.read()
|
|
.await
|
|
.get(uid)
|
|
.and_then(|map| map.get(oid).cloned());
|
|
|
|
let member_status = match member_status {
|
|
None => {
|
|
reload_collab_member_status_from_db(uid, oid, &self.pg_pool, &self.member_status_by_uid)
|
|
.await?
|
|
},
|
|
Some(status) => status,
|
|
};
|
|
|
|
match member_status {
|
|
MemberStatus::Deleted => Err(AppError::NotEnoughPermissions(format!(
|
|
"user:{} is not a member of collab:{}",
|
|
uid, oid
|
|
))),
|
|
MemberStatus::Valid(access_level) => Ok(access_level),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn spawn_listen_on_collab_member_change(
|
|
mut listener: broadcast::Receiver<CollabMemberNotification>,
|
|
pg_pool: PgPool,
|
|
member_status_by_uid: Arc<RwLock<MemberStatusByUid>>,
|
|
) {
|
|
tokio::spawn(async move {
|
|
while let Ok(change) = listener.recv().await {
|
|
match change.action_type {
|
|
CollabMemberAction::INSERT | CollabMemberAction::UPDATE => {
|
|
if let (Some(oid), Some(uid)) = (change.new_oid(), change.new_uid()) {
|
|
if let Err(err) =
|
|
reload_collab_member_status_from_db(uid, oid, &pg_pool, &member_status_by_uid).await
|
|
{
|
|
warn!(
|
|
"Failed to reload the collab member status from db: {:?}, error: {}",
|
|
change, err
|
|
);
|
|
}
|
|
} else {
|
|
warn!("The oid or uid is None")
|
|
}
|
|
},
|
|
CollabMemberAction::DELETE => {
|
|
if let (Some(oid), Some(uid)) = (change.old_oid(), change.old_uid()) {
|
|
if let Some(inner_map) = member_status_by_uid.write().await.get_mut(uid) {
|
|
inner_map.insert(oid.to_string(), MemberStatus::Deleted);
|
|
}
|
|
} else {
|
|
warn!("The oid or uid is None")
|
|
}
|
|
},
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
#[inline]
|
|
async fn cache_collab_member_status(
|
|
uid: &i64,
|
|
oid: &str,
|
|
access_level: AFAccessLevel,
|
|
member_status_by_uid: &Arc<RwLock<MemberStatusByUid>>,
|
|
) {
|
|
let mut outer_map = member_status_by_uid.write().await;
|
|
let inner_map = outer_map.entry(*uid).or_insert_with(HashMap::new);
|
|
inner_map.insert(oid.to_string(), MemberStatus::Valid(access_level));
|
|
}
|
|
|
|
#[instrument(level = "debug", skip(pg_pool, member_status_by_uid))]
|
|
async fn reload_collab_member_status_from_db(
|
|
uid: &i64,
|
|
oid: &str,
|
|
pg_pool: &PgPool,
|
|
member_status_by_uid: &Arc<RwLock<MemberStatusByUid>>,
|
|
) -> Result<MemberStatus, AppError> {
|
|
let member = database::collab::select_collab_member(uid, oid, pg_pool).await?;
|
|
let status = MemberStatus::Valid(member.permission.access_level);
|
|
cache_collab_member_status(
|
|
uid,
|
|
oid,
|
|
member.permission.access_level,
|
|
member_status_by_uid,
|
|
)
|
|
.await;
|
|
Ok(status)
|
|
}
|
|
|
|
#[async_trait]
|
|
impl CollabAccessControl for CollabAccessControlImpl {
|
|
async fn get_collab_access_level(
|
|
&self,
|
|
user: CollabUserId<'_>,
|
|
oid: &str,
|
|
) -> Result<AFAccessLevel, AppError> {
|
|
match user {
|
|
CollabUserId::UserId(uid) => self.get_user_collab_access_level(uid, oid).await,
|
|
CollabUserId::UserUuid(uuid) => {
|
|
let uid = select_uid_from_uuid(&self.pg_pool, uuid).await?;
|
|
self.get_user_collab_access_level(&uid, oid).await
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn cache_collab_access_level(
|
|
&self,
|
|
user: CollabUserId<'_>,
|
|
oid: &str,
|
|
level: AFAccessLevel,
|
|
) -> Result<(), AppError> {
|
|
let uid = match user {
|
|
CollabUserId::UserId(uid) => *uid,
|
|
CollabUserId::UserUuid(uuid) => select_uid_from_uuid(&self.pg_pool, uuid).await?,
|
|
};
|
|
|
|
self.update_member(&uid, oid, level).await;
|
|
Ok(())
|
|
}
|
|
|
|
#[instrument(level = "debug", skip_all, err)]
|
|
async fn can_access_http_method(
|
|
&self,
|
|
user: CollabUserId<'_>,
|
|
oid: &str,
|
|
method: &Method,
|
|
) -> Result<bool, AppError> {
|
|
match self.get_collab_access_level(user, oid).await {
|
|
Ok(level) => {
|
|
if Method::POST == method || Method::PUT == method || Method::DELETE == method {
|
|
Ok(level.can_write())
|
|
} else {
|
|
Ok(true)
|
|
}
|
|
},
|
|
Err(err) => {
|
|
if err.is_record_not_found() {
|
|
Ok(true)
|
|
} else {
|
|
Err(err)
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
#[instrument(level = "debug", skip_all, err)]
|
|
async fn can_send_collab_update(&self, uid: &i64, oid: &str) -> Result<bool, AppError> {
|
|
let result = self
|
|
.get_collab_access_level(CollabUserId::UserId(uid), oid)
|
|
.await;
|
|
|
|
match result {
|
|
Ok(level) => match level {
|
|
AFAccessLevel::ReadOnly | AFAccessLevel::ReadAndComment => Ok(false),
|
|
AFAccessLevel::ReadAndWrite | AFAccessLevel::FullAccess => Ok(true),
|
|
},
|
|
Err(err) => {
|
|
// // If the collab object with given oid is not found which means the collab object is created
|
|
// // by the user. So the user is allowed to send the message
|
|
// if err.is_record_not_found() {
|
|
// return Ok(true);
|
|
// }
|
|
return Err(err);
|
|
},
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
#[instrument(level = "debug", skip_all, err)]
|
|
async fn can_receive_collab_update(&self, uid: &i64, oid: &str) -> Result<bool, AppError> {
|
|
Ok(
|
|
self
|
|
.get_collab_access_level(CollabUserId::UserId(uid), oid)
|
|
.await
|
|
.is_ok(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct CollabHttpAccessControl<AC: CollabAccessControl>(pub Arc<AC>);
|
|
|
|
#[async_trait]
|
|
impl<AC> HttpAccessControlService for CollabHttpAccessControl<AC>
|
|
where
|
|
AC: CollabAccessControl,
|
|
{
|
|
fn resource(&self) -> AccessResource {
|
|
AccessResource::Collab
|
|
}
|
|
|
|
#[instrument(level = "debug", skip_all, err)]
|
|
async fn check_collab_permission(
|
|
&self,
|
|
oid: &str,
|
|
user_uuid: &Uuid,
|
|
method: Method,
|
|
_path: &Path<Url>,
|
|
) -> Result<(), AppError> {
|
|
let can_access = self
|
|
.0
|
|
.can_access_http_method(CollabUserId::UserUuid(user_uuid), oid, &method)
|
|
.await?;
|
|
|
|
if !can_access {
|
|
return Err(AppError::NotEnoughPermissions(format!(
|
|
"Not enough permissions to access the collab: {} with http method: {}",
|
|
oid, method
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct CollabStorageAccessControlImpl<CollabAC, WorkspaceAC> {
|
|
pub(crate) collab_access_control: Arc<CollabAC>,
|
|
pub(crate) workspace_access_control: Arc<WorkspaceAC>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<CollabAC, WorkspaceAC> CollabStorageAccessControl
|
|
for CollabStorageAccessControlImpl<CollabAC, WorkspaceAC>
|
|
where
|
|
CollabAC: CollabAccessControl,
|
|
WorkspaceAC: WorkspaceAccessControl,
|
|
{
|
|
async fn get_collab_access_level(&self, uid: &i64, oid: &str) -> Result<AFAccessLevel, AppError> {
|
|
self
|
|
.collab_access_control
|
|
.get_collab_access_level(uid.into(), oid)
|
|
.await
|
|
}
|
|
|
|
async fn cache_collab_access_level(
|
|
&self,
|
|
uid: &i64,
|
|
oid: &str,
|
|
level: AFAccessLevel,
|
|
) -> Result<(), AppError> {
|
|
self
|
|
.collab_access_control
|
|
.cache_collab_access_level(uid.into(), oid, level)
|
|
.await
|
|
}
|
|
|
|
async fn get_user_workspace_role(
|
|
&self,
|
|
uid: &i64,
|
|
workspace_id: &str,
|
|
) -> Result<AFRole, AppError> {
|
|
self
|
|
.workspace_access_control
|
|
.get_role_from_uid(uid, &workspace_id.parse()?)
|
|
.await
|
|
}
|
|
}
|