Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions crates/defguard_core/src/enterprise/handlers/device_posture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1163,12 +1163,6 @@ pub async fn set_postures_for_location(
.await?
.ok_or_else(|| WebError::ObjectNotFound(format!("Location {location_id} not found")))?;

if location.is_service_location() && !data.postures.is_empty() {
return Err(WebError::BadRequest(
"Posture checks cannot be assigned to service locations".to_owned(),
));
}

let mut tx = appstate.pool.begin().await?;
let old_postures = DevicePostureLocation::find_by_location(&mut *tx, location_id).await?;
let result =
Expand Down Expand Up @@ -1234,16 +1228,6 @@ pub async fn set_locations_for_posture(
WebError::ObjectNotFound(format!("Device posture check {posture_id} not found"))
})?;

for location_id in &data.locations {
if let Some(location) = WireguardNetwork::find_by_id(&appstate.pool, *location_id).await?
&& location.is_service_location()
{
return Err(WebError::BadRequest(
"Posture checks cannot be assigned to service locations".to_owned(),
));
}
}

let mut tx = appstate.pool.begin().await?;
let old_locations = DevicePostureLocation::find_by_posture(&mut *tx, posture_id).await?;
let result =
Expand Down
505 changes: 435 additions & 70 deletions crates/defguard_core/src/grpc/proxy/client_mfa.rs

Large diffs are not rendered by default.

50 changes: 35 additions & 15 deletions crates/defguard_core/src/handlers/wireguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,32 @@ impl WireguardNetworkData {
Ok(())
}

/// Rejects service-location mode combined with location MFA: core cannot serve it and the
/// client cannot represent it (`Location::is_service_location()` requires MFA disabled).
pub(crate) fn validate_service_location_mfa(&self) -> Result<(), WebError> {
if self.service_location_mode == ServiceLocationMode::Disabled
|| self.location_mfa_mode == LocationMfaMode::Disabled
{
return Ok(());
}

Err(WebError::BadRequest(
"Service location mode cannot be combined with location MFA".into(),
))
}

/// Rejects a zero (or negative) keepalive interval to prevent idle service locations
/// from disconnecting.
pub(crate) fn validate_keepalive_interval(&self) -> Result<(), WebError> {
if self.keepalive_interval >= 1 {
return Ok(());
}

Err(WebError::BadRequest(
"keepalive_interval must be at least 1".into(),
))
}

pub(crate) fn validate_allowed_groups(&self) -> Result<(), WebError> {
if self.allow_all_groups || !self.allowed_groups.is_empty() {
return Ok(());
Expand Down Expand Up @@ -233,6 +259,8 @@ pub(crate) async fn create_network(

data.validate_peer_disconnect_threshold()?;
data.validate_location_mfa_mode(&appstate.pool).await?;
data.validate_service_location_mfa()?;
data.validate_keepalive_interval()?;
data.validate_allowed_groups()?;

let allowed_ips = data.parse_allowed_ips();
Expand Down Expand Up @@ -362,6 +390,8 @@ pub(crate) async fn modify_network(

data.validate_peer_disconnect_threshold()?;
data.validate_location_mfa_mode(&appstate.pool).await?;
data.validate_service_location_mfa()?;
data.validate_keepalive_interval()?;
data.validate_allowed_groups()?;

let network = find_network(network_id, &appstate.pool).await?;
Expand All @@ -385,35 +415,25 @@ pub(crate) async fn modify_network(
network.acl_enabled = data.acl_enabled;
network.acl_default_allow = data.acl_default_allow;
network.allowed_ips_from_acl = data.allowed_ips_from_acl;
network.service_location_mode = if data.location_mfa_mode == LocationMfaMode::Disabled {
data.service_location_mode
} else {
warn!(
"Disabling service location mode for location {} because location MFA is enabled",
network.name
);
ServiceLocationMode::Disabled
};
network.service_location_mode = data.service_location_mode;
network.location_mfa_mode = data.location_mfa_mode;

network.save(&mut *transaction).await?;
network
.set_allowed_groups(&mut transaction, &data.allowed_groups)
.await?;

let _events = sync_location_allowed_devices(&network, &mut transaction, None).await?;

let peers = get_location_allowed_peers(&network, &mut transaction).await?;
let maybe_firewall_config =
try_get_location_firewall_config(&network, &mut transaction).await?;
appstate.send_gateway_command(GatewayCommand::NetworkModified(
network.id,
network.clone(),
peers,
maybe_firewall_config,
));
let gateway_command =
GatewayCommand::NetworkModified(network.id, network.clone(), peers, maybe_firewall_config);

// commit DB transaction
transaction.commit().await?;
appstate.send_gateway_command(gateway_command);

info!(
"User {} updated WireGuard network {network_id}",
Expand Down
58 changes: 44 additions & 14 deletions crates/defguard_core/tests/integration/api/device_posture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,7 @@ async fn make_service_location(client: &TestClient, name: &str) -> i64 {
}

#[sqlx::test]
async fn test_set_postures_for_service_location_rejected(
async fn test_set_postures_for_service_location_allowed(
_: PgPoolOptions,
options: PgConnectOptions,
) {
Expand All @@ -1319,24 +1319,32 @@ async fn test_set_postures_for_service_location_rejected(
.await;
client.drain_all_events();

// assigning posture checks to a service location is rejected
// assigning posture checks to a service location is allowed
let response = client
.put(format!("/api/v1/network/{service_location_id}/postures"))
.json(&AssignPosturesData {
postures: vec![posture.id],
})
.send()
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
client.assert_event_queue_is_empty();
assert_eq!(response.status(), StatusCode::OK);
let result: Vec<i64> = response.json().await;
assert_eq!(result, vec![posture.id]);

let events = client.drain_all_events();
assert_eq!(events.len(), 1);
assert!(matches!(
events[0].0,
ApiEventType::LocationPosturesAssigned { .. }
));

// nothing was assigned to the posture
// the assignment is visible on the posture
let response = client
.get(format!("/api/v1/device-posture/{}", posture.id))
.send()
.await;
let fetched: ApiDevicePosture = response.json().await;
assert!(fetched.locations.is_empty());
assert_eq!(fetched.locations, vec![service_location_id]);

// clearing (empty list) is still allowed on a service location
let response = client
Expand All @@ -1347,10 +1355,18 @@ async fn test_set_postures_for_service_location_rejected(
.send()
.await;
assert_eq!(response.status(), StatusCode::OK);
client.drain_all_events();

let response = client
.get(format!("/api/v1/device-posture/{}", posture.id))
.send()
.await;
let fetched: ApiDevicePosture = response.json().await;
assert!(fetched.locations.is_empty());
}

#[sqlx::test]
async fn test_set_locations_for_posture_rejects_service_location(
async fn test_set_locations_for_posture_allows_service_location(
_: PgPoolOptions,
options: PgConnectOptions,
) {
Expand All @@ -1369,34 +1385,48 @@ async fn test_set_locations_for_posture_rejects_service_location(
.await;
client.drain_all_events();

// assigning a service location to a posture is rejected
// assigning a service location to a posture is allowed
let response = client
.put(format!("/api/v1/device-posture/{}/locations", posture.id))
.json(&AssignLocationsData {
locations: vec![service_location_id],
})
.send()
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
client.assert_event_queue_is_empty();
assert_eq!(response.status(), StatusCode::OK);
let result: Vec<i64> = response.json().await;
assert_eq!(result, vec![service_location_id]);

// a mix containing a service location is rejected too — nothing is assigned
let events = client.drain_all_events();
assert_eq!(events.len(), 1);
assert!(matches!(
events[0].0,
ApiEventType::DevicePostureLocationsAssigned { .. }
));

// a mix containing a service location is accepted too
let response = client
.put(format!("/api/v1/device-posture/{}/locations", posture.id))
.json(&AssignLocationsData {
locations: vec![regular_location_id, service_location_id],
})
.send()
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
client.assert_event_queue_is_empty();
assert_eq!(response.status(), StatusCode::OK);
let result: Vec<i64> = response.json().await;
assert_eq!(result.len(), 2);
assert!(result.contains(&regular_location_id));
assert!(result.contains(&service_location_id));
client.drain_all_events();

let response = client
.get(format!("/api/v1/device-posture/{}", posture.id))
.send()
.await;
let fetched: ApiDevicePosture = response.json().await;
assert!(fetched.locations.is_empty());
assert_eq!(fetched.locations.len(), 2);
assert!(fetched.locations.contains(&regular_location_id));
assert!(fetched.locations.contains(&service_location_id));

// assigning only regular locations still works
let response = client
Expand Down
Loading
Loading