From b712b5937ea2d0bb2532f5cd3e6254100334860a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 12 Aug 2024 12:23:21 +0200 Subject: [PATCH 01/26] Add permissions settings tab --- web/src/i18n/en/index.ts | 12 ++++ web/src/i18n/i18n-types.ts | 52 +++++++++++++++++ web/src/i18n/pl/index.ts | 12 ++++ web/src/pages/settings/SettingsPage.tsx | 17 ++++-- .../PermissionsSettings.tsx | 10 ++++ .../components/PermissionsForm.tsx | 58 +++++++++++++++++++ .../components/styles.scss | 12 ++++ web/src/shared/types.ts | 7 ++- 8 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 web/src/pages/settings/components/PermissionsSettings/PermissionsSettings.tsx create mode 100644 web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx create mode 100644 web/src/pages/settings/components/PermissionsSettings/components/styles.scss diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index 8ead815853..4fb352b922 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -892,6 +892,7 @@ const en: BaseTranslation = { global: 'Global settings', ldap: 'LDAP', openid: 'OpenID', + permissions: 'Permissions', }, messages: { editSuccess: 'Settings updated', @@ -1180,6 +1181,17 @@ const en: BaseTranslation = { }, }, }, + permissions: { + header: 'Permissions', + helper: '

Here you can change basic user permissions.

', + fields: { + deviceCreation: { + label: 'Disable users ability to add their own devices', + helper: + "When this option is enabled, only users in the Admin group can add devices in user profile (it's disabled for all other users)", + }, + }, + }, }, openidOverview: { pageTitle: 'OpenID Apps', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 9e14eab004..16ca5d9dc1 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -2198,6 +2198,10 @@ type RootTranslation = { * O​p​e​n​I​D */ openid: string + /** + * P​e​r​m​i​s​s​i​o​n​s + */ + permissions: string } messages: { /** @@ -2801,6 +2805,28 @@ type RootTranslation = { } } } + permissions: { + /** + * P​e​r​m​i​s​s​i​o​n​s + */ + header: string + /** + * <​p​>​H​e​r​e​ ​y​o​u​ ​c​a​n​ ​c​h​a​n​g​e​ ​b​a​s​i​c​ ​u​s​e​r​ ​p​e​r​m​i​s​s​i​o​n​s​.​<​/​p​> + */ + helper: string + fields: { + deviceCreation: { + /** + * D​i​s​a​b​l​e​ ​u​s​e​r​s​ ​a​b​i​l​i​t​y​ ​t​o​ ​a​d​d​ ​t​h​e​i​r​ ​o​w​n​ ​d​e​v​i​c​e​s + */ + label: string + /** + * W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​o​n​l​y​ ​u​s​e​r​s​ ​i​n​ ​t​h​e​ ​A​d​m​i​n​ ​g​r​o​u​p​ ​c​a​n​ ​a​d​d​ ​d​e​v​i​c​e​s​ ​i​n​ ​u​s​e​r​ ​p​r​o​f​i​l​e​ ​(​i​t​'​s​ ​d​i​s​a​b​l​e​d​ ​f​o​r​ ​a​l​l​ ​o​t​h​e​r​ ​u​s​e​r​s​) + */ + helper: string + } + } + } } openidOverview: { /** @@ -6296,6 +6322,10 @@ export type TranslationFunctions = { * OpenID */ openid: () => LocalizedString + /** + * Permissions + */ + permissions: () => LocalizedString } messages: { /** @@ -6896,6 +6926,28 @@ export type TranslationFunctions = { } } } + permissions: { + /** + * Permissions + */ + header: () => LocalizedString + /** + *

Here you can change basic user permissions.

