Skip to content

Commit 6e64db2

Browse files
committed
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
1 parent 4a4b469 commit 6e64db2

11 files changed

Lines changed: 70 additions & 21 deletions

Xcodes/Backend/AppState+Install.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ extension AppState {
1313

1414
// check to see if we should auto install for the user
1515
public func autoInstallIfNeeded() {
16-
guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
17-
16+
guard let storageValue = Current.defaults.get(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
17+
1818
if autoInstallType == .none { return }
1919

2020
// get newest xcode version

Xcodes/Backend/AppState+Runtimes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ extension AppState {
9090
// sets a proper cookie for runtimes
9191
try await validateADCSession(path: runtime.downloadPath)
9292

93-
let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2
93+
let downloader = Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2
9494

9595
let url = URL(string: runtime.source)!
9696
let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)"

Xcodes/Backend/AppState.swift

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@ import os.log
1010
import DockProgress
1111
import XcodesKit
1212

13+
enum PreferenceKey: String {
14+
case installPath
15+
case localPath
16+
case unxipExperiment
17+
case createSymLinkOnSelect
18+
case onSelectActionType
19+
case showOpenInRosettaOption
20+
case autoInstallation
21+
case SUEnableAutomaticChecks
22+
case includePrereleaseVersions
23+
case downloader
24+
case dataSource
25+
case xcodeListCategory
26+
27+
func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) }
28+
}
29+
1330
class AppState: ObservableObject {
1431
private let client = AppleAPI.Client()
1532
internal let runtimeService = RuntimeService()
@@ -66,26 +83,32 @@ class AppState: ObservableObject {
6683
}
6784
}
6885

86+
var disableLocalPathChange: Bool { PreferenceKey.localPath.isManaged() }
87+
6988
@Published var installPath = "" {
7089
didSet {
7190
Current.defaults.set(installPath, forKey: "installPath")
7291
}
7392
}
74-
93+
94+
var disableInstallPathChange: Bool { PreferenceKey.installPath.isManaged() }
95+
7596
@Published var unxipExperiment = false {
7697
didSet {
7798
Current.defaults.set(unxipExperiment, forKey: "unxipExperiment")
7899
}
79100
}
80101

102+
var disableUnxipExperiment: Bool { PreferenceKey.unxipExperiment.isManaged() }
103+
81104
@Published var createSymLinkOnSelect = false {
82105
didSet {
83106
Current.defaults.set(createSymLinkOnSelect, forKey: "createSymLinkOnSelect")
84107
}
85108
}
86109

87110
var createSymLinkOnSelectDisabled: Bool {
88-
return onSelectActionType == .rename
111+
return onSelectActionType == .rename || PreferenceKey.createSymLinkOnSelect.isManaged()
89112
}
90113

91114
@Published var onSelectActionType = SelectedActionType.none {
@@ -98,6 +121,8 @@ class AppState: ObservableObject {
98121
}
99122
}
100123

124+
var onSelectActionTypeDisabled: Bool { PreferenceKey.onSelectActionType.isManaged() }
125+
101126
@Published var showOpenInRosettaOption = false {
102127
didSet {
103128
Current.defaults.set(showOpenInRosettaOption, forKey: "showOpenInRosettaOption")
@@ -178,8 +203,8 @@ class AppState: ObservableObject {
178203
// MARK: Timer
179204
/// Runs a timer every 6 hours when app is open to check if it needs to auto install any xcodes
180205
func setupAutoInstallTimer() {
181-
guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
182-
206+
guard let storageValue = Current.defaults.get(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
207+
183208
if autoInstallType == .none { return }
184209

185210
autoInstallTimer = Timer.scheduledTimer(withTimeInterval: 60*60*6, repeats: true) { [weak self] _ in
@@ -479,7 +504,7 @@ class AppState: ObservableObject {
479504
.mapError { $0 as Error }
480505
}
481506
.flatMap { [unowned self] in
482-
self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2)
507+
self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2)
483508
}
484509
.receive(on: DispatchQueue.main)
485510
.sink(
@@ -505,7 +530,7 @@ class AppState: ObservableObject {
505530
func installWithoutLogin(id: Xcode.ID) {
506531
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
507532

508-
installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2)
533+
installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2)
509534
.receive(on: DispatchQueue.main)
510535
.sink(
511536
receiveCompletion: { [unowned self] completion in

Xcodes/Backend/DataSource.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ public enum DataSource: String, CaseIterable, Identifiable, CustomStringConverti
1414
case .xcodeReleases: return "Xcode Releases"
1515
}
1616
}
17+
18+
var isManaged: Bool { PreferenceKey.dataSource.isManaged() }
1719
}

Xcodes/Backend/Downloader.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ public enum Downloader: String, CaseIterable, Identifiable, CustomStringConverti
1313
case .aria2: return "aria2"
1414
}
1515
}
16+
17+
var isManaged: Bool { PreferenceKey.downloader.isManaged() }
1618
}

Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ struct AdvancedPreferencePane: View {
3636
self.appState.installPath = path.string
3737
}
3838
}
39+
.disabled(appState.disableInstallPathChange)
3940
Text("InstallPathDescription")
4041
.font(.footnote)
4142
.foregroundStyle(.secondary)
@@ -72,6 +73,7 @@ struct AdvancedPreferencePane: View {
7273
self.appState.localPath = path.string
7374
}
7475
}
76+
.disabled(appState.disableLocalPathChange)
7577
Text("LocalCachePathDescription")
7678
.font(.footnote)
7779
.foregroundStyle(.secondary)
@@ -93,7 +95,8 @@ struct AdvancedPreferencePane: View {
9395
}
9496
.labelsHidden()
9597
.pickerStyle(.inline)
96-
98+
.disabled(appState.onSelectActionTypeDisabled)
99+
97100
Text(appState.onSelectActionType.detailedDescription)
98101
.font(.footnote)
99102
.foregroundStyle(.secondary)

Xcodes/Frontend/Preferences/DownloadPreferencePane.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@ struct DownloadPreferencePane: View {
1919
}
2020
.labelsHidden()
2121
.fixedSize()
22-
22+
2323
Text("DataSourceDescription")
2424
.font(.footnote)
2525
.foregroundStyle(.secondary)
2626
.fixedSize(horizontal: false, vertical: true)
2727
}
2828
}
2929
.groupBoxStyle(PreferencesGroupBoxStyle())
30-
30+
.disabled(dataSource.isManaged)
31+
3132
GroupBox(label: Text("Downloader")) {
3233
VStack(alignment: .leading) {
3334
Picker("Downloader", selection: $downloader) {
@@ -38,14 +39,15 @@ struct DownloadPreferencePane: View {
3839
}
3940
.labelsHidden()
4041
.fixedSize()
41-
42+
4243
Text("DownloaderDescription")
4344
.font(.footnote)
4445
.foregroundStyle(.secondary)
4546
.fixedSize(horizontal: false, vertical: true)
4647
}
4748
}
4849
.groupBoxStyle(PreferencesGroupBoxStyle())
50+
.disabled(downloader.isManaged)
4951
}
5052
}
5153
}

Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ struct ExperimentsPreferencePane: View {
1313
"UseUnxipExperiment",
1414
isOn: $appState.unxipExperiment
1515
)
16+
.disabled(appState.disableUnxipExperiment)
1617
Text("FasterUnxipDescription")
1718
.font(.footnote)
1819
.foregroundStyle(.secondary)

Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ struct UpdatesPreferencePane: View {
1515
"AutomaticInstallNewVersion",
1616
isOn: $autoInstallationType.isAutoInstalling
1717
)
18-
18+
.disabled(updater.disableAutoInstallNewVersions)
19+
1920
Toggle(
2021
"IncludePreRelease",
2122
isOn: $autoInstallationType.isAutoInstallingBeta
2223
)
24+
.disabled(updater.disableIncludePrereleaseVersions)
2325
}
2426
.fixedSize(horizontal: false, vertical: true)
2527
}
@@ -34,17 +36,20 @@ struct UpdatesPreferencePane: View {
3436
isOn: $updater.automaticallyChecksForUpdates
3537
)
3638
.fixedSize(horizontal: true, vertical: false)
37-
39+
.disabled(updater.disableAutoUpdateXcodesApp)
40+
3841
Toggle(
3942
"IncludePreRelease",
4043
isOn: $updater.includePrereleaseVersions
4144
)
42-
45+
.disabled(updater.disableAutoUpdateXcodesAppPrereleaseVersions)
46+
4347
Button("CheckNow") {
4448
updater.checkForUpdates()
4549
}
4650
.padding(.top)
47-
51+
.disabled(updater.disableAutoUpdateXcodesApp)
52+
4853
Text(String(format: localizeString("LastChecked"), lastUpdatedString))
4954
.font(.footnote)
5055
.foregroundStyle(.secondary)
@@ -83,12 +88,18 @@ class ObservableUpdater: ObservableObject {
8388
private var lastUpdateCheckDateObservation: NSKeyValueObservation?
8489
@Published var includePrereleaseVersions = false {
8590
didSet {
86-
UserDefaults.standard.setValue(includePrereleaseVersions, forKey: "includePrereleaseVersions")
87-
91+
Current.defaults.set(includePrereleaseVersions, forKey: "includePrereleaseVersions")
92+
8893
updaterDelegate.includePrereleaseVersions = includePrereleaseVersions
8994
}
9095
}
91-
96+
97+
var disableAutoInstallNewVersions: Bool { PreferenceKey.autoInstallation.isManaged() }
98+
var disableIncludePrereleaseVersions: Bool { PreferenceKey.autoInstallation.isManaged() }
99+
100+
var disableAutoUpdateXcodesApp: Bool { PreferenceKey.SUEnableAutomaticChecks.isManaged() }
101+
var disableAutoUpdateXcodesAppPrereleaseVersions: Bool { PreferenceKey.includePrereleaseVersions.isManaged() }
102+
92103
init() {
93104
updater = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: updaterDelegate, userDriverDelegate: nil).updater
94105

@@ -111,7 +122,7 @@ class ObservableUpdater: ObservableObject {
111122
self.lastUpdateCheckDate = updater.lastUpdateCheckDate
112123
}
113124
)
114-
includePrereleaseVersions = UserDefaults.standard.bool(forKey: "includePrereleaseVersions")
125+
includePrereleaseVersions = Current.defaults.bool(forKey: "includePrereleaseVersions") ?? false
115126
}
116127

117128
func checkForUpdates() {

Xcodes/Frontend/XcodeList/MainToolbar.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ struct MainToolbarModifier: ViewModifier {
4444
}
4545
}
4646
.help("FilterAvailableDescription")
47+
.disabled(category.isManaged)
4748

4849
Button(action: {
4950
isInstalledOnly.toggle()

0 commit comments

Comments
 (0)