Merge pull request #885 from AppFlowy-IO/additional-ac-config

feat: additional access control config
This commit is contained in:
Khor Shu Heng 2024-10-16 17:00:54 +08:00 committed by GitHub
commit ec124bcb46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 159 additions and 145 deletions

View File

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n WITH request_id_workspace_member_count AS (\n SELECT\n request_id,\n COUNT(*) AS member_count\n FROM af_access_request\n JOIN af_workspace_member USING (workspace_id)\n WHERE request_id = $1\n GROUP BY request_id\n )\n SELECT\n request_id,\n view_id,\n (\n workspace_id,\n af_workspace.database_storage_id,\n af_workspace.owner_uid,\n owner_profile.name,\n owner_profile.email,\n af_workspace.created_at,\n af_workspace.workspace_type,\n af_workspace.deleted_at,\n af_workspace.workspace_name,\n af_workspace.icon,\n request_id_workspace_member_count.member_count\n ) AS \"workspace!: AFWorkspaceWithMemberCountRow\",\n (\n af_user.uuid,\n af_user.name,\n af_user.email,\n af_user.metadata ->> 'icon_url'\n ) AS \"requester!: AFAccessRequesterColumn\",\n status AS \"status: AFAccessRequestStatusColumn\",\n af_access_request.created_at AS created_at\n FROM af_access_request\n JOIN af_user USING (uid)\n JOIN af_workspace USING (workspace_id)\n JOIN af_user AS owner_profile ON af_workspace.owner_uid = owner_profile.uid\n JOIN request_id_workspace_member_count USING (request_id)\n WHERE request_id = $1\n ", "query": "\n WITH request_id_workspace_member_count AS (\n SELECT\n request_id,\n COUNT(*) AS member_count\n FROM af_access_request\n JOIN af_workspace_member USING (workspace_id)\n WHERE request_id = $1\n GROUP BY request_id\n )\n SELECT\n request_id,\n view_id,\n (\n workspace_id,\n af_workspace.database_storage_id,\n af_workspace.owner_uid,\n owner_profile.name,\n owner_profile.email,\n af_workspace.created_at,\n af_workspace.workspace_type,\n af_workspace.deleted_at,\n af_workspace.workspace_name,\n af_workspace.icon,\n request_id_workspace_member_count.member_count\n ) AS \"workspace!: AFWorkspaceWithMemberCountRow\",\n (\n af_user.uid,\n af_user.uuid,\n af_user.name,\n af_user.email,\n af_user.metadata ->> 'icon_url'\n ) AS \"requester!: AFAccessRequesterColumn\",\n status AS \"status: AFAccessRequestStatusColumn\",\n af_access_request.created_at AS created_at\n FROM af_access_request\n JOIN af_user USING (uid)\n JOIN af_workspace USING (workspace_id)\n JOIN af_user AS owner_profile ON af_workspace.owner_uid = owner_profile.uid\n JOIN request_id_workspace_member_count USING (request_id)\n WHERE request_id = $1\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -48,5 +48,5 @@
false false
] ]
}, },
"hash": "0c3ae560880e82218d13c5992540386ea1566e45e31acd5fb51886aabcd98479" "hash": "67b381fdcd20f8cfe782d939e56bf94f105cdb23a59fefb846afe8105d91d129"
} }

View File

