diff --git a/.sqlx/query-6915bd0015a04c1caf439b1932ab4fd9100d098098f181d8ebef6f77b8cd7097.json b/.sqlx/query-1f81c51b91ccc2e7c038122d1165157859aa5901d8bc74fa23c121a2a8c13c4d.json similarity index 64% rename from .sqlx/query-6915bd0015a04c1caf439b1932ab4fd9100d098098f181d8ebef6f77b8cd7097.json rename to .sqlx/query-1f81c51b91ccc2e7c038122d1165157859aa5901d8bc74fa23c121a2a8c13c4d.json index a4a593a9a9..e3c2d1a12e 100644 --- a/.sqlx/query-6915bd0015a04c1caf439b1932ab4fd9100d098098f181d8ebef6f77b8cd7097.json +++ b/.sqlx/query-1f81c51b91ccc2e7c038122d1165157859aa5901d8bc74fa23c121a2a8c13c4d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"aclrule\" SET \"parent_id\" = $2,\"state\" = $3,\"name\" = $4,\"allow_all_users\" = $5,\"deny_all_users\" = $6,\"allow_all_network_devices\" = $7,\"deny_all_network_devices\" = $8,\"all_networks\" = $9,\"destination\" = $10,\"ports\" = $11,\"protocols\" = $12,\"enabled\" = $13,\"expires\" = $14,\"any_destination\" = $15,\"any_port\" = $16,\"any_protocol\" = $17,\"manual_settings\" = $18 WHERE id = $1", + "query": "UPDATE \"aclrule\" SET \"parent_id\" = $2,\"state\" = $3,\"name\" = $4,\"allow_all_users\" = $5,\"deny_all_users\" = $6,\"allow_all_groups\" = $7,\"deny_all_groups\" = $8,\"allow_all_network_devices\" = $9,\"deny_all_network_devices\" = $10,\"all_locations\" = $11,\"addresses\" = $12,\"ports\" = $13,\"protocols\" = $14,\"enabled\" = $15,\"expires\" = $16,\"any_address\" = $17,\"any_port\" = $18,\"any_protocol\" = $19,\"use_manual_destination_settings\" = $20 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -27,6 +27,8 @@ "Bool", "Bool", "Bool", + "Bool", + "Bool", "InetArray", "Int4RangeArray", "Int4Array", @@ -40,5 +42,5 @@ }, "nullable": [] }, - "hash": "6915bd0015a04c1caf439b1932ab4fd9100d098098f181d8ebef6f77b8cd7097" + "hash": "1f81c51b91ccc2e7c038122d1165157859aa5901d8bc74fa23c121a2a8c13c4d" } diff --git a/.sqlx/query-f7c6882463818d70c1f092d440175269185fe6b6d021bed8623fe25b891e01d5.json b/.sqlx/query-2adf5c7560026fa0c311aba80ae45aa5f93430b81d20e6e5b84d664590ba73d6.json similarity index 63% rename from .sqlx/query-f7c6882463818d70c1f092d440175269185fe6b6d021bed8623fe25b891e01d5.json rename to .sqlx/query-2adf5c7560026fa0c311aba80ae45aa5f93430b81d20e6e5b84d664590ba73d6.json index 73de745775..a891ef095e 100644 --- a/.sqlx/query-f7c6882463818d70c1f092d440175269185fe6b6d021bed8623fe25b891e01d5.json +++ b/.sqlx/query-2adf5c7560026fa0c311aba80ae45aa5f93430b81d20e6e5b84d664590ba73d6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"aclrule\" (\"parent_id\",\"state\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_networks\",\"destination\",\"ports\",\"protocols\",\"enabled\",\"expires\",\"any_destination\",\"any_port\",\"any_protocol\",\"manual_settings\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) RETURNING id", + "query": "INSERT INTO \"aclrule\" (\"parent_id\",\"state\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_groups\",\"deny_all_groups\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_locations\",\"addresses\",\"ports\",\"protocols\",\"enabled\",\"expires\",\"any_address\",\"any_port\",\"any_protocol\",\"use_manual_destination_settings\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) RETURNING id", "describe": { "columns": [ { @@ -32,6 +32,8 @@ "Bool", "Bool", "Bool", + "Bool", + "Bool", "InetArray", "Int4RangeArray", "Int4Array", @@ -47,5 +49,5 @@ false ] }, - "hash": "f7c6882463818d70c1f092d440175269185fe6b6d021bed8623fe25b891e01d5" + "hash": "2adf5c7560026fa0c311aba80ae45aa5f93430b81d20e6e5b84d664590ba73d6" } diff --git a/.sqlx/query-1faddb4c68980e6b922422c90b1ae4c971a4c8c8da454627dc7f2542a9796c3a.json b/.sqlx/query-3456b6ba4d6f4f37ef0db33a8d30ae180d62382bec045c3ac32c76f163fe3adf.json similarity index 84% rename from .sqlx/query-1faddb4c68980e6b922422c90b1ae4c971a4c8c8da454627dc7f2542a9796c3a.json rename to .sqlx/query-3456b6ba4d6f4f37ef0db33a8d30ae180d62382bec045c3ac32c76f163fe3adf.json index 3c72f27599..99434543c2 100644 --- a/.sqlx/query-1faddb4c68980e6b922422c90b1ae4c971a4c8c8da454627dc7f2542a9796c3a.json +++ b/.sqlx/query-3456b6ba4d6f4f37ef0db33a8d30ae180d62382bec045c3ac32c76f163fe3adf.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"parent_id\",\"name\",\"kind\" \"kind: _\",\"state\" \"state: _\",\"destination\" \"destination: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"any_destination\",\"any_port\",\"any_protocol\" FROM \"aclalias\"", + "query": "SELECT id, \"parent_id\",\"name\",\"kind\" \"kind: _\",\"state\" \"state: _\",\"addresses\" \"addresses: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"any_address\",\"any_port\",\"any_protocol\" FROM \"aclalias\"", "describe": { "columns": [ { @@ -50,7 +50,7 @@ }, { "ordinal": 5, - "name": "destination: _", + "name": "addresses: _", "type_info": "InetArray" }, { @@ -65,7 +65,7 @@ }, { "ordinal": 8, - "name": "any_destination", + "name": "any_address", "type_info": "Bool" }, { @@ -96,5 +96,5 @@ false ] }, - "hash": "1faddb4c68980e6b922422c90b1ae4c971a4c8c8da454627dc7f2542a9796c3a" + "hash": "3456b6ba4d6f4f37ef0db33a8d30ae180d62382bec045c3ac32c76f163fe3adf" } diff --git a/.sqlx/query-617d61862f5fa1f3fde44d67e34b7adc7ea6fc6cb3a04e5cd815c382671b3e32.json b/.sqlx/query-605463af2515f219a7b53878d459db6026387d79e1975dd7ed44cd9ff4600214.json similarity index 78% rename from .sqlx/query-617d61862f5fa1f3fde44d67e34b7adc7ea6fc6cb3a04e5cd815c382671b3e32.json rename to .sqlx/query-605463af2515f219a7b53878d459db6026387d79e1975dd7ed44cd9ff4600214.json index b974c4dfe5..cdee5a491d 100644 --- a/.sqlx/query-617d61862f5fa1f3fde44d67e34b7adc7ea6fc6cb3a04e5cd815c382671b3e32.json +++ b/.sqlx/query-605463af2515f219a7b53878d459db6026387d79e1975dd7ed44cd9ff4600214.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE aclrule SET state = 'expired'::aclrule_state WHERE state = 'applied'::aclrule_state AND expires < NOW() RETURNING id, parent_id, state AS \"state: RuleState\", name, allow_all_users, deny_all_users, allow_all_network_devices, deny_all_network_devices, all_networks, destination, ports, protocols, enabled, expires, any_destination, any_port, any_protocol, manual_settings", + "query": "UPDATE aclrule SET state = 'expired'::aclrule_state WHERE state = 'applied'::aclrule_state AND expires < NOW() RETURNING id, parent_id, state AS \"state: RuleState\", name, allow_all_users, deny_all_users, allow_all_groups, deny_all_groups, allow_all_network_devices, deny_all_network_devices, all_locations, addresses, ports, protocols, enabled, expires, any_address, any_port, any_protocol, use_manual_destination_settings", "describe": { "columns": [ { @@ -48,62 +48,72 @@ }, { "ordinal": 6, - "name": "allow_all_network_devices", + "name": "allow_all_groups", "type_info": "Bool" }, { "ordinal": 7, - "name": "deny_all_network_devices", + "name": "deny_all_groups", "type_info": "Bool" }, { "ordinal": 8, - "name": "all_networks", + "name": "allow_all_network_devices", "type_info": "Bool" }, { "ordinal": 9, - "name": "destination", - "type_info": "InetArray" + "name": "deny_all_network_devices", + "type_info": "Bool" }, { "ordinal": 10, + "name": "all_locations", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "addresses", + "type_info": "InetArray" + }, + { + "ordinal": 12, "name": "ports", "type_info": "Int4RangeArray" }, { - "ordinal": 11, + "ordinal": 13, "name": "protocols", "type_info": "Int4Array" }, { - "ordinal": 12, + "ordinal": 14, "name": "enabled", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 15, "name": "expires", "type_info": "Timestamp" }, { - "ordinal": 14, - "name": "any_destination", + "ordinal": 16, + "name": "any_address", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 17, "name": "any_port", "type_info": "Bool" }, { - "ordinal": 16, + "ordinal": 18, "name": "any_protocol", "type_info": "Bool" }, { - "ordinal": 17, - "name": "manual_settings", + "ordinal": 19, + "name": "use_manual_destination_settings", "type_info": "Bool" } ], @@ -124,6 +134,8 @@ false, false, false, + false, + false, true, false, false, @@ -131,5 +143,5 @@ false ] }, - "hash": "617d61862f5fa1f3fde44d67e34b7adc7ea6fc6cb3a04e5cd815c382671b3e32" + "hash": "605463af2515f219a7b53878d459db6026387d79e1975dd7ed44cd9ff4600214" } diff --git a/.sqlx/query-e907d432de9692381c67b0eaf66a58a39e79ace0cc7798df471d26a786352b89.json b/.sqlx/query-707ad10c1504f8b90f3005a33f02c7741c93938e28cc40d57dc43b035852c707.json similarity index 75% rename from .sqlx/query-e907d432de9692381c67b0eaf66a58a39e79ace0cc7798df471d26a786352b89.json rename to .sqlx/query-707ad10c1504f8b90f3005a33f02c7741c93938e28cc40d57dc43b035852c707.json index 21b384d146..5ea7d38dd7 100644 --- a/.sqlx/query-e907d432de9692381c67b0eaf66a58a39e79ace0cc7798df471d26a786352b89.json +++ b/.sqlx/query-707ad10c1504f8b90f3005a33f02c7741c93938e28cc40d57dc43b035852c707.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT ar.id, parent_id, state AS \"state: RuleState\", name, allow_all_users, deny_all_users, allow_all_network_devices, deny_all_network_devices, all_networks, destination, ports, protocols, enabled, expires, any_destination, any_port, any_protocol, manual_settings FROM aclrulealias ara JOIN aclrule ar ON ar.id = ara.rule_id WHERE ara.alias_id = $1", + "query": "SELECT ar.id, parent_id, state AS \"state: RuleState\", name, allow_all_users, deny_all_users, allow_all_groups, deny_all_groups, allow_all_network_devices, deny_all_network_devices, all_locations, addresses, ports, protocols, enabled, expires, any_address, any_port, any_protocol, use_manual_destination_settings FROM aclrulealias ara JOIN aclrule ar ON ar.id = ara.rule_id WHERE ara.alias_id = $1", "describe": { "columns": [ { @@ -48,62 +48,72 @@ }, { "ordinal": 6, - "name": "allow_all_network_devices", + "name": "allow_all_groups", "type_info": "Bool" }, { "ordinal": 7, - "name": "deny_all_network_devices", + "name": "deny_all_groups", "type_info": "Bool" }, { "ordinal": 8, - "name": "all_networks", + "name": "allow_all_network_devices", "type_info": "Bool" }, { "ordinal": 9, - "name": "destination", - "type_info": "InetArray" + "name": "deny_all_network_devices", + "type_info": "Bool" }, { "ordinal": 10, + "name": "all_locations", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "addresses", + "type_info": "InetArray" + }, + { + "ordinal": 12, "name": "ports", "type_info": "Int4RangeArray" }, { - "ordinal": 11, + "ordinal": 13, "name": "protocols", "type_info": "Int4Array" }, { - "ordinal": 12, + "ordinal": 14, "name": "enabled", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 15, "name": "expires", "type_info": "Timestamp" }, { - "ordinal": 14, - "name": "any_destination", + "ordinal": 16, + "name": "any_address", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 17, "name": "any_port", "type_info": "Bool" }, { - "ordinal": 16, + "ordinal": 18, "name": "any_protocol", "type_info": "Bool" }, { - "ordinal": 17, - "name": "manual_settings", + "ordinal": 19, + "name": "use_manual_destination_settings", "type_info": "Bool" } ], @@ -126,6 +136,8 @@ false, false, false, + false, + false, true, false, false, @@ -133,5 +145,5 @@ false ] }, - "hash": "e907d432de9692381c67b0eaf66a58a39e79ace0cc7798df471d26a786352b89" + "hash": "707ad10c1504f8b90f3005a33f02c7741c93938e28cc40d57dc43b035852c707" } diff --git a/.sqlx/query-4095f28a023f9fb2f96cd890c795c36d353c4d93e9afe577131c61a3550dc14c.json b/.sqlx/query-9457aa22a755f600e1cb2be0a1b5b54c25f6ce675b8d445cd4a8e60f5617ebd0.json similarity index 79% rename from .sqlx/query-4095f28a023f9fb2f96cd890c795c36d353c4d93e9afe577131c61a3550dc14c.json rename to .sqlx/query-9457aa22a755f600e1cb2be0a1b5b54c25f6ce675b8d445cd4a8e60f5617ebd0.json index 2e8999a6a5..21b3d35624 100644 --- a/.sqlx/query-4095f28a023f9fb2f96cd890c795c36d353c4d93e9afe577131c61a3550dc14c.json +++ b/.sqlx/query-9457aa22a755f600e1cb2be0a1b5b54c25f6ce675b8d445cd4a8e60f5617ebd0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"aclalias\" (\"parent_id\",\"name\",\"kind\",\"state\",\"destination\",\"ports\",\"protocols\",\"any_destination\",\"any_port\",\"any_protocol\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING id", + "query": "INSERT INTO \"aclalias\" (\"parent_id\",\"name\",\"kind\",\"state\",\"addresses\",\"ports\",\"protocols\",\"any_address\",\"any_port\",\"any_protocol\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING id", "describe": { "columns": [ { @@ -47,5 +47,5 @@ false ] }, - "hash": "4095f28a023f9fb2f96cd890c795c36d353c4d93e9afe577131c61a3550dc14c" + "hash": "9457aa22a755f600e1cb2be0a1b5b54c25f6ce675b8d445cd4a8e60f5617ebd0" } diff --git a/.sqlx/query-07f4a663712e84d62e115cfe2585598bd90270df2e0b75d4b8a94da0cd426009.json b/.sqlx/query-958d562bda9f1f4a4c01d08daeb6db4985eca201b7a01b5cec090606c355d1d0.json similarity index 84% rename from .sqlx/query-07f4a663712e84d62e115cfe2585598bd90270df2e0b75d4b8a94da0cd426009.json rename to .sqlx/query-958d562bda9f1f4a4c01d08daeb6db4985eca201b7a01b5cec090606c355d1d0.json index f351aead3d..ad8e6500cd 100644 --- a/.sqlx/query-07f4a663712e84d62e115cfe2585598bd90270df2e0b75d4b8a94da0cd426009.json +++ b/.sqlx/query-958d562bda9f1f4a4c01d08daeb6db4985eca201b7a01b5cec090606c355d1d0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT a.id, parent_id, name, kind \"kind: AliasKind\",state \"state: AliasState\", destination, ports, protocols, any_destination, any_port, any_protocol FROM aclrulealias r JOIN aclalias a ON a.id = r.alias_id WHERE r.rule_id = $1", + "query": "SELECT a.id, parent_id, name, kind \"kind: AliasKind\",state \"state: AliasState\", addresses, ports, protocols, any_address, any_port, any_protocol FROM aclrulealias r JOIN aclalias a ON a.id = r.alias_id WHERE r.rule_id = $1", "describe": { "columns": [ { @@ -50,7 +50,7 @@ }, { "ordinal": 5, - "name": "destination", + "name": "addresses", "type_info": "InetArray" }, { @@ -65,7 +65,7 @@ }, { "ordinal": 8, - "name": "any_destination", + "name": "any_address", "type_info": "Bool" }, { @@ -98,5 +98,5 @@ false ] }, - "hash": "07f4a663712e84d62e115cfe2585598bd90270df2e0b75d4b8a94da0cd426009" + "hash": "958d562bda9f1f4a4c01d08daeb6db4985eca201b7a01b5cec090606c355d1d0" } diff --git a/.sqlx/query-e886ab4f4c0ac47d41e96005d8c82eea1705b9849ec2afb19b998a91644bac25.json b/.sqlx/query-9b66761e2d6e6dac398176981c3c59c79bd103c034de0ac684649ee40ebc04c7.json similarity index 74% rename from .sqlx/query-e886ab4f4c0ac47d41e96005d8c82eea1705b9849ec2afb19b998a91644bac25.json rename to .sqlx/query-9b66761e2d6e6dac398176981c3c59c79bd103c034de0ac684649ee40ebc04c7.json index 133445ad9d..7bb0f97454 100644 --- a/.sqlx/query-e886ab4f4c0ac47d41e96005d8c82eea1705b9849ec2afb19b998a91644bac25.json +++ b/.sqlx/query-9b66761e2d6e6dac398176981c3c59c79bd103c034de0ac684649ee40ebc04c7.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"parent_id\",\"state\" \"state: _\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_networks\",\"destination\" \"destination: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"enabled\",\"expires\",\"any_destination\",\"any_port\",\"any_protocol\",\"manual_settings\" FROM \"aclrule\"", + "query": "SELECT id, \"parent_id\",\"state\" \"state: _\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_groups\",\"deny_all_groups\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_locations\",\"addresses\" \"addresses: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"enabled\",\"expires\",\"any_address\",\"any_port\",\"any_protocol\",\"use_manual_destination_settings\" FROM \"aclrule\"", "describe": { "columns": [ { @@ -48,62 +48,72 @@ }, { "ordinal": 6, - "name": "allow_all_network_devices", + "name": "allow_all_groups", "type_info": "Bool" }, { "ordinal": 7, - "name": "deny_all_network_devices", + "name": "deny_all_groups", "type_info": "Bool" }, { "ordinal": 8, - "name": "all_networks", + "name": "allow_all_network_devices", "type_info": "Bool" }, { "ordinal": 9, - "name": "destination: _", - "type_info": "InetArray" + "name": "deny_all_network_devices", + "type_info": "Bool" }, { "ordinal": 10, + "name": "all_locations", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "addresses: _", + "type_info": "InetArray" + }, + { + "ordinal": 12, "name": "ports: _", "type_info": "Int4RangeArray" }, { - "ordinal": 11, + "ordinal": 13, "name": "protocols: _", "type_info": "Int4Array" }, { - "ordinal": 12, + "ordinal": 14, "name": "enabled", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 15, "name": "expires", "type_info": "Timestamp" }, { - "ordinal": 14, - "name": "any_destination", + "ordinal": 16, + "name": "any_address", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 17, "name": "any_port", "type_info": "Bool" }, { - "ordinal": 16, + "ordinal": 18, "name": "any_protocol", "type_info": "Bool" }, { - "ordinal": 17, - "name": "manual_settings", + "ordinal": 19, + "name": "use_manual_destination_settings", "type_info": "Bool" } ], @@ -124,6 +134,8 @@ false, false, false, + false, + false, true, false, false, @@ -131,5 +143,5 @@ false ] }, - "hash": "e886ab4f4c0ac47d41e96005d8c82eea1705b9849ec2afb19b998a91644bac25" + "hash": "9b66761e2d6e6dac398176981c3c59c79bd103c034de0ac684649ee40ebc04c7" } diff --git a/.sqlx/query-f49f70e1e6c0e250f95e790016142b39dd99f34a47caa0cebe57cee504e90cde.json b/.sqlx/query-a5ad0dcf3a8e364ddb42094e1b3bc22c8e9013eff9505b9d06a2e55f5ac62d8d.json similarity index 88% rename from .sqlx/query-f49f70e1e6c0e250f95e790016142b39dd99f34a47caa0cebe57cee504e90cde.json rename to .sqlx/query-a5ad0dcf3a8e364ddb42094e1b3bc22c8e9013eff9505b9d06a2e55f5ac62d8d.json index 93792833c5..c3dc6a2353 100644 --- a/.sqlx/query-f49f70e1e6c0e250f95e790016142b39dd99f34a47caa0cebe57cee504e90cde.json +++ b/.sqlx/query-a5ad0dcf3a8e364ddb42094e1b3bc22c8e9013eff9505b9d06a2e55f5ac62d8d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", destination, ports, protocols, any_destination, any_port, any_protocol FROM aclalias WHERE id = $1 AND kind = $2", + "query": "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", addresses, ports, protocols, any_address, any_port, any_protocol FROM aclalias WHERE id = $1 AND kind = $2", "describe": { "columns": [ { @@ -50,7 +50,7 @@ }, { "ordinal": 5, - "name": "destination", + "name": "addresses", "type_info": "InetArray" }, { @@ -65,7 +65,7 @@ }, { "ordinal": 8, - "name": "any_destination", + "name": "any_address", "type_info": "Bool" }, { @@ -109,5 +109,5 @@ false ] }, - "hash": "f49f70e1e6c0e250f95e790016142b39dd99f34a47caa0cebe57cee504e90cde" + "hash": "a5ad0dcf3a8e364ddb42094e1b3bc22c8e9013eff9505b9d06a2e55f5ac62d8d" } diff --git a/.sqlx/query-2829f4f5fe69dcf6ea08a0346b91407a9ad71d5f76a7741882282fef0a2e928b.json b/.sqlx/query-bd0d9b7dfce0d3a4e55ca45aeda52bbe18c9871cf30ad82fbea85f9b459002c2.json similarity index 76% rename from .sqlx/query-2829f4f5fe69dcf6ea08a0346b91407a9ad71d5f76a7741882282fef0a2e928b.json rename to .sqlx/query-bd0d9b7dfce0d3a4e55ca45aeda52bbe18c9871cf30ad82fbea85f9b459002c2.json index 49ac96f605..ddec1c4bd9 100644 --- a/.sqlx/query-2829f4f5fe69dcf6ea08a0346b91407a9ad71d5f76a7741882282fef0a2e928b.json +++ b/.sqlx/query-bd0d9b7dfce0d3a4e55ca45aeda52bbe18c9871cf30ad82fbea85f9b459002c2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"aclalias\" SET \"parent_id\" = $2,\"name\" = $3,\"kind\" = $4,\"state\" = $5,\"destination\" = $6,\"ports\" = $7,\"protocols\" = $8,\"any_destination\" = $9,\"any_port\" = $10,\"any_protocol\" = $11 WHERE id = $1", + "query": "UPDATE \"aclalias\" SET \"parent_id\" = $2,\"name\" = $3,\"kind\" = $4,\"state\" = $5,\"addresses\" = $6,\"ports\" = $7,\"protocols\" = $8,\"any_address\" = $9,\"any_port\" = $10,\"any_protocol\" = $11 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -40,5 +40,5 @@ }, "nullable": [] }, - "hash": "2829f4f5fe69dcf6ea08a0346b91407a9ad71d5f76a7741882282fef0a2e928b" + "hash": "bd0d9b7dfce0d3a4e55ca45aeda52bbe18c9871cf30ad82fbea85f9b459002c2" } diff --git a/.sqlx/query-e958dccd2e8b944bd051ddbde69612da6e2a687c5c5588e8ad4c1381b31946d8.json b/.sqlx/query-d21c30bfd10d3e47c065ae2485eba079437dab3a0540cbbbceac6874505d672d.json similarity index 74% rename from .sqlx/query-e958dccd2e8b944bd051ddbde69612da6e2a687c5c5588e8ad4c1381b31946d8.json rename to .sqlx/query-d21c30bfd10d3e47c065ae2485eba079437dab3a0540cbbbceac6874505d672d.json index 07966a453e..50f82f5c41 100644 --- a/.sqlx/query-e958dccd2e8b944bd051ddbde69612da6e2a687c5c5588e8ad4c1381b31946d8.json +++ b/.sqlx/query-d21c30bfd10d3e47c065ae2485eba079437dab3a0540cbbbceac6874505d672d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"parent_id\",\"state\" \"state: _\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_networks\",\"destination\" \"destination: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"enabled\",\"expires\",\"any_destination\",\"any_port\",\"any_protocol\",\"manual_settings\" FROM \"aclrule\" WHERE id = $1", + "query": "SELECT id, \"parent_id\",\"state\" \"state: _\",\"name\",\"allow_all_users\",\"deny_all_users\",\"allow_all_groups\",\"deny_all_groups\",\"allow_all_network_devices\",\"deny_all_network_devices\",\"all_locations\",\"addresses\" \"addresses: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"enabled\",\"expires\",\"any_address\",\"any_port\",\"any_protocol\",\"use_manual_destination_settings\" FROM \"aclrule\" WHERE id = $1", "describe": { "columns": [ { @@ -48,62 +48,72 @@ }, { "ordinal": 6, - "name": "allow_all_network_devices", + "name": "allow_all_groups", "type_info": "Bool" }, { "ordinal": 7, - "name": "deny_all_network_devices", + "name": "deny_all_groups", "type_info": "Bool" }, { "ordinal": 8, - "name": "all_networks", + "name": "allow_all_network_devices", "type_info": "Bool" }, { "ordinal": 9, - "name": "destination: _", - "type_info": "InetArray" + "name": "deny_all_network_devices", + "type_info": "Bool" }, { "ordinal": 10, + "name": "all_locations", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "addresses: _", + "type_info": "InetArray" + }, + { + "ordinal": 12, "name": "ports: _", "type_info": "Int4RangeArray" }, { - "ordinal": 11, + "ordinal": 13, "name": "protocols: _", "type_info": "Int4Array" }, { - "ordinal": 12, + "ordinal": 14, "name": "enabled", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 15, "name": "expires", "type_info": "Timestamp" }, { - "ordinal": 14, - "name": "any_destination", + "ordinal": 16, + "name": "any_address", "type_info": "Bool" }, { - "ordinal": 15, + "ordinal": 17, "name": "any_port", "type_info": "Bool" }, { - "ordinal": 16, + "ordinal": 18, "name": "any_protocol", "type_info": "Bool" }, { - "ordinal": 17, - "name": "manual_settings", + "ordinal": 19, + "name": "use_manual_destination_settings", "type_info": "Bool" } ], @@ -126,6 +136,8 @@ false, false, false, + false, + false, true, false, false, @@ -133,5 +145,5 @@ false ] }, - "hash": "e958dccd2e8b944bd051ddbde69612da6e2a687c5c5588e8ad4c1381b31946d8" + "hash": "d21c30bfd10d3e47c065ae2485eba079437dab3a0540cbbbceac6874505d672d" } diff --git a/.sqlx/query-2f971e30e3e976848107845d8d69ae1bb0ac7ebe995b9aac9e3528f3bb84c264.json b/.sqlx/query-d8810e9960be47e01929d6c0c96c3390820e042ecaf4e542fcdd413c9a7ae507.json similarity index 83% rename from .sqlx/query-2f971e30e3e976848107845d8d69ae1bb0ac7ebe995b9aac9e3528f3bb84c264.json rename to .sqlx/query-d8810e9960be47e01929d6c0c96c3390820e042ecaf4e542fcdd413c9a7ae507.json index 18448beb88..2800fd43fe 100644 --- a/.sqlx/query-2f971e30e3e976848107845d8d69ae1bb0ac7ebe995b9aac9e3528f3bb84c264.json +++ b/.sqlx/query-d8810e9960be47e01929d6c0c96c3390820e042ecaf4e542fcdd413c9a7ae507.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"parent_id\",\"name\",\"kind\" \"kind: _\",\"state\" \"state: _\",\"destination\" \"destination: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"any_destination\",\"any_port\",\"any_protocol\" FROM \"aclalias\" WHERE id = $1", + "query": "SELECT id, \"parent_id\",\"name\",\"kind\" \"kind: _\",\"state\" \"state: _\",\"addresses\" \"addresses: _\",\"ports\" \"ports: _\",\"protocols\" \"protocols: _\",\"any_address\",\"any_port\",\"any_protocol\" FROM \"aclalias\" WHERE id = $1", "describe": { "columns": [ { @@ -50,7 +50,7 @@ }, { "ordinal": 5, - "name": "destination: _", + "name": "addresses: _", "type_info": "InetArray" }, { @@ -65,7 +65,7 @@ }, { "ordinal": 8, - "name": "any_destination", + "name": "any_address", "type_info": "Bool" }, { @@ -98,5 +98,5 @@ false ] }, - "hash": "2f971e30e3e976848107845d8d69ae1bb0ac7ebe995b9aac9e3528f3bb84c264" + "hash": "d8810e9960be47e01929d6c0c96c3390820e042ecaf4e542fcdd413c9a7ae507" } diff --git a/.sqlx/query-25fd6f4b68013a7f4d532840adfa6ef3d1292dee92c02f41912ad87280658622.json b/.sqlx/query-ef93737a937eed2cb1a192d71f52ed0eb93c05d42b6d864f354ffe0135d9d094.json similarity index 88% rename from .sqlx/query-25fd6f4b68013a7f4d532840adfa6ef3d1292dee92c02f41912ad87280658622.json rename to .sqlx/query-ef93737a937eed2cb1a192d71f52ed0eb93c05d42b6d864f354ffe0135d9d094.json index adbaa53331..831f3d4c92 100644 --- a/.sqlx/query-25fd6f4b68013a7f4d532840adfa6ef3d1292dee92c02f41912ad87280658622.json +++ b/.sqlx/query-ef93737a937eed2cb1a192d71f52ed0eb93c05d42b6d864f354ffe0135d9d094.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", destination, ports, protocols, any_destination, any_port, any_protocol FROM aclalias WHERE kind = $1", + "query": "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", addresses, ports, protocols, any_address, any_port, any_protocol FROM aclalias WHERE kind = $1", "describe": { "columns": [ { @@ -50,7 +50,7 @@ }, { "ordinal": 5, - "name": "destination", + "name": "addresses", "type_info": "InetArray" }, { @@ -65,7 +65,7 @@ }, { "ordinal": 8, - "name": "any_destination", + "name": "any_address", "type_info": "Bool" }, { @@ -108,5 +108,5 @@ false ] }, - "hash": "25fd6f4b68013a7f4d532840adfa6ef3d1292dee92c02f41912ad87280658622" + "hash": "ef93737a937eed2cb1a192d71f52ed0eb93c05d42b6d864f354ffe0135d9d094" } diff --git a/Cargo.lock b/Cargo.lock index 3e67cca254..f52463cc66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3285,7 +3285,7 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "chrono", "getrandom 0.2.17", "http", @@ -3831,7 +3831,7 @@ dependencies = [ "aes-gcm", "aes-kw", "argon2", - "base64 0.21.7", + "base64 0.22.1", "bitfields", "block-padding", "blowfish", diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index d9b635c5aa..ed242d0b04 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -163,43 +163,47 @@ pub struct AclRuleInfo { pub parent_id: Option, pub state: RuleState, pub name: String, - pub all_networks: bool, - pub networks: Vec>, + pub all_locations: bool, + pub locations: Vec>, pub expires: Option, pub enabled: bool, // source pub allow_all_users: bool, pub deny_all_users: bool, + pub allow_all_groups: bool, + pub deny_all_groups: bool, pub allow_all_network_devices: bool, pub deny_all_network_devices: bool, pub allowed_users: Vec>, pub denied_users: Vec>, pub allowed_groups: Vec>, pub denied_groups: Vec>, - pub allowed_devices: Vec>, - pub denied_devices: Vec>, + pub allowed_network_devices: Vec>, + pub denied_network_devices: Vec>, // destination - pub destination: Vec, - pub destination_ranges: Vec>, - pub aliases: Vec>, + pub addresses: Vec, + pub address_ranges: Vec>, pub ports: Vec, pub protocols: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, - pub manual_settings: bool, + pub use_manual_destination_settings: bool, + // aliases & destinations + pub aliases: Vec>, + pub destinations: Vec>, } impl AclRuleInfo { /// Constructs a [`String`] of comma-separated addresses and address ranges. pub(crate) fn format_destination(&self) -> String { // process single addresses - let addrs = match &self.destination { + let addrs = match &self.addresses { d if d.is_empty() => String::new(), d => d.iter().map(|a| a.to_string() + ", ").collect::(), }; // process address ranges - let ranges = match &self.destination_ranges { + let ranges = match &self.address_ranges { r if r.is_empty() => String::new(), r => r.iter().fold(String::new(), |acc, r| { acc + &format!("{}-{}, ", r.start, r.end) @@ -237,7 +241,7 @@ impl AclRuleInfo { /// Those objects have their dedicated tables and structures so we provide /// [`AclRuleInfo`] and [`ApiAclRule`] structs that implement appropriate methods /// to combine all the related objects for easier downstream processing. -#[derive(Clone, Debug, Default, Eq, FromRow, Model, PartialEq, ToSchema)] +#[derive(Clone, Debug, Eq, FromRow, Model, PartialEq, ToSchema)] pub struct AclRule { pub id: I, // if present points to the original rule before modification / deletion @@ -247,12 +251,14 @@ pub struct AclRule { pub name: String, pub allow_all_users: bool, pub deny_all_users: bool, + pub allow_all_groups: bool, + pub deny_all_groups: bool, pub allow_all_network_devices: bool, pub deny_all_network_devices: bool, - pub all_networks: bool, + pub all_locations: bool, #[model(ref)] #[schema(value_type = Vec)] - pub destination: Vec, + pub addresses: Vec, #[model(ref)] #[schema(value_type = Vec)] pub ports: Vec>, @@ -260,10 +266,37 @@ pub struct AclRule { pub protocols: Vec, pub enabled: bool, pub expires: Option, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, - pub manual_settings: bool, + pub use_manual_destination_settings: bool, +} + +impl Default for AclRule { + fn default() -> Self { + Self { + id: NoId, + parent_id: Default::default(), + state: RuleState::New, + name: "ACL rule".to_string(), + allow_all_users: false, + deny_all_users: false, + allow_all_groups: false, + deny_all_groups: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + all_locations: false, + addresses: Vec::new(), + ports: Vec::new(), + protocols: Vec::new(), + enabled: true, + expires: None, + any_address: true, + any_port: true, + any_protocol: true, + use_manual_destination_settings: true, + } + } } impl AclRule { @@ -527,7 +560,9 @@ pub(crate) struct ParsedDestination { /// Perses a destination string into singular ip addresses or networks and address /// ranges. We should be able to parse a string like this one: /// `10.0.0.1/24, 10.1.1.10-10.1.1.20, 192.168.1.10, 10.1.1.1-10.10.1.1` -pub(crate) fn parse_destination(destination: &str) -> Result { +pub(crate) fn parse_destination_addresses( + destination: &str, +) -> Result { debug!("Parsing destination string: {destination}"); let destination: String = destination.chars().filter(|c| !c.is_whitespace()).collect(); let mut result = ParsedDestination::default(); @@ -607,9 +642,10 @@ impl AclRule { ) -> Result<(), AclError> { let rule_id = self.id; debug!("Creating related objects for ACL rule {api_rule:?}"); - // save related networks - debug!("Creating related networks for ACL rule {rule_id}"); - for network_id in &api_rule.networks { + + // save related locations + debug!("Creating related locations for ACL rule {rule_id}"); + for network_id in &api_rule.locations { AclRuleNetwork::new(rule_id, *network_id) .save(&mut *transaction) .await @@ -652,14 +688,16 @@ impl AclRule { .map_err(|err| map_relation_error(err, "Group", *group_id))?; } - // save related aliases - debug!("Creating related aliases for ACL rule {rule_id}"); + // save related aliases and destinations + debug!("Creating related aliases and destinations for ACL rule {rule_id}"); // verify if all aliases have a correct state // aliases used for tracking modifications (`AliasState::Modified`) cannot be used by ACL // rules + // FIXME: handle aliases and destinations separately + let all_aliases = [api_rule.aliases.clone(), api_rule.destinations.clone()].concat(); let invalid_alias_ids: Vec = query_scalar!( "SELECT id FROM aclalias WHERE id = ANY($1) AND state != 'applied'::aclalias_state", - &api_rule.aliases + &all_aliases ) .fetch_all(&mut *transaction) .await?; @@ -672,7 +710,7 @@ impl AclRule { invalid_alias_ids, )); } - for alias_id in &api_rule.aliases { + for alias_id in &all_aliases { AclRuleAlias::new(rule_id, *alias_id) .save(&mut *transaction) .await @@ -681,7 +719,7 @@ impl AclRule { // allowed devices debug!("Creating related allowed devices for ACL rule {rule_id}"); - for device_id in &api_rule.allowed_devices { + for device_id in &api_rule.allowed_network_devices { AclRuleDevice::new(rule_id, *device_id, true) .save(&mut *transaction) .await @@ -690,7 +728,7 @@ impl AclRule { // denied devices debug!("Creating related denied devices for ACL rule {rule_id}"); - for device_id in &api_rule.denied_devices { + for device_id in &api_rule.denied_network_devices { AclRuleDevice::new(rule_id, *device_id, false) .save(&mut *transaction) .await @@ -698,7 +736,7 @@ impl AclRule { } // destination - let destination = parse_destination(&api_rule.destination)?; + let destination = parse_destination_addresses(&api_rule.addresses)?; debug!("Creating related destination ranges for ACL rule {rule_id}"); for range in destination.ranges { if range.1 <= range.0 { @@ -794,7 +832,7 @@ impl TryFrom for AclRule { fn try_from(rule: EditAclRule) -> Result { Ok(Self { - destination: parse_destination(&rule.destination)?.addrs, + addresses: parse_destination_addresses(&rule.addresses)?.addrs, ports: parse_ports(&rule.ports)? .into_iter() .map(Into::into) @@ -805,16 +843,18 @@ impl TryFrom for AclRule { name: rule.name, allow_all_users: rule.allow_all_users, deny_all_users: rule.deny_all_users, + allow_all_groups: rule.allow_all_groups, + deny_all_groups: rule.deny_all_groups, allow_all_network_devices: rule.allow_all_network_devices, deny_all_network_devices: rule.deny_all_network_devices, - all_networks: rule.all_networks, + all_locations: rule.all_locations, protocols: rule.protocols, enabled: rule.enabled, expires: rule.expires, - any_destination: rule.any_destination, + any_address: rule.any_address, any_port: rule.any_port, any_protocol: rule.any_protocol, - manual_settings: true, + use_manual_destination_settings: true, }) } } @@ -886,7 +926,7 @@ impl AclRule { where E: PgExecutor<'e>, { - if self.all_networks { + if self.all_locations { WireguardNetwork::all(executor).await } else { query_as!( @@ -917,7 +957,7 @@ impl AclRule { query_as!( AclAlias, "SELECT a.id, parent_id, name, kind \"kind: AliasKind\",state \"state: AliasState\", \ - destination, ports, protocols, any_destination, any_port, any_protocol \ + addresses, ports, protocols, any_address, any_port, any_protocol \ FROM aclrulealias r \ JOIN aclalias a \ ON a.id = r.alias_id \ @@ -1075,7 +1115,7 @@ impl AclRule { } /// Returns all [`AclRuleDestinationRanges`]es the rule applies to - pub(crate) async fn get_destination_ranges<'e, E>( + pub(crate) async fn get_destination_address_ranges<'e, E>( &self, executor: E, ) -> Result>, SqlxError> @@ -1096,17 +1136,22 @@ impl AclRule { /// Retrieves all related objects from the db and converts [`AclRule`] /// instance to [`AclRuleInfo`]. pub async fn to_info(&self, conn: &mut PgConnection) -> Result, SqlxError> { - let aliases = self.get_aliases(&mut *conn).await?; - let networks = self.get_networks(&mut *conn).await?; + let locations = self.get_networks(&mut *conn).await?; let allowed_users = self.get_users(&mut *conn, true).await?; let denied_users = self.get_users(&mut *conn, false).await?; let allowed_groups = self.get_groups(&mut *conn, true).await?; let denied_groups = self.get_groups(&mut *conn, false).await?; - let allowed_devices = self.get_network_devices(&mut *conn, true).await?; - let denied_devices = self.get_network_devices(&mut *conn, false).await?; - let destination_ranges = self.get_destination_ranges(&mut *conn).await?; + let allowed_network_devices = self.get_network_devices(&mut *conn, true).await?; + let denied_network_devices = self.get_network_devices(&mut *conn, false).await?; + let address_ranges = self.get_destination_address_ranges(&mut *conn).await?; let ports = self.ports.clone().into_iter().map(Into::into).collect(); + // FIXME: split into two separate structs to be less ambiguous + let aliases = self.get_aliases(&mut *conn).await?; + let (aliases, destinations) = aliases + .into_iter() + .partition(|alias| alias.kind == AliasKind::Component); + Ok(AclRuleInfo { id: self.id, parent_id: self.parent_id, @@ -1114,27 +1159,30 @@ impl AclRule { name: self.name.clone(), allow_all_users: self.allow_all_users, deny_all_users: self.deny_all_users, + allow_all_groups: self.allow_all_groups, + deny_all_groups: self.deny_all_groups, allow_all_network_devices: self.allow_all_network_devices, deny_all_network_devices: self.deny_all_network_devices, - all_networks: self.all_networks, - destination: self.destination.clone(), + all_locations: self.all_locations, + addresses: self.addresses.clone(), protocols: self.protocols.clone(), enabled: self.enabled, expires: self.expires, - destination_ranges, + address_ranges, ports, aliases, - networks, + destinations, + locations, allowed_users, denied_users, allowed_groups, denied_groups, - allowed_devices, - denied_devices, - any_destination: self.any_destination, + allowed_network_devices, + denied_network_devices, + any_address: self.any_address, any_port: self.any_port, any_protocol: self.any_protocol, - manual_settings: false, + use_manual_destination_settings: self.use_manual_destination_settings, }) } } @@ -1142,9 +1190,9 @@ impl AclRule { impl AclRuleInfo { /// Wrapper function which combines explicitly specified allowed users with members of allowed /// groups to generate a list of all unique allowed users for a given ACL. - pub(crate) async fn get_all_allowed_users<'e, E: sqlx::PgExecutor<'e>>( + pub(crate) async fn get_all_allowed_users( &self, - executor: E, + conn: &mut PgConnection, ) -> Result>, SqlxError> { debug!( "Preparing list of all allowed users for ACL rule {}", @@ -1156,18 +1204,22 @@ impl AclRuleInfo { "allow_all_users flag is enabled for ACL rule {}. Fetching all active users", self.id ); - return User::::all_active(executor).await; + return User::::all_active(&mut *conn).await; } // get explicitly allowed users let mut allowed_users = self.allowed_users.clone(); // get allowed groups IDs - let allowed_group_ids = self - .allowed_groups - .iter() - .map(|group| group.id) - .collect::>(); + let allowed_group_ids = if self.allow_all_groups { + let all_groups = Group::all(&mut *conn).await?; + all_groups.iter().map(|group| group.id).collect() + } else { + self.allowed_groups + .iter() + .map(|group| group.id) + .collect::>() + }; // fetch all active members of allowed groups let allowed_groups_users = query_as!( @@ -1181,7 +1233,7 @@ impl AclRuleInfo { WHERE u.is_active=true AND gu.group_id=ANY($1)", &allowed_group_ids ) - .fetch_all(executor) + .fetch_all(conn) .await?; // get unique users from both lists @@ -1194,9 +1246,9 @@ impl AclRuleInfo { /// Wrapper function which combines explicitly specified denied users with members of denied /// groups to generate a list of all unique denied users for a given ACL. - pub(crate) async fn get_all_denied_users<'e, E: sqlx::PgExecutor<'e>>( + pub(crate) async fn get_all_denied_users( &self, - executor: E, + conn: &mut PgConnection, ) -> Result>, SqlxError> { debug!( "Preparing list of all denied users for ACL rule {}", @@ -1208,14 +1260,22 @@ impl AclRuleInfo { "deny_all_users flag is enabled for ACL rule {}. Fetching all active users", self.id ); - return User::::all_active(executor).await; + return User::::all_active(&mut *conn).await; } // get explicitly denied users let mut denied_users = self.denied_users.clone(); // get denied groups IDs - let denied_group_ids: Vec = self.denied_groups.iter().map(|group| group.id).collect(); + let denied_group_ids = if self.deny_all_groups { + let all_groups = Group::all(&mut *conn).await?; + all_groups.iter().map(|group| group.id).collect() + } else { + self.denied_groups + .iter() + .map(|group| group.id) + .collect::>() + }; // fetch all active members of denied groups let denied_groups_users = query_as!( @@ -1230,7 +1290,7 @@ impl AclRuleInfo { WHERE u.is_active=true AND gu.group_id=ANY($1)", &denied_group_ids ) - .fetch_all(executor) + .fetch_all(conn) .await?; // get unique users from both lists @@ -1269,7 +1329,7 @@ impl AclRuleInfo { .await } else { // return explicitly configured allowed devices otherwise - Ok(self.allowed_devices.clone()) + Ok(self.allowed_network_devices.clone()) } } @@ -1301,7 +1361,7 @@ impl AclRuleInfo { .await } else { // return explicitly configured denied devices otherwise - Ok(self.denied_devices.clone()) + Ok(self.denied_network_devices.clone()) } } } @@ -1316,13 +1376,13 @@ pub(crate) struct AclAliasInfo { pub kind: AliasKind, pub state: AliasState, #[schema(value_type = Vec)] - pub destination: Vec, - pub destination_ranges: Vec>, + pub addresses: Vec, + pub address_ranges: Vec>, #[schema(value_type = Vec)] pub ports: Vec, pub protocols: Vec, pub rules: Vec>, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, } @@ -1331,12 +1391,12 @@ impl AclAliasInfo { /// Constructs a [`String`] of comma-separated addresses and address ranges pub(crate) fn format_destination(&self) -> String { // process single addresses - let addrs = match &self.destination { + let addrs = match &self.addresses { d if d.is_empty() => String::new(), d => d.iter().map(|a| a.to_string() + ", ").collect::(), }; // process address ranges - let ranges = match &self.destination_ranges { + let ranges = match &self.address_ranges { r if r.is_empty() => String::new(), r => r.iter().fold(String::new(), |acc, r| { acc + &format!("{}-{}, ", r.start, r.end) @@ -1408,12 +1468,12 @@ pub struct AclAlias { #[model(enum)] pub state: AliasState, #[model(ref)] - pub destination: Vec, + pub addresses: Vec, #[model(ref)] pub ports: Vec>, #[model(ref)] pub protocols: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, } @@ -1424,10 +1484,10 @@ impl AclAlias { name: S, state: AliasState, kind: AliasKind, - destination: Vec, + addresses: Vec, ports: Vec>, protocols: Vec, - any_destination: bool, + any_address: bool, any_port: bool, any_protocol: bool, ) -> Self { @@ -1437,10 +1497,10 @@ impl AclAlias { name: name.into(), kind, state, - destination, + addresses, ports, protocols, - any_destination, + any_address, any_port, any_protocol, } @@ -1573,7 +1633,7 @@ impl TryFrom<&EditAclAlias> for AclAlias { fn try_from(alias: &EditAclAlias) -> Result { Ok(Self { - destination: parse_destination(&alias.destination)?.addrs, + addresses: parse_destination_addresses(&alias.addresses)?.addrs, ports: parse_ports(&alias.ports)? .into_iter() .map(Into::into) @@ -1584,7 +1644,7 @@ impl TryFrom<&EditAclAlias> for AclAlias { kind: AliasKind::Component, state: AliasState::Applied, protocols: alias.protocols.clone(), - any_destination: true, + any_address: true, any_port: true, any_protocol: true, }) @@ -1603,7 +1663,7 @@ impl AclAlias { sqlx::query_as!( Self, "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", \ - destination, ports, protocols, any_destination, any_port, any_protocol \ + addresses, ports, protocols, any_address, any_port, any_protocol \ FROM aclalias WHERE kind = $1", kind as AliasKind ) @@ -1622,7 +1682,7 @@ impl AclAlias { sqlx::query_as!( Self, "SELECT id, parent_id, name, kind \"kind: _\", state \"state: _\", \ - destination, ports, protocols, any_destination, any_port, any_protocol \ + addresses, ports, protocols, any_address, any_port, any_protocol \ FROM aclalias WHERE id = $1 AND kind = $2", id, kind as AliasKind @@ -1637,7 +1697,7 @@ impl TryFrom<&EditAclDestination> for AclAlias { fn try_from(alias: &EditAclDestination) -> Result { Ok(Self { - destination: parse_destination(&alias.destination)?.addrs, + addresses: parse_destination_addresses(&alias.addresses)?.addrs, ports: parse_ports(&alias.ports)? .into_iter() .map(Into::into) @@ -1648,7 +1708,7 @@ impl TryFrom<&EditAclDestination> for AclAlias { kind: AliasKind::Destination, state: AliasState::Applied, protocols: alias.protocols.clone(), - any_destination: alias.any_destination, + any_address: alias.any_address, any_port: alias.any_port, any_protocol: alias.any_protocol, }) @@ -1705,9 +1765,9 @@ impl AclAlias { query_as!( AclRule, "SELECT ar.id, parent_id, state AS \"state: RuleState\", name, allow_all_users, \ - deny_all_users, allow_all_network_devices, deny_all_network_devices, all_networks, \ - destination, ports, protocols, enabled, expires, any_destination, any_port, \ - any_protocol, manual_settings \ + deny_all_users, allow_all_groups, deny_all_groups, allow_all_network_devices, deny_all_network_devices, all_locations, \ + addresses, ports, protocols, enabled, expires, any_address, any_port, \ + any_protocol, use_manual_destination_settings \ FROM aclrulealias ara \ JOIN aclrule ar ON ar.id = ara.rule_id \ WHERE ara.alias_id = $1", @@ -1729,12 +1789,12 @@ impl AclAlias { name: self.name.clone(), kind: self.kind.clone(), state: self.state.clone(), - destination: self.destination.clone(), + addresses: self.addresses.clone(), ports: self.ports.clone().into_iter().map(Into::into).collect(), protocols: self.protocols.clone(), - destination_ranges, + address_ranges: destination_ranges, rules, - any_destination: self.any_destination, + any_address: self.any_address, any_port: self.any_port, any_protocol: self.any_protocol, }) diff --git a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs index 119639c5fc..86ddeb33a0 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs @@ -44,7 +44,7 @@ async fn test_alias(_: PgPoolOptions, options: PgConnectOptions) { let retrieved = AclAlias::find_by_id(&pool, 1).await.unwrap().unwrap(); assert_eq!(retrieved.id, 1); - assert_eq!(retrieved.destination, destination); + assert_eq!(retrieved.addresses, destination); assert_eq!(retrieved.ports, ports); } @@ -55,23 +55,22 @@ async fn test_allow_conflicting_sources(_: PgPoolOptions, options: PgConnectOpti // create the rule let rule = AclRule { id: NoId, - parent_id: Default::default(), - state: Default::default(), name: "rule".to_string(), enabled: true, allow_all_users: false, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - all_networks: false, - destination: Vec::new(), + all_locations: false, + addresses: Vec::new(), ports: Vec::new(), protocols: Vec::new(), expires: None, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() } .save(&pool) .await @@ -129,23 +128,22 @@ async fn test_rule_relations(_: PgPoolOptions, options: PgConnectOptions) { // create the rule let mut rule = AclRule { id: NoId, - parent_id: Default::default(), - state: Default::default(), name: "rule".to_string(), enabled: true, allow_all_users: false, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - all_networks: false, - destination: Vec::new(), + all_locations: false, + addresses: Vec::new(), ports: Vec::new(), protocols: Vec::new(), expires: None, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() } .save(&pool) .await @@ -312,8 +310,8 @@ async fn test_rule_relations(_: PgPoolOptions, options: PgConnectOptions) { // convert to [`AclRuleInfo`] and verify results let info = rule.to_info(&mut conn).await.unwrap(); - assert_eq!(info.aliases.len(), 1); - assert_eq!(info.aliases[0], alias1); + assert_eq!(info.destinations.len(), 1); + assert_eq!(info.destinations[0], alias1); assert_eq!(info.allowed_users.len(), 1); assert_eq!(info.allowed_users[0], user1); @@ -327,17 +325,17 @@ async fn test_rule_relations(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(info.denied_groups.len(), 1); assert_eq!(info.denied_groups[0], group2); - assert_eq!(info.allowed_devices.len(), 1); - assert_eq!(info.allowed_devices[0].id, device1.id); // db modifies datetime precision + assert_eq!(info.allowed_network_devices.len(), 1); + assert_eq!(info.allowed_network_devices[0].id, device1.id); // db modifies datetime precision - assert_eq!(info.denied_devices.len(), 1); - assert_eq!(info.denied_devices[0].id, device2.id); // db modifies datetime precision + assert_eq!(info.denied_network_devices.len(), 1); + assert_eq!(info.denied_network_devices[0].id, device2.id); // db modifies datetime precision - assert_eq!(info.networks.len(), 1); - assert_eq!(info.networks[0], network1); + assert_eq!(info.locations.len(), 1); + assert_eq!(info.locations[0], network1); // test all_networks flag - rule.all_networks = true; + rule.all_locations = true; rule.save(&pool).await.unwrap(); assert_eq!(rule.get_networks(&pool).await.unwrap().len(), 2); @@ -484,18 +482,19 @@ async fn test_all_allowed_users(_: PgPoolOptions, options: PgConnectOptions) { deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - all_networks: false, - destination: Vec::new(), + all_locations: false, + addresses: Vec::new(), ports: Vec::new(), protocols: Vec::new(), expires: None, enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() } .save(&pool) .await @@ -524,7 +523,7 @@ async fn test_all_allowed_users(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(rule_info.allowed_groups.len(), 1); // Get all allowed users - let allowed_users = rule_info.get_all_allowed_users(&pool).await.unwrap(); + let allowed_users = rule_info.get_all_allowed_users(&mut conn).await.unwrap(); // Should contain user1 (explicit) and user3 (from group2), but not inactive user_4 assert_eq!(allowed_users.len(), 2); @@ -598,18 +597,19 @@ async fn test_all_denied_users(_: PgPoolOptions, options: PgConnectOptions) { deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - all_networks: false, - destination: Vec::new(), + all_locations: false, + addresses: Vec::new(), ports: Vec::new(), protocols: Vec::new(), expires: None, enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - manual_settings: true, + use_manual_destination_settings: true, + ..Default::default() } .save(&pool) .await @@ -647,7 +647,7 @@ async fn test_all_denied_users(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(rule_info.denied_groups.len(), 1); // Get all denied users - let denied_users = rule_info.get_all_denied_users(&pool).await.unwrap(); + let denied_users = rule_info.get_all_denied_users(&mut conn).await.unwrap(); // Should contain user_1 (explicit), user_2 and user_3 (from group_1), but not inactive user_4 assert_eq!(denied_users.len(), 3); diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index f298d957dd..1a621f4bdd 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -23,7 +23,7 @@ use super::{ utils::merge_ranges, }; use crate::enterprise::{ - db::models::{acl::AliasKind, snat::UserSnatBinding}, + db::models::{acl::AclAlias, snat::UserSnatBinding}, is_business_license_active, }; @@ -41,6 +41,9 @@ pub enum FirewallError { /// - ALLOW which determines which devices can access a destination /// - DENY which stops all other traffic to a given destination /// +/// Additionally a separate set of rules is created for each pre-defined `Destination` used +/// as part of the rule. +/// /// In the resulting list all ALLOW rules are placed first and then DENY rules are added to the /// end. This way we can avoid conflicts when some ACLs are overlapping. pub async fn generate_firewall_rules_from_acls( @@ -61,205 +64,336 @@ pub async fn generate_firewall_rules_from_acls( // convert each ACL into a corresponding `FirewallRule`s for acl in acl_rules { debug!("Processing ACL rule: {acl:?}"); - // fetch allowed users - let allowed_users = acl.get_all_allowed_users(&mut *conn).await?; + // prepare source IPs + let (ipv4_source_addrs, ipv6_source_addrs) = + get_source_ips(&mut *conn, location_id, &acl).await?; - // fetch denied users - let denied_users = acl.get_all_denied_users(&mut *conn).await?; + // extract destination parameters from ACL rule + let AclRuleInfo { + id, + name: rule_name, + addresses, + address_ranges, + ports, + protocols, + aliases, + destinations, + any_address, + any_port, + any_protocol, + use_manual_destination_settings, + .. + } = acl; - // get relevant users for determining source IPs - let users = get_source_users(allowed_users, &denied_users); - // prepare a list of user IDs - let user_ids: Vec = users.iter().map(|user| user.id).collect(); + // check if we need to add rules for manually defined destination + if use_manual_destination_settings { + let (manual_destination_allow_rules, manual_destination_deny_rules) = + get_manual_destination_rules( + &mut *conn, + id, + &rule_name, + has_ipv4_addresses, + has_ipv6_addresses, + (&ipv4_source_addrs, &ipv6_source_addrs), + aliases, + addresses, + address_ranges, + ports, + protocols, + any_address, + any_port, + any_protocol, + ) + .await?; + + // append generated rules to output + allow_rules.extend(manual_destination_allow_rules); + deny_rules.extend(manual_destination_deny_rules); + }; - // get network IPs for devices belonging to those users - let user_device_ips = get_user_device_ips(&user_ids, location_id, &mut *conn).await?; - // separate IPv4 and IPv6 user-device addresses - let user_device_ips = user_device_ips - .iter() - .flatten() - .partition(|ip| ip.is_ipv4()); + // process destination aliases by creating a dedicated set of rules for each of them + if !destinations.is_empty() { + debug!( + "Generating firewall rules for {} pre-defined destinations used in ACL rule {id:?}", + destinations.len() + ); + } + for destination in destinations { + debug!("Processing ACL pre-defined destination: {destination:?}"); + let (destination_allow_rules, destination_deny_rules) = + get_predefined_destination_rules( + &mut *conn, + destination, + acl.id, + &rule_name, + has_ipv4_addresses, + has_ipv6_addresses, + (&ipv4_source_addrs, &ipv6_source_addrs), + ) + .await?; + + // append generated rules to output + allow_rules.extend(destination_allow_rules); + deny_rules.extend(destination_deny_rules); + } + } - // fetch allowed network devices - let allowed_network_devices = acl.get_all_allowed_devices(&mut *conn, location_id).await?; + // combine both rule lists + Ok(allow_rules.into_iter().chain(deny_rules).collect()) +} - // fetch denied network devices - let denied_network_devices = acl.get_all_denied_devices(&mut *conn, location_id).await?; +/// Prepare two lists of source IPs split between IPv4 and IPv6. +/// +/// This is achieved on first determining allowed users and network devices +/// and then getting assigned IP addresses of their devices. +async fn get_source_ips( + conn: &mut PgConnection, + location_id: Id, + acl: &AclRuleInfo, +) -> Result<(Vec, Vec), FirewallError> { + // fetch allowed users + let allowed_users = acl.get_all_allowed_users(&mut *conn).await?; - // get network device IPs for rule source - let network_devices = - get_source_network_devices(allowed_network_devices, &denied_network_devices); - let network_device_ips = - get_network_device_ips(&network_devices, location_id, &mut *conn).await?; + // fetch denied users + let denied_users = acl.get_all_denied_users(&mut *conn).await?; - // separate IPv4 and IPv6 network-device addresses - let network_device_ips = network_device_ips - .iter() - .flatten() - .partition(|ip| ip.is_ipv4()); + // get relevant users for determining source IPs + let source_users = get_source_users(allowed_users, &denied_users); - // convert device IPs into source addresses for a firewall rule - let ipv4_source_addrs = - get_source_addrs(user_device_ips.0, network_device_ips.0, IpVersion::Ipv4); - let ipv6_source_addrs = - get_source_addrs(user_device_ips.1, network_device_ips.1, IpVersion::Ipv6); + // prepare a list of user IDs + let source_user_ids: Vec = source_users.iter().map(|user| user.id).collect(); - // extract destination parameters from ACL rule - let AclRuleInfo { - id, - mut destination, - destination_ranges, - mut ports, - mut protocols, - aliases, - .. - } = acl; + // get network IPs for devices belonging to those users + let source_user_device_ips = + get_user_device_ips(&source_user_ids, location_id, &mut *conn).await?; + // separate IPv4 and IPv6 user-device addresses + let source_user_device_ips = source_user_device_ips + .iter() + .flatten() + .partition(|ip| ip.is_ipv4()); - // split aliases into types - let (destination_aliases, component_aliases): (Vec<_>, Vec<_>) = aliases - .into_iter() - .partition(|alias| alias.kind == AliasKind::Destination); + // fetch allowed network devices + let allowed_network_devices = acl.get_all_allowed_devices(&mut *conn, location_id).await?; - // store alias ranges separately since they use a different struct - let mut alias_destination_ranges = Vec::new(); + // fetch denied network devices + let denied_network_devices = acl.get_all_denied_devices(&mut *conn, location_id).await?; - // process component aliases by appending destination parameters from each of them to - // existing lists - for alias in component_aliases { - // fetch destination ranges for a given alias - alias_destination_ranges.extend(alias.get_destination_ranges(&mut *conn).await?); + // get network device IPs for rule source + let source_network_devices = + get_source_network_devices(allowed_network_devices, &denied_network_devices); + let source_network_device_ips = + get_network_device_ips(&source_network_devices, location_id, &mut *conn).await?; - // extend existing parameter lists - destination.extend(alias.destination); - ports.extend(alias.ports.into_iter().map(Into::into).collect::>()); - protocols.extend(alias.protocols); - } + // separate IPv4 and IPv6 network-device addresses + let source_network_device_ips = source_network_device_ips + .iter() + .flatten() + .partition(|ip| ip.is_ipv4()); + + // convert device IPs into source addresses for a firewall rule + let ipv4_source_addrs = get_source_addrs( + source_user_device_ips.0, + source_network_device_ips.0, + IpVersion::Ipv4, + ); + let ipv6_source_addrs = get_source_addrs( + source_user_device_ips.1, + source_network_device_ips.1, + IpVersion::Ipv6, + ); + Ok((ipv4_source_addrs, ipv6_source_addrs)) +} - // prepare destination addresses - let (dest_addrs_v4, dest_addrs_v6) = - process_destination_addrs(&destination, &destination_ranges); +/// Generates firewall rules for destination manually specified in ACL rule. +async fn get_manual_destination_rules( + conn: &mut PgConnection, + rule_id: Id, + rule_name: &str, + location_has_ipv4_addresses: bool, + location_has_ipv6_addresses: bool, + source_addrs: (&[IpAddress], &[IpAddress]), + aliases: Vec>, + mut addresses: Vec, + address_ranges: Vec>, + mut ports: Vec, + mut protocols: Vec, + any_address: bool, + any_port: bool, + any_protocol: bool, +) -> Result<(Vec, Vec), FirewallError> { + debug!("Generating firewall rules for manually configured destination in ACL rule {rule_id}"); + // store alias ranges separately since they use a different struct + let mut alias_destination_ranges = Vec::new(); + + // process aliases by appending destination parameters from each of them to + // existing lists + for alias in aliases { + // fetch destination ranges for a given alias + alias_destination_ranges.extend(alias.get_destination_ranges(&mut *conn).await?); + + // extend existing parameter lists + addresses.extend(alias.addresses); + ports.extend(alias.ports.into_iter().map(Into::into).collect::>()); + protocols.extend(alias.protocols); + } - // prepare destination ports - let destination_ports = merge_port_ranges(ports); + // prepare destination addresses + let (dest_addrs_v4, dest_addrs_v6) = process_destination_addrs(&addresses, &address_ranges); - // remove duplicate protocol entries + // prepare destination ports + let destination_ports = if any_port { + Vec::new() + } else { + merge_port_ranges(ports) + }; + + // remove duplicate protocol entries + let destination_protocols = if any_protocol { + Vec::new() + } else { protocols.sort_unstable(); protocols.dedup(); + protocols + }; - // skip creating default firewall rules if given ACL includes only destination aliases and no manual destination config - // at this point component aliases have been added to the manual config so they don't need to be handled separately - let has_no_manual_destination = dest_addrs_v4.is_empty() - && dest_addrs_v6.is_empty() - && destination_ports.is_empty() - && protocols.is_empty(); - let has_destination_aliases = !destination_aliases.is_empty(); - let is_destination_alias_only_rule = has_destination_aliases && has_no_manual_destination; - - if !is_destination_alias_only_rule { - let comment = format!("ACL {} - {}", acl.id, acl.name); - if has_ipv4_addresses { - // create IPv4 rules - let ipv4_rules = create_rules( - acl.id, - IpVersion::Ipv4, - &ipv4_source_addrs, - &dest_addrs_v4, - &destination_ports, - &protocols, - &comment, - ); - if let Some(rule) = ipv4_rules.0 { - allow_rules.push(rule); - } - deny_rules.push(ipv4_rules.1); - } + let (ipv4_source_addrs, ipv6_source_addrs) = source_addrs; - if has_ipv6_addresses { - // create IPv6 rules - let ipv6_rules = create_rules( - acl.id, - IpVersion::Ipv6, - &ipv6_source_addrs, - &dest_addrs_v6, - &destination_ports, - &protocols, - &comment, - ); - if let Some(rule) = ipv6_rules.0 { - allow_rules.push(rule); - } - deny_rules.push(ipv6_rules.1); - } + // only generate rules for a given IP version if there is a destination address of a given type + // or any destination toggle is enabled and location uses addresses of a given type + let has_ipv4_destination = + !dest_addrs_v4.is_empty() || (location_has_ipv4_addresses && any_address); + let has_ipv6_destination = + !dest_addrs_v6.is_empty() || (location_has_ipv6_addresses && any_address); + + let comment = format!("ACL {rule_id} - {rule_name}"); + let mut allow_rules = Vec::new(); + let mut deny_rules = Vec::new(); + if has_ipv4_destination { + // create IPv4 rules + let ipv4_rules = create_rules( + rule_id, + IpVersion::Ipv4, + ipv4_source_addrs, + &dest_addrs_v4, + &destination_ports, + &destination_protocols, + &comment, + ); + if let Some(rule) = ipv4_rules.0 { + allow_rules.push(rule); } + deny_rules.push(ipv4_rules.1); + } - // process destination aliases by creating a dedicated set of rules for each of them - if !destination_aliases.is_empty() { - debug!( - "Generating firewall rules for {} aliases used in ACL rule {id:?}", - destination_aliases.len() - ); + if has_ipv6_destination { + // create IPv6 rules + let ipv6_rules = create_rules( + rule_id, + IpVersion::Ipv6, + ipv6_source_addrs, + &dest_addrs_v6, + &destination_ports, + &destination_protocols, + &comment, + ); + if let Some(rule) = ipv6_rules.0 { + allow_rules.push(rule); } - for alias in destination_aliases { - debug!("Processing ACL alias: {alias:?}"); + deny_rules.push(ipv6_rules.1); + } + + Ok((allow_rules, deny_rules)) +} - // fetch destination ranges for a given alias - let alias_destination_ranges = alias.get_destination_ranges(&mut *conn).await?; +/// Generates firewall rules for pre-defined destination used in ACL rule. +async fn get_predefined_destination_rules( + conn: &mut PgConnection, + destination: AclAlias, + rule_id: Id, + rule_name: &str, + location_has_ipv4_addresses: bool, + location_has_ipv6_addresses: bool, + source_addrs: (&[IpAddress], &[IpAddress]), +) -> Result<(Vec, Vec), FirewallError> { + // fetch destination ranges for a given destination + 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); + + // process alias ports + let destination_ports = if destination.any_port { + Vec::new() + } else { + let alias_ports = destination + .ports + .into_iter() + .map(Into::into) + .collect::>(); + merge_port_ranges(alias_ports) + }; - // combine destination addrs - let (dest_addrs_v4, dest_addrs_v6) = - process_alias_destination_addrs(&alias.destination, &alias_destination_ranges); + // process destination protocols + let destination_protocols = if destination.any_protocol { + Vec::new() + } else { + let mut protocols = destination.protocols; + protocols.sort_unstable(); + protocols.dedup(); + protocols + }; - // process alias ports - let alias_ports = alias.ports.into_iter().map(Into::into).collect::>(); - let destination_ports = merge_port_ranges(alias_ports); + let (ipv4_source_addrs, ipv6_source_addrs) = source_addrs; - // remove duplicate protocol entries - let mut protocols = alias.protocols; - protocols.sort_unstable(); - protocols.dedup(); + // only generate rules for a given IP version if there is a destination address of a given type + // or any destination toggle is enabled and location uses addresses of a given type + let has_ipv4_destination = + !dest_addrs_v4.is_empty() || (location_has_ipv4_addresses && destination.any_address); + let has_ipv6_destination = + !dest_addrs_v6.is_empty() || (location_has_ipv6_addresses && destination.any_address); - let comment = format!( - "ACL {} - {}, ALIAS {} - {}", - acl.id, acl.name, alias.id, alias.name - ); - if has_ipv4_addresses { - // create IPv4 rules - let ipv4_rules = create_rules( - alias.id, - IpVersion::Ipv4, - &ipv4_source_addrs, - &dest_addrs_v4, - &destination_ports, - &protocols, - &comment, - ); - if let Some(rule) = ipv4_rules.0 { - allow_rules.push(rule); - } - deny_rules.push(ipv4_rules.1); - } + let comment = format!( + "ACL {rule_id} - {rule_name}, ALIAS {} - {}", + destination.id, destination.name + ); + let mut allow_rules = Vec::new(); + let mut deny_rules = Vec::new(); + if has_ipv4_destination { + // create IPv4 rules + let ipv4_rules = create_rules( + destination.id, + IpVersion::Ipv4, + ipv4_source_addrs, + &dest_addrs_v4, + &destination_ports, + &destination_protocols, + &comment, + ); + if let Some(rule) = ipv4_rules.0 { + allow_rules.push(rule); + } + deny_rules.push(ipv4_rules.1); + } - if has_ipv6_addresses { - // create IPv6 rules - let ipv6_rules = create_rules( - alias.id, - IpVersion::Ipv6, - &ipv6_source_addrs, - &dest_addrs_v6, - &destination_ports, - &protocols, - &comment, - ); - if let Some(rule) = ipv6_rules.0 { - allow_rules.push(rule); - } - deny_rules.push(ipv6_rules.1); - } + if has_ipv6_destination { + // create IPv6 rules + let ipv6_rules = create_rules( + destination.id, + IpVersion::Ipv6, + ipv6_source_addrs, + &dest_addrs_v6, + &destination_ports, + &destination_protocols, + &comment, + ); + if let Some(rule) = ipv6_rules.0 { + allow_rules.push(rule); } + deny_rules.push(ipv6_rules.1); } - // combine both rule lists - Ok(allow_rules.into_iter().chain(deny_rules).collect()) + Ok((allow_rules, deny_rules)) } /// Creates ALLOW and DENY rules for given set of source, destination @@ -872,13 +1006,14 @@ pub(crate) async fn get_location_active_acl_rules( ) -> Result>, SqlxError> { debug!("Fetching active ACL rules for location {location}"); let rules: Vec> = query_as( - "SELECT DISTINCT ON (a.id) a.id, name, allow_all_users, deny_all_users, all_networks, \ - allow_all_network_devices, deny_all_network_devices, destination, ports, protocols, \ - expires, enabled, parent_id, state, any_destination, any_port, any_protocol, - manual_settings \ + "SELECT DISTINCT ON (a.id) a.id, name, allow_all_users, deny_all_users, all_locations, \ + allow_all_groups, deny_all_groups, \ + allow_all_network_devices, deny_all_network_devices, addresses, ports, protocols, \ + expires, enabled, parent_id, state, any_address, any_port, any_protocol, + use_manual_destination_settings \ FROM aclrule a \ LEFT JOIN aclrulenetwork an ON a.id = an.rule_id \ - WHERE (an.network_id = $1 OR a.all_networks = true) AND enabled = true \ + WHERE (an.network_id = $1 OR a.all_locations = true) AND enabled = true \ AND state = 'applied'::aclrule_state \ AND (expires IS NULL OR expires > NOW())", ) diff --git a/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs b/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs new file mode 100644 index 0000000000..37da5cc891 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/all_locations.rs @@ -0,0 +1,341 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_common::db::{NoId, models::WireguardNetwork, setup_pool}; +use ipnetwork::IpNetwork; +use rand::thread_rng; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + +use crate::enterprise::{ + db::models::acl::{AclRule, AclRuleNetwork, RuleState}, + firewall::{tests::create_test_users_and_devices, try_get_location_firewall_config}, +}; + +#[sqlx::test] +async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location_1 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location_1 = location_1.save(&pool).await.unwrap(); + + // Create another test location + let location_2 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location_2 = location_2.save(&pool).await.unwrap(); + + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location_1, &location_2]).await; + + // create ACL rules + let acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + allow_all_users: true, + state: RuleState::Applied, + addresses: vec!["192.168.1.0/24".parse().unwrap()], + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_locations: true, + allow_all_users: true, + state: RuleState::Applied, + addresses: vec!["192.168.2.0/24".parse().unwrap()], + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let _acl_rule_3 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_locations: true, + allow_all_users: true, + state: RuleState::Applied, + addresses: vec!["192.168.3.0/24".parse().unwrap()], + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to locations + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location_1.id) + .save(&pool) + .await + .unwrap(); + } + for rule in [&acl_rule_2] { + AclRuleNetwork::new(rule.id, location_2.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // all rules were assigned to this location + assert_eq!(generated_firewall_rules.len(), 6); + + let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // rule with `all_networks` enabled was used for this location + assert_eq!(generated_firewall_rules.len(), 4); +} + +#[sqlx::test] +async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location_1 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location_1 = location_1.save(&pool).await.unwrap(); + + // Create another test location + let location_2 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location_2 = location_2.save(&pool).await.unwrap(); + + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location_1, &location_2]).await; + + // create ACL rules + let acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + allow_all_users: true, + state: RuleState::Applied, + use_manual_destination_settings: true, + addresses: vec!["fc00::0/112".parse().unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + allow_all_users: true, + all_locations: true, + state: RuleState::Applied, + use_manual_destination_settings: true, + addresses: vec!["fb00::0/112".parse().unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let _acl_rule_3 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_locations: true, + allow_all_users: true, + state: RuleState::Applied, + use_manual_destination_settings: true, + addresses: vec!["fa00::0/112".parse().unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to locations + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location_1.id) + .save(&pool) + .await + .unwrap(); + } + for rule in [&acl_rule_2] { + AclRuleNetwork::new(rule.id, location_2.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were assigned to this location + assert_eq!(generated_firewall_rules.len(), 6); + + let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // rule with `all_networks` enabled was used for this location + assert_eq!(generated_firewall_rules.len(), 4); +} + +#[sqlx::test] +async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location_1 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location_1 = location_1.save(&pool).await.unwrap(); + + // Create another test location + let location_2 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location_2 = location_2.save(&pool).await.unwrap(); + + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location_1, &location_2]).await; + + // create ACL rules + let acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + allow_all_users: true, + state: RuleState::Applied, + use_manual_destination_settings: true, + addresses: vec![ + "192.168.1.0/24".parse().unwrap(), + "fc00::0/112".parse().unwrap(), + ], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_locations: true, + allow_all_users: true, + state: RuleState::Applied, + use_manual_destination_settings: true, + addresses: vec![ + "192.168.2.0/24".parse().unwrap(), + "fb00::0/112".parse().unwrap(), + ], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + let _acl_rule_3 = AclRule { + id: NoId, + expires: None, + enabled: true, + all_locations: true, + allow_all_users: true, + state: RuleState::Applied, + use_manual_destination_settings: true, + addresses: vec![ + "192.168.3.0/24".parse().unwrap(), + "fa00::0/112".parse().unwrap(), + ], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to locations + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location_1.id) + .save(&pool) + .await + .unwrap(); + } + for rule in [&acl_rule_2] { + AclRuleNetwork::new(rule.id, location_2.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // all rules were used to this location + assert_eq!(generated_firewall_rules.len(), 12); + + let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // rule with `all_networks` enabled was also used for this location + assert_eq!(generated_firewall_rules.len(), 8); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/destination.rs b/crates/defguard_core/src/enterprise/firewall/tests/destination.rs new file mode 100644 index 0000000000..fd8e62bab7 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/destination.rs @@ -0,0 +1,117 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_proto::enterprise::firewall::{IpAddress, IpRange, ip_address::Address}; + +use crate::enterprise::{ + db::models::acl::AclRuleDestinationRange, firewall::process_destination_addrs, +}; + +#[test] +fn test_process_destination_addrs_v4() { + // Test data with mixed IPv4 and IPv6 networks + let destination_ips = [ + "10.0.1.0/24".parse().unwrap(), + "10.0.2.0/24".parse().unwrap(), + "2001:db8::/64".parse().unwrap(), // Should be filtered out + "192.168.1.0/24".parse().unwrap(), + ]; + + let destination_ranges = [ + AclRuleDestinationRange { + start: IpAddr::V4(Ipv4Addr::new(10, 0, 3, 255)), + end: IpAddr::V4(Ipv4Addr::new(10, 0, 4, 0)), + ..Default::default() + }, + AclRuleDestinationRange { + start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out + end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 100)), + ..Default::default() + }, + ]; + + let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); + + assert_eq!( + destination_addrs.0, + [ + IpAddress { + address: Some(Address::IpSubnet("10.0.1.0/24".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.0/24".to_string())), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.3.255".to_string(), + end: "10.0.4.0".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }, + ] + ); + + // Test with empty input + let empty_addrs = process_destination_addrs(&[], &[]); + assert!(empty_addrs.0.is_empty()); + + // Test with only IPv6 addresses - should return empty result for IPv4 + let ipv6_only = process_destination_addrs(&["2001:db8::/64".parse().unwrap()], &[]); + assert!(ipv6_only.0.is_empty()); +} + +#[test] +fn test_process_destination_addrs_v6() { + // Test data with mixed IPv4 and IPv6 networks + let destination_ips = vec![ + "2001:db8:1::/64".parse().unwrap(), + "2001:db8:2::/64".parse().unwrap(), + "10.0.1.0/24".parse().unwrap(), // Should be filtered out + "2001:db8:3::/64".parse().unwrap(), + ]; + + let destination_ranges = vec![ + AclRuleDestinationRange { + start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 1)), + end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 3)), + ..Default::default() + }, + AclRuleDestinationRange { + start: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), // Should be filtered out + end: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), + ..Default::default() + }, + ]; + + let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); + + assert_eq!( + destination_addrs.1, + [ + IpAddress { + address: Some(Address::IpSubnet("2001:db8:1::/64".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:2::/64".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:3::/64".to_string())), + }, + IpAddress { + address: Some(Address::Ip("2001:db8:4::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:4::2/127".to_string())) + } + ] + ); + + // Test with empty input + let empty_addrs = process_destination_addrs(&[], &[]); + assert!(empty_addrs.1.is_empty()); + + // Test with only IPv4 addresses - should return empty result for IPv6 + let ipv4_only = process_destination_addrs(&["192.168.1.0/24".parse().unwrap()], &[]); + assert!(ipv4_only.1.is_empty()); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs b/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs new file mode 100644 index 0000000000..280e283933 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/disabled_rules.rs @@ -0,0 +1,242 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_common::db::{NoId, models::WireguardNetwork, setup_pool}; +use ipnetwork::IpNetwork; +use rand::thread_rng; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + +use crate::enterprise::{ + db::models::acl::{AclRule, AclRuleNetwork, RuleState}, + firewall::{tests::create_test_users_and_devices, try_get_location_firewall_config}, +}; + +#[sqlx::test] +async fn test_disabled_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + + // create disabled ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + allow_all_users: true, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + allow_all_users: true, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); +} + +#[sqlx::test] +async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + + // create disabled ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + allow_all_users: true, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + allow_all_users: true, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); +} + +#[sqlx::test] +async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + + // create disabled ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + allow_all_users: true, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: false, + state: RuleState::Applied, + allow_all_users: true, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were disabled + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules enabled + acl_rule_1.enabled = true; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.enabled = true; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 8); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/expired_rules.rs b/crates/defguard_core/src/enterprise/firewall/tests/expired_rules.rs new file mode 100644 index 0000000000..8c20773985 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/expired_rules.rs @@ -0,0 +1,214 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use chrono::{DateTime, NaiveDateTime}; +use defguard_common::db::{NoId, models::WireguardNetwork, setup_pool}; +use ipnetwork::IpNetwork; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + +use crate::enterprise::{ + db::models::acl::{AclRule, AclRuleNetwork, RuleState}, + firewall::try_get_location_firewall_config, +}; + +#[sqlx::test] +async fn test_expired_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create expired ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were expired + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules not expired + acl_rule_1.expires = None; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.expires = Some(NaiveDateTime::MAX); + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_expired_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create expired ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were expired + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules not expired + acl_rule_1.expires = None; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.expires = Some(NaiveDateTime::MAX); + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 2); +} + +#[sqlx::test] +async fn test_expired_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create expired ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: Some(DateTime::UNIX_EPOCH.naive_utc()), + enabled: true, + state: RuleState::Applied, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were expired + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules not expired + acl_rule_1.expires = None; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.expires = Some(NaiveDateTime::MAX); + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs new file mode 100644 index 0000000000..23c2afba28 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/gh1868.rs @@ -0,0 +1,264 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_common::db::{ + Id, NoId, + models::{Device, DeviceType, User, WireguardNetwork, device::WireguardNetworkDevice}, + setup_pool, +}; +use defguard_proto::enterprise::firewall::{FirewallPolicy, IpVersion}; +use ipnetwork::IpNetwork; +use rand::{Rng, rngs::ThreadRng, thread_rng}; +use sqlx::{ + PgPool, + postgres::{PgConnectOptions, PgPoolOptions}, +}; + +use crate::enterprise::{ + db::models::acl::{AclRule, RuleState}, + firewall::try_get_location_firewall_config, +}; + +async fn setup_user_and_device( + rng: &mut ThreadRng, + pool: &PgPool, + location: &WireguardNetwork, +) { + let user: User = rng.r#gen(); + let user = user.save(pool).await.unwrap(); + + let device = Device { + id: NoId, + name: format!("device-{}", user.id), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(pool).await.unwrap(); + + let wireguard_ips = location + .address + .iter() + .map(|subnet| match subnet { + IpNetwork::V4(ipv4_network) => { + let octets = ipv4_network.network().octets(); + IpAddr::V4(Ipv4Addr::new( + octets[0], + octets[1], + user.id as u8, + device.id as u8, + )) + } + IpNetwork::V6(ipv6_network) => { + let mut octets = ipv6_network.network().octets(); + // Set the last two octets (bytes 14 and 15) + octets[14] = user.id as u8; + octets[15] = device.id as u8; + IpAddr::V6(Ipv6Addr::from(octets)) + } + }) + .collect(); + + // assign network address to device + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips, + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(pool).await.unwrap(); +} + +#[sqlx::test] +async fn test_gh1868_ipv6_rule_is_not_created_with_v4_only_destination( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location with both IPv4 and IPv6 subnet + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 0, 80, 1)), 24).unwrap(), + IpNetwork::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + 64, + ) + .unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // setup user & device + setup_user_and_device(&mut rng, &pool, &location).await; + + // create a rule with only an IPv4 destination + let acl_rule = AclRule { + all_locations: true, + allow_all_users: true, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + any_address: false, + addresses: vec!["192.168.1.0/24".parse().unwrap()], + use_manual_destination_settings: true, + enabled: true, + state: RuleState::Applied, + ..Default::default() + }; + acl_rule.save(&pool).await.unwrap(); + + // verify only IPv4 rules are created + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap(); + let generated_firewall_rules = generated_firewall_config.rules; + println!("{generated_firewall_rules:#?}"); + assert_eq!(generated_firewall_rules.len(), 2); + + let allow_rule = &generated_firewall_rules[0]; + assert_eq!(allow_rule.verdict(), FirewallPolicy::Allow); + assert_eq!(allow_rule.ip_version(), IpVersion::Ipv4); + + let deny_rule = &generated_firewall_rules[1]; + assert_eq!(deny_rule.verdict(), FirewallPolicy::Deny); + assert_eq!(allow_rule.ip_version(), IpVersion::Ipv4); +} + +#[sqlx::test] +async fn test_gh1868_ipv4_rule_is_not_created_with_v6_only_destination( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + // Create test location with both IPv4 and IPv6 subnet + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 0, 80, 1)), 24).unwrap(), + IpNetwork::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + 64, + ) + .unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // setup user & device + setup_user_and_device(&mut rng, &pool, &location).await; + + // create a rule with only an IPv6 destination + let acl_rule = AclRule { + all_locations: true, + allow_all_users: true, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + any_address: false, + addresses: vec!["fc00::0/112".parse().unwrap()], + enabled: true, + state: RuleState::Applied, + ..Default::default() + }; + acl_rule.save(&pool).await.unwrap(); + + // verify only IPv6 rules are created + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap(); + let generated_firewall_rules = generated_firewall_config.rules; + 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.ip_version, i32::from(IpVersion::Ipv6)); + + let deny_rule = &generated_firewall_rules[1]; + assert_eq!(deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert_eq!(allow_rule.ip_version, i32::from(IpVersion::Ipv6)); +} + +#[sqlx::test] +async fn test_gh1868_ipv4_and_ipv6_rules_are_created_with_any_destination( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + // Create test location with both IPv4 and IPv6 subnet + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 0, 80, 1)), 24).unwrap(), + IpNetwork::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + 64, + ) + .unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // setup user & device + setup_user_and_device(&mut rng, &pool, &location).await; + + // create a rule with any destination enabled + let acl_rule = AclRule { + all_locations: true, + allow_all_users: true, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + any_address: true, + addresses: vec!["fc00::0/112".parse().unwrap()], + enabled: true, + state: RuleState::Applied, + ..Default::default() + }; + acl_rule.save(&pool).await.unwrap(); + + // verify only IPv4 rules are created + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap(); + let generated_firewall_rules = generated_firewall_config.rules; + assert_eq!(generated_firewall_rules.len(), 4); + + let allow_rule_ipv4 = &generated_firewall_rules[0]; + assert_eq!(allow_rule_ipv4.verdict(), FirewallPolicy::Allow); + assert_eq!(allow_rule_ipv4.ip_version(), IpVersion::Ipv4); + let allow_rule_ipv6 = &generated_firewall_rules[1]; + assert_eq!(allow_rule_ipv6.verdict(), FirewallPolicy::Allow); + assert_eq!(allow_rule_ipv6.ip_version(), IpVersion::Ipv6); + + let deny_rule_ipv4 = &generated_firewall_rules[2]; + assert_eq!(deny_rule_ipv4.verdict(), FirewallPolicy::Deny); + assert_eq!(allow_rule_ipv4.ip_version(), IpVersion::Ipv4); + let deny_rule_ipv6 = &generated_firewall_rules[3]; + assert_eq!(deny_rule_ipv6.verdict(), FirewallPolicy::Deny); + assert_eq!(allow_rule_ipv6.ip_version(), IpVersion::Ipv6); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/ip_address_handling.rs b/crates/defguard_core/src/enterprise/firewall/tests/ip_address_handling.rs new file mode 100644 index 0000000000..9b4650ad1a --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/ip_address_handling.rs @@ -0,0 +1,500 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_proto::enterprise::firewall::{ + IpAddress, IpRange, Port, PortRange as PortRangeProto, ip_address::Address, + port::Port as PortInner, +}; +use ipnetwork::Ipv6Network; + +use crate::enterprise::{ + db::models::acl::PortRange, + firewall::{ + find_largest_subnet_in_range, get_last_ip_in_v6_subnet, merge_addrs, merge_port_ranges, + }, +}; + +#[test] +fn test_merge_v4_addrs() { + let addr_ranges = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 60, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 60, 25)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 22)), + IpAddr::V4(Ipv4Addr::new(10, 0, 8, 127))..=IpAddr::V4(Ipv4Addr::new(10, 0, 9, 12)), + IpAddr::V4(Ipv4Addr::new(10, 0, 9, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 12)), + IpAddr::V4(Ipv4Addr::new(10, 0, 9, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 31)), + IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20))..=IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20)), + IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20)), + ]; + + let merged_addrs = merge_addrs(addr_ranges); + + assert_eq!( + merged_addrs, + [ + IpAddress { + address: Some(Address::Ip("10.0.8.127".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.8.128/25".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.9.0/24".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.10.0/27".to_string())), + }, + IpAddress { + address: Some(Address::Ip("10.0.20.20".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.60.20/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.60.24/31".to_string())), + }, + IpAddress { + address: Some(Address::Ip("192.168.0.20".to_string())), + }, + ] + ); + + // merge single IPs into a range + let addr_ranges = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3)), + IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20)), + ]; + + let merged_addrs = merge_addrs(addr_ranges); + assert_eq!( + merged_addrs, + [ + IpAddress { + address: Some(Address::IpSubnet("10.0.10.0/30".to_string())), + }, + IpAddress { + address: Some(Address::Ip("10.0.10.20".to_string())), + }, + ] + ); +} + +#[test] +fn test_merge_v6_addrs() { + let addr_ranges = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x5)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x3)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x8)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x1)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x3)), + ]; + + let merged_addrs = merge_addrs(addr_ranges); + assert_eq!( + merged_addrs, + [ + IpAddress { + address: Some(Address::Ip("2001:db8:1::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:1::2/127".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:1::4/126".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:1::8".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:2::1".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:3::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8:3::2/127".to_string())) + } + ] + ); +} + +#[test] +fn test_merge_addrs_extracts_ipv4_subnets() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 255)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("192.168.2.0/24".to_string())) + }, + ] + ); +} + +#[test] +fn test_merge_addrs_extracts_ipv6_subnets() { + let start = "2001:db8::".parse::().unwrap(); + let end = "2001:db9::ffff".parse::().unwrap(); + let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("2001:db8::/32".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db9::/112".to_string())) + }, + ] + ); +} + +#[test] +fn test_merge_addrs_falls_back_to_range_when_no_subnet_fits() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 0)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::IpRange(IpRange { + start: "192.168.1.255".to_string(), + end: "192.168.2.0".to_string(), + })), + },] + ); + + let start = "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff" + .parse::() + .unwrap(); + let end = "2001:db9::".parse::().unwrap(); + let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::IpRange(IpRange { + start: "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff".to_string(), + end: "2001:db9::".to_string(), + })), + },] + ); +} + +#[test] +fn test_merge_addrs_handles_single_ip() { + // Test case: single IP should remain as IP + let ranges = + vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::Ip("192.168.1.1".to_string())), + },] + ); + + let start = "2001:db8::".parse::().unwrap(); + let end = "2001:db8::".parse::().unwrap(); + let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [IpAddress { + address: Some(Address::Ip("2001:db8::".to_string())), + },] + ); +} + +#[test] +fn test_find_largest_ipv4_subnet_perfect_match() { + // Test /24 subnet + let start = Ipv4Addr::new(192, 168, 1, 0); + let end = Ipv4Addr::new(192, 168, 1, 255); + + let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); + + assert!(result.is_some()); + let subnet = result.unwrap(); + assert_eq!(subnet.to_string(), "192.168.1.0/24"); + + // Test /28 subnet (16 addresses) + let start = Ipv4Addr::new(192, 168, 1, 0); + let end = Ipv4Addr::new(192, 168, 1, 15); + + let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); + + assert!(result.is_some()); + let subnet = result.unwrap(); + assert_eq!(subnet.to_string(), "192.168.1.0/28"); +} + +#[test] +fn test_find_largest_ipv6_subnet_perfect_match() { + // Test /112 subnet + let start = "2001:db8::".parse::().unwrap(); + let end = "2001:db8::ffff".parse::().unwrap(); + + let result = find_largest_subnet_in_range(IpAddr::V6(start), IpAddr::V6(end)); + + assert!(result.is_some()); + let subnet = result.unwrap(); + assert_eq!(subnet.to_string(), "2001:db8::/112"); +} + +#[test] +fn test_find_largest_subnet_mixed_ip_versions() { + // Test mixed IP versions should return None + let start = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)); + let end = IpAddr::V6("2001:db8::1".parse().unwrap()); + + let result = find_largest_subnet_in_range(start, end); + + assert!(result.is_none()); +} + +#[test] +fn test_find_largest_subnet_invalid_range() { + // Test invalid range (start > end) should return None + let start = Ipv4Addr::new(192, 168, 1, 10); + let end = Ipv4Addr::new(192, 168, 1, 5); + + let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); + + assert!(result.is_none()); +} + +#[test] +fn test_merge_addrs_subnet_at_start_of_range() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 64)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/26".to_string())), + }, + IpAddress { + address: Some(Address::Ip("192.168.1.64".to_string())), + }, + ] + ); + + let ranges = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x40)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::IpSubnet("2001:db8::/122".to_string())), + }, + IpAddress { + address: Some(Address::Ip("2001:db8::40".to_string())), + }, + ] + ); +} + +#[test] +fn test_merge_addrs_subnet_at_end_of_range() { + let ranges = vec![ + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 15))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 31)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::Ip("192.168.1.15".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("192.168.1.16/28".to_string())), + }, + ] + ); + + let ranges = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x0f)) + ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x1f)), + ]; + + let result = merge_addrs(ranges); + + assert_eq!( + result, + [ + IpAddress { + address: Some(Address::Ip("2001:db8::f".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8::10/124".to_string())), + }, + ] + ); +} + +#[test] +fn test_merge_port_ranges() { + // single port + let input_ranges = vec![PortRange::new(100, 100)]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [Port { + port: Some(PortInner::SinglePort(100)) + }] + ); + + // overlapping ranges + let input_ranges = vec![ + PortRange::new(100, 200), + PortRange::new(150, 220), + PortRange::new(210, 300), + ]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 100, + end: 300 + })) + }] + ); + + // duplicate ranges + let input_ranges = vec![ + PortRange::new(100, 200), + PortRange::new(100, 200), + PortRange::new(150, 220), + PortRange::new(150, 220), + PortRange::new(210, 300), + PortRange::new(210, 300), + PortRange::new(350, 400), + PortRange::new(350, 400), + PortRange::new(350, 400), + ]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [ + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 100, + end: 300 + })) + }, + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 350, + end: 400 + })) + } + ] + ); + + // non-consecutive ranges + let input_ranges = vec![ + PortRange::new(501, 699), + PortRange::new(151, 220), + PortRange::new(210, 300), + PortRange::new(800, 800), + PortRange::new(200, 210), + PortRange::new(50, 50), + ]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [ + Port { + port: Some(PortInner::SinglePort(50)) + }, + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 151, + end: 300 + })) + }, + Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 501, + end: 699 + })) + }, + Port { + port: Some(PortInner::SinglePort(800)) + } + ] + ); + + // fully contained range + let input_ranges = vec![PortRange::new(100, 200), PortRange::new(120, 180)]; + let merged = merge_port_ranges(input_ranges); + assert_eq!( + merged, + [Port { + port: Some(PortInner::PortRange(PortRangeProto { + start: 100, + end: 200 + })) + }] + ); +} + +#[test] +fn test_last_ip_in_v6_subnet() { + let subnet: Ipv6Network = "2001:db8:85a3::8a2e:370:7334/64".parse().unwrap(); + let last_ip = get_last_ip_in_v6_subnet(&subnet); + assert_eq!( + last_ip, + IpAddr::V6(Ipv6Addr::new( + 0x2001, 0x0db8, 0x85a3, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff + )) + ); + + let subnet: Ipv6Network = "280b:47f8:c9d7:634c:cb35:11f3:14e1:5016/119" + .parse() + .unwrap(); + let last_ip = get_last_ip_in_v6_subnet(&subnet); + assert_eq!( + last_ip, + IpAddr::V6(Ipv6Addr::new( + 0x280b, 0x47f8, 0xc9d7, 0x634c, 0xcb35, 0x11f3, 0x14e1, 0x51ff + )) + ); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs similarity index 56% rename from crates/defguard_core/src/enterprise/firewall/tests.rs rename to crates/defguard_core/src/enterprise/firewall/tests/mod.rs index 3f0d480b67..fcd68c4e13 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests/mod.rs @@ -1,6 +1,5 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use chrono::{DateTime, NaiveDateTime}; use defguard_common::db::{ Id, NoId, models::{ @@ -13,26 +12,31 @@ use defguard_proto::enterprise::firewall::{ FirewallPolicy, IpAddress, IpRange, IpVersion, Port, PortRange as PortRangeProto, Protocol, ip_address::Address, port::Port as PortInner, }; -use ipnetwork::{IpNetwork, Ipv6Network}; -use rand::{Rng, thread_rng}; +use ipnetwork::IpNetwork; +use rand::{Rng, rngs::ThreadRng, thread_rng}; use sqlx::{ PgPool, postgres::{PgConnectOptions, PgPoolOptions}, query, }; -use super::{ - find_largest_subnet_in_range, get_last_ip_in_v6_subnet, get_source_users, merge_addrs, - merge_port_ranges, process_destination_addrs, -}; use crate::enterprise::{ db::models::acl::{ AclAlias, AclRule, AclRuleAlias, AclRuleDestinationRange, AclRuleDevice, AclRuleGroup, AclRuleInfo, AclRuleNetwork, AclRuleUser, AliasKind, PortRange, RuleState, }, - firewall::{get_source_addrs, get_source_network_devices, try_get_location_firewall_config}, + firewall::try_get_location_firewall_config, }; +mod all_locations; +mod destination; +mod disabled_rules; +mod expired_rules; +mod gh1868; +mod ip_address_handling; +mod source; +mod unapplied_rules; + impl Default for AclRuleDestinationRange { fn default() -> Self { Self { @@ -57,902 +61,654 @@ fn random_network_device_with_id(rng: &mut R, id: Id) -> Device { device } -#[test] -fn test_get_relevant_users() { - let mut rng = thread_rng(); - - // prepare allowed and denied users lists with shared elements - let user_1 = random_user_with_id(&mut rng, 1); - let user_2 = random_user_with_id(&mut rng, 2); - let user_3 = random_user_with_id(&mut rng, 3); - let user_4 = random_user_with_id(&mut rng, 4); - let user_5 = random_user_with_id(&mut rng, 5); - let allowed_users = vec![user_1.clone(), user_2.clone(), user_4.clone()]; - let denied_users = vec![user_3.clone(), user_4, user_5.clone()]; - - let users = get_source_users(allowed_users, &denied_users); - assert_eq!(users, vec![user_1, user_2]); -} - -#[test] -fn test_get_relevant_network_devices() { - let mut rng = thread_rng(); - - // prepare allowed and denied network devices lists with shared elements - let device_1 = random_network_device_with_id(&mut rng, 1); - let device_2 = random_network_device_with_id(&mut rng, 2); - let device_3 = random_network_device_with_id(&mut rng, 3); - let device_4 = random_network_device_with_id(&mut rng, 4); - let device_5 = random_network_device_with_id(&mut rng, 5); - let allowed_devices = vec![ - device_1.clone(), - device_3.clone(), - device_4.clone(), - device_5.clone(), - ]; - let denied_devices = vec![device_2.clone(), device_4, device_5.clone()]; +async fn create_test_users_and_devices( + rng: &mut ThreadRng, + pool: &PgPool, + test_locations: Vec<&WireguardNetwork>, +) { + // create two users + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(pool).await.unwrap(); - let devices = get_source_network_devices(allowed_devices, &denied_devices); - assert_eq!(devices, vec![device_1, device_3]); + // create two devices for each user and create network configurations for all test locations + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(pool).await.unwrap(); + + // Add device to locations' VPN network + for location in test_locations.iter() { + let wireguard_ips = location + .address + .iter() + .map(|subnet| match subnet { + IpNetwork::V4(ipv4_network) => { + let octets = ipv4_network.network().octets(); + IpAddr::V4(Ipv4Addr::new( + octets[0], + octets[1], + user.id as u8, + device_num, + )) + } + IpNetwork::V6(ipv6_network) => { + let mut octets = ipv6_network.network().octets(); + // Set the last two octets (bytes 14 and 15) + octets[14] = user.id as u8; + octets[15] = device_num; + IpAddr::V6(Ipv6Addr::from(octets)) + } + }) + .collect(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips, + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(pool).await.unwrap(); + } + } + } } -#[test] -fn test_process_source_addrs_v4() { - // Test data with mixed IPv4 and IPv6 addresses - let user_device_ips = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 2)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 5)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), - ]; +async fn create_acl_rule( + pool: &PgPool, + rule: AclRule, + locations: Vec, + allowed_users: Vec, + denied_users: Vec, + allowed_groups: Vec, + denied_groups: Vec, + allowed_network_devices: Vec, + denied_network_devices: Vec, + destination_ranges: Vec<(IpAddr, IpAddr)>, + aliases: Vec, +) -> AclRuleInfo { + let mut conn = pool.acquire().await.unwrap(); - let network_device_ips = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 3)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 4)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), // Should be filtered out - IpAddr::V4(Ipv4Addr::new(172, 16, 1, 1)), - ]; + // create base rule + let rule = rule.save(&mut *conn).await.unwrap(); + let rule_id = rule.id; - let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv4); + // create related objects + // locations + for location_id in locations { + AclRuleNetwork::new(rule_id, location_id) + .save(&mut *conn) + .await + .unwrap(); + } - // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges - assert_eq!( - source_addrs, - [ - IpAddress { - address: Some(Address::Ip("10.0.1.1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.2/31".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.4/31".to_string())) - }, - IpAddress { - address: Some(Address::Ip("172.16.1.1".to_string())), - }, - IpAddress { - address: Some(Address::Ip("192.168.1.100".to_string())), - }, - ] - ); + // allowed users + for user_id in allowed_users { + AclRuleUser::new(rule_id, user_id, true) + .save(&mut *conn) + .await + .unwrap(); + } - // Test with empty input - let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv4); - assert!(empty_addrs.is_empty()); + // denied users + for user_id in denied_users { + AclRuleUser::new(rule_id, user_id, false) + .save(&mut *conn) + .await + .unwrap(); + } - // Test with only IPv6 addresses - should return empty result for IPv4 - let ipv6_only = get_source_addrs( - vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))], - vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2))], - IpVersion::Ipv4, - ); - assert!(ipv6_only.is_empty()); -} + // allowed groups + for group_id in allowed_groups { + AclRuleGroup::new(rule_id, group_id, true) + .save(&mut *conn) + .await + .unwrap(); + } -#[test] -fn test_process_source_addrs_v6() { - // Test data with mixed IPv4 and IPv6 addresses - let user_device_ips = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 5)), - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), // Should be filtered out - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, 1)), - ]; + // denied groups + for group_id in denied_groups { + AclRuleGroup::new(rule_id, group_id, false) + .save(&mut *conn) + .await + .unwrap(); + } - let network_device_ips = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 3)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 4)), - IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), // Should be filtered out - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 2, 0, 0, 0, 1)), - ]; + // allowed devices + for device_id in allowed_network_devices { + AclRuleDevice::new(rule_id, device_id, true) + .save(&mut *conn) + .await + .unwrap(); + } - let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv6); + // denied devices + for device_id in denied_network_devices { + AclRuleDevice::new(rule_id, device_id, false) + .save(&mut *conn) + .await + .unwrap(); + } - // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges - assert_eq!( - source_addrs, - [ - IpAddress { - address: Some(Address::Ip("2001:db8::1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8::2/127".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8::4/127".to_string())) - }, - IpAddress { - address: Some(Address::Ip("2001:db8:0:1::1".to_string())), - }, - IpAddress { - address: Some(Address::Ip("2001:db8:0:2::1".to_string())), - }, - ] - ); + // destination ranges + for range in destination_ranges { + AclRuleDestinationRange { + id: NoId, + rule_id, + start: range.0, + end: range.1, + } + .save(&mut *conn) + .await + .unwrap(); + } - // Test with empty input - let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv6); - assert!(empty_addrs.is_empty()); + // aliases + for alias_id in aliases { + AclRuleAlias::new(rule_id, alias_id) + .save(&mut *conn) + .await + .unwrap(); + } - // Test with only IPv4 addresses - should return empty result for IPv6 - let ipv4_only = get_source_addrs( - vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))], - vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))], - IpVersion::Ipv6, - ); - assert!(ipv4_only.is_empty()); + // convert to output format + rule.to_info(&mut conn).await.unwrap() } -#[test] -fn test_process_destination_addrs_v4() { - // Test data with mixed IPv4 and IPv6 networks - let destination_ips = [ - "10.0.1.0/24".parse().unwrap(), - "10.0.2.0/24".parse().unwrap(), - "2001:db8::/64".parse().unwrap(), // Should be filtered out - "192.168.1.0/24".parse().unwrap(), - ]; +#[sqlx::test] +async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; - let destination_ranges = [ - AclRuleDestinationRange { - start: IpAddr::V4(Ipv4Addr::new(10, 0, 3, 255)), - end: IpAddr::V4(Ipv4Addr::new(10, 0, 4, 0)), - ..Default::default() - }, - AclRuleDestinationRange { - start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out - end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 100)), - ..Default::default() - }, - ]; + let mut rng = thread_rng(); - let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: false, + ..Default::default() + }; + let mut location = location.save(&pool).await.unwrap(); - assert_eq!( - destination_addrs.0, - [ - IpAddress { - address: Some(Address::IpSubnet("10.0.1.0/24".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.0/24".to_string())), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.3.255".to_string(), - end: "10.0.4.0".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }, - ] - ); + // Setup test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + let user_3: User = rng.r#gen(); + let user_3 = user_3.save(&pool).await.unwrap(); + let user_4: User = rng.r#gen(); + let user_4 = user_4.save(&pool).await.unwrap(); + let user_5: User = rng.r#gen(); + let user_5 = user_5.save(&pool).await.unwrap(); - // Test with empty input - let empty_addrs = process_destination_addrs(&[], &[]); - assert!(empty_addrs.0.is_empty()); + for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); - // Test with only IPv6 addresses - should return empty result for IPv4 - let ipv6_only = process_destination_addrs(&["2001:db8::/64".parse().unwrap()], &[]); - assert!(ipv6_only.0.is_empty()); -} + // Add device to location's VPN network + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location.id, + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } -#[test] -fn test_process_destination_addrs_v6() { - // Test data with mixed IPv4 and IPv6 networks - let destination_ips = vec![ - "2001:db8:1::/64".parse().unwrap(), - "2001:db8:2::/64".parse().unwrap(), - "10.0.1.0/24".parse().unwrap(), // Should be filtered out - "2001:db8:3::/64".parse().unwrap(), - ]; + // Setup test groups + let group_1 = Group { + id: NoId, + name: "group_1".into(), + ..Default::default() + }; + let group_1 = group_1.save(&pool).await.unwrap(); + let group_2 = Group { + id: NoId, + name: "group_2".into(), + ..Default::default() + }; + let group_2 = group_2.save(&pool).await.unwrap(); - let destination_ranges = vec![ - AclRuleDestinationRange { - start: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 1)), - end: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 4, 0, 0, 0, 0, 3)), - ..Default::default() - }, - AclRuleDestinationRange { - start: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), // Should be filtered out - end: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), - ..Default::default() - }, + // Assign users to groups: + // Group 1: users 1,2 + // Group 2: users 3,4 + let group_assignments = vec![ + (&group_1, vec![&user_1, &user_2]), + (&group_2, vec![&user_3, &user_4]), ]; - let destination_addrs = process_destination_addrs(&destination_ips, &destination_ranges); - - assert_eq!( - destination_addrs.1, - [ - IpAddress { - address: Some(Address::IpSubnet("2001:db8:1::/64".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:2::/64".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:3::/64".to_string())), - }, - IpAddress { - address: Some(Address::Ip("2001:db8:4::1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:4::2/127".to_string())) - } - ] - ); + for (group, users) in group_assignments { + for user in users { + query!( + "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", + user.id, + group.id + ) + .execute(&pool) + .await + .unwrap(); + } + } + + // Create some network devices + let network_device_1 = Device { + id: NoId, + name: "network-device-1".into(), + user_id: user_1.id, // Owned by user 1 + device_type: DeviceType::Network, + description: Some("Test network device 1".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_1 = network_device_1.save(&pool).await.unwrap(); - // Test with empty input - let empty_addrs = process_destination_addrs(&[], &[]); - assert!(empty_addrs.1.is_empty()); + let network_device_2 = Device { + id: NoId, + name: "network-device-2".into(), + user_id: user_2.id, // Owned by user 2 + device_type: DeviceType::Network, + description: Some("Test network device 2".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_2 = network_device_2.save(&pool).await.unwrap(); - // Test with only IPv4 addresses - should return empty result for IPv6 - let ipv4_only = process_destination_addrs(&["192.168.1.0/24".parse().unwrap()], &[]); - assert!(ipv4_only.1.is_empty()); -} + let network_device_3 = Device { + id: NoId, + name: "network-device-3".into(), + user_id: user_3.id, // Owned by user 3 + device_type: DeviceType::Network, + description: Some("Test network device 3".into()), + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let network_device_3 = network_device_3.save(&pool).await.unwrap(); + + // Add network devices to location's VPN network + let network_devices = vec![ + ( + network_device_1.id, + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), + ), + ( + network_device_2.id, + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), + ), + ( + network_device_3.id, + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), + ), + ]; + + for (device_id, ip) in network_devices { + let network_device = WireguardNetworkDevice { + device_id, + wireguard_network_id: location.id, + wireguard_ips: vec![ip], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + + // Create first ACL rule - Web access + let acl_rule_1 = AclRule { + id: NoId, + name: "Web Access".into(), + all_locations: false, + expires: None, + allow_all_users: false, + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + addresses: vec!["192.168.1.0/24".parse().unwrap()], + ports: vec![ + PortRange::new(80, 80).into(), + PortRange::new(443, 443).into(), + ], + protocols: vec![Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + any_address: false, + any_port: false, + any_protocol: false, + use_manual_destination_settings: true, + ..Default::default() + }; + let locations = vec![location.id]; + let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web + let denied_users = vec![user_3.id]; // Third user explicitly denied + let allowed_groups = vec![group_1.id]; // First group allowed + let denied_groups = Vec::new(); + let allowed_devices = vec![network_device_1.id]; + let denied_devices = vec![network_device_2.id, network_device_3.id]; + let destination_ranges = Vec::new(); + let aliases = Vec::new(); + + let _acl_rule_1 = create_acl_rule( + &pool, + acl_rule_1, + locations, + allowed_users, + denied_users, + allowed_groups, + denied_groups, + allowed_devices, + denied_devices, + destination_ranges, + aliases, + ) + .await; -#[test] -fn test_merge_v4_addrs() { - let addr_ranges = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 60, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 60, 25)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 22)), - IpAddr::V4(Ipv4Addr::new(10, 0, 8, 127))..=IpAddr::V4(Ipv4Addr::new(10, 0, 9, 12)), - IpAddr::V4(Ipv4Addr::new(10, 0, 9, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 12)), - IpAddr::V4(Ipv4Addr::new(10, 0, 9, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 31)), - IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20))..=IpAddr::V4(Ipv4Addr::new(192, 168, 0, 20)), - IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 20, 20)), + // Create second ACL rule - DNS access + let acl_rule_2 = AclRule { + id: NoId, + name: "DNS Access".into(), + all_locations: false, + expires: None, + allow_all_users: true, // Allow all users + deny_all_users: false, + allow_all_network_devices: false, + deny_all_network_devices: false, + addresses: Vec::new(), // Will use destination ranges instead + ports: vec![PortRange::new(53, 53).into()], + protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], + enabled: true, + parent_id: None, + state: RuleState::Applied, + any_address: false, + any_port: false, + any_protocol: false, + use_manual_destination_settings: true, + ..Default::default() + }; + let locations_2 = vec![location.id]; + let allowed_users_2 = Vec::new(); + let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS + let allowed_groups_2 = Vec::new(); + let denied_groups_2 = vec![group_2.id]; + let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed + let denied_devices_2 = vec![network_device_3.id]; // Third network device denied + let destination_ranges_2 = vec![ + ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), + ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), ]; + let aliases_2 = Vec::new(); + + let _acl_rule_2 = create_acl_rule( + &pool, + acl_rule_2, + locations_2, + allowed_users_2, + denied_users_2, + allowed_groups_2, + denied_groups_2, + allowed_devices_2, + denied_devices_2, + destination_ranges_2, + aliases_2, + ) + .await; + + let mut conn = pool.acquire().await.unwrap(); - let merged_addrs = merge_addrs(addr_ranges); + // try to generate firewall config with ACL disabled + location.acl_enabled = false; + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap(); + assert!(generated_firewall_config.is_none()); + // generate firewall config with default policy Allow + location.acl_enabled = true; + location.acl_default_allow = true; + let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap(); assert_eq!( - merged_addrs, - [ - IpAddress { - address: Some(Address::Ip("10.0.8.127".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.8.128/25".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.9.0/24".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.10.0/27".to_string())), - }, - IpAddress { - address: Some(Address::Ip("10.0.20.20".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.60.20/30".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.60.24/31".to_string())), - }, - IpAddress { - address: Some(Address::Ip("192.168.0.20".to_string())), - }, - ] + generated_firewall_config.default_policy, + i32::from(FirewallPolicy::Allow) ); - // merge single IPs into a range - let addr_ranges = vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 0)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 1)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 2)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 3)), - IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20))..=IpAddr::V4(Ipv4Addr::new(10, 0, 10, 20)), - ]; + let generated_firewall_rules = generated_firewall_config.rules; + + assert_eq!(generated_firewall_rules.len(), 4); - let merged_addrs = merge_addrs(addr_ranges); + // First ACL - Web Access ALLOW + let web_allow_rule = &generated_firewall_rules[0]; + assert_eq!(web_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(web_allow_rule.protocols, vec![i32::from(Protocol::Tcp)]); + assert_eq!( + web_allow_rule.destination_addrs, + [IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] + ); assert_eq!( - merged_addrs, + web_allow_rule.destination_ports, [ - IpAddress { - address: Some(Address::IpSubnet("10.0.10.0/30".to_string())), - }, - IpAddress { - address: Some(Address::Ip("10.0.10.20".to_string())), + Port { + port: Some(PortInner::SinglePort(80)) }, + Port { + port: Some(PortInner::SinglePort(443)) + } ] ); -} - -#[test] -fn test_merge_v6_addrs() { - let addr_ranges = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x5)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x3)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x1, 0x0, 0x0, 0x0, 0x0, 0x8)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1)), - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x1)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x3)), - ]; - - let merged_addrs = merge_addrs(addr_ranges); + // Source addresses should include devices of users 1,2 and network_device_1 assert_eq!( - merged_addrs, + web_allow_rule.source_addrs, [ IpAddress { - address: Some(Address::Ip("2001:db8:1::1".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:1::2/127".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:1::4/126".to_string())) - }, - IpAddress { - address: Some(Address::Ip("2001:db8:1::8".to_string())) + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), }, IpAddress { - address: Some(Address::Ip("2001:db8:2::1".to_string())) + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), }, IpAddress { - address: Some(Address::Ip("2001:db8:3::1".to_string())) + address: Some(Address::Ip("10.0.100.1".to_string())), }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8:3::2/127".to_string())) - } ] ); -} - -#[test] -fn test_merge_addrs_extracts_ipv4_subnets() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 255)), - ]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("192.168.2.0/24".to_string())) - }, - ] - ); -} - -#[test] -fn test_merge_addrs_extracts_ipv6_subnets() { - let start = "2001:db8::".parse::().unwrap(); - let end = "2001:db9::ffff".parse::().unwrap(); - let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::IpSubnet("2001:db8::/32".to_string())) - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db9::/112".to_string())) - }, - ] - ); -} - -#[test] -fn test_merge_addrs_falls_back_to_range_when_no_subnet_fits() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255))..=IpAddr::V4(Ipv4Addr::new(192, 168, 2, 0)), - ]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [IpAddress { - address: Some(Address::IpRange(IpRange { - start: "192.168.1.255".to_string(), - end: "192.168.2.0".to_string(), - })), - },] - ); - - let start = "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff" - .parse::() - .unwrap(); - let end = "2001:db9::".parse::().unwrap(); - let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [IpAddress { - address: Some(Address::IpRange(IpRange { - start: "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff".to_string(), - end: "2001:db9::".to_string(), - })), - },] - ); -} - -#[test] -fn test_merge_addrs_handles_single_ip() { - // Test case: single IP should remain as IP - let ranges = - vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))]; - - let result = merge_addrs(ranges); + // First ACL - Web Access DENY + let web_deny_rule = &generated_firewall_rules[2]; + assert_eq!(web_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule.protocols.is_empty()); + assert!(web_deny_rule.destination_ports.is_empty()); + assert!(web_deny_rule.source_addrs.is_empty()); assert_eq!( - result, + web_deny_rule.destination_addrs, [IpAddress { - address: Some(Address::Ip("192.168.1.1".to_string())), - },] + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] ); - let start = "2001:db8::".parse::().unwrap(); - let end = "2001:db8::".parse::().unwrap(); - let ranges = vec![IpAddr::V6(start)..=IpAddr::V6(end)]; - - let result = merge_addrs(ranges); - + // Second ACL - DNS Access ALLOW + let dns_allow_rule = &generated_firewall_rules[1]; + assert_eq!(dns_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); assert_eq!( - result, - [IpAddress { - address: Some(Address::Ip("2001:db8::".to_string())), - },] + dns_allow_rule.protocols, + [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] ); -} - -#[test] -fn test_find_largest_ipv4_subnet_perfect_match() { - // Test /24 subnet - let start = Ipv4Addr::new(192, 168, 1, 0); - let end = Ipv4Addr::new(192, 168, 1, 255); - - let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); - - assert!(result.is_some()); - let subnet = result.unwrap(); - assert_eq!(subnet.to_string(), "192.168.1.0/24"); - - // Test /28 subnet (16 addresses) - let start = Ipv4Addr::new(192, 168, 1, 0); - let end = Ipv4Addr::new(192, 168, 1, 15); - - let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); - - assert!(result.is_some()); - let subnet = result.unwrap(); - assert_eq!(subnet.to_string(), "192.168.1.0/28"); -} - -#[test] -fn test_find_largest_ipv6_subnet_perfect_match() { - // Test /112 subnet - let start = "2001:db8::".parse::().unwrap(); - let end = "2001:db8::ffff".parse::().unwrap(); - - let result = find_largest_subnet_in_range(IpAddr::V6(start), IpAddr::V6(end)); - - assert!(result.is_some()); - let subnet = result.unwrap(); - assert_eq!(subnet.to_string(), "2001:db8::/112"); -} - -#[test] -fn test_find_largest_subnet_mixed_ip_versions() { - // Test mixed IP versions should return None - let start = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)); - let end = IpAddr::V6("2001:db8::1".parse().unwrap()); - - let result = find_largest_subnet_in_range(start, end); - - assert!(result.is_none()); -} - -#[test] -fn test_find_largest_subnet_invalid_range() { - // Test invalid range (start > end) should return None - let start = Ipv4Addr::new(192, 168, 1, 10); - let end = Ipv4Addr::new(192, 168, 1, 5); - - let result = find_largest_subnet_in_range(IpAddr::V4(start), IpAddr::V4(end)); - - assert!(result.is_none()); -} - -#[test] -fn test_merge_addrs_subnet_at_start_of_range() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 64)), - ]; - - let result = merge_addrs(ranges); - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/26".to_string())), - }, - IpAddress { - address: Some(Address::Ip("192.168.1.64".to_string())), - }, - ] + dns_allow_rule.destination_ports, + [Port { + port: Some(PortInner::SinglePort(53)) + }] ); - - let ranges = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x40)), - ]; - - let result = merge_addrs(ranges); - + // Source addresses should include network_devices 1,2 assert_eq!( - result, + dns_allow_rule.source_addrs, [ IpAddress { - address: Some(Address::IpSubnet("2001:db8::/122".to_string())), - }, - IpAddress { - address: Some(Address::Ip("2001:db8::40".to_string())), + address: Some(Address::IpRange(IpRange { + start: "10.0.1.1".to_string(), + end: "10.0.1.2".to_string(), + })), }, - ] - ); -} - -#[test] -fn test_merge_addrs_subnet_at_end_of_range() { - let ranges = vec![ - IpAddr::V4(Ipv4Addr::new(192, 168, 1, 15))..=IpAddr::V4(Ipv4Addr::new(192, 168, 1, 31)), - ]; - - let result = merge_addrs(ranges); - - assert_eq!( - result, - [ IpAddress { - address: Some(Address::Ip("192.168.1.15".to_string())), + address: Some(Address::IpRange(IpRange { + start: "10.0.2.1".to_string(), + end: "10.0.2.2".to_string(), + })), }, IpAddress { - address: Some(Address::IpSubnet("192.168.1.16/28".to_string())), + address: Some(Address::IpRange(IpRange { + start: "10.0.100.1".to_string(), + end: "10.0.100.2".to_string(), + })), }, ] ); - let ranges = vec![ - IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x0f)) - ..=IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x1f)), + let expected_destination_addrs = vec![ + IpAddress { + address: Some(Address::Ip("10.0.1.13".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), + }, ]; - let result = merge_addrs(ranges); + assert_eq!(dns_allow_rule.destination_addrs, expected_destination_addrs); - assert_eq!( - result, - [ - IpAddress { - address: Some(Address::Ip("2001:db8::f".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("2001:db8::10/124".to_string())), - }, - ] - ); + // Second ACL - DNS Access DENY + let dns_deny_rule = &generated_firewall_rules[3]; + assert_eq!(dns_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule.protocols.is_empty(),); + assert!(dns_deny_rule.destination_ports.is_empty(),); + assert!(dns_deny_rule.source_addrs.is_empty(),); + assert_eq!(dns_deny_rule.destination_addrs, expected_destination_addrs); } -#[test] -fn test_merge_port_ranges() { - // single port - let input_ranges = vec![PortRange::new(100, 100)]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [Port { - port: Some(PortInner::SinglePort(100)) - }] - ); +#[sqlx::test] +async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); - // overlapping ranges - let input_ranges = vec![ - PortRange::new(100, 200), - PortRange::new(150, 220), - PortRange::new(210, 300), - ]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 100, - end: 300 - })) - }] - ); + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: false, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let mut location = location.save(&pool).await.unwrap(); - // duplicate ranges - let input_ranges = vec![ - PortRange::new(100, 200), - PortRange::new(100, 200), - PortRange::new(150, 220), - PortRange::new(150, 220), - PortRange::new(210, 300), - PortRange::new(210, 300), - PortRange::new(350, 400), - PortRange::new(350, 400), - PortRange::new(350, 400), - ]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [ - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 100, - end: 300 - })) - }, - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 350, - end: 400 - })) - } - ] - ); + // Setup test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + let user_3: User = rng.r#gen(); + let user_3 = user_3.save(&pool).await.unwrap(); + let user_4: User = rng.r#gen(); + let user_4 = user_4.save(&pool).await.unwrap(); + let user_5: User = rng.r#gen(); + let user_5 = user_5.save(&pool).await.unwrap(); - // non-consecutive ranges - let input_ranges = vec![ - PortRange::new(501, 699), - PortRange::new(151, 220), - PortRange::new(210, 300), - PortRange::new(800, 800), - PortRange::new(200, 210), - PortRange::new(50, 50), - ]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [ - Port { - port: Some(PortInner::SinglePort(50)) - }, - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 151, - end: 300 - })) - }, - Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 501, - end: 699 - })) - }, - Port { - port: Some(PortInner::SinglePort(800)) - } - ] - ); - - // fully contained range - let input_ranges = vec![PortRange::new(100, 200), PortRange::new(120, 180)]; - let merged = merge_port_ranges(input_ranges); - assert_eq!( - merged, - [Port { - port: Some(PortInner::PortRange(PortRangeProto { - start: 100, - end: 200 - })) - }] - ); -} - -#[test] -fn test_last_ip_in_v6_subnet() { - let subnet: Ipv6Network = "2001:db8:85a3::8a2e:370:7334/64".parse().unwrap(); - let last_ip = get_last_ip_in_v6_subnet(&subnet); - assert_eq!( - last_ip, - IpAddr::V6(Ipv6Addr::new( - 0x2001, 0x0db8, 0x85a3, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff - )) - ); - - let subnet: Ipv6Network = "280b:47f8:c9d7:634c:cb35:11f3:14e1:5016/119" - .parse() - .unwrap(); - let last_ip = get_last_ip_in_v6_subnet(&subnet); - assert_eq!( - last_ip, - IpAddr::V6(Ipv6Addr::new( - 0x280b, 0x47f8, 0xc9d7, 0x634c, 0xcb35, 0x11f3, 0x14e1, 0x51ff - )) - ); -} - -async fn create_acl_rule( - pool: &PgPool, - rule: AclRule, - locations: Vec, - allowed_users: Vec, - denied_users: Vec, - allowed_groups: Vec, - denied_groups: Vec, - allowed_network_devices: Vec, - denied_network_devices: Vec, - destination_ranges: Vec<(IpAddr, IpAddr)>, - aliases: Vec, -) -> AclRuleInfo { - let mut conn = pool.acquire().await.unwrap(); - - // create base rule - let rule = rule.save(&mut *conn).await.unwrap(); - let rule_id = rule.id; - - // create related objects - // locations - for location_id in locations { - AclRuleNetwork::new(rule_id, location_id) - .save(&mut *conn) - .await - .unwrap(); - } - - // allowed users - for user_id in allowed_users { - AclRuleUser::new(rule_id, user_id, true) - .save(&mut *conn) - .await - .unwrap(); - } - - // denied users - for user_id in denied_users { - AclRuleUser::new(rule_id, user_id, false) - .save(&mut *conn) - .await - .unwrap(); - } - - // allowed groups - for group_id in allowed_groups { - AclRuleGroup::new(rule_id, group_id, true) - .save(&mut *conn) - .await - .unwrap(); - } - - // denied groups - for group_id in denied_groups { - AclRuleGroup::new(rule_id, group_id, false) - .save(&mut *conn) - .await - .unwrap(); - } - - // allowed devices - for device_id in allowed_network_devices { - AclRuleDevice::new(rule_id, device_id, true) - .save(&mut *conn) - .await - .unwrap(); - } - - // denied devices - for device_id in denied_network_devices { - AclRuleDevice::new(rule_id, device_id, false) - .save(&mut *conn) - .await - .unwrap(); - } - - // destination ranges - for range in destination_ranges { - AclRuleDestinationRange { - id: NoId, - rule_id, - start: range.0, - end: range.1, - } - .save(&mut *conn) - .await - .unwrap(); - } - - // aliases - for alias_id in aliases { - AclRuleAlias::new(rule_id, alias_id) - .save(&mut *conn) - .await - .unwrap(); - } - - // convert to output format - rule.to_info(&mut conn).await.unwrap() -} - -#[sqlx::test] -async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - - let mut rng = thread_rng(); - - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: false, - ..Default::default() - }; - let mut location = location.save(&pool).await.unwrap(); - - // Setup test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - let user_3: User = rng.r#gen(); - let user_3 = user_3.save(&pool).await.unwrap(); - let user_4: User = rng.r#gen(); - let user_4 = user_4.save(&pool).await.unwrap(); - let user_5: User = rng.r#gen(); - let user_5 = user_5.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); + for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{}", user.id, device_num), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); // Add device to location's VPN network let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, + wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( + 0xff00, 0, - user.id as u8, - device_num as u8, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, ))], preshared_key: None, is_authorized: true, @@ -1038,15 +794,15 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO let network_devices = vec![ ( network_device_1.id, - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), ), ( network_device_2.id, - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), ), ( network_device_3.id, - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), ), ]; @@ -1066,13 +822,13 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO let acl_rule_1 = AclRule { id: NoId, name: "Web Access".into(), - all_networks: false, + all_locations: false, expires: None, allow_all_users: false, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: vec!["192.168.1.0/24".parse().unwrap()], + addresses: vec!["fc00::0/112".parse().unwrap()], ports: vec![ PortRange::new(80, 80).into(), PortRange::new(443, 443).into(), @@ -1081,10 +837,11 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, + any_address: false, + any_port: false, + any_protocol: false, + use_manual_destination_settings: true, + ..Default::default() }; let locations = vec![location.id]; let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web @@ -1115,22 +872,23 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO let acl_rule_2 = AclRule { id: NoId, name: "DNS Access".into(), - all_networks: false, + all_locations: false, expires: None, allow_all_users: true, // Allow all users deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: Vec::new(), // Will use destination ranges instead + addresses: Vec::new(), // Will use destination ranges instead ports: vec![PortRange::new(53, 53).into()], protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, + any_address: false, + any_port: false, + any_protocol: false, + use_manual_destination_settings: true, + ..Default::default() }; let locations_2 = vec![location.id]; let allowed_users_2 = Vec::new(); @@ -1140,8 +898,8 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed let denied_devices_2 = vec![network_device_3.id]; // Third network device denied let destination_ranges_2 = vec![ - ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), - ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), + ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), + ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), ]; let aliases_2 = Vec::new(); @@ -1192,7 +950,7 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO assert_eq!( web_allow_rule.destination_addrs, [IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + address: Some(Address::IpSubnet("fc00::/112".to_string())), }] ); assert_eq!( @@ -1212,18 +970,18 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO [ IpAddress { address: Some(Address::IpRange(IpRange { - start: "10.0.1.1".to_string(), - end: "10.0.1.2".to_string(), + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), })), }, IpAddress { address: Some(Address::IpRange(IpRange { - start: "10.0.2.1".to_string(), - end: "10.0.2.2".to_string(), + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), })), }, IpAddress { - address: Some(Address::Ip("10.0.100.1".to_string())), + address: Some(Address::Ip("ff00::100:1".to_string())), }, ] ); @@ -1237,7 +995,7 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO assert_eq!( web_deny_rule.destination_addrs, [IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + address: Some(Address::IpSubnet("fc00::/112".to_string())), }] ); @@ -1254,91 +1012,119 @@ async fn test_generate_firewall_rules_ipv4(_: PgPoolOptions, options: PgConnectO port: Some(PortInner::SinglePort(53)) }] ); - // Source addresses should include network_devices 1,2 - assert_eq!( - dns_allow_rule.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(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.100.1".to_string(), - end: "10.0.100.2".to_string(), - })), - }, - ] - ); let expected_destination_addrs = vec![ IpAddress { - address: Some(Address::Ip("10.0.1.13".to_string())), + address: Some(Address::Ip("fc00::1:13".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), + address: Some(Address::IpSubnet("fc00::1:14/126".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), + address: Some(Address::IpSubnet("fc00::1:18/125".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), + address: Some(Address::IpSubnet("fc00::1:20/123".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), + address: Some(Address::IpSubnet("fc00::1:40/126".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), + address: Some(Address::IpSubnet("fc00::1:52/127".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), + address: Some(Address::IpSubnet("fc00::1:54/126".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), + address: Some(Address::IpSubnet("fc00::1:58/125".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), + address: Some(Address::IpSubnet("fc00::1:60/123".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), + address: Some(Address::IpSubnet("fc00::1:80/121".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), + address: Some(Address::IpSubnet("fc00::1:100/120".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), + address: Some(Address::IpSubnet("fc00::1:200/119".to_string())), }, - ]; - - assert_eq!(dns_allow_rule.destination_addrs, expected_destination_addrs); - - // Second ACL - DNS Access DENY - let dns_deny_rule = &generated_firewall_rules[3]; - assert_eq!(dns_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert!(dns_deny_rule.protocols.is_empty(),); - assert!(dns_deny_rule.destination_ports.is_empty(),); - assert!(dns_deny_rule.source_addrs.is_empty(),); - assert_eq!(dns_deny_rule.destination_addrs, expected_destination_addrs); -} - -#[sqlx::test] -async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + IpAddress { + address: Some(Address::IpSubnet("fc00::1:400/118".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:800/117".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:1000/116".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:2000/115".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:4000/114".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:8000/113".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::2:0/122".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::2:40/126".to_string())), + }, + ]; + + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::100:1".to_string(), + end: "ff00::100:2".to_string(), + })), + }, + ] + ); + assert_eq!(dns_allow_rule.destination_addrs, expected_destination_addrs); + + // Second ACL - DNS Access DENY + let dns_deny_rule = &generated_firewall_rules[3]; + assert_eq!(dns_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule.protocols.is_empty(),); + assert!(dns_deny_rule.destination_ports.is_empty(),); + assert!(dns_deny_rule.source_addrs.is_empty(),); + assert_eq!(dns_deny_rule.destination_addrs, expected_destination_addrs); +} + +#[sqlx::test] +async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; + let mut rng = thread_rng(); // Create test location let location = WireguardNetwork { id: NoId, acl_enabled: false, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], ..Default::default() }; let mut location = location.save(&pool).await.unwrap(); @@ -1374,16 +1160,19 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let network_device = WireguardNetworkDevice { device_id: device.id, wireguard_network_id: location.id, - wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - ))], + wireguard_ips: vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + )), + ], preshared_key: None, is_authorized: true, authorized_at: None, @@ -1468,23 +1257,32 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let network_devices = vec![ ( network_device_1.id, - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), + ], ), ( network_device_2.id, - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), + ], ), ( network_device_3.id, - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), + vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), + IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), + ], ), ]; - for (device_id, ip) in network_devices { + for (device_id, ips) in network_devices { let network_device = WireguardNetworkDevice { device_id, wireguard_network_id: location.id, - wireguard_ips: vec![ip], + wireguard_ips: ips, preshared_key: None, is_authorized: true, authorized_at: None, @@ -1496,13 +1294,16 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let acl_rule_1 = AclRule { id: NoId, name: "Web Access".into(), - all_networks: false, + all_locations: false, expires: None, allow_all_users: false, deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: vec!["fc00::0/112".parse().unwrap()], + addresses: vec![ + "192.168.1.0/24".parse().unwrap(), + "fc00::0/112".parse().unwrap(), + ], ports: vec![ PortRange::new(80, 80).into(), PortRange::new(443, 443).into(), @@ -1511,10 +1312,11 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, + any_address: false, + any_port: false, + any_protocol: false, + use_manual_destination_settings: true, + ..Default::default() }; let locations = vec![location.id]; let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web @@ -1545,22 +1347,23 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let acl_rule_2 = AclRule { id: NoId, name: "DNS Access".into(), - all_networks: false, + all_locations: false, expires: None, allow_all_users: true, // Allow all users deny_all_users: false, allow_all_network_devices: false, deny_all_network_devices: false, - destination: Vec::new(), // Will use destination ranges instead + addresses: Vec::new(), // Will use destination ranges instead ports: vec![PortRange::new(53, 53).into()], protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], enabled: true, parent_id: None, state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, + any_address: false, + any_port: false, + any_protocol: false, + use_manual_destination_settings: true, + ..Default::default() }; let locations_2 = vec![location.id]; let allowed_users_2 = Vec::new(); @@ -1570,6 +1373,8 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed let denied_devices_2 = vec![network_device_3.id]; // Third network device denied let destination_ranges_2 = vec![ + ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), + ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), ]; @@ -1613,20 +1418,71 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO let generated_firewall_rules = generated_firewall_config.rules; - assert_eq!(generated_firewall_rules.len(), 4); + assert_eq!(generated_firewall_rules.len(), 8); // First ACL - Web Access ALLOW - let web_allow_rule = &generated_firewall_rules[0]; - assert_eq!(web_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); - assert_eq!(web_allow_rule.protocols, vec![i32::from(Protocol::Tcp)]); + let web_allow_rule_ipv4 = &generated_firewall_rules[0]; assert_eq!( - web_allow_rule.destination_addrs, + web_allow_rule_ipv4.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + web_allow_rule_ipv4.protocols, + vec![i32::from(Protocol::Tcp)] + ); + assert_eq!( + web_allow_rule_ipv4.destination_addrs, + vec![IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] + ); + assert_eq!( + web_allow_rule_ipv4.destination_ports, + vec![ + Port { + port: Some(PortInner::SinglePort(80)) + }, + Port { + port: Some(PortInner::SinglePort(443)) + } + ] + ); + // Source addresses should include devices of users 1,2 and network_device_1 + assert_eq!( + web_allow_rule_ipv4.source_addrs, + vec![ + 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(), + })), + }, + IpAddress { + address: Some(Address::Ip("10.0.100.1".to_string())), + }, + ] + ); + + let web_allow_rule_ipv6 = &generated_firewall_rules[1]; + assert_eq!( + web_allow_rule_ipv6.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!(web_allow_rule_ipv6.protocols, [i32::from(Protocol::Tcp)]); + assert_eq!( + web_allow_rule_ipv6.destination_addrs, [IpAddress { address: Some(Address::IpSubnet("fc00::/112".to_string())), }] ); assert_eq!( - web_allow_rule.destination_ports, + web_allow_rule_ipv6.destination_ports, [ Port { port: Some(PortInner::SinglePort(80)) @@ -1638,7 +1494,7 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO ); // Source addresses should include devices of users 1,2 and network_device_1 assert_eq!( - web_allow_rule.source_addrs, + web_allow_rule_ipv6.source_addrs, [ IpAddress { address: Some(Address::IpRange(IpRange { @@ -1659,47 +1515,170 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO ); // First ACL - Web Access DENY - let web_deny_rule = &generated_firewall_rules[2]; - assert_eq!(web_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert!(web_deny_rule.protocols.is_empty()); - assert!(web_deny_rule.destination_ports.is_empty()); - assert!(web_deny_rule.source_addrs.is_empty()); + let web_deny_rule_ipv4 = &generated_firewall_rules[4]; + assert_eq!(web_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule_ipv4.protocols.is_empty()); + assert!(web_deny_rule_ipv4.destination_ports.is_empty()); + assert!(web_deny_rule_ipv4.source_addrs.is_empty()); assert_eq!( - web_deny_rule.destination_addrs, + web_deny_rule_ipv4.destination_addrs, + [IpAddress { + address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), + }] + ); + + let web_deny_rule_ipv6 = &generated_firewall_rules[5]; + assert_eq!(web_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(web_deny_rule_ipv6.protocols.is_empty()); + assert!(web_deny_rule_ipv6.destination_ports.is_empty()); + assert!(web_deny_rule_ipv6.source_addrs.is_empty()); + assert_eq!( + web_deny_rule_ipv6.destination_addrs, [IpAddress { address: Some(Address::IpSubnet("fc00::/112".to_string())), }] ); // Second ACL - DNS Access ALLOW - let dns_allow_rule = &generated_firewall_rules[1]; - assert_eq!(dns_allow_rule.verdict, i32::from(FirewallPolicy::Allow)); + let dns_allow_rule_ipv4 = &generated_firewall_rules[2]; assert_eq!( - dns_allow_rule.protocols, + dns_allow_rule_ipv4.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + dns_allow_rule_ipv4.protocols, [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] ); assert_eq!( - dns_allow_rule.destination_ports, + dns_allow_rule_ipv4.destination_ports, [Port { port: Some(PortInner::SinglePort(53)) }] ); + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule_ipv4.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(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "10.0.100.1".to_string(), + end: "10.0.100.2".to_string(), + })), + }, + ] + ); - let expected_destination_addrs = vec![ + let expected_destination_addrs_v4 = vec![ IpAddress { - address: Some(Address::Ip("fc00::1:13".to_string())), + address: Some(Address::Ip("10.0.1.13".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("fc00::1:14/126".to_string())), + address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("fc00::1:18/125".to_string())), + address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("fc00::1:20/123".to_string())), + address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), }, IpAddress { - address: Some(Address::IpSubnet("fc00::1:40/126".to_string())), + address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), + }, + ]; + + assert_eq!( + dns_allow_rule_ipv4.destination_addrs, + expected_destination_addrs_v4 + ); + + let dns_allow_rule_ipv6 = &generated_firewall_rules[3]; + assert_eq!( + dns_allow_rule_ipv6.verdict, + i32::from(FirewallPolicy::Allow) + ); + assert_eq!( + dns_allow_rule_ipv6.protocols, + [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] + ); + assert_eq!( + dns_allow_rule_ipv6.destination_ports, + [Port { + port: Some(PortInner::SinglePort(53)) + }] + ); + // Source addresses should include network_devices 1,2 + assert_eq!( + dns_allow_rule_ipv6.source_addrs, + [ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::100:1".to_string(), + end: "ff00::100:2".to_string(), + })), + }, + ] + ); + + let expected_destination_addrs_v6 = vec![ + IpAddress { + address: Some(Address::Ip("fc00::1:13".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:14/126".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:18/125".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:20/123".to_string())), + }, + IpAddress { + address: Some(Address::IpSubnet("fc00::1:40/126".to_string())), }, IpAddress { address: Some(Address::IpSubnet("fc00::1:52/127".to_string())), @@ -1748,43 +1727,35 @@ async fn test_generate_firewall_rules_ipv6(_: PgPoolOptions, options: PgConnectO }, ]; - // Source addresses should include network_devices 1,2 assert_eq!( - dns_allow_rule.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::1:1".to_string(), - end: "ff00::1:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::2:1".to_string(), - end: "ff00::2:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::100:1".to_string(), - end: "ff00::100:2".to_string(), - })), - }, - ] + dns_allow_rule_ipv6.destination_addrs, + expected_destination_addrs_v6 ); - assert_eq!(dns_allow_rule.destination_addrs, expected_destination_addrs); // Second ACL - DNS Access DENY - let dns_deny_rule = &generated_firewall_rules[3]; - assert_eq!(dns_deny_rule.verdict, i32::from(FirewallPolicy::Deny)); - assert!(dns_deny_rule.protocols.is_empty(),); - assert!(dns_deny_rule.destination_ports.is_empty(),); - assert!(dns_deny_rule.source_addrs.is_empty(),); - assert_eq!(dns_deny_rule.destination_addrs, expected_destination_addrs); + let dns_deny_rule_ipv4 = &generated_firewall_rules[6]; + assert_eq!(dns_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule_ipv4.protocols.is_empty(),); + assert!(dns_deny_rule_ipv4.destination_ports.is_empty(),); + assert!(dns_deny_rule_ipv4.source_addrs.is_empty(),); + assert_eq!( + dns_deny_rule_ipv4.destination_addrs, + expected_destination_addrs_v4 + ); + + let dns_deny_rule_ipv6 = &generated_firewall_rules[7]; + assert_eq!(dns_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(dns_deny_rule_ipv6.protocols.is_empty(),); + assert!(dns_deny_rule_ipv6.destination_ports.is_empty(),); + assert!(dns_deny_rule_ipv6.source_addrs.is_empty(),); + assert_eq!( + dns_deny_rule_ipv6.destination_addrs, + expected_destination_addrs_v6 + ); } #[sqlx::test] -async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { +async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; let mut rng = thread_rng(); @@ -1792,1772 +1763,16 @@ async fn test_generate_firewall_rules_ipv4_and_ipv6(_: PgPoolOptions, options: P // Create test location let location = WireguardNetwork { id: NoId, - acl_enabled: false, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let mut location = location.save(&pool).await.unwrap(); - - // Setup test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - let user_3: User = rng.r#gen(); - let user_3 = user_3.save(&pool).await.unwrap(); - let user_4: User = rng.r#gen(); - let user_4 = user_4.save(&pool).await.unwrap(); - let user_5: User = rng.r#gen(); - let user_5 = user_5.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2, &user_3, &user_4, &user_5] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location.id, - wireguard_ips: vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), - IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - )), - ], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // Setup test groups - let group_1 = Group { - id: NoId, - name: "group_1".into(), - ..Default::default() - }; - let group_1 = group_1.save(&pool).await.unwrap(); - let group_2 = Group { - id: NoId, - name: "group_2".into(), - ..Default::default() - }; - let group_2 = group_2.save(&pool).await.unwrap(); - - // Assign users to groups: - // Group 1: users 1,2 - // Group 2: users 3,4 - let group_assignments = vec![ - (&group_1, vec![&user_1, &user_2]), - (&group_2, vec![&user_3, &user_4]), - ]; - - for (group, users) in group_assignments { - for user in users { - query!( - "INSERT INTO group_user (user_id, group_id) VALUES ($1, $2)", - user.id, - group.id - ) - .execute(&pool) - .await - .unwrap(); - } - } - - // Create some network devices - let network_device_1 = Device { - id: NoId, - name: "network-device-1".into(), - user_id: user_1.id, // Owned by user 1 - device_type: DeviceType::Network, - description: Some("Test network device 1".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_1 = network_device_1.save(&pool).await.unwrap(); - - let network_device_2 = Device { - id: NoId, - name: "network-device-2".into(), - user_id: user_2.id, // Owned by user 2 - device_type: DeviceType::Network, - description: Some("Test network device 2".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_2 = network_device_2.save(&pool).await.unwrap(); - - let network_device_3 = Device { - id: NoId, - name: "network-device-3".into(), - user_id: user_3.id, // Owned by user 3 - device_type: DeviceType::Network, - description: Some("Test network device 3".into()), - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let network_device_3 = network_device_3.save(&pool).await.unwrap(); - - // Add network devices to location's VPN network - let network_devices = vec![ - ( - network_device_1.id, - vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 1)), - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 1)), - ], - ), - ( - network_device_2.id, - vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 2)), - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 2)), - ], - ), - ( - network_device_3.id, - vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, 100, 3)), - IpAddr::V6(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0x0100, 3)), - ], - ), - ]; - - for (device_id, ips) in network_devices { - let network_device = WireguardNetworkDevice { - device_id, - wireguard_network_id: location.id, - wireguard_ips: ips, - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - - // Create first ACL rule - Web access - let acl_rule_1 = AclRule { - id: NoId, - name: "Web Access".into(), - all_networks: false, - expires: None, - allow_all_users: false, - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: vec![ - "192.168.1.0/24".parse().unwrap(), - "fc00::0/112".parse().unwrap(), - ], - ports: vec![ - PortRange::new(80, 80).into(), - PortRange::new(443, 443).into(), - ], - protocols: vec![Protocol::Tcp.into()], - enabled: true, - parent_id: None, - state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, - }; - let locations = vec![location.id]; - let allowed_users = vec![user_1.id, user_2.id]; // First two users can access web - let denied_users = vec![user_3.id]; // Third user explicitly denied - let allowed_groups = vec![group_1.id]; // First group allowed - let denied_groups = Vec::new(); - let allowed_devices = vec![network_device_1.id]; - let denied_devices = vec![network_device_2.id, network_device_3.id]; - let destination_ranges = Vec::new(); - let aliases = Vec::new(); - - let _acl_rule_1 = create_acl_rule( - &pool, - acl_rule_1, - locations, - allowed_users, - denied_users, - allowed_groups, - denied_groups, - allowed_devices, - denied_devices, - destination_ranges, - aliases, - ) - .await; - - // Create second ACL rule - DNS access - let acl_rule_2 = AclRule { - id: NoId, - name: "DNS Access".into(), - all_networks: false, - expires: None, - allow_all_users: true, // Allow all users - deny_all_users: false, - allow_all_network_devices: false, - deny_all_network_devices: false, - destination: Vec::new(), // Will use destination ranges instead - ports: vec![PortRange::new(53, 53).into()], - protocols: vec![Protocol::Udp.into(), Protocol::Tcp.into()], - enabled: true, - parent_id: None, - state: RuleState::Applied, - any_destination: true, - any_port: true, - any_protocol: true, - manual_settings: true, - }; - let locations_2 = vec![location.id]; - let allowed_users_2 = Vec::new(); - let denied_users_2 = vec![user_5.id]; // Fifth user denied DNS - let allowed_groups_2 = Vec::new(); - let denied_groups_2 = vec![group_2.id]; - let allowed_devices_2 = vec![network_device_1.id, network_device_2.id]; // First two network devices allowed - let denied_devices_2 = vec![network_device_3.id]; // Third network device denied - let destination_ranges_2 = vec![ - ("10.0.1.13".parse().unwrap(), "10.0.1.43".parse().unwrap()), - ("10.0.1.52".parse().unwrap(), "10.0.2.43".parse().unwrap()), - ("fc00::1:13".parse().unwrap(), "fc00::1:43".parse().unwrap()), - ("fc00::1:52".parse().unwrap(), "fc00::2:43".parse().unwrap()), - ]; - let aliases_2 = Vec::new(); - - let _acl_rule_2 = create_acl_rule( - &pool, - acl_rule_2, - locations_2, - allowed_users_2, - denied_users_2, - allowed_groups_2, - denied_groups_2, - allowed_devices_2, - denied_devices_2, - destination_ranges_2, - aliases_2, - ) - .await; - - let mut conn = pool.acquire().await.unwrap(); - - // try to generate firewall config with ACL disabled - location.acl_enabled = false; - let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap(); - assert!(generated_firewall_config.is_none()); - - // generate firewall config with default policy Allow - location.acl_enabled = true; - location.acl_default_allow = true; - let generated_firewall_config = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap(); - assert_eq!( - generated_firewall_config.default_policy, - i32::from(FirewallPolicy::Allow) - ); - - let generated_firewall_rules = generated_firewall_config.rules; - - assert_eq!(generated_firewall_rules.len(), 8); - - // First ACL - Web Access ALLOW - let web_allow_rule_ipv4 = &generated_firewall_rules[0]; - assert_eq!( - web_allow_rule_ipv4.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!( - web_allow_rule_ipv4.protocols, - vec![i32::from(Protocol::Tcp)] - ); - assert_eq!( - web_allow_rule_ipv4.destination_addrs, - vec![IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }] - ); - assert_eq!( - web_allow_rule_ipv4.destination_ports, - vec![ - Port { - port: Some(PortInner::SinglePort(80)) - }, - Port { - port: Some(PortInner::SinglePort(443)) - } - ] - ); - // Source addresses should include devices of users 1,2 and network_device_1 - assert_eq!( - web_allow_rule_ipv4.source_addrs, - vec![ - 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(), - })), - }, - IpAddress { - address: Some(Address::Ip("10.0.100.1".to_string())), - }, - ] - ); - - let web_allow_rule_ipv6 = &generated_firewall_rules[1]; - assert_eq!( - web_allow_rule_ipv6.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!(web_allow_rule_ipv6.protocols, [i32::from(Protocol::Tcp)]); - assert_eq!( - web_allow_rule_ipv6.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("fc00::/112".to_string())), - }] - ); - assert_eq!( - web_allow_rule_ipv6.destination_ports, - [ - Port { - port: Some(PortInner::SinglePort(80)) - }, - Port { - port: Some(PortInner::SinglePort(443)) - } - ] - ); - // Source addresses should include devices of users 1,2 and network_device_1 - assert_eq!( - web_allow_rule_ipv6.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::1:1".to_string(), - end: "ff00::1:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::2:1".to_string(), - end: "ff00::2:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::Ip("ff00::100:1".to_string())), - }, - ] - ); - - // First ACL - Web Access DENY - let web_deny_rule_ipv4 = &generated_firewall_rules[4]; - assert_eq!(web_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); - assert!(web_deny_rule_ipv4.protocols.is_empty()); - assert!(web_deny_rule_ipv4.destination_ports.is_empty()); - assert!(web_deny_rule_ipv4.source_addrs.is_empty()); - assert_eq!( - web_deny_rule_ipv4.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("192.168.1.0/24".to_string())), - }] - ); - - let web_deny_rule_ipv6 = &generated_firewall_rules[5]; - assert_eq!(web_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); - assert!(web_deny_rule_ipv6.protocols.is_empty()); - assert!(web_deny_rule_ipv6.destination_ports.is_empty()); - assert!(web_deny_rule_ipv6.source_addrs.is_empty()); - assert_eq!( - web_deny_rule_ipv6.destination_addrs, - [IpAddress { - address: Some(Address::IpSubnet("fc00::/112".to_string())), - }] - ); - - // Second ACL - DNS Access ALLOW - let dns_allow_rule_ipv4 = &generated_firewall_rules[2]; - assert_eq!( - dns_allow_rule_ipv4.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!( - dns_allow_rule_ipv4.protocols, - [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] - ); - assert_eq!( - dns_allow_rule_ipv4.destination_ports, - [Port { - port: Some(PortInner::SinglePort(53)) - }] - ); - // Source addresses should include network_devices 1,2 - assert_eq!( - dns_allow_rule_ipv4.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(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "10.0.100.1".to_string(), - end: "10.0.100.2".to_string(), - })), - }, - ] - ); - - let expected_destination_addrs_v4 = vec![ - IpAddress { - address: Some(Address::Ip("10.0.1.13".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.14/31".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.16/28".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.32/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.40/30".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.52/30".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.56/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.64/26".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.1.128/25".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.0/27".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.32/29".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("10.0.2.40/30".to_string())), - }, - ]; - - assert_eq!( - dns_allow_rule_ipv4.destination_addrs, - expected_destination_addrs_v4 - ); - - let dns_allow_rule_ipv6 = &generated_firewall_rules[3]; - assert_eq!( - dns_allow_rule_ipv6.verdict, - i32::from(FirewallPolicy::Allow) - ); - assert_eq!( - dns_allow_rule_ipv6.protocols, - [i32::from(Protocol::Tcp), i32::from(Protocol::Udp)] - ); - assert_eq!( - dns_allow_rule_ipv6.destination_ports, - [Port { - port: Some(PortInner::SinglePort(53)) - }] - ); - // Source addresses should include network_devices 1,2 - assert_eq!( - dns_allow_rule_ipv6.source_addrs, - [ - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::1:1".to_string(), - end: "ff00::1:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::2:1".to_string(), - end: "ff00::2:2".to_string(), - })), - }, - IpAddress { - address: Some(Address::IpRange(IpRange { - start: "ff00::100:1".to_string(), - end: "ff00::100:2".to_string(), - })), - }, - ] - ); - - let expected_destination_addrs_v6 = vec![ - IpAddress { - address: Some(Address::Ip("fc00::1:13".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:14/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:18/125".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:20/123".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:40/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:52/127".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:54/126".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:58/125".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:60/123".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:80/121".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:100/120".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:200/119".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:400/118".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:800/117".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:1000/116".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:2000/115".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:4000/114".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::1:8000/113".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::2:0/122".to_string())), - }, - IpAddress { - address: Some(Address::IpSubnet("fc00::2:40/126".to_string())), - }, - ]; - - assert_eq!( - dns_allow_rule_ipv6.destination_addrs, - expected_destination_addrs_v6 - ); - - // Second ACL - DNS Access DENY - let dns_deny_rule_ipv4 = &generated_firewall_rules[6]; - assert_eq!(dns_deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); - assert!(dns_deny_rule_ipv4.protocols.is_empty(),); - assert!(dns_deny_rule_ipv4.destination_ports.is_empty(),); - assert!(dns_deny_rule_ipv4.source_addrs.is_empty(),); - assert_eq!( - dns_deny_rule_ipv4.destination_addrs, - expected_destination_addrs_v4 - ); - - let dns_deny_rule_ipv6 = &generated_firewall_rules[7]; - assert_eq!(dns_deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); - assert!(dns_deny_rule_ipv6.protocols.is_empty(),); - assert!(dns_deny_rule_ipv6.destination_ports.is_empty(),); - assert!(dns_deny_rule_ipv6.source_addrs.is_empty(),); - assert_eq!( - dns_deny_rule_ipv6.destination_addrs, - expected_destination_addrs_v6 - ); -} - -#[sqlx::test] -async fn test_expired_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create expired ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were expired - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules not expired - acl_rule_1.expires = None; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.expires = Some(NaiveDateTime::MAX); - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_expired_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create expired ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were expired - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules not expired - acl_rule_1.expires = None; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.expires = Some(NaiveDateTime::MAX); - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_expired_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create expired ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: Some(DateTime::UNIX_EPOCH.naive_utc()), - enabled: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were expired - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules not expired - acl_rule_1.expires = None; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.expires = Some(NaiveDateTime::MAX); - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 4); -} - -#[sqlx::test] -async fn test_disabled_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create disabled ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were disabled - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules enabled - acl_rule_1.enabled = true; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.enabled = true; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_disabled_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create disabled ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were disabled - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules enabled - acl_rule_1.enabled = true; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.enabled = true; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_disabled_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create disabled ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: false, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were disabled - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules enabled - acl_rule_1.enabled = true; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.enabled = true; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 4); -} - -#[sqlx::test] -async fn test_unapplied_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create unapplied ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::New, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Modified, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were not applied - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules applied - acl_rule_1.state = RuleState::Applied; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.state = RuleState::Applied; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create unapplied ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::New, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Modified, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were not applied - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules applied - acl_rule_1.state = RuleState::Applied; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.state = RuleState::Applied; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 2); -} - -#[sqlx::test] -async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location = location.save(&pool).await.unwrap(); - - // create unapplied ACL rules - let mut acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::New, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - let mut acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Modified, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to location - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were not applied - assert_eq!(generated_firewall_rules.len(), 0); - - // make both rules applied - acl_rule_1.state = RuleState::Applied; - acl_rule_1.save(&pool).await.unwrap(); - - acl_rule_2.state = RuleState::Applied; - acl_rule_2.save(&pool).await.unwrap(); - - let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - assert_eq!(generated_firewall_rules.len(), 4); -} - -#[sqlx::test] -async fn test_acl_rules_all_locations_ipv4(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - let mut rng = thread_rng(); - - // Create test location - let location_1 = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location_1 = location_1.save(&pool).await.unwrap(); - - // Create another test location - let location_2 = WireguardNetwork { - id: NoId, - acl_enabled: true, - ..Default::default() - }; - let location_2 = location_2.save(&pool).await.unwrap(); - // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_1.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_2.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 10, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // create ACL rules - let acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Applied, - destination: vec!["192.168.1.0/24".parse().unwrap()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let _acl_rule_3 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - allow_all_users: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to locations - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location_1.id) - .save(&pool) - .await - .unwrap(); - } - for rule in [&acl_rule_2] { - AclRuleNetwork::new(rule.id, location_2.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were assigned to this location - assert_eq!(generated_firewall_rules.len(), 4); - - let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // rule with `all_networks` enabled was used for this location - assert_eq!(generated_firewall_rules.len(), 3); -} - -#[sqlx::test] -async fn test_acl_rules_all_locations_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - let mut rng = thread_rng(); - - // Create test location - let location_1 = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location_1 = location_1.save(&pool).await.unwrap(); - - // Create another test location - let location_2 = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], - ..Default::default() - }; - let location_2 = location_2.save(&pool).await.unwrap(); - - // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_1.id, - wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_2.id, - wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 10, - 10, - user.id as u16, - device_num as u16, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // create ACL rules - let acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Applied, - destination: vec!["fc00::0/112".parse().unwrap()], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let _acl_rule_3 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - allow_all_users: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to locations - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location_1.id) - .save(&pool) - .await - .unwrap(); - } - for rule in [&acl_rule_2] { - AclRuleNetwork::new(rule.id, location_2.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were assigned to this location - assert_eq!(generated_firewall_rules.len(), 4); - - let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // rule with `all_networks` enabled was used for this location - assert_eq!(generated_firewall_rules.len(), 3); -} - -#[sqlx::test] -async fn test_acl_rules_all_locations_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - let mut rng = thread_rng(); - - // Create test location - let location_1 = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location_1 = location_1.save(&pool).await.unwrap(); - - // Create another test location - let location_2 = WireguardNetwork { - id: NoId, - acl_enabled: true, - address: vec![ - IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), - IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), - ], - ..Default::default() - }; - let location_2 = location_2.save(&pool).await.unwrap(); - // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_1.id, - wireguard_ips: vec![ - IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), - IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 0, - 0, - user.id as u16, - device_num as u16, - )), - ], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location_2.id, - wireguard_ips: vec![ - IpAddr::V4(Ipv4Addr::new(10, 10, user.id as u8, device_num as u8)), - IpAddr::V6(Ipv6Addr::new( - 0xff00, - 0, - 0, - 0, - 10, - 10, - user.id as u16, - device_num as u16, - )), - ], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } - - // create ACL rules - let acl_rule_1 = AclRule { - id: NoId, - expires: None, - enabled: true, - state: RuleState::Applied, - destination: vec![ - "192.168.1.0/24".parse().unwrap(), - "fc00::0/112".parse().unwrap(), - ], - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let acl_rule_2 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - let _acl_rule_3 = AclRule { - id: NoId, - expires: None, - enabled: true, - all_networks: true, - allow_all_users: true, - state: RuleState::Applied, - ..Default::default() - } - .save(&pool) - .await - .unwrap(); - - // assign rules to locations - for rule in [&acl_rule_1, &acl_rule_2] { - AclRuleNetwork::new(rule.id, location_1.id) - .save(&pool) - .await - .unwrap(); - } - for rule in [&acl_rule_2] { - AclRuleNetwork::new(rule.id, location_2.id) - .save(&pool) - .await - .unwrap(); - } - - let mut conn = pool.acquire().await.unwrap(); - let generated_firewall_rules = try_get_location_firewall_config(&location_1, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // both rules were assigned to this location - assert_eq!(generated_firewall_rules.len(), 8); - - let generated_firewall_rules = try_get_location_firewall_config(&location_2, &mut conn) - .await - .unwrap() - .unwrap() - .rules; - - // rule with `all_networks` enabled was used for this location - assert_eq!(generated_firewall_rules.len(), 6); -} - -#[sqlx::test] -async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - - let mut rng = thread_rng(); - - // Create test location - let location = WireguardNetwork { - id: NoId, - acl_enabled: true, + acl_enabled: true, + address: vec!["10.0.0.0/16".parse().unwrap()], ..Default::default() } .save(&pool) .await - .unwrap(); - - // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{}", user.id, device_num), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } + .unwrap(); + + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; // create ACL rule let acl_rule = AclRule { @@ -3566,8 +1781,9 @@ async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { expires: None, enabled: true, state: RuleState::Applied, - destination: vec!["192.168.1.0/24".parse().unwrap()], + addresses: vec!["192.168.1.0/24".parse().unwrap()], allow_all_users: true, + use_manual_destination_settings: true, ..Default::default() } .save(&pool) @@ -3580,6 +1796,8 @@ async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { name: "destination alias".to_string(), kind: AliasKind::Destination, ports: vec![PortRange::new(100, 200).into()], + any_address: true, + any_protocol: true, ..Default::default() } .save(&pool) @@ -3588,7 +1806,7 @@ async fn test_alias_kinds(_: PgPoolOptions, options: PgConnectOptions) { let component_alias = AclAlias { id: NoId, kind: AliasKind::Component, - destination: vec!["10.0.2.3".parse().unwrap()], + addresses: vec!["10.0.2.3".parse().unwrap()], ..Default::default() } .save(&pool) @@ -3702,6 +1920,7 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt let location = WireguardNetwork { id: NoId, acl_enabled: true, + address: vec!["10.0.0.0/16".parse().unwrap()], ..Default::default() } .save(&pool) @@ -3709,43 +1928,7 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt .unwrap(); // Setup some test users and their devices - let user_1: User = rng.r#gen(); - let user_1 = user_1.save(&pool).await.unwrap(); - let user_2: User = rng.r#gen(); - let user_2 = user_2.save(&pool).await.unwrap(); - - for user in [&user_1, &user_2] { - // Create 2 devices per user - for device_num in 1..3 { - let device = Device { - id: NoId, - name: format!("device-{}-{device_num}", user.id), - user_id: user.id, - device_type: DeviceType::User, - description: None, - wireguard_pubkey: Default::default(), - created: Default::default(), - configured: true, - }; - let device = device.save(&pool).await.unwrap(); - - // Add device to location's VPN network - let network_device = WireguardNetworkDevice { - device_id: device.id, - wireguard_network_id: location.id, - wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( - 10, - 0, - user.id as u8, - device_num as u8, - ))], - preshared_key: None, - is_authorized: true, - authorized_at: None, - }; - network_device.insert(&pool).await.unwrap(); - } - } + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; // create ACL rule without manually configured destination let acl_rule = AclRule { @@ -3754,8 +1937,9 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt expires: None, enabled: true, state: RuleState::Applied, - destination: Vec::new(), + addresses: Vec::new(), allow_all_users: true, + use_manual_destination_settings: false, ..Default::default() } .save(&pool) @@ -3767,7 +1951,7 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt id: NoId, name: "postgres".to_string(), kind: AliasKind::Destination, - destination: vec!["10.0.2.3".parse().unwrap()], + addresses: vec!["10.0.2.3".parse().unwrap()], ports: vec![PortRange::new(5432, 5432).into()], ..Default::default() } @@ -3778,7 +1962,7 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt id: NoId, name: "redis".to_string(), kind: AliasKind::Destination, - destination: vec!["10.0.2.4".parse().unwrap()], + addresses: vec!["10.0.2.4".parse().unwrap()], ports: vec![PortRange::new(6379, 6379).into()], ..Default::default() } @@ -3896,3 +2080,307 @@ async fn test_destination_alias_only_acl(_: PgPoolOptions, options: PgConnectOpt Some("ACL 1 - test rule, ALIAS 2 - redis DENY".to_string()) ); } + +#[sqlx::test] +async fn test_no_allowed_users_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // create ACL rules + let acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Applied, + allow_all_users: true, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + state: RuleState::Applied, + allow_all_users: true, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // only deny rules are generated + assert_eq!(generated_firewall_rules.len(), 2); + for rule in generated_firewall_rules { + assert_eq!(rule.verdict(), FirewallPolicy::Deny); + } +} + +#[sqlx::test] +async fn test_empty_manual_destination_only_acl(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let mut rng = thread_rng(); + + // Create test locations with IPv4 and IPv6 addresses + let location_ipv4 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let location_ipv6 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let location_ipv4_and_ipv6 = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // Setup some test users and their devices + let user_1: User = rng.r#gen(); + let user_1 = user_1.save(&pool).await.unwrap(); + let user_2: User = rng.r#gen(); + let user_2 = user_2.save(&pool).await.unwrap(); + + for user in [&user_1, &user_2] { + // Create 2 devices per user + for device_num in 1..3 { + let device = Device { + id: NoId, + name: format!("device-{}-{device_num}", user.id), + user_id: user.id, + device_type: DeviceType::User, + description: None, + wireguard_pubkey: Default::default(), + created: Default::default(), + configured: true, + }; + let device = device.save(&pool).await.unwrap(); + + // Add device to all locations' VPN networks + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_ipv4.id, + wireguard_ips: vec![IpAddr::V4(Ipv4Addr::new( + 10, + 0, + user.id as u8, + device_num as u8, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_ipv6.id, + wireguard_ips: vec![IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + ))], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + let network_device = WireguardNetworkDevice { + device_id: device.id, + wireguard_network_id: location_ipv4_and_ipv6.id, + wireguard_ips: vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, user.id as u8, device_num as u8)), + IpAddr::V6(Ipv6Addr::new( + 0xff00, + 0, + 0, + 0, + 0, + 0, + user.id as u16, + device_num as u16, + )), + ], + preshared_key: None, + is_authorized: true, + authorized_at: None, + }; + network_device.insert(&pool).await.unwrap(); + } + } + + // create ACL rule without manually configured destination and no aliases + let acl_rule = AclRule { + id: NoId, + name: "test rule".to_string(), + expires: None, + enabled: true, + state: RuleState::Applied, + addresses: Vec::new(), + allow_all_users: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rule to all locations + for location in [&location_ipv4, &location_ipv6, &location_ipv4_and_ipv6] { + AclRuleNetwork::new(acl_rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + + // check generated rules for IPv4 only location + let generated_firewall_rules_ipv4 = try_get_location_firewall_config(&location_ipv4, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + assert_eq!(generated_firewall_rules_ipv4.len(), 2); + let expected_source_addrs_ipv4 = vec![ + 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(), + })), + }, + ]; + let allow_rule_ipv4 = &generated_firewall_rules_ipv4[0]; + assert_eq!(allow_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!(allow_rule_ipv4.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv4.source_addrs, expected_source_addrs_ipv4); + assert!(allow_rule_ipv4.destination_addrs.is_empty()); + + let deny_rule_ipv4 = &generated_firewall_rules_ipv4[1]; + assert_eq!(deny_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!(deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule_ipv4.source_addrs.is_empty()); + assert!(deny_rule_ipv4.destination_addrs.is_empty()); + + // check generated rules for IPv6 only location + let generated_firewall_rules_ipv6 = try_get_location_firewall_config(&location_ipv6, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + assert_eq!(generated_firewall_rules_ipv6.len(), 2); + let expected_source_addrs_ipv6 = vec![ + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::1:1".to_string(), + end: "ff00::1:2".to_string(), + })), + }, + IpAddress { + address: Some(Address::IpRange(IpRange { + start: "ff00::2:1".to_string(), + end: "ff00::2:2".to_string(), + })), + }, + ]; + let allow_rule_ipv6 = &generated_firewall_rules_ipv6[0]; + assert_eq!(allow_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!(allow_rule_ipv6.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv6.source_addrs, expected_source_addrs_ipv6); + assert!(allow_rule_ipv6.destination_addrs.is_empty()); + + let deny_rule_ipv6 = &generated_firewall_rules_ipv6[1]; + assert_eq!(deny_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!(deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule_ipv6.source_addrs.is_empty()); + assert!(deny_rule_ipv6.destination_addrs.is_empty()); + + // check generated rules for IPv4 and IPv6 location + let generated_firewall_rules_ipv4_and_ipv6 = + try_get_location_firewall_config(&location_ipv4_and_ipv6, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + assert_eq!(generated_firewall_rules_ipv4_and_ipv6.len(), 4); + let allow_rule_ipv4 = &generated_firewall_rules_ipv4_and_ipv6[0]; + assert_eq!(allow_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!(allow_rule_ipv4.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv4.source_addrs, expected_source_addrs_ipv4); + assert!(allow_rule_ipv4.destination_addrs.is_empty()); + + let allow_rule_ipv6 = &generated_firewall_rules_ipv4_and_ipv6[1]; + assert_eq!(allow_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!(allow_rule_ipv6.verdict, i32::from(FirewallPolicy::Allow)); + assert_eq!(allow_rule_ipv6.source_addrs, expected_source_addrs_ipv6); + assert!(allow_rule_ipv6.destination_addrs.is_empty()); + + let deny_rule_ipv4 = &generated_firewall_rules_ipv4_and_ipv6[2]; + assert_eq!(deny_rule_ipv4.ip_version, i32::from(IpVersion::Ipv4)); + assert_eq!(deny_rule_ipv4.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule_ipv4.source_addrs.is_empty()); + assert!(deny_rule_ipv4.destination_addrs.is_empty()); + + let deny_rule_ipv6 = &generated_firewall_rules_ipv4_and_ipv6[3]; + assert_eq!(deny_rule_ipv6.ip_version, i32::from(IpVersion::Ipv6)); + assert_eq!(deny_rule_ipv6.verdict, i32::from(FirewallPolicy::Deny)); + assert!(deny_rule_ipv6.source_addrs.is_empty()); + assert!(deny_rule_ipv6.destination_addrs.is_empty()); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/source.rs b/crates/defguard_core/src/enterprise/firewall/tests/source.rs new file mode 100644 index 0000000000..063e18b370 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/source.rs @@ -0,0 +1,158 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_proto::enterprise::firewall::{IpAddress, IpVersion, ip_address::Address}; +use rand::thread_rng; + +use crate::enterprise::firewall::{ + get_source_addrs, get_source_network_devices, get_source_users, + tests::{random_network_device_with_id, random_user_with_id}, +}; + +#[test] +fn test_get_relevant_users() { + let mut rng = thread_rng(); + + // prepare allowed and denied users lists with shared elements + let user_1 = random_user_with_id(&mut rng, 1); + let user_2 = random_user_with_id(&mut rng, 2); + let user_3 = random_user_with_id(&mut rng, 3); + let user_4 = random_user_with_id(&mut rng, 4); + let user_5 = random_user_with_id(&mut rng, 5); + let allowed_users = vec![user_1.clone(), user_2.clone(), user_4.clone()]; + let denied_users = vec![user_3.clone(), user_4, user_5.clone()]; + + let users = get_source_users(allowed_users, &denied_users); + assert_eq!(users, vec![user_1, user_2]); +} + +#[test] +fn test_get_relevant_network_devices() { + let mut rng = thread_rng(); + + // prepare allowed and denied network devices lists with shared elements + let device_1 = random_network_device_with_id(&mut rng, 1); + let device_2 = random_network_device_with_id(&mut rng, 2); + let device_3 = random_network_device_with_id(&mut rng, 3); + let device_4 = random_network_device_with_id(&mut rng, 4); + let device_5 = random_network_device_with_id(&mut rng, 5); + let allowed_devices = vec![ + device_1.clone(), + device_3.clone(), + device_4.clone(), + device_5.clone(), + ]; + let denied_devices = vec![device_2.clone(), device_4, device_5.clone()]; + + let devices = get_source_network_devices(allowed_devices, &denied_devices); + assert_eq!(devices, vec![device_1, device_3]); +} + +#[test] +fn test_process_source_addrs_v4() { + // Test data with mixed IPv4 and IPv6 addresses + let user_device_ips = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 2)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 5)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), // Should be filtered out + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), + ]; + + let network_device_ips = vec![ + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 3)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 4)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), // Should be filtered out + IpAddr::V4(Ipv4Addr::new(172, 16, 1, 1)), + ]; + + let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv4); + + // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges + assert_eq!( + source_addrs, + [ + IpAddress { + address: Some(Address::Ip("10.0.1.1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.2/31".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("10.0.1.4/31".to_string())) + }, + IpAddress { + address: Some(Address::Ip("172.16.1.1".to_string())), + }, + IpAddress { + address: Some(Address::Ip("192.168.1.100".to_string())), + }, + ] + ); + + // Test with empty input + let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv4); + assert!(empty_addrs.is_empty()); + + // Test with only IPv6 addresses - should return empty result for IPv4 + let ipv6_only = get_source_addrs( + vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))], + vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2))], + IpVersion::Ipv4, + ); + assert!(ipv6_only.is_empty()); +} + +#[test] +fn test_process_source_addrs_v6() { + // Test data with mixed IPv4 and IPv6 addresses + let user_device_ips = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 5)), + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), // Should be filtered out + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, 1)), + ]; + + let network_device_ips = vec![ + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 3)), + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 4)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), // Should be filtered out + IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 2, 0, 0, 0, 1)), + ]; + + let source_addrs = get_source_addrs(user_device_ips, network_device_ips, IpVersion::Ipv6); + + // Should merge consecutive IPs into ranges and keep separate non-consecutive ranges + assert_eq!( + source_addrs, + [ + IpAddress { + address: Some(Address::Ip("2001:db8::1".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8::2/127".to_string())) + }, + IpAddress { + address: Some(Address::IpSubnet("2001:db8::4/127".to_string())) + }, + IpAddress { + address: Some(Address::Ip("2001:db8:0:1::1".to_string())), + }, + IpAddress { + address: Some(Address::Ip("2001:db8:0:2::1".to_string())), + }, + ] + ); + + // Test with empty input + let empty_addrs = get_source_addrs(Vec::new(), Vec::new(), IpVersion::Ipv6); + assert!(empty_addrs.is_empty()); + + // Test with only IPv4 addresses - should return empty result for IPv6 + let ipv4_only = get_source_addrs( + vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))], + vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))], + IpVersion::Ipv6, + ); + assert!(ipv4_only.is_empty()); +} diff --git a/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs b/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs new file mode 100644 index 0000000000..630e8025a6 --- /dev/null +++ b/crates/defguard_core/src/enterprise/firewall/tests/unapplied_rules.rs @@ -0,0 +1,242 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use defguard_common::db::{NoId, models::WireguardNetwork, setup_pool}; +use ipnetwork::IpNetwork; +use rand::thread_rng; +use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; + +use crate::enterprise::{ + db::models::acl::{AclRule, AclRuleNetwork, RuleState}, + firewall::{tests::create_test_users_and_devices, try_get_location_firewall_config}, +}; + +#[sqlx::test] +async fn test_unapplied_acl_rules_ipv4(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + allow_all_users: true, + state: RuleState::New, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + allow_all_users: true, + state: RuleState::Modified, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); +} + +#[sqlx::test] +async fn test_unapplied_acl_rules_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + allow_all_users: true, + state: RuleState::New, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + allow_all_users: true, + state: RuleState::Modified, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 4); +} + +#[sqlx::test] +async fn test_unapplied_acl_rules_ipv4_and_ipv6(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let mut rng = thread_rng(); + + // Create test location + let location = WireguardNetwork { + id: NoId, + acl_enabled: true, + address: vec![ + IpNetwork::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap(), + IpNetwork::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap(), + ], + ..Default::default() + }; + let location = location.save(&pool).await.unwrap(); + + // Setup some test users and their devices + create_test_users_and_devices(&mut rng, &pool, vec![&location]).await; + + // create unapplied ACL rules + let mut acl_rule_1 = AclRule { + id: NoId, + expires: None, + enabled: true, + allow_all_users: true, + state: RuleState::New, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + let mut acl_rule_2 = AclRule { + id: NoId, + expires: None, + enabled: true, + allow_all_users: true, + state: RuleState::Modified, + use_manual_destination_settings: true, + ..Default::default() + } + .save(&pool) + .await + .unwrap(); + + // assign rules to location + for rule in [&acl_rule_1, &acl_rule_2] { + AclRuleNetwork::new(rule.id, location.id) + .save(&pool) + .await + .unwrap(); + } + + let mut conn = pool.acquire().await.unwrap(); + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + + // both rules were not applied + assert_eq!(generated_firewall_rules.len(), 0); + + // make both rules applied + acl_rule_1.state = RuleState::Applied; + acl_rule_1.save(&pool).await.unwrap(); + + acl_rule_2.state = RuleState::Applied; + acl_rule_2.save(&pool).await.unwrap(); + + let generated_firewall_rules = try_get_location_firewall_config(&location, &mut conn) + .await + .unwrap() + .unwrap() + .rules; + assert_eq!(generated_firewall_rules.len(), 8); +} diff --git a/crates/defguard_core/src/enterprise/handlers/acl.rs b/crates/defguard_core/src/enterprise/handlers/acl.rs index c13d36a71f..b1ad7243c3 100644 --- a/crates/defguard_core/src/enterprise/handlers/acl.rs +++ b/crates/defguard_core/src/enterprise/handlers/acl.rs @@ -28,59 +28,68 @@ pub struct ApiAclRule { pub parent_id: Option, pub state: RuleState, pub name: String, - pub all_networks: bool, - pub networks: Vec, + pub all_locations: bool, + pub locations: Vec, pub expires: Option, pub enabled: bool, // source pub allow_all_users: bool, pub deny_all_users: bool, + pub allow_all_groups: bool, + pub deny_all_groups: bool, pub allow_all_network_devices: bool, pub deny_all_network_devices: bool, pub allowed_users: Vec, pub denied_users: Vec, pub allowed_groups: Vec, pub denied_groups: Vec, - pub allowed_devices: Vec, - pub denied_devices: Vec, + pub allowed_network_devices: Vec, + pub denied_network_devices: Vec, // destination - pub destination: String, - pub aliases: Vec, + pub use_manual_destination_settings: bool, + pub addresses: String, pub ports: String, pub protocols: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, + // aliases + pub aliases: Vec, + pub destinations: Vec, } impl From> for ApiAclRule { fn from(info: AclRuleInfo) -> Self { Self { - destination: info.format_destination(), + addresses: info.format_destination(), ports: info.format_ports(), id: info.id, parent_id: info.parent_id, state: info.state, name: info.name, - all_networks: info.all_networks, - networks: info.networks.iter().map(|v| v.id).collect(), + all_locations: info.all_locations, + locations: info.locations.iter().map(|v| v.id).collect(), expires: info.expires, allow_all_users: info.allow_all_users, deny_all_users: info.deny_all_users, + allow_all_groups: info.allow_all_groups, + deny_all_groups: info.deny_all_groups, allow_all_network_devices: info.allow_all_network_devices, deny_all_network_devices: info.deny_all_network_devices, allowed_users: info.allowed_users.iter().map(|v| v.id).collect(), denied_users: info.denied_users.iter().map(|v| v.id).collect(), allowed_groups: info.allowed_groups.iter().map(|v| v.id).collect(), denied_groups: info.denied_groups.iter().map(|v| v.id).collect(), - allowed_devices: info.allowed_devices.iter().map(|v| v.id).collect(), - denied_devices: info.denied_devices.iter().map(|v| v.id).collect(), + allowed_network_devices: info.allowed_network_devices.iter().map(|v| v.id).collect(), + denied_network_devices: info.denied_network_devices.iter().map(|v| v.id).collect(), aliases: info.aliases.iter().map(|v| v.id).collect(), + destinations: info.destinations.iter().map(|v| v.id).collect(), protocols: info.protocols, enabled: info.enabled, - any_destination: info.any_destination, + any_address: info.any_address, any_port: info.any_port, any_protocol: info.any_protocol, + use_manual_destination_settings: info.use_manual_destination_settings, } } } @@ -89,39 +98,46 @@ impl From> for ApiAclRule { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, ToSchema)] pub struct EditAclRule { pub name: String, - pub all_networks: bool, - pub networks: Vec, + pub all_locations: bool, + pub locations: Vec, pub expires: Option, pub enabled: bool, // source pub allow_all_users: bool, pub deny_all_users: bool, + pub allow_all_groups: bool, + pub deny_all_groups: bool, pub allow_all_network_devices: bool, pub deny_all_network_devices: bool, pub allowed_users: Vec, pub denied_users: Vec, pub allowed_groups: Vec, pub denied_groups: Vec, - pub allowed_devices: Vec, - pub denied_devices: Vec, + pub allowed_network_devices: Vec, + pub denied_network_devices: Vec, // destination - pub destination: String, - pub aliases: Vec, + pub use_manual_destination_settings: bool, + pub addresses: String, pub ports: String, pub protocols: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, + // aliases & destinations + pub aliases: Vec, + pub destinations: Vec, } impl EditAclRule { pub fn validate(&self) -> Result<(), WebError> { + // FIXME: validate that destination is defined // check if some allowed users/group/devices are configured - if !(self.allow_all_users - || self.allow_all_network_devices - || !self.allowed_users.is_empty() - || !self.allowed_groups.is_empty() - || !self.allowed_devices.is_empty()) + if !self.allow_all_users + && !self.allow_all_groups + && !self.allow_all_network_devices + && self.allowed_users.is_empty() + && self.allowed_groups.is_empty() + && self.allowed_network_devices.is_empty() { return Err(WebError::BadRequest( "Must provide some allowed users, groups or devices".to_string(), @@ -135,28 +151,32 @@ impl EditAclRule { impl From> for EditAclRule { fn from(info: AclRuleInfo) -> Self { Self { - destination: info.format_destination(), + addresses: info.format_destination(), ports: info.format_ports(), name: info.name, - all_networks: info.all_networks, - networks: info.networks.iter().map(|v| v.id).collect(), + all_locations: info.all_locations, + locations: info.locations.iter().map(|v| v.id).collect(), expires: info.expires, allow_all_users: info.allow_all_users, deny_all_users: info.deny_all_users, + allow_all_groups: info.allow_all_groups, + deny_all_groups: info.deny_all_groups, allow_all_network_devices: info.allow_all_network_devices, deny_all_network_devices: info.deny_all_network_devices, allowed_users: info.allowed_users.iter().map(|v| v.id).collect(), denied_users: info.denied_users.iter().map(|v| v.id).collect(), allowed_groups: info.allowed_groups.iter().map(|v| v.id).collect(), denied_groups: info.denied_groups.iter().map(|v| v.id).collect(), - allowed_devices: info.allowed_devices.iter().map(|v| v.id).collect(), - denied_devices: info.denied_devices.iter().map(|v| v.id).collect(), + allowed_network_devices: info.allowed_network_devices.iter().map(|v| v.id).collect(), + denied_network_devices: info.denied_network_devices.iter().map(|v| v.id).collect(), aliases: info.aliases.iter().map(|v| v.id).collect(), + destinations: info.destinations.iter().map(|v| v.id).collect(), protocols: info.protocols, enabled: info.enabled, - any_destination: info.any_destination, + any_address: info.any_address, any_port: info.any_port, any_protocol: info.any_protocol, + use_manual_destination_settings: info.use_manual_destination_settings, } } } diff --git a/crates/defguard_core/src/enterprise/handlers/acl/alias.rs b/crates/defguard_core/src/enterprise/handlers/acl/alias.rs index d0cbb64f5f..401300b1ae 100644 --- a/crates/defguard_core/src/enterprise/handlers/acl/alias.rs +++ b/crates/defguard_core/src/enterprise/handlers/acl/alias.rs @@ -14,7 +14,7 @@ use crate::{ auth::{AdminRole, SessionInfo}, enterprise::db::models::acl::{ AclAlias, AclAliasDestinationRange, AclAliasInfo, AclError, AliasKind, AliasState, - Protocol, acl_delete_related_objects, parse_destination, + Protocol, acl_delete_related_objects, parse_destination_addresses, }, handlers::{ApiResponse, ApiResult}, }; @@ -23,7 +23,7 @@ use crate::{ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, ToSchema)] pub struct EditAclAlias { pub name: String, - pub destination: String, + pub addresses: String, pub ports: String, pub protocols: Vec, } @@ -37,7 +37,7 @@ impl EditAclAlias { ) -> Result<(), AclError> { debug!("Creating related objects for ACL alias {self:?}"); // save related destination ranges - let destination = parse_destination(&self.destination)?; + let destination = parse_destination_addresses(&self.addresses)?; for range in destination.ranges { let obj = AclAliasDestinationRange { id: NoId, @@ -63,7 +63,7 @@ pub struct ApiAclAlias { pub name: String, pub kind: AliasKind, pub state: AliasState, - pub destination: String, + pub addresses: String, pub ports: String, pub protocols: Vec, pub rules: Vec, @@ -164,7 +164,7 @@ impl ApiAclAlias { impl From for ApiAclAlias { fn from(info: AclAliasInfo) -> Self { Self { - destination: info.format_destination(), + addresses: info.format_destination(), ports: info.format_ports(), id: info.id, parent_id: info.parent_id, diff --git a/crates/defguard_core/src/enterprise/handlers/acl/destination.rs b/crates/defguard_core/src/enterprise/handlers/acl/destination.rs index 6986346cd5..3a770a109c 100644 --- a/crates/defguard_core/src/enterprise/handlers/acl/destination.rs +++ b/crates/defguard_core/src/enterprise/handlers/acl/destination.rs @@ -14,7 +14,7 @@ use crate::{ auth::{AdminRole, SessionInfo}, enterprise::db::models::acl::{ AclAlias, AclAliasDestinationRange, AclAliasInfo, AclError, AliasKind, AliasState, - Protocol, acl_delete_related_objects, parse_destination, + Protocol, acl_delete_related_objects, parse_destination_addresses, }, handlers::{ApiResponse, ApiResult}, }; @@ -23,10 +23,10 @@ use crate::{ #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, ToSchema)] pub(crate) struct EditAclDestination { pub name: String, - pub destination: String, + pub addresses: String, pub ports: String, pub protocols: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, } @@ -40,7 +40,7 @@ impl EditAclDestination { ) -> Result<(), AclError> { debug!("Creating related objects for ACL alias {self:?}"); // save related destination ranges - let destination = parse_destination(&self.destination)?; + let destination = parse_destination_addresses(&self.addresses)?; for range in destination.ranges { let obj = AclAliasDestinationRange { id: NoId, @@ -66,11 +66,11 @@ pub(crate) struct ApiAclDestination { pub name: String, pub kind: AliasKind, pub state: AliasState, - pub destination: String, + pub addresses: String, pub ports: String, pub protocols: Vec, pub rules: Vec, - pub any_destination: bool, + pub any_address: bool, pub any_port: bool, pub any_protocol: bool, } @@ -170,7 +170,7 @@ impl ApiAclDestination { impl From for ApiAclDestination { fn from(info: AclAliasInfo) -> Self { Self { - destination: info.format_destination(), + addresses: info.format_destination(), ports: info.format_ports(), id: info.id, parent_id: info.parent_id, @@ -179,7 +179,7 @@ impl From for ApiAclDestination { state: info.state, protocols: info.protocols, rules: info.rules.iter().map(|v| v.id).collect(), - any_destination: info.any_destination, + any_address: info.any_address, any_port: info.any_port, any_protocol: info.any_protocol, } diff --git a/crates/defguard_core/src/utility_thread.rs b/crates/defguard_core/src/utility_thread.rs index 123cb619b6..c4d1f416d2 100644 --- a/crates/defguard_core/src/utility_thread.rs +++ b/crates/defguard_core/src/utility_thread.rs @@ -240,9 +240,9 @@ async fn expired_acl_rules_check( "UPDATE aclrule SET state = 'expired'::aclrule_state \ WHERE state = 'applied'::aclrule_state AND expires < NOW() \ RETURNING id, parent_id, state AS \"state: RuleState\", name, allow_all_users, \ - deny_all_users, allow_all_network_devices, deny_all_network_devices, all_networks, \ - destination, ports, protocols, enabled, expires, any_destination, any_port, \ - any_protocol, manual_settings" + deny_all_users, allow_all_groups, deny_all_groups, allow_all_network_devices, deny_all_network_devices, all_locations, \ + addresses, ports, protocols, enabled, expires, any_address, any_port, \ + any_protocol, use_manual_destination_settings" ) .fetch_all(pool) .await?; diff --git a/crates/defguard_core/tests/integration/api/acl.rs b/crates/defguard_core/tests/integration/api/acl.rs index 93740547da..db3a0f5b1c 100644 --- a/crates/defguard_core/tests/integration/api/acl.rs +++ b/crates/defguard_core/tests/integration/api/acl.rs @@ -50,27 +50,31 @@ async fn make_client_v2(pool: PgPool, config: DefGuardConfig) -> TestClient { fn make_rule() -> EditAclRule { EditAclRule { name: "rule".to_string(), - all_networks: false, - networks: Vec::new(), + all_locations: false, + locations: Vec::new(), expires: None, allow_all_users: false, deny_all_users: false, + allow_all_groups: false, + deny_all_groups: false, allow_all_network_devices: false, deny_all_network_devices: false, allowed_users: vec![1], denied_users: Vec::new(), allowed_groups: Vec::new(), denied_groups: Vec::new(), - allowed_devices: Vec::new(), - denied_devices: Vec::new(), - destination: "10.2.2.2, 10.0.0.1/24, 10.0.10.1-10.0.20.1".to_string(), + allowed_network_devices: Vec::new(), + denied_network_devices: Vec::new(), + addresses: "10.2.2.2, 10.0.0.1/24, 10.0.10.1-10.0.20.1".to_string(), aliases: Vec::new(), + destinations: Vec::new(), enabled: true, protocols: vec![6, 17], ports: "1, 2, 3, 10-20, 30-40".to_string(), - any_destination: true, - any_port: true, - any_protocol: true, + any_address: false, + any_port: false, + any_protocol: false, + use_manual_destination_settings: true, } } @@ -84,7 +88,7 @@ async fn set_rule_state(pool: &PgPool, id: Id, state: RuleState, parent_id: Opti fn make_alias() -> EditAclAlias { EditAclAlias { name: "alias".to_string(), - destination: "10.2.2.2, 10.0.0.1/24, 10.0.10.1-10.0.20.1".to_string(), + addresses: "10.2.2.2, 10.0.0.1/24, 10.0.10.1-10.0.20.1".to_string(), protocols: vec![6, 17], ports: "1, 2, 3, 10-20, 30-40".to_string(), } @@ -101,27 +105,31 @@ fn edit_rule_data_into_api_response( parent_id, state, name: data.name.clone(), - all_networks: data.all_networks, - networks: data.networks.clone(), + all_locations: data.all_locations, + locations: data.locations.clone(), expires: data.expires, enabled: data.enabled, allow_all_users: data.allow_all_users, deny_all_users: data.deny_all_users, + allow_all_groups: data.allow_all_groups, + deny_all_groups: data.deny_all_groups, allow_all_network_devices: data.allow_all_network_devices, deny_all_network_devices: data.deny_all_network_devices, allowed_users: data.allowed_users.clone(), denied_users: data.denied_users.clone(), allowed_groups: data.allowed_groups.clone(), denied_groups: data.denied_groups.clone(), - allowed_devices: data.allowed_devices.clone(), - denied_devices: data.denied_devices.clone(), - destination: data.destination.clone(), + allowed_network_devices: data.allowed_network_devices.clone(), + denied_network_devices: data.denied_network_devices.clone(), + addresses: data.addresses.clone(), aliases: data.aliases.clone(), + destinations: data.destinations.clone(), ports: data.ports.clone(), protocols: data.protocols.clone(), - any_destination: data.any_destination, + any_address: data.any_address, any_port: data.any_port, any_protocol: data.any_protocol, + use_manual_destination_settings: data.use_manual_destination_settings, } } @@ -139,7 +147,7 @@ fn edit_alias_data_into_api_response( state, name: data.name, kind, - destination: data.destination, + addresses: data.addresses, ports: data.ports, protocols: data.protocols, rules, @@ -344,7 +352,7 @@ async fn test_empty_strings(_: PgPoolOptions, options: PgConnectOptions) { // rule let mut rule = make_rule(); - rule.destination = String::new(); + rule.addresses = String::new(); rule.ports = String::new(); let response = client.post("/api/v1/acl/rule").json(&rule).send().await; @@ -360,7 +368,7 @@ async fn test_empty_strings(_: PgPoolOptions, options: PgConnectOptions) { // alias let mut alias = make_alias(); - alias.destination = String::new(); + alias.addresses = String::new(); alias.ports = String::new(); let response = client.post("/api/v1/acl/alias").json(&alias).send().await; assert_eq!(response.status(), StatusCode::CREATED); @@ -521,10 +529,10 @@ async fn test_related_objects(_: PgPoolOptions, options: PgConnectOptions) { // create an acl rule with related objects let mut rule = make_rule(); - rule.networks = vec![1, 2]; + rule.locations = vec![1, 2]; rule.allowed_users = vec![1, 2]; rule.allowed_groups = vec![1, 2]; - rule.allowed_devices = vec![1, 2]; + rule.allowed_network_devices = vec![1, 2]; rule.aliases = vec![1, 2]; // create @@ -574,7 +582,7 @@ async fn test_invalid_related_objects(_: PgPoolOptions, options: PgConnectOption // networks let mut rule = make_rule(); - rule.networks = vec![100]; + rule.locations = vec![100]; let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); let response = client.put("/api/v1/acl/rule/1").json(&rule).send().await; @@ -622,7 +630,7 @@ async fn test_invalid_related_objects(_: PgPoolOptions, options: PgConnectOption // allowed_devices let mut rule = make_rule(); - rule.allowed_devices = vec![100]; + rule.allowed_network_devices = vec![100]; let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); let response = client.put("/api/v1/acl/rule/1").json(&rule).send().await; @@ -630,7 +638,7 @@ async fn test_invalid_related_objects(_: PgPoolOptions, options: PgConnectOption // denied_devices let mut rule = make_rule(); - rule.denied_devices = vec![100]; + rule.denied_network_devices = vec![100]; let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); let response = client.put("/api/v1/acl/rule/1").json(&rule).send().await; @@ -690,11 +698,11 @@ async fn test_invalid_data(_: PgPoolOptions, options: PgConnectOptions) { // invalid ip range let mut rule = make_rule(); - rule.destination = "10.10.10.20-10.10.10.10".into(); + rule.addresses = "10.10.10.20-10.10.10.10".into(); let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); - rule.destination = "10.10.10.10-10.10.10.20".into(); + rule.addresses = "10.10.10.10-10.10.10.20".into(); let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::CREATED); } @@ -804,7 +812,7 @@ async fn test_rule_delete_state_applied(_: PgPoolOptions, options: PgConnectOpti // test APPLIED rule deletion let mut rule = make_rule(); - rule.networks = vec![1]; + rule.locations = vec![1]; let response = client.post("/api/v1/acl/rule").json(&rule).send().await; assert_eq!(response.status(), StatusCode::CREATED); assert_eq!(AclRule::all(&pool).await.unwrap().len(), 1); @@ -825,7 +833,7 @@ async fn test_rule_delete_state_applied(_: PgPoolOptions, options: PgConnectOpti assert_eq!(rule_after_mods, rule_child); // related networks are returned correctly - assert_eq!(rule_child.networks, vec![1]); + assert_eq!(rule_child.locations, vec![1]); // cannot modify a DELETED rule let response = client diff --git a/flake.lock b/flake.lock index 49cfdf332e..ec96501451 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770019141, - "narHash": "sha256-VKS4ZLNx4PNrABoB0L8KUpc1fE7CLpQXQs985tGfaCU=", + "lastModified": 1770197578, + "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cb369ef2efd432b3cdf8622b0ffc0a97a02f3137", + "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1770088046, - "narHash": "sha256-4hfYDnUTvL1qSSZEA4CEThxfz+KlwSFQ30Z9jgDguO0=", + "lastModified": 1770347142, + "narHash": "sha256-uz+ZSqXpXEPtdRPYwvgsum/CfNq7AUQ/0gZHqTigiPM=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "71f9daa4e05e49c434d08627e755495ae222bc34", + "rev": "2859683cd9ef7858d324c5399b0d8d6652bf4044", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index abaeea210b..a364211049 100644 --- a/flake.nix +++ b/flake.nix @@ -55,6 +55,8 @@ playwright # release assets verification cosign + # vulnerability scanner + trivy ]; # Specify the rust-src path (many editors rely on this) diff --git a/migrations/20260127094513_[2.0.0]_aclalias_any.down.sql b/migrations/20260127094513_[2.0.0]_aclalias_any.down.sql index cbcfad0bf5..fd8d3a00ac 100644 --- a/migrations/20260127094513_[2.0.0]_aclalias_any.down.sql +++ b/migrations/20260127094513_[2.0.0]_aclalias_any.down.sql @@ -1,9 +1,15 @@ ALTER TABLE aclrule - DROP COLUMN any_destination, + DROP COLUMN any_address, DROP COLUMN any_port, DROP COLUMN any_protocol, - DROP COLUMN manual_settings; + DROP COLUMN use_manual_destination_settings, + DROP COLUMN allow_all_groups, + DROP COLUMN deny_all_groups; +ALTER TABLE aclrule RENAME COLUMN addresses TO destination; +ALTER TABLE aclrule RENAME COLUMN all_locations TO all_networks; + ALTER TABLE aclalias - DROP COLUMN any_destination, + DROP COLUMN any_address, DROP COLUMN any_port, DROP COLUMN any_protocol; +ALTER TABLE aclalias RENAME COLUMN addresses TO destination; diff --git a/migrations/20260127094513_[2.0.0]_aclalias_any.up.sql b/migrations/20260127094513_[2.0.0]_aclalias_any.up.sql index 52083d52ef..a652c451b6 100644 --- a/migrations/20260127094513_[2.0.0]_aclalias_any.up.sql +++ b/migrations/20260127094513_[2.0.0]_aclalias_any.up.sql @@ -1,17 +1,31 @@ +-- add new toggle columns ALTER TABLE aclalias - ADD COLUMN any_destination boolean NOT NULL DEFAULT false, + ADD COLUMN any_address boolean NOT NULL DEFAULT false, ADD COLUMN any_port boolean NOT NULL DEFAULT false, ADD COLUMN any_protocol boolean NOT NULL DEFAULT false; + +-- set values for new columns based on existing data UPDATE aclalias SET - any_destination = array_length(destination, 1) IS NULL, + any_address = array_length(destination, 1) IS NULL, any_port = array_length(ports, 1) IS NULL, any_protocol = array_length(protocols, 1) IS NULL; + +-- rename destination column to avoid confusion +ALTER TABLE aclalias RENAME COLUMN destination TO addresses; + +-- do the same for the aclrule table itself ALTER TABLE aclrule - ADD COLUMN any_destination boolean NOT NULL DEFAULT false, + ADD COLUMN any_address boolean NOT NULL DEFAULT false, ADD COLUMN any_port boolean NOT NULL DEFAULT false, ADD COLUMN any_protocol boolean NOT NULL DEFAULT false, - ADD COLUMN manual_settings boolean NOT NULL DEFAULT false; + ADD COLUMN use_manual_destination_settings boolean NOT NULL DEFAULT true, + ADD COLUMN allow_all_groups boolean NOT NULL DEFAULT false, + ADD COLUMN deny_all_groups boolean NOT NULL DEFAULT false; + UPDATE aclrule SET - any_destination = array_length(destination, 1) IS NULL, + any_address = array_length(destination, 1) IS NULL, any_port = array_length(ports, 1) IS NULL, any_protocol = array_length(protocols, 1) IS NULL; + +ALTER TABLE aclrule RENAME COLUMN destination TO addresses; +ALTER TABLE aclrule RENAME COLUMN all_networks TO all_locations; diff --git a/web/src/pages/AliasesPage/AliasTable.tsx b/web/src/pages/AliasesPage/AliasTable.tsx index ab4b97da89..179a4228bf 100644 --- a/web/src/pages/AliasesPage/AliasTable.tsx +++ b/web/src/pages/AliasesPage/AliasTable.tsx @@ -64,7 +64,7 @@ export const AliasTable = ({ data: rowData }: Props) => { ), }), - columnHelper.accessor('destination', { + columnHelper.accessor('addresses', { header: 'IP4/6 CIDR range address', enableSorting: false, size: 430, diff --git a/web/src/pages/CEAliasPage/CEAliasPage.tsx b/web/src/pages/CEAliasPage/CEAliasPage.tsx index 9174c774aa..e131d36225 100644 --- a/web/src/pages/CEAliasPage/CEAliasPage.tsx +++ b/web/src/pages/CEAliasPage/CEAliasPage.tsx @@ -74,7 +74,7 @@ export const CEAliasPage = ({ alias }: Props) => { const formSchema = z.object({ name: z.string(m.form_error_required()).trim().min(1, m.form_error_required()), ports: aclPortsValidator, - destination: aclDestinationValidator, + addresses: aclDestinationValidator, protocols: z.set(z.enum(AclProtocol)), }); @@ -83,7 +83,7 @@ type FormFields = z.infer; const anyComponentDefined = (fields: FormFields): boolean => { return ( fields.ports.trim().length > 0 || - fields.destination.trim().length > 0 || + fields.addresses.trim().length > 0 || fields.protocols.size > 0 ); }; @@ -95,14 +95,14 @@ const FormContent = ({ alias }: { alias?: AclAlias }) => { if (isPresent(alias)) { return { name: alias.name, - destination: alias.destination, + addresses: alias.addresses, ports: alias.ports, protocols: new Set(alias.protocols), }; } return { name: '', - destination: '', + addresses: '', ports: '', protocols: new Set(), }; @@ -165,7 +165,7 @@ const FormContent = ({ alias }: { alias?: AclAlias }) => {

