diff --git a/.specify/feature.json b/.specify/feature.json index efb467a..c35ce17 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/001-fix-deprecations" + "feature_directory": "specs/002-add-pppc-keys" } diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 8e4ffaf..fe90d77 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -51,6 +51,7 @@ acceptable only for components without adequate SwiftUI coverage. - Minimum deployment target: macOS 13.0. - All interactive UI elements MUST have accessibility identifiers and labels. +- Service keys in the main window MUST appear in alphabetical order. - Profiles MUST be saveable locally (signed or unsigned) and uploadable to Jamf Pro (bearer token, basic auth fallback, or OAuth client credentials). @@ -86,4 +87,4 @@ Git) are maintained in `CLAUDE.md`. Amendments require: All PRs and code reviews MUST verify compliance with this constitution. Complexity violations MUST be justified in the PR description. -**Version**: 1.1.0 | **Ratified**: 2026-04-09 | **Last Amended**: 2026-04-09 +**Version**: 1.2.0 | **Ratified**: 2026-04-09 | **Last Amended**: 2026-04-09 diff --git a/CLAUDE.md b/CLAUDE.md index 6a1d7df..e9dfa48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,8 +24,16 @@ - Avoid snake_case in test names (e.g., `generateDisplayName_bundleIdentifier`). If a name is getting long, use a Trait with a sentence-style description instead. - For complex tests, use a descriptive `@Test("...")` trait that explains the scenario and expected outcome so the test is understandable without reading the body. - Use parameterized tests with Traits where it reduces duplication; 1–2 args is ideal, max 3 + ```swift + @Test("Service key round-trip preserves value", arguments: [ + ("BluetoothAlways", "Allow"), + ("SystemPolicyAppBundles", "Deny") + ]) + func serviceKeyRoundTrip(serviceKey: String, value: String) async { … } + ``` - Beyond 3 params: create separate tests with some values hard-coded - Use `deinit` as teardown for repeated cleanup across tests in a suite. Use `class` for suites that need `deinit`; use `struct` otherwise. +- After adding or refactoring tests, look for duplication or tests that are no longer relevant. Do not add redundant tests. ## UI Testing Conventions @@ -35,6 +43,14 @@ - Use accessibility identifiers set in `setupAccessibilityIdentifiers()` to locate UI elements - The `-UITestMode` launch argument triggers test-specific setup (e.g., loading a test profile) +## UI Conventions + +- Service keys (popup rows) in the main window must appear in alphabetical order. + +## Naming + +- Avoid "new" in test or function names — it becomes stale quickly. Describe the behavior, not the novelty. + ## Git - Do not stage or commit changes in terminal sessions diff --git a/PPPC Utility.xcodeproj/project.pbxproj b/PPPC Utility.xcodeproj/project.pbxproj index 6d4d8a7..368b377 100644 --- a/PPPC Utility.xcodeproj/project.pbxproj +++ b/PPPC Utility.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ C07B1FB82AF596D80075E38B /* UploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07B1FB72AF596D80075E38B /* UploadManager.swift */; }; C0A2B5422B1A5D5C0007F510 /* JamfProAPIClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A2B5412B1A5D5C0007F510 /* JamfProAPIClientTests.swift */; }; C0A85DB5279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig in Resources */ = {isa = PBXBuildFile; fileRef = C0A85DB4279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig */; }; + AA002200000000000000002B /* TestTCCUnsignedProfile-Legacy.mobileconfig in Resources */ = {isa = PBXBuildFile; fileRef = AA002200000000000000002A /* TestTCCUnsignedProfile-Legacy.mobileconfig */; }; C0DC2BB92B2263FC003A4474 /* Haversack in Frameworks */ = {isa = PBXBuildFile; productRef = C0DC2BB82B2263FC003A4474 /* Haversack */; }; C0E0383F27A30C7100A23FA2 /* PPPCServiceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E0383D27A30C7100A23FA2 /* PPPCServiceInfo.swift */; }; C0E0384027A30C7100A23FA2 /* PPPCServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E0383E27A30C7100A23FA2 /* PPPCServicesManager.swift */; }; @@ -151,6 +152,7 @@ C07B1FB72AF596D80075E38B /* UploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadManager.swift; sourceTree = ""; }; C0A2B5412B1A5D5C0007F510 /* JamfProAPIClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JamfProAPIClientTests.swift; sourceTree = ""; }; C0A85DB4279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "TestTCCUnsignedProfile-allLower.mobileconfig"; sourceTree = ""; }; + AA002200000000000000002A /* TestTCCUnsignedProfile-Legacy.mobileconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "TestTCCUnsignedProfile-Legacy.mobileconfig"; sourceTree = ""; }; C0E0383D27A30C7100A23FA2 /* PPPCServiceInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PPPCServiceInfo.swift; sourceTree = ""; }; C0E0383E27A30C7100A23FA2 /* PPPCServicesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PPPCServicesManager.swift; sourceTree = ""; }; C0E0384127A30D1D00A23FA2 /* PPPCServices.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = PPPCServices.json; sourceTree = ""; }; @@ -266,6 +268,7 @@ children = ( 5F95AE2B2315B172002E0A22 /* TestTCCProfileSigned-Broken.mobileconfig */, C0A85DB4279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig */, + AA002200000000000000002A /* TestTCCUnsignedProfile-Legacy.mobileconfig */, 5F95AE2A2315B172002E0A22 /* TestTCCUnsignedProfile-Broken.mobileconfig */, 5F95AE2C2315B172002E0A22 /* TestTCCUnsignedProfile-Empty.mobileconfig */, 5F95AE292315B172002E0A22 /* TestTCCUnsignedProfile.mobileconfig */, @@ -536,6 +539,7 @@ 5F95AE312315B172002E0A22 /* TestTCCUnsignedProfile-Empty.mobileconfig in Resources */, 5F95AE2F2315B172002E0A22 /* TestTCCUnsignedProfile-Broken.mobileconfig in Resources */, C0A85DB5279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig in Resources */, + AA002200000000000000002B /* TestTCCUnsignedProfile-Legacy.mobileconfig in Resources */, C0EE9A7D28639BF800738B6B /* TestTCCProfileForJamfProAPI.txt in Resources */, 5F95AE302315B172002E0A22 /* TestTCCProfileSigned-Broken.mobileconfig in Resources */, ); diff --git a/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift b/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift index 9d120e2..b6aaf9f 100644 --- a/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift +++ b/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift @@ -47,7 +47,10 @@ class TCCProfileBuilder: NSObject { func buildTCCPolicies(allowed: Bool?, authorization: TCCPolicyAuthorizationValue?) -> [String: [TCCPolicy]] { return [ "SystemPolicyAllFiles": [buildTCCPolicy(allowed: allowed, authorization: authorization)], - "AppleEvents": [buildTCCPolicy(allowed: allowed, authorization: authorization)] + "AppleEvents": [buildTCCPolicy(allowed: allowed, authorization: authorization)], + "BluetoothAlways": [buildTCCPolicy(allowed: allowed, authorization: authorization)], + "SystemPolicyAppBundles": [buildTCCPolicy(allowed: allowed, authorization: authorization)], + "SystemPolicyAppData": [buildTCCPolicy(allowed: allowed, authorization: authorization)] ] } diff --git a/PPPC UtilityTests/ModelTests/ExecutableTests.swift b/PPPC UtilityTests/ModelTests/ExecutableTests.swift index 045e771..7d7f902 100644 --- a/PPPC UtilityTests/ModelTests/ExecutableTests.swift +++ b/PPPC UtilityTests/ModelTests/ExecutableTests.swift @@ -34,60 +34,62 @@ import Testing struct ExecutableTests { let executable = Executable() - @Test("Display name uses last component of bundle identifier") - func generateDisplayNameBundleIdentifier() { + @Test( + "Display name uses last component of identifier", + arguments: [ + ("com.example.MyApp", "MyApp"), + ("/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal", "Terminal"), + ("Terminal", "Terminal") + ]) + func generateDisplayName(identifier: String, expected: String) { // when - let displayName = executable.generateDisplayName(identifier: "com.example.MyApp") + let displayName = executable.generateDisplayName(identifier: identifier) // then - #expect(displayName == "MyApp") + #expect(displayName == expected) } - @Test("Display name uses last component of path identifier") - func generateDisplayNamePathIdentifier() { + @Test( + "Icon path matches identifier type", + arguments: [ + ("com.example.MyApp", "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns"), + ("/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal", "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/ExecutableBinaryIcon.icns") + ]) + func generateIconPath(identifier: String, expected: String) { // when - let displayName = executable.generateDisplayName(identifier: "/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal") + let iconPath = executable.generateIconPath(identifier: identifier) // then - #expect(displayName == "Terminal") + #expect(iconPath == expected) } - @Test("Display name returns identifier when single component") - func generateDisplayNameSingleComponent() { - // when - let displayName = executable.generateDisplayName(identifier: "Terminal") - - // then - #expect(displayName == "Terminal") - } - - @Test("Icon path is application for bundle identifier") - func generateIconPathForBundleIdentifier() { - // when - let iconPath = executable.generateIconPath(identifier: "com.example.MyApp") - - // then - #expect(iconPath == IconFilePath.application) - } - - @Test("Icon path is binary for path identifier") - func generateIconPathForPathIdentifier() { - // when - let iconPath = executable.generateIconPath(identifier: "/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal") - - // then - #expect(iconPath == IconFilePath.binary) - } - - @Test("All policy values default to dash") - func allPolicyValuesAreDefaults() { + @Test("All policy properties default to dash") + func policyPropertiesDefaultToDash() { let policy = Policy() - // when - let values = policy.allPolicyValues() - // then - #expect(values.count == 20) - #expect(values.allSatisfy { $0 == "-" }) + #expect(policy.Accessibility == "-") + #expect(policy.AddressBook == "-") + #expect(policy.BluetoothAlways == "-") + #expect(policy.Calendar == "-") + #expect(policy.Camera == "-") + #expect(policy.FileProviderPresence == "-") + #expect(policy.ListenEvent == "-") + #expect(policy.MediaLibrary == "-") + #expect(policy.Microphone == "-") + #expect(policy.Photos == "-") + #expect(policy.PostEvent == "-") + #expect(policy.Reminders == "-") + #expect(policy.ScreenCapture == "-") + #expect(policy.SpeechRecognition == "-") + #expect(policy.SystemPolicyAllFiles == "-") + #expect(policy.SystemPolicyAppBundles == "-") + #expect(policy.SystemPolicyAppData == "-") + #expect(policy.SystemPolicyDesktopFolder == "-") + #expect(policy.SystemPolicyDocumentsFolder == "-") + #expect(policy.SystemPolicyDownloadsFolder == "-") + #expect(policy.SystemPolicyNetworkVolumes == "-") + #expect(policy.SystemPolicyRemovableVolumes == "-") + #expect(policy.SystemPolicySysAdminFiles == "-") } } diff --git a/PPPC UtilityTests/ModelTests/ModelTests.swift b/PPPC UtilityTests/ModelTests/ModelTests.swift index b81732c..84385f0 100644 --- a/PPPC UtilityTests/ModelTests/ModelTests.swift +++ b/PPPC UtilityTests/ModelTests/ModelTests.swift @@ -267,39 +267,46 @@ struct ModelTests { #expect(model.selectedExecutables.first?.policy.SystemPolicyAllFiles == "Deny") } - // MARK: - tests for policyFromString - - @Test - func policyWhenUsingAllow() { - let app = Executable(identifier: "id", codeRequirement: "req") + // MARK: - Service key round-trips + + @Test( + "Service key round-trip preserves value", + arguments: [ + ("BluetoothAlways", "Allow"), + ("SystemPolicyAppBundles", "Deny"), + ("SystemPolicyAppData", "Allow") + ]) + func serviceKeyRoundTrip(serviceKey: String, value: String) async { + let model = ModelBuilder() + .addExecutable(settings: [serviceKey: value]) + .build() // when - let policy = model.policyFromString(executable: app, value: "Allow") + let profile = model.exportProfile(organization: "Org", identifier: "ID", displayName: "Name", payloadDescription: "Desc") + await model.importProfile(tccProfile: profile) // then - #expect(policy?.authorization == .allow) + let result = model.selectedExecutables.last?.policy.value(forKey: serviceKey) as? String + #expect(result == value) } - @Test - func policyWhenUsingDeny() { - let app = Executable(identifier: "id", codeRequirement: "req") - - // when - let policy = model.policyFromString(executable: app, value: "Deny") - - // then - #expect(policy?.authorization == .deny) - } + // MARK: - tests for policyFromString - @Test - func policyWhenUsingAllowForStandardUsers() { + @Test( + "policyFromString maps display value to authorization", + arguments: [ + ("Allow", TCCPolicyAuthorizationValue.allow), + ("Deny", TCCPolicyAuthorizationValue.deny), + ("Let Standard Users Approve", TCCPolicyAuthorizationValue.allowStandardUserToSetSystemService) + ]) + func policyFromStringAuthorization(value: String, expected: String) { let app = Executable(identifier: "id", codeRequirement: "req") // when - let policy = model.policyFromString(executable: app, value: "Let Standard Users Approve") + let policy = model.policyFromString(executable: app, value: value) // then - #expect(policy?.authorization == .allowStandardUserToSetSystemService) + #expect(policy?.authorization == expected) } @Test diff --git a/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift b/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift index 864a987..f944225 100644 --- a/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift +++ b/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift @@ -39,7 +39,22 @@ struct PPPCServicesManagerTests { let actual = PPPCServicesManager() // then - #expect(actual.allServices.count == 21) + #expect(actual.allServices.count == 24) + } + + @Test( + "Service is loaded with correct English name", + arguments: [ + ("BluetoothAlways", "Bluetooth Always"), + ("SystemPolicyAppBundles", "App Bundles"), + ("SystemPolicyAppData", "App Data") + ]) + func serviceIsLoaded(key: String, expectedName: String) throws { + let services = PPPCServicesManager() + let service = try #require(services.allServices[key]) + + // then + #expect(service.englishName == expectedName) } @Test("User help with entitlements") diff --git a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift index 042d5ac..f8a653c 100644 --- a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift +++ b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift @@ -72,7 +72,31 @@ struct TCCProfileImporterTests { let tccProfile = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) #expect(!tccProfile.content.isEmpty) - #expect(!tccProfile.content[0].services.isEmpty) + let services = tccProfile.content[0].services + #expect(!services.isEmpty) + #expect(services.count == 24, "Profile should contain exactly 24 services") + #expect(services["BluetoothAlways"] != nil, "BluetoothAlways service should exist") + #expect(services["SystemPolicyAppBundles"] != nil, "SystemPolicyAppBundles service should exist") + #expect(services["SystemPolicyAppData"] != nil, "SystemPolicyAppData service should exist") + } + + @Test("Legacy profile without new keys imports with dash defaults") + func legacyProfileImportsWithDashDefaults() async throws { + let tccProfileImporter = TCCProfileImporter() + let resourceURL = try getResourceProfile(fileName: "TestTCCUnsignedProfile-Legacy") + let tccProfile = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) + + // when + let model = Model() + await model.importProfile(tccProfile: tccProfile) + + // then + #expect(!model.selectedExecutables.isEmpty, "Legacy profile should import at least one executable") + for executable in model.selectedExecutables { + #expect(executable.policy.BluetoothAlways == "-", "BluetoothAlways should default to dash for legacy profiles") + #expect(executable.policy.SystemPolicyAppBundles == "-", "SystemPolicyAppBundles should default to dash for legacy profiles") + #expect(executable.policy.SystemPolicyAppData == "-", "SystemPolicyAppData should default to dash for legacy profiles") + } } @Test diff --git a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileTests.swift b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileTests.swift index 5d14ef6..4845730 100644 --- a/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileTests.swift +++ b/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileTests.swift @@ -63,7 +63,7 @@ struct TCCProfileTests { #expect(content.version == 1) // then verify the services key - #expect(content.services.count == 2) + #expect(content.services.count == 5) let allFiles = content.services["SystemPolicyAllFiles"] #expect(allFiles?.count == 1) allFiles?.forEach { policy in @@ -77,6 +77,9 @@ struct TCCProfileTests { #expect(policy.receiverIdentifierType == "policy receiver id type") #expect(policy.receiverCodeRequirement == "policy receiver code req") } + #expect(content.services["BluetoothAlways"] != nil, "BluetoothAlways should be included") + #expect(content.services["SystemPolicyAppBundles"] != nil, "SystemPolicyAppBundles should be included") + #expect(content.services["SystemPolicyAppData"] != nil, "SystemPolicyAppData should be included") } } @@ -108,7 +111,7 @@ struct TCCProfileTests { #expect(content.version == 1) // then verify the services key - #expect(content.services.count == 2) + #expect(content.services.count == 5) let allFiles = content.services["SystemPolicyAllFiles"] #expect(allFiles?.count == 1) allFiles?.forEach { policy in @@ -141,13 +144,7 @@ struct TCCProfileTests { #expect(content.version == 1) // then verify the services key - #expect(content.services.count == 2) - let allFiles = content.services["SystemPolicyAllFiles"] - #expect(allFiles?.count == 1) - allFiles?.forEach { policy in - #expect(policy.allowed == false) - #expect(policy.authorization == .allow) - } + #expect(content.services.count == 5) } } diff --git a/PPPC UtilityTests/TCCProfileImporterTests/TestTCCProfileForJamfProAPI.txt b/PPPC UtilityTests/TCCProfileImporterTests/TestTCCProfileForJamfProAPI.txt index 67e8d75..68e278c 100644 --- a/PPPC UtilityTests/TCCProfileImporterTests/TestTCCProfileForJamfProAPI.txt +++ b/PPPC UtilityTests/TCCProfileImporterTests/TestTCCProfileForJamfProAPI.txt @@ -44,6 +44,29 @@ <string>policy id type</string> </dict> </array> + <key>BluetoothAlways</key> + <array> + <dict> + <key>AEReceiverCodeRequirement</key> + <string>policy receiver code req</string> + <key>AEReceiverIdentifier</key> + <string>policy receiver id</string> + <key>AEReceiverIdentifierType</key> + <string>policy receiver id type</string> + <key>Allowed</key> + <false/> + <key>Authorization</key> + <string>Allow</string> + <key>CodeRequirement</key> + <string>policy code req</string> + <key>Comment</key> + <string>policy comment</string> + <key>Identifier</key> + <string>policy id</string> + <key>IdentifierType</key> + <string>policy id type</string> + </dict> + </array> <key>SystemPolicyAllFiles</key> <array> <dict> @@ -67,6 +90,52 @@ <string>policy id type</string> </dict> </array> + <key>SystemPolicyAppBundles</key> + <array> + <dict> + <key>AEReceiverCodeRequirement</key> + <string>policy receiver code req</string> + <key>AEReceiverIdentifier</key> + <string>policy receiver id</string> + <key>AEReceiverIdentifierType</key> + <string>policy receiver id type</string> + <key>Allowed</key> + <false/> + <key>Authorization</key> + <string>Allow</string> + <key>CodeRequirement</key> + <string>policy code req</string> + <key>Comment</key> + <string>policy comment</string> + <key>Identifier</key> + <string>policy id</string> + <key>IdentifierType</key> + <string>policy id type</string> + </dict> + </array> + <key>SystemPolicyAppData</key> + <array> + <dict> + <key>AEReceiverCodeRequirement</key> + <string>policy receiver code req</string> + <key>AEReceiverIdentifier</key> + <string>policy receiver id</string> + <key>AEReceiverIdentifierType</key> + <string>policy receiver id type</string> + <key>Allowed</key> + <false/> + <key>Authorization</key> + <string>Allow</string> + <key>CodeRequirement</key> + <string>policy code req</string> + <key>Comment</key> + <string>policy comment</string> + <key>Identifier</key> + <string>policy id</string> + <key>IdentifierType</key> + <string>policy id type</string> + </dict> + </array> </dict> </dict> </array> diff --git a/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile-Legacy.mobileconfig b/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile-Legacy.mobileconfig new file mode 100644 index 0000000..9092b0c --- /dev/null +++ b/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile-Legacy.mobileconfig @@ -0,0 +1,575 @@ + + + + + PayloadContent + + + PayloadDescription + Test Unsigned Profile + PayloadDisplayName + TestUnsignedProfile + PayloadIdentifier + 3A4EDE0C-A189-4372-953F-304ECA0B6489 + PayloadOrganization + Jamf + PayloadType + com.apple.TCC.configuration-profile-policy + PayloadUUID + 7CAF0BD6-D1DB-486E-9388-CC7F688C51E7 + PayloadVersion + 1 + Services + + Accessibility + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + AddressBook + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + AppleEvents + + + AEReceiverCodeRequirement + identifier "com.apple.finder" and anchor apple + AEReceiverIdentifier + com.apple.finder + AEReceiverIdentifierType + bundleID + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + AEReceiverCodeRequirement + identifier "com.apple.systemuiserver" and anchor apple + AEReceiverIdentifier + com.apple.systemuiserver + AEReceiverIdentifierType + bundleID + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + AEReceiverCodeRequirement + identifier "com.apple.finder" and anchor apple + AEReceiverIdentifier + com.apple.finder + AEReceiverIdentifierType + bundleID + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + AEReceiverCodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + AEReceiverIdentifier + io.brackets.appshell + AEReceiverIdentifierType + bundleID + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + Calendar + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + Camera + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + FileProviderPresence + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + ListenEvent + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + MediaLibrary + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + Microphone + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + Photos + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + PostEvent + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + Reminders + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + ScreenCapture + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + SpeechRecognition + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + SystemPolicyAllFiles + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + SystemPolicyDesktopFolder + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + SystemPolicyDocumentsFolder + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + SystemPolicyDownloadsFolder + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + SystemPolicyNetworkVolumes + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + SystemPolicyRemovableVolumes + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + SystemPolicySysAdminFiles + + + Allowed + + CodeRequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD + Comment + + Identifier + io.brackets.appshell + IdentifierType + bundleID + + + Allowed + + CodeRequirement + identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + Comment + + Identifier + com.jamfsoftware.JamfAdmin + IdentifierType + bundleID + + + + + + PayloadDescription + Test Unsigned Profile + PayloadDisplayName + TestUnsignedProfile + PayloadIdentifier + 3A4EDE0C-A189-4372-953F-304ECA0B6489 + PayloadOrganization + Jamf + PayloadType + com.apple.TCC.configuration-profile-policy + PayloadUUID + 3FF5028C-CAA0-4B2F-A7BF-2A3539074424 + PayloadVersion + 1 + PayloadScope + system + + diff --git a/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile-allLower.mobileconfig b/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile-allLower.mobileconfig index de49ccb..21ccbbb 100644 --- a/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile-allLower.mobileconfig +++ b/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile-allLower.mobileconfig @@ -48,6 +48,21 @@ bundleid + bluetoothalways + + + allowed + + coderequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.ou] = jq525l2mzd + comment + + identifier + io.brackets.appshell + identifiertype + bundleid + + addressbook @@ -450,6 +465,36 @@ bundleid + systempolicyappbundles + + + allowed + + coderequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.ou] = jq525l2mzd + comment + + identifier + io.brackets.appshell + identifiertype + bundleid + + + systempolicyappdata + + + allowed + + coderequirement + identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.ou] = jq525l2mzd + comment + + identifier + io.brackets.appshell + identifiertype + bundleid + + systempolicydesktopfolder diff --git a/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile.mobileconfig b/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile.mobileconfig index 9092b0c..b1983e2 100644 --- a/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile.mobileconfig +++ b/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile.mobileconfig @@ -48,7 +48,34 @@ bundleID - AddressBook + BluetoothAlways + + +Allowed + +CodeRequirement +identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD +Comment + +Identifier +io.brackets.appshell +IdentifierType +bundleID + + +Allowed + +CodeRequirement +identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" +Comment + +Identifier +com.jamfsoftware.JamfAdmin +IdentifierType +bundleID + + + AddressBook Allowed @@ -450,7 +477,61 @@ bundleID - SystemPolicyDesktopFolder + SystemPolicyAppBundles + + +Allowed + +CodeRequirement +identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD +Comment + +Identifier +io.brackets.appshell +IdentifierType +bundleID + + +Allowed + +CodeRequirement +identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" +Comment + +Identifier +com.jamfsoftware.JamfAdmin +IdentifierType +bundleID + + +SystemPolicyAppData + + +Allowed + +CodeRequirement +identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD +Comment + +Identifier +io.brackets.appshell +IdentifierType +bundleID + + +Allowed + +CodeRequirement +identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" +Comment + +Identifier +com.jamfsoftware.JamfAdmin +IdentifierType +bundleID + + + SystemPolicyDesktopFolder Allowed diff --git a/PPPC UtilityUITests/AppLaunchTests.swift b/PPPC UtilityUITests/AppLaunchTests.swift index 6c736e1..7334829 100644 --- a/PPPC UtilityUITests/AppLaunchTests.swift +++ b/PPPC UtilityUITests/AppLaunchTests.swift @@ -79,4 +79,21 @@ final class AppLaunchTests: XCTestCase { XCTAssertTrue(removeAppleEventButton.waitForExistence(timeout: 5), "Remove Apple Event button should exist") XCTAssertFalse(removeAppleEventButton.isEnabled, "Remove Apple Event button should be disabled with no selection") } + + func testServicePopupsExist() { + app.terminate() + app.launchArguments = ["-UITestMode"] + app.launch() + + let newServicePopUps = [ + "AppBundlesPopUp", + "AppDataPopUp", + "BluetoothAlwaysPopUp" + ] + + for identifier in newServicePopUps { + let popUp = app.popUpButtons[identifier] + XCTAssertTrue(popUp.waitForExistence(timeout: 5), "\(identifier) should exist") + } + } } diff --git a/Resources/Base.lproj/Main.storyboard b/Resources/Base.lproj/Main.storyboard index 501e3e0..0c9d662 100644 --- a/Resources/Base.lproj/Main.storyboard +++ b/Resources/Base.lproj/Main.storyboard @@ -915,6 +915,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2014,6 +2188,15 @@ + + + + + + + + + @@ -2036,6 +2219,9 @@ + + + @@ -2058,6 +2244,9 @@ + + + @@ -2541,6 +2730,18 @@ + + + + + + + + + + + + @@ -2591,6 +2792,9 @@ + + + diff --git a/Resources/PPPCServices.json b/Resources/PPPCServices.json index 72d6410..dd1f938 100644 --- a/Resources/PPPCServices.json +++ b/Resources/PPPCServices.json @@ -4,6 +4,11 @@ "englishName": "Accessibility", "englishDescription": "Allows specified apps to control the Mac via Accessibility APIs." }, + { + "mdmKey": "BluetoothAlways", + "englishName": "Bluetooth Always", + "englishDescription": "Specifies the policies for the app to access Bluetooth devices." + }, { "mdmKey": "AppleEvents", "englishName": "AppleEvents", @@ -181,6 +186,16 @@ "com.apple.security.files.user-selected.read-write" ] }, + { + "mdmKey": "SystemPolicyAppBundles", + "englishName": "App Bundles", + "englishDescription": "Allows the app to update or delete other apps." + }, + { + "mdmKey": "SystemPolicyAppData", + "englishName": "App Data", + "englishDescription": "Specifies the policies for the app to access the data of other apps." + }, { "mdmKey": "SystemPolicySysAdminFiles", "englishName": "Administrator Files", diff --git a/Resources/TestTCCUnsignedProfile.mobileconfig b/Resources/TestTCCUnsignedProfile.mobileconfig index 9092b0c..b1983e2 100644 --- a/Resources/TestTCCUnsignedProfile.mobileconfig +++ b/Resources/TestTCCUnsignedProfile.mobileconfig @@ -48,7 +48,34 @@ bundleID - AddressBook + BluetoothAlways + + +Allowed + +CodeRequirement +identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD +Comment + +Identifier +io.brackets.appshell +IdentifierType +bundleID + + +Allowed + +CodeRequirement +identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" +Comment + +Identifier +com.jamfsoftware.JamfAdmin +IdentifierType +bundleID + + + AddressBook Allowed @@ -450,7 +477,61 @@ bundleID - SystemPolicyDesktopFolder + SystemPolicyAppBundles + + +Allowed + +CodeRequirement +identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD +Comment + +Identifier +io.brackets.appshell +IdentifierType +bundleID + + +Allowed + +CodeRequirement +identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" +Comment + +Identifier +com.jamfsoftware.JamfAdmin +IdentifierType +bundleID + + +SystemPolicyAppData + + +Allowed + +CodeRequirement +identifier "io.brackets.appshell" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = JQ525L2MZD +Comment + +Identifier +io.brackets.appshell +IdentifierType +bundleID + + +Allowed + +CodeRequirement +identifier "com.jamfsoftware.JamfAdmin" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" +Comment + +Identifier +com.jamfsoftware.JamfAdmin +IdentifierType +bundleID + + + SystemPolicyDesktopFolder Allowed diff --git a/Source/Model/Executable.swift b/Source/Model/Executable.swift index 9e4cc25..aaaab3e 100644 --- a/Source/Model/Executable.swift +++ b/Source/Model/Executable.swift @@ -76,6 +76,7 @@ class Executable: NSObject { class Policy: NSObject { @objc dynamic var AddressBook: String = "-" + @objc dynamic var BluetoothAlways: String = "-" @objc dynamic var Calendar: String = "-" @objc dynamic var Reminders: String = "-" @objc dynamic var Photos: String = "-" @@ -84,6 +85,8 @@ class Policy: NSObject { @objc dynamic var Accessibility: String = "-" @objc dynamic var PostEvent: String = "-" @objc dynamic var SystemPolicyAllFiles: String = "-" + @objc dynamic var SystemPolicyAppBundles: String = "-" + @objc dynamic var SystemPolicyAppData: String = "-" @objc dynamic var SystemPolicySysAdminFiles: String = "-" @objc dynamic var FileProviderPresence: String = "-" @objc dynamic var ListenEvent: String = "-" diff --git a/Source/Model/TCCProfile.swift b/Source/Model/TCCProfile.swift index 2c01a38..b161f40 100644 --- a/Source/Model/TCCProfile.swift +++ b/Source/Model/TCCProfile.swift @@ -194,25 +194,28 @@ public struct TCCProfile: Codable { } enum ServicesKeys: String { + case accessibility = "Accessibility" case addressBook = "AddressBook" + case adminFiles = "SystemPolicySysAdminFiles" + case allFiles = "SystemPolicyAllFiles" + case appBundles = "SystemPolicyAppBundles" + case appData = "SystemPolicyAppData" + case appleEvents = "AppleEvents" + case bluetoothAlways = "BluetoothAlways" case calendar = "Calendar" - case reminders = "Reminders" - case photos = "Photos" case camera = "Camera" - case microphone = "Microphone" - case accessibility = "Accessibility" - case postEvent = "PostEvent" - case allFiles = "SystemPolicyAllFiles" - case adminFiles = "SystemPolicySysAdminFiles" - case fileProviderPresence = "FileProviderPresence" - case listenEvent = "ListenEvent" - case mediaLibrary = "MediaLibrary" - case screenCapture = "ScreenCapture" - case speechRecognition = "SpeechRecognition" case desktopFolder = "SystemPolicyDesktopFolder" case documentsFolder = "SystemPolicyDocumentsFolder" case downloadsFolder = "SystemPolicyDownloadsFolder" + case fileProviderPresence = "FileProviderPresence" + case listenEvent = "ListenEvent" + case mediaLibrary = "MediaLibrary" + case microphone = "Microphone" case networkVolumes = "SystemPolicyNetworkVolumes" + case photos = "Photos" + case postEvent = "PostEvent" + case reminders = "Reminders" case removableVolumes = "SystemPolicyRemovableVolumes" - case appleEvents = "AppleEvents" + case screenCapture = "ScreenCapture" + case speechRecognition = "SpeechRecognition" } diff --git a/Source/View Controllers/TCCProfileViewController.swift b/Source/View Controllers/TCCProfileViewController.swift index 9b670e0..a85daf5 100644 --- a/Source/View Controllers/TCCProfileViewController.swift +++ b/Source/View Controllers/TCCProfileViewController.swift @@ -52,80 +52,92 @@ class TCCProfileViewController: NSViewController { @IBOutlet weak var identifierLabel: NSTextField! @IBOutlet weak var codeRequirementLabel: NSTextField! - @IBOutlet weak var addressBookPopUp: NSPopUpButton! - @IBOutlet weak var photosPopUp: NSPopUpButton! - @IBOutlet weak var remindersPopUp: NSPopUpButton! - @IBOutlet weak var calendarPopUp: NSPopUpButton! @IBOutlet weak var accessibilityPopUp: NSPopUpButton! - @IBOutlet weak var postEventsPopUp: NSPopUpButton! + @IBOutlet weak var addressBookPopUp: NSPopUpButton! @IBOutlet weak var adminFilesPopUp: NSPopUpButton! @IBOutlet weak var allFilesPopUp: NSPopUpButton! + @IBOutlet weak var appBundlesPopUp: NSPopUpButton! + @IBOutlet weak var appDataPopUp: NSPopUpButton! + @IBOutlet weak var bluetoothAlwaysPopUp: NSPopUpButton! + @IBOutlet weak var calendarPopUp: NSPopUpButton! @IBOutlet weak var cameraPopUp: NSPopUpButton! - @IBOutlet weak var microphonePopUp: NSPopUpButton! - @IBOutlet weak var fileProviderPresencePopUp: NSPopUpButton! - @IBOutlet weak var listenEventPopUp: NSPopUpButton! - @IBOutlet weak var mediaLibraryPopUp: NSPopUpButton! - @IBOutlet weak var screenCapturePopUp: NSPopUpButton! - @IBOutlet weak var speechRecognitionPopUp: NSPopUpButton! @IBOutlet weak var dekstopFolderPopUp: NSPopUpButton! @IBOutlet weak var documentsFolderPopUp: NSPopUpButton! @IBOutlet weak var downloadsFolderPopUp: NSPopUpButton! + @IBOutlet weak var fileProviderPresencePopUp: NSPopUpButton! + @IBOutlet weak var listenEventPopUp: NSPopUpButton! + @IBOutlet weak var mediaLibraryPopUp: NSPopUpButton! + @IBOutlet weak var microphonePopUp: NSPopUpButton! @IBOutlet weak var networkVolumesPopUp: NSPopUpButton! + @IBOutlet weak var photosPopUp: NSPopUpButton! + @IBOutlet weak var postEventsPopUp: NSPopUpButton! + @IBOutlet weak var remindersPopUp: NSPopUpButton! @IBOutlet weak var removableVolumesPopUp: NSPopUpButton! + @IBOutlet weak var screenCapturePopUp: NSPopUpButton! + @IBOutlet weak var speechRecognitionPopUp: NSPopUpButton! // Labels with descriptions - @IBOutlet weak var addressBookHelpButton: InfoButton! - @IBOutlet weak var photosHelpButton: InfoButton! - @IBOutlet weak var remindersHelpButton: InfoButton! - @IBOutlet weak var calendarHelpButton: InfoButton! @IBOutlet weak var accessibilityHelpButton: InfoButton! - @IBOutlet weak var postEventsHelpButton: InfoButton! + @IBOutlet weak var addressBookHelpButton: InfoButton! @IBOutlet weak var adminFilesHelpButton: InfoButton! @IBOutlet weak var allFilesHelpButton: InfoButton! + @IBOutlet weak var appBundlesHelpButton: InfoButton! + @IBOutlet weak var appDataHelpButton: InfoButton! + @IBOutlet weak var bluetoothAlwaysHelpButton: InfoButton! + @IBOutlet weak var calendarHelpButton: InfoButton! @IBOutlet weak var cameraHelpButton: InfoButton! - @IBOutlet weak var microphoneHelpButton: InfoButton! - @IBOutlet weak var fileProviderHelpButton: InfoButton! - @IBOutlet weak var listenEventHelpButton: InfoButton! - @IBOutlet weak var mediaLibraryHelpButton: InfoButton! - @IBOutlet weak var screenCaptureHelpButton: InfoButton! - @IBOutlet weak var speechRecognitionHelpButton: InfoButton! @IBOutlet weak var desktopFolderHelpButton: InfoButton! @IBOutlet weak var documentsFolderHelpButton: InfoButton! @IBOutlet weak var downloadsFolderHelpButton: InfoButton! + @IBOutlet weak var fileProviderHelpButton: InfoButton! + @IBOutlet weak var listenEventHelpButton: InfoButton! + @IBOutlet weak var mediaLibraryHelpButton: InfoButton! + @IBOutlet weak var microphoneHelpButton: InfoButton! @IBOutlet weak var networkVolumesHelpButton: InfoButton! + @IBOutlet weak var photosHelpButton: InfoButton! + @IBOutlet weak var postEventsHelpButton: InfoButton! + @IBOutlet weak var remindersHelpButton: InfoButton! @IBOutlet weak var removableVolumesHelpButton: InfoButton! + @IBOutlet weak var screenCaptureHelpButton: InfoButton! + @IBOutlet weak var speechRecognitionHelpButton: InfoButton! @IBOutlet weak var adminFilesStackView: NSStackView! + @IBOutlet weak var allFilesStackView: NSStackView! + @IBOutlet weak var appBundlesStackView: NSStackView! + @IBOutlet weak var appDataStackView: NSStackView! + @IBOutlet weak var bluetoothAlwaysStackView: NSStackView! @IBOutlet weak var cameraStackView: NSStackView! @IBOutlet weak var desktopFolderStackView: NSStackView! @IBOutlet weak var downloadsFolderStackView: NSStackView! - @IBOutlet weak var allFilesStackView: NSStackView! @IBOutlet weak var mediaLibraryStackView: NSStackView! @IBOutlet weak var networkVolumesStackView: NSStackView! @IBOutlet weak var postEventsStackView: NSStackView! @IBOutlet weak var removableVolumesStackView: NSStackView! @IBOutlet weak var speechRecognitionStackView: NSStackView! - @IBOutlet weak var addressBookPopUpAC: NSArrayController! - @IBOutlet weak var photosPopUpAC: NSArrayController! - @IBOutlet weak var remindersPopUpAC: NSArrayController! - @IBOutlet weak var calendarPopUpAC: NSArrayController! @IBOutlet weak var accessibilityPopUpAC: NSArrayController! - @IBOutlet weak var postEventsPopUpAC: NSArrayController! + @IBOutlet weak var addressBookPopUpAC: NSArrayController! @IBOutlet weak var adminFilesPopUpAC: NSArrayController! @IBOutlet weak var allFilesPopUpAC: NSArrayController! + @IBOutlet weak var appBundlesPopUpAC: NSArrayController! + @IBOutlet weak var appDataPopUpAC: NSArrayController! + @IBOutlet weak var bluetoothAlwaysPopUpAC: NSArrayController! + @IBOutlet weak var calendarPopUpAC: NSArrayController! @IBOutlet weak var cameraPopUpAC: NSArrayController! - @IBOutlet weak var microphonePopUpAC: NSArrayController! - @IBOutlet weak var fileProviderPresencePopUpAC: NSArrayController! - @IBOutlet weak var listenEventPopUpAC: NSArrayController! - @IBOutlet weak var mediaLibraryPopUpAC: NSArrayController! - @IBOutlet weak var screenCapturePopUpAC: NSArrayController! - @IBOutlet weak var speechRecognitionPopUpAC: NSArrayController! @IBOutlet weak var dekstopFolderPopUpAC: NSArrayController! @IBOutlet weak var documentsFolderPopUpAC: NSArrayController! @IBOutlet weak var downloadsFolderPopUpAC: NSArrayController! + @IBOutlet weak var fileProviderPresencePopUpAC: NSArrayController! + @IBOutlet weak var listenEventPopUpAC: NSArrayController! + @IBOutlet weak var mediaLibraryPopUpAC: NSArrayController! + @IBOutlet weak var microphonePopUpAC: NSArrayController! @IBOutlet weak var networkVolumesPopUpAC: NSArrayController! + @IBOutlet weak var photosPopUpAC: NSArrayController! + @IBOutlet weak var remindersPopUpAC: NSArrayController! @IBOutlet weak var removableVolumesPopUpAC: NSArrayController! + @IBOutlet weak var screenCapturePopUpAC: NSArrayController! + @IBOutlet weak var speechRecognitionPopUpAC: NSArrayController! + @IBOutlet weak var postEventsPopUpAC: NSArrayController! @IBOutlet weak var saveButton: NSButton! @IBOutlet weak var uploadButton: NSButton! @@ -238,22 +250,25 @@ class TCCProfileViewController: NSViewController { // Setup policy pop up setupAllowDeny(policies: [ - addressBookPopUpAC, - photosPopUpAC, - remindersPopUpAC, - calendarPopUpAC, accessibilityPopUpAC, - postEventsPopUpAC, + addressBookPopUpAC, adminFilesPopUpAC, allFilesPopUpAC, - fileProviderPresencePopUpAC, - mediaLibraryPopUpAC, - speechRecognitionPopUpAC, + appBundlesPopUpAC, + appDataPopUpAC, + bluetoothAlwaysPopUpAC, + calendarPopUpAC, dekstopFolderPopUpAC, documentsFolderPopUpAC, downloadsFolderPopUpAC, + fileProviderPresencePopUpAC, + mediaLibraryPopUpAC, networkVolumesPopUpAC, - removableVolumesPopUpAC + photosPopUpAC, + postEventsPopUpAC, + remindersPopUpAC, + removableVolumesPopUpAC, + speechRecognitionPopUpAC ]) setupStandardUserAllowAndDeny(policies: [ @@ -270,10 +285,13 @@ class TCCProfileViewController: NSViewController { setupStackViewsWithBackground(stackViews: [ adminFilesStackView, + allFilesStackView, + appBundlesStackView, + appDataStackView, + bluetoothAlwaysStackView, cameraStackView, desktopFolderStackView, downloadsFolderStackView, - allFilesStackView, mediaLibraryStackView, networkVolumesStackView, postEventsStackView, @@ -340,26 +358,29 @@ class TCCProfileViewController: NSViewController { private func setupDescriptions() { let services = PPPCServicesManager.shared - addressBookHelpButton.setHelpMessage(services.allServices["AddressBook"]?.userHelp) - photosHelpButton.setHelpMessage(services.allServices["Photos"]?.userHelp) - remindersHelpButton.setHelpMessage(services.allServices["Reminders"]?.userHelp) - calendarHelpButton.setHelpMessage(services.allServices["Calendar"]?.userHelp) accessibilityHelpButton.setHelpMessage(services.allServices["Accessibility"]?.userHelp) - postEventsHelpButton.setHelpMessage(services.allServices["PostEvent"]?.userHelp) + addressBookHelpButton.setHelpMessage(services.allServices["AddressBook"]?.userHelp) adminFilesHelpButton.setHelpMessage(services.allServices["SystemPolicySysAdminFiles"]?.userHelp) allFilesHelpButton.setHelpMessage(services.allServices["SystemPolicyAllFiles"]?.userHelp) + appBundlesHelpButton.setHelpMessage(services.allServices["SystemPolicyAppBundles"]?.userHelp) + appDataHelpButton.setHelpMessage(services.allServices["SystemPolicyAppData"]?.userHelp) + bluetoothAlwaysHelpButton.setHelpMessage(services.allServices["BluetoothAlways"]?.userHelp) + calendarHelpButton.setHelpMessage(services.allServices["Calendar"]?.userHelp) cameraHelpButton.setHelpMessage(services.allServices["Camera"]?.userHelp) - microphoneHelpButton.setHelpMessage(services.allServices["Microphone"]?.userHelp) - fileProviderHelpButton.setHelpMessage(services.allServices["FileProviderPresence"]?.userHelp) - listenEventHelpButton.setHelpMessage(services.allServices["ListenEvent"]?.userHelp) - mediaLibraryHelpButton.setHelpMessage(services.allServices["MediaLibrary"]?.userHelp) - screenCaptureHelpButton.setHelpMessage(services.allServices["ScreenCapture"]?.userHelp) - speechRecognitionHelpButton.setHelpMessage(services.allServices["SpeechRecognition"]?.userHelp) desktopFolderHelpButton.setHelpMessage(services.allServices["SystemPolicyDesktopFolder"]?.userHelp) documentsFolderHelpButton.setHelpMessage(services.allServices["SystemPolicyDocumentsFolder"]?.userHelp) downloadsFolderHelpButton.setHelpMessage(services.allServices["SystemPolicyDownloadsFolder"]?.userHelp) + fileProviderHelpButton.setHelpMessage(services.allServices["FileProviderPresence"]?.userHelp) + listenEventHelpButton.setHelpMessage(services.allServices["ListenEvent"]?.userHelp) + mediaLibraryHelpButton.setHelpMessage(services.allServices["MediaLibrary"]?.userHelp) + microphoneHelpButton.setHelpMessage(services.allServices["Microphone"]?.userHelp) networkVolumesHelpButton.setHelpMessage(services.allServices["SystemPolicyNetworkVolumes"]?.userHelp) + photosHelpButton.setHelpMessage(services.allServices["Photos"]?.userHelp) + postEventsHelpButton.setHelpMessage(services.allServices["PostEvent"]?.userHelp) + remindersHelpButton.setHelpMessage(services.allServices["Reminders"]?.userHelp) removableVolumesHelpButton.setHelpMessage(services.allServices["SystemPolicyRemovableVolumes"]?.userHelp) + screenCaptureHelpButton.setHelpMessage(services.allServices["ScreenCapture"]?.userHelp) + speechRecognitionHelpButton.setHelpMessage(services.allServices["SpeechRecognition"]?.userHelp) } override func prepare(for segue: NSStoryboardSegue, sender: Any?) { @@ -414,6 +435,29 @@ class TCCProfileViewController: NSViewController { addAppleEventButton.setAccessibilityIdentifier("AddAppleEventButton") removeAppleEventButton.setAccessibilityIdentifier("RemoveAppleEventButton") removeExecutableButton.setAccessibilityIdentifier("RemoveExecutableButton") + accessibilityPopUp.setAccessibilityIdentifier("AccessibilityPopUp") + addressBookPopUp.setAccessibilityIdentifier("AddressBookPopUp") + adminFilesPopUp.setAccessibilityIdentifier("AdminFilesPopUp") + allFilesPopUp.setAccessibilityIdentifier("AllFilesPopUp") + appBundlesPopUp.setAccessibilityIdentifier("AppBundlesPopUp") + appDataPopUp.setAccessibilityIdentifier("AppDataPopUp") + bluetoothAlwaysPopUp.setAccessibilityIdentifier("BluetoothAlwaysPopUp") + calendarPopUp.setAccessibilityIdentifier("CalendarPopUp") + cameraPopUp.setAccessibilityIdentifier("CameraPopUp") + dekstopFolderPopUp.setAccessibilityIdentifier("DesktopFolderPopUp") + documentsFolderPopUp.setAccessibilityIdentifier("DocumentsFolderPopUp") + downloadsFolderPopUp.setAccessibilityIdentifier("DownloadsFolderPopUp") + fileProviderPresencePopUp.setAccessibilityIdentifier("FileProviderPresencePopUp") + listenEventPopUp.setAccessibilityIdentifier("ListenEventPopUp") + mediaLibraryPopUp.setAccessibilityIdentifier("MediaLibraryPopUp") + microphonePopUp.setAccessibilityIdentifier("MicrophonePopUp") + networkVolumesPopUp.setAccessibilityIdentifier("NetworkVolumesPopUp") + photosPopUp.setAccessibilityIdentifier("PhotosPopUp") + postEventsPopUp.setAccessibilityIdentifier("PostEventsPopUp") + remindersPopUp.setAccessibilityIdentifier("RemindersPopUp") + removableVolumesPopUp.setAccessibilityIdentifier("RemovableVolumesPopUp") + screenCapturePopUp.setAccessibilityIdentifier("ScreenCapturePopUp") + speechRecognitionPopUp.setAccessibilityIdentifier("SpeechRecognitionPopUp") } private func loadUITestProfileIfNeeded() { diff --git a/specs/002-add-pppc-keys/checklists/requirements.md b/specs/002-add-pppc-keys/checklists/requirements.md new file mode 100644 index 0000000..1feff89 --- /dev/null +++ b/specs/002-add-pppc-keys/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: Add New PPPC Keys + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-04-09 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- All items pass validation. Spec is ready for `/speckit.clarify` or `/speckit.plan`. +- The term "MDM key" is used as domain terminology (not implementation detail) since it is the formal name for these identifiers in the Apple MDM specification and Jamf Pro documentation. diff --git a/specs/002-add-pppc-keys/clarifications.md b/specs/002-add-pppc-keys/clarifications.md new file mode 100644 index 0000000..6db8ba1 --- /dev/null +++ b/specs/002-add-pppc-keys/clarifications.md @@ -0,0 +1,69 @@ +# Clarification Log: Add New PPPC Keys + +**Feature**: 002-add-pppc-keys +**Date**: 2026-04-09 +**Topic**: Unit testing and UI testing plans + +## Questions & Resolutions + +### Q1: Unit test scope for the 3 new keys + +**Options presented**: (A) All: services manager, policy defaults, export/import round-trip; (B) Minimal: only services manager + policy defaults; (C) Export/import only. + +**Resolution**: **A — Full coverage** across all existing service-related test areas. Services manager count assertion (21→24), policy defaults, and export/import round-trip with both Allow and Deny values. + +--- + +### Q2: UI test updates + +**Options presented**: (A) Add UI assertions for each new column; (B) No new UI tests; (C) Minimal — one test verifying total column count increased. + +**Resolution**: **C — Minimal**. One UI test verifying the total column count increased. Column-level assertions would be brittle. + +--- + +### Q3: Special behaviors (deny-only, allowStandardUsers) + +**Options presented**: (A) All 3 are standard; (B) One or more have special behavior. + +**Resolution**: **A — All 3 are standard**. Confirmed via Apple's official MDM documentation: +- None of the 3 new keys contain "can't be given in a profile; it can only be denied" language. +- `AllowStandardUserToSetSystemService` is explicitly limited to `ListenEvent` and `ScreenCapture` only. + +User initially selected B but confirmed A after reviewing Apple docs. + +--- + +### Q4: Test fixture strategy + +**Options presented**: (A) Update existing fixtures; (B) Create new fixtures; (C) Both. + +**Resolution**: **A+legacy — Update existing fixtures AND maintain a legacy fixture**. Existing `.mobileconfig` fixtures should be updated to include the 3 new keys. A legacy fixture (without the new keys) must be kept or added to verify backward-compatible import (new columns default to "–"). + +## Spec Changes Made + +- Added **Testing Requirements** section (TR-001 through TR-006) to the spec. +- Updated FR-005 to explicitly state all 3 keys are standard (not deny-only, no allowStandardUsers) — confirmed fact rather than assumption. +- Updated Assumptions to reflect confirmed deny-only/allowStandardUsers status from Apple docs. + +--- + +## Clarification Round 2 — 2026-04-09 + +**Topic**: Test fixture strategy for legacy import testing + +### Q5: How to test legacy import without adding a test file? + +**Context**: The plan called for both updating existing `.mobileconfig` fixtures (TR-004) and maintaining a legacy fixture (TR-005), but these goals conflicted without adding a new file. + +**Resolution**: Use a **rename + replace** approach: +- **Rename** the existing `TestTCCUnsignedProfile.mobileconfig` → `TestTCCUnsignedProfile-Legacy.mobileconfig` (preserves the original without new keys) +- **Create** a new `TestTCCUnsignedProfile.mobileconfig` with all 24 services +- **Update** `TestTCCUnsignedProfile-allLower.mobileconfig` with the 3 new keys in lowercase +- **Update** `Resources/TestTCCUnsignedProfile.mobileconfig` (app-bundled UI test profile) with new keys +- **Add** a test in `TCCProfileImporterTests` that imports the legacy fixture and verifies new columns default to "–" + +### Spec & Plan Changes Made + +- Updated TR-004 and TR-005 in spec to reflect the rename + new file strategy. +- Updated plan project structure to show `TestTCCUnsignedProfile-Legacy.mobileconfig`. diff --git a/specs/002-add-pppc-keys/data-model.md b/specs/002-add-pppc-keys/data-model.md new file mode 100644 index 0000000..eaf10b4 --- /dev/null +++ b/specs/002-add-pppc-keys/data-model.md @@ -0,0 +1,96 @@ +# Data Model: Add New PPPC Keys + +**Feature**: 002-add-pppc-keys +**Date**: 2026-04-09 + +## Entities + +### PPPCServiceInfo (existing — 3 new instances) + +Represents a single PPPC service definition loaded from `PPPCServices.json`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| mdmKey | String | Yes | MDM payload key (e.g., "BluetoothAlways") | +| englishName | String | Yes | Human-readable display name | +| englishDescription | String | Yes | Service description for help text | +| entitlements | [String]? | No | Related Apple entitlements (nil for these 3 keys) | +| denyOnly | Bool? | No | If true, only Deny is available (nil/false for these 3 keys) | +| allowStandardUsers | Bool? | No | If true, standard users can approve (nil/false for these 3 keys) | + +**New instances**: + +```json +{ + "mdmKey": "BluetoothAlways", + "englishName": "Bluetooth Always", + "englishDescription": "Specifies the policies for the app to access Bluetooth devices." +} +``` + +```json +{ + "mdmKey": "SystemPolicyAppBundles", + "englishName": "App Bundles", + "englishDescription": "Allows the app to update or delete other apps." +} +``` + +```json +{ + "mdmKey": "SystemPolicyAppData", + "englishName": "App Data", + "englishDescription": "Specifies the policies for the app to access the data of other apps." +} +``` + +### ServicesKeys Enum (existing — 3 new cases) + +Maps human-readable case names to MDM key strings. Used for programmatic service identification. + +| Case | Raw Value | +|------|-----------| +| bluetoothAlways | "BluetoothAlways" | +| appBundles | "SystemPolicyAppBundles" | +| appData | "SystemPolicyAppData" | + +### Policy Class (existing — 3 new properties) + +KVC-bound properties for Cocoa Bindings. Each property name must exactly match the MDM key. + +| Property | Type | Default | KVC Binding | +|----------|------|---------|-------------| +| BluetoothAlways | String | "-" | Bound to popup via NSArrayController | +| SystemPolicyAppBundles | String | "-" | Bound to popup via NSArrayController | +| SystemPolicyAppData | String | "-" | Bound to popup via NSArrayController | + +## Relationships + +``` +PPPCServices.json → PPPCServicesManager.allServices[mdmKey] + ↓ +ServicesKeys.rawValue == mdmKey == Policy.propertyName + ↓ +TCCProfile.Content.services[mdmKey] → [TCCPolicy] + ↓ +Main.storyboard popup ← Cocoa Binding → Policy.{mdmKey} +``` + +## State Transitions + +Policy values follow the existing state model: + +``` +"-" (not set) → "Allow" | "Deny" +"Allow" → "-" | "Deny" +"Deny" → "-" | "Allow" +``` + +No state persistence beyond the in-memory model and exported profile file. + +## Validation Rules + +- `mdmKey` must be unique across all services in `PPPCServices.json`. +- Policy property names must exactly match `mdmKey` values (KVC requirement). +- `ServicesKeys` enum raw values must exactly match `mdmKey` values. +- Exported profiles must only include services where the policy value is not "-". diff --git a/specs/002-add-pppc-keys/plan.md b/specs/002-add-pppc-keys/plan.md new file mode 100644 index 0000000..82f89e9 --- /dev/null +++ b/specs/002-add-pppc-keys/plan.md @@ -0,0 +1,98 @@ +# Implementation Plan: Add New PPPC Keys + +**Branch**: `002-add-pppc-keys` | **Date**: 2026-04-09 | **Spec**: [spec.md](spec.md) +**Input**: Feature specification from `/specs/002-add-pppc-keys/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow. + +## Summary + +Add three new PPPC service keys (BluetoothAlways, SystemPolicyAppBundles, SystemPolicyAppData) to the PPPC Utility, following the existing pattern of data-driven service registration. Each key requires additions to the JSON service registry, Swift enum, KVC-bound Policy class, storyboard UI, and view controller wiring. All three are standard Allow/Deny services with no special behaviors (confirmed via Apple MDM documentation). + +## Technical Context + +**Language/Version**: Swift 6.0 (main targets), Swift 5.0 (UI test target) +**Primary Dependencies**: AppKit (storyboard-based NSViewController), Cocoa Bindings (KVC) +**Storage**: N/A (in-memory model, file-based export/import) +**Testing**: Swift Testing (unit tests), XCTest/XCUITest (UI tests) +**Target Platform**: macOS 13.0+ +**Project Type**: Desktop app (macOS) +**Performance Goals**: N/A (data-addition change, no performance-sensitive paths) +**Constraints**: Must follow existing storyboard + Cocoa Bindings pattern for UI; KVC requires `@objc dynamic` properties +**Scale/Scope**: 3 new services added to existing 21 (→ 24 total) + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Principle | Status | Notes | +|-----------|--------|-------| +| I. Simplicity | ✅ PASS | Pure data additions following existing patterns. No new abstractions, helpers, or compatibility shims. | +| II. macOS Platform Compliance | ✅ PASS | UI additions follow existing storyboard layout with accessibility identifiers and HIG-compliant popup buttons. Target remains macOS 13.0+. | + +**Quality Gates**: +| Gate | Plan | +|------|------| +| Compiler warnings | Baseline before changes; compare after. | +| Unit tests | Update existing suites + add new assertions. All must pass. | +| UI tests | Add column count test. All must pass. | +| Constitution check | Re-verify after Phase 1 design. | + +## Project Structure + +### Documentation (this feature) + +```text +specs/002-add-pppc-keys/ +├── spec.md # Feature specification +├── clarifications.md # Clarification log +├── plan.md # This file +├── research.md # Phase 0 output (trivial — no unknowns) +├── data-model.md # Phase 1 output +├── quickstart.md # Phase 1 output +└── checklists/ + └── requirements.md # Spec quality checklist +``` + +### Source Code (repository root) + +```text +# Files requiring changes (existing — no new files in production code) +Resources/ +├── PPPCServices.json # Add 3 new service entries +└── Base.lproj/Main.storyboard # Add UI elements for 3 new services + +Source/ +├── Model/ +│ ├── TCCProfile.swift # Add 3 new ServicesKeys enum cases +│ └── Executable.swift # Add 3 new @objc dynamic Policy properties +└── View Controllers/ + └── TCCProfileViewController.swift # Add IBOutlets, setup calls, descriptions + +# Test files requiring changes +Resources/ +└── TestTCCUnsignedProfile.mobileconfig # Update with 3 new service keys + +PPPC UtilityTests/ +├── ModelTests/ +│ ├── PPPCServicesManagerTests.swift # Update service count (21 → 24) +│ ├── ExecutableTests.swift # Update policy default count +│ ├── ModelTests.swift # Add export/import round-trip tests +│ └── TCCProfileTests.swift # (if needed for serialization) +├── TCCProfileImporterTests/ +│ ├── TestTCCUnsignedProfile.mobileconfig # NEW: all 24 services +│ ├── TestTCCUnsignedProfile-Legacy.mobileconfig # RENAMED: original without new keys +│ ├── TestTCCUnsignedProfile-allLower.mobileconfig # Update with new keys lowercase +│ └── TCCProfileImporterTests.swift # Add legacy import test +└── Helpers/ + └── TCCProfileBuilder.swift # Add new keys to built policies + +PPPC UtilityUITests/ +└── AppLaunchTests.swift # Add column count test +``` + +**Structure Decision**: No new directories or modules needed. All changes extend existing files following established patterns. + +## Complexity Tracking + +> No violations. All changes are data-driven additions within existing architecture. diff --git a/specs/002-add-pppc-keys/quickstart.md b/specs/002-add-pppc-keys/quickstart.md new file mode 100644 index 0000000..63e9d07 --- /dev/null +++ b/specs/002-add-pppc-keys/quickstart.md @@ -0,0 +1,64 @@ +# Quickstart: Add New PPPC Keys + +**Feature**: 002-add-pppc-keys +**Branch**: `002-add-pppc-keys` + +## Overview + +Add BluetoothAlways, SystemPolicyAppBundles, and SystemPolicyAppData to the PPPC Utility. All three are standard Allow/Deny services following the identical pattern as the existing 21 services. + +## Implementation Order + +### Layer 1: Data Model (no UI impact, enables all tests) + +1. **PPPCServices.json** — Add 3 new service entries (alphabetical placement) +2. **TCCProfile.swift** — Add 3 new `ServicesKeys` enum cases +3. **Executable.swift** — Add 3 new `@objc dynamic` properties to `Policy` + +### Layer 2: View Controller Wiring + +4. **TCCProfileViewController.swift** — Add IBOutlet declarations (popup, array controller, help button, stack view) +5. **TCCProfileViewController.swift** — Add to `setupAllowDeny()`, `setupDescriptions()`, `setupStackViewsWithBackground()`, `setupAccessibilityIdentifiers()` + +### Layer 3: Storyboard UI + +6. **Main.storyboard** — Add popup buttons, labels, info buttons, stack views for each new service, wire IBOutlets and Cocoa Bindings + +### Layer 4: Tests + +7. **Unit tests** — Update service count, policy defaults, add export/import round-trip tests +8. **Test fixtures** — Update .mobileconfig files with new keys; preserve legacy fixture +9. **UI tests** — Add column count verification + +## Key Files + +| File | Change | +|------|--------| +| `Resources/PPPCServices.json` | +3 entries | +| `Source/Model/TCCProfile.swift` | +3 enum cases | +| `Source/Model/Executable.swift` | +3 properties | +| `Source/View Controllers/TCCProfileViewController.swift` | +IBOutlets, setup calls | +| `Resources/Base.lproj/Main.storyboard` | +UI elements | +| `PPPC UtilityTests/` | Updated assertions + new tests | +| `PPPC UtilityUITests/AppLaunchTests.swift` | +column count test | + +## Build & Test + +```bash +# Build +xcodebuild clean build -project "PPPC Utility.xcodeproj" -scheme "PPPC Utility" -destination "platform=macOS" + +# Unit tests +xcodebuild test -project "PPPC Utility.xcodeproj" -scheme "PPPC Utility" -destination "platform=macOS" -testPlan "PPPC Utility" + +# UI tests +xcodebuild test -project "PPPC Utility.xcodeproj" -scheme "PPPC Utility UI Tests" -destination "platform=macOS" + +# Compiler warnings check +xcodebuild clean build-for-testing -project "PPPC Utility.xcodeproj" -scheme "PPPC Utility" -destination "platform=macOS" 2>&1 | grep -i "warning:" | grep -v "xcodebuild: WARNING" +``` + +## Risks + +- **Storyboard merge conflicts**: The Main.storyboard is XML and prone to merge conflicts. Minimize changes and commit storyboard changes atomically. +- **KVC property name mismatch**: If a Policy property name doesn't exactly match the MDM key, Cocoa Bindings will silently fail. Verify by testing the UI after binding. diff --git a/specs/002-add-pppc-keys/research.md b/specs/002-add-pppc-keys/research.md new file mode 100644 index 0000000..c4380f9 --- /dev/null +++ b/specs/002-add-pppc-keys/research.md @@ -0,0 +1,54 @@ +# Research: Add New PPPC Keys + +**Feature**: 002-add-pppc-keys +**Date**: 2026-04-09 + +## Status: Complete — No Unknowns + +All technical details were resolved during the specification and clarification phases. No NEEDS CLARIFICATION items existed in the Technical Context. + +## Research Findings + +### 1. Apple MDM Key Definitions + +**Source**: Apple Developer Documentation — `PrivacyPreferencesPolicyControl.Services` (official JSON API) + +| Key | Description | Introduced | Deny-Only | AllowStandardUsers | +|-----|-------------|------------|-----------|-------------------| +| BluetoothAlways | Specifies the policies for the app to access Bluetooth devices. | macOS 10.14 (MDM schema), macOS 14 (Jamf Pro support) | No | No | +| SystemPolicyAppBundles | Allows the application to update or delete other apps. Available in macOS 13 and later. | macOS 10.14 (MDM schema), macOS 13 (OS support) | No | No | +| SystemPolicyAppData | Specifies the policies for the app to access the data of other apps. | macOS 10.14 (MDM schema), macOS 14 (Jamf Pro support) | No | No | + +- **Decision**: All three are standard Allow/Deny services. +- **Rationale**: Apple's MDM documentation does not include deny-only language for any of the three keys. `AllowStandardUserToSetSystemService` is explicitly restricted to `ListenEvent` and `ScreenCapture` only. +- **Alternatives considered**: None — Apple's documentation is authoritative. + +### 2. Service Registration Pattern + +**Source**: Codebase exploration of existing 21 services. + +- **Decision**: Follow the existing pattern — JSON registry + enum + KVC property + storyboard UI. +- **Rationale**: All 21 existing services follow this exact pattern. No architectural changes needed. +- **Alternatives considered**: Dynamic column generation (rejected — would require rewriting the storyboard-based UI and Cocoa Bindings, violating Simplicity principle). + +### 3. Entitlements + +**Source**: Apple MDM documentation (Services schema). + +- **Decision**: None of the three new keys specify associated entitlements in Apple's documentation. +- **Rationale**: The `entitlements` field in `PPPCServiceInfo` is optional. Services without entitlements (e.g., Accessibility, PostEvent, Reminders) simply omit the field. +- **Alternatives considered**: N/A. + +### 4. English Names for Display + +**Source**: Apple support documentation + Jamf Pro release notes. + +| MDM Key | English Name (for UI) | +|---------|-----------------------| +| BluetoothAlways | Bluetooth Always | +| SystemPolicyAppBundles | App Bundles | +| SystemPolicyAppData | App Data | + +- **Decision**: Use concise names consistent with the naming pattern of existing services (e.g., "Full Disk Access" for SystemPolicyAllFiles, "Administrator Files" for SystemPolicySysAdminFiles). +- **Rationale**: Names should be recognizable to IT administrators familiar with macOS TCC terminology. +- **Alternatives considered**: "Bluetooth" (too generic, could confuse with future keys), "Application Bundles" / "Application Data" (longer than necessary). diff --git a/specs/002-add-pppc-keys/spec.md b/specs/002-add-pppc-keys/spec.md new file mode 100644 index 0000000..383f39f --- /dev/null +++ b/specs/002-add-pppc-keys/spec.md @@ -0,0 +1,121 @@ +# Feature Specification: Add New PPPC Keys + +**Feature Branch**: `002-add-pppc-keys` +**Created**: 2026-04-09 +**Status**: Draft +**Input**: User description: "Add 3 new PPPC keys (BluetoothAlways, SystemPolicyAppBundles, SystemPolicyAppData) to the PPPC Utility, as introduced in Jamf Pro 11.23.0 release notes." + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Configure Bluetooth Access Policy (Priority: P1) + +An IT administrator needs to control which applications can access Bluetooth devices on managed Macs running macOS 14 or later. Using PPPC Utility, the administrator adds an application to the profile and sets the "Bluetooth Always" permission to Allow or Deny, then exports the configuration profile for deployment via Jamf Pro. + +**Why this priority**: Bluetooth access is a significant privacy and security concern. Unauthorized Bluetooth access can expose device pairing data and enable unwanted wireless communication. This was the specific key the user requested. + +**Independent Test**: Can be fully tested by adding an application in PPPC Utility, selecting a Bluetooth Always policy value, exporting the profile, and verifying the `BluetoothAlways` key appears in the exported payload with the correct authorization value. + +**Acceptance Scenarios**: + +1. **Given** the PPPC Utility is open and an application has been added, **When** the administrator selects a policy value for Bluetooth Always, **Then** the selected value (Allow/Deny) is stored and displayed in the policy table. +2. **Given** a profile with a Bluetooth Always policy set, **When** the administrator exports the profile, **Then** the exported configuration profile contains a `BluetoothAlways` key under the TCC services payload with the correct authorization value. +3. **Given** the administrator imports an existing profile that includes a `BluetoothAlways` key, **When** the profile loads, **Then** the Bluetooth Always policy value is correctly displayed in the UI. + +--- + +### User Story 2 - Configure App Bundles Access Policy (Priority: P1) + +An IT administrator needs to control which applications can update or delete other applications on managed Macs running macOS 13 or later. Using PPPC Utility, the administrator sets the "App Bundles" permission for a given application, then exports the profile for deployment. + +**Why this priority**: Controlling which apps can modify other app bundles is a critical security concern — unauthorized modification of application bundles could introduce malware or compromise system integrity. + +**Independent Test**: Can be fully tested by adding an application in PPPC Utility, selecting a policy value for App Bundles, exporting the profile, and verifying the `SystemPolicyAppBundles` key appears in the exported payload. + +**Acceptance Scenarios**: + +1. **Given** the PPPC Utility is open and an application has been added, **When** the administrator selects a policy value for App Bundles, **Then** the selected value is stored and displayed in the policy table. +2. **Given** a profile with an App Bundles policy set, **When** the administrator exports the profile, **Then** the exported configuration profile contains a `SystemPolicyAppBundles` key under the TCC services payload with the correct authorization value. +3. **Given** the administrator imports an existing profile that includes a `SystemPolicyAppBundles` key, **When** the profile loads, **Then** the App Bundles policy value is correctly displayed in the UI. + +--- + +### User Story 3 - Configure App Data Access Policy (Priority: P1) + +An IT administrator needs to control which applications can access the data of other applications on managed Macs running macOS 14 or later. Using PPPC Utility, the administrator sets the "App Data" permission for a given application, then exports the profile for deployment. + +**Why this priority**: Controlling cross-application data access is essential for data privacy — without it, a compromised application could read sensitive data from other apps (e.g., password managers, financial tools). + +**Independent Test**: Can be fully tested by adding an application in PPPC Utility, selecting a policy value for App Data, exporting the profile, and verifying the `SystemPolicyAppData` key appears in the exported payload. + +**Acceptance Scenarios**: + +1. **Given** the PPPC Utility is open and an application has been added, **When** the administrator selects a policy value for App Data, **Then** the selected value is stored and displayed in the policy table. +2. **Given** a profile with an App Data policy set, **When** the administrator exports the profile, **Then** the exported configuration profile contains a `SystemPolicyAppData` key under the TCC services payload with the correct authorization value. +3. **Given** the administrator imports an existing profile that includes a `SystemPolicyAppData` key, **When** the profile loads, **Then** the App Data policy value is correctly displayed in the UI. + +--- + +### User Story 4 - Round-Trip Consistency (Priority: P2) + +An IT administrator creates a profile with all three new PPPC keys configured, exports it, and then re-imports it. All three new service policies should be preserved exactly as configured. + +**Why this priority**: Data integrity on export/import is fundamental to administrator trust in the tool, but this is a natural consequence of correct implementation of the individual keys. + +**Independent Test**: Can be tested by creating a profile with all three new keys set, exporting to a file, re-importing the file, and comparing all policy values. + +**Acceptance Scenarios**: + +1. **Given** a profile with BluetoothAlways, SystemPolicyAppBundles, and SystemPolicyAppData policies set to specific values, **When** the profile is exported and then re-imported, **Then** all three policy values match the originally configured values. + +--- + +### Edge Cases + +- What happens when an existing profile created before these keys were added is imported? The three new service columns should default to "–" (not set) for those applications. +- What happens when a profile exported from another tool includes these keys with unexpected or unrecognized authorization values? The system should handle gracefully, applying known values and defaulting unknown values. +- What happens when the user sets a new key and then clears it back to the default? The key should not appear in the exported profile if set to the default/unset value. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The system MUST include "Bluetooth Always" as a configurable PPPC service with the MDM key `BluetoothAlways`. +- **FR-002**: The system MUST include "App Bundles" as a configurable PPPC service with the MDM key `SystemPolicyAppBundles`. +- **FR-003**: The system MUST include "App Data" as a configurable PPPC service with the MDM key `SystemPolicyAppData`. +- **FR-004**: Each new service MUST appear in the policy table as a selectable column alongside existing services. +- **FR-005**: Each new service MUST support Allow, Deny, and "not set" policy values. None of the three new keys are deny-only or support allowStandardUsers (confirmed via Apple MDM documentation). +- **FR-006**: Exported configuration profiles MUST include the correct MDM key and authorization value for each new service when a policy value has been set. +- **FR-007**: Imported configuration profiles containing any of the three new MDM keys MUST correctly parse and display the policy values in the UI. +- **FR-008**: The service description for each new key MUST accurately describe what the permission controls, consistent with Apple's MDM documentation. + +### Testing Requirements + +- **TR-001**: Unit tests MUST verify the services manager loads all services including the 3 new keys (expected count increases from 21 to 24). +- **TR-002**: Unit tests MUST verify that each new key's Policy property defaults to "–" (not set) on a new Executable. +- **TR-003**: Unit tests MUST verify export/import round-trip fidelity for each of the 3 new keys with both Allow and Deny authorization values. +- **TR-004**: A new `TestTCCUnsignedProfile.mobileconfig` fixture MUST be created in TCCProfileImporterTests/ containing all 24 services (21 existing + 3 new keys). The `TestTCCUnsignedProfile-allLower.mobileconfig` fixture MUST also be updated with the 3 new keys in lowercase. The `Resources/TestTCCUnsignedProfile.mobileconfig` (app-bundled UI test profile) MUST be updated with the 3 new keys. +- **TR-005**: The original `TestTCCUnsignedProfile.mobileconfig` fixture (without the new keys) MUST be preserved as `TestTCCUnsignedProfile-Legacy.mobileconfig` in TCCProfileImporterTests/. A test MUST verify that importing this legacy fixture defaults the 3 new service columns to "–" (not set) without errors. +- **TR-006**: One UI test MUST be added (or an existing test updated) to verify the total column count in the policy table has increased to reflect the 3 new services. + +### Key Entities + +- **PPPC Service (BluetoothAlways)**: Controls app access to Bluetooth devices. MDM key: `BluetoothAlways`. Description: Specifies the policies for the app to access Bluetooth devices. +- **PPPC Service (SystemPolicyAppBundles)**: Controls whether an app can update or delete other applications. MDM key: `SystemPolicyAppBundles`. Description: Allows the app to update or delete other apps. +- **PPPC Service (SystemPolicyAppData)**: Controls whether an app can access data belonging to other applications. MDM key: `SystemPolicyAppData`. Description: Specifies the policies for the app to access the data of other apps. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: All three new PPPC services are visible and configurable in the PPPC Utility policy table without requiring any workaround by the administrator. +- **SC-002**: A configuration profile exported with any of the three new keys set produces a valid payload that Jamf Pro accepts and deploys successfully. +- **SC-003**: 100% round-trip fidelity — any profile created with the new keys, exported, and re-imported retains all configured values. +- **SC-004**: Existing profiles without the new keys import without errors or data loss; the new service columns default to "not set." + +## Assumptions + +- The three new keys follow the same TCC payload structure (`com.apple.TCC.configuration-profile-policy`) as existing PPPC services — no new payload types or structures are required. +- BluetoothAlways and SystemPolicyAppData require macOS 14 or later; SystemPolicyAppBundles requires macOS 13 or later. The PPPC Utility does not enforce minimum OS version per service (consistent with existing behavior). +- The existing service infrastructure (service registry, policy model, profile export/import) supports adding new services without architectural changes — only data additions are needed. +- None of the three new services are "deny only" or "allow standard users" — confirmed via Apple's official MDM documentation (the `AllowStandardUserToSetSystemService` authorization value is explicitly limited to `ListenEvent` and `ScreenCapture` only). +- The alphabetical or categorical ordering of services in the UI will incorporate the new services naturally based on existing sort logic. diff --git a/specs/002-add-pppc-keys/tasks.md b/specs/002-add-pppc-keys/tasks.md new file mode 100644 index 0000000..43b13ff --- /dev/null +++ b/specs/002-add-pppc-keys/tasks.md @@ -0,0 +1,171 @@ +# Tasks: Add New PPPC Keys + +**Input**: Design documents from `/specs/002-add-pppc-keys/` +**Prerequisites**: plan.md, spec.md, data-model.md, research.md, quickstart.md + +**Tests**: Included — explicitly requested in spec (TR-001 through TR-006). + +**Organization**: US1 (BluetoothAlways), US2 (SystemPolicyAppBundles), and US3 (SystemPolicyAppData) all modify the same files and are implemented together in shared phases. US4 (Round-Trip Consistency) is a separate testing phase. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) +- Include exact file paths in descriptions + +## Path Conventions + +- **Desktop app (macOS)**: `Source/`, `Resources/`, tests at `PPPC UtilityTests/`, `PPPC UtilityUITests/` + +--- + +## Phase 1: Setup + +**Purpose**: Capture baseline before any changes + +- [X] T001 Capture baseline compiler warnings by running: `xcodebuild clean build-for-testing -project "PPPC Utility.xcodeproj" -scheme "PPPC Utility" -destination "platform=macOS" 2>&1 | grep -i "warning:" | grep -v "xcodebuild: WARNING"` +- [X] T002 Run existing unit tests to confirm green baseline: `xcodebuild test -project "PPPC Utility.xcodeproj" -scheme "PPPC Utility" -destination "platform=macOS" -testPlan "PPPC Utility"` +- [X] T003 Run existing UI tests to confirm green baseline: `xcodebuild test -project "PPPC Utility.xcodeproj" -scheme "PPPC Utility" -destination "platform=macOS" -testPlan "PPPC Utility UI Tests"` + +--- + +## Phase 2: Foundational — Data Model (All 3 Keys) + +**Purpose**: Add BluetoothAlways, SystemPolicyAppBundles, and SystemPolicyAppData to the data layer. MUST be complete before UI or test work can begin. + +**⚠️ CRITICAL**: No UI or test work can begin until this phase is complete. + +- [X] T004 [P] [US1] [US2] [US3] Add 3 new service entries to `Resources/PPPCServices.json` — insert `BluetoothAlways` (after Accessibility, alphabetically), `SystemPolicyAppBundles` (after SystemPolicyAllFiles), and `SystemPolicyAppData` (after SystemPolicyAppBundles). Each entry has `mdmKey`, `englishName`, and `englishDescription` per data-model.md. No `entitlements`, `denyOnly`, or `allowStandardUsers` fields. +- [X] T005 [P] [US1] [US2] [US3] Add 3 new enum cases to `ServicesKeys` in `Source/Model/TCCProfile.swift` — add `case bluetoothAlways = "BluetoothAlways"`, `case appBundles = "SystemPolicyAppBundles"`, `case appData = "SystemPolicyAppData"`. +- [X] T006 [P] [US1] [US2] [US3] Add 3 new `@objc dynamic` properties to the `Policy` class in `Source/Model/Executable.swift` — add `@objc dynamic var BluetoothAlways: String = "-"`, `@objc dynamic var SystemPolicyAppBundles: String = "-"`, `@objc dynamic var SystemPolicyAppData: String = "-"`. Property names MUST exactly match MDM key strings (KVC requirement). + +**Checkpoint**: Data model complete. Build should succeed with no new warnings. Existing tests may now fail (service count assertions) — expected. + +--- + +## Phase 3: US1+US2+US3 — View Controller & Storyboard UI + +**Goal**: Make all 3 new services visible and configurable in the PPPC Utility UI. + +**Independent Test**: Open the app, add an executable, verify Bluetooth Always / App Bundles / App Data popups appear with Allow/Deny options. + +### Implementation + +- [X] T007 [US1] [US2] [US3] Add IBOutlet declarations for all 3 new services in `Source/View Controllers/TCCProfileViewController.swift` — for each service, add: `NSPopUpButton` outlet, `NSArrayController` outlet, `InfoButton` outlet, and `NSStackView` outlet. Follow the naming pattern of existing outlets (e.g., `bluetoothAlwaysPopUp`, `bluetoothAlwaysPopUpAC`, `bluetoothAlwaysHelpButton`, `bluetoothAlwaysStackView`). +- [X] T008 [US1] [US2] [US3] Wire new services into setup methods in `Source/View Controllers/TCCProfileViewController.swift` — add the 3 new `NSArrayController` outlets to the `setupAllowDeny()` call in `viewDidLoad()`. Add the 3 new `InfoButton` outlets to `setupDescriptions()` with their MDM keys. Add the 3 new `NSStackView` outlets to `setupStackViewsWithBackground()` (for alternating row backgrounds). Add accessibility identifiers for the 3 new popups in `setupAccessibilityIdentifiers()`. +- [X] T009 [US1] [US2] [US3] Add UI elements for all 3 new services in `Resources/Base.lproj/Main.storyboard` — for each service, duplicate an existing service row (e.g., the Reminders row pattern) and update: label text, popup button binding to the corresponding `Policy` property via Cocoa Bindings, NSArrayController connection, InfoButton connection, and NSStackView connection. Wire all IBOutlets to the view controller. + +**Checkpoint**: All 3 services visible in UI. Build and launch app to verify popups appear. Cocoa Bindings functional (selecting Allow/Deny updates the Policy object). + +--- + +## Phase 4: US1+US2+US3 — Unit Tests + +**Goal**: Verify data model correctness for all 3 new keys via automated tests. + +### Tests + +- [X] T010 [P] [US1] [US2] [US3] Update service count assertion in `PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift` — change expected `allServices.count` from 21 to 24. Add assertions that `allServices["BluetoothAlways"]`, `allServices["SystemPolicyAppBundles"]`, and `allServices["SystemPolicyAppData"]` are non-nil with correct `englishName` values. (Satisfies TR-001) +- [X] T011 [P] [US1] [US2] [US3] Update policy defaults test in `PPPC UtilityTests/ModelTests/ExecutableTests.swift` — verify that a new `Executable`'s `policy.BluetoothAlways`, `policy.SystemPolicyAppBundles`, and `policy.SystemPolicyAppData` all default to `"-"`. Update any existing count-based assertions for total policy properties. (Satisfies TR-002) +- [X] T012 [P] [US1] [US2] [US3] Add new keys to `buildTCCPolicies()` in `PPPC UtilityTests/Helpers/TCCProfileBuilder.swift` — add `"BluetoothAlways"`, `"SystemPolicyAppBundles"`, and `"SystemPolicyAppData"` entries to the returned dictionary so round-trip tests exercise the new keys. +- [X] T013 [US1] [US2] [US3] Add export/import round-trip tests in `PPPC UtilityTests/ModelTests/ModelTests.swift` — add tests verifying that for each of the 3 new keys, setting a policy to Allow or Deny, exporting, and re-importing preserves the value. Use `ModelBuilder` to create executables with specific policy settings. (Satisfies TR-003) + +**Checkpoint**: Run unit test plan — all tests pass including updated service count, defaults, and round-trip assertions. + +--- + +## Phase 5: US4 — Import Testing & Legacy Fixtures + +**Goal**: Verify round-trip fidelity and backward-compatible import of older profiles. + +**Independent Test**: Import a legacy profile (without new keys) — new columns default to "–". Import a modern profile (with new keys) — values correctly displayed. + +### Test Fixtures + +- [X] T014 [P] [US4] Rename existing fixture `PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile.mobileconfig` to `TestTCCUnsignedProfile-Legacy.mobileconfig` — this preserves the original file (without new keys) as the legacy import fixture. Update the Xcode project file if the resource is referenced by name. +- [X] T015 [P] [US4] Create new `PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile.mobileconfig` — copy from the legacy file and add `BluetoothAlways`, `SystemPolicyAppBundles`, and `SystemPolicyAppData` service entries with test policy dictionaries (Allowed: true, Identifier, CodeRequirement, IdentifierType, Comment). Follow the exact XML plist structure of existing service entries. +- [X] T016 [P] [US4] Update `PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile-allLower.mobileconfig` — add `bluetoothalways`, `systempolicyappbundles`, and `systempolicyappdata` entries (lowercase keys) following the existing lowercase pattern. +- [X] T017 [P] [US4] Update `Resources/TestTCCUnsignedProfile.mobileconfig` — add the 3 new service entries (same as T015) so the app-bundled UI test profile includes all 24 services. + +### Tests + +- [X] T018 [US4] Add legacy import test in `PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift` — add a test that imports `TestTCCUnsignedProfile-Legacy.mobileconfig`, feeds it through `Model.importProfile()`, and verifies the 3 new policy columns default to `"-"` on all imported executables. (Satisfies TR-005) +- [X] T019 [US4] Verify existing `correctUnsignedProfileContentData` test passes with the new fixture in `PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift` — the test should now import a profile with all 24 services and succeed. (Satisfies TR-004) + +**Checkpoint**: All importer tests pass. Legacy profile imports cleanly. Modern profile imports with all 24 services. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: UI test, final validation, quality gate checks + +- [X] T020 [US1] [US2] [US3] Add or update a UI test in `PPPC UtilityUITests/AppLaunchTests.swift` to verify the policy table column count reflects the 3 new services. Use the `-UITestMode` launch argument (which loads the test profile) and assert expected popup/column count. (Satisfies TR-006) +- [X] T021 Compare compiler warnings against T001 baseline — verify no new warnings introduced by running the same command and diffing output. +- [X] T022 Run full unit test plan and verify all tests pass: `xcodebuild test -project "PPPC Utility.xcodeproj" -scheme "PPPC Utility" -destination "platform=macOS" -testPlan "PPPC Utility"` +- [X] T023 Run full UI test plan and verify all tests pass: `xcodebuild test -project "PPPC Utility.xcodeproj" -scheme "PPPC Utility" -destination "platform=macOS" -testPlan "PPPC Utility UI Tests"` +- [X] T024 Validate quickstart.md build and test commands still work per `specs/002-add-pppc-keys/quickstart.md` + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — start immediately +- **Foundational (Phase 2)**: Depends on Setup (T001-T003) — BLOCKS all subsequent phases +- **View Controller & UI (Phase 3)**: Depends on Phase 2 (data model must exist for bindings) +- **Unit Tests (Phase 4)**: Depends on Phase 2 (data model). Can overlap with Phase 3 (different files). +- **Import Testing (Phase 5)**: Depends on Phase 2 (data model). Can overlap with Phase 3 and 4 (different files). +- **Polish (Phase 6)**: Depends on Phases 3, 4, 5 all complete + +### User Story Dependencies + +- **US1, US2, US3 (all P1)**: Implemented together — same files, same pattern, no benefit to separation. All depend on Phase 2. +- **US4 (P2)**: Depends on Phase 2 data model only. Can be worked in parallel with Phase 3 (UI) and Phase 4 (unit tests). + +### Within-Phase Task Dependencies + +- **Phase 2**: T004, T005, T006 are all [P] — different files, run in parallel +- **Phase 3**: T007 → T008 → T009 (sequential within same files) +- **Phase 4**: T010, T011, T012 are [P] — different files. T013 depends on T012 (builder) +- **Phase 5**: T014, T015, T016, T017 are [P] — different files. T018, T019 depend on T014+T015 + +### Parallel Opportunities + +```text +Phase 2 (all parallel): + T004 (PPPCServices.json) ║ T005 (TCCProfile.swift) ║ T006 (Executable.swift) + +Phase 4 + Phase 5 fixtures (overlap with Phase 3): + T010 (ServicesManagerTests) ║ T011 (ExecutableTests) ║ T012 (TCCProfileBuilder) + T014 (rename fixture) ║ T015 (new fixture) ║ T016 (allLower fixture) ║ T017 (Resources fixture) +``` + +--- + +## Implementation Strategy + +### MVP First (Phase 1 + 2 + 3) + +1. Complete Phase 1: Capture baselines +2. Complete Phase 2: Data model (3 parallel tasks) +3. Complete Phase 3: View controller + storyboard +4. **STOP and VALIDATE**: Launch app, add an executable, verify all 3 new popups appear with Allow/Deny + +### Incremental Delivery + +1. Setup + Foundational → Data model ready +2. View Controller + UI → All 3 services visible and functional (MVP!) +3. Unit Tests → Automated verification of data model +4. Import Tests + Legacy → Backward compatibility verified +5. Polish → Quality gates pass, ready to merge + +--- + +## Notes + +- [P] tasks = different files, no dependencies +- US1+US2+US3 are co-implemented because they share all files (PPPCServices.json, TCCProfile.swift, Executable.swift, TCCProfileViewController.swift, Main.storyboard). Splitting them would create constant same-file conflicts with no benefit. +- Storyboard changes (T009) are the highest-risk task due to XML merge fragility — do this in a focused commit. +- KVC property names in Policy class MUST exactly match MDM key strings — a mismatch causes silent binding failure.