From 0a1425076330b2495083c216dbaa2d8d07dbaafd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:29:19 +0000 Subject: [PATCH 01/18] Initial plan From 6741832d7f2e3a81cad80ec0df7279457ba8aabb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:33:58 +0000 Subject: [PATCH 02/18] Stage 2: Convert NetworkAuthManager from actor to class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, all types are MainActor by default. The actor's synchronization purpose (single-flight token refresh) is now provided by MainActor serialization. Changes: - actor NetworkAuthManager → class NetworkAuthManager - bearerAuthSupported(): drop async (no longer needed without actor) - basicAuthString(): drop nonisolated (no longer an actor) - JamfProAPIClient: drop await from bearerAuthSupported() calls - NetworkAuthManagerTests: drop await from bearerAuthSupported() calls Side effect: Resolves Token.isValid cross-isolation warning because Token and NetworkAuthManager are now both on MainActor. Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> --- .../NetworkingTests/NetworkAuthManagerTests.swift | 4 ++-- Source/Networking/JamfProAPIClient.swift | 4 ++-- Source/Networking/NetworkAuthManager.swift | 12 +++++++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift b/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift index d54ddd4..8cd1768 100644 --- a/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift +++ b/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift @@ -88,7 +88,7 @@ class NetworkAuthManagerTests: XCTestCase { networking.errorToThrow = NetworkingError.serverResponse(404, "No such page") // default is that bearer auth is supported. - let firstCheckBearerAuthSupported = await authManager.bearerAuthSupported() + let firstCheckBearerAuthSupported = authManager.bearerAuthSupported() XCTAssertTrue(firstCheckBearerAuthSupported) // when @@ -101,7 +101,7 @@ class NetworkAuthManagerTests: XCTestCase { } // The authManager should now know that bearer auth is not supported - let secondCheckBearerAuthSupported = await authManager.bearerAuthSupported() + let secondCheckBearerAuthSupported = authManager.bearerAuthSupported() XCTAssertFalse(secondCheckBearerAuthSupported) } diff --git a/Source/Networking/JamfProAPIClient.swift b/Source/Networking/JamfProAPIClient.swift index 2d1d695..fb08bf1 100644 --- a/Source/Networking/JamfProAPIClient.swift +++ b/Source/Networking/JamfProAPIClient.swift @@ -77,7 +77,7 @@ class JamfProAPIClient: Networking { func load(request: URLRequest) async throws -> T { let result: T - if await authManager.bearerAuthSupported() { + if authManager.bearerAuthSupported() { do { result = try await loadBearerAuthorized(request: request) } catch AuthError.bearerAuthNotSupported { @@ -98,7 +98,7 @@ class JamfProAPIClient: Networking { func send(request: URLRequest) async throws -> Data { let result: Data - if await authManager.bearerAuthSupported() { + if authManager.bearerAuthSupported() { do { result = try await sendBearerAuthorized(request: request) } catch AuthError.bearerAuthNotSupported { diff --git a/Source/Networking/NetworkAuthManager.swift b/Source/Networking/NetworkAuthManager.swift index 3e43b02..9074a77 100644 --- a/Source/Networking/NetworkAuthManager.swift +++ b/Source/Networking/NetworkAuthManager.swift @@ -46,8 +46,10 @@ enum AuthenticationInfo { case clientCreds(id: String, secret: String) } -/// This actor ensures that only one token refresh occurs at the same time. -actor NetworkAuthManager { +/// This class ensures that only one token refresh occurs at the same time. +/// With MainActor default isolation, all types are MainActor by default, +/// providing the same serialization that the actor previously offered. +class NetworkAuthManager { private let authInfo: AuthenticationInfo private var currentToken: Token? @@ -111,15 +113,15 @@ actor NetworkAuthManager { /// The default is that bearer authentication is supported. After the first network call attempting to use bearer auth, if the /// server does not actually support it this will return false. /// - Returns: True if bearer auth is supported. - func bearerAuthSupported() async -> Bool { + func bearerAuthSupported() -> Bool { return supportsBearerAuth } /// Properly encodes the username and password for use in Basic authentication. /// - /// This doesn't mutate any state and only accesses `let` constants so it doesn't need to be actor isolated. + /// This doesn't mutate any state and only accesses `let` constants so it doesn't need special isolation. /// - Returns: The encoded data string for use with Basic Auth. - nonisolated func basicAuthString() throws -> String { + func basicAuthString() throws -> String { guard case .basicAuth(let username, let password) = authInfo, !username.isEmpty && !password.isEmpty else { throw AuthError.invalidUsernamePassword From 9256bd019a4851ffec7af206d6708d004c03c0c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:34:27 +0000 Subject: [PATCH 03/18] Stage 3a: Remove DispatchQueue.main.async wrappers With MainActor default isolation, manual dispatch to the main thread is redundant. All three locations are already MainActor-isolated. Changes: - Alert.swift: Remove DispatchQueue.main.async wrapper in display() - OpenViewController.swift: Remove wrapper in tableView selectionIndexes - SaveViewController.swift: Remove wrapper in savePressed panel callback Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> --- Source/View Controllers/OpenViewController.swift | 8 +++----- Source/View Controllers/SaveViewController.swift | 5 +---- Source/Views/Alert.swift | 14 ++++++-------- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/Source/View Controllers/OpenViewController.swift b/Source/View Controllers/OpenViewController.swift index d8bb09b..5d7721a 100644 --- a/Source/View Controllers/OpenViewController.swift +++ b/Source/View Controllers/OpenViewController.swift @@ -48,11 +48,9 @@ class OpenViewController: NSViewController, NSTableViewDataSource, NSTableViewDe } func tableView(_ tableView: NSTableView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet { - DispatchQueue.main.async { - guard let index = proposedSelectionIndexes.first else { return } - self.completionBlock?([.success(self.choices[index])]) - self.dismiss(self) - } + guard let index = proposedSelectionIndexes.first else { return proposedSelectionIndexes } + self.completionBlock?([.success(self.choices[index])]) + self.dismiss(self) return proposedSelectionIndexes } diff --git a/Source/View Controllers/SaveViewController.swift b/Source/View Controllers/SaveViewController.swift index c2d6081..28a0558 100644 --- a/Source/View Controllers/SaveViewController.swift +++ b/Source/View Controllers/SaveViewController.swift @@ -83,10 +83,7 @@ class SaveViewController: NSViewController { panel.begin { response in if response == .OK { - // Let the save panel fully close itself before doing any work that may require keychain access. - DispatchQueue.main.async { - self.saveTo(url: panel.url!) - } + self.saveTo(url: panel.url!) } } } diff --git a/Source/Views/Alert.swift b/Source/Views/Alert.swift index 89f50f0..956d26c 100644 --- a/Source/Views/Alert.swift +++ b/Source/Views/Alert.swift @@ -29,14 +29,12 @@ import Cocoa class Alert: NSObject { func display(header: String, message: String) { - DispatchQueue.main.async { - let dialog: NSAlert = NSAlert() - dialog.messageText = header - dialog.informativeText = message - dialog.alertStyle = NSAlert.Style.warning - dialog.addButton(withTitle: "OK") - dialog.runModal() - } + let dialog: NSAlert = NSAlert() + dialog.messageText = header + dialog.informativeText = message + dialog.alertStyle = NSAlert.Style.warning + dialog.addButton(withTitle: "OK") + dialog.runModal() } /// Displays a message with a cancel button and returns true if OK was pressed From 7b7711321904d5a4369480108d161e666f1aed55 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:35:33 +0000 Subject: [PATCH 04/18] Stage 3b: Convert UploadManager to async throws Replace Task/completion handler pattern with direct async throws methods. Changes: - UploadManager.verifyConnection: async throws -> VerificationInfo - UploadManager.upload: async throws (no completion handler) - UploadInfoView.verifyConnection: now async, uses try await - UploadInfoView.performUpload: now async, uses try await - Button action bridges to async with Task { await ... } Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> --- Source/Networking/UploadManager.swift | 68 +++++++++--------------- Source/SwiftUI/UploadInfoView.swift | 75 ++++++++++++++------------- 2 files changed, 63 insertions(+), 80 deletions(-) diff --git a/Source/Networking/UploadManager.swift b/Source/Networking/UploadManager.swift index e7d3246..71194ba 100644 --- a/Source/Networking/UploadManager.swift +++ b/Source/Networking/UploadManager.swift @@ -23,61 +23,43 @@ struct UploadManager { case anyError(String) } - func verifyConnection(authManager: NetworkAuthManager, completionHandler: @escaping (Result) -> Void) { + func verifyConnection(authManager: NetworkAuthManager) async throws -> VerificationInfo { logger.info("Checking connection to Jamf Pro server") - Task { - let networking = JamfProAPIClient(serverUrlString: serverURL, tokenManager: authManager) - let result: Result + let networking = JamfProAPIClient(serverUrlString: serverURL, tokenManager: authManager) - do { - let version = try await networking.getJamfProVersion() + do { + let version = try await networking.getJamfProVersion() - // Must sign if Jamf Pro is less than v10.7.1 - let mustSign = (version.semantic() < SemanticVersion(major: 10, minor: 7, patch: 1)) + // Must sign if Jamf Pro is less than v10.7.1 + let mustSign = (version.semantic() < SemanticVersion(major: 10, minor: 7, patch: 1)) - let orgName = try await networking.getOrganizationName() + let orgName = try await networking.getOrganizationName() - result = .success(VerificationInfo(mustSign: mustSign, organization: orgName)) - } catch is AuthError { - logger.error("Invalid credentials.") - result = .failure(VerificationError.anyError("Invalid credentials.")) - } catch { - logger.error("Jamf Pro server is unavailable.") - result = .failure(VerificationError.anyError("Jamf Pro server is unavailable.")) - } - - completionHandler(result) + return VerificationInfo(mustSign: mustSign, organization: orgName) + } catch is AuthError { + logger.error("Invalid credentials.") + throw VerificationError.anyError("Invalid credentials.") + } catch { + logger.error("Jamf Pro server is unavailable.") + throw VerificationError.anyError("Jamf Pro server is unavailable.") } } - func upload(profile: TCCProfile, authMgr: NetworkAuthManager, siteInfo: (String, String)?, signingIdentity: SigningIdentity?, completionHandler: @escaping (Error?) -> Void) { + func upload(profile: TCCProfile, authMgr: NetworkAuthManager, siteInfo: (String, String)?, signingIdentity: SigningIdentity?) async throws { logger.info("Uploading profile: \(profile.displayName, privacy: .public)") let networking = JamfProAPIClient(serverUrlString: serverURL, tokenManager: authMgr) - Task { - let success: Error? - var identity: SecIdentity? - if let signingIdentity = signingIdentity { - logger.info("Signing profile with \(signingIdentity.displayName)") - identity = signingIdentity.reference - } - - do { - let profileData = try profile.jamfProAPIData(signingIdentity: identity, site: siteInfo) - - _ = try await networking.upload(computerConfigProfile: profileData) - - success = nil - logger.info("Uploaded successfully") - } catch { - logger.error("Error creating or uploading profile: \(error.localizedDescription)") - success = error - } - - DispatchQueue.main.async { - completionHandler(success) - } + var identity: SecIdentity? + if let signingIdentity = signingIdentity { + logger.info("Signing profile with \(signingIdentity.displayName)") + identity = signingIdentity.reference } + + let profileData = try profile.jamfProAPIData(signingIdentity: identity, site: siteInfo) + + _ = try await networking.upload(computerConfigProfile: profileData) + + logger.info("Uploaded successfully") } } diff --git a/Source/SwiftUI/UploadInfoView.swift b/Source/SwiftUI/UploadInfoView.swift index 18cdc98..78bf5e4 100644 --- a/Source/SwiftUI/UploadInfoView.swift +++ b/Source/SwiftUI/UploadInfoView.swift @@ -119,10 +119,12 @@ struct UploadInfoView: View { .keyboardShortcut(.cancelAction) Button(verifiedConnection ? "Upload" : "Check connection") { - if verifiedConnection { - performUpload() - } else { - verifyConnection() + Task { + if verifiedConnection { + await performUpload() + } else { + await verifyConnection() + } } } .keyboardShortcut(.defaultAction) @@ -271,7 +273,7 @@ struct UploadInfoView: View { return NetworkAuthManager(clientId: username, clientSecret: password) } - func verifyConnection() { + func verifyConnection() async { guard connectionInfoPassesValidation(setWarningInfo: true) else { return } @@ -279,29 +281,29 @@ struct UploadInfoView: View { networkOperationInfo = "Checking Jamf Pro server" let uploadMgr = UploadManager(serverURL: serverURL) - uploadMgr.verifyConnection(authManager: makeAuthManager()) { result in - if case .success(let success) = result { - mustSign = success.mustSign - organization = success.organization - verifiedConnectionHash = hashOfConnectionInfo - if saveToKeychain { - do { - try SecurityWrapper.saveCredentials(username: username, - password: password, - server: serverURL) - } catch { - logger.error("Failed to save credentials with error: \(error.localizedDescription)") - } + do { + let info = try await uploadMgr.verifyConnection(authManager: makeAuthManager()) + mustSign = info.mustSign + organization = info.organization + verifiedConnectionHash = hashOfConnectionInfo + if saveToKeychain { + do { + try SecurityWrapper.saveCredentials(username: username, + password: password, + server: serverURL) + } catch { + logger.error("Failed to save credentials with error: \(error.localizedDescription)") } - // Future on macOS 12+: focus on Payload Name field - } else if case .failure(let failure) = result, - case .anyError(let errorString) = failure { - warningInfo = errorString - verifiedConnectionHash = 0 } - - networkOperationInfo = nil + } catch UploadManager.VerificationError.anyError(let errorString) { + warningInfo = errorString + verifiedConnectionHash = 0 + } catch { + warningInfo = error.localizedDescription + verifiedConnectionHash = 0 } + + networkOperationInfo = nil } private func dismissView() { @@ -314,7 +316,7 @@ struct UploadInfoView: View { } } - func performUpload() { + func performUpload() async { guard connectionInfoPassesValidation(setWarningInfo: true) else { return } @@ -338,18 +340,17 @@ struct UploadInfoView: View { } let uploadMgr = UploadManager(serverURL: serverURL) - uploadMgr.upload(profile: profile, - authMgr: makeAuthManager(), - siteInfo: siteIdAndName, - signingIdentity: mustSign ? signingId : nil) { possibleError in - if let error = possibleError { - warningInfo = error.localizedDescription - } else { - Alert().display(header: "Success", message: "Profile uploaded succesfully") - dismissView() - } - networkOperationInfo = nil + do { + try await uploadMgr.upload(profile: profile, + authMgr: makeAuthManager(), + siteInfo: siteIdAndName, + signingIdentity: mustSign ? signingId : nil) + Alert().display(header: "Success", message: "Profile uploaded succesfully") + dismissView() + } catch { + warningInfo = error.localizedDescription } + networkOperationInfo = nil } } From a64ed560bb399e897ed86403529a5ec593919f4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:36:51 +0000 Subject: [PATCH 05/18] Stage 3c: Convert Model.loadExecutable to direct return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace completion handler pattern with throws -> Executable since the work is synchronous (reads bundle info and code requirements). Changes: - Model.loadExecutable(url:) now throws -> Executable (no completion) - Remove LoadExecutableCompletion typealias - findExecutableOnComputerUsing → findExecutable, returns directly - getExecutableFrom: uses do/catch instead of completion handler - getAppleEventChoices: uses do/catch for each loadExecutable call - TCCProfileViewController: promptForExecutables and acceptDrop updated - OpenViewController.prompt: updated to use try/catch Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> --- Source/Model/Model.swift | 68 +++++++------------ .../View Controllers/OpenViewController.swift | 9 ++- .../TCCProfileViewController.swift | 56 +++++++-------- 3 files changed, 59 insertions(+), 74 deletions(-) diff --git a/Source/Model/Model.swift b/Source/Model/Model.swift index 327029d..a838ded 100644 --- a/Source/Model/Model.swift +++ b/Source/Model/Model.swift @@ -40,31 +40,22 @@ import OSLog func getAppleEventChoices(executable: Executable) -> [Executable] { var executables: [Executable] = [] - loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/System Events.app")) { result in - switch result { - case .success(let executable): - executables.append(executable) - case .failure(let error): - self.logger.error("\(error)") - } + do { + executables.append(try loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/System Events.app"))) + } catch { + self.logger.error("\(error)") } - loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/SystemUIServer.app")) { result in - switch result { - case .success(let executable): - executables.append(executable) - case .failure(let error): - self.logger.error("\(error)") - } + do { + executables.append(try loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/SystemUIServer.app"))) + } catch { + self.logger.error("\(error)") } - loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/Finder.app")) { result in - switch result { - case .success(let executable): - executables.append(executable) - case .failure(let error): - self.logger.error("\(error)") - } + do { + executables.append(try loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/Finder.app"))) + } catch { + self.logger.error("\(error)") } let others = store.values.filter { $0 != executable && !Set(executables).contains($0) } @@ -87,17 +78,16 @@ struct IconFilePath { } typealias LoadExecutableResult = Result -typealias LoadExecutableCompletion = ((LoadExecutableResult) -> Void) extension Model { - func loadExecutable(url: URL, completion: @escaping LoadExecutableCompletion) { + func loadExecutable(url: URL) throws -> Executable { let executable = Executable() if let bundle = Bundle(url: url) { switch populateFromBundle(executable, bundle: bundle, url: url) { case .failure(let error): - return completion(.failure(error)) + throw error case .success: break } @@ -106,15 +96,15 @@ extension Model { } if let alreadyFoundExecutable = store[executable.identifier] { - return completion(.success(alreadyFoundExecutable)) + return alreadyFoundExecutable } do { executable.codeRequirement = try SecurityWrapper.copyDesignatedRequirement(url: url) store[executable.identifier] = executable - return completion(.success(executable)) + return executable } catch { - return completion(.failure(.codeRequirementError(description: error.localizedDescription))) + throw LoadExecutableError.codeRequirementError(description: error.localizedDescription) } } @@ -278,19 +268,16 @@ extension Model { func getExecutableFrom(identifier: String, codeRequirement: String) -> Executable { var executable = Executable(identifier: identifier, codeRequirement: codeRequirement) - findExecutableOnComputerUsing(bundleIdentifier: identifier) { result in - switch result { - case .success(let goodExecutable): - executable = goodExecutable - case .failure(let error): - self.logger.error("\(error)") - } + do { + executable = try findExecutable(bundleIdentifier: identifier) + } catch { + self.logger.error("\(error)") } return executable } - private func findExecutableOnComputerUsing(bundleIdentifier: String, completion: @escaping LoadExecutableCompletion) { + private func findExecutable(bundleIdentifier: String) throws -> Executable { var urlToLoad: URL? if bundleIdentifier.contains("/") { urlToLoad = URL(string: "file://\(bundleIdentifier)") @@ -299,16 +286,9 @@ extension Model { } if let fileURL = urlToLoad { - self.loadExecutable(url: fileURL) { result in - switch result { - case .success(let executable): - return completion(.success(executable)) - case .failure(let error): - return completion(.failure(error)) - } - } + return try self.loadExecutable(url: fileURL) } - return completion(.failure(.executableNotFound)) + throw LoadExecutableError.executableNotFound } private func cleanUpAndRemoveDependencies() { diff --git a/Source/View Controllers/OpenViewController.swift b/Source/View Controllers/OpenViewController.swift index 5d7721a..3e622fa 100644 --- a/Source/View Controllers/OpenViewController.swift +++ b/Source/View Controllers/OpenViewController.swift @@ -64,8 +64,13 @@ class OpenViewController: NSViewController, NSTableViewDataSource, NSTableViewDe if response == .OK { var selections: [LoadExecutableResult] = [] panel.urls.forEach { - Model.shared.loadExecutable(url: $0) { result in - selections.append(result) + do { + let executable = try Model.shared.loadExecutable(url: $0) + selections.append(.success(executable)) + } catch { + if let loadError = error as? LoadExecutableError { + selections.append(.failure(loadError)) + } } } block?(selections) diff --git a/Source/View Controllers/TCCProfileViewController.swift b/Source/View Controllers/TCCProfileViewController.swift index 30e7a9e..5f40d5a 100644 --- a/Source/View Controllers/TCCProfileViewController.swift +++ b/Source/View Controllers/TCCProfileViewController.swift @@ -205,19 +205,19 @@ class TCCProfileViewController: NSViewController { panel.begin { response in if response == .OK { panel.urls.forEach { - self.model.loadExecutable(url: $0) { [weak self] result in - switch result { - case .success(let executable): - guard self?.shouldExecutableBeAdded(executable) ?? false else { - let error = LoadExecutableError.executableAlreadyExists - self?.showAlert(error, for: window) - return - } - block(executable) - case .failure(let error): - self?.showAlert(error, for: window) - self?.logger.error("\(error)") + do { + let executable = try self.model.loadExecutable(url: $0) + guard self.shouldExecutableBeAdded(executable) else { + let error = LoadExecutableError.executableAlreadyExists + self.showAlert(error, for: window) + return + } + block(executable) + } catch { + if let loadError = error as? LoadExecutableError { + self.showAlert(loadError, for: window) } + self.logger.error("\(error)") } } } @@ -405,25 +405,25 @@ extension TCCProfileViewController: NSTableViewDataSource { var addedAny = false urls?.forEach { (url) in - model.loadExecutable(url: url) { [weak self] result in - switch result { - case .success(let newExecutable): - if tableView == self?.executablesTable { - guard self?.executablesAC.canInsert ?? false else { - return - } - if self?.shouldExecutableBeAdded(newExecutable) ?? false { - self?.executablesAC.insert(newExecutable, atArrangedObjectIndex: row) - addedAny = true - } - } else { - self?.insertIntoAppleEvents(newExecutable) + do { + let newExecutable = try model.loadExecutable(url: url) + if tableView == self.executablesTable { + guard self.executablesAC.canInsert else { + return + } + if self.shouldExecutableBeAdded(newExecutable) { + self.executablesAC.insert(newExecutable, atArrangedObjectIndex: row) addedAny = true } - case .failure(let error): - self?.showAlert(error, for: window) - self?.logger.error("\(error)") + } else { + self.insertIntoAppleEvents(newExecutable) + addedAny = true + } + } catch { + if let loadError = error as? LoadExecutableError { + self.showAlert(loadError, for: window) } + self.logger.error("\(error)") } } From 542a2873ca37fb3807c3d85ede0fc8ed06e7602a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:37:56 +0000 Subject: [PATCH 06/18] Stage 3d: Convert TCCProfileImporter to direct return Replace completion handler pattern with throws -> TCCProfile since the work is synchronous (decode data, read file). Changes: - TCCProfileImporter.decodeTCCProfile(data:) now throws -> TCCProfile - TCCProfileImporter.decodeTCCProfile(fileUrl:) now throws -> TCCProfile - Remove TCCProfileImportCompletion typealias - TCCProfileConfigurationPanel: uses try/catch in panel callback - TCCProfileImporterTests: updated to use try/catch pattern Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> --- .../TCCProfileImporterTests.swift | 66 +++++++------------ .../TCCProfileConfigurationPanel.swift | 18 +++-- .../TCCProfileImporter.swift | 31 +++++---- 3 files changed, 53 insertions(+), 62 deletions(-) diff --git a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift index a70dc76..696f06e 100644 --- a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift +++ b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift @@ -37,14 +37,12 @@ class TCCProfileImporterTests: XCTestCase { let resourceURL = getResourceProfile(fileName: "TestTCCProfileSigned-Broken") - tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in - switch tccProfileResult { - case .success: - XCTFail("Malformed profile should not succeed") - case .failure(let tccProfileError): - if case TCCProfileImportError.invalidProfileFile = tccProfileError { } else { - XCTFail("Expected invalidProfileFile error, got \(tccProfileError)") - } + do { + _ = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) + XCTFail("Malformed profile should not succeed") + } catch { + if case TCCProfileImportError.invalidProfileFile = error { } else { + XCTFail("Expected invalidProfileFile error, got \(error)") } } } @@ -56,46 +54,32 @@ class TCCProfileImporterTests: XCTestCase { let expectedTCCProfileError = TCCProfileImportError.invalidProfileFile(description: "PayloadContent") - tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in - switch tccProfileResult { - case .success: - XCTFail("Empty Content, it shouldn't be success") - case .failure(let tccProfileError): - XCTAssertEqual(tccProfileError.localizedDescription, expectedTCCProfileError.localizedDescription) - } + do { + _ = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) + XCTFail("Empty Content, it shouldn't be success") + } catch { + XCTAssertEqual(error.localizedDescription, expectedTCCProfileError.localizedDescription) } } - func testCorrectUnsignedProfileContentData() { + func testCorrectUnsignedProfileContentData() throws { let tccProfileImporter = TCCProfileImporter() let resourceURL = getResourceProfile(fileName: "TestTCCUnsignedProfile") - tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in - switch tccProfileResult { - case .success(let tccProfile): - XCTAssertNotNil(tccProfile.content) - XCTAssertNotNil(tccProfile.content[0].services) - case .failure(let tccProfileError): - XCTFail("Unable to read tccProfile \(tccProfileError.localizedDescription)") - } - } + let tccProfile = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) + XCTAssertNotNil(tccProfile.content) + XCTAssertNotNil(tccProfile.content[0].services) } - func testCorrectUnsignedProfileContentDataAllLowercase() { + func testCorrectUnsignedProfileContentDataAllLowercase() throws { let tccProfileImporter = TCCProfileImporter() let resourceURL = getResourceProfile(fileName: "TestTCCUnsignedProfile-allLower") - tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in - switch tccProfileResult { - case .success(let tccProfile): - XCTAssertNotNil(tccProfile.content) - XCTAssertNotNil(tccProfile.content[0].services) - case .failure(let tccProfileError): - XCTFail("Unable to read tccProfile \(tccProfileError.localizedDescription)") - } - } + let tccProfile = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) + XCTAssertNotNil(tccProfile.content) + XCTAssertNotNil(tccProfile.content[0].services) } func testBrokenUnsignedProfile() { @@ -105,13 +89,11 @@ class TCCProfileImporterTests: XCTestCase { let expectedTCCProfileError = TCCProfileImportError.invalidProfileFile(description: "The given data was not a valid property list.") - tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in - switch tccProfileResult { - case .success: - XCTFail("Broken Unsigned Profile, it shouldn't be success") - case .failure(let tccProfileError): - XCTAssertEqual(tccProfileError.localizedDescription, expectedTCCProfileError.localizedDescription) - } + do { + _ = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) + XCTFail("Broken Unsigned Profile, it shouldn't be success") + } catch { + XCTAssertEqual(error.localizedDescription, expectedTCCProfileError.localizedDescription) } } diff --git a/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift b/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift index c9fc3f6..f2f00b9 100644 --- a/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift +++ b/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift @@ -31,8 +31,11 @@ import Foundation class TCCProfileConfigurationPanel { /// Load TCC Profile data from file /// - /// - Parameter completion: TCCProfileImportCompletion - success with TCCProfile or failure with TCCProfileImport Error - func loadTCCProfileFromFile(importer: TCCProfileImporter, window: NSWindow, _ completion: @escaping TCCProfileImportCompletion) { + /// - Parameters: + /// - importer: The TCCProfileImporter to use + /// - window: The window to present the open panel in + /// - completion: Called with the result of the import + func loadTCCProfileFromFile(importer: TCCProfileImporter, window: NSWindow, _ completion: @escaping (TCCProfileImportResult) -> Void) { let openPanel = NSOpenPanel.init() openPanel.allowedFileTypes = ["mobileconfig", "plist"] openPanel.allowsMultipleSelection = false @@ -46,8 +49,15 @@ class TCCProfileConfigurationPanel { completion(.failure(.cancelled)) } else { if let result = openPanel.url { - importer.decodeTCCProfile(fileUrl: result) { tccProfileResult in - return completion(tccProfileResult) + do { + let tccProfile = try importer.decodeTCCProfile(fileUrl: result) + completion(.success(tccProfile)) + } catch { + if let importError = error as? TCCProfileImportError { + completion(.failure(importError)) + } else { + completion(.failure(.invalidProfileFile(description: error.localizedDescription))) + } } } else { completion(.failure(TCCProfileImportError.unableToOpenFile)) diff --git a/Source/TCCProfileImporter/TCCProfileImporter.swift b/Source/TCCProfileImporter/TCCProfileImporter.swift index 0ac1111..3157d0d 100644 --- a/Source/TCCProfileImporter/TCCProfileImporter.swift +++ b/Source/TCCProfileImporter/TCCProfileImporter.swift @@ -28,45 +28,44 @@ import Foundation typealias TCCProfileImportResult = Result -typealias TCCProfileImportCompletion = ((TCCProfileImportResult) -> Void) /// Load tcc profiles public class TCCProfileImporter { // MARK: Load TCCProfile - /// Mapping & Decoding tcc profile + /// Mapping & Decoding tcc profile from data /// - /// - Parameter fileUrl: path with a file to load, completion: TCCProfileImportCompletion - success with TCCProfile or failure with TCCProfileImport Error - func decodeTCCProfile(data: Data, _ completion: @escaping TCCProfileImportCompletion) { + /// - Parameter data: The raw data to decode + /// - Returns: The decoded TCCProfile + func decodeTCCProfile(data: Data) throws -> TCCProfile { do { // Note that parse will ignore the signing portion of the data - let tccProfile = try TCCProfile.parse(from: data) - return completion(.success(tccProfile)) + return try TCCProfile.parse(from: data) } catch TCCProfile.ParseError.failedToCreateDecoder { - return completion(.failure(.decodeProfileError)) + throw TCCProfileImportError.decodeProfileError } catch let DecodingError.keyNotFound(codingKey, _) { - return completion(TCCProfileImportResult.failure(.invalidProfileFile(description: codingKey.stringValue))) + throw TCCProfileImportError.invalidProfileFile(description: codingKey.stringValue) } catch let DecodingError.typeMismatch(type, context) { let errorDescription = "Type \(type) mismatch: \(context.debugDescription) codingPath: \(context.codingPath)" - return completion(.failure(.invalidProfileFile(description: errorDescription))) + throw TCCProfileImportError.invalidProfileFile(description: errorDescription) } catch let error as NSError { let errorDescription = error.userInfo["NSDebugDescription"] as? String - return completion(.failure(.invalidProfileFile(description: errorDescription ?? error.localizedDescription))) + throw TCCProfileImportError.invalidProfileFile(description: errorDescription ?? error.localizedDescription) } } - /// Mapping & Decoding tcc profile + /// Mapping & Decoding tcc profile from a file URL /// - /// - Parameter fileUrl: path with a file to load, completion: TCCProfileImportCompletion - success with TCCProfile or failure with TCCProfileImport Error - func decodeTCCProfile(fileUrl: URL, _ completion: @escaping TCCProfileImportCompletion) { + /// - Parameter fileUrl: path with a file to load + /// - Returns: The decoded TCCProfile + func decodeTCCProfile(fileUrl: URL) throws -> TCCProfile { let data: Data do { data = try Data(contentsOf: fileUrl) - return decodeTCCProfile(data: data, completion) } catch { - return completion(.failure(.unableToOpenFile)) + throw TCCProfileImportError.unableToOpenFile } - + return try decodeTCCProfile(data: data) } } From f38e4fcd1420cc5b3bfe4d868cda4ffba3ea844a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:41:28 +0000 Subject: [PATCH 07/18] Stage 4: Add @concurrent for background I/O Mark SecurityWrapper and Model methods with @concurrent to move disk I/O and security operations off MainActor onto the cooperative pool. Changes: - SecurityWrapper.copyDesignatedRequirement: @concurrent async throws - SecurityWrapper.sign: @concurrent async throws - SecurityWrapper.loadSigningIdentities: @concurrent async throws - Model.loadExecutable: @concurrent async throws -> Executable - Model.getAppleEventChoices: now async (calls loadExecutable) - Model.getExecutableFrom/findExecutable: now async - Model.getExecutablesFromAllPolicies: now async - Model.importProfile: now async - All callers updated to use Task/await where needed - ModelTests: updated to async test methods Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> --- PPPC UtilityTests/ModelTests/ModelTests.swift | 54 +++++------ Source/Model/Model.swift | 30 +++--- Source/SecurityWrapper.swift | 6 +- .../View Controllers/OpenViewController.swift | 24 +++-- .../View Controllers/SaveViewController.swift | 36 ++++--- .../TCCProfileViewController.swift | 97 ++++++++++--------- 6 files changed, 131 insertions(+), 116 deletions(-) diff --git a/PPPC UtilityTests/ModelTests/ModelTests.swift b/PPPC UtilityTests/ModelTests/ModelTests.swift index 85ea06e..5021027 100644 --- a/PPPC UtilityTests/ModelTests/ModelTests.swift +++ b/PPPC UtilityTests/ModelTests/ModelTests.swift @@ -41,13 +41,13 @@ class ModelTests: XCTestCase { // MARK: - tests for getExecutableFrom* - func testGetExecutableBasedOnIdentifierAndCodeRequirement_BundleIdentifierType() { + func testGetExecutableBasedOnIdentifierAndCodeRequirement_BundleIdentifierType() async { // given let identifier = "com.example.App" let codeRequirement = "testCodeRequirement" // when - let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) + let executable = await model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) // then XCTAssertEqual(executable.displayName, "App") @@ -55,13 +55,13 @@ class ModelTests: XCTestCase { XCTAssertEqual(executable.iconPath, IconFilePath.application) } - func testGetExecutableBasedOnIdentifierAndCodeRequirement_PathIdentifierType() { + func testGetExecutableBasedOnIdentifierAndCodeRequirement_PathIdentifierType() async { // given let identifier = "/myGreatPath/Awesome/Binary" let codeRequirement = "testCodeRequirement" // when - let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) + let executable = await model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) // then XCTAssertEqual(executable.displayName, "Binary") @@ -69,13 +69,13 @@ class ModelTests: XCTestCase { XCTAssertEqual(executable.iconPath, IconFilePath.binary) } - func testGetExecutableFromComputerBasedOnIdentifier() { + func testGetExecutableFromComputerBasedOnIdentifier() async { // given let identifier = "com.apple.Safari" let codeRequirement = "randomReq" // when - let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) + let executable = await model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) // then XCTAssertEqual(executable.displayName, "Safari") @@ -83,11 +83,11 @@ class ModelTests: XCTestCase { XCTAssertNotEqual(codeRequirement, executable.codeRequirement) } - func testGetExecutableFromSelectedExecutables() { + func testGetExecutableFromSelectedExecutables() async { // given let expectedIdentifier = "com.something.1" - let executable = model.getExecutableFrom(identifier: expectedIdentifier, codeRequirement: "testReq") - let executableSecond = model.getExecutableFrom(identifier: "com.something.2", codeRequirement: "testReq2") + let executable = await model.getExecutableFrom(identifier: expectedIdentifier, codeRequirement: "testReq") + let executableSecond = await model.getExecutableFrom(identifier: "com.something.2", codeRequirement: "testReq2") model.selectedExecutables = [executable, executableSecond] // when @@ -100,12 +100,12 @@ class ModelTests: XCTestCase { XCTAssertEqual(existingExecutable?.iconPath, IconFilePath.application) } - func testGetExecutableFromSelectedExecutables_Path() { + func testGetExecutableFromSelectedExecutables_Path() async { // given let expectedIdentifier = "/path/something/Special" - let executableOneMore = model.getExecutableFrom(identifier: "/path/something/Special1", codeRequirement: "testReq") - let executable = model.getExecutableFrom(identifier: expectedIdentifier, codeRequirement: "testReq") - let executableSecond = model.getExecutableFrom(identifier: "com.something.2", codeRequirement: "testReq2") + let executableOneMore = await model.getExecutableFrom(identifier: "/path/something/Special1", codeRequirement: "testReq") + let executable = await model.getExecutableFrom(identifier: expectedIdentifier, codeRequirement: "testReq") + let executableSecond = await model.getExecutableFrom(identifier: "com.something.2", codeRequirement: "testReq2") model.selectedExecutables = [executableOneMore, executable, executableSecond] // when @@ -186,84 +186,84 @@ class ModelTests: XCTestCase { // MARK: - tests for importProfile - func testImportProfileUsingAuthorizationKeyAllow() { + func testImportProfileUsingAuthorizationKeyAllow() async { // given let profile = TCCProfileBuilder().buildProfile(authorization: .allow) // when - model.importProfile(tccProfile: profile) + await model.importProfile(tccProfile: profile) // then XCTAssertEqual(1, model.selectedExecutables.count) XCTAssertEqual("Allow", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) } - func testImportProfileUsingAuthorizationKeyDeny() { + func testImportProfileUsingAuthorizationKeyDeny() async { // given let profile = TCCProfileBuilder().buildProfile(authorization: .deny) // when - model.importProfile(tccProfile: profile) + await model.importProfile(tccProfile: profile) // then XCTAssertEqual(1, model.selectedExecutables.count) XCTAssertEqual("Deny", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) } - func testImportProfileUsingAuthorizationKeyAllowStandardUsers() { + func testImportProfileUsingAuthorizationKeyAllowStandardUsers() async { // given let profile = TCCProfileBuilder().buildProfile(authorization: .allowStandardUserToSetSystemService) // when - model.importProfile(tccProfile: profile) + await model.importProfile(tccProfile: profile) // then XCTAssertEqual(1, model.selectedExecutables.count) XCTAssertEqual("Let Standard Users Approve", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) } - func testImportProfileUsingLegacyAllowKeyTrue() { + func testImportProfileUsingLegacyAllowKeyTrue() async { // given let profile = TCCProfileBuilder().buildProfile(allowed: true) // when - model.importProfile(tccProfile: profile) + await model.importProfile(tccProfile: profile) // then XCTAssertEqual(1, model.selectedExecutables.count) XCTAssertEqual("Allow", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) } - func testImportProfileUsingLegacyAllowKeyFalse() { + func testImportProfileUsingLegacyAllowKeyFalse() async { // given let profile = TCCProfileBuilder().buildProfile(allowed: false) // when - model.importProfile(tccProfile: profile) + await model.importProfile(tccProfile: profile) // then XCTAssertEqual(1, model.selectedExecutables.count) XCTAssertEqual("Deny", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) } - func testImportProfileUsingAuthorizationKeyThatIsInvalid() { + func testImportProfileUsingAuthorizationKeyThatIsInvalid() async { // given let profile = TCCProfileBuilder().buildProfile(authorization: "invalidkey") // when - model.importProfile(tccProfile: profile) + await model.importProfile(tccProfile: profile) // then XCTAssertEqual(1, model.selectedExecutables.count) XCTAssertEqual("Deny", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) } - func testImportProfileUsingAuthorizationKeyTranslatesToAppleEvents() { + func testImportProfileUsingAuthorizationKeyTranslatesToAppleEvents() async { // given let profile = TCCProfileBuilder().buildProfile(authorization: "deny") // when - model.importProfile(tccProfile: profile) + await model.importProfile(tccProfile: profile) // then XCTAssertEqual(1, model.selectedExecutables.count) diff --git a/Source/Model/Model.swift b/Source/Model/Model.swift index a838ded..6dc6ae5 100644 --- a/Source/Model/Model.swift +++ b/Source/Model/Model.swift @@ -37,23 +37,23 @@ import OSLog let logger = Logger.Model - func getAppleEventChoices(executable: Executable) -> [Executable] { + func getAppleEventChoices(executable: Executable) async -> [Executable] { var executables: [Executable] = [] do { - executables.append(try loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/System Events.app"))) + executables.append(try await loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/System Events.app"))) } catch { self.logger.error("\(error)") } do { - executables.append(try loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/SystemUIServer.app"))) + executables.append(try await loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/SystemUIServer.app"))) } catch { self.logger.error("\(error)") } do { - executables.append(try loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/Finder.app"))) + executables.append(try await loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/Finder.app"))) } catch { self.logger.error("\(error)") } @@ -81,7 +81,7 @@ typealias LoadExecutableResult = Result extension Model { - func loadExecutable(url: URL) throws -> Executable { + @concurrent func loadExecutable(url: URL) async throws -> Executable { let executable = Executable() if let bundle = Bundle(url: url) { @@ -100,7 +100,7 @@ extension Model { } do { - executable.codeRequirement = try SecurityWrapper.copyDesignatedRequirement(url: url) + executable.codeRequirement = try await SecurityWrapper.copyDesignatedRequirement(url: url) store[executable.identifier] = executable return executable } catch { @@ -200,14 +200,14 @@ extension Model { services: services) } - func importProfile(tccProfile: TCCProfile) { + func importProfile(tccProfile: TCCProfile) async { if let content = tccProfile.content.first { self.cleanUpAndRemoveDependencies() self.importedTCCProfile = tccProfile for (key, policies) in content.services { - getExecutablesFromAllPolicies(policies: policies) + await getExecutablesFromAllPolicies(policies: policies) for policy in policies { let executable = getExecutableFromSelectedExecutables(bundleIdentifier: policy.identifier) @@ -215,7 +215,7 @@ extension Model { if let source = executable, let rIdentifier = policy.receiverIdentifier, let rCodeRequirement = policy.receiverCodeRequirement { - let destination = getExecutableFrom(identifier: rIdentifier, codeRequirement: rCodeRequirement) + let destination = await getExecutableFrom(identifier: rIdentifier, codeRequirement: rCodeRequirement) let allowed: Bool = (policy.allowed == true || policy.authorization == TCCPolicyAuthorizationValue.allow) let appleEvent = AppleEventRule(source: source, destination: destination, value: allowed) executable?.appleEvents.appendIfNew(appleEvent) @@ -252,9 +252,9 @@ extension Model { return policy } - func getExecutablesFromAllPolicies(policies: [TCCPolicy]) { + func getExecutablesFromAllPolicies(policies: [TCCPolicy]) async { for tccPolicy in policies where getExecutableFromSelectedExecutables(bundleIdentifier: tccPolicy.identifier) == nil { - let executable = getExecutableFrom(identifier: tccPolicy.identifier, codeRequirement: tccPolicy.codeRequirement) + let executable = await getExecutableFrom(identifier: tccPolicy.identifier, codeRequirement: tccPolicy.codeRequirement) self.selectedExecutables.append(executable) } } @@ -266,10 +266,10 @@ extension Model { return nil } - func getExecutableFrom(identifier: String, codeRequirement: String) -> Executable { + func getExecutableFrom(identifier: String, codeRequirement: String) async -> Executable { var executable = Executable(identifier: identifier, codeRequirement: codeRequirement) do { - executable = try findExecutable(bundleIdentifier: identifier) + executable = try await findExecutable(bundleIdentifier: identifier) } catch { self.logger.error("\(error)") } @@ -277,7 +277,7 @@ extension Model { return executable } - private func findExecutable(bundleIdentifier: String) throws -> Executable { + private func findExecutable(bundleIdentifier: String) async throws -> Executable { var urlToLoad: URL? if bundleIdentifier.contains("/") { urlToLoad = URL(string: "file://\(bundleIdentifier)") @@ -286,7 +286,7 @@ extension Model { } if let fileURL = urlToLoad { - return try self.loadExecutable(url: fileURL) + return try await self.loadExecutable(url: fileURL) } throw LoadExecutableError.executableNotFound } diff --git a/Source/SecurityWrapper.swift b/Source/SecurityWrapper.swift index ad83621..4f4e775 100644 --- a/Source/SecurityWrapper.swift +++ b/Source/SecurityWrapper.swift @@ -70,7 +70,7 @@ struct SecurityWrapper { return nil } - static func copyDesignatedRequirement(url: URL) throws -> String { + @concurrent static func copyDesignatedRequirement(url: URL) async throws -> String { let flags = SecCSFlags(rawValue: 0) var staticCode: SecStaticCode? var requirement: SecRequirement? @@ -83,7 +83,7 @@ struct SecurityWrapper { return text! as String } - static func sign(data: Data, using identity: SecIdentity) throws -> Data { + @concurrent static func sign(data: Data, using identity: SecIdentity) async throws -> Data { var outputData: CFData? var encoder: CMSEncoder? @@ -96,7 +96,7 @@ struct SecurityWrapper { return outputData! as Data } - static func loadSigningIdentities() throws -> [SigningIdentity] { + @concurrent static func loadSigningIdentities() async throws -> [SigningIdentity] { let haversack = Haversack() let query = IdentityQuery().matching(mustBeValidOnDate: Date()).returning(.reference) diff --git a/Source/View Controllers/OpenViewController.swift b/Source/View Controllers/OpenViewController.swift index 3e622fa..f36f2a4 100644 --- a/Source/View Controllers/OpenViewController.swift +++ b/Source/View Controllers/OpenViewController.swift @@ -43,7 +43,9 @@ class OpenViewController: NSViewController, NSTableViewDataSource, NSTableViewDe // Reload executables current = Model.shared.current if let value = current { - choices = Model.shared.getAppleEventChoices(executable: value) + Task { + choices = await Model.shared.getAppleEventChoices(executable: value) + } } } @@ -62,18 +64,20 @@ class OpenViewController: NSViewController, NSTableViewDataSource, NSTableViewDe panel.directoryURL = URL(fileURLWithPath: "/Applications", isDirectory: true) panel.begin { response in if response == .OK { - var selections: [LoadExecutableResult] = [] - panel.urls.forEach { - do { - let executable = try Model.shared.loadExecutable(url: $0) - selections.append(.success(executable)) - } catch { - if let loadError = error as? LoadExecutableError { - selections.append(.failure(loadError)) + Task { + var selections: [LoadExecutableResult] = [] + for url in panel.urls { + do { + let executable = try await Model.shared.loadExecutable(url: url) + selections.append(.success(executable)) + } catch { + if let loadError = error as? LoadExecutableError { + selections.append(.failure(loadError)) + } } } + block?(selections) } - block?(selections) } } } diff --git a/Source/View Controllers/SaveViewController.swift b/Source/View Controllers/SaveViewController.swift index 28a0558..6feb725 100644 --- a/Source/View Controllers/SaveViewController.swift +++ b/Source/View Controllers/SaveViewController.swift @@ -91,12 +91,14 @@ class SaveViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() payloadIdentifier = UUID().uuidString - do { - var identities = try SecurityWrapper.loadSigningIdentities() - identities.insert(SigningIdentity(name: "Not signed", reference: nil), at: 0) - identitiesPopUpAC.add(contentsOf: identities) - } catch { - logger.error("Error loading identities: \(error)") + Task { + do { + var identities = try await SecurityWrapper.loadSigningIdentities() + identities.insert(SigningIdentity(name: "Not signed", reference: nil), at: 0) + identitiesPopUpAC.add(contentsOf: identities) + } catch { + logger.error("Error loading identities: \(error)") + } } loadImportedTCCProfileInfo() @@ -131,18 +133,20 @@ class SaveViewController: NSViewController { identifier: payloadIdentifier, displayName: payloadName, payloadDescription: payloadDescription ?? payloadName) - do { - var outputData = try profile.xmlData() - if let identity = identitiesPopUpAC.selectedObjects.first as? SigningIdentity, let ref = identity.reference { - logger.info("Signing profile with \(identity.displayName)") - outputData = try SecurityWrapper.sign(data: outputData, using: ref) + Task { + do { + var outputData = try profile.xmlData() + if let identity = identitiesPopUpAC.selectedObjects.first as? SigningIdentity, let ref = identity.reference { + logger.info("Signing profile with \(identity.displayName)") + outputData = try await SecurityWrapper.sign(data: outputData, using: ref) + } + try outputData.write(to: url) + logger.info("Saved successfully") + } catch { + logger.error("Error: \(error)") } - try outputData.write(to: url) - logger.info("Saved successfully") - } catch { - logger.error("Error: \(error)") + self.dismiss(nil) } - self.dismiss(nil) } func loadImportedTCCProfileInfo() { diff --git a/Source/View Controllers/TCCProfileViewController.swift b/Source/View Controllers/TCCProfileViewController.swift index 5f40d5a..4d3378c 100644 --- a/Source/View Controllers/TCCProfileViewController.swift +++ b/Source/View Controllers/TCCProfileViewController.swift @@ -147,21 +147,23 @@ class TCCProfileViewController: NSViewController { let logger = Logger.TCCProfileViewController @IBAction func uploadAction(_ sender: NSButton) { - let identities: [SigningIdentity] - do { - identities = try SecurityWrapper.loadSigningIdentities() - } catch { - identities = [] - logger.error("Error loading identities: \(error.localizedDescription)") - } + Task { + let identities: [SigningIdentity] + do { + identities = try await SecurityWrapper.loadSigningIdentities() + } catch { + identities = [] + logger.error("Error loading identities: \(error.localizedDescription)") + } - let uploadView = UploadInfoView(signingIdentities: identities) { - // Dismiss the sheet when the UploadInfoView decides it is done - if let controller = self.presentedViewControllers?.first { - self.dismiss(controller) + let uploadView = UploadInfoView(signingIdentities: identities) { + // Dismiss the sheet when the UploadInfoView decides it is done + if let controller = self.presentedViewControllers?.first { + self.dismiss(controller) + } } + self.presentAsSheet(NSHostingController(rootView: uploadView)) } - self.presentAsSheet(NSHostingController(rootView: uploadView)) } fileprivate func showAlert(_ error: LocalizedError, for window: NSWindow) { @@ -185,7 +187,9 @@ class TCCProfileViewController: NSViewController { guard let weakSelf = self else { return } switch tccProfileResult { case .success(let tccProfile): - weakSelf.model.importProfile(tccProfile: tccProfile) + Task { + await weakSelf.model.importProfile(tccProfile: tccProfile) + } case .failure(let tccProfileImportError): if !tccProfileImportError.isCancelled { weakSelf.showAlert(tccProfileImportError, for: window) @@ -204,20 +208,22 @@ class TCCProfileViewController: NSViewController { } panel.begin { response in if response == .OK { - panel.urls.forEach { - do { - let executable = try self.model.loadExecutable(url: $0) - guard self.shouldExecutableBeAdded(executable) else { - let error = LoadExecutableError.executableAlreadyExists - self.showAlert(error, for: window) - return - } - block(executable) - } catch { - if let loadError = error as? LoadExecutableError { - self.showAlert(loadError, for: window) + Task { + for url in panel.urls { + do { + let executable = try await self.model.loadExecutable(url: url) + guard self.shouldExecutableBeAdded(executable) else { + let error = LoadExecutableError.executableAlreadyExists + self.showAlert(error, for: window) + continue + } + block(executable) + } catch { + if let loadError = error as? LoadExecutableError { + self.showAlert(loadError, for: window) + } + self.logger.error("\(error)") } - self.logger.error("\(error)") } } } @@ -403,31 +409,32 @@ extension TCCProfileViewController: NSTableViewDataSource { guard let window = self.view.window else { return false } - var addedAny = false - urls?.forEach { (url) in - do { - let newExecutable = try model.loadExecutable(url: url) - if tableView == self.executablesTable { - guard self.executablesAC.canInsert else { - return + guard let urls = urls, !urls.isEmpty else { return false } + + Task { + for url in urls { + do { + let newExecutable = try await model.loadExecutable(url: url) + if tableView == self.executablesTable { + guard self.executablesAC.canInsert else { + continue + } + if self.shouldExecutableBeAdded(newExecutable) { + self.executablesAC.insert(newExecutable, atArrangedObjectIndex: row) + } + } else { + self.insertIntoAppleEvents(newExecutable) } - if self.shouldExecutableBeAdded(newExecutable) { - self.executablesAC.insert(newExecutable, atArrangedObjectIndex: row) - addedAny = true + } catch { + if let loadError = error as? LoadExecutableError { + self.showAlert(loadError, for: window) } - } else { - self.insertIntoAppleEvents(newExecutable) - addedAny = true - } - } catch { - if let loadError = error as? LoadExecutableError { - self.showAlert(loadError, for: window) + self.logger.error("\(error)") } - self.logger.error("\(error)") } } - return addedAny + return true } } From 23583a42c6f131aac6a1d1181b29505c287a4030 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:42:44 +0000 Subject: [PATCH 08/18] Stage 5: Enable Swift 6 language mode Flip SWIFT_VERSION from 5.0 to 6.0 so all concurrency warnings become hard errors. Fix override isolation mismatches. Changes: - SWIFT_VERSION = 6.0 in all 4 build configurations - SaveViewController.observeValue: add nonisolated + MainActor.assumeIsolated - ModelTests.setUp: add nonisolated + MainActor.assumeIsolated - Update plan document to reflect all stages complete Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> --- PPPC Utility.xcodeproj/project.pbxproj | 8 ++++---- PPPC UtilityTests/ModelTests/ModelTests.swift | 6 ++++-- Source/View Controllers/SaveViewController.swift | 12 +++++++----- docs/plans/approachable-concurrency.md | 14 +++++++------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/PPPC Utility.xcodeproj/project.pbxproj b/PPPC Utility.xcodeproj/project.pbxproj index fd7dfb4..551b2de 100644 --- a/PPPC Utility.xcodeproj/project.pbxproj +++ b/PPPC Utility.xcodeproj/project.pbxproj @@ -586,7 +586,7 @@ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PPPC Utility.app/Contents/MacOS/PPPC Utility"; }; name = Debug; @@ -612,7 +612,7 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PPPC Utility.app/Contents/MacOS/PPPC Utility"; }; name = Release; @@ -756,7 +756,7 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -782,7 +782,7 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Release; }; diff --git a/PPPC UtilityTests/ModelTests/ModelTests.swift b/PPPC UtilityTests/ModelTests/ModelTests.swift index 5021027..6bb3e5a 100644 --- a/PPPC UtilityTests/ModelTests/ModelTests.swift +++ b/PPPC UtilityTests/ModelTests/ModelTests.swift @@ -34,9 +34,11 @@ class ModelTests: XCTestCase { var model: Model! - override func setUp() { + nonisolated override func setUp() { super.setUp() - model = Model() + MainActor.assumeIsolated { + model = Model() + } } // MARK: - tests for getExecutableFrom* diff --git a/Source/View Controllers/SaveViewController.swift b/Source/View Controllers/SaveViewController.swift index 6feb725..7dd9b9d 100644 --- a/Source/View Controllers/SaveViewController.swift +++ b/Source/View Controllers/SaveViewController.swift @@ -118,11 +118,13 @@ class SaveViewController: NSViewController { } // swiftlint:disable:next block_based_kvo - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - if context == &SaveViewController.saveProfileKVOContext { - updateIsReadyToSave() - } else { - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + nonisolated override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { + MainActor.assumeIsolated { + if context == &SaveViewController.saveProfileKVOContext { + updateIsReadyToSave() + } else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + } } } diff --git a/docs/plans/approachable-concurrency.md b/docs/plans/approachable-concurrency.md index 127629f..7ea242f 100644 --- a/docs/plans/approachable-concurrency.md +++ b/docs/plans/approachable-concurrency.md @@ -17,13 +17,13 @@ PPPC Utility is on Swift 5.0 with zero concurrency checking. The goal is to adop | PR | Stage | Status | Description | |----|-------|--------|-------------| | 1 | Stage 1 | ✅ Done | Enable Approachable Concurrency build settings | -| 2 | Stage 2 | Pending | Remove `NetworkAuthManager` actor → class | -| 3a | Stage 3a | Pending | Remove 3 `DispatchQueue.main.async` wrappers | -| 3b | Stage 3b | Pending | Convert `UploadManager` to async throws | -| 3c | Stage 3c | Pending | Convert `Model.loadExecutable` to direct return | -| 3d | Stage 3d | Pending | Convert `TCCProfileImporter` to direct return | -| 4 | Stage 4 | Pending | Add `@concurrent` for background I/O | -| 5 | Stage 5 | Pending | Enable Swift 6 language mode (warnings → errors) | +| 2 | Stage 2 | ✅ Done | Remove `NetworkAuthManager` actor → class | +| 3a | Stage 3a | ✅ Done | Remove 3 `DispatchQueue.main.async` wrappers | +| 3b | Stage 3b | ✅ Done | Convert `UploadManager` to async throws | +| 3c | Stage 3c | ✅ Done | Convert `Model.loadExecutable` to direct return | +| 3d | Stage 3d | ✅ Done | Convert `TCCProfileImporter` to direct return | +| 4 | Stage 4 | ✅ Done | Add `@concurrent` for background I/O | +| 5 | Stage 5 | ✅ Done | Enable Swift 6 language mode (warnings → errors) | PRs 3a–3d can be one PR or individual PRs — each is independently functional. PR 4 depends on PR 3c. PR 5 depends on all prior stages. From c24c1e9f8aac467f44ac5c5285215c6f960ec6e8 Mon Sep 17 00:00:00 2001 From: JJ Pritzl Date: Tue, 31 Mar 2026 12:12:39 -0500 Subject: [PATCH 09/18] create-separate story/JPCFM-5564 Fix failing build and tests --- PPPC Utility.xcodeproj/project.pbxproj | 12 ++++++------ PPPC UtilityTests/Helpers/ModelBuilder.swift | 1 + PPPC UtilityTests/ModelTests/ModelTests.swift | 2 +- .../ModelTests/PPPCServicesManagerTests.swift | 12 ++++++------ .../ModelTests/SemanticVersionTests.swift | 2 +- .../NetworkingTests/JamfProAPIClientTests.swift | 4 ++-- .../NetworkAuthManagerTests.swift | 8 ++++---- .../NetworkingTests/TokenTests.swift | 2 +- .../TCCProfileImporterTests.swift | 12 ++++++------ .../TCCProfileTests.swift | 16 ++++++++-------- Source/AppDelegate.swift | 2 +- Source/Model/Model.swift | 2 +- Source/Model/SigningIdentity.swift | 8 ++++---- Source/Model/TCCProfile.swift | 4 ++-- Source/Networking/UploadManager.swift | 2 +- Source/SecurityWrapper.swift | 9 ++++++--- Source/View Controllers/SaveViewController.swift | 8 ++++---- 17 files changed, 55 insertions(+), 51 deletions(-) diff --git a/PPPC Utility.xcodeproj/project.pbxproj b/PPPC Utility.xcodeproj/project.pbxproj index 551b2de..5d1b451 100644 --- a/PPPC Utility.xcodeproj/project.pbxproj +++ b/PPPC Utility.xcodeproj/project.pbxproj @@ -572,7 +572,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = XPLDEEDNHE; + DEVELOPMENT_TEAM = 483DWKW443; INFOPLIST_FILE = "PPPC UtilityTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -586,7 +586,7 @@ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PPPC Utility.app/Contents/MacOS/PPPC Utility"; }; name = Debug; @@ -599,7 +599,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = XPLDEEDNHE; + DEVELOPMENT_TEAM = 483DWKW443; INFOPLIST_FILE = "PPPC UtilityTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -612,7 +612,7 @@ SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PPPC Utility.app/Contents/MacOS/PPPC Utility"; }; name = Release; @@ -742,7 +742,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = XPLDEEDNHE; + DEVELOPMENT_TEAM = 483DWKW443; ENABLE_HARDENED_RUNTIME = NO; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -768,7 +768,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = XPLDEEDNHE; + DEVELOPMENT_TEAM = 483DWKW443; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/PPPC UtilityTests/Helpers/ModelBuilder.swift b/PPPC UtilityTests/Helpers/ModelBuilder.swift index 30514ee..7073724 100644 --- a/PPPC UtilityTests/Helpers/ModelBuilder.swift +++ b/PPPC UtilityTests/Helpers/ModelBuilder.swift @@ -28,6 +28,7 @@ import Cocoa @testable import PPPC_Utility +@MainActor class ModelBuilder { var model: Model diff --git a/PPPC UtilityTests/ModelTests/ModelTests.swift b/PPPC UtilityTests/ModelTests/ModelTests.swift index 6bb3e5a..4556bc9 100644 --- a/PPPC UtilityTests/ModelTests/ModelTests.swift +++ b/PPPC UtilityTests/ModelTests/ModelTests.swift @@ -26,7 +26,7 @@ // import Foundation -import XCTest +@preconcurrency import XCTest @testable import PPPC_Utility diff --git a/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift b/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift index d92bc82..d8d5571 100644 --- a/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift +++ b/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift @@ -26,13 +26,13 @@ // import Foundation -import XCTest +@preconcurrency import XCTest @testable import PPPC_Utility class PPPCServicesManagerTests: XCTestCase { - func testLoadAllServices() { + @MainActor func testLoadAllServices() async { // given/when let actual = PPPCServicesManager() @@ -40,7 +40,7 @@ class PPPCServicesManagerTests: XCTestCase { XCTAssertEqual(actual.allServices.count, 21) } - func testUserHelp_withEntitlements() throws { + @MainActor func testUserHelp_withEntitlements() async throws { // given let services = PPPCServicesManager() let service = try XCTUnwrap(services.allServices["Camera"]) @@ -52,7 +52,7 @@ class PPPCServicesManagerTests: XCTestCase { XCTAssertEqual(actual, "Use to deny specified apps access to the camera.\n\nMDM Key: Camera\nRelated entitlements: [\"com.apple.developer.avfoundation.multitasking-camera-access\", \"com.apple.security.device.camera\"]") } - func testUserHelp_withoutEntitlements() throws { + @MainActor func testUserHelp_withoutEntitlements() async throws { // given let services = PPPCServicesManager() let service = try XCTUnwrap(services.allServices["ScreenCapture"]) @@ -64,7 +64,7 @@ class PPPCServicesManagerTests: XCTestCase { XCTAssertEqual(actual, "Deny specified apps access to capture (read) the contents of the system display.\n\nMDM Key: ScreenCapture") } - func testCameraIsDenyOnly() throws { + @MainActor func testCameraIsDenyOnly() async throws { // given let services = PPPCServicesManager() let service = try XCTUnwrap(services.allServices["Camera"]) @@ -76,7 +76,7 @@ class PPPCServicesManagerTests: XCTestCase { XCTAssertTrue(actual) } - func testScreenCaptureAllowsStandardUsers() throws { + @MainActor func testScreenCaptureAllowsStandardUsers() async throws { // given let services = PPPCServicesManager() let service = try XCTUnwrap(services.allServices["ScreenCapture"]) diff --git a/PPPC UtilityTests/ModelTests/SemanticVersionTests.swift b/PPPC UtilityTests/ModelTests/SemanticVersionTests.swift index 93e788c..2219545 100644 --- a/PPPC UtilityTests/ModelTests/SemanticVersionTests.swift +++ b/PPPC UtilityTests/ModelTests/SemanticVersionTests.swift @@ -27,7 +27,7 @@ import Foundation @testable import PPPC_Utility -import XCTest +@preconcurrency import XCTest class SemanticVersionTests: XCTestCase { func testLessThan() { diff --git a/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift b/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift index 308decd..0deddb5 100644 --- a/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift +++ b/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift @@ -6,12 +6,12 @@ // Copyright (c) 2023 Jamf Software import Foundation -import XCTest +@preconcurrency import XCTest @testable import PPPC_Utility class JamfProAPIClientTests: XCTestCase { - func testOAuthTokenRequest() throws { + @MainActor func testOAuthTokenRequest() async throws { // given let authManager = NetworkAuthManager(username: "", password: "") let apiClient = JamfProAPIClient(serverUrlString: "https://something", tokenManager: authManager) diff --git a/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift b/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift index 8cd1768..0d8c14e 100644 --- a/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift +++ b/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift @@ -26,7 +26,7 @@ // import Foundation -import XCTest +@preconcurrency import XCTest @testable import PPPC_Utility @@ -121,7 +121,7 @@ class NetworkAuthManagerTests: XCTestCase { } } - func testBasicAuthString() throws { + @MainActor func testBasicAuthString() async throws { // given let authManager = NetworkAuthManager(username: "test", password: "none") @@ -132,7 +132,7 @@ class NetworkAuthManagerTests: XCTestCase { XCTAssertEqual(actual, "dGVzdDpub25l") } - func testBasicAuthStringEmptyUsername() throws { + @MainActor func testBasicAuthStringEmptyUsername() async throws { // given let authManager = NetworkAuthManager(username: "", password: "none") @@ -146,7 +146,7 @@ class NetworkAuthManagerTests: XCTestCase { } } - func testBasicAuthStringEmptyPassword() throws { + @MainActor func testBasicAuthStringEmptyPassword() async throws { // given let authManager = NetworkAuthManager(username: "mine", password: "") diff --git a/PPPC UtilityTests/NetworkingTests/TokenTests.swift b/PPPC UtilityTests/NetworkingTests/TokenTests.swift index 973e1d5..4d02f10 100644 --- a/PPPC UtilityTests/NetworkingTests/TokenTests.swift +++ b/PPPC UtilityTests/NetworkingTests/TokenTests.swift @@ -26,7 +26,7 @@ // import Foundation -import XCTest +@preconcurrency import XCTest @testable import PPPC_Utility diff --git a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift index 696f06e..c6d7a9c 100644 --- a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift +++ b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift @@ -26,13 +26,13 @@ // import Foundation -import XCTest +@preconcurrency import XCTest @testable import PPPC_Utility class TCCProfileImporterTests: XCTestCase { - func testMalformedTCCProfile() { + @MainActor func testMalformedTCCProfile() async { let tccProfileImporter = TCCProfileImporter() let resourceURL = getResourceProfile(fileName: "TestTCCProfileSigned-Broken") @@ -47,7 +47,7 @@ class TCCProfileImporterTests: XCTestCase { } } - func testEmptyContentTCCProfile() { + @MainActor func testEmptyContentTCCProfile() async { let tccProfileImporter = TCCProfileImporter() let resourceURL = getResourceProfile(fileName: "TestTCCUnsignedProfile-Empty") @@ -62,7 +62,7 @@ class TCCProfileImporterTests: XCTestCase { } } - func testCorrectUnsignedProfileContentData() throws { + @MainActor func testCorrectUnsignedProfileContentData() async throws { let tccProfileImporter = TCCProfileImporter() let resourceURL = getResourceProfile(fileName: "TestTCCUnsignedProfile") @@ -72,7 +72,7 @@ class TCCProfileImporterTests: XCTestCase { XCTAssertNotNil(tccProfile.content[0].services) } - func testCorrectUnsignedProfileContentDataAllLowercase() throws { + @MainActor func testCorrectUnsignedProfileContentDataAllLowercase() async throws { let tccProfileImporter = TCCProfileImporter() let resourceURL = getResourceProfile(fileName: "TestTCCUnsignedProfile-allLower") @@ -82,7 +82,7 @@ class TCCProfileImporterTests: XCTestCase { XCTAssertNotNil(tccProfile.content[0].services) } - func testBrokenUnsignedProfile() { + @MainActor func testBrokenUnsignedProfile() async { let tccProfileImporter = TCCProfileImporter() let resourceURL = getResourceProfile(fileName: "TestTCCUnsignedProfile-Broken") diff --git a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileTests.swift b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileTests.swift index f643e93..d37b811 100644 --- a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileTests.swift +++ b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileTests.swift @@ -23,7 +23,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // -import XCTest +@preconcurrency import XCTest @testable import PPPC_Utility @@ -31,7 +31,7 @@ class TCCProfileTests: XCTestCase { // MARK: - tests for serializing to and from xml - func testSerializationOfComplexProfileUsingAuthorization() throws { + @MainActor func testSerializationOfComplexProfileUsingAuthorization() async throws { // when we export to xml and reimport it should still have the same attributes let plistData = try TCCProfileBuilder().buildProfile(authorization: .allowStandardUserToSetSystemService).xmlData() let profile = try TCCProfile.parse(from: plistData) @@ -75,7 +75,7 @@ class TCCProfileTests: XCTestCase { } } - func testSerializationOfProfileUsingLegacyAllowedKey() throws { + @MainActor func testSerializationOfProfileUsingLegacyAllowedKey() async throws { // when we export to xml and reimport it should still have the same attributes let plistData = try TCCProfileBuilder().buildProfile(allowed: true).xmlData() let profile = try TCCProfile.parse(from: plistData) @@ -119,7 +119,7 @@ class TCCProfileTests: XCTestCase { } } - func testSerializationOfProfileWhenBothAllowedAndAuthorizationUsed() throws { + @MainActor func testSerializationOfProfileWhenBothAllowedAndAuthorizationUsed() async throws { // when we export to xml and reimport it should still have the same attributes let plistData = try TCCProfileBuilder().buildProfile(allowed: false, authorization: .allow).xmlData() let profile = try TCCProfile.parse(from: plistData) @@ -146,7 +146,7 @@ class TCCProfileTests: XCTestCase { // unit tests for handling both Auth and allowed keys should fail? - func testSettingLegacyAllowValueNullifiesAuthorization() throws { + @MainActor func testSettingLegacyAllowValueNullifiesAuthorization() async throws { // given var tccPolicy = TCCPolicy(identifier: "id", codeRequirement: "req", receiverIdentifier: "recId", receiverCodeRequirement: "recreq") tccPolicy.authorization = .allow @@ -159,7 +159,7 @@ class TCCProfileTests: XCTestCase { XCTAssertTrue(try XCTUnwrap(tccPolicy.allowed)) } - func testSettingAuthorizationValueDoesNotNullifyAllowed() { + @MainActor func testSettingAuthorizationValueDoesNotNullifyAllowed() async { // given var tccPolicy = TCCPolicy(identifier: "id", codeRequirement: "req", receiverIdentifier: "recId", receiverCodeRequirement: "recreq") tccPolicy.allowed = false @@ -172,13 +172,13 @@ class TCCProfileTests: XCTestCase { XCTAssertEqual(tccPolicy.authorization, TCCPolicyAuthorizationValue.allowStandardUserToSetSystemService) } - func testJamfProAPIData() throws { + func testJamfProAPIData() async throws { // given - build the test profile let tccProfile = TCCProfileBuilder().buildProfile(allowed: false, authorization: .allow) let expected = try loadTextFile(fileName: "TestTCCProfileForJamfProAPI").trimmingCharacters(in: .whitespacesAndNewlines) // when - wrap in Jamf Pro API xml - let data = try tccProfile.jamfProAPIData(signingIdentity: nil, site: nil) + let data = try await tccProfile.jamfProAPIData(signingIdentity: nil, site: nil) // then let xmlString = String(data: data, encoding: .utf8) diff --git a/Source/AppDelegate.swift b/Source/AppDelegate.swift index ec32034..041ac5b 100644 --- a/Source/AppDelegate.swift +++ b/Source/AppDelegate.swift @@ -27,7 +27,7 @@ import Cocoa -@NSApplicationMain +@main class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) {} diff --git a/Source/Model/Model.swift b/Source/Model/Model.swift index 6dc6ae5..3854d7f 100644 --- a/Source/Model/Model.swift +++ b/Source/Model/Model.swift @@ -81,7 +81,7 @@ typealias LoadExecutableResult = Result extension Model { - @concurrent func loadExecutable(url: URL) async throws -> Executable { + func loadExecutable(url: URL) async throws -> Executable { let executable = Executable() if let bundle = Bundle(url: url) { diff --git a/Source/Model/SigningIdentity.swift b/Source/Model/SigningIdentity.swift index 7ba021a..469412f 100644 --- a/Source/Model/SigningIdentity.swift +++ b/Source/Model/SigningIdentity.swift @@ -30,11 +30,11 @@ import Cocoa class SigningIdentity: NSObject { @objc dynamic var displayName: String - var reference: SecIdentity? + nonisolated(unsafe) var reference: SecIdentity? - init(name: String, reference: SecIdentity?) { - displayName = name - super.init() + nonisolated init(name: String, reference: SecIdentity?) { + self.displayName = name self.reference = reference + super.init() } } diff --git a/Source/Model/TCCProfile.swift b/Source/Model/TCCProfile.swift index 329228e..92b0e8d 100644 --- a/Source/Model/TCCProfile.swift +++ b/Source/Model/TCCProfile.swift @@ -160,11 +160,11 @@ public struct TCCProfile: Codable { /// - signingIdentity: A signing identity; can be nil to leave the profile unsigned. /// - site: A Jamf Pro site /// - Returns: XML data for use with the Jamf Pro API. - func jamfProAPIData(signingIdentity: SecIdentity?, site: (String, String)?) throws -> Data { + func jamfProAPIData(signingIdentity: SecIdentity?, site: (String, String)?) async throws -> Data { var profileText: String var profileData = try xmlData() if let identity = signingIdentity { - profileData = try SecurityWrapper.sign(data: profileData, using: identity) + profileData = try await SecurityWrapper.sign(data: profileData, using: identity) } profileText = String(data: profileData, encoding: .utf8) ?? "" diff --git a/Source/Networking/UploadManager.swift b/Source/Networking/UploadManager.swift index 71194ba..2ca5a25 100644 --- a/Source/Networking/UploadManager.swift +++ b/Source/Networking/UploadManager.swift @@ -56,7 +56,7 @@ struct UploadManager { identity = signingIdentity.reference } - let profileData = try profile.jamfProAPIData(signingIdentity: identity, site: siteInfo) + let profileData = try await profile.jamfProAPIData(signingIdentity: identity, site: siteInfo) _ = try await networking.upload(computerConfigProfile: profileData) diff --git a/Source/SecurityWrapper.swift b/Source/SecurityWrapper.swift index 4f4e775..3960b1d 100644 --- a/Source/SecurityWrapper.swift +++ b/Source/SecurityWrapper.swift @@ -27,10 +27,13 @@ import Foundation import Haversack +import Security + +extension SecIdentity: @unchecked @retroactive Sendable {} struct SecurityWrapper { - static func execute(block: () -> (OSStatus)) throws { + nonisolated static func execute(block: () -> (OSStatus)) throws { let status = block() if status != 0 { throw NSError(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: nil) @@ -100,7 +103,7 @@ struct SecurityWrapper { let haversack = Haversack() let query = IdentityQuery().matching(mustBeValidOnDate: Date()).returning(.reference) - let identities = try haversack.search(where: query) + let identities = try await haversack.search(where: query) return identities.compactMap { guard let secIdentity = $0.reference else { @@ -113,7 +116,7 @@ struct SecurityWrapper { } } - static func getCertificateCommonName(for identity: SecIdentity) throws -> String { + nonisolated static func getCertificateCommonName(for identity: SecIdentity) throws -> String { var certificate: SecCertificate? var commonName: CFString? try execute { SecIdentityCopyCertificate(identity, &certificate) } diff --git a/Source/View Controllers/SaveViewController.swift b/Source/View Controllers/SaveViewController.swift index 7dd9b9d..da9c97f 100644 --- a/Source/View Controllers/SaveViewController.swift +++ b/Source/View Controllers/SaveViewController.swift @@ -119,12 +119,12 @@ class SaveViewController: NSViewController { // swiftlint:disable:next block_based_kvo nonisolated override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - MainActor.assumeIsolated { - if context == &SaveViewController.saveProfileKVOContext { + if context == &SaveViewController.saveProfileKVOContext { + MainActor.assumeIsolated { updateIsReadyToSave() - } else { - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } + } else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } } From 24d36a5500cff216f7931aee74e6af170e38227b Mon Sep 17 00:00:00 2001 From: JJ Pritzl Date: Tue, 31 Mar 2026 12:18:37 -0500 Subject: [PATCH 10/18] create-separate story/JPCFM-5564 Fix code signing mistake --- PPPC Utility.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PPPC Utility.xcodeproj/project.pbxproj b/PPPC Utility.xcodeproj/project.pbxproj index 5d1b451..e916348 100644 --- a/PPPC Utility.xcodeproj/project.pbxproj +++ b/PPPC Utility.xcodeproj/project.pbxproj @@ -572,7 +572,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 483DWKW443; + DEVELOPMENT_TEAM = "XPLDEEDNHE"; INFOPLIST_FILE = "PPPC UtilityTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -599,7 +599,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 483DWKW443; + DEVELOPMENT_TEAM = "XPLDEEDNHE"; INFOPLIST_FILE = "PPPC UtilityTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -742,7 +742,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 483DWKW443; + DEVELOPMENT_TEAM = XPLDEEDNHE; ENABLE_HARDENED_RUNTIME = NO; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -768,7 +768,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 483DWKW443; + DEVELOPMENT_TEAM = XPLDEEDNHE; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( From cf78ace64656e26d9365ca8ab49ed2a4a89599b2 Mon Sep 17 00:00:00 2001 From: JJ Pritzl Date: Tue, 31 Mar 2026 12:20:03 -0500 Subject: [PATCH 11/18] create-separate story/JPCFM-5564 Fix bad syntax in build settings --- PPPC Utility.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PPPC Utility.xcodeproj/project.pbxproj b/PPPC Utility.xcodeproj/project.pbxproj index e916348..6b2735e 100644 --- a/PPPC Utility.xcodeproj/project.pbxproj +++ b/PPPC Utility.xcodeproj/project.pbxproj @@ -742,7 +742,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = XPLDEEDNHE; + DEVELOPMENT_TEAM = "XPLDEEDNHE"; ENABLE_HARDENED_RUNTIME = NO; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -768,7 +768,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = XPLDEEDNHE; + DEVELOPMENT_TEAM = "XPLDEEDNHE"; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( From 60390d533814e39b5ff74e5396477a8687e91983 Mon Sep 17 00:00:00 2001 From: JJ Pritzl Date: Tue, 31 Mar 2026 12:22:24 -0500 Subject: [PATCH 12/18] create-separate story/JPCFM-5564 Fix bad syntax in project settings for last time --- PPPC Utility.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PPPC Utility.xcodeproj/project.pbxproj b/PPPC Utility.xcodeproj/project.pbxproj index 6b2735e..94d7001 100644 --- a/PPPC Utility.xcodeproj/project.pbxproj +++ b/PPPC Utility.xcodeproj/project.pbxproj @@ -572,7 +572,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = "XPLDEEDNHE"; + DEVELOPMENT_TEAM = XPLDEEDNHE; INFOPLIST_FILE = "PPPC UtilityTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -599,7 +599,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = "XPLDEEDNHE"; + DEVELOPMENT_TEAM = XPLDEEDNHE; INFOPLIST_FILE = "PPPC UtilityTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -742,7 +742,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = "XPLDEEDNHE"; + DEVELOPMENT_TEAM = XPLDEEDNHE; ENABLE_HARDENED_RUNTIME = NO; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -768,7 +768,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = "XPLDEEDNHE"; + DEVELOPMENT_TEAM = XPLDEEDNHE; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( From ebbd218e93cc9d9a9625c0227778fadd8fe3c066 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:12:47 -0500 Subject: [PATCH 13/18] Add swift-format GitHub Action, config, and pre-commit hook (#142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Add swift-format config, GitHub Action workflow, and pre-commit hook Co-authored-by: jjpritzl <108085430+jjpritzl@users.noreply.github.com> Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/1f5e71ff-f5b8-4e2c-8b0e-bc262951f2fd * add-swift Disable AlwaysUseLowerCamelCase and ReplaceForEachWithForLoop rules These rules conflict with existing codebase conventions (Logger constants, enum-style variable names, and forEach usage patterns). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * add-swift Apply swift-format to entire codebase One-time bulk format using xcrun swift-format --in-place -r -p . This commit contains only formatting changes — no logic or functional changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * add-swift Fix SwiftLint conflicts with swift-format - Disable opening_brace rule in .swiftlint.yml (conflicts with swift-format's multiline condition brace placement) - Set multiElementCollectionTrailingCommas to false in .swift-format to avoid trailing comma conflicts with SwiftLint - Remove trailing commas added by swift-format - Remove superfluous swiftlint:disable file_length comment - Fix function_body_length violations caused by array expansion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * add-swift Increase file_length limit to 500 in .swiftlint.yml TCCProfileViewController.swift exceeds the default 400-line limit after swift-format expanded multi-line arrays. Raising the threshold avoids needing inline swiftlint:disable comments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * add-swift Fix pre-commit hook to handle special characters in filenames Use NUL-delimited output (git diff -z) with xargs -0 to safely handle filenames containing spaces, tabs, or newlines. Pass -- before the file list to prevent paths beginning with - from being interpreted as options. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * add-swift Remove SwiftLint — swift-format now handles all formatting - Delete .swiftlint.yml config - Delete .github/workflows/swiftlint.yml GitHub Action - Remove SwiftLint build phase from Xcode project - Remove all inline swiftlint:disable/enable comments from Swift files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jjpritzl <108085430+jjpritzl@users.noreply.github.com> Co-authored-by: JJ Pritzl Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/swift-format.yml | 19 + .github/workflows/swiftlint.yml | 21 - .swift-format | 12 + .swiftlint.yml | 12 - PPPC Utility.xcodeproj/project.pbxproj | 23 - .../Helpers/TCCProfileBuilder.swift | 39 +- PPPC UtilityTests/ModelTests/ModelTests.swift | 57 +- .../ModelTests/PPPCServicesManagerTests.swift | 5 +- .../ModelTests/SemanticVersionTests.swift | 3 +- .../JamfProAPIClientTests.swift | 22 +- .../NetworkAuthManagerTests.swift | 8 +- .../NetworkingTests/TokenTests.swift | 140 ++-- .../TCCProfileImporterTests.swift | 3 +- README.md | 10 + Source/Extensions/ArrayExtensions.swift | 1 + Source/Extensions/LoggerExtensions.swift | 2 +- Source/Extensions/TCCProfileExtensions.swift | 13 +- Source/Model/Executable.swift | 2 - Source/Model/LoadExecutableError.swift | 1 + Source/Model/Model.swift | 46 +- Source/Model/TCCProfile.swift | 18 +- Source/Networking/JamfProAPIClient.swift | 84 +-- Source/Networking/JamfProAPITypes.swift | 20 +- Source/Networking/NetworkAuthManager.swift | 26 +- Source/Networking/Networking.swift | 42 +- Source/Networking/Token.swift | 70 +- .../URLSessionAsyncCompatibility.swift | 34 +- Source/Networking/UploadManager.swift | 94 +-- Source/SecurityWrapper.swift | 68 +- Source/SwiftUI/UploadInfoView.swift | 691 +++++++++--------- .../TCCProfileConfigurationPanel.swift | 44 +- .../TCCProfileImportError.swift | 1 + .../View Controllers/OpenViewController.swift | 2 +- .../View Controllers/SaveViewController.swift | 24 +- .../TCCProfileViewController.swift | 116 +-- Source/Views/Alert.swift | 2 +- git-hooks/pre-commit.swift-format | 24 + 37 files changed, 921 insertions(+), 878 deletions(-) create mode 100644 .github/workflows/swift-format.yml delete mode 100644 .github/workflows/swiftlint.yml create mode 100644 .swift-format delete mode 100644 .swiftlint.yml create mode 100755 git-hooks/pre-commit.swift-format diff --git a/.github/workflows/swift-format.yml b/.github/workflows/swift-format.yml new file mode 100644 index 0000000..6ac795c --- /dev/null +++ b/.github/workflows/swift-format.yml @@ -0,0 +1,19 @@ +name: swift-format + +on: + pull_request: + paths: + - '.github/workflows/swift-format.yml' + - '.swift-format' + - '**/*.swift' + +permissions: + contents: read + +jobs: + swift-format: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Lint with swift-format + run: xcrun swift-format lint --strict -r -p . diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml deleted file mode 100644 index b35dc26..0000000 --- a/.github/workflows/swiftlint.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: SwiftLint - -on: - pull_request: - paths: - - '.github/workflows/swiftlint.yml' - - '.swiftlint.yml' - - '**/*.swift' - -permissions: - contents: read - -jobs: - SwiftLint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Swiftlint verification - uses: norio-nomura/action-swiftlint@3.2.1 - with: - args: --strict diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..7ab3302 --- /dev/null +++ b/.swift-format @@ -0,0 +1,12 @@ +{ + "version": 1, + "lineLength": 200, + "indentation": { + "spaces": 4 + }, + "multiElementCollectionTrailingCommas": false, + "rules": { + "AlwaysUseLowerCamelCase": false, + "ReplaceForEachWithForLoop": false + } +} diff --git a/.swiftlint.yml b/.swiftlint.yml deleted file mode 100644 index 3695d01..0000000 --- a/.swiftlint.yml +++ /dev/null @@ -1,12 +0,0 @@ -opt_in_rules: - - sorted_imports - - trailing_closure - - orphaned_doc_comment - -file_length: - ignore_comment_only_lines: true - -disabled_rules: - - line_length - - type_body_length - - todo diff --git a/PPPC Utility.xcodeproj/project.pbxproj b/PPPC Utility.xcodeproj/project.pbxproj index 660a02b..5c9d327 100644 --- a/PPPC Utility.xcodeproj/project.pbxproj +++ b/PPPC Utility.xcodeproj/project.pbxproj @@ -372,7 +372,6 @@ isa = PBXNativeTarget; buildConfigurationList = 6EC409EA214D65BD00BE4F17 /* Build configuration list for PBXNativeTarget "PPPC Utility" */; buildPhases = ( - 49DB95D624991AA800F433CA /* SwiftLint */, 6EC409D6214D65BC00BE4F17 /* Sources */, 6EC409D7214D65BC00BE4F17 /* Frameworks */, 6EC409D8214D65BC00BE4F17 /* Resources */, @@ -466,28 +465,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 49DB95D624991AA800F433CA /* SwiftLint */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = SwiftLint; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 5F95AE172315A6AD002E0A22 /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift b/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift index 67d9830..9d120e2 100644 --- a/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift +++ b/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift @@ -33,8 +33,9 @@ class TCCProfileBuilder: NSObject { // MARK: - build testing objects func buildTCCPolicy(allowed: Bool?, authorization: TCCPolicyAuthorizationValue?) -> TCCPolicy { - var policy = TCCPolicy(identifier: "policy id", codeRequirement: "policy code req", - receiverIdentifier: "policy receiver id", receiverCodeRequirement: "policy receiver code req") + var policy = TCCPolicy( + identifier: "policy id", codeRequirement: "policy code req", + receiverIdentifier: "policy receiver id", receiverCodeRequirement: "policy receiver code req") policy.comment = "policy comment" policy.identifierType = "policy id type" policy.receiverIdentifierType = "policy receiver id type" @@ -44,27 +45,31 @@ class TCCProfileBuilder: NSObject { } func buildTCCPolicies(allowed: Bool?, authorization: TCCPolicyAuthorizationValue?) -> [String: [TCCPolicy]] { - return ["SystemPolicyAllFiles": [buildTCCPolicy(allowed: allowed, authorization: authorization)], - "AppleEvents": [buildTCCPolicy(allowed: allowed, authorization: authorization)]] + return [ + "SystemPolicyAllFiles": [buildTCCPolicy(allowed: allowed, authorization: authorization)], + "AppleEvents": [buildTCCPolicy(allowed: allowed, authorization: authorization)] + ] } func buildTCCContent(_ contentIndex: Int, allowed: Bool?, authorization: TCCPolicyAuthorizationValue?) -> TCCProfile.Content { - return TCCProfile.Content(payloadDescription: "Content Desc \(contentIndex)", - displayName: "Content Name \(contentIndex)", - identifier: "Content ID \(contentIndex)", - organization: "Content Org \(contentIndex)", - type: "Content type \(contentIndex)", - uuid: "Content UUID \(contentIndex)", - version: contentIndex, - services: buildTCCPolicies(allowed: allowed, authorization: authorization)) + return TCCProfile.Content( + payloadDescription: "Content Desc \(contentIndex)", + displayName: "Content Name \(contentIndex)", + identifier: "Content ID \(contentIndex)", + organization: "Content Org \(contentIndex)", + type: "Content type \(contentIndex)", + uuid: "Content UUID \(contentIndex)", + version: contentIndex, + services: buildTCCPolicies(allowed: allowed, authorization: authorization)) } func buildProfile(allowed: Bool? = nil, authorization: TCCPolicyAuthorizationValue? = nil) -> TCCProfile { - var profile = TCCProfile(organization: "Test Org", - identifier: "Test ID", - displayName: "Test Name", - payloadDescription: "Test Desc", - services: [:]) + var profile = TCCProfile( + organization: "Test Org", + identifier: "Test ID", + displayName: "Test Name", + payloadDescription: "Test Desc", + services: [:]) profile.content = [buildTCCContent(1, allowed: allowed, authorization: authorization)] profile.version = 100 profile.uuid = "the uuid" diff --git a/PPPC UtilityTests/ModelTests/ModelTests.swift b/PPPC UtilityTests/ModelTests/ModelTests.swift index a07cb54..38f3b5e 100644 --- a/PPPC UtilityTests/ModelTests/ModelTests.swift +++ b/PPPC UtilityTests/ModelTests/ModelTests.swift @@ -43,16 +43,16 @@ class ModelTests: XCTestCase { func testGetExecutableBasedOnIdentifierAndCodeRequirement_BundleIdentifierType() { // given - let identifier = "com.example.App" - let codeRequirement = "testCodeRequirement" + let identifier = "com.example.App" + let codeRequirement = "testCodeRequirement" // when - let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) + let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) // then - XCTAssertEqual(executable.displayName, "App") - XCTAssertEqual(executable.codeRequirement, codeRequirement) - XCTAssertEqual(executable.iconPath, IconFilePath.application) + XCTAssertEqual(executable.displayName, "App") + XCTAssertEqual(executable.codeRequirement, codeRequirement) + XCTAssertEqual(executable.iconPath, IconFilePath.application) } func testGetExecutableBasedOnIdentifierAndCodeRequirement_PathIdentifierType() { @@ -71,16 +71,16 @@ class ModelTests: XCTestCase { func testGetExecutableFromComputerBasedOnIdentifier() { // given - let identifier = "com.apple.Safari" - let codeRequirement = "randomReq" + let identifier = "com.apple.Safari" + let codeRequirement = "randomReq" // when - let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) + let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) // then - XCTAssertEqual(executable.displayName, "Safari") - XCTAssertNotEqual(executable.iconPath, IconFilePath.application) - XCTAssertNotEqual(codeRequirement, executable.codeRequirement) + XCTAssertEqual(executable.displayName, "Safari") + XCTAssertNotEqual(executable.iconPath, IconFilePath.application) + XCTAssertNotEqual(codeRequirement, executable.codeRequirement) } func testGetExecutableFromSelectedExecutables() { @@ -189,10 +189,8 @@ class ModelTests: XCTestCase { // given let exe1 = Executable(identifier: "one", codeRequirement: "oneReq") let exe2 = Executable(identifier: "two", codeRequirement: "twoReq") - exe1.appleEvents = [AppleEventRule(source: exe1, destination: exe2, value: true)] exe2.policy.SystemPolicyAllFiles = "Allow" - model.selectedExecutables = [exe1, exe2] model.usingLegacyAllowKey = true @@ -209,7 +207,6 @@ class ModelTests: XCTestCase { XCTAssertNotNil(profile.uuid) XCTAssertEqual(1, profile.version) - // then check policy settings // then verify the payload content top level XCTAssertEqual(1, profile.content.count) profile.content.forEach { content in @@ -218,9 +215,8 @@ class ModelTests: XCTestCase { // then verify the services XCTAssertEqual(2, content.services.count) - let appleEvents = content.services["AppleEvents"] - XCTAssertNotNil(appleEvents) - let appleEventsPolicy = appleEvents?.first + let appleEventsPolicy = content.services["AppleEvents"]?.first + XCTAssertNotNil(appleEventsPolicy) XCTAssertEqual("one", appleEventsPolicy?.identifier) XCTAssertEqual("oneReq", appleEventsPolicy?.codeRequirement) XCTAssertEqual("bundleID", appleEventsPolicy?.identifierType) @@ -230,9 +226,8 @@ class ModelTests: XCTestCase { XCTAssertTrue(appleEventsPolicy?.allowed == true) XCTAssertNil(appleEventsPolicy?.authorization) - let allFiles = content.services["SystemPolicyAllFiles"] - XCTAssertNotNil(allFiles) - let allFilesPolicy = allFiles?.first + let allFilesPolicy = content.services["SystemPolicyAllFiles"]?.first + XCTAssertNotNil(allFilesPolicy) XCTAssertEqual("two", allFilesPolicy?.identifier) XCTAssertEqual("twoReq", allFilesPolicy?.codeRequirement) XCTAssertEqual("bundleID", allFilesPolicy?.identifierType) @@ -481,14 +476,18 @@ class ModelTests: XCTestCase { func testChangingFromAuthorizationKeyToLegacyAllowKeyWithMoreComplexVaues() { // given let allowStandard = TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue - let p1Settings = ["SystemPolicyAllFiles": "Allow", - "ListenEvent": allowStandard, - "ScreenCapture": "Deny", - "Camera": "Deny"] - - let p2Settings = ["SystemPolicyAllFiles": "Deny", - "ScreenCapture": allowStandard, - "Calendar": "Allow"] + let p1Settings = [ + "SystemPolicyAllFiles": "Allow", + "ListenEvent": allowStandard, + "ScreenCapture": "Deny", + "Camera": "Deny" + ] + + let p2Settings = [ + "SystemPolicyAllFiles": "Deny", + "ScreenCapture": allowStandard, + "Calendar": "Allow" + ] let builder = ModelBuilder().addExecutable(settings: p1Settings) model = builder.addExecutable(settings: p2Settings).build() model.usingLegacyAllowKey = false diff --git a/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift b/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift index 767cdae..3cd1315 100644 --- a/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift +++ b/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift @@ -49,7 +49,10 @@ class PPPCServicesManagerTests: XCTestCase { let actual = service.userHelp // then - XCTAssertEqual(actual, "Use to deny specified apps access to the camera.\n\nMDM Key: Camera\nRelated entitlements: [\"com.apple.developer.avfoundation.multitasking-camera-access\", \"com.apple.security.device.camera\"]") + XCTAssertEqual( + actual, + "Use to deny specified apps access to the camera.\n\nMDM Key: Camera\nRelated entitlements: [\"com.apple.developer.avfoundation.multitasking-camera-access\", \"com.apple.security.device.camera\"]" + ) } func testUserHelp_withoutEntitlements() throws { diff --git a/PPPC UtilityTests/ModelTests/SemanticVersionTests.swift b/PPPC UtilityTests/ModelTests/SemanticVersionTests.swift index 93e788c..c37c5e4 100644 --- a/PPPC UtilityTests/ModelTests/SemanticVersionTests.swift +++ b/PPPC UtilityTests/ModelTests/SemanticVersionTests.swift @@ -26,9 +26,10 @@ // import Foundation -@testable import PPPC_Utility import XCTest +@testable import PPPC_Utility + class SemanticVersionTests: XCTestCase { func testLessThan() { // given diff --git a/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift b/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift index 308decd..39e2798 100644 --- a/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift +++ b/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift @@ -11,17 +11,17 @@ import XCTest @testable import PPPC_Utility class JamfProAPIClientTests: XCTestCase { - func testOAuthTokenRequest() throws { - // given - let authManager = NetworkAuthManager(username: "", password: "") - let apiClient = JamfProAPIClient(serverUrlString: "https://something", tokenManager: authManager) + func testOAuthTokenRequest() throws { + // given + let authManager = NetworkAuthManager(username: "", password: "") + let apiClient = JamfProAPIClient(serverUrlString: "https://something", tokenManager: authManager) - // when - let request = try apiClient.oauthTokenRequest(clientId: "mine&yours", clientSecret: "foo bar") + // when + let request = try apiClient.oauthTokenRequest(clientId: "mine&yours", clientSecret: "foo bar") - // then - let body = try XCTUnwrap(request.httpBody) - let bodyString = String(data: body, encoding: .utf8) - XCTAssertEqual(bodyString, "grant_type=client_credentials&client_id=mine%26yours&client_secret=foo%20bar") - } + // then + let body = try XCTUnwrap(request.httpBody) + let bodyString = String(data: body, encoding: .utf8) + XCTAssertEqual(bodyString, "grant_type=client_credentials&client_id=mine%26yours&client_secret=foo%20bar") + } } diff --git a/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift b/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift index d54ddd4..f78eb03 100644 --- a/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift +++ b/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift @@ -43,11 +43,11 @@ class MockNetworking: Networking { throw error } - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - let expiration = try XCTUnwrap(formatter.date(from: "2950-06-22T22:05:58.81Z")) + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let expiration = try XCTUnwrap(formatter.date(from: "2950-06-22T22:05:58.81Z")) - return Token(value: "xyz", expiresAt: expiration) + return Token(value: "xyz", expiresAt: expiration) } } diff --git a/PPPC UtilityTests/NetworkingTests/TokenTests.swift b/PPPC UtilityTests/NetworkingTests/TokenTests.swift index 973e1d5..6ca3c6d 100644 --- a/PPPC UtilityTests/NetworkingTests/TokenTests.swift +++ b/PPPC UtilityTests/NetworkingTests/TokenTests.swift @@ -33,10 +33,10 @@ import XCTest class TokenTests: XCTestCase { func testPastIsNotValid() throws { // given - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - let expiration = try XCTUnwrap(formatter.date(from: "2021-06-22T22:05:58.81Z")) - let token = Token(value: "abc", expiresAt: expiration) + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let expiration = try XCTUnwrap(formatter.date(from: "2021-06-22T22:05:58.81Z")) + let token = Token(value: "abc", expiresAt: expiration) // when let valid = token.isValid @@ -47,9 +47,9 @@ class TokenTests: XCTestCase { func testFutureIsValid() throws { // given - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - let expiration = try XCTUnwrap(formatter.date(from: "2750-06-22T22:05:58.81Z")) + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let expiration = try XCTUnwrap(formatter.date(from: "2750-06-22T22:05:58.81Z")) let token = Token(value: "abc", expiresAt: expiration) // when @@ -59,67 +59,67 @@ class TokenTests: XCTestCase { XCTAssertTrue(valid) } - // MARK: - Decoding - - func testDecodeBasicAuthToken() throws { - // given - let jsonText = """ - { - "token": "abc", - "expires": "2750-06-22T22:05:58.81Z" - } - """ - let jsonData = try XCTUnwrap(jsonText.data(using: .utf8)) - let decoder = JSONDecoder() - - // when - let actual = try decoder.decode(Token.self, from: jsonData) - - // then - XCTAssertEqual(actual.value, "abc") - XCTAssertNotNil(actual.expiresAt) - XCTAssertTrue(actual.isValid) - } - - func testDecodeExpiredBasicAuthToken() throws { - // given - let jsonText = """ - { - "token": "abc", - "expires": "1970-10-24T22:05:58.81Z" - } - """ - let jsonData = try XCTUnwrap(jsonText.data(using: .utf8)) - let decoder = JSONDecoder() - - // when - let actual = try decoder.decode(Token.self, from: jsonData) - - // then - XCTAssertEqual(actual.value, "abc") - XCTAssertNotNil(actual.expiresAt) - XCTAssertFalse(actual.isValid) - } - - func testDecodeClientCredentialsAuthToken() throws { - // given - let jsonText = """ - { - "access_token": "abc", - "scope": "api-role:2", - "token_type": "Bearer", - "expires_in": 599 - } - """ - let jsonData = try XCTUnwrap(jsonText.data(using: .utf8)) - let decoder = JSONDecoder() - - // when - let actual = try decoder.decode(Token.self, from: jsonData) - - // then - XCTAssertEqual(actual.value, "abc") - XCTAssertNotNil(actual.expiresAt) - XCTAssertTrue(actual.isValid) - } + // MARK: - Decoding + + func testDecodeBasicAuthToken() throws { + // given + let jsonText = """ + { + "token": "abc", + "expires": "2750-06-22T22:05:58.81Z" + } + """ + let jsonData = try XCTUnwrap(jsonText.data(using: .utf8)) + let decoder = JSONDecoder() + + // when + let actual = try decoder.decode(Token.self, from: jsonData) + + // then + XCTAssertEqual(actual.value, "abc") + XCTAssertNotNil(actual.expiresAt) + XCTAssertTrue(actual.isValid) + } + + func testDecodeExpiredBasicAuthToken() throws { + // given + let jsonText = """ + { + "token": "abc", + "expires": "1970-10-24T22:05:58.81Z" + } + """ + let jsonData = try XCTUnwrap(jsonText.data(using: .utf8)) + let decoder = JSONDecoder() + + // when + let actual = try decoder.decode(Token.self, from: jsonData) + + // then + XCTAssertEqual(actual.value, "abc") + XCTAssertNotNil(actual.expiresAt) + XCTAssertFalse(actual.isValid) + } + + func testDecodeClientCredentialsAuthToken() throws { + // given + let jsonText = """ + { + "access_token": "abc", + "scope": "api-role:2", + "token_type": "Bearer", + "expires_in": 599 + } + """ + let jsonData = try XCTUnwrap(jsonText.data(using: .utf8)) + let decoder = JSONDecoder() + + // when + let actual = try decoder.decode(Token.self, from: jsonData) + + // then + XCTAssertEqual(actual.value, "abc") + XCTAssertNotNil(actual.expiresAt) + XCTAssertTrue(actual.isValid) + } } diff --git a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift index a70dc76..db7f9db 100644 --- a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift +++ b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift @@ -42,7 +42,8 @@ class TCCProfileImporterTests: XCTestCase { case .success: XCTFail("Malformed profile should not succeed") case .failure(let tccProfileError): - if case TCCProfileImportError.invalidProfileFile = tccProfileError { } else { + if case TCCProfileImportError.invalidProfileFile = tccProfileError { + } else { XCTFail("Expected invalidProfileFile error, got \(tccProfileError)") } } diff --git a/README.md b/README.md index 8354018..9992a35 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,13 @@ To upload the Privacy Preferences Policy Control Payload to Jamf Pro 10.7.0 and Signed and unsigned profiles can be imported. ![Import any profile](/Images/ImportProfile.png "Import profiles") + +## Development + +### Pre-commit Hook + +A pre-commit hook is available that automatically formats Swift files using `swift-format` before each commit. To install it, run: + +```bash +ln -sf ../../git-hooks/pre-commit.swift-format .git/hooks/pre-commit +``` diff --git a/Source/Extensions/ArrayExtensions.swift b/Source/Extensions/ArrayExtensions.swift index 8b96a40..5d5706b 100644 --- a/Source/Extensions/ArrayExtensions.swift +++ b/Source/Extensions/ArrayExtensions.swift @@ -26,6 +26,7 @@ // import Foundation + extension Array where Element: Equatable { mutating func appendIfNew(_ item: Element) { if !contains(item) { diff --git a/Source/Extensions/LoggerExtensions.swift b/Source/Extensions/LoggerExtensions.swift index b91e54d..aa7bf58 100644 --- a/Source/Extensions/LoggerExtensions.swift +++ b/Source/Extensions/LoggerExtensions.swift @@ -8,7 +8,7 @@ // This extension simplifies the logger instance creation by calling the bundle Id // and pre-declaring categories. Currently the predefined categories match the -// class name. +// class name. import OSLog diff --git a/Source/Extensions/TCCProfileExtensions.swift b/Source/Extensions/TCCProfileExtensions.swift index d60fdee..c45650f 100644 --- a/Source/Extensions/TCCProfileExtensions.swift +++ b/Source/Extensions/TCCProfileExtensions.swift @@ -27,9 +27,9 @@ import Foundation -public extension TCCProfile { +extension TCCProfile { - enum ParseError: Error { + public enum ParseError: Error { case failedToCreateDecoder } @@ -37,7 +37,7 @@ public extension TCCProfile { /// - Parameter profileData: The raw profile data (generally from a file read operation). This may be CMS encoded. /// - Returns: A ``TCCProfile`` instance. /// - Throws: Either a ``TCCProfile.ParseError`` or a `DecodingError`. - static func parse(from profileData: Data) throws -> TCCProfile { + public static func parse(from profileData: Data) throws -> TCCProfile { // The profile may be CMS encoded; let's try to decode it. guard let cmsDecoder = SwiftyCMSDecoder() else { throw ParseError.failedToCreateDecoder @@ -73,9 +73,10 @@ public extension TCCProfile { // converts it to the standard letter case. let conversionBlock = { (codingKey: CodingKey) in let requiredString = ">\(codingKey.stringValue)<" - newString = newString.replacingOccurrences(of: requiredString, - with: requiredString, - options: .caseInsensitive) + newString = newString.replacingOccurrences( + of: requiredString, + with: requiredString, + options: .caseInsensitive) } // Currently there are three model structs that are used to decode the profile. diff --git a/Source/Model/Executable.swift b/Source/Model/Executable.swift index 5eb2971..9e4cc25 100644 --- a/Source/Model/Executable.swift +++ b/Source/Model/Executable.swift @@ -75,7 +75,6 @@ class Executable: NSObject { } class Policy: NSObject { - // swiftlint:disable identifier_name @objc dynamic var AddressBook: String = "-" @objc dynamic var Calendar: String = "-" @objc dynamic var Reminders: String = "-" @@ -96,7 +95,6 @@ class Policy: NSObject { @objc dynamic var SystemPolicyDownloadsFolder: String = "-" @objc dynamic var SystemPolicyNetworkVolumes: String = "-" @objc dynamic var SystemPolicyRemovableVolumes: String = "-" - // swiftlint:enable identifier_name func allPolicyValues() -> [String] { let mirror = Mirror(reflecting: self) diff --git a/Source/Model/LoadExecutableError.swift b/Source/Model/LoadExecutableError.swift index 0234647..509942e 100644 --- a/Source/Model/LoadExecutableError.swift +++ b/Source/Model/LoadExecutableError.swift @@ -26,6 +26,7 @@ // import Foundation + public enum LoadExecutableError: Error { case identifierNotFound case resourceURLNotFound diff --git a/Source/Model/Model.swift b/Source/Model/Model.swift index 7600d15..8dc2315 100644 --- a/Source/Model/Model.swift +++ b/Source/Model/Model.swift @@ -115,7 +115,6 @@ extension Model { } // TODO - refactor this method so it isn't so complex - // swiftlint:disable:next cyclomatic_complexity func loadExecutable(url: URL, completion: @escaping LoadExecutableCompletion) { let executable = Executable() @@ -185,11 +184,11 @@ extension Model { for attr in mirroredServices.children { if let key = attr.label, let value = attr.value as? String { - if let policyToAppend = policyFromString(executable: executable, value: value) { - services[key] = services[key] ?? [] - services[key]?.append(policyToAppend) - } - } + if let policyToAppend = policyFromString(executable: executable, value: value) { + services[key] = services[key] ?? [] + services[key]?.append(policyToAppend) + } + } } executable.appleEvents.forEach { event in @@ -202,11 +201,12 @@ extension Model { } } - return TCCProfile(organization: organization, - identifier: identifier, - displayName: displayName, - payloadDescription: payloadDescription, - services: services) + return TCCProfile( + organization: organization, + identifier: identifier, + displayName: displayName, + payloadDescription: payloadDescription, + services: services) } func importProfile(tccProfile: TCCProfile) { @@ -223,7 +223,8 @@ extension Model { if key == ServicesKeys.appleEvents.rawValue { if let source = executable, let rIdentifier = policy.receiverIdentifier, - let rCodeRequirement = policy.receiverCodeRequirement { + let rCodeRequirement = policy.receiverCodeRequirement + { let destination = getExecutableFrom(identifier: rIdentifier, codeRequirement: rCodeRequirement) let allowed: Bool = (policy.allowed == true || policy.authorization == TCCPolicyAuthorizationValue.allow) let appleEvent = AppleEventRule(source: source, destination: destination, value: allowed) @@ -244,10 +245,11 @@ extension Model { } func policyFromString(executable: Executable, value: String, event: AppleEventRule? = nil) -> TCCPolicy? { - var policy = TCCPolicy(identifier: executable.identifier, - codeRequirement: executable.codeRequirement, - receiverIdentifier: event?.destination.identifier, - receiverCodeRequirement: event?.destination.codeRequirement) + var policy = TCCPolicy( + identifier: executable.identifier, + codeRequirement: executable.codeRequirement, + receiverIdentifier: event?.destination.identifier, + receiverCodeRequirement: event?.destination.codeRequirement) if usingLegacyAllowKey { switch value { case TCCProfileDisplayValue.allow.rawValue: @@ -274,8 +276,8 @@ extension Model { func getExecutablesFromAllPolicies(policies: [TCCPolicy]) { for tccPolicy in policies where getExecutableFromSelectedExecutables(bundleIdentifier: tccPolicy.identifier) == nil { - let executable = getExecutableFrom(identifier: tccPolicy.identifier, codeRequirement: tccPolicy.codeRequirement) - self.selectedExecutables.append(executable) + let executable = getExecutableFrom(identifier: tccPolicy.identifier, codeRequirement: tccPolicy.codeRequirement) + self.selectedExecutables.append(executable) } } @@ -301,14 +303,14 @@ extension Model { } private func findExecutableOnComputerUsing(bundleIdentifier: String, completion: @escaping LoadExecutableCompletion) { - var urlToLoad: URL? + var urlToLoad: URL? if bundleIdentifier.contains("/") { - urlToLoad = URL(string: "file://\(bundleIdentifier)") + urlToLoad = URL(string: "file://\(bundleIdentifier)") } else { - urlToLoad = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) + urlToLoad = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) } - if let fileURL = urlToLoad { + if let fileURL = urlToLoad { self.loadExecutable(url: fileURL) { result in switch result { case .success(let executable): diff --git a/Source/Model/TCCProfile.swift b/Source/Model/TCCProfile.swift index 329228e..8d67958 100644 --- a/Source/Model/TCCProfile.swift +++ b/Source/Model/TCCProfile.swift @@ -96,7 +96,6 @@ public struct TCCProfile: Codable { var version: Int var services: [String: [TCCPolicy]] - // swiftlint:disable:next nesting enum CodingKeys: String, CodingKey, CaseIterable { case payloadDescription = "PayloadDescription" case displayName = "PayloadDisplayName" @@ -130,14 +129,15 @@ public struct TCCProfile: Codable { case content = "PayloadContent" } init(organization: String, identifier: String, displayName: String, payloadDescription: String, services: [String: [TCCPolicy]]) { - let content = Content(payloadDescription: payloadDescription, - displayName: displayName, - identifier: identifier, - organization: organization, - type: "com.apple.TCC.configuration-profile-policy", - uuid: UUID().uuidString, - version: 1, - services: services) + let content = Content( + payloadDescription: payloadDescription, + displayName: displayName, + identifier: identifier, + organization: organization, + type: "com.apple.TCC.configuration-profile-policy", + uuid: UUID().uuidString, + version: 1, + services: services) self.version = 1 self.uuid = UUID().uuidString self.type = "Configuration" diff --git a/Source/Networking/JamfProAPIClient.swift b/Source/Networking/JamfProAPIClient.swift index 2d1d695..99ac649 100644 --- a/Source/Networking/JamfProAPIClient.swift +++ b/Source/Networking/JamfProAPIClient.swift @@ -31,45 +31,47 @@ class JamfProAPIClient: Networking { let applicationJson = "application/json" override func getBearerToken(authInfo: AuthenticationInfo) async throws -> Token { - switch authInfo { - case .basicAuth: - let endpoint = "api/v1/auth/token" - var request = try url(forEndpoint: endpoint) + switch authInfo { + case .basicAuth: + let endpoint = "api/v1/auth/token" + var request = try url(forEndpoint: endpoint) - request.httpMethod = "POST" - request.setValue(applicationJson, forHTTPHeaderField: "Accept") + request.httpMethod = "POST" + request.setValue(applicationJson, forHTTPHeaderField: "Accept") - return try await loadBasicAuthorized(request: request) - case .clientCreds(let id, let secret): - let request = try oauthTokenRequest(clientId: id, clientSecret: secret) + return try await loadBasicAuthorized(request: request) + case .clientCreds(let id, let secret): + let request = try oauthTokenRequest(clientId: id, clientSecret: secret) - return try await loadPreAuthorized(request: request) - } + return try await loadPreAuthorized(request: request) + } } - /// Creates the OAuth client credentials token request - /// - Parameters: - /// - clientId: The client ID - /// - clientSecret: The client secret - /// - Returns: A `URLRequest` that is ready to send to acquire an OAuth token. - func oauthTokenRequest(clientId: String, clientSecret: String) throws -> URLRequest { - let endpoint = "api/oauth/token" - var request = try url(forEndpoint: endpoint) + /// Creates the OAuth client credentials token request + /// - Parameters: + /// - clientId: The client ID + /// - clientSecret: The client secret + /// - Returns: A `URLRequest` that is ready to send to acquire an OAuth token. + func oauthTokenRequest(clientId: String, clientSecret: String) throws -> URLRequest { + let endpoint = "api/oauth/token" + var request = try url(forEndpoint: endpoint) - request.httpMethod = "POST" - request.setValue(applicationJson, forHTTPHeaderField: "Accept") + request.httpMethod = "POST" + request.setValue(applicationJson, forHTTPHeaderField: "Accept") - var components = URLComponents() - components.queryItems = [URLQueryItem(name: "grant_type", value: "client_credentials"), - URLQueryItem(name: "client_id", value: clientId), - URLQueryItem(name: "client_secret", value: clientSecret)] + var components = URLComponents() + components.queryItems = [ + URLQueryItem(name: "grant_type", value: "client_credentials"), + URLQueryItem(name: "client_id", value: clientId), + URLQueryItem(name: "client_secret", value: clientSecret) + ] - request.httpBody = components.percentEncodedQuery?.data(using: .utf8) + request.httpBody = components.percentEncodedQuery?.data(using: .utf8) - return request - } + return request + } - // MARK: - Requests with fallback auth + // MARK: - Requests with fallback auth /// Make a network request and decode the response using bearer auth if possible, falling back to basic auth if needed. /// - Parameter request: The `URLRequest` to make @@ -115,10 +117,10 @@ class JamfProAPIClient: Networking { // MARK: - Useful API endpoints - /// Reads the Jamf Pro organization name - /// - /// Requires "Read Activation Code" permission in the API - /// - Parameter profileData: The prepared profile data + /// Reads the Jamf Pro organization name + /// + /// Requires "Read Activation Code" permission in the API + /// - Parameter profileData: The prepared profile data func getOrganizationName() async throws -> String { let endpoint = "JSSResource/activationcode" var request = try url(forEndpoint: endpoint) @@ -132,10 +134,10 @@ class JamfProAPIClient: Networking { return info.activationCode.organizationName } - /// Gets the Jamf Pro version - /// - /// No specific permissions required. - /// - Returns: The Jamf Pro server version + /// Gets the Jamf Pro version + /// + /// No specific permissions required. + /// - Returns: The Jamf Pro server version func getJamfProVersion() async throws -> JamfProVersion { let endpoint = "api/v1/jamf-pro-version" var request = try url(forEndpoint: endpoint) @@ -156,10 +158,10 @@ class JamfProAPIClient: Networking { return info } - /// Uploads a computer configuration profile - /// - /// Requires "Create macOS Configuration Profiles" permission in the API - /// - Parameter profileData: The prepared profile data + /// Uploads a computer configuration profile + /// + /// Requires "Create macOS Configuration Profiles" permission in the API + /// - Parameter profileData: The prepared profile data func upload(computerConfigProfile profileData: Data) async throws { let endpoint = "JSSResource/osxconfigurationprofiles" var request = try url(forEndpoint: endpoint) diff --git a/Source/Networking/JamfProAPITypes.swift b/Source/Networking/JamfProAPITypes.swift index 43d4e1e..7b751ac 100644 --- a/Source/Networking/JamfProAPITypes.swift +++ b/Source/Networking/JamfProAPITypes.swift @@ -31,9 +31,10 @@ struct JamfProVersion: Decodable { let version: String func mainVersionInfo() -> String { - return String(version.prefix { character in - return (character.isNumber || character == ".") - }) + return String( + version.prefix { character in + return (character.isNumber || character == ".") + }) } func semantic() -> SemanticVersion { @@ -47,14 +48,16 @@ struct JamfProVersion: Decodable { init(fromHTMLString text: String?) throws { // we take version from HTML response body if let text = text, - let startRange = text.range(of: "? @@ -56,21 +56,22 @@ actor NetworkAuthManager { private var supportsBearerAuth = true init(username: String, password: String) { - authInfo = .basicAuth(username: username, password: password) + authInfo = .basicAuth(username: username, password: password) } - init(clientId: String, clientSecret: String) { - authInfo = .clientCreds(id: clientId, secret: clientSecret) - } + init(clientId: String, clientSecret: String) { + authInfo = .clientCreds(id: clientId, secret: clientSecret) + } - func validToken(networking: Networking) async throws -> Token { + func validToken(networking: Networking) async throws -> Token { if let task = refreshTask { // A refresh is already running; we'll use those results when ready. return try await task.value } if let token = currentToken, - token.isValid { + token.isValid + { return token } @@ -88,7 +89,7 @@ actor NetworkAuthManager { defer { refreshTask = nil } do { - let newToken = try await networking.getBearerToken(authInfo: authInfo) + let newToken = try await networking.getBearerToken(authInfo: authInfo) currentToken = newToken return newToken } catch NetworkingError.serverResponse(let responseCode, _) where responseCode == 404 { @@ -120,8 +121,9 @@ actor NetworkAuthManager { /// This doesn't mutate any state and only accesses `let` constants so it doesn't need to be actor isolated. /// - Returns: The encoded data string for use with Basic Auth. nonisolated func basicAuthString() throws -> String { - guard case .basicAuth(let username, let password) = authInfo, - !username.isEmpty && !password.isEmpty else { + guard case .basicAuth(let username, let password) = authInfo, + !username.isEmpty && !password.isEmpty + else { throw AuthError.invalidUsernamePassword } return Data("\(username):\(password)".utf8).base64EncodedString() diff --git a/Source/Networking/Networking.swift b/Source/Networking/Networking.swift index 2c45d10..7c6b850 100644 --- a/Source/Networking/Networking.swift +++ b/Source/Networking/Networking.swift @@ -71,27 +71,27 @@ class Networking { return URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 45.0) } - /// Sends a `URLRequest` and decodes a response to an endpoint. - /// - Parameter request: A request that already has authorization info. - /// - Returns: The result. - func loadPreAuthorized(request: URLRequest) async throws -> T { - let (data, urlResponse) = try await URLSession.shared.data(for: request) - - if let httpResponse = urlResponse as? HTTPURLResponse { - if httpResponse.statusCode == 401 { - throw AuthError.invalidUsernamePassword - } else if !(200...299).contains(httpResponse.statusCode) { - throw NetworkingError.serverResponse(httpResponse.statusCode, request.url?.absoluteString ?? "") - } - } - - let decoder = JSONDecoder() - let response = try decoder.decode(T.self, from: data) - - return response - } - - /// Sends a `URLRequest` and decodes a response to a Basic Auth protected endpoint. + /// Sends a `URLRequest` and decodes a response to an endpoint. + /// - Parameter request: A request that already has authorization info. + /// - Returns: The result. + func loadPreAuthorized(request: URLRequest) async throws -> T { + let (data, urlResponse) = try await URLSession.shared.data(for: request) + + if let httpResponse = urlResponse as? HTTPURLResponse { + if httpResponse.statusCode == 401 { + throw AuthError.invalidUsernamePassword + } else if !(200...299).contains(httpResponse.statusCode) { + throw NetworkingError.serverResponse(httpResponse.statusCode, request.url?.absoluteString ?? "") + } + } + + let decoder = JSONDecoder() + let response = try decoder.decode(T.self, from: data) + + return response + } + + /// Sends a `URLRequest` and decodes a response to a Basic Auth protected endpoint. /// - Parameter request: A request that does not yet include authorization info. /// - Returns: The result. func loadBasicAuthorized(request: URLRequest) async throws -> T { diff --git a/Source/Networking/Token.swift b/Source/Networking/Token.swift index 3501fe9..12b68b9 100644 --- a/Source/Networking/Token.swift +++ b/Source/Networking/Token.swift @@ -13,49 +13,49 @@ import Foundation /// and the older basic-auth-based flow. struct Token: Decodable { let value: String - let expiresAt: Date? + let expiresAt: Date? var isValid: Bool { - if let expiration = expiresAt { - return expiration > Date() - } + if let expiration = expiresAt { + return expiration > Date() + } - return true + return true } - enum OAuthTokenCodingKeys: String, CodingKey { - case value = "access_token" - case expire = "expires_in" - } + enum OAuthTokenCodingKeys: String, CodingKey { + case value = "access_token" + case expire = "expires_in" + } - enum BasicAuthCodingKeys: String, CodingKey { + enum BasicAuthCodingKeys: String, CodingKey { case value = "token" case expireTime = "expires" } - init(from decoder: Decoder) throws { - // First try to decode with oauth client credentials token response - let container = try decoder.container(keyedBy: OAuthTokenCodingKeys.self) - let possibleValue = try? container.decode(String.self, forKey: .value) - if let value = possibleValue { - self.value = value - let expireIn = try container.decode(Double.self, forKey: .expire) - self.expiresAt = Date().addingTimeInterval(expireIn) - return - } - - // If that fails try to decode with basic auth token response - let container1 = try decoder.container(keyedBy: BasicAuthCodingKeys.self) - self.value = try container1.decode(String.self, forKey: .value) - let expireTime = try container1.decode(String.self, forKey: .expireTime) - - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - self.expiresAt = formatter.date(from: expireTime) - } - - init(value: String, expiresAt: Date) { - self.value = value - self.expiresAt = expiresAt - } + init(from decoder: Decoder) throws { + // First try to decode with oauth client credentials token response + let container = try decoder.container(keyedBy: OAuthTokenCodingKeys.self) + let possibleValue = try? container.decode(String.self, forKey: .value) + if let value = possibleValue { + self.value = value + let expireIn = try container.decode(Double.self, forKey: .expire) + self.expiresAt = Date().addingTimeInterval(expireIn) + return + } + + // If that fails try to decode with basic auth token response + let container1 = try decoder.container(keyedBy: BasicAuthCodingKeys.self) + self.value = try container1.decode(String.self, forKey: .value) + let expireTime = try container1.decode(String.self, forKey: .expireTime) + + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + self.expiresAt = formatter.date(from: expireTime) + } + + init(value: String, expiresAt: Date) { + self.value = value + self.expiresAt = expiresAt + } } diff --git a/Source/Networking/URLSessionAsyncCompatibility.swift b/Source/Networking/URLSessionAsyncCompatibility.swift index 5f38e53..c7c5c34 100644 --- a/Source/Networking/URLSessionAsyncCompatibility.swift +++ b/Source/Networking/URLSessionAsyncCompatibility.swift @@ -10,13 +10,13 @@ import Foundation @available(macOS, deprecated: 12.0, message: "AsyncCompatibilityKit is only useful when targeting macOS versions earlier than 12") -public extension URLSession { +extension URLSession { /// Start a data task with a URL using async/await. /// - parameter url: The URL to send a request to. /// - returns: A tuple containing the binary `Data` that was downloaded, /// as well as a `URLResponse` representing the server's response. /// - throws: Any error encountered while performing the data task. - func data(from url: URL) async throws -> (Data, URLResponse) { + public func data(from url: URL) async throws -> (Data, URLResponse) { try await data(for: URLRequest(url: url)) } @@ -25,27 +25,27 @@ public extension URLSession { /// - returns: A tuple containing the binary `Data` that was downloaded, /// as well as a `URLResponse` representing the server's response. /// - throws: Any error encountered while performing the data task. - func data(for request: URLRequest) async throws -> (Data, URLResponse) { + public func data(for request: URLRequest) async throws -> (Data, URLResponse) { var dataTask: URLSessionDataTask? let onCancel = { dataTask?.cancel() } return try await withTaskCancellationHandler( - operation: { - try await withCheckedThrowingContinuation { continuation in - dataTask = self.dataTask(with: request) { data, response, error in - guard let data = data, let response = response else { - let error = error ?? URLError(.badServerResponse) - return continuation.resume(throwing: error) - } + operation: { + try await withCheckedThrowingContinuation { continuation in + dataTask = self.dataTask(with: request) { data, response, error in + guard let data = data, let response = response else { + let error = error ?? URLError(.badServerResponse) + return continuation.resume(throwing: error) + } - continuation.resume(returning: (data, response)) - } + continuation.resume(returning: (data, response)) + } - dataTask?.resume() - } - }, - onCancel: { - onCancel() + dataTask?.resume() + } + }, + onCancel: { + onCancel() } ) } diff --git a/Source/Networking/UploadManager.swift b/Source/Networking/UploadManager.swift index e7d3246..794dac9 100644 --- a/Source/Networking/UploadManager.swift +++ b/Source/Networking/UploadManager.swift @@ -10,74 +10,74 @@ import Foundation import OSLog struct UploadManager { - let serverURL: String + let serverURL: String let logger = Logger.UploadManager - struct VerificationInfo { - let mustSign: Bool - let organization: String - } + struct VerificationInfo { + let mustSign: Bool + let organization: String + } - enum VerificationError: Error { - case anyError(String) - } + enum VerificationError: Error { + case anyError(String) + } - func verifyConnection(authManager: NetworkAuthManager, completionHandler: @escaping (Result) -> Void) { + func verifyConnection(authManager: NetworkAuthManager, completionHandler: @escaping (Result) -> Void) { logger.info("Checking connection to Jamf Pro server") - Task { - let networking = JamfProAPIClient(serverUrlString: serverURL, tokenManager: authManager) - let result: Result + Task { + let networking = JamfProAPIClient(serverUrlString: serverURL, tokenManager: authManager) + let result: Result - do { - let version = try await networking.getJamfProVersion() + do { + let version = try await networking.getJamfProVersion() - // Must sign if Jamf Pro is less than v10.7.1 - let mustSign = (version.semantic() < SemanticVersion(major: 10, minor: 7, patch: 1)) + // Must sign if Jamf Pro is less than v10.7.1 + let mustSign = (version.semantic() < SemanticVersion(major: 10, minor: 7, patch: 1)) - let orgName = try await networking.getOrganizationName() + let orgName = try await networking.getOrganizationName() - result = .success(VerificationInfo(mustSign: mustSign, organization: orgName)) - } catch is AuthError { + result = .success(VerificationInfo(mustSign: mustSign, organization: orgName)) + } catch is AuthError { logger.error("Invalid credentials.") - result = .failure(VerificationError.anyError("Invalid credentials.")) - } catch { + result = .failure(VerificationError.anyError("Invalid credentials.")) + } catch { logger.error("Jamf Pro server is unavailable.") - result = .failure(VerificationError.anyError("Jamf Pro server is unavailable.")) - } + result = .failure(VerificationError.anyError("Jamf Pro server is unavailable.")) + } - completionHandler(result) - } - } + completionHandler(result) + } + } - func upload(profile: TCCProfile, authMgr: NetworkAuthManager, siteInfo: (String, String)?, signingIdentity: SigningIdentity?, completionHandler: @escaping (Error?) -> Void) { + func upload(profile: TCCProfile, authMgr: NetworkAuthManager, siteInfo: (String, String)?, signingIdentity: SigningIdentity?, completionHandler: @escaping (Error?) -> Void) { logger.info("Uploading profile: \(profile.displayName, privacy: .public)") - let networking = JamfProAPIClient(serverUrlString: serverURL, tokenManager: authMgr) - Task { - let success: Error? - var identity: SecIdentity? - if let signingIdentity = signingIdentity { + let networking = JamfProAPIClient(serverUrlString: serverURL, tokenManager: authMgr) + Task { + let success: Error? + var identity: SecIdentity? + if let signingIdentity = signingIdentity { logger.info("Signing profile with \(signingIdentity.displayName)") - identity = signingIdentity.reference - } + identity = signingIdentity.reference + } - do { - let profileData = try profile.jamfProAPIData(signingIdentity: identity, site: siteInfo) + do { + let profileData = try profile.jamfProAPIData(signingIdentity: identity, site: siteInfo) - _ = try await networking.upload(computerConfigProfile: profileData) + _ = try await networking.upload(computerConfigProfile: profileData) - success = nil + success = nil logger.info("Uploaded successfully") - } catch { + } catch { logger.error("Error creating or uploading profile: \(error.localizedDescription)") - success = error - } - - DispatchQueue.main.async { - completionHandler(success) - } - } - } + success = error + } + + DispatchQueue.main.async { + completionHandler(success) + } + } + } } diff --git a/Source/SecurityWrapper.swift b/Source/SecurityWrapper.swift index ad83621..2de002c 100644 --- a/Source/SecurityWrapper.swift +++ b/Source/SecurityWrapper.swift @@ -38,36 +38,37 @@ struct SecurityWrapper { } static func saveCredentials(username: String, password: String, server: String) throws { - let haversack = Haversack() - let item = InternetPasswordEntity() - item.server = server - item.account = username - item.passwordData = password.data(using: .utf8) + let haversack = Haversack() + let item = InternetPasswordEntity() + item.server = server + item.account = username + item.passwordData = password.data(using: .utf8) - try haversack.save(item, itemSecurity: .standard, updateExisting: true) + try haversack.save(item, itemSecurity: .standard, updateExisting: true) } static func removeCredentials(server: String, username: String) throws { - let haversack = Haversack() - let query = InternetPasswordQuery(server: server) - .matching(account: username) + let haversack = Haversack() + let query = InternetPasswordQuery(server: server) + .matching(account: username) - try haversack.delete(where: query, treatNotFoundAsSuccess: true) + try haversack.delete(where: query, treatNotFoundAsSuccess: true) } static func loadCredentials(server: String) throws -> (username: String, password: String)? { - let haversack = Haversack() - let query = InternetPasswordQuery(server: server) - .returning([.attributes, .data]) - - if let item = try? haversack.first(where: query), - let username = item.account, - let passwordData = item.passwordData, - let password = String(data: passwordData, encoding: .utf8) { - return (username: username, password: password) - } - - return nil + let haversack = Haversack() + let query = InternetPasswordQuery(server: server) + .returning([.attributes, .data]) + + if let item = try? haversack.first(where: query), + let username = item.account, + let passwordData = item.passwordData, + let password = String(data: passwordData, encoding: .utf8) + { + return (username: username, password: password) + } + + return nil } static func copyDesignatedRequirement(url: URL) throws -> String { @@ -97,20 +98,21 @@ struct SecurityWrapper { } static func loadSigningIdentities() throws -> [SigningIdentity] { - let haversack = Haversack() - let query = IdentityQuery().matching(mustBeValidOnDate: Date()).returning(.reference) + let haversack = Haversack() + let query = IdentityQuery().matching(mustBeValidOnDate: Date()).returning(.reference) - let identities = try haversack.search(where: query) + let identities = try haversack.search(where: query) - return identities.compactMap { - guard let secIdentity = $0.reference else { - return nil - } + return identities.compactMap { + guard let secIdentity = $0.reference else { + return nil + } - let name = try? getCertificateCommonName(for: secIdentity) - return SigningIdentity(name: name ?? "Unknown \(secIdentity.hashValue)", - reference: secIdentity) - } + let name = try? getCertificateCommonName(for: secIdentity) + return SigningIdentity( + name: name ?? "Unknown \(secIdentity.hashValue)", + reference: secIdentity) + } } static func getCertificateCommonName(for identity: SecIdentity) throws -> String { diff --git a/Source/SwiftUI/UploadInfoView.swift b/Source/SwiftUI/UploadInfoView.swift index 18cdc98..c37bfc3 100644 --- a/Source/SwiftUI/UploadInfoView.swift +++ b/Source/SwiftUI/UploadInfoView.swift @@ -9,351 +9,358 @@ import OSLog import SwiftUI struct UploadInfoView: View { - /// The signing identities available to be used. - let signingIdentities: [SigningIdentity] - /// Function to call when this view needs to be removed - let dismissAction: (() -> Void)? - - // Communicate this info to the user - @State private var warningInfo: String? - @State private var networkOperationInfo: String? - /// Must sign the profile if Jamf Pro is less than v10.7.1 - @State private var mustSign = false - /// The hash of connection info that has been verified with a succesful connection - @State private var verifiedConnectionHash: Int = 0 - - // MARK: User entry fields - @AppStorage("jamfProServer") private var serverURL = "https://" - @AppStorage("organization") private var organization = "" - @AppStorage("authType") private var authType = AuthenticationType.clientCredentials - - @State private var username = "" - @State private var password = "" - @State private var saveToKeychain: Bool = true - @State private var payloadName = "" - @State private var payloadId = UUID().uuidString - @State private var payloadDescription = "" - @State private var signingId: SigningIdentity? - @State private var useSite: Bool = false - @State private var siteId: Int = -1 - @State private var siteName: String = "" - - let logger = Logger.UploadInfoView - - /// The type of authentication the user wants to use. - /// - /// `String` type so it can be saved with `@AppStorage` above - enum AuthenticationType: String { - case basicAuth - case clientCredentials - } - - let intFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .none - return formatter - }() - - var body: some View { - VStack { - Form { - TextField("Jamf Pro Server *:", text: $serverURL) - Picker("Authorization Type:", selection: $authType) { - Text("Basic/Bearer Auth").tag(AuthenticationType.basicAuth) - Text("Client Credentials (v10.49+):").tag(AuthenticationType.clientCredentials) - } - TextField(authType == .basicAuth ? "Username *:" : "Client ID *:", text: $username) - SecureField(authType == .basicAuth ? "Password *:" : "Client Secret *:", text: $password) - - HStack { - Toggle("Save in Keychain", isOn: $saveToKeychain) - .help("Store the username & password or client id & secret in the login keychain") - if verifiedConnection { - Spacer() - Text("✔️ Verified") - .font(.footnote) - } - } - Divider() - .padding(.vertical) - - TextField("Organization *:", text: $organization) - TextField("Payload Name *:", text: $payloadName) - TextField("Payload Identifier *:", text: $payloadId) - TextField("Payload Description:", text: $payloadDescription) - Picker("Signing Identity:", selection: $signingId) { - Text("Profile signed by server").tag(nil as SigningIdentity?) - ForEach(signingIdentities, id: \.self) { identity in - Text(identity.displayName).tag(identity) - } - } - .disabled(!mustSign) - Toggle("Use Site", isOn: $useSite) - TextField("Site ID", value: $siteId, formatter: intFormatter) - .disabled(!useSite) - TextField("Site Name", text: $siteName) - .disabled(!useSite) - } - .padding(.bottom) - - if let warning = warningInfo { - Text(warning) - .font(.headline) - .foregroundColor(.red) - } - if let networkInfo = networkOperationInfo { - HStack { - Text(networkInfo) - .font(.headline) - ProgressView() - .padding(.leading) - } - } - - HStack { - Spacer() - - Button("Cancel") { - dismissView() - } - .keyboardShortcut(.cancelAction) - - Button(verifiedConnection ? "Upload" : "Check connection") { - if verifiedConnection { - performUpload() - } else { - verifyConnection() - } - } - .keyboardShortcut(.defaultAction) - .disabled(!buttonEnabled()) - } - } - .padding() - .frame(minWidth: 450) - .background(Color(.windowBackgroundColor)) - .onAppear { - // Load keychain values - if let creds = try? SecurityWrapper.loadCredentials(server: serverURL) { - username = creds.username - password = creds.password - } - - // Use model payload values if it was imported - if let tccProfile = Model.shared.importedTCCProfile { - organization = tccProfile.organization - payloadName = tccProfile.displayName - payloadDescription = tccProfile.payloadDescription - payloadId = tccProfile.identifier - } - } - } - - /// Creates a hash of the currently entered connection info - var hashOfConnectionInfo: Int { - var hasher = Hasher() - hasher.combine(serverURL) - hasher.combine(username) - hasher.combine(password) - hasher.combine(authType) - return hasher.finalize() - } - - /// Compare the last verified connection hash with the current hash of connection info - var verifiedConnection: Bool { - verifiedConnectionHash == hashOfConnectionInfo - } - - func buttonEnabled() -> Bool { - if verifiedConnection { - return payloadInfoPassesValidation() - } - return connectionInfoPassesValidation() - } - - private func warning(_ info: StaticString, shouldDisplay: Bool) { - if shouldDisplay { + /// The signing identities available to be used. + let signingIdentities: [SigningIdentity] + /// Function to call when this view needs to be removed + let dismissAction: (() -> Void)? + + // Communicate this info to the user + @State private var warningInfo: String? + @State private var networkOperationInfo: String? + /// Must sign the profile if Jamf Pro is less than v10.7.1 + @State private var mustSign = false + /// The hash of connection info that has been verified with a succesful connection + @State private var verifiedConnectionHash: Int = 0 + + // MARK: User entry fields + @AppStorage("jamfProServer") private var serverURL = "https://" + @AppStorage("organization") private var organization = "" + @AppStorage("authType") private var authType = AuthenticationType.clientCredentials + + @State private var username = "" + @State private var password = "" + @State private var saveToKeychain: Bool = true + @State private var payloadName = "" + @State private var payloadId = UUID().uuidString + @State private var payloadDescription = "" + @State private var signingId: SigningIdentity? + @State private var useSite: Bool = false + @State private var siteId: Int = -1 + @State private var siteName: String = "" + + let logger = Logger.UploadInfoView + + /// The type of authentication the user wants to use. + /// + /// `String` type so it can be saved with `@AppStorage` above + enum AuthenticationType: String { + case basicAuth + case clientCredentials + } + + let intFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .none + return formatter + }() + + var body: some View { + VStack { + Form { + TextField("Jamf Pro Server *:", text: $serverURL) + Picker("Authorization Type:", selection: $authType) { + Text("Basic/Bearer Auth").tag(AuthenticationType.basicAuth) + Text("Client Credentials (v10.49+):").tag(AuthenticationType.clientCredentials) + } + TextField(authType == .basicAuth ? "Username *:" : "Client ID *:", text: $username) + SecureField(authType == .basicAuth ? "Password *:" : "Client Secret *:", text: $password) + + HStack { + Toggle("Save in Keychain", isOn: $saveToKeychain) + .help("Store the username & password or client id & secret in the login keychain") + if verifiedConnection { + Spacer() + Text("✔️ Verified") + .font(.footnote) + } + } + Divider() + .padding(.vertical) + + TextField("Organization *:", text: $organization) + TextField("Payload Name *:", text: $payloadName) + TextField("Payload Identifier *:", text: $payloadId) + TextField("Payload Description:", text: $payloadDescription) + Picker("Signing Identity:", selection: $signingId) { + Text("Profile signed by server").tag(nil as SigningIdentity?) + ForEach(signingIdentities, id: \.self) { identity in + Text(identity.displayName).tag(identity) + } + } + .disabled(!mustSign) + Toggle("Use Site", isOn: $useSite) + TextField("Site ID", value: $siteId, formatter: intFormatter) + .disabled(!useSite) + TextField("Site Name", text: $siteName) + .disabled(!useSite) + } + .padding(.bottom) + + if let warning = warningInfo { + Text(warning) + .font(.headline) + .foregroundColor(.red) + } + if let networkInfo = networkOperationInfo { + HStack { + Text(networkInfo) + .font(.headline) + ProgressView() + .padding(.leading) + } + } + + HStack { + Spacer() + + Button("Cancel") { + dismissView() + } + .keyboardShortcut(.cancelAction) + + Button(verifiedConnection ? "Upload" : "Check connection") { + if verifiedConnection { + performUpload() + } else { + verifyConnection() + } + } + .keyboardShortcut(.defaultAction) + .disabled(!buttonEnabled()) + } + } + .padding() + .frame(minWidth: 450) + .background(Color(.windowBackgroundColor)) + .onAppear { + // Load keychain values + if let creds = try? SecurityWrapper.loadCredentials(server: serverURL) { + username = creds.username + password = creds.password + } + + // Use model payload values if it was imported + if let tccProfile = Model.shared.importedTCCProfile { + organization = tccProfile.organization + payloadName = tccProfile.displayName + payloadDescription = tccProfile.payloadDescription + payloadId = tccProfile.identifier + } + } + } + + /// Creates a hash of the currently entered connection info + var hashOfConnectionInfo: Int { + var hasher = Hasher() + hasher.combine(serverURL) + hasher.combine(username) + hasher.combine(password) + hasher.combine(authType) + return hasher.finalize() + } + + /// Compare the last verified connection hash with the current hash of connection info + var verifiedConnection: Bool { + verifiedConnectionHash == hashOfConnectionInfo + } + + func buttonEnabled() -> Bool { + if verifiedConnection { + return payloadInfoPassesValidation() + } + return connectionInfoPassesValidation() + } + + private func warning(_ info: StaticString, shouldDisplay: Bool) { + if shouldDisplay { logger.info("\(info)") - warningInfo = "\(info)" - } - } - - /// Does some simple validation of the user-entered connection info - /// - /// The `setWarningInfo` parameter is optional, and should only be set to `true` during - /// actions triggered by the user. This function can be called with `false` (or no parameters) - /// from SwiftUI's `body` function to enable/disable controls. - /// - Parameter setWarningInfo: Whether to set the warning text so the user knows something needs to be updated. Default is `false`. - /// - Returns: True if the user entered connection info passes simple local validation - func connectionInfoPassesValidation(setWarningInfo: Bool = false) -> Bool { - guard !serverURL.isEmpty else { - warning("Server URL not set", shouldDisplay: setWarningInfo) - // Future on macOS 12+: focus on serverURL field - return false - } - - guard let url = URL(string: serverURL), - url.scheme == "http" || url.scheme == "https" else { - warning("Invalid Jamf Pro Server URL", shouldDisplay: setWarningInfo) - // Future on macOS 12+: focus on serverURL field - return false - } - - if authType == .basicAuth { - guard !username.isEmpty, !password.isEmpty else { - warning("Username or password not set", shouldDisplay: setWarningInfo) - // Future on macOS 12+: focus on username or password field - return false - } - - guard username.firstIndex(of: ":") == nil else { - warning("Username cannot contain a colon", shouldDisplay: setWarningInfo) - // Future on macOS 12+: focus on username field - return false - } - } else { - guard !username.isEmpty, !password.isEmpty else { - warning("Client ID or secret not set", shouldDisplay: setWarningInfo) - // Future on macOS 12+: focus on username or password field - return false - } - } - - if setWarningInfo { - warningInfo = nil - } - return true - } - - /// Does some simple validation of the user-entered payload info - /// - /// The `setWarningInfo` parameter is optional, and should only be set to `true` during - /// actions triggered by the user. This function can be called with `false` (or no parameters) - /// from SwiftUI's `body` function to enable/disable controls. - /// - Parameter setWarningInfo: Whether to set the warning text so the user knows something needs to be updated. Default is `false`. - /// - Returns: True if the user entered payload info passes simple local validation - func payloadInfoPassesValidation(setWarningInfo: Bool = false) -> Bool { - guard !organization.isEmpty else { - warning("Must provide an organization name", shouldDisplay: setWarningInfo) - // Future on macOS 12+: focus on organization field - return false - } - - guard !payloadId.isEmpty else { - warning("Must provide a payload identifier", shouldDisplay: setWarningInfo) - // Future on macOS 12+: focus on payload ID field - return false - } - - guard !payloadName.isEmpty else { - warning("Must provide a payload name", shouldDisplay: setWarningInfo) - // Future on macOS 12+: focus on payloadName field - return false - } - - guard useSite == false || (useSite == true && siteId != -1 && !siteName.isEmpty) else { - warning("Must provide both an ID and name for the site", shouldDisplay: setWarningInfo) - // Future on macOS 12+: focus on siteId or siteName field - return false - } - - if setWarningInfo { - warningInfo = nil - } - return true - } - - func makeAuthManager() -> NetworkAuthManager { - if authType == .basicAuth { - return NetworkAuthManager(username: username, password: password) - } - - return NetworkAuthManager(clientId: username, clientSecret: password) - } - - func verifyConnection() { - guard connectionInfoPassesValidation(setWarningInfo: true) else { - return - } - - networkOperationInfo = "Checking Jamf Pro server" - - let uploadMgr = UploadManager(serverURL: serverURL) - uploadMgr.verifyConnection(authManager: makeAuthManager()) { result in - if case .success(let success) = result { - mustSign = success.mustSign - organization = success.organization - verifiedConnectionHash = hashOfConnectionInfo - if saveToKeychain { - do { - try SecurityWrapper.saveCredentials(username: username, - password: password, - server: serverURL) - } catch { + warningInfo = "\(info)" + } + } + + /// Does some simple validation of the user-entered connection info + /// + /// The `setWarningInfo` parameter is optional, and should only be set to `true` during + /// actions triggered by the user. This function can be called with `false` (or no parameters) + /// from SwiftUI's `body` function to enable/disable controls. + /// - Parameter setWarningInfo: Whether to set the warning text so the user knows something needs to be updated. Default is `false`. + /// - Returns: True if the user entered connection info passes simple local validation + func connectionInfoPassesValidation(setWarningInfo: Bool = false) -> Bool { + guard !serverURL.isEmpty else { + warning("Server URL not set", shouldDisplay: setWarningInfo) + // Future on macOS 12+: focus on serverURL field + return false + } + + guard let url = URL(string: serverURL), + url.scheme == "http" || url.scheme == "https" + else { + warning("Invalid Jamf Pro Server URL", shouldDisplay: setWarningInfo) + // Future on macOS 12+: focus on serverURL field + return false + } + + if authType == .basicAuth { + guard !username.isEmpty, !password.isEmpty else { + warning("Username or password not set", shouldDisplay: setWarningInfo) + // Future on macOS 12+: focus on username or password field + return false + } + + guard username.firstIndex(of: ":") == nil else { + warning("Username cannot contain a colon", shouldDisplay: setWarningInfo) + // Future on macOS 12+: focus on username field + return false + } + } else { + guard !username.isEmpty, !password.isEmpty else { + warning("Client ID or secret not set", shouldDisplay: setWarningInfo) + // Future on macOS 12+: focus on username or password field + return false + } + } + + if setWarningInfo { + warningInfo = nil + } + return true + } + + /// Does some simple validation of the user-entered payload info + /// + /// The `setWarningInfo` parameter is optional, and should only be set to `true` during + /// actions triggered by the user. This function can be called with `false` (or no parameters) + /// from SwiftUI's `body` function to enable/disable controls. + /// - Parameter setWarningInfo: Whether to set the warning text so the user knows something needs to be updated. Default is `false`. + /// - Returns: True if the user entered payload info passes simple local validation + func payloadInfoPassesValidation(setWarningInfo: Bool = false) -> Bool { + guard !organization.isEmpty else { + warning("Must provide an organization name", shouldDisplay: setWarningInfo) + // Future on macOS 12+: focus on organization field + return false + } + + guard !payloadId.isEmpty else { + warning("Must provide a payload identifier", shouldDisplay: setWarningInfo) + // Future on macOS 12+: focus on payload ID field + return false + } + + guard !payloadName.isEmpty else { + warning("Must provide a payload name", shouldDisplay: setWarningInfo) + // Future on macOS 12+: focus on payloadName field + return false + } + + guard useSite == false || (useSite == true && siteId != -1 && !siteName.isEmpty) else { + warning("Must provide both an ID and name for the site", shouldDisplay: setWarningInfo) + // Future on macOS 12+: focus on siteId or siteName field + return false + } + + if setWarningInfo { + warningInfo = nil + } + return true + } + + func makeAuthManager() -> NetworkAuthManager { + if authType == .basicAuth { + return NetworkAuthManager(username: username, password: password) + } + + return NetworkAuthManager(clientId: username, clientSecret: password) + } + + func verifyConnection() { + guard connectionInfoPassesValidation(setWarningInfo: true) else { + return + } + + networkOperationInfo = "Checking Jamf Pro server" + + let uploadMgr = UploadManager(serverURL: serverURL) + uploadMgr.verifyConnection(authManager: makeAuthManager()) { result in + if case .success(let success) = result { + mustSign = success.mustSign + organization = success.organization + verifiedConnectionHash = hashOfConnectionInfo + if saveToKeychain { + do { + try SecurityWrapper.saveCredentials( + username: username, + password: password, + server: serverURL) + } catch { logger.error("Failed to save credentials with error: \(error.localizedDescription)") - } - } - // Future on macOS 12+: focus on Payload Name field - } else if case .failure(let failure) = result, - case .anyError(let errorString) = failure { - warningInfo = errorString - verifiedConnectionHash = 0 - } - - networkOperationInfo = nil - } - } - - private func dismissView() { - if !saveToKeychain { - try? SecurityWrapper.removeCredentials(server: serverURL, username: username) - } - - if let dismiss = dismissAction { - dismiss() - } - } - - func performUpload() { - guard connectionInfoPassesValidation(setWarningInfo: true) else { - return - } - - guard payloadInfoPassesValidation(setWarningInfo: true) else { - return - } - - let profile = Model.shared.exportProfile(organization: organization, - identifier: payloadId, - displayName: payloadName, - payloadDescription: payloadDescription) - - networkOperationInfo = "Uploading '\(profile.displayName)'..." - - var siteIdAndName: (String, String)? - if useSite { - if siteId != -1 && !siteName.isEmpty { - siteIdAndName = ("\(siteId)", siteName) - } - } - - let uploadMgr = UploadManager(serverURL: serverURL) - uploadMgr.upload(profile: profile, - authMgr: makeAuthManager(), - siteInfo: siteIdAndName, - signingIdentity: mustSign ? signingId : nil) { possibleError in - if let error = possibleError { - warningInfo = error.localizedDescription - } else { - Alert().display(header: "Success", message: "Profile uploaded succesfully") - dismissView() - } - networkOperationInfo = nil - } - } + } + } + // Future on macOS 12+: focus on Payload Name field + } else if case .failure(let failure) = result, + case .anyError(let errorString) = failure + { + warningInfo = errorString + verifiedConnectionHash = 0 + } + + networkOperationInfo = nil + } + } + + private func dismissView() { + if !saveToKeychain { + try? SecurityWrapper.removeCredentials(server: serverURL, username: username) + } + + if let dismiss = dismissAction { + dismiss() + } + } + + func performUpload() { + guard connectionInfoPassesValidation(setWarningInfo: true) else { + return + } + + guard payloadInfoPassesValidation(setWarningInfo: true) else { + return + } + + let profile = Model.shared.exportProfile( + organization: organization, + identifier: payloadId, + displayName: payloadName, + payloadDescription: payloadDescription) + + networkOperationInfo = "Uploading '\(profile.displayName)'..." + + var siteIdAndName: (String, String)? + if useSite { + if siteId != -1 && !siteName.isEmpty { + siteIdAndName = ("\(siteId)", siteName) + } + } + + let uploadMgr = UploadManager(serverURL: serverURL) + uploadMgr.upload( + profile: profile, + authMgr: makeAuthManager(), + siteInfo: siteIdAndName, + signingIdentity: mustSign ? signingId : nil + ) { possibleError in + if let error = possibleError { + warningInfo = error.localizedDescription + } else { + Alert().display(header: "Success", message: "Profile uploaded succesfully") + dismissView() + } + networkOperationInfo = nil + } + } } #Preview { - UploadInfoView(signingIdentities: [], - dismissAction: nil) + UploadInfoView( + signingIdentities: [], + dismissAction: nil) } diff --git a/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift b/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift index c9fc3f6..75174e3 100644 --- a/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift +++ b/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift @@ -30,30 +30,30 @@ import Foundation class TCCProfileConfigurationPanel { /// Load TCC Profile data from file - /// - /// - Parameter completion: TCCProfileImportCompletion - success with TCCProfile or failure with TCCProfileImport Error + /// + /// - Parameter completion: TCCProfileImportCompletion - success with TCCProfile or failure with TCCProfileImport Error func loadTCCProfileFromFile(importer: TCCProfileImporter, window: NSWindow, _ completion: @escaping TCCProfileImportCompletion) { - let openPanel = NSOpenPanel.init() - openPanel.allowedFileTypes = ["mobileconfig", "plist"] - openPanel.allowsMultipleSelection = false - openPanel.canChooseDirectories = false - openPanel.canCreateDirectories = false - openPanel.canChooseFiles = true - openPanel.title = "Open TCCProfile File" + let openPanel = NSOpenPanel.init() + openPanel.allowedFileTypes = ["mobileconfig", "plist"] + openPanel.allowsMultipleSelection = false + openPanel.canChooseDirectories = false + openPanel.canCreateDirectories = false + openPanel.canChooseFiles = true + openPanel.title = "Open TCCProfile File" - openPanel.beginSheetModal(for: window) { (response) in - if response != .OK { + openPanel.beginSheetModal(for: window) { (response) in + if response != .OK { completion(.failure(.cancelled)) - } else { - if let result = openPanel.url { - importer.decodeTCCProfile(fileUrl: result) { tccProfileResult in - return completion(tccProfileResult) - } - } else { - completion(.failure(TCCProfileImportError.unableToOpenFile)) - } - } - } + } else { + if let result = openPanel.url { + importer.decodeTCCProfile(fileUrl: result) { tccProfileResult in + return completion(tccProfileResult) + } + } else { + completion(.failure(TCCProfileImportError.unableToOpenFile)) + } + } + } - } + } } diff --git a/Source/TCCProfileImporter/TCCProfileImportError.swift b/Source/TCCProfileImporter/TCCProfileImportError.swift index 7f1c383..53f40e3 100644 --- a/Source/TCCProfileImporter/TCCProfileImportError.swift +++ b/Source/TCCProfileImporter/TCCProfileImportError.swift @@ -26,6 +26,7 @@ // import Foundation + public enum TCCProfileImportError: Error { case cancelled case unableToOpenFile diff --git a/Source/View Controllers/OpenViewController.swift b/Source/View Controllers/OpenViewController.swift index d8bb09b..733315b 100644 --- a/Source/View Controllers/OpenViewController.swift +++ b/Source/View Controllers/OpenViewController.swift @@ -60,7 +60,7 @@ class OpenViewController: NSViewController, NSTableViewDataSource, NSTableViewDe let block = completionBlock let panel = NSOpenPanel() panel.allowsMultipleSelection = true - panel.allowedFileTypes = [ kUTTypeBundle, kUTTypeUnixExecutable ] as [String] + panel.allowedFileTypes = [kUTTypeBundle, kUTTypeUnixExecutable] as [String] panel.directoryURL = URL(fileURLWithPath: "/Applications", isDirectory: true) panel.begin { response in if response == .OK { diff --git a/Source/View Controllers/SaveViewController.swift b/Source/View Controllers/SaveViewController.swift index c2d6081..0f8a525 100644 --- a/Source/View Controllers/SaveViewController.swift +++ b/Source/View Controllers/SaveViewController.swift @@ -64,12 +64,14 @@ class SaveViewController: NSViewController { var defaultsController = NSUserDefaultsController.shared func updateIsReadyToSave() { - guard isReadyToSave != ( - !organizationLabel.stringValue.isEmpty - && (payloadName != nil) - && !payloadName.isEmpty - && (payloadIdentifier != nil) - && !payloadIdentifier.isEmpty ) else { return } + guard + isReadyToSave + != (!organizationLabel.stringValue.isEmpty + && (payloadName != nil) + && !payloadName.isEmpty + && (payloadIdentifier != nil) + && !payloadIdentifier.isEmpty) + else { return } isReadyToSave = !isReadyToSave } @@ -118,7 +120,6 @@ class SaveViewController: NSViewController { defaultsController.removeObserver(self, forKeyPath: "values.organization", context: &SaveViewController.saveProfileKVOContext) } - // swiftlint:disable:next block_based_kvo override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { if context == &SaveViewController.saveProfileKVOContext { updateIsReadyToSave() @@ -130,10 +131,11 @@ class SaveViewController: NSViewController { func saveTo(url: URL) { logger.info("Saving to \(url, privacy: .public)") let model = Model.shared - let profile = model.exportProfile(organization: organizationLabel.stringValue, - identifier: payloadIdentifier, - displayName: payloadName, - payloadDescription: payloadDescription ?? payloadName) + let profile = model.exportProfile( + organization: organizationLabel.stringValue, + identifier: payloadIdentifier, + displayName: payloadName, + payloadDescription: payloadDescription ?? payloadName) do { var outputData = try profile.xmlData() if let identity = identitiesPopUpAC.selectedObjects.first as? SigningIdentity, let ref = identity.reference { diff --git a/Source/View Controllers/TCCProfileViewController.swift b/Source/View Controllers/TCCProfileViewController.swift index 835439a..9baae5e 100644 --- a/Source/View Controllers/TCCProfileViewController.swift +++ b/Source/View Controllers/TCCProfileViewController.swift @@ -24,7 +24,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // -// swiftlint:disable file_length import Cocoa import OSLog @@ -153,10 +152,10 @@ class TCCProfileViewController: NSViewController { case .on: if model.usingLegacyAllowKey && showAlert { let message = """ - Enabling Big Sur Compatibility will require the profile to be installed on macOS versions Big Sur (11.0) or greater. + Enabling Big Sur Compatibility will require the profile to be installed on macOS versions Big Sur (11.0) or greater. - Deploying this profile to computers with macOS 10.15 or earlier will result in an error. - """ + Deploying this profile to computers with macOS 10.15 or earlier will result in an error. + """ Alert().display(header: "Compatibility Warning", message: message) } model.usingLegacyAllowKey = false @@ -179,23 +178,23 @@ class TCCProfileViewController: NSViewController { toggleAuthorizationKey(theSwitch: sender, showAlert: true) } - @IBAction func uploadAction(_ sender: NSButton) { - let identities: [SigningIdentity] - do { - identities = try SecurityWrapper.loadSigningIdentities() - } catch { - identities = [] + @IBAction func uploadAction(_ sender: NSButton) { + let identities: [SigningIdentity] + do { + identities = try SecurityWrapper.loadSigningIdentities() + } catch { + identities = [] logger.error("Error loading identities: \(error.localizedDescription)") - } + } - let uploadView = UploadInfoView(signingIdentities: identities) { - // Dismiss the sheet when the UploadInfoView decides it is done - if let controller = self.presentedViewControllers?.first { - self.dismiss(controller) - } - } - self.presentAsSheet(NSHostingController(rootView: uploadView)) - } + let uploadView = UploadInfoView(signingIdentities: identities) { + // Dismiss the sheet when the UploadInfoView decides it is done + if let controller = self.presentedViewControllers?.first { + self.dismiss(controller) + } + } + self.presentAsSheet(NSHostingController(rootView: uploadView)) + } fileprivate func showAlert(_ error: LocalizedError, for window: NSWindow) { let alertWindow: NSAlert = NSAlert() @@ -238,7 +237,7 @@ class TCCProfileViewController: NSViewController { func promptForExecutables(_ block: @escaping (Executable) -> Void) { let panel = NSOpenPanel() panel.allowsMultipleSelection = true - panel.allowedFileTypes = [ kUTTypeBundle, kUTTypeUnixExecutable ] as [String] + panel.allowedFileTypes = [kUTTypeBundle, kUTTypeUnixExecutable] as [String] panel.directoryURL = URL(fileURLWithPath: "/Applications", isDirectory: true) guard let window = self.view.window else { return @@ -266,7 +265,7 @@ class TCCProfileViewController: NSViewController { } let pasteboardOptions: [NSPasteboard.ReadingOptionKey: Any] = [ - .urlReadingContentsConformToTypes: [ kUTTypeBundle, kUTTypeUnixExecutable ] + .urlReadingContentsConformToTypes: [kUTTypeBundle, kUTTypeUnixExecutable] ] @IBAction func checkForAuthorizationFeaturesUsed(_ sender: NSPopUpButton) { @@ -280,42 +279,44 @@ class TCCProfileViewController: NSViewController { super.viewDidLoad() // Setup policy pop up - setupAllowDeny(policies: [addressBookPopUpAC, - photosPopUpAC, - remindersPopUpAC, - calendarPopUpAC, - accessibilityPopUpAC, - postEventsPopUpAC, - adminFilesPopUpAC, - allFilesPopUpAC, - fileProviderPresencePopUpAC, - mediaLibraryPopUpAC, - speechRecognitionPopUpAC, - dekstopFolderPopUpAC, - documentsFolderPopUpAC, - downloadsFolderPopUpAC, - networkVolumesPopUpAC, - removableVolumesPopUpAC]) - - setupStandardUserAllowAndDeny(policies: [screenCapturePopUpAC, - listenEventPopUpAC]) + setupAllowDeny(policies: [ + addressBookPopUpAC, + photosPopUpAC, + remindersPopUpAC, + calendarPopUpAC, + accessibilityPopUpAC, + postEventsPopUpAC, + adminFilesPopUpAC, + allFilesPopUpAC, + fileProviderPresencePopUpAC, + mediaLibraryPopUpAC, + speechRecognitionPopUpAC, + dekstopFolderPopUpAC, + documentsFolderPopUpAC, + downloadsFolderPopUpAC, + networkVolumesPopUpAC, + removableVolumesPopUpAC + ]) + + setupStandardUserAllowAndDeny(policies: [screenCapturePopUpAC, listenEventPopUpAC]) setupActionForStandardUserAllowedDropDowns(dropDowns: [listenEventPopUp, screenCapturePopUp]) - setupDenyOnly(policies: [cameraPopUpAC, - microphonePopUpAC]) + setupDenyOnly(policies: [cameraPopUpAC, microphonePopUpAC]) setupDescriptions() - setupStackViewsWithBackground(stackViews: [adminFilesStackView, - cameraStackView, - desktopFolderStackView, - downloadsFolderStackView, - allFilesStackView, - mediaLibraryStackView, - networkVolumesStackView, - postEventsStackView, - removableVolumesStackView, - speechRecognitionStackView]) + setupStackViewsWithBackground(stackViews: [ + adminFilesStackView, + cameraStackView, + desktopFolderStackView, + downloadsFolderStackView, + allFilesStackView, + mediaLibraryStackView, + networkVolumesStackView, + postEventsStackView, + removableVolumesStackView, + speechRecognitionStackView + ]) // Setup table views executablesTable.registerForDraggedTypes([.fileURL]) @@ -341,9 +342,11 @@ class TCCProfileViewController: NSViewController { private func setupStandardUserAllowAndDeny(policies: [NSArrayController]) { for policy in policies { - policy.add(contentsOf: ["-", - TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue, - TCCProfileDisplayValue.deny.rawValue]) + policy.add(contentsOf: [ + "-", + TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue, + TCCProfileDisplayValue.deny.rawValue + ]) } } @@ -430,7 +433,8 @@ class TCCProfileViewController: NSViewController { guard let source = self.executablesAC.selectedObjects.first as? Executable else { return } let rule = AppleEventRule(source: source, destination: executable, value: true) guard self.appleEventsAC.canInsert, - self.shouldAppleEventRuleBeAdded(rule) else { return } + self.shouldAppleEventRuleBeAdded(rule) + else { return } self.appleEventsAC.insert(rule, atArrangedObjectIndex: 0) } diff --git a/Source/Views/Alert.swift b/Source/Views/Alert.swift index 89f50f0..7c61b23 100644 --- a/Source/Views/Alert.swift +++ b/Source/Views/Alert.swift @@ -41,7 +41,7 @@ class Alert: NSObject { /// Displays a message with a cancel button and returns true if OK was pressed /// Assumes this method is called from the main queue. - /// + /// /// - Parameters: /// - header: The header message /// - message: The message body diff --git a/git-hooks/pre-commit.swift-format b/git-hooks/pre-commit.swift-format new file mode 100755 index 0000000..ea12b1d --- /dev/null +++ b/git-hooks/pre-commit.swift-format @@ -0,0 +1,24 @@ +#!/bin/bash +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +STAGED_SWIFT_FILES=$(git diff --cached --name-only -z --diff-filter=ACM | tr '\0' '\n' | grep '\.swift$') + +if [ -z "$STAGED_SWIFT_FILES" ]; then + exit 0 +fi + +echo -e "${GREEN}Running swift-format...${NC}" + +echo "$STAGED_SWIFT_FILES" | tr '\n' '\0' | xargs -0 xcrun swift-format --in-place -- +FAILED=$? + +if [ $FAILED -ne 0 ]; then + echo -e "${RED}Commit rejected: Swift formatting failed${NC}" + exit 1 +fi + +echo "$STAGED_SWIFT_FILES" | tr '\n' '\0' | xargs -0 git add -- +echo -e "${GREEN}✓ Swift files were formatted${NC}" +exit 0 From 2f85f1b61b3396ba190dfc3aacca38402eb47920 Mon Sep 17 00:00:00 2001 From: JJ Pritzl Date: Wed, 1 Apr 2026 10:02:35 -0500 Subject: [PATCH 14/18] create-separate story/JPCFM-5564 Try to fix failing tests on GH PR --- Source/Model/Model.swift | 4 +++- Source/Model/PPPCServicesManager.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Model/Model.swift b/Source/Model/Model.swift index 3854d7f..2cb9a4f 100644 --- a/Source/Model/Model.swift +++ b/Source/Model/Model.swift @@ -31,7 +31,9 @@ import OSLog @objc class Model: NSObject { @objc dynamic var current: Executable? - @objc dynamic static let shared = Model() + @objc dynamic nonisolated(unsafe) static let shared: Model = { + MainActor.assumeIsolated { Model() } + }() @objc dynamic var identities: [SigningIdentity] = [] @objc dynamic var selectedExecutables: [Executable] = [] diff --git a/Source/Model/PPPCServicesManager.swift b/Source/Model/PPPCServicesManager.swift index 0f5306b..becd5b4 100644 --- a/Source/Model/PPPCServicesManager.swift +++ b/Source/Model/PPPCServicesManager.swift @@ -34,7 +34,9 @@ class PPPCServicesManager { let logger = Logger.PPPCServicesManager - static let shared = PPPCServicesManager() + nonisolated(unsafe) static let shared: PPPCServicesManager = { + MainActor.assumeIsolated { PPPCServicesManager() } + }() let allServices: [MDMServiceKey: PPPCServiceInfo] From 2ed25d9de25907878ba3f2d007c2f58ed8cbcebb Mon Sep 17 00:00:00 2001 From: JJ Pritzl Date: Wed, 1 Apr 2026 13:05:57 -0500 Subject: [PATCH 15/18] create-separate story/JPCFM-5564 Try to fix unit test check issue --- Source/Model/Model.swift | 2 +- Source/Model/PPPCServicesManager.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Model/Model.swift b/Source/Model/Model.swift index 2cb9a4f..525ffd7 100644 --- a/Source/Model/Model.swift +++ b/Source/Model/Model.swift @@ -28,7 +28,7 @@ import Cocoa import OSLog -@objc class Model: NSObject { +@objc class Model: NSObject, @unchecked Sendable { @objc dynamic var current: Executable? @objc dynamic nonisolated(unsafe) static let shared: Model = { diff --git a/Source/Model/PPPCServicesManager.swift b/Source/Model/PPPCServicesManager.swift index becd5b4..fc22a61 100644 --- a/Source/Model/PPPCServicesManager.swift +++ b/Source/Model/PPPCServicesManager.swift @@ -28,7 +28,7 @@ import Foundation import OSLog -class PPPCServicesManager { +class PPPCServicesManager: @unchecked Sendable { typealias MDMServiceKey = String From 83f107dd24ddd0f25e48f52b7a48ff76745e557b Mon Sep 17 00:00:00 2001 From: JJ Pritzl Date: Wed, 1 Apr 2026 14:00:38 -0500 Subject: [PATCH 16/18] create-separate story/JPCFM-5564 Make actual change with the plan to fix Unit Test issues --- .../TCCProfileConfigurationPanel.swift | 55 +++++++------------ .../TCCProfileImporter.swift | 2 - .../TCCProfileViewController.swift | 18 +++--- Source/Views/Alert.swift | 2 +- 4 files changed, 30 insertions(+), 47 deletions(-) diff --git a/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift b/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift index f2f00b9..e46f02b 100644 --- a/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift +++ b/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift @@ -30,40 +30,27 @@ import Foundation class TCCProfileConfigurationPanel { /// Load TCC Profile data from file - /// - /// - Parameters: - /// - importer: The TCCProfileImporter to use - /// - window: The window to present the open panel in - /// - completion: Called with the result of the import - func loadTCCProfileFromFile(importer: TCCProfileImporter, window: NSWindow, _ completion: @escaping (TCCProfileImportResult) -> Void) { - let openPanel = NSOpenPanel.init() - openPanel.allowedFileTypes = ["mobileconfig", "plist"] - openPanel.allowsMultipleSelection = false - openPanel.canChooseDirectories = false - openPanel.canCreateDirectories = false - openPanel.canChooseFiles = true - openPanel.title = "Open TCCProfile File" + /// + /// - Parameters: + /// - importer: The TCCProfileImporter to use + /// - window: The window to present the open panel in + /// - Returns: The decoded TCCProfile, or nil if the user cancelled + func loadTCCProfileFromFile(importer: TCCProfileImporter, window: NSWindow) async throws -> TCCProfile? { + let openPanel = NSOpenPanel() + openPanel.allowedFileTypes = ["mobileconfig", "plist"] + openPanel.allowsMultipleSelection = false + openPanel.canChooseDirectories = false + openPanel.canCreateDirectories = false + openPanel.canChooseFiles = true + openPanel.title = "Open TCCProfile File" - openPanel.beginSheetModal(for: window) { (response) in - if response != .OK { - completion(.failure(.cancelled)) - } else { - if let result = openPanel.url { - do { - let tccProfile = try importer.decodeTCCProfile(fileUrl: result) - completion(.success(tccProfile)) - } catch { - if let importError = error as? TCCProfileImportError { - completion(.failure(importError)) - } else { - completion(.failure(.invalidProfileFile(description: error.localizedDescription))) - } - } - } else { - completion(.failure(TCCProfileImportError.unableToOpenFile)) - } - } - } + let response = await openPanel.beginSheetModal(for: window) + guard response == .OK else { return nil } - } + guard let fileUrl = openPanel.url else { + throw TCCProfileImportError.unableToOpenFile + } + + return try importer.decodeTCCProfile(fileUrl: fileUrl) + } } diff --git a/Source/TCCProfileImporter/TCCProfileImporter.swift b/Source/TCCProfileImporter/TCCProfileImporter.swift index 3157d0d..6b61f49 100644 --- a/Source/TCCProfileImporter/TCCProfileImporter.swift +++ b/Source/TCCProfileImporter/TCCProfileImporter.swift @@ -27,8 +27,6 @@ import Foundation -typealias TCCProfileImportResult = Result - /// Load tcc profiles public class TCCProfileImporter { diff --git a/Source/View Controllers/TCCProfileViewController.swift b/Source/View Controllers/TCCProfileViewController.swift index 4d3378c..40862de 100644 --- a/Source/View Controllers/TCCProfileViewController.swift +++ b/Source/View Controllers/TCCProfileViewController.swift @@ -183,17 +183,15 @@ class TCCProfileViewController: NSViewController { let tccProfileImporter = TCCProfileImporter() let tccConfigPanel = TCCProfileConfigurationPanel() - tccConfigPanel.loadTCCProfileFromFile(importer: tccProfileImporter, window: window) { [weak self] tccProfileResult in - guard let weakSelf = self else { return } - switch tccProfileResult { - case .success(let tccProfile): - Task { - await weakSelf.model.importProfile(tccProfile: tccProfile) - } - case .failure(let tccProfileImportError): - if !tccProfileImportError.isCancelled { - weakSelf.showAlert(tccProfileImportError, for: window) + Task { + do { + if let tccProfile = try await tccConfigPanel.loadTCCProfileFromFile(importer: tccProfileImporter, window: window) { + await model.importProfile(tccProfile: tccProfile) } + } catch let error as TCCProfileImportError { + showAlert(error, for: window) + } catch { + showAlert(TCCProfileImportError.invalidProfileFile(description: error.localizedDescription), for: window) } } } diff --git a/Source/Views/Alert.swift b/Source/Views/Alert.swift index 956d26c..43e92fe 100644 --- a/Source/Views/Alert.swift +++ b/Source/Views/Alert.swift @@ -27,7 +27,7 @@ import Cocoa -class Alert: NSObject { +class Alert { func display(header: String, message: String) { let dialog: NSAlert = NSAlert() dialog.messageText = header From e0862fec1c4b41eb7535e7318c9ccb2116c1aeef Mon Sep 17 00:00:00 2001 From: JJ Pritzl <108085430+jjpritzl@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:16:39 -0500 Subject: [PATCH 17/18] Big sur compatibility removal (#135) * AI generated removal of the big sur compatibility logic. Needs vetted and tested * big-sur story/JPCFM-5531 Add minor changes to update minimum supported OS and remove remaining Big Sur things * big-sur story/JPCFM-5531 Remove swiftlint disable call due to reduced file length * big-sur story/JPCFM-5531 Add Copilot suggestion for generating test results * big-sur story/JPCFM-5531 Add Copilot refactor to loadExecutable function * big-sur story/JPCFM-5531 Remove code coverage addition --------- Co-authored-by: Tony Eichelberger --- CHANGELOG.md | 3 +- PPPC Utility.xcodeproj/project.pbxproj | 8 +- PPPC UtilityTests/ModelTests/ModelTests.swift | 196 +----------------- .../ModelTests/PPPCServicesManagerTests.swift | 2 +- README.md | 2 +- Resources/Base.lproj/Main.storyboard | 21 +- Resources/PPPCServices.json | 4 +- Source/Model/Model.swift | 147 ++++++------- Source/Model/PPPCServiceInfo.swift | 2 +- .../URLSessionAsyncCompatibility.swift | 52 ----- .../TCCProfileViewController.swift | 59 ------ 11 files changed, 81 insertions(+), 415 deletions(-) delete mode 100644 Source/Networking/URLSessionAsyncCompatibility.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b4616..3819149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed - Update print and os_log calls to the modern OSLog class calls for updated logging. ([Issue #112](https://github.com/jamf/PPPC-Utility/issues/112)) [@SkylerGodfrey](https://github.com/SkylerGodfrey) - Now using [Haversack](https://github.com/jamf/Haversack) for simplified access to the keychain ([Issue #124](https://github.com/jamf/PPPC-Utility/issues/124)) [@macblazer](https://github.com/macblazer). -- PPPC Utility now requires macOS 11+ to run. It can still produce profiles usable on older versions of macOS. +- PPPC Utility now requires macOS 13+ to run. It can still produce profiles usable on older versions of macOS. +- Removed Big Sur compatibility toggle and legacy `Allowed` key support. The `Authorization` key is now always used. ## [1.5.0] - 2022-10-04 diff --git a/PPPC Utility.xcodeproj/project.pbxproj b/PPPC Utility.xcodeproj/project.pbxproj index 5c9d327..b4f1ed4 100644 --- a/PPPC Utility.xcodeproj/project.pbxproj +++ b/PPPC Utility.xcodeproj/project.pbxproj @@ -59,7 +59,6 @@ C0EE9A7D28639BF800738B6B /* TestTCCProfileForJamfProAPI.txt in Resources */ = {isa = PBXBuildFile; fileRef = C0EE9A7C28639BF800738B6B /* TestTCCProfileForJamfProAPI.txt */; }; C0EE9A7F2863BDE300738B6B /* JamfProAPITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0EE9A7E2863BDE300738B6B /* JamfProAPITypes.swift */; }; C0EE9A812863BE2B00738B6B /* NetworkAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0EE9A802863BE2B00738B6B /* NetworkAuthManager.swift */; }; - C0EE9A832863BEEB00738B6B /* URLSessionAsyncCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0EE9A822863BEEB00738B6B /* URLSessionAsyncCompatibility.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -131,7 +130,6 @@ C0EE9A7C28639BF800738B6B /* TestTCCProfileForJamfProAPI.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = TestTCCProfileForJamfProAPI.txt; sourceTree = ""; }; C0EE9A7E2863BDE300738B6B /* JamfProAPITypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JamfProAPITypes.swift; sourceTree = ""; }; C0EE9A802863BE2B00738B6B /* NetworkAuthManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkAuthManager.swift; sourceTree = ""; }; - C0EE9A822863BEEB00738B6B /* URLSessionAsyncCompatibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionAsyncCompatibility.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -324,7 +322,6 @@ C03270BF28636397008B38E0 /* JamfProAPIClient.swift */, C0EE9A7E2863BDE300738B6B /* JamfProAPITypes.swift */, C05844B72AD4512D00141353 /* Token.swift */, - C0EE9A822863BEEB00738B6B /* URLSessionAsyncCompatibility.swift */, ); path = Networking; sourceTree = ""; @@ -512,7 +509,6 @@ 6E6216F9215321CE0043DF18 /* OpenViewController.swift in Sources */, C05844B82AD4512D00141353 /* Token.swift in Sources */, 6EC40A10214DE3B200BE4F17 /* Executable.swift in Sources */, - C0EE9A832863BEEB00738B6B /* URLSessionAsyncCompatibility.swift in Sources */, C0EE9A812863BE2B00738B6B /* NetworkAuthManager.swift in Sources */, 6EC40A16214ECF1E00BE4F17 /* SaveViewController.swift in Sources */, C05844BE2AD45F7900141353 /* UploadInfoView.swift in Sources */, @@ -644,7 +640,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IBSC_NOTICES = NO; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -700,7 +696,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IBSC_NOTICES = NO; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; diff --git a/PPPC UtilityTests/ModelTests/ModelTests.swift b/PPPC UtilityTests/ModelTests/ModelTests.swift index 38f3b5e..e6a64e2 100644 --- a/PPPC UtilityTests/ModelTests/ModelTests.swift +++ b/PPPC UtilityTests/ModelTests/ModelTests.swift @@ -130,7 +130,6 @@ class ModelTests: XCTestCase { func testExportProfileWithAppleEventsAndAuthorization() { // given - model.usingLegacyAllowKey = false let exe1 = Executable(identifier: "one", codeRequirement: "oneReq") let exe2 = Executable(identifier: "two", codeRequirement: "twoReq") @@ -185,60 +184,6 @@ class ModelTests: XCTestCase { } } - func testExportProfileWithAppleEventsAndLegacyAllowed() { - // given - let exe1 = Executable(identifier: "one", codeRequirement: "oneReq") - let exe2 = Executable(identifier: "two", codeRequirement: "twoReq") - exe1.appleEvents = [AppleEventRule(source: exe1, destination: exe2, value: true)] - exe2.policy.SystemPolicyAllFiles = "Allow" - model.selectedExecutables = [exe1, exe2] - model.usingLegacyAllowKey = true - - // when - let profile = model.exportProfile(organization: "Org", identifier: "ID", displayName: "Name", payloadDescription: "Desc") - - // then check top level settings - XCTAssertEqual("Org", profile.organization) - XCTAssertEqual("ID", profile.identifier) - XCTAssertEqual("Name", profile.displayName) - XCTAssertEqual("Desc", profile.payloadDescription) - XCTAssertEqual("System", profile.scope) - XCTAssertEqual("Configuration", profile.type) - XCTAssertNotNil(profile.uuid) - XCTAssertEqual(1, profile.version) - - // then verify the payload content top level - XCTAssertEqual(1, profile.content.count) - profile.content.forEach { content in - XCTAssertNotNil(content.uuid) - XCTAssertEqual(1, content.version) - - // then verify the services - XCTAssertEqual(2, content.services.count) - let appleEventsPolicy = content.services["AppleEvents"]?.first - XCTAssertNotNil(appleEventsPolicy) - XCTAssertEqual("one", appleEventsPolicy?.identifier) - XCTAssertEqual("oneReq", appleEventsPolicy?.codeRequirement) - XCTAssertEqual("bundleID", appleEventsPolicy?.identifierType) - XCTAssertEqual("two", appleEventsPolicy?.receiverIdentifier) - XCTAssertEqual("twoReq", appleEventsPolicy?.receiverCodeRequirement) - XCTAssertEqual("bundleID", appleEventsPolicy?.receiverIdentifierType) - XCTAssertTrue(appleEventsPolicy?.allowed == true) - XCTAssertNil(appleEventsPolicy?.authorization) - - let allFilesPolicy = content.services["SystemPolicyAllFiles"]?.first - XCTAssertNotNil(allFilesPolicy) - XCTAssertEqual("two", allFilesPolicy?.identifier) - XCTAssertEqual("twoReq", allFilesPolicy?.codeRequirement) - XCTAssertEqual("bundleID", allFilesPolicy?.identifierType) - XCTAssertNil(allFilesPolicy?.receiverIdentifier) - XCTAssertNil(allFilesPolicy?.receiverCodeRequirement) - XCTAssertNil(allFilesPolicy?.receiverIdentifierType) - XCTAssertTrue(allFilesPolicy?.allowed == true) - XCTAssertNil(allFilesPolicy?.authorization) - } - } - // MARK: - tests for importProfile func testImportProfileUsingAuthorizationKeyAllow() { @@ -325,11 +270,10 @@ class ModelTests: XCTestCase { XCTAssertEqual("Deny", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) } - // MARK: - tests for profileToString + // MARK: - tests for policyFromString - func testPolicyWhenUsingAllowAndAuthorizationKey() { + func testPolicyWhenUsingAllow() { // given - model.usingLegacyAllowKey = false let app = Executable(identifier: "id", codeRequirement: "req") // when @@ -342,7 +286,6 @@ class ModelTests: XCTestCase { func testPolicyWhenUsingDeny() { // given - model.usingLegacyAllowKey = false let app = Executable(identifier: "id", codeRequirement: "req") // when @@ -355,7 +298,6 @@ class ModelTests: XCTestCase { func testPolicyWhenUsingAllowForStandardUsers() { // given - model.usingLegacyAllowKey = false let app = Executable(identifier: "id", codeRequirement: "req") // when @@ -377,138 +319,4 @@ class ModelTests: XCTestCase { XCTAssertNil(policy, "should have not created the policy with an unknown value") } - func testPolicyWhenUsingLegacyDeny() { - // given - let app = Executable(identifier: "id", codeRequirement: "req") - model.usingLegacyAllowKey = true - - // when - let policy = model.policyFromString(executable: app, value: "Deny") - - // then - XCTAssertNil(policy?.authorization, "should not set authorization when in legacy mode") - XCTAssertEqual(policy?.allowed, false) - } - - func testPolicyWhenUsingLegacyAllow() { - // given - let app = Executable(identifier: "id", codeRequirement: "req") - model.usingLegacyAllowKey = true - - // when - let policy = model.policyFromString(executable: app, value: "Allow") - - // then - XCTAssertNil(policy?.authorization, "should not set authorization when in legacy mode") - XCTAssertEqual(policy?.allowed, true) - } - - // test for the unrecognized strings for both legacy and normal - func testPolicyWhenUsingLegacyAllowButNonLegacyValueUsed() { - // given - let app = Executable(identifier: "id", codeRequirement: "req") - model.usingLegacyAllowKey = true - - // when - let policy = model.policyFromString(executable: app, value: "Let Standard Users Approve") - - // then - XCTAssertNil(policy, "should have errored out because of an invalid value") - } - - // MARK: - tests for requiresAuthorizationKey - - func testWhenServiceIsUsingAllowStandarUsersToApprove() { - // given - let profile = TCCProfileBuilder().buildProfile(authorization: .allowStandardUserToSetSystemService) - - // when - model.importProfile(tccProfile: profile) - - // then - XCTAssertTrue(model.requiresAuthorizationKey()) - } - - func testWhenServiceIsUsingOnlyAllowKey() { - // given - let profile = TCCProfileBuilder().buildProfile(authorization: .allow) - - // when - model.importProfile(tccProfile: profile) - - // then - XCTAssertFalse(model.requiresAuthorizationKey()) - } - - func testWhenServiceIsUsingOnlyDenyKey() { - // given - let profile = TCCProfileBuilder().buildProfile(authorization: .deny) - - // when - model.importProfile(tccProfile: profile) - - // then - XCTAssertFalse(model.requiresAuthorizationKey()) - } - - // MARK: - tests for changeToUseLegacyAllowKey - - func testChangingFromAuthorizationKeyToLegacyAllowKey() { - // given - let allowStandard = TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue - let exeSettings = ["AddressBook": "Allow", "ListenEvent": allowStandard, "ScreenCapture": allowStandard] - let model = ModelBuilder().addExecutable(settings: exeSettings).build() - model.usingLegacyAllowKey = false - - // when - model.changeToUseLegacyAllowKey() - - // then - XCTAssertEqual(1, model.selectedExecutables.count, "should have only one exe") - let policy = model.selectedExecutables.first?.policy - XCTAssertEqual("Allow", policy?.AddressBook) - XCTAssertEqual("-", policy?.Camera) - XCTAssertEqual("-", policy?.ListenEvent) - XCTAssertEqual("-", policy?.ScreenCapture) - XCTAssertTrue(model.usingLegacyAllowKey) - } - - func testChangingFromAuthorizationKeyToLegacyAllowKeyWithMoreComplexVaues() { - // given - let allowStandard = TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue - let p1Settings = [ - "SystemPolicyAllFiles": "Allow", - "ListenEvent": allowStandard, - "ScreenCapture": "Deny", - "Camera": "Deny" - ] - - let p2Settings = [ - "SystemPolicyAllFiles": "Deny", - "ScreenCapture": allowStandard, - "Calendar": "Allow" - ] - let builder = ModelBuilder().addExecutable(settings: p1Settings) - model = builder.addExecutable(settings: p2Settings).build() - model.usingLegacyAllowKey = false - - // when - model.changeToUseLegacyAllowKey() - - // then - XCTAssertEqual(2, model.selectedExecutables.count, "should have only one exe") - let policy1 = model.selectedExecutables[0].policy - XCTAssertEqual("Allow", policy1.SystemPolicyAllFiles) - XCTAssertEqual("-", policy1.ListenEvent) - XCTAssertEqual("Deny", policy1.ScreenCapture) - XCTAssertEqual("Deny", policy1.Camera) - - let policy2 = model.selectedExecutables[1].policy - XCTAssertEqual("Deny", policy2.SystemPolicyAllFiles) - XCTAssertEqual("-", policy2.ListenEvent) - XCTAssertEqual("-", policy2.ScreenCapture) - XCTAssertEqual("Allow", policy2.Calendar) - XCTAssertTrue(model.usingLegacyAllowKey) - } - } diff --git a/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift b/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift index 3cd1315..70df11a 100644 --- a/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift +++ b/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift @@ -85,7 +85,7 @@ class PPPCServicesManagerTests: XCTestCase { let service = try XCTUnwrap(services.allServices["ScreenCapture"]) // when - let actual = try XCTUnwrap(service.allowStandardUsersMacOS11Plus) + let actual = try XCTUnwrap(service.allowStandardUsers) // then XCTAssertTrue(actual) diff --git a/README.md b/README.md index 9992a35..f183273 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [logo]: /Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32%402x.png "PPPC Utility" -PPPC Utility is a macOS (10.15 and newer) application for creating configuration profiles containing the Privacy Preferences Policy Control payload for macOS. The profiles can be saved locally, signed or unsigned. Profiles can also be uploaded directly to a Jamf Pro server. +PPPC Utility is a macOS (13.0 and newer) application for creating configuration profiles containing the Privacy Preferences Policy Control payload for macOS. The profiles can be saved locally, signed or unsigned. Profiles can also be uploaded directly to a Jamf Pro server. All changes to the application are tracked in [the changelog](https://github.com/jamf/PPPC-Utility/blob/master/CHANGELOG.md). diff --git a/Resources/Base.lproj/Main.storyboard b/Resources/Base.lproj/Main.storyboard index 76e1e55..bd1a5b6 100644 --- a/Resources/Base.lproj/Main.storyboard +++ b/Resources/Base.lproj/Main.storyboard @@ -2397,20 +2397,6 @@ - - - - - - - - - - - - - -