feat: User deletion (#177)

* feat: database trigger for hard delete of user

* feat: clean delete user data

* feat: hard delete when admin deletes
This commit is contained in:
Zack 2023-11-27 01:07:45 +08:00 committed by GitHub
parent 1d07afa304
commit 3890f5d306
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 123 additions and 8 deletions

View File

@ -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"
}

View File

@ -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?;

View File

@ -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.

View File

@ -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();

View File

@ -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';
ALTER USER supabase_auth_admin SET search_path = 'auth';

View File

@ -241,14 +241,14 @@ async fn setup_admin_account(
},
_ => {
let user_id = admin_user.id.parse::<uuid::Uuid>()?;
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")?;

60
tests/user/delete.rs Normal file
View File

@ -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);
}

View File

@ -1,3 +1,4 @@
mod delete;
mod refresh;
mod sign_in;
mod sign_out;

View File

@ -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)
}