diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Configuration.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Configuration.swift index cc00edd3a..6ea0347ef 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Configuration.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Configuration.swift @@ -18,6 +18,7 @@ public class Configuration: ObservableObject { @Published private(set) var publishedInternetResourceEnabled = false @Published private(set) var publishedHideAdminPortalMenuItem = false + @Published private(set) var publishedHideResourceList = false var isAuthURLForced: Bool { defaults.objectIsForced(forKey: Keys.authURL) } var isApiURLForced: Bool { defaults.objectIsForced(forKey: Keys.apiURL) } @@ -51,6 +52,11 @@ public class Configuration: ObservableObject { set { defaults.set(newValue, forKey: Keys.hideAdminPortalMenuItem) } } + var hideResourceList: Bool { + get { defaults.bool(forKey: Keys.hideResourceList) } + set { defaults.set(newValue, forKey: Keys.hideResourceList) } + } + var connectOnStart: Bool { get { defaults.bool(forKey: Keys.connectOnStart) } set { defaults.set(newValue, forKey: Keys.connectOnStart) } @@ -102,6 +108,7 @@ public class Configuration: ObservableObject { static let accountSlug = "accountSlug" static let internetResourceEnabled = "internetResourceEnabled" static let hideAdminPortalMenuItem = "hideAdminPortalMenuItem" + static let hideResourceList = "hideResourceList" static let connectOnStart = "connectOnStart" static let startOnLogin = "startOnLogin" static let disableUpdateCheck = "disableUpdateCheck" @@ -119,6 +126,7 @@ public class Configuration: ObservableObject { self.publishedInternetResourceEnabled = internetResourceEnabled self.publishedHideAdminPortalMenuItem = hideAdminPortalMenuItem + self.publishedHideResourceList = hideResourceList NotificationCenter.default.addObserver( self, @@ -173,6 +181,7 @@ public class Configuration: ObservableObject { // Update published properties self.publishedInternetResourceEnabled = internetResourceEnabled self.publishedHideAdminPortalMenuItem = hideAdminPortalMenuItem + self.publishedHideResourceList = hideResourceList // Announce we changed objectWillChange.send() diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift index 9eaf4cadf..5c0644094 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift @@ -43,7 +43,7 @@ public final class Store: ObservableObject { private var resourcesTimer: Timer? private var resourceUpdateTask: Task? - private var configuration: Configuration + public let configuration: Configuration private var vpnConfigurationManager: VPNConfigurationManager? private var cancellables: Set = [] diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift index 960b5546b..c79fd4009 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift @@ -27,6 +27,7 @@ import SwiftUI var cancellables: Set = [] var updateChecker: UpdateChecker var updateMenuDisplayed: Bool = false + var hideResourceList: Bool var signedOutIcon: NSImage? var signedInConnectedIcon: NSImage? var signedOutIconNotification: NSImage? @@ -164,10 +165,7 @@ import SwiftUI return menuItem }() - private let configuration: Configuration - - public init(store: Store, configuration: Configuration? = nil) { - self.configuration = configuration ?? Configuration.shared + public init(store: Store) { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) self.store = store self.updateChecker = UpdateChecker() @@ -182,6 +180,7 @@ import SwiftUI self.connectingAnimationImages[.first] = NSImage(named: "MenuBarIconConnecting1") self.connectingAnimationImages[.second] = NSImage(named: "MenuBarIconConnecting2") self.connectingAnimationImages[.last] = NSImage(named: "MenuBarIconConnecting3") + self.hideResourceList = self.store.configuration.publishedHideResourceList super.init() @@ -215,24 +214,32 @@ import SwiftUI self.handleStatusChanged() }).store(in: &cancellables) - configuration.$publishedInternetResourceEnabled + store.configuration.$publishedInternetResourceEnabled .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] newEnabled in guard let self = self else { return } - if configuration.internetResourceEnabled != newEnabled { + if store.configuration.internetResourceEnabled != newEnabled { handleResourceListChanged() } }) .store(in: &cancellables) - configuration.$publishedHideAdminPortalMenuItem + store.configuration.$publishedHideAdminPortalMenuItem .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] newValue in self?.updateConfigurableMenuItems(hideAdminPortalMenuItem: newValue) }) .store(in: &cancellables) + store.configuration.$publishedHideResourceList + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] newValue in + self?.hideResourceList = newValue + self?.handleResourceListChanged() + }) + .store(in: &cancellables) + updateChecker.$updateAvailable .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] _ in @@ -273,23 +280,28 @@ import SwiftUI } func populateResourceMenus(_ newResources: [Resource]) { - // If we have no favorites, then everything is a favorite - let hasAnyFavorites = newResources.contains { store.favorites.contains($0.id) } - let newFavorites = - if hasAnyFavorites { - newResources.filter { store.favorites.contains($0.id) || $0.isInternetResource() } - } else { - newResources - } - let newOthers: [Resource] = - if hasAnyFavorites { - newResources.filter { !store.favorites.contains($0.id) && !$0.isInternetResource() } - } else { - [] - } + if self.hideResourceList { + populateFavoriteResourcesMenu([]) + populateOtherResourcesMenu([]) + } else { + // If we have no favorites, then everything is a favorite + let hasAnyFavorites = newResources.contains { store.favorites.contains($0.id) } + let newFavorites = + if hasAnyFavorites { + newResources.filter { store.favorites.contains($0.id) || $0.isInternetResource() } + } else { + newResources + } + let newOthers: [Resource] = + if hasAnyFavorites { + newResources.filter { !store.favorites.contains($0.id) && !$0.isInternetResource() } + } else { + [] + } - populateFavoriteResourcesMenu(newFavorites) - populateOtherResourcesMenu(newOthers) + populateFavoriteResourcesMenu(newFavorites) + populateOtherResourcesMenu(newOthers) + } } func populateFavoriteResourcesMenu(_ newFavorites: [Resource]) { @@ -310,7 +322,7 @@ import SwiftUI } } lastShownFavorites = newFavorites - wasInternetResourceEnabled = configuration.internetResourceEnabled + wasInternetResourceEnabled = store.configuration.internetResourceEnabled } func populateOtherResourcesMenu(_ newOthers: [Resource]) { @@ -338,7 +350,7 @@ import SwiftUI } } lastShownOthers = newOthers - wasInternetResourceEnabled = configuration.internetResourceEnabled + wasInternetResourceEnabled = store.configuration.internetResourceEnabled } func updateStatusItemIcon() { @@ -386,47 +398,55 @@ import SwiftUI } } + // Update resources "header" menu items. An administrator can choose to set a configuration to + // hide the Resource List at which point we avoid displaying it. func updateResourcesMenuItems() { - // Update resources "header" menu items - switch store.vpnStatus { - case .connecting: - resourcesTitleMenuItem.isHidden = true - resourcesUnavailableMenuItem.isHidden = false - resourcesUnavailableReasonMenuItem.isHidden = false - resourcesUnavailableReasonMenuItem.target = nil - resourcesUnavailableReasonMenuItem.title = "Connecting…" - resourcesSeparatorMenuItem.isHidden = false - case .connected: - resourcesTitleMenuItem.isHidden = false - resourcesUnavailableMenuItem.isHidden = true - resourcesUnavailableReasonMenuItem.isHidden = true - resourcesTitleMenuItem.title = resourceMenuTitle(store.resourceList) - resourcesSeparatorMenuItem.isHidden = false - case .reasserting: - resourcesTitleMenuItem.isHidden = true - resourcesUnavailableMenuItem.isHidden = false - resourcesUnavailableReasonMenuItem.isHidden = false - resourcesUnavailableReasonMenuItem.target = nil - resourcesUnavailableReasonMenuItem.title = "No network connectivity" - resourcesSeparatorMenuItem.isHidden = false - case .disconnecting: - resourcesTitleMenuItem.isHidden = true - resourcesUnavailableMenuItem.isHidden = false - resourcesUnavailableReasonMenuItem.isHidden = false - resourcesUnavailableReasonMenuItem.target = nil - resourcesUnavailableReasonMenuItem.title = "Disconnecting…" - resourcesSeparatorMenuItem.isHidden = false - case nil, .disconnected, .invalid: - // We should never be in a state where the tunnel is - // down but the user is signed in, but we have - // code to handle it just for the sake of completion. + if self.hideResourceList { resourcesTitleMenuItem.isHidden = true resourcesUnavailableMenuItem.isHidden = true resourcesUnavailableReasonMenuItem.isHidden = true - resourcesUnavailableReasonMenuItem.title = "Disconnected" resourcesSeparatorMenuItem.isHidden = true - @unknown default: - break + } else { + switch store.vpnStatus { + case .connecting: + resourcesTitleMenuItem.isHidden = true + resourcesUnavailableMenuItem.isHidden = false + resourcesUnavailableReasonMenuItem.isHidden = false + resourcesUnavailableReasonMenuItem.target = nil + resourcesUnavailableReasonMenuItem.title = "Connecting…" + resourcesSeparatorMenuItem.isHidden = false + case .connected: + resourcesTitleMenuItem.isHidden = false + resourcesUnavailableMenuItem.isHidden = true + resourcesUnavailableReasonMenuItem.isHidden = true + resourcesTitleMenuItem.title = resourceMenuTitle(store.resourceList) + resourcesSeparatorMenuItem.isHidden = false + case .reasserting: + resourcesTitleMenuItem.isHidden = true + resourcesUnavailableMenuItem.isHidden = false + resourcesUnavailableReasonMenuItem.isHidden = false + resourcesUnavailableReasonMenuItem.target = nil + resourcesUnavailableReasonMenuItem.title = "No network connectivity" + resourcesSeparatorMenuItem.isHidden = false + case .disconnecting: + resourcesTitleMenuItem.isHidden = true + resourcesUnavailableMenuItem.isHidden = false + resourcesUnavailableReasonMenuItem.isHidden = false + resourcesUnavailableReasonMenuItem.target = nil + resourcesUnavailableReasonMenuItem.title = "Disconnecting…" + resourcesSeparatorMenuItem.isHidden = false + case nil, .disconnected, .invalid: + // We should never be in a state where the tunnel is + // down but the user is signed in, but we have + // code to handle it just for the sake of completion. + resourcesTitleMenuItem.isHidden = true + resourcesUnavailableMenuItem.isHidden = true + resourcesUnavailableReasonMenuItem.isHidden = true + resourcesUnavailableReasonMenuItem.title = "Disconnected" + resourcesSeparatorMenuItem.isHidden = true + @unknown default: + break + } } } @@ -503,7 +523,7 @@ import SwiftUI return false } - return wasInternetResourceEnabled != configuration.internetResourceEnabled + return wasInternetResourceEnabled != store.configuration.internetResourceEnabled } func refreshUpdateItem() { @@ -540,7 +560,7 @@ import SwiftUI func internetResourceTitle(resource: Resource) -> String { let status = - configuration.internetResourceEnabled ? StatusSymbol.enabled : StatusSymbol.disabled + store.configuration.internetResourceEnabled ? StatusSymbol.enabled : StatusSymbol.disabled return status + " " + resource.name } @@ -564,7 +584,7 @@ import SwiftUI } func internetResourceToggleTitle() -> String { - let isEnabled = configuration.internetResourceEnabled + let isEnabled = store.configuration.internetResourceEnabled return isEnabled ? "Disable this resource" : "Enable this resource" } @@ -767,13 +787,13 @@ import SwiftUI } @objc func adminPortalButtonTapped() { - guard let baseURL = URL(string: configuration.authURL) + guard let baseURL = URL(string: store.configuration.authURL) else { - Log.warning("Admin portal URL invalid: \(configuration.authURL)") + Log.warning("Admin portal URL invalid: \(store.configuration.authURL)") return } - let accountSlug = configuration.accountSlug + let accountSlug = store.configuration.accountSlug let authURL = baseURL.appendingPathComponent(accountSlug) Task { await NSWorkspace.shared.openAsync(authURL) } @@ -791,7 +811,7 @@ import SwiftUI @objc func supportButtonTapped() { let url = - URL(string: configuration.supportURL) ?? URL(string: Configuration.defaultSupportURL)! + URL(string: store.configuration.supportURL) ?? URL(string: Configuration.defaultSupportURL)! Task { await NSWorkspace.shared.openAsync(url) } } @@ -812,7 +832,7 @@ import SwiftUI } @objc func internetResourceToggle(_ sender: NSMenuItem) { - configuration.internetResourceEnabled = !configuration.internetResourceEnabled + store.configuration.internetResourceEnabled = !store.configuration.internetResourceEnabled sender.title = internetResourceToggleTitle() } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift index 451a6bcd9..d1982d1fc 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift @@ -17,28 +17,32 @@ import SwiftUI var body: some View { switch store.vpnStatus { case .connected: - switch store.resourceList { - case .loaded(let resources): - if resources.isEmpty { - Text("No Resources. Contact your admin to be granted access.") - } else { - List { - if !store.favorites.isEmpty() { - Section("Favorites") { - ResourceSection(resources: favoriteResources()) - } + if store.configuration.publishedHideResourceList { + Text("Signed in as \(store.actorName)") + } else { + switch store.resourceList { + case .loaded(let resources): + if resources.isEmpty { + Text("No Resources. Contact your admin to be granted access.") + } else { + List { + if !store.favorites.isEmpty() { + Section("Favorites") { + ResourceSection(resources: favoriteResources()) + } - Section("Other Resources") { - ResourceSection(resources: nonFavoriteResources()) + Section("Other Resources") { + ResourceSection(resources: nonFavoriteResources()) + } + } else { + ResourceSection(resources: resources) } - } else { - ResourceSection(resources: resources) } + .listStyle(GroupedListStyle()) } - .listStyle(GroupedListStyle()) + case .loading: + Text("Loading Resources...") } - case .loading: - Text("Loading Resources...") } case nil: Text("Loading VPN configurations from system settings…") diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift index f7212d5e4..69d40027c 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SettingsView.swift @@ -128,9 +128,9 @@ public struct SettingsView: View { ) } - public init(store: Store, configuration: Configuration? = nil) { + public init(store: Store) { self.store = store - self.configuration = configuration ?? Configuration.shared + self.configuration = store.configuration _viewModel = StateObject(wrappedValue: SettingsViewModel()) } diff --git a/website/public/policy-templates/macos/examples/all-keys.mobileconfig b/website/public/policy-templates/macos/examples/all-keys.mobileconfig index 23c26f3fe..736417cce 100644 --- a/website/public/policy-templates/macos/examples/all-keys.mobileconfig +++ b/website/public/policy-templates/macos/examples/all-keys.mobileconfig @@ -58,6 +58,10 @@ + hideResourceList + + + logFilter info diff --git a/website/public/policy-templates/macos/profile-manifests/dev.firezone.firezone.plist b/website/public/policy-templates/macos/profile-manifests/dev.firezone.firezone.plist index 12f86b009..e13d095b4 100644 --- a/website/public/policy-templates/macos/profile-manifests/dev.firezone.firezone.plist +++ b/website/public/policy-templates/macos/profile-manifests/dev.firezone.firezone.plist @@ -175,6 +175,16 @@ A profile can consist of payloads with different version numbers. For example, c pfm_type boolean + + pfm_description + Hide the Resource List in the Firezone menu in the macOS menu bar. If unset, defaults to false. + pfm_name + hideResourceList + pfm_title + Hide resource list + pfm_type + boolean + pfm_description Try to connect to Firezone using the saved token and configuration when the client application starts. If the authentication token is expired, the client will start in a disconnected state. Setting this field will override the user's setting. If unset, defaults to false. diff --git a/website/src/app/kb/deploy/clients/readme.mdx b/website/src/app/kb/deploy/clients/readme.mdx index b9fac4a66..527a7a5c8 100644 --- a/website/src/app/kb/deploy/clients/readme.mdx +++ b/website/src/app/kb/deploy/clients/readme.mdx @@ -80,6 +80,7 @@ managed configuration available and to which platforms they apply. | `disableUpdateCheck` | `Boolean` | `false` | Whether to disable the periodic update checker. The update checker is enabled by default for standalone macOS Clients. | macOS, iOS, Android | 1.5.0 | | `checkForUpdates` | `Boolean` | `false` | Enable or disable the periodic update checker. The update checker is enabled by default for Windows Clients. | Windows | 1.5.0 | | `hideAdminPortalMenuItem` | `Boolean` | `false` | Whether to show or hide the admin portal link in the main menu. | macOS, Windows | 1.5.0 | +| `hideResourceList` | `Boolean` | `false` | Whether to show or hide the resource list in the main menu. | macOS, iOS | 1.5.10 | | `supportURL` | `String` | `https://www.firezone.dev/support` | The destination URL used for the support link in the main menu. | macOS, iOS, Windows | 1.5.0 | ### Applying managed configuration diff --git a/website/src/components/Changelog/Apple.tsx b/website/src/components/Changelog/Apple.tsx index 176cebb72..8ace1064a 100644 --- a/website/src/components/Changelog/Apple.tsx +++ b/website/src/components/Changelog/Apple.tsx @@ -32,6 +32,10 @@ export default function Apple() { Fixes an issue where the order of upstream / system DNS resolvers was not respected. + + Adds support for hideResourceList managed configuration + key to hide the Resource List in the macOS and iOS apps. +