+ */ + helper: () => LocalizedString + fields: { + deviceCreation: { + /** + * Disable users ability to add their own devices + */ + label: () => LocalizedString + /** + * When this option is enabled, only users in the Admin group can add devices in user profile (it's disabled for all other users) + */ + helper: () => LocalizedString + } + } + } } openidOverview: { /** diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index df0ac2c6e3..4d23483fd3 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -879,6 +879,7 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe global: 'Globalne', ldap: 'LDAP', openid: 'OpenID', + permissions: 'Uprawnienia', }, messages: { editSuccess: 'Ustawienia zaktualizowane.', @@ -1167,6 +1168,17 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe }, }, }, + permissions: { + header: 'Uprawnienia', + helper: '

Tutaj możesz zmienić podstawowe uprawnienia użytkowników.

', + fields: { + deviceCreation: { + label: 'Zablokuj możliwość tworzenia urządzeń przez użytkowników', + helper: + 'Kiedy ta opcja jest włączona, tylko użytkownicy w grupie "Admin" mogą dodawać urządzenia w profilu użytkownika', + }, + }, + }, }, openidOverview: { pageTitle: 'Aplikacje OpenID', diff --git a/web/src/pages/settings/SettingsPage.tsx b/web/src/pages/settings/SettingsPage.tsx index c66e05973c..b3001c88ee 100644 --- a/web/src/pages/settings/SettingsPage.tsx +++ b/web/src/pages/settings/SettingsPage.tsx @@ -15,6 +15,7 @@ import { QueryKeys } from '../../shared/queries'; import { GlobalSettings } from './components/GlobalSettings/GlobalSettings'; import { LdapSettings } from './components/LdapSettings/LdapSettings'; import { OpenIdSettings } from './components/OpenIdSettings/OpenIdSettings'; +import { PermissionsSettings } from './components/PermissionsSettings/PermissionsSettings'; import { SmtpSettings } from './components/SmtpSettings/SmtpSettings'; import { useSettingsPage } from './hooks/useSettingsPage'; @@ -23,6 +24,7 @@ const tabsContent: ReactNode[] = [ , , , + , ]; export const SettingsPage = () => { @@ -47,8 +49,8 @@ export const SettingsPage = () => { refetchOnWindowFocus: false, }); - const tabs = useMemo((): CardTabsData[] => { - return [ + const tabs = useMemo( + (): CardTabsData[] => [ { key: 0, content: LL.settingsPage.tabs.global(), @@ -73,8 +75,15 @@ export const SettingsPage = () => { active: activeCard === 3, onClick: () => setActiveCard(3), }, - ]; - }, [LL.settingsPage.tabs, activeCard]); + { + key: 4, + content: LL.settingsPage.tabs.permissions(), + active: activeCard === 4, + onClick: () => setActiveCard(4), + }, + ], + [LL.settingsPage.tabs, activeCard], + ); // set store useEffect(() => { diff --git a/web/src/pages/settings/components/PermissionsSettings/PermissionsSettings.tsx b/web/src/pages/settings/components/PermissionsSettings/PermissionsSettings.tsx new file mode 100644 index 0000000000..a7aa06837d --- /dev/null +++ b/web/src/pages/settings/components/PermissionsSettings/PermissionsSettings.tsx @@ -0,0 +1,10 @@ +import { PermissionsForm } from './components/PermissionsForm'; + +export const PermissionsSettings = () => ( + <> +
+ +
+
+ +); diff --git a/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx b/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx new file mode 100644 index 0000000000..f587460c47 --- /dev/null +++ b/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx @@ -0,0 +1,58 @@ +import './styles.scss'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import parse from 'html-react-parser'; + +import { useI18nContext } from '../../../../../i18n/i18n-react'; +import { Card } from '../../../../../shared/defguard-ui/components/Layout/Card/Card'; +import { Helper } from '../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; +import { LabeledCheckbox } from '../../../../../shared/defguard-ui/components/Layout/LabeledCheckbox/LabeledCheckbox'; +import useApi from '../../../../../shared/hooks/useApi'; +import { useToaster } from '../../../../../shared/hooks/useToaster'; +import { MutationKeys } from '../../../../../shared/mutations'; +import { QueryKeys } from '../../../../../shared/queries'; +import { useSettingsPage } from '../../../hooks/useSettingsPage'; + +export const PermissionsForm = () => { + const { LL } = useI18nContext(); + const toaster = useToaster(); + const { + settings: { patchSettings }, + } = useApi(); + + const settings = useSettingsPage((state) => state.settings); + + const queryClient = useQueryClient(); + + const { mutate, isLoading } = useMutation([MutationKeys.EDIT_SETTINGS], patchSettings, { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.FETCH_ESSENTIAL_SETTINGS]); + queryClient.invalidateQueries([QueryKeys.FETCH_SETTINGS]); + toaster.success(LL.settingsPage.messages.editSuccess()); + }, + onError: () => { + toaster.error(LL.messages.error()); + }, + }); + + if (!settings) return null; + + return ( +
+
+

{LL.settingsPage.permissions.header()}

+ {parse(LL.settingsPage.permissions.helper())} +
+ + + mutate({ disable_device_creation: !settings.disable_device_creation }) + } + /> + +
+ ); +}; diff --git a/web/src/pages/settings/components/PermissionsSettings/components/styles.scss b/web/src/pages/settings/components/PermissionsSettings/components/styles.scss new file mode 100644 index 0000000000..f233e834ce --- /dev/null +++ b/web/src/pages/settings/components/PermissionsSettings/components/styles.scss @@ -0,0 +1,12 @@ +@use '@scssutils' as *; + +#modules-settings { + & > .card { + display: flex; + flex-flow: column; + row-gap: 16px; + @include media-breakpoint-up(lg) { + padding: 16px 15px; + } + } +} diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index a89624e155..3e9a585d41 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -810,7 +810,8 @@ export type Settings = SettingsModules & SettingsBranding & SettingsLDAP & SettingsOpenID & - SettingsLicense; + SettingsLicense & + SettingsPermissions; // essentials for core frontend, includes only those that are required for frontend operations export type SettingsEssentials = SettingsModules & SettingsBranding; @@ -871,6 +872,10 @@ export type SettingsLicense = { license: string; }; +export type SettingsPermissions = { + disable_device_creation: boolean; +}; + export interface Webhook { id: string; url: string; From 04ea23c4285679f8d0d3a2d4848ff52cb6da61a3 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 19 Aug 2024 16:17:34 +0200 Subject: [PATCH 02/26] Model, migration & working edit device creation settings form --- ...0240819134151_device_creation_permission.down.sql | 1 + .../20240819134151_device_creation_permission.up.sql | 1 + src/db/models/settings.rs | 2 ++ web/src/i18n/i18n-types.ts | 12 +++++------- .../components/PermissionsForm.tsx | 7 ++++--- 5 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 migrations/20240819134151_device_creation_permission.down.sql create mode 100644 migrations/20240819134151_device_creation_permission.up.sql diff --git a/migrations/20240819134151_device_creation_permission.down.sql b/migrations/20240819134151_device_creation_permission.down.sql new file mode 100644 index 0000000000..bc870dfc6a --- /dev/null +++ b/migrations/20240819134151_device_creation_permission.down.sql @@ -0,0 +1 @@ +ALTER TABLE settings DROP COLUMN disable_device_creation; diff --git a/migrations/20240819134151_device_creation_permission.up.sql b/migrations/20240819134151_device_creation_permission.up.sql new file mode 100644 index 0000000000..bc68806207 --- /dev/null +++ b/migrations/20240819134151_device_creation_permission.up.sql @@ -0,0 +1 @@ +ALTER TABLE settings ADD COLUMN disable_device_creation BOOLEAN NOT NULL DEFAULT false; diff --git a/src/db/models/settings.rs b/src/db/models/settings.rs index 569db58efc..67e061e50d 100644 --- a/src/db/models/settings.rs +++ b/src/db/models/settings.rs @@ -65,6 +65,8 @@ pub struct Settings { // Whether to create a new account when users try to log in with external OpenID pub openid_create_account: bool, pub license: Option, + // If true, only admins can create devices + pub disable_device_creation: bool, } impl Settings { diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 16ca5d9dc1..e81e415d91 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -1946,7 +1946,7 @@ type RootTranslation = { support: string } /** - * C​o​p​y​r​i​g​h​t​ ​©​ ​2​0​2​3​ + * C​o​p​y​r​i​g​h​t​ ​©​2​0​2​3​-​2​0​2​4 */ copyright: string version: { @@ -1956,7 +1956,7 @@ type RootTranslation = { */ open: RequiredParams<'version'> /** - * v​ ​{​v​e​r​s​i​o​n​} + * v​{​v​e​r​s​i​o​n​} * @param {string} version */ closed: RequiredParams<'version'> @@ -3625,8 +3625,7 @@ type RootTranslation = { * D​e​f​g​u​a​r​d​ ​r​e​q​u​i​r​e​s​ ​t​o​ ​d​e​p​l​o​y​ ​a​ ​g​a​t​e​w​a​y​ ​n​o​d​e​ ​t​o​ ​c​o​n​t​r​o​l​ ​w​i​r​e​g​u​a​r​d​ ​V​P​N​ ​o​n​ ​t​h​e​ ​v​p​n​ ​s​e​r​v​e​r​.​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​M​o​r​e​ ​d​e​t​a​i​l​s​ ​c​a​n​ ​b​e​ ​f​o​u​n​d​ ​i​n​ ​t​h​e​ ​[​d​o​c​u​m​e​n​t​a​t​i​o​n​]​(​{​s​e​t​u​p​G​a​t​e​w​a​y​D​o​c​s​}​)​.​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​T​h​e​r​e​ ​a​r​e​ ​s​e​v​e​r​a​l​ ​w​a​y​s​ ​t​o​ ​d​e​p​l​o​y​ ​t​h​e​ ​g​a​t​e​w​a​y​ ​s​e​r​v​e​r​,​ - ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​b​e​l​o​w​ ​i​s​ ​a​ ​D​o​c​k​e​r​ ​b​a​s​e​d​ ​e​x​a​m​p​l​e​,​ ​f​o​r​ ​o​t​h​e​r​ ​e​x​a​m​p​l​e​s​ ​p​l​e​a​s​e​ ​v​i​s​i​t​ ​[​d​o​c​u​m​e​n​t​a​t​i​o​n​]​(​{​s​e​t​u​p​G​a​t​e​w​a​y​D​o​c​s​}​)​.​ - ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​b​e​l​o​w​ ​i​s​ ​a​ ​D​o​c​k​e​r​ ​b​a​s​e​d​ ​e​x​a​m​p​l​e​,​ ​f​o​r​ ​o​t​h​e​r​ ​e​x​a​m​p​l​e​s​ ​p​l​e​a​s​e​ ​v​i​s​i​t​ ​[​d​o​c​u​m​e​n​t​a​t​i​o​n​]​(​{​s​e​t​u​p​G​a​t​e​w​a​y​D​o​c​s​}​)​. * @param {string} setupGatewayDocs */ runCommand: RequiredParams<'setupGatewayDocs' | 'setupGatewayDocs'> @@ -6074,7 +6073,7 @@ export type TranslationFunctions = { support: () => LocalizedString } /** - * Copyright © 2023 + * Copyright ©2023-2024 */ copyright: () => LocalizedString version: { @@ -6083,7 +6082,7 @@ export type TranslationFunctions = { */ open: (arg: { version: string }) => LocalizedString /** - * v {version} + * v{version} */ closed: (arg: { version: string }) => LocalizedString } @@ -7740,7 +7739,6 @@ export type TranslationFunctions = { More details can be found in the [documentation]({setupGatewayDocs}). There are several ways to deploy the gateway server, below is a Docker based example, for other examples please visit [documentation]({setupGatewayDocs}). - */ runCommand: (arg: { setupGatewayDocs: string }) => LocalizedString /** diff --git a/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx b/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx index f587460c47..abaf44d77b 100644 --- a/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx +++ b/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx @@ -12,6 +12,7 @@ import { useToaster } from '../../../../../shared/hooks/useToaster'; import { MutationKeys } from '../../../../../shared/mutations'; import { QueryKeys } from '../../../../../shared/queries'; import { useSettingsPage } from '../../../hooks/useSettingsPage'; +import { AxiosError } from 'axios'; export const PermissionsForm = () => { const { LL } = useI18nContext(); @@ -26,12 +27,12 @@ export const PermissionsForm = () => { const { mutate, isLoading } = useMutation([MutationKeys.EDIT_SETTINGS], patchSettings, { onSuccess: () => { - queryClient.invalidateQueries([QueryKeys.FETCH_ESSENTIAL_SETTINGS]); queryClient.invalidateQueries([QueryKeys.FETCH_SETTINGS]); toaster.success(LL.settingsPage.messages.editSuccess()); }, - onError: () => { + onError: (err: AxiosError) => { toaster.error(LL.messages.error()); + console.error(err); }, }); @@ -47,7 +48,7 @@ export const PermissionsForm = () => { mutate({ disable_device_creation: !settings.disable_device_creation }) } From 30c340578e97dec2e120e974d00085e1da476138 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 19 Aug 2024 16:18:59 +0200 Subject: [PATCH 03/26] Lint permissions form --- .../PermissionsSettings/components/PermissionsForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx b/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx index abaf44d77b..e38287c3c2 100644 --- a/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx +++ b/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx @@ -1,6 +1,7 @@ import './styles.scss'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; import parse from 'html-react-parser'; import { useI18nContext } from '../../../../../i18n/i18n-react'; @@ -12,7 +13,6 @@ import { useToaster } from '../../../../../shared/hooks/useToaster'; import { MutationKeys } from '../../../../../shared/mutations'; import { QueryKeys } from '../../../../../shared/queries'; import { useSettingsPage } from '../../../hooks/useSettingsPage'; -import { AxiosError } from 'axios'; export const PermissionsForm = () => { const { LL } = useI18nContext(); From b926b9331dd9e34fc95dc675958590bc2d2e7818 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 19 Aug 2024 18:03:54 +0200 Subject: [PATCH 04/26] Block device creation on backend --- src/handlers/wireguard.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 7fb332d8aa..5d86510b21 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -25,8 +25,7 @@ use crate::{ DeviceConfig, DeviceInfo, DeviceNetworkInfo, ModifyDevice, WireguardNetworkDevice, }, wireguard::{DateTimeAggregation, MappedDevice, WireguardNetworkInfo}, - }, - AddDevice, DbPool, Device, GatewayEvent, WireguardNetwork, + }, AddDevice, DbPool, Device, GatewayEvent, Settings, WireguardNetwork }, grpc::GatewayMap, handlers::mail::send_new_device_added_email, @@ -530,6 +529,7 @@ pub async fn add_device( ); let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; + can_manage_devices_or_error(&appstate.pool, &session).await?; // Let admins manage devices for disabled users if !user.is_active && !session.is_admin { @@ -630,6 +630,14 @@ pub async fn add_device( }) } +async fn can_manage_devices_or_error(pool: &DbPool, session: &SessionInfo) -> Result<(), WebError> { + let settings = Settings::get_settings(pool).await?; + if settings.disable_device_creation && !session.is_admin { + return Err(WebError::Forbidden("Only admin users can manage devices".into())); + } + Ok(()) +} + /// Modify device /// /// Update a device for a user by sending `ModifyDevice` object. From 9a573692f7e880ef8984d88c76ee6454ffad662a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 20 Aug 2024 08:10:06 +0200 Subject: [PATCH 05/26] Permission check for device modification and deletion --- src/handlers/wireguard.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 5d86510b21..68af097998 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -25,7 +25,8 @@ use crate::{ DeviceConfig, DeviceInfo, DeviceNetworkInfo, ModifyDevice, WireguardNetworkDevice, }, wireguard::{DateTimeAggregation, MappedDevice, WireguardNetworkInfo}, - }, AddDevice, DbPool, Device, GatewayEvent, Settings, WireguardNetwork + }, + AddDevice, DbPool, Device, GatewayEvent, Settings, WireguardNetwork, }, grpc::GatewayMap, handlers::mail::send_new_device_added_email, @@ -630,10 +631,13 @@ pub async fn add_device( }) } +/// Returns an error if current session user cannot manage devices. async fn can_manage_devices_or_error(pool: &DbPool, session: &SessionInfo) -> Result<(), WebError> { let settings = Settings::get_settings(pool).await?; if settings.disable_device_creation && !session.is_admin { - return Err(WebError::Forbidden("Only admin users can manage devices".into())); + return Err(WebError::Forbidden( + "Only admin users can manage devices".into(), + )); } Ok(()) } @@ -678,6 +682,7 @@ pub async fn modify_device( ) -> ApiResult { debug!("User {} updating device {device_id}", session.user.username); let mut device = device_for_admin_or_self(&appstate.pool, &session, device_id).await?; + can_manage_devices_or_error(&appstate.pool, &session).await?; let networks = WireguardNetwork::all(&appstate.pool).await?; if networks.is_empty() { @@ -799,6 +804,7 @@ pub async fn delete_device( ) -> ApiResult { debug!("User {} deleting device {device_id}", session.user.username); let device = device_for_admin_or_self(&appstate.pool, &session, device_id).await?; + can_manage_devices_or_error(&appstate.pool, &session).await?; appstate.send_wireguard_event(GatewayEvent::DeviceDeleted( DeviceInfo::from_device(&appstate.pool, device.clone()).await?, )); From 72b5e48d9a790799fd4c325589994bbd33e03262 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 20 Aug 2024 09:41:19 +0200 Subject: [PATCH 06/26] Block device management on frontend --- src/db/models/settings.rs | 4 +++- .../UserDevices/DeviceCard/DeviceCard.tsx | 5 ++++- .../users/UserProfile/UserDevices/UserDevices.tsx | 14 ++++++++++++-- web/src/shared/types.ts | 2 +- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/db/models/settings.rs b/src/db/models/settings.rs index 67e061e50d..8946fe74a6 100644 --- a/src/db/models/settings.rs +++ b/src/db/models/settings.rs @@ -124,6 +124,7 @@ pub struct SettingsEssentials { pub webhooks_enabled: bool, pub worker_enabled: bool, pub openid_enabled: bool, + pub disable_device_creation: bool, } impl SettingsEssentials { @@ -134,7 +135,7 @@ impl SettingsEssentials { query_as!( SettingsEssentials, "SELECT instance_name, main_logo_url, nav_logo_url, wireguard_enabled, \ - webhooks_enabled, worker_enabled, openid_enabled \ + webhooks_enabled, worker_enabled, openid_enabled, disable_device_creation \ FROM settings WHERE id = 1" ) .fetch_one(executor) @@ -152,6 +153,7 @@ impl From for SettingsEssentials { nav_logo_url: settings.nav_logo_url, instance_name: settings.instance_name, main_logo_url: settings.main_logo_url, + disable_device_creation: settings.disable_device_creation, } } } diff --git a/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx b/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx index a630bf1131..54e1407c5c 100644 --- a/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx +++ b/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx @@ -37,9 +37,10 @@ const formatDate = (date: string): string => { interface Props { device: Device; + modifiable: boolean; } -export const DeviceCard = ({ device }: Props) => { +export const DeviceCard = ({ device, modifiable }: Props) => { const [hovered, setHovered] = useState(false); const [expanded, setExpanded] = useState(false); const { LL } = useI18nContext(); @@ -144,6 +145,7 @@ export const DeviceCard = ({ device }: Props) => { { setEditDeviceModal({ visible: true, @@ -171,6 +173,7 @@ export const DeviceCard = ({ device }: Props) => { setDeleteDeviceModal({ visible: true, diff --git a/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx b/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx index 13a7f02dea..14495742d9 100644 --- a/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx +++ b/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx @@ -4,6 +4,7 @@ import Skeleton from 'react-loading-skeleton'; import { useNavigate } from 'react-router'; import { useI18nContext } from '../../../../i18n/i18n-react'; +import { isUserAdmin } from '../../../../shared/helpers/isUserAdmin'; import { useAppStore } from '../../../../shared/hooks/store/useAppStore'; import { useUserProfileStore } from '../../../../shared/hooks/store/useUserProfileStore'; import { useAddDevicePageStore } from '../../../addDevice/hooks/useAddDevicePageStore'; @@ -16,9 +17,14 @@ import { EditUserDeviceModal } from './modals/EditUserDeviceModal/EditUserDevice export const UserDevices = () => { const navigate = useNavigate(); const appInfo = useAppStore((state) => state.appInfo); + const settings = useAppStore((state) => state.settings); const { LL } = useI18nContext(); const userProfile = useUserProfileStore((state) => state.userProfile); const initAddDevice = useAddDevicePageStore((state) => state.init); + const canManageDevices = !!( + userProfile && + (!settings?.disable_device_creation || isUserAdmin(userProfile.user)) + ); return (
@@ -37,7 +43,11 @@ export const UserDevices = () => { {userProfile.devices && userProfile.devices.length > 0 && (
{userProfile.devices.map((device) => ( - + ))}
)} @@ -45,7 +55,7 @@ export const UserDevices = () => { { initAddDevice({ username: userProfile.user.username, diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 3e9a585d41..b0742be576 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -814,7 +814,7 @@ export type Settings = SettingsModules & SettingsPermissions; // essentials for core frontend, includes only those that are required for frontend operations -export type SettingsEssentials = SettingsModules & SettingsBranding; +export type SettingsEssentials = SettingsModules & SettingsBranding & SettingsPermissions; export type SettingsEnrollment = { enrollment_vpn_step_optional: boolean; From 1c8fa423888b5dad9a70d4f65b69574116128c6d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 20 Aug 2024 09:54:36 +0200 Subject: [PATCH 07/26] Enterprise-only behaviour tab --- .../PermissionsSettings.tsx | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/web/src/pages/settings/components/PermissionsSettings/PermissionsSettings.tsx b/web/src/pages/settings/components/PermissionsSettings/PermissionsSettings.tsx index a7aa06837d..3d06aaa500 100644 --- a/web/src/pages/settings/components/PermissionsSettings/PermissionsSettings.tsx +++ b/web/src/pages/settings/components/PermissionsSettings/PermissionsSettings.tsx @@ -1,10 +1,33 @@ +import { useI18nContext } from '../../../../i18n/i18n-react'; +import { useAppStore } from '../../../../shared/hooks/store/useAppStore'; import { PermissionsForm } from './components/PermissionsForm'; -export const PermissionsSettings = () => ( - <> -
- -
-
- -); +export const PermissionsSettings = () => { + const enterpriseEnabled = useAppStore((state) => state.enterprise_enabled); + const { LL } = useI18nContext(); + const localLL = LL.settingsPage.enterpriseOnly; + return ( + <> + {!enterpriseEnabled && ( +
+
+
+

{localLL.title()}

+

+ {localLL.subtitle()}{' '} + + {localLL.website()} + + . +

+
+
+
+ )} +
+ +
+
+ + ); +}; From 783295e933c93a3181084681a130d6895131fe97 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 20 Aug 2024 11:26:20 +0200 Subject: [PATCH 08/26] EnterpriseSettings model, migration, default --- ...134151_device_creation_permission.down.sql | 1 - ...19134151_device_creation_permission.up.sql | 1 - ...0240819134151_enterprise_settings.down.sql | 1 + .../20240819134151_enterprise_settings.up.sql | 6 ++++ src/db/models/settings.rs | 6 +--- .../db/models/enterprise_settings.rs | 29 ++++++++++++++++++ src/enterprise/db/models/mod.rs | 1 + .../handlers/enterprise_settings.rs | 30 +++++++++++++++++++ src/enterprise/handlers/mod.rs | 1 + src/handlers/wireguard.rs | 17 ++++------- 10 files changed, 74 insertions(+), 19 deletions(-) delete mode 100644 migrations/20240819134151_device_creation_permission.down.sql delete mode 100644 migrations/20240819134151_device_creation_permission.up.sql create mode 100644 migrations/20240819134151_enterprise_settings.down.sql create mode 100644 migrations/20240819134151_enterprise_settings.up.sql create mode 100644 src/enterprise/db/models/enterprise_settings.rs create mode 100644 src/enterprise/handlers/enterprise_settings.rs diff --git a/migrations/20240819134151_device_creation_permission.down.sql b/migrations/20240819134151_device_creation_permission.down.sql deleted file mode 100644 index bc870dfc6a..0000000000 --- a/migrations/20240819134151_device_creation_permission.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE settings DROP COLUMN disable_device_creation; diff --git a/migrations/20240819134151_device_creation_permission.up.sql b/migrations/20240819134151_device_creation_permission.up.sql deleted file mode 100644 index bc68806207..0000000000 --- a/migrations/20240819134151_device_creation_permission.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE settings ADD COLUMN disable_device_creation BOOLEAN NOT NULL DEFAULT false; diff --git a/migrations/20240819134151_enterprise_settings.down.sql b/migrations/20240819134151_enterprise_settings.down.sql new file mode 100644 index 0000000000..525e164fdf --- /dev/null +++ b/migrations/20240819134151_enterprise_settings.down.sql @@ -0,0 +1 @@ +DROP TABLE enterprisesettings; diff --git a/migrations/20240819134151_enterprise_settings.up.sql b/migrations/20240819134151_enterprise_settings.up.sql new file mode 100644 index 0000000000..a0b6cb6388 --- /dev/null +++ b/migrations/20240819134151_enterprise_settings.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE enterprisesettings ( + id bigserial PRIMARY KEY, + disable_device_management BOOLEAN NOT NULL DEFAULT false +); + +INSERT INTO enterprisesettings (disable_device_management) values (false); diff --git a/src/db/models/settings.rs b/src/db/models/settings.rs index 8946fe74a6..569db58efc 100644 --- a/src/db/models/settings.rs +++ b/src/db/models/settings.rs @@ -65,8 +65,6 @@ pub struct Settings { // Whether to create a new account when users try to log in with external OpenID pub openid_create_account: bool, pub license: Option, - // If true, only admins can create devices - pub disable_device_creation: bool, } impl Settings { @@ -124,7 +122,6 @@ pub struct SettingsEssentials { pub webhooks_enabled: bool, pub worker_enabled: bool, pub openid_enabled: bool, - pub disable_device_creation: bool, } impl SettingsEssentials { @@ -135,7 +132,7 @@ impl SettingsEssentials { query_as!( SettingsEssentials, "SELECT instance_name, main_logo_url, nav_logo_url, wireguard_enabled, \ - webhooks_enabled, worker_enabled, openid_enabled, disable_device_creation \ + webhooks_enabled, worker_enabled, openid_enabled \ FROM settings WHERE id = 1" ) .fetch_one(executor) @@ -153,7 +150,6 @@ impl From for SettingsEssentials { nav_logo_url: settings.nav_logo_url, instance_name: settings.instance_name, main_logo_url: settings.main_logo_url, - disable_device_creation: settings.disable_device_creation, } } } diff --git a/src/enterprise/db/models/enterprise_settings.rs b/src/enterprise/db/models/enterprise_settings.rs new file mode 100644 index 0000000000..71c7896c66 --- /dev/null +++ b/src/enterprise/db/models/enterprise_settings.rs @@ -0,0 +1,29 @@ +use model_derive::Model; +use sqlx::PgExecutor; +use struct_patch::Patch; + +use crate::enterprise::license::{get_cached_license, validate_license}; + +#[derive(Model, Deserialize, Serialize, Patch, Default)] +pub struct EnterpriseSettings { + pub id: Option, + // If true, only admins can manage devices + pub disable_device_management: bool, +} + +impl EnterpriseSettings { + /// If license is valid returns current [`EnterpriseSettings`] object. + /// Otherwise returns [`EnterpriseSettings::default()`]. + pub async fn get<'e, E>(executor: E) -> Result + where + E: PgExecutor<'e>, + { + let license = get_cached_license(); + if validate_license((*license).as_ref()).is_ok() { + let settings = Self::find_by_id(executor, 1).await?; + Ok(settings.expect("EnterpriseSettings not found")) + } else { + Ok(EnterpriseSettings::default()) + } + } +} diff --git a/src/enterprise/db/models/mod.rs b/src/enterprise/db/models/mod.rs index 972680e374..742323f1da 100644 --- a/src/enterprise/db/models/mod.rs +++ b/src/enterprise/db/models/mod.rs @@ -1 +1,2 @@ +pub mod enterprise_settings; pub mod openid_provider; diff --git a/src/enterprise/handlers/enterprise_settings.rs b/src/enterprise/handlers/enterprise_settings.rs new file mode 100644 index 0000000000..77fd0b2586 --- /dev/null +++ b/src/enterprise/handlers/enterprise_settings.rs @@ -0,0 +1,30 @@ +use axum::{extract::State, Json}; + +use crate::{ + appstate::AppState, + auth::{AdminRole, SessionInfo}, + enterprise::db::models::enterprise_settings::{EnterpriseSettings, EnterpriseSettingsPatch}, + handlers::{ApiResponse, ApiResult}, +}; +use struct_patch::Patch; + +use super::LicenseInfo; + +pub async fn patch_enterprise_settings( + _license: LicenseInfo, + _admin: AdminRole, + State(appstate): State, + session: SessionInfo, + Json(data): Json, +) -> ApiResult { + debug!( + "Admin {} patching enterprise settings.", + &session.user.username + ); + let mut settings = EnterpriseSettings::get(&appstate.pool).await?; + + settings.apply(data); + settings.save(&appstate.pool).await?; + info!("Admin {} patched settings.", &session.user.username); + Ok(ApiResponse::default()) +} diff --git a/src/enterprise/handlers/mod.rs b/src/enterprise/handlers/mod.rs index d926bf0bbf..039baeb8e1 100644 --- a/src/enterprise/handlers/mod.rs +++ b/src/enterprise/handlers/mod.rs @@ -3,6 +3,7 @@ use crate::{ handlers::{ApiResponse, ApiResult}, }; +pub mod enterprise_settings; pub mod openid_login; pub mod openid_providers; diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 68af097998..79bdbfcdf8 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -17,22 +17,15 @@ use uuid::Uuid; use super::{device_for_admin_or_self, user_for_admin_or_self, ApiResponse, ApiResult, WebError}; use crate::{ - appstate::AppState, - auth::{Claims, ClaimsType, SessionInfo, VpnRole}, - db::{ + appstate::AppState, auth::{Claims, ClaimsType, SessionInfo, VpnRole}, db::{ models::{ device::{ DeviceConfig, DeviceInfo, DeviceNetworkInfo, ModifyDevice, WireguardNetworkDevice, }, wireguard::{DateTimeAggregation, MappedDevice, WireguardNetworkInfo}, }, - AddDevice, DbPool, Device, GatewayEvent, Settings, WireguardNetwork, - }, - grpc::GatewayMap, - handlers::mail::send_new_device_added_email, - server_config, - templates::TemplateLocation, - wg_config::{parse_wireguard_config, ImportedDevice}, + AddDevice, DbPool, Device, GatewayEvent, WireguardNetwork, + }, enterprise::db::models::enterprise_settings::EnterpriseSettings, grpc::GatewayMap, handlers::mail::send_new_device_added_email, server_config, templates::TemplateLocation, wg_config::{parse_wireguard_config, ImportedDevice} }; #[derive(Deserialize, Serialize, ToSchema)] @@ -633,8 +626,8 @@ pub async fn add_device( /// Returns an error if current session user cannot manage devices. async fn can_manage_devices_or_error(pool: &DbPool, session: &SessionInfo) -> Result<(), WebError> { - let settings = Settings::get_settings(pool).await?; - if settings.disable_device_creation && !session.is_admin { + let settings = EnterpriseSettings::get(pool).await?; + if settings.disable_device_management && !session.is_admin { return Err(WebError::Forbidden( "Only admin users can manage devices".into(), )); From d1b5e201fdae0dc87bd2d878bad600aa524f280d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 20 Aug 2024 12:18:15 +0200 Subject: [PATCH 09/26] Enterprise settings routes, useApi hook --- .../handlers/enterprise_settings.rs | 27 ++++++++++++++++--- src/handlers/settings.rs | 4 +-- src/lib.rs | 17 +++++++----- web/src/shared/hooks/useApi.tsx | 3 +++ web/src/shared/types.ts | 10 +++---- 5 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/enterprise/handlers/enterprise_settings.rs b/src/enterprise/handlers/enterprise_settings.rs index 77fd0b2586..4966917947 100644 --- a/src/enterprise/handlers/enterprise_settings.rs +++ b/src/enterprise/handlers/enterprise_settings.rs @@ -1,4 +1,5 @@ -use axum::{extract::State, Json}; +use axum::{extract::State, http::StatusCode, Json}; +use serde_json::json; use crate::{ appstate::AppState, @@ -10,6 +11,26 @@ use struct_patch::Patch; use super::LicenseInfo; +pub async fn get_enterprise_settings( + session: SessionInfo, + State(appstate): State, +) -> ApiResult { + debug!( + "User {} retrieving enterprise settings", + session.user.username + ); + let settings = EnterpriseSettings::get(&appstate.pool).await?; + info!( + "User {} retrieved enterprise settings", + session.user.username + ); + Ok(ApiResponse { + json: json!(settings), + // json: json!({}), + status: StatusCode::OK, + }) +} + pub async fn patch_enterprise_settings( _license: LicenseInfo, _admin: AdminRole, @@ -19,12 +40,12 @@ pub async fn patch_enterprise_settings( ) -> ApiResult { debug!( "Admin {} patching enterprise settings.", - &session.user.username + session.user.username, ); let mut settings = EnterpriseSettings::get(&appstate.pool).await?; settings.apply(data); settings.save(&appstate.pool).await?; - info!("Admin {} patched settings.", &session.user.username); + info!("Admin {} patched settings.", session.user.username); Ok(ApiResponse::default()) } diff --git a/src/handlers/settings.rs b/src/handlers/settings.rs index 16c167d686..00282c7099 100644 --- a/src/handlers/settings.rs +++ b/src/handlers/settings.rs @@ -108,7 +108,7 @@ pub async fn patch_settings( session: SessionInfo, Json(data): Json, ) -> ApiResult { - debug!("Admin {} patching settings.", &session.user.username); + debug!("Admin {} patching settings.", session.user.username); let mut settings = Settings::get_settings(&appstate.pool).await?; // Handle updating the cached license @@ -119,7 +119,7 @@ pub async fn patch_settings( settings.apply(data); settings.save(&appstate.pool).await?; - info!("Admin {} patched settings.", &session.user.username); + info!("Admin {} patched settings.", session.user.username); Ok(ApiResponse::default()) } diff --git a/src/lib.rs b/src/lib.rs index 224a31cfee..6545bbd8dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,15 +13,16 @@ use axum::{ }; use enterprise::handlers::{ check_enterprise_status, + enterprise_settings::{get_enterprise_settings, patch_enterprise_settings}, openid_login::{auth_callback, get_auth_info}, openid_providers::{add_openid_provider, delete_openid_provider, get_current_openid_provider}, }; -use handlers::ssh_authorized_keys::{ - add_authentication_key, delete_authentication_key, fetch_authentication_keys, -}; use handlers::{ group::{bulk_assign_to_groups, list_groups_info}, - ssh_authorized_keys::rename_authentication_key, + ssh_authorized_keys::{ + add_authentication_key, delete_authentication_key, fetch_authentication_keys, + rename_authentication_key, + }, yubikey::{delete_yubikey, rename_yubikey}, }; use ipnetwork::IpNetwork; @@ -157,10 +158,10 @@ mod openapi { AddDevice, UserDetails, UserInfo, }; use error::WebError; - use handlers::wireguard as device; use handlers::{ group::{self, BulkAssignToGroupsRequest, Groups}, user::{self, WalletInfoShort}, + wireguard as device, wireguard::AddDeviceResult, ApiResponse, EditGroupInfo, GroupInfo, PasswordChange, PasswordChangeSelf, StartEnrollmentRequest, Username, WalletChange, WalletSignature, @@ -406,11 +407,15 @@ pub fn build_webapp( let webapp = webapp.nest( "/api/v1/openid", Router::new() + // OpenID .route("/provider", get(get_current_openid_provider)) .route("/provider", post(add_openid_provider)) .route("/provider/:name", delete(delete_openid_provider)) .route("/callback", post(auth_callback)) - .route("/auth_info", get(get_auth_info)), + .route("/auth_info", get(get_auth_info)) + // Settings + .route("/enterprise_settings", get(get_enterprise_settings)) + .route("/enterprise_settings", patch(patch_enterprise_settings)), ); let webapp = webapp.route("/api/v1/enterprise_status", get(check_enterprise_status)); diff --git a/web/src/shared/hooks/useApi.tsx b/web/src/shared/hooks/useApi.tsx index 7b66824ce1..200d843946 100644 --- a/web/src/shared/hooks/useApi.tsx +++ b/web/src/shared/hooks/useApi.tsx @@ -429,6 +429,9 @@ const useApi = (props?: HookProps): ApiHook => { const getEssentialSettings: ApiHook['settings']['getEssentialSettings'] = () => client.get('/settings_essentials').then(unpackRequest); + const getEnterpriseSettings: ApiHook['settings']['getEnterpriseSettings'] = () => + client.get('/settings_essentials').then(unpackRequest); + const testLdapSettings: ApiHook['settings']['testLdapSettings'] = () => client.get('/ldap/test').then(unpackRequest); diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index b0742be576..8ccd6684df 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -586,6 +586,7 @@ export interface ApiHook { setDefaultBranding: (id: string) => Promise; patchSettings: (data: Partial) => EmptyApiResponse; getEssentialSettings: () => Promise; + getEnterpriseSettings: () => Promise; testLdapSettings: () => Promise; fetchOpenIdProviders: () => Promise; addOpenIdProvider: (data: OpenIdProvider) => Promise; @@ -810,11 +811,10 @@ export type Settings = SettingsModules & SettingsBranding & SettingsLDAP & SettingsOpenID & - SettingsLicense & - SettingsPermissions; + SettingsLicense; // essentials for core frontend, includes only those that are required for frontend operations -export type SettingsEssentials = SettingsModules & SettingsBranding & SettingsPermissions; +export type SettingsEssentials = SettingsModules & SettingsBranding; export type SettingsEnrollment = { enrollment_vpn_step_optional: boolean; @@ -872,8 +872,8 @@ export type SettingsLicense = { license: string; }; -export type SettingsPermissions = { - disable_device_creation: boolean; +export type SettingsEnterprise = { + disable_device_management: boolean; }; export interface Webhook { From c2b06415265c89b4ba9baee7bdf6e96e9c0c271c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Tue, 20 Aug 2024 14:41:05 +0200 Subject: [PATCH 10/26] Fix compilation --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- .../db/models/enterprise_settings.rs | 8 ++++++-- .../handlers/enterprise_settings.rs | 5 ++--- src/enterprise/handlers/mod.rs | 2 +- src/handlers/wireguard.rs | 19 ++++++++++++++----- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f6e4c0170..94fb6cf043 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4848,18 +4848,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "struct-patch" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084489a427e3470993d4008c935a3d3f10936d520c5b2a527da392bb1e604eee" +checksum = "af1baa236355594336eb27127ae98c55ced19a6611456dd3bfe445b496e3b3e6" dependencies = [ "struct-patch-derive", ] [[package]] name = "struct-patch-derive" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e542f637a488f779cf52b27208dbc425dbb858a6ddbc9e77087d268f72494b3" +checksum = "10449466e3f3aa069ad534cbce19f38f2eaae74cdfdfbacc64d6afd99e36d7cf" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d3d7d8f639..19062b78f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,7 @@ sqlx = { version = "0.7", features = [ "uuid", ] } ssh-key = "0.6" -struct-patch = "0.7" +struct-patch = "0.8" tera = "1.20" thiserror = "1.0" # match axum-extra -> cookies diff --git a/src/enterprise/db/models/enterprise_settings.rs b/src/enterprise/db/models/enterprise_settings.rs index 71c7896c66..dadd5211cd 100644 --- a/src/enterprise/db/models/enterprise_settings.rs +++ b/src/enterprise/db/models/enterprise_settings.rs @@ -5,6 +5,7 @@ use struct_patch::Patch; use crate::enterprise::license::{get_cached_license, validate_license}; #[derive(Model, Deserialize, Serialize, Patch, Default)] +#[patch(attribute(derive(Serialize, Deserialize)))] pub struct EnterpriseSettings { pub id: Option, // If true, only admins can manage devices @@ -18,8 +19,11 @@ impl EnterpriseSettings { where E: PgExecutor<'e>, { - let license = get_cached_license(); - if validate_license((*license).as_ref()).is_ok() { + let is_valid = { + let license = get_cached_license(); + validate_license(license.as_ref()).is_ok() + }; + if is_valid { let settings = Self::find_by_id(executor, 1).await?; Ok(settings.expect("EnterpriseSettings not found")) } else { diff --git a/src/enterprise/handlers/enterprise_settings.rs b/src/enterprise/handlers/enterprise_settings.rs index 4966917947..84d56117fc 100644 --- a/src/enterprise/handlers/enterprise_settings.rs +++ b/src/enterprise/handlers/enterprise_settings.rs @@ -1,15 +1,14 @@ use axum::{extract::State, http::StatusCode, Json}; use serde_json::json; +use struct_patch::Patch; +use super::LicenseInfo; use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, enterprise::db::models::enterprise_settings::{EnterpriseSettings, EnterpriseSettingsPatch}, handlers::{ApiResponse, ApiResult}, }; -use struct_patch::Patch; - -use super::LicenseInfo; pub async fn get_enterprise_settings( session: SessionInfo, diff --git a/src/enterprise/handlers/mod.rs b/src/enterprise/handlers/mod.rs index 039baeb8e1..4dea48b5b5 100644 --- a/src/enterprise/handlers/mod.rs +++ b/src/enterprise/handlers/mod.rs @@ -31,7 +31,7 @@ where async fn from_request_parts(_parts: &mut Parts, _state: &S) -> Result { let license = get_cached_license(); - match validate_license((*license).as_ref()) { + match validate_license(license.as_ref()) { // Useless struct, but may come in handy later Ok(_) => Ok(LicenseInfo { valid: true }), Err(e) => Err(WebError::Forbidden(e.to_string())), diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 79bdbfcdf8..11ebccf275 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -17,7 +17,9 @@ use uuid::Uuid; use super::{device_for_admin_or_self, user_for_admin_or_self, ApiResponse, ApiResult, WebError}; use crate::{ - appstate::AppState, auth::{Claims, ClaimsType, SessionInfo, VpnRole}, db::{ + appstate::AppState, + auth::{Claims, ClaimsType, SessionInfo, VpnRole}, + db::{ models::{ device::{ DeviceConfig, DeviceInfo, DeviceNetworkInfo, ModifyDevice, WireguardNetworkDevice, @@ -25,7 +27,13 @@ use crate::{ wireguard::{DateTimeAggregation, MappedDevice, WireguardNetworkInfo}, }, AddDevice, DbPool, Device, GatewayEvent, WireguardNetwork, - }, enterprise::db::models::enterprise_settings::EnterpriseSettings, grpc::GatewayMap, handlers::mail::send_new_device_added_email, server_config, templates::TemplateLocation, wg_config::{parse_wireguard_config, ImportedDevice} + }, + enterprise::db::models::enterprise_settings::EnterpriseSettings, + grpc::GatewayMap, + handlers::mail::send_new_device_added_email, + server_config, + templates::TemplateLocation, + wg_config::{parse_wireguard_config, ImportedDevice}, }; #[derive(Deserialize, Serialize, ToSchema)] @@ -628,11 +636,12 @@ pub async fn add_device( async fn can_manage_devices_or_error(pool: &DbPool, session: &SessionInfo) -> Result<(), WebError> { let settings = EnterpriseSettings::get(pool).await?; if settings.disable_device_management && !session.is_admin { - return Err(WebError::Forbidden( + Err(WebError::Forbidden( "Only admin users can manage devices".into(), - )); + )) + } else { + Ok(()) } - Ok(()) } /// Modify device From 84a492edb2a5eb4f8e2d5e85b638baabce4fe156 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 20 Aug 2024 14:28:41 +0200 Subject: [PATCH 11/26] Use RwLock for License, fix patch_enterprise_settings endpoint --- src/enterprise/license.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/enterprise/license.rs b/src/enterprise/license.rs index 4cffd1ea3e..e2521d95ff 100644 --- a/src/enterprise/license.rs +++ b/src/enterprise/license.rs @@ -1,4 +1,4 @@ -use std::sync::{Mutex, MutexGuard}; +use std::sync::{RwLock, RwLockReadGuard}; use anyhow::Result; use base64::prelude::*; @@ -14,17 +14,17 @@ use crate::{ server_config, }; -static LICENSE: Mutex> = Mutex::new(None); +static LICENSE: RwLock> = RwLock::new(None); pub fn set_cached_license(license: Option) { *LICENSE - .lock() + .write() .expect("Failed to acquire lock on the license mutex.") = license; } -pub fn get_cached_license() -> MutexGuard<'static, Option> { +pub fn get_cached_license() -> RwLockReadGuard<'static, Option> { LICENSE - .lock() + .read() .expect("Failed to acquire lock on the license mutex.") } From 86c590184f736e60c6e56e22e2a4611176f83f97 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 20 Aug 2024 15:49:17 +0200 Subject: [PATCH 12/26] Move enterprise settings entpoints, update frontend --- .../db/models/enterprise_settings.rs | 1 + .../handlers/enterprise_settings.rs | 1 - src/lib.rs | 8 ++--- web/src/components/AppLoader.tsx | 14 +++++++- .../components/PermissionsForm.tsx | 32 +++++++++++-------- .../UserProfile/UserDevices/UserDevices.tsx | 5 +-- web/src/shared/hooks/store/useAppStore.ts | 4 ++- web/src/shared/hooks/useApi.tsx | 8 ++++- web/src/shared/queries.ts | 1 + web/src/shared/types.ts | 3 +- 10 files changed, 52 insertions(+), 25 deletions(-) diff --git a/src/enterprise/db/models/enterprise_settings.rs b/src/enterprise/db/models/enterprise_settings.rs index dadd5211cd..443ef7f58b 100644 --- a/src/enterprise/db/models/enterprise_settings.rs +++ b/src/enterprise/db/models/enterprise_settings.rs @@ -7,6 +7,7 @@ use crate::enterprise::license::{get_cached_license, validate_license}; #[derive(Model, Deserialize, Serialize, Patch, Default)] #[patch(attribute(derive(Serialize, Deserialize)))] pub struct EnterpriseSettings { + #[serde(skip)] pub id: Option, // If true, only admins can manage devices pub disable_device_management: bool, diff --git a/src/enterprise/handlers/enterprise_settings.rs b/src/enterprise/handlers/enterprise_settings.rs index 84d56117fc..07442b9b31 100644 --- a/src/enterprise/handlers/enterprise_settings.rs +++ b/src/enterprise/handlers/enterprise_settings.rs @@ -25,7 +25,6 @@ pub async fn get_enterprise_settings( ); Ok(ApiResponse { json: json!(settings), - // json: json!({}), status: StatusCode::OK, }) } diff --git a/src/lib.rs b/src/lib.rs index 6545bbd8dc..930bb1f195 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -389,6 +389,9 @@ pub fn build_webapp( .route("/settings/:id", put(set_default_branding)) // settings for frontend .route("/settings_essentials", get(get_settings_essentials)) + // enterprise settings + .route("/settings_enterprise", get(get_enterprise_settings)) + .route("/settings_enterprise", patch(patch_enterprise_settings)) // support .route("/support/configuration", get(configuration)) .route("/support/logs", get(logs)) @@ -412,10 +415,7 @@ pub fn build_webapp( .route("/provider", post(add_openid_provider)) .route("/provider/:name", delete(delete_openid_provider)) .route("/callback", post(auth_callback)) - .route("/auth_info", get(get_auth_info)) - // Settings - .route("/enterprise_settings", get(get_enterprise_settings)) - .route("/enterprise_settings", patch(patch_enterprise_settings)), + .route("/auth_info", get(get_auth_info)), ); let webapp = webapp.route("/api/v1/enterprise_status", get(check_enterprise_status)); diff --git a/web/src/components/AppLoader.tsx b/web/src/components/AppLoader.tsx index c0a4726cf4..2ff5456120 100644 --- a/web/src/components/AppLoader.tsx +++ b/web/src/components/AppLoader.tsx @@ -30,7 +30,7 @@ export const AppLoader = () => { getAppInfo, getEnterpriseStatus, user: { getMe }, - settings: { getEssentialSettings }, + settings: { getEssentialSettings, getEnterpriseSettings }, } = useApi(); const [userLoading, setUserLoading] = useState(true); const { setLocale } = useI18nContext(); @@ -81,6 +81,18 @@ export const AppLoader = () => { retry: false, }); + useQuery([QueryKeys.FETCH_ENTERPRISE_SETTINGS], getEnterpriseSettings, { + onSuccess: (settings) => { + setAppStore({ enterprise_settings: settings }); + }, + onError: (err) => { + // FIXME: Add a proper error message + toaster.error(LL.messages.errorVersion()); + console.error(err); + }, + refetchOnWindowFocus: false, + retry: false, + }); const { isLoading: settingsLoading, data: essentialSettings } = useQuery( [QueryKeys.FETCH_ESSENTIAL_SETTINGS], getEssentialSettings, diff --git a/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx b/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx index e38287c3c2..179f9092d5 100644 --- a/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx +++ b/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx @@ -8,33 +8,37 @@ import { useI18nContext } from '../../../../../i18n/i18n-react'; import { Card } from '../../../../../shared/defguard-ui/components/Layout/Card/Card'; import { Helper } from '../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; import { LabeledCheckbox } from '../../../../../shared/defguard-ui/components/Layout/LabeledCheckbox/LabeledCheckbox'; +import { useAppStore } from '../../../../../shared/hooks/store/useAppStore'; import useApi from '../../../../../shared/hooks/useApi'; import { useToaster } from '../../../../../shared/hooks/useToaster'; import { MutationKeys } from '../../../../../shared/mutations'; import { QueryKeys } from '../../../../../shared/queries'; -import { useSettingsPage } from '../../../hooks/useSettingsPage'; export const PermissionsForm = () => { const { LL } = useI18nContext(); const toaster = useToaster(); const { - settings: { patchSettings }, + settings: { patchEnterpriseSettings }, } = useApi(); - const settings = useSettingsPage((state) => state.settings); + const settings = useAppStore((state) => state.enterprise_settings); const queryClient = useQueryClient(); - const { mutate, isLoading } = useMutation([MutationKeys.EDIT_SETTINGS], patchSettings, { - onSuccess: () => { - queryClient.invalidateQueries([QueryKeys.FETCH_SETTINGS]); - toaster.success(LL.settingsPage.messages.editSuccess()); + const { mutate, isLoading } = useMutation( + [MutationKeys.EDIT_SETTINGS], + patchEnterpriseSettings, + { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.FETCH_ENTERPRISE_SETTINGS]); + toaster.success(LL.settingsPage.messages.editSuccess()); + }, + onError: (err: AxiosError) => { + toaster.error(LL.messages.error()); + console.error(err); + }, }, - onError: (err: AxiosError) => { - toaster.error(LL.messages.error()); - console.error(err); - }, - }); + ); if (!settings) return null; @@ -48,9 +52,9 @@ export const PermissionsForm = () => { - mutate({ disable_device_creation: !settings.disable_device_creation }) + mutate({ disable_device_management: !settings.disable_device_management }) } /> diff --git a/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx b/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx index 14495742d9..a270739b78 100644 --- a/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx +++ b/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx @@ -17,13 +17,14 @@ import { EditUserDeviceModal } from './modals/EditUserDeviceModal/EditUserDevice export const UserDevices = () => { const navigate = useNavigate(); const appInfo = useAppStore((state) => state.appInfo); - const settings = useAppStore((state) => state.settings); + const settings = useAppStore((state) => state.enterprise_settings); const { LL } = useI18nContext(); const userProfile = useUserProfileStore((state) => state.userProfile); const initAddDevice = useAddDevicePageStore((state) => state.init); + console.log(settings?.disable_device_management); const canManageDevices = !!( userProfile && - (!settings?.disable_device_creation || isUserAdmin(userProfile.user)) + (!settings?.disable_device_management || isUserAdmin(userProfile.user)) ); return ( diff --git a/web/src/shared/hooks/store/useAppStore.ts b/web/src/shared/hooks/store/useAppStore.ts index c978975759..7b53bc151d 100644 --- a/web/src/shared/hooks/store/useAppStore.ts +++ b/web/src/shared/hooks/store/useAppStore.ts @@ -3,13 +3,14 @@ import { createJSONStorage, persist } from 'zustand/middleware'; import { createWithEqualityFn } from 'zustand/traditional'; import { Locales } from '../../../i18n/i18n-types'; -import { AppInfo, SettingsEssentials } from '../../types'; +import { AppInfo, SettingsEnterprise, SettingsEssentials } from '../../types'; const defaultValues: StoreValues = { settings: undefined, language: undefined, appInfo: undefined, enterprise_enabled: false, + enterprise_settings: undefined, }; const persistKeys: Array = ['language']; @@ -37,6 +38,7 @@ type StoreValues = { language?: Locales; appInfo?: AppInfo; enterprise_enabled?: boolean; + enterprise_settings?: SettingsEnterprise; }; type StoreMethods = { diff --git a/web/src/shared/hooks/useApi.tsx b/web/src/shared/hooks/useApi.tsx index 200d843946..659340bbc1 100644 --- a/web/src/shared/hooks/useApi.tsx +++ b/web/src/shared/hooks/useApi.tsx @@ -430,7 +430,11 @@ const useApi = (props?: HookProps): ApiHook => { client.get('/settings_essentials').then(unpackRequest); const getEnterpriseSettings: ApiHook['settings']['getEnterpriseSettings'] = () => - client.get('/settings_essentials').then(unpackRequest); + client.get('/settings_enterprise').then(unpackRequest); + + const patchEnterpriseSettings: ApiHook['settings']['patchEnterpriseSettings'] = ( + data, + ) => client.patch('/settings_enterprise', data).then(unpackRequest); const testLdapSettings: ApiHook['settings']['testLdapSettings'] = () => client.get('/ldap/test').then(unpackRequest); @@ -631,6 +635,8 @@ const useApi = (props?: HookProps): ApiHook => { setDefaultBranding: setDefaultBranding, patchSettings, getEssentialSettings, + getEnterpriseSettings, + patchEnterpriseSettings, testLdapSettings, fetchOpenIdProviders: fetchOpenIdProvider, addOpenIdProvider, diff --git a/web/src/shared/queries.ts b/web/src/shared/queries.ts index 7834db9ad0..0d539d21ad 100644 --- a/web/src/shared/queries.ts +++ b/web/src/shared/queries.ts @@ -29,4 +29,5 @@ export const QueryKeys = { FETCH_OPENID_PROVIDERS: 'FETCH_OPENID_PROVIDERS', FETCH_OPENID_INFO: 'FETCH_OPENID_INFO', FETCH_ENTERPRISE_STATUS: 'FETCH_ENTERPRISE_STATUS', + FETCH_ENTERPRISE_SETTINGS: 'FETCH_ENTERPRISE_SETTINGS', }; diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 8ccd6684df..3ef2b2a3c9 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -586,7 +586,8 @@ export interface ApiHook { setDefaultBranding: (id: string) => Promise; patchSettings: (data: Partial) => EmptyApiResponse; getEssentialSettings: () => Promise; - getEnterpriseSettings: () => Promise; + getEnterpriseSettings: () => Promise; + patchEnterpriseSettings: (data: Partial) => EmptyApiResponse; testLdapSettings: () => Promise; fetchOpenIdProviders: () => Promise; addOpenIdProvider: (data: OpenIdProvider) => Promise; From 985d48b2325a6aee08c02e02d2c44affa25be4a8 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 09:00:52 +0200 Subject: [PATCH 13/26] Remove unnecessary dereferences --- src/enterprise/handlers/mod.rs | 2 +- src/handlers/app_info.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/enterprise/handlers/mod.rs b/src/enterprise/handlers/mod.rs index 4dea48b5b5..8869493a90 100644 --- a/src/enterprise/handlers/mod.rs +++ b/src/enterprise/handlers/mod.rs @@ -42,7 +42,7 @@ where pub async fn check_enterprise_status() -> ApiResult { let license = get_cached_license(); - let valid = validate_license((*license).as_ref()).is_ok(); + let valid = validate_license((license).as_ref()).is_ok(); Ok(ApiResponse { json: serde_json::json!({ "enabled": valid }), diff --git a/src/handlers/app_info.rs b/src/handlers/app_info.rs index dc7f387e42..c0f60f693d 100644 --- a/src/handlers/app_info.rs +++ b/src/handlers/app_info.rs @@ -26,7 +26,7 @@ pub(crate) async fn get_app_info( let networks = WireguardNetwork::all(&appstate.pool).await?; let settings = Settings::get_settings(&appstate.pool).await?; let license = get_cached_license(); - let enterprise = validate_license((*license).as_ref()).is_ok(); + let enterprise = validate_license((license).as_ref()).is_ok(); let res = AppInfo { network_present: !networks.is_empty(), smtp_enabled: settings.smtp_configured(), From 8e16a1d846e964fe3ded4d3c381a5707a24c4545 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 09:13:47 +0200 Subject: [PATCH 14/26] Don't derive default enterprise settings --- src/enterprise/db/models/enterprise_settings.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/enterprise/db/models/enterprise_settings.rs b/src/enterprise/db/models/enterprise_settings.rs index 443ef7f58b..d61a09a068 100644 --- a/src/enterprise/db/models/enterprise_settings.rs +++ b/src/enterprise/db/models/enterprise_settings.rs @@ -4,7 +4,7 @@ use struct_patch::Patch; use crate::enterprise::license::{get_cached_license, validate_license}; -#[derive(Model, Deserialize, Serialize, Patch, Default)] +#[derive(Model, Deserialize, Serialize, Patch)] #[patch(attribute(derive(Serialize, Deserialize)))] pub struct EnterpriseSettings { #[serde(skip)] @@ -13,6 +13,17 @@ pub struct EnterpriseSettings { pub disable_device_management: bool, } +// We want to be conscious of what the defaults are here +#[allow(clippy::derivable_impls)] +impl Default for EnterpriseSettings { + fn default() -> Self { + Self { + id: None, + disable_device_management: false, + } + } +} + impl EnterpriseSettings { /// If license is valid returns current [`EnterpriseSettings`] object. /// Otherwise returns [`EnterpriseSettings::default()`]. From 80b6f30eaedc1ec96e214abf29d73a918b346ab5 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 09:17:24 +0200 Subject: [PATCH 15/26] Rename disable_device_management -> admin_device_management --- migrations/20240819134151_enterprise_settings.up.sql | 4 ++-- src/enterprise/db/models/enterprise_settings.rs | 4 ++-- src/handlers/wireguard.rs | 2 +- .../PermissionsSettings/components/PermissionsForm.tsx | 4 ++-- web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx | 3 +-- web/src/shared/types.ts | 2 +- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/migrations/20240819134151_enterprise_settings.up.sql b/migrations/20240819134151_enterprise_settings.up.sql index a0b6cb6388..4db6f235ef 100644 --- a/migrations/20240819134151_enterprise_settings.up.sql +++ b/migrations/20240819134151_enterprise_settings.up.sql @@ -1,6 +1,6 @@ CREATE TABLE enterprisesettings ( id bigserial PRIMARY KEY, - disable_device_management BOOLEAN NOT NULL DEFAULT false + admin_device_management BOOLEAN NOT NULL DEFAULT false ); -INSERT INTO enterprisesettings (disable_device_management) values (false); +INSERT INTO enterprisesettings (admin_device_management) values (false); diff --git a/src/enterprise/db/models/enterprise_settings.rs b/src/enterprise/db/models/enterprise_settings.rs index d61a09a068..59bfb2cc35 100644 --- a/src/enterprise/db/models/enterprise_settings.rs +++ b/src/enterprise/db/models/enterprise_settings.rs @@ -10,7 +10,7 @@ pub struct EnterpriseSettings { #[serde(skip)] pub id: Option, // If true, only admins can manage devices - pub disable_device_management: bool, + pub admin_device_management: bool, } // We want to be conscious of what the defaults are here @@ -19,7 +19,7 @@ impl Default for EnterpriseSettings { fn default() -> Self { Self { id: None, - disable_device_management: false, + admin_device_management: false, } } } diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 11ebccf275..cfe4360739 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -635,7 +635,7 @@ pub async fn add_device( /// Returns an error if current session user cannot manage devices. async fn can_manage_devices_or_error(pool: &DbPool, session: &SessionInfo) -> Result<(), WebError> { let settings = EnterpriseSettings::get(pool).await?; - if settings.disable_device_management && !session.is_admin { + if settings.admin_device_management && !session.is_admin { Err(WebError::Forbidden( "Only admin users can manage devices".into(), )) diff --git a/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx b/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx index 179f9092d5..92f00362aa 100644 --- a/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx +++ b/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx @@ -52,9 +52,9 @@ export const PermissionsForm = () => { - mutate({ disable_device_management: !settings.disable_device_management }) + mutate({ admin_device_management: !settings.disable_device_management }) } /> diff --git a/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx b/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx index a270739b78..9280d473e3 100644 --- a/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx +++ b/web/src/pages/users/UserProfile/UserDevices/UserDevices.tsx @@ -21,10 +21,9 @@ export const UserDevices = () => { const { LL } = useI18nContext(); const userProfile = useUserProfileStore((state) => state.userProfile); const initAddDevice = useAddDevicePageStore((state) => state.init); - console.log(settings?.disable_device_management); const canManageDevices = !!( userProfile && - (!settings?.disable_device_management || isUserAdmin(userProfile.user)) + (!settings?.admin_device_management || isUserAdmin(userProfile.user)) ); return ( diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index 3ef2b2a3c9..ba2df30198 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -874,7 +874,7 @@ export type SettingsLicense = { }; export type SettingsEnterprise = { - disable_device_management: boolean; + admin_device_management: boolean; }; export interface Webhook { From 584fb109e8d8c1720d47934b5ad05d0253b17b3d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 09:35:09 +0200 Subject: [PATCH 16/26] Rename permissions -> behaviour --- web/src/i18n/en/index.ts | 14 ++++---- web/src/i18n/i18n-types.ts | 32 +++++++++---------- web/src/i18n/pl/index.ts | 14 ++++---- web/src/pages/settings/SettingsPage.tsx | 6 ++-- .../BehaviourSettings.tsx} | 6 ++-- .../components/BehaviourForm.tsx} | 12 +++---- .../components/styles.scss | 0 7 files changed, 42 insertions(+), 42 deletions(-) rename web/src/pages/settings/components/{PermissionsSettings/PermissionsSettings.tsx => BehaviourSettings/BehaviourSettings.tsx} (86%) rename web/src/pages/settings/components/{PermissionsSettings/components/PermissionsForm.tsx => BehaviourSettings/components/BehaviourForm.tsx} (83%) rename web/src/pages/settings/components/{PermissionsSettings => BehaviourSettings}/components/styles.scss (100%) diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index 4fb352b922..2f527eab09 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -892,7 +892,7 @@ const en: BaseTranslation = { global: 'Global settings', ldap: 'LDAP', openid: 'OpenID', - permissions: 'Permissions', + behaviour: 'Behaviour', }, messages: { editSuccess: 'Settings updated', @@ -1181,14 +1181,14 @@ const en: BaseTranslation = { }, }, }, - permissions: { - header: 'Permissions', - helper: '

Here you can change basic user permissions.

', + behaviour: { + header: 'Behaviour', + helper: '

Here you can change app behaviour.

', fields: { - deviceCreation: { - label: 'Disable users ability to add their own devices', + deviceManagement: { + label: 'Disable users ability to manage their devices', helper: - "When this option is enabled, only users in the Admin group can add devices in user profile (it's disabled for all other users)", + "When this option is enabled, only users in the Admin group can manage devices in user profile (it's disabled for all other users)", }, }, }, diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index e81e415d91..8a98e1860e 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -2199,9 +2199,9 @@ type RootTranslation = { */ openid: string /** - * P​e​r​m​i​s​s​i​o​n​s + * B​e​h​a​v​i​o​u​r */ - permissions: string + behaviour: string } messages: { /** @@ -2805,23 +2805,23 @@ type RootTranslation = { } } } - permissions: { + behaviour: { /** - * P​e​r​m​i​s​s​i​o​n​s + * B​e​h​a​v​i​o​u​r */ header: string /** - * <​p​>​H​e​r​e​ ​y​o​u​ ​c​a​n​ ​c​h​a​n​g​e​ ​b​a​s​i​c​ ​u​s​e​r​ ​p​e​r​m​i​s​s​i​o​n​s​.​<​/​p​> + * <​p​>​H​e​r​e​ ​y​o​u​ ​c​a​n​ ​c​h​a​n​g​e​ ​a​p​p​ ​b​e​h​a​v​i​o​u​r​.​<​/​p​> */ helper: string fields: { - deviceCreation: { + deviceManagement: { /** - * D​i​s​a​b​l​e​ ​u​s​e​r​s​ ​a​b​i​l​i​t​y​ ​t​o​ ​a​d​d​ ​t​h​e​i​r​ ​o​w​n​ ​d​e​v​i​c​e​s + * D​i​s​a​b​l​e​ ​u​s​e​r​s​ ​a​b​i​l​i​t​y​ ​t​o​ ​m​a​n​a​g​e​ ​t​h​e​i​r​ ​d​e​v​i​c​e​s */ label: string /** - * W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​o​n​l​y​ ​u​s​e​r​s​ ​i​n​ ​t​h​e​ ​A​d​m​i​n​ ​g​r​o​u​p​ ​c​a​n​ ​a​d​d​ ​d​e​v​i​c​e​s​ ​i​n​ ​u​s​e​r​ ​p​r​o​f​i​l​e​ ​(​i​t​'​s​ ​d​i​s​a​b​l​e​d​ ​f​o​r​ ​a​l​l​ ​o​t​h​e​r​ ​u​s​e​r​s​) + * W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​o​n​l​y​ ​u​s​e​r​s​ ​i​n​ ​t​h​e​ ​A​d​m​i​n​ ​g​r​o​u​p​ ​c​a​n​ ​m​a​n​a​g​e​ ​d​e​v​i​c​e​s​ ​i​n​ ​u​s​e​r​ ​p​r​o​f​i​l​e​ ​(​i​t​'​s​ ​d​i​s​a​b​l​e​d​ ​f​o​r​ ​a​l​l​ ​o​t​h​e​r​ ​u​s​e​r​s​) */ helper: string } @@ -6322,9 +6322,9 @@ export type TranslationFunctions = { */ openid: () => LocalizedString /** - * Permissions + * Behaviour */ - permissions: () => LocalizedString + behaviour: () => LocalizedString } messages: { /** @@ -6925,23 +6925,23 @@ export type TranslationFunctions = { } } } - permissions: { + behaviour: { /** - * Permissions + * Behaviour */ header: () => LocalizedString /** - *

Here you can change basic user permissions.

+ *

Here you can change app behaviour.

*/ helper: () => LocalizedString fields: { - deviceCreation: { + deviceManagement: { /** - * Disable users ability to add their own devices + * Disable users ability to manage their devices */ label: () => LocalizedString /** - * When this option is enabled, only users in the Admin group can add devices in user profile (it's disabled for all other users) + * When this option is enabled, only users in the Admin group can manage devices in user profile (it's disabled for all other users) */ helper: () => LocalizedString } diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 4d23483fd3..73170d62b4 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -879,7 +879,7 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe global: 'Globalne', ldap: 'LDAP', openid: 'OpenID', - permissions: 'Uprawnienia', + behaviour: 'Zachowanie', }, messages: { editSuccess: 'Ustawienia zaktualizowane.', @@ -1168,14 +1168,14 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe }, }, }, - permissions: { - header: 'Uprawnienia', - helper: '

Tutaj możesz zmienić podstawowe uprawnienia użytkowników.

', + behaviour: { + header: 'Zachowanie', + helper: '

Tutaj możesz zmienić zachowanie aplikacji.

', fields: { - deviceCreation: { - label: 'Zablokuj możliwość tworzenia urządzeń przez użytkowników', + deviceManagement: { + label: 'Zablokuj możliwość zarządzania urządzeniami przez użytkowników', helper: - 'Kiedy ta opcja jest włączona, tylko użytkownicy w grupie "Admin" mogą dodawać urządzenia w profilu użytkownika', + 'Kiedy ta opcja jest włączona, tylko użytkownicy w grupie "Admin" mogą zarządzać urządzeniami w profilu użytkownika', }, }, }, diff --git a/web/src/pages/settings/SettingsPage.tsx b/web/src/pages/settings/SettingsPage.tsx index b3001c88ee..09ddd4b296 100644 --- a/web/src/pages/settings/SettingsPage.tsx +++ b/web/src/pages/settings/SettingsPage.tsx @@ -12,10 +12,10 @@ import { CardTabsData } from '../../shared/defguard-ui/components/Layout/CardTab import { LoaderSpinner } from '../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; import useApi from '../../shared/hooks/useApi'; import { QueryKeys } from '../../shared/queries'; +import { BehaviourSettings } from './components/BehaviourSettings/BehaviourSettings'; import { GlobalSettings } from './components/GlobalSettings/GlobalSettings'; import { LdapSettings } from './components/LdapSettings/LdapSettings'; import { OpenIdSettings } from './components/OpenIdSettings/OpenIdSettings'; -import { PermissionsSettings } from './components/PermissionsSettings/PermissionsSettings'; import { SmtpSettings } from './components/SmtpSettings/SmtpSettings'; import { useSettingsPage } from './hooks/useSettingsPage'; @@ -24,7 +24,7 @@ const tabsContent: ReactNode[] = [ , , , - , + , ]; export const SettingsPage = () => { @@ -77,7 +77,7 @@ export const SettingsPage = () => { }, { key: 4, - content: LL.settingsPage.tabs.permissions(), + content: LL.settingsPage.tabs.behaviour(), active: activeCard === 4, onClick: () => setActiveCard(4), }, diff --git a/web/src/pages/settings/components/PermissionsSettings/PermissionsSettings.tsx b/web/src/pages/settings/components/BehaviourSettings/BehaviourSettings.tsx similarity index 86% rename from web/src/pages/settings/components/PermissionsSettings/PermissionsSettings.tsx rename to web/src/pages/settings/components/BehaviourSettings/BehaviourSettings.tsx index 3d06aaa500..28c9f9ffc5 100644 --- a/web/src/pages/settings/components/PermissionsSettings/PermissionsSettings.tsx +++ b/web/src/pages/settings/components/BehaviourSettings/BehaviourSettings.tsx @@ -1,8 +1,8 @@ import { useI18nContext } from '../../../../i18n/i18n-react'; import { useAppStore } from '../../../../shared/hooks/store/useAppStore'; -import { PermissionsForm } from './components/PermissionsForm'; +import { BehaviourForm } from './components/BehaviourForm'; -export const PermissionsSettings = () => { +export const BehaviourSettings = () => { const enterpriseEnabled = useAppStore((state) => state.enterprise_enabled); const { LL } = useI18nContext(); const localLL = LL.settingsPage.enterpriseOnly; @@ -25,7 +25,7 @@ export const PermissionsSettings = () => { )}
- +
diff --git a/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx b/web/src/pages/settings/components/BehaviourSettings/components/BehaviourForm.tsx similarity index 83% rename from web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx rename to web/src/pages/settings/components/BehaviourSettings/components/BehaviourForm.tsx index 92f00362aa..0093cc602a 100644 --- a/web/src/pages/settings/components/PermissionsSettings/components/PermissionsForm.tsx +++ b/web/src/pages/settings/components/BehaviourSettings/components/BehaviourForm.tsx @@ -14,7 +14,7 @@ import { useToaster } from '../../../../../shared/hooks/useToaster'; import { MutationKeys } from '../../../../../shared/mutations'; import { QueryKeys } from '../../../../../shared/queries'; -export const PermissionsForm = () => { +export const BehaviourForm = () => { const { LL } = useI18nContext(); const toaster = useToaster(); const { @@ -43,18 +43,18 @@ export const PermissionsForm = () => { if (!settings) return null; return ( -
+
-

{LL.settingsPage.permissions.header()}

- {parse(LL.settingsPage.permissions.helper())} +

{LL.settingsPage.behaviour.header()}

+ {parse(LL.settingsPage.behaviour.helper())}
- mutate({ admin_device_management: !settings.disable_device_management }) + mutate({ admin_device_management: !settings.admin_device_management }) } /> diff --git a/web/src/pages/settings/components/PermissionsSettings/components/styles.scss b/web/src/pages/settings/components/BehaviourSettings/components/styles.scss similarity index 100% rename from web/src/pages/settings/components/PermissionsSettings/components/styles.scss rename to web/src/pages/settings/components/BehaviourSettings/components/styles.scss From 96d1de346da88622ab00a69a4c453184f21fbb41 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 09:43:00 +0200 Subject: [PATCH 17/26] Comments --- src/enterprise/db/models/enterprise_settings.rs | 2 ++ src/lib.rs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/enterprise/db/models/enterprise_settings.rs b/src/enterprise/db/models/enterprise_settings.rs index 59bfb2cc35..2d97e2c668 100644 --- a/src/enterprise/db/models/enterprise_settings.rs +++ b/src/enterprise/db/models/enterprise_settings.rs @@ -31,6 +31,8 @@ impl EnterpriseSettings { where E: PgExecutor<'e>, { + // avoid holding the rwlock across await, makes the future !Send + // and therefore unusable in axum handlers let is_valid = { let license = get_cached_license(); validate_license(license.as_ref()).is_ok() diff --git a/src/lib.rs b/src/lib.rs index 930bb1f195..2778d21042 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -410,7 +410,6 @@ pub fn build_webapp( let webapp = webapp.nest( "/api/v1/openid", Router::new() - // OpenID .route("/provider", get(get_current_openid_provider)) .route("/provider", post(add_openid_provider)) .route("/provider/:name", delete(delete_openid_provider)) From b0a78fbb9ceabc89dd45cfed3e25e4dcf0f3c952 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 09:48:20 +0200 Subject: [PATCH 18/26] Update sqlx offline queries --- ...72929d6a612846acf4a5489c36a04cb57de59.json | 22 --------------- ...ab52c14293fa64d260db812c89ed43a5a7cc8.json | 15 ---------- ...bbc71c19a867b626493b9f973a5cc09f5e3c0.json | 15 ++++++++++ ...8ea5591fe28ea9e07170906eb78c3ae4531d8.json | 14 ++++++++++ ...8741a269f654ef6eb1fa2f6b95e68ed7e04e1.json | 28 +++++++++++++++++++ ...b4ec9ffc2f61e38c74630dac5e32f3736c113.json | 26 +++++++++++++++++ ...e4841bf9cdce2d87fa952457b157afbe2d756.json | 22 +++++++++++++++ 7 files changed, 105 insertions(+), 37 deletions(-) delete mode 100644 .sqlx/query-0ad599ef120ecc02b030bd1276172929d6a612846acf4a5489c36a04cb57de59.json delete mode 100644 .sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json create mode 100644 .sqlx/query-226259d2006f1a0bc904f5a25f9bbc71c19a867b626493b9f973a5cc09f5e3c0.json create mode 100644 .sqlx/query-30b117d8b863a0785fcb164bc428ea5591fe28ea9e07170906eb78c3ae4531d8.json create mode 100644 .sqlx/query-a1836fa232271fdd5d61f0d34048741a269f654ef6eb1fa2f6b95e68ed7e04e1.json create mode 100644 .sqlx/query-bcba1892b88c1aecf5fb4112c03b4ec9ffc2f61e38c74630dac5e32f3736c113.json create mode 100644 .sqlx/query-c0883840b92550c4a2ba682d15ee4841bf9cdce2d87fa952457b157afbe2d756.json diff --git a/.sqlx/query-0ad599ef120ecc02b030bd1276172929d6a612846acf4a5489c36a04cb57de59.json b/.sqlx/query-0ad599ef120ecc02b030bd1276172929d6a612846acf4a5489c36a04cb57de59.json deleted file mode 100644 index ce67e42930..0000000000 --- a/.sqlx/query-0ad599ef120ecc02b030bd1276172929d6a612846acf4a5489c36a04cb57de59.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT recovery_codes FROM \"user\" WHERE id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "recovery_codes", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "0ad599ef120ecc02b030bd1276172929d6a612846acf4a5489c36a04cb57de59" -} diff --git a/.sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json b/.sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json deleted file mode 100644 index 8d314a5faa..0000000000 --- a/.sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE session SET expires = $1 WHERE id = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Timestamp", - "Text" - ] - }, - "nullable": [] - }, - "hash": "1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8" -} diff --git a/.sqlx/query-226259d2006f1a0bc904f5a25f9bbc71c19a867b626493b9f973a5cc09f5e3c0.json b/.sqlx/query-226259d2006f1a0bc904f5a25f9bbc71c19a867b626493b9f973a5cc09f5e3c0.json new file mode 100644 index 0000000000..0fda229cca --- /dev/null +++ b/.sqlx/query-226259d2006f1a0bc904f5a25f9bbc71c19a867b626493b9f973a5cc09f5e3c0.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE \"enterprisesettings\" SET \"admin_device_management\" = $2 WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "226259d2006f1a0bc904f5a25f9bbc71c19a867b626493b9f973a5cc09f5e3c0" +} diff --git a/.sqlx/query-30b117d8b863a0785fcb164bc428ea5591fe28ea9e07170906eb78c3ae4531d8.json b/.sqlx/query-30b117d8b863a0785fcb164bc428ea5591fe28ea9e07170906eb78c3ae4531d8.json new file mode 100644 index 0000000000..f26afcd0af --- /dev/null +++ b/.sqlx/query-30b117d8b863a0785fcb164bc428ea5591fe28ea9e07170906eb78c3ae4531d8.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM \"enterprisesettings\" WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "30b117d8b863a0785fcb164bc428ea5591fe28ea9e07170906eb78c3ae4531d8" +} diff --git a/.sqlx/query-a1836fa232271fdd5d61f0d34048741a269f654ef6eb1fa2f6b95e68ed7e04e1.json b/.sqlx/query-a1836fa232271fdd5d61f0d34048741a269f654ef6eb1fa2f6b95e68ed7e04e1.json new file mode 100644 index 0000000000..31a8e79fb3 --- /dev/null +++ b/.sqlx/query-a1836fa232271fdd5d61f0d34048741a269f654ef6eb1fa2f6b95e68ed7e04e1.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id \"id?\", \"admin_device_management\" FROM \"enterprisesettings\" WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "admin_device_management", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "a1836fa232271fdd5d61f0d34048741a269f654ef6eb1fa2f6b95e68ed7e04e1" +} diff --git a/.sqlx/query-bcba1892b88c1aecf5fb4112c03b4ec9ffc2f61e38c74630dac5e32f3736c113.json b/.sqlx/query-bcba1892b88c1aecf5fb4112c03b4ec9ffc2f61e38c74630dac5e32f3736c113.json new file mode 100644 index 0000000000..ba99ad5635 --- /dev/null +++ b/.sqlx/query-bcba1892b88c1aecf5fb4112c03b4ec9ffc2f61e38c74630dac5e32f3736c113.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id \"id?\", \"admin_device_management\" FROM \"enterprisesettings\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "admin_device_management", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false + ] + }, + "hash": "bcba1892b88c1aecf5fb4112c03b4ec9ffc2f61e38c74630dac5e32f3736c113" +} diff --git a/.sqlx/query-c0883840b92550c4a2ba682d15ee4841bf9cdce2d87fa952457b157afbe2d756.json b/.sqlx/query-c0883840b92550c4a2ba682d15ee4841bf9cdce2d87fa952457b157afbe2d756.json new file mode 100644 index 0000000000..9c7724501b --- /dev/null +++ b/.sqlx/query-c0883840b92550c4a2ba682d15ee4841bf9cdce2d87fa952457b157afbe2d756.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO \"enterprisesettings\" (\"admin_device_management\") VALUES ($1) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Bool" + ] + }, + "nullable": [ + false + ] + }, + "hash": "c0883840b92550c4a2ba682d15ee4841bf9cdce2d87fa952457b157afbe2d756" +} From aa7cb70eeff08fdd9bdef34ea4de302ea5243693 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 10:06:20 +0200 Subject: [PATCH 19/26] Move the check to middleware --- src/enterprise/handlers/mod.rs | 28 +++++++++++++++++++++++++++- src/handlers/wireguard.rs | 30 +++++------------------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/enterprise/handlers/mod.rs b/src/enterprise/handlers/mod.rs index 8869493a90..377651d594 100644 --- a/src/enterprise/handlers/mod.rs +++ b/src/enterprise/handlers/mod.rs @@ -1,4 +1,5 @@ use crate::{ + auth::SessionInfo, enterprise::license::validate_license, handlers::{ApiResponse, ApiResult}, }; @@ -13,13 +14,15 @@ use axum::{ http::{request::Parts, StatusCode}, }; -use super::license::get_cached_license; +use super::{db::models::enterprise_settings::EnterpriseSettings, license::get_cached_license}; use crate::{appstate::AppState, error::WebError}; pub struct LicenseInfo { pub valid: bool, } +pub struct CanManageDevices; + #[async_trait] impl FromRequestParts for LicenseInfo where @@ -49,3 +52,26 @@ pub async fn check_enterprise_status() -> ApiResult { status: StatusCode::OK, }) } + +#[async_trait] +impl FromRequestParts for CanManageDevices +where + S: Send + Sync, + AppState: FromRef, +{ + type Rejection = WebError; + + /// Returns an error if current session user is not allowed to manage devices. + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let appstate = AppState::from_ref(state); + let session = SessionInfo::from_request_parts(parts, state).await?; + let settings = EnterpriseSettings::get(&appstate.pool).await?; + if settings.admin_device_management && !session.is_admin { + Err(WebError::Forbidden( + "Only admin users can manage devices".into(), + )) + } else { + Ok(Self) + } + } +} diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index cfe4360739..42477d2375 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -17,9 +17,7 @@ use uuid::Uuid; use super::{device_for_admin_or_self, user_for_admin_or_self, ApiResponse, ApiResult, WebError}; use crate::{ - appstate::AppState, - auth::{Claims, ClaimsType, SessionInfo, VpnRole}, - db::{ + appstate::AppState, auth::{Claims, ClaimsType, SessionInfo, VpnRole}, db::{ models::{ device::{ DeviceConfig, DeviceInfo, DeviceNetworkInfo, ModifyDevice, WireguardNetworkDevice, @@ -27,13 +25,7 @@ use crate::{ wireguard::{DateTimeAggregation, MappedDevice, WireguardNetworkInfo}, }, AddDevice, DbPool, Device, GatewayEvent, WireguardNetwork, - }, - enterprise::db::models::enterprise_settings::EnterpriseSettings, - grpc::GatewayMap, - handlers::mail::send_new_device_added_email, - server_config, - templates::TemplateLocation, - wg_config::{parse_wireguard_config, ImportedDevice}, + }, enterprise::handlers::CanManageDevices, grpc::GatewayMap, handlers::mail::send_new_device_added_email, server_config, templates::TemplateLocation, wg_config::{parse_wireguard_config, ImportedDevice} }; #[derive(Deserialize, Serialize, ToSchema)] @@ -518,6 +510,7 @@ pub struct AddDeviceResult { ) )] pub async fn add_device( + _can_manage_devices: CanManageDevices, session: SessionInfo, State(appstate): State, // Alias, because otherwise `axum` reports conflicting routes. @@ -531,7 +524,6 @@ pub async fn add_device( ); let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; - can_manage_devices_or_error(&appstate.pool, &session).await?; // Let admins manage devices for disabled users if !user.is_active && !session.is_admin { @@ -632,18 +624,6 @@ pub async fn add_device( }) } -/// Returns an error if current session user cannot manage devices. -async fn can_manage_devices_or_error(pool: &DbPool, session: &SessionInfo) -> Result<(), WebError> { - let settings = EnterpriseSettings::get(pool).await?; - if settings.admin_device_management && !session.is_admin { - Err(WebError::Forbidden( - "Only admin users can manage devices".into(), - )) - } else { - Ok(()) - } -} - /// Modify device /// /// Update a device for a user by sending `ModifyDevice` object. @@ -677,6 +657,7 @@ async fn can_manage_devices_or_error(pool: &DbPool, session: &SessionInfo) -> Re ) )] pub async fn modify_device( + _can_manage_devices: CanManageDevices, session: SessionInfo, Path(device_id): Path, State(appstate): State, @@ -684,7 +665,6 @@ pub async fn modify_device( ) -> ApiResult { debug!("User {} updating device {device_id}", session.user.username); let mut device = device_for_admin_or_self(&appstate.pool, &session, device_id).await?; - can_manage_devices_or_error(&appstate.pool, &session).await?; let networks = WireguardNetwork::all(&appstate.pool).await?; if networks.is_empty() { @@ -800,13 +780,13 @@ pub async fn get_device( ) )] pub async fn delete_device( + _can_manage_devices: CanManageDevices, session: SessionInfo, Path(device_id): Path, State(appstate): State, ) -> ApiResult { debug!("User {} deleting device {device_id}", session.user.username); let device = device_for_admin_or_self(&appstate.pool, &session, device_id).await?; - can_manage_devices_or_error(&appstate.pool, &session).await?; appstate.send_wireguard_event(GatewayEvent::DeviceDeleted( DeviceInfo::from_device(&appstate.pool, device.clone()).await?, )); From 05a0de1b45a0a0ae39e4ee862063653c897b222f Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 10:08:07 +0200 Subject: [PATCH 20/26] Comments --- src/enterprise/handlers/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/enterprise/handlers/mod.rs b/src/enterprise/handlers/mod.rs index 377651d594..c9b3d616b9 100644 --- a/src/enterprise/handlers/mod.rs +++ b/src/enterprise/handlers/mod.rs @@ -21,6 +21,7 @@ pub struct LicenseInfo { pub valid: bool, } +/// Used to check if user is allowed to manage his devices. pub struct CanManageDevices; #[async_trait] @@ -62,6 +63,7 @@ where type Rejection = WebError; /// Returns an error if current session user is not allowed to manage devices. + /// The permission is defined by [`EnterpriseSettings::admin_device_management`] setting. async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let appstate = AppState::from_ref(state); let session = SessionInfo::from_request_parts(parts, state).await?; From dc77da2b8679b915ebe61df7db623bad96694e57 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 10:12:44 +0200 Subject: [PATCH 21/26] Format --- src/handlers/wireguard.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 42477d2375..34a72f831d 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -17,7 +17,9 @@ use uuid::Uuid; use super::{device_for_admin_or_self, user_for_admin_or_self, ApiResponse, ApiResult, WebError}; use crate::{ - appstate::AppState, auth::{Claims, ClaimsType, SessionInfo, VpnRole}, db::{ + appstate::AppState, + auth::{Claims, ClaimsType, SessionInfo, VpnRole}, + db::{ models::{ device::{ DeviceConfig, DeviceInfo, DeviceNetworkInfo, ModifyDevice, WireguardNetworkDevice, @@ -25,7 +27,13 @@ use crate::{ wireguard::{DateTimeAggregation, MappedDevice, WireguardNetworkInfo}, }, AddDevice, DbPool, Device, GatewayEvent, WireguardNetwork, - }, enterprise::handlers::CanManageDevices, grpc::GatewayMap, handlers::mail::send_new_device_added_email, server_config, templates::TemplateLocation, wg_config::{parse_wireguard_config, ImportedDevice} + }, + enterprise::handlers::CanManageDevices, + grpc::GatewayMap, + handlers::mail::send_new_device_added_email, + server_config, + templates::TemplateLocation, + wg_config::{parse_wireguard_config, ImportedDevice}, }; #[derive(Deserialize, Serialize, ToSchema)] From 94525ec5c60cf3c3e3de02e20c7ea0fd97ab3df9 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 10:19:56 +0200 Subject: [PATCH 22/26] Test sqlx fixtures --- ...72929d6a612846acf4a5489c36a04cb57de59.json | 22 +++++++++++++++++++ ...ab52c14293fa64d260db812c89ed43a5a7cc8.json | 15 +++++++++++++ tests/common/mod.rs | 1 - tests/openid_login.rs | 1 + 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .sqlx/query-0ad599ef120ecc02b030bd1276172929d6a612846acf4a5489c36a04cb57de59.json create mode 100644 .sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json diff --git a/.sqlx/query-0ad599ef120ecc02b030bd1276172929d6a612846acf4a5489c36a04cb57de59.json b/.sqlx/query-0ad599ef120ecc02b030bd1276172929d6a612846acf4a5489c36a04cb57de59.json new file mode 100644 index 0000000000..ce67e42930 --- /dev/null +++ b/.sqlx/query-0ad599ef120ecc02b030bd1276172929d6a612846acf4a5489c36a04cb57de59.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT recovery_codes FROM \"user\" WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "recovery_codes", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0ad599ef120ecc02b030bd1276172929d6a612846acf4a5489c36a04cb57de59" +} diff --git a/.sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json b/.sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json new file mode 100644 index 0000000000..8d314a5faa --- /dev/null +++ b/.sqlx/query-1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE session SET expires = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamp", + "Text" + ] + }, + "nullable": [] + }, + "hash": "1bb3c8ecbd6500717d639678e08ab52c14293fa64d260db812c89ed43a5a7cc8" +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 7eed5c45ef..e926773886 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -2,7 +2,6 @@ pub(crate) mod client; use std::sync::{Arc, Mutex}; -use chrono::{DateTime, TimeZone, Utc}; use defguard::{ auth::failed_login::FailedLoginMap, build_webapp, diff --git a/tests/openid_login.rs b/tests/openid_login.rs index 93c6936a01..89ac2b1b24 100644 --- a/tests/openid_login.rs +++ b/tests/openid_login.rs @@ -13,6 +13,7 @@ async fn make_client() -> TestClient { client } +#[allow(dead_code)] async fn make_client_v2(pool: DbPool, config: DefGuardConfig) -> TestClient { let (client, _) = make_base_client(pool, config).await; client From 0a855adaffb4234a641e15576f57164bc91e3945 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 10:48:33 +0200 Subject: [PATCH 23/26] Behaviour -> behavior --- tests/enterprise_settings.rs | 34 +++++++++++++++++++ web/src/i18n/en/index.ts | 8 ++--- web/src/i18n/i18n-types.ts | 20 +++++------ web/src/i18n/pl/index.ts | 4 +-- web/src/pages/settings/SettingsPage.tsx | 6 ++-- .../BehaviorSettings.tsx} | 6 ++-- .../components/BehaviorForm.tsx} | 10 +++--- .../components/styles.scss | 0 8 files changed, 61 insertions(+), 27 deletions(-) create mode 100644 tests/enterprise_settings.rs rename web/src/pages/settings/components/{BehaviourSettings/BehaviourSettings.tsx => BehaviorSettings/BehaviorSettings.tsx} (87%) rename web/src/pages/settings/components/{BehaviourSettings/components/BehaviourForm.tsx => BehaviorSettings/components/BehaviorForm.tsx} (87%) rename web/src/pages/settings/components/{BehaviourSettings => BehaviorSettings}/components/styles.scss (100%) diff --git a/tests/enterprise_settings.rs b/tests/enterprise_settings.rs new file mode 100644 index 0000000000..2d54a659d1 --- /dev/null +++ b/tests/enterprise_settings.rs @@ -0,0 +1,34 @@ +// mod common; + +use defguard::handlers::Auth; +use reqwest::StatusCode; + +use self::common::make_test_client; +use defguard::{ + db::{ + models::{ + device::WireguardNetworkDevice, + wireguard::{DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL}, + }, + Device, GatewayEvent, WireguardNetwork, + }, + handlers::{wireguard::WireguardNetworkData, Auth, GroupInfo}, +}; +use matches::assert_matches; +use serde_json::{json, Value}; + +#[tokio::test] +async fn test_only_enterprise_can_modify() { + let (client, client_state) = make_test_client().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 response = client + .post("/api/v1/openid/provider") + .json(&provider_data) + .send() + .await; + +} diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index 2f527eab09..9af0300583 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -892,7 +892,7 @@ const en: BaseTranslation = { global: 'Global settings', ldap: 'LDAP', openid: 'OpenID', - behaviour: 'Behaviour', + behavior: 'Behavior', }, messages: { editSuccess: 'Settings updated', @@ -1181,9 +1181,9 @@ const en: BaseTranslation = { }, }, }, - behaviour: { - header: 'Behaviour', - helper: '

Here you can change app behaviour.

', + behavior: { + header: 'Behavior', + helper: '

Here you can change app behavior.

', fields: { deviceManagement: { label: 'Disable users ability to manage their devices', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 8a98e1860e..a44dd172b2 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -2199,9 +2199,9 @@ type RootTranslation = { */ openid: string /** - * B​e​h​a​v​i​o​u​r + * B​e​h​a​v​i​o​r */ - behaviour: string + behavior: string } messages: { /** @@ -2805,13 +2805,13 @@ type RootTranslation = { } } } - behaviour: { + behavior: { /** - * B​e​h​a​v​i​o​u​r + * B​e​h​a​v​i​o​r */ header: string /** - * <​p​>​H​e​r​e​ ​y​o​u​ ​c​a​n​ ​c​h​a​n​g​e​ ​a​p​p​ ​b​e​h​a​v​i​o​u​r​.​<​/​p​> + * <​p​>​H​e​r​e​ ​y​o​u​ ​c​a​n​ ​c​h​a​n​g​e​ ​a​p​p​ ​b​e​h​a​v​i​o​r​.​<​/​p​> */ helper: string fields: { @@ -6322,9 +6322,9 @@ export type TranslationFunctions = { */ openid: () => LocalizedString /** - * Behaviour + * Behavior */ - behaviour: () => LocalizedString + behavior: () => LocalizedString } messages: { /** @@ -6925,13 +6925,13 @@ export type TranslationFunctions = { } } } - behaviour: { + behavior: { /** - * Behaviour + * Behavior */ header: () => LocalizedString /** - *

Here you can change app behaviour.

+ *

Here you can change app behavior.

*/ helper: () => LocalizedString fields: { diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 73170d62b4..484b78e46d 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -879,7 +879,7 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe global: 'Globalne', ldap: 'LDAP', openid: 'OpenID', - behaviour: 'Zachowanie', + behavior: 'Zachowanie', }, messages: { editSuccess: 'Ustawienia zaktualizowane.', @@ -1168,7 +1168,7 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe }, }, }, - behaviour: { + behavior: { header: 'Zachowanie', helper: '

Tutaj możesz zmienić zachowanie aplikacji.

', fields: { diff --git a/web/src/pages/settings/SettingsPage.tsx b/web/src/pages/settings/SettingsPage.tsx index 09ddd4b296..7f22363225 100644 --- a/web/src/pages/settings/SettingsPage.tsx +++ b/web/src/pages/settings/SettingsPage.tsx @@ -12,7 +12,7 @@ import { CardTabsData } from '../../shared/defguard-ui/components/Layout/CardTab import { LoaderSpinner } from '../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner'; import useApi from '../../shared/hooks/useApi'; import { QueryKeys } from '../../shared/queries'; -import { BehaviourSettings } from './components/BehaviourSettings/BehaviourSettings'; +import { BehaviorSettings } from './components/BehaviorSettings/BehaviorSettings'; import { GlobalSettings } from './components/GlobalSettings/GlobalSettings'; import { LdapSettings } from './components/LdapSettings/LdapSettings'; import { OpenIdSettings } from './components/OpenIdSettings/OpenIdSettings'; @@ -24,7 +24,7 @@ const tabsContent: ReactNode[] = [ , , , - , + , ]; export const SettingsPage = () => { @@ -77,7 +77,7 @@ export const SettingsPage = () => { }, { key: 4, - content: LL.settingsPage.tabs.behaviour(), + content: LL.settingsPage.tabs.behavior(), active: activeCard === 4, onClick: () => setActiveCard(4), }, diff --git a/web/src/pages/settings/components/BehaviourSettings/BehaviourSettings.tsx b/web/src/pages/settings/components/BehaviorSettings/BehaviorSettings.tsx similarity index 87% rename from web/src/pages/settings/components/BehaviourSettings/BehaviourSettings.tsx rename to web/src/pages/settings/components/BehaviorSettings/BehaviorSettings.tsx index 28c9f9ffc5..edddcd9aaf 100644 --- a/web/src/pages/settings/components/BehaviourSettings/BehaviourSettings.tsx +++ b/web/src/pages/settings/components/BehaviorSettings/BehaviorSettings.tsx @@ -1,8 +1,8 @@ import { useI18nContext } from '../../../../i18n/i18n-react'; import { useAppStore } from '../../../../shared/hooks/store/useAppStore'; -import { BehaviourForm } from './components/BehaviourForm'; +import { BehaviorForm } from './components/BehaviorForm'; -export const BehaviourSettings = () => { +export const BehaviorSettings = () => { const enterpriseEnabled = useAppStore((state) => state.enterprise_enabled); const { LL } = useI18nContext(); const localLL = LL.settingsPage.enterpriseOnly; @@ -25,7 +25,7 @@ export const BehaviourSettings = () => { )}
- +
diff --git a/web/src/pages/settings/components/BehaviourSettings/components/BehaviourForm.tsx b/web/src/pages/settings/components/BehaviorSettings/components/BehaviorForm.tsx similarity index 87% rename from web/src/pages/settings/components/BehaviourSettings/components/BehaviourForm.tsx rename to web/src/pages/settings/components/BehaviorSettings/components/BehaviorForm.tsx index 0093cc602a..4211f45d07 100644 --- a/web/src/pages/settings/components/BehaviourSettings/components/BehaviourForm.tsx +++ b/web/src/pages/settings/components/BehaviorSettings/components/BehaviorForm.tsx @@ -14,7 +14,7 @@ import { useToaster } from '../../../../../shared/hooks/useToaster'; import { MutationKeys } from '../../../../../shared/mutations'; import { QueryKeys } from '../../../../../shared/queries'; -export const BehaviourForm = () => { +export const BehaviorForm = () => { const { LL } = useI18nContext(); const toaster = useToaster(); const { @@ -43,15 +43,15 @@ export const BehaviourForm = () => { if (!settings) return null; return ( -
+
-

{LL.settingsPage.behaviour.header()}

- {parse(LL.settingsPage.behaviour.helper())} +

{LL.settingsPage.behavior.header()}

+ {parse(LL.settingsPage.behavior.helper())}
mutate({ admin_device_management: !settings.admin_device_management }) diff --git a/web/src/pages/settings/components/BehaviourSettings/components/styles.scss b/web/src/pages/settings/components/BehaviorSettings/components/styles.scss similarity index 100% rename from web/src/pages/settings/components/BehaviourSettings/components/styles.scss rename to web/src/pages/settings/components/BehaviorSettings/components/styles.scss From 048aeb5c870b004b58155db3b91dc0f0754bf043 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 12:16:27 +0200 Subject: [PATCH 24/26] Tests --- tests/enterprise_settings.rs | 215 ++++++++++++++++++++++++++++++++--- 1 file changed, 199 insertions(+), 16 deletions(-) diff --git a/tests/enterprise_settings.rs b/tests/enterprise_settings.rs index 2d54a659d1..3239870e82 100644 --- a/tests/enterprise_settings.rs +++ b/tests/enterprise_settings.rs @@ -1,34 +1,217 @@ -// mod common; +mod common; -use defguard::handlers::Auth; +use defguard::enterprise::{ + db::models::enterprise_settings::EnterpriseSettings, + license::{get_cached_license, set_cached_license}, +}; use reqwest::StatusCode; use self::common::make_test_client; -use defguard::{ - db::{ - models::{ - device::WireguardNetworkDevice, - wireguard::{DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL}, - }, - Device, GatewayEvent, WireguardNetwork, - }, - handlers::{wireguard::WireguardNetworkData, Auth, GroupInfo}, -}; -use matches::assert_matches; +use defguard::handlers::Auth; use serde_json::{json, Value}; +fn make_network() -> Value { + 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", + "allowed_groups": [], + "mfa_enabled": false, + "keepalive_interval": 25, + "peer_disconnect_threshold": 180 + }) +} + #[tokio::test] async fn test_only_enterprise_can_modify() { - let (client, client_state) = make_test_client().await; + // admin login + let (client, _client_state) = make_test_client().await; + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // unset the license + let license = get_cached_license().clone(); + set_cached_license(None); + + // try to patch enterprise settings + let settings = EnterpriseSettings { + id: None, + admin_device_management: true, + }; + + let response = client + .patch("/api/v1/settings_enterprise") + .json(&settings) + .send() + .await; + + // server should say nono + assert_eq!(response.status(), StatusCode::FORBIDDEN); + + // restore valid license and try again + set_cached_license(license); + let response = client + .patch("/api/v1/settings_enterprise") + .json(&settings) + .send() + .await; + // server should say ok + assert_eq!(response.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_admin_devices_management_is_enforced() { + // admin login + let (client, _) = make_test_client().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 network let response = client - .post("/api/v1/openid/provider") - .json(&provider_data) + .post("/api/v1/network") + .json(&make_network()) .send() .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // setup admin devices management + let settings = EnterpriseSettings { + id: None, + admin_device_management: true, + }; + let response = client + .patch("/api/v1/settings_enterprise") + .json(&settings) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + // make sure admin can still manage devices + let device = json!({ + "name": "device", + "wireguard_pubkey": "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=", + }); + let response = client + .post("/api/v1/device/hpotter") + .json(&device) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // ensure normal users can't manage devices + let auth = Auth::new("hpotter", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // add + let device = json!({ + "name": "userdevice", + "wireguard_pubkey": "AJwxGkzvVVn5Q1xjpCDFo5RJSU9KOPHeoEixYaj+20M=", + }); + let response = client + .post("/api/v1/device/hpotter") + .json(&device) + .send() + .await; + assert_eq!(response.status(), StatusCode::FORBIDDEN); + + // modify + let device = json!({ + "name": "modifieddevice", + "wireguard_pubkey": "AJwxGkzvVVn5Q1xjpCDFo5RJSU9KOPHeoEixYaj+20M=", + }); + let response = client.put("/api/v1/device/2").json(&device).send().await; + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + + // delete + let device = json!({ + "name": "modifieddevice", + "wireguard_pubkey": "AJwxGkzvVVn5Q1xjpCDFo5RJSU9KOPHeoEixYaj+20M=", + }); + let response = client.put("/api/v1/device/2").json(&device).send().await; + + assert_eq!(response.status(), StatusCode::FORBIDDEN); +} + +#[tokio::test] +async fn test_regular_user_device_management() { + // admin login + let (client, _) = make_test_client().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 network + let response = client + .post("/api/v1/network") + .json(&make_network()) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // setup admin devices management + let settings = EnterpriseSettings { + id: None, + admin_device_management: false, + }; + let response = client + .patch("/api/v1/settings_enterprise") + .json(&settings) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + + // make sure admin can manage devices + let device = json!({ + "name": "device", + "wireguard_pubkey": "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=", + }); + let response = client + .post("/api/v1/device/hpotter") + .json(&device) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // ensure normal users can manage devices + let auth = Auth::new("hpotter", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // add + let device = json!({ + "name": "userdevice", + "wireguard_pubkey": "AJwxGkzvVVn5Q1xjpCDFo5RJSU9KOPHeoEixYaj+20M=", + }); + let response = client + .post("/api/v1/device/hpotter") + .json(&device) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // modify + let device = json!({ + "name": "modifieddevice", + "wireguard_pubkey": "AJwxGkzvVVn5Q1xjpCDFo5RJSU9KOPHeoEixYaj+20M=", + }); + let response = client.put("/api/v1/device/2").json(&device).send().await; + + assert_eq!(response.status(), StatusCode::OK); + + // delete + let device = json!({ + "name": "modifieddevice", + "wireguard_pubkey": "AJwxGkzvVVn5Q1xjpCDFo5RJSU9KOPHeoEixYaj+20M=", + }); + let response = client.put("/api/v1/device/2").json(&device).send().await; + + assert_eq!(response.status(), StatusCode::OK); } From 5b637d10a3f53a9baf12f4e9cec07c78b9471e80 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 12:28:03 +0200 Subject: [PATCH 25/26] Add helper msg --- .../components/BehaviorForm.tsx | 21 ++++++++++++------- .../BehaviorSettings/components/styles.scss | 11 ++++++++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/web/src/pages/settings/components/BehaviorSettings/components/BehaviorForm.tsx b/web/src/pages/settings/components/BehaviorSettings/components/BehaviorForm.tsx index 4211f45d07..def9f85058 100644 --- a/web/src/pages/settings/components/BehaviorSettings/components/BehaviorForm.tsx +++ b/web/src/pages/settings/components/BehaviorSettings/components/BehaviorForm.tsx @@ -49,14 +49,19 @@ export const BehaviorForm = () => { {parse(LL.settingsPage.behavior.helper())} - - mutate({ admin_device_management: !settings.admin_device_management }) - } - /> +
+ + mutate({ admin_device_management: !settings.admin_device_management }) + } + /> + + {parse(LL.settingsPage.behavior.fields.deviceManagement.helper())} + +
); diff --git a/web/src/pages/settings/components/BehaviorSettings/components/styles.scss b/web/src/pages/settings/components/BehaviorSettings/components/styles.scss index f233e834ce..04beabe005 100644 --- a/web/src/pages/settings/components/BehaviorSettings/components/styles.scss +++ b/web/src/pages/settings/components/BehaviorSettings/components/styles.scss @@ -1,12 +1,19 @@ @use '@scssutils' as *; -#modules-settings { - & > .card { +#behavior-settings { + &>.card { display: flex; flex-flow: column; row-gap: 16px; + @include media-breakpoint-up(lg) { padding: 16px 15px; } + + &>.checkbox-row { + display: flex; + align-items: center; + gap: 10px; + } } } From ed6f2f024baa2339634b1673a3982f46f3158da0 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 21 Aug 2024 12:39:18 +0200 Subject: [PATCH 26/26] Frontend lint --- .../components/BehaviorSettings/components/styles.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/settings/components/BehaviorSettings/components/styles.scss b/web/src/pages/settings/components/BehaviorSettings/components/styles.scss index 04beabe005..a5536bc53e 100644 --- a/web/src/pages/settings/components/BehaviorSettings/components/styles.scss +++ b/web/src/pages/settings/components/BehaviorSettings/components/styles.scss @@ -1,7 +1,7 @@ @use '@scssutils' as *; #behavior-settings { - &>.card { + & > .card { display: flex; flex-flow: column; row-gap: 16px; @@ -10,7 +10,7 @@ padding: 16px 15px; } - &>.checkbox-row { + & > .checkbox-row { display: flex; align-items: center; gap: 10px;