diff --git a/.sqlx/query-884c44d3a87ca4e520f9e8cec6ba673ea4e196920636e4a4db9d42fad3ef4d73.json b/.sqlx/query-884c44d3a87ca4e520f9e8cec6ba673ea4e196920636e4a4db9d42fad3ef4d73.json new file mode 100644 index 00000000..aafc6f23 --- /dev/null +++ b/.sqlx/query-884c44d3a87ca4e520f9e8cec6ba673ea4e196920636e4a4db9d42fad3ef4d73.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE auth.users\n SET role = 'supabase_admin', email_confirmed_at = NOW()\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "884c44d3a87ca4e520f9e8cec6ba673ea4e196920636e4a4db9d42fad3ef4d73" +} diff --git a/admin_frontend/src/web_api.rs b/admin_frontend/src/web_api.rs index 4ca23e3e..51ccb02d 100644 --- a/admin_frontend/src/web_api.rs +++ b/admin_frontend/src/web_api.rs @@ -163,7 +163,7 @@ pub async fn admin_delete_user_handler( &session.token.access_token, &user_uuid, &AdminDeleteUserParams { - should_soft_delete: true, + should_soft_delete: false, }, ) .await?; diff --git a/libs/client-api/src/http.rs b/libs/client-api/src/http.rs index 343269ca..75f4fa05 100644 --- a/libs/client-api/src/http.rs +++ b/libs/client-api/src/http.rs @@ -1,5 +1,6 @@ use crate::notify::{ClientToken, TokenStateReceiver}; use anyhow::Context; +use gotrue_entity::dto::AuthProvider; use prost::Message as ProstMessage; use app_error::AppError; @@ -44,7 +45,7 @@ use url::Url; use crate::retry::{RefreshTokenAction, RefreshTokenRetryCondition}; use crate::ws::{WSClientHttpSender, WSError}; use gotrue_entity::dto::SignUpResponse::{Authenticated, NotAuthenticated}; -use gotrue_entity::dto::{AuthProvider, GotrueTokenResponse, UpdateGotrueUserParams, User}; +use gotrue_entity::dto::{GotrueTokenResponse, UpdateGotrueUserParams, User}; use realtime_entity::realtime_proto::HttpRealtimeMessage; /// `Client` is responsible for managing communication with the GoTrue API and cloud storage. diff --git a/migrations/20231130140000_user_delete_trigger.sql b/migrations/20231130140000_user_delete_trigger.sql new file mode 100644 index 00000000..c89c669a --- /dev/null +++ b/migrations/20231130140000_user_delete_trigger.sql @@ -0,0 +1,35 @@ +GRANT SELECT, INSERT, UPDATE, DELETE ON public.af_user TO supabase_auth_admin; + +-- Trigger Function to delete a user from the pulic.af_user table +-- when a user is deleted from auth.users table (with matching uuid) field +CREATE OR REPLACE FUNCTION public.delete_user() +RETURNS TRIGGER AS $$ +BEGIN + DELETE FROM public.af_user WHERE uuid = OLD.id; + RETURN OLD; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER delete_user_trigger +AFTER DELETE ON auth.users +FOR EACH ROW EXECUTE FUNCTION public.delete_user(); + +-- Trigger Function to update the 'deleted_at' field in the pulic.af_user table +-- (Soft Delete) +CREATE OR REPLACE FUNCTION public.update_af_user_deleted_at() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if 'deleted_at' field is modified + IF OLD.deleted_at IS DISTINCT FROM NEW.deleted_at THEN + -- Update 'deleted_at' in public.af_user + UPDATE public.af_user + SET deleted_at = NEW.deleted_at + WHERE uuid = NEW.id; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_af_user_deleted_at_trigger +AFTER UPDATE OF deleted_at ON auth.users +FOR EACH ROW EXECUTE FUNCTION public.update_af_user_deleted_at(); diff --git a/migrations/before/20230312043000_supabase_auth.sql b/migrations/before/20230312043000_supabase_auth.sql index a913dfca..70a44b0e 100644 --- a/migrations/before/20230312043000_supabase_auth.sql +++ b/migrations/before/20230312043000_supabase_auth.sql @@ -26,7 +26,7 @@ DO $$ BEGIN IF NOT EXISTS ( SELECT FROM pg_catalog.pg_roles WHERE rolname = 'supabase_auth_admin' -) THEN CREATE USER supabase_auth_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION PASSWORD 'root'; +) THEN CREATE USER supabase_auth_admin BYPASSRLS NOINHERIT CREATEROLE LOGIN NOREPLICATION PASSWORD 'root'; END IF; END $$; -- Create auth schema if it does not exist @@ -34,5 +34,4 @@ CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION supabase_auth_admin; -- Grant permissions GRANT CREATE ON DATABASE postgres TO supabase_auth_admin; -- Set search_path for supabase_auth_admin -ALTER USER supabase_auth_admin -SET search_path = 'auth'; \ No newline at end of file +ALTER USER supabase_auth_admin SET search_path = 'auth'; diff --git a/src/application.rs b/src/application.rs index d1058266..07546d13 100644 --- a/src/application.rs +++ b/src/application.rs @@ -241,14 +241,14 @@ async fn setup_admin_account( }, _ => { let user_id = admin_user.id.parse::()?; - let result = sqlx::query( + let result = sqlx::query!( r#" UPDATE auth.users SET role = 'supabase_admin', email_confirmed_at = NOW() WHERE id = $1 "#, + user_id, ) - .bind(user_id) .execute(pg_pool) .await .context("failed to update the admin user")?; diff --git a/tests/user/delete.rs b/tests/user/delete.rs new file mode 100644 index 00000000..08b4436b --- /dev/null +++ b/tests/user/delete.rs @@ -0,0 +1,60 @@ +use gotrue::params::{AdminDeleteUserParams, AdminUserParams}; + +use crate::{ + localhost_client, + user::utils::{ + admin_user_client, generate_unique_registered_user_client, localhost_gotrue_client, + }, +}; + +#[tokio::test] +async fn admin_delete_create_same_user_hard() { + let (client, user) = generate_unique_registered_user_client().await; + let workspaces = client.get_workspaces().await.unwrap().0; + assert_eq!(workspaces.len(), 1); + let workspace_id = workspaces[0].workspace_id; + let user_uuid = client.get_profile().await.unwrap().uuid; + + let admin_token = { + let admin_client = admin_user_client().await; + admin_client.access_token().unwrap() + }; + + // Delete user as admin + let gotrue_client: gotrue::api::Client = localhost_gotrue_client(); + gotrue_client + .admin_delete_user( + &admin_token, + &user_uuid.to_string(), + &AdminDeleteUserParams { + should_soft_delete: false, + }, + ) + .await + .unwrap(); + + // Recreate same user + gotrue_client + .admin_add_user( + &admin_token, + &AdminUserParams { + email: user.email.clone(), + password: Some(user.password.clone()), + email_confirm: true, + ..Default::default() + }, + ) + .await + .unwrap(); + + // Login with recreated user + let client = localhost_client(); + client + .sign_in_password(&user.email, &user.password) + .await + .unwrap(); + let recreated_user_uuid = client.get_profile().await.unwrap().uuid; + let recreated_workspace_uuid = client.get_workspaces().await.unwrap().0[0].workspace_id; + assert_ne!(user_uuid, recreated_user_uuid); + assert_ne!(workspace_id, recreated_workspace_uuid); +} diff --git a/tests/user/mod.rs b/tests/user/mod.rs index 8de19d85..09317ff1 100644 --- a/tests/user/mod.rs +++ b/tests/user/mod.rs @@ -1,3 +1,4 @@ +mod delete; mod refresh; mod sign_in; mod sign_out; diff --git a/tests/user/utils.rs b/tests/user/utils.rs index ee8454a8..40aaa583 100644 --- a/tests/user/utils.rs +++ b/tests/user/utils.rs @@ -5,8 +5,8 @@ use sqlx::types::Uuid; use lazy_static::lazy_static; -use crate::localhost_client; use crate::util::setup_log; +use crate::{localhost_client, LOCALHOST_GOTRUE}; lazy_static! { pub static ref ADMIN_USER: User = { @@ -73,3 +73,8 @@ pub async fn generate_sign_in_action_link(email: &str) -> String { .await .unwrap() } + +pub fn localhost_gotrue_client() -> gotrue::api::Client { + let reqwest_client = reqwest::Client::new(); + gotrue::api::Client::new(reqwest_client, LOCALHOST_GOTRUE) +}