From 83c9e5b0d1a2196351f680b13315e65db5f842de Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Mon, 15 Jun 2026 13:54:07 +0200 Subject: [PATCH] fix allowserverssh mdm --- client/ui/frontend/CLAUDE.md | 2 +- client/ui/frontend/src/modules/main/MainPage.tsx | 2 +- .../src/modules/settings/SettingsNavigation.tsx | 2 +- .../src/modules/settings/SettingsPage.tsx | 2 +- client/ui/services/settings.go | 15 +++++++-------- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/client/ui/frontend/CLAUDE.md b/client/ui/frontend/CLAUDE.md index dc7d84e7e..c341b55a6 100644 --- a/client/ui/frontend/CLAUDE.md +++ b/client/ui/frontend/CLAUDE.md @@ -106,7 +106,7 @@ State that crosses screens/windows lives in context, each provider mounted exact - **`useStatus`** (`StatusContext`) — `{ status, error, refresh, isReady, isDaemonAvailable, isDaemonUnavailable }`. Owns the single `DaemonFeed.Get` + `netbird:status` subscription and the daemon gate (see Layouts). `refresh()` after Connect/Disconnect to dodge a few hundred ms of event-stream lag. - **`ProfileContext`** — `username`, `activeProfile`, `profiles`, plus `refresh` / `switchProfile` / `addProfile` / `removeProfile` / `logoutProfile`. `switchProfile` delegates to `ProfileSwitcher.SwitchActive` (the Go-side single source of truth — drives the optimistic-Connecting paint and `Peers` suppression). The other methods are thin wrappers over `Profiles.*` / `Connection.Logout` + a `refresh()`. - **`SettingsContext`** — `setField` / `saveField` / `saveFields` / `saveNow` over `Settings.GetConfig|SetConfig` with 400ms debounce. Renders `` while `config === null`. **PSK mask quirk:** `GetConfig` returns existing PSKs as `"**********"`; sending the mask back round-trips it into storage and `wgtypes.ParseKey` fails on the next connect — `save` drops the field when it equals the mask. -- **`MdmContext`** — `useMdm()` returns `config.managedFields` as `Record`, **keyed by the daemon's `mdm.Key*` names exactly as written in the policy source** (`managementURL`, `allowServerSSH`, `preSharedKey`, `wireguardPort`, `rosenpassEnabled`/`Permissive`, `disableClientRoutes`/`disableServerRoutes`, `disableAutoConnect`, `blockInbound`). No GUI-side renaming — what the Group Policy admin writes is what the lookup key is. Mounted in `AppLayout` (under `ProfileProvider`); fetches `Settings.GetConfig` once, re-fetches on the daemon's `netbird:event` `metadata.type=config_changed` push so policy flips paint live. No second copy of the locked *values* — MDM is a global override, so the active profile's resolved `useSettings().config.` already carries the MDM-enforced value. Consumers: Settings tabs hide individual toggles/sections (both rosenpass keys managed ⇒ whole encryption section hidden); `SettingsNavigation` + `SettingsPage` hide the SSH tab when `managed.allowServerSSH` is set and bounce `active="ssh"` back to General; `ProfileCreationModal` skips the Cloud/self-hosted picker when `managed.managementURL` is set and submits the resolved URL verbatim; `WelcomeDialog` reads `config.managedFields.managementURL` directly (sits outside `AppLayout`) to skip the management step on a fresh install. +- **`MdmContext`** — `useMdm()` returns `config.managedFields` as `Record`, **keyed by the daemon's `mdm.Key*` names exactly as written in the policy source** (`managementURL`, `allowServerSSH`, `preSharedKey`, `wireguardPort`, `rosenpassEnabled`/`Permissive`, `disableClientRoutes`/`disableServerRoutes`, `disableAutoConnect`, `blockInbound`). No GUI-side renaming — what the Group Policy admin writes is what the lookup key is. Mounted in `AppLayout` (under `ProfileProvider`); fetches `Settings.GetConfig` once, re-fetches on the daemon's `netbird:event` `metadata.type=config_changed` push so policy flips paint live. No second copy of the locked *values* — MDM is a global override, so the active profile's resolved `useSettings().config.` already carries the MDM-enforced value. Consumers: Settings tabs hide individual toggles/sections (both rosenpass keys managed ⇒ whole encryption section hidden); `mdm.*` fields are **is-managed flags** — `true` when the field name appears in `MDMManagedFields`, `false` otherwise. Consumers check truthiness (`!mdm.x` to mean "not managed"). The *resolved value* of a managed field is already enforced into `Config`, so consumers read it via `useSettings().config.` and hide the toggle when `mdm.` is true. **Three carve-outs** that carry the resolved value instead, populated outside the reflective is-managed loop in `services/settings.go` `GetRestrictions`: `mdm.managementURL` (string — used by `ProfileCreationModal` / `WelcomeDialog`, which need the URL to render the form); `mdm.allowServerSSH` (`*bool` / `boolean | null` — used by `SettingsNavigation` + `SettingsPage` to gate the SSH tab on `mdm.allowServerSSH ?? !features.disableUpdateSettings`; tri-state needed because the tab gate lives outside `SettingsProvider` and can't read `config.serverSshAllowed`, and MDM-managed-with-value-`false` must hide the tab — falsy fallthrough would leak it); and `mdm.disableAdvancedView` (`bool` — MDM-only on the daemon, no CLI fallback; plumbed via the `GetFeatures` RPC rather than `MDMManagedFields`, so set directly from `featResp.GetDisableAdvancedView()`. Value semantic: `true` iff MDM explicitly disables; the nil/false collapse is fine because both mean "advanced view available"). The three `features.*` gates (`disableProfiles`/`Networks`/`UpdateSettings`) live in `Features` because they accept a CLI-flag fallback in addition to MDM (`--disable-profiles` etc.) — `disableAdvancedView` doesn't, which is why it sits with the MDM-only carve-outs. `ProfileCreationModal` skips the Cloud/self-hosted picker when `managed.managementURL` is set and submits the resolved URL verbatim; `WelcomeDialog` reads `config.managedFields.managementURL` directly (sits outside `AppLayout`) to skip the management step on a fresh install. - **`DebugBundleContext`** — stages `idle → preparing-trace → reconnecting → capturing → restoring-level → bundling → uploading → done`. Cancellable via `AbortController` at any stage; cancel restores the original log level best-effort. Upload URL is the hardcoded `NETBIRD_UPLOAD_URL`. - **`ClientVersionContext`** — seeds from `Update.GetState()`, subscribes to `netbird:update:state`; exposes `{ updateAvailable, updateVersion, enforced, installing, triggerUpdate, updating }`. Three branches: 1. `available && !enforced` — download-only; `UpdateVersionCard` → opens GitHub releases. diff --git a/client/ui/frontend/src/modules/main/MainPage.tsx b/client/ui/frontend/src/modules/main/MainPage.tsx index 805674783..b87d38ca6 100644 --- a/client/ui/frontend/src/modules/main/MainPage.tsx +++ b/client/ui/frontend/src/modules/main/MainPage.tsx @@ -34,7 +34,7 @@ const MainBody = () => { const { viewMode, setViewMode } = useViewMode(); const { mdm, features } = useRestrictions(); - // Force flip the view if mdm changed it + // Force flip the view if MDM disabled advanced useEffect(() => { if (mdm.disableAdvancedView && viewMode === "advanced") { setViewMode("default"); diff --git a/client/ui/frontend/src/modules/settings/SettingsNavigation.tsx b/client/ui/frontend/src/modules/settings/SettingsNavigation.tsx index 1907c2114..13dbbe30f 100644 --- a/client/ui/frontend/src/modules/settings/SettingsNavigation.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsNavigation.tsx @@ -19,7 +19,7 @@ export const SettingsNavigation = () => { const { t } = useTranslation(); const { updateAvailable } = useClientVersion(); const { mdm, features } = useRestrictions(); - const showSsh = mdm.allowServerSSH || !features.disableUpdateSettings; + const showSsh = mdm.allowServerSSH ?? !features.disableUpdateSettings; const aboutAdornment = updateAvailable ? ( diff --git a/client/ui/frontend/src/modules/settings/SettingsPage.tsx b/client/ui/frontend/src/modules/settings/SettingsPage.tsx index ce3924647..49ba99754 100644 --- a/client/ui/frontend/src/modules/settings/SettingsPage.tsx +++ b/client/ui/frontend/src/modules/settings/SettingsPage.tsx @@ -54,7 +54,7 @@ export const SettingsPage = () => { [Tab.Network]: editable, [Tab.Security]: editable, [Tab.Profiles]: !features.disableProfiles, - [Tab.SSH]: mdm.allowServerSSH || editable, + [Tab.SSH]: mdm.allowServerSSH ?? editable, [Tab.Advanced]: editable, [Tab.Troubleshooting]: true, [Tab.About]: true, diff --git a/client/ui/services/settings.go b/client/ui/services/settings.go index eb01b1e8a..6bcc8a21b 100644 --- a/client/ui/services/settings.go +++ b/client/ui/services/settings.go @@ -18,13 +18,13 @@ type MDMFields struct { RosenpassPermissive bool `json:"rosenpassPermissive"` DisableClientRoutes bool `json:"disableClientRoutes"` DisableServerRoutes bool `json:"disableServerRoutes"` - AllowServerSSH bool `json:"allowServerSSH"` + AllowServerSSH *bool `json:"allowServerSSH"` DisableAutoConnect bool `json:"disableAutoConnect"` BlockInbound bool `json:"blockInbound"` DisableMetricsCollection bool `json:"disableMetricsCollection"` SplitTunnelMode bool `json:"splitTunnelMode"` SplitTunnelApps bool `json:"splitTunnelApps"` - DisableAdvancedView bool `json:"disableAdvancedView"` + DisableAdvancedView bool `json:"disableAdvancedView"` } type Features struct { @@ -222,6 +222,9 @@ func (s *Settings) GetRestrictions(ctx context.Context) (Restrictions, error) { DisableNetworks: featResp.GetDisableNetworks(), DisableUpdateSettings: featResp.GetDisableUpdateSettings(), }, + MDM: MDMFields{ + DisableAdvancedView: featResp.GetDisableAdvancedView(), + }, } managed := cfgResp.GetMDMManagedFields() if len(managed) > 0 { @@ -235,10 +238,6 @@ func (s *Settings) GetRestrictions(ctx context.Context) (Restrictions, error) { if v.Field(i).Kind() != reflect.Bool { continue } - // AllowServerSSH carries the resolved value (like ManagementURL), not the is-managed flag. - if t.Field(i).Name == "AllowServerSSH" { - continue - } if _, ok := set[t.Field(i).Tag.Get("json")]; ok { v.Field(i).SetBool(true) } @@ -247,9 +246,9 @@ func (s *Settings) GetRestrictions(ctx context.Context) (Restrictions, error) { r.MDM.ManagementURL = cfgResp.GetManagementUrl() } if _, ok := set["allowServerSSH"]; ok { - r.MDM.AllowServerSSH = cfgResp.GetServerSSHAllowed() + allowed := cfgResp.GetServerSSHAllowed() + r.MDM.AllowServerSSH = &allowed } } - r.MDM.DisableAdvancedView = featResp.GetDisableAdvancedView() return r, nil }