diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index afe826623e..442750b8a0 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -238,7 +238,11 @@ async fn get_manual_destination_rules( } // prepare destination addresses - let (dest_addrs_v4, dest_addrs_v6) = process_destination_addrs(&addresses, &address_ranges); + let (dest_addrs_v4, dest_addrs_v6) = if any_address { + (Vec::new(), Vec::new()) + } else { + process_destination_addrs(&addresses, &address_ranges) + }; // prepare destination ports let destination_ports = if any_port { @@ -319,8 +323,11 @@ async fn get_predefined_destination_rules( let alias_destination_ranges = destination.get_destination_ranges(&mut *conn).await?; // combine destination addrs - let (dest_addrs_v4, dest_addrs_v6) = - process_alias_destination_addrs(&destination.addresses, &alias_destination_ranges); + let (dest_addrs_v4, dest_addrs_v6) = if destination.any_address { + (Vec::new(), Vec::new()) + } else { + process_alias_destination_addrs(&destination.addresses, &alias_destination_ranges) + }; // process alias ports let destination_ports = if destination.any_port { @@ -430,6 +437,7 @@ fn create_rules( debug!("ALLOW rule generated from ACL: {rule:?}"); Some(rule) }; + // prepare DENY rule // it should specify only the destination addrs to block all remaining traffic let deny = FirewallRule { diff --git a/crates/defguard_core/src/enterprise/firewall/tests/destination.rs b/crates/defguard_core/src/enterprise/firewall/tests/destination.rs index fd8e62bab7..19f4d68add 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/destination.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/destination.rs @@ -1,11 +1,21 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use defguard_proto::enterprise::firewall::{IpAddress, IpRange, ip_address::Address}; +use defguard_common::db::{NoId, models::WireguardNetwork, setup_pool}; +use defguard_proto::enterprise::firewall::{ + FirewallPolicy, IpAddress, IpRange, ip_address::Address, +}; +use rand::thread_rng; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use crate::enterprise::{ - db::models::acl::AclRuleDestinationRange, firewall::process_destination_addrs, + db::models::acl::{ + AclAlias, AclAliasDestinationRange, AclRule, AclRuleDestinationRange, AliasKind, RuleState, + }, + firewall::{process_destination_addrs, try_get_location_firewall_config}, }; +use super::{create_acl_rule, create_test_users_and_devices, set_test_license_business}; + #[test] fn test_process_destination_addrs_v4() { // Test data with mixed IPv4 and IPv6 networks @@ -115,3 +125,194 @@ fn test_process_destination_addrs_v6() { let ipv4_only = process_destination_addrs(&["192.168.1.0/24".parse().unwrap()], &[]); assert!(ipv4_only.1.is_empty()); } + +#[sqlx::test] +async fn test_any_address_overwrites_manual_destination( + _: PgPoolOptions, + options: PgConnectOptions, +) { + set_test_license_business(); + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec!["10.0.0.0/16".parse().unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + + let acl_rule = AclRule { + id: NoId, + name: "any destination rule".to_string(), + state: RuleState::Applied, + allow_all_users: true, + any_address: true, + addresses: vec!["192.168.1.0/24".parse().unwrap()], + use_manual_destination_settings: true, + ..Default::default() + }; + + create_acl_rule( + &pool, + acl_rule, + vec![location.id], + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + vec![( + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)), + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)), + )], + Vec::new(), + ) + .await; + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + let expected_source_addrs = [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + ]; + + assert_eq!(generated_firewall_rules.len(), 2); + + let allow_rule = &generated_firewall_rules[0]; + assert_eq!(allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule.source_addrs, expected_source_addrs); + assert!(allow_rule.destination_addrs.is_empty()); + + let deny_rule = &generated_firewall_rules[1]; + assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule.source_addrs.is_empty()); + assert!(deny_rule.destination_addrs.is_empty()); +} + +#[sqlx::test] +async fn test_any_address_overwrites_destination_alias_addrs( + _: PgPoolOptions, + options: PgConnectOptions, +) { + set_test_license_business(); + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec!["10.0.0.0/16".parse().unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + + let destination_alias = AclAlias { + id: NoId, + name: "any destination alias".to_string(), + kind: AliasKind::Destination, + any_address: true, + any_port: true, + any_protocol: true, + addresses: vec!["10.1.0.0/24".parse().unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + AclAliasDestinationRange { + id: NoId, + alias_id: destination_alias.id, + start: IpAddr::V4(Ipv4Addr::new(10, 2, 0, 10)), + end: IpAddr::V4(Ipv4Addr::new(10, 2, 0, 20)), + } + .save(&pool) + .await + .unwrap(); + + let acl_rule = AclRule { + id: NoId, + name: "any destination alias rule".to_string(), + state: RuleState::Applied, + allow_all_users: true, + use_manual_destination_settings: false, + ..Default::default() + }; + + create_acl_rule( + &pool, + acl_rule, + vec![location.id], + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + vec![destination_alias.id], + ) + .await; + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + let expected_source_addrs = [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), + }, + ]; + + assert_eq!(generated_firewall_rules.len(), 2); + + let allow_rule = &generated_firewall_rules[0]; + assert_eq!(allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule.source_addrs, expected_source_addrs); + assert!(allow_rule.destination_addrs.is_empty()); + + let deny_rule = &generated_firewall_rules[1]; + assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule.source_addrs.is_empty()); + assert!(deny_rule.destination_addrs.is_empty()); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs index 40e6d233ba..78c8ca896d 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs @@ -1801,6 +1801,7 @@ async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { addresses: vec!["192.168.1.0/24".parse().unwrap()], allow_all_users: true, use_manual_destination_settings: true, + any_address: false, ..Default::default() } .save(&pool) diff --git a/web/src/pages/CERulePage/CERulePage.tsx b/web/src/pages/CERulePage/CERulePage.tsx index 9ac7a5a167..57dd5b4522 100644 --- a/web/src/pages/CERulePage/CERulePage.tsx +++ b/web/src/pages/CERulePage/CERulePage.tsx @@ -30,6 +30,7 @@ import type { import { AppText } from '../../shared/defguard-ui/components/AppText/AppText'; import { Button } from '../../shared/defguard-ui/components/Button/Button'; import { ButtonsGroup } from '../../shared/defguard-ui/components/ButtonsGroup/ButtonsGroup'; +import { Checkbox } from '../../shared/defguard-ui/components/Checkbox/Checkbox'; import { CheckboxIndicator } from '../../shared/defguard-ui/components/CheckboxIndicator/CheckboxIndicator'; import { Chip } from '../../shared/defguard-ui/components/Chip/Chip'; import { Divider } from '../../shared/defguard-ui/components/Divider/Divider'; @@ -253,8 +254,22 @@ const Content = ({ rule: initialRule }: Props) => { return []; }, [networkDevices]); - const [_restrictionsPresent, _setRestrictionsPresent] = useState(false); - // const [manualDestination, setManualDestination] = useState(false); + const [restrictUsers, setRestrictUsers] = useState(() => + isPresent(initialRule) + ? initialRule.deny_all_users || initialRule.denied_users.length > 0 + : false, + ); + const [restrictGroups, setRestrictGroups] = useState(() => + isPresent(initialRule) + ? initialRule.deny_all_groups || initialRule.denied_groups.length > 0 + : false, + ); + const [restrictDevices, setRestrictDevices] = useState(() => + isPresent(initialRule) + ? initialRule.deny_all_network_devices || + initialRule.denied_network_devices.length > 0 + : false, + ); const formSchema = useMemo( () => @@ -417,13 +432,18 @@ const Content = ({ rule: initialRule }: Props) => { }, onSubmit: async ({ value }) => { const toSend = cloneDeep(value); - // FIXME: When restrictions section is reworked - toSend.deny_all_network_devices = false; - toSend.deny_all_users = false; - toSend.deny_all_groups = false; - toSend.denied_network_devices = []; - toSend.denied_groups = []; - toSend.denied_users = []; + if (!restrictUsers) { + toSend.deny_all_users = false; + toSend.denied_users = []; + } + if (!restrictGroups) { + toSend.deny_all_groups = false; + toSend.denied_groups = []; + } + if (!restrictDevices) { + toSend.deny_all_network_devices = false; + toSend.denied_network_devices = []; + } if (isPresent(initialRule)) { await editRule({ ...toSend, @@ -797,66 +817,172 @@ const Content = ({ rule: initialRule }: Props) => { )} - {/* + {`Restrictions`} - -

