Merge pull request #885 from AppFlowy-IO/additional-ac-config
feat: additional access control config
This commit is contained in:
commit
ec124bcb46
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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!(
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
|
|
|
||||||
|
|
@ -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?;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue