mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-21 07:51:29 +02:00
Add VNC allow and approval settings to MDM policy
This commit is contained in:
@@ -735,6 +735,8 @@ func (config *Config) applyMDMPolicy(policy *mdm.Policy) {
|
||||
}
|
||||
|
||||
applyBool(mdm.KeyAllowServerSSH, func(v bool) { bv := v; config.ServerSSHAllowed = &bv })
|
||||
applyBool(mdm.KeyAllowServerVNC, func(v bool) { bv := v; config.ServerVNCAllowed = &bv })
|
||||
applyBool(mdm.KeyDisableVNCApproval, func(v bool) { bv := v; config.DisableVNCApproval = &bv })
|
||||
applyBool(mdm.KeyDisableClientRoutes, func(v bool) { config.DisableClientRoutes = v })
|
||||
applyBool(mdm.KeyDisableServerRoutes, func(v bool) { config.DisableServerRoutes = v })
|
||||
applyBool(mdm.KeyBlockInbound, func(v bool) { config.BlockInbound = v })
|
||||
|
||||
@@ -130,6 +130,36 @@ func TestApply_MDMBoolKeysOverrideOnDiskValue(t *testing.T) {
|
||||
assert.True(t, cfg.Policy().HasKey(mdm.KeyRosenpassEnabled))
|
||||
}
|
||||
|
||||
func TestApply_MDMVNCKeys(t *testing.T) {
|
||||
tmp := filepath.Join(t.TempDir(), "config.json")
|
||||
|
||||
// Seed without MDM: VNC off, approval prompt on.
|
||||
withMDMPolicy(t, mdm.NewPolicy(nil))
|
||||
_, err := UpdateOrCreateConfig(ConfigInput{
|
||||
ConfigPath: tmp,
|
||||
ServerVNCAllowed: boolPtr(false),
|
||||
DisableVNCApproval: boolPtr(false),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// MDM enforces VNC on and disables the approval prompt.
|
||||
withMDMPolicy(t, mdm.NewPolicy(map[string]any{
|
||||
mdm.KeyAllowServerVNC: true,
|
||||
mdm.KeyDisableVNCApproval: true,
|
||||
}))
|
||||
|
||||
cfg, err := UpdateOrCreateConfig(ConfigInput{ConfigPath: tmp})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
require.NotNil(t, cfg.ServerVNCAllowed)
|
||||
assert.True(t, *cfg.ServerVNCAllowed, "MDM override should flip on-disk false to true")
|
||||
require.NotNil(t, cfg.DisableVNCApproval)
|
||||
assert.True(t, *cfg.DisableVNCApproval)
|
||||
assert.True(t, cfg.Policy().HasKey(mdm.KeyAllowServerVNC))
|
||||
assert.True(t, cfg.Policy().HasKey(mdm.KeyDisableVNCApproval))
|
||||
}
|
||||
|
||||
func TestApply_MDMLazyConnection(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
@@ -21,6 +21,8 @@ var allKeys = []string{
|
||||
KeyBlockInbound,
|
||||
KeyDisableMetricsCollection,
|
||||
KeyAllowServerSSH,
|
||||
KeyAllowServerVNC,
|
||||
KeyDisableVNCApproval,
|
||||
KeyDisableAutoConnect,
|
||||
KeyPreSharedKey,
|
||||
KeyRosenpassEnabled,
|
||||
|
||||
@@ -20,10 +20,10 @@ import (
|
||||
// names (lowerCamelCase) so the daemon can map a Policy key directly to a
|
||||
// configuration field.
|
||||
const (
|
||||
KeyManagementURL = "managementURL"
|
||||
KeyDisableUpdateSettings = "disableUpdateSettings"
|
||||
KeyDisableProfiles = "disableProfiles"
|
||||
KeyDisableNetworks = "disableNetworks"
|
||||
KeyManagementURL = "managementURL"
|
||||
KeyDisableUpdateSettings = "disableUpdateSettings"
|
||||
KeyDisableProfiles = "disableProfiles"
|
||||
KeyDisableNetworks = "disableNetworks"
|
||||
// KeyDisableAdvancedView gates the advanced-view section in the
|
||||
// upcoming UI revision. UI-only: NOT stored on Config, not
|
||||
// applied by applyMDMPolicy, not rejectable via SetConfig. The
|
||||
@@ -36,6 +36,8 @@ const (
|
||||
KeyBlockInbound = "blockInbound"
|
||||
KeyDisableMetricsCollection = "disableMetricsCollection"
|
||||
KeyAllowServerSSH = "allowServerSSH"
|
||||
KeyAllowServerVNC = "allowServerVNC"
|
||||
KeyDisableVNCApproval = "disableVNCApproval"
|
||||
KeyDisableAutoConnect = "disableAutoConnect"
|
||||
KeyPreSharedKey = "preSharedKey"
|
||||
KeyRosenpassEnabled = "rosenpassEnabled"
|
||||
|
||||
@@ -297,6 +297,8 @@ func mdmManagedFieldConflicts(msg *proto.SetConfigRequest, policy *mdm.Policy) [
|
||||
conflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive),
|
||||
conflictBool(mdm.KeyDisableAutoConnect, msg.DisableAutoConnect),
|
||||
conflictBool(mdm.KeyAllowServerSSH, msg.ServerSSHAllowed),
|
||||
conflictBool(mdm.KeyAllowServerVNC, msg.ServerVNCAllowed),
|
||||
conflictBool(mdm.KeyDisableVNCApproval, msg.DisableVNCApproval),
|
||||
conflictBool(mdm.KeyDisableClientRoutes, msg.DisableClientRoutes),
|
||||
conflictBool(mdm.KeyDisableServerRoutes, msg.DisableServerRoutes),
|
||||
conflictBool(mdm.KeyBlockInbound, msg.BlockInbound),
|
||||
@@ -332,6 +334,8 @@ func setConfigRequestHasConfigOverrides(msg *proto.SetConfigRequest) bool {
|
||||
msg.Mtu != nil ||
|
||||
msg.DisableAutoConnect != nil ||
|
||||
msg.ServerSSHAllowed != nil ||
|
||||
msg.ServerVNCAllowed != nil ||
|
||||
msg.DisableVNCApproval != nil ||
|
||||
msg.NetworkMonitor != nil ||
|
||||
msg.DisableClientRoutes != nil ||
|
||||
msg.DisableServerRoutes != nil ||
|
||||
@@ -370,6 +374,8 @@ func loginRequestHasConfigOverrides(msg *proto.LoginRequest) bool {
|
||||
msg.WireguardPort != nil ||
|
||||
msg.DisableAutoConnect != nil ||
|
||||
msg.ServerSSHAllowed != nil ||
|
||||
msg.ServerVNCAllowed != nil ||
|
||||
msg.DisableVNCApproval != nil ||
|
||||
msg.RosenpassPermissive != nil ||
|
||||
len(msg.ExtraIFaceBlacklist) > 0 ||
|
||||
msg.NetworkMonitor != nil ||
|
||||
@@ -418,6 +424,8 @@ func loginRequestMDMConflicts(msg *proto.LoginRequest, policy *mdm.Policy) []str
|
||||
conflictBool(mdm.KeyRosenpassPermissive, msg.RosenpassPermissive),
|
||||
conflictBool(mdm.KeyDisableAutoConnect, msg.DisableAutoConnect),
|
||||
conflictBool(mdm.KeyAllowServerSSH, msg.ServerSSHAllowed),
|
||||
conflictBool(mdm.KeyAllowServerVNC, msg.ServerVNCAllowed),
|
||||
conflictBool(mdm.KeyDisableVNCApproval, msg.DisableVNCApproval),
|
||||
conflictBool(mdm.KeyDisableClientRoutes, msg.DisableClientRoutes),
|
||||
conflictBool(mdm.KeyDisableServerRoutes, msg.DisableServerRoutes),
|
||||
conflictBool(mdm.KeyBlockInbound, msg.BlockInbound),
|
||||
|
||||
@@ -105,6 +105,30 @@ func TestSetConfig_MDMReject_SingleField(t *testing.T) {
|
||||
assert.Equal(t, []string{mdm.KeyManagementURL}, v.GetFields())
|
||||
}
|
||||
|
||||
func TestSetConfig_MDMReject_VNCFields(t *testing.T) {
|
||||
withMDMPolicy(t, mdm.NewPolicy(map[string]any{
|
||||
mdm.KeyAllowServerVNC: true,
|
||||
mdm.KeyDisableVNCApproval: false,
|
||||
}))
|
||||
|
||||
s, ctx, profName, username, _ := setupServerWithProfile(t)
|
||||
|
||||
vncAllowed := false
|
||||
disableApproval := true
|
||||
_, err := s.SetConfig(ctx, &proto.SetConfigRequest{
|
||||
ProfileName: profName,
|
||||
Username: username,
|
||||
ServerVNCAllowed: &vncAllowed,
|
||||
DisableVNCApproval: &disableApproval,
|
||||
})
|
||||
|
||||
v := extractViolation(t, err)
|
||||
assert.ElementsMatch(t, []string{
|
||||
mdm.KeyAllowServerVNC,
|
||||
mdm.KeyDisableVNCApproval,
|
||||
}, v.GetFields())
|
||||
}
|
||||
|
||||
func TestSetConfig_MDMReject_MultipleFields(t *testing.T) {
|
||||
withMDMPolicy(t, mdm.NewPolicy(map[string]any{
|
||||
mdm.KeyManagementURL: "https://mdm.example.com:443",
|
||||
|
||||
@@ -21,6 +21,7 @@ export const SettingsNavigation = () => {
|
||||
const { updateAvailable } = useClientVersion();
|
||||
const { mdm, features } = useRestrictions();
|
||||
const showSsh = mdm.allowServerSSH ?? !features.disableUpdateSettings;
|
||||
const showVnc = mdm.allowServerVNC ?? !features.disableUpdateSettings;
|
||||
|
||||
const aboutAdornment = updateAvailable ? (
|
||||
<Tooltip content={t("settings.tabs.updateAvailable")} side={"right"}>
|
||||
@@ -64,7 +65,7 @@ export const SettingsNavigation = () => {
|
||||
title={t("settings.tabs.ssh")}
|
||||
/>
|
||||
)}
|
||||
{!features.disableUpdateSettings && (
|
||||
{showVnc && (
|
||||
<VerticalTabs.Trigger
|
||||
value={"vnc"}
|
||||
icon={MonitorIcon}
|
||||
|
||||
@@ -58,13 +58,18 @@ export const SettingsPage = () => {
|
||||
[Tab.Security]: editable,
|
||||
[Tab.Profiles]: !features.disableProfiles,
|
||||
[Tab.SSH]: mdm.allowServerSSH ?? editable,
|
||||
[Tab.VNC]: editable,
|
||||
[Tab.VNC]: mdm.allowServerVNC ?? editable,
|
||||
[Tab.Advanced]: editable,
|
||||
[Tab.Troubleshooting]: true,
|
||||
[Tab.About]: true,
|
||||
};
|
||||
return (Object.keys(visibility) as Tab[]).filter((t) => visibility[t]);
|
||||
}, [features.disableUpdateSettings, features.disableProfiles, mdm.allowServerSSH]);
|
||||
}, [
|
||||
features.disableUpdateSettings,
|
||||
features.disableProfiles,
|
||||
mdm.allowServerSSH,
|
||||
mdm.allowServerVNC,
|
||||
]);
|
||||
|
||||
const defaultTab = visibleTabs[0];
|
||||
const [active, setActive] = useState<string>(() => navState?.tab ?? defaultTab);
|
||||
|
||||
@@ -2,11 +2,14 @@ import { useTranslation } from "react-i18next";
|
||||
import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch";
|
||||
import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
|
||||
import { useSettings } from "@/contexts/SettingsContext.tsx";
|
||||
import { useRestrictions } from "@/contexts/RestrictionsContext.tsx";
|
||||
|
||||
export function SettingsVNC() {
|
||||
const { t } = useTranslation();
|
||||
const { config, setField } = useSettings();
|
||||
const { mdm } = useRestrictions();
|
||||
const isVNCServerEnabled = config.serverVncAllowed;
|
||||
const vncServerManaged = mdm.allowServerVNC != null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -16,17 +19,23 @@ export function SettingsVNC() {
|
||||
onChange={(v) => setField("serverVncAllowed", v)}
|
||||
label={t("settings.vnc.server.label")}
|
||||
helpText={t("settings.vnc.server.help")}
|
||||
disabled={vncServerManaged}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={t("settings.vnc.section.approval")} disabled={!isVNCServerEnabled}>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableVncApproval}
|
||||
onChange={(v) => setField("disableVncApproval", !v)}
|
||||
label={t("settings.vnc.approval.label")}
|
||||
helpText={t("settings.vnc.approval.help")}
|
||||
/>
|
||||
</SectionGroup>
|
||||
{!mdm.disableVNCApproval && (
|
||||
<SectionGroup
|
||||
title={t("settings.vnc.section.approval")}
|
||||
disabled={!isVNCServerEnabled}
|
||||
>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableVncApproval}
|
||||
onChange={(v) => setField("disableVncApproval", !v)}
|
||||
label={t("settings.vnc.approval.label")}
|
||||
helpText={t("settings.vnc.approval.help")}
|
||||
/>
|
||||
</SectionGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ type MDMFields struct {
|
||||
DisableClientRoutes bool `json:"disableClientRoutes"`
|
||||
DisableServerRoutes bool `json:"disableServerRoutes"`
|
||||
AllowServerSSH *bool `json:"allowServerSSH"`
|
||||
AllowServerVNC *bool `json:"allowServerVNC"`
|
||||
DisableVNCApproval bool `json:"disableVNCApproval"`
|
||||
DisableAutoConnect bool `json:"disableAutoConnect"`
|
||||
BlockInbound bool `json:"blockInbound"`
|
||||
DisableMetricsCollection bool `json:"disableMetricsCollection"`
|
||||
@@ -261,4 +263,8 @@ func applyMDMRestrictions(mdm *MDMFields, cfgResp *proto.GetConfigResponse) {
|
||||
allowed := cfgResp.GetServerSSHAllowed()
|
||||
mdm.AllowServerSSH = &allowed
|
||||
}
|
||||
if _, ok := set["allowServerVNC"]; ok {
|
||||
allowed := cfgResp.GetServerVNCAllowed()
|
||||
mdm.AllowServerVNC = &allowed
|
||||
}
|
||||
}
|
||||
|
||||
44
client/ui/services/settings_mdm_test.go
Normal file
44
client/ui/services/settings_mdm_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
//go:build !android && !ios && !freebsd && !js
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
func TestApplyMDMRestrictions_VNCFields(t *testing.T) {
|
||||
t.Run("unmanaged leaves fields at zero", func(t *testing.T) {
|
||||
var mdm MDMFields
|
||||
applyMDMRestrictions(&mdm, &proto.GetConfigResponse{})
|
||||
assert.Nil(t, mdm.AllowServerVNC)
|
||||
assert.False(t, mdm.DisableVNCApproval)
|
||||
})
|
||||
|
||||
t.Run("managed surfaces enforced values", func(t *testing.T) {
|
||||
var mdm MDMFields
|
||||
applyMDMRestrictions(&mdm, &proto.GetConfigResponse{
|
||||
MDMManagedFields: []string{"allowServerVNC", "disableVNCApproval"},
|
||||
ServerVNCAllowed: true,
|
||||
DisableVNCApproval: true,
|
||||
})
|
||||
require.NotNil(t, mdm.AllowServerVNC)
|
||||
assert.True(t, *mdm.AllowServerVNC, "AllowServerVNC should carry the enforced value")
|
||||
assert.True(t, mdm.DisableVNCApproval, "DisableVNCApproval should be flagged managed")
|
||||
})
|
||||
|
||||
t.Run("managed VNC disallowed surfaces false", func(t *testing.T) {
|
||||
var mdm MDMFields
|
||||
applyMDMRestrictions(&mdm, &proto.GetConfigResponse{
|
||||
MDMManagedFields: []string{"allowServerVNC"},
|
||||
ServerVNCAllowed: false,
|
||||
})
|
||||
require.NotNil(t, mdm.AllowServerVNC)
|
||||
assert.False(t, *mdm.AllowServerVNC)
|
||||
assert.False(t, mdm.DisableVNCApproval, "unmanaged approval stays zero")
|
||||
})
|
||||
}
|
||||
@@ -63,6 +63,12 @@
|
||||
<true/>
|
||||
|
||||
<!--
|
||||
<key>allowServerVNC</key>
|
||||
<true/>
|
||||
|
||||
<key>disableVNCApproval</key>
|
||||
<false/>
|
||||
|
||||
<key>disableAutoConnect</key>
|
||||
<false/>
|
||||
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
<key>allowServerSSH</key>
|
||||
<true/>
|
||||
<!--
|
||||
<key>allowServerVNC</key>
|
||||
<true/>
|
||||
<key>disableVNCApproval</key>
|
||||
<false/>
|
||||
<key>rosenpassEnabled</key>
|
||||
<true/>
|
||||
<key>rosenpassPermissive</key>
|
||||
|
||||
@@ -56,6 +56,8 @@ NULL='__UNSET__'
|
||||
managementURL='https://api.netbird.io:443'
|
||||
preSharedKey="$NULL" # secret; redacted in log
|
||||
allowServerSSH='true'
|
||||
allowServerVNC="$NULL"
|
||||
disableVNCApproval="$NULL"
|
||||
blockInbound="$NULL"
|
||||
disableAutoConnect="$NULL"
|
||||
disableClientRoutes="$NULL"
|
||||
@@ -153,6 +155,8 @@ main() {
|
||||
is_set "$managementURL" && emit_string managementURL "$managementURL"
|
||||
is_set "$preSharedKey" && emit_string preSharedKey "$preSharedKey"
|
||||
is_set "$allowServerSSH" && emit_bool allowServerSSH "$allowServerSSH"
|
||||
is_set "$allowServerVNC" && emit_bool allowServerVNC "$allowServerVNC"
|
||||
is_set "$disableVNCApproval" && emit_bool disableVNCApproval "$disableVNCApproval"
|
||||
is_set "$blockInbound" && emit_bool blockInbound "$blockInbound"
|
||||
is_set "$disableAutoConnect" && emit_bool disableAutoConnect "$disableAutoConnect"
|
||||
is_set "$disableClientRoutes" && emit_bool disableClientRoutes "$disableClientRoutes"
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
|
||||
<string id="AllowServerSSH_Name">Allow server SSH</string>
|
||||
<string id="AllowServerSSH_Help">When enabled, this client accepts incoming SSH sessions via NetBird SSH. Equivalent to --allow-server-ssh.</string>
|
||||
<string id="AllowServerVNC_Name">Allow server VNC</string>
|
||||
<string id="AllowServerVNC_Help">When enabled, this client accepts incoming remote desktop (VNC) sessions via NetBird. Equivalent to --allow-server-vnc.</string>
|
||||
<string id="DisableVNCApproval_Name">Disable VNC connection approval</string>
|
||||
<string id="DisableVNCApproval_Help">When enabled, incoming VNC sessions are accepted without prompting the local user for approval.</string>
|
||||
|
||||
<string id="RosenpassEnabled_Name">Enable Rosenpass</string>
|
||||
<string id="RosenpassEnabled_Help">Enables Rosenpass post-quantum key exchange on WireGuard tunnels. Both peers must support it.</string>
|
||||
|
||||
@@ -112,6 +112,30 @@
|
||||
<disabledValue><decimal value="0" /></disabledValue>
|
||||
</policy>
|
||||
|
||||
<policy name="AllowServerVNC"
|
||||
class="Machine"
|
||||
displayName="$(string.AllowServerVNC_Name)"
|
||||
explainText="$(string.AllowServerVNC_Help)"
|
||||
key="Software\Policies\NetBird"
|
||||
valueName="AllowServerVNC">
|
||||
<parentCategory ref="NetBird" />
|
||||
<supportedOn ref="SUPPORTED_NetBird_All" />
|
||||
<enabledValue><decimal value="1" /></enabledValue>
|
||||
<disabledValue><decimal value="0" /></disabledValue>
|
||||
</policy>
|
||||
|
||||
<policy name="DisableVNCApproval"
|
||||
class="Machine"
|
||||
displayName="$(string.DisableVNCApproval_Name)"
|
||||
explainText="$(string.DisableVNCApproval_Help)"
|
||||
key="Software\Policies\NetBird"
|
||||
valueName="DisableVNCApproval">
|
||||
<parentCategory ref="NetBird" />
|
||||
<supportedOn ref="SUPPORTED_NetBird_All" />
|
||||
<enabledValue><decimal value="1" /></enabledValue>
|
||||
<disabledValue><decimal value="0" /></disabledValue>
|
||||
</policy>
|
||||
|
||||
<policy name="RosenpassEnabled"
|
||||
class="Machine"
|
||||
displayName="$(string.RosenpassEnabled_Name)"
|
||||
|
||||
Reference in New Issue
Block a user