{`If needed, you may exclude specific users, groups, or devices from accessing this location.`}

+ +

{`Choose who or what should be blocked from accessing this location.`}

- { - setRestrictionsPresent((s) => !s); - }} - text="Add restriction settings" - /> - - - {isPresent(usersOptions) && ( - - {(field) => ( - `Users ${counter}`} - editText={`Edit users`} - modalTitle="Select restricted users" - options={usersOptions} - /> - )} - - )} - - {isPresent(groupsOptions) && ( - - {(field) => ( - `Groups ${counter}`} - editText="Edit groups" - modalTitle="Select restricted groups" - toggleText="Exclude specific groups" - /> - )} - - )} - - {isPresent(networkDevicesOptions) && ( - - {(field) => ( - `Devices ${counter}`} - editText="Edit devices" - modalTitle="Select restricted devices" - toggleText="Exclude specific network devices" - /> - )} - - )} - -
*/} + {isPresent(usersOptions) && ( +
+
+ { + setRestrictUsers((current) => !current); + }} + text="Limit access for users" + /> +
+ +
+
+ + {(field) => ( + + )} + + + {(field) => ( + + )} + +
+ s.values.deny_all_users === false && restrictUsers} + > + {(open) => ( + + {isPresent(usersOptions) && ( + + {(field) => ( + {}} + counterText={(counter) => `Users ${counter}`} + editText="Edit users" + modalTitle="Select restricted users" + options={usersOptions} + /> + )} + + )} + + )} + +
+
+
+ )} + + {isPresent(groupsOptions) && ( +
+
+ { + setRestrictGroups((current) => !current); + }} + text="Limit access for groups" + /> +
+ +
+
+ + {(field) => ( + + )} + + + {(field) => ( + + )} + +
+ s.values.deny_all_groups === false && restrictGroups} + > + {(open) => ( + + {isPresent(groupsOptions) && ( + + {(field) => ( + {}} + counterText={(counter) => `Groups ${counter}`} + editText="Edit groups" + modalTitle="Select restricted groups" + options={groupsOptions} + /> + )} + + )} + + )} + +
+
+
+ )} + + {isPresent(networkDevicesOptions) && ( +
+
+ { + setRestrictDevices((current) => !current); + }} + text="Limit access for devices" + /> +
+ +
+
+ + {(field) => ( + + )} + + + {(field) => ( + + )} + +
+ + s.values.deny_all_network_devices === false && restrictDevices + } + > + {(open) => ( + + {isPresent(networkDevicesOptions) && ( + + {(field) => ( + {}} + counterText={(counter) => `Devices ${counter}`} + editText="Edit devices" + modalTitle="Select restricted devices" + options={networkDevicesOptions} + /> + )} + + )} + + )} + +
+
+
+ )} + ({ isSubmitting: s.isSubmitting })}> {({ isSubmitting }) => ( diff --git a/web/src/pages/CERulePage/style.scss b/web/src/pages/CERulePage/style.scss index 7e0c4db899..7db047d241 100644 --- a/web/src/pages/CERulePage/style.scss +++ b/web/src/pages/CERulePage/style.scss @@ -75,6 +75,39 @@ } } } + + .restriction-block { + display: flex; + flex-direction: column; + + .fold.folded .fold-content { + padding: 0; + } + + & > .fold { + margin-top: var(--spacing-sm); + } + + & > .fold.folded { + margin-top: 0; + } + } + + .restriction-toggle { + display: flex; + align-items: center; + } + + .restriction-body { + display: flex; + flex-direction: column; + } + + .restriction-radio { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + } } .selection-section .item .destination-selection-item {