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