mirror of
https://github.com/netbirdio/ios-client.git
synced 2026-09-23 09:28:33 -07:00
Add a Troubleshoot toggle for remote debug bundle jobs (#216)
* Add a Troubleshoot toggle for remote debug bundle jobs The client refuses management-requested remote jobs unless RemoteJobsAllowed is enabled, and the iOS and tvOS apps had no way to turn it on. Expose the flag through ConfigurationProvider, add an "Allow remote debug bundles" toggle to the iOS Troubleshoot screen and a Troubleshooting section in the tvOS settings, and bump netbird-core to the commit that adds the SDK preference accessors. * Update submodule * Route the iOS remote jobs toggle through the view model setter The Toggle wrote the published property before onChange invoked setRemoteJobsAllowed, so on a failed commit the rollback restored the new value instead of the previous one. Use a Binding whose setter calls the view model directly, matching the tvOS settings view. * Lock the remote debug bundle toggle when MDM manages it (#224) * Lock the remote debug bundle toggle when MDM manages it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Decode the remote jobs snapshot key as allowRemoteJobs The Go snapshot reports this field under the policy key name, allowRemoteJobs, not remoteJobsAllowed. The synthesized coding key used the property name, so decodeIfPresent never found the key and the flag stayed false — the toggle looked editable under an MDM policy that manages it. Map the coding key explicitly and keep the property name, which the views already reference. Bumps netbird-core to pick up the matching Go-side fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Bump submodule * Use allowRemoteJobs key in MDM restriction test fixtures Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Enforce the remote jobs policy before the next poll setRemoteJobsAllowed validated against the cached MDM snapshot, which is only refreshed on onAppear or a 30s stale poll. The OS writes managed configuration from another process, so the in-process change notification never fires for it and a policy pushed mid-session went unenforced until the next poll. Refresh the snapshot before validating. The guard also checked only mdm.remoteJobsAllowed, while both toggles lock on remoteJobsAllowed || disableUpdateSettings, so the backstop could disagree with the control the user sees. Extract remoteJobsForbiddenByPolicy to mirror the lock. Route the rejection through commitSettings() so an MDM-refused commit raises the standard alert instead of a silent print. This matters most on tvOS, where commit() always reports success and the pre-commit guard is the only backstop. * Restore the persisted remote jobs setting after the test testUnmanagedDeviceStillAcceptsTheChange writes through ConfigurationProvider, which persists into the Go preferences store. tearDown() only restored the MDM dictionary, so an enabled remoteJobsAllowed could leak into later tests or a reused test-host launch. Capture the original value and restore it via defer, which runs while the device is still unmanaged so the setter is not rejected. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6116ea921c
commit
ecf3b2a82f
@@ -155,6 +155,7 @@
|
||||
71DD928A21BE4943BB0FEC1D /* iOSNetworksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A1CFF65CC44187912007EC /* iOSNetworksView.swift */; };
|
||||
94F739DA3E076313908BA6DF /* GlobalConstantsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E054D83063E440DAD0C52FA /* GlobalConstantsTests.swift */; };
|
||||
AA0011012F22001100000001 /* MDMBridgeIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0011002F22001100000001 /* MDMBridgeIntegrationTests.swift */; };
|
||||
AA0012012F22001200000001 /* RemoteJobsPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0012002F22001200000001 /* RemoteJobsPolicyTests.swift */; };
|
||||
AA0010012F22001000000001 /* MDMRestrictionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0010002F22001000000001 /* MDMRestrictionsTests.swift */; };
|
||||
962925F1DAA24D40B98D395B /* iOSPeersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42873052F9544A89B1408339 /* iOSPeersView.swift */; };
|
||||
978FC4702EEDF167002D0EB8 /* AppLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978FC46F2EEDF167002D0EB8 /* AppLogger.swift */; };
|
||||
@@ -305,6 +306,7 @@
|
||||
2F8A4B98A775451786C5DDF8 /* iOSSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSSettingsView.swift; sourceTree = "<group>"; };
|
||||
3E054D83063E440DAD0C52FA /* GlobalConstantsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlobalConstantsTests.swift; sourceTree = "<group>"; };
|
||||
AA0011002F22001100000001 /* MDMBridgeIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDMBridgeIntegrationTests.swift; sourceTree = "<group>"; };
|
||||
AA0012002F22001200000001 /* RemoteJobsPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteJobsPolicyTests.swift; sourceTree = "<group>"; };
|
||||
AA0010002F22001000000001 /* MDMRestrictionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDMRestrictionsTests.swift; sourceTree = "<group>"; };
|
||||
42873052F9544A89B1408339 /* iOSPeersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSPeersView.swift; sourceTree = "<group>"; };
|
||||
441C5AEE2EDF0DAE0055EEFC /* NetBird TV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "NetBird TV.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -750,6 +752,7 @@
|
||||
children = (
|
||||
3E054D83063E440DAD0C52FA /* GlobalConstantsTests.swift */,
|
||||
AA0011002F22001100000001 /* MDMBridgeIntegrationTests.swift */,
|
||||
AA0012002F22001200000001 /* RemoteJobsPolicyTests.swift */,
|
||||
AA0010002F22001000000001 /* MDMRestrictionsTests.swift */,
|
||||
8AA7193B3AE82DF185EDEB1B /* AppLoggerTests.swift */,
|
||||
53CB9305A9DC6CAD1895495A /* SharedUserDefaultsTests.swift */,
|
||||
@@ -1288,6 +1291,7 @@
|
||||
files = (
|
||||
94F739DA3E076313908BA6DF /* GlobalConstantsTests.swift in Sources */,
|
||||
AA0011012F22001100000001 /* MDMBridgeIntegrationTests.swift in Sources */,
|
||||
AA0012012F22001200000001 /* RemoteJobsPolicyTests.swift in Sources */,
|
||||
AA0010012F22001000000001 /* MDMRestrictionsTests.swift in Sources */,
|
||||
9CC0E000AE3F165CA72FD465 /* AppLoggerTests.swift in Sources */,
|
||||
1C9E4E97130030CE0D6C8F59 /* SharedUserDefaultsTests.swift in Sources */,
|
||||
|
||||
@@ -134,6 +134,7 @@ class ViewModel: ObservableObject {
|
||||
@Published var forceRelayConnection = true
|
||||
@Published var showForceRelayAlert = false
|
||||
@Published var disableIPv6 = false
|
||||
@Published var remoteJobsAllowed = false
|
||||
@Published var connectOnDemand = false
|
||||
@Published var showOnDemandAlert = false
|
||||
@Published var showOnDemandConflictAlert = false
|
||||
@@ -1156,6 +1157,39 @@ class ViewModel: ObservableObject {
|
||||
self.disableIPv6 = configProvider.disableIPv6
|
||||
}
|
||||
|
||||
/// Whether the policy owns the remote-jobs toggle, by managing it
|
||||
/// directly or by forbidding settings edits at all. Mirrors the lock the
|
||||
/// iOS and tvOS toggles apply, so the backstop cannot disagree with the
|
||||
/// control the user sees.
|
||||
private var remoteJobsForbiddenByPolicy: Bool {
|
||||
mdmRestrictions.mdm.remoteJobsAllowed || mdmRestrictions.features.disableUpdateSettings
|
||||
}
|
||||
|
||||
func setRemoteJobsAllowed(allowed: Bool) {
|
||||
// The snapshot can be up to a poll behind a policy pushed from
|
||||
// another process, and on tvOS commit() always reports success, so
|
||||
// the enforced value has to be re-read before it is trusted.
|
||||
refreshMDMRestrictions()
|
||||
guard !remoteJobsForbiddenByPolicy else {
|
||||
AppLogger.shared.log("MDM: refusing to change remote debug bundles while the setting is managed")
|
||||
self.remoteJobsAllowed = configProvider.remoteJobsAllowed
|
||||
settingsRejectedMessage = "This setting is managed by your organization and cannot be changed."
|
||||
showSettingsRejectedAlert = true
|
||||
return
|
||||
}
|
||||
let previous = self.remoteJobsAllowed
|
||||
self.remoteJobsAllowed = allowed
|
||||
configProvider.remoteJobsAllowed = allowed
|
||||
if !commitSettings() {
|
||||
self.remoteJobsAllowed = previous
|
||||
configProvider.remoteJobsAllowed = previous
|
||||
}
|
||||
}
|
||||
|
||||
func loadRemoteJobsSettings() {
|
||||
self.remoteJobsAllowed = configProvider.remoteJobsAllowed
|
||||
}
|
||||
|
||||
func setForcedRelayConnection(isEnabled: Bool) {
|
||||
let userDefaults = UserDefaults(suiteName: GlobalConstants.userPreferencesSuiteName)
|
||||
userDefaults?.set(isEnabled, forKey: GlobalConstants.keyForceRelayConnection)
|
||||
|
||||
@@ -34,6 +34,10 @@ struct TVSettingsView: View {
|
||||
viewModel.mdmRestrictions.mdm.rosenpassPermissive || editingDisabled
|
||||
}
|
||||
|
||||
private var remoteJobsLocked: Bool {
|
||||
viewModel.mdmRestrictions.mdm.remoteJobsAllowed || editingDisabled
|
||||
}
|
||||
|
||||
/// tvOS rows carry their explanation in the subtitle - there is no footer
|
||||
/// to put it in, and a row that is merely dimmed gives the user no reason.
|
||||
private func subtitle(_ text: String, managed: Bool) -> String {
|
||||
@@ -163,6 +167,22 @@ struct TVSettingsView: View {
|
||||
)
|
||||
}
|
||||
|
||||
TVSettingsSection(title: "Troubleshooting") {
|
||||
TVSettingsToggleRow(
|
||||
icon: "doc.zipper",
|
||||
title: "Remote Debug Bundles",
|
||||
subtitle: subtitle("Let your administrator request a debug bundle from this device",
|
||||
managed: remoteJobsLocked),
|
||||
isOn: Binding(
|
||||
get: { viewModel.remoteJobsAllowed },
|
||||
set: { newValue in
|
||||
viewModel.setRemoteJobsAllowed(allowed: newValue)
|
||||
}
|
||||
),
|
||||
isDisabled: remoteJobsLocked
|
||||
)
|
||||
}
|
||||
|
||||
TVSettingsSection(title: "Info") {
|
||||
TVSettingsRow(
|
||||
icon: "qrcode.viewfinder",
|
||||
@@ -202,6 +222,7 @@ struct TVSettingsView: View {
|
||||
viewModel.loadRosenpassSettings()
|
||||
viewModel.loadPreSharedKey()
|
||||
viewModel.loadIPv6Settings()
|
||||
viewModel.loadRemoteJobsSettings()
|
||||
}
|
||||
.sheet(isPresented: $showDocsQRCode) {
|
||||
TVQRCodeSheet(
|
||||
|
||||
@@ -13,6 +13,11 @@ struct TroubleshootView: View {
|
||||
@State private var uploadKey = ""
|
||||
@State private var showCopiedAlert = false
|
||||
|
||||
private var remoteJobsLocked: Bool {
|
||||
viewModel.mdmRestrictions.mdm.remoteJobsAllowed
|
||||
|| viewModel.mdmRestrictions.features.disableUpdateSettings
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Logging")) {
|
||||
@@ -20,15 +25,36 @@ struct TroubleshootView: View {
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
|
||||
Section(header: Text("Debug Bundle"), footer: Text("Sensitive data includes IP addresses, domain names, and private keys.")) {
|
||||
Section {
|
||||
Toggle("Anonymize sensitive data", isOn: $viewModel.anonymizeDebugBundle)
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle("Allow remote debug bundles", isOn: Binding(
|
||||
get: { viewModel.remoteJobsAllowed },
|
||||
set: { newValue in
|
||||
viewModel.setRemoteJobsAllowed(allowed: newValue)
|
||||
}
|
||||
))
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.mdmLocked(remoteJobsLocked)
|
||||
|
||||
bundleActionContent
|
||||
} header: {
|
||||
Text("Debug Bundle")
|
||||
} footer: {
|
||||
if remoteJobsLocked {
|
||||
MDMManagedFooter()
|
||||
} else {
|
||||
Text("Sensitive data includes IP addresses, domain names, and private keys. Allowing remote debug bundles lets your administrator request one from this device through the management server; this takes effect on the next connection.")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Troubleshoot")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear {
|
||||
viewModel.refreshMDMRestrictions()
|
||||
viewModel.loadRemoteJobsSettings()
|
||||
}
|
||||
.alert(isPresented: $viewModel.showLogLevelChangedAlert) {
|
||||
Alert(
|
||||
title: Text("Changing Log Level"),
|
||||
|
||||
@@ -30,6 +30,7 @@ final class MDMRestrictionsTests: XCTestCase {
|
||||
"disableMetricsCollection": false,
|
||||
"splitTunnelMode": false,
|
||||
"splitTunnelApps": false,
|
||||
"allowRemoteJobs": true,
|
||||
"disableAdvancedView": false
|
||||
},
|
||||
"features": {
|
||||
@@ -50,11 +51,19 @@ final class MDMRestrictionsTests: XCTestCase {
|
||||
XCTAssertTrue(r.mdm.disableAutoConnect)
|
||||
XCTAssertEqual(r.mdm.disableAdvancedView, false)
|
||||
XCTAssertFalse(r.mdm.hidesAdvancedView)
|
||||
XCTAssertTrue(r.mdm.remoteJobsAllowed)
|
||||
XCTAssertTrue(r.features.disableProfiles)
|
||||
XCTAssertFalse(r.features.disableNetworks)
|
||||
XCTAssertFalse(r.features.disableUpdateSettings)
|
||||
}
|
||||
|
||||
func testRemoteJobsAllowedDefaultsToUnmanaged() {
|
||||
XCTAssertFalse(MDMRestrictions.decode(#"{"mdm":{}}"#).mdm.remoteJobsAllowed)
|
||||
XCTAssertFalse(MDMRestrictions.empty.mdm.remoteJobsAllowed)
|
||||
XCTAssertTrue(MDMRestrictions.decode(#"{"mdm":{"allowRemoteJobs":true}}"#).mdm.remoteJobsAllowed)
|
||||
XCTAssertFalse(MDMRestrictions.decode(#"{"mdm":{"allowRemoteJobs":false}}"#).mdm.remoteJobsAllowed)
|
||||
}
|
||||
|
||||
/// `allowServerSSH` is tri-state: absent and explicit null both mean
|
||||
/// "not managed", and must not collapse into `false`.
|
||||
func testAllowServerSSHTriState() {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// RemoteJobsPolicyTests.swift
|
||||
// NetBirdTests
|
||||
//
|
||||
// Covers the backstop on the remote debug bundle toggle: a policy pushed
|
||||
// from another process must be enforced by the very next write, not from
|
||||
// the next poll onwards. Runs on iOS and tvOS - on tvOS commit() always
|
||||
// reports success, so the pre-commit guard is the only thing standing
|
||||
// between a managed device and an enabled remote job.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import NetBird
|
||||
|
||||
final class RemoteJobsPolicyTests: XCTestCase {
|
||||
|
||||
private let key = MDMPolicyFetcher.managedConfigKey
|
||||
private var saved: [String: Any]?
|
||||
|
||||
private var policyForcedByLaunchArgument: Bool {
|
||||
UserDefaults.standard
|
||||
.volatileDomain(forName: UserDefaults.argumentDomain)[key] != nil
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
try XCTSkipIf(
|
||||
policyForcedByLaunchArgument,
|
||||
"A policy is pinned by the scheme's launch arguments "
|
||||
+ "(-\(MDMPolicyFetcher.managedConfigKey)). Uncheck it in "
|
||||
+ "Edit Scheme > Run > Arguments to run these tests."
|
||||
)
|
||||
saved = UserDefaults.standard.dictionary(forKey: key)
|
||||
UserDefaults.standard.removeObject(forKey: key)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let saved = saved {
|
||||
UserDefaults.standard.set(saved, forKey: key)
|
||||
} else {
|
||||
UserDefaults.standard.removeObject(forKey: key)
|
||||
}
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
private func push(_ policy: [String: Any]) {
|
||||
UserDefaults.standard.set(policy, forKey: key)
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
|
||||
/// A ViewModel holding the unmanaged snapshot the app starts with, which
|
||||
/// is what a policy arriving mid-session has to override.
|
||||
@MainActor
|
||||
private func makeViewModelWithStaleSnapshot() -> ViewModel {
|
||||
let viewModel = ViewModel()
|
||||
viewModel.mdmRestrictions = .empty
|
||||
XCTAssertFalse(viewModel.mdmRestrictions.mdm.remoteJobsAllowed)
|
||||
XCTAssertFalse(viewModel.mdmRestrictions.features.disableUpdateSettings)
|
||||
return viewModel
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testAllowRemoteJobsPolicyIsEnforcedBeforeTheNextPoll() throws {
|
||||
let viewModel = makeViewModelWithStaleSnapshot()
|
||||
push(["allowRemoteJobs": false])
|
||||
try XCTSkipUnless(
|
||||
MDMRestrictions.current().mdm.remoteJobsAllowed,
|
||||
"the Go policy loader did not render allowRemoteJobs as managed"
|
||||
)
|
||||
|
||||
viewModel.setRemoteJobsAllowed(allowed: true)
|
||||
|
||||
XCTAssertTrue(viewModel.showSettingsRejectedAlert)
|
||||
XCTAssertEqual(
|
||||
viewModel.settingsRejectedMessage,
|
||||
"This setting is managed by your organization and cannot be changed."
|
||||
)
|
||||
XCTAssertTrue(viewModel.mdmRestrictions.mdm.remoteJobsAllowed)
|
||||
}
|
||||
|
||||
/// disableUpdateSettings locks the toggle in both UIs, so the setter has
|
||||
/// to refuse on it too - otherwise the lock is cosmetic.
|
||||
@MainActor
|
||||
func testDisableUpdateSettingsPolicyIsEnforcedBeforeTheNextPoll() throws {
|
||||
let viewModel = makeViewModelWithStaleSnapshot()
|
||||
push(["disableUpdateSettings": true])
|
||||
try XCTSkipUnless(
|
||||
MDMRestrictions.current().features.disableUpdateSettings,
|
||||
"the Go policy loader did not render disableUpdateSettings as managed"
|
||||
)
|
||||
|
||||
viewModel.setRemoteJobsAllowed(allowed: true)
|
||||
|
||||
XCTAssertTrue(viewModel.showSettingsRejectedAlert)
|
||||
XCTAssertEqual(
|
||||
viewModel.settingsRejectedMessage,
|
||||
"This setting is managed by your organization and cannot be changed."
|
||||
)
|
||||
XCTAssertTrue(viewModel.mdmRestrictions.features.disableUpdateSettings)
|
||||
}
|
||||
|
||||
/// The refresh must not turn an unmanaged device read-only.
|
||||
@MainActor
|
||||
func testUnmanagedDeviceStillAcceptsTheChange() throws {
|
||||
let viewModel = makeViewModelWithStaleSnapshot()
|
||||
try XCTSkipUnless(
|
||||
MDMRestrictions.current() == .empty,
|
||||
"the environment reports a policy for an unmanaged device"
|
||||
)
|
||||
|
||||
viewModel.loadRemoteJobsSettings()
|
||||
let original = viewModel.remoteJobsAllowed
|
||||
defer { viewModel.setRemoteJobsAllowed(allowed: original) }
|
||||
|
||||
viewModel.setRemoteJobsAllowed(allowed: true)
|
||||
|
||||
XCTAssertFalse(viewModel.showSettingsRejectedAlert)
|
||||
XCTAssertTrue(viewModel.remoteJobsAllowed)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,11 @@ protocol ConfigurationProvider {
|
||||
/// Whether IPv6 overlay addressing is disabled
|
||||
var disableIPv6: Bool { get set }
|
||||
|
||||
// MARK: - Remote Jobs
|
||||
|
||||
/// Whether management may run remote jobs (debug bundle requests) on this peer
|
||||
var remoteJobsAllowed: Bool { get set }
|
||||
|
||||
// MARK: - Pre-Shared Key
|
||||
|
||||
/// Stages a new pre-shared key; an empty string clears it. Write-only by
|
||||
@@ -118,6 +123,23 @@ final class iOSConfigurationProvider: ConfigurationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Remote Jobs
|
||||
|
||||
var remoteJobsAllowed: Bool {
|
||||
get {
|
||||
var result = ObjCBool(false)
|
||||
do {
|
||||
try preferences.getRemoteJobsAllowed(&result)
|
||||
} catch {
|
||||
print("ConfigurationProvider: Failed to read remoteJobsAllowed - \(error)")
|
||||
}
|
||||
return result.boolValue
|
||||
}
|
||||
set {
|
||||
preferences.setRemoteJobsAllowed(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Pre-Shared Key
|
||||
|
||||
func setPreSharedKey(_ key: String) {
|
||||
@@ -235,6 +257,13 @@ final class tvOSConfigurationProvider: ConfigurationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Remote Jobs
|
||||
|
||||
var remoteJobsAllowed: Bool {
|
||||
get { extractJSONBool(field: "RemoteJobsAllowed") ?? false }
|
||||
set { updateJSONField(field: "RemoteJobsAllowed", value: newValue) }
|
||||
}
|
||||
|
||||
// MARK: - Pre-Shared Key
|
||||
|
||||
func setPreSharedKey(_ key: String) {
|
||||
|
||||
@@ -34,6 +34,7 @@ struct MDMRestrictions: Equatable {
|
||||
var disableMetricsCollection: Bool = false
|
||||
var splitTunnelMode: Bool = false
|
||||
var splitTunnelApps: Bool = false
|
||||
var remoteJobsAllowed: Bool = false
|
||||
/// Tri-state, like `allowServerSSH`: nil means the key is not managed,
|
||||
/// and an explicit false means the section is allowed - only true
|
||||
/// hides it.
|
||||
@@ -87,6 +88,7 @@ extension MDMRestrictions.Fields: Decodable {
|
||||
allowServerSSH, disableAutoConnect, disableAutostart, blockInbound,
|
||||
disableMetricsCollection, splitTunnelMode, splitTunnelApps,
|
||||
disableAdvancedView
|
||||
case remoteJobsAllowed = "allowRemoteJobs"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
@@ -109,6 +111,7 @@ extension MDMRestrictions.Fields: Decodable {
|
||||
disableMetricsCollection = try flag(.disableMetricsCollection)
|
||||
splitTunnelMode = try flag(.splitTunnelMode)
|
||||
splitTunnelApps = try flag(.splitTunnelApps)
|
||||
remoteJobsAllowed = try flag(.remoteJobsAllowed)
|
||||
// Tri-state: absent and JSON null both mean "not managed".
|
||||
disableAdvancedView = try c.decodeIfPresent(Bool.self, forKey: .disableAdvancedView)
|
||||
}
|
||||
|
||||
+1
-1
Submodule netbird-core updated: 27991aab98...9615d2ab16
Reference in New Issue
Block a user