chore: merge with main

This commit is contained in:
Zack Fu Zi Xiang 2024-02-20 15:54:14 +08:00
commit 7c49359278
No known key found for this signature in database
GPG Key ID: 39DE600AFEEED522
14 changed files with 97 additions and 79 deletions

View File

@ -1,4 +1,4 @@
use anyhow::{anyhow, Error}; use anyhow::anyhow;
use collab::core::awareness; use collab::core::awareness;
use std::future::Future; use std::future::Future;
use std::iter::Take; use std::iter::Take;
@ -130,19 +130,18 @@ impl CollabBroadcast {
pub fn subscribe<Sink, Stream, E>( pub fn subscribe<Sink, Stream, E>(
&self, &self,
subscriber_origin: CollabOrigin, subscriber_origin: CollabOrigin,
sink: Sink, mut sink: Sink,
mut stream: Stream, mut stream: Stream,
modified_at: Arc<Mutex<Instant>>, modified_at: Arc<Mutex<Instant>>,
) -> Subscription ) -> Subscription
where where
Sink: SinkExt<CollabMessage> + Send + Sync + Unpin + 'static, Sink: SinkExt<CollabMessage> + Clone + Send + Sync + Unpin + 'static,
Stream: StreamExt<Item = Result<CollabMessage, E>> + Send + Sync + Unpin + 'static, Stream: StreamExt<Item = Result<CollabMessage, E>> + Send + Sync + Unpin + 'static,
<Sink as futures_util::Sink<CollabMessage>>::Error: std::error::Error + Send + Sync, <Sink as futures_util::Sink<CollabMessage>>::Error: std::error::Error + Send + Sync,
E: Into<anyhow::Error> + Send + Sync + 'static, E: Into<anyhow::Error> + Send + Sync + 'static,
{ {
let cloned_origin = subscriber_origin.clone(); let cloned_origin = subscriber_origin.clone();
trace!("[realtime]: new subscriber: {}", subscriber_origin); trace!("[realtime]: new subscriber: {}", subscriber_origin);
let sink = Arc::new(Mutex::new(sink));
// Receive a update from the document observer and forward the update to all // Receive a update from the document observer and forward the update to all
// connected subscribers using its Sink. // connected subscribers using its Sink.
let sink_stop_tx = { let sink_stop_tx = {
@ -199,7 +198,7 @@ impl CollabBroadcast {
match result { match result {
Some(Ok(collab_msg)) => { Some(Ok(collab_msg)) => {
if object_id == collab_msg.object_id() && collab_msg.payload().is_some() { if object_id == collab_msg.object_id() && collab_msg.payload().is_some() {
handle_user_collab_message(&object_id, &sink, &collab_msg, &collab).await; handle_client_collab_message(&object_id, &mut sink, &collab_msg, &collab).await;
if let Ok(mut modified_at) = modified_at.try_lock() { if let Ok(mut modified_at) = modified_at.try_lock() {
*modified_at = Instant::now(); *modified_at = Instant::now();
} }
@ -226,9 +225,10 @@ impl CollabBroadcast {
} }
} }
async fn handle_user_collab_message<Sink>( /// Handle the message sent from the client
async fn handle_client_collab_message<Sink>(
object_id: &str, object_id: &str,
sink: &Arc<Mutex<Sink>>, sink: &mut Sink,
collab_msg: &CollabMessage, collab_msg: &CollabMessage,
collab: &MutexCollab, collab: &MutexCollab,
) where ) where
@ -247,13 +247,11 @@ async fn handle_user_collab_message<Sink>(
Ok(msg) => { Ok(msg) => {
let cloned_collab = collab.clone(); let cloned_collab = collab.clone();
let cloned_origin = origin.clone(); let cloned_origin = origin.clone();
let result = tokio::task::spawn_blocking(move || { let result =
handle_collab_message(&cloned_origin, &ServerSyncProtocol, &cloned_collab, msg) handle_collab_message(&cloned_origin, &ServerSyncProtocol, &cloned_collab, msg);
})
.await;
match result { match result {
Ok(Ok(payload)) => match origin.as_ref() { Ok(payload) => match origin.as_ref() {
None => warn!("Client message does not have a origin"), None => warn!("Client message does not have a origin"),
Some(origin) => { Some(origin) => {
if let Some(msg_id) = collab_msg.msg_id() { if let Some(msg_id) = collab_msg.msg_id() {
@ -266,22 +264,14 @@ async fn handle_user_collab_message<Sink>(
); );
trace!("Send response to client: {}", resp); trace!("Send response to client: {}", resp);
match sink.try_lock() { if let Err(err) = sink.send(resp.into()).await {
Ok(mut sink) => { trace!("fail to send response to client: {}", err);
if let Err(err) = sink.send(resp.into()).await {
trace!("fail to send response to client: {}", err);
}
},
Err(err) => error!("Requires sink lock failed: {:?}", err),
} }
} }
}, },
}, },
Ok(Err(err)) => {
error!("object id:{} =>{}", object_id, err);
},
Err(err) => { Err(err) => {
error!("internal error when handle user ws message: {}", err); error!("object id:{} =>{}", object_id, err);
}, },
} }
}, },
@ -356,14 +346,14 @@ fn gen_awareness_update_message(
Ok(update) Ok(update)
} }
pub struct SinkCollabMessageAction<'a, Sink> { pub struct SinkCollabMessageAction<'a, Sink: Clone> {
pub sink: &'a Arc<tokio::sync::Mutex<Sink>>, pub sink: &'a Sink,
pub message: CollabMessage, pub message: CollabMessage,
} }
impl<'a, Sink> SinkCollabMessageAction<'a, Sink> impl<'a, Sink> SinkCollabMessageAction<'a, Sink>
where where
Sink: SinkExt<CollabMessage> + Send + Sync + Unpin + 'a, Sink: SinkExt<CollabMessage> + Clone + Send + Sync + Unpin + 'a,
{ {
pub fn run(self) -> Retry<Take<FixedInterval>, SinkCollabMessageAction<'a, Sink>> { pub fn run(self) -> Retry<Take<FixedInterval>, SinkCollabMessageAction<'a, Sink>> {
let retry_strategy = FixedInterval::new(Duration::from_secs(2)).take(5); let retry_strategy = FixedInterval::new(Duration::from_secs(2)).take(5);
@ -373,19 +363,16 @@ where
impl<'a, Sink> Action for SinkCollabMessageAction<'a, Sink> impl<'a, Sink> Action for SinkCollabMessageAction<'a, Sink>
where where
Sink: SinkExt<CollabMessage> + Send + Sync + Unpin + 'a, Sink: SinkExt<CollabMessage> + Clone + Send + Sync + Unpin + 'a,
{ {
type Future = Pin<Box<dyn Future<Output = Result<Self::Item, Self::Error>> + Send + Sync + 'a>>; type Future = Pin<Box<dyn Future<Output = Result<Self::Item, Self::Error>> + Send + Sync + 'a>>;
type Item = (); type Item = ();
type Error = RealtimeError; type Error = RealtimeError;
fn run(&mut self) -> Self::Future { fn run(&mut self) -> Self::Future {
let sink = self.sink.clone(); let mut sink = self.sink.clone();
let message = self.message.clone(); let message = self.message.clone();
Box::pin(async move { Box::pin(async move {
let mut sink = sink
.try_lock()
.map_err(|err| RealtimeError::Internal(Error::from(err)))?;
sink sink
.send(message) .send(message)
.await .await

View File

@ -228,7 +228,7 @@ where
sink: Sink, sink: Sink,
stream: Stream, stream: Stream,
) where ) where
Sink: SinkExt<CollabMessage> + Send + Sync + Unpin + 'static, Sink: SinkExt<CollabMessage> + Clone + Send + Sync + Unpin + 'static,
Stream: StreamExt<Item = Result<CollabMessage, E>> + Send + Sync + Unpin + 'static, Stream: StreamExt<Item = Result<CollabMessage, E>> + Send + Sync + Unpin + 'static,
<Sink as futures_util::Sink<CollabMessage>>::Error: std::error::Error + Send + Sync, <Sink as futures_util::Sink<CollabMessage>>::Error: std::error::Error + Send + Sync,
E: Into<Error> + Send + Sync + 'static, E: Into<Error> + Send + Sync + 'static,

View File

@ -46,8 +46,8 @@ impl RealtimeMetrics {
} }
pub fn record_mem_cache_usage(&self, size_in_bytes: usize) { pub fn record_mem_cache_usage(&self, size_in_bytes: usize) {
let size_in_mb = size_in_bytes / (1024 * 1024); let size_in_mb = size_in_bytes / 1024;
trace!("[metrics]: mem_cache_usage: {} MB", size_in_mb); trace!("[metrics]: mem_cache_usage: {} KB", size_in_mb);
self.mem_cache_usage.set(size_in_mb as i64); self.mem_cache_usage.set(size_in_mb as i64);
} }

View File

@ -4,6 +4,7 @@ use std::fmt::Debug;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
#[derive(Clone)]
pub struct UnboundedSenderSink<T>(pub tokio::sync::mpsc::UnboundedSender<T>); pub struct UnboundedSenderSink<T>(pub tokio::sync::mpsc::UnboundedSender<T>);
impl<T> UnboundedSenderSink<T> { impl<T> UnboundedSenderSink<T> {

View File

@ -14,7 +14,7 @@ use anyhow::anyhow;
use dashmap::DashMap; use dashmap::DashMap;
use sqlx::PgPool; use sqlx::PgPool;
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant;
use tokio::sync::broadcast; use tokio::sync::broadcast;
/// Manages access control. /// Manages access control.
@ -32,6 +32,7 @@ use tokio::sync::broadcast;
#[derive(Clone)] #[derive(Clone)]
pub struct AccessControl { pub struct AccessControl {
enforcer: Arc<AFEnforcer>, enforcer: Arc<AFEnforcer>,
#[allow(dead_code)]
access_control_metrics: Arc<AccessControlMetrics>, access_control_metrics: Arc<AccessControlMetrics>,
} }
@ -97,12 +98,7 @@ impl AccessControl {
where where
A: ToCasbinAction, A: ToCasbinAction,
{ {
let start = Instant::now(); self.enforcer.enforce(uid, obj, act).await
let result = self.enforcer.enforce(uid, obj, act).await;
self
.access_control_metrics
.record_enforce_duration(start.elapsed().as_millis() as u64);
result
} }
pub async fn get_access_level(&self, uid: &i64, oid: &str) -> Option<AFAccessLevel> { pub async fn get_access_level(&self, uid: &i64, oid: &str) -> Option<AFAccessLevel> {
@ -124,6 +120,38 @@ impl AccessControl {
} }
} }
/// policy in db:
/// p = 1, 123, 1 (1 mean AFRole::Owner)
/// p = 1, 456, 50 (50 mean AFAccessLevel::FullAccess)
///
/// role_definition in db:
/// g = _, _
/// af role:
/// ["1", "delete"], ["1", "write"], ["1", "read"],
/// ["2", "write"], ["2", "read"],
/// ["3", "read"],
/// af access level:
/// ["10", "read"],
/// ["20", "read"],
/// ["30", "read"], ["30", "write"],
/// ["50", "read"], ["50", "write"], ["50", "delete"]
///
/// matchers:
/// r.sub == p.sub && p.obj == r.obj && g(p.act, r.act)
///
/// Example:
/// request:
/// 1. api/workspace/123, user=1, workspace_id=123 GET
/// r = sub = 1, obj = 123, act =read
/// p = sub = 1, obj = 123, act = 1
///
/// Evaluation:
/// 1. Subject Match: r.sub == p.sub
/// 2. Object Match: p.obj == r.obj
/// 3. Action Permission: g(p.act, r.act) => g(1, read) => ["1", "read"]
/// Result:
/// Allow
///
pub const MODEL_CONF: &str = r###" pub const MODEL_CONF: &str = r###"
[request_definition] [request_definition]
r = sub, obj, act r = sub, obj, act
@ -133,13 +161,12 @@ p = sub, obj, act
[role_definition] [role_definition]
g = _, _ # rule for action g = _, _ # rule for action
g2 = _, _ # rule for collab object id
[policy_effect] [policy_effect]
e = some(where (p.eft == allow)) e = some(where (p.eft == allow))
[matchers] [matchers]
m = r.sub == p.sub && g2(p.obj, r.obj) && g(p.act, r.act) m = r.sub == p.sub && p.obj == r.obj && g(p.act, r.act)
"###; "###;
/// Represents the entity stored at the index of the access control policy. /// Represents the entity stored at the index of the access control policy.

View File

@ -134,7 +134,6 @@ impl Adapter for PgAdapter {
}, },
} }
} }
// Grouping definition `g` of type `g`. See `model.conf` // Grouping definition `g` of type `g`. See `model.conf`
model.add_policies("g", "g", grouping_policies); model.add_policies("g", "g", grouping_policies);
self self

View File

@ -1,4 +1,4 @@
use crate::biz::casbin::access_control::AccessControl; use crate::biz::casbin::access_control::{AccessControl, Action};
use crate::biz::casbin::access_control::{ActionType, ObjectType}; use crate::biz::casbin::access_control::{ActionType, ObjectType};
use actix_http::Method; use actix_http::Method;
use app_error::AppError; use app_error::AppError;
@ -69,31 +69,28 @@ impl CollabAccessControl for CollabAccessControlImpl {
async fn can_access_http_method( async fn can_access_http_method(
&self, &self,
_uid: &i64, uid: &i64,
_oid: &str, oid: &str,
_method: &Method, method: &Method,
) -> Result<bool, AppError> { ) -> Result<bool, AppError> {
Ok(true) let action = Action::from(method);
// let action = Action::from(method); self
// self .access_control
// .access_control .enforce(uid, &ObjectType::Collab(oid), action)
// .enforce(uid, &ObjectType::Collab(oid), action) .await
// .await
} }
async fn can_send_collab_update(&self, _uid: &i64, _oid: &str) -> Result<bool, AppError> { async fn can_send_collab_update(&self, uid: &i64, oid: &str) -> Result<bool, AppError> {
Ok(true) self
// self .access_control
// .access_control .enforce(uid, &ObjectType::Collab(oid), Action::Write)
// .enforce(uid, &ObjectType::Collab(oid), Action::Write) .await
// .await
} }
async fn can_receive_collab_update(&self, _uid: &i64, _oid: &str) -> Result<bool, AppError> { async fn can_receive_collab_update(&self, uid: &i64, oid: &str) -> Result<bool, AppError> {
Ok(true) self
// self .access_control
// .access_control .enforce(uid, &ObjectType::Collab(oid), Action::Read)
// .enforce(uid, &ObjectType::Collab(oid), Action::Read) .await
// .await
} }
} }

View File

@ -160,6 +160,7 @@ impl AFEnforcer {
.get_filtered_policy(POLICY_FIELD_INDEX_OBJECT, vec![obj.to_object_id()]); .get_filtered_policy(POLICY_FIELD_INDEX_OBJECT, vec![obj.to_object_id()]);
if policies_for_object.is_empty() { if policies_for_object.is_empty() {
self.enforcer_result_cache.insert(policy_key, true);
return Ok(true); return Ok(true);
} }

View File

@ -240,7 +240,7 @@ where
Some(encoded_collab) => { Some(encoded_collab) => {
event!( event!(
tracing::Level::DEBUG, tracing::Level::DEBUG,
"Get encoded collab:{} from redis", "Get encoded collab:{} from cache",
params.object_id params.object_id
); );
Ok(encoded_collab) Ok(encoded_collab)

View File

@ -120,7 +120,9 @@ async fn test_collab_access_control_when_obj_not_exist(pool: PgPool) -> anyhow::
let user = create_user(&pool).await?; let user = create_user(&pool).await?;
for method in [Method::GET, Method::POST, Method::PUT, Method::DELETE] { for method in [Method::GET, Method::POST, Method::PUT, Method::DELETE] {
assert_can_access_http_method(&collab_access_control, &user.uid, "fake_id", method, true).await; assert_can_access_http_method(&collab_access_control, &user.uid, "fake_id", method, true)
.await
.unwrap();
} }
Ok(()) Ok(())
@ -160,7 +162,8 @@ async fn test_collab_access_control_access_http_method(pool: PgPool) -> anyhow::
method, method,
true, true,
) )
.await; .await
.unwrap();
} }
assert!( assert!(
@ -178,7 +181,8 @@ async fn test_collab_access_control_access_http_method(pool: PgPool) -> anyhow::
Method::GET, Method::GET,
true, true,
) )
.await; .await
.unwrap();
// guest should not have write access // guest should not have write access
assert_can_access_http_method( assert_can_access_http_method(
@ -188,7 +192,8 @@ async fn test_collab_access_control_access_http_method(pool: PgPool) -> anyhow::
Method::POST, Method::POST,
false, false,
) )
.await; .await
.unwrap();
assert!( assert!(
!collab_access_control !collab_access_control
@ -242,7 +247,6 @@ async fn test_collab_access_control_send_receive_collab_update(pool: PgPool) ->
.await; .await;
// Need to wait for the listener(spawn_listen_on_workspace_member_change) to receive the event // Need to wait for the listener(spawn_listen_on_workspace_member_change) to receive the event
//
sleep(Duration::from_secs(2)).await; sleep(Duration::from_secs(2)).await;
assert!( assert!(

View File

@ -1,5 +1,5 @@
use actix_http::Method; use actix_http::Method;
use anyhow::Context; use anyhow::{Context, Error};
use app_error::ErrorCode; use app_error::ErrorCode;
use appflowy_cloud::biz; use appflowy_cloud::biz;
use appflowy_cloud::biz::casbin::{CollabAccessControlImpl, WorkspaceAccessControlImpl}; use appflowy_cloud::biz::casbin::{CollabAccessControlImpl, WorkspaceAccessControlImpl};
@ -276,7 +276,7 @@ pub async fn assert_can_access_http_method(
object_id: &str, object_id: &str,
method: Method, method: Method,
expected: bool, expected: bool,
) { ) -> Result<(), Error> {
let timeout_duration = Duration::from_secs(10); let timeout_duration = Duration::from_secs(10);
let retry_interval = Duration::from_millis(300); let retry_interval = Duration::from_millis(300);
let mut retries = 0usize; let mut retries = 0usize;
@ -307,9 +307,8 @@ pub async fn assert_can_access_http_method(
} }
}; };
timeout(timeout_duration, operation) timeout(timeout_duration, operation).await?;
.await Ok(())
.expect("Operation timed out");
} }
pub async fn add_workspace_members_in_tx( pub async fn add_workspace_members_in_tx(

View File

@ -434,6 +434,9 @@ async fn multiple_user_with_read_and_write_permission_edit_same_collab_test() {
expected_json.insert(index.to_string(), s); expected_json.insert(index.to_string(), s);
} }
// wait 5 seconds to make sure all the server broadcast the updates to all the clients
sleep(Duration::from_secs(5)).await;
// all the clients should have the same collab object // all the clients should have the same collab object
assert_json_include!( assert_json_include!(
actual: json!(expected_json), actual: json!(expected_json),

View File

@ -467,7 +467,7 @@ async fn post_realtime_message_test() {
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut new_user = TestClient::new_user().await; let mut new_user = TestClient::new_user().await;
// sleep 2 secs to make sure it do not trigger register user too fast in gotrue // sleep 2 secs to make sure it do not trigger register user too fast in gotrue
sleep(Duration::from_secs(i % 3)).await; sleep(Duration::from_secs(i % 5)).await;
let object_id = Uuid::new_v4().to_string(); let object_id = Uuid::new_v4().to_string();
let workspace_id = new_user.workspace_id().await; let workspace_id = new_user.workspace_id().await;

View File

@ -64,7 +64,7 @@ async fn sign_up_oauth_not_available() {
#[tokio::test] #[tokio::test]
async fn concurrent_user_sign_up_test() { async fn concurrent_user_sign_up_test() {
let mut tasks = Vec::new(); let mut tasks = Vec::new();
for _i in 0..50 { for _i in 0..30 {
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let _ = TestClient::new_user().await; let _ = TestClient::new_user().await;
tokio::time::sleep(Duration::from_millis(300)).await; tokio::time::sleep(Duration::from_millis(300)).await;