From 6e64db26fb3bcd1b57a977e6e04b8ee0c00ce212 Mon Sep 17 00:00:00 2001 From: Anand Biligiri Date: Sat, 8 Jun 2024 18:15:23 -0700 Subject: [PATCH] Disallow changes to managed preferences - Define enumerations for preferences that can be managed in an enterprise environment using MDM - Add methods in AppState to check for managed preferences - Update Advanced, Download, Experiments and Update preference panes to disable controls to modify any of the managed preferences - Update Xcode category list button to be disabled if preference is managed --- Xcodes/Backend/AppState+Install.swift | 4 +- Xcodes/Backend/AppState+Runtimes.swift | 2 +- Xcodes/Backend/AppState.swift | 37 ++++++++++++++++--- Xcodes/Backend/DataSource.swift | 2 + Xcodes/Backend/Downloader.swift | 2 + .../Preferences/AdvancedPreferencePane.swift | 5 ++- .../Preferences/DownloadPreferencePane.swift | 8 ++-- .../ExperiementsPreferencePane.swift | 1 + .../Preferences/UpdatesPreferencePane.swift | 27 ++++++++++---- Xcodes/Frontend/XcodeList/MainToolbar.swift | 1 + .../XcodeList/XcodeListCategory.swift | 2 + 11 files changed, 70 insertions(+), 21 deletions(-) diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 325f314f..1b2a30d5 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -13,8 +13,8 @@ extension AppState { // check to see if we should auto install for the user public func autoInstallIfNeeded() { - guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return } - + guard let storageValue = Current.defaults.get(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return } + if autoInstallType == .none { return } // get newest xcode version diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index 838f1856..50cfd003 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -90,7 +90,7 @@ extension AppState { // sets a proper cookie for runtimes try await validateADCSession(path: runtime.downloadPath) - let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2 + let downloader = Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2 let url = URL(string: runtime.source)! let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)" diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 46a5d633..9d551bf4 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -10,6 +10,23 @@ import os.log import DockProgress import XcodesKit +enum PreferenceKey: String { + case installPath + case localPath + case unxipExperiment + case createSymLinkOnSelect + case onSelectActionType + case showOpenInRosettaOption + case autoInstallation + case SUEnableAutomaticChecks + case includePrereleaseVersions + case downloader + case dataSource + case xcodeListCategory + + func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) } +} + class AppState: ObservableObject { private let client = AppleAPI.Client() internal let runtimeService = RuntimeService() @@ -66,18 +83,24 @@ class AppState: ObservableObject { } } + var disableLocalPathChange: Bool { PreferenceKey.localPath.isManaged() } + @Published var installPath = "" { didSet { Current.defaults.set(installPath, forKey: "installPath") } } - + + var disableInstallPathChange: Bool { PreferenceKey.installPath.isManaged() } + @Published var unxipExperiment = false { didSet { Current.defaults.set(unxipExperiment, forKey: "unxipExperiment") } } + var disableUnxipExperiment: Bool { PreferenceKey.unxipExperiment.isManaged() } + @Published var createSymLinkOnSelect = false { didSet { Current.defaults.set(createSymLinkOnSelect, forKey: "createSymLinkOnSelect") @@ -85,7 +108,7 @@ class AppState: ObservableObject { } var createSymLinkOnSelectDisabled: Bool { - return onSelectActionType == .rename + return onSelectActionType == .rename || PreferenceKey.createSymLinkOnSelect.isManaged() } @Published var onSelectActionType = SelectedActionType.none { @@ -98,6 +121,8 @@ class AppState: ObservableObject { } } + var onSelectActionTypeDisabled: Bool { PreferenceKey.onSelectActionType.isManaged() } + @Published var showOpenInRosettaOption = false { didSet { Current.defaults.set(showOpenInRosettaOption, forKey: "showOpenInRosettaOption") @@ -178,8 +203,8 @@ class AppState: ObservableObject { // MARK: Timer /// Runs a timer every 6 hours when app is open to check if it needs to auto install any xcodes func setupAutoInstallTimer() { - guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return } - + guard let storageValue = Current.defaults.get(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return } + if autoInstallType == .none { return } autoInstallTimer = Timer.scheduledTimer(withTimeInterval: 60*60*6, repeats: true) { [weak self] _ in @@ -479,7 +504,7 @@ class AppState: ObservableObject { .mapError { $0 as Error } } .flatMap { [unowned self] in - self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2) + self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2) } .receive(on: DispatchQueue.main) .sink( @@ -505,7 +530,7 @@ class AppState: ObservableObject { func installWithoutLogin(id: Xcode.ID) { guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return } - installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2) + installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { [unowned self] completion in diff --git a/Xcodes/Backend/DataSource.swift b/Xcodes/Backend/DataSource.swift index 69363ae3..03911848 100644 --- a/Xcodes/Backend/DataSource.swift +++ b/Xcodes/Backend/DataSource.swift @@ -14,4 +14,6 @@ public enum DataSource: String, CaseIterable, Identifiable, CustomStringConverti case .xcodeReleases: return "Xcode Releases" } } + + var isManaged: Bool { PreferenceKey.dataSource.isManaged() } } diff --git a/Xcodes/Backend/Downloader.swift b/Xcodes/Backend/Downloader.swift index 41eb6807..e155703a 100644 --- a/Xcodes/Backend/Downloader.swift +++ b/Xcodes/Backend/Downloader.swift @@ -13,4 +13,6 @@ public enum Downloader: String, CaseIterable, Identifiable, CustomStringConverti case .aria2: return "aria2" } } + + var isManaged: Bool { PreferenceKey.downloader.isManaged() } } diff --git a/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift b/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift index c9531c5c..13ab718e 100644 --- a/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift @@ -36,6 +36,7 @@ struct AdvancedPreferencePane: View { self.appState.installPath = path.string } } + .disabled(appState.disableInstallPathChange) Text("InstallPathDescription") .font(.footnote) .foregroundStyle(.secondary) @@ -72,6 +73,7 @@ struct AdvancedPreferencePane: View { self.appState.localPath = path.string } } + .disabled(appState.disableLocalPathChange) Text("LocalCachePathDescription") .font(.footnote) .foregroundStyle(.secondary) @@ -93,7 +95,8 @@ struct AdvancedPreferencePane: View { } .labelsHidden() .pickerStyle(.inline) - + .disabled(appState.onSelectActionTypeDisabled) + Text(appState.onSelectActionType.detailedDescription) .font(.footnote) .foregroundStyle(.secondary) diff --git a/Xcodes/Frontend/Preferences/DownloadPreferencePane.swift b/Xcodes/Frontend/Preferences/DownloadPreferencePane.swift index 62adf539..854c6159 100644 --- a/Xcodes/Frontend/Preferences/DownloadPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/DownloadPreferencePane.swift @@ -19,7 +19,7 @@ struct DownloadPreferencePane: View { } .labelsHidden() .fixedSize() - + Text("DataSourceDescription") .font(.footnote) .foregroundStyle(.secondary) @@ -27,7 +27,8 @@ struct DownloadPreferencePane: View { } } .groupBoxStyle(PreferencesGroupBoxStyle()) - + .disabled(dataSource.isManaged) + GroupBox(label: Text("Downloader")) { VStack(alignment: .leading) { Picker("Downloader", selection: $downloader) { @@ -38,7 +39,7 @@ struct DownloadPreferencePane: View { } .labelsHidden() .fixedSize() - + Text("DownloaderDescription") .font(.footnote) .foregroundStyle(.secondary) @@ -46,6 +47,7 @@ struct DownloadPreferencePane: View { } } .groupBoxStyle(PreferencesGroupBoxStyle()) + .disabled(downloader.isManaged) } } } diff --git a/Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift b/Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift index 5633f8f0..68dad51e 100644 --- a/Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift @@ -13,6 +13,7 @@ struct ExperimentsPreferencePane: View { "UseUnxipExperiment", isOn: $appState.unxipExperiment ) + .disabled(appState.disableUnxipExperiment) Text("FasterUnxipDescription") .font(.footnote) .foregroundStyle(.secondary) diff --git a/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift b/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift index 2f52418a..edad69bd 100644 --- a/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift @@ -15,11 +15,13 @@ struct UpdatesPreferencePane: View { "AutomaticInstallNewVersion", isOn: $autoInstallationType.isAutoInstalling ) - + .disabled(updater.disableAutoInstallNewVersions) + Toggle( "IncludePreRelease", isOn: $autoInstallationType.isAutoInstallingBeta ) + .disabled(updater.disableIncludePrereleaseVersions) } .fixedSize(horizontal: false, vertical: true) } @@ -34,17 +36,20 @@ struct UpdatesPreferencePane: View { isOn: $updater.automaticallyChecksForUpdates ) .fixedSize(horizontal: true, vertical: false) - + .disabled(updater.disableAutoUpdateXcodesApp) + Toggle( "IncludePreRelease", isOn: $updater.includePrereleaseVersions ) - + .disabled(updater.disableAutoUpdateXcodesAppPrereleaseVersions) + Button("CheckNow") { updater.checkForUpdates() } .padding(.top) - + .disabled(updater.disableAutoUpdateXcodesApp) + Text(String(format: localizeString("LastChecked"), lastUpdatedString)) .font(.footnote) .foregroundStyle(.secondary) @@ -83,12 +88,18 @@ class ObservableUpdater: ObservableObject { private var lastUpdateCheckDateObservation: NSKeyValueObservation? @Published var includePrereleaseVersions = false { didSet { - UserDefaults.standard.setValue(includePrereleaseVersions, forKey: "includePrereleaseVersions") - + Current.defaults.set(includePrereleaseVersions, forKey: "includePrereleaseVersions") + updaterDelegate.includePrereleaseVersions = includePrereleaseVersions } } - + + var disableAutoInstallNewVersions: Bool { PreferenceKey.autoInstallation.isManaged() } + var disableIncludePrereleaseVersions: Bool { PreferenceKey.autoInstallation.isManaged() } + + var disableAutoUpdateXcodesApp: Bool { PreferenceKey.SUEnableAutomaticChecks.isManaged() } + var disableAutoUpdateXcodesAppPrereleaseVersions: Bool { PreferenceKey.includePrereleaseVersions.isManaged() } + init() { updater = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: updaterDelegate, userDriverDelegate: nil).updater @@ -111,7 +122,7 @@ class ObservableUpdater: ObservableObject { self.lastUpdateCheckDate = updater.lastUpdateCheckDate } ) - includePrereleaseVersions = UserDefaults.standard.bool(forKey: "includePrereleaseVersions") + includePrereleaseVersions = Current.defaults.bool(forKey: "includePrereleaseVersions") ?? false } func checkForUpdates() { diff --git a/Xcodes/Frontend/XcodeList/MainToolbar.swift b/Xcodes/Frontend/XcodeList/MainToolbar.swift index 24b5193d..4ba2967a 100644 --- a/Xcodes/Frontend/XcodeList/MainToolbar.swift +++ b/Xcodes/Frontend/XcodeList/MainToolbar.swift @@ -44,6 +44,7 @@ struct MainToolbarModifier: ViewModifier { } } .help("FilterAvailableDescription") + .disabled(category.isManaged) Button(action: { isInstalledOnly.toggle() diff --git a/Xcodes/Frontend/XcodeList/XcodeListCategory.swift b/Xcodes/Frontend/XcodeList/XcodeListCategory.swift index 52ea7905..328f6364 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListCategory.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListCategory.swift @@ -14,4 +14,6 @@ enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConverti case .beta: return localizeString("Beta") } } + + var isManaged: Bool { PreferenceKey.xcodeListCategory.isManaged() } }