diff --git a/CHANGELOG.md b/CHANGELOG.md index 701e2d9..60b8a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added +- @(author) description + +## [1.3.0] + +### Added + +- (@hisaac) Added this changelog file +- (@ty-wilson) Fixed issue #72 changing the default value on Apple Events to "Allow" +- (@watkyn) Added support for the new Authorization key in Big Sur +- (@watkyn) Changed minimum deployment target to macOS 10.15 -- Added this changelog file ## [1.2.1] - 2020-09-17 diff --git a/PPPC Utility.xcodeproj/project.pbxproj b/PPPC Utility.xcodeproj/project.pbxproj index 09e3477..ef79e1c 100644 --- a/PPPC Utility.xcodeproj/project.pbxproj +++ b/PPPC Utility.xcodeproj/project.pbxproj @@ -8,10 +8,11 @@ /* Begin PBXBuildFile section */ 345B01D623FDBF55008838B6 /* TCCProfileExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345B01D523FDBF55008838B6 /* TCCProfileExtensions.swift */; }; - 345B01D723FDBF55008838B6 /* TCCProfileExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345B01D523FDBF55008838B6 /* TCCProfileExtensions.swift */; }; - 34DED4D323FDC77E00C53FB9 /* TCCProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC409F4214D95D200BE4F17 /* TCCProfile.swift */; }; 34DED4D423FDCAFD00C53FB9 /* SwiftyCMSDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F95AE1523158F03002E0A22 /* SwiftyCMSDecoder.swift */; }; 34DED4D623FDDB2B00C53FB9 /* TCCProfileConfigurationPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34DED4D523FDDB2B00C53FB9 /* TCCProfileConfigurationPanel.swift */; }; + 5901A2772534DF1400A1CD2F /* ModelBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5901A2762534DF1400A1CD2F /* ModelBuilder.swift */; }; + 59206D6925265F0C00B94795 /* TCCProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59206D6825265F0C00B94795 /* TCCProfileTests.swift */; }; + 59798B82252D16310070A204 /* TCCProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59798B81252D16310070A204 /* TCCProfileBuilder.swift */; }; 5F90EBDD2319934F00738D09 /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F90EBDC2319934F00738D09 /* ArrayExtensions.swift */; }; 5F90EBDF2319970000738D09 /* TCCProfileImportError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F90EBDE2319970000738D09 /* TCCProfileImportError.swift */; }; 5F90EBE12319970500738D09 /* TCCProfileImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F90EBE02319970500738D09 /* TCCProfileImporter.swift */; }; @@ -56,6 +57,9 @@ /* Begin PBXFileReference section */ 345B01D523FDBF55008838B6 /* TCCProfileExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCCProfileExtensions.swift; sourceTree = ""; }; 34DED4D523FDDB2B00C53FB9 /* TCCProfileConfigurationPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCCProfileConfigurationPanel.swift; sourceTree = ""; }; + 5901A2762534DF1400A1CD2F /* ModelBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelBuilder.swift; sourceTree = ""; }; + 59206D6825265F0C00B94795 /* TCCProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCCProfileTests.swift; sourceTree = ""; }; + 59798B81252D16310070A204 /* TCCProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCCProfileBuilder.swift; sourceTree = ""; }; 5F90EBDC2319934F00738D09 /* ArrayExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtensions.swift; sourceTree = ""; }; 5F90EBDE2319970000738D09 /* TCCProfileImportError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TCCProfileImportError.swift; sourceTree = ""; }; 5F90EBE02319970500738D09 /* TCCProfileImporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TCCProfileImporter.swift; sourceTree = ""; }; @@ -112,6 +116,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 5901A2752534DEFF00A1CD2F /* Helpers */ = { + isa = PBXGroup; + children = ( + 59798B81252D16310070A204 /* TCCProfileBuilder.swift */, + 5901A2762534DF1400A1CD2F /* ModelBuilder.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 5F90EBDB2319931100738D09 /* Extensions */ = { isa = PBXGroup; children = ( @@ -151,6 +164,7 @@ 5F95AE1C2315A6AD002E0A22 /* PPPC UtilityTests */ = { isa = PBXGroup; children = ( + 5901A2752534DEFF00A1CD2F /* Helpers */, 5F90EBE22319991600738D09 /* ModelTests */, 5F95AE272315B069002E0A22 /* TCCProfileImporter */, 5F95AE1F2315A6AD002E0A22 /* Info.plist */, @@ -166,6 +180,7 @@ 5F95AE2C2315B172002E0A22 /* TestTCCUnsignedProfile-Empty.mobileconfig */, 5F95AE292315B172002E0A22 /* TestTCCUnsignedProfile.mobileconfig */, 5F95AE252315A7CB002E0A22 /* TCCProfileImporterTests.swift */, + 59206D6825265F0C00B94795 /* TCCProfileTests.swift */, ); path = TCCProfileImporter; sourceTree = ""; @@ -173,6 +188,7 @@ 6E651CC623143969001CC974 /* Views */ = { isa = PBXGroup; children = ( + B5E09547250BCCFC00A40409 /* Alert.swift */, 6E651CC72314397D001CC974 /* FlippedClipView.swift */, 6E651CC9231439CE001CC974 /* InfoButton.swift */, ); @@ -230,7 +246,6 @@ 6EC40A1D214EF87E00BE4F17 /* Model */ = { isa = PBXGroup; children = ( - B5E09547250BCCFC00A40409 /* Alert.swift */, 6EC40A1B214EF87800BE4F17 /* SigningIdentity.swift */, 6EC40A13214DFB5800BE4F17 /* Model.swift */, 6EC40A0F214DE3B200BE4F17 /* Executable.swift */, @@ -391,9 +406,10 @@ files = ( 34DED4D423FDCAFD00C53FB9 /* SwiftyCMSDecoder.swift in Sources */, 5F95AE262315A7CB002E0A22 /* TCCProfileImporterTests.swift in Sources */, + 59206D6925265F0C00B94795 /* TCCProfileTests.swift in Sources */, + 5901A2772534DF1400A1CD2F /* ModelBuilder.swift in Sources */, + 59798B82252D16310070A204 /* TCCProfileBuilder.swift in Sources */, 5F90EBE42319992B00738D09 /* ModelTests.swift in Sources */, - 34DED4D323FDC77E00C53FB9 /* TCCProfile.swift in Sources */, - 345B01D723FDBF55008838B6 /* TCCProfileExtensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -454,18 +470,19 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = XPLDEEDNHE; INFOPLIST_FILE = "PPPC UtilityTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = "com.jamf.PPPC-UtilityTests"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PPPC Utility.app/Contents/MacOS/PPPC Utility"; @@ -477,18 +494,19 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = XPLDEEDNHE; INFOPLIST_FILE = "PPPC UtilityTests/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = "com.jamf.PPPC-UtilityTests"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PPPC Utility.app/Contents/MacOS/PPPC Utility"; }; @@ -544,7 +562,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + IBSC_NOTICES = NO; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -598,7 +617,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + IBSC_NOTICES = NO; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -612,16 +632,17 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "Resources/PPPC Utility.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; - ENABLE_HARDENED_RUNTIME = YES; + DEVELOPMENT_TEAM = XPLDEEDNHE; + ENABLE_HARDENED_RUNTIME = NO; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.3.0; PRODUCT_BUNDLE_IDENTIFIER = com.jamf.opensource.pppcutility; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -634,16 +655,17 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "Resources/PPPC Utility.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = XPLDEEDNHE; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.3.0; PRODUCT_BUNDLE_IDENTIFIER = com.jamf.opensource.pppcutility; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PPPC UtilityTests/Helpers/ModelBuilder.swift b/PPPC UtilityTests/Helpers/ModelBuilder.swift new file mode 100644 index 0000000..30514ee --- /dev/null +++ b/PPPC UtilityTests/Helpers/ModelBuilder.swift @@ -0,0 +1,53 @@ +// +// ModelBuilder.swift +// +// MIT License +// +// Copyright (c) 2019 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Cocoa + +@testable import PPPC_Utility + +class ModelBuilder { + + var model: Model + + init() { + model = Model() + } + + func build() -> Model { + return model + } + + func addExecutable(settings: [String: String]) -> ModelBuilder { + let exe = Executable(identifier: "id", codeRequirement: "req", "display") + settings.forEach { key, value in + exe.policy.setValue(value, forKey: key) + } + model.selectedExecutables.append(exe) + + return self + } + +} diff --git a/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift b/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift new file mode 100644 index 0000000..67d9830 --- /dev/null +++ b/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift @@ -0,0 +1,74 @@ +// +// TCCProfileBuilder.swift +// +// MIT License +// +// Copyright (c) 2019 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Cocoa + +@testable import PPPC_Utility + +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") + policy.comment = "policy comment" + policy.identifierType = "policy id type" + policy.receiverIdentifierType = "policy receiver id type" + policy.allowed = allowed + policy.authorization = authorization + return policy + } + + func buildTCCPolicies(allowed: Bool?, authorization: TCCPolicyAuthorizationValue?) -> [String: [TCCPolicy]] { + 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)) + } + + 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: [:]) + profile.content = [buildTCCContent(1, allowed: allowed, authorization: authorization)] + profile.version = 100 + profile.uuid = "the uuid" + return profile + } + +} diff --git a/PPPC UtilityTests/ModelTests/ModelTests.swift b/PPPC UtilityTests/ModelTests/ModelTests.swift index c6f294e..63a39ff 100644 --- a/PPPC UtilityTests/ModelTests/ModelTests.swift +++ b/PPPC UtilityTests/ModelTests/ModelTests.swift @@ -32,13 +32,20 @@ import XCTest class ModelTests: XCTestCase { + var model: Model! + + override func setUp() { + super.setUp() + model = Model() + } + + // MARK: - tests for getExecutableFrom* + func testGetExecutableBasedOnIdentifierAndCodeRequirement_BundleIdentifierType() { //given let identifier = "com.example.App" let codeRequirement = "testCodeRequirement" - let model = Model() - //when let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) @@ -53,8 +60,6 @@ class ModelTests: XCTestCase { let identifier = "/myGreatPath/Awesome/Binary" let codeRequirement = "testCodeRequirement" - let model = Model() - //when let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) @@ -69,7 +74,6 @@ class ModelTests: XCTestCase { let identifier = "com.apple.Safari" let codeRequirement = "randomReq" - let model = Model() //when let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement) @@ -81,8 +85,6 @@ class ModelTests: XCTestCase { func testGetExecutableFromSelectedExecutables() { //given - let model = Model() - let expectedIdentifier = "com.something.1" let executable = model.getExecutableFrom(identifier: expectedIdentifier, codeRequirement: "testReq") let executableSecond = model.getExecutableFrom(identifier: "com.something.2", codeRequirement: "testReq2") @@ -100,10 +102,7 @@ class ModelTests: XCTestCase { func testGetExecutableFromSelectedExecutables_Path() { //given - let model = Model() - let expectedIdentifier = "/path/something/Special" - let executableOneMore = model.getExecutableFrom(identifier: "/path/something/Special1", codeRequirement: "testReq") let executable = model.getExecutableFrom(identifier: expectedIdentifier, codeRequirement: "testReq") let executableSecond = model.getExecutableFrom(identifier: "com.something.2", codeRequirement: "testReq2") @@ -120,9 +119,6 @@ class ModelTests: XCTestCase { } func testGetExecutableFromSelectedExecutables_Empty() { - // given - let model = Model() - //when let existingExecutable = model.getExecutableFromSelectedExecutables(bundleIdentifier: "com.something.1") @@ -130,4 +126,391 @@ class ModelTests: XCTestCase { XCTAssertNil(existingExecutable) } + // MARK: - tests for exportProfile + + func testExportProfileWithAppleEventsAndAuthorization() { + // given + model.usingLegacyAllowKey = false + 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 = "Let Standard Users Approve" + + model.selectedExecutables = [exe1, exe2] + + // when + let profile = model.exportProfile(organization: "Org", identifier: "ID", displayName: "Name", payloadDescription: "Desc") + + // then check top level settings + XCTAssertEqual("Org", profile.organization) + XCTAssertEqual("ID", profile.identifier) + XCTAssertEqual("Name", profile.displayName) + XCTAssertEqual("Desc", profile.payloadDescription) + XCTAssertEqual("System", profile.scope) + XCTAssertEqual("Configuration", profile.type) + XCTAssertNotNil(profile.uuid) + XCTAssertEqual(1, profile.version) + + // then check policy settings + // then verify the payload content top level + XCTAssertEqual(1, profile.content.count) + profile.content.forEach { content in + XCTAssertNotNil(content.uuid) + XCTAssertEqual(1, content.version) + + // then verify the services + XCTAssertEqual(2, content.services.count) + let appleEvents = content.services["AppleEvents"] + XCTAssertNotNil(appleEvents) + let appleEventsPolicy = appleEvents?.first + XCTAssertEqual("one", appleEventsPolicy?.identifier) + XCTAssertEqual("oneReq", appleEventsPolicy?.codeRequirement) + XCTAssertEqual("bundleID", appleEventsPolicy?.identifierType) + XCTAssertEqual("two", appleEventsPolicy?.receiverIdentifier) + XCTAssertEqual("twoReq", appleEventsPolicy?.receiverCodeRequirement) + XCTAssertEqual("bundleID", appleEventsPolicy?.receiverIdentifierType) + XCTAssertTrue(appleEventsPolicy?.authorization == .allow) + + let allFiles = content.services["SystemPolicyAllFiles"] + XCTAssertNotNil(allFiles) + let allFilesPolicy = allFiles?.first + XCTAssertEqual("two", allFilesPolicy?.identifier) + XCTAssertEqual("twoReq", allFilesPolicy?.codeRequirement) + XCTAssertEqual("bundleID", allFilesPolicy?.identifierType) + XCTAssertNil(allFilesPolicy?.receiverIdentifier) + XCTAssertNil(allFilesPolicy?.receiverCodeRequirement) + XCTAssertNil(allFilesPolicy?.receiverIdentifierType) + XCTAssertTrue(allFilesPolicy?.authorization == .allowStandardUserToSetSystemService) + } + } + + //swiftlint:disable:next function_body_length + func testExportProfileWithAppleEventsAndLegacyAllowed() { + // given + let exe1 = Executable(identifier: "one", codeRequirement: "oneReq") + let exe2 = Executable(identifier: "two", codeRequirement: "twoReq") + + exe1.appleEvents = [AppleEventRule(source: exe1, destination: exe2, value: true)] + exe2.policy.SystemPolicyAllFiles = "Allow" + + model.selectedExecutables = [exe1, exe2] + model.usingLegacyAllowKey = true + + // when + let profile = model.exportProfile(organization: "Org", identifier: "ID", displayName: "Name", payloadDescription: "Desc") + + // then check top level settings + XCTAssertEqual("Org", profile.organization) + XCTAssertEqual("ID", profile.identifier) + XCTAssertEqual("Name", profile.displayName) + XCTAssertEqual("Desc", profile.payloadDescription) + XCTAssertEqual("System", profile.scope) + XCTAssertEqual("Configuration", profile.type) + XCTAssertNotNil(profile.uuid) + XCTAssertEqual(1, profile.version) + + // then check policy settings + // then verify the payload content top level + XCTAssertEqual(1, profile.content.count) + profile.content.forEach { content in + XCTAssertNotNil(content.uuid) + XCTAssertEqual(1, content.version) + + // then verify the services + XCTAssertEqual(2, content.services.count) + let appleEvents = content.services["AppleEvents"] + XCTAssertNotNil(appleEvents) + let appleEventsPolicy = appleEvents?.first + XCTAssertEqual("one", appleEventsPolicy?.identifier) + XCTAssertEqual("oneReq", appleEventsPolicy?.codeRequirement) + XCTAssertEqual("bundleID", appleEventsPolicy?.identifierType) + XCTAssertEqual("two", appleEventsPolicy?.receiverIdentifier) + XCTAssertEqual("twoReq", appleEventsPolicy?.receiverCodeRequirement) + XCTAssertEqual("bundleID", appleEventsPolicy?.receiverIdentifierType) + XCTAssertTrue(appleEventsPolicy?.allowed == true) + XCTAssertNil(appleEventsPolicy?.authorization) + + let allFiles = content.services["SystemPolicyAllFiles"] + XCTAssertNotNil(allFiles) + let allFilesPolicy = allFiles?.first + XCTAssertEqual("two", allFilesPolicy?.identifier) + XCTAssertEqual("twoReq", allFilesPolicy?.codeRequirement) + XCTAssertEqual("bundleID", allFilesPolicy?.identifierType) + XCTAssertNil(allFilesPolicy?.receiverIdentifier) + XCTAssertNil(allFilesPolicy?.receiverCodeRequirement) + XCTAssertNil(allFilesPolicy?.receiverIdentifierType) + XCTAssertTrue(allFilesPolicy?.allowed == true) + XCTAssertNil(allFilesPolicy?.authorization) + } + } + + // MARK: - tests for importProfile + + func testImportProfileUsingAuthorizationKeyAllow() { + // given + let profile = TCCProfileBuilder().buildProfile(authorization: .allow) + + // when + model.importProfile(tccProfile: profile) + + // then + XCTAssertEqual(1, model.selectedExecutables.count) + XCTAssertEqual("Allow", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) + } + + func testImportProfileUsingAuthorizationKeyDeny() { + // given + let profile = TCCProfileBuilder().buildProfile(authorization: .deny) + + // when + model.importProfile(tccProfile: profile) + + // then + XCTAssertEqual(1, model.selectedExecutables.count) + XCTAssertEqual("Deny", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) + } + + func testImportProfileUsingAuthorizationKeyAllowStandardUsers() { + // given + let profile = TCCProfileBuilder().buildProfile(authorization: .allowStandardUserToSetSystemService) + + // when + model.importProfile(tccProfile: profile) + + // then + XCTAssertEqual(1, model.selectedExecutables.count) + XCTAssertEqual("Let Standard Users Approve", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) + } + + func testImportProfileUsingLegacyAllowKeyTrue() { + // given + let profile = TCCProfileBuilder().buildProfile(allowed: true) + + // when + model.importProfile(tccProfile: profile) + + // then + XCTAssertEqual(1, model.selectedExecutables.count) + XCTAssertEqual("Allow", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) + } + + func testImportProfileUsingLegacyAllowKeyFalse() { + // given + let profile = TCCProfileBuilder().buildProfile(allowed: false) + + // when + model.importProfile(tccProfile: profile) + + // then + XCTAssertEqual(1, model.selectedExecutables.count) + XCTAssertEqual("Deny", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) + } + + func testImportProfileUsingAuthorizationKeyThatIsInvalid() { + // given + let profile = TCCProfileBuilder().buildProfile(authorization: "invalidkey") + + // when + model.importProfile(tccProfile: profile) + + // then + XCTAssertEqual(1, model.selectedExecutables.count) + XCTAssertEqual("Deny", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) + } + + func testImportProfileUsingAuthorizationKeyTranslatesToAppleEvents() { + // given + let profile = TCCProfileBuilder().buildProfile(authorization: "deny") + + // when + model.importProfile(tccProfile: profile) + + // then + XCTAssertEqual(1, model.selectedExecutables.count) + XCTAssertEqual("Deny", model.selectedExecutables.first?.policy.SystemPolicyAllFiles) + } + + // MARK: - tests for profileToString + + func testPolicyWhenUsingAllowAndAuthorizationKey() { + // given + model.usingLegacyAllowKey = false + let app = Executable(identifier: "id", codeRequirement: "req") + + // when + let policy = model.policyFromString(executable: app, value: "Allow") + + // then + XCTAssertEqual(policy?.authorization, TCCPolicyAuthorizationValue.allow) + XCTAssertNil(policy?.allowed) + } + + func testPolicyWhenUsingDeny() { + // given + model.usingLegacyAllowKey = false + let app = Executable(identifier: "id", codeRequirement: "req") + + // when + let policy = model.policyFromString(executable: app, value: "Deny") + + // then + XCTAssertEqual(policy?.authorization, TCCPolicyAuthorizationValue.deny) + XCTAssertNil(policy?.allowed) + } + + func testPolicyWhenUsingAllowForStandardUsers() { + // given + model.usingLegacyAllowKey = false + let app = Executable(identifier: "id", codeRequirement: "req") + + // when + let policy = model.policyFromString(executable: app, value: "Let Standard Users Approve") + + // then + XCTAssertEqual(policy?.authorization, TCCPolicyAuthorizationValue.allowStandardUserToSetSystemService) + XCTAssertNil(policy?.allowed) + } + + func testPolicyWhenUsingUnknownValue() { + // given + let app = Executable(identifier: "id", codeRequirement: "req") + + // when + let policy = model.policyFromString(executable: app, value: "For MDM Admins Only") + + // then + XCTAssertNil(policy, "should have not created the policy with an unknown value") + } + + func testPolicyWhenUsingLegacyDeny() { + // given + let app = Executable(identifier: "id", codeRequirement: "req") + model.usingLegacyAllowKey = true + + // when + let policy = model.policyFromString(executable: app, value: "Deny") + + // then + XCTAssertNil(policy?.authorization, "should not set authorization when in legacy mode") + XCTAssertEqual(policy?.allowed, false) + } + + func testPolicyWhenUsingLegacyAllow() { + // given + let app = Executable(identifier: "id", codeRequirement: "req") + model.usingLegacyAllowKey = true + + // when + let policy = model.policyFromString(executable: app, value: "Allow") + + // then + XCTAssertNil(policy?.authorization, "should not set authorization when in legacy mode") + XCTAssertEqual(policy?.allowed, true) + } + + // test for the unrecognized strings for both legacy and normal + func testPolicyWhenUsingLegacyAllowButNonLegacyValueUsed() { + // given + let app = Executable(identifier: "id", codeRequirement: "req") + model.usingLegacyAllowKey = true + + // when + let policy = model.policyFromString(executable: app, value: "Let Standard Users Approve") + + // then + XCTAssertNil(policy, "should have errored out because of an invalid value") + } + + // MARK: - tests for requiresAuthorizationKey + + func testWhenServiceIsUsingAllowStandarUsersToApprove() { + // given + let profile = TCCProfileBuilder().buildProfile(authorization: .allowStandardUserToSetSystemService) + + // when + model.importProfile(tccProfile: profile) + + // then + XCTAssertTrue(model.requiresAuthorizationKey()) + } + + func testWhenServiceIsUsingOnlyAllowKey() { + // given + let profile = TCCProfileBuilder().buildProfile(authorization: .allow) + + // when + model.importProfile(tccProfile: profile) + + // then + XCTAssertFalse(model.requiresAuthorizationKey()) + } + + func testWhenServiceIsUsingOnlyDenyKey() { + // given + let profile = TCCProfileBuilder().buildProfile(authorization: .deny) + + // when + model.importProfile(tccProfile: profile) + + // then + XCTAssertFalse(model.requiresAuthorizationKey()) + } + + // MARK: - tests for changeToUseLegacyAllowKey + + func testChangingFromAuthorizationKeyToLegacyAllowKey() { + // given + let allowStandard = TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue + let exeSettings = ["AddressBook": "Allow", "ListenEvent": allowStandard, "ScreenCapture": allowStandard] + let model = ModelBuilder().addExecutable(settings: exeSettings).build() + model.usingLegacyAllowKey = false + + // when + model.changeToUseLegacyAllowKey() + + // then + XCTAssertEqual(1, model.selectedExecutables.count, "should have only one exe") + let policy = model.selectedExecutables.first?.policy + XCTAssertEqual("Allow", policy?.AddressBook) + XCTAssertEqual("-", policy?.Camera) + XCTAssertEqual("-", policy?.ListenEvent) + XCTAssertEqual("-", policy?.ScreenCapture) + XCTAssertTrue(model.usingLegacyAllowKey) + } + + func testChangingFromAuthorizationKeyToLegacyAllowKeyWithMoreComplexVaues() { + // given + let allowStandard = TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue + let p1Settings = ["SystemPolicyAllFiles": "Allow", + "ListenEvent": allowStandard, + "ScreenCapture": "Deny", + "Camera": "Deny"] + + let p2Settings = ["SystemPolicyAllFiles": "Deny", + "ScreenCapture": allowStandard, + "Calendar": "Allow"] + let builder = ModelBuilder().addExecutable(settings: p1Settings) + model = builder.addExecutable(settings: p2Settings).build() + model.usingLegacyAllowKey = false + + // when + model.changeToUseLegacyAllowKey() + + // then + XCTAssertEqual(2, model.selectedExecutables.count, "should have only one exe") + let policy1 = model.selectedExecutables[0].policy + XCTAssertEqual("Allow", policy1.SystemPolicyAllFiles) + XCTAssertEqual("-", policy1.ListenEvent) + XCTAssertEqual("Deny", policy1.ScreenCapture) + XCTAssertEqual("Deny", policy1.Camera) + + let policy2 = model.selectedExecutables[1].policy + XCTAssertEqual("Deny", policy2.SystemPolicyAllFiles) + XCTAssertEqual("-", policy2.ListenEvent) + XCTAssertEqual("-", policy2.ScreenCapture) + XCTAssertEqual("Allow", policy2.Calendar) + XCTAssertTrue(model.usingLegacyAllowKey) + } + } diff --git a/PPPC UtilityTests/TCCProfileImporter/TCCProfileTests.swift b/PPPC UtilityTests/TCCProfileImporter/TCCProfileTests.swift new file mode 100644 index 0000000..bd189dd --- /dev/null +++ b/PPPC UtilityTests/TCCProfileImporter/TCCProfileTests.swift @@ -0,0 +1,175 @@ +// +// TCCProfileTests.swift +// +// MIT License +// +// Copyright (c) 2019 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +import XCTest + +@testable import PPPC_Utility + +class TCCProfileTests: XCTestCase { + + // MARK: - tests for serializing to and from xml + + func testSerializationOfComplexProfileUsingAuthorization() throws { + // when we export to xml and reimport it should still have the same attributes + let plistData = try TCCProfileBuilder().buildProfile(authorization: .allowStandardUserToSetSystemService).xmlData() + let profile = try TCCProfile.parse(from: plistData) + + // then verify the config profile top level + XCTAssertEqual("Configuration", profile.type) + XCTAssertEqual(100, profile.version) + XCTAssertEqual("the uuid", profile.uuid) + XCTAssertEqual("System", profile.scope) + XCTAssertEqual("Test Org", profile.organization) + XCTAssertEqual("Test ID", profile.identifier) + XCTAssertEqual("Test Name", profile.displayName) + XCTAssertEqual("Test Desc", profile.payloadDescription) + + // then verify the payload content top level + XCTAssertEqual(1, profile.content.count) + profile.content.forEach { content in + XCTAssertEqual("Content Desc 1", content.payloadDescription) + XCTAssertEqual("Content Name 1", content.displayName) + XCTAssertEqual("Content ID 1", content.identifier) + XCTAssertEqual("Content Org 1", content.organization) + XCTAssertEqual("Content type 1", content.type) + XCTAssertEqual("Content UUID 1", content.uuid) + XCTAssertEqual(1, content.version) + + // then verify the services key + XCTAssertEqual(2, content.services.count) + let allFiles = content.services["SystemPolicyAllFiles"] + XCTAssertEqual(1, allFiles?.count) + allFiles?.forEach { policy in + XCTAssertEqual("policy id", policy.identifier) + XCTAssertEqual("policy id type", policy.identifierType) + XCTAssertEqual("policy code req", policy.codeRequirement) + XCTAssertNil(policy.allowed) + XCTAssertEqual(TCCPolicyAuthorizationValue.allowStandardUserToSetSystemService, policy.authorization) + XCTAssertEqual("policy comment", policy.comment) + XCTAssertEqual("policy receiver id", policy.receiverIdentifier) + XCTAssertEqual("policy receiver id type", policy.receiverIdentifierType) + XCTAssertEqual("policy receiver code req", policy.receiverCodeRequirement) + } + } + } + + func testSerializationOfProfileUsingLegacyAllowedKey() throws { + // when we export to xml and reimport it should still have the same attributes + let plistData = try TCCProfileBuilder().buildProfile(allowed: true).xmlData() + let profile = try TCCProfile.parse(from: plistData) + + // then verify the config profile top level + XCTAssertEqual("Configuration", profile.type) + XCTAssertEqual(100, profile.version) + XCTAssertEqual("the uuid", profile.uuid) + XCTAssertEqual("System", profile.scope) + XCTAssertEqual("Test Org", profile.organization) + XCTAssertEqual("Test ID", profile.identifier) + XCTAssertEqual("Test Name", profile.displayName) + XCTAssertEqual("Test Desc", profile.payloadDescription) + + // then verify the payload content top level + XCTAssertEqual(1, profile.content.count) + profile.content.forEach { content in + XCTAssertEqual("Content Desc 1", content.payloadDescription) + XCTAssertEqual("Content Name 1", content.displayName) + XCTAssertEqual("Content ID 1", content.identifier) + XCTAssertEqual("Content Org 1", content.organization) + XCTAssertEqual("Content type 1", content.type) + XCTAssertEqual("Content UUID 1", content.uuid) + XCTAssertEqual(1, content.version) + + // then verify the services key + XCTAssertEqual(2, content.services.count) + let allFiles = content.services["SystemPolicyAllFiles"] + XCTAssertEqual(1, allFiles?.count) + allFiles?.forEach { policy in + XCTAssertEqual("policy id", policy.identifier) + XCTAssertEqual("policy id type", policy.identifierType) + XCTAssertEqual("policy code req", policy.codeRequirement) + XCTAssertEqual(true, policy.allowed) + XCTAssertNil(policy.authorization) + XCTAssertEqual("policy comment", policy.comment) + XCTAssertEqual("policy receiver id", policy.receiverIdentifier) + XCTAssertEqual("policy receiver id type", policy.receiverIdentifierType) + XCTAssertEqual("policy receiver code req", policy.receiverCodeRequirement) + } + } + } + + func testSerializationOfProfileWhenBothAllowedAndAuthorizationUsed() throws { + // when we export to xml and reimport it should still have the same attributes + let plistData = try TCCProfileBuilder().buildProfile(allowed: false, authorization: .allow).xmlData() + let profile = try TCCProfile.parse(from: plistData) + + // then verify the config profile top level + XCTAssertEqual("Configuration", profile.type) + + // then verify the payload content top level + XCTAssertEqual(1, profile.content.count) + profile.content.forEach { content in + XCTAssertEqual("Content UUID 1", content.uuid) + XCTAssertEqual(1, content.version) + + // then verify the services key + XCTAssertEqual(2, content.services.count) + let allFiles = content.services["SystemPolicyAllFiles"] + XCTAssertEqual(1, allFiles?.count) + allFiles?.forEach { policy in + XCTAssertEqual(false, policy.allowed) + XCTAssertEqual(policy.authorization, TCCPolicyAuthorizationValue.allow) + } + } + } + + // unit tests for handling both Auth and allowed keys should fail? + + func testSettingLegacyAllowValueNullifiesAuthorization() { + // given + var tccPolicy = TCCPolicy(identifier: "id", codeRequirement: "req", receiverIdentifier: "recId", receiverCodeRequirement: "recreq") + tccPolicy.authorization = .allow + + // when + tccPolicy.allowed = true + + // then + XCTAssertNil(tccPolicy.authorization) + XCTAssertTrue(tccPolicy.allowed!) + } + + func testSettingAuthorizationValueDoesNotNullifyAllowed() { + // given + var tccPolicy = TCCPolicy(identifier: "id", codeRequirement: "req", receiverIdentifier: "recId", receiverCodeRequirement: "recreq") + tccPolicy.allowed = false + + // when + tccPolicy.authorization = .allowStandardUserToSetSystemService + + // then + XCTAssertEqual(tccPolicy.allowed, false, "we don't have to nil this out because we use authorization by default if present") + XCTAssertEqual(tccPolicy.authorization, TCCPolicyAuthorizationValue.allowStandardUserToSetSystemService) + } + +} diff --git a/README.md b/README.md index 401a2f3..79b07ad 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [logo]: /Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32%402x.png "PPPC Utility" -PPPC Utility is a macOS (10.13 and newer) application for creating configuration profiles containing the Privacy Preferences Policy Control payload for macOS. The profiles can be saved locally, signed or unsigned. Profiles can also be uploaded directly to a Jamf Pro server. +PPPC Utility is a macOS (10.15 and newer) application for creating configuration profiles containing the Privacy Preferences Policy Control payload for macOS. The profiles can be saved locally, signed or unsigned. Profiles can also be uploaded directly to a Jamf Pro server. All changes to the application are tracked in [the changelog](https://github.com/jamf/PPPC-Utility/blob/master/CHANGELOG.md). diff --git a/Resources/Base.lproj/Main.storyboard b/Resources/Base.lproj/Main.storyboard index 645daf1..7cca1d4 100644 --- a/Resources/Base.lproj/Main.storyboard +++ b/Resources/Base.lproj/Main.storyboard @@ -1,7 +1,6 @@ - @@ -788,13 +787,13 @@ - + - + @@ -802,7 +801,7 @@ - + @@ -852,7 +851,7 @@ - + @@ -860,9 +859,9 @@ - + - + @@ -914,7 +913,7 @@ - + @@ -922,7 +921,7 @@ - + @@ -972,7 +971,7 @@ - + @@ -980,7 +979,7 @@ - + @@ -1030,7 +1029,7 @@ - + @@ -1038,7 +1037,7 @@ - + @@ -1090,7 +1089,7 @@ - + @@ -1098,7 +1097,7 @@ - + @@ -1147,7 +1146,7 @@ - + @@ -1155,7 +1154,7 @@ - + @@ -1204,7 +1203,7 @@ - + @@ -1212,7 +1211,7 @@ - + @@ -1261,7 +1260,7 @@ - + @@ -1269,7 +1268,7 @@ - + @@ -1318,7 +1317,7 @@ - + @@ -1326,7 +1325,7 @@ - + @@ -1375,7 +1374,7 @@ - + @@ -1383,7 +1382,7 @@ - + @@ -1432,7 +1431,7 @@ - + @@ -1440,7 +1439,7 @@ - + @@ -1489,7 +1488,7 @@ - + @@ -1497,7 +1496,7 @@ - + @@ -1546,7 +1545,7 @@ - + @@ -1554,7 +1553,7 @@ - + @@ -1603,7 +1602,7 @@ - + @@ -1611,7 +1610,7 @@ - + @@ -1661,7 +1660,7 @@ - + @@ -1669,7 +1668,7 @@ - + @@ -1719,7 +1718,7 @@ - + @@ -1727,7 +1726,7 @@ - + @@ -1777,7 +1776,7 @@ - + @@ -1785,7 +1784,7 @@ - + @@ -1834,7 +1833,7 @@ - + @@ -1842,7 +1841,7 @@ - + @@ -1891,7 +1890,7 @@ - + @@ -1899,7 +1898,7 @@ - + @@ -2080,7 +2079,7 @@ - + @@ -2103,7 +2102,7 @@ - + @@ -2119,7 +2118,7 @@ - + @@ -2133,7 +2132,7 @@ - + @@ -2153,6 +2152,7 @@ + @@ -2228,7 +2228,7 @@ - + @@ -2247,9 +2247,8 @@ - - - + + @@ -2258,8 +2257,6 @@ - - @@ -2290,6 +2287,7 @@ + @@ -2323,7 +2321,7 @@ - + @@ -2371,7 +2369,7 @@ - + @@ -2380,19 +2378,6 @@ - @@ -2401,72 +2386,79 @@ - - - - + + - + - - + + - - - + - - + + + - + - @@ -2487,6 +2479,7 @@ + @@ -2535,7 +2528,6 @@ - @@ -2544,6 +2536,7 @@ + @@ -2587,7 +2580,7 @@ - + @@ -2603,7 +2596,7 @@ - + @@ -2622,9 +2615,8 @@ - + - @@ -2654,6 +2646,9 @@ + + + @@ -2665,6 +2660,7 @@ + @@ -2882,7 +2878,7 @@ - + @@ -3097,10 +3093,6 @@ - + - - + @@ -3130,10 +3121,7 @@ - - - - + @@ -3150,13 +3138,16 @@ - - - + + + - - + + + + + @@ -3226,6 +3217,7 @@ Gw + @@ -3457,6 +3449,7 @@ Gw + diff --git a/Source/Model/AppleEventRule.swift b/Source/Model/AppleEventRule.swift index 537538e..48bbbe4 100644 --- a/Source/Model/AppleEventRule.swift +++ b/Source/Model/AppleEventRule.swift @@ -31,13 +31,13 @@ class AppleEventRule: NSObject { @objc dynamic var source: Executable! @objc dynamic var destination: Executable! - @objc dynamic var valueString: String! = "Allow" + @objc dynamic var valueString: String! = TCCProfileDisplayValue.allow.rawValue - var value: Bool { return valueString == "Allow" } + var value: Bool { return valueString == TCCProfileDisplayValue.allow.rawValue } init(source: Executable, destination: Executable, value: Bool) { self.source = source self.destination = destination - self.valueString = value ? "Allow" : "Deny" + self.valueString = value ? TCCProfileDisplayValue.allow.rawValue : TCCProfileDisplayValue.deny.rawValue } } diff --git a/Source/Model/Executable.swift b/Source/Model/Executable.swift index fd0c1e6..5eb2971 100644 --- a/Source/Model/Executable.swift +++ b/Source/Model/Executable.swift @@ -97,4 +97,12 @@ class Policy: NSObject { @objc dynamic var SystemPolicyNetworkVolumes: String = "-" @objc dynamic var SystemPolicyRemovableVolumes: String = "-" // swiftlint:enable identifier_name + + func allPolicyValues() -> [String] { + let mirror = Mirror(reflecting: self) + return mirror.children.compactMap { _, value in + return value as? String + } + } + } diff --git a/Source/Model/Model.swift b/Source/Model/Model.swift index f18e9a3..abb4835 100644 --- a/Source/Model/Model.swift +++ b/Source/Model/Model.swift @@ -27,10 +27,11 @@ import Cocoa -class Model: NSObject { +@objc class Model: NSObject { - @objc dynamic var current: Executable? + var usingLegacyAllowKey = true + @objc dynamic var current: Executable? @objc dynamic static let shared = Model() @objc dynamic var identities: [SigningIdentity] = [] @objc dynamic var selectedExecutables: [Executable] = [] @@ -89,6 +90,27 @@ typealias LoadExecutableCompletion = ((LoadExecutableResult) -> Void) extension Model { + func requiresAuthorizationKey() -> Bool { + return selectedExecutables.contains { exe -> Bool in + return exe.policy.allPolicyValues().contains { value -> Bool in + return value == TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue + } + } + } + + /// Will convert any Authorization key values to the legacy Allowed key + func changeToUseLegacyAllowKey() { + usingLegacyAllowKey = true + selectedExecutables.forEach { exe in + if exe.policy.ListenEvent == TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue { + exe.policy.ListenEvent = "-" + } + if exe.policy.ScreenCapture == TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue { + exe.policy.ScreenCapture = "-" + } + } + } + // TODO - refactor this method so it isn't so complex // swiftlint:disable:next cyclomatic_complexity func loadExecutable(url: URL, completion: @escaping LoadExecutableCompletion) { @@ -114,10 +136,14 @@ extension Model { if !FileManager.default.fileExists(atPath: executable.iconPath) { switch url.pathExtension { - case "app": executable.iconPath = IconFilePath.application - case "bundle": executable.iconPath = IconFilePath.kext - case "xpc": executable.iconPath = IconFilePath.kext - default: executable.iconPath = IconFilePath.unknown + case "app": + executable.iconPath = IconFilePath.application + case "bundle": + executable.iconPath = IconFilePath.kext + case "xpc": + executable.iconPath = IconFilePath.kext + default: + executable.iconPath = IconFilePath.unknown } } } else { @@ -164,15 +190,12 @@ extension Model { } executable.appleEvents.forEach { event in - let policy = TCCPolicy(identifier: executable.identifier, - codeRequirement: executable.codeRequirement, - allowed: event.value, - receiverIdentifier: event.destination.identifier, - receiverCodeRequirement: event.destination.codeRequirement) - let appleEventsKey = ServicesKeys.appleEvents.rawValue - services[appleEventsKey] = services[appleEventsKey] ?? [] - services[appleEventsKey]?.append(policy) - + let policy = policyFromString(executable: executable, value: event.valueString, event: event) + if let policy = policy { + let appleEventsKey = ServicesKeys.appleEvents.rawValue + services[appleEventsKey] = services[appleEventsKey] ?? [] + services[appleEventsKey]?.append(policy) + } } } @@ -195,16 +218,21 @@ extension Model { for policy in policies { let executable = getExecutableFromSelectedExecutables(bundleIdentifier: policy.identifier) if key == ServicesKeys.appleEvents.rawValue { - if let source = executable, let rIdentifier = policy.receiverIdentifier, let rCodeRequirement = policy.receiverCodeRequirement { + if let source = executable, + let rIdentifier = policy.receiverIdentifier, + let rCodeRequirement = policy.receiverCodeRequirement { let destination = getExecutableFrom(identifier: rIdentifier, codeRequirement: rCodeRequirement) - let appleEvent = AppleEventRule(source: source, destination: destination, value: policy.allowed) + let allowed: Bool = (policy.allowed == true || policy.authorization == TCCPolicyAuthorizationValue.allow) + let appleEvent = AppleEventRule(source: source, destination: destination, value: allowed) executable?.appleEvents.appendIfNew(appleEvent) } } else { - if policy.allowed { - executable?.policy.setValue("Allow", forKey: key) + if policy.authorization == .allow || policy.allowed == true { + executable?.policy.setValue(TCCProfileDisplayValue.allow.rawValue, forKey: key) + } else if policy.authorization == .allowStandardUserToSetSystemService { + executable?.policy.setValue(TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue, forKey: key) } else { - executable?.policy.setValue("Deny", forKey: key) + executable?.policy.setValue(TCCProfileDisplayValue.deny.rawValue, forKey: key) } } } @@ -212,16 +240,33 @@ extension Model { } } - func policyFromString(executable: Executable, value: String) -> TCCPolicy? { - let allowed: Bool - switch value { - case "Allow": allowed = true - case "Deny": allowed = false - default: return nil - } - return TCCPolicy(identifier: executable.identifier, + func policyFromString(executable: Executable, value: String, event: AppleEventRule? = nil) -> TCCPolicy? { + var policy = TCCPolicy(identifier: executable.identifier, codeRequirement: executable.codeRequirement, - allowed: allowed) + receiverIdentifier: event?.destination.identifier, + receiverCodeRequirement: event?.destination.codeRequirement) + if usingLegacyAllowKey { + switch value { + case TCCProfileDisplayValue.allow.rawValue: + policy.allowed = true + case TCCProfileDisplayValue.deny.rawValue: + policy.allowed = false + default: + return nil + } + } else { + switch value { + case TCCProfileDisplayValue.allow.rawValue: + policy.authorization = .allow + case TCCProfileDisplayValue.deny.rawValue: + policy.authorization = .deny + case TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue: + policy.authorization = .allowStandardUserToSetSystemService + default: + return nil + } + } + return policy } func getExecutablesFromAllPolicies(policies: [TCCPolicy]) { diff --git a/Source/Model/TCCProfile.swift b/Source/Model/TCCProfile.swift index 61c49ae..f1ed9b0 100644 --- a/Source/Model/TCCProfile.swift +++ b/Source/Model/TCCProfile.swift @@ -28,33 +28,51 @@ import Foundation typealias TCCPolicyIdentifierType = String +typealias TCCPolicyAuthorizationValue = String extension TCCPolicyIdentifierType { static let bundleID = "bundleID" static let path = "path" } +extension TCCPolicyAuthorizationValue { + static let allow = "Allow" + static let deny = "Deny" + static let allowStandardUserToSetSystemService = "AllowStandardUserToSetSystemService" +} + struct TCCPolicy: Codable { var comment: String var identifier: String var identifierType: TCCPolicyIdentifierType var codeRequirement: String - var allowed: Bool + + /// legacy value to allow or deny a service. When setting this value, the authorization will be set to nil + /// as the values are mutually exclusive. If authorization is present it will always be used, so we have no + /// need to nil out this value if authorization is set. + var allowed: Bool? { + didSet { + authorization = nil + } + } + var authorization: TCCPolicyAuthorizationValue? var receiverIdentifier: String? var receiverIdentifierType: TCCPolicyIdentifierType? var receiverCodeRequirement: String? + enum CodingKeys: String, CodingKey { case identifier = "Identifier" case identifierType = "IdentifierType" case allowed = "Allowed" + case authorization = "Authorization" case codeRequirement = "CodeRequirement" case comment = "Comment" case receiverIdentifier = "AEReceiverIdentifier" case receiverIdentifierType = "AEReceiverIdentifierType" case receiverCodeRequirement = "AEReceiverCodeRequirement" } - init(identifier: String, codeRequirement: String, allowed: Bool, receiverIdentifier: String? = nil, receiverCodeRequirement: String? = nil) { - self.allowed = allowed + + init(identifier: String, codeRequirement: String, receiverIdentifier: String? = nil, receiverCodeRequirement: String? = nil) { self.comment = "" self.identifier = identifier self.identifierType = identifier.contains("/") ? .path : .bundleID diff --git a/Source/TCCProfileImporter/TCCProfileImporter.swift b/Source/TCCProfileImporter/TCCProfileImporter.swift index a1912ab..2fe71bb 100644 --- a/Source/TCCProfileImporter/TCCProfileImporter.swift +++ b/Source/TCCProfileImporter/TCCProfileImporter.swift @@ -38,22 +38,15 @@ public class TCCProfileImporter { /// Mapping & Decoding tcc profile /// /// - Parameter fileUrl: path with a file to load, completion: TCCProfileImportCompletion - success with TCCProfile or failure with TCCProfileImport Error - func decodeTCCProfile(fileUrl: URL, _ completion: @escaping TCCProfileImportCompletion) { - let data: Data - do { - data = try Data(contentsOf: fileUrl) - } catch { - return completion(.failure(.unableToOpenFile)) - } - + func decodeTCCProfile(data: Data, _ completion: @escaping TCCProfileImportCompletion) { do { // Note that parse will ignore the signing portion of the data let tccProfile = try TCCProfile.parse(from: data) - return completion(.success(tccProfile)) + return completion(.success(tccProfile)) } catch TCCProfile.ParseError.failedToCreateData { - return completion(.failure(.decodeProfileError)) + return completion(.failure(.decodeProfileError)) } catch TCCProfile.ParseError.failedToCreateDecoder { - return completion(.failure(.decodeProfileError)) + return completion(.failure(.decodeProfileError)) } catch let DecodingError.keyNotFound(codingKey, _) { return completion(TCCProfileImportResult.failure(.invalidProfileFile(description: codingKey.stringValue))) } catch let DecodingError.typeMismatch(type, context) { @@ -64,4 +57,18 @@ public class TCCProfileImporter { return completion(.failure(.invalidProfileFile(description: errorDescription ?? error.localizedDescription))) } } + + /// Mapping & Decoding tcc profile + /// + /// - Parameter fileUrl: path with a file to load, completion: TCCProfileImportCompletion - success with TCCProfile or failure with TCCProfileImport Error + func decodeTCCProfile(fileUrl: URL, _ completion: @escaping TCCProfileImportCompletion) { + let data: Data + do { + data = try Data(contentsOf: fileUrl) + return decodeTCCProfile(data: data, completion) + } catch { + return completion(.failure(.unableToOpenFile)) + } + + } } diff --git a/Source/View Controllers/TCCProfileViewController.swift b/Source/View Controllers/TCCProfileViewController.swift index e419d78..21271bb 100644 --- a/Source/View Controllers/TCCProfileViewController.swift +++ b/Source/View Controllers/TCCProfileViewController.swift @@ -27,6 +27,12 @@ import Cocoa +enum TCCProfileDisplayValue: String { + case allow = "Allow" + case deny = "Deny" + case allowStandardUsersToApprove = "Let Standard Users Approve" +} + class TCCProfileViewController: NSViewController { @objc dynamic var model = Model.shared @@ -122,17 +128,8 @@ class TCCProfileViewController: NSViewController { @IBOutlet weak var uploadButton: NSButton! @IBOutlet weak var addAppleEventButton: NSButton! @IBOutlet weak var removeAppleEventButton: NSButton! - - @IBOutlet weak var recordButton: NSButton! - - @IBAction func recordPressed(_ sender: NSButton) { - canEdit = !canEdit - if canEdit { - recordButton.title = "Record" - } else { - recordButton.title = "Stop" - } - } + @IBOutlet weak var removeExecutableButton: NSButton! + @IBOutlet weak var authorizationKeySwitch: NSSwitch! @IBAction func addToProfile(_ sender: NSButton) { promptForExecutables { @@ -140,16 +137,43 @@ class TCCProfileViewController: NSViewController { } } - // Binding currently deletes at index - @IBAction func removeButtonPressed(_ sender: NSButton) { - } - @IBAction func addToExecutable(_ sender: NSButton) { promptForExecutables { self.insertIntoAppleEvents($0) } } + private func toggleAuthorizationKey(theSwitch: NSSwitch, showAlert: Bool) { + switch theSwitch.state { + 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. + + 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 + default: + var allowToggle = true + if model.requiresAuthorizationKey() && showAlert { + let message = "Disabling Big Sur Compatibility will cause some settings you configured to be lost." + allowToggle = Alert().displayWithCancel(header: "Compatibility Warning", message: message) + } + if allowToggle { + model.changeToUseLegacyAllowKey() + } else { + theSwitch.state = .on + model.usingLegacyAllowKey = false + } + } + } + + @IBAction func toggleAuthorizationKeyUsage(_ sender: NSSwitch) { + toggleAuthorizationKey(theSwitch: sender, showAlert: true) + } + fileprivate func showAlert(_ error: LocalizedError, for window: NSWindow) { let alertWindow: NSAlert = NSAlert() alertWindow.messageText = "Operation Failed" @@ -159,6 +183,13 @@ class TCCProfileViewController: NSViewController { alertWindow.beginSheetModal(for: window) } + func turnOnBigSurCompatibilityIfImportedProfileNeedsTo() { + if model.requiresAuthorizationKey() { + authorizationKeySwitch.state = .on + toggleAuthorizationKey(theSwitch: authorizationKeySwitch, showAlert: false) + } + } + @IBAction func importProfile(_ sender: NSButton) { guard let window = self.view.window else { return @@ -168,12 +199,14 @@ class TCCProfileViewController: NSViewController { let tccConfigPanel = TCCProfileConfigurationPanel() tccConfigPanel.loadTCCProfileFromFile(importer: tccProfileImporter, window: window) { [weak self] tccProfileResult in + guard let weakSelf = self else { return } switch tccProfileResult { case .success(let tccProfile): - self?.model.importProfile(tccProfile: tccProfile) + weakSelf.model.importProfile(tccProfile: tccProfile) + weakSelf.turnOnBigSurCompatibilityIfImportedProfileNeedsTo() case .failure(let tccProfileImportError): if !tccProfileImportError.isCancelled { - self?.showAlert(tccProfileImportError, for: window) + weakSelf.showAlert(tccProfileImportError, for: window) } } } @@ -213,6 +246,13 @@ class TCCProfileViewController: NSViewController { .urlReadingContentsConformToTypes: [ kUTTypeBundle, kUTTypeExecutable ] ] + @IBAction func checkForAuthorizationFeaturesUsed(_ sender: NSPopUpButton) { + if model.requiresAuthorizationKey() { + authorizationKeySwitch.state = .on + toggleAuthorizationKeyUsage(authorizationKeySwitch) + } + } + override func viewDidLoad() { super.viewDidLoad() @@ -234,10 +274,12 @@ class TCCProfileViewController: NSViewController { networkVolumesPopUpAC, removableVolumesPopUpAC]) + setupStandardUserAllowAndDeny(policies: [screenCapturePopUpAC, + listenEventPopUpAC]) + setupActionForStandardUserAllowedDropDowns(dropDowns: [listenEventPopUp, screenCapturePopUp]) + setupDenyOnly(policies: [cameraPopUpAC, - microphonePopUpAC, - listenEventPopUpAC, - screenCapturePopUpAC]) + microphonePopUpAC]) setupDescriptions() @@ -258,22 +300,39 @@ class TCCProfileViewController: NSViewController { appleEventsTable.registerForDraggedTypes([.fileURL]) appleEventsTable.dataSource = self - // Record button + authorizationKeySwitch.state = model.usingLegacyAllowKey ? .off : .on } @IBAction func showHelpMessage(_ sender: InfoButton) { sender.showHelpMessage() } + /// Setup actions to display a warning when certain values are selected + /// that are not supported on all macOS versions + /// - Parameter dropDowns: NSPopupButtons to add the action to + private func setupActionForStandardUserAllowedDropDowns(dropDowns: [NSPopUpButton]) { + dropDowns.forEach { popup in + popup.action = #selector(self.checkForAuthorizationFeaturesUsed(_:)) + } + } + + private func setupStandardUserAllowAndDeny(policies: [NSArrayController]) { + for policy in policies { + policy.add(contentsOf: ["-", + TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue, + TCCProfileDisplayValue.deny.rawValue]) + } + } + private func setupAllowDeny(policies: [NSArrayController]) { for policy in policies { - policy.add(contentsOf: ["-", "Allow", "Deny"]) + policy.add(contentsOf: ["-", TCCProfileDisplayValue.allow.rawValue, TCCProfileDisplayValue.deny.rawValue]) } } private func setupDenyOnly(policies: [NSArrayController]) { for policy in policies { - policy.add(contentsOf: ["-", "Deny"]) + policy.add(contentsOf: ["-", TCCProfileDisplayValue.deny.rawValue]) } } @@ -292,12 +351,9 @@ class TCCProfileViewController: NSViewController { private func isDarkModeEnabled() -> Bool { var darkModeEnabled = false - if #available(OSX 10.14, *) { - if view.effectiveAppearance.name == .darkAqua { - darkModeEnabled = true - } + if view.effectiveAppearance.name == .darkAqua { + darkModeEnabled = true } - return darkModeEnabled } diff --git a/Source/Model/Alert.swift b/Source/Views/Alert.swift similarity index 67% rename from Source/Model/Alert.swift rename to Source/Views/Alert.swift index 7fccf00..89f50f0 100644 --- a/Source/Model/Alert.swift +++ b/Source/Views/Alert.swift @@ -38,4 +38,24 @@ class Alert: NSObject { dialog.runModal() } } + + /// 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 + /// - Returns: True if the ok button was pressed + func displayWithCancel(header: String, message: String) -> Bool { + let dialog: NSAlert = NSAlert() + dialog.messageText = header + dialog.informativeText = message + dialog.alertStyle = NSAlert.Style.warning + dialog.addButton(withTitle: "OK") + dialog.addButton(withTitle: "Cancel") + let response = dialog.runModal() + let okPressed = (response.rawValue == 1000) + return okPressed + } + }