@ -64,8 +64,8 @@ pub enum AppError {
#[error("Not Logged In:{0}")] #[error("Not Logged In:{0}")]
NotLoggedIn(String), NotLoggedIn(String),
#[error("{user}: do not have permissions to {action}")] #[error("User does not have permissions to execute this action")]
NotEnoughPermissions { user: String, action: String }, NotEnoughPermissions,
#[error("s3 response error:{0}")] #[error("s3 response error:{0}")]
S3ResponseError(String), S3ResponseError(String),

View File

@ -1403,6 +1403,7 @@ pub struct AccessRequestWithViewId {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct AccessRequesterInfo { pub struct AccessRequesterInfo {
pub uid: i64,
pub uuid: Uuid, pub uuid: Uuid,
pub email: String, pub email: String,
pub name: String, pub name: String,

View File

@ -80,6 +80,7 @@ pub async fn select_access_request_by_request_id<'a, E: Executor<'a, Database =
request_id_workspace_member_count.member_count request_id_workspace_member_count.member_count
) AS "workspace!: AFWorkspaceWithMemberCountRow", ) AS "workspace!: AFWorkspaceWithMemberCountRow",
( (
af_user.uid,
af_user.uuid, af_user.uuid,
af_user.name, af_user.name,
af_user.email, af_user.email,

View File

@ -581,6 +581,7 @@ impl From<AFAccessRequestStatusColumn> for AccessRequestStatus {
#[derive(sqlx::Type, Serialize, Debug)] #[derive(sqlx::Type, Serialize, Debug)]
pub struct AFAccessRequesterColumn { pub struct AFAccessRequesterColumn {
pub uid: i64,
pub uuid: Uuid, pub uuid: Uuid,
pub name: String, pub name: String,
pub email: String, pub email: String,
@ -590,6 +591,7 @@ pub struct AFAccessRequesterColumn {
impl From<AFAccessRequesterColumn> for AccessRequesterInfo { impl From<AFAccessRequesterColumn> for AccessRequesterInfo {
fn from(value: AFAccessRequesterColumn) -> Self { fn from(value: AFAccessRequesterColumn) -> Self {
Self { Self {
uid: value.uid,
uuid: value.uuid, uuid: value.uuid,
name: value.name, name: value.name,
email: value.email, email: value.email,

View File

@ -517,10 +517,7 @@ pub async fn delete_workspace_members(
.unwrap_or(false); .unwrap_or(false);
if is_owner { if is_owner {
return Err(AppError::NotEnoughPermissions { return Err(AppError::NotEnoughPermissions);
user: member_email.to_string(),
action: format!("delete member from workspace {}", workspace_id),
});
} }
sqlx::query!( sqlx::query!(

View File

@ -92,10 +92,7 @@ where
.await?; .await?;
if !can_write_workspace { if !can_write_workspace {
return Err(AppError::NotEnoughPermissions { return Err(AppError::NotEnoughPermissions);
user: uid.to_string(),
action: format!("write workspace:{}", workspace_id),
});
} }
Ok(()) Ok(())
} }
@ -113,10 +110,7 @@ where
.await?; .await?;
if !can_write { if !can_write {
return Err(AppError::NotEnoughPermissions { return Err(AppError::NotEnoughPermissions);
user: uid.to_string(),
action: format!("update collab:{}", object_id),
});
} }
Ok(()) Ok(())
} }
@ -375,10 +369,7 @@ where
.await?; .await?;
if !can_read { if !can_read {
return Err(AppError::NotEnoughPermissions { return Err(AppError::NotEnoughPermissions);
user: uid.to_string(),
action: format!("read collab:{}", params.object_id),
});
} }
}, },
GetCollabOrigin::Server => {}, GetCollabOrigin::Server => {},
@ -470,10 +461,7 @@ where
.enforce_delete(workspace_id, uid, object_id) .enforce_delete(workspace_id, uid, object_id)
.await? .await?
{ {
return Err(AppError::NotEnoughPermissions { return Err(AppError::NotEnoughPermissions);
user: uid.to_string(),
action: format!("delete collab:{}", object_id),
});
} }
self.cache.delete_collab(object_id).await?; self.cache.delete_collab(object_id).await?;
Ok(()) Ok(())

View File

@ -42,7 +42,6 @@ async fn get_access_request_handler(
&state.pg_pool, &state.pg_pool,
state.collab_access_control_storage.clone(), state.collab_access_control_storage.clone(),
access_request_id, access_request_id,
*uuid,
uid, uid,
) )
.await?; .await?;
@ -100,11 +99,11 @@ async fn post_approve_access_request_handler(
)))?; )))?;
approve_or_reject_access_request( approve_or_reject_access_request(
&state.pg_pool, &state.pg_pool,
state.workspace_access_control.clone(),
state.mailer.clone(), state.mailer.clone(),
&appflowy_web_url, &appflowy_web_url,
access_request_id, access_request_id,
uid, uid,
*uuid,
is_approved, is_approved,
) )
.await?; .await?;

View File

@ -1,3 +1,4 @@
use access_control::act::Action;
use actix_web::web::{Bytes, Payload}; use actix_web::web::{Bytes, Payload};
use actix_web::web::{Data, Json, PayloadConfig}; use actix_web::web::{Data, Json, PayloadConfig};
use actix_web::{web, Scope}; use actix_web::{web, Scope};
@ -266,11 +267,18 @@ async fn patch_workspace_handler(
} }
async fn delete_workspace_handler( async fn delete_workspace_handler(
_user_id: UserUuid, user_uuid: UserUuid,
workspace_id: web::Path<Uuid>, workspace_id: web::Path<Uuid>,
state: Data<AppState>, state: Data<AppState>,
) -> Result<Json<AppResponse<()>>> { ) -> Result<Json<AppResponse<()>>> {
// TODO: add permission for workspace deletion let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let has_access = state
.workspace_access_control
.enforce_role(&uid, &workspace_id.to_string(), AFRole::Owner)
.await?;
if !has_access {
return Err(AppError::NotEnoughPermissions.into());
}
workspace::ops::delete_workspace_for_user( workspace::ops::delete_workspace_for_user(
state.pg_pool.clone(), state.pg_pool.clone(),
*workspace_id, *workspace_id,
@ -303,6 +311,15 @@ async fn post_workspace_invite_handler(
payload: Json<Vec<WorkspaceMemberInvitation>>, payload: Json<Vec<WorkspaceMemberInvitation>>,
state: Data<AppState>, state: Data<AppState>,
) -> Result<JsonAppResponse<()>> { ) -> Result<JsonAppResponse<()>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let has_access = state
.workspace_access_control
.enforce_role(&uid, &workspace_id.to_string(), AFRole::Owner)
.await?;
if !has_access {
return Err(AppError::NotEnoughPermissions.into());
}
let invited_members = payload.into_inner(); let invited_members = payload.into_inner();
workspace::ops::invite_workspace_members( workspace::ops::invite_workspace_members(
&state.mailer, &state.mailer,
@ -371,13 +388,14 @@ async fn get_workspace_settings_handler(
workspace_id: web::Path<Uuid>, workspace_id: web::Path<Uuid>,
) -> Result<JsonAppResponse<AFWorkspaceSettings>> { ) -> Result<JsonAppResponse<AFWorkspaceSettings>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?; let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let settings = workspace::ops::get_workspace_settings( let has_access = state
&state.pg_pool, .workspace_access_control
state.workspace_access_control.clone(), .enforce_action(&uid, &workspace_id.to_string(), Action::Read)
&workspace_id,
&uid,
)
.await?; .await?;
if !has_access {
return Err(AppError::NotEnoughPermissions.into());
}
let settings = workspace::ops::get_workspace_settings(&state.pg_pool, &workspace_id).await?;
Ok(AppResponse::Ok().with_data(settings).into()) Ok(AppResponse::Ok().with_data(settings).into())
} }
@ -391,23 +409,32 @@ async fn post_workspace_settings_handler(
let data = data.into_inner(); let data = data.into_inner();
trace!("workspace settings: {:?}", data); trace!("workspace settings: {:?}", data);
let uid = state.user_cache.get_user_uid(&user_uuid).await?; let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let settings = workspace::ops::update_workspace_settings( let has_access = state
&state.pg_pool, .workspace_access_control
state.workspace_access_control.clone(), .enforce_action(&uid, &workspace_id.to_string(), Action::Write)
&workspace_id,
&uid,
data,
)
.await?; .await?;
if !has_access {
return Err(AppError::NotEnoughPermissions.into());
}
let settings =
workspace::ops::update_workspace_settings(&state.pg_pool, &workspace_id, data).await?;
Ok(AppResponse::Ok().with_data(settings).into()) Ok(AppResponse::Ok().with_data(settings).into())
} }
#[instrument(skip_all, err)] #[instrument(skip_all, err)]
async fn get_workspace_members_handler( async fn get_workspace_members_handler(
_user_uuid: UserUuid, user_uuid: UserUuid,
state: Data<AppState>, state: Data<AppState>,
workspace_id: web::Path<Uuid>, workspace_id: web::Path<Uuid>,
) -> Result<JsonAppResponse<Vec<AFWorkspaceMember>>> { ) -> Result<JsonAppResponse<Vec<AFWorkspaceMember>>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let has_access = state
.workspace_access_control
.enforce_action(&uid, &workspace_id.to_string(), Action::Read)
.await?;
if !has_access {
return Err(AppError::NotEnoughPermissions.into());
}
let members = workspace::ops::get_workspace_members(&state.pg_pool, &workspace_id) let members = workspace::ops::get_workspace_members(&state.pg_pool, &workspace_id)
.await? .await?
.into_iter() .into_iter()
@ -424,11 +451,20 @@ async fn get_workspace_members_handler(
#[instrument(skip_all, err)] #[instrument(skip_all, err)]
async fn remove_workspace_member_handler( async fn remove_workspace_member_handler(
_user_uuid: UserUuid, user_uuid: UserUuid,
payload: Json<WorkspaceMembers>, payload: Json<WorkspaceMembers>,
state: Data<AppState>, state: Data<AppState>,
workspace_id: web::Path<Uuid>, workspace_id: web::Path<Uuid>,
) -> Result<JsonAppResponse<()>> { ) -> Result<JsonAppResponse<()>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let has_access = state
.workspace_access_control
.enforce_role(&uid, &workspace_id.to_string(), AFRole::Owner)
.await?;
if !has_access {
return Err(AppError::NotEnoughPermissions.into());
}
let member_emails = payload let member_emails = payload
.into_inner() .into_inner()
.0 .0
@ -448,12 +484,22 @@ async fn remove_workspace_member_handler(
#[instrument(skip_all, err)] #[instrument(skip_all, err)]
async fn get_workspace_member_handler( async fn get_workspace_member_handler(
user_uuid: UserUuid,
state: Data<AppState>, state: Data<AppState>,
path: web::Path<(Uuid, i64)>, path: web::Path<(Uuid, i64)>,
) -> Result<JsonAppResponse<AFWorkspaceMember>> { ) -> Result<JsonAppResponse<AFWorkspaceMember>> {
let (workspace_id, user_id) = path.into_inner(); let (workspace_id, user_uuid_to_retrieved) = path.into_inner();
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let has_access = state
.workspace_access_control
.enforce_action(&uid, &workspace_id.to_string(), Action::Read)
.await?;
if !has_access {
return Err(AppError::NotEnoughPermissions.into());
}
let member_row = let member_row =
workspace::ops::get_workspace_member(&user_id, &state.pg_pool, &workspace_id).await?; workspace::ops::get_workspace_member(&user_uuid_to_retrieved, &state.pg_pool, &workspace_id)
.await?;
let member = AFWorkspaceMember { let member = AFWorkspaceMember {
name: member_row.name, name: member_row.name,
email: member_row.email, email: member_row.email,
@ -494,21 +540,29 @@ async fn leave_workspace_handler(
#[instrument(level = "debug", skip_all, err)] #[instrument(level = "debug", skip_all, err)]
async fn update_workspace_member_handler( async fn update_workspace_member_handler(
user_uuid: UserUuid,
payload: Json<WorkspaceMemberChangeset>, payload: Json<WorkspaceMemberChangeset>,
state: Data<AppState>, state: Data<AppState>,
workspace_id: web::Path<Uuid>, workspace_id: web::Path<Uuid>,
) -> Result<JsonAppResponse<()>> { ) -> Result<JsonAppResponse<()>> {
// TODO: only owner is allowed to update member role
let workspace_id = workspace_id.into_inner(); let workspace_id = workspace_id.into_inner();
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let has_access = state
.workspace_access_control
.enforce_role(&uid, &workspace_id.to_string(), AFRole::Owner)
.await?;
if !has_access {
return Err(AppError::NotEnoughPermissions.into());
}
let changeset = payload.into_inner(); let changeset = payload.into_inner();
if changeset.role.is_some() { if changeset.role.is_some() {
let uid = select_uid_from_email(&state.pg_pool, &changeset.email) let changeset_uid = select_uid_from_email(&state.pg_pool, &changeset.email)
.await .await
.map_err(AppResponseError::from)?; .map_err(AppResponseError::from)?;
workspace::ops::update_workspace_member( workspace::ops::update_workspace_member(
&uid, &changeset_uid,
&state.pg_pool, &state.pg_pool,
&workspace_id, &workspace_id,
&changeset, &changeset,

View File

@ -165,8 +165,10 @@ pub async fn run_actix_server(
SessionMiddleware::builder(redis_store.clone(), key.clone()) SessionMiddleware::builder(redis_store.clone(), key.clone())
.build(), .build(),
) )
// .wrap(DecryptPayloadMiddleware) .wrap(Condition::new(
.wrap(Condition::new(config.access_control.is_enabled, access_control.clone())) config.access_control.is_enabled && config.access_control.enable_middleware,
access_control.clone())
)
.wrap(RequestIdMiddleware) .wrap(RequestIdMiddleware)
.service(server_info_scope()) .service(server_info_scope())
.service(user_scope()) .service(user_scope())
@ -270,7 +272,6 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result<A
info!("Setting up Pg listeners..."); info!("Setting up Pg listeners...");
let pg_listeners = Arc::new(PgListeners::new(&pg_pool).await?); let pg_listeners = Arc::new(PgListeners::new(&pg_pool).await?);
// let collab_member_listener = pg_listeners.subscribe_collab_member_change(); // let collab_member_listener = pg_listeners.subscribe_collab_member_change();
// let workspace_member_listener = pg_listeners.subscribe_workspace_member_change();
info!( info!(
"Setting up access controls, is_enable: {}", "Setting up access controls, is_enable: {}",
@ -279,27 +280,21 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result<A
let access_control = let access_control =
AccessControl::new(pg_pool.clone(), metrics.access_control_metrics.clone()).await?; AccessControl::new(pg_pool.clone(), metrics.access_control_metrics.clone()).await?;
// spawn_listen_on_workspace_member_change(workspace_member_listener, access_control.clone());
// spawn_listen_on_collab_member_change(
// pg_pool.clone(),
// collab_member_listener,
// access_control.clone(),
// );
let user_cache = UserCache::new(pg_pool.clone()).await; let user_cache = UserCache::new(pg_pool.clone()).await;
let collab_access_control: Arc<dyn CollabAccessControl> = if config.access_control.is_enabled { let collab_access_control: Arc<dyn CollabAccessControl> =
if config.access_control.is_enabled && config.access_control.enable_collab_access_control {
Arc::new(CollabAccessControlImpl::new(access_control.clone())) Arc::new(CollabAccessControlImpl::new(access_control.clone()))
} else { } else {
Arc::new(NoOpsCollabAccessControlImpl::new()) Arc::new(NoOpsCollabAccessControlImpl::new())
}; };
let workspace_access_control: Arc<dyn WorkspaceAccessControl> = let workspace_access_control: Arc<dyn WorkspaceAccessControl> =
if config.access_control.is_enabled { if config.access_control.is_enabled && config.access_control.enable_workspace_access_control {
Arc::new(WorkspaceAccessControlImpl::new(access_control.clone())) Arc::new(WorkspaceAccessControlImpl::new(access_control.clone()))
} else { } else {
Arc::new(NoOpsWorkspaceAccessControlImpl::new()) Arc::new(NoOpsWorkspaceAccessControlImpl::new())
}; };
let realtime_access_control: Arc<dyn RealtimeAccessControl> = if config.access_control.is_enabled let realtime_access_control: Arc<dyn RealtimeAccessControl> =
{ if config.access_control.is_enabled && config.access_control.enable_realtime_access_control {
Arc::new(RealtimeCollabAccessControlImpl::new(access_control)) Arc::new(RealtimeCollabAccessControlImpl::new(access_control))
} else { } else {
Arc::new(NoOpsRealtimeCollabAccessControlImpl::new()) Arc::new(NoOpsRealtimeCollabAccessControlImpl::new())

View File

@ -8,6 +8,7 @@ use crate::{
}, },
mailer::{WorkspaceAccessRequestApprovedMailerParam, WorkspaceAccessRequestMailerParam}, mailer::{WorkspaceAccessRequestApprovedMailerParam, WorkspaceAccessRequestMailerParam},
}; };
use access_control::workspace::WorkspaceAccessControl;
use anyhow::Context; use anyhow::Context;
use app_error::AppError; use app_error::AppError;
use appflowy_collaborate::collab::storage::CollabAccessControlStorage; use appflowy_collaborate::collab::storage::CollabAccessControlStorage;
@ -73,16 +74,12 @@ pub async fn get_access_request(
pg_pool: &PgPool, pg_pool: &PgPool,
collab_storage: Arc<CollabAccessControlStorage>, collab_storage: Arc<CollabAccessControlStorage>,
access_request_id: Uuid, access_request_id: Uuid,
user_uuid: Uuid,
user_uid: i64, user_uid: i64,
) -> Result<AccessRequest, AppError> { ) -> Result<AccessRequest, AppError> {
let access_request_with_view_id = let access_request_with_view_id =
select_access_request_by_request_id(pg_pool, access_request_id).await?; select_access_request_by_request_id(pg_pool, access_request_id).await?;
if access_request_with_view_id.workspace.owner_uid != user_uid { if access_request_with_view_id.workspace.owner_uid != user_uid {
return Err(AppError::NotEnoughPermissions { return Err(AppError::NotEnoughPermissions);
user: user_uuid.to_string(),
action: "get access request".to_string(),
});
} }
let folder = get_latest_collab_folder( let folder = get_latest_collab_folder(
collab_storage, collab_storage,
@ -116,21 +113,26 @@ pub async fn get_access_request(
Ok(access_request) Ok(access_request)
} }
#[allow(clippy::too_many_arguments)]
pub async fn approve_or_reject_access_request( pub async fn approve_or_reject_access_request(
pg_pool: &PgPool, pg_pool: &PgPool,
workspace_access_control: Arc<dyn WorkspaceAccessControl>,
mailer: AFCloudMailer, mailer: AFCloudMailer,
appflowy_web_url: &str, appflowy_web_url: &str,
request_id: Uuid, request_id: Uuid,
uid: i64, uid: i64,
user_uuid: Uuid,
is_approved: bool, is_approved: bool,
) -> Result<(), AppError> { ) -> Result<(), AppError> {
let access_request = select_access_request_by_request_id(pg_pool, request_id).await?; let access_request = select_access_request_by_request_id(pg_pool, request_id).await?;
if access_request.workspace.owner_uid != uid { let has_access = workspace_access_control
return Err(AppError::NotEnoughPermissions { .enforce_role(
user: user_uuid.to_string(), &uid,
action: "approve access request".to_string(), &access_request.workspace.workspace_id.to_string(),
}); AFRole::Owner,
)
.await?;
if !has_access {
return Err(AppError::NotEnoughPermissions);
} }
let mut txn = pg_pool.begin().await.context("approving request")?; let mut txn = pg_pool.begin().await.context("approving request")?;
@ -140,7 +142,14 @@ pub async fn approve_or_reject_access_request(
&mut txn, &mut txn,
&access_request.workspace.workspace_id, &access_request.workspace.workspace_id,
&access_request.requester.email, &access_request.requester.email,
role, role.clone(),
)
.await?;
workspace_access_control
.insert_role(
&access_request.requester.uid,
&access_request.workspace.workspace_id,
role.clone(),
) )
.await?; .await?;
let cloned_mailer = mailer.clone(); let cloned_mailer = mailer.clone();

View File

@ -119,15 +119,7 @@ impl MiddlewareAccessControl for CollabMiddlewareAccessControl {
if result { if result {
Ok(()) Ok(())
} else { } else {
Err(AppError::NotEnoughPermissions { Err(AppError::NotEnoughPermissions)
user: uid.to_string(),
action: format!(
"access collab:{} with url:{}, method:{}",
oid,
path.as_str(),
method
),
})
} }
} }
} }

View File

@ -142,15 +142,7 @@ impl MiddlewareAccessControl for WorkspaceMiddlewareAccessControl {
if result { if result {
Ok(()) Ok(())
} else { } else {
Err(AppError::NotEnoughPermissions { Err(AppError::NotEnoughPermissions)
user: uid.to_string(),
action: format!(
"access workspace:{} with given url:{}, method: {}",
resource_id,
path.as_str(),
method,
),
})
} }
} }
} }

View File

@ -19,7 +19,7 @@ use uuid::Uuid;
use yrs::updates::encoder::Encode; use yrs::updates::encoder::Encode;
use access_control::workspace::WorkspaceAccessControl; use access_control::workspace::WorkspaceAccessControl;
use app_error::{AppError, ErrorCode}; use app_error::AppError;
use appflowy_collaborate::collab::storage::CollabAccessControlStorage; use appflowy_collaborate::collab::storage::CollabAccessControlStorage;
use database::collab::{upsert_collab_member_with_txn, CollabStorage}; use database::collab::{upsert_collab_member_with_txn, CollabStorage};
use database::file::s3_client_impl::S3BucketStorage; use database::file::s3_client_impl::S3BucketStorage;
@ -617,43 +617,17 @@ pub async fn get_workspace_document_total_bytes(
pub async fn get_workspace_settings( pub async fn get_workspace_settings(
pg_pool: &PgPool, pg_pool: &PgPool,
workspace_access_control: Arc<dyn WorkspaceAccessControl>,
workspace_id: &Uuid, workspace_id: &Uuid,
owner_uid: &i64,
) -> Result<AFWorkspaceSettings, AppResponseError> { ) -> Result<AFWorkspaceSettings, AppResponseError> {
let has_access = workspace_access_control
.enforce_role(owner_uid, &workspace_id.to_string(), AFRole::Owner)
.await?;
if !has_access {
return Err(AppResponseError::new(
ErrorCode::UserUnAuthorized,
"Only workspace owner can access workspace settings",
));
}
let settings = select_workspace_settings(pg_pool, workspace_id).await?; let settings = select_workspace_settings(pg_pool, workspace_id).await?;
Ok(settings.unwrap_or_default()) Ok(settings.unwrap_or_default())
} }
pub async fn update_workspace_settings( pub async fn update_workspace_settings(
pg_pool: &PgPool, pg_pool: &PgPool,
workspace_access_control: Arc<dyn WorkspaceAccessControl>,
workspace_id: &Uuid, workspace_id: &Uuid,
owner_uid: &i64,
change: AFWorkspaceSettingsChange, change: AFWorkspaceSettingsChange,
) -> Result<AFWorkspaceSettings, AppResponseError> { ) -> Result<AFWorkspaceSettings, AppResponseError> {
let has_access = workspace_access_control
.enforce_role(owner_uid, &workspace_id.to_string(), AFRole::Owner)
.await?;
if !has_access {
return Err(AppResponseError::new(
ErrorCode::UserUnAuthorized,
"Only workspace owner can edit workspace settings",
));
}
let mut tx = pg_pool.begin().await?; let mut tx = pg_pool.begin().await?;
let mut setting = select_workspace_settings(tx.deref_mut(), workspace_id) let mut setting = select_workspace_settings(tx.deref_mut(), workspace_id)
.await? .await?

View File

@ -33,6 +33,10 @@ pub struct Config {
pub struct AccessControlSetting { pub struct AccessControlSetting {
pub is_enabled: bool, pub is_enabled: bool,
pub enable_middleware: bool,
pub enable_workspace_access_control: bool,
pub enable_collab_access_control: bool,
pub enable_realtime_access_control: bool,
} }
#[derive(serde::Deserialize, Clone, Debug)] #[derive(serde::Deserialize, Clone, Debug)]
@ -176,6 +180,18 @@ pub fn get_configuration() -> Result<Config, anyhow::Error> {
is_enabled: get_env_var("APPFLOWY_ACCESS_CONTROL", "false") is_enabled: get_env_var("APPFLOWY_ACCESS_CONTROL", "false")
.parse() .parse()
.context("fail to get APPFLOWY_ACCESS_CONTROL")?, .context("fail to get APPFLOWY_ACCESS_CONTROL")?,
enable_middleware: get_env_var("APPFLOWY_ACCESS_CONTROL_MIDDLEWARE", "true")
.parse()
.context("fail to get APPFLOWY_ACCESS_CONTROL_MIDDLEWARE")?,
enable_workspace_access_control: get_env_var("APPFLOWY_ACCESS_CONTROL_WORKSPACE", "true")
.parse()
.context("fail to get APPFLOWY_ACCESS_CONTROL_WORKSPACE")?,
enable_collab_access_control: get_env_var("APPFLOWY_ACCESS_CONTROL_COLLAB", "true")
.parse()
.context("fail to get APPFLOWY_ACCESS_CONTROL_COLLAB")?,
enable_realtime_access_control: get_env_var("APPFLOWY_ACCESS_CONTROL_REALTIME", "true")
.parse()
.context("fail to get APPFLOWY_ACCESS_CONTROL_REALTIME")?,
}, },
db_settings: DatabaseSetting { db_settings: DatabaseSetting {
pg_conn_opts: PgConnectOptions::from_str(&get_env_var( pg_conn_opts: PgConnectOptions::from_str(&get_env_var(

View File

@ -1,4 +1,3 @@
use app_error::ErrorCode;
use client_api::Client; use client_api::Client;
use client_api_test::generate_unique_registered_user_client; use client_api_test::generate_unique_registered_user_client;
use database_entity::dto::{AFRole, AFWorkspaceInvitationStatus, AFWorkspaceSettingsChange}; use database_entity::dto::{AFRole, AFWorkspaceInvitationStatus, AFWorkspaceSettingsChange};
@ -31,6 +30,9 @@ async fn get_and_set_workspace_by_owner() {
#[tokio::test] #[tokio::test]
async fn get_and_set_workspace_by_non_owner() { async fn get_and_set_workspace_by_non_owner() {
// TODO: currently, workspace settings contains only AI preference, which is
// better suited as a user setting. Meanwhile, we can permit workspace members
// to view the settings.
let (alice_client, _alice) = generate_unique_registered_user_client().await; let (alice_client, _alice) = generate_unique_registered_user_client().await;
let workspaces = alice_client.get_workspaces().await.unwrap(); let workspaces = alice_client.get_workspaces().await.unwrap();
let alice_workspace_id = workspaces.first().unwrap().workspace_id; let alice_workspace_id = workspaces.first().unwrap().workspace_id;
@ -39,26 +41,18 @@ async fn get_and_set_workspace_by_non_owner() {
invite_user_to_workspace(&alice_workspace_id, &alice_client, &bob_client, &bob.email).await; invite_user_to_workspace(&alice_workspace_id, &alice_client, &bob_client, &bob.email).await;
let resp = bob_client bob_client
.get_workspace_settings(&alice_workspace_id.to_string()) .get_workspace_settings(&alice_workspace_id.to_string())
.await; .await
assert!( .unwrap();
resp.is_err(),
"non-owner should not have access to workspace settings"
);
assert_eq!(resp.err().unwrap().code, ErrorCode::UserUnAuthorized);
let resp = bob_client bob_client
.update_workspace_settings( .update_workspace_settings(
&alice_workspace_id.to_string(), &alice_workspace_id.to_string(),
&AFWorkspaceSettingsChange::new().disable_search_indexing(true), &AFWorkspaceSettingsChange::new().disable_search_indexing(true),
) )
.await; .await
assert!( .unwrap();
resp.is_err(),
"non-owner should not be able to edit workspace settings"
);
assert_eq!(resp.err().unwrap().code, ErrorCode::UserUnAuthorized);
} }
async fn invite_user_to_workspace( async fn invite_user_to_workspace(