feat(apple): config to hide resource list (#10824)

Adds a configuration variable `hideResourceList` accessible by
provisioning profile only to hide or show the Resource list. This is
helpful when end-users need not be concerned with the resources
available to their account.

Also updates the associated ProfileManifests, docs, and a little bit of
housekeeping around `configuration`, making it public for direct access.

<img width="292" height="228" alt="Screenshot 2025-11-09 at 9 12 47 PM"
src="https://github.com/user-attachments/assets/a4ce5586-bf92-4ebc-bc0d-51215e1efd61"
/>


Related: https://github.com/ProfileManifests/ProfileManifests/pull/839
Fixes: #10808

---------

Signed-off-by: Jamil <jamilbk@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jamil
2025-11-10 09:55:27 -10:00
committed by GitHub
parent 5ae2707719
commit bd2abbaae3
9 changed files with 141 additions and 89 deletions

View File

@@ -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()

View File

@@ -43,7 +43,7 @@ public final class Store: ObservableObject {
private var resourcesTimer: Timer?
private var resourceUpdateTask: Task<Void, Never>?
private var configuration: Configuration
public let configuration: Configuration
private var vpnConfigurationManager: VPNConfigurationManager?
private var cancellables: Set<AnyCancellable> = []

View File

@@ -27,6 +27,7 @@ import SwiftUI
var cancellables: Set<AnyCancellable> = []
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()
}

View File

@@ -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…")

View File

@@ -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())
}

View File

@@ -58,6 +58,10 @@
<true/>
<!-- Whether to show or hide the admin portal link in the main menu. -->
<key>hideResourceList</key>
<true/>
<!-- Whether to show or hide the resource list in the main menu. -->
<key>logFilter</key>
<string>info</string>

View File

@@ -175,6 +175,16 @@ A profile can consist of payloads with different version numbers. For example, c
<key>pfm_type</key>
<string>boolean</string>
</dict>
<dict>
<key>pfm_description</key>
<string>Hide the Resource List in the Firezone menu in the macOS menu bar. If unset, defaults to false.</string>
<key>pfm_name</key>
<string>hideResourceList</string>
<key>pfm_title</key>
<string>Hide resource list</string>
<key>pfm_type</key>
<string>boolean</string>
</dict>
<dict>
<key>pfm_description</key>
<string>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.</string>

View File

@@ -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

View File

@@ -32,6 +32,10 @@ export default function Apple() {
Fixes an issue where the order of upstream / system DNS resolvers was
not respected.
</ChangeItem>
<ChangeItem pull="10824">
Adds support for <code>hideResourceList</code> managed configuration
key to hide the Resource List in the macOS and iOS apps.
</ChangeItem>
</Unreleased>
<Entry version="1.5.9" date={new Date("2025-10-20")}>
<ChangeItem pull="10603">