chore: simplify collab level access control (#1008)

This commit is contained in:
Khor Shu Heng 2024-11-20 12:29:16 +08:00 committed by GitHub
parent e6dbc95641
commit afeaeb7796
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 91 additions and 160 deletions

View File

@ -6,7 +6,7 @@ use tracing::instrument;
use crate::{
act::{Action, ActionVariant},
collab::{CollabAccessControl, RealtimeAccessControl},
entity::{ObjectType, SubjectType},
entity::ObjectType,
};
use super::access::AccessControl;
@ -28,16 +28,25 @@ impl CollabAccessControl for CollabAccessControlImpl {
&self,
workspace_id: &str,
uid: &i64,
oid: &str,
_oid: &str,
action: Action,
) -> Result<(), AppError> {
// TODO: allow non workspace member to read a collab.
// Anyone who can write to a workspace, can also delete a collab.
let workspace_action = match action {
Action::Read => Action::Read,
Action::Write => Action::Write,
Action::Delete => Action::Write,
};
self
.access_control
.enforce(
workspace_id,
uid,
ObjectType::Collab(oid),
ActionVariant::FromAction(&action),
ObjectType::Workspace(workspace_id),
ActionVariant::FromAction(&workspace_action),
)
.await
}
@ -46,16 +55,26 @@ impl CollabAccessControl for CollabAccessControlImpl {
&self,
workspace_id: &str,
uid: &i64,
oid: &str,
_oid: &str,
access_level: AFAccessLevel,
) -> Result<(), AppError> {
// TODO: allow non workspace member to read a collab.
// Anyone who can write to a workspace, also have full access to a collab.
let workspace_action = match access_level {
AFAccessLevel::ReadOnly => Action::Read,
AFAccessLevel::ReadAndComment => Action::Read,
AFAccessLevel::ReadAndWrite => Action::Write,
AFAccessLevel::FullAccess => Action::Write,
};
self
.access_control
.enforce(
workspace_id,
uid,
ObjectType::Collab(oid),
ActionVariant::FromAccessLevel(&access_level),
ObjectType::Workspace(workspace_id),
ActionVariant::FromAction(&workspace_action),
)
.await
}
@ -63,28 +82,17 @@ impl CollabAccessControl for CollabAccessControlImpl {
#[instrument(level = "info", skip_all)]
async fn update_access_level_policy(
&self,
uid: &i64,
oid: &str,
level: AFAccessLevel,
_uid: &i64,
_oid: &str,
_level: AFAccessLevel,
) -> Result<(), AppError> {
self
.access_control
.update_policy(
SubjectType::User(*uid),
ObjectType::Collab(oid),
ActionVariant::FromAccessLevel(&level),
)
.await?;
// TODO: allow non workspace member to read a collab.
Ok(())
}
#[instrument(level = "info", skip_all)]
async fn remove_access_level(&self, uid: &i64, oid: &str) -> Result<(), AppError> {
self
.access_control
.remove_policy(&SubjectType::User(*uid), &ObjectType::Collab(oid))
.await?;
async fn remove_access_level(&self, _uid: &i64, _oid: &str) -> Result<(), AppError> {
// TODO: allow non workspace member to read a collab.
Ok(())
}
}
@ -103,20 +111,35 @@ impl RealtimeCollabAccessControlImpl {
&self,
workspace_id: &str,
uid: &i64,
oid: &str,
_oid: &str,
required_action: Action,
) -> Result<bool, AppError> {
self
// TODO: allow non workspace member to read a collab.
// Anyone who can write to a workspace, can also delete a collab.
let workspace_action = match required_action {
Action::Read => Action::Read,
Action::Write => Action::Write,
Action::Delete => Action::Write,
};
let enforcement_result = self
.access_control
.enforce(
workspace_id,
uid,
ObjectType::Collab(oid),
ActionVariant::FromAction(&required_action),
ObjectType::Workspace(workspace_id),
ActionVariant::FromAction(&workspace_action),
)
.await?;
Ok(true)
.await;
match enforcement_result {
Ok(_) => Ok(true),
Err(AppError::NotEnoughPermissions {
user: _user,
workspace_id: _workspace_id,
}) => Ok(false),
Err(e) => Err(e),
}
}
}

View File

@ -37,10 +37,9 @@ use client_api::entity::{
};
use client_api::ws::{WSClient, WSClientConfig};
use database_entity::dto::{
AFAccessLevel, AFRole, AFSnapshotMeta, AFSnapshotMetas, AFUserProfile, AFUserWorkspaceInfo,
AFWorkspace, AFWorkspaceInvitationStatus, AFWorkspaceMember, BatchQueryCollabResult,
CollabParams, CreateCollabParams, InsertCollabMemberParams, QueryCollab, QueryCollabParams,
QuerySnapshotParams, SnapshotData, UpdateCollabMemberParams,
AFRole, AFSnapshotMeta, AFSnapshotMetas, AFUserProfile, AFUserWorkspaceInfo, AFWorkspace,
AFWorkspaceInvitationStatus, AFWorkspaceMember, BatchQueryCollabResult, CollabParams,
CreateCollabParams, QueryCollab, QueryCollabParams, QuerySnapshotParams, SnapshotData,
};
use shared_entity::dto::workspace_dto::{
BlobMetadata, CollabResponse, PublishedDuplicate, WorkspaceMemberChangeset,
@ -441,46 +440,6 @@ impl TestClient {
self.api_client.get_workspace_member(params).await
}
pub async fn add_collab_member(
&self,
workspace_id: &str,
object_id: &str,
other_client: &TestClient,
access_level: AFAccessLevel,
) {
let uid = other_client.uid().await;
self
.api_client
.add_collab_member(InsertCollabMemberParams {
uid,
workspace_id: workspace_id.to_string(),
object_id: object_id.to_string(),
access_level,
})
.await
.unwrap();
}
pub async fn update_collab_member_access_level(
&self,
workspace_id: &str,
object_id: &str,
other_client: &TestClient,
access_level: AFAccessLevel,
) {
let uid = other_client.uid().await;
self
.api_client
.update_collab_member(UpdateCollabMemberParams {
uid,
workspace_id: workspace_id.to_string(),
object_id: object_id.to_string(),
access_level,
})
.await
.unwrap();
}
pub async fn wait_object_sync_complete(&self, object_id: &str) -> Result<(), Error> {
self
.wait_object_sync_complete_with_secs(object_id, 60)

View File

@ -4,7 +4,7 @@ use collab_entity::CollabType;
use tokio::time::sleep;
use client_api_test::TestClient;
use database_entity::dto::{AFAccessLevel, AFRole};
use database_entity::dto::AFRole;
#[tokio::test]
async fn viewing_document_editing_users_test() {
@ -27,14 +27,6 @@ async fn viewing_document_editing_users_test() {
assert_eq!(clients.len(), 1);
assert_eq!(clients[0], owner_uid);
owner
.add_collab_member(
&workspace_id,
&object_id,
&guest,
AFAccessLevel::ReadAndWrite,
)
.await;
guest
.open_collab(&workspace_id, &object_id, collab_type)
.await;

View File

@ -1,11 +1,11 @@
use std::time::Duration;
use client_api::entity::AFRole;
use collab_entity::CollabType;
use serde_json::{json, Value};
use tokio::time::sleep;
use client_api_test::{assert_client_collab_include_value, TestClient};
use database_entity::dto::AFAccessLevel;
#[tokio::test]
async fn client_apply_update_find_missing_update_test() {
@ -56,13 +56,9 @@ async fn make_clients() -> (TestClient, TestClient, String, Value) {
.create_and_edit_collab(&workspace_id, collab_type.clone())
.await;
client_1
.add_collab_member(
&workspace_id,
&object_id,
&client_2,
AFAccessLevel::ReadAndWrite,
)
.await;
.invite_and_accepted_workspace_member(&workspace_id, &client_2, AFRole::Member)
.await
.unwrap();
// after client 2 finish init sync and then disable receive message
client_2

View File

@ -1,5 +1,6 @@
use std::time::Duration;
use client_api::entity::AFRole;
use collab_entity::CollabType;
use serde_json::json;
use sqlx::types::uuid;
@ -7,7 +8,7 @@ use tokio::time::sleep;
use tracing::trace;
use client_api_test::*;
use database_entity::dto::{AFAccessLevel, QueryCollabParams};
use database_entity::dto::QueryCollabParams;
#[tokio::test]
async fn sync_collab_content_after_reconnect_test() {
@ -200,15 +201,11 @@ async fn edit_document_with_both_clients_offline_then_online_sync_test() {
.create_and_edit_collab(&workspace_id, collab_type.clone())
.await;
// add client 2 as a member of the collab
// add client 2 as a member of the workspace
client_1
.add_collab_member(
&workspace_id,
&object_id,
&client_2,
AFAccessLevel::ReadAndWrite,
)
.await;
.invite_and_accepted_workspace_member(&workspace_id, &client_2, AFRole::Member)
.await
.unwrap();
client_1.disconnect().await;
client_2

View File

@ -12,7 +12,7 @@ use client_api_test::{
assert_client_collab_include_value, assert_client_collab_within_secs, assert_server_collab,
TestClient,
};
use database_entity::dto::{AFAccessLevel, AFRole};
use database_entity::dto::AFRole;
use crate::collab::util::generate_random_string;
@ -164,13 +164,9 @@ async fn edit_collab_with_readonly_permission_test() {
// Add client 2 as the member of the collab then the client 2 will receive the update.
client_1
.add_collab_member(
&workspace_id,
&object_id,
&client_2,
AFAccessLevel::ReadOnly,
)
.await;
.invite_and_accepted_workspace_member(&workspace_id, &client_2, AFRole::Guest)
.await
.unwrap();
client_2
.open_collab(&workspace_id, &object_id, collab_type.clone())
@ -214,13 +210,9 @@ async fn edit_collab_with_read_and_write_permission_test() {
// Add client 2 as the member of the collab then the client 2 will receive the update.
client_1
.add_collab_member(
&workspace_id,
&object_id,
&client_2,
AFAccessLevel::ReadAndWrite,
)
.await;
.invite_and_accepted_workspace_member(&workspace_id, &client_2, AFRole::Member)
.await
.unwrap();
client_2
.open_collab(&workspace_id, &object_id, collab_type.clone())
@ -265,13 +257,9 @@ async fn edit_collab_with_full_access_permission_test() {
// Add client 2 as the member of the collab then the client 2 will receive the update.
client_1
.add_collab_member(
&workspace_id,
&object_id,
&client_2,
AFAccessLevel::FullAccess,
)
.await;
.invite_and_accepted_workspace_member(&workspace_id, &client_2, AFRole::Member)
.await
.unwrap();
client_2
.open_collab(&workspace_id, &object_id, collab_type.clone())
@ -314,13 +302,9 @@ async fn edit_collab_with_full_access_then_readonly_permission() {
// Add client 2 as the member of the collab then the client 2 will receive the update.
client_1
.add_collab_member(
&workspace_id,
&object_id,
&client_2,
AFAccessLevel::FullAccess,
)
.await;
.invite_and_accepted_workspace_member(&workspace_id, &client_2, AFRole::Member)
.await
.unwrap();
// client 2 edit the collab and then the server will broadcast the update
{
@ -340,13 +324,9 @@ async fn edit_collab_with_full_access_then_readonly_permission() {
// updates generated by client 2
{
client_1
.update_collab_member_access_level(
&workspace_id,
&object_id,
&client_2,
AFAccessLevel::ReadOnly,
)
.await;
.try_update_workspace_member(&workspace_id, &client_2, AFRole::Guest)
.await
.unwrap();
client_2
.insert_into(&object_id, "subtitle", "Writing Rust, fun")
.await;
@ -404,14 +384,6 @@ async fn multiple_user_with_read_and_write_permission_edit_same_collab_test() {
.invite_and_accepted_workspace_member(&workspace_id, &new_member, AFRole::Member)
.await
.unwrap();
owner
.add_collab_member(
&workspace_id,
&object_id,
&new_member,
AFAccessLevel::ReadAndWrite,
)
.await;
new_member
.open_collab(&workspace_id, &object_id, collab_type.clone())
@ -490,13 +462,9 @@ async fn multiple_user_with_read_only_permission_edit_same_collab_test() {
// sleep 2 secs to make sure it do not trigger register user too fast in gotrue
sleep(Duration::from_secs(i % 2)).await;
owner
.add_collab_member(
&workspace_id,
&object_id,
&new_user,
AFAccessLevel::ReadOnly,
)
.await;
.invite_and_accepted_workspace_member(&workspace_id, &new_user, AFRole::Guest)
.await
.unwrap();
new_user
.open_collab(&workspace_id, &object_id, collab_type.clone())

View File

@ -3,6 +3,7 @@ use std::sync::Arc;
use std::time::Duration;
use assert_json_diff::assert_json_eq;
use client_api::entity::AFRole;
use collab::core::origin::CollabOrigin;
use collab_entity::CollabType;
use serde_json::json;
@ -11,7 +12,6 @@ use uuid::Uuid;
use client_api_test::*;
use collab_rt_entity::{CollabMessage, RealtimeMessage, UpdateSync, MAXIMUM_REALTIME_MESSAGE_SIZE};
use database_entity::dto::AFAccessLevel;
use crate::collab::util::{
generate_random_bytes, generate_random_string, make_big_collab_doc_state,
@ -325,13 +325,9 @@ async fn two_direction_peer_sync_test() {
// Before the client_2 want to edit the collab object, it needs to become a member of the collab
// Otherwise, the server will reject the edit request
client_1
.add_collab_member(
&workspace_id,
&object_id,
&client_2,
AFAccessLevel::FullAccess,
)
.await;
.invite_and_accepted_workspace_member(&workspace_id, &client_2, AFRole::Member)
.await
.unwrap();
client_2
.open_collab(&workspace_id, &object_id, collab_type.clone())