From 8e4108a4b28e39f7fd27583db436711d9b63ab7b Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 9 Feb 2024 01:12:17 +0800 Subject: [PATCH 1/5] feat: add env var and docs for admin frontend --- deploy.env | 17 ++++++++++++----- docker-compose.yml | 4 ++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/deploy.env b/deploy.env index b4ce4d7d..e0e36608 100644 --- a/deploy.env +++ b/deploy.env @@ -1,11 +1,18 @@ -# GoTrue URL that the appflowy service will use to connect to gotrue -# In docker environment, `gotrue` is the hostname of the gotrue service -APPFLOWY_GOTRUE_BASE_URL=http://gotrue:9999 -APPFLOWY_DATABASE_URL=postgres://postgres:password@postgres:5432/postgres - # This file is a template for docker compose deployment # Copy this file to .env and change the values as needed +# AppFlowy Cloud +## URL that connects to the gotrue docker container +APPFLOWY_GOTRUE_BASE_URL=http://gotrue:9999 +## URL that connects to the postgres docker container +APPFLOWY_DATABASE_URL=postgres://postgres:password@postgres:5432/postgres + +# admin frontend +## URL that connects to redis docker container +ADMIN_FRONTEND_REDIS_URL=redis://redis:6379 +## URL that connects to gotrue docker container +ADMIN_FRONTEND_GOTRUE_URL=http://gotrue:9999 + # authentication key, change this and keep the key safe and secret # self defined key, you can use any string GOTRUE_JWT_SECRET=hello456 diff --git a/docker-compose.yml b/docker-compose.yml index f6270ef2..c4d56e9b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -115,6 +115,10 @@ services: context: . dockerfile: ./admin_frontend/Dockerfile image: appflowyinc/admin_frontend:${BACKEND_VERSION:-latest} + environment: + - RUST_LOG=${RUST_LOG:-info} + - ADMIN_FRONTEND_REDIS_URL=${ADMIN_FRONTEND_REDIS_URL:-redis://redis:6379} + - ADMIN_FRONTEND_GOTRUE_URL=${ADMIN_FRONTEND_GOTRUE_URL:-http://gotrue:9999} volumes: postgres_data: From 8626b54de80c8f2613683b3ec7ff482b2946263e Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 9 Feb 2024 02:13:11 +0800 Subject: [PATCH 2/5] fix: redirect users after signup via magiclink --- admin_frontend/src/web_api.rs | 24 +++++++++++++++--------- libs/client-api/src/http.rs | 4 ++-- libs/gotrue/src/api.rs | 24 +++++++++++------------- src/application.rs | 2 +- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/admin_frontend/src/web_api.rs b/admin_frontend/src/web_api.rs index fad51267..1f8f983e 100644 --- a/admin_frontend/src/web_api.rs +++ b/admin_frontend/src/web_api.rs @@ -117,10 +117,13 @@ pub async fn invite_handler( ) -> Result, WebApiError<'static>> { state .gotrue_client - .magic_link(&MagicLinkParams { - email: param.email, - ..Default::default() - }) + .magic_link( + &MagicLinkParams { + email: param.email, + ..Default::default() + }, + Some("/".to_owned()), + ) .await?; Ok(WebApiResponse::<()>::from_str("Invitation sent".into())) } @@ -308,7 +311,7 @@ pub async fn sign_up_handler( let sign_up_res = state .gotrue_client - .sign_up_with_referrer(¶m.email, ¶m.password, Some("/")) + .sign_up(¶m.email, ¶m.password, Some("/")) .await?; match sign_up_res { @@ -378,10 +381,13 @@ async fn send_magic_link( ) -> Result, WebApiError<'static>> { state .gotrue_client - .magic_link(&MagicLinkParams { - email: email.to_owned(), - ..Default::default() - }) + .magic_link( + &MagicLinkParams { + email: email.to_owned(), + ..Default::default() + }, + Some("/".to_owned()), + ) .await?; Ok(WebApiResponse::<()>::from_str("Magic Link Sent".into())) } diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index 4240ebdd..c83ee7fb 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -376,7 +376,7 @@ impl Client { .magic_link(&MagicLinkParams { email: email.to_owned(), ..Default::default() - }) + }, None) .await?; Ok(()) } @@ -676,7 +676,7 @@ impl Client { #[instrument(level = "debug", skip_all, err)] pub async fn sign_up(&self, email: &str, password: &str) -> Result<(), AppResponseError> { - match self.gotrue_client.sign_up(email, password).await? { + match self.gotrue_client.sign_up(email, password, None).await? { Authenticated(access_token_resp) => { self.token.write().set(access_token_resp); Ok(()) diff --git a/libs/gotrue/src/api.rs b/libs/gotrue/src/api.rs index f1ff73ca..292d2301 100644 --- a/libs/gotrue/src/api.rs +++ b/libs/gotrue/src/api.rs @@ -55,12 +55,7 @@ impl Client { } #[tracing::instrument(skip_all, err)] - pub async fn sign_up(&self, email: &str, password: &str) -> Result { - self.sign_up_with_referrer(email, password, None).await - } - - #[tracing::instrument(skip_all, err)] - pub async fn sign_up_with_referrer( + pub async fn sign_up( &self, email: &str, password: &str, @@ -224,14 +219,17 @@ impl Client { to_gotrue_result(resp).await } - pub async fn magic_link(&self, magic_link_params: &MagicLinkParams) -> Result<(), GoTrueError> { + pub async fn magic_link( + &self, + magic_link_params: &MagicLinkParams, + redirect_to: Option, + ) -> Result<(), GoTrueError> { let url = format!("{}/magiclink", self.base_url); - let resp = self - .client - .request(Method::POST, &url) - .json(&magic_link_params) - .send() - .await?; + let mut req_builder = self.client.request(Method::POST, &url); + if let Some(redirect_to) = redirect_to { + req_builder = req_builder.header("redirect_to", redirect_to); + } + let resp = req_builder.json(&magic_link_params).send().await?; check_gotrue_result(resp).await } diff --git a/src/application.rs b/src/application.rs index c03c4511..e0cf6377 100644 --- a/src/application.rs +++ b/src/application.rs @@ -229,7 +229,7 @@ async fn setup_admin_account( ) -> Result<(), Error> { let admin_email = gotrue_setting.admin_email.as_str(); let password = gotrue_setting.admin_password.as_str(); - let res_resp = gotrue_client.sign_up(admin_email, password).await; + let res_resp = gotrue_client.sign_up(admin_email, password, None).await; match res_resp { Err(err) => { if let app_error::gotrue::GoTrueError::Internal(err) = err { From 0665a456d2472b379394c6b4cc7d8fbda5a9001e Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 9 Feb 2024 10:55:07 +0800 Subject: [PATCH 3/5] chore: cargo fmt --- libs/client-api/src/http.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index c83ee7fb..d7ea08b7 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -373,10 +373,13 @@ impl Client { pub async fn invite(&self, email: &str) -> Result<(), AppResponseError> { self .gotrue_client - .magic_link(&MagicLinkParams { - email: email.to_owned(), - ..Default::default() - }, None) + .magic_link( + &MagicLinkParams { + email: email.to_owned(), + ..Default::default() + }, + None, + ) .await?; Ok(()) } From 8b271eedd50171275b3a3a06ddc7d2b7a73bb3c4 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 9 Feb 2024 12:22:25 +0800 Subject: [PATCH 4/5] chore: cargo clippy --- src/biz/collab/access_control.rs | 1 + src/biz/collab/storage.rs | 98 +++++++++++++++++------------ src/biz/workspace/access_control.rs | 38 +++++------ src/middleware/access_control_mw.rs | 5 +- 4 files changed, 81 insertions(+), 61 deletions(-) diff --git a/src/biz/collab/access_control.rs b/src/biz/collab/access_control.rs index 5fa9f688..b040c4ef 100644 --- a/src/biz/collab/access_control.rs +++ b/src/biz/collab/access_control.rs @@ -36,6 +36,7 @@ where } #[instrument(level = "debug", skip_all, err)] + #[allow(clippy::blocks_in_conditions)] async fn check_collab_permission( &self, oid: &str, diff --git a/src/biz/collab/storage.rs b/src/biz/collab/storage.rs index 6720bd35..a877b2e8 100644 --- a/src/biz/collab/storage.rs +++ b/src/biz/collab/storage.rs @@ -73,6 +73,60 @@ where opened_collab_by_object_id: Arc::new(RwLock::new(HashMap::new())), } } + + async fn check_collab_permission( + &self, + workspace_id: &str, + uid: &i64, + params: &CollabParams, + transaction: &mut Transaction<'_, sqlx::Postgres>, + ) -> Result<(), AppError> { + // Check if the user has enough permissions to insert collab + // 1. If the collab already exists, check if the user has enough permissions to update collab + // 2. If the collab doesn't exist, check if the user has enough permissions to create collab. + let collab_exists = is_collab_exists(¶ms.object_id, transaction.deref_mut()).await?; + if collab_exists { + // If the collab already exists, check if the user has enough permissions to update collab + let can_write = self + .access_control + .get_or_refresh_collab_access_level(uid, ¶ms.object_id, transaction.deref_mut()) + .await + .context(format!( + "Can't find the access level when user:{} try to insert collab", + uid + ))? + .can_write(); + if !can_write { + return Err(AppError::NotEnoughPermissions(format!( + "user:{} doesn't have enough permissions to update collab {}", + uid, params.object_id + ))); + } + } else { + // If the collab doesn't exist, check if the user has enough permissions to create collab. + // If the user is the owner or member of the workspace, the user can create collab. + let can_write_workspace = self + .access_control + .get_user_workspace_role(uid, workspace_id, transaction.deref_mut()) + .await? + .can_create_collab(); + + if !can_write_workspace { + return Err(AppError::NotEnoughPermissions(format!( + "user:{} doesn't have enough permissions to insert collab {}", + uid, params.object_id + ))); + } + + // Cache the access level if the user has enough permissions to create collab. + self + .access_control + .cache_collab_access_level(uid, ¶ms.object_id, AFAccessLevel::FullAccess) + .await?; + } + + Ok(()) + } } #[async_trait] @@ -124,6 +178,7 @@ where } #[instrument(level = "trace", skip(self, params), oid = %params.oid, err)] + #[allow(clippy::blocks_in_conditions)] async fn upsert_collab_with_transaction( &self, workspace_id: &str, @@ -132,46 +187,9 @@ where transaction: &mut Transaction<'_, sqlx::Postgres>, ) -> DatabaseResult<()> { params.validate()?; - - // Check if the user has enough permissions to insert collab - // 1. If the collab already exists, check if the user has enough permissions to update collab - // 2. If the collab doesn't exist, check if the user has enough permissions to create collab. - let has_permission = if is_collab_exists(¶ms.object_id, transaction.deref_mut()).await? { - // If the collab already exists, check if the user has enough permissions to update collab - let level = self - .access_control - .get_or_refresh_collab_access_level(uid, ¶ms.object_id, transaction.deref_mut()) - .await - .context(format!( - "Can't find the access level when user:{} try to insert collab", - uid - ))?; - level.can_write() - } else { - // If the collab doesn't exist, check if the user has enough permissions to create collab. - // If the user is the owner or member of the workspace, the user can create collab. - let can_write_workspace = self - .access_control - .get_user_workspace_role(uid, workspace_id, transaction.deref_mut()) - .await? - .can_create_collab(); - - // Cache the access level if the user has enough permissions to create collab. - if can_write_workspace { - self - .access_control - .cache_collab_access_level(uid, ¶ms.object_id, AFAccessLevel::FullAccess) - .await?; - } - can_write_workspace - }; - - if !has_permission { - return Err(AppError::NotEnoughPermissions(format!( - "user:{} doesn't have enough permissions to insert collab {}", - uid, params.object_id - ))); - } + self + .check_collab_permission(workspace_id, uid, ¶ms, transaction) + .await?; let object_id = params.object_id.clone(); let encoded_collab = params.encoded_collab_v1.clone(); self diff --git a/src/biz/workspace/access_control.rs b/src/biz/workspace/access_control.rs index 7559dde2..3d72887d 100644 --- a/src/biz/workspace/access_control.rs +++ b/src/biz/workspace/access_control.rs @@ -201,6 +201,7 @@ where } #[instrument(level = "trace", skip_all, err)] + #[allow(clippy::blocks_in_conditions)] async fn check_workspace_permission( &self, workspace_id: &Uuid, @@ -208,29 +209,28 @@ where method: Method, ) -> Result<(), AppError> { trace!("workspace_id: {:?}, uid: {:?}", workspace_id, uid); - match self + let role = self .access_control .get_role_from_uid(uid, workspace_id, &self.pg_pool) .await - { - Ok(role) => { - if method == Method::DELETE || method == Method::POST || method == Method::PUT { - if matches!(role, AFRole::Owner) { - Ok(()) - } else { - Err(AppError::NotEnoughPermissions(format!( - "User:{:?} doesn't have the enough permission to access workspace:{}", - uid, workspace_id - ))) - } - } else { - Ok(()) - } + .map_err(|err| { + AppError::NotEnoughPermissions(format!( + "Can't find the role of the user:{:?} in the workspace:{:?}. error: {}", + uid, workspace_id, err + )) + })?; + + match method { + Method::DELETE | Method::POST | Method::PUT => match role { + AFRole::Owner => return Ok(()), + _ => { + return Err(AppError::NotEnoughPermissions(format!( + "User:{:?} doesn't have the enough permission to access workspace:{}", + uid, workspace_id + ))) + }, }, - Err(err) => Err(AppError::NotEnoughPermissions(format!( - "Can't find the role of the user:{:?} in the workspace:{:?}. error: {}", - uid, workspace_id, err - ))), + _ => Ok(()), } } diff --git a/src/middleware/access_control_mw.rs b/src/middleware/access_control_mw.rs index 8bec37b1..4603a9e9 100644 --- a/src/middleware/access_control_mw.rs +++ b/src/middleware/access_control_mw.rs @@ -175,12 +175,13 @@ where forward_ready!(service); fn call(&self, mut req: ServiceRequest) -> Self::Future { - match req.match_pattern().map(|pattern| { + let path = req.match_pattern().map(|pattern| { let resource_ref = ResourceDef::new(pattern); let mut path = req.match_info().clone(); resource_ref.capture_match_info(&mut path); path - }) { + }); + match path { None => { let fut = self.service.call(req); Box::pin(fut) From 0b08ab34ea30804385759682f98722403d073aa6 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 9 Feb 2024 12:49:29 +0800 Subject: [PATCH 5/5] chore: cargo clippy --- src/biz/collab/access_control.rs | 2 +- src/biz/collab/storage.rs | 2 +- src/biz/workspace/access_control.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/biz/collab/access_control.rs b/src/biz/collab/access_control.rs index b040c4ef..6d4f6968 100644 --- a/src/biz/collab/access_control.rs +++ b/src/biz/collab/access_control.rs @@ -36,7 +36,7 @@ where } #[instrument(level = "debug", skip_all, err)] - #[allow(clippy::blocks_in_conditions)] + #[allow(clippy::blocks_in_if_conditions)] async fn check_collab_permission( &self, oid: &str, diff --git a/src/biz/collab/storage.rs b/src/biz/collab/storage.rs index a877b2e8..a4cc5762 100644 --- a/src/biz/collab/storage.rs +++ b/src/biz/collab/storage.rs @@ -178,7 +178,7 @@ where } #[instrument(level = "trace", skip(self, params), oid = %params.oid, err)] - #[allow(clippy::blocks_in_conditions)] + #[allow(clippy::blocks_in_if_conditions)] async fn upsert_collab_with_transaction( &self, workspace_id: &str, diff --git a/src/biz/workspace/access_control.rs b/src/biz/workspace/access_control.rs index 3d72887d..beb4d318 100644 --- a/src/biz/workspace/access_control.rs +++ b/src/biz/workspace/access_control.rs @@ -201,7 +201,7 @@ where } #[instrument(level = "trace", skip_all, err)] - #[allow(clippy::blocks_in_conditions)] + #[allow(clippy::blocks_in_if_conditions)] async fn check_workspace_permission( &self, workspace_id: &Uuid,