From f560c35cd884f8fc565143d649e9931b02181b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 18 Mar 2026 07:28:47 +0100 Subject: [PATCH 01/19] add preshared key to vpn sessions table --- ...0]_vpn_client_session_preshared_key.down.sql | 17 +++++++++++++++++ ...0.0]_vpn_client_session_preshared_key.up.sql | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql create mode 100644 migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql diff --git a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql new file mode 100644 index 0000000000..1effb86459 --- /dev/null +++ b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql @@ -0,0 +1,17 @@ +-- Preserve unmatched legacy values during rollback; refresh only rows with canonical active session data. +UPDATE wireguard_network_device AS network_device +SET preshared_key = latest_active_session.preshared_key +FROM ( + SELECT DISTINCT ON (session.device_id, session.location_id) + session.device_id, + session.location_id, + session.preshared_key + FROM vpn_client_session AS session + WHERE session.state IN ('new', 'connected') + AND session.preshared_key IS NOT NULL + ORDER BY session.device_id, session.location_id, session.created_at DESC, session.id DESC +) AS latest_active_session +WHERE network_device.device_id = latest_active_session.device_id + AND network_device.wireguard_network_id = latest_active_session.location_id; + +ALTER TABLE vpn_client_session DROP COLUMN preshared_key; diff --git a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql new file mode 100644 index 0000000000..2bd78db04e --- /dev/null +++ b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql @@ -0,0 +1,2 @@ +-- Transitional additive step: add session-level preshared_key now; application rollout moves data later. +ALTER TABLE vpn_client_session ADD COLUMN preshared_key text NULL; From 27ba7a6377addef01b56ebc26c085c36fa123031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 18 Mar 2026 09:45:43 +0100 Subject: [PATCH 02/19] use new field in mfa flow --- .../defguard_common/src/db/models/device.rs | 267 ++++++++++++++++-- .../src/db/models/vpn_client_session.rs | 17 +- .../src/db/models/wireguard.rs | 2 +- .../src/grpc/proxy/client_mfa.rs | 69 ++--- .../defguard_core/src/handlers/wireguard.rs | 11 +- .../src/location_management/allowed_peers.rs | 78 ++++- .../src/location_management/mod.rs | 51 ++-- crates/defguard_session_manager/src/lib.rs | 10 - .../tests/common/mod.rs | 32 ++- .../tests/session_manager/disconnects.rs | 2 + .../tests/session_manager/event_flow.rs | 2 + .../tests/session_manager/mfa.rs | 26 +- .../tests/session_manager/sessions.rs | 4 +- .../tests/session_manager/stats.rs | 1 + 14 files changed, 419 insertions(+), 153 deletions(-) diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index 42956fd515..51dc2453cf 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -26,7 +26,7 @@ use crate::{ models::{ ModelError, WireguardNetwork, user::User, - vpn_client_session::VpnClientSessionState, + vpn_client_session::{VpnClientSession, VpnClientSessionState}, wireguard::{ LocationMfaMode, NetworkAddressError, ServiceLocationMode, WireguardNetworkError, }, @@ -163,11 +163,29 @@ impl DeviceInfo { debug!("Generating device info for {device}"); let network_info = query_as!( DeviceNetworkInfo, - "SELECT wireguard_network_id network_id, \ - wireguard_ips \"device_wireguard_ips: Vec\", \ - preshared_key, is_authorized \ - FROM wireguard_network_device \ - WHERE device_id = $1", + "SELECT wnd.wireguard_network_id network_id, \ + wnd.wireguard_ips \"device_wireguard_ips: Vec\", \ + CASE \ + WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN wnd.preshared_key \ + ELSE COALESCE(active_session.preshared_key, wnd.preshared_key) \ + END preshared_key, \ + CASE \ + WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN wnd.is_authorized \ + ELSE active_session.id IS NOT NULL \ + END \"is_authorized!\" \ + FROM wireguard_network_device wnd \ + JOIN wireguard_network n ON n.id = wnd.wireguard_network_id \ + LEFT JOIN LATERAL ( \ + SELECT id, preshared_key \ + FROM vpn_client_session \ + WHERE location_id = wnd.wireguard_network_id \ + AND device_id = wnd.device_id \ + AND state IN ('new', 'connected') \ + ORDER BY created_at DESC, id DESC \ + LIMIT 1 \ + ) active_session ON true \ + WHERE wnd.device_id = $1 \ + ORDER BY wnd.wireguard_network_id ASC", device.id ) .fetch_all(executor) @@ -221,7 +239,7 @@ impl UserDevice { LIMIT 1 \ ) vss ON vss.session_id = vpn_client_session.id \ WHERE location_id = n.id and device_id = $1 \ - ORDER BY created_at DESC \ + ORDER BY created_at DESC, id DESC \ LIMIT 1 \ ) vs ON vs.location_id = n.id \ WHERE wnd.device_id = $1", @@ -296,6 +314,100 @@ pub struct ModifyDevice { } impl WireguardNetworkDevice { + async fn latest_active_session<'e, E>( + executor: E, + network: &WireguardNetwork, + device_id: Id, + ) -> Result>, SqlxError> + where + E: PgExecutor<'e>, + { + if !network.mfa_enabled() { + return Ok(None); + } + + VpnClientSession::try_get_active_session(executor, network.id, device_id).await + } + + fn runtime_mfa_state( + &self, + network: &WireguardNetwork, + active_session: Option<&VpnClientSession>, + ) -> (Option, bool, Option) { + if !network.mfa_enabled() { + return ( + self.preshared_key.clone(), + self.is_authorized, + self.authorized_at, + ); + } + + let Some(session) = active_session else { + return (None, false, None); + }; + + // TODO: remove legacy device PSK fallback after pre-rollout MFA sessions age out. + let preshared_key = session + .preshared_key + .clone() + .or_else(|| self.preshared_key.clone()); + + ( + preshared_key, + true, + session.connected_at.or(Some(session.created_at)), + ) + } + + #[must_use] + pub fn to_device_network_info( + &self, + network: &WireguardNetwork, + active_session: Option<&VpnClientSession>, + ) -> DeviceNetworkInfo { + let (preshared_key, is_authorized, _) = self.runtime_mfa_state(network, active_session); + + DeviceNetworkInfo { + network_id: network.id, + device_wireguard_ips: self.wireguard_ips.clone(), + preshared_key, + is_authorized, + } + } + + pub async fn to_device_network_info_runtime<'e, E>( + &self, + executor: E, + network: &WireguardNetwork, + ) -> Result + where + E: PgExecutor<'e>, + { + let active_session = Self::latest_active_session(executor, network, self.device_id).await?; + + Ok(self.to_device_network_info(network, active_session.as_ref())) + } + + pub async fn with_runtime_authorization<'e, E>( + &self, + executor: E, + network: &WireguardNetwork, + ) -> Result + where + E: PgExecutor<'e>, + { + let active_session = Self::latest_active_session(executor, network, self.device_id).await?; + let (preshared_key, is_authorized, authorized_at) = + self.runtime_mfa_state(network, active_session.as_ref()); + + let mut runtime_device = self.clone(); + runtime_device.preshared_key = preshared_key; + runtime_device.is_authorized = is_authorized; + runtime_device.authorized_at = authorized_at; + + Ok(runtime_device) + } + #[must_use] pub fn new(network_id: Id, device_id: Id, wireguard_ips: I) -> Self where @@ -701,12 +813,9 @@ impl Device { WireguardNetworkDevice::find(&mut *transaction, self.id, network.id) .await? .ok_or_else(|| DeviceError::Unexpected("Device not found in network".into()))?; - let device_network_info = DeviceNetworkInfo { - network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips.clone(), - preshared_key: wireguard_network_device.preshared_key.clone(), - is_authorized: wireguard_network_device.is_authorized, - }; + let device_network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *transaction, network) + .await?; let config = Self::create_config(network, &wireguard_network_device); let device_config = DeviceConfig { @@ -735,12 +844,9 @@ impl Device { let wireguard_network_device = self .assign_network_ips(&mut *transaction, network, ip) .await?; - let device_network_info = DeviceNetworkInfo { - network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips.clone(), - preshared_key: wireguard_network_device.preshared_key.clone(), - is_authorized: wireguard_network_device.is_authorized, - }; + let device_network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *transaction, network) + .await?; let config = Self::create_config(network, &wireguard_network_device); let device_config = DeviceConfig { @@ -812,12 +918,9 @@ impl Device { self.name, self.user_id ); - let device_network_info = DeviceNetworkInfo { - network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips.clone(), - preshared_key: wireguard_network_device.preshared_key.clone(), - is_authorized: wireguard_network_device.is_authorized, - }; + let device_network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *conn, &network) + .await?; network_info.push(device_network_info); let config = Self::create_config(&network, &wireguard_network_device); @@ -1427,6 +1530,120 @@ mod test { assert_ok!(Device::validate_pubkey(valid_test_key)); } + #[test] + fn test_runtime_mfa_state_falls_back_to_legacy_preshared_key() { + let defaults = WireguardNetwork::::default(); + let network = WireguardNetwork { + id: 1, + name: defaults.name, + address: defaults.address, + port: defaults.port, + pubkey: defaults.pubkey, + prvkey: defaults.prvkey, + endpoint: defaults.endpoint, + dns: defaults.dns, + mtu: defaults.mtu, + fwmark: defaults.fwmark, + allowed_ips: defaults.allowed_ips, + allow_all_groups: defaults.allow_all_groups, + connected_at: defaults.connected_at, + acl_enabled: defaults.acl_enabled, + acl_default_allow: defaults.acl_default_allow, + keepalive_interval: defaults.keepalive_interval, + peer_disconnect_threshold: defaults.peer_disconnect_threshold, + location_mfa_mode: LocationMfaMode::Internal, + service_location_mode: defaults.service_location_mode, + }; + let wireguard_network_device = WireguardNetworkDevice { + wireguard_network_id: network.id, + wireguard_ips: vec![IpAddr::from_str("10.1.1.2").unwrap()], + device_id: 1, + preshared_key: Some("legacy-psk".into()), + is_authorized: false, + authorized_at: None, + }; + let active_session = VpnClientSession { + id: 1, + location_id: network.id, + user_id: 1, + device_id: wireguard_network_device.device_id, + created_at: Utc::now().naive_utc(), + connected_at: None, + disconnected_at: None, + mfa_method: None, + state: VpnClientSessionState::New, + preshared_key: None, + }; + + let network_info = + wireguard_network_device.to_device_network_info(&network, Some(&active_session)); + + assert_eq!(network_info.preshared_key.as_deref(), Some("legacy-psk")); + assert!(network_info.is_authorized); + } + + #[sqlx::test] + async fn test_device_info_uses_legacy_preshared_key_for_rollout_compatibility( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password"), + "Tester", + "Test", + "test@test.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device".into(), + "pubkey".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let mut network = WireguardNetwork:: { + location_mfa_mode: LocationMfaMode::Internal, + ..Default::default() + }; + network.try_set_address("10.1.1.1/24").unwrap(); + let network = network.save(&pool).await.unwrap(); + + let mut wireguard_network_device = WireguardNetworkDevice::new( + network.id, + device.id, + [IpAddr::from_str("10.1.1.2").unwrap()], + ); + wireguard_network_device.preshared_key = Some("legacy-psk".into()); + wireguard_network_device.insert(&pool).await.unwrap(); + + VpnClientSession::new(network.id, user.id, device.id, None, None) + .save(&pool) + .await + .unwrap(); + + let device_info = DeviceInfo::from_device(&pool, device).await.unwrap(); + let network_info = device_info + .network_info + .into_iter() + .find(|info| info.network_id == network.id) + .unwrap(); + + assert!(network_info.is_authorized); + assert_eq!(network_info.preshared_key.as_deref(), Some("legacy-psk")); + } + #[sqlx::test] fn test_all_for_network_and_user(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; diff --git a/crates/defguard_common/src/db/models/vpn_client_session.rs b/crates/defguard_common/src/db/models/vpn_client_session.rs index f31ddca8b1..14937dd40e 100644 --- a/crates/defguard_common/src/db/models/vpn_client_session.rs +++ b/crates/defguard_common/src/db/models/vpn_client_session.rs @@ -41,6 +41,7 @@ pub struct VpnClientSession { pub mfa_method: Option, #[model(enum)] pub state: VpnClientSessionState, + pub preshared_key: Option, } impl VpnClientSession { @@ -69,6 +70,7 @@ impl VpnClientSession { disconnected_at: None, mfa_method, state, + preshared_key: None, } } } @@ -85,9 +87,11 @@ impl VpnClientSession { query_as!( Self, "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, \ - mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" \ + mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key \ FROM vpn_client_session \ - WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected')", + WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') \ + ORDER BY created_at DESC, id DESC \ + LIMIT 1", location_id, device_id ) @@ -121,7 +125,7 @@ impl VpnClientSession { query_as!( Self, "SELECT s.id, location_id, user_id, device_id, created_at, s.connected_at, disconnected_at, \ - mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" \ + mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key \ FROM vpn_client_session s \ LEFT JOIN LATERAL ( \ SELECT latest_handshake \ @@ -145,7 +149,7 @@ impl VpnClientSession { query_as!( Self, "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, \ - mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" \ + mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key \ FROM vpn_client_session \ WHERE location_id = $1 AND state = 'new' \ AND (NOW() - created_at) > $2 * interval '1 second'", @@ -163,9 +167,10 @@ impl VpnClientSession { query_as!( Self, "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, \ - mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" \ + mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key \ FROM vpn_client_session \ - WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected')", + WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') \ + ORDER BY created_at DESC, id DESC", location_id, device_id, ).fetch_all(executor).await diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index b78bd38aaf..cd6433519a 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -1323,7 +1323,7 @@ impl WireguardNetwork { VpnClientSession, "SELECT id, location_id, user_id, device_id, \ created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", \ - state \"state: VpnClientSessionState\" \ + state \"state: VpnClientSessionState\", preshared_key \ FROM vpn_client_session \ WHERE location_id = $1 AND state = 'connected'::vpn_client_session_state", self.id, diff --git a/crates/defguard_core/src/grpc/proxy/client_mfa.rs b/crates/defguard_core/src/grpc/proxy/client_mfa.rs index 9f27f76af1..58a8d4ad1e 100644 --- a/crates/defguard_core/src/grpc/proxy/client_mfa.rs +++ b/crates/defguard_core/src/grpc/proxy/client_mfa.rs @@ -660,13 +660,16 @@ impl ClientMfaServer { })?; // fetch device config for the location - let Ok(Some(mut network_device)) = + let Ok(Some(network_device)) = WireguardNetworkDevice::find(&mut *transaction, device.id, location.id).await else { error!("Failed to fetch network config for device {device} and location {location}"); return Err(Status::internal("unexpected error")); }; + // generate PSK + let key = WireguardNetwork::genkey(); + // create new VPN client session let vpn_client_session = self.create_new_mfa_session( &mut transaction, @@ -674,6 +677,7 @@ impl ClientMfaServer { &user, &device, method.into(), + key.public.clone(), ) .await .map_err(|err| { @@ -682,26 +686,20 @@ impl ClientMfaServer { })?; debug!("Created new VPN client session: {vpn_client_session:?}"); - // generate PSK - let key = WireguardNetwork::genkey(); - network_device.preshared_key = Some(key.public.clone()); - - // authorize device for given location - network_device.is_authorized = true; - network_device.authorized_at = Some(Utc::now().naive_utc()); - - // save updated network config - network_device - .update(&mut *transaction) - .await - .map_err(|err| { - error!("Failed to update device network config {network_device:?}: {err}"); - Status::internal("unexpected error") - })?; + let mut runtime_network_device = network_device; + runtime_network_device.preshared_key = Some(key.public.clone()); + runtime_network_device.is_authorized = true; + runtime_network_device.authorized_at = vpn_client_session + .connected_at + .or(Some(vpn_client_session.created_at)); // send gateway event debug!("Sending `peer_create` message to gateway"); - let event = GatewayEvent::MfaSessionAuthorized(location.id, device.clone(), network_device); + let event = GatewayEvent::MfaSessionAuthorized( + location.id, + device.clone(), + runtime_network_device, + ); self.wireguard_tx.send(event).map_err(|err| { error!("Error sending WireGuard event: {err}"); Status::internal("unexpected error") @@ -766,6 +764,7 @@ impl ClientMfaServer { user: &User, device: &Device, mfa_method: VpnClientMfaMethod, + preshared_key: String, ) -> Result, Status> { debug!( "Creating new VPN session for device {device} of user {user} in location {location} after successful MFA authorization." @@ -792,11 +791,12 @@ impl ClientMfaServer { } // create new MFA session - VpnClientSession::new(location.id, user.id, device.id, None, Some(mfa_method)).save(conn).await - .map_err(|err| { - error!("Failed to create new VPN client session for device {device} in location {location}: {err}"); - Status::internal("unexpected error") - }) + let mut session = VpnClientSession::new(location.id, user.id, device.id, None, Some(mfa_method)); + session.preshared_key = Some(preshared_key); + session.save(conn).await.map_err(|err| { + error!("Failed to create new VPN client session for device {device} in location {location}: {err}"); + Status::internal("unexpected error") + }) } /// Update session state as disconnected and send relevant gateway update @@ -816,29 +816,6 @@ impl ClientMfaServer { Status::internal("unexpected error") })?; - // FIXME: remove once MFA-related data is no longer stored here - // update device network config - if let Some(mut device_network_info) = WireguardNetworkDevice::find( - &mut *conn, - device.id, - location.id, - ) - .await - .map_err(|err| { - error!( - "Failed to fetch WireGuard config for device {device} in location {location}: {err}" - ); - Status::internal("unexpected error") - })? { - device_network_info.is_authorized = false; - device_network_info.preshared_key = None; - device_network_info.update(&mut *conn).await.map_err(|err| { - error!( - "Failed to update WireGuard config for device {device} in location {location}: {err}" - ); - Status::internal("unexpected error") - })?; - } let event = GatewayEvent::MfaSessionDisconnected(location.id, device.clone()); self.wireguard_tx.send(event).map_err(|err| { error!("Error sending WireGuard event: {err}"); diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index d3e02bdf27..67fd8b3b24 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -9,7 +9,7 @@ use defguard_common::{ db::{ Id, models::{ - Device, DeviceConfig, DeviceNetworkInfo, DeviceType, WireguardNetwork, + Device, DeviceConfig, DeviceType, WireguardNetwork, device::{AddDevice, DeviceInfo, ModifyDevice, WireguardNetworkDevice}, wireguard::{LocationMfaMode, MappedDevice, ServiceLocationMode}, }, @@ -1019,12 +1019,9 @@ pub(crate) async fn modify_device( let wireguard_network_device = WireguardNetworkDevice::find(&appstate.pool, device.id, network.id).await?; if let Some(wireguard_network_device) = wireguard_network_device { - let device_network_info = DeviceNetworkInfo { - network_id: network.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips, - preshared_key: wireguard_network_device.preshared_key, - is_authorized: wireguard_network_device.is_authorized, - }; + let device_network_info = wireguard_network_device + .to_device_network_info_runtime(&appstate.pool, network) + .await?; network_info.push(device_network_info); } } diff --git a/crates/defguard_core/src/location_management/allowed_peers.rs b/crates/defguard_core/src/location_management/allowed_peers.rs index c5848d43b8..37521cf645 100644 --- a/crates/defguard_core/src/location_management/allowed_peers.rs +++ b/crates/defguard_core/src/location_management/allowed_peers.rs @@ -28,7 +28,11 @@ where } let rows = query!( - "SELECT d.wireguard_pubkey pubkey, preshared_key, \ + "SELECT d.wireguard_pubkey pubkey, \ + CASE \ + WHEN $2 THEN COALESCE(active_session.preshared_key, wnd.preshared_key) \ + ELSE wnd.preshared_key \ + END preshared_key, \ -- TODO possible to not use ARRAY-unnest here? ARRAY( SELECT host(ip) @@ -37,7 +41,16 @@ where FROM wireguard_network_device wnd \ JOIN device d ON wnd.device_id = d.id \ JOIN \"user\" u ON d.user_id = u.id \ - WHERE wireguard_network_id = $1 AND (is_authorized OR NOT $2) \ + LEFT JOIN LATERAL ( \ + SELECT id, preshared_key \ + FROM vpn_client_session \ + WHERE location_id = wnd.wireguard_network_id \ + AND device_id = wnd.device_id \ + AND state IN ('new', 'connected') \ + ORDER BY created_at DESC, id DESC \ + LIMIT 1 \ + ) active_session ON true \ + WHERE wireguard_network_id = $1 AND (NOT $2 OR active_session.id IS NOT NULL) \ AND d.configured AND u.is_active \ ORDER BY d.id ASC", location.id, @@ -77,6 +90,7 @@ mod test { Device, DeviceType, WireguardNetwork, device::WireguardNetworkDevice, user::User, + vpn_client_session::VpnClientSession, wireguard::{LocationMfaMode, ServiceLocationMode}, }, setup_pool, @@ -223,4 +237,64 @@ mod test { ); assert_eq!(peers_alwayson[0].pubkey, "pubkey3"); } + + #[sqlx::test] + async fn test_get_location_allowed_peers_uses_legacy_preshared_key_for_active_mfa_session( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password123"), + "Test", + "User", + "test@example.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device1".into(), + "pubkey1".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let mut network = WireguardNetwork { + name: "mfa-location".to_string(), + service_location_mode: ServiceLocationMode::Disabled, + location_mfa_mode: LocationMfaMode::Internal, + ..Default::default() + }; + network.try_set_address("10.4.1.1/24").unwrap(); + let network = network.save(&pool).await.unwrap(); + + let mut network_device = WireguardNetworkDevice::new( + network.id, + device.id, + vec![IpAddr::from_str("10.4.1.2").unwrap()], + ); + network_device.preshared_key = Some("legacy-psk".into()); + network_device.insert(&pool).await.unwrap(); + + VpnClientSession::new(network.id, user.id, device.id, None, None) + .save(&pool) + .await + .unwrap(); + + let peers = get_location_allowed_peers(&network, &pool).await.unwrap(); + + assert_eq!(peers.len(), 1); + assert_eq!(peers[0].pubkey, "pubkey1"); + assert_eq!(peers[0].preshared_key.as_deref(), Some("legacy-psk")); + } } diff --git a/crates/defguard_core/src/location_management/mod.rs b/crates/defguard_core/src/location_management/mod.rs index 18e5fc1d9b..f2049b945a 100644 --- a/crates/defguard_core/src/location_management/mod.rs +++ b/crates/defguard_core/src/location_management/mod.rs @@ -5,7 +5,7 @@ use defguard_common::{ db::{ Id, models::{ - Device, DeviceNetworkInfo, DeviceType, ModelError, WireguardNetwork, + Device, DeviceType, ModelError, WireguardNetwork, WireguardNetworkError, device::{DeviceInfo, WireguardNetworkDevice}, user::User, @@ -186,14 +186,12 @@ pub async fn process_device_access_changes( ) .await?; used_ips.extend(wireguard_network_device.wireguard_ips.iter().copied()); + let network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *transaction, location) + .await?; events.push(GatewayEvent::DeviceModified(DeviceInfo { device, - network_info: vec![DeviceNetworkInfo { - network_id: location.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips, - preshared_key: wireguard_network_device.preshared_key, - is_authorized: wireguard_network_device.is_authorized, - }], + network_info: vec![network_info], })); } // Device is no longer allowed @@ -208,14 +206,10 @@ pub async fn process_device_access_changes( if let Some(device) = Device::find_by_id(&mut *transaction, device_network_config.device_id).await? { + let network_info = device_network_config.to_device_network_info(location, None); events.push(GatewayEvent::DeviceDeleted(DeviceInfo { device, - network_info: vec![DeviceNetworkInfo { - network_id: location.id, - device_wireguard_ips: device_network_config.wireguard_ips, - preshared_key: device_network_config.preshared_key, - is_authorized: device_network_config.is_authorized, - }], + network_info: vec![network_info], })); } else { let msg = format!("Device {} does not exist", device_network_config.device_id); @@ -230,14 +224,12 @@ pub async fn process_device_access_changes( .assign_next_network_ip(&mut *transaction, location, &used_ips, reserved_ips, None) .await?; used_ips.extend(wireguard_network_device.wireguard_ips.iter().copied()); + let network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *transaction, location) + .await?; events.push(GatewayEvent::DeviceCreated(DeviceInfo { device, - network_info: vec![DeviceNetworkInfo { - network_id: location.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips, - preshared_key: wireguard_network_device.preshared_key, - is_authorized: wireguard_network_device.is_authorized, - }], + network_info: vec![network_info], })); } @@ -283,15 +275,13 @@ pub(crate) async fn handle_imported_devices( wireguard_network_device.insert(&mut *transaction).await?; // store ID of device with already generated config assigned_device_ids.push(existing_device.id); + let network_info = wireguard_network_device + .to_device_network_info_runtime(&mut *transaction, location) + .await?; // send device to connected gateways events.push(GatewayEvent::DeviceModified(DeviceInfo { device: existing_device, - network_info: vec![DeviceNetworkInfo { - network_id: location.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips, - preshared_key: wireguard_network_device.preshared_key, - is_authorized: wireguard_network_device.is_authorized, - }], + network_info: vec![network_info], })); } None => { @@ -365,12 +355,11 @@ pub(crate) async fn handle_mapped_devices( mapped_device.wireguard_ips.clone(), ); wireguard_network_device.insert(&mut *conn).await?; - network_info.push(DeviceNetworkInfo { - network_id: location.id, - device_wireguard_ips: wireguard_network_device.wireguard_ips, - preshared_key: wireguard_network_device.preshared_key, - is_authorized: wireguard_network_device.is_authorized, - }); + network_info.push( + wireguard_network_device + .to_device_network_info_runtime(&mut *conn, location) + .await?, + ); } // Assign IP addresses in other networks. diff --git a/crates/defguard_session_manager/src/lib.rs b/crates/defguard_session_manager/src/lib.rs index 9ee8e4a463..8a647b7fbf 100644 --- a/crates/defguard_session_manager/src/lib.rs +++ b/crates/defguard_session_manager/src/lib.rs @@ -6,7 +6,6 @@ use defguard_common::{ Id, models::{ Device, User, WireguardNetwork, - device::WireguardNetworkDevice, vpn_client_session::{VpnClientSession, VpnClientSessionState}, }, }, @@ -294,15 +293,6 @@ impl SessionManager { // remove peers from GW for MFA locations if location.mfa_enabled() { - // FIXME: remove once MFA-related data is no longer stored here - // update device network config - if let Some(mut device_network_info) = - WireguardNetworkDevice::find(&mut *transaction, device.id, location.id).await? - { - device_network_info.is_authorized = false; - device_network_info.preshared_key = None; - device_network_info.update(&mut *transaction).await?; - } self.send_peer_disconnect_message(location, &device)?; } diff --git a/crates/defguard_session_manager/tests/common/mod.rs b/crates/defguard_session_manager/tests/common/mod.rs index ddd8181288..8c6789749b 100644 --- a/crates/defguard_session_manager/tests/common/mod.rs +++ b/crates/defguard_session_manager/tests/common/mod.rs @@ -11,7 +11,7 @@ use defguard_common::{ Device, DeviceType, User, WireguardNetwork, device::WireguardNetworkDevice, gateway::Gateway, - vpn_client_session::{VpnClientMfaMethod, VpnClientSession}, + vpn_client_session::{VpnClientMfaMethod, VpnClientSession, VpnClientSessionState}, vpn_session_stats::VpnSessionStats, wireguard::{LocationMfaMode, ServiceLocationMode}, }, @@ -215,20 +215,23 @@ pub(crate) async fn create_gateway_named( pub(crate) async fn authorize_device_in_location( pool: &sqlx::PgPool, location_id: Id, + user_id: Id, device_id: Id, preshared_key: &str, -) { - let mut network_device = WireguardNetworkDevice::find(pool, device_id, location_id) - .await - .expect("failed to load device network info") - .expect("expected device network info"); - network_device.is_authorized = true; - network_device.authorized_at = Some(chrono::Utc::now().naive_utc()); - network_device.preshared_key = Some(preshared_key.to_string()); - network_device - .update(pool) +) -> VpnClientSession { + let mut session = VpnClientSession::new( + location_id, + user_id, + device_id, + Some(truncate_timestamp(chrono::Utc::now().naive_utc())), + Some(VpnClientMfaMethod::Totp), + ); + session.preshared_key = Some(preshared_key.to_string()); + session.state = VpnClientSessionState::Connected; + session + .save(pool) .await - .expect("failed to authorize device in location"); + .expect("failed to create authorized session") } #[allow(clippy::too_many_arguments)] @@ -276,8 +279,11 @@ pub(crate) async fn create_session( device_id: Id, connected_at: Option, mfa_method: Option, + preshared_key: Option<&str>, ) -> VpnClientSession { - VpnClientSession::new(location_id, user_id, device_id, connected_at, mfa_method) + let mut session = VpnClientSession::new(location_id, user_id, device_id, connected_at, mfa_method); + session.preshared_key = preshared_key.map(str::to_owned); + session .save(pool) .await .expect("failed to create vpn client session") diff --git a/crates/defguard_session_manager/tests/session_manager/disconnects.rs b/crates/defguard_session_manager/tests/session_manager/disconnects.rs index 938c428761..d288bedeb4 100644 --- a/crates/defguard_session_manager/tests/session_manager/disconnects.rs +++ b/crates/defguard_session_manager/tests/session_manager/disconnects.rs @@ -33,6 +33,7 @@ async fn test_inactive_connected_sessions_are_disconnected_after_threshold( device.id, Some(stale_handshake), None, + None, ) .await; create_session_stats( @@ -80,6 +81,7 @@ async fn test_recent_connected_sessions_remain_active(_: PgPoolOptions, options: device.id, Some(recent_handshake), None, + None, ) .await; create_session_stats( diff --git a/crates/defguard_session_manager/tests/session_manager/event_flow.rs b/crates/defguard_session_manager/tests/session_manager/event_flow.rs index 744ddd0893..a34cb0f7d9 100644 --- a/crates/defguard_session_manager/tests/session_manager/event_flow.rs +++ b/crates/defguard_session_manager/tests/session_manager/event_flow.rs @@ -79,6 +79,7 @@ async fn test_reusing_existing_connected_session_does_not_emit_duplicate_connect device.id, Some(connected_at), None, + None, ) .await; @@ -120,6 +121,7 @@ async fn test_session_manager_emits_disconnect_event_for_inactive_standard_sessi device.id, Some(stale_handshake), None, + None, ) .await; create_session_stats( diff --git a/crates/defguard_session_manager/tests/session_manager/mfa.rs b/crates/defguard_session_manager/tests/session_manager/mfa.rs index f167211854..ea2e1c777a 100644 --- a/crates/defguard_session_manager/tests/session_manager/mfa.rs +++ b/crates/defguard_session_manager/tests/session_manager/mfa.rs @@ -3,7 +3,6 @@ use std::net::SocketAddr; use chrono::{TimeDelta, Utc}; use defguard_common::db::{ models::{ - device::WireguardNetworkDevice, vpn_client_session::{VpnClientMfaMethod, VpnClientSession, VpnClientSessionState}, vpn_session_stats::VpnSessionStats, wireguard::LocationMfaMode, @@ -84,6 +83,7 @@ async fn test_mfa_new_session_upgrades_to_connected_on_stats( device.id, None, Some(VpnClientMfaMethod::Totp), + None, ) .await; @@ -179,6 +179,7 @@ async fn test_duplicate_first_stats_on_mfa_new_session_are_idempotent( device.id, None, Some(VpnClientMfaMethod::Totp), + None, ) .await; @@ -250,6 +251,7 @@ async fn test_repeated_later_stats_on_mfa_session_remain_idempotent( device.id, None, Some(VpnClientMfaMethod::Totp), + None, ) .await; @@ -331,20 +333,21 @@ async fn test_inactive_mfa_connected_sessions_disconnect_and_clear_authorization let user = create_user(&pool).await; let device = create_device(&pool, user.id).await; attach_device_to_location(&pool, location.id, device.id).await; - authorize_device_in_location(&pool, location.id, device.id, "psk-before-disconnect").await; let gateway = create_gateway(&pool, location.id, user.fullname()).await; let mut harness = SessionManagerHarness::new(pool.clone()); let stale_handshake = stale_session_timestamp(&location); - let session = create_session( + let mut session = authorize_device_in_location( &pool, location.id, user.id, device.id, - Some(stale_handshake), - Some(VpnClientMfaMethod::Totp), + "psk-before-disconnect", ) .await; + session.connected_at = Some(stale_handshake); + session.created_at = stale_handshake; + session.save(&pool).await.expect("failed to age session"); create_session_stats( &pool, session.id, @@ -370,12 +373,12 @@ async fn test_inactive_mfa_connected_sessions_disconnect_and_clear_authorization VpnClientSessionState::Disconnected ); - let network_device = WireguardNetworkDevice::find(&pool, device.id, location.id) - .await - .expect("failed to query network device") - .expect("expected network device"); - assert!(!network_device.is_authorized); - assert_eq!(network_device.preshared_key, None); + assert!( + VpnClientSession::try_get_active_session(&pool, location.id, device.id) + .await + .expect("failed to query active session") + .is_none() + ); let gateway_event = timeout(RECEIVE_TIMEOUT, harness.gateway_rx.recv()) .await @@ -409,6 +412,7 @@ async fn test_never_connected_mfa_new_sessions_disconnect_after_threshold( device.id, None, Some(VpnClientMfaMethod::Totp), + Some("psk-before-timeout"), ) .await; set_session_created_at(&pool, session.id, stale_session_timestamp(&location)).await; diff --git a/crates/defguard_session_manager/tests/session_manager/sessions.rs b/crates/defguard_session_manager/tests/session_manager/sessions.rs index 77bd99d639..531f1dbbb6 100644 --- a/crates/defguard_session_manager/tests/session_manager/sessions.rs +++ b/crates/defguard_session_manager/tests/session_manager/sessions.rs @@ -270,7 +270,8 @@ async fn test_existing_new_session_becomes_connected_on_stats( let gateway = create_gateway(&pool, location.id, user.fullname()).await; let mut harness = SessionManagerHarness::new(pool.clone()); - let existing_session = create_session(&pool, location.id, user.id, device.id, None, None).await; + let existing_session = + create_session(&pool, location.id, user.id, device.id, None, None, None).await; assert_eq!(existing_session.state, VpnClientSessionState::New); let endpoint: SocketAddr = "203.0.113.10:51820".parse().unwrap(); @@ -495,6 +496,7 @@ async fn test_existing_session_in_db_is_reused_instead_of_creating_duplicate( device.id, Some(base_time - TimeDelta::seconds(5)), None, + None, ) .await; create_session_stats( diff --git a/crates/defguard_session_manager/tests/session_manager/stats.rs b/crates/defguard_session_manager/tests/session_manager/stats.rs index a8a6cf3293..f74bf82120 100644 --- a/crates/defguard_session_manager/tests/session_manager/stats.rs +++ b/crates/defguard_session_manager/tests/session_manager/stats.rs @@ -203,6 +203,7 @@ async fn test_out_of_order_updates_for_existing_db_session_are_discarded( device.id, Some(first_handshake), None, + None, ) .await; create_session_stats( From 9c7474d0a1c80e2feb7be44666af3855e70aeae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 18 Mar 2026 10:02:33 +0100 Subject: [PATCH 03/19] remove legacy key usage --- ...0f7be1914de4263c8db6d16110d7e75d6bb15.json | 35 ++++++++++++++++ ...1cd3eab7aad7a61ee274b7efea9e25f0b6766.json | 40 +++++++++++++++++++ .../defguard_common/src/db/models/device.rs | 18 +++------ .../src/location_management/allowed_peers.rs | 6 +-- 4 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 .sqlx/query-0b00683559108a14bf73ce9f6430f7be1914de4263c8db6d16110d7e75d6bb15.json create mode 100644 .sqlx/query-3db9abdf258314698fe189960921cd3eab7aad7a61ee274b7efea9e25f0b6766.json diff --git a/.sqlx/query-0b00683559108a14bf73ce9f6430f7be1914de4263c8db6d16110d7e75d6bb15.json b/.sqlx/query-0b00683559108a14bf73ce9f6430f7be1914de4263c8db6d16110d7e75d6bb15.json new file mode 100644 index 0000000000..3070bf48a3 --- /dev/null +++ b/.sqlx/query-0b00683559108a14bf73ce9f6430f7be1914de4263c8db6d16110d7e75d6bb15.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.wireguard_pubkey pubkey, CASE WHEN $2 THEN active_session.preshared_key ELSE wnd.preshared_key END preshared_key, -- TODO possible to not use ARRAY-unnest here?\n ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wireguard_network_id = $1 AND (NOT $2 OR active_session.id IS NOT NULL) AND d.configured AND u.is_active ORDER BY d.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "preshared_key", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "allowed_ips!: Vec", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Int8", + "Bool" + ] + }, + "nullable": [ + false, + null, + null + ] + }, + "hash": "0b00683559108a14bf73ce9f6430f7be1914de4263c8db6d16110d7e75d6bb15" +} diff --git a/.sqlx/query-3db9abdf258314698fe189960921cd3eab7aad7a61ee274b7efea9e25f0b6766.json b/.sqlx/query-3db9abdf258314698fe189960921cd3eab7aad7a61ee274b7efea9e25f0b6766.json new file mode 100644 index 0000000000..d4bdf28bd4 --- /dev/null +++ b/.sqlx/query-3db9abdf258314698fe189960921cd3eab7aad7a61ee274b7efea9e25f0b6766.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wnd.wireguard_network_id network_id, wnd.wireguard_ips \"device_wireguard_ips: Vec\", CASE WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN wnd.preshared_key ELSE active_session.preshared_key END preshared_key, CASE WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN wnd.is_authorized ELSE active_session.id IS NOT NULL END \"is_authorized!\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wnd.device_id = $1 ORDER BY wnd.wireguard_network_id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "network_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "device_wireguard_ips: Vec", + "type_info": "InetArray" + }, + { + "ordinal": 2, + "name": "preshared_key", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "is_authorized!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + null, + null + ] + }, + "hash": "3db9abdf258314698fe189960921cd3eab7aad7a61ee274b7efea9e25f0b6766" +} diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index 51dc2453cf..675b290c06 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -167,7 +167,7 @@ impl DeviceInfo { wnd.wireguard_ips \"device_wireguard_ips: Vec\", \ CASE \ WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN wnd.preshared_key \ - ELSE COALESCE(active_session.preshared_key, wnd.preshared_key) \ + ELSE active_session.preshared_key \ END preshared_key, \ CASE \ WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN wnd.is_authorized \ @@ -346,14 +346,8 @@ impl WireguardNetworkDevice { return (None, false, None); }; - // TODO: remove legacy device PSK fallback after pre-rollout MFA sessions age out. - let preshared_key = session - .preshared_key - .clone() - .or_else(|| self.preshared_key.clone()); - ( - preshared_key, + session.preshared_key.clone(), true, session.connected_at.or(Some(session.created_at)), ) @@ -1531,7 +1525,7 @@ mod test { } #[test] - fn test_runtime_mfa_state_falls_back_to_legacy_preshared_key() { + fn test_runtime_mfa_state_requires_session_preshared_key_for_mfa_runtime_reads() { let defaults = WireguardNetwork::::default(); let network = WireguardNetwork { id: 1, @@ -1578,12 +1572,12 @@ mod test { let network_info = wireguard_network_device.to_device_network_info(&network, Some(&active_session)); - assert_eq!(network_info.preshared_key.as_deref(), Some("legacy-psk")); + assert_eq!(network_info.preshared_key, None); assert!(network_info.is_authorized); } #[sqlx::test] - async fn test_device_info_uses_legacy_preshared_key_for_rollout_compatibility( + async fn test_device_info_does_not_expose_legacy_preshared_key_for_active_mfa_session( _: PgPoolOptions, options: PgConnectOptions, ) { @@ -1641,7 +1635,7 @@ mod test { .unwrap(); assert!(network_info.is_authorized); - assert_eq!(network_info.preshared_key.as_deref(), Some("legacy-psk")); + assert_eq!(network_info.preshared_key, None); } #[sqlx::test] diff --git a/crates/defguard_core/src/location_management/allowed_peers.rs b/crates/defguard_core/src/location_management/allowed_peers.rs index 37521cf645..e2797a2c79 100644 --- a/crates/defguard_core/src/location_management/allowed_peers.rs +++ b/crates/defguard_core/src/location_management/allowed_peers.rs @@ -30,7 +30,7 @@ where let rows = query!( "SELECT d.wireguard_pubkey pubkey, \ CASE \ - WHEN $2 THEN COALESCE(active_session.preshared_key, wnd.preshared_key) \ + WHEN $2 THEN active_session.preshared_key \ ELSE wnd.preshared_key \ END preshared_key, \ -- TODO possible to not use ARRAY-unnest here? @@ -239,7 +239,7 @@ mod test { } #[sqlx::test] - async fn test_get_location_allowed_peers_uses_legacy_preshared_key_for_active_mfa_session( + async fn test_get_location_allowed_peers_does_not_expose_legacy_preshared_key_for_active_mfa_session( _: PgPoolOptions, options: PgConnectOptions, ) { @@ -295,6 +295,6 @@ mod test { assert_eq!(peers.len(), 1); assert_eq!(peers[0].pubkey, "pubkey1"); - assert_eq!(peers[0].preshared_key.as_deref(), Some("legacy-psk")); + assert_eq!(peers[0].preshared_key, None); } } From e941641ac86edbb4bc7f0481a4dbb7ed3d7fd671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 18 Mar 2026 11:13:15 +0100 Subject: [PATCH 04/19] proceed with cleanup --- .../defguard_common/src/db/models/device.rs | 52 +--- .../src/grpc/proxy/client_mfa.rs | 38 ++- .../src/location_management/mod.rs | 3 +- .../defguard_gateway_manager/src/handler.rs | 268 +++++++++++++----- .../tests/common/mod.rs | 3 +- 5 files changed, 229 insertions(+), 135 deletions(-) diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index 675b290c06..1a02a0cf9f 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -329,37 +329,19 @@ impl WireguardNetworkDevice { VpnClientSession::try_get_active_session(executor, network.id, device_id).await } - fn runtime_mfa_state( - &self, - network: &WireguardNetwork, - active_session: Option<&VpnClientSession>, - ) -> (Option, bool, Option) { - if !network.mfa_enabled() { - return ( - self.preshared_key.clone(), - self.is_authorized, - self.authorized_at, - ); - } - - let Some(session) = active_session else { - return (None, false, None); - }; - - ( - session.preshared_key.clone(), - true, - session.connected_at.or(Some(session.created_at)), - ) - } - #[must_use] pub fn to_device_network_info( &self, network: &WireguardNetwork, active_session: Option<&VpnClientSession>, ) -> DeviceNetworkInfo { - let (preshared_key, is_authorized, _) = self.runtime_mfa_state(network, active_session); + let (preshared_key, is_authorized) = if !network.mfa_enabled() { + (self.preshared_key.clone(), self.is_authorized) + } else if let Some(session) = active_session { + (session.preshared_key.clone(), true) + } else { + (None, false) + }; DeviceNetworkInfo { network_id: network.id, @@ -382,26 +364,6 @@ impl WireguardNetworkDevice { Ok(self.to_device_network_info(network, active_session.as_ref())) } - pub async fn with_runtime_authorization<'e, E>( - &self, - executor: E, - network: &WireguardNetwork, - ) -> Result - where - E: PgExecutor<'e>, - { - let active_session = Self::latest_active_session(executor, network, self.device_id).await?; - let (preshared_key, is_authorized, authorized_at) = - self.runtime_mfa_state(network, active_session.as_ref()); - - let mut runtime_device = self.clone(); - runtime_device.preshared_key = preshared_key; - runtime_device.is_authorized = is_authorized; - runtime_device.authorized_at = authorized_at; - - Ok(runtime_device) - } - #[must_use] pub fn new(network_id: Id, device_id: Id, wireguard_ips: I) -> Self where diff --git a/crates/defguard_core/src/grpc/proxy/client_mfa.rs b/crates/defguard_core/src/grpc/proxy/client_mfa.rs index 58a8d4ad1e..cd88594d73 100644 --- a/crates/defguard_core/src/grpc/proxy/client_mfa.rs +++ b/crates/defguard_core/src/grpc/proxy/client_mfa.rs @@ -79,6 +79,23 @@ pub struct ClientMfaServer { } impl ClientMfaServer { + fn build_mfa_authorized_gateway_network_device( + network_device: WireguardNetworkDevice, + vpn_client_session: &VpnClientSession, + preshared_key: String, + ) -> WireguardNetworkDevice { + WireguardNetworkDevice { + wireguard_network_id: network_device.wireguard_network_id, + wireguard_ips: network_device.wireguard_ips, + device_id: network_device.device_id, + preshared_key: Some(preshared_key), + is_authorized: true, + authorized_at: vpn_client_session + .connected_at + .or(Some(vpn_client_session.created_at)), + } + } + #[must_use] pub fn new( pool: PgPool, @@ -686,20 +703,16 @@ impl ClientMfaServer { })?; debug!("Created new VPN client session: {vpn_client_session:?}"); - let mut runtime_network_device = network_device; - runtime_network_device.preshared_key = Some(key.public.clone()); - runtime_network_device.is_authorized = true; - runtime_network_device.authorized_at = vpn_client_session - .connected_at - .or(Some(vpn_client_session.created_at)); + let gateway_network_device = Self::build_mfa_authorized_gateway_network_device( + network_device, + &vpn_client_session, + key.public.clone(), + ); // send gateway event debug!("Sending `peer_create` message to gateway"); - let event = GatewayEvent::MfaSessionAuthorized( - location.id, - device.clone(), - runtime_network_device, - ); + let event = + GatewayEvent::MfaSessionAuthorized(location.id, device.clone(), gateway_network_device); self.wireguard_tx.send(event).map_err(|err| { error!("Error sending WireGuard event: {err}"); Status::internal("unexpected error") @@ -791,7 +804,8 @@ impl ClientMfaServer { } // create new MFA session - let mut session = VpnClientSession::new(location.id, user.id, device.id, None, Some(mfa_method)); + let mut session = + VpnClientSession::new(location.id, user.id, device.id, None, Some(mfa_method)); session.preshared_key = Some(preshared_key); session.save(conn).await.map_err(|err| { error!("Failed to create new VPN client session for device {device} in location {location}: {err}"); diff --git a/crates/defguard_core/src/location_management/mod.rs b/crates/defguard_core/src/location_management/mod.rs index f2049b945a..a057ae8128 100644 --- a/crates/defguard_core/src/location_management/mod.rs +++ b/crates/defguard_core/src/location_management/mod.rs @@ -5,8 +5,7 @@ use defguard_common::{ db::{ Id, models::{ - Device, DeviceType, ModelError, WireguardNetwork, - WireguardNetworkError, + Device, DeviceType, ModelError, WireguardNetwork, WireguardNetworkError, device::{DeviceInfo, WireguardNetworkDevice}, user::User, wireguard::MappedDevice, diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index e10a012959..18f34e1d7f 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -404,6 +404,74 @@ impl GatewayUpdatesHandler { } } + #[must_use] + fn runtime_peer_update( + &self, + peer_label: &str, + peer_pubkey: String, + allowed_ips: Vec, + is_authorized: bool, + preshared_key: Option, + ) -> Option { + if !self.network.mfa_enabled() { + return Some(Peer { + pubkey: peer_pubkey, + allowed_ips, + preshared_key: None, + keepalive_interval: Some(self.network.keepalive_interval.cast_unsigned()), + }); + } + + if !is_authorized { + debug!( + "Skipping gateway peer update for WireGuard device {} in MFA enabled location {} because there is no active MFA session", + peer_label, self.network.name + ); + return None; + } + + let Some(preshared_key) = preshared_key else { + debug!( + "Skipping gateway peer update for WireGuard device {} in location {} because the runtime preshared key is missing", + peer_label, self.network.name + ); + return None; + }; + + Some(Peer { + pubkey: peer_pubkey, + allowed_ips, + preshared_key: Some(preshared_key), + keepalive_interval: Some(self.network.keepalive_interval.cast_unsigned()), + }) + } + + fn send_runtime_device_update( + &self, + peer_label: &str, + peer_pubkey: String, + network_info: &defguard_common::db::models::DeviceNetworkInfo, + update_type: i32, + ) -> Result<(), Status> { + let allowed_ips = network_info + .device_wireguard_ips + .iter() + .map(IpAddr::to_string) + .collect(); + + let Some(peer) = self.runtime_peer_update( + peer_label, + peer_pubkey, + allowed_ips, + network_info.is_authorized, + network_info.preshared_key.clone(), + ) else { + return Ok(()); + }; + + self.send_peer_update(peer, update_type) + } + /// Process incoming Gateway events /// /// Main gRPC server uses a shared channel for broadcasting all gateway events @@ -453,33 +521,12 @@ impl GatewayUpdatesHandler { .iter() .find(|info| info.network_id == self.network_id) { - Some(network_info) => { - // FIXME: this shouldn't happen, since when the device is created - // it's impossible for MFA authorization to already be completed - if self.network.mfa_enabled() && !network_info.is_authorized { - debug!( - "Created WireGuard device {} is not authorized to connect to \ - MFA enabled location {}", - device.device.name, self.network.name - ); - continue; - } - self.send_peer_update( - Peer { - pubkey: device.device.wireguard_pubkey, - allowed_ips: network_info - .device_wireguard_ips - .iter() - .map(IpAddr::to_string) - .collect(), - preshared_key: network_info.preshared_key.clone(), - keepalive_interval: Some( - self.network.keepalive_interval.cast_unsigned(), - ), - }, - 0, - ) - } + Some(network_info) => self.send_runtime_device_update( + &device.device.name, + device.device.wireguard_pubkey, + network_info, + 0, + ), None => Ok(()), } } @@ -490,31 +537,12 @@ impl GatewayUpdatesHandler { .iter() .find(|info| info.network_id == self.network_id) { - Some(network_info) => { - if self.network.mfa_enabled() && !network_info.is_authorized { - debug!( - "Modified WireGuard device {} is not authorized to connect to \ - MFA enabled location {}", - device.device.name, self.network.name - ); - continue; - } - self.send_peer_update( - Peer { - pubkey: device.device.wireguard_pubkey, - allowed_ips: network_info - .device_wireguard_ips - .iter() - .map(IpAddr::to_string) - .collect(), - preshared_key: network_info.preshared_key.clone(), - keepalive_interval: Some( - self.network.keepalive_interval.cast_unsigned(), - ), - }, - 1, - ) - } + Some(network_info) => self.send_runtime_device_update( + &device.device.name, + device.device.wireguard_pubkey, + network_info, + 1, + ), None => Ok(()), } } @@ -560,31 +588,23 @@ impl GatewayUpdatesHandler { continue; } - // FIXME: at this point the device authorization should already have been verified - if self.network.mfa_enabled() && !network_device.is_authorized { - debug!( - "Created WireGuard device {} is not authorized to connect to \ - MFA enabled location {}", - device.name, self.network.name - ); + let allowed_ips = network_device + .wireguard_ips + .iter() + .map(IpAddr::to_string) + .collect(); + + let Some(peer) = self.runtime_peer_update( + &device.name, + device.wireguard_pubkey, + allowed_ips, + true, + network_device.preshared_key.clone(), + ) else { continue; - } + }; - self.send_peer_update( - Peer { - pubkey: device.wireguard_pubkey, - allowed_ips: network_device - .wireguard_ips - .iter() - .map(IpAddr::to_string) - .collect(), - preshared_key: network_device.preshared_key.clone(), - keepalive_interval: Some( - self.network.keepalive_interval.cast_unsigned(), - ), - }, - 0, - ) + self.send_peer_update(peer, 0) } else { Ok(()) } @@ -810,3 +830,101 @@ fn gen_config( fwmark: network.fwmark as u32, } } + +#[cfg(test)] +mod tests { + use tokio::sync::{broadcast, mpsc::unbounded_channel}; + + use super::GatewayUpdatesHandler; + use defguard_common::db::{ + Id, + models::wireguard::{LocationMfaMode, ServiceLocationMode, WireguardNetwork}, + }; + + fn test_network(location_mfa_mode: LocationMfaMode) -> WireguardNetwork { + let defaults = WireguardNetwork::default(); + + WireguardNetwork { + id: 1, + name: "test-network".into(), + address: defaults.address, + port: 51820, + pubkey: "network-pubkey".into(), + prvkey: "network-prvkey".into(), + endpoint: "127.0.0.1".into(), + dns: None, + mtu: 1420, + fwmark: 0, + allowed_ips: Vec::new(), + allow_all_groups: true, + connected_at: None, + keepalive_interval: 25, + peer_disconnect_threshold: 300, + acl_enabled: false, + acl_default_allow: false, + location_mfa_mode, + service_location_mode: ServiceLocationMode::Disabled, + } + } + + fn test_handler(location_mfa_mode: LocationMfaMode) -> GatewayUpdatesHandler { + let network = test_network(location_mfa_mode); + let (events_tx, events_rx) = broadcast::channel(1); + let (tx, _rx) = unbounded_channel(); + drop(events_tx); + + GatewayUpdatesHandler::new(network.id, network, "gateway".into(), events_rx, tx) + } + + #[test] + fn test_runtime_peer_update_strips_preshared_key_for_non_mfa_locations() { + let handler = test_handler(LocationMfaMode::Disabled); + + let peer = handler + .runtime_peer_update( + "device", + "device-pubkey".into(), + vec!["10.1.1.2".into()], + true, + Some("legacy-psk".into()), + ) + .unwrap(); + + assert_eq!(peer.pubkey, "device-pubkey"); + assert_eq!(peer.allowed_ips, vec!["10.1.1.2"]); + assert_eq!(peer.preshared_key, None); + assert_eq!(peer.keepalive_interval, Some(25)); + } + + #[test] + fn test_runtime_peer_update_skips_authorized_mfa_peer_without_session_preshared_key() { + let handler = test_handler(LocationMfaMode::Internal); + + let peer = handler.runtime_peer_update( + "device", + "device-pubkey".into(), + vec!["10.1.1.2".into()], + true, + None, + ); + + assert_eq!(peer, None); + } + + #[test] + fn test_runtime_peer_update_preserves_session_preshared_key_for_authorized_mfa_peer() { + let handler = test_handler(LocationMfaMode::Internal); + + let peer = handler + .runtime_peer_update( + "device", + "device-pubkey".into(), + vec!["10.1.1.2".into()], + true, + Some("session-psk".into()), + ) + .unwrap(); + + assert_eq!(peer.preshared_key, Some("session-psk".into())); + } +} diff --git a/crates/defguard_session_manager/tests/common/mod.rs b/crates/defguard_session_manager/tests/common/mod.rs index 8c6789749b..6db5d27cf1 100644 --- a/crates/defguard_session_manager/tests/common/mod.rs +++ b/crates/defguard_session_manager/tests/common/mod.rs @@ -281,7 +281,8 @@ pub(crate) async fn create_session( mfa_method: Option, preshared_key: Option<&str>, ) -> VpnClientSession { - let mut session = VpnClientSession::new(location_id, user_id, device_id, connected_at, mfa_method); + let mut session = + VpnClientSession::new(location_id, user_id, device_id, connected_at, mfa_method); session.preshared_key = preshared_key.map(str::to_owned); session .save(pool) From 32e266c7efc2bc59b5accaac5dfc214407961143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 18 Mar 2026 11:27:47 +0100 Subject: [PATCH 05/19] event cleanup --- .../defguard_common/src/db/models/device.rs | 19 ++++++++++ crates/defguard_core/src/grpc/mod.rs | 4 +-- .../src/grpc/proxy/client_mfa.rs | 35 +++++++------------ .../defguard_gateway_manager/src/handler.rs | 33 +++++++---------- 4 files changed, 46 insertions(+), 45 deletions(-) diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index 1a02a0cf9f..8ef8a53c8a 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -155,6 +155,25 @@ pub struct DeviceNetworkInfo { pub is_authorized: bool, } +impl DeviceNetworkInfo { + #[must_use] + pub fn from_authorized_mfa_session( + network_id: Id, + device_wireguard_ips: I, + preshared_key: String, + ) -> Self + where + I: Into>, + { + Self { + network_id, + device_wireguard_ips: device_wireguard_ips.into(), + preshared_key: Some(preshared_key), + is_authorized: true, + } + } +} + impl DeviceInfo { pub async fn from_device<'e, E>(executor: E, device: Device) -> Result where diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index beb66fd8a1..204b836717 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -12,7 +12,7 @@ use defguard_common::{ Id, models::{ Device, Settings, WireguardNetwork, - device::{DeviceInfo, WireguardNetworkDevice}, + device::{DeviceInfo, DeviceNetworkInfo}, wireguard::ServiceLocationMode, }, }, @@ -229,7 +229,7 @@ pub enum GatewayEvent { DeviceDeleted(DeviceInfo), FirewallConfigChanged(Id, FirewallConfig), FirewallDisabled(Id), - MfaSessionAuthorized(Id, Device, WireguardNetworkDevice), + MfaSessionAuthorized(Id, Device, DeviceNetworkInfo), MfaSessionDisconnected(Id, Device), } diff --git a/crates/defguard_core/src/grpc/proxy/client_mfa.rs b/crates/defguard_core/src/grpc/proxy/client_mfa.rs index cd88594d73..65fd4f0b7a 100644 --- a/crates/defguard_core/src/grpc/proxy/client_mfa.rs +++ b/crates/defguard_core/src/grpc/proxy/client_mfa.rs @@ -11,7 +11,7 @@ use defguard_common::{ Id, models::{ BiometricAuth, BiometricChallenge, Device, User, WireguardNetwork, - device::WireguardNetworkDevice, + device::{DeviceNetworkInfo, WireguardNetworkDevice}, vpn_client_session::{VpnClientMfaMethod, VpnClientSession, VpnClientSessionState}, wireguard::LocationMfaMode, }, @@ -79,21 +79,15 @@ pub struct ClientMfaServer { } impl ClientMfaServer { - fn build_mfa_authorized_gateway_network_device( + fn build_mfa_authorized_gateway_network_info( network_device: WireguardNetworkDevice, - vpn_client_session: &VpnClientSession, preshared_key: String, - ) -> WireguardNetworkDevice { - WireguardNetworkDevice { - wireguard_network_id: network_device.wireguard_network_id, - wireguard_ips: network_device.wireguard_ips, - device_id: network_device.device_id, - preshared_key: Some(preshared_key), - is_authorized: true, - authorized_at: vpn_client_session - .connected_at - .or(Some(vpn_client_session.created_at)), - } + ) -> DeviceNetworkInfo { + DeviceNetworkInfo::from_authorized_mfa_session( + network_device.wireguard_network_id, + network_device.wireguard_ips, + preshared_key, + ) } #[must_use] @@ -689,30 +683,27 @@ impl ClientMfaServer { // create new VPN client session let vpn_client_session = self.create_new_mfa_session( - &mut transaction, + &mut transaction, &location, &user, &device, method.into(), key.public.clone(), ) - .await + .await .map_err(|err| { error!("Failed to create new VPN client session for device {device} in location {location}: {err}"); Status::internal("unexpected error") })?; debug!("Created new VPN client session: {vpn_client_session:?}"); - let gateway_network_device = Self::build_mfa_authorized_gateway_network_device( - network_device, - &vpn_client_session, - key.public.clone(), - ); + let gateway_network_info = + Self::build_mfa_authorized_gateway_network_info(network_device, key.public.clone()); // send gateway event debug!("Sending `peer_create` message to gateway"); let event = - GatewayEvent::MfaSessionAuthorized(location.id, device.clone(), gateway_network_device); + GatewayEvent::MfaSessionAuthorized(location.id, device.clone(), gateway_network_info); self.wireguard_tx.send(event).map_err(|err| { error!("Error sending WireGuard event: {err}"); Status::internal("unexpected error") diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index 18f34e1d7f..a28844b942 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -13,7 +13,10 @@ use defguard_common::{ VERSION, db::{ Id, - models::{Settings, WireguardNetwork, gateway::Gateway, wireguard::DEFAULT_WIREGUARD_MTU}, + models::{ + DeviceNetworkInfo, Settings, WireguardNetwork, gateway::Gateway, + wireguard::DEFAULT_WIREGUARD_MTU, + }, }, messages::peer_stats_update::PeerStatsUpdate, }; @@ -450,7 +453,7 @@ impl GatewayUpdatesHandler { &self, peer_label: &str, peer_pubkey: String, - network_info: &defguard_common::db::models::DeviceNetworkInfo, + network_info: &DeviceNetworkInfo, update_type: i32, ) -> Result<(), Status> { let allowed_ips = network_info @@ -578,33 +581,21 @@ impl GatewayUpdatesHandler { Ok(()) } } - GatewayEvent::MfaSessionAuthorized(location_id, device, network_device) => { + GatewayEvent::MfaSessionAuthorized(location_id, device, network_info) => { if location_id == self.network_id { - // validate that network info is for the correct location - if network_device.wireguard_network_id != location_id { + if network_info.network_id != location_id { error!( - "Received MFA authorization success event for location {location_id} with invalid device config: {network_device:?}" + "Received MFA authorization success event for location {location_id} with invalid runtime network info: {network_info:?}" ); continue; } - let allowed_ips = network_device - .wireguard_ips - .iter() - .map(IpAddr::to_string) - .collect(); - - let Some(peer) = self.runtime_peer_update( + self.send_runtime_device_update( &device.name, device.wireguard_pubkey, - allowed_ips, - true, - network_device.preshared_key.clone(), - ) else { - continue; - }; - - self.send_peer_update(peer, 0) + &network_info, + 0, + ) } else { Ok(()) } From 41d3cb3d3b3c7f60f9450e12925350e235455797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 18 Mar 2026 12:10:00 +0100 Subject: [PATCH 06/19] drop legacy fields --- .../defguard_common/src/db/models/device.rs | 51 +++++-------------- .../src/enterprise/firewall/tests/gh1868.rs | 3 -- .../src/enterprise/firewall/tests/mod.rs | 30 ----------- .../src/location_management/allowed_peers.rs | 8 +-- ..._vpn_client_session_preshared_key.down.sql | 7 ++- ...0]_vpn_client_session_preshared_key.up.sql | 7 ++- 6 files changed, 27 insertions(+), 79 deletions(-) diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index 8ef8a53c8a..8a3a77bbec 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -184,12 +184,9 @@ impl DeviceInfo { DeviceNetworkInfo, "SELECT wnd.wireguard_network_id network_id, \ wnd.wireguard_ips \"device_wireguard_ips: Vec\", \ + active_session.preshared_key preshared_key, \ CASE \ - WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN wnd.preshared_key \ - ELSE active_session.preshared_key \ - END preshared_key, \ - CASE \ - WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN wnd.is_authorized \ + WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN TRUE \ ELSE active_session.id IS NOT NULL \ END \"is_authorized!\" \ FROM wireguard_network_device wnd \ @@ -314,9 +311,6 @@ pub struct WireguardNetworkDevice { pub wireguard_network_id: Id, pub wireguard_ips: Vec, pub device_id: Id, - pub preshared_key: Option, - pub is_authorized: bool, - pub authorized_at: Option, } #[derive(Debug, Deserialize, Serialize, ToSchema)] @@ -355,7 +349,7 @@ impl WireguardNetworkDevice { active_session: Option<&VpnClientSession>, ) -> DeviceNetworkInfo { let (preshared_key, is_authorized) = if !network.mfa_enabled() { - (self.preshared_key.clone(), self.is_authorized) + (None, true) } else if let Some(session) = active_session { (session.preshared_key.clone(), true) } else { @@ -392,9 +386,6 @@ impl WireguardNetworkDevice { wireguard_network_id: network_id, wireguard_ips: wireguard_ips.into(), device_id, - preshared_key: None, - is_authorized: false, - authorized_at: None, } } @@ -412,17 +403,13 @@ impl WireguardNetworkDevice { { query!( "INSERT INTO wireguard_network_device \ - (device_id, wireguard_network_id, wireguard_ips, is_authorized, authorized_at, \ - preshared_key) \ - VALUES ($1, $2, $3, $4, $5, $6) \ + (device_id, wireguard_network_id, wireguard_ips) \ + VALUES ($1, $2, $3) \ ON CONFLICT ON CONSTRAINT device_network \ - DO UPDATE SET wireguard_ips = $3, is_authorized = $4", + DO UPDATE SET wireguard_ips = $3", self.device_id, self.wireguard_network_id, &self.ips_as_network(), - self.is_authorized, - self.authorized_at, - self.preshared_key, ) .execute(executor) .await?; @@ -436,14 +423,11 @@ impl WireguardNetworkDevice { { query!( "UPDATE wireguard_network_device \ - SET wireguard_ips = $3, is_authorized = $4, authorized_at = $5, preshared_key = $6 \ + SET wireguard_ips = $3 \ WHERE device_id = $1 AND wireguard_network_id = $2", self.device_id, self.wireguard_network_id, &self.ips_as_network(), - self.is_authorized, - self.authorized_at, - self.preshared_key, ) .execute(executor) .await?; @@ -478,8 +462,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ips \"wireguard_ips: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device \ WHERE device_id = $1 AND wireguard_network_id = $2", device_id, @@ -500,8 +483,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ips \"wireguard_ips: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device \ WHERE device_id = $1 ORDER BY id LIMIT 1", device_id @@ -522,8 +504,7 @@ impl WireguardNetworkDevice { let result = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ips \"wireguard_ips: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device WHERE device_id = $1", device_id ) @@ -544,8 +525,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ips \"wireguard_ips: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device \ WHERE wireguard_network_id = $1", network_id @@ -570,8 +550,7 @@ impl WireguardNetworkDevice { let res = query_as!( Self, "SELECT device_id, wireguard_network_id, \ - wireguard_ips \"wireguard_ips: Vec\", \ - preshared_key, is_authorized, authorized_at \ + wireguard_ips \"wireguard_ips: Vec\" \ FROM wireguard_network_device \ WHERE wireguard_network_id = $1 AND device_id IN \ (SELECT id FROM device WHERE user_id = $2 AND device_type = 'user'::device_type)", @@ -1533,9 +1512,6 @@ mod test { wireguard_network_id: network.id, wireguard_ips: vec![IpAddr::from_str("10.1.1.2").unwrap()], device_id: 1, - preshared_key: Some("legacy-psk".into()), - is_authorized: false, - authorized_at: None, }; let active_session = VpnClientSession { id: 1, @@ -1595,12 +1571,11 @@ mod test { network.try_set_address("10.1.1.1/24").unwrap(); let network = network.save(&pool).await.unwrap(); - let mut wireguard_network_device = WireguardNetworkDevice::new( + let wireguard_network_device = WireguardNetworkDevice::new( network.id, device.id, [IpAddr::from_str("10.1.1.2").unwrap()], ); - wireguard_network_device.preshared_key = Some("legacy-psk".into()); wireguard_network_device.insert(&pool).await.unwrap(); VpnClientSession::new(network.id, user.id, device.id, None, None) diff --git a/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs index 24ee604d2c..50320efc9d 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs @@ -67,9 +67,6 @@ async fn setup_user_and_device( device_id: device.id, wireguard_network_id: location.id, wireguard_ips, - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(pool).await.unwrap(); } diff --git a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs index c8117ac25a..edd4972c7e 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs @@ -130,9 +130,6 @@ async fn create_test_users_and_devices( device_id: device.id, wireguard_network_id: location.id, wireguard_ips, - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(pool).await.unwrap(); } @@ -292,9 +289,6 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO user.id as u8, device_num as u8, ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -391,9 +385,6 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO device_id, wireguard_network_id: location.id, wireguard_ips: vec![ip], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -720,9 +711,6 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO user.id as u16, device_num as u16, ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -819,9 +807,6 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO device_id, wireguard_network_id: location.id, wireguard_ips: vec![ip], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -1179,9 +1164,6 @@ async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: P device_num as u16, )), ], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -1287,9 +1269,6 @@ async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: P device_id, wireguard_network_id: location.id, wireguard_ips: ips, - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } @@ -2204,9 +2183,6 @@ async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgCon user.id as u8, device_num as u8, ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); let network_device = WireguardNetworkDevice { @@ -2222,9 +2198,6 @@ async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgCon user.id as u16, device_num as u16, ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); let network_device = WireguardNetworkDevice { @@ -2243,9 +2216,6 @@ async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgCon device_num as u16, )), ], - preshared_key: None, - is_authorized: true, - authorized_at: None, }; network_device.insert(&pool).await.unwrap(); } diff --git a/crates/defguard_core/src/location_management/allowed_peers.rs b/crates/defguard_core/src/location_management/allowed_peers.rs index e2797a2c79..81f4c20555 100644 --- a/crates/defguard_core/src/location_management/allowed_peers.rs +++ b/crates/defguard_core/src/location_management/allowed_peers.rs @@ -29,10 +29,7 @@ where let rows = query!( "SELECT d.wireguard_pubkey pubkey, \ - CASE \ - WHEN $2 THEN active_session.preshared_key \ - ELSE wnd.preshared_key \ - END preshared_key, \ + active_session.preshared_key preshared_key, \ -- TODO possible to not use ARRAY-unnest here? ARRAY( SELECT host(ip) @@ -278,12 +275,11 @@ mod test { network.try_set_address("10.4.1.1/24").unwrap(); let network = network.save(&pool).await.unwrap(); - let mut network_device = WireguardNetworkDevice::new( + let network_device = WireguardNetworkDevice::new( network.id, device.id, vec![IpAddr::from_str("10.4.1.2").unwrap()], ); - network_device.preshared_key = Some("legacy-psk".into()); network_device.insert(&pool).await.unwrap(); VpnClientSession::new(network.id, user.id, device.id, None, None) diff --git a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql index 1effb86459..b7bd32d944 100644 --- a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql +++ b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql @@ -1,4 +1,9 @@ --- Preserve unmatched legacy values during rollback; refresh only rows with canonical active session data. +ALTER TABLE wireguard_network_device + ADD COLUMN preshared_key text NULL, + ADD COLUMN is_authorized bool NOT NULL DEFAULT false, + ADD COLUMN authorized_at timestamp without time zone NULL; + +-- Restore legacy preshared keys when canonical active session data exists; unmatched rows stay NULL. UPDATE wireguard_network_device AS network_device SET preshared_key = latest_active_session.preshared_key FROM ( diff --git a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql index 2bd78db04e..ad48ba35b7 100644 --- a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql +++ b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql @@ -1,2 +1,7 @@ --- Transitional additive step: add session-level preshared_key now; application rollout moves data later. +-- Transitional squash: add session-level preshared_key and remove legacy device-level MFA state. ALTER TABLE vpn_client_session ADD COLUMN preshared_key text NULL; + +ALTER TABLE wireguard_network_device + DROP COLUMN preshared_key, + DROP COLUMN is_authorized, + DROP COLUMN authorized_at; From cb57469e846208262a5d148c0ad19ba992191a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 18 Mar 2026 12:27:48 +0100 Subject: [PATCH 07/19] update query data --- ...14c32c219b73f3afa358f838cb833d5544842.json | 19 ---- ...0f7be1914de4263c8db6d16110d7e75d6bb15.json | 35 ------ ...6841ab88a9361a52129ef199768e900dd12ad.json | 16 +++ ...125ce288466a847cf58b3e9be7659e7360933.json | 19 ---- ...17c9555d0350d03679d9cf3b7ccc6451c7ae.json} | 7 +- ...1cd3eab7aad7a61ee274b7efea9e25f0b6766.json | 40 ------- ...b5f8fa8c6ab81da002d0d1ddb5445e7c6d31.json} | 4 +- ...e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json | 52 --------- ...45938371b3f39822c4ff68a1a9515767e0ad.json} | 12 ++- ...9703bcc3163f4935898d0988a6045c29e7dd8.json | 35 ------ ...5da77a32a11145804dff5692df7c3d74ce7b.json} | 12 ++- ...633fafa49b98d2fd958fcafc66c4cd624ec0.json} | 24 +---- ...c6a9b3fa7d140b30bfee7657e3eacf6de6aa.json} | 12 ++- ...a7d8596d6499193933c58fa1d7ba0a8f445bd.json | 40 ------- ...9cafa2c1e5161eef7c5f35a7705be21235087.json | 53 --------- ...724039e1a281dd4d3122b5bb8e1cb499070b0.json | 16 +++ ...aa1f93553237be1c6767564c2499b9d8f67d.json} | 7 +- ...e0f094c419e60e3f8d8bba86f9df5b7d4c9f.json} | 12 ++- ...5413d1dd6ce5a24b87b9fc4060dd1ae704be.json} | 12 ++- ...528dccc0eaa54d3adbaa7e4a336c992a974d.json} | 24 +---- ...2473bfce4408e941822dd782bab65fed9ed6b.json | 35 ++++++ ...d6051e67990518cfcf9f6d1889548b3ab9b4.json} | 25 +---- ...3ee77d9cb3c80d8028d99bcc08ccd4c78dd0.json} | 12 ++- ...90aee96054585a87e6673a5efa275795dc060.json | 101 ++++++++++++++++++ ...4ac34a15e6026f3d571bff9266065b04c14c0.json | 34 ++++++ ...3ef3e8928bea54bd8407fccf74b868f6059b2.json | 40 +++++++ ...1a3ab49a5a2f9fd81eed714678ec272718ea4.json | 34 ++++++ 27 files changed, 350 insertions(+), 382 deletions(-) delete mode 100644 .sqlx/query-09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842.json delete mode 100644 .sqlx/query-0b00683559108a14bf73ce9f6430f7be1914de4263c8db6d16110d7e75d6bb15.json create mode 100644 .sqlx/query-153fa42e3a61b24b7a264d9ef236841ab88a9361a52129ef199768e900dd12ad.json delete mode 100644 .sqlx/query-20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933.json rename .sqlx/{query-dd0190b514cf61ba7c444d14f9c47bda3869b74fcb598ec6f2cf8352424f87d6.json => query-32b5d4d820a72da19cbd3bb1a33e17c9555d0350d03679d9cf3b7ccc6451c7ae.json} (83%) delete mode 100644 .sqlx/query-3db9abdf258314698fe189960921cd3eab7aad7a61ee274b7efea9e25f0b6766.json rename .sqlx/{query-f64a22a8141030a41aa9b85275158a3a75e5ebfa0f14c84b1aefc8582156eb03.json => query-3ea2a5fbb1ec0dc86448b6145cf8b5f8fa8c6ab81da002d0d1ddb5445e7c6d31.json} (91%) delete mode 100644 .sqlx/query-469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json rename .sqlx/{query-73965013a68538139aadcf0c2346e315923c12c54b9446c7b18e4b4fb1791f42.json => query-4b05abebeafeda2f88fff48f6d9d45938371b3f39822c4ff68a1a9515767e0ad.json} (85%) delete mode 100644 .sqlx/query-54fada56be8b91633550c77f7259703bcc3163f4935898d0988a6045c29e7dd8.json rename .sqlx/{query-45e1c056d0868dd8072abd5cdab00c42e268149b730b7e4f2add5cbdbe7843c8.json => query-5a856149fa68d294e5a15ceacdfc5da77a32a11145804dff5692df7c3d74ce7b.json} (82%) rename .sqlx/{query-748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475.json => query-6363977f3e290a8ee991c9442ab9633fafa49b98d2fd958fcafc66c4cd624ec0.json} (50%) rename .sqlx/{query-86b6f0f850b4b8436379a9da38c0280867b8f432fc4bd52ba7194a5895540b4e.json => query-675fe2562e81e9886d488ce639a8c6a9b3fa7d140b30bfee7657e3eacf6de6aa.json} (85%) delete mode 100644 .sqlx/query-72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd.json delete mode 100644 .sqlx/query-812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087.json create mode 100644 .sqlx/query-8aa6ed2a7f4069cd6e0e0176ce4724039e1a281dd4d3122b5bb8e1cb499070b0.json rename .sqlx/{query-51dffd9c1ae018de2ade5fde93ae829ad4a5825707e8b2f55b86801039cd784e.json => query-973c64873ac510cc407d43708efaaa1f93553237be1c6767564c2499b9d8f67d.json} (86%) rename .sqlx/{query-2bc56fca85ec693d2d53fe08e53143fef55fb878076487fc97738f120573820f.json => query-9cd4fb6b8bb2f231d136f7e02bb9e0f094c419e60e3f8d8bba86f9df5b7d4c9f.json} (83%) rename .sqlx/{query-743a5ec9c0ef4e1f92465ad287dc211dedd7b66f89cdf11b36e4a5d7306258be.json => query-a2a31a9e9d53d830f658131eab155413d1dd6ce5a24b87b9fc4060dd1ae704be.json} (77%) rename .sqlx/{query-f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3.json => query-b1c28cf7a7c919c5951dfc050a44528dccc0eaa54d3adbaa7e4a336c992a974d.json} (50%) create mode 100644 .sqlx/query-bc69afce4a51d831b436e2ea4d82473bfce4408e941822dd782bab65fed9ed6b.json rename .sqlx/{query-98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f.json => query-d3bfcc8f183cf03b369c7defab29d6051e67990518cfcf9f6d1889548b3ab9b4.json} (50%) rename .sqlx/{query-2700bf01e6a2afbe3ac2a4b686f715ad1262d5eb8cdf76cf4234b6a4971e6769.json => query-d86d5f9cb508b1840f0de3c40a993ee77d9cb3c80d8028d99bcc08ccd4c78dd0.json} (82%) create mode 100644 .sqlx/query-e87af2b4e3fd79709a28381e04690aee96054585a87e6673a5efa275795dc060.json create mode 100644 .sqlx/query-eec3959fe7c4cbfc4ec5215f1664ac34a15e6026f3d571bff9266065b04c14c0.json create mode 100644 .sqlx/query-febd6a2dc4a335cc25d730d3e823ef3e8928bea54bd8407fccf74b868f6059b2.json create mode 100644 .sqlx/query-fed5b29a44329968a6f134990261a3ab49a5a2f9fd81eed714678ec272718ea4.json diff --git a/.sqlx/query-09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842.json b/.sqlx/query-09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842.json deleted file mode 100644 index 032ed79111..0000000000 --- a/.sqlx/query-09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO wireguard_network_device (device_id, wireguard_network_id, wireguard_ips, is_authorized, authorized_at, preshared_key) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT ON CONSTRAINT device_network DO UPDATE SET wireguard_ips = $3, is_authorized = $4", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "InetArray", - "Bool", - "Timestamp", - "Text" - ] - }, - "nullable": [] - }, - "hash": "09b6f2fc7ec101117a99f85a64314c32c219b73f3afa358f838cb833d5544842" -} diff --git a/.sqlx/query-0b00683559108a14bf73ce9f6430f7be1914de4263c8db6d16110d7e75d6bb15.json b/.sqlx/query-0b00683559108a14bf73ce9f6430f7be1914de4263c8db6d16110d7e75d6bb15.json deleted file mode 100644 index 3070bf48a3..0000000000 --- a/.sqlx/query-0b00683559108a14bf73ce9f6430f7be1914de4263c8db6d16110d7e75d6bb15.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey pubkey, CASE WHEN $2 THEN active_session.preshared_key ELSE wnd.preshared_key END preshared_key, -- TODO possible to not use ARRAY-unnest here?\n ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wireguard_network_id = $1 AND (NOT $2 OR active_session.id IS NOT NULL) AND d.configured AND u.is_active ORDER BY d.id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "pubkey", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "allowed_ips!: Vec", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [ - "Int8", - "Bool" - ] - }, - "nullable": [ - false, - null, - null - ] - }, - "hash": "0b00683559108a14bf73ce9f6430f7be1914de4263c8db6d16110d7e75d6bb15" -} diff --git a/.sqlx/query-153fa42e3a61b24b7a264d9ef236841ab88a9361a52129ef199768e900dd12ad.json b/.sqlx/query-153fa42e3a61b24b7a264d9ef236841ab88a9361a52129ef199768e900dd12ad.json new file mode 100644 index 0000000000..356dbc859a --- /dev/null +++ b/.sqlx/query-153fa42e3a61b24b7a264d9ef236841ab88a9361a52129ef199768e900dd12ad.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO wireguard_network_device (device_id, wireguard_network_id, wireguard_ips) VALUES ($1, $2, $3) ON CONFLICT ON CONSTRAINT device_network DO UPDATE SET wireguard_ips = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "InetArray" + ] + }, + "nullable": [] + }, + "hash": "153fa42e3a61b24b7a264d9ef236841ab88a9361a52129ef199768e900dd12ad" +} diff --git a/.sqlx/query-20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933.json b/.sqlx/query-20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933.json deleted file mode 100644 index f8ce8a0666..0000000000 --- a/.sqlx/query-20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE wireguard_network_device SET wireguard_ips = $3, is_authorized = $4, authorized_at = $5, preshared_key = $6 WHERE device_id = $1 AND wireguard_network_id = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "InetArray", - "Bool", - "Timestamp", - "Text" - ] - }, - "nullable": [] - }, - "hash": "20efd0ac76bd8a6ca51dd31ff89125ce288466a847cf58b3e9be7659e7360933" -} diff --git a/.sqlx/query-dd0190b514cf61ba7c444d14f9c47bda3869b74fcb598ec6f2cf8352424f87d6.json b/.sqlx/query-32b5d4d820a72da19cbd3bb1a33e17c9555d0350d03679d9cf3b7ccc6451c7ae.json similarity index 83% rename from .sqlx/query-dd0190b514cf61ba7c444d14f9c47bda3869b74fcb598ec6f2cf8352424f87d6.json rename to .sqlx/query-32b5d4d820a72da19cbd3bb1a33e17c9555d0350d03679d9cf3b7ccc6451c7ae.json index 238f7541aa..35150a6a35 100644 --- a/.sqlx/query-dd0190b514cf61ba7c444d14f9c47bda3869b74fcb598ec6f2cf8352424f87d6.json +++ b/.sqlx/query-32b5d4d820a72da19cbd3bb1a33e17c9555d0350d03679d9cf3b7ccc6451c7ae.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"vpn_client_session\" (\"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\",\"state\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", + "query": "INSERT INTO \"vpn_client_session\" (\"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\",\"state\",\"preshared_key\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING id", "describe": { "columns": [ { @@ -42,12 +42,13 @@ ] } } - } + }, + "Text" ] }, "nullable": [ false ] }, - "hash": "dd0190b514cf61ba7c444d14f9c47bda3869b74fcb598ec6f2cf8352424f87d6" + "hash": "32b5d4d820a72da19cbd3bb1a33e17c9555d0350d03679d9cf3b7ccc6451c7ae" } diff --git a/.sqlx/query-3db9abdf258314698fe189960921cd3eab7aad7a61ee274b7efea9e25f0b6766.json b/.sqlx/query-3db9abdf258314698fe189960921cd3eab7aad7a61ee274b7efea9e25f0b6766.json deleted file mode 100644 index d4bdf28bd4..0000000000 --- a/.sqlx/query-3db9abdf258314698fe189960921cd3eab7aad7a61ee274b7efea9e25f0b6766.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT wnd.wireguard_network_id network_id, wnd.wireguard_ips \"device_wireguard_ips: Vec\", CASE WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN wnd.preshared_key ELSE active_session.preshared_key END preshared_key, CASE WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN wnd.is_authorized ELSE active_session.id IS NOT NULL END \"is_authorized!\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wnd.device_id = $1 ORDER BY wnd.wireguard_network_id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "network_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "device_wireguard_ips: Vec", - "type_info": "InetArray" - }, - { - "ordinal": 2, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "is_authorized!", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - null, - null - ] - }, - "hash": "3db9abdf258314698fe189960921cd3eab7aad7a61ee274b7efea9e25f0b6766" -} diff --git a/.sqlx/query-f64a22a8141030a41aa9b85275158a3a75e5ebfa0f14c84b1aefc8582156eb03.json b/.sqlx/query-3ea2a5fbb1ec0dc86448b6145cf8b5f8fa8c6ab81da002d0d1ddb5445e7c6d31.json similarity index 91% rename from .sqlx/query-f64a22a8141030a41aa9b85275158a3a75e5ebfa0f14c84b1aefc8582156eb03.json rename to .sqlx/query-3ea2a5fbb1ec0dc86448b6145cf8b5f8fa8c6ab81da002d0d1ddb5445e7c6d31.json index 72e7fb3e2e..cb1765ce7d 100644 --- a/.sqlx/query-f64a22a8141030a41aa9b85275158a3a75e5ebfa0f14c84b1aefc8582156eb03.json +++ b/.sqlx/query-3ea2a5fbb1ec0dc86448b6145cf8b5f8fa8c6ab81da002d0d1ddb5445e7c6d31.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT n.id network_id, n.name network_name, n.endpoint gateway_endpoint, wnd.wireguard_ips \"device_wireguard_ips: Vec\", vs.endpoint \"device_endpoint?\", vs.latest_handshake \"latest_handshake?\", vs.state \"state?: VpnClientSessionState\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN LATERAL ( SELECT id, state, location_id, endpoint, latest_handshake FROM vpn_client_session LEFT JOIN LATERAL ( SELECT session_id, endpoint, latest_handshake FROM vpn_session_stats WHERE session_id = vpn_client_session.id ORDER BY collected_at DESC LIMIT 1 ) vss ON vss.session_id = vpn_client_session.id WHERE location_id = n.id and device_id = $1 ORDER BY created_at DESC LIMIT 1 ) vs ON vs.location_id = n.id WHERE wnd.device_id = $1", + "query": "SELECT n.id network_id, n.name network_name, n.endpoint gateway_endpoint, wnd.wireguard_ips \"device_wireguard_ips: Vec\", vs.endpoint \"device_endpoint?\", vs.latest_handshake \"latest_handshake?\", vs.state \"state?: VpnClientSessionState\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN LATERAL ( SELECT id, state, location_id, endpoint, latest_handshake FROM vpn_client_session LEFT JOIN LATERAL ( SELECT session_id, endpoint, latest_handshake FROM vpn_session_stats WHERE session_id = vpn_client_session.id ORDER BY collected_at DESC LIMIT 1 ) vss ON vss.session_id = vpn_client_session.id WHERE location_id = n.id and device_id = $1 ORDER BY created_at DESC, id DESC LIMIT 1 ) vs ON vs.location_id = n.id WHERE wnd.device_id = $1", "describe": { "columns": [ { @@ -65,5 +65,5 @@ false ] }, - "hash": "f64a22a8141030a41aa9b85275158a3a75e5ebfa0f14c84b1aefc8582156eb03" + "hash": "3ea2a5fbb1ec0dc86448b6145cf8b5f8fa8c6ab81da002d0d1ddb5445e7c6d31" } diff --git a/.sqlx/query-469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json b/.sqlx/query-469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json deleted file mode 100644 index 0ba89d9c7a..0000000000 --- a/.sqlx/query-469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "device_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "wireguard_network_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "wireguard_ips: Vec", - "type_info": "InetArray" - }, - { - "ordinal": 3, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_authorized", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "authorized_at", - "type_info": "Timestamp" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - true - ] - }, - "hash": "469bf9a1de598cac208a943d0e6e32c9ebb2231dd6da663cdeaaf29b93afc8ac" -} diff --git a/.sqlx/query-73965013a68538139aadcf0c2346e315923c12c54b9446c7b18e4b4fb1791f42.json b/.sqlx/query-4b05abebeafeda2f88fff48f6d9d45938371b3f39822c4ff68a1a9515767e0ad.json similarity index 85% rename from .sqlx/query-73965013a68538139aadcf0c2346e315923c12c54b9446c7b18e4b4fb1791f42.json rename to .sqlx/query-4b05abebeafeda2f88fff48f6d9d45938371b3f39822c4ff68a1a9515767e0ad.json index bd6db45ee8..cdbe9ae673 100644 --- a/.sqlx/query-73965013a68538139aadcf0c2346e315923c12c54b9446c7b18e4b4fb1791f42.json +++ b/.sqlx/query-4b05abebeafeda2f88fff48f6d9d45938371b3f39822c4ff68a1a9515767e0ad.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\" \"mfa_method?: _\",\"state\" \"state: _\" FROM \"vpn_client_session\" WHERE id = $1", + "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\" \"mfa_method?: _\",\"state\" \"state: _\",\"preshared_key\" FROM \"vpn_client_session\" WHERE id = $1", "describe": { "columns": [ { @@ -71,6 +71,11 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -87,8 +92,9 @@ true, true, true, - false + false, + true ] }, - "hash": "73965013a68538139aadcf0c2346e315923c12c54b9446c7b18e4b4fb1791f42" + "hash": "4b05abebeafeda2f88fff48f6d9d45938371b3f39822c4ff68a1a9515767e0ad" } diff --git a/.sqlx/query-54fada56be8b91633550c77f7259703bcc3163f4935898d0988a6045c29e7dd8.json b/.sqlx/query-54fada56be8b91633550c77f7259703bcc3163f4935898d0988a6045c29e7dd8.json deleted file mode 100644 index 8ef5321bb0..0000000000 --- a/.sqlx/query-54fada56be8b91633550c77f7259703bcc3163f4935898d0988a6045c29e7dd8.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey pubkey, preshared_key, -- TODO possible to not use ARRAY-unnest here?\n ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id WHERE wireguard_network_id = $1 AND (is_authorized OR NOT $2) AND d.configured AND u.is_active ORDER BY d.id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "pubkey", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "allowed_ips!: Vec", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [ - "Int8", - "Bool" - ] - }, - "nullable": [ - false, - true, - null - ] - }, - "hash": "54fada56be8b91633550c77f7259703bcc3163f4935898d0988a6045c29e7dd8" -} diff --git a/.sqlx/query-45e1c056d0868dd8072abd5cdab00c42e268149b730b7e4f2add5cbdbe7843c8.json b/.sqlx/query-5a856149fa68d294e5a15ceacdfc5da77a32a11145804dff5692df7c3d74ce7b.json similarity index 82% rename from .sqlx/query-45e1c056d0868dd8072abd5cdab00c42e268149b730b7e4f2add5cbdbe7843c8.json rename to .sqlx/query-5a856149fa68d294e5a15ceacdfc5da77a32a11145804dff5692df7c3d74ce7b.json index 5ead8ddc67..9698c37940 100644 --- a/.sqlx/query-45e1c056d0868dd8072abd5cdab00c42e268149b730b7e4f2add5cbdbe7843c8.json +++ b/.sqlx/query-5a856149fa68d294e5a15ceacdfc5da77a32a11145804dff5692df7c3d74ce7b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected')", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key FROM vpn_client_session WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC", "describe": { "columns": [ { @@ -71,6 +71,11 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -88,8 +93,9 @@ true, true, true, - false + false, + true ] }, - "hash": "45e1c056d0868dd8072abd5cdab00c42e268149b730b7e4f2add5cbdbe7843c8" + "hash": "5a856149fa68d294e5a15ceacdfc5da77a32a11145804dff5692df7c3d74ce7b" } diff --git a/.sqlx/query-748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475.json b/.sqlx/query-6363977f3e290a8ee991c9442ab9633fafa49b98d2fd958fcafc66c4cd624ec0.json similarity index 50% rename from .sqlx/query-748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475.json rename to .sqlx/query-6363977f3e290a8ee991c9442ab9633fafa49b98d2fd958fcafc66c4cd624ec0.json index a3db757922..4f920f9865 100644 --- a/.sqlx/query-748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475.json +++ b/.sqlx/query-6363977f3e290a8ee991c9442ab9633fafa49b98d2fd958fcafc66c4cd624ec0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1", "describe": { "columns": [ { @@ -17,21 +17,6 @@ "ordinal": 2, "name": "wireguard_ips: Vec", "type_info": "InetArray" - }, - { - "ordinal": 3, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_authorized", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "authorized_at", - "type_info": "Timestamp" } ], "parameters": { @@ -42,11 +27,8 @@ "nullable": [ false, false, - false, - true, - false, - true + false ] }, - "hash": "748a71bef374acac63add73aca333a3ac6c360be2c949e5567afb8383ec92475" + "hash": "6363977f3e290a8ee991c9442ab9633fafa49b98d2fd958fcafc66c4cd624ec0" } diff --git a/.sqlx/query-86b6f0f850b4b8436379a9da38c0280867b8f432fc4bd52ba7194a5895540b4e.json b/.sqlx/query-675fe2562e81e9886d488ce639a8c6a9b3fa7d140b30bfee7657e3eacf6de6aa.json similarity index 85% rename from .sqlx/query-86b6f0f850b4b8436379a9da38c0280867b8f432fc4bd52ba7194a5895540b4e.json rename to .sqlx/query-675fe2562e81e9886d488ce639a8c6a9b3fa7d140b30bfee7657e3eacf6de6aa.json index ac7a8d312f..5583f37d4e 100644 --- a/.sqlx/query-86b6f0f850b4b8436379a9da38c0280867b8f432fc4bd52ba7194a5895540b4e.json +++ b/.sqlx/query-675fe2562e81e9886d488ce639a8c6a9b3fa7d140b30bfee7657e3eacf6de6aa.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\" \"mfa_method?: _\",\"state\" \"state: _\" FROM \"vpn_client_session\"", + "query": "SELECT id, \"location_id\",\"user_id\",\"device_id\",\"created_at\",\"connected_at\",\"disconnected_at\",\"mfa_method\" \"mfa_method?: _\",\"state\" \"state: _\",\"preshared_key\" FROM \"vpn_client_session\"", "describe": { "columns": [ { @@ -71,6 +71,11 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -85,8 +90,9 @@ true, true, true, - false + false, + true ] }, - "hash": "86b6f0f850b4b8436379a9da38c0280867b8f432fc4bd52ba7194a5895540b4e" + "hash": "675fe2562e81e9886d488ce639a8c6a9b3fa7d140b30bfee7657e3eacf6de6aa" } diff --git a/.sqlx/query-72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd.json b/.sqlx/query-72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd.json deleted file mode 100644 index 5df6af55de..0000000000 --- a/.sqlx/query-72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT wireguard_network_id network_id, wireguard_ips \"device_wireguard_ips: Vec\", preshared_key, is_authorized FROM wireguard_network_device WHERE device_id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "network_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "device_wireguard_ips: Vec", - "type_info": "InetArray" - }, - { - "ordinal": 2, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "is_authorized", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - true, - false - ] - }, - "hash": "72d1ffa9d9b2c35c82c4c05d82aa7d8596d6499193933c58fa1d7ba0a8f445bd" -} diff --git a/.sqlx/query-812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087.json b/.sqlx/query-812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087.json deleted file mode 100644 index f56db2e413..0000000000 --- a/.sqlx/query-812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE wireguard_network_id = $1 AND device_id IN (SELECT id FROM device WHERE user_id = $2 AND device_type = 'user'::device_type)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "device_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "wireguard_network_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "wireguard_ips: Vec", - "type_info": "InetArray" - }, - { - "ordinal": 3, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_authorized", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "authorized_at", - "type_info": "Timestamp" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - true - ] - }, - "hash": "812635247539785e93a0b0e78239cafa2c1e5161eef7c5f35a7705be21235087" -} diff --git a/.sqlx/query-8aa6ed2a7f4069cd6e0e0176ce4724039e1a281dd4d3122b5bb8e1cb499070b0.json b/.sqlx/query-8aa6ed2a7f4069cd6e0e0176ce4724039e1a281dd4d3122b5bb8e1cb499070b0.json new file mode 100644 index 0000000000..96542cccbd --- /dev/null +++ b/.sqlx/query-8aa6ed2a7f4069cd6e0e0176ce4724039e1a281dd4d3122b5bb8e1cb499070b0.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE wireguard_network_device SET wireguard_ips = $3 WHERE device_id = $1 AND wireguard_network_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "InetArray" + ] + }, + "nullable": [] + }, + "hash": "8aa6ed2a7f4069cd6e0e0176ce4724039e1a281dd4d3122b5bb8e1cb499070b0" +} diff --git a/.sqlx/query-51dffd9c1ae018de2ade5fde93ae829ad4a5825707e8b2f55b86801039cd784e.json b/.sqlx/query-973c64873ac510cc407d43708efaaa1f93553237be1c6767564c2499b9d8f67d.json similarity index 86% rename from .sqlx/query-51dffd9c1ae018de2ade5fde93ae829ad4a5825707e8b2f55b86801039cd784e.json rename to .sqlx/query-973c64873ac510cc407d43708efaaa1f93553237be1c6767564c2499b9d8f67d.json index 0dc3ac7eed..304137add8 100644 --- a/.sqlx/query-51dffd9c1ae018de2ade5fde93ae829ad4a5825707e8b2f55b86801039cd784e.json +++ b/.sqlx/query-973c64873ac510cc407d43708efaaa1f93553237be1c6767564c2499b9d8f67d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"vpn_client_session\" SET \"location_id\" = $2,\"user_id\" = $3,\"device_id\" = $4,\"created_at\" = $5,\"connected_at\" = $6,\"disconnected_at\" = $7,\"mfa_method\" = $8,\"state\" = $9 WHERE id = $1", + "query": "UPDATE \"vpn_client_session\" SET \"location_id\" = $2,\"user_id\" = $3,\"device_id\" = $4,\"created_at\" = $5,\"connected_at\" = $6,\"disconnected_at\" = $7,\"mfa_method\" = $8,\"state\" = $9,\"preshared_key\" = $10 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -37,10 +37,11 @@ ] } } - } + }, + "Text" ] }, "nullable": [] }, - "hash": "51dffd9c1ae018de2ade5fde93ae829ad4a5825707e8b2f55b86801039cd784e" + "hash": "973c64873ac510cc407d43708efaaa1f93553237be1c6767564c2499b9d8f67d" } diff --git a/.sqlx/query-2bc56fca85ec693d2d53fe08e53143fef55fb878076487fc97738f120573820f.json b/.sqlx/query-9cd4fb6b8bb2f231d136f7e02bb9e0f094c419e60e3f8d8bba86f9df5b7d4c9f.json similarity index 83% rename from .sqlx/query-2bc56fca85ec693d2d53fe08e53143fef55fb878076487fc97738f120573820f.json rename to .sqlx/query-9cd4fb6b8bb2f231d136f7e02bb9e0f094c419e60e3f8d8bba86f9df5b7d4c9f.json index e12c06233c..d5be1a7441 100644 --- a/.sqlx/query-2bc56fca85ec693d2d53fe08e53143fef55fb878076487fc97738f120573820f.json +++ b/.sqlx/query-9cd4fb6b8bb2f231d136f7e02bb9e0f094c419e60e3f8d8bba86f9df5b7d4c9f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND state = 'connected'::vpn_client_session_state", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key FROM vpn_client_session WHERE location_id = $1 AND state = 'connected'::vpn_client_session_state", "describe": { "columns": [ { @@ -71,6 +71,11 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -87,8 +92,9 @@ true, true, true, - false + false, + true ] }, - "hash": "2bc56fca85ec693d2d53fe08e53143fef55fb878076487fc97738f120573820f" + "hash": "9cd4fb6b8bb2f231d136f7e02bb9e0f094c419e60e3f8d8bba86f9df5b7d4c9f" } diff --git a/.sqlx/query-743a5ec9c0ef4e1f92465ad287dc211dedd7b66f89cdf11b36e4a5d7306258be.json b/.sqlx/query-a2a31a9e9d53d830f658131eab155413d1dd6ce5a24b87b9fc4060dd1ae704be.json similarity index 77% rename from .sqlx/query-743a5ec9c0ef4e1f92465ad287dc211dedd7b66f89cdf11b36e4a5d7306258be.json rename to .sqlx/query-a2a31a9e9d53d830f658131eab155413d1dd6ce5a24b87b9fc4060dd1ae704be.json index b98cbb6ba2..f5b56480af 100644 --- a/.sqlx/query-743a5ec9c0ef4e1f92465ad287dc211dedd7b66f89cdf11b36e4a5d7306258be.json +++ b/.sqlx/query-a2a31a9e9d53d830f658131eab155413d1dd6ce5a24b87b9fc4060dd1ae704be.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT s.id, location_id, user_id, device_id, created_at, s.connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" FROM vpn_client_session s LEFT JOIN LATERAL ( SELECT latest_handshake FROM vpn_session_stats WHERE session_id = s.id ORDER BY latest_handshake DESC LIMIT 1 ) ss ON true WHERE location_id = $1 AND state = 'connected' AND (NOW() - ss.latest_handshake) > $2 * interval '1 second'", + "query": "SELECT s.id, location_id, user_id, device_id, created_at, s.connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key FROM vpn_client_session s LEFT JOIN LATERAL ( SELECT latest_handshake FROM vpn_session_stats WHERE session_id = s.id ORDER BY latest_handshake DESC LIMIT 1 ) ss ON true WHERE location_id = $1 AND state = 'connected' AND (NOW() - ss.latest_handshake) > $2 * interval '1 second'", "describe": { "columns": [ { @@ -71,6 +71,11 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -88,8 +93,9 @@ true, true, true, - false + false, + true ] }, - "hash": "743a5ec9c0ef4e1f92465ad287dc211dedd7b66f89cdf11b36e4a5d7306258be" + "hash": "a2a31a9e9d53d830f658131eab155413d1dd6ce5a24b87b9fc4060dd1ae704be" } diff --git a/.sqlx/query-f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3.json b/.sqlx/query-b1c28cf7a7c919c5951dfc050a44528dccc0eaa54d3adbaa7e4a336c992a974d.json similarity index 50% rename from .sqlx/query-f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3.json rename to .sqlx/query-b1c28cf7a7c919c5951dfc050a44528dccc0eaa54d3adbaa7e4a336c992a974d.json index aebfd05555..1147544502 100644 --- a/.sqlx/query-f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3.json +++ b/.sqlx/query-b1c28cf7a7c919c5951dfc050a44528dccc0eaa54d3adbaa7e4a336c992a974d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device WHERE device_id = $1 AND wireguard_network_id = $2", "describe": { "columns": [ { @@ -17,21 +17,6 @@ "ordinal": 2, "name": "wireguard_ips: Vec", "type_info": "InetArray" - }, - { - "ordinal": 3, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_authorized", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "authorized_at", - "type_info": "Timestamp" } ], "parameters": { @@ -43,11 +28,8 @@ "nullable": [ false, false, - false, - true, - false, - true + false ] }, - "hash": "f7dcde071795cb2b14cd1d459259985e4f0d3d88810b317244ac40d2d976b6f3" + "hash": "b1c28cf7a7c919c5951dfc050a44528dccc0eaa54d3adbaa7e4a336c992a974d" } diff --git a/.sqlx/query-bc69afce4a51d831b436e2ea4d82473bfce4408e941822dd782bab65fed9ed6b.json b/.sqlx/query-bc69afce4a51d831b436e2ea4d82473bfce4408e941822dd782bab65fed9ed6b.json new file mode 100644 index 0000000000..c2da8a22c6 --- /dev/null +++ b/.sqlx/query-bc69afce4a51d831b436e2ea4d82473bfce4408e941822dd782bab65fed9ed6b.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.wireguard_pubkey pubkey, active_session.preshared_key preshared_key, -- TODO possible to not use ARRAY-unnest here?\n ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wireguard_network_id = $1 AND (NOT $2 OR active_session.id IS NOT NULL) AND d.configured AND u.is_active ORDER BY d.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "preshared_key", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "allowed_ips!: Vec", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Int8", + "Bool" + ] + }, + "nullable": [ + false, + true, + null + ] + }, + "hash": "bc69afce4a51d831b436e2ea4d82473bfce4408e941822dd782bab65fed9ed6b" +} diff --git a/.sqlx/query-98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f.json b/.sqlx/query-d3bfcc8f183cf03b369c7defab29d6051e67990518cfcf9f6d1889548b3ab9b4.json similarity index 50% rename from .sqlx/query-98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f.json rename to .sqlx/query-d3bfcc8f183cf03b369c7defab29d6051e67990518cfcf9f6d1889548b3ab9b4.json index 4341335409..365066c36b 100644 --- a/.sqlx/query-98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f.json +++ b/.sqlx/query-d3bfcc8f183cf03b369c7defab29d6051e67990518cfcf9f6d1889548b3ab9b4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\", preshared_key, is_authorized, authorized_at FROM wireguard_network_device WHERE device_id = $1", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device WHERE wireguard_network_id = $1 AND device_id IN (SELECT id FROM device WHERE user_id = $2 AND device_type = 'user'::device_type)", "describe": { "columns": [ { @@ -17,36 +17,19 @@ "ordinal": 2, "name": "wireguard_ips: Vec", "type_info": "InetArray" - }, - { - "ordinal": 3, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_authorized", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "authorized_at", - "type_info": "Timestamp" } ], "parameters": { "Left": [ + "Int8", "Int8" ] }, "nullable": [ false, false, - false, - true, - false, - true + false ] }, - "hash": "98d44b2f444407cdb93574eda9a29d6b74b3288be8d31dfcd32394685ba54e0f" + "hash": "d3bfcc8f183cf03b369c7defab29d6051e67990518cfcf9f6d1889548b3ab9b4" } diff --git a/.sqlx/query-2700bf01e6a2afbe3ac2a4b686f715ad1262d5eb8cdf76cf4234b6a4971e6769.json b/.sqlx/query-d86d5f9cb508b1840f0de3c40a993ee77d9cb3c80d8028d99bcc08ccd4c78dd0.json similarity index 82% rename from .sqlx/query-2700bf01e6a2afbe3ac2a4b686f715ad1262d5eb8cdf76cf4234b6a4971e6769.json rename to .sqlx/query-d86d5f9cb508b1840f0de3c40a993ee77d9cb3c80d8028d99bcc08ccd4c78dd0.json index efcd4c5101..f18173e22b 100644 --- a/.sqlx/query-2700bf01e6a2afbe3ac2a4b686f715ad1262d5eb8cdf76cf4234b6a4971e6769.json +++ b/.sqlx/query-d86d5f9cb508b1840f0de3c40a993ee77d9cb3c80d8028d99bcc08ccd4c78dd0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\" FROM vpn_client_session WHERE location_id = $1 AND state = 'new' AND (NOW() - created_at) > $2 * interval '1 second'", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key FROM vpn_client_session WHERE location_id = $1 AND state = 'new' AND (NOW() - created_at) > $2 * interval '1 second'", "describe": { "columns": [ { @@ -71,6 +71,11 @@ } } } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" } ], "parameters": { @@ -88,8 +93,9 @@ true, true, true, - false + false, + true ] }, - "hash": "2700bf01e6a2afbe3ac2a4b686f715ad1262d5eb8cdf76cf4234b6a4971e6769" + "hash": "d86d5f9cb508b1840f0de3c40a993ee77d9cb3c80d8028d99bcc08ccd4c78dd0" } diff --git a/.sqlx/query-e87af2b4e3fd79709a28381e04690aee96054585a87e6673a5efa275795dc060.json b/.sqlx/query-e87af2b4e3fd79709a28381e04690aee96054585a87e6673a5efa275795dc060.json new file mode 100644 index 0000000000..2eebd02c59 --- /dev/null +++ b/.sqlx/query-e87af2b4e3fd79709a28381e04690aee96054585a87e6673a5efa275795dc060.json @@ -0,0 +1,101 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, location_id, user_id, device_id, created_at, connected_at, disconnected_at, mfa_method \"mfa_method: VpnClientMfaMethod\", state \"state: VpnClientSessionState\", preshared_key FROM vpn_client_session WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "location_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "mfa_method: VpnClientMfaMethod", + "type_info": { + "Custom": { + "name": "vpn_client_mfa_method", + "kind": { + "Enum": [ + "totp", + "email", + "oidc", + "biometric", + "mobileapprove" + ] + } + } + } + }, + { + "ordinal": 8, + "name": "state: VpnClientSessionState", + "type_info": { + "Custom": { + "name": "vpn_client_session_state", + "kind": { + "Enum": [ + "new", + "connected", + "disconnected" + ] + } + } + } + }, + { + "ordinal": 9, + "name": "preshared_key", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + true, + false, + true + ] + }, + "hash": "e87af2b4e3fd79709a28381e04690aee96054585a87e6673a5efa275795dc060" +} diff --git a/.sqlx/query-eec3959fe7c4cbfc4ec5215f1664ac34a15e6026f3d571bff9266065b04c14c0.json b/.sqlx/query-eec3959fe7c4cbfc4ec5215f1664ac34a15e6026f3d571bff9266065b04c14c0.json new file mode 100644 index 0000000000..15053df8d7 --- /dev/null +++ b/.sqlx/query-eec3959fe7c4cbfc4ec5215f1664ac34a15e6026f3d571bff9266065b04c14c0.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device WHERE device_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "wireguard_network_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "wireguard_ips: Vec", + "type_info": "InetArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "eec3959fe7c4cbfc4ec5215f1664ac34a15e6026f3d571bff9266065b04c14c0" +} diff --git a/.sqlx/query-febd6a2dc4a335cc25d730d3e823ef3e8928bea54bd8407fccf74b868f6059b2.json b/.sqlx/query-febd6a2dc4a335cc25d730d3e823ef3e8928bea54bd8407fccf74b868f6059b2.json new file mode 100644 index 0000000000..329b1309e2 --- /dev/null +++ b/.sqlx/query-febd6a2dc4a335cc25d730d3e823ef3e8928bea54bd8407fccf74b868f6059b2.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wnd.wireguard_network_id network_id, wnd.wireguard_ips \"device_wireguard_ips: Vec\", active_session.preshared_key preshared_key, CASE WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN TRUE ELSE active_session.id IS NOT NULL END \"is_authorized!\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wnd.device_id = $1 ORDER BY wnd.wireguard_network_id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "network_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "device_wireguard_ips: Vec", + "type_info": "InetArray" + }, + { + "ordinal": 2, + "name": "preshared_key", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "is_authorized!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + true, + null + ] + }, + "hash": "febd6a2dc4a335cc25d730d3e823ef3e8928bea54bd8407fccf74b868f6059b2" +} diff --git a/.sqlx/query-fed5b29a44329968a6f134990261a3ab49a5a2f9fd81eed714678ec272718ea4.json b/.sqlx/query-fed5b29a44329968a6f134990261a3ab49a5a2f9fd81eed714678ec272718ea4.json new file mode 100644 index 0000000000..3851a84ee2 --- /dev/null +++ b/.sqlx/query-fed5b29a44329968a6f134990261a3ab49a5a2f9fd81eed714678ec272718ea4.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT device_id, wireguard_network_id, wireguard_ips \"wireguard_ips: Vec\" FROM wireguard_network_device WHERE wireguard_network_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "wireguard_network_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "wireguard_ips: Vec", + "type_info": "InetArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "fed5b29a44329968a6f134990261a3ab49a5a2f9fd81eed714678ec272718ea4" +} From e85d7b4997675051f07b1026f63322bdff9a9d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 18 Mar 2026 14:50:08 +0100 Subject: [PATCH 08/19] add db constraints --- .../src/grpc/proxy/client_mfa.rs | 149 ++++++++++++++++++ .../tests/session_manager/db_invariants.rs | 126 +++++++++++++++ .../tests/session_manager/mod.rs | 1 + ..._vpn_client_session_preshared_key.down.sql | 6 +- ...0]_vpn_client_session_preshared_key.up.sql | 9 +- 5 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 crates/defguard_session_manager/tests/session_manager/db_invariants.rs diff --git a/crates/defguard_core/src/grpc/proxy/client_mfa.rs b/crates/defguard_core/src/grpc/proxy/client_mfa.rs index 65fd4f0b7a..3333a45566 100644 --- a/crates/defguard_core/src/grpc/proxy/client_mfa.rs +++ b/crates/defguard_core/src/grpc/proxy/client_mfa.rs @@ -832,3 +832,152 @@ impl ClientMfaServer { Ok(()) } } + +#[cfg(test)] +mod tests { + use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr}, + sync::{Arc, RwLock}, + }; + + use defguard_common::db::{ + Id, + models::{ + Device, DeviceType, User, WireguardNetwork, + vpn_client_session::{VpnClientMfaMethod, VpnClientSession, VpnClientSessionState}, + wireguard::{LocationMfaMode, ServiceLocationMode}, + }, + setup_pool, + }; + use ipnetwork::IpNetwork; + use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + use tokio::sync::{broadcast, mpsc, oneshot}; + + use super::{ClientLoginSession, ClientMfaServer}; + use crate::grpc::GatewayEvent; + + async fn create_location(pool: &sqlx::PgPool) -> WireguardNetwork { + WireguardNetwork::new( + "TestNet".to_string(), + vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)), 24).unwrap()], + 51820, + "10.0.0.1".to_string(), + None, + 1420, + 0, + vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], + true, + 25, + 300, + false, + false, + LocationMfaMode::Internal, + ServiceLocationMode::Disabled, + ) + .save(pool) + .await + .expect("failed to create WireGuard location") + } + + async fn create_user(pool: &sqlx::PgPool) -> User { + User::new("tester", None, "Test", "User", "tester@example.com", None) + .save(pool) + .await + .expect("failed to create user") + } + + async fn create_device(pool: &sqlx::PgPool, user_id: Id) -> Device { + Device::new( + "test-device".to_string(), + "device-pubkey-test".to_string(), + user_id, + DeviceType::User, + None, + true, + ) + .save(pool) + .await + .expect("failed to create device") + } + + #[sqlx::test] + async fn test_create_new_mfa_session_disconnects_previous_active_session( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + let location = create_location(&pool).await; + let user = create_user(&pool).await; + let device = create_device(&pool, user.id).await; + + let mut previous_session = VpnClientSession::new( + location.id, + user.id, + device.id, + Some(chrono::Utc::now().naive_utc()), + Some(VpnClientMfaMethod::Totp), + ); + previous_session.preshared_key = Some("old-psk".to_string()); + previous_session.state = VpnClientSessionState::Connected; + let previous_session = previous_session + .save(&pool) + .await + .expect("failed to create previous active MFA session"); + + let (gateway_tx, mut gateway_rx) = broadcast::channel(4); + let (bidi_event_tx, _bidi_event_rx) = mpsc::unbounded_channel(); + let server = ClientMfaServer::new( + pool.clone(), + gateway_tx, + bidi_event_tx, + Arc::new(RwLock::new( + HashMap::>::new(), + )), + Arc::new(RwLock::new(HashMap::::new())), + ); + let mut conn = pool + .acquire() + .await + .expect("failed to acquire database connection"); + + let new_session = server + .create_new_mfa_session( + &mut conn, + &location, + &user, + &device, + VpnClientMfaMethod::Totp, + "new-psk".to_string(), + ) + .await + .expect("failed to create replacement MFA session"); + + let previous_session = VpnClientSession::find_by_id(&pool, previous_session.id) + .await + .expect("failed to reload previous session") + .expect("expected previous session to exist"); + assert_eq!(previous_session.state, VpnClientSessionState::Disconnected); + assert!(previous_session.disconnected_at.is_some()); + + let active_sessions = VpnClientSession::get_all_active_device_sessions_in_location( + &pool, + location.id, + device.id, + ) + .await + .expect("failed to fetch active sessions"); + assert_eq!(active_sessions.len(), 1); + assert_eq!(active_sessions[0].id, new_session.id); + assert_eq!(active_sessions[0].preshared_key.as_deref(), Some("new-psk")); + + match gateway_rx.try_recv() { + Ok(GatewayEvent::MfaSessionDisconnected(location_id, disconnected_device)) => { + assert_eq!(location_id, location.id); + assert_eq!(disconnected_device.id, device.id); + } + Ok(other) => panic!("unexpected gateway event: {other:?}"), + Err(error) => panic!("expected MFA disconnect gateway event, got {error:?}"), + } + } +} diff --git a/crates/defguard_session_manager/tests/session_manager/db_invariants.rs b/crates/defguard_session_manager/tests/session_manager/db_invariants.rs new file mode 100644 index 0000000000..3b62ccf2fb --- /dev/null +++ b/crates/defguard_session_manager/tests/session_manager/db_invariants.rs @@ -0,0 +1,126 @@ +use chrono::Utc; +use defguard_common::db::{Id, setup_pool}; +use sqlx::{ + postgres::{PgConnectOptions, PgPoolOptions}, + query, query_scalar, +}; + +use crate::common::{attach_device_to_location, create_device, create_location, create_user}; + +const ACTIVE_SESSION_UNIQUE_INDEX: &str = "vpn_client_session_active_location_device_unique"; + +async fn insert_session( + pool: &sqlx::PgPool, + location_id: Id, + user_id: Id, + device_id: Id, + state: &str, +) -> Result { + let connected_at = (state == "connected").then(|| Utc::now().naive_utc()); + + query_scalar( + "INSERT INTO vpn_client_session (location_id, user_id, device_id, connected_at, mfa_method, state, preshared_key) \ + VALUES ($1, $2, $3, $4, NULL, $5::vpn_client_session_state, NULL) \ + RETURNING id", + ) + .bind(location_id) + .bind(user_id) + .bind(device_id) + .bind(connected_at) + .bind(state) + .fetch_one(pool) + .await +} + +async fn count_active_sessions(pool: &sqlx::PgPool, location_id: Id, device_id: Id) -> i64 { + query_scalar( + "SELECT COUNT(*) FROM vpn_client_session \ + WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected')", + ) + .bind(location_id) + .bind(device_id) + .fetch_one(pool) + .await + .expect("failed to count active sessions") +} + +#[sqlx::test] +async fn test_db_rejects_second_active_session_for_same_device_location( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let location = create_location(&pool).await; + let user = create_user(&pool).await; + let device = create_device(&pool, user.id).await; + attach_device_to_location(&pool, location.id, device.id).await; + + insert_session(&pool, location.id, user.id, device.id, "new") + .await + .expect("failed to create first active session"); + + let error = insert_session(&pool, location.id, user.id, device.id, "connected") + .await + .expect_err("expected unique index to reject duplicate active session"); + + match error { + sqlx::Error::Database(database_error) => { + assert_eq!(database_error.code().as_deref(), Some("23505")); + assert_eq!( + database_error.constraint(), + Some(ACTIVE_SESSION_UNIQUE_INDEX) + ); + } + other => panic!("expected database uniqueness error, got {other:?}"), + } +} + +#[sqlx::test] +async fn test_db_allows_new_active_session_after_previous_session_disconnects( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let location = create_location(&pool).await; + let user = create_user(&pool).await; + let device = create_device(&pool, user.id).await; + attach_device_to_location(&pool, location.id, device.id).await; + + let disconnected_session_id = + insert_session(&pool, location.id, user.id, device.id, "connected") + .await + .expect("failed to create initial active session"); + + query( + "UPDATE vpn_client_session \ + SET state = 'disconnected', disconnected_at = NOW() \ + WHERE id = $1", + ) + .bind(disconnected_session_id) + .execute(&pool) + .await + .expect("failed to disconnect initial session"); + + let new_session_id = insert_session(&pool, location.id, user.id, device.id, "new") + .await + .expect("disconnected session should not block new active session"); + + assert_eq!( + count_active_sessions(&pool, location.id, device.id).await, + 1 + ); + + let active_session_id: Id = query_scalar( + "SELECT id FROM vpn_client_session \ + WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') \ + ORDER BY created_at DESC, id DESC \ + LIMIT 1", + ) + .bind(location.id) + .bind(device.id) + .fetch_one(&pool) + .await + .expect("failed to fetch remaining active session"); + + assert_eq!(active_session_id, new_session_id); +} diff --git a/crates/defguard_session_manager/tests/session_manager/mod.rs b/crates/defguard_session_manager/tests/session_manager/mod.rs index 1cf8461d46..1ffdee11f2 100644 --- a/crates/defguard_session_manager/tests/session_manager/mod.rs +++ b/crates/defguard_session_manager/tests/session_manager/mod.rs @@ -1,3 +1,4 @@ +mod db_invariants; mod disconnects; mod event_flow; mod mfa; diff --git a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql index b7bd32d944..d19a7eae07 100644 --- a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql +++ b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql @@ -3,7 +3,9 @@ ALTER TABLE wireguard_network_device ADD COLUMN is_authorized bool NOT NULL DEFAULT false, ADD COLUMN authorized_at timestamp without time zone NULL; --- Restore legacy preshared keys when canonical active session data exists; unmatched rows stay NULL. +-- Rollback is lossy: only preshared_key is repopulated from the latest active +-- session per (device_id, location_id); is_authorized and authorized_at are +-- recreated with default/NULL values and are not reconstructed. UPDATE wireguard_network_device AS network_device SET preshared_key = latest_active_session.preshared_key FROM ( @@ -19,4 +21,6 @@ FROM ( WHERE network_device.device_id = latest_active_session.device_id AND network_device.wireguard_network_id = latest_active_session.location_id; +DROP INDEX IF EXISTS vpn_client_session_active_location_device_unique; + ALTER TABLE vpn_client_session DROP COLUMN preshared_key; diff --git a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql index ad48ba35b7..d530297d2a 100644 --- a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql +++ b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql @@ -1,6 +1,13 @@ --- Transitional squash: add session-level preshared_key and remove legacy device-level MFA state. +-- Add session-level preshared_key, enforce at most one active session per +-- (location_id, device_id), and drop device-level preshared_key/auth fields. +-- WARNING: rollback is lossy for dropped wireguard_network_device columns. +-- Do not yet require preshared_key for active MFA sessions. ALTER TABLE vpn_client_session ADD COLUMN preshared_key text NULL; +CREATE UNIQUE INDEX vpn_client_session_active_location_device_unique + ON vpn_client_session(location_id, device_id) + WHERE state IN ('new', 'connected'); + ALTER TABLE wireguard_network_device DROP COLUMN preshared_key, DROP COLUMN is_authorized, From 60678b3147c0d5499f754ce9cd909a91ec44a78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 18 Mar 2026 16:40:03 +0100 Subject: [PATCH 09/19] post-merge fixes --- ...e2a67880adeaca1e10e50bea9b9fc53e08845.json | 2 +- ...c52cb537dfded663e6bbee73d5f908f0ca89e.json | 2 +- ...9eb172facf3f2f9b1a46cf22a8979373edf8.json} | 4 +- .../defguard_common/src/db/models/device.rs | 72 ++++++++++-------- .../src/location_management/allowed_peers.rs | 75 ++++++++++++++++--- .../defguard_gateway_manager/src/handler.rs | 46 ++++++------ .../tests/session_manager/mfa.rs | 15 ++-- 7 files changed, 141 insertions(+), 75 deletions(-) rename .sqlx/{query-bc69afce4a51d831b436e2ea4d82473bfce4408e941822dd782bab65fed9ed6b.json => query-60d741f459aaef3a77e69f7396c29eb172facf3f2f9b1a46cf22a8979373edf8.json} (87%) diff --git a/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json b/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json index 0e36c94cdb..8a6776d365 100644 --- a/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json +++ b/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json @@ -82,7 +82,7 @@ false, false, true, - false, + true, false, false, false, diff --git a/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json b/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json index d62e957d33..548a89c193 100644 --- a/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json +++ b/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json @@ -80,7 +80,7 @@ false, false, true, - false, + true, false, false, false, diff --git a/.sqlx/query-bc69afce4a51d831b436e2ea4d82473bfce4408e941822dd782bab65fed9ed6b.json b/.sqlx/query-60d741f459aaef3a77e69f7396c29eb172facf3f2f9b1a46cf22a8979373edf8.json similarity index 87% rename from .sqlx/query-bc69afce4a51d831b436e2ea4d82473bfce4408e941822dd782bab65fed9ed6b.json rename to .sqlx/query-60d741f459aaef3a77e69f7396c29eb172facf3f2f9b1a46cf22a8979373edf8.json index c2da8a22c6..340c16264e 100644 --- a/.sqlx/query-bc69afce4a51d831b436e2ea4d82473bfce4408e941822dd782bab65fed9ed6b.json +++ b/.sqlx/query-60d741f459aaef3a77e69f7396c29eb172facf3f2f9b1a46cf22a8979373edf8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey pubkey, active_session.preshared_key preshared_key, -- TODO possible to not use ARRAY-unnest here?\n ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wireguard_network_id = $1 AND (NOT $2 OR active_session.id IS NOT NULL) AND d.configured AND u.is_active ORDER BY d.id ASC", + "query": "SELECT d.wireguard_pubkey pubkey, active_session.preshared_key preshared_key, -- TODO possible to not use ARRAY-unnest here?\n ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wireguard_network_id = $1 AND (NOT $2 OR active_session.preshared_key IS NOT NULL) AND d.configured AND u.is_active ORDER BY d.id ASC", "describe": { "columns": [ { @@ -31,5 +31,5 @@ null ] }, - "hash": "bc69afce4a51d831b436e2ea4d82473bfce4408e941822dd782bab65fed9ed6b" + "hash": "60d741f459aaef3a77e69f7396c29eb172facf3f2f9b1a46cf22a8979373edf8" } diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index 98436089de..d5e0ba2196 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -328,7 +328,7 @@ impl WireguardNetworkDevice { executor: E, network: &WireguardNetwork, device_id: Id, - ) -> Result>, SqlxError> + ) -> sqlx::Result>> where E: PgExecutor<'e>, { @@ -365,7 +365,7 @@ impl WireguardNetworkDevice { &self, executor: E, network: &WireguardNetwork, - ) -> Result + ) -> sqlx::Result where E: PgExecutor<'e>, { @@ -1462,30 +1462,31 @@ mod test { assert_ok!(Device::validate_pubkey(valid_test_key)); } - #[test] - fn test_runtime_mfa_state_requires_session_preshared_key_for_mfa_runtime_reads() { - let defaults = WireguardNetwork::::default(); - let network = WireguardNetwork { - id: 1, - name: defaults.name, - address: defaults.address, - port: defaults.port, - pubkey: defaults.pubkey, - prvkey: defaults.prvkey, - endpoint: defaults.endpoint, - dns: defaults.dns, - mtu: defaults.mtu, - fwmark: defaults.fwmark, - allowed_ips: defaults.allowed_ips, - allow_all_groups: defaults.allow_all_groups, - connected_at: defaults.connected_at, - acl_enabled: defaults.acl_enabled, - acl_default_allow: defaults.acl_default_allow, - keepalive_interval: defaults.keepalive_interval, - peer_disconnect_threshold: defaults.peer_disconnect_threshold, - location_mfa_mode: LocationMfaMode::Internal, - service_location_mode: defaults.service_location_mode, - }; + #[sqlx::test] + async fn test_runtime_mfa_state_requires_session_preshared_key_for_mfa_runtime_reads( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let network = WireguardNetwork::new( + "runtime-mfa-network".into(), + 51820, + "vpn.example.com".into(), + None, + Vec::::new(), + false, + false, + false, + LocationMfaMode::Internal, + ServiceLocationMode::Disabled, + ) + .try_set_address("10.1.1.1/24") + .unwrap() + .save(&pool) + .await + .unwrap(); + let wireguard_network_device = WireguardNetworkDevice { wireguard_network_id: network.id, wireguard_ips: vec![IpAddr::from_str("10.1.1.2").unwrap()], @@ -1542,11 +1543,20 @@ mod test { .await .unwrap(); - let mut network = WireguardNetwork:: { - location_mfa_mode: LocationMfaMode::Internal, - ..Default::default() - }; - network.try_set_address("10.1.1.1/24").unwrap(); + let network = WireguardNetwork::new( + "device-info-network".into(), + 51820, + "vpn.example.com".into(), + None, + Vec::::new(), + false, + false, + false, + LocationMfaMode::Internal, + ServiceLocationMode::Disabled, + ) + .try_set_address("10.1.1.1/24") + .unwrap(); let network = network.save(&pool).await.unwrap(); let wireguard_network_device = WireguardNetworkDevice::new( diff --git a/crates/defguard_core/src/location_management/allowed_peers.rs b/crates/defguard_core/src/location_management/allowed_peers.rs index 61b839ff5f..5feee82182 100644 --- a/crates/defguard_core/src/location_management/allowed_peers.rs +++ b/crates/defguard_core/src/location_management/allowed_peers.rs @@ -10,6 +10,7 @@ use crate::grpc::should_prevent_service_location_usage; /// which enables enforcing peer disconnect in MFA-protected networks. /// /// If the location is a service location, only returns peers if enterprise features are enabled. +/// MFA-enabled locations only return peers backed by an active session with a runtime preshared key. pub async fn get_location_allowed_peers<'e, E>( location: &WireguardNetwork, executor: E, @@ -47,7 +48,8 @@ where ORDER BY created_at DESC, id DESC \ LIMIT 1 \ ) active_session ON true \ - WHERE wireguard_network_id = $1 AND (NOT $2 OR active_session.id IS NOT NULL) \ + WHERE wireguard_network_id = $1 \ + AND (NOT $2 OR active_session.preshared_key IS NOT NULL) \ AND d.configured AND u.is_active \ ORDER BY d.id ASC", location.id, @@ -233,7 +235,7 @@ mod test { } #[sqlx::test] - async fn test_get_location_allowed_peers_does_not_expose_legacy_preshared_key_for_active_mfa_session( + async fn test_get_location_allowed_peers_skips_active_mfa_session_without_preshared_key( _: PgPoolOptions, options: PgConnectOptions, ) { @@ -263,13 +265,12 @@ mod test { .await .unwrap(); - let mut network = WireguardNetwork { - name: "mfa-location".to_string(), - service_location_mode: ServiceLocationMode::Disabled, - location_mfa_mode: LocationMfaMode::Internal, - ..Default::default() - }; - network.try_set_address("10.4.1.1/24").unwrap(); + let mut network = WireguardNetwork::default() + .try_set_address("10.4.1.1/24") + .unwrap(); + network.name = "mfa-location".to_string(); + network.service_location_mode = ServiceLocationMode::Disabled; + network.location_mfa_mode = LocationMfaMode::Internal; let network = network.save(&pool).await.unwrap(); let network_device = WireguardNetworkDevice::new( @@ -286,6 +287,62 @@ mod test { let peers = get_location_allowed_peers(&network, &pool).await.unwrap(); + assert!(peers.is_empty()); + } + + #[sqlx::test] + async fn test_get_location_allowed_peers_keeps_non_mfa_peer_without_session_preshared_key( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password123"), + "Test", + "User", + "test@example.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device1".into(), + "pubkey1".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let mut network = WireguardNetwork::default() + .try_set_address("10.5.1.1/24") + .unwrap(); + network.name = "non-mfa-location".to_string(); + network.service_location_mode = ServiceLocationMode::Disabled; + network.location_mfa_mode = LocationMfaMode::Disabled; + let network = network.save(&pool).await.unwrap(); + + let network_device = WireguardNetworkDevice::new( + network.id, + device.id, + vec![IpAddr::from_str("10.5.1.2").unwrap()], + ); + network_device.insert(&pool).await.unwrap(); + + VpnClientSession::new(network.id, user.id, device.id, None, None) + .save(&pool) + .await + .unwrap(); + + let peers = get_location_allowed_peers(&network, &pool).await.unwrap(); + assert_eq!(peers.len(), 1); assert_eq!(peers[0].pubkey, "pubkey1"); assert_eq!(peers[0].preshared_key, None); diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index 5f87c47334..37f38e680b 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -866,6 +866,7 @@ fn gen_config( #[cfg(test)] mod tests { + use serde_json::json; use tokio::sync::{broadcast, mpsc::unbounded_channel}; use super::GatewayUpdatesHandler; @@ -875,29 +876,28 @@ mod tests { }; fn test_network(location_mfa_mode: LocationMfaMode) -> WireguardNetwork { - let defaults = WireguardNetwork::default(); - - WireguardNetwork { - id: 1, - name: "test-network".into(), - address: defaults.address, - port: 51820, - pubkey: "network-pubkey".into(), - prvkey: "network-prvkey".into(), - endpoint: "127.0.0.1".into(), - dns: None, - mtu: 1420, - fwmark: 0, - allowed_ips: Vec::new(), - allow_all_groups: true, - connected_at: None, - keepalive_interval: 25, - peer_disconnect_threshold: 300, - acl_enabled: false, - acl_default_allow: false, - location_mfa_mode, - service_location_mode: ServiceLocationMode::Disabled, - } + serde_json::from_value(json!({ + "id": 1, + "name": "test-network", + "address": ["10.1.1.1/24"], + "port": 51820, + "pubkey": "network-pubkey", + "prvkey": "network-prvkey", + "endpoint": "127.0.0.1", + "dns": null, + "mtu": 1420, + "fwmark": 0, + "allowed_ips": [], + "allow_all_groups": true, + "connected_at": null, + "acl_enabled": false, + "acl_default_allow": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 300, + "location_mfa_mode": location_mfa_mode, + "service_location_mode": ServiceLocationMode::Disabled, + })) + .unwrap() } fn test_handler(location_mfa_mode: LocationMfaMode) -> GatewayUpdatesHandler { diff --git a/crates/defguard_session_manager/tests/session_manager/mfa.rs b/crates/defguard_session_manager/tests/session_manager/mfa.rs index 09990a2bfc..a3a688ccdc 100644 --- a/crates/defguard_session_manager/tests/session_manager/mfa.rs +++ b/crates/defguard_session_manager/tests/session_manager/mfa.rs @@ -386,6 +386,7 @@ async fn test_closed_event_channel_keeps_mfa_first_stats_upgrade_idempotent( device.id, None, Some(VpnClientMfaMethod::Totp), + None, ) .await; @@ -532,7 +533,6 @@ async fn test_never_connected_mfa_new_sessions_disconnect_after_threshold( let user = create_user(&pool).await; let device = create_device(&pool, user.id).await; attach_device_to_location(&pool, location.id, device.id).await; - authorize_device_in_location(&pool, location.id, device.id, "psk-before-timeout").await; let mut harness = SessionManagerHarness::new(pool.clone()); let session = create_session( @@ -558,13 +558,12 @@ async fn test_never_connected_mfa_new_sessions_disconnect_after_threshold( VpnClientSessionState::Disconnected ); assert!(disconnected_session.disconnected_at.is_some()); - - let network_device = WireguardNetworkDevice::find(&pool, device.id, location.id) - .await - .expect("failed to query network device") - .expect("expected network device"); - assert!(!network_device.is_authorized); - assert_eq!(network_device.preshared_key, None); + assert!( + VpnClientSession::try_get_active_session(&pool, location.id, device.id) + .await + .expect("failed to query active session") + .is_none() + ); let gateway_event = timeout(RECEIVE_TIMEOUT, harness.gateway_rx.recv()) .await From ee34c61c6f14273f2a5d23721437699b4be0bc1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 18 Mar 2026 19:36:11 +0100 Subject: [PATCH 10/19] extend tests --- .../src/location_management/allowed_peers.rs | 99 +++++++++++++ .../defguard_gateway_manager/src/handler.rs | 139 +++++++++++++++++- 2 files changed, 233 insertions(+), 5 deletions(-) diff --git a/crates/defguard_core/src/location_management/allowed_peers.rs b/crates/defguard_core/src/location_management/allowed_peers.rs index 5feee82182..fef967652d 100644 --- a/crates/defguard_core/src/location_management/allowed_peers.rs +++ b/crates/defguard_core/src/location_management/allowed_peers.rs @@ -84,6 +84,7 @@ where mod test { use std::{net::IpAddr, str::FromStr}; + use chrono::Utc; use defguard_common::db::{ models::{ Device, DeviceType, WireguardNetwork, @@ -347,4 +348,102 @@ mod test { assert_eq!(peers[0].pubkey, "pubkey1"); assert_eq!(peers[0].preshared_key, None); } + + #[sqlx::test] + async fn test_get_location_allowed_peers_includes_active_mfa_peers_with_session_preshared_key( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password123"), + "Test", + "User", + "test@example.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let new_device = Device::new( + "device-new".into(), + "pubkey-new".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let connected_device = Device::new( + "device-connected".into(), + "pubkey-connected".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let mut network = WireguardNetwork::default() + .try_set_address("10.6.1.1/24") + .unwrap(); + network.name = "mfa-location-with-session-psk".to_string(); + network.service_location_mode = ServiceLocationMode::Disabled; + network.location_mfa_mode = LocationMfaMode::Internal; + let network = network.save(&pool).await.unwrap(); + + WireguardNetworkDevice::new( + network.id, + new_device.id, + vec![IpAddr::from_str("10.6.1.2").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + WireguardNetworkDevice::new( + network.id, + connected_device.id, + vec![IpAddr::from_str("10.6.1.3").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + let mut new_session = VpnClientSession::new(network.id, user.id, new_device.id, None, None); + new_session.preshared_key = Some("new-session-psk".into()); + new_session.save(&pool).await.unwrap(); + + let mut connected_session = VpnClientSession::new( + network.id, + user.id, + connected_device.id, + Some(Utc::now().naive_utc()), + None, + ); + connected_session.preshared_key = Some("connected-session-psk".into()); + connected_session.save(&pool).await.unwrap(); + + let peers = get_location_allowed_peers(&network, &pool).await.unwrap(); + + assert_eq!(peers.len(), 2); + assert_eq!( + peers + .iter() + .map(|peer| (peer.pubkey.as_str(), peer.preshared_key.as_deref())) + .collect::>(), + vec![ + ("pubkey-new", Some("new-session-psk")), + ("pubkey-connected", Some("connected-session-psk")), + ] + ); + } } diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index 37f38e680b..f3925f0bcd 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -866,14 +866,28 @@ fn gen_config( #[cfg(test)] mod tests { - use serde_json::json; - use tokio::sync::{broadcast, mpsc::unbounded_channel}; + use std::{collections::HashMap, net::IpAddr, str::FromStr, sync::Arc}; - use super::GatewayUpdatesHandler; + use chrono::Utc; use defguard_common::db::{ - Id, - models::wireguard::{LocationMfaMode, ServiceLocationMode, WireguardNetwork}, + models::{ + Device, DeviceType, User, + device::WireguardNetworkDevice, + gateway::Gateway, + vpn_client_session::VpnClientSession, + wireguard::{LocationMfaMode, ServiceLocationMode, WireguardNetwork}, + }, + setup_pool, }; + use defguard_core::grpc::GatewayEvent; + use defguard_proto::gateway::core_response; + use serde_json::json; + use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + use tokio::sync::{broadcast, mpsc::unbounded_channel}; + use tokio::sync::watch; + + use super::{GatewayHandler, GatewayUpdatesHandler}; + use defguard_common::db::Id; fn test_network(location_mfa_mode: LocationMfaMode) -> WireguardNetwork { serde_json::from_value(json!({ @@ -960,4 +974,119 @@ mod tests { assert_eq!(peer.preshared_key, Some("session-psk".into())); } + + #[sqlx::test] + async fn test_send_configuration_includes_mfa_peers_with_session_preshared_key( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password123"), + "Test", + "User", + "test@example.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let new_device = Device::new( + "device-new".into(), + "pubkey-new".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let connected_device = Device::new( + "device-connected".into(), + "pubkey-connected".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let mut network = WireguardNetwork::default() + .try_set_address("10.7.1.1/24") + .unwrap(); + network.name = "mfa-full-config-location".to_string(); + network.location_mfa_mode = LocationMfaMode::Internal; + network.service_location_mode = ServiceLocationMode::Disabled; + let network = network.save(&pool).await.unwrap(); + + WireguardNetworkDevice::new( + network.id, + new_device.id, + vec![IpAddr::from_str("10.7.1.2").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + WireguardNetworkDevice::new( + network.id, + connected_device.id, + vec![IpAddr::from_str("10.7.1.3").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + let mut new_session = VpnClientSession::new(network.id, user.id, new_device.id, None, None); + new_session.preshared_key = Some("new-session-psk".into()); + new_session.save(&pool).await.unwrap(); + + let mut connected_session = VpnClientSession::new( + network.id, + user.id, + connected_device.id, + Some(Utc::now().naive_utc()), + None, + ); + connected_session.preshared_key = Some("connected-session-psk".into()); + connected_session.save(&pool).await.unwrap(); + + let gateway = Gateway::new(network.id, "gateway", "127.0.0.1", 50051, "test") + .save(&pool) + .await + .unwrap(); + let (events_tx, _events_rx) = broadcast::channel::(1); + let (peer_stats_tx, _peer_stats_rx) = unbounded_channel(); + let (_certs_tx, certs_rx) = watch::channel(Arc::new(HashMap::::new())); + let handler = + GatewayHandler::new(gateway, pool.clone(), events_tx, peer_stats_tx, certs_rx).unwrap(); + let (tx, mut rx) = unbounded_channel(); + + handler.send_configuration(&tx).await.unwrap(); + + let response = rx.recv().await.unwrap(); + let Some(core_response::Payload::Config(configuration)) = response.payload else { + panic!("expected gateway config payload"); + }; + + assert_eq!(configuration.peers.len(), 2); + assert_eq!( + configuration + .peers + .iter() + .map(|peer| (peer.pubkey.as_str(), peer.preshared_key.as_deref())) + .collect::>(), + vec![ + ("pubkey-new", Some("new-session-psk")), + ("pubkey-connected", Some("connected-session-psk")), + ] + ); + } } From ef1cd9e3607c986bb001901f6397dbb721ce4257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 18 Mar 2026 21:45:35 +0100 Subject: [PATCH 11/19] review fixes --- ...29eb172facf3f2f9b1a46cf22a8979373edf8.json | 35 --- ...d3e7a35e12060a60ddc70ba3cc135c89db642.json | 40 +++ ...a03d9b14fe56a118b31220c572a4412c711cf.json | 28 +++ ...a9efc87a8696f46c6f0dea5618b6aac0ff962.json | 34 +++ ...3ef3e8928bea54bd8407fccf74b868f6059b2.json | 40 --- .../defguard_common/src/db/models/device.rs | 235 +++++++++++++++++- .../src/location_management/allowed_peers.rs | 67 +++-- .../defguard_gateway_manager/src/handler.rs | 2 +- .../tests/session_manager/db_invariants.rs | 137 ++++++++++ 9 files changed, 503 insertions(+), 115 deletions(-) delete mode 100644 .sqlx/query-60d741f459aaef3a77e69f7396c29eb172facf3f2f9b1a46cf22a8979373edf8.json create mode 100644 .sqlx/query-8b9ede93cc39f26e6006bd26eb6d3e7a35e12060a60ddc70ba3cc135c89db642.json create mode 100644 .sqlx/query-990350e22dea5e90064d5acd42ea03d9b14fe56a118b31220c572a4412c711cf.json create mode 100644 .sqlx/query-fda76aeb98f21838bf502c3492da9efc87a8696f46c6f0dea5618b6aac0ff962.json delete mode 100644 .sqlx/query-febd6a2dc4a335cc25d730d3e823ef3e8928bea54bd8407fccf74b868f6059b2.json diff --git a/.sqlx/query-60d741f459aaef3a77e69f7396c29eb172facf3f2f9b1a46cf22a8979373edf8.json b/.sqlx/query-60d741f459aaef3a77e69f7396c29eb172facf3f2f9b1a46cf22a8979373edf8.json deleted file mode 100644 index 340c16264e..0000000000 --- a/.sqlx/query-60d741f459aaef3a77e69f7396c29eb172facf3f2f9b1a46cf22a8979373edf8.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey pubkey, active_session.preshared_key preshared_key, -- TODO possible to not use ARRAY-unnest here?\n ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wireguard_network_id = $1 AND (NOT $2 OR active_session.preshared_key IS NOT NULL) AND d.configured AND u.is_active ORDER BY d.id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "pubkey", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "allowed_ips!: Vec", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [ - "Int8", - "Bool" - ] - }, - "nullable": [ - false, - true, - null - ] - }, - "hash": "60d741f459aaef3a77e69f7396c29eb172facf3f2f9b1a46cf22a8979373edf8" -} diff --git a/.sqlx/query-8b9ede93cc39f26e6006bd26eb6d3e7a35e12060a60ddc70ba3cc135c89db642.json b/.sqlx/query-8b9ede93cc39f26e6006bd26eb6d3e7a35e12060a60ddc70ba3cc135c89db642.json new file mode 100644 index 0000000000..126c4d8db2 --- /dev/null +++ b/.sqlx/query-8b9ede93cc39f26e6006bd26eb6d3e7a35e12060a60ddc70ba3cc135c89db642.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wnd.wireguard_network_id network_id, wnd.wireguard_ips \"device_wireguard_ips: Vec\", CASE WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN NULL::text ELSE active_session.preshared_key END \"preshared_key?\", CASE WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN TRUE ELSE active_session.preshared_key IS NOT NULL END \"is_authorized!\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wnd.device_id = $1 ORDER BY wnd.wireguard_network_id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "network_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "device_wireguard_ips: Vec", + "type_info": "InetArray" + }, + { + "ordinal": 2, + "name": "preshared_key?", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "is_authorized!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + null, + null + ] + }, + "hash": "8b9ede93cc39f26e6006bd26eb6d3e7a35e12060a60ddc70ba3cc135c89db642" +} diff --git a/.sqlx/query-990350e22dea5e90064d5acd42ea03d9b14fe56a118b31220c572a4412c711cf.json b/.sqlx/query-990350e22dea5e90064d5acd42ea03d9b14fe56a118b31220c572a4412c711cf.json new file mode 100644 index 0000000000..d9f37238e8 --- /dev/null +++ b/.sqlx/query-990350e22dea5e90064d5acd42ea03d9b14fe56a118b31220c572a4412c711cf.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.wireguard_pubkey pubkey, ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id WHERE wireguard_network_id = $1 AND d.configured AND u.is_active ORDER BY d.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "allowed_ips!: Vec", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + null + ] + }, + "hash": "990350e22dea5e90064d5acd42ea03d9b14fe56a118b31220c572a4412c711cf" +} diff --git a/.sqlx/query-fda76aeb98f21838bf502c3492da9efc87a8696f46c6f0dea5618b6aac0ff962.json b/.sqlx/query-fda76aeb98f21838bf502c3492da9efc87a8696f46c6f0dea5618b6aac0ff962.json new file mode 100644 index 0000000000..8a0404b8af --- /dev/null +++ b/.sqlx/query-fda76aeb98f21838bf502c3492da9efc87a8696f46c6f0dea5618b6aac0ff962.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.wireguard_pubkey pubkey, active_session.preshared_key \"preshared_key!\", ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id JOIN LATERAL ( SELECT preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') AND preshared_key IS NOT NULL ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wireguard_network_id = $1 AND d.configured AND u.is_active ORDER BY d.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "preshared_key!", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "allowed_ips!: Vec", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + true, + null + ] + }, + "hash": "fda76aeb98f21838bf502c3492da9efc87a8696f46c6f0dea5618b6aac0ff962" +} diff --git a/.sqlx/query-febd6a2dc4a335cc25d730d3e823ef3e8928bea54bd8407fccf74b868f6059b2.json b/.sqlx/query-febd6a2dc4a335cc25d730d3e823ef3e8928bea54bd8407fccf74b868f6059b2.json deleted file mode 100644 index 329b1309e2..0000000000 --- a/.sqlx/query-febd6a2dc4a335cc25d730d3e823ef3e8928bea54bd8407fccf74b868f6059b2.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT wnd.wireguard_network_id network_id, wnd.wireguard_ips \"device_wireguard_ips: Vec\", active_session.preshared_key preshared_key, CASE WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN TRUE ELSE active_session.id IS NOT NULL END \"is_authorized!\" FROM wireguard_network_device wnd JOIN wireguard_network n ON n.id = wnd.wireguard_network_id LEFT JOIN LATERAL ( SELECT id, preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wnd.device_id = $1 ORDER BY wnd.wireguard_network_id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "network_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "device_wireguard_ips: Vec", - "type_info": "InetArray" - }, - { - "ordinal": 2, - "name": "preshared_key", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "is_authorized!", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - true, - null - ] - }, - "hash": "febd6a2dc4a335cc25d730d3e823ef3e8928bea54bd8407fccf74b868f6059b2" -} diff --git a/crates/defguard_common/src/db/models/device.rs b/crates/defguard_common/src/db/models/device.rs index d5e0ba2196..218e5f7596 100644 --- a/crates/defguard_common/src/db/models/device.rs +++ b/crates/defguard_common/src/db/models/device.rs @@ -181,10 +181,13 @@ impl DeviceInfo { DeviceNetworkInfo, "SELECT wnd.wireguard_network_id network_id, \ wnd.wireguard_ips \"device_wireguard_ips: Vec\", \ - active_session.preshared_key preshared_key, \ + CASE \ + WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN NULL::text \ + ELSE active_session.preshared_key \ + END \"preshared_key?\", \ CASE \ WHEN n.location_mfa_mode = 'disabled'::location_mfa_mode THEN TRUE \ - ELSE active_session.id IS NOT NULL \ + ELSE active_session.preshared_key IS NOT NULL \ END \"is_authorized!\" \ FROM wireguard_network_device wnd \ JOIN wireguard_network n ON n.id = wnd.wireguard_network_id \ @@ -347,10 +350,10 @@ impl WireguardNetworkDevice { ) -> DeviceNetworkInfo { let (preshared_key, is_authorized) = if !network.mfa_enabled() { (None, true) - } else if let Some(session) = active_session { - (session.preshared_key.clone(), true) } else { - (None, false) + let preshared_key = active_session.and_then(|session| session.preshared_key.clone()); + let is_authorized = preshared_key.is_some(); + (preshared_key, is_authorized) }; DeviceNetworkInfo { @@ -1100,7 +1103,7 @@ mod test { use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; - use crate::db::setup_pool; + use crate::db::{models::vpn_client_session::VpnClientMfaMethod, setup_pool}; impl Device { /// Create new device and assign IP in a given network @@ -1463,7 +1466,7 @@ mod test { } #[sqlx::test] - async fn test_runtime_mfa_state_requires_session_preshared_key_for_mfa_runtime_reads( + async fn test_runtime_mfa_state_marks_mfa_session_without_preshared_key_as_unauthorized( _: PgPoolOptions, options: PgConnectOptions, ) { @@ -1500,7 +1503,7 @@ mod test { created_at: Utc::now().naive_utc(), connected_at: None, disconnected_at: None, - mfa_method: None, + mfa_method: Some(VpnClientMfaMethod::Totp), state: VpnClientSessionState::New, preshared_key: None, }; @@ -1509,11 +1512,64 @@ mod test { wireguard_network_device.to_device_network_info(&network, Some(&active_session)); assert_eq!(network_info.preshared_key, None); + assert!(!network_info.is_authorized); + } + + #[sqlx::test] + async fn test_runtime_mfa_state_keeps_session_preshared_key_for_authorized_runtime_reads( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let network = WireguardNetwork::new( + "runtime-mfa-network".into(), + 51820, + "vpn.example.com".into(), + None, + Vec::::new(), + false, + false, + false, + LocationMfaMode::Internal, + ServiceLocationMode::Disabled, + ) + .try_set_address("10.1.1.1/24") + .unwrap() + .save(&pool) + .await + .unwrap(); + + let wireguard_network_device = WireguardNetworkDevice { + wireguard_network_id: network.id, + wireguard_ips: vec![IpAddr::from_str("10.1.1.2").unwrap()], + device_id: 1, + }; + let active_session = VpnClientSession { + id: 1, + location_id: network.id, + user_id: 1, + device_id: wireguard_network_device.device_id, + created_at: Utc::now().naive_utc(), + connected_at: Some(Utc::now().naive_utc()), + disconnected_at: None, + mfa_method: Some(VpnClientMfaMethod::Totp), + state: VpnClientSessionState::Connected, + preshared_key: Some("runtime-session-psk".into()), + }; + + let network_info = + wireguard_network_device.to_device_network_info(&network, Some(&active_session)); + + assert_eq!( + network_info.preshared_key, + Some("runtime-session-psk".into()) + ); assert!(network_info.is_authorized); } #[sqlx::test] - async fn test_device_info_does_not_expose_legacy_preshared_key_for_active_mfa_session( + async fn test_device_info_marks_mfa_session_without_preshared_key_as_unauthorized( _: PgPoolOptions, options: PgConnectOptions, ) { @@ -1566,11 +1622,166 @@ mod test { ); wireguard_network_device.insert(&pool).await.unwrap(); - VpnClientSession::new(network.id, user.id, device.id, None, None) - .save(&pool) - .await + let session = VpnClientSession::new( + network.id, + user.id, + device.id, + None, + Some(VpnClientMfaMethod::Totp), + ); + session.save(&pool).await.unwrap(); + + let device_info = DeviceInfo::from_device(&pool, device).await.unwrap(); + let network_info = device_info + .network_info + .into_iter() + .find(|info| info.network_id == network.id) .unwrap(); + assert!(!network_info.is_authorized); + assert_eq!(network_info.preshared_key, None); + } + + #[sqlx::test] + async fn test_device_info_keeps_mfa_session_preshared_key_for_authorized_full_sync_reads( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password"), + "Tester", + "Test", + "test@test.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device".into(), + "pubkey".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let network = WireguardNetwork::new( + "device-info-network".into(), + 51820, + "vpn.example.com".into(), + None, + Vec::::new(), + false, + false, + false, + LocationMfaMode::Internal, + ServiceLocationMode::Disabled, + ) + .try_set_address("10.1.1.1/24") + .unwrap() + .save(&pool) + .await + .unwrap(); + + let wireguard_network_device = WireguardNetworkDevice::new( + network.id, + device.id, + [IpAddr::from_str("10.1.1.2").unwrap()], + ); + wireguard_network_device.insert(&pool).await.unwrap(); + + let mut session = VpnClientSession::new( + network.id, + user.id, + device.id, + Some(Utc::now().naive_utc()), + Some(VpnClientMfaMethod::Totp), + ); + session.preshared_key = Some("device-info-session-psk".into()); + session.save(&pool).await.unwrap(); + + let device_info = DeviceInfo::from_device(&pool, device).await.unwrap(); + let network_info = device_info + .network_info + .into_iter() + .find(|info| info.network_id == network.id) + .unwrap(); + + assert!(network_info.is_authorized); + assert_eq!( + network_info.preshared_key, + Some("device-info-session-psk".into()) + ); + } + + #[sqlx::test] + async fn test_device_info_keeps_non_mfa_location_authorized_without_exposing_session_preshared_key( + _: PgPoolOptions, + options: PgConnectOptions, + ) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password"), + "Tester", + "Test", + "test@test.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device = Device::new( + "device".into(), + "pubkey".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let network = WireguardNetwork::new( + "device-info-network".into(), + 51820, + "vpn.example.com".into(), + None, + Vec::::new(), + false, + false, + false, + LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, + ) + .try_set_address("10.1.1.1/24") + .unwrap() + .save(&pool) + .await + .unwrap(); + + let wireguard_network_device = WireguardNetworkDevice::new( + network.id, + device.id, + [IpAddr::from_str("10.1.1.2").unwrap()], + ); + wireguard_network_device.insert(&pool).await.unwrap(); + + let mut session = VpnClientSession::new(network.id, user.id, device.id, None, None); + session.preshared_key = Some("legacy-session-psk".into()); + session.save(&pool).await.unwrap(); + let device_info = DeviceInfo::from_device(&pool, device).await.unwrap(); let network_info = device_info .network_info diff --git a/crates/defguard_core/src/location_management/allowed_peers.rs b/crates/defguard_core/src/location_management/allowed_peers.rs index fef967652d..3a4ccc4677 100644 --- a/crates/defguard_core/src/location_management/allowed_peers.rs +++ b/crates/defguard_core/src/location_management/allowed_peers.rs @@ -28,10 +28,39 @@ where return Ok(Vec::new()); } + if !location.mfa_enabled() { + let rows = query!( + "SELECT d.wireguard_pubkey pubkey, \ + ARRAY( + SELECT host(ip) + FROM unnest(wnd.wireguard_ips) AS ip + ) \"allowed_ips!: Vec\" \ + FROM wireguard_network_device wnd \ + JOIN device d ON wnd.device_id = d.id \ + JOIN \"user\" u ON d.user_id = u.id \ + WHERE wireguard_network_id = $1 \ + AND d.configured \ + AND u.is_active \ + ORDER BY d.id ASC", + location.id, + ) + .fetch_all(executor) + .await?; + + return Ok(rows + .into_iter() + .map(|row| Peer { + pubkey: row.pubkey, + allowed_ips: row.allowed_ips, + preshared_key: None, + keepalive_interval: Some(location.keepalive_interval.cast_unsigned()), + }) + .collect()); + } + let rows = query!( "SELECT d.wireguard_pubkey pubkey, \ - active_session.preshared_key preshared_key, \ - -- TODO possible to not use ARRAY-unnest here? + active_session.preshared_key \"preshared_key!\", \ ARRAY( SELECT host(ip) FROM unnest(wnd.wireguard_ips) AS ip @@ -39,45 +68,34 @@ where FROM wireguard_network_device wnd \ JOIN device d ON wnd.device_id = d.id \ JOIN \"user\" u ON d.user_id = u.id \ - LEFT JOIN LATERAL ( \ - SELECT id, preshared_key \ + JOIN LATERAL ( \ + SELECT preshared_key \ FROM vpn_client_session \ WHERE location_id = wnd.wireguard_network_id \ AND device_id = wnd.device_id \ AND state IN ('new', 'connected') \ + AND preshared_key IS NOT NULL \ ORDER BY created_at DESC, id DESC \ LIMIT 1 \ ) active_session ON true \ WHERE wireguard_network_id = $1 \ - AND (NOT $2 OR active_session.preshared_key IS NOT NULL) \ - AND d.configured AND u.is_active \ + AND d.configured \ + AND u.is_active \ ORDER BY d.id ASC", location.id, - location.mfa_enabled() ) .fetch_all(executor) .await?; - // keepalive has to be added manually because Postgres - // doesn't support unsigned integers - let result = rows + Ok(rows .into_iter() .map(|row| Peer { pubkey: row.pubkey, allowed_ips: row.allowed_ips, - // Don't send preshared key if MFA is not enabled, it can't be used and may - // cause issues with clients connecting if they expect no preshared key - // e.g. when you disable MFA on a location - preshared_key: if location.mfa_enabled() { - row.preshared_key - } else { - None - }, + preshared_key: Some(row.preshared_key), keepalive_interval: Some(location.keepalive_interval.cast_unsigned()), }) - .collect(); - - Ok(result) + .collect()) } #[cfg(test)] @@ -292,7 +310,7 @@ mod test { } #[sqlx::test] - async fn test_get_location_allowed_peers_keeps_non_mfa_peer_without_session_preshared_key( + async fn test_get_location_allowed_peers_keeps_non_mfa_peer_without_session_lookup_dependency( _: PgPoolOptions, options: PgConnectOptions, ) { @@ -337,11 +355,6 @@ mod test { ); network_device.insert(&pool).await.unwrap(); - VpnClientSession::new(network.id, user.id, device.id, None, None) - .save(&pool) - .await - .unwrap(); - let peers = get_location_allowed_peers(&network, &pool).await.unwrap(); assert_eq!(peers.len(), 1); diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index f3925f0bcd..9b39e424db 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -883,8 +883,8 @@ mod tests { use defguard_proto::gateway::core_response; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - use tokio::sync::{broadcast, mpsc::unbounded_channel}; use tokio::sync::watch; + use tokio::sync::{broadcast, mpsc::unbounded_channel}; use super::{GatewayHandler, GatewayUpdatesHandler}; use defguard_common::db::Id; diff --git a/crates/defguard_session_manager/tests/session_manager/db_invariants.rs b/crates/defguard_session_manager/tests/session_manager/db_invariants.rs index 3b62ccf2fb..50efa2d353 100644 --- a/crates/defguard_session_manager/tests/session_manager/db_invariants.rs +++ b/crates/defguard_session_manager/tests/session_manager/db_invariants.rs @@ -8,6 +8,40 @@ use sqlx::{ use crate::common::{attach_device_to_location, create_device, create_location, create_user}; const ACTIVE_SESSION_UNIQUE_INDEX: &str = "vpn_client_session_active_location_device_unique"; +const PRESHARED_KEY_MIGRATION_SQL: &str = include_str!( + "../../../../migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql" +); + +fn extract_migration_statement(start_marker: &str, end_marker: &str) -> &'static str { + let Some(start_offset) = PRESHARED_KEY_MIGRATION_SQL.find(start_marker) else { + panic!("migration SQL is missing expected start marker: {start_marker}"); + }; + + let migration_from_start = &PRESHARED_KEY_MIGRATION_SQL[start_offset..]; + let Some(end_offset) = migration_from_start.find(end_marker) else { + panic!("migration SQL is missing expected end marker: {end_marker}"); + }; + + &migration_from_start[..end_offset + end_marker.len()] +} + +fn duplicate_active_session_cleanup_sql() -> &'static str { + extract_migration_statement( + "WITH ranked_active_sessions AS (", + " AND ranked_session.rank > 1;", + ) +} + +fn create_active_session_unique_index_sql() -> &'static str { + extract_migration_statement( + "CREATE UNIQUE INDEX vpn_client_session_active_location_device_unique", + " WHERE state IN ('new', 'connected');", + ) +} + +fn active_mfa_session_precondition_sql() -> &'static str { + extract_migration_statement("DO $$", "END $$;") +} async fn insert_session( pool: &sqlx::PgPool, @@ -44,6 +78,109 @@ async fn count_active_sessions(pool: &sqlx::PgPool, location_id: Id, device_id: .expect("failed to count active sessions") } +#[sqlx::test] +async fn test_migration_cleanup_keeps_newest_active_session_before_unique_index( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let location = create_location(&pool).await; + let user = create_user(&pool).await; + let device = create_device(&pool, user.id).await; + attach_device_to_location(&pool, location.id, device.id).await; + + query("DROP INDEX IF EXISTS vpn_client_session_active_location_device_unique") + .execute(&pool) + .await + .expect("failed to drop active-session unique index"); + + let older_session_id = insert_session(&pool, location.id, user.id, device.id, "new") + .await + .expect("failed to create older active session"); + let newer_session_id = insert_session(&pool, location.id, user.id, device.id, "connected") + .await + .expect("failed to create newer active session"); + + query(duplicate_active_session_cleanup_sql()) + .execute(&pool) + .await + .expect("failed to run duplicate-session cleanup"); + + query(create_active_session_unique_index_sql()) + .execute(&pool) + .await + .expect("failed to recreate active-session unique index"); + + assert_eq!( + count_active_sessions(&pool, location.id, device.id).await, + 1 + ); + + let remaining_active_session_id: Id = query_scalar( + "SELECT id FROM vpn_client_session + WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') + ORDER BY created_at DESC, id DESC + LIMIT 1", + ) + .bind(location.id) + .bind(device.id) + .fetch_one(&pool) + .await + .expect("failed to fetch remaining active session"); + + assert_eq!(remaining_active_session_id, newer_session_id); + + let disconnected_at = query_scalar::<_, Option>( + "SELECT disconnected_at FROM vpn_client_session WHERE id = $1", + ) + .bind(older_session_id) + .fetch_one(&pool) + .await + .expect("failed to fetch disconnected_at for older session"); + + assert!(disconnected_at.is_some()); +} + +#[sqlx::test] +async fn test_migration_precondition_rejects_active_mfa_sessions( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let location = create_location(&pool).await; + let user = create_user(&pool).await; + let device = create_device(&pool, user.id).await; + attach_device_to_location(&pool, location.id, device.id).await; + + query_scalar::<_, Id>( + "INSERT INTO vpn_client_session (location_id, user_id, device_id, connected_at, mfa_method, state, preshared_key) + VALUES ($1, $2, $3, NULL, 'totp'::vpn_client_mfa_method, 'new'::vpn_client_session_state, NULL) + RETURNING id", + ) + .bind(location.id) + .bind(user.id) + .bind(device.id) + .fetch_one(&pool) + .await + .expect("failed to create active MFA session"); + + let error = query(active_mfa_session_precondition_sql()) + .execute(&pool) + .await + .expect_err("expected migration precondition to reject active MFA sessions"); + + match error { + sqlx::Error::Database(database_error) => { + assert!( + database_error + .message() + .contains("Active MFA VPN sessions must be disconnected before migration") + ); + } + other => panic!("expected migration precondition error, got {other:?}"), + } +} + #[sqlx::test] async fn test_db_rejects_second_active_session_for_same_device_location( _: PgPoolOptions, From e17cd4f25b5dac63a654780b7c29c52c54e14998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 19 Mar 2026 06:56:19 +0100 Subject: [PATCH 12/19] review fixes --- .../tests/session_manager/db_invariants.rs | 137 ------------------ ..._vpn_client_session_preshared_key.down.sql | 7 +- ...0]_vpn_client_session_preshared_key.up.sql | 5 +- 3 files changed, 6 insertions(+), 143 deletions(-) diff --git a/crates/defguard_session_manager/tests/session_manager/db_invariants.rs b/crates/defguard_session_manager/tests/session_manager/db_invariants.rs index 50efa2d353..3b62ccf2fb 100644 --- a/crates/defguard_session_manager/tests/session_manager/db_invariants.rs +++ b/crates/defguard_session_manager/tests/session_manager/db_invariants.rs @@ -8,40 +8,6 @@ use sqlx::{ use crate::common::{attach_device_to_location, create_device, create_location, create_user}; const ACTIVE_SESSION_UNIQUE_INDEX: &str = "vpn_client_session_active_location_device_unique"; -const PRESHARED_KEY_MIGRATION_SQL: &str = include_str!( - "../../../../migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql" -); - -fn extract_migration_statement(start_marker: &str, end_marker: &str) -> &'static str { - let Some(start_offset) = PRESHARED_KEY_MIGRATION_SQL.find(start_marker) else { - panic!("migration SQL is missing expected start marker: {start_marker}"); - }; - - let migration_from_start = &PRESHARED_KEY_MIGRATION_SQL[start_offset..]; - let Some(end_offset) = migration_from_start.find(end_marker) else { - panic!("migration SQL is missing expected end marker: {end_marker}"); - }; - - &migration_from_start[..end_offset + end_marker.len()] -} - -fn duplicate_active_session_cleanup_sql() -> &'static str { - extract_migration_statement( - "WITH ranked_active_sessions AS (", - " AND ranked_session.rank > 1;", - ) -} - -fn create_active_session_unique_index_sql() -> &'static str { - extract_migration_statement( - "CREATE UNIQUE INDEX vpn_client_session_active_location_device_unique", - " WHERE state IN ('new', 'connected');", - ) -} - -fn active_mfa_session_precondition_sql() -> &'static str { - extract_migration_statement("DO $$", "END $$;") -} async fn insert_session( pool: &sqlx::PgPool, @@ -78,109 +44,6 @@ async fn count_active_sessions(pool: &sqlx::PgPool, location_id: Id, device_id: .expect("failed to count active sessions") } -#[sqlx::test] -async fn test_migration_cleanup_keeps_newest_active_session_before_unique_index( - _: PgPoolOptions, - options: PgConnectOptions, -) { - let pool = setup_pool(options).await; - let location = create_location(&pool).await; - let user = create_user(&pool).await; - let device = create_device(&pool, user.id).await; - attach_device_to_location(&pool, location.id, device.id).await; - - query("DROP INDEX IF EXISTS vpn_client_session_active_location_device_unique") - .execute(&pool) - .await - .expect("failed to drop active-session unique index"); - - let older_session_id = insert_session(&pool, location.id, user.id, device.id, "new") - .await - .expect("failed to create older active session"); - let newer_session_id = insert_session(&pool, location.id, user.id, device.id, "connected") - .await - .expect("failed to create newer active session"); - - query(duplicate_active_session_cleanup_sql()) - .execute(&pool) - .await - .expect("failed to run duplicate-session cleanup"); - - query(create_active_session_unique_index_sql()) - .execute(&pool) - .await - .expect("failed to recreate active-session unique index"); - - assert_eq!( - count_active_sessions(&pool, location.id, device.id).await, - 1 - ); - - let remaining_active_session_id: Id = query_scalar( - "SELECT id FROM vpn_client_session - WHERE location_id = $1 AND device_id = $2 AND state IN ('new', 'connected') - ORDER BY created_at DESC, id DESC - LIMIT 1", - ) - .bind(location.id) - .bind(device.id) - .fetch_one(&pool) - .await - .expect("failed to fetch remaining active session"); - - assert_eq!(remaining_active_session_id, newer_session_id); - - let disconnected_at = query_scalar::<_, Option>( - "SELECT disconnected_at FROM vpn_client_session WHERE id = $1", - ) - .bind(older_session_id) - .fetch_one(&pool) - .await - .expect("failed to fetch disconnected_at for older session"); - - assert!(disconnected_at.is_some()); -} - -#[sqlx::test] -async fn test_migration_precondition_rejects_active_mfa_sessions( - _: PgPoolOptions, - options: PgConnectOptions, -) { - let pool = setup_pool(options).await; - let location = create_location(&pool).await; - let user = create_user(&pool).await; - let device = create_device(&pool, user.id).await; - attach_device_to_location(&pool, location.id, device.id).await; - - query_scalar::<_, Id>( - "INSERT INTO vpn_client_session (location_id, user_id, device_id, connected_at, mfa_method, state, preshared_key) - VALUES ($1, $2, $3, NULL, 'totp'::vpn_client_mfa_method, 'new'::vpn_client_session_state, NULL) - RETURNING id", - ) - .bind(location.id) - .bind(user.id) - .bind(device.id) - .fetch_one(&pool) - .await - .expect("failed to create active MFA session"); - - let error = query(active_mfa_session_precondition_sql()) - .execute(&pool) - .await - .expect_err("expected migration precondition to reject active MFA sessions"); - - match error { - sqlx::Error::Database(database_error) => { - assert!( - database_error - .message() - .contains("Active MFA VPN sessions must be disconnected before migration") - ); - } - other => panic!("expected migration precondition error, got {other:?}"), - } -} - #[sqlx::test] async fn test_db_rejects_second_active_session_for_same_device_location( _: PgPoolOptions, diff --git a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql index d19a7eae07..5fd9557e6a 100644 --- a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql +++ b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.down.sql @@ -3,9 +3,10 @@ ALTER TABLE wireguard_network_device ADD COLUMN is_authorized bool NOT NULL DEFAULT false, ADD COLUMN authorized_at timestamp without time zone NULL; --- Rollback is lossy: only preshared_key is repopulated from the latest active --- session per (device_id, location_id); is_authorized and authorized_at are --- recreated with default/NULL values and are not reconstructed. +-- Rollback is lossy: only preshared_key is repopulated from an active +-- session with a non-null preshared_key per (device_id, location_id); +-- is_authorized and authorized_at are recreated with default/NULL values and +-- are not reconstructed. UPDATE wireguard_network_device AS network_device SET preshared_key = latest_active_session.preshared_key FROM ( diff --git a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql index d530297d2a..ceac7eef68 100644 --- a/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql +++ b/migrations/20260317120000_[2.0.0]_vpn_client_session_preshared_key.up.sql @@ -1,7 +1,6 @@ --- Add session-level preshared_key, enforce at most one active session per --- (location_id, device_id), and drop device-level preshared_key/auth fields. +-- Add session-level preshared_key, enforce the active-session invariant, +-- then drop device-level preshared_key/auth fields. -- WARNING: rollback is lossy for dropped wireguard_network_device columns. --- Do not yet require preshared_key for active MFA sessions. ALTER TABLE vpn_client_session ADD COLUMN preshared_key text NULL; CREATE UNIQUE INDEX vpn_client_session_active_location_device_unique From cb2391a9cbc620d7ccab4d0a719671ff84a1ba9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 19 Mar 2026 07:55:32 +0100 Subject: [PATCH 13/19] update flake inputs --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index d909857580..1b6bbdcda0 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1773646010, - "narHash": "sha256-iYrs97hS7p5u4lQzuNWzuALGIOdkPXvjz7bviiBjUu8=", + "lastModified": 1773821835, + "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5b2c2d84341b2afb5647081c1386a80d7a8d8605", + "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1773630837, - "narHash": "sha256-zJhgAGnbVKeBMJOb9ctZm4BGS/Rnrz+5lfSXTVah4HQ=", + "lastModified": 1773889863, + "narHash": "sha256-tSsmZOHBgq4qfu5MNCAEsKZL1cI4avNLw2oUTXWeb74=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "f600ea449c7b5bb596fa1cf21c871cc5b9e31316", + "rev": "dbfd51be2692cb7022e301d14c139accb4ee63f0", "type": "github" }, "original": { From d4575b5a25577d4b4200c17b8c4676e6dfdfc54f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 19 Mar 2026 07:56:30 +0100 Subject: [PATCH 14/19] update query data --- ...d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json | 2 +- ...10a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json b/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json index 8a6776d365..0e36c94cdb 100644 --- a/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json +++ b/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json @@ -82,7 +82,7 @@ false, false, true, - true, + false, false, false, false, diff --git a/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json b/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json index 548a89c193..d62e957d33 100644 --- a/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json +++ b/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json @@ -80,7 +80,7 @@ false, false, true, - true, + false, false, false, false, From 20143d3130e9c2e5847bc72a56c45a5f22975786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 19 Mar 2026 09:32:08 +0100 Subject: [PATCH 15/19] review fixes --- .../src/grpc/proxy/client_mfa.rs | 16 ++++-- .../src/location_management/allowed_peers.rs | 12 ++--- .../defguard_gateway_manager/src/handler.rs | 53 ++++++++----------- 3 files changed, 38 insertions(+), 43 deletions(-) diff --git a/crates/defguard_core/src/grpc/proxy/client_mfa.rs b/crates/defguard_core/src/grpc/proxy/client_mfa.rs index d0fffad104..e379eed6c1 100644 --- a/crates/defguard_core/src/grpc/proxy/client_mfa.rs +++ b/crates/defguard_core/src/grpc/proxy/client_mfa.rs @@ -889,6 +889,9 @@ mod tests { grpc::GatewayEvent, }; + const REPLACEMENT_MFA_PRESHARED_KEY: &str = "replacement-mfa-psk"; + const NEW_MFA_PRESHARED_KEY: &str = "new-psk"; + #[sqlx::test] async fn test_replacing_connected_mfa_session_emits_mfa_disconnect_event( _: PgPoolOptions, @@ -920,7 +923,7 @@ mod tests { &user, &device, VpnClientMfaMethod::Totp, - "replacement-mfa-psk".to_string(), + REPLACEMENT_MFA_PRESHARED_KEY.to_string(), ) .await .expect("should replace connected MFA session"); @@ -995,7 +998,7 @@ mod tests { &user, &device, VpnClientMfaMethod::Totp, - "replacement-mfa-psk".to_string(), + REPLACEMENT_MFA_PRESHARED_KEY.to_string(), ) .await .expect("should replace new MFA session"); @@ -1054,7 +1057,7 @@ mod tests { &user, &device, VpnClientMfaMethod::Totp, - "replacement-mfa-psk".to_string(), + REPLACEMENT_MFA_PRESHARED_KEY.to_string(), ) .await .expect("should replace connected non-MFA session"); @@ -1194,7 +1197,7 @@ mod tests { &user, &device, VpnClientMfaMethod::Totp, - "new-psk".to_string(), + NEW_MFA_PRESHARED_KEY.to_string(), ) .await .expect("failed to create replacement MFA session"); @@ -1215,7 +1218,10 @@ mod tests { .expect("failed to fetch active sessions"); assert_eq!(active_sessions.len(), 1); assert_eq!(active_sessions[0].id, new_session.id); - assert_eq!(active_sessions[0].preshared_key.as_deref(), Some("new-psk")); + assert_eq!( + active_sessions[0].preshared_key.as_deref(), + Some(NEW_MFA_PRESHARED_KEY) + ); match gateway_rx.try_recv() { Ok(GatewayEvent::MfaSessionDisconnected(location_id, disconnected_device)) => { diff --git a/crates/defguard_core/src/location_management/allowed_peers.rs b/crates/defguard_core/src/location_management/allowed_peers.rs index 3a4ccc4677..88e5bc1c14 100644 --- a/crates/defguard_core/src/location_management/allowed_peers.rs +++ b/crates/defguard_core/src/location_management/allowed_peers.rs @@ -31,9 +31,9 @@ where if !location.mfa_enabled() { let rows = query!( "SELECT d.wireguard_pubkey pubkey, \ - ARRAY( - SELECT host(ip) - FROM unnest(wnd.wireguard_ips) AS ip + ARRAY( \ + SELECT host(ip) \ + FROM unnest(wnd.wireguard_ips) AS ip \ ) \"allowed_ips!: Vec\" \ FROM wireguard_network_device wnd \ JOIN device d ON wnd.device_id = d.id \ @@ -61,9 +61,9 @@ where let rows = query!( "SELECT d.wireguard_pubkey pubkey, \ active_session.preshared_key \"preshared_key!\", \ - ARRAY( - SELECT host(ip) - FROM unnest(wnd.wireguard_ips) AS ip + ARRAY( \ + SELECT host(ip) \ + FROM unnest(wnd.wireguard_ips) AS ip \ ) \"allowed_ips!: Vec\" \ FROM wireguard_network_device wnd \ JOIN device d ON wnd.device_id = d.id \ diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index 9b39e424db..9a66343f69 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -469,16 +469,16 @@ impl GatewayUpdatesHandler { if !is_authorized { debug!( - "Skipping gateway peer update for WireGuard device {} in MFA enabled location {} because there is no active MFA session", - peer_label, self.network.name + "Skipping gateway peer update for WireGuard device {peer_label} in MFA enabled location {} because there is no active MFA session", + self.network.name ); return None; } let Some(preshared_key) = preshared_key else { debug!( - "Skipping gateway peer update for WireGuard device {} in location {} because the runtime preshared key is missing", - peer_label, self.network.name + "Skipping gateway peer update for WireGuard device {peer_label} in location {} because the runtime preshared key is missing", + self.network.name ); return None; }; @@ -870,6 +870,7 @@ mod tests { use chrono::Utc; use defguard_common::db::{ + Id, models::{ Device, DeviceType, User, device::WireguardNetworkDevice, @@ -881,37 +882,25 @@ mod tests { }; use defguard_core::grpc::GatewayEvent; use defguard_proto::gateway::core_response; - use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; - use tokio::sync::watch; - use tokio::sync::{broadcast, mpsc::unbounded_channel}; + use tokio::sync::{broadcast, mpsc::unbounded_channel, watch}; use super::{GatewayHandler, GatewayUpdatesHandler}; - use defguard_common::db::Id; fn test_network(location_mfa_mode: LocationMfaMode) -> WireguardNetwork { - serde_json::from_value(json!({ - "id": 1, - "name": "test-network", - "address": ["10.1.1.1/24"], - "port": 51820, - "pubkey": "network-pubkey", - "prvkey": "network-prvkey", - "endpoint": "127.0.0.1", - "dns": null, - "mtu": 1420, - "fwmark": 0, - "allowed_ips": [], - "allow_all_groups": true, - "connected_at": null, - "acl_enabled": false, - "acl_default_allow": false, - "keepalive_interval": 25, - "peer_disconnect_threshold": 300, - "location_mfa_mode": location_mfa_mode, - "service_location_mode": ServiceLocationMode::Disabled, - })) - .unwrap() + WireguardNetwork::new( + "test-network".into(), + 51820, + "127.0.0.1".into(), + None, + Vec::new(), + true, + false, + false, + location_mfa_mode, + ServiceLocationMode::Disabled, + ) + .with_id(1) } fn test_handler(location_mfa_mode: LocationMfaMode) -> GatewayUpdatesHandler { @@ -938,7 +927,7 @@ mod tests { .unwrap(); assert_eq!(peer.pubkey, "device-pubkey"); - assert_eq!(peer.allowed_ips, vec!["10.1.1.2"]); + assert_eq!(peer.allowed_ips, ["10.1.1.2"]); assert_eq!(peer.preshared_key, None); assert_eq!(peer.keepalive_interval, Some(25)); } @@ -1083,7 +1072,7 @@ mod tests { .iter() .map(|peer| (peer.pubkey.as_str(), peer.preshared_key.as_deref())) .collect::>(), - vec![ + [ ("pubkey-new", Some("new-session-psk")), ("pubkey-connected", Some("connected-session-psk")), ] From 6f48e1c3e61a571968d63a21df3b8528e3e87f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 19 Mar 2026 09:32:16 +0100 Subject: [PATCH 16/19] update frontend dependencies --- web/package.json | 8 +- web/pnpm-lock.yaml | 324 ++++++++++++++++++++++----------------------- 2 files changed, 165 insertions(+), 167 deletions(-) diff --git a/web/package.json b/web/package.json index 34e6e8fcba..9a46c21577 100644 --- a/web/package.json +++ b/web/package.json @@ -21,8 +21,8 @@ "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", "@tanstack/react-form": "^1.28.5", - "@tanstack/react-query": "^5.90.21", - "@tanstack/react-router": "^1.167.4", + "@tanstack/react-query": "^5.91.0", + "@tanstack/react-router": "^1.167.5", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.23", "@uidotdev/usehooks": "^2.4.1", @@ -57,7 +57,7 @@ "@tanstack/react-devtools": "^0.10.0", "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-router-devtools": "^1.166.9", - "@tanstack/router-plugin": "^1.166.13", + "@tanstack/router-plugin": "^1.166.14", "@types/byte-size": "^8.1.2", "@types/humanize-duration": "^3.27.4", "@types/lodash-es": "^4.17.12", @@ -75,7 +75,7 @@ "stylelint-config-standard-scss": "^17.0.0", "stylelint-scss": "^7.0.0", "typescript": "~5.9.3", - "vite": "^8.0.0", + "vite": "^8.0.1", "vite-plugin-image-optimizer": "^2.0.3" } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 8246963e53..53c7ff7e5b 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -33,11 +33,11 @@ importers: specifier: ^1.28.5 version: 1.28.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': - specifier: ^5.90.21 - version: 5.90.21(react@19.2.4) + specifier: ^5.91.0 + version: 5.91.0(react@19.2.4) '@tanstack/react-router': - specifier: ^1.167.4 - version: 1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^1.167.5 + version: 1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -122,19 +122,19 @@ importers: version: 2.4.7 '@tanstack/devtools-vite': specifier: ^0.6.0 - version: 0.6.0(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) + version: 0.6.0(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) '@tanstack/react-devtools': specifier: ^0.10.0 version: 0.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.9) '@tanstack/react-query-devtools': specifier: ^5.91.3 - version: 5.91.3(@tanstack/react-query@5.90.21(react@19.2.4))(react@19.2.4) + version: 5.91.3(@tanstack/react-query@5.91.0(react@19.2.4))(react@19.2.4) '@tanstack/react-router-devtools': specifier: ^1.166.9 - version: 1.166.9(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.4)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.166.9(@tanstack/react-router@1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.5)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/router-plugin': - specifier: ^1.166.13 - version: 1.166.13(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) + specifier: ^1.166.14 + version: 1.166.14(@tanstack/react-router@1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 @@ -158,7 +158,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) + version: 6.0.1(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) autoprefixer: specifier: ^10.4.27 version: 10.4.27(postcss@8.5.8) @@ -187,11 +187,11 @@ importers: specifier: ~5.9.3 version: 5.9.3 vite: - specifier: ^8.0.0 - version: 8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) + specifier: ^8.0.1 + version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) vite-plugin-image-optimizer: specifier: ^2.0.3 - version: 2.0.3(sharp@0.34.5)(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) + version: 2.0.3(sharp@0.34.5)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) packages: @@ -788,12 +788,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/runtime@0.115.0': - resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==} - engines: {node: ^20.19.0 || >=22.12.0} - - '@oxc-project/types@0.115.0': - resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + '@oxc-project/types@0.120.0': + resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} @@ -909,107 +905,107 @@ packages: react-redux: optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} + '@rolldown/binding-android-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.9': - resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} + '@rolldown/binding-darwin-x64@1.0.0-rc.10': + resolution: {integrity: sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.9': - resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': + resolution: {integrity: sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': - resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': + resolution: {integrity: sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': - resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': + resolution: {integrity: sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': - resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': + resolution: {integrity: sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': - resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': + resolution: {integrity: sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': - resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': + resolution: {integrity: sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': - resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': + resolution: {integrity: sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-rc.10': + resolution: {integrity: sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==} + '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} - '@rolldown/pluginutils@1.0.0-rc.9': - resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} - '@shortercode/webzip@1.1.1-0': resolution: {integrity: sha512-c+9LbU7MTqWDXS73Pf7vEm5AyFFfbuuHiMgqmApwkwD5BrlELKaSqKjofG4Gqd3c/NgF8fO0iR5NbPdE26jF2Q==} @@ -1128,8 +1124,8 @@ packages: resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/query-core@5.90.20': - resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} + '@tanstack/query-core@5.91.0': + resolution: {integrity: sha512-FYXN8Kk9Q5VKuV6AIVaNwMThSi0nvAtR4X7HQoigf6ePOtFcavJYVIzgFhOVdtbBQtCJE3KimDIMMJM2DR1hjw==} '@tanstack/query-devtools@5.93.0': resolution: {integrity: sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg==} @@ -1158,8 +1154,8 @@ packages: '@tanstack/react-query': ^5.90.20 react: ^18 || ^19 - '@tanstack/react-query@5.90.21': - resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==} + '@tanstack/react-query@5.91.0': + resolution: {integrity: sha512-S8FODsDTNv0Ym+o/JVBvA6EWiWVhg6K2Q4qFehZyFKk6uW4H9OPbXl4kyiN9hAly0uHJ/1GEbR6kAI4MZWfjEA==} peerDependencies: react: ^18 || ^19 @@ -1175,8 +1171,8 @@ packages: '@tanstack/router-core': optional: true - '@tanstack/react-router@1.167.4': - resolution: {integrity: sha512-VpbZh382zX3WF4+X2Z+EUyd8eJhJyjg9C6ByYwrVZiWbhgbMK4+zQQIG2+lCAlIlDi7SV8fDcGL09NA8Z2kpGQ==} + '@tanstack/react-router@1.167.5': + resolution: {integrity: sha512-s1nP6l/7BYZfSwhoNbB7/rUmZ07q/AvkmhBoiDQl3tgy5dpb9Q1qjtIapYdvCOrao1aA/QCaWqxcbGc2Ct1bvQ==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -1201,8 +1197,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.167.4': - resolution: {integrity: sha512-Gk5V9Zr5JFJ4SbLyCheQLJ3MnXddccENPA+DJRz+9g3QxtN8DJB8w8KCUCgDeYlWp4LvmO4nX3fy3tupqVP2Pw==} + '@tanstack/router-core@1.167.5': + resolution: {integrity: sha512-8fRgJ0zNJf77R4grCaJQ5Imatjyc4YT5v8rlsPkYYYeUlcFNLbuFRhLlAMdND9gRUMznpnbRDXngpTPgx2K7HQ==} engines: {node: '>=20.19'} hasBin: true @@ -1216,17 +1212,17 @@ packages: csstype: optional: true - '@tanstack/router-generator@1.166.12': - resolution: {integrity: sha512-2HdxSTbCkbU9JeYogKVigIlXoLtIJE1x5rbEov+ZLTPjGCO9kicNQuljqg9Js+u2/ahtWewNrE5u1QCAyxmpIg==} + '@tanstack/router-generator@1.166.13': + resolution: {integrity: sha512-ALxSs6OzimiSgpOuIm+AXmc7eUx/oGPwSPpdQbpZ/kX7WHRh6qM7lv8DAN0K3jWcBpzF8eeOIdryWryX8gH+Yg==} engines: {node: '>=20.19'} - '@tanstack/router-plugin@1.166.13': - resolution: {integrity: sha512-xG3ND3AlMe6DN9PihJAYUbQJevqJvVdzN1QpZbfU1/jkHurL97ynP2yXfmMTh8Qgi1K+SWRko4bi7iZlYP9SUw==} + '@tanstack/router-plugin@1.166.14': + resolution: {integrity: sha512-hypyj0qlsAbJf60/glmVYqSVwnRB4hKRrMCUsSXjrPdO2g6gs3z6xHmcWsHQ831C4G9+bSFEK9Uy5EjO3A4THQ==} engines: {node: '>=20.19'} hasBin: true peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.167.4 + '@tanstack/react-router': ^1.167.5 vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' vite-plugin-solid: ^2.11.10 webpack: '>=5.92.0' @@ -1294,8 +1290,8 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -1459,8 +1455,8 @@ packages: '@75lb/nature': optional: true - cacheable@2.3.3: - resolution: {integrity: sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==} + cacheable@2.3.4: + resolution: {integrity: sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==} call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} @@ -1664,8 +1660,8 @@ packages: easy-file-picker@1.2.0: resolution: {integrity: sha512-GJxOW5s+g/pBr8Ha86a768yx0UZ6fYw+iAOrxK5HOzQ8q9hZxEJF0C8ztdAsH0mcze58FSpzv/d9flRCAuUKHg==} - electron-to-chromium@1.5.313: - resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} + electron-to-chromium@1.5.321: + resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1756,11 +1752,11 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - flat-cache@6.1.20: - resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} + flat-cache@6.1.21: + resolution: {integrity: sha512-2u7cJfSf7Th7NxEk/VzQjnPoglok2YCsevS7TSbJjcDQWJPbqUUnSYtriHSvtnq+fRZHy1s0ugk4ApnQyhPGoQ==} - flatted@3.4.1: - resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} @@ -1895,6 +1891,9 @@ packages: hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + hookified@2.1.0: + resolution: {integrity: sha512-ootKng4eaxNxa7rx6FJv2YKef3DuhqbEj3l70oGXwddPQEEnISm50TEZQclqiLTAtilT2nu7TErtCO523hHkyg==} + html-tags@5.1.0: resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} engines: {node: '>=20.10'} @@ -2363,8 +2362,8 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - qified@0.6.0: - resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==} + qified@0.9.0: + resolution: {integrity: sha512-4q61YgkHbY6gmwkqm0BsxyLDO3UYdrdiJTJ7JiaZb3xpW1duxn135SB7KqUEkCiuu5O4W+TtwEWP2VjmSRanvA==} engines: {node: '>=20'} qrcode.react@4.2.0: @@ -2482,8 +2481,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.0.0-rc.9: - resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} + rolldown@1.0.0-rc.10: + resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2850,13 +2849,13 @@ packages: svgo: optional: true - vite@8.0.0: - resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==} + vite@8.0.1: + resolution: {integrity: sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.0.0-alpha.31 + '@vitejs/devtools': ^0.1.0 esbuild: ^0.27.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -3444,9 +3443,7 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-project/runtime@0.115.0': {} - - '@oxc-project/types@0.115.0': {} + '@oxc-project/types@0.120.0': {} '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -3535,56 +3532,56 @@ snapshots: react: 19.2.4 react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) - '@rolldown/binding-android-arm64@1.0.0-rc.9': + '@rolldown/binding-android-arm64@1.0.0-rc.10': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.9': + '@rolldown/binding-darwin-x64@1.0.0-rc.10': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': optional: true - '@rolldown/pluginutils@1.0.0-rc.7': {} + '@rolldown/pluginutils@1.0.0-rc.10': {} - '@rolldown/pluginutils@1.0.0-rc.9': {} + '@rolldown/pluginutils@1.0.0-rc.7': {} '@shortercode/webzip@1.1.1-0': {} @@ -3681,7 +3678,7 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-vite@0.6.0(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': + '@tanstack/devtools-vite@0.6.0(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -3693,7 +3690,7 @@ snapshots: chalk: 5.6.2 launch-editor: 2.13.1 picomatch: 4.0.3 - vite: 8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) transitivePeerDependencies: - bufferutil - supports-color @@ -3725,7 +3722,7 @@ snapshots: '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/query-core@5.90.20': {} + '@tanstack/query-core@5.91.0': {} '@tanstack/query-devtools@5.93.0': {} @@ -3750,33 +3747,33 @@ snapshots: transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.91.3(@tanstack/react-query@5.90.21(react@19.2.4))(react@19.2.4)': + '@tanstack/react-query-devtools@5.91.3(@tanstack/react-query@5.91.0(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/query-devtools': 5.93.0 - '@tanstack/react-query': 5.90.21(react@19.2.4) + '@tanstack/react-query': 5.91.0(react@19.2.4) react: 19.2.4 - '@tanstack/react-query@5.90.21(react@19.2.4)': + '@tanstack/react-query@5.91.0(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.90.20 + '@tanstack/query-core': 5.91.0 react: 19.2.4 - '@tanstack/react-router-devtools@1.166.9(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.4)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-router-devtools@1.166.9(@tanstack/react-router@1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.5)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/react-router': 1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-devtools-core': 1.166.9(@tanstack/router-core@1.167.4)(csstype@3.2.3) + '@tanstack/react-router': 1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-devtools-core': 1.166.9(@tanstack/router-core@1.167.5)(csstype@3.2.3) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@tanstack/router-core': 1.167.4 + '@tanstack/router-core': 1.167.5 transitivePeerDependencies: - csstype - '@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-router@1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/history': 1.161.6 '@tanstack/react-store': 0.9.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-core': 1.167.4 + '@tanstack/router-core': 1.167.5 isbot: 5.1.36 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -3802,7 +3799,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@tanstack/router-core@1.167.4': + '@tanstack/router-core@1.167.5': dependencies: '@tanstack/history': 1.161.6 '@tanstack/store': 0.9.2 @@ -3812,18 +3809,18 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.166.9(@tanstack/router-core@1.167.4)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.166.9(@tanstack/router-core@1.167.5)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.167.4 + '@tanstack/router-core': 1.167.5 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) tiny-invariant: 1.3.3 optionalDependencies: csstype: 3.2.3 - '@tanstack/router-generator@1.166.12': + '@tanstack/router-generator@1.166.13': dependencies: - '@tanstack/router-core': 1.167.4 + '@tanstack/router-core': 1.167.5 '@tanstack/router-utils': 1.161.6 '@tanstack/virtual-file-routes': 1.161.7 prettier: 3.8.1 @@ -3834,7 +3831,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.166.13(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': + '@tanstack/router-plugin@1.166.14(@tanstack/react-router@1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) @@ -3842,16 +3839,16 @@ snapshots: '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 - '@tanstack/router-core': 1.167.4 - '@tanstack/router-generator': 1.166.12 + '@tanstack/router-core': 1.167.5 + '@tanstack/router-generator': 1.166.13 '@tanstack/router-utils': 1.161.6 '@tanstack/virtual-file-routes': 1.161.7 chokidar: 3.6.0 unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - vite: 8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) + '@tanstack/react-router': 1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -3908,7 +3905,7 @@ snapshots: '@types/d3-timer@3.0.2': {} - '@types/debug@4.1.12': + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -3963,10 +3960,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': + '@vitejs/plugin-react@6.0.1(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) acorn@8.16.0: {} @@ -4046,19 +4043,19 @@ snapshots: dependencies: baseline-browser-mapping: 2.10.8 caniuse-lite: 1.0.30001780 - electron-to-chromium: 1.5.313 + electron-to-chromium: 1.5.321 node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) byte-size@9.0.1: {} - cacheable@2.3.3: + cacheable@2.3.4: dependencies: '@cacheable/memory': 2.0.8 '@cacheable/utils': 2.4.0 hookified: 1.15.1 keyv: 5.6.0 - qified: 0.6.0 + qified: 0.9.0 call-bind-apply-helpers@1.0.2: dependencies: @@ -4223,7 +4220,7 @@ snapshots: easy-file-picker@1.2.0: {} - electron-to-chromium@1.5.313: {} + electron-to-chromium@1.5.321: {} emoji-regex@8.0.0: {} @@ -4315,19 +4312,19 @@ snapshots: file-entry-cache@11.1.2: dependencies: - flat-cache: 6.1.20 + flat-cache: 6.1.21 fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - flat-cache@6.1.20: + flat-cache@6.1.21: dependencies: - cacheable: 2.3.3 - flatted: 3.4.1 + cacheable: 2.3.4 + flatted: 3.4.2 hookified: 1.15.1 - flatted@3.4.1: {} + flatted@3.4.2: {} follow-redirects@1.15.11: {} @@ -4505,6 +4502,8 @@ snapshots: hookified@1.15.1: {} + hookified@2.1.0: {} + html-tags@5.1.0: {} html-url-attributes@3.0.1: {} @@ -4881,7 +4880,7 @@ snapshots: micromark@4.0.2: dependencies: - '@types/debug': 4.1.12 + '@types/debug': 4.1.13 debug: 4.4.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 @@ -5003,9 +5002,9 @@ snapshots: proxy-from-env@1.1.0: {} - qified@0.6.0: + qified@0.9.0: dependencies: - hookified: 1.15.1 + hookified: 2.1.0 qrcode.react@4.2.0(react@19.2.4): dependencies: @@ -5138,26 +5137,26 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.9: + rolldown@1.0.0-rc.10: dependencies: - '@oxc-project/types': 0.115.0 - '@rolldown/pluginutils': 1.0.0-rc.9 + '@oxc-project/types': 0.120.0 + '@rolldown/pluginutils': 1.0.0-rc.10 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.9 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 - '@rolldown/binding-darwin-x64': 1.0.0-rc.9 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 + '@rolldown/binding-android-arm64': 1.0.0-rc.10 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.10 + '@rolldown/binding-darwin-x64': 1.0.0-rc.10 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.10 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.10 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.10 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.10 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.10 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.10 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10 run-parallel@1.2.0: dependencies: @@ -5630,21 +5629,20 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)): + vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)): dependencies: ansi-colors: 4.1.3 pathe: 2.0.3 - vite: 8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) optionalDependencies: sharp: 0.34.5 - vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0): + vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0): dependencies: - '@oxc-project/runtime': 0.115.0 lightningcss: 1.32.0 picomatch: 4.0.3 postcss: 8.5.8 - rolldown: 1.0.0-rc.9 + rolldown: 1.0.0-rc.10 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 25.5.0 From 9d1bed17033a0801f8328800a06b1a169a1ef6c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 19 Mar 2026 09:33:46 +0100 Subject: [PATCH 17/19] supress trivy warning --- .trivyignore.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.trivyignore.yaml b/.trivyignore.yaml index bb760342b8..bdb80882a1 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -11,3 +11,6 @@ vulnerabilities: - id: CVE-2025-7709 expired_at: 2026-03-30 statement: "Not yet fixed in Debian" + - id: CVE-2026-32763 + expired_at: 2026-03-26 + statement: "Waiting for upstream patch in paraglide" From f5d482a7d72ab564b1d933179c659dc397510675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 19 Mar 2026 09:53:07 +0100 Subject: [PATCH 18/19] Revert "update frontend dependencies" This reverts commit 6f48e1c3e61a571968d63a21df3b8528e3e87f37. --- web/package.json | 8 +- web/pnpm-lock.yaml | 324 +++++++++++++++++++++++---------------------- 2 files changed, 167 insertions(+), 165 deletions(-) diff --git a/web/package.json b/web/package.json index 9a46c21577..34e6e8fcba 100644 --- a/web/package.json +++ b/web/package.json @@ -21,8 +21,8 @@ "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", "@tanstack/react-form": "^1.28.5", - "@tanstack/react-query": "^5.91.0", - "@tanstack/react-router": "^1.167.5", + "@tanstack/react-query": "^5.90.21", + "@tanstack/react-router": "^1.167.4", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.23", "@uidotdev/usehooks": "^2.4.1", @@ -57,7 +57,7 @@ "@tanstack/react-devtools": "^0.10.0", "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-router-devtools": "^1.166.9", - "@tanstack/router-plugin": "^1.166.14", + "@tanstack/router-plugin": "^1.166.13", "@types/byte-size": "^8.1.2", "@types/humanize-duration": "^3.27.4", "@types/lodash-es": "^4.17.12", @@ -75,7 +75,7 @@ "stylelint-config-standard-scss": "^17.0.0", "stylelint-scss": "^7.0.0", "typescript": "~5.9.3", - "vite": "^8.0.1", + "vite": "^8.0.0", "vite-plugin-image-optimizer": "^2.0.3" } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 53c7ff7e5b..8246963e53 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -33,11 +33,11 @@ importers: specifier: ^1.28.5 version: 1.28.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': - specifier: ^5.91.0 - version: 5.91.0(react@19.2.4) + specifier: ^5.90.21 + version: 5.90.21(react@19.2.4) '@tanstack/react-router': - specifier: ^1.167.5 - version: 1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^1.167.4 + version: 1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -122,19 +122,19 @@ importers: version: 2.4.7 '@tanstack/devtools-vite': specifier: ^0.6.0 - version: 0.6.0(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) + version: 0.6.0(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) '@tanstack/react-devtools': specifier: ^0.10.0 version: 0.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.9) '@tanstack/react-query-devtools': specifier: ^5.91.3 - version: 5.91.3(@tanstack/react-query@5.91.0(react@19.2.4))(react@19.2.4) + version: 5.91.3(@tanstack/react-query@5.90.21(react@19.2.4))(react@19.2.4) '@tanstack/react-router-devtools': specifier: ^1.166.9 - version: 1.166.9(@tanstack/react-router@1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.5)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.166.9(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.4)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/router-plugin': - specifier: ^1.166.14 - version: 1.166.14(@tanstack/react-router@1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) + specifier: ^1.166.13 + version: 1.166.13(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 @@ -158,7 +158,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) + version: 6.0.1(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) autoprefixer: specifier: ^10.4.27 version: 10.4.27(postcss@8.5.8) @@ -187,11 +187,11 @@ importers: specifier: ~5.9.3 version: 5.9.3 vite: - specifier: ^8.0.1 - version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) + specifier: ^8.0.0 + version: 8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) vite-plugin-image-optimizer: specifier: ^2.0.3 - version: 2.0.3(sharp@0.34.5)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) + version: 2.0.3(sharp@0.34.5)(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)) packages: @@ -788,8 +788,12 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.120.0': - resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} + '@oxc-project/runtime@0.115.0': + resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.115.0': + resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} @@ -905,107 +909,107 @@ packages: react-redux: optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} + '@rolldown/binding-android-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.10': - resolution: {integrity: sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==} + '@rolldown/binding-darwin-x64@1.0.0-rc.9': + resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.10': - resolution: {integrity: sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': - resolution: {integrity: sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': - resolution: {integrity: sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': - resolution: {integrity: sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': - resolution: {integrity: sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': - resolution: {integrity: sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': - resolution: {integrity: sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.10': - resolution: {integrity: sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==} - '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@rolldown/pluginutils@1.0.0-rc.9': + resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} + '@shortercode/webzip@1.1.1-0': resolution: {integrity: sha512-c+9LbU7MTqWDXS73Pf7vEm5AyFFfbuuHiMgqmApwkwD5BrlELKaSqKjofG4Gqd3c/NgF8fO0iR5NbPdE26jF2Q==} @@ -1124,8 +1128,8 @@ packages: resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/query-core@5.91.0': - resolution: {integrity: sha512-FYXN8Kk9Q5VKuV6AIVaNwMThSi0nvAtR4X7HQoigf6ePOtFcavJYVIzgFhOVdtbBQtCJE3KimDIMMJM2DR1hjw==} + '@tanstack/query-core@5.90.20': + resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} '@tanstack/query-devtools@5.93.0': resolution: {integrity: sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg==} @@ -1154,8 +1158,8 @@ packages: '@tanstack/react-query': ^5.90.20 react: ^18 || ^19 - '@tanstack/react-query@5.91.0': - resolution: {integrity: sha512-S8FODsDTNv0Ym+o/JVBvA6EWiWVhg6K2Q4qFehZyFKk6uW4H9OPbXl4kyiN9hAly0uHJ/1GEbR6kAI4MZWfjEA==} + '@tanstack/react-query@5.90.21': + resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==} peerDependencies: react: ^18 || ^19 @@ -1171,8 +1175,8 @@ packages: '@tanstack/router-core': optional: true - '@tanstack/react-router@1.167.5': - resolution: {integrity: sha512-s1nP6l/7BYZfSwhoNbB7/rUmZ07q/AvkmhBoiDQl3tgy5dpb9Q1qjtIapYdvCOrao1aA/QCaWqxcbGc2Ct1bvQ==} + '@tanstack/react-router@1.167.4': + resolution: {integrity: sha512-VpbZh382zX3WF4+X2Z+EUyd8eJhJyjg9C6ByYwrVZiWbhgbMK4+zQQIG2+lCAlIlDi7SV8fDcGL09NA8Z2kpGQ==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -1197,8 +1201,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.167.5': - resolution: {integrity: sha512-8fRgJ0zNJf77R4grCaJQ5Imatjyc4YT5v8rlsPkYYYeUlcFNLbuFRhLlAMdND9gRUMznpnbRDXngpTPgx2K7HQ==} + '@tanstack/router-core@1.167.4': + resolution: {integrity: sha512-Gk5V9Zr5JFJ4SbLyCheQLJ3MnXddccENPA+DJRz+9g3QxtN8DJB8w8KCUCgDeYlWp4LvmO4nX3fy3tupqVP2Pw==} engines: {node: '>=20.19'} hasBin: true @@ -1212,17 +1216,17 @@ packages: csstype: optional: true - '@tanstack/router-generator@1.166.13': - resolution: {integrity: sha512-ALxSs6OzimiSgpOuIm+AXmc7eUx/oGPwSPpdQbpZ/kX7WHRh6qM7lv8DAN0K3jWcBpzF8eeOIdryWryX8gH+Yg==} + '@tanstack/router-generator@1.166.12': + resolution: {integrity: sha512-2HdxSTbCkbU9JeYogKVigIlXoLtIJE1x5rbEov+ZLTPjGCO9kicNQuljqg9Js+u2/ahtWewNrE5u1QCAyxmpIg==} engines: {node: '>=20.19'} - '@tanstack/router-plugin@1.166.14': - resolution: {integrity: sha512-hypyj0qlsAbJf60/glmVYqSVwnRB4hKRrMCUsSXjrPdO2g6gs3z6xHmcWsHQ831C4G9+bSFEK9Uy5EjO3A4THQ==} + '@tanstack/router-plugin@1.166.13': + resolution: {integrity: sha512-xG3ND3AlMe6DN9PihJAYUbQJevqJvVdzN1QpZbfU1/jkHurL97ynP2yXfmMTh8Qgi1K+SWRko4bi7iZlYP9SUw==} engines: {node: '>=20.19'} hasBin: true peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.167.5 + '@tanstack/react-router': ^1.167.4 vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' vite-plugin-solid: ^2.11.10 webpack: '>=5.92.0' @@ -1290,8 +1294,8 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - '@types/debug@4.1.13': - resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -1455,8 +1459,8 @@ packages: '@75lb/nature': optional: true - cacheable@2.3.4: - resolution: {integrity: sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==} + cacheable@2.3.3: + resolution: {integrity: sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==} call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} @@ -1660,8 +1664,8 @@ packages: easy-file-picker@1.2.0: resolution: {integrity: sha512-GJxOW5s+g/pBr8Ha86a768yx0UZ6fYw+iAOrxK5HOzQ8q9hZxEJF0C8ztdAsH0mcze58FSpzv/d9flRCAuUKHg==} - electron-to-chromium@1.5.321: - resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==} + electron-to-chromium@1.5.313: + resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1752,11 +1756,11 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - flat-cache@6.1.21: - resolution: {integrity: sha512-2u7cJfSf7Th7NxEk/VzQjnPoglok2YCsevS7TSbJjcDQWJPbqUUnSYtriHSvtnq+fRZHy1s0ugk4ApnQyhPGoQ==} + flat-cache@6.1.20: + resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flatted@3.4.1: + resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==} follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} @@ -1891,9 +1895,6 @@ packages: hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} - hookified@2.1.0: - resolution: {integrity: sha512-ootKng4eaxNxa7rx6FJv2YKef3DuhqbEj3l70oGXwddPQEEnISm50TEZQclqiLTAtilT2nu7TErtCO523hHkyg==} - html-tags@5.1.0: resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} engines: {node: '>=20.10'} @@ -2362,8 +2363,8 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - qified@0.9.0: - resolution: {integrity: sha512-4q61YgkHbY6gmwkqm0BsxyLDO3UYdrdiJTJ7JiaZb3xpW1duxn135SB7KqUEkCiuu5O4W+TtwEWP2VjmSRanvA==} + qified@0.6.0: + resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==} engines: {node: '>=20'} qrcode.react@4.2.0: @@ -2481,8 +2482,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.0.0-rc.10: - resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} + rolldown@1.0.0-rc.9: + resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2849,13 +2850,13 @@ packages: svgo: optional: true - vite@8.0.1: - resolution: {integrity: sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==} + vite@8.0.0: + resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 + '@vitejs/devtools': ^0.0.0-alpha.31 esbuild: ^0.27.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -3443,7 +3444,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-project/types@0.120.0': {} + '@oxc-project/runtime@0.115.0': {} + + '@oxc-project/types@0.115.0': {} '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -3532,57 +3535,57 @@ snapshots: react: 19.2.4 react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) - '@rolldown/binding-android-arm64@1.0.0-rc.10': + '@rolldown/binding-android-arm64@1.0.0-rc.9': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.10': + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.10': + '@rolldown/binding-darwin-x64@1.0.0-rc.9': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.10': + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': optional: true - '@rolldown/pluginutils@1.0.0-rc.10': {} - '@rolldown/pluginutils@1.0.0-rc.7': {} + '@rolldown/pluginutils@1.0.0-rc.9': {} + '@shortercode/webzip@1.1.1-0': {} '@sinclair/typebox@0.31.28': {} @@ -3678,7 +3681,7 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-vite@0.6.0(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': + '@tanstack/devtools-vite@0.6.0(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -3690,7 +3693,7 @@ snapshots: chalk: 5.6.2 launch-editor: 2.13.1 picomatch: 4.0.3 - vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) transitivePeerDependencies: - bufferutil - supports-color @@ -3722,7 +3725,7 @@ snapshots: '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/query-core@5.91.0': {} + '@tanstack/query-core@5.90.20': {} '@tanstack/query-devtools@5.93.0': {} @@ -3747,33 +3750,33 @@ snapshots: transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.91.3(@tanstack/react-query@5.91.0(react@19.2.4))(react@19.2.4)': + '@tanstack/react-query-devtools@5.91.3(@tanstack/react-query@5.90.21(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/query-devtools': 5.93.0 - '@tanstack/react-query': 5.91.0(react@19.2.4) + '@tanstack/react-query': 5.90.21(react@19.2.4) react: 19.2.4 - '@tanstack/react-query@5.91.0(react@19.2.4)': + '@tanstack/react-query@5.90.21(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.91.0 + '@tanstack/query-core': 5.90.20 react: 19.2.4 - '@tanstack/react-router-devtools@1.166.9(@tanstack/react-router@1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.5)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-router-devtools@1.166.9(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.4)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/react-router': 1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-devtools-core': 1.166.9(@tanstack/router-core@1.167.5)(csstype@3.2.3) + '@tanstack/react-router': 1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-devtools-core': 1.166.9(@tanstack/router-core@1.167.4)(csstype@3.2.3) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) optionalDependencies: - '@tanstack/router-core': 1.167.5 + '@tanstack/router-core': 1.167.4 transitivePeerDependencies: - csstype - '@tanstack/react-router@1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/history': 1.161.6 '@tanstack/react-store': 0.9.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-core': 1.167.5 + '@tanstack/router-core': 1.167.4 isbot: 5.1.36 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -3799,7 +3802,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@tanstack/router-core@1.167.5': + '@tanstack/router-core@1.167.4': dependencies: '@tanstack/history': 1.161.6 '@tanstack/store': 0.9.2 @@ -3809,18 +3812,18 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.166.9(@tanstack/router-core@1.167.5)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.166.9(@tanstack/router-core@1.167.4)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.167.5 + '@tanstack/router-core': 1.167.4 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) tiny-invariant: 1.3.3 optionalDependencies: csstype: 3.2.3 - '@tanstack/router-generator@1.166.13': + '@tanstack/router-generator@1.166.12': dependencies: - '@tanstack/router-core': 1.167.5 + '@tanstack/router-core': 1.167.4 '@tanstack/router-utils': 1.161.6 '@tanstack/virtual-file-routes': 1.161.7 prettier: 3.8.1 @@ -3831,7 +3834,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.166.14(@tanstack/react-router@1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': + '@tanstack/router-plugin@1.166.13(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) @@ -3839,16 +3842,16 @@ snapshots: '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 - '@tanstack/router-core': 1.167.5 - '@tanstack/router-generator': 1.166.13 + '@tanstack/router-core': 1.167.4 + '@tanstack/router-generator': 1.166.12 '@tanstack/router-utils': 1.161.6 '@tanstack/virtual-file-routes': 1.161.7 chokidar: 3.6.0 unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.167.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) + '@tanstack/react-router': 1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + vite: 8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -3905,7 +3908,7 @@ snapshots: '@types/d3-timer@3.0.2': {} - '@types/debug@4.1.13': + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -3960,10 +3963,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': + '@vitejs/plugin-react@6.0.1(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) acorn@8.16.0: {} @@ -4043,19 +4046,19 @@ snapshots: dependencies: baseline-browser-mapping: 2.10.8 caniuse-lite: 1.0.30001780 - electron-to-chromium: 1.5.321 + electron-to-chromium: 1.5.313 node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) byte-size@9.0.1: {} - cacheable@2.3.4: + cacheable@2.3.3: dependencies: '@cacheable/memory': 2.0.8 '@cacheable/utils': 2.4.0 hookified: 1.15.1 keyv: 5.6.0 - qified: 0.9.0 + qified: 0.6.0 call-bind-apply-helpers@1.0.2: dependencies: @@ -4220,7 +4223,7 @@ snapshots: easy-file-picker@1.2.0: {} - electron-to-chromium@1.5.321: {} + electron-to-chromium@1.5.313: {} emoji-regex@8.0.0: {} @@ -4312,19 +4315,19 @@ snapshots: file-entry-cache@11.1.2: dependencies: - flat-cache: 6.1.21 + flat-cache: 6.1.20 fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - flat-cache@6.1.21: + flat-cache@6.1.20: dependencies: - cacheable: 2.3.4 - flatted: 3.4.2 + cacheable: 2.3.3 + flatted: 3.4.1 hookified: 1.15.1 - flatted@3.4.2: {} + flatted@3.4.1: {} follow-redirects@1.15.11: {} @@ -4502,8 +4505,6 @@ snapshots: hookified@1.15.1: {} - hookified@2.1.0: {} - html-tags@5.1.0: {} html-url-attributes@3.0.1: {} @@ -4880,7 +4881,7 @@ snapshots: micromark@4.0.2: dependencies: - '@types/debug': 4.1.13 + '@types/debug': 4.1.12 debug: 4.4.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 @@ -5002,9 +5003,9 @@ snapshots: proxy-from-env@1.1.0: {} - qified@0.9.0: + qified@0.6.0: dependencies: - hookified: 2.1.0 + hookified: 1.15.1 qrcode.react@4.2.0(react@19.2.4): dependencies: @@ -5137,26 +5138,26 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.10: + rolldown@1.0.0-rc.9: dependencies: - '@oxc-project/types': 0.120.0 - '@rolldown/pluginutils': 1.0.0-rc.10 + '@oxc-project/types': 0.115.0 + '@rolldown/pluginutils': 1.0.0-rc.9 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.10 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.10 - '@rolldown/binding-darwin-x64': 1.0.0-rc.10 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.10 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.10 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.10 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.10 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.10 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.10 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10 + '@rolldown/binding-android-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-x64': 1.0.0-rc.9 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 run-parallel@1.2.0: dependencies: @@ -5629,20 +5630,21 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)): + vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0)): dependencies: ansi-colors: 4.1.3 pathe: 2.0.3 - vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) + vite: 8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0) optionalDependencies: sharp: 0.34.5 - vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0): + vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.4)(sass@1.98.0)(tsx@4.21.0): dependencies: + '@oxc-project/runtime': 0.115.0 lightningcss: 1.32.0 picomatch: 4.0.3 postcss: 8.5.8 - rolldown: 1.0.0-rc.10 + rolldown: 1.0.0-rc.9 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 25.5.0 From 72d6f3821fa92ac4e6c54c60b21bd79ddb75f4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 19 Mar 2026 09:54:43 +0100 Subject: [PATCH 19/19] update query data --- ...9951c8ddab38dd1ba41e9a3f657273574988f.json | 28 +++++++++++++++ ...e2a67880adeaca1e10e50bea9b9fc53e08845.json | 2 +- ...c52cb537dfded663e6bbee73d5f908f0ca89e.json | 2 +- ...a03d9b14fe56a118b31220c572a4412c711cf.json | 28 --------------- ...8bf72f6b253c79b8429d1c0dfa9a935393afd.json | 34 +++++++++++++++++++ ...a9efc87a8696f46c6f0dea5618b6aac0ff962.json | 34 ------------------- 6 files changed, 64 insertions(+), 64 deletions(-) create mode 100644 .sqlx/query-0f1e325349ad365965d6fb7b1e09951c8ddab38dd1ba41e9a3f657273574988f.json delete mode 100644 .sqlx/query-990350e22dea5e90064d5acd42ea03d9b14fe56a118b31220c572a4412c711cf.json create mode 100644 .sqlx/query-cfd57ee3fa1f81e46368f8ee1488bf72f6b253c79b8429d1c0dfa9a935393afd.json delete mode 100644 .sqlx/query-fda76aeb98f21838bf502c3492da9efc87a8696f46c6f0dea5618b6aac0ff962.json diff --git a/.sqlx/query-0f1e325349ad365965d6fb7b1e09951c8ddab38dd1ba41e9a3f657273574988f.json b/.sqlx/query-0f1e325349ad365965d6fb7b1e09951c8ddab38dd1ba41e9a3f657273574988f.json new file mode 100644 index 0000000000..2590d55e75 --- /dev/null +++ b/.sqlx/query-0f1e325349ad365965d6fb7b1e09951c8ddab38dd1ba41e9a3f657273574988f.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.wireguard_pubkey pubkey, ARRAY( SELECT host(ip) FROM unnest(wnd.wireguard_ips) AS ip ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id WHERE wireguard_network_id = $1 AND d.configured AND u.is_active ORDER BY d.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "allowed_ips!: Vec", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + null + ] + }, + "hash": "0f1e325349ad365965d6fb7b1e09951c8ddab38dd1ba41e9a3f657273574988f" +} diff --git a/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json b/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json index 0e36c94cdb..8a6776d365 100644 --- a/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json +++ b/.sqlx/query-47c406366d0b53ca805cff303dfe2a67880adeaca1e10e50bea9b9fc53e08845.json @@ -82,7 +82,7 @@ false, false, true, - false, + true, false, false, false, diff --git a/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json b/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json index d62e957d33..548a89c193 100644 --- a/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json +++ b/.sqlx/query-59d048f8110a53745afd80c607fc52cb537dfded663e6bbee73d5f908f0ca89e.json @@ -80,7 +80,7 @@ false, false, true, - false, + true, false, false, false, diff --git a/.sqlx/query-990350e22dea5e90064d5acd42ea03d9b14fe56a118b31220c572a4412c711cf.json b/.sqlx/query-990350e22dea5e90064d5acd42ea03d9b14fe56a118b31220c572a4412c711cf.json deleted file mode 100644 index d9f37238e8..0000000000 --- a/.sqlx/query-990350e22dea5e90064d5acd42ea03d9b14fe56a118b31220c572a4412c711cf.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey pubkey, ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id WHERE wireguard_network_id = $1 AND d.configured AND u.is_active ORDER BY d.id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "pubkey", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "allowed_ips!: Vec", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - null - ] - }, - "hash": "990350e22dea5e90064d5acd42ea03d9b14fe56a118b31220c572a4412c711cf" -} diff --git a/.sqlx/query-cfd57ee3fa1f81e46368f8ee1488bf72f6b253c79b8429d1c0dfa9a935393afd.json b/.sqlx/query-cfd57ee3fa1f81e46368f8ee1488bf72f6b253c79b8429d1c0dfa9a935393afd.json new file mode 100644 index 0000000000..80f736a1b0 --- /dev/null +++ b/.sqlx/query-cfd57ee3fa1f81e46368f8ee1488bf72f6b253c79b8429d1c0dfa9a935393afd.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT d.wireguard_pubkey pubkey, active_session.preshared_key \"preshared_key!\", ARRAY( SELECT host(ip) FROM unnest(wnd.wireguard_ips) AS ip ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id JOIN LATERAL ( SELECT preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') AND preshared_key IS NOT NULL ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wireguard_network_id = $1 AND d.configured AND u.is_active ORDER BY d.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "preshared_key!", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "allowed_ips!: Vec", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + true, + null + ] + }, + "hash": "cfd57ee3fa1f81e46368f8ee1488bf72f6b253c79b8429d1c0dfa9a935393afd" +} diff --git a/.sqlx/query-fda76aeb98f21838bf502c3492da9efc87a8696f46c6f0dea5618b6aac0ff962.json b/.sqlx/query-fda76aeb98f21838bf502c3492da9efc87a8696f46c6f0dea5618b6aac0ff962.json deleted file mode 100644 index 8a0404b8af..0000000000 --- a/.sqlx/query-fda76aeb98f21838bf502c3492da9efc87a8696f46c6f0dea5618b6aac0ff962.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT d.wireguard_pubkey pubkey, active_session.preshared_key \"preshared_key!\", ARRAY(\n SELECT host(ip)\n FROM unnest(wnd.wireguard_ips) AS ip\n ) \"allowed_ips!: Vec\" FROM wireguard_network_device wnd JOIN device d ON wnd.device_id = d.id JOIN \"user\" u ON d.user_id = u.id JOIN LATERAL ( SELECT preshared_key FROM vpn_client_session WHERE location_id = wnd.wireguard_network_id AND device_id = wnd.device_id AND state IN ('new', 'connected') AND preshared_key IS NOT NULL ORDER BY created_at DESC, id DESC LIMIT 1 ) active_session ON true WHERE wireguard_network_id = $1 AND d.configured AND u.is_active ORDER BY d.id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "pubkey", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "preshared_key!", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "allowed_ips!: Vec", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - true, - null - ] - }, - "hash": "fda76aeb98f21838bf502c3492da9efc87a8696f46c6f0dea5618b6aac0ff962" -}