{`Define the IP addresses or ranges that form the destination of this ACL rule.`}

- + {(field) => ( { - if (!values.any_destination && values.destination.trim().length === 0) { + if (!values.any_address && values.addresses.trim().length === 0) { ctx.addIssue({ code: 'custom', continue: true, - path: ['destination'], + path: ['addresses'], message: m.form_error_required(), }); } @@ -107,12 +108,7 @@ export const CEDestinationPage = ({ destination }: Props) => { const defaultValues = useMemo((): FormFields => { if (isPresent(destination)) { return { - name: destination.name, - any_destination: true, - any_port: true, - any_protocol: true, - destination: destination.destination, - ports: destination.ports, + ...omit(destination, ['id', 'state', 'rules']), protocols: new Set(destination.protocols), }; } @@ -120,10 +116,10 @@ export const CEDestinationPage = ({ destination }: Props) => { return { name: '', ports: '', - any_destination: true, + any_address: true, any_port: true, any_protocol: true, - destination: '', + addresses: '', protocols: new Set(), }; }, [destination]); @@ -191,14 +187,14 @@ export const CEDestinationPage = ({ destination }: Props) => {

{`Define the IP addresses or ranges that form the destination of this ACL rule.`}

- + {(field) => } - !s.values.any_destination}> + !s.values.any_address}> {(open) => ( - + {(field) => ( AclProtocolName[protocol]) @@ -253,39 +252,42 @@ const Content = ({ rule: initialRule }: Props) => { }, [networkDevices]); const [_restrictionsPresent, _setRestrictionsPresent] = useState(false); - const [manualDestination, setManualDestination] = useState(false); + // const [manualDestination, setManualDestination] = useState(false); const formSchema = useMemo( () => z .object({ name: z.string(m.form_error_required()).min(1, m.form_error_required()), - networks: z.number().array(), + locations: z.number().array(), expires: z.string().nullable(), enabled: z.boolean(), - all_networks: z.boolean(), + all_locations: z.boolean(), allow_all_users: z.boolean(), deny_all_users: z.boolean(), + allow_all_groups: z.boolean(), + deny_all_groups: z.boolean(), allow_all_network_devices: z.boolean(), deny_all_network_devices: z.boolean(), allowed_users: z.number().array(), denied_users: z.number().array(), - allow_all_groups: z.boolean(), allowed_groups: z.number().array(), denied_groups: z.number().array(), - allowed_devices: z.number().array(), - denied_devices: z.number().array(), - destinations: z.set(z.number()), - aliases: z.set(z.number()), + allowed_network_devices: z.number().array(), + denied_network_devices: z.number().array(), + addresses: aclDestinationValidator, + ports: aclPortsValidator, protocols: z.set(z.number()), - any_protocol: z.boolean(), + any_address: z.boolean(), any_port: z.boolean(), - any_destination: z.boolean(), - destination: aclDestinationValidator, - ports: aclPortsValidator, + any_protocol: z.boolean(), + destinations: z.set(z.number()), + aliases: z.set(z.number()), + use_manual_destination_settings: z.boolean(), }) .superRefine((vals, ctx) => { // check for collisions + // FIXME: add handling for all_groups toggles const message = 'Allow Deny conflict error placeholder.'; if (!vals.allow_all_users && !vals.deny_all_users) { if (intersection(vals.allowed_users, vals.denied_users).length) { @@ -314,7 +316,10 @@ const Content = ({ rule: initialRule }: Props) => { } } if (!vals.allow_all_network_devices && !vals.deny_all_network_devices) { - if (intersection(vals.allowed_devices, vals.denied_devices).length) { + if ( + intersection(vals.allowed_network_devices, vals.denied_network_devices) + .length + ) { ctx.addIssue({ path: ['allowed_devices'], code: 'custom', @@ -331,10 +336,11 @@ const Content = ({ rule: initialRule }: Props) => { // check if one of allowed users/groups/devices fields is set const isAllowConfigured = vals.allow_all_users || + vals.allow_all_groups || vals.allow_all_network_devices || vals.allowed_users.length !== 0 || vals.allowed_groups.length !== 0 || - vals.allowed_devices.length !== 0; + vals.allowed_network_devices.length !== 0; if (!isAllowConfigured) { const message = 'Must configure some allowed users, groups or devices'; ctx.addIssue({ @@ -363,9 +369,8 @@ const Content = ({ rule: initialRule }: Props) => { if (isPresent(initialRule)) { return { ...omit(initialRule, ['id', 'state', 'expires', 'parent_id']), - allow_all_groups: true, aliases: new Set(initialRule.aliases), - destinations: new Set(), + destinations: new Set(initialRule.destinations), protocols: new Set(initialRule.protocols), expires: null, }; @@ -373,29 +378,31 @@ const Content = ({ rule: initialRule }: Props) => { return { name: '', - destination: '', + addresses: '', ports: '', aliases: new Set(), destinations: new Set(), - allowed_devices: [], + allowed_network_devices: [], allowed_groups: [], allowed_users: [], - denied_devices: [], + denied_network_devices: [], denied_groups: [], denied_users: [], - networks: [], + locations: [], protocols: new Set(), - all_networks: true, - allow_all_groups: true, + all_locations: true, allow_all_users: true, + allow_all_groups: true, allow_all_network_devices: true, deny_all_users: false, + deny_all_groups: false, deny_all_network_devices: false, enabled: true, expires: null, - any_destination: true, + any_address: true, any_port: true, any_protocol: true, + use_manual_destination_settings: false, }; }, [initialRule]); @@ -411,36 +418,24 @@ const Content = ({ rule: initialRule }: Props) => { // FIXME: When restrictions section is reworked toSend.deny_all_network_devices = false; toSend.deny_all_users = false; - toSend.denied_devices = []; + toSend.deny_all_groups = false; + toSend.denied_network_devices = []; toSend.denied_groups = []; toSend.denied_users = []; - if (toSend.any_destination) { - toSend.destination = ''; - } - if (toSend.any_port) { - toSend.ports = ''; - } - if (toSend.any_protocol) { - toSend.protocols = new Set(); - } if (isPresent(initialRule)) { await editRule({ ...toSend, - any_destination: true, - any_port: true, - any_protocol: true, protocols: Array.from(toSend.protocols), aliases: Array.from(toSend.aliases), + destinations: Array.from(toSend.destinations), id: initialRule.id, }); } else { await addRule({ ...toSend, - any_destination: true, - any_port: true, - any_protocol: true, protocols: Array.from(toSend.protocols), aliases: Array.from(toSend.aliases), + destinations: Array.from(toSend.destinations), }); } }, @@ -471,9 +466,9 @@ const Content = ({ rule: initialRule }: Props) => {

{`Specify which locations this rule applies to. You can select all available locations or choose specific ones based on your requirements.`}

- s.values.all_networks}> + s.values.all_locations}> {(allValue) => ( - + {(field) => ( { selectionCustomItemRender={renderLocationSelectionItem} toggleValue={allValue} onToggleChange={(value) => { - form.setFieldValue('all_networks', value); + form.setFieldValue('all_locations', value); }} /> )} @@ -544,7 +539,7 @@ const Content = ({ rule: initialRule }: Props) => { AclProtocolName[p]) @@ -569,160 +564,162 @@ const Content = ({ rule: initialRule }: Props) => {

{`Manually configure destinations parameters for this rule.`}

- { - setManualDestination((s) => !s); - }} - /> - - - - {isPresent(aliasesOptions) && aliasesOptions.length === 0 && ( -
-
- -
-

{`You don't have any aliases to use yet — create them in the “Aliases” section to create reusable elements for defining destinations in multiple firewall ACL rules.`}

-
- )} - {isPresent(aliasesOptions) && aliasesOptions.length > 0 && ( - <> - -

{`Aliases can optionally define some or all of the manual destination settings. They are combined with the values you specify to form the final destination for firewall rule generation.`}

-
- - - {(field) => ( - <> - -