Repository: DefGuard/defguard (language: rust)

Issue:
Enable disable All traffic feature for users
In the Settings "Enterprise features" tab add a "toggle" for enabling/disabling users the ability to use: "All traffic".
If ability to use "All traffic" is disabled, the client on the (i) information box should have a help message: *This option is disabled by the instance administrator.* and the option should be disabled (always on the Predefined traffic).

Depends on #717 - as information about this feature needs to be synced with "Enterprise" license state.


Discussion / hints:

Fix patch:
diff --git a/.sqlx/query-35950188e70029c8314055621857ea0091c302e9c27de798a2f50901ffd747e0.json b/.sqlx/query-48223bdfca4f2bd5c7cd14594f5d81db67dfb1c0c2c01cf1fd9c8df421f366ef.json
similarity index 57%
rename from .sqlx/query-35950188e70029c8314055621857ea0091c302e9c27de798a2f50901ffd747e0.json
rename to .sqlx/query-48223bdfca4f2bd5c7cd14594f5d81db67dfb1c0c2c01cf1fd9c8df421f366ef.json
index 3fa364d32..2eb5251a9 100644
--- a/.sqlx/query-35950188e70029c8314055621857ea0091c302e9c27de798a2f50901ffd747e0.json
+++ b/.sqlx/query-48223bdfca4f2bd5c7cd14594f5d81db67dfb1c0c2c01cf1fd9c8df421f366ef.json
@@ -1,16 +1,17 @@
 {
   "db_name": "PostgreSQL",
-  "query": "UPDATE \"enterprisesettings\" SET \"admin_device_management\" = $2,\"only_client_activation\" = $3 WHERE id = $1",
+  "query": "UPDATE \"enterprisesettings\" SET \"admin_device_management\" = $2,\"disable_all_traffic\" = $3,\"only_client_activation\" = $4 WHERE id = $1",
   "describe": {
     "columns": [],
     "parameters": {
       "Left": [
         "Int8",
         "Bool",
+        "Bool",
         "Bool"
       ]
     },
     "nullable": []
   },
-  "hash": "35950188e70029c8314055621857ea0091c302e9c27de798a2f50901ffd747e0"
+  "hash": "48223bdfca4f2bd5c7cd14594f5d81db67dfb1c0c2c01cf1fd9c8df421f366ef"
 }
diff --git a/.sqlx/query-6332fe33694ebd090eba2ef0777ea91e86978f827cbff94a6a28338a7b0bf6ee.json b/.sqlx/query-68fc762354c30e66a83ba47746a0d4707e9b8e9829965bf6e4b334d1ab41bb6b.json
similarity index 59%
rename from .sqlx/query-6332fe33694ebd090eba2ef0777ea91e86978f827cbff94a6a28338a7b0bf6ee.json
rename to .sqlx/query-68fc762354c30e66a83ba47746a0d4707e9b8e9829965bf6e4b334d1ab41bb6b.json
index b861d4934..010db59cc 100644
--- a/.sqlx/query-6332fe33694ebd090eba2ef0777ea91e86978f827cbff94a6a28338a7b0bf6ee.json
+++ b/.sqlx/query-68fc762354c30e66a83ba47746a0d4707e9b8e9829965bf6e4b334d1ab41bb6b.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id \"id?\", \"admin_device_management\",\"only_client_activation\" FROM \"enterprisesettings\" WHERE id = $1",
+  "query": "SELECT id \"id?\", \"admin_device_management\",\"disable_all_traffic\",\"only_client_activation\" FROM \"enterprisesettings\" WHERE id = $1",
   "describe": {
     "columns": [
       {
@@ -15,6 +15,11 @@
       },
       {
         "ordinal": 2,
+        "name": "disable_all_traffic",
+        "type_info": "Bool"
+      },
+      {
+        "ordinal": 3,
         "name": "only_client_activation",
         "type_info": "Bool"
       }
@@ -25,10 +30,11 @@
       ]
     },
     "nullable": [
+      false,
       false,
       false,
       false
     ]
   },
