diff --git a/.sqlx/query-5144da61424400216e10d9327f5136f77d96a83d462bf815f13ebe70d88fada3.json b/.sqlx/query-f18d6e075a522b0ce5935351dd57ab0dda4d8b4ed3881c2ad0bc09c07c43e6fe.json similarity index 74% rename from .sqlx/query-5144da61424400216e10d9327f5136f77d96a83d462bf815f13ebe70d88fada3.json rename to .sqlx/query-f18d6e075a522b0ce5935351dd57ab0dda4d8b4ed3881c2ad0bc09c07c43e6fe.json index 3d251f0a..61361749 100644 --- a/.sqlx/query-5144da61424400216e10d9327f5136f77d96a83d462bf815f13ebe70d88fada3.json +++ b/.sqlx/query-f18d6e075a522b0ce5935351dd57ab0dda4d8b4ed3881c2ad0bc09c07c43e6fe.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO af_collab_snapshot (oid, blob, len, encrypt, workspace_id) \n VALUES ($1, $2, $3, $4, $5)\n RETURNING sid AS snapshot_id, oid AS object_id, created_at\n ", + "query": "\n INSERT INTO af_collab_snapshot (oid, blob, len, encrypt, workspace_id)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING sid AS snapshot_id, oid AS object_id, created_at\n ", "describe": { "columns": [ { @@ -34,5 +34,5 @@ false ] }, - "hash": "5144da61424400216e10d9327f5136f77d96a83d462bf815f13ebe70d88fada3" + "hash": "f18d6e075a522b0ce5935351dd57ab0dda4d8b4ed3881c2ad0bc09c07c43e6fe" } diff --git a/.sqlx/query-eff32b7631cb1b715776c66749706943c7ba23eac358cae6d36537991a8f8975.json b/.sqlx/query-f409626142553d4496d15b5dfa7da8a5a238da86f56c930c09a261f2efa1f55c.json similarity index 73% rename from .sqlx/query-eff32b7631cb1b715776c66749706943c7ba23eac358cae6d36537991a8f8975.json rename to .sqlx/query-f409626142553d4496d15b5dfa7da8a5a238da86f56c930c09a261f2efa1f55c.json index 2f4c119e..4e14b48b 100644 --- a/.sqlx/query-eff32b7631cb1b715776c66749706943c7ba23eac358cae6d36537991a8f8975.json +++ b/.sqlx/query-f409626142553d4496d15b5dfa7da8a5a238da86f56c930c09a261f2efa1f55c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT sid as \"snapshot_id\", oid as \"object_id\", created_at\n FROM af_collab_snapshot \n WHERE oid = $1 AND deleted_at IS NULL\n ORDER BY created_at DESC;\n ", + "query": "\n SELECT sid as \"snapshot_id\", oid as \"object_id\", created_at\n FROM af_collab_snapshot\n WHERE oid = $1 AND deleted_at IS NULL\n ORDER BY created_at DESC;\n ", "describe": { "columns": [ { @@ -30,5 +30,5 @@ false ] }, - "hash": "eff32b7631cb1b715776c66749706943c7ba23eac358cae6d36537991a8f8975" + "hash": "f409626142553d4496d15b5dfa7da8a5a238da86f56c930c09a261f2efa1f55c" } diff --git a/admin_frontend/src/web_app.rs b/admin_frontend/src/web_app.rs index 4e3796d0..d8873f63 100644 --- a/admin_frontend/src/web_app.rs +++ b/admin_frontend/src/web_app.rs @@ -146,7 +146,7 @@ pub async fn admin_users_handler( ) -> Result, WebAppError> { let users = state .gotrue_client - .admin_list_user(&session.token.access_token) + .admin_list_user(&session.token.access_token, None) .await .map_or_else( |err| { diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index d7ea08b7..538042b9 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -408,20 +408,32 @@ impl Client { email: &str, password: &str, ) -> Result { - Ok( - self - .gotrue_client - .admin_add_user( - &self.access_token()?, - &AdminUserParams { - email: email.to_owned(), - password: Some(password.to_owned()), - email_confirm: true, - ..Default::default() - }, - ) - .await?, - ) + let user = self + .gotrue_client + .admin_add_user( + &self.access_token()?, + &AdminUserParams { + email: email.to_owned(), + password: Some(password.to_owned()), + email_confirm: true, + ..Default::default() + }, + ) + .await?; + Ok(user) + } + + // filter is postgre sql like filter + #[instrument(level = "debug", skip_all, err)] + pub async fn admin_list_users( + &self, + filter: Option<&str>, + ) -> Result, AppResponseError> { + let user = self + .gotrue_client + .admin_list_user(&self.access_token()?, filter) + .await?; + Ok(user.users) } /// Only expose this method for testing diff --git a/libs/database/src/collab/collab_db_ops.rs b/libs/database/src/collab/collab_db_ops.rs index f4acc1d8..a84117a3 100644 --- a/libs/database/src/collab/collab_db_ops.rs +++ b/libs/database/src/collab/collab_db_ops.rs @@ -318,7 +318,7 @@ pub async fn should_create_snapshot<'a, E: Executor<'a, Database = Postgres>>( ) -> Result { let hours = Utc::now() - Duration::hours(SNAPSHOT_PER_HOUR); let latest_snapshot_time: Option> = sqlx::query_scalar( - "SELECT created_at FROM af_collab_snapshot + "SELECT created_at FROM af_collab_snapshot WHERE oid = $1 ORDER BY created_at DESC LIMIT 1", ) .bind(oid) @@ -344,7 +344,7 @@ pub(crate) async fn create_snapshot_and_maintain_limit<'a>( let snapshot_meta = sqlx::query_as!( AFSnapshotMeta, r#" - INSERT INTO af_collab_snapshot (oid, blob, len, encrypt, workspace_id) + INSERT INTO af_collab_snapshot (oid, blob, len, encrypt, workspace_id) VALUES ($1, $2, $3, $4, $5) RETURNING sid AS snapshot_id, oid AS object_id, created_at "#, @@ -360,7 +360,7 @@ pub(crate) async fn create_snapshot_and_maintain_limit<'a>( // When a new snapshot is created that surpasses the preset limit, older snapshots will be deleted to maintain the limit sqlx::query( r#" - DELETE FROM af_collab_snapshot + DELETE FROM af_collab_snapshot WHERE oid = $1 AND sid NOT IN ( SELECT sid FROM af_collab_snapshot WHERE oid = $1 ORDER BY created_at DESC LIMIT $2) "#, ) @@ -403,7 +403,7 @@ pub async fn get_all_collab_snapshot_meta( AFSnapshotMeta, r#" SELECT sid as "snapshot_id", oid as "object_id", created_at - FROM af_collab_snapshot + FROM af_collab_snapshot WHERE oid = $1 AND deleted_at IS NULL ORDER BY created_at DESC; "#, diff --git a/libs/database/src/workspace.rs b/libs/database/src/workspace.rs index 68c6c851..24594a83 100644 --- a/libs/database/src/workspace.rs +++ b/libs/database/src/workspace.rs @@ -366,6 +366,7 @@ pub async fn select_workspace<'a, E: Executor<'a, Database = Postgres>>( .await?; Ok(workspace) } + #[inline] pub async fn update_updated_at_of_workspace<'a, E: Executor<'a, Database = Postgres>>( executor: E, diff --git a/libs/gotrue/src/api.rs b/libs/gotrue/src/api.rs index 292d2301..d7d487a5 100644 --- a/libs/gotrue/src/api.rs +++ b/libs/gotrue/src/api.rs @@ -1,7 +1,7 @@ use super::grant::Grant; use crate::params::{ AdminDeleteUserParams, AdminUserParams, CreateSSOProviderParams, GenerateLinkParams, - GenerateLinkResponse, MagicLinkParams, + GenerateLinkResponse, InviteUserParams, MagicLinkParams, }; use anyhow::Context; use gotrue_entity::dto::{ @@ -139,12 +139,14 @@ impl Client { pub async fn admin_list_user( &self, access_token: &str, + filter: Option<&str>, ) -> Result { let url = format!("{}/admin/users", self.base_url); - let resp = self - .http_client_with_auth(Method::GET, &url, access_token) - .send() - .await?; + let mut req = self.http_client_with_auth(Method::GET, &url, access_token); + if let Some(filter) = filter { + req = req.query(&[("filter", filter)]); + } + let resp = req.send().await?; to_gotrue_result(resp).await } @@ -191,6 +193,20 @@ impl Client { to_gotrue_result(resp).await } + pub async fn admin_invite_user( + &self, + access_token: &str, + admin_user_params: &InviteUserParams, + ) -> Result { + let url = format!("{}/invite", self.base_url); + let resp = self + .http_client_with_auth(Method::POST, &url, access_token) + .json(&admin_user_params) + .send() + .await?; + to_gotrue_result(resp).await + } + pub async fn admin_add_user( &self, access_token: &str, diff --git a/libs/gotrue/src/params.rs b/libs/gotrue/src/params.rs index 3a1dd390..92988b3d 100644 --- a/libs/gotrue/src/params.rs +++ b/libs/gotrue/src/params.rs @@ -8,6 +8,12 @@ pub struct AdminDeleteUserParams { pub should_soft_delete: bool, } +#[derive(Default, Serialize)] +pub struct InviteUserParams { + pub email: String, + pub data: serde_json::Value, +} + #[derive(Debug, Default, Deserialize, Serialize)] pub struct AdminUserParams { pub aud: String, diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 92e5ac52..a3a37913 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -176,15 +176,13 @@ async fn add_workspace_members_handler( state: Data, ) -> Result> { let create_members = payload.into_inner(); - let role_by_uid = workspace::ops::add_workspace_members( - &state.pg_pool, - &user_uuid, - &workspace_id, - create_members.0, - ) - .await?; + + let role_by_uid = + workspace::ops::add_workspace_members(&state, &user_uuid, &workspace_id, create_members.0) + .await?; for (uid, role) in role_by_uid { + // TODO: use one single query to insert all roles state .workspace_access_control .insert_workspace_role(&uid, &workspace_id, role) diff --git a/src/application.rs b/src/application.rs index 40bd5eea..20867b47 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,5 +1,32 @@ use crate::api::metrics::metrics_scope; +use crate::component::auth::HEADER_TOKEN; +use crate::config::config::{Config, DatabaseSetting, GoTrueSetting, S3Setting}; +use crate::middleware::request_id::RequestIdMiddleware; +use crate::self_signed::create_self_signed_certificate; +use crate::state::{AppState, UserCache}; +use actix_identity::IdentityMiddleware; +use actix_session::storage::RedisSessionStore; +use actix_session::SessionMiddleware; +use actix_web::cookie::Key; +use actix_web::{dev::Server, web, web::Data, App, HttpServer}; + +use actix::Actor; +use anyhow::{Context, Error}; +use app_error::AppError; +use gotrue::grant::{Grant, PasswordGrant}; +use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod}; +use openssl::x509::X509; +use secrecy::{ExposeSecret, Secret}; +use snowflake::Snowflake; +use sqlx::{postgres::PgPoolOptions, PgPool}; +use std::net::TcpListener; +use std::sync::Arc; +use std::time::Duration; + +use tokio::sync::RwLock; +use tracing::info; + use crate::api::file_storage::file_storage_scope; use crate::api::user::user_scope; use crate::api::workspace::{collab_scope, workspace_scope}; @@ -10,32 +37,11 @@ use crate::biz::collab::storage::init_collab_storage; use crate::biz::pg_listener::PgListeners; use crate::biz::user::RealtimeUserImpl; use crate::biz::workspace::access_control::WorkspaceHttpAccessControl; -use crate::component::auth::HEADER_TOKEN; -use crate::config::config::{Config, DatabaseSetting, GoTrueSetting, S3Setting}; use crate::middleware::access_control_mw::WorkspaceAccessControl; use crate::middleware::metrics_mw::MetricsMiddleware; -use crate::middleware::request_id::RequestIdMiddleware; -use crate::self_signed::create_self_signed_certificate; -use crate::state::{AppMetrics, AppState, UserCache}; -use actix::Actor; -use actix_identity::IdentityMiddleware; -use actix_session::storage::RedisSessionStore; -use actix_session::SessionMiddleware; -use actix_web::cookie::Key; -use actix_web::{dev::Server, web, web::Data, App, HttpServer}; -use anyhow::{Context, Error}; +use crate::state::AppMetrics; use database::file::bucket_s3_impl::S3BucketStorage; -use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod}; -use openssl::x509::X509; use realtime::collaborate::CollabServer; -use secrecy::{ExposeSecret, Secret}; -use snowflake::Snowflake; -use sqlx::{postgres::PgPoolOptions, PgPool}; -use std::net::TcpListener; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::RwLock; -use tracing::info; pub struct Application { port: u16, @@ -167,6 +173,12 @@ pub async fn init_state(config: &Config) -> Result { let gotrue_client = get_gotrue_client(&config.gotrue).await?; setup_admin_account(&gotrue_client, &pg_pool, &config.gotrue).await?; + // Gotrue Admin + let gotrue_admin = GoTrueAdmin::new( + config.gotrue.admin_email.clone(), + config.gotrue.admin_password.clone(), + ); + // Redis info!("Connecting to Redis..."); let redis_client = get_redis_client(config.redis_uri.expose_secret()).await?; @@ -207,6 +219,7 @@ pub async fn init_state(config: &Config) -> Result { users: Arc::new(users), id_gen: Arc::new(RwLock::new(Snowflake::new(1))), gotrue_client, + gotrue_admin, redis_client, collab_storage, collab_access_control, @@ -218,6 +231,31 @@ pub async fn init_state(config: &Config) -> Result { }) } +#[derive(Debug, Clone)] +pub struct GoTrueAdmin { + pub admin_email: String, + pub password: Secret, +} + +impl GoTrueAdmin { + pub fn new(admin_email: String, password: String) -> Self { + Self { + admin_email, + password: password.into(), + } + } + + pub async fn token(&self, client: &gotrue::api::Client) -> Result { + let token = client + .token(&Grant::Password(PasswordGrant { + email: self.admin_email.clone(), + password: self.password.expose_secret().clone(), + })) + .await?; + Ok(token.access_token) + } +} + async fn setup_admin_account( gotrue_client: &gotrue::api::Client, pg_pool: &PgPool, diff --git a/src/biz/user.rs b/src/biz/user.rs index 743881aa..0f5a501e 100644 --- a/src/biz/user.rs +++ b/src/biz/user.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use gotrue_entity::dto::User; use crate::biz::workspace::access_control::WorkspaceAccessControl; use crate::state::AppState; @@ -26,15 +27,30 @@ use workspace_template::{WorkspaceTemplate, WorkspaceTemplateBuilder}; #[instrument(skip_all, err)] pub async fn verify_token(access_token: &str, state: &AppState) -> Result { let user = state.gotrue_client.user_info(access_token).await?; - let user_uuid = uuid::Uuid::parse_str(&user.id)?; - let name = name_from_user_metadata(&user.user_metadata); - let mut txn = state .pg_pool .begin() .await .context("acquire transaction to verify token")?; + let is_new = create_user_if_not_exists(&user, &mut txn, state).await; + txn + .commit() + .await + .context("fail to commit transaction to verify token")?; + + is_new +} + +/// Returns if user is new +pub async fn create_user_if_not_exists( + user: &User, + txn: &mut Transaction<'_, sqlx::Postgres>, + state: &AppState, +) -> Result { + let user_uuid = uuid::Uuid::parse_str(&user.id)?; + let name = name_from_user_metadata(&user.user_metadata); + // To prevent concurrent creation of the same user with the same workspace resources, we lock // the user row when `verify_token` is called. This means that if multiple requests try to // create the same user simultaneously, the first request will acquire the lock, create the user, @@ -67,16 +83,12 @@ pub async fn verify_token(access_token: &str, state: &AppState) -> Result Result { + let users = gotrue_client + .admin_list_user(admin_token, Some(email)) + .await?; + for user in users.users { + if user.email == email { + return Ok(true); + } + } + Ok(false) +} diff --git a/src/biz/workspace/ops.rs b/src/biz/workspace/ops.rs index d8a02252..18ba7bc5 100644 --- a/src/biz/workspace/ops.rs +++ b/src/biz/workspace/ops.rs @@ -9,6 +9,7 @@ use database::workspace::{ select_workspace_member_list, update_updated_at_of_workspace, upsert_workspace_member, }; use database_entity::dto::{AFAccessLevel, AFRole, AFWorkspace}; +use gotrue::params::InviteUserParams; use shared_entity::dto::workspace_dto::{CreateWorkspaceMember, WorkspaceMemberChangeset}; use shared_entity::response::AppResponseError; use sqlx::{types::uuid, PgPool}; @@ -17,6 +18,10 @@ use std::ops::DerefMut; use tracing::instrument; use uuid::Uuid; +use crate::biz::user::create_user_if_not_exists; +use crate::biz::utils::check_user_exists; +use crate::state::AppState; + pub async fn delete_workspace_for_user( pg_pool: &PgPool, workspace_id: &Uuid, @@ -83,16 +88,52 @@ pub async fn open_workspace( /// #[instrument(level = "debug", skip_all, err)] pub async fn add_workspace_members( - pg_pool: &PgPool, + state: &AppState, _user_uuid: &Uuid, workspace_id: &Uuid, members: Vec, ) -> Result, AppError> { - let mut txn = pg_pool + // Invite user to workspace if user is not registered + let admin_token = state.gotrue_admin.token(&state.gotrue_client).await?; + let mut txn = state + .pg_pool .begin() .await .context("Begin transaction to insert workspace members")?; + let gotrue_client = &state.gotrue_client; + for member in members.iter() { + let user_exists = check_user_exists(&admin_token, gotrue_client, &member.email).await?; + if !user_exists { + let user = gotrue_client + .admin_invite_user( + &admin_token, + &InviteUserParams { + email: member.email.clone(), + ..Default::default() + }, + ) + .await?; + let is_new = create_user_if_not_exists(&user, &mut txn, state).await?; + if !is_new { + tracing::error!("User should be new but is not new. User: {:?}", user); + } + } + } + let role_by_uid = add_workspace_members_db(workspace_id, members, &mut txn).await?; + + txn + .commit() + .await + .context("Commit transaction to insert workspace members")?; + Ok(role_by_uid) +} + +pub async fn add_workspace_members_db( + workspace_id: &Uuid, + members: Vec, + txn: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result, AppError> { let mut role_by_uid = HashMap::new(); for member in members.into_iter() { let access_level = match &member.role { @@ -106,16 +147,11 @@ pub async fn add_workspace_members( // "Failed to get uid from email {} when adding workspace members", // member.email // ))?; - insert_workspace_member_with_txn(&mut txn, workspace_id, &member.email, member.role.clone()) - .await?; - upsert_collab_member_with_txn(uid, workspace_id.to_string(), &access_level, &mut txn).await?; + insert_workspace_member_with_txn(txn, workspace_id, &member.email, member.role.clone()).await?; + upsert_collab_member_with_txn(uid, workspace_id.to_string(), &access_level, txn).await?; role_by_uid.insert(uid, member.role); } - txn - .commit() - .await - .context("Commit transaction to insert workspace members")?; Ok(role_by_uid) } diff --git a/src/state.rs b/src/state.rs index 54685a23..fcf85706 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,3 +1,4 @@ +use crate::application::GoTrueAdmin; use crate::biz::casbin::{CollabAccessControlImpl, WorkspaceAccessControlImpl}; use crate::biz::collab::storage::CollabPostgresDBStorage; use crate::biz::pg_listener::PgListeners; @@ -29,6 +30,7 @@ pub struct AppState { pub users: Arc, pub id_gen: Arc>, pub gotrue_client: gotrue::api::Client, + pub gotrue_admin: GoTrueAdmin, pub redis_client: RedisClient, pub collab_storage: Arc, pub collab_access_control: CollabAccessControlImpl, diff --git a/tests/access_control/collab_ac_test.rs b/tests/access_control/collab_ac_test.rs index e4519edd..aeef6733 100644 --- a/tests/access_control/collab_ac_test.rs +++ b/tests/access_control/collab_ac_test.rs @@ -1,7 +1,6 @@ use crate::access_control::*; use actix_http::Method; use anyhow::{anyhow, Context}; -use appflowy_cloud::biz; use appflowy_cloud::biz::casbin::access_control::{Action, ActionType, ObjectType}; use database_entity::dto::{AFAccessLevel, AFRole}; @@ -42,10 +41,7 @@ async fn test_collab_access_control(pool: PgPool) -> anyhow::Result<()> { role: AFRole::Guest, }, ]; - let _ = - biz::workspace::ops::add_workspace_members(&pool, &user.uuid, &workspace.workspace_id, members) - .await - .context("adding users to workspace")?; + let _ = add_workspace_members_in_tx(&pool, &workspace.workspace_id, members).await; // user that created the workspace should have full access assert_access_level( @@ -148,18 +144,15 @@ async fn test_collab_access_control_access_http_method(pool: PgPool) -> anyhow:: .next() .ok_or(anyhow!("workspace should be created"))?; - let _ = biz::workspace::ops::add_workspace_members( + let _ = add_workspace_members_in_tx( &pool, - &guest.uuid, &workspace.workspace_id, vec![CreateWorkspaceMember { email: guest.email, role: AFRole::Guest, }], ) - .await - .context("adding users to workspace") - .unwrap(); + .await; for method in [Method::GET, Method::POST, Method::PUT, Method::DELETE] { assert_can_access_http_method( @@ -243,17 +236,15 @@ async fn test_collab_access_control_send_receive_collab_update(pool: PgPool) -> .next() .ok_or(anyhow!("workspace should be created"))?; - let _ = biz::workspace::ops::add_workspace_members( + let _ = add_workspace_members_in_tx( &pool, - &guest.uuid, &workspace.workspace_id, vec![CreateWorkspaceMember { email: guest.email, role: AFRole::Guest, }], ) - .await - .context("adding users to workspace")?; + .await; // Need to wait for the listener(spawn_listen_on_workspace_member_change) to receive the event sleep(Duration::from_secs(2)).await; diff --git a/tests/access_control/member_ac_test.rs b/tests/access_control/member_ac_test.rs index a4cb616d..297ffd20 100644 --- a/tests/access_control/member_ac_test.rs +++ b/tests/access_control/member_ac_test.rs @@ -1,5 +1,6 @@ use crate::access_control::{ - assert_workspace_role, assert_workspace_role_error, create_user, setup_access_control, + add_workspace_members_in_tx, assert_workspace_role, assert_workspace_role_error, create_user, + setup_access_control, }; use anyhow::{anyhow, Context}; use app_error::ErrorCode; @@ -33,17 +34,16 @@ async fn test_workspace_access_control_get_role(pool: PgPool) -> anyhow::Result< .await; let member = create_user(&pool).await?; - let _ = biz::workspace::ops::add_workspace_members( + + let _ = add_workspace_members_in_tx( &pool, - &member.uuid, &workspace.workspace_id, vec![CreateWorkspaceMember { email: member.email.clone(), role: AFRole::Member, }], ) - .await - .context("adding users to workspace")?; + .await; assert_workspace_role( &workspace_access_control, diff --git a/tests/access_control/mod.rs b/tests/access_control/mod.rs index a677c536..00207aab 100644 --- a/tests/access_control/mod.rs +++ b/tests/access_control/mod.rs @@ -1,18 +1,21 @@ use actix_http::Method; use anyhow::{Context, Error}; use app_error::ErrorCode; +use appflowy_cloud::biz; use appflowy_cloud::biz::casbin::{CollabAccessControlImpl, WorkspaceAccessControlImpl}; use appflowy_cloud::biz::workspace::access_control::WorkspaceAccessControl; use client_api_test_util::setup_log; use database_entity::dto::{AFAccessLevel, AFRole}; use lazy_static::lazy_static; use realtime::collaborate::CollabAccessControl; +use shared_entity::dto::workspace_dto::CreateWorkspaceMember; use snowflake::Snowflake; use sqlx::PgPool; use std::sync::Arc; use casbin::{CoreApi, DefaultModel, Enforcer}; use dashmap::DashMap; +use std::collections::HashMap; use std::time::Duration; use tokio::sync::RwLock; use tokio::time::{interval, timeout}; @@ -307,3 +310,22 @@ pub async fn assert_can_access_http_method( timeout(timeout_duration, operation).await?; Ok(()) } + +pub async fn add_workspace_members_in_tx( + pool: &PgPool, + workspace_id: &Uuid, + members: Vec, +) -> HashMap { + let mut txn = pool + .begin() + .await + .expect("acquire transaction to add workspace members"); + let res = biz::workspace::ops::add_workspace_members_db(workspace_id, members, &mut txn) + .await + .expect("add workspace members"); + txn + .commit() + .await + .expect("commit transaction to add workspace members"); + res +} diff --git a/tests/access_control/user_ac_test.rs b/tests/access_control/user_ac_test.rs index 93bfe455..da6642ce 100644 --- a/tests/access_control/user_ac_test.rs +++ b/tests/access_control/user_ac_test.rs @@ -1,8 +1,6 @@ use crate::access_control::*; use anyhow::anyhow; -use appflowy_cloud::biz; use appflowy_cloud::biz::casbin::access_control::{Action, ObjectType, ToCasbinAction}; - use casbin::CoreApi; use database_entity::dto::{AFAccessLevel, AFRole}; @@ -96,14 +94,7 @@ async fn test_add_users_to_workspace(pool: PgPool) -> anyhow::Result<()> { role: AFRole::Guest, }, ]; - let _ = biz::workspace::ops::add_workspace_members( - &pool, - &user_main.uuid, - &workspace.workspace_id, - members, - ) - .await - .context("adding users to workspace")?; + let _ = add_workspace_members_in_tx(&pool, &workspace.workspace_id, members).await; let enforcer = setup_enforcer(&pool).await?; { // Owner @@ -232,14 +223,8 @@ async fn test_reload_policy_after_adding_user_to_workspace(pool: PgPool) -> anyh email: user_member.email.clone(), role: AFRole::Member, }]; - let _ = biz::workspace::ops::add_workspace_members( - &pool, - &user_owner.uuid, - &workspace.workspace_id, - members, - ) - .await - .context("adding users to workspace")?; + + let _ = add_workspace_members_in_tx(&pool, &workspace.workspace_id, members).await; assert!(!enforcer .enforce(( diff --git a/tests/gotrue/admin.rs b/tests/gotrue/admin.rs index 5eda6a46..28e91179 100644 --- a/tests/gotrue/admin.rs +++ b/tests/gotrue/admin.rs @@ -47,7 +47,7 @@ async fn admin_user_create_list_edit_delete() { // list users let users = gotrue_client - .admin_list_user(&admin_token.access_token) + .admin_list_user(&admin_token.access_token, None) .await .unwrap() .users; @@ -94,7 +94,7 @@ async fn admin_user_create_list_edit_delete() { .unwrap(); let users = gotrue_client - .admin_list_user(&admin_token.access_token) + .admin_list_user(&admin_token.access_token, None) .await .unwrap() .users; diff --git a/tests/workspace/member_crud.rs b/tests/workspace/member_crud.rs index 7a4b599c..4350b5de 100644 --- a/tests/workspace/member_crud.rs +++ b/tests/workspace/member_crud.rs @@ -1,5 +1,5 @@ use app_error::ErrorCode; -use client_api_test_util::TestClient; +use client_api_test_util::{admin_user_client, TestClient}; use database_entity::dto::AFRole; use shared_entity::dto::workspace_dto::CreateWorkspaceMember; @@ -36,25 +36,6 @@ async fn add_duplicate_workspace_members() { .await; } -#[tokio::test] -async fn add_not_exist_workspace_members() { - let c1 = TestClient::new_user_without_ws_conn().await; - let workspace_id = c1.workspace_id().await; - let email = format!("{}@appflowy.io", uuid::Uuid::new_v4()); - let err = c1 - .api_client - .add_workspace_members( - workspace_id, - vec![CreateWorkspaceMember { - email, - role: AFRole::Member, - }], - ) - .await - .unwrap_err(); - - assert_eq!(err.code, ErrorCode::RecordNotFound); -} #[tokio::test] async fn update_workspace_member_role_not_enough_permission() { let c1 = TestClient::new_user_without_ws_conn().await; @@ -253,3 +234,34 @@ async fn get_user_workspace_info_after_open_workspace() { workspace_id_c1 ); } + +#[tokio::test] +async fn add_workspace_member_not_exists() { + let c1 = TestClient::new_user_without_ws_conn().await; + let workspace_id_c1 = c1.workspace_id().await; + let non_exist_email = format!("{}@appflowy.io", uuid::Uuid::new_v4()); + c1.api_client + .add_workspace_members( + &workspace_id_c1, + vec![CreateWorkspaceMember { + email: non_exist_email.clone(), + role: AFRole::Member, + }], + ) + .await + .expect("should be able to add member whose email not exists yet"); + + let admin_client = admin_user_client().await; + let users = admin_client + .admin_list_users(Some(&non_exist_email)) + .await + .expect("should be able to list users"); + println!("---------non_exist_email: {}", non_exist_email); + let non_exist_user = users + .iter() + .find(|u| u.email == non_exist_email) + .expect("should find the user"); + + // since user does not exist and is just created, it should not be confirmed yet + assert!(non_exist_user.confirmed_at.is_none()); +}