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.

+
+## 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