diff --git a/.sqlx/query-a9612a0e2437298c9e43b09621273f529d85b1e7d271f4fd4b1600c87a85e3b9.json b/.sqlx/query-a9612a0e2437298c9e43b09621273f529d85b1e7d271f4fd4b1600c87a85e3b9.json new file mode 100644 index 000000000..88d4ea59a --- /dev/null +++ b/.sqlx/query-a9612a0e2437298c9e43b09621273f529d85b1e7d271f4fd4b1600c87a85e3b9.json @@ -0,0 +1,152 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, mtu, fwmark, allowed_ips, allow_all_groups, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM wireguard_network WHERE id IN (SELECT wireguard_network_id FROM wireguard_network_device WHERE device_id = $1) AND location_mfa_mode = 'disabled'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "address", + "type_info": "InetArray" + }, + { + "ordinal": 3, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "pubkey", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "prvkey", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "endpoint", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "dns", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "mtu", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "fwmark", + "type_info": "Int8" + }, + { + "ordinal": 10, + "name": "allowed_ips", + "type_info": "InetArray" + }, + { + "ordinal": 11, + "name": "allow_all_groups", + "type_info": "Bool" + }, + { + "ordinal": 12, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 13, + "name": "keepalive_interval", + "type_info": "Int4" + }, + { + "ordinal": 14, + "name": "peer_disconnect_threshold", + "type_info": "Int4" + }, + { + "ordinal": 15, + "name": "acl_enabled", + "type_info": "Bool" + }, + { + "ordinal": 16, + "name": "acl_default_allow", + "type_info": "Bool" + }, + { + "ordinal": 17, + "name": "location_mfa_mode: LocationMfaMode", + "type_info": { + "Custom": { + "name": "location_mfa_mode", + "kind": { + "Enum": [ + "disabled", + "internal", + "external" + ] + } + } + } + }, + { + "ordinal": 18, + "name": "service_location_mode: ServiceLocationMode", + "type_info": { + "Custom": { + "name": "service_location_mode", + "kind": { + "Enum": [ + "disabled", + "prelogon", + "alwayson" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "a9612a0e2437298c9e43b09621273f529d85b1e7d271f4fd4b1600c87a85e3b9" +} diff --git a/Cargo.lock b/Cargo.lock index c2b7dfe6c..45f115b15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,9 +387,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -397,9 +397,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -833,9 +833,9 @@ checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18" [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -855,9 +855,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -963,7 +963,7 @@ dependencies = [ "base64 0.22.1", "hkdf", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "sha2", "subtle", "time", @@ -1331,7 +1331,7 @@ dependencies = [ "matches", "model_derive", "openidconnect", - "rand 0.8.5", + "rand 0.8.6", "reqwest", "rsa", "secrecy", @@ -1391,7 +1391,7 @@ dependencies = [ "paste", "pgp", "prost", - "rand 0.8.5", + "rand 0.8.6", "regex", "reqwest", "rsa", @@ -1501,7 +1501,7 @@ dependencies = [ "clap", "defguard_common", "defguard_core", - "rand 0.8.5", + "rand 0.8.6", "sqlx", "tokio", "tracing", @@ -1580,7 +1580,7 @@ dependencies = [ "jsonwebkey", "jsonwebtoken", "openidconnect", - "rand 0.8.5", + "rand 0.8.6", "reqwest", "rsa", "semver", @@ -3194,7 +3194,7 @@ dependencies = [ "p256", "p384", "pem", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "serde_json", @@ -3702,7 +3702,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "serde", "smallvec", "zeroize", @@ -3807,7 +3807,7 @@ dependencies = [ "chrono", "getrandom 0.2.17", "http", - "rand 0.8.5", + "rand 0.8.6", "reqwest", "serde", "serde_json", @@ -4041,7 +4041,7 @@ dependencies = [ "oauth2", "p256", "p384", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "serde-value", @@ -4374,7 +4374,7 @@ dependencies = [ "p256", "p384", "p521", - "rand 0.8.5", + "rand 0.8.6", "regex", "replace_with", "ripemd", @@ -4418,7 +4418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -4712,9 +4712,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "qoi" @@ -4830,9 +4830,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -5917,7 +5917,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "sha1", @@ -5958,7 +5958,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", "sha2", @@ -6229,7 +6229,7 @@ dependencies = [ "percent-encoding", "pest", "pest_derive", - "rand 0.8.5", + "rand 0.8.6", "regex", "serde", "serde_json", @@ -6369,9 +6369,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -6894,9 +6894,9 @@ checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -7257,9 +7257,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] diff --git a/crates/defguard_common/src/db/models/wireguard.rs b/crates/defguard_common/src/db/models/wireguard.rs index d6291bd0d..7099bd871 100644 --- a/crates/defguard_common/src/db/models/wireguard.rs +++ b/crates/defguard_common/src/db/models/wireguard.rs @@ -347,6 +347,33 @@ impl WireguardNetwork { .await } + /// Gets all non-MFA locations a user device is allowed to access. + /// Locations requiring MFA (Internal/External) are excluded — their connections + /// are managed by the defguard client and cannot be represented as a static config. + pub async fn find_user_device_networks<'e, E>( + executor: E, + device_id: Id, + ) -> sqlx::Result> + where + E: PgExecutor<'e>, + { + query_as!( + Self, + "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, mtu, fwmark, \ + allowed_ips, allow_all_groups, connected_at, keepalive_interval, \ + peer_disconnect_threshold, acl_enabled, acl_default_allow, \ + location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ + service_location_mode \"service_location_mode: ServiceLocationMode\" \ + FROM wireguard_network WHERE id IN \ + (SELECT wireguard_network_id FROM wireguard_network_device \ + WHERE device_id = $1) \ + AND location_mfa_mode = 'disabled'", + device_id + ) + .fetch_all(executor) + .await + } + /// Find all for a given rule `Id`. pub async fn all_for_rule<'e, E>(executor: E, rule_id: Id) -> sqlx::Result> where diff --git a/crates/defguard_core/src/handlers/network_devices.rs b/crates/defguard_core/src/handlers/network_devices.rs index 970e2d427..78a826664 100644 --- a/crates/defguard_core/src/handlers/network_devices.rs +++ b/crates/defguard_core/src/handlers/network_devices.rs @@ -116,10 +116,10 @@ impl NetworkDeviceInfo { } #[derive(Serialize)] -struct DeviceWireGuardConfig { - network_id: Id, - network_name: String, - config: String, +pub(crate) struct DeviceWireGuardConfig { + pub(crate) network_id: Id, + pub(crate) network_name: String, + pub(crate) config: String, } /// For a given device, retrieve all WireGuard configuations for all networks. diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index 584cdf44f..c21a43ebc 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -36,7 +36,7 @@ use crate::{ }, events::{ApiEvent, ApiEventType, ApiRequestContext}, grpc::GatewayEvent, - handlers::gateway::GatewayInfo, + handlers::{gateway::GatewayInfo, network_devices::DeviceWireGuardConfig}, location_management::{ allowed_peers::get_location_allowed_peers, handle_imported_devices, handle_mapped_devices, sync_location_allowed_devices, @@ -1355,3 +1355,48 @@ pub(crate) async fn download_config( ))) } } + +/// For a given user device, retrieve WireGuard configurations for all allowed locations. +/// +/// GET /device/{device_id}/config +pub(crate) async fn user_device_configs( + session: SessionInfo, + State(appstate): State, + Path(device_id): Path, +) -> ApiResult { + debug!("Creating WireGuard configs for user device {device_id}."); + + let settings = EnterpriseSettings::get(&appstate.pool).await?; + if settings.only_client_activation && !session.is_admin { + warn!( + "User {} tried to download device config, but manual device management is disabled", + session.user.username + ); + return Err(WebError::Forbidden("Manual device management is disabled")); + } + + let device = device_for_admin_or_self(&appstate.pool, &session, device_id).await?; + let locations = WireguardNetwork::find_user_device_networks(&appstate.pool, device_id).await?; + + let mut result = Vec::new(); + for location in locations { + let location_device = WireguardNetworkDevice::find(&appstate.pool, device_id, location.id) + .await? + .ok_or(WebError::ObjectNotFound(format!( + "No IP address found for device: {}({})", + device.name, device.id + )))?; + debug!( + "Created WireGuard config for user device {device_id} in location {}.", + location.name + ); + let config = Device::create_config(&location, &location_device); + result.push(DeviceWireGuardConfig { + network_id: location.id, + network_name: location.name, + config, + }); + } + + Ok(ApiResponse::json(result, StatusCode::OK)) +} diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index ad0997577..28e21b088 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -165,7 +165,7 @@ use crate::{ add_device, add_user_devices, count_networks, create_network, delete_device, delete_network, download_config, gateway_status, get_device, import_network, list_devices, list_networks, list_user_devices, modify_device, modify_network, - network_details, + network_details, user_device_configs, }, worker::{create_job, create_worker_token, job_status, list_workers, remove_worker}, }, @@ -511,6 +511,7 @@ pub fn build_webapp( "/device/{device_id}", put(modify_device).get(get_device).delete(delete_device), ) + .route("/device/{device_id}/config", get(user_device_configs)) .route("/device", get(list_devices)) .route("/device/user/{username}", get(list_user_devices)) .route( diff --git a/crates/defguard_core/tests/integration/api/wireguard.rs b/crates/defguard_core/tests/integration/api/wireguard.rs index 5612a2395..178dbb425 100644 --- a/crates/defguard_core/tests/integration/api/wireguard.rs +++ b/crates/defguard_core/tests/integration/api/wireguard.rs @@ -1042,3 +1042,282 @@ async fn test_network_size_validation(_: PgPoolOptions, options: PgConnectOption .await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); } + +#[derive(serde::Deserialize)] +struct DeviceWireGuardConfig { + network_id: Id, + network_name: String, + config: String, +} + +/// A user allowed in a single location returns exactly one device config. +#[sqlx::test] +async fn test_user_device_configs_single_network(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (client, _) = make_test_client(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + let network: WireguardNetwork = make_network(&client, "network").await.json().await; + + let device_payload = json!({ + "name": "device", + "wireguard_pubkey": "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=", + }); + let response = client + .post("/api/v1/device/admin") + .json(&device_payload) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let device: serde_json::Value = response.json().await; + let device_id = device["device"]["id"].as_i64().unwrap(); + + let response = client + .get(format!("/api/v1/device/{device_id}/config")) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + let configs: Vec = response.json().await; + + assert_eq!(configs.len(), 1); + assert_eq!(configs[0].network_id, network.id); + assert_eq!(configs[0].network_name, network.name); + assert!(configs[0].config.contains("[Interface]")); + assert!(configs[0].config.contains("[Peer]")); +} + +/// A user allowed in multiple networks returns a device config entry for each location. +#[sqlx::test] +async fn test_user_device_configs_multiple_networks(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (client, _) = make_test_client(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + let location1: WireguardNetwork = make_network(&client, "location-1").await.json().await; + let location2: WireguardNetwork = make_network(&client, "location-2").await.json().await; + + // Both locations use allow_all_groups=true (make_network default), so the device + // will be allowed in both when created. + let device_payload = json!({ + "name": "device", + "wireguard_pubkey": "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=", + }); + let response = client + .post("/api/v1/device/admin") + .json(&device_payload) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let device: serde_json::Value = response.json().await; + let device_id = device["device"]["id"].as_i64().unwrap(); + + let response = client + .get(format!("/api/v1/device/{device_id}/config")) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + let configs: Vec = response.json().await; + + assert_eq!(configs.len(), 2, "expected configs for both locations"); + let ids: Vec = configs.iter().map(|c| c.network_id).collect(); + assert!(ids.contains(&location1.id), "config for location-1 missing"); + assert!(ids.contains(&location2.id), "config for location-2 missing"); + for cfg in &configs { + assert!(cfg.config.contains("[Interface]")); + assert!(cfg.config.contains("[Peer]")); + } +} + +/// A non-admin user can fetch configs for their own device but not for another user's device. +#[sqlx::test] +async fn test_user_device_configs_auth(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (client, _) = make_test_client(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create a location that allows all users (not just admin group) + client + .post("/api/v1/network") + .json(&json!({ + "name": "network", + "address": "10.1.1.1/24", + "port": 55555, + "endpoint": "192.168.4.14", + "allowed_ips": "10.1.1.0/24", + "dns": "1.1.1.1", + "mtu": 1420, + "fwmark": 0, + "allowed_groups": [], + "allow_all_groups": true, + "keepalive_interval": 25, + "peer_disconnect_threshold": 300, + "acl_enabled": false, + "acl_default_allow": false, + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" + })) + .send() + .await; + + // Create a device for hpotter (non-admin user) + let device_payload = json!({ + "name": "hpotter-device", + "wireguard_pubkey": "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=", + }); + let response = client + .post("/api/v1/device/hpotter") + .json(&device_payload) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let hpotter_device: serde_json::Value = response.json().await; + let hpotter_device_id = hpotter_device["device"]["id"].as_i64().unwrap(); + + // Create a device for admin + let device_payload = json!({ + "name": "admin-device", + "wireguard_pubkey": "sIhx53MsX+iLk83sssybHrD7M+5m+CmpLzWL/zo8C38=", + }); + let response = client + .post("/api/v1/device/admin") + .json(&device_payload) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let admin_device: serde_json::Value = response.json().await; + let admin_device_id = admin_device["device"]["id"].as_i64().unwrap(); + + // Switch to hpotter + let auth = Auth::new("hpotter", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // hpotter can fetch their own device config + let response = client + .get(format!("/api/v1/device/{hpotter_device_id}/config")) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + let configs: Vec = response.json().await; + assert_eq!(configs.len(), 1); + + // hpotter cannot fetch admin's device config + let response = client + .get(format!("/api/v1/device/{admin_device_id}/config")) + .send() + .await; + assert_eq!( + response.status(), + StatusCode::NOT_FOUND, + "non-admin user should not access another user's device config" + ); +} + +/// MFA locations (internal/external) must be excluded from the user device config endpoint. +/// A user should only receive configs for regular (non-MFA) locations since MFA location +/// connections are possible only with the Defguard client apps, not standard WireGuard clients. +#[sqlx::test] +async fn test_user_device_configs_excludes_mfa_locations( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + let (client, _) = make_test_client(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create a normal location (allow_all_groups so the device is allowed) + let normal_location: WireguardNetwork = client + .post("/api/v1/network") + .json(&json!({ + "name": "normal-location", + "address": "10.1.1.1/24", + "port": 55555, + "endpoint": "192.168.4.14", + "allowed_ips": "10.1.1.0/24", + "dns": "1.1.1.1", + "mtu": 1420, + "fwmark": 0, + "allowed_groups": [], + "allow_all_groups": true, + "keepalive_interval": 25, + "peer_disconnect_threshold": 300, + "acl_enabled": false, + "acl_default_allow": false, + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" + })) + .send() + .await + .json() + .await; + + // Create an MFA location (internal mode, no enterprise license required). + let mfa_location: WireguardNetwork = client + .post("/api/v1/network") + .json(&json!({ + "name": "mfa-location", + "address": "10.1.2.1/24", + "port": 55556, + "endpoint": "192.168.4.15", + "allowed_ips": "10.1.2.0/24", + "dns": "1.1.1.1", + "mtu": 1420, + "fwmark": 0, + "allowed_groups": [], + "allow_all_groups": true, + "keepalive_interval": 25, + "peer_disconnect_threshold": 300, + "acl_enabled": false, + "acl_default_allow": false, + "location_mfa_mode": "internal", + "service_location_mode": "disabled" + })) + .send() + .await + .json() + .await; + + // Create a user device + let device_payload = json!({ + "name": "device", + "wireguard_pubkey": "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=", + }); + let response = client + .post("/api/v1/device/admin") + .json(&device_payload) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let device: serde_json::Value = response.json().await; + let device_id = device["device"]["id"].as_i64().unwrap(); + + let response = client + .get(format!("/api/v1/device/{device_id}/config")) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + let configs: Vec = response.json().await; + + // Only the normal location config should be returned + assert_eq!(configs.len(), 1, "MFA location should be excluded"); + assert_eq!( + configs[0].network_id, normal_location.id, + "config should belong to the normal location" + ); + assert_ne!( + configs[0].network_id, mfa_location.id, + "MFA location config must not be returned" + ); +} diff --git a/flake.lock b/flake.lock index d2092c130..4a5a6b951 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1775423009, - "narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=", + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1775617983, - "narHash": "sha256-2NWGA/I4j/qlx6qbg86QvJiK1/GyH9gnf0hFiARWVwE=", + "lastModified": 1776395632, + "narHash": "sha256-Mi1uF5f2FsdBIvy+v7MtsqxD3Xjhd0ARJdwoqqqPtJo=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "d98b91b1feae7ef07fa2ccb3aa3f83f11abfae54", + "rev": "8087ff1f47fff983a1fba70fa88b759f2fd8ae97", "type": "github" }, "original": { diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index e244e1025..1390ecfb3 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -1719,8 +1719,8 @@ packages: easy-file-picker@1.2.0: resolution: {integrity: sha512-GJxOW5s+g/pBr8Ha86a768yx0UZ6fYw+iAOrxK5HOzQ8q9hZxEJF0C8ztdAsH0mcze58FSpzv/d9flRCAuUKHg==} - electron-to-chromium@1.5.339: - resolution: {integrity: sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==} + electron-to-chromium@1.5.340: + resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2064,8 +2064,8 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} - isbot@5.1.38: - resolution: {integrity: sha512-Cus2702JamTNMEY4zTP+TShgq/3qzjvGcBC4XMOV45BLaxD4iUFENkqu7ZhFeSzwNsCSZLjnGlihDQznnpnEEA==} + isbot@5.1.39: + resolution: {integrity: sha512-obH0yYahGXdzNxo+djmHhBYThUKDkz565cxkIlt2L9hXfv1NlaLKoDBHo6KxXsYrIXx2RK3x5vY36CfZcobxEw==} engines: {node: '>=18'} isexe@2.0.0: @@ -3924,7 +3924,7 @@ snapshots: '@tanstack/history': 1.161.6 '@tanstack/react-store': 0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/router-core': 1.168.15 - isbot: 5.1.38 + isbot: 5.1.39 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -4244,7 +4244,7 @@ snapshots: dependencies: baseline-browser-mapping: 2.10.19 caniuse-lite: 1.0.30001788 - electron-to-chromium: 1.5.339 + electron-to-chromium: 1.5.340 node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) @@ -4423,7 +4423,7 @@ snapshots: easy-file-picker@1.2.0: {} - electron-to-chromium@1.5.339: {} + electron-to-chromium@1.5.340: {} emoji-regex@8.0.0: {} @@ -4783,7 +4783,7 @@ snapshots: is-plain-object@5.0.0: {} - isbot@5.1.38: {} + isbot@5.1.39: {} isexe@2.0.0: {} diff --git a/web/src/shared/api/api.ts b/web/src/shared/api/api.ts index bffb021f5..f445185d3 100644 --- a/web/src/shared/api/api.ts +++ b/web/src/shared/api/api.ts @@ -413,7 +413,7 @@ const api = { getDevices: () => client.get('/device'), getDeviceConfigs: async (device: Device): Promise => { const { data: configs } = await client.get( - `/device/network/${device.id}/config`, + `/device/${device.id}/config`, ); return { configs,