From 4429fcff4d870afb72c4e75c8eb036cfd5d3cfcf Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 21 Aug 2026 02:16:49 +0200 Subject: [PATCH 01/21] posture checks part of location update --- .../defguard_core/src/handlers/wireguard.rs | 12 ++++ .../tests/integration/api/wireguard.rs | 45 +++++++++++--- .../EditLocationPage/EditLocationPage.tsx | 58 ++++++++++--------- web/src/shared/api/api.ts | 3 - web/src/shared/api/types.ts | 5 +- 5 files changed, 81 insertions(+), 42 deletions(-) diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index 9d2d1c180..38dc375f2 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -470,6 +470,18 @@ pub(crate) async fn modify_network( .set_allowed_groups(&mut transaction, &data.allowed_groups) .await?; + if let Some(posture_checks) = &data.posture_checks { + if !has_enterprise_access(Some(LicenseFeature::DevicePosture)) && !posture_checks.is_empty() + { + return Ok(WebError::Forbidden( + "Cannot assign posture checks to location: Enterprise license required.", + ) + .into()); + } + DevicePostureLocation::set_for_location(&mut transaction, network.id, posture_checks) + .await?; + } + let _events = sync_location_allowed_devices(&network, &mut transaction, None).await?; let peers = get_location_allowed_peers(&network, &mut transaction).await?; diff --git a/crates/defguard_core/tests/integration/api/wireguard.rs b/crates/defguard_core/tests/integration/api/wireguard.rs index 61d1ae59a..3c8eb6098 100644 --- a/crates/defguard_core/tests/integration/api/wireguard.rs +++ b/crates/defguard_core/tests/integration/api/wireguard.rs @@ -854,7 +854,7 @@ async fn test_modify_network_rejects_service_location_with_mfa( } #[sqlx::test] -async fn test_modify_network_without_posture_checks_keeps_assignments( +async fn test_modify_network_updates_posture_checks_only_when_supplied( _: PgPoolOptions, options: PgConnectOptions, ) { @@ -864,6 +864,7 @@ async fn test_modify_network_without_posture_checks_keeps_assignments( set_enterprise_license(); let posture = make_posture_check(&client, "Posture").await; + let replacement_posture = make_posture_check(&client, "Replacement posture").await; let mut payload = location_payload("location", "10.1.1.1/24", false, "disabled"); payload["posture_checks"] = json!([posture]); @@ -875,11 +876,27 @@ async fn test_modify_network_without_posture_checks_keeps_assignments( vec![posture] ); - // a payload with the field omitted must leave the assignment alone + // an explicit list replaces the current assignments with the location save + let mut payload = location_payload("renamed-location", "10.1.1.1/24", false, "disabled"); + payload["posture_checks"] = json!([replacement_posture]); + let response = client + .put(format!("/api/v1/network/{}", location.id)) + .json(&payload) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + let modified: WireguardNetwork = response.json().await; + assert_eq!(modified.name, "renamed-location"); + assert_eq!( + fetch_location_postures(&client, location.id).await, + vec![replacement_posture] + ); + + // a payload with the field omitted or explicitly null keeps assignments for compatibility let response = client .put(format!("/api/v1/network/{}", location.id)) .json(&location_payload( - "renamed-location", + "location", "10.1.1.1/24", false, "disabled", @@ -887,14 +904,11 @@ async fn test_modify_network_without_posture_checks_keeps_assignments( .send() .await; assert_eq!(response.status(), StatusCode::OK); - let modified: WireguardNetwork = response.json().await; - assert_eq!(modified.name, "renamed-location"); assert_eq!( fetch_location_postures(&client, location.id).await, - vec![posture] + vec![replacement_posture] ); - // an explicit `null` behaves the same way let mut payload = location_payload("location", "10.1.1.1/24", false, "disabled"); payload["posture_checks"] = json!(null); let response = client @@ -905,7 +919,22 @@ async fn test_modify_network_without_posture_checks_keeps_assignments( assert_eq!(response.status(), StatusCode::OK); assert_eq!( fetch_location_postures(&client, location.id).await, - vec![posture] + vec![replacement_posture] + ); + + // an explicit empty list clears assignments + let mut payload = location_payload("location", "10.1.1.1/24", false, "disabled"); + payload["posture_checks"] = json!([]); + let response = client + .put(format!("/api/v1/network/{}", location.id)) + .json(&payload) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + assert!( + fetch_location_postures(&client, location.id) + .await + .is_empty() ); } diff --git a/web/src/pages/EditLocationPage/EditLocationPage.tsx b/web/src/pages/EditLocationPage/EditLocationPage.tsx index 849696c6c..4ba0a7f9b 100644 --- a/web/src/pages/EditLocationPage/EditLocationPage.tsx +++ b/web/src/pages/EditLocationPage/EditLocationPage.tsx @@ -3,7 +3,7 @@ import './style.scss'; import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query'; import { Link, useNavigate, useParams } from '@tanstack/react-router'; import { cloneDeep, omit } from 'lodash-es'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import z from 'zod'; import { m } from '../../paraglide/messages'; import api from '../../shared/api/api'; @@ -371,16 +371,22 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => { }); const serviceLocationLocked = isPresent(canUseServiceLocations) && !canUseServiceLocations; + const [pendingPostureChecks, setPendingPostureChecks] = useState( + location.posture_checks ?? [], + ); const postureChecksSectionState = useMemo( () => getPostureChecksSectionState({ - assignedPostureChecksCount: location.posture_checks?.length ?? 0, + assignedPostureChecksCount: pendingPostureChecks.length, canUseEnterprise: canUseDevicePosture, postureChecksCount: postureChecks.length, }), - [canUseDevicePosture, location.posture_checks?.length, postureChecks.length], + [canUseDevicePosture, pendingPostureChecks.length, postureChecks.length], ); const firewallLocked = isPresent(canUseBusiness) && !canUseBusiness; + const hasPendingPostureCheckChanges = + pendingPostureChecks.length !== (location.posture_checks?.length ?? 0) || + pendingPostureChecks.some((id) => !location.posture_checks?.includes(id)); const postureCheckOptions = useMemo( () => @@ -464,26 +470,10 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => { }, }); - const { mutateAsync: setLocationPosturesAsync, isPending: isUpdatingLocationPostures } = - useMutation({ - mutationFn: (data: { postures: number[] }) => - api.devicePosture.setLocationPostures(location.id, data), - meta: { - invalidate: [['device-posture'], ['network'], ['activity-log']], - }, - onError: () => { - Snackbar.error(m.location_posture_checks_update_failed()); - }, - }); - const handlePostureSelection = (values: (string | number)[]) => { - const next = values.filter((value): value is number => typeof value === 'number'); - confirmLocationPostureChange({ - current: location.posture_checks ?? [], - next, - options: postureCheckOptions, - actionPromise: () => setLocationPosturesAsync({ postures: next }), - }); + setPendingPostureChecks( + values.filter((value): value is number => typeof value === 'number'), + ); }; const openPostureChecksSelection = () => { @@ -499,7 +489,7 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => { unknown >, searchPlaceholder: m.controls_search(), - selected: new Set(location.posture_checks), + selected: new Set(pendingPostureChecks), visibleItemsLimit: 4, onSubmit: handlePostureSelection, }); @@ -531,7 +521,10 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => { const submitLocationChanges = async (value: FormFields) => { await editLocation({ id: location.id, - data: buildLocationSubmissionData(value, location), + data: { + ...buildLocationSubmissionData(value, location), + posture_checks: pendingPostureChecks, + }, }); }; @@ -576,6 +569,17 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => { return; } + if ( + confirmLocationPostureChange({ + current: location.posture_checks ?? [], + next: pendingPostureChecks, + options: postureCheckOptions, + actionPromise: () => submitLocationChanges(value), + }) + ) { + return; + } + await submitLocationChanges(value); }, }); @@ -914,7 +918,7 @@ const EditLocationForm = ({ location }: { location: NetworkLocation }) => {
{