-  "hash": "6332fe33694ebd090eba2ef0777ea91e86978f827cbff94a6a28338a7b0bf6ee"
+  "hash": "68fc762354c30e66a83ba47746a0d4707e9b8e9829965bf6e4b334d1ab41bb6b"
 }
diff --git a/.sqlx/query-bd0665f9401b5919b55e8b68ef238b514c12becf8e9dfbb8641b02e4deb8b5e5.json b/.sqlx/query-834d5b83aebddf1e23511025d5f98b8a69e1227e8bfbd2b1d0110c8def72f8aa.json
similarity index 64%
rename from .sqlx/query-bd0665f9401b5919b55e8b68ef238b514c12becf8e9dfbb8641b02e4deb8b5e5.json
rename to .sqlx/query-834d5b83aebddf1e23511025d5f98b8a69e1227e8bfbd2b1d0110c8def72f8aa.json
index b2dae11eb..96a0c7b4e 100644
--- a/.sqlx/query-bd0665f9401b5919b55e8b68ef238b514c12becf8e9dfbb8641b02e4deb8b5e5.json
+++ b/.sqlx/query-834d5b83aebddf1e23511025d5f98b8a69e1227e8bfbd2b1d0110c8def72f8aa.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "INSERT INTO \"enterprisesettings\" (\"admin_device_management\",\"only_client_activation\") VALUES ($1,$2) RETURNING id",
+  "query": "INSERT INTO \"enterprisesettings\" (\"admin_device_management\",\"disable_all_traffic\",\"only_client_activation\") VALUES ($1,$2,$3) RETURNING id",
   "describe": {
     "columns": [
       {
@@ -11,6 +11,7 @@
     ],
     "parameters": {
       "Left": [
+        "Bool",
         "Bool",
         "Bool"
       ]
@@ -19,5 +20,5 @@
       false
     ]
   },
-  "hash": "bd0665f9401b5919b55e8b68ef238b514c12becf8e9dfbb8641b02e4deb8b5e5"
+  "hash": "834d5b83aebddf1e23511025d5f98b8a69e1227e8bfbd2b1d0110c8def72f8aa"
 }
diff --git a/.sqlx/query-205507ac888563e614157c6977abb744104284cb6bc7070127797bd6df0ac7af.json b/.sqlx/query-e2483b9d167af9476ba49c63fa20a07aa53bfe7f783fcb9257312304094d5edc.json
similarity index 59%
rename from .sqlx/query-205507ac888563e614157c6977abb744104284cb6bc7070127797bd6df0ac7af.json
rename to .sqlx/query-e2483b9d167af9476ba49c63fa20a07aa53bfe7f783fcb9257312304094d5edc.json
index 749e67273..beb848d75 100644
--- a/.sqlx/query-205507ac888563e614157c6977abb744104284cb6bc7070127797bd6df0ac7af.json
+++ b/.sqlx/query-e2483b9d167af9476ba49c63fa20a07aa53bfe7f783fcb9257312304094d5edc.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id \"id?\", \"admin_device_management\",\"only_client_activation\" FROM \"enterprisesettings\"",
+  "query": "SELECT id \"id?\", \"admin_device_management\",\"disable_all_traffic\",\"only_client_activation\" FROM \"enterprisesettings\"",
   "describe": {
     "columns": [
       {
@@ -15,6 +15,11 @@
       },
       {
         "ordinal": 2,
+        "name": "disable_all_traffic",
+        "type_info": "Bool"
+      },
+      {
+        "ordinal": 3,
         "name": "only_client_activation",
         "type_info": "Bool"
       }
@@ -23,10 +28,11 @@
       "Left": []
     },
     "nullable": [
+      false,
       false,
       false,
       false
     ]
   },
-  "hash": "205507ac888563e614157c6977abb744104284cb6bc7070127797bd6df0ac7af"
+  "hash": "e2483b9d167af9476ba49c63fa20a07aa53bfe7f783fcb9257312304094d5edc"
 }
