Repository: DefGuard/defguard (language: rust)

Issue:
The gateway is not automatically reconfigured after deleting a user
User devices (peers) are still present on the wireguard interface after deleting their owner. They disappear only after restarting the gateway. 


Discussion / hints:

Fix patch:
diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs
index 73722c102..e52eb7f1a 100644
--- a/src/db/models/mod.rs
+++ b/src/db/models/mod.rs
@@ -198,7 +198,7 @@ pub struct UserDetails {
 
 impl UserDetails {
     pub async fn from_user(pool: &DbPool, user: &User) -> Result<Self, SqlxError> {
-        let devices = user.devices(pool).await?;
+        let devices = user.user_devices(pool).await?;
         let wallets = user.wallets(pool).await?;
         let security_keys = user.security_keys(pool).await?;
 
diff --git a/src/db/models/user.rs b/src/db/models/user.rs
index b9b594865..4b873e3c9 100644
--- a/src/db/models/user.rs
+++ b/src/db/models/user.rs
@@ -683,24 +683,35 @@ impl User {
         }
     }
 
-    pub async fn devices(&self, pool: &DbPool) -> Result<Vec<UserDevice>, SqlxError> {
+    /// Returns a vector of [`UserDevice`]s (hence the name).
+    /// [`UserDevice`] is a struct containing additional network info about a device.
+    /// If you only need [`Device`]s, use [`User::devices()`] instead.
+    pub async fn user_devices(&self, pool: &DbPool) -> Result<Vec<UserDevice>, SqlxError> {
+        let devices = self.devices(pool).await?;
+        let mut user_devices = Vec::new();
+        for device in devices {
+            if let Some(user_device) = UserDevice::from_device(pool, device).await? {
+                user_devices.push(user_device);
+            }
+        }
+        Ok(user_devices)
+    }
+
+    /// Returns a vector of [`Device`]s related to a user. If you want to get [`UserDevice`]s (which contain additional network info),
+    /// use [`User::user_devices()`] instead.
+    pub async fn devices<'e, E>(&self, executor: E) -> Result<Vec<Device>, SqlxError>
+    where
+        E: PgExecutor<'e>,
+    {
         if let Some(id) = self.id {
-            let devices = query_as!(
+            query_as!(
                 Device,
                 "SELECT device.id \"id?\", name, wireguard_pubkey, user_id, created \
                 FROM device WHERE user_id = $1",
                 id
             )
-            .fetch_all(pool)
-            .await?;
-
-            let mut user_devices = Vec::new();
-            for device in devices {
-                if let Some(user_device) = UserDevice::from_device(pool, device).await? {
-                    user_devices.push(user_device);
-                }
-            }
-            Ok(user_devices)
+            .fetch_all(executor)
+            .await
         } else {
             Ok(Vec::new())
         }
diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs
index 0307b250a..59e902750 100644
--- a/src/grpc/enrollment.rs
+++ b/src/grpc/enrollment.rs
@@ -573,7 +573,7 @@ impl From<User> for AdminInfo {
 impl InitialUserInfo {
     async fn from_user(pool: &DbPool, user: User) -> Result<Self, sqlx::Error> {
         let enrolled = user.is_enrolled();
-        let devices = user.devices(pool).await?;
+        let devices = user.user_devices(pool).await?;
         let device_names = devices.into_iter().map(|dev| dev.device.name).collect();
         Ok(Self {
             first_name: user.first_name,
diff --git a/src/handlers/user.rs b/src/handlers/user.rs
index 17b98d70c..a9411e94f 100644
--- a/src/handlers/user.rs
+++ b/src/handlers/user.rs
@@ -15,9 +15,12 @@ use crate::{
     appstate::AppState,
     auth::{SessionInfo, UserAdminRole},
     db::{
-        models::enrollment::{Token, PASSWORD_RESET_TOKEN_TYPE},
-        AppEvent, MFAMethod, OAuth2AuthorizedApp, Settings, User, UserDetails, UserInfo, Wallet,
-        WebAuthn, WireguardNetwork,
+        models::{
+            device::DeviceInfo,
+            enrollment::{Token, PASSWORD_RESET_TOKEN_TYPE},
+        },
+        AppEvent, GatewayEvent, MFAMethod, OAuth2AuthorizedApp, Settings, User, UserDetails,
+        UserInfo, Wallet, WebAuthn, WireguardNetwork,
     },
     error::WebError,
     ldap::utils::{ldap_add_user, ldap_change_password, ldap_delete_user, ldap_modify_user},
@@ -710,9 +713,27 @@ pub async fn delete_user(
         });
     }
     if let Some(user) = User::find_by_username(&appstate.pool, &username).await? {
-        user.delete(&appstate.pool).await?;
-        let _result = ldap_delete_user(&appstate.pool, &username).await;
+        // Get rid of all devices of the deleted user from networks first
+        debug!(
+            "User {} deleted user {username}, purging their network devices across all networks.",
+            session.user.username
+        );
+        let mut transaction = appstate.pool.begin().await?;
+        let devices = user.devices(&mut *transaction).await?;
+        let mut events = Vec::new();
+        for device in devices {
+            events.push(GatewayEvent::DeviceDeleted(
+                DeviceInfo::from_device(&mut *transaction, device).await?,
+            ));
+        }
+        appstate.send_multiple_wireguard_events(events);
+        debug!("Devices of user {username} purged from networks.");
+
+        user.delete(&mut *transaction).await?;
+        let _result = ldap_delete_user(&mut *transaction, &username).await;
         appstate.trigger_action(AppEvent::UserDeleted(username.clone()));
+        transaction.commit().await?;
+
         info!("User {} deleted user {}", session.user.username, &username);
         Ok(ApiResponse::default())
     } else {


Test patch: