From e68aa8a4e8685b72d59ad22956a8abd7323c10fb Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 17 Nov 2025 10:00:42 +0100 Subject: [PATCH 01/14] EnterpriseSettings::force_all_traffic model, migration --- .../src/enterprise/db/models/enterprise_settings.rs | 9 +++++++-- crates/defguard_core/src/grpc/mod.rs | 3 +++ migrations/20251117084546_force_all_traffic.down.sql | 1 + migrations/20251117084546_force_all_traffic.up.sql | 1 + proto | 2 +- 5 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 migrations/20251117084546_force_all_traffic.down.sql create mode 100644 migrations/20251117084546_force_all_traffic.up.sql diff --git a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs index f2e85ced8b..639f1da576 100644 --- a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs +++ b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs @@ -10,6 +10,8 @@ pub struct EnterpriseSettings { pub admin_device_management: bool, // If true, the option to route all traffic through the vpn is disabled in the client pub disable_all_traffic: bool, + // If true, client is forced to route all traffic via vpn + pub force_all_traffic: bool, // If true, manual WireGuard setup is disabled pub only_client_activation: bool, } @@ -21,6 +23,7 @@ impl Default for EnterpriseSettings { Self { admin_device_management: false, disable_all_traffic: false, + force_all_traffic: false, only_client_activation: false, } } @@ -39,7 +42,7 @@ impl EnterpriseSettings { let settings = query_as!( Self, "SELECT admin_device_management, \ - disable_all_traffic, only_client_activation \ + disable_all_traffic, force_all_traffic, only_client_activation \ FROM \"enterprisesettings\" WHERE id = 1", ) .fetch_optional(executor) @@ -58,10 +61,12 @@ impl EnterpriseSettings { "UPDATE \"enterprisesettings\" SET \ admin_device_management = $1, \ disable_all_traffic = $2, \ - only_client_activation = $3 \ + force_all_traffic = $3, \ + only_client_activation = $4 \ WHERE id = 1", self.admin_device_management, self.disable_all_traffic, + self.force_all_traffic, self.only_client_activation, ) .execute(executor) diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index 96dbd24e69..0163d9c5c2 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -807,6 +807,7 @@ pub struct InstanceInfo { proxy_url: Url, username: String, disable_all_traffic: bool, + force_all_traffic: bool, enterprise_enabled: bool, openid_display_name: Option, } @@ -830,6 +831,7 @@ impl InstanceInfo { proxy_url: config.enrollment_url.clone(), username: username.into(), disable_all_traffic: enterprise_settings.disable_all_traffic, + force_all_traffic: enterprise_settings.force_all_traffic, enterprise_enabled: is_enterprise_enabled(), openid_display_name, } @@ -845,6 +847,7 @@ impl From for defguard_proto::proxy::InstanceInfo { proxy_url: instance.proxy_url.to_string(), username: instance.username, disable_all_traffic: instance.disable_all_traffic, + force_all_traffic: Some(instance.force_all_traffic), enterprise_enabled: instance.enterprise_enabled, openid_display_name: instance.openid_display_name, } diff --git a/migrations/20251117084546_force_all_traffic.down.sql b/migrations/20251117084546_force_all_traffic.down.sql new file mode 100644 index 0000000000..dbe90531b4 --- /dev/null +++ b/migrations/20251117084546_force_all_traffic.down.sql @@ -0,0 +1 @@ +ALTER TABLE enterprisesettings DROP COLUMN force_all_traffic; diff --git a/migrations/20251117084546_force_all_traffic.up.sql b/migrations/20251117084546_force_all_traffic.up.sql new file mode 100644 index 0000000000..291b0329e3 --- /dev/null +++ b/migrations/20251117084546_force_all_traffic.up.sql @@ -0,0 +1 @@ +ALTER TABLE enterprisesettings ADD COLUMN force_all_traffic BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/proto b/proto index 96249ebde0..e4495e8136 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 96249ebde0556f4ae8c47eebc6015efb04ed0104 +Subproject commit e4495e8136d8023f7bde20e4ebc4c33e8841531e From 3f67fd9f53916f337a43a737f37b6f39f49d149e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 17 Nov 2025 12:25:36 +0100 Subject: [PATCH 02/14] add force_all_traffic to enterprise settings frontend --- web/src/i18n/en/index.ts | 5 +++++ web/src/i18n/i18n-types.ts | 20 +++++++++++++++++++ web/src/i18n/pl/index.ts | 5 +++++ .../components/EnterpriseForm.tsx | 15 +++++++++++++- web/src/shared/types.ts | 1 + 5 files changed, 45 insertions(+), 1 deletion(-) diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index f4b94015e6..7b05a1d887 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1713,6 +1713,11 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do helper: 'When this option is enabled, users will not be able to route all traffic through the VPN using the defguard client.', }, + forceAllTraffic: { + label: 'Force the clients to route all traffic through VPN', + helper: + 'When this option is enabled, the users will always route all traffic through the VPN using the defguard client.', + }, manualConfig: { label: "Disable users' ability to manually configure WireGuard client", helper: diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 2313f93c1c..b1a5b261d6 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -4092,6 +4092,16 @@ type RootTranslation = { */ helper: string } + forceAllTraffic: { + /** + * F​o​r​c​e​ ​t​h​e​ ​c​l​i​e​n​t​s​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​V​P​N + */ + label: string + /** + * W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​t​h​e​ ​u​s​e​r​s​ ​w​i​l​l​ ​a​l​w​a​y​s​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​V​P​N​ ​u​s​i​n​g​ ​t​h​e​ ​d​e​f​g​u​a​r​d​ ​c​l​i​e​n​t​. + */ + helper: string + } manualConfig: { /** * D​i​s​a​b​l​e​ ​u​s​e​r​s​'​ ​a​b​i​l​i​t​y​ ​t​o​ ​m​a​n​u​a​l​l​y​ ​c​o​n​f​i​g​u​r​e​ ​W​i​r​e​G​u​a​r​d​ ​c​l​i​e​n​t @@ -10823,6 +10833,16 @@ export type TranslationFunctions = { */ helper: () => LocalizedString } + forceAllTraffic: { + /** + * Force the clients to route all traffic through VPN + */ + label: () => LocalizedString + /** + * When this option is enabled, the users will always route all traffic through the VPN using the defguard client. + */ + helper: () => LocalizedString + } manualConfig: { /** * Disable users' ability to manually configure WireGuard client diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 6525901519..a5ae0669dd 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -1499,6 +1499,11 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe helper: 'Kiedy ta opcja jest włączona, użytkownicy nie będą mogli przekierować całego ruchu przez VPN za pomocą klienta Defguard.', }, + forceAllTraffic: { + label: 'Wymuś przekierowanie całego ruchu przez VPN', + helper: + 'Kiedy ta opcja jest włączona, użytkownicy będą zawsze przekierowywać cały ruch przez VPN za pomocą klienta Defguard.', + }, manualConfig: { label: 'Wyłącz manualną konfigurację WireGuard', helper: diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx index d7df932dec..bea144cc8b 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx @@ -78,7 +78,7 @@ export const EnterpriseForm = () => {
@@ -89,6 +89,19 @@ export const EnterpriseForm = () => { {parse(LL.settingsPage.enterprise.fields.disableAllTraffic.helper())}
+
+ + mutate({ force_all_traffic: !settings.force_all_traffic }) + } + /> + + {parse(LL.settingsPage.enterprise.fields.forceAllTraffic.helper())} + +
); diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 3c181cb11e..b1f71a1119 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -1124,6 +1124,7 @@ export type SettingsGatewayNotifications = { export type SettingsEnterprise = { admin_device_management: boolean; disable_all_traffic: boolean; + force_all_traffic: boolean; only_client_activation: boolean; }; From e61e5e4b76d27b81c7f5775751e23569a6f4189f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 18 Nov 2025 10:03:55 +0100 Subject: [PATCH 03/14] client traffic policy enum - migration --- .../20251117084546_force_all_traffic.down.sql | 17 +++++++++++++- .../20251117084546_force_all_traffic.up.sql | 23 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/migrations/20251117084546_force_all_traffic.down.sql b/migrations/20251117084546_force_all_traffic.down.sql index dbe90531b4..30db35457a 100644 --- a/migrations/20251117084546_force_all_traffic.down.sql +++ b/migrations/20251117084546_force_all_traffic.down.sql @@ -1 +1,16 @@ -ALTER TABLE enterprisesettings DROP COLUMN force_all_traffic; +-- restore boolean `mfa_enabled` column +ALTER TABLE enterprisesettings ADD COLUMN "disable_all_traffic" BOOLEAN DEFAULT false; + +-- populate based on client traffic policy +UPDATE enterprisesettings +SET disable_all_traffic = CASE + WHEN client_traffic_policy = 'disable_all_traffic'::client_traffic_policy THEN true + ELSE false +END; + +-- make the restored column NOT NULL +ALTER TABLE enterprisesettings ALTER COLUMN "disable_all_traffic" SET NOT NULL; + +-- drop new column and type +ALTER TABLE enterprisesettings DROP COLUMN "client_traffic_policy"; +DROP TYPE client_traffic_policy; diff --git a/migrations/20251117084546_force_all_traffic.up.sql b/migrations/20251117084546_force_all_traffic.up.sql index 291b0329e3..1070ee24f5 100644 --- a/migrations/20251117084546_force_all_traffic.up.sql +++ b/migrations/20251117084546_force_all_traffic.up.sql @@ -1 +1,22 @@ -ALTER TABLE enterprisesettings ADD COLUMN force_all_traffic BOOLEAN NOT NULL DEFAULT FALSE; +-- add enum representing client traffic policy +CREATE TYPE client_traffic_policy AS ENUM ( + 'none', + 'disable_all_traffic', + 'force_all_traffic' +); + +-- add nullable column to `enterprisesettings` table +ALTER TABLE enterprisesettings ADD COLUMN "client_traffic_policy" client_traffic_policy DEFAULT 'none'; + +-- populate new column based on value in `disable_all_traffic` column +UPDATE enterprisesettings +SET client_traffic_policy = CASE + WHEN disable_all_traffic = true THEN 'disable_all_traffic'::client_traffic_policy + ELSE 'none'::client_traffic_policy +END; + +-- make new column NOT NULL +ALTER TABLE enterprisesettings ALTER COLUMN "client_traffic_policy" SET NOT NULL; + +-- drop the `disable_all_traffic` column since it's no longer needed +ALTER TABLE enterprisesettings DROP COLUMN "disable_all_traffic"; From 5a480fdb1960006d7d0f361fb5cffa43b46c7715 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 19 Nov 2025 08:34:15 +0100 Subject: [PATCH 04/14] wip display client traffic policy select --- .../db/models/enterprise_settings.rs | 39 +++++---- crates/defguard_core/src/grpc/mod.rs | 21 +++-- proto | 2 +- .../components/EnterpriseForm.tsx | 38 ++++----- .../TrafficPolicySelect.tsx | 83 +++++++++++++++++++ .../components/TrafficPolicySelect/style.scss | 59 +++++++++++++ web/src/shared/types.ts | 9 +- 7 files changed, 202 insertions(+), 49 deletions(-) create mode 100644 web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx create mode 100644 web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/style.scss diff --git a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs index 639f1da576..e3ecf5b118 100644 --- a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs +++ b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs @@ -1,4 +1,4 @@ -use sqlx::{PgExecutor, query, query_as}; +use sqlx::{PgExecutor, Type, query, query_as}; use struct_patch::Patch; use crate::enterprise::is_enterprise_enabled; @@ -6,13 +6,11 @@ use crate::enterprise::is_enterprise_enabled; #[derive(Debug, Deserialize, Patch, Serialize)] #[patch(attribute(derive(Deserialize, Serialize)))] pub struct EnterpriseSettings { - // If true, only admins can manage devices + /// If true, only admins can manage devices pub admin_device_management: bool, - // If true, the option to route all traffic through the vpn is disabled in the client - pub disable_all_traffic: bool, - // If true, client is forced to route all traffic via vpn - pub force_all_traffic: bool, - // If true, manual WireGuard setup is disabled + /// Describes allowed routing options for clients connecting to the instance. + pub client_traffic_policy: ClientTrafficPolicy, + /// If true, manual WireGuard setup is disabled pub only_client_activation: bool, } @@ -22,9 +20,8 @@ impl Default for EnterpriseSettings { fn default() -> Self { Self { admin_device_management: false, - disable_all_traffic: false, - force_all_traffic: false, only_client_activation: false, + client_traffic_policy: ClientTrafficPolicy::default(), } } } @@ -42,7 +39,8 @@ impl EnterpriseSettings { let settings = query_as!( Self, "SELECT admin_device_management, \ - disable_all_traffic, force_all_traffic, only_client_activation \ + client_traffic_policy \"client_traffic_policy: ClientTrafficPolicy\", \ + only_client_activation \ FROM \"enterprisesettings\" WHERE id = 1", ) .fetch_optional(executor) @@ -60,13 +58,11 @@ impl EnterpriseSettings { query!( "UPDATE \"enterprisesettings\" SET \ admin_device_management = $1, \ - disable_all_traffic = $2, \ - force_all_traffic = $3, \ - only_client_activation = $4 \ + client_traffic_policy = $2, \ + only_client_activation = $3 \ WHERE id = 1", self.admin_device_management, - self.disable_all_traffic, - self.force_all_traffic, + self.client_traffic_policy as ClientTrafficPolicy, self.only_client_activation, ) .execute(executor) @@ -75,3 +71,16 @@ impl EnterpriseSettings { Ok(()) } } + +/// Describes allowed traffic options for clients connecting to the instance. +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Type, Debug, Default, Copy)] +#[sqlx(type_name = "client_traffic_policy", rename_all = "snake_case")] +pub enum ClientTrafficPolicy { + /// No restrictions + #[default] + None, + /// Clients are not allowed to route all traffic through the VPN. + DisableAllTraffic, + /// Clients are forced to route all traffic through the VPN. + ForceAllTraffic, +} diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index 0163d9c5c2..a924699e26 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -46,15 +46,14 @@ pub use crate::version::MIN_GATEWAY_VERSION; use crate::{ auth::failed_login::FailedLoginMap, db::{ - AppEvent, GatewayEvent, - models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token}, + models::enrollment::{Token, ENROLLMENT_TOKEN_TYPE}, AppEvent, GatewayEvent }, enterprise::{ - db::models::{enterprise_settings::EnterpriseSettings, openid_provider::OpenIdProvider}, + db::models::{enterprise_settings::{ClientTrafficPolicy, EnterpriseSettings}, openid_provider::OpenIdProvider}, directory_sync::sync_user_groups_if_configured, grpc::polling::PollingServer, handlers::openid_login::{ - SELECT_ACCOUNT_SUPPORTED_PROVIDERS, build_state, make_oidc_client, user_from_claims, + build_state, make_oidc_client, user_from_claims, SELECT_ACCOUNT_SUPPORTED_PROVIDERS }, is_enterprise_enabled, ldap::utils::ldap_update_user_state, @@ -62,7 +61,7 @@ use crate::{ events::{BidiStreamEvent, GrpcEvent}, grpc::gateway::{client_state::ClientMap, map::GatewayMap}, server_config, - version::{IncompatibleComponents, IncompatibleProxyData, is_proxy_version_supported}, + version::{is_proxy_version_supported, IncompatibleComponents, IncompatibleProxyData}, }; static VERSION_ZERO: Version = Version::new(0, 0, 0); @@ -806,8 +805,7 @@ pub struct InstanceInfo { url: Url, proxy_url: Url, username: String, - disable_all_traffic: bool, - force_all_traffic: bool, + client_traffic_policy: ClientTrafficPolicy, enterprise_enabled: bool, openid_display_name: Option, } @@ -830,8 +828,7 @@ impl InstanceInfo { url: config.url.clone(), proxy_url: config.enrollment_url.clone(), username: username.into(), - disable_all_traffic: enterprise_settings.disable_all_traffic, - force_all_traffic: enterprise_settings.force_all_traffic, + client_traffic_policy: enterprise_settings.client_traffic_policy, enterprise_enabled: is_enterprise_enabled(), openid_display_name, } @@ -846,8 +843,10 @@ impl From for defguard_proto::proxy::InstanceInfo { url: instance.url.to_string(), proxy_url: instance.proxy_url.to_string(), username: instance.username, - disable_all_traffic: instance.disable_all_traffic, - force_all_traffic: Some(instance.force_all_traffic), + // Ensure backwards compatibility. + #[allow(deprecated)] + disable_all_traffic: instance.client_traffic_policy == ClientTrafficPolicy::DisableAllTraffic, + client_traffic_policy: Some(instance.client_traffic_policy as i32), enterprise_enabled: instance.enterprise_enabled, openid_display_name: instance.openid_display_name, } diff --git a/proto b/proto index e4495e8136..74d60d9171 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit e4495e8136d8023f7bde20e4ebc4c33e8841531e +Subproject commit 74d60d9171048ba0ccaf8a21b05950fb7a673f09 diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx index bea144cc8b..ba7163079f 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx @@ -5,13 +5,18 @@ import type { AxiosError } from 'axios'; import parse from 'html-react-parser'; import { useI18nContext } from '../../../../../i18n/i18n-react'; +// import { FormServiceLocationModeSelect } from '../../../../../shared/components/Form/FormServiceLocationModeSelect/FormServiceLocationModeSelect'; import { Helper } from '../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; import { LabeledCheckbox } from '../../../../../shared/defguard-ui/components/Layout/LabeledCheckbox/LabeledCheckbox'; +// import { Select } from '../../../../../shared/defguard-ui/components/Layout/Select/Select'; +// import { SelectOption } from '../../../../../shared/defguard-ui/components/Layout/Select/types'; import { useAppStore } from '../../../../../shared/hooks/store/useAppStore'; import useApi from '../../../../../shared/hooks/useApi'; import { useToaster } from '../../../../../shared/hooks/useToaster'; import { MutationKeys } from '../../../../../shared/mutations'; import { QueryKeys } from '../../../../../shared/queries'; +import type { ClientTrafficPolicy } from '../../../../../shared/types'; +import { ClientTrafficPolicySelect } from './TrafficPolicySelect/TrafficPolicySelect'; export const EnterpriseForm = () => { const { LL } = useI18nContext(); @@ -39,6 +44,16 @@ export const EnterpriseForm = () => { }, }); + // const trafficPolicyOptions: SelectOption[] = + // Object.values(ClientTrafficPolicy).map((v) => ({ + // value: v, + // label: v, + // key: v, + // })); + + const onPolicyChange = (newPolicy: ClientTrafficPolicy) => + console.log('New policy:', newPolicy); + if (!settings) return null; return ( @@ -77,26 +92,9 @@ export const EnterpriseForm = () => {
- - mutate({ disable_all_traffic: !settings.disable_all_traffic }) - } - /> - - {parse(LL.settingsPage.enterprise.fields.disableAllTraffic.helper())} - -
-
- - mutate({ force_all_traffic: !settings.force_all_traffic }) - } + {parse(LL.settingsPage.enterprise.fields.forceAllTraffic.helper())} diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx new file mode 100644 index 0000000000..cf7d5d1f38 --- /dev/null +++ b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx @@ -0,0 +1,83 @@ +import './style.scss'; +import clsx from 'clsx'; +import { useMemo } from 'react'; +// import { +// type FieldValues, +// type UseControllerProps, +// useController, +// } from 'react-hook-form'; +import { useI18nContext } from '../../../../../../i18n/i18n-react'; +import { RadioButton } from '../../../../../../shared/defguard-ui/components/Layout/RadioButton/Radiobutton'; +// import { useAppStore } from '../../../../../shared/hooks/store/useAppStore'; +import type { SelectOption } from '../../../../../../shared/defguard-ui/components/Layout/Select/types'; +import { ClientTrafficPolicy } from '../../../../../../shared/types'; + +type Props = { + // controller: UseControllerProps; + // disabled?: boolean; + onChange: (event: ClientTrafficPolicy) => void; + fieldValue: ClientTrafficPolicy; +}; + +export const ClientTrafficPolicySelect = ({ + // controller, + // disabled = false, + onChange, + fieldValue, +}: Props) => { + const { LL } = useI18nContext(); + // const { + // field: { onChange, value: fieldValue }, + // } = useController(controller); + + const options = useMemo( + (): SelectOption[] => [ + { + key: ClientTrafficPolicy.NONE, + value: ClientTrafficPolicy.NONE, + label: LL.components.serviceLocationModeSelect.options.disabled(), + }, + { + key: ClientTrafficPolicy.DISABLE_ALL_TRAFFIC, + value: ClientTrafficPolicy.DISABLE_ALL_TRAFFIC, + label: LL.components.serviceLocationModeSelect.options.prelogon(), + }, + { + key: ClientTrafficPolicy.FORCE_ALL_TRAFFIC, + value: ClientTrafficPolicy.FORCE_ALL_TRAFFIC, + label: LL.components.serviceLocationModeSelect.options.alwayson(), + }, + ], + [ + LL.components.serviceLocationModeSelect.options.disabled, + LL.components.serviceLocationModeSelect.options.prelogon, + LL.components.serviceLocationModeSelect.options.alwayson, + ], + ); + + return ( +
+ + {options.map(({ key, value, label, disabled = false }) => { + const active = fieldValue === value; + return ( +
{ + if (!disabled) { + onChange(value); + } + }} + > +

{label}

+ +
+ ); + })} +
+ ); +}; diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/style.scss b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/style.scss new file mode 100644 index 0000000000..692d1123b8 --- /dev/null +++ b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/style.scss @@ -0,0 +1,59 @@ +.client-traffic-policy-select { + display: flex; + flex-flow: column; + row-gap: var(--spacing-s); + margin-bottom: 25px; + + .client-traffic-policy { + display: flex; + align-items: center; + justify-content: space-between; + column-gap: var(--spacing-xs); + min-height: 30px; + border: 1px solid var(--border-primary); + padding: var(--spacing-xs) var(--spacing-s); + border-radius: 10px; + cursor: pointer; + user-select: none; + transition-property: border-color, opacity; + @include animate-standard; + + &:not(.active) { + &:hover { + border-color: var(--border-separator); + } + } + + &.active { + border-color: var(--surface-main-primary); + } + + &.active, + &:hover { + .label { + color: var(--text-body-primary); + } + } + + &.disabled { + opacity: 0.6; + cursor: not-allowed; + background-color: var(--surface-secondary); + + .label { + color: var(--text-body-disabled); + } + + &:hover { + border-color: var(--border-primary); + } + } + + .label { + color: var(--text-body-secondary); + transition-property: color; + @include typography(app-modal-1); + @include animate-standard; + } + } +} diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index b1f71a1119..33258f66b6 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -1121,10 +1121,15 @@ export type SettingsGatewayNotifications = { gateway_disconnect_notifications_reconnect_notification_enabled: boolean; }; +export enum ClientTrafficPolicy { + NONE = 'none', + DISABLE_ALL_TRAFFIC = 'disable_all_traffic', + FORCE_ALL_TRAFFIC = 'force_all_traffic', +} + export type SettingsEnterprise = { admin_device_management: boolean; - disable_all_traffic: boolean; - force_all_traffic: boolean; + client_traffic_policy: ClientTrafficPolicy; only_client_activation: boolean; }; From 6ba86cddf07c3bc3d4bae7fc1c64a13007cb752a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 19 Nov 2025 08:35:06 +0100 Subject: [PATCH 05/14] cleanup --- .../components/EnterpriseForm.tsx | 10 ---------- .../TrafficPolicySelect/TrafficPolicySelect.tsx | 13 ------------- 2 files changed, 23 deletions(-) diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx index ba7163079f..602b01e350 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx @@ -5,11 +5,8 @@ import type { AxiosError } from 'axios'; import parse from 'html-react-parser'; import { useI18nContext } from '../../../../../i18n/i18n-react'; -// import { FormServiceLocationModeSelect } from '../../../../../shared/components/Form/FormServiceLocationModeSelect/FormServiceLocationModeSelect'; import { Helper } from '../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; import { LabeledCheckbox } from '../../../../../shared/defguard-ui/components/Layout/LabeledCheckbox/LabeledCheckbox'; -// import { Select } from '../../../../../shared/defguard-ui/components/Layout/Select/Select'; -// import { SelectOption } from '../../../../../shared/defguard-ui/components/Layout/Select/types'; import { useAppStore } from '../../../../../shared/hooks/store/useAppStore'; import useApi from '../../../../../shared/hooks/useApi'; import { useToaster } from '../../../../../shared/hooks/useToaster'; @@ -44,13 +41,6 @@ export const EnterpriseForm = () => { }, }); - // const trafficPolicyOptions: SelectOption[] = - // Object.values(ClientTrafficPolicy).map((v) => ({ - // value: v, - // label: v, - // key: v, - // })); - const onPolicyChange = (newPolicy: ClientTrafficPolicy) => console.log('New policy:', newPolicy); diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx index cf7d5d1f38..fd326cc22d 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx @@ -1,34 +1,21 @@ import './style.scss'; import clsx from 'clsx'; import { useMemo } from 'react'; -// import { -// type FieldValues, -// type UseControllerProps, -// useController, -// } from 'react-hook-form'; import { useI18nContext } from '../../../../../../i18n/i18n-react'; import { RadioButton } from '../../../../../../shared/defguard-ui/components/Layout/RadioButton/Radiobutton'; -// import { useAppStore } from '../../../../../shared/hooks/store/useAppStore'; import type { SelectOption } from '../../../../../../shared/defguard-ui/components/Layout/Select/types'; import { ClientTrafficPolicy } from '../../../../../../shared/types'; type Props = { - // controller: UseControllerProps; - // disabled?: boolean; onChange: (event: ClientTrafficPolicy) => void; fieldValue: ClientTrafficPolicy; }; export const ClientTrafficPolicySelect = ({ - // controller, - // disabled = false, onChange, fieldValue, }: Props) => { const { LL } = useI18nContext(); - // const { - // field: { onChange, value: fieldValue }, - // } = useController(controller); const options = useMemo( (): SelectOption[] => [ From c3a2d91fedda81b44a4af7885d7b1ebec0476b92 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 19 Nov 2025 08:39:53 +0100 Subject: [PATCH 06/14] snake_case traffic policy for the frontend --- .../src/enterprise/db/models/enterprise_settings.rs | 1 + .../components/TrafficPolicySelect/TrafficPolicySelect.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs index e3ecf5b118..6e9db0dab2 100644 --- a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs +++ b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs @@ -75,6 +75,7 @@ impl EnterpriseSettings { /// Describes allowed traffic options for clients connecting to the instance. #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Type, Debug, Default, Copy)] #[sqlx(type_name = "client_traffic_policy", rename_all = "snake_case")] +#[serde(rename_all = "snake_case")] pub enum ClientTrafficPolicy { /// No restrictions #[default] diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx index fd326cc22d..8658bc7e55 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx @@ -17,6 +17,7 @@ export const ClientTrafficPolicySelect = ({ }: Props) => { const { LL } = useI18nContext(); + console.log("fieldValue:", fieldValue); const options = useMemo( (): SelectOption[] => [ { From 45043fa055b53149a5f56f3d347d4172ba52de95 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 19 Nov 2025 09:04:13 +0100 Subject: [PATCH 07/14] translations, helpers --- web/src/i18n/en/index.ts | 28 +++-- web/src/i18n/i18n-types.ts | 112 +++++++++++------- web/src/i18n/pl/index.ts | 28 +++-- .../components/EnterpriseForm.tsx | 3 - .../TrafficPolicySelect.tsx | 29 +++-- 5 files changed, 127 insertions(+), 73 deletions(-) diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index 7b05a1d887..ce572bb6bb 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1708,21 +1708,29 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do helper: "When this option is enabled, only users in the Admin group can manage devices in user profile (it's disabled for all other users)", }, - disableAllTraffic: { - label: 'Disable the option to route all traffic through VPN', - helper: - 'When this option is enabled, users will not be able to route all traffic through the VPN using the defguard client.', - }, - forceAllTraffic: { - label: 'Force the clients to route all traffic through VPN', - helper: - 'When this option is enabled, the users will always route all traffic through the VPN using the defguard client.', - }, manualConfig: { label: "Disable users' ability to manually configure WireGuard client", helper: "When this option is enabled, users won't be able to view or download configuration for the manual WireGuard client setup. Only the Defguard desktop client configuration will be available.", }, + clientTrafficPolicy: { + header: 'Client traffic policy', + none: { + label: 'None', + helper: + 'When this option is enabled, users will be able to select all routing options.', + }, + disableAllTraffic: { + label: 'Disable the option to route all traffic through VPN', + helper: + 'When this option is enabled, users will not be able to route all traffic through the VPN.', + }, + forceAllTraffic: { + label: 'Force the clients to route all traffic through VPN', + helper: + 'When this option is enabled, the users will always route all traffic through the VPN.', + }, + }, }, }, gatewayNotifications: { diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index b1a5b261d6..517c50734c 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -4082,26 +4082,6 @@ type RootTranslation = { */ helper: string } - disableAllTraffic: { - /** - * D​i​s​a​b​l​e​ ​t​h​e​ ​o​p​t​i​o​n​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​V​P​N - */ - label: string - /** - * W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​u​s​e​r​s​ ​w​i​l​l​ ​n​o​t​ ​b​e​ ​a​b​l​e​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​V​P​N​ ​u​s​i​n​g​ ​t​h​e​ ​d​e​f​g​u​a​r​d​ ​c​l​i​e​n​t​. - */ - helper: string - } - forceAllTraffic: { - /** - * F​o​r​c​e​ ​t​h​e​ ​c​l​i​e​n​t​s​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​V​P​N - */ - label: string - /** - * W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​t​h​e​ ​u​s​e​r​s​ ​w​i​l​l​ ​a​l​w​a​y​s​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​V​P​N​ ​u​s​i​n​g​ ​t​h​e​ ​d​e​f​g​u​a​r​d​ ​c​l​i​e​n​t​. - */ - helper: string - } manualConfig: { /** * D​i​s​a​b​l​e​ ​u​s​e​r​s​'​ ​a​b​i​l​i​t​y​ ​t​o​ ​m​a​n​u​a​l​l​y​ ​c​o​n​f​i​g​u​r​e​ ​W​i​r​e​G​u​a​r​d​ ​c​l​i​e​n​t @@ -4112,6 +4092,42 @@ type RootTranslation = { */ helper: string } + clientTrafficPolicy: { + /** + * C​l​i​e​n​t​ ​t​r​a​f​f​i​c​ ​p​o​l​i​c​y + */ + header: string + none: { + /** + * N​o​n​e + */ + label: string + /** + * W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​u​s​e​r​s​ ​w​i​l​l​ ​b​e​ ​a​b​l​e​ ​t​o​ ​s​e​l​e​c​t​ ​a​l​l​ ​r​o​u​t​i​n​g​ ​o​p​t​i​o​n​s​. + */ + helper: string + } + disableAllTraffic: { + /** + * D​i​s​a​b​l​e​ ​t​h​e​ ​o​p​t​i​o​n​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​V​P​N + */ + label: string + /** + * W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​u​s​e​r​s​ ​w​i​l​l​ ​n​o​t​ ​b​e​ ​a​b​l​e​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​V​P​N​. + */ + helper: string + } + forceAllTraffic: { + /** + * F​o​r​c​e​ ​t​h​e​ ​c​l​i​e​n​t​s​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​V​P​N + */ + label: string + /** + * W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​t​h​e​ ​u​s​e​r​s​ ​w​i​l​l​ ​a​l​w​a​y​s​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​V​P​N​. + */ + helper: string + } + } } } gatewayNotifications: { @@ -10823,26 +10839,6 @@ export type TranslationFunctions = { */ helper: () => LocalizedString } - disableAllTraffic: { - /** - * Disable the option to route all traffic through VPN - */ - label: () => LocalizedString - /** - * When this option is enabled, users will not be able to route all traffic through the VPN using the defguard client. - */ - helper: () => LocalizedString - } - forceAllTraffic: { - /** - * Force the clients to route all traffic through VPN - */ - label: () => LocalizedString - /** - * When this option is enabled, the users will always route all traffic through the VPN using the defguard client. - */ - helper: () => LocalizedString - } manualConfig: { /** * Disable users' ability to manually configure WireGuard client @@ -10853,6 +10849,42 @@ export type TranslationFunctions = { */ helper: () => LocalizedString } + clientTrafficPolicy: { + /** + * Client traffic policy + */ + header: () => LocalizedString + none: { + /** + * None + */ + label: () => LocalizedString + /** + * When this option is enabled, users will be able to select all routing options. + */ + helper: () => LocalizedString + } + disableAllTraffic: { + /** + * Disable the option to route all traffic through VPN + */ + label: () => LocalizedString + /** + * When this option is enabled, users will not be able to route all traffic through the VPN. + */ + helper: () => LocalizedString + } + forceAllTraffic: { + /** + * Force the clients to route all traffic through VPN + */ + label: () => LocalizedString + /** + * When this option is enabled, the users will always route all traffic through the VPN. + */ + helper: () => LocalizedString + } + } } } gatewayNotifications: { diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index a5ae0669dd..7356101f64 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -1494,21 +1494,29 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe helper: 'Kiedy ta opcja jest włączona, tylko użytkownicy w grupie "Admin" mogą zarządzać urządzeniami w profilu użytkownika', }, - disableAllTraffic: { - label: 'Zablokuj możliwość przekierowania całego ruchu przez VPN', - helper: - 'Kiedy ta opcja jest włączona, użytkownicy nie będą mogli przekierować całego ruchu przez VPN za pomocą klienta Defguard.', - }, - forceAllTraffic: { - label: 'Wymuś przekierowanie całego ruchu przez VPN', - helper: - 'Kiedy ta opcja jest włączona, użytkownicy będą zawsze przekierowywać cały ruch przez VPN za pomocą klienta Defguard.', - }, manualConfig: { label: 'Wyłącz manualną konfigurację WireGuard', helper: 'Kiedy ta opcja jest włączona, użytkownicy nie będą mogli pobrać ani wyświetlić danych do manualnej konfiguracji WireGuard. Możliwe będzie wyłącznie skonfigurowanie klienta Defguard.', }, + clientTrafficPolicy: { + header: 'Polityka przekierowania ruchu klientów', + none: { + label: 'Brak', + helper: + 'Kiedy ta opcja jest włączona, użytkownicy mogą wybierać dowolny typ przekierowania ruchu.', + }, + disableAllTraffic: { + label: 'Zablokuj możliwość przekierowania całego ruchu przez VPN', + helper: + 'Kiedy ta opcja jest włączona, użytkownicy nie będą mogli przekierować całego ruchu przez VPN.', + }, + forceAllTraffic: { + label: 'Wymuś przekierowanie całego ruchu przez VPN', + helper: + 'Kiedy ta opcja jest włączona, użytkownicy będą zawsze przekierowywać cały ruch przez VPN.', + }, + } }, }, gatewayNotifications: { diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx index 602b01e350..08ab46ce25 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx @@ -86,9 +86,6 @@ export const EnterpriseForm = () => { onChange={onPolicyChange} fieldValue={settings.client_traffic_policy} /> - - {parse(LL.settingsPage.enterprise.fields.forceAllTraffic.helper())} -
diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx index 8658bc7e55..accc8205c5 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx @@ -1,10 +1,12 @@ import './style.scss'; +import parse from 'html-react-parser'; import clsx from 'clsx'; import { useMemo } from 'react'; import { useI18nContext } from '../../../../../../i18n/i18n-react'; import { RadioButton } from '../../../../../../shared/defguard-ui/components/Layout/RadioButton/Radiobutton'; import type { SelectOption } from '../../../../../../shared/defguard-ui/components/Layout/Select/types'; import { ClientTrafficPolicy } from '../../../../../../shared/types'; +import { Helper } from '../../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; type Props = { onChange: (event: ClientTrafficPolicy) => void; @@ -16,37 +18,41 @@ export const ClientTrafficPolicySelect = ({ fieldValue, }: Props) => { const { LL } = useI18nContext(); - - console.log("fieldValue:", fieldValue); const options = useMemo( (): SelectOption[] => [ { key: ClientTrafficPolicy.NONE, value: ClientTrafficPolicy.NONE, - label: LL.components.serviceLocationModeSelect.options.disabled(), + label: LL.settingsPage.enterprise.fields.clientTrafficPolicy.none.label(), + meta: LL.settingsPage.enterprise.fields.clientTrafficPolicy.none.helper(), }, { key: ClientTrafficPolicy.DISABLE_ALL_TRAFFIC, value: ClientTrafficPolicy.DISABLE_ALL_TRAFFIC, - label: LL.components.serviceLocationModeSelect.options.prelogon(), + label: LL.settingsPage.enterprise.fields.clientTrafficPolicy.disableAllTraffic.label(), + meta: LL.settingsPage.enterprise.fields.clientTrafficPolicy.disableAllTraffic.helper(), }, { key: ClientTrafficPolicy.FORCE_ALL_TRAFFIC, value: ClientTrafficPolicy.FORCE_ALL_TRAFFIC, - label: LL.components.serviceLocationModeSelect.options.alwayson(), + label: LL.settingsPage.enterprise.fields.clientTrafficPolicy.forceAllTraffic.label(), + meta: LL.settingsPage.enterprise.fields.clientTrafficPolicy.forceAllTraffic.helper(), }, ], [ - LL.components.serviceLocationModeSelect.options.disabled, - LL.components.serviceLocationModeSelect.options.prelogon, - LL.components.serviceLocationModeSelect.options.alwayson, + LL.settingsPage.enterprise.fields.clientTrafficPolicy.none.label, + LL.settingsPage.enterprise.fields.clientTrafficPolicy.none.helper, + LL.settingsPage.enterprise.fields.clientTrafficPolicy.forceAllTraffic.label, + LL.settingsPage.enterprise.fields.clientTrafficPolicy.forceAllTraffic.helper, + LL.settingsPage.enterprise.fields.clientTrafficPolicy.disableAllTraffic.label, + LL.settingsPage.enterprise.fields.clientTrafficPolicy.disableAllTraffic.helper, ], ); return (
- - {options.map(({ key, value, label, disabled = false }) => { + + {options.map(({ key, value, label, meta, disabled = false }) => { const active = fieldValue === value; return (

{label}

+ + {parse(meta)} +
); })} From 377aed8a85cf5f47517509c3997a39ac94b959a8 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 19 Nov 2025 09:07:21 +0100 Subject: [PATCH 08/14] mutate settings.clientTrafficPolicy --- .../components/EnterpriseSettings/components/EnterpriseForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx index 08ab46ce25..055d6fd060 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx @@ -83,7 +83,7 @@ export const EnterpriseForm = () => {
mutate({ client_traffic_policy: value })} fieldValue={settings.client_traffic_policy} />
From 9e560523aa737db812e6bbac2cfb4d45fe816ebd Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 19 Nov 2025 09:10:52 +0100 Subject: [PATCH 09/14] remove unused callback --- .../EnterpriseSettings/components/EnterpriseForm.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx index 055d6fd060..426206a549 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx @@ -41,9 +41,6 @@ export const EnterpriseForm = () => { }, }); - const onPolicyChange = (newPolicy: ClientTrafficPolicy) => - console.log('New policy:', newPolicy); - if (!settings) return null; return ( From 02e30e6e52f55e4552f8774d6a250a50cde1169a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 19 Nov 2025 13:28:33 +0100 Subject: [PATCH 10/14] tweak migrations --- ...n.sql => 20251119122424_client_traffic_policy.down.sql} | 0 ....up.sql => 20251119122424_client_traffic_policy.up.sql} | 7 ++----- 2 files changed, 2 insertions(+), 5 deletions(-) rename migrations/{20251117084546_force_all_traffic.down.sql => 20251119122424_client_traffic_policy.down.sql} (100%) rename migrations/{20251117084546_force_all_traffic.up.sql => 20251119122424_client_traffic_policy.up.sql} (75%) diff --git a/migrations/20251117084546_force_all_traffic.down.sql b/migrations/20251119122424_client_traffic_policy.down.sql similarity index 100% rename from migrations/20251117084546_force_all_traffic.down.sql rename to migrations/20251119122424_client_traffic_policy.down.sql diff --git a/migrations/20251117084546_force_all_traffic.up.sql b/migrations/20251119122424_client_traffic_policy.up.sql similarity index 75% rename from migrations/20251117084546_force_all_traffic.up.sql rename to migrations/20251119122424_client_traffic_policy.up.sql index 1070ee24f5..a5bfb8d8ac 100644 --- a/migrations/20251117084546_force_all_traffic.up.sql +++ b/migrations/20251119122424_client_traffic_policy.up.sql @@ -5,8 +5,8 @@ CREATE TYPE client_traffic_policy AS ENUM ( 'force_all_traffic' ); --- add nullable column to `enterprisesettings` table -ALTER TABLE enterprisesettings ADD COLUMN "client_traffic_policy" client_traffic_policy DEFAULT 'none'; +-- add column to `enterprisesettings` table +ALTER TABLE enterprisesettings ADD COLUMN "client_traffic_policy" client_traffic_policy NOT NULL DEFAULT 'none'; -- populate new column based on value in `disable_all_traffic` column UPDATE enterprisesettings @@ -15,8 +15,5 @@ SET client_traffic_policy = CASE ELSE 'none'::client_traffic_policy END; --- make new column NOT NULL -ALTER TABLE enterprisesettings ALTER COLUMN "client_traffic_policy" SET NOT NULL; - -- drop the `disable_all_traffic` column since it's no longer needed ALTER TABLE enterprisesettings DROP COLUMN "disable_all_traffic"; From 8ca72f9b599665e39e3bb9ca8903990b262fbb38 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 19 Nov 2025 13:48:33 +0100 Subject: [PATCH 11/14] tweak down migration --- migrations/20251119122424_client_traffic_policy.down.sql | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/migrations/20251119122424_client_traffic_policy.down.sql b/migrations/20251119122424_client_traffic_policy.down.sql index 30db35457a..db730678d6 100644 --- a/migrations/20251119122424_client_traffic_policy.down.sql +++ b/migrations/20251119122424_client_traffic_policy.down.sql @@ -1,5 +1,5 @@ -- restore boolean `mfa_enabled` column -ALTER TABLE enterprisesettings ADD COLUMN "disable_all_traffic" BOOLEAN DEFAULT false; +ALTER TABLE enterprisesettings ADD COLUMN "disable_all_traffic" BOOLEAN NOT NULL DEFAULT false; -- populate based on client traffic policy UPDATE enterprisesettings @@ -8,9 +8,6 @@ SET disable_all_traffic = CASE ELSE false END; --- make the restored column NOT NULL -ALTER TABLE enterprisesettings ALTER COLUMN "disable_all_traffic" SET NOT NULL; - -- drop new column and type ALTER TABLE enterprisesettings DROP COLUMN "client_traffic_policy"; DROP TYPE client_traffic_policy; From 784497ad84944eb7e0b0166b0aad5e71fd81764f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 19 Nov 2025 13:48:47 +0100 Subject: [PATCH 12/14] reformat frontend --- .../components/EnterpriseForm.tsx | 1 - .../TrafficPolicySelect.tsx | 19 ++++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx index 426206a549..c8bcdc885d 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx @@ -12,7 +12,6 @@ import useApi from '../../../../../shared/hooks/useApi'; import { useToaster } from '../../../../../shared/hooks/useToaster'; import { MutationKeys } from '../../../../../shared/mutations'; import { QueryKeys } from '../../../../../shared/queries'; -import type { ClientTrafficPolicy } from '../../../../../shared/types'; import { ClientTrafficPolicySelect } from './TrafficPolicySelect/TrafficPolicySelect'; export const EnterpriseForm = () => { diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx index accc8205c5..43fa5f3f9c 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx @@ -1,22 +1,19 @@ import './style.scss'; -import parse from 'html-react-parser'; import clsx from 'clsx'; +import parse from 'html-react-parser'; import { useMemo } from 'react'; import { useI18nContext } from '../../../../../../i18n/i18n-react'; +import { Helper } from '../../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; import { RadioButton } from '../../../../../../shared/defguard-ui/components/Layout/RadioButton/Radiobutton'; import type { SelectOption } from '../../../../../../shared/defguard-ui/components/Layout/Select/types'; import { ClientTrafficPolicy } from '../../../../../../shared/types'; -import { Helper } from '../../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; type Props = { onChange: (event: ClientTrafficPolicy) => void; fieldValue: ClientTrafficPolicy; }; -export const ClientTrafficPolicySelect = ({ - onChange, - fieldValue, -}: Props) => { +export const ClientTrafficPolicySelect = ({ onChange, fieldValue }: Props) => { const { LL } = useI18nContext(); const options = useMemo( (): SelectOption[] => [ @@ -29,13 +26,15 @@ export const ClientTrafficPolicySelect = ({ { key: ClientTrafficPolicy.DISABLE_ALL_TRAFFIC, value: ClientTrafficPolicy.DISABLE_ALL_TRAFFIC, - label: LL.settingsPage.enterprise.fields.clientTrafficPolicy.disableAllTraffic.label(), + label: + LL.settingsPage.enterprise.fields.clientTrafficPolicy.disableAllTraffic.label(), meta: LL.settingsPage.enterprise.fields.clientTrafficPolicy.disableAllTraffic.helper(), }, { key: ClientTrafficPolicy.FORCE_ALL_TRAFFIC, value: ClientTrafficPolicy.FORCE_ALL_TRAFFIC, - label: LL.settingsPage.enterprise.fields.clientTrafficPolicy.forceAllTraffic.label(), + label: + LL.settingsPage.enterprise.fields.clientTrafficPolicy.forceAllTraffic.label(), meta: LL.settingsPage.enterprise.fields.clientTrafficPolicy.forceAllTraffic.helper(), }, ], @@ -69,9 +68,7 @@ export const ClientTrafficPolicySelect = ({ >

{label}

- - {parse(meta)} - + {parse(meta)} ); })} From 7716b9293f19635a328bf10c12699aefd6aab9fe Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 20 Nov 2025 13:44:21 +0100 Subject: [PATCH 13/14] cargo fmt --- .../db/models/enterprise_settings.rs | 18 ++++++++-------- crates/defguard_core/src/grpc/mod.rs | 21 ++++++++++++------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs index 6e9db0dab2..d1c9be350b 100644 --- a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs +++ b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs @@ -8,7 +8,7 @@ use crate::enterprise::is_enterprise_enabled; pub struct EnterpriseSettings { /// If true, only admins can manage devices pub admin_device_management: bool, - /// Describes allowed routing options for clients connecting to the instance. + /// Describes allowed routing options for clients connecting to the instance. pub client_traffic_policy: ClientTrafficPolicy, /// If true, manual WireGuard setup is disabled pub only_client_activation: bool, @@ -21,7 +21,7 @@ impl Default for EnterpriseSettings { Self { admin_device_management: false, only_client_activation: false, - client_traffic_policy: ClientTrafficPolicy::default(), + client_traffic_policy: ClientTrafficPolicy::default(), } } } @@ -77,11 +77,11 @@ impl EnterpriseSettings { #[sqlx(type_name = "client_traffic_policy", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum ClientTrafficPolicy { - /// No restrictions - #[default] - None, - /// Clients are not allowed to route all traffic through the VPN. - DisableAllTraffic, - /// Clients are forced to route all traffic through the VPN. - ForceAllTraffic, + /// No restrictions + #[default] + None, + /// Clients are not allowed to route all traffic through the VPN. + DisableAllTraffic, + /// Clients are forced to route all traffic through the VPN. + ForceAllTraffic, } diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index a924699e26..a4c4ba3dcf 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -46,14 +46,18 @@ pub use crate::version::MIN_GATEWAY_VERSION; use crate::{ auth::failed_login::FailedLoginMap, db::{ - models::enrollment::{Token, ENROLLMENT_TOKEN_TYPE}, AppEvent, GatewayEvent + AppEvent, GatewayEvent, + models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token}, }, enterprise::{ - db::models::{enterprise_settings::{ClientTrafficPolicy, EnterpriseSettings}, openid_provider::OpenIdProvider}, + db::models::{ + enterprise_settings::{ClientTrafficPolicy, EnterpriseSettings}, + openid_provider::OpenIdProvider, + }, directory_sync::sync_user_groups_if_configured, grpc::polling::PollingServer, handlers::openid_login::{ - build_state, make_oidc_client, user_from_claims, SELECT_ACCOUNT_SUPPORTED_PROVIDERS + SELECT_ACCOUNT_SUPPORTED_PROVIDERS, build_state, make_oidc_client, user_from_claims, }, is_enterprise_enabled, ldap::utils::ldap_update_user_state, @@ -61,7 +65,7 @@ use crate::{ events::{BidiStreamEvent, GrpcEvent}, grpc::gateway::{client_state::ClientMap, map::GatewayMap}, server_config, - version::{is_proxy_version_supported, IncompatibleComponents, IncompatibleProxyData}, + version::{IncompatibleComponents, IncompatibleProxyData, is_proxy_version_supported}, }; static VERSION_ZERO: Version = Version::new(0, 0, 0); @@ -805,7 +809,7 @@ pub struct InstanceInfo { url: Url, proxy_url: Url, username: String, - client_traffic_policy: ClientTrafficPolicy, + client_traffic_policy: ClientTrafficPolicy, enterprise_enabled: bool, openid_display_name: Option, } @@ -843,9 +847,10 @@ impl From for defguard_proto::proxy::InstanceInfo { url: instance.url.to_string(), proxy_url: instance.proxy_url.to_string(), username: instance.username, - // Ensure backwards compatibility. - #[allow(deprecated)] - disable_all_traffic: instance.client_traffic_policy == ClientTrafficPolicy::DisableAllTraffic, + // Ensure backwards compatibility. + #[allow(deprecated)] + disable_all_traffic: instance.client_traffic_policy + == ClientTrafficPolicy::DisableAllTraffic, client_traffic_policy: Some(instance.client_traffic_policy as i32), enterprise_enabled: instance.enterprise_enabled, openid_display_name: instance.openid_display_name, From 5374b7e3f7aaafc4515884c601cb41cf96838954 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 20 Nov 2025 14:08:08 +0100 Subject: [PATCH 14/14] fix tests, add sqlx fixtures --- ...ba68521649ba86985d45049487ae50d7dfde8.json | 43 +++++++++++++++++++ ...b715db1c1df67da573b0f132fea265e42b416.json | 32 -------------- ...b3dbb0fc747ae0843baa001e47ea328e49c25.json | 27 ++++++++++++ ...a5e7829eae217ad9f7f3b0b03aa02f8808dc2.json | 16 ------- .../integration/api/enterprise_settings.rs | 12 +++--- 5 files changed, 76 insertions(+), 54 deletions(-) create mode 100644 .sqlx/query-160d23b882d0465fbc8c5453b7dba68521649ba86985d45049487ae50d7dfde8.json delete mode 100644 .sqlx/query-283e1c3d082f1388fc2b806bdcab715db1c1df67da573b0f132fea265e42b416.json create mode 100644 .sqlx/query-a644507ebcfb9ef04883ad8b07bb3dbb0fc747ae0843baa001e47ea328e49c25.json delete mode 100644 .sqlx/query-ccd62ea7526078c9db47812e7f6a5e7829eae217ad9f7f3b0b03aa02f8808dc2.json diff --git a/.sqlx/query-160d23b882d0465fbc8c5453b7dba68521649ba86985d45049487ae50d7dfde8.json b/.sqlx/query-160d23b882d0465fbc8c5453b7dba68521649ba86985d45049487ae50d7dfde8.json new file mode 100644 index 0000000000..3b31770567 --- /dev/null +++ b/.sqlx/query-160d23b882d0465fbc8c5453b7dba68521649ba86985d45049487ae50d7dfde8.json @@ -0,0 +1,43 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT admin_device_management, client_traffic_policy \"client_traffic_policy: ClientTrafficPolicy\", only_client_activation FROM \"enterprisesettings\" WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "admin_device_management", + "type_info": "Bool" + }, + { + "ordinal": 1, + "name": "client_traffic_policy: ClientTrafficPolicy", + "type_info": { + "Custom": { + "name": "client_traffic_policy", + "kind": { + "Enum": [ + "none", + "disable_all_traffic", + "force_all_traffic" + ] + } + } + } + }, + { + "ordinal": 2, + "name": "only_client_activation", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "160d23b882d0465fbc8c5453b7dba68521649ba86985d45049487ae50d7dfde8" +} diff --git a/.sqlx/query-283e1c3d082f1388fc2b806bdcab715db1c1df67da573b0f132fea265e42b416.json b/.sqlx/query-283e1c3d082f1388fc2b806bdcab715db1c1df67da573b0f132fea265e42b416.json deleted file mode 100644 index c1696c5b03..0000000000 --- a/.sqlx/query-283e1c3d082f1388fc2b806bdcab715db1c1df67da573b0f132fea265e42b416.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT admin_device_management, disable_all_traffic, only_client_activation FROM \"enterprisesettings\" WHERE id = 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "admin_device_management", - "type_info": "Bool" - }, - { - "ordinal": 1, - "name": "disable_all_traffic", - "type_info": "Bool" - }, - { - "ordinal": 2, - "name": "only_client_activation", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "283e1c3d082f1388fc2b806bdcab715db1c1df67da573b0f132fea265e42b416" -} diff --git a/.sqlx/query-a644507ebcfb9ef04883ad8b07bb3dbb0fc747ae0843baa001e47ea328e49c25.json b/.sqlx/query-a644507ebcfb9ef04883ad8b07bb3dbb0fc747ae0843baa001e47ea328e49c25.json new file mode 100644 index 0000000000..509af4943e --- /dev/null +++ b/.sqlx/query-a644507ebcfb9ef04883ad8b07bb3dbb0fc747ae0843baa001e47ea328e49c25.json @@ -0,0 +1,27 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE \"enterprisesettings\" SET admin_device_management = $1, client_traffic_policy = $2, only_client_activation = $3 WHERE id = 1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + { + "Custom": { + "name": "client_traffic_policy", + "kind": { + "Enum": [ + "none", + "disable_all_traffic", + "force_all_traffic" + ] + } + } + }, + "Bool" + ] + }, + "nullable": [] + }, + "hash": "a644507ebcfb9ef04883ad8b07bb3dbb0fc747ae0843baa001e47ea328e49c25" +} diff --git a/.sqlx/query-ccd62ea7526078c9db47812e7f6a5e7829eae217ad9f7f3b0b03aa02f8808dc2.json b/.sqlx/query-ccd62ea7526078c9db47812e7f6a5e7829eae217ad9f7f3b0b03aa02f8808dc2.json deleted file mode 100644 index 787a75d7d9..0000000000 --- a/.sqlx/query-ccd62ea7526078c9db47812e7f6a5e7829eae217ad9f7f3b0b03aa02f8808dc2.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE \"enterprisesettings\" SET admin_device_management = $1, disable_all_traffic = $2, only_client_activation = $3 WHERE id = 1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Bool", - "Bool", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "ccd62ea7526078c9db47812e7f6a5e7829eae217ad9f7f3b0b03aa02f8808dc2" -} diff --git a/crates/defguard_core/tests/integration/api/enterprise_settings.rs b/crates/defguard_core/tests/integration/api/enterprise_settings.rs index f24582a659..c065dde6cc 100644 --- a/crates/defguard_core/tests/integration/api/enterprise_settings.rs +++ b/crates/defguard_core/tests/integration/api/enterprise_settings.rs @@ -1,6 +1,6 @@ use defguard_core::{ enterprise::{ - db::models::enterprise_settings::EnterpriseSettings, + db::models::enterprise_settings::{ClientTrafficPolicy, EnterpriseSettings}, license::{get_cached_license, set_cached_license}, }, handlers::Auth, @@ -33,7 +33,7 @@ async fn test_only_enterprise_can_modify_enterpise_settings( // try to patch enterprise settings let settings = EnterpriseSettings { admin_device_management: false, - disable_all_traffic: false, + client_traffic_policy: ClientTrafficPolicy::None, only_client_activation: false, }; @@ -81,7 +81,7 @@ async fn test_admin_devices_management_is_enforced(_: PgPoolOptions, options: Pg // setup admin devices management let settings = EnterpriseSettings { admin_device_management: true, - disable_all_traffic: false, + client_traffic_policy: ClientTrafficPolicy::None, only_client_activation: false, }; let response = client @@ -177,7 +177,7 @@ async fn test_regular_user_device_management(_: PgPoolOptions, options: PgConnec // setup admin devices management let settings = EnterpriseSettings { admin_device_management: false, - disable_all_traffic: false, + client_traffic_policy: ClientTrafficPolicy::None, only_client_activation: false, }; let response = client @@ -265,7 +265,7 @@ async fn dg25_12_test_enforce_client_activation_only(_: PgPoolOptions, options: // disable manual device management let settings = EnterpriseSettings { admin_device_management: false, - disable_all_traffic: false, + client_traffic_policy: ClientTrafficPolicy::None, only_client_activation: true, }; let response = client @@ -346,7 +346,7 @@ async fn dg25_13_test_disable_device_config(_: PgPoolOptions, options: PgConnect // disable manual device management let settings = EnterpriseSettings { admin_device_management: false, - disable_all_traffic: false, + client_traffic_policy: ClientTrafficPolicy::None, only_client_activation: true, }; let response = client