diff --git a/migrations/20240902103930_add_option_to_disable_all_traffic.down.sql b/migrations/20240902103930_add_option_to_disable_all_traffic.down.sql
new file mode 100644
index 000000000..d07635dbc
--- /dev/null
+++ b/migrations/20240902103930_add_option_to_disable_all_traffic.down.sql
@@ -0,0 +1,1 @@
+ALTER TABLE enterprisesettings DROP COLUMN disable_all_traffic;
diff --git a/migrations/20240902103930_add_option_to_disable_all_traffic.up.sql b/migrations/20240902103930_add_option_to_disable_all_traffic.up.sql
new file mode 100644
index 000000000..21de95022
--- /dev/null
+++ b/migrations/20240902103930_add_option_to_disable_all_traffic.up.sql
@@ -0,0 +1,1 @@
+ALTER TABLE enterprisesettings ADD COLUMN disable_all_traffic BOOLEAN NOT NULL DEFAULT FALSE;
diff --git a/proto b/proto
index d069a0e53..de58067ab 160000
--- a/proto
+++ b/proto
@@ -1,1 +1,1 @@
-Subproject commit d069a0e5304281cfc8b09e949a8e7a9feb5fc115
+Subproject commit de58067ab652f6e5ddf84c7bbc6e4d2363914738
diff --git a/src/db/models/polling_token.rs b/src/db/models/polling_token.rs
index 3235f6b05..99f611909 100644
--- a/src/db/models/polling_token.rs
+++ b/src/db/models/polling_token.rs
@@ -2,9 +2,8 @@ use chrono::{NaiveDateTime, Utc};
 use model_derive::Model;
 use sqlx::{query_as, Error as SqlxError};
 
-use crate::random::gen_alphanumeric;
-
 use super::DbPool;
+use crate::random::gen_alphanumeric;
 
 // Token used for polling requests.
 #[derive(Clone, Debug, Model)]
diff --git a/src/enterprise/db/models/enterprise_settings.rs b/src/enterprise/db/models/enterprise_settings.rs
index cebf54e74..6b1a4f33c 100644
--- a/src/enterprise/db/models/enterprise_settings.rs
+++ b/src/enterprise/db/models/enterprise_settings.rs
@@ -11,6 +11,8 @@ pub struct EnterpriseSettings {
     pub id: Option<i64>,
     // 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, manual WireGuard setup is disabled
     pub only_client_activation: bool,
 }
@@ -22,6 +24,7 @@ impl Default for EnterpriseSettings {
         Self {
             id: None,
             admin_device_management: false,
+            disable_all_traffic: false,
             only_client_activation: false,
         }
     }
diff --git a/src/enterprise/grpc/polling.rs b/src/enterprise/grpc/polling.rs
index f351a89be..88fb28a07 100644
--- a/src/enterprise/grpc/polling.rs
+++ b/src/enterprise/grpc/polling.rs
@@ -1,11 +1,13 @@
+use tonic::Status;
+
 use crate::{
     db::{models::polling_token::PollingToken, DbPool, Device, User},
     enterprise::license::{get_cached_license, validate_license},
-    grpc::utils::build_device_config_response,
+    grpc::{
+        proto::{InstanceInfoRequest, InstanceInfoResponse},
+        utils::{build_device_config_response, build_instance_config_response},
+    },
 };
-use tonic::Status;
-
-use crate::grpc::proto::{InstanceInfoRequest, InstanceInfoResponse};
 
 pub struct PollingServer {
     pool: DbPool,
@@ -81,8 +83,10 @@ impl PollingServer {
         // Build & return polling info
         let device_config =
             build_device_config_response(&self.pool, &device.wireguard_pubkey).await?;
+        let instance_config = build_instance_config_response(&self.pool).await?;
         Ok(InstanceInfoResponse {
             device_config: Some(device_config),
+            instance_config: Some(instance_config),
         })
     }
 }
diff --git a/src/enterprise/handlers/openid_login.rs b/src/enterprise/handlers/openid_login.rs
index e02d57bda..e180d9c26 100644
--- a/src/enterprise/handlers/openid_login.rs
+++ b/src/enterprise/handlers/openid_login.rs
@@ -1,32 +1,38 @@
-use axum::extract::State;
-use axum::http::StatusCode;
-use axum::Json;
+use axum::{extract::State, http::StatusCode, Json};
 use axum_client_ip::{InsecureClientIp, LeftmostXForwardedFor};
-use axum_extra::extract::cookie::{Cookie, SameSite};
-use axum_extra::extract::{CookieJar, PrivateCookieJar};
-use axum_extra::headers::UserAgent;
-use axum_extra::TypedHeader;
-use openidconnect::core::{
-    CoreClient, CoreGenderClaim, CoreJsonWebKeyType, CoreJweContentEncryptionAlgorithm,
-    CoreJwsSigningAlgorithm, CoreResponseType,
+use axum_extra::{
+    extract::{
+        cookie::{Cookie, SameSite},
+        CookieJar, PrivateCookieJar,
+    },
+    headers::UserAgent,
+    TypedHeader,
 };
 use openidconnect::{
-    core::CoreProviderMetadata, reqwest::async_http_client, ClientId, ClientSecret, IssuerUrl,
-    ProviderMetadata, RedirectUrl,
+    core::{
+        CoreClient, CoreGenderClaim, CoreJsonWebKeyType, CoreJweContentEncryptionAlgorithm,
+        CoreJwsSigningAlgorithm, CoreProviderMetadata, CoreResponseType,
+    },
+    reqwest::async_http_client,
+    AuthenticationFlow, ClientId, ClientSecret, CsrfToken, EmptyAdditionalClaims, IdToken,
+    IssuerUrl, Nonce, ProviderMetadata, RedirectUrl, Scope,
 };
-use openidconnect::{AuthenticationFlow, CsrfToken, EmptyAdditionalClaims, IdToken, Nonce, Scope};
 use serde_json::json;
 use time::Duration;
 
 use super::LicenseInfo;
-use crate::appstate::AppState;
-use crate::db::{DbPool, MFAInfo, Session, SessionState, Settings, User, UserInfo};
-use crate::enterprise::db::models::openid_provider::OpenIdProvider;
-use crate::error::WebError;
-use crate::handlers::user::{check_username, prune_username};
-use crate::handlers::{ApiResponse, AuthResponse, SESSION_COOKIE_NAME, SIGN_IN_COOKIE_NAME};
-use crate::headers::{check_new_device_login, get_user_agent_device, parse_user_agent};
-use crate::server_config;
+use crate::{
+    appstate::AppState,
+    db::{DbPool, MFAInfo, Session, SessionState, Settings, User, UserInfo},
+    enterprise::db::models::openid_provider::OpenIdProvider,
+    error::WebError,
+    handlers::{
+        user::{check_username, prune_username},
+        ApiResponse, AuthResponse, SESSION_COOKIE_NAME, SIGN_IN_COOKIE_NAME,
+    },
+    headers::{check_new_device_login, get_user_agent_device, parse_user_agent},
+    server_config,
+};
 
 type ProvMeta = ProviderMetadata<
     openidconnect::EmptyAdditionalProviderMetadata,
diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs
index 6cf74f4c5..21ac12f78 100644
--- a/src/grpc/enrollment.rs
+++ b/src/grpc/enrollment.rs
@@ -1,16 +1,18 @@
 use std::sync::Arc;
 
-use super::InstanceInfo;
 use ipnetwork::IpNetwork;
 use sqlx::Transaction;
 use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender};
 use tonic::Status;
 use uaparser::UserAgentParser;
 
-use super::proto::{
-    ActivateUserRequest, AdminInfo, Device as ProtoDevice, DeviceConfig as ProtoDeviceConfig,
-    DeviceConfigResponse, EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice,
-    InitialUserInfo, NewDevice,
+use super::{
+    proto::{
+        ActivateUserRequest, AdminInfo, Device as ProtoDevice, DeviceConfig as ProtoDeviceConfig,
+        DeviceConfigResponse, EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice,
+        InitialUserInfo, NewDevice,
+    },
+    InstanceInfo,
 };
 use crate::{
     db::{
diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs
index 63c36e6c7..ef6390714 100644
--- a/src/grpc/mod.rs
+++ b/src/grpc/mod.rs
@@ -9,7 +9,6 @@ use std::{
     sync::{Arc, Mutex},
 };
 
-use crate::enterprise::grpc::polling::PollingServer;
 use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc};
 use reqwest::Url;
 use serde::Serialize;
@@ -46,6 +45,7 @@ use self::{
 use crate::{
     auth::failed_login::FailedLoginMap,
     db::{AppEvent, Settings},
+    enterprise::grpc::polling::PollingServer,
     handlers::mail::send_gateway_disconnected_email,
     mail::Mail,
     server_config,
diff --git a/src/grpc/utils.rs b/src/grpc/utils.rs
index bb9e511ea..38bd8a1ed 100644
--- a/src/grpc/utils.rs
+++ b/src/grpc/utils.rs
@@ -1,11 +1,19 @@
-use super::InstanceInfo;
 use ipnetwork::IpNetwork;
 use tonic::Status;
 
-use super::proto::{DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse};
-use crate::db::{
-    models::{device::WireguardNetworkDevice, wireguard::WireguardNetwork},
-    DbPool, Device, Settings, User,
+use super::{
+    proto::{DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, InstanceConfigResponse},
+    InstanceInfo,
+};
+use crate::{
+    db::{
+        models::{device::WireguardNetworkDevice, wireguard::WireguardNetwork},
+        DbPool, Device, Settings, User,
+    },
+    enterprise::{
+        db::models::enterprise_settings::EnterpriseSettings,
+        license::{get_cached_license, validate_license},
+    },
 };
 
 pub(crate) async fn build_device_config_response(
@@ -93,3 +101,20 @@ pub(crate) async fn build_device_config_response(
         token: None,
     })
 }
+
+pub(crate) async fn build_instance_config_response(
+    pool: &DbPool,
+) -> Result<InstanceConfigResponse, Status> {
+    debug!("Building instance config response");
+    let enterprise = validate_license(get_cached_license().as_ref()).is_ok();
+    let enterprise_settings = EnterpriseSettings::get(pool).await.map_err(|err| {
+        error!("Failed to get enterprise settings while building instance config response: {err}");
+        Status::internal("unexpected error")
+    })?;
+    debug!("Instance config response built");
+
+    Ok(InstanceConfigResponse {
+        enterprise,
+        disable_all_traffic: enterprise_settings.disable_all_traffic,
+    })
+}
diff --git a/src/handlers/app_info.rs b/src/handlers/app_info.rs
index c0f60f693..a8bc4294c 100644
--- a/src/handlers/app_info.rs
+++ b/src/handlers/app_info.rs
@@ -2,11 +2,11 @@ use axum::{extract::State, http::StatusCode};
 use serde_json::json;
 
 use super::{ApiResponse, ApiResult, VERSION};
-use crate::db::Settings;
-use crate::enterprise::license::get_cached_license;
 use crate::{
-    appstate::AppState, auth::SessionInfo, db::WireguardNetwork,
-    enterprise::license::validate_license,
+    appstate::AppState,
+    auth::SessionInfo,
+    db::{Settings, WireguardNetwork},
+    enterprise::license::{get_cached_license, validate_license},
 };
 
 /// Additional information about core state.
diff --git a/src/secret.rs b/src/secret.rs
index ff062e195..d0434a8f9 100644
--- a/src/secret.rs
+++ b/src/secret.rs
@@ -1,6 +1,4 @@
-use std::convert::Infallible;
-use std::error::Error;
-use std::str::FromStr;
+use std::{convert::Infallible, error::Error, str::FromStr};
 
 use secrecy::{ExposeSecret, Secret};
 use serde::{Deserialize, Serialize};
diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts
index 6217edfb7..c8c7fb13f 100644
--- a/web/src/i18n/en/index.ts
+++ b/web/src/i18n/en/index.ts
@@ -1236,6 +1236,11 @@ const en: BaseTranslation = {
           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.',
+        },
         manualConfig: {
           label: 'Disable users ability to download manual WireGuard configuration',
           helper:
diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts
index 1046d6e97..b5feb55d1 100644
--- a/web/src/i18n/i18n-types.ts
+++ b/web/src/i18n/i18n-types.ts
@@ -2936,6 +2936,16 @@ 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
+				}
 				manualConfig: {
 					/**
 					 * D​i​s​a​b​l​e​ ​u​s​e​r​s​ ​a​b​i​l​i​t​y​ ​t​o​ ​d​o​w​n​l​o​a​d​ ​m​a​n​u​a​l​ ​W​i​r​e​G​u​a​r​d​ ​c​o​n​f​i​g​u​r​a​t​i​o​n
@@ -7176,6 +7186,16 @@ 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
+				}
 				manualConfig: {
 					/**
 					 * Disable users ability to download manual WireGuard configuration
diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts
index 723d73afa..8c73d9bce 100644
--- a/web/src/i18n/pl/index.ts
+++ b/web/src/i18n/pl/index.ts
@@ -1223,6 +1223,11 @@ 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.',
+        },
         manualConfig: {
           label: 'Wyłącz wyświetlanie konfiguracji 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 912fd3d80..fac922a09 100644
--- a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx
+++ b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx
@@ -75,6 +75,19 @@ export const EnterpriseForm = () => {
             {parse(LL.settingsPage.enterprise.fields.manualConfig.helper())}
           </Helper>
         </div>
+        <div className="checkbox-row">
+          <LabeledCheckbox
+            disabled={isLoading}
+            label={LL.settingsPage.enterprise.fields.disableAllTraffic.label()}
+            value={settings.disable_all_traffic}
+            onChange={() =>
+              mutate({ disable_all_traffic: !settings.disable_all_traffic })
+            }
+          />
+          <Helper>
+            {parse(LL.settingsPage.enterprise.fields.disableAllTraffic.helper())}
+          </Helper>
+        </div>
       </Card>
     </section>
   );
diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts
index 06336c7ae..fd02238e0 100644
--- a/web/src/shared/types.ts
+++ b/web/src/shared/types.ts
@@ -871,6 +871,7 @@ export type SettingsLicense = {
 
 export type SettingsEnterprise = {
   admin_device_management: boolean;
+  disable_all_traffic: boolean;
   only_client_activation: boolean;
 };
 


Test patch:
diff --git a/tests/enterprise_settings.rs b/tests/enterprise_settings.rs
index 6f064d2a3..88a3fc41c 100644
--- a/tests/enterprise_settings.rs
+++ b/tests/enterprise_settings.rs
@@ -1,14 +1,16 @@
 mod common;
 
-use defguard::enterprise::{
-    db::models::enterprise_settings::EnterpriseSettings,
-    license::{get_cached_license, set_cached_license},
+use defguard::{
+    enterprise::{
+        db::models::enterprise_settings::EnterpriseSettings,
+        license::{get_cached_license, set_cached_license},
+    },
+    handlers::Auth,
 };
 use reqwest::StatusCode;
+use serde_json::{json, Value};
 
 use self::common::make_test_client;
-use defguard::handlers::Auth;
-use serde_json::{json, Value};
 
 fn make_network() -> Value {
     json!({
@@ -41,6 +43,7 @@ async fn test_only_enterprise_can_modify() {
     let settings = EnterpriseSettings {
         id: None,
         admin_device_management: true,
+        disable_all_traffic: false,
         only_client_activation: false,
     };
 
@@ -85,6 +88,7 @@ async fn test_admin_devices_management_is_enforced() {
     let settings = EnterpriseSettings {
         id: None,
         admin_device_management: true,
+        disable_all_traffic: false,
         only_client_activation: false,
     };
     let response = client
@@ -162,6 +166,7 @@ async fn test_regular_user_device_management() {
     let settings = EnterpriseSettings {
         id: None,
         admin_device_management: false,
+        disable_all_traffic: false,
         only_client_activation: false,
     };
     let response = client