Skip to content
Open
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
11 changes: 1 addition & 10 deletions crates/defguard_core/src/enterprise/handlers/device_posture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,6 @@ pub async fn create_device_posture(
responses(
(status = 200, description = "Valid device posture OS and client versions", body = DevicePostureVersionMetadata),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - enterprise license required")
),
security(
("cookie" = []),
Expand All @@ -681,11 +680,7 @@ pub async fn create_device_posture(
/// # Errors
///
/// Returns an error when the requester is unauthorized or lacks the required license.
pub async fn get_device_posture_versions(
_license: LicenseGated<DevicePostureFeature>,
_admin: AdminRole,
session: SessionInfo,
) -> ApiResult {
pub async fn get_device_posture_versions(_admin: AdminRole, session: SessionInfo) -> ApiResult {
debug!(
"User {} fetching device posture version metadata",
session.user.username
Expand All @@ -708,7 +703,6 @@ pub async fn get_device_posture_versions(
responses(
(status = 200, description = "Paginated list of device posture check policies", body = [ApiDevicePosture]),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - enterprise license required"),
(status = 500, description = "Internal server error")
),
security(
Expand All @@ -717,7 +711,6 @@ pub async fn get_device_posture_versions(
)
)]
pub async fn list_device_postures(
_license: LicenseGated<DevicePostureFeature>,
_admin: AdminRole,
session: SessionInfo,
pagination: Query<PaginationParams>,
Expand Down Expand Up @@ -783,7 +776,6 @@ pub async fn list_device_postures(
responses(
(status = 200, description = "Device posture check policy", body = ApiDevicePosture),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden - enterprise license required"),
(status = 404, description = "Not found"),
(status = 500, description = "Internal server error")
),
Expand All @@ -793,7 +785,6 @@ pub async fn list_device_postures(
)
)]
pub async fn get_device_posture(
_license: LicenseGated<DevicePostureFeature>,
_admin: AdminRole,
session: SessionInfo,
Path(id): Path<Id>,
Expand Down
45 changes: 22 additions & 23 deletions crates/defguard_core/src/grpc/proxy/client_mfa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,17 +227,17 @@ impl ClientMfaServer {
pubkey: request.pubkey.clone(),
device_posture_data: request.posture_data.clone(),
};
let posture_result = validate_posture(&self.pool, &posture_request)
.await
.map_err(|err| match err {
PostureCheckError::NoActiveEnterpriseLicense => Status::failed_precondition(
"enterprise license required for posture checks",
),
PostureCheckError::DbError(e) => {
error!("DB error during posture validation: {e}");
Status::internal("unexpected error")
}
})?;
let posture_result = match validate_posture(&self.pool, &posture_request).await {
Ok(result) => result,
Err(PostureCheckError::NoActiveEnterpriseLicense) => {
debug!("No active license - skipping posture check for location {location}");
PostureResult::Pass
}
Err(PostureCheckError::DbError(e)) => {
error!("DB error during posture validation: {e}");
return Err(Status::internal("unexpected error"));
}
};

let (ip, _user_agent) = parse_client_ip_agent(&info).map_err(Status::internal)?;
let context =
Expand Down Expand Up @@ -930,18 +930,17 @@ impl ClientMfaServer {
Self::validate_location_access(&self.pool, &location, &user_info).await?;

// Evaluate posture.
let posture_result =
validate_posture(&self.pool, &request)
.await
.map_err(|err| match err {
PostureCheckError::NoActiveEnterpriseLicense => Status::failed_precondition(
"enterprise license required for posture checks",
),
PostureCheckError::DbError(e) => {
error!("DB error during posture validation: {e}");
Status::internal("unexpected error")
}
})?;
let posture_result = match validate_posture(&self.pool, &request).await {
Ok(result) => result,
Err(PostureCheckError::NoActiveEnterpriseLicense) => {
debug!("No active license - skipping posture check for location {location}");
PostureResult::Pass
}
Err(PostureCheckError::DbError(e)) => {
error!("DB error during posture validation: {e}");
return Err(Status::internal("unexpected error"));
}
};

// Posture check failed - return payload with reasons
if let PostureResult::Fail(reasons) = posture_result {
Expand Down
21 changes: 18 additions & 3 deletions crates/defguard_core/src/handlers/wireguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1034,17 +1034,32 @@ pub(crate) async fn modify_device(
debug!("User {} updating device {device_id}", session.user.username);

let settings = EnterpriseSettings::get(&appstate.pool).await?;
if settings.only_client_activation && !session.is_admin {
if settings.admin_device_management && !session.is_admin {
warn!(
"User {} tried to add a device, but manual device management is disaled",
"User {} tried to edit a device, but manual device management is disaled",
session.user.username
);
return Err(WebError::Forbidden("Manual device management is disabled"));
}

let mut device = device_for_admin_or_self(&appstate.pool, &session, device_id).await?;
// store device before mods
let before = device.clone();

if settings.only_client_activation
&& !session.is_admin
&& (data.wireguard_pubkey != before.wireguard_pubkey
|| data.description != before.description)
{
warn!(
"User {} tried to modify fields other than device name for device {device_id}, but \
only client activation is enabled",
session.user.username
);
return Err(WebError::BadRequest(
"Only the device name can be edited when only client activation is enabled".into(),
));
}

let networks = WireguardNetwork::all(&appstate.pool).await?;

if networks.is_empty() {
Expand Down
12 changes: 9 additions & 3 deletions crates/defguard_core/tests/integration/api/device_posture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,22 @@ async fn test_device_posture_enterprise_license_required(
let edit = make_edit("test");
let saved = get_cached_license().clone();

// no license → 403
// no license → GET is accessible, POST is forbidden
set_cached_license(None);
let response = client.get("/api/v1/device-posture").send().await;
assert_eq!(response.status(), StatusCode::OK);
let response = client
.post("/api/v1/device-posture")
.json(&edit)
.send()
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
client.assert_event_queue_is_empty();

// business-only license (default from make_test_client) → 403
// business-only license (default from make_test_client) → GET is accessible, POST is forbidden
set_cached_license(saved.clone()); // restore Business tier
let response = client.get("/api/v1/device-posture").send().await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert_eq!(response.status(), StatusCode::OK);
let response = client
.post("/api/v1/device-posture")
.json(&edit)
Expand Down
40 changes: 32 additions & 8 deletions crates/defguard_core/tests/integration/api/enterprise_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,11 @@ async fn dg25_12_test_enforce_client_activation_only(_: PgPoolOptions, options:
.send()
.await;
assert_eq!(response.status(), StatusCode::CREATED);
let created_device: serde_json::Value = response.json().await;
let device_id = created_device["device"]["id"].as_i64().unwrap();
let device_pubkey = created_device["device"]["wireguard_pubkey"]
.as_str()
.unwrap();

// ensure normal users can't manage devices
let auth = Auth::new("hpotter", "pass123");
Expand All @@ -371,23 +376,42 @@ async fn dg25_12_test_enforce_client_activation_only(_: PgPoolOptions, options:
.await;
assert_eq!(response.status(), StatusCode::FORBIDDEN);

// modify
// modify: renaming an existing device is still allowed
let device = json!({
"name": "modifieddevice",
"wireguard_pubkey": "AJwxGkzvVVn5Q1xjpCDFo5RJSU9KOPHeoEixYaj+20M=",
"wireguard_pubkey": device_pubkey,
});
let response = client.put("/api/v1/device/2").json(&device).send().await;

assert_eq!(response.status(), StatusCode::FORBIDDEN);
let response = client
.put(format!("/api/v1/device/{device_id}"))
.json(&device)
.send()
.await;
assert_eq!(response.status(), StatusCode::OK);

// delete
// modify: changing the pubkey is not allowed
let device = json!({
"name": "modifieddevice",
"wireguard_pubkey": "AJwxGkzvVVn5Q1xjpCDFo5RJSU9KOPHeoEixYaj+20M=",
});
let response = client.put("/api/v1/device/2").json(&device).send().await;
let response = client
.put(format!("/api/v1/device/{device_id}"))
.json(&device)
.send()
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);

assert_eq!(response.status(), StatusCode::FORBIDDEN);
// modify: changing the description is not allowed
let device = json!({
"name": "modifieddevice",
"wireguard_pubkey": device_pubkey,
"description": "new description",
});
let response = client
.put(format!("/api/v1/device/{device_id}"))
.json(&device)
.send()
.await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}

#[sqlx::test]
Expand Down
2 changes: 2 additions & 0 deletions web/messages/en/modal.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
"modal_ce_openid_client_label_redirect": "Redirect URL {index}",
"modal_ce_openid_client_label_redirect_add": "Add URL",
"modal_ce_openid_client_label_scopes_title": "Scopes",
"modal_ce_openid_client_label_scopes_text": "Scopes define what information this application can request about a user, such as their profile details, email address, or group membership. Select only the scopes required for the application to function.",
"modal_ce_openid_client_label_name": "Application name",
"modal_ce_openid_client_helper_name": "",
"modal_ce_openid_client_helper_redirect_uri": "",
Expand All @@ -206,6 +207,7 @@
"modal_assign_user_ip_success": "{firstName} {lastName}'s IP addresses were successfully updated.",
"modal_assign_user_ip_error": "Failed to update IP addresses",
"modal_assign_user_ip_validation_error": "Invalid or already taken",
"modal_assign_user_ip_duplicate_error": "This IP address is already used in this location",
"modal_assign_user_ip_no_locations": "No locations available. Add a location first.",
"modal_assign_user_ip_no_devices": "This user has no devices.",
"modal_assign_posture_check_locations_title": "Assign locations",
Expand Down
3 changes: 2 additions & 1 deletion web/messages/en/openid.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"settings_openid_provider_label_okta_directory_sync_client_id": "Directory sync client ID",
"settings_openid_provider_helper_okta_directory_sync_client_id": "",
"settings_openid_provider_label_okta_directory_sync_client_private_key": "Directory sync client private key",
"settings_openid_provider_helper_okta_directory_sync_client_private_key": "",
"settings_openid_provider_helper_okta_directory_sync_client_private_key": "Leave blank to keep the current key unchanged.",
"settings_openid_provider_placeholder_okta_private_jwk": "Change private key",
"settings_openid_provider_label_jumpcloud_api_key": "JumpCloud API key",
"settings_openid_provider_helper_jumpcloud_api_key": "",
"settings_openid_provider_label_jumpcloud_region": "JumpCloud region",
Expand Down
25 changes: 25 additions & 0 deletions web/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,36 @@ import './day';

import { QueryClientProvider } from '@tanstack/react-query';
import { RouterProvider } from '@tanstack/react-router';
import { useEffect } from 'react';
import { AppThemeProvider } from '../shared/providers/AppThemeProvider';
import { queryClient } from './query';
import { router } from './router';

export const App = () => {
useEffect(() => {
// Safety net: if a modal is open and the user navigates to a route that
// doesn't render it, ModalFoundation's unmount cleanup can fail to fire,
// leaving body scroll locked with no modal actually on screen.
const modalsRoot = document.getElementById('modals-root');
const rootElement = document.getElementById('root');
if (!modalsRoot || !rootElement) return;

const observer = new MutationObserver(() => {
if (
modalsRoot.childElementCount === 0 &&
rootElement.style.overflowY === 'hidden'
) {
rootElement.style.overflowY = 'auto';
}
});

observer.observe(modalsRoot, { childList: true });

return () => {
observer.disconnect();
};
}, []);

return (
<AppThemeProvider>
<QueryClientProvider client={queryClient}>
Expand Down
30 changes: 16 additions & 14 deletions web/src/pages/GroupsPage/modals/CEGroupModal/CEGroupModal.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import z from 'zod';
import { m } from '../../../../paraglide/messages';
import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal';
import {
closeModal,
subscribeCloseModal,
subscribeOpenModal,
} from '../../../../shared/hooks/modalControls/modalsSubjects';
import { ModalName } from '../../../../shared/hooks/modalControls/modalTypes';
import type { OpenCEGroupModal } from '../../../../shared/hooks/modalControls/types';
import './style.scss';
import { useMutation } from '@tanstack/react-query';
import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react';
import z from 'zod';
import { m } from '../../../../paraglide/messages';
import api from '../../../../shared/api/api';
import type { CreateGroupRequest, User } from '../../../../shared/api/types';
import { DescriptionBlock } from '../../../../shared/components/DescriptionBlock/DescriptionBlock';
import { SelectionSection } from '../../../../shared/components/SelectionSection/SelectionSection';
import type { SelectionOption } from '../../../../shared/components/SelectionSection/type';
import { Checkbox } from '../../../../shared/defguard-ui/components/Checkbox/Checkbox';
import { Divider } from '../../../../shared/defguard-ui/components/Divider/Divider';
import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal';
import { ModalControls } from '../../../../shared/defguard-ui/components/ModalControls/ModalControls';
import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/SizedBox';
import { ThemeSpacing } from '../../../../shared/defguard-ui/types';
import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent';
import { useAppForm } from '../../../../shared/form';
import { formChangeLogic } from '../../../../shared/formLogic';
import {
closeModal,
subscribeCloseModal,
subscribeOpenModal,
} from '../../../../shared/hooks/modalControls/modalsSubjects';
import { ModalName } from '../../../../shared/hooks/modalControls/modalTypes';
import type { OpenCEGroupModal } from '../../../../shared/hooks/modalControls/types';

interface ModalState extends OpenCEGroupModal {
step: 'start' | 'users';
Expand Down Expand Up @@ -225,9 +226,10 @@ const StartStep = ({ reservedNames, setModalState, groupInfo, startForm }: StepP
</form.AppField>
</form.AppForm>
<Divider spacing={ThemeSpacing.Xl} />
<p>{m.modal_add_group_admin_title()}</p>
<p>{m.modal_add_group_admin_explain()}</p>
<Divider spacing={ThemeSpacing.Lg} />
<DescriptionBlock title={m.modal_add_group_admin_title()}>
<p>{m.modal_add_group_admin_explain()}</p>
</DescriptionBlock>
<SizedBox height={ThemeSpacing.Lg} />
<Checkbox
text={m.modal_add_group_form_label_admin()}
active={isAdmin}
Expand Down
12 changes: 0 additions & 12 deletions web/src/pages/GroupsPage/modals/CEGroupModal/style.scss

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ const ModalContent = ({ reservedNames, openIdClient }: ModalData) => {
</form.AppField>
<Divider spacing={ThemeSpacing.Xl} />
<DescriptionBlock title={m.modal_ce_openid_client_label_scopes_title()}>
<p>{m.test_placeholder_long()}</p>
<p>{m.modal_ce_openid_client_label_scopes_text()}</p>
</DescriptionBlock>
<SizedBox height={ThemeSpacing.Xl} />
<div className="scopes">
Expand Down
1 change: 1 addition & 0 deletions web/src/pages/UsersOverviewPage/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ export const UsersTable = () => {
reservedNames: reservedDeviceNames,
reservedPubkeys,
username,
hidePubkey: false,
});
},
},
Expand Down
Loading
Loading