├── .github
└── workflows
│ └── swiftlint.yml
├── .gitignore
├── .swiftlint.yml
├── CHANGELOG.md
├── External
└── SwiftyCMSDecoder.swift
├── Images
├── Building.png
├── ImportProfile.png
├── SavingSigned.png
├── SavingUnsigned.png
├── UploadSigned.png
└── UploadUnsigned.png
├── LICENSE
├── PPPC Utility.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm
│ └── Package.resolved
├── PPPC UtilityTests
├── Helpers
│ ├── ModelBuilder.swift
│ └── TCCProfileBuilder.swift
├── Info.plist
├── ModelTests
│ ├── ModelTests.swift
│ ├── PPPCServicesManagerTests.swift
│ └── SemanticVersionTests.swift
├── NetworkingTests
│ ├── JamfProAPIClientTests.swift
│ ├── NetworkAuthManagerTests.swift
│ └── TokenTests.swift
└── TCCProfileImporterTests
│ ├── TCCProfileImporterTests.swift
│ ├── TCCProfileTests.swift
│ ├── TestTCCProfileForJamfProAPI.txt
│ ├── TestTCCProfileSigned-Broken.mobileconfig
│ ├── TestTCCUnsignedProfile-Broken.mobileconfig
│ ├── TestTCCUnsignedProfile-Empty.mobileconfig
│ ├── TestTCCUnsignedProfile-allLower.mobileconfig
│ └── TestTCCUnsignedProfile.mobileconfig
├── README.md
├── Resources
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── PPPC_Logo_128.png
│ │ ├── PPPC_Logo_128@2x.png
│ │ ├── PPPC_Logo_16.png
│ │ ├── PPPC_Logo_16@2x.png
│ │ ├── PPPC_Logo_256.png
│ │ ├── PPPC_Logo_256@2x.png
│ │ ├── PPPC_Logo_32.png
│ │ ├── PPPC_Logo_32@2x.png
│ │ ├── PPPC_Logo_512.png
│ │ └── PPPC_Logo_512@2x.png
│ └── Contents.json
├── Base.lproj
│ └── Main.storyboard
├── Info.plist
├── PPPC Utility.entitlements
└── PPPCServices.json
└── Source
├── AppDelegate.swift
├── Extensions
├── ArrayExtensions.swift
├── LoggerExtensions.swift
└── TCCProfileExtensions.swift
├── Model
├── AppleEventRule.swift
├── Executable.swift
├── LoadExecutableError.swift
├── Model.swift
├── PPPCServiceInfo.swift
├── PPPCServicesManager.swift
├── SemanticVersion.swift
├── SigningIdentity.swift
└── TCCProfile.swift
├── Networking
├── JamfProAPIClient.swift
├── JamfProAPITypes.swift
├── NetworkAuthManager.swift
├── Networking.swift
├── Token.swift
├── URLSessionAsyncCompatibility.swift
└── UploadManager.swift
├── SecurityWrapper.swift
├── SwiftUI
└── UploadInfoView.swift
├── TCCProfileImporter
├── TCCProfileConfigurationPanel.swift
├── TCCProfileImportError.swift
└── TCCProfileImporter.swift
├── View Controllers
├── OpenViewController.swift
├── SaveViewController.swift
└── TCCProfileViewController.swift
└── Views
├── Alert.swift
├── FlippedClipView.swift
└── InfoButton.swift
/.github/workflows/swiftlint.yml:
--------------------------------------------------------------------------------
1 | name: SwiftLint
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - '.github/workflows/swiftlint.yml'
7 | - '.swiftlint.yml'
8 | - '**/*.swift'
9 |
10 | jobs:
11 | SwiftLint:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Swiftlint verification
16 | uses: norio-nomura/action-swiftlint@3.2.1
17 | with:
18 | args: --strict
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 | .DS_Store
25 | *.swp
26 |
27 | ## Obj-C/Swift specific
28 | *.hmap
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | .build/
44 |
45 | # CocoaPods
46 | #
47 | # We recommend against adding the Pods directory to your .gitignore. However
48 | # you should judge for yourself, the pros and cons are mentioned at:
49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
50 | #
51 | # Pods/
52 |
53 | # Carthage
54 | #
55 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
56 | # Carthage/Checkouts
57 |
58 | Carthage/Build
59 |
60 | # fastlane
61 | #
62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
63 | # screenshots whenever they are needed.
64 | # For more information about the recommended setup visit:
65 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
66 |
67 | fastlane/report.xml
68 | fastlane/Preview.html
69 | fastlane/screenshots/**/*.png
70 | fastlane/test_output
71 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | opt_in_rules:
2 | - sorted_imports
3 | - trailing_closure
4 | - orphaned_doc_comment
5 |
6 | file_length:
7 | ignore_comment_only_lines: true
8 |
9 | disabled_rules:
10 | - line_length
11 | - type_body_length
12 | - todo
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6 |
7 | ## [Unreleased]
8 |
9 |
10 | ### Added
11 | - Connection to Jamf Pro can now use client credentials with Jamf Pro v10.49+ ([Issue #120](https://github.com/jamf/PPPC-Utility/issues/120)) [@macblazer](https://github.com/macblazer).
12 |
13 | ### Changed
14 | - Update print and os_log calls to the modern OSLog class calls for updated logging. ([Issue #112](https://github.com/jamf/PPPC-Utility/issues/112)) [@SkylerGodfrey](https://github.com/SkylerGodfrey)
15 | - Now using [Haversack](https://github.com/jamf/Haversack) for simplified access to the keychain ([Issue #124](https://github.com/jamf/PPPC-Utility/issues/124)) [@macblazer](https://github.com/macblazer).
16 | - PPPC Utility now requires macOS 11+ to run. It can still produce profiles usable on older versions of macOS.
17 |
18 | ## [1.5.0] - 2022-10-04
19 |
20 | ### Added
21 | - Help buttons now list related codesigning entitlements ([Issue #105](https://github.com/jamf/PPPC-Utility/issues/105)) [@macblazer](https://github.com/macblazer).
22 |
23 | ### Changed
24 | - Uses token authentication to Jamf Pro API and falls back to Basic authentication if that fails ([Issue #113](https://github.com/jamf/PPPC-Utility/issues/113)) [@macblazer](https://github.com/macblazer).
25 | - Now reads profile keys in a case-insensitive manner during import ([Issue #88](https://github.com/jamf/PPPC-Utility/issues/88)) [@macblazer](https://github.com/macblazer).
26 | - The items in this changelog have been formatted for consistency with links to GitHub issues and contributor profiles.
27 |
28 |
29 | ## [1.4.0] - 2021-08-11
30 |
31 | ### Added
32 | - Changed the property labels to match System Preferences with the MDM key listed in the help ([Issue #79](https://github.com/jamf/PPPC-Utility/issues/79)) [@ty-wilson](https://github.com/ty-wilson).
33 | - Application list and Apple Events app list both support multiple apps being dragged into the list ([Issue #85](https://github.com/jamf/PPPC-Utility/issues/85)) [@macblazer](https://github.com/macblazer).
34 |
35 | ### Fixed
36 | - The code signing label will no longer be truncated ([Issue #54](https://github.com/jamf/PPPC-Utility/issues/54)) [@ty-wilson](https://github.com/ty-wilson).
37 | - Deleting an Apple Event removes the selected item instead of always removing the first one in the list ([Issue #83](https://github.com/jamf/PPPC-Utility/issues/83)) [@ty-wilson](https://github.com/ty-wilson).
38 |
39 |
40 | ## [1.3.0] - 2020-10-22
41 |
42 | ### Added
43 | - Added this Changelog.md file [@hisaac](https://github.com/hisaac).
44 | - The default value on Apple Events is now "Allow" ([Issue #72](https://github.com/jamf/PPPC-Utility/issues/72)) [@ty-wilson](https://github.com/ty-wilson).
45 | - Added support for the new `Authorization` key in Big Sur [@watkyn](https://github.com/watkyn).
46 | - Changed minimum deployment target to macOS 10.15 [@watkyn](https://github.com/watkyn).
47 |
48 |
49 | ## [1.2.1] - 2020-09-17
50 |
51 | Thank you to all the contributors in this release!
52 |
53 | ### Added
54 | - Added swiftlint to the project [@stavares843](https://github.com/stavares843).
55 | - Added a swiftlint GitHub Action [@stavares843](https://github.com/stavares843).
56 | - Added some alerts for better error reporting [@BIG-RAT](https://github.com/BIG-RAT).
57 |
58 | ### Fixed
59 | - Buttons can no longer go off the screen in certain circumstances [@BIG-RAT](https://github.com/BIG-RAT).
60 | - PPPC Utility now properly uploads profiles to Jamf Pro version 10.23 and greater [@kkot](https://github.com/kkot).
61 |
62 |
63 | ## [1.2.0] - 2020-04-29
64 |
65 | ### Added
66 | - Can now import existing profiles from disk [@adku](https://github.com/adku).
67 |
68 | ### Fixed
69 | - TCC properties are ordered alphabetically [@ty-wilson](https://github.com/ty-wilson).
70 | - Duplicate apps can no longer be added to the view [@BIG-RAT](https://github.com/BIG-RAT).
71 | - TCC profile xml properties fixes so profiles can be added to Jamf Now [@pirkla](https://github.com/pirkla).
72 | - Updated labels and placeholders [@pirkla](https://github.com/pirkla).
73 |
74 |
75 | ## [1.1.2] - 2019-10-07
76 |
77 | ### Fixed
78 | - Locally saved profiles are now properly signed if that option is selected while saving. ([Issues #2](https://github.com/jamf/PPPC-Utility/issues/2) and [#25](https://github.com/jamf/PPPC-Utility/issues/25)) [@adku](https://github.com/adku).
79 |
80 |
81 | ## [1.1.1] - 2019-09-20
82 |
83 | ### Fixed
84 | - Updated old app name that was used in PPPC Utility menu. ([Issue #18](https://github.com/jamf/PPPC-Utility/issues/18)) [@adku](https://github.com/adku).
85 |
86 |
87 | ## [1.1.0] - 2019-09-10
88 |
89 | ### Changed
90 | - Updated with the new macOS 10.15 Privacy Preferences Policy Control keys [@mm512](https://github.com/mm512).
91 | - Minor user interface updates [@mm512](https://github.com/mm512).
92 |
93 |
94 | ## [1.0.1] - 2018-10-03
95 |
96 | ### Fixed
97 | - Rules using `SystemPolicySysAdminFiles` are now created correctly ([Issue #4](https://github.com/jamf/PPPC-Utility/issues/4)) [@cyrusingraham](https://github.com/cyrusingraham).
98 |
99 |
100 | ## [1.0.0] - 2018-09-21
101 |
102 | Initial release [@cyrusingraham](https://github.com/cyrusingraham).
103 |
104 |
105 |
106 | [unreleased]: https://github.com/jamf/PPPC-Utility/compare/1.5.0...master
107 | [1.5.0]: https://github.com/jamf/PPPC-Utility/compare/1.4.0...1.5.0
108 | [1.4.0]: https://github.com/jamf/PPPC-Utility/compare/1.3.0...1.4.0
109 | [1.3.0]: https://github.com/jamf/PPPC-Utility/compare/1.2.1...1.3.0
110 | [1.2.1]: https://github.com/jamf/PPPC-Utility/compare/1.2.0...1.2.1
111 | [1.2.0]: https://github.com/jamf/PPPC-Utility/compare/1.1.2...1.2.0
112 | [1.1.2]: https://github.com/jamf/PPPC-Utility/compare/1.1.1...1.1.2
113 | [1.1.1]: https://github.com/jamf/PPPC-Utility/compare/1.1.0...1.1.1
114 | [1.1.0]: https://github.com/jamf/PPPC-Utility/compare/1.0.1...1.1.0
115 | [1.0.1]: https://github.com/jamf/PPPC-Utility/compare/1.0.0...1.0.1
116 | [1.0.0]: https://github.com/jamf/PPPC-Utility/compare/047786dad486e8cc1e159d3f315adb695a566465...1.0.0
117 |
--------------------------------------------------------------------------------
/External/SwiftyCMSDecoder.swift:
--------------------------------------------------------------------------------
1 | // SwiftyCMSDecoder.swift
2 | //
3 | // MIT License
4 | //
5 | // Copyright (c) 2018 James Sherlock https://twitter.com/JamesSherlouk/
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in all
15 | // copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | // SOFTWARE.
24 | //
25 |
26 | import Foundation
27 |
28 | /// Source: https://github.com/Sherlouk/SwiftProvisioningProfile
29 | /// Swift wrapper around Apple's Security `CMSDecoder` class
30 | final class SwiftyCMSDecoder {
31 |
32 | var decoder: CMSDecoder
33 |
34 | /// Initialises a new `SwiftyCMSDecoder` which in turn creates a new `CMSDecoder`
35 | init?() {
36 | var newDecoder: CMSDecoder?
37 | CMSDecoderCreate(&newDecoder)
38 |
39 | guard let decoder = newDecoder else {
40 | return nil
41 | }
42 |
43 | self.decoder = decoder
44 | }
45 |
46 | /// Feed raw bytes of the message to be decoded into the decoder. Can be called multiple times.
47 | ///
48 | /// - Parameter data: The raw data you want to have decoded
49 | /// - Returns: Success - `false` upon detection of improperly formatted CMS message.
50 | @discardableResult
51 | func updateMessage(data: NSData) -> Bool {
52 | return CMSDecoderUpdateMessage(decoder, data.bytes, data.length) != errSecUnknownFormat
53 | }
54 |
55 | /// Indicate that no more `updateMessage()` calls are coming; finish decoding the message.
56 | ///
57 | /// - Returns: Success - `false` upon detection of improperly formatted CMS message.
58 | @discardableResult
59 | func finaliseMessage() -> Bool {
60 | return CMSDecoderFinalizeMessage(decoder) != errSecUnknownFormat
61 | }
62 |
63 | /// Obtain the actual message content (payload), if any. If the message was signed with
64 | /// detached content then this will return `nil`.
65 | ///
66 | /// - Warning: This cannot be called until after `finaliseMessage()` is called!
67 | var data: Data? {
68 | var newData: CFData?
69 | CMSDecoderCopyContent(decoder, &newData)
70 | return newData as Data?
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Images/Building.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Images/Building.png
--------------------------------------------------------------------------------
/Images/ImportProfile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Images/ImportProfile.png
--------------------------------------------------------------------------------
/Images/SavingSigned.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Images/SavingSigned.png
--------------------------------------------------------------------------------
/Images/SavingUnsigned.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Images/SavingUnsigned.png
--------------------------------------------------------------------------------
/Images/UploadSigned.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Images/UploadSigned.png
--------------------------------------------------------------------------------
/Images/UploadUnsigned.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Images/UploadUnsigned.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Jamf Software
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PPPC Utility.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/PPPC Utility.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/PPPC Utility.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "haversack",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/jamf/Haversack.git",
7 | "state" : {
8 | "revision" : "840fd566ca709fe0932b231df63833d50488e127",
9 | "version" : "1.1.0"
10 | }
11 | },
12 | {
13 | "identity" : "swift-collections",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/apple/swift-collections",
16 | "state" : {
17 | "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307",
18 | "version" : "1.0.5"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/Helpers/ModelBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModelBuilder.swift
3 | //
4 | // MIT License
5 | //
6 | // Copyright (c) 2019 Jamf Software
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in all
16 | // copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | // SOFTWARE.
25 | //
26 |
27 | import Cocoa
28 |
29 | @testable import PPPC_Utility
30 |
31 | class ModelBuilder {
32 |
33 | var model: Model
34 |
35 | init() {
36 | model = Model()
37 | }
38 |
39 | func build() -> Model {
40 | return model
41 | }
42 |
43 | func addExecutable(settings: [String: String]) -> ModelBuilder {
44 | let exe = Executable(identifier: "id", codeRequirement: "req", "display")
45 | settings.forEach { key, value in
46 | exe.policy.setValue(value, forKey: key)
47 | }
48 | model.selectedExecutables.append(exe)
49 |
50 | return self
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/Helpers/TCCProfileBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TCCProfileBuilder.swift
3 | //
4 | // MIT License
5 | //
6 | // Copyright (c) 2019 Jamf Software
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in all
16 | // copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | // SOFTWARE.
25 | //
26 |
27 | import Cocoa
28 |
29 | @testable import PPPC_Utility
30 |
31 | class TCCProfileBuilder: NSObject {
32 |
33 | // MARK: - build testing objects
34 |
35 | func buildTCCPolicy(allowed: Bool?, authorization: TCCPolicyAuthorizationValue?) -> TCCPolicy {
36 | var policy = TCCPolicy(identifier: "policy id", codeRequirement: "policy code req",
37 | receiverIdentifier: "policy receiver id", receiverCodeRequirement: "policy receiver code req")
38 | policy.comment = "policy comment"
39 | policy.identifierType = "policy id type"
40 | policy.receiverIdentifierType = "policy receiver id type"
41 | policy.allowed = allowed
42 | policy.authorization = authorization
43 | return policy
44 | }
45 |
46 | func buildTCCPolicies(allowed: Bool?, authorization: TCCPolicyAuthorizationValue?) -> [String: [TCCPolicy]] {
47 | return ["SystemPolicyAllFiles": [buildTCCPolicy(allowed: allowed, authorization: authorization)],
48 | "AppleEvents": [buildTCCPolicy(allowed: allowed, authorization: authorization)]]
49 | }
50 |
51 | func buildTCCContent(_ contentIndex: Int, allowed: Bool?, authorization: TCCPolicyAuthorizationValue?) -> TCCProfile.Content {
52 | return TCCProfile.Content(payloadDescription: "Content Desc \(contentIndex)",
53 | displayName: "Content Name \(contentIndex)",
54 | identifier: "Content ID \(contentIndex)",
55 | organization: "Content Org \(contentIndex)",
56 | type: "Content type \(contentIndex)",
57 | uuid: "Content UUID \(contentIndex)",
58 | version: contentIndex,
59 | services: buildTCCPolicies(allowed: allowed, authorization: authorization))
60 | }
61 |
62 | func buildProfile(allowed: Bool? = nil, authorization: TCCPolicyAuthorizationValue? = nil) -> TCCProfile {
63 | var profile = TCCProfile(organization: "Test Org",
64 | identifier: "Test ID",
65 | displayName: "Test Name",
66 | payloadDescription: "Test Desc",
67 | services: [:])
68 | profile.content = [buildTCCContent(1, allowed: allowed, authorization: authorization)]
69 | profile.version = 100
70 | profile.uuid = "the uuid"
71 | return profile
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PPPCServicesManagerTests.swift
3 | // PPPC UtilityTests
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2022 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 | import XCTest
30 |
31 | @testable import PPPC_Utility
32 |
33 | class PPPCServicesManagerTests: XCTestCase {
34 |
35 | func testLoadAllServices() {
36 | // given/when
37 | let actual = PPPCServicesManager()
38 |
39 | // then
40 | XCTAssertEqual(actual.allServices.count, 21)
41 | }
42 |
43 | func testUserHelp_withEntitlements() throws {
44 | // given
45 | let services = PPPCServicesManager()
46 | let service = try XCTUnwrap(services.allServices["Camera"])
47 |
48 | // when
49 | let actual = service.userHelp
50 |
51 | // then
52 | XCTAssertEqual(actual, "Use to deny specified apps access to the camera.\n\nMDM Key: Camera\nRelated entitlements: [\"com.apple.developer.avfoundation.multitasking-camera-access\", \"com.apple.security.device.camera\"]")
53 | }
54 |
55 | func testUserHelp_withoutEntitlements() throws {
56 | // given
57 | let services = PPPCServicesManager()
58 | let service = try XCTUnwrap(services.allServices["ScreenCapture"])
59 |
60 | // when
61 | let actual = service.userHelp
62 |
63 | // then
64 | XCTAssertEqual(actual, "Deny specified apps access to capture (read) the contents of the system display.\n\nMDM Key: ScreenCapture")
65 | }
66 |
67 | func testCameraIsDenyOnly() throws {
68 | // given
69 | let services = PPPCServicesManager()
70 | let service = try XCTUnwrap(services.allServices["Camera"])
71 |
72 | // when
73 | let actual = try XCTUnwrap(service.denyOnly)
74 |
75 | // then
76 | XCTAssertTrue(actual)
77 | }
78 |
79 | func testScreenCaptureAllowsStandardUsers() throws {
80 | // given
81 | let services = PPPCServicesManager()
82 | let service = try XCTUnwrap(services.allServices["ScreenCapture"])
83 |
84 | // when
85 | let actual = try XCTUnwrap(service.allowStandardUsersMacOS11Plus)
86 |
87 | // then
88 | XCTAssertTrue(actual)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/ModelTests/SemanticVersionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SemanticVersionTests.swift
3 | // PPPC UtilityTests
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2022 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 | @testable import PPPC_Utility
30 | import XCTest
31 |
32 | class SemanticVersionTests: XCTestCase {
33 | func testLessThan() {
34 | // given
35 | let version = SemanticVersion(major: 10, minor: 7, patch: 1)
36 |
37 | // when
38 | let shouldBeLessThan = version < SemanticVersion(major: 10, minor: 7, patch: 4)
39 | let shouldBeLessThan2 = version < SemanticVersion(major: 10, minor: 8, patch: 1)
40 | let shouldBeLessThan3 = version < SemanticVersion(major: 11, minor: 7, patch: 1)
41 | let shouldNotBeLessThan = version < SemanticVersion(major: 10, minor: 7, patch: 1)
42 | let shouldNotBeLessThan2 = version < SemanticVersion(major: 10, minor: 7, patch: 0)
43 | let shouldNotBeLessThan3 = version < SemanticVersion(major: 10, minor: 6, patch: 1)
44 | let shouldNotBeLessThan4 = version < SemanticVersion(major: 9, minor: 7, patch: 1)
45 |
46 | // then
47 | XCTAssertTrue(shouldBeLessThan)
48 | XCTAssertTrue(shouldBeLessThan2)
49 | XCTAssertTrue(shouldBeLessThan3)
50 | XCTAssertFalse(shouldNotBeLessThan)
51 | XCTAssertFalse(shouldNotBeLessThan2)
52 | XCTAssertFalse(shouldNotBeLessThan3)
53 | XCTAssertFalse(shouldNotBeLessThan4)
54 | }
55 |
56 | func testEquality() {
57 | // given
58 | let version = SemanticVersion(major: 10, minor: 7, patch: 1)
59 |
60 | // when
61 | let shouldBeEqual = version == SemanticVersion(major: 10, minor: 7, patch: 1)
62 | let shouldBeNotEqual = version == SemanticVersion(major: 10, minor: 7, patch: 4)
63 | let shouldBeNotEqual2 = version == SemanticVersion(major: 10, minor: 8, patch: 1)
64 | let shouldBeNotEqual3 = version == SemanticVersion(major: 11, minor: 7, patch: 1)
65 |
66 | // then
67 | XCTAssertTrue(shouldBeEqual)
68 | XCTAssertFalse(shouldBeNotEqual)
69 | XCTAssertFalse(shouldBeNotEqual2)
70 | XCTAssertFalse(shouldBeNotEqual3)
71 | }
72 |
73 | func testGreaterThan() {
74 | // given
75 | let version = SemanticVersion(major: 10, minor: 7, patch: 1)
76 |
77 | // when
78 | let shouldNotBeGreaterThan = version > SemanticVersion(major: 10, minor: 7, patch: 4)
79 | let shouldNotBeGreaterThan2 = version > SemanticVersion(major: 10, minor: 8, patch: 1)
80 | let shouldNotBeGreaterThan3 = version > SemanticVersion(major: 11, minor: 7, patch: 1)
81 | let shouldNotBeGreaterThan4 = version > SemanticVersion(major: 10, minor: 7, patch: 1)
82 | let shouldBeGreaterThan = version > SemanticVersion(major: 10, minor: 7, patch: 0)
83 | let shouldBeGreaterThan2 = version > SemanticVersion(major: 10, minor: 6, patch: 1)
84 | let shouldBeGreaterThan3 = version > SemanticVersion(major: 9, minor: 7, patch: 1)
85 |
86 | // then
87 | XCTAssertFalse(shouldNotBeGreaterThan)
88 | XCTAssertFalse(shouldNotBeGreaterThan2)
89 | XCTAssertFalse(shouldNotBeGreaterThan3)
90 | XCTAssertFalse(shouldNotBeGreaterThan4)
91 | XCTAssertTrue(shouldBeGreaterThan)
92 | XCTAssertTrue(shouldBeGreaterThan2)
93 | XCTAssertTrue(shouldBeGreaterThan3)
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/NetworkingTests/JamfProAPIClientTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JamfProAPIClientTests.swift
3 | // PPPC UtilityTests
4 | //
5 | // SPDX-License-Identifier: MIT
6 | // Copyright (c) 2023 Jamf Software
7 |
8 | import Foundation
9 | import XCTest
10 |
11 | @testable import PPPC_Utility
12 |
13 | class JamfProAPIClientTests: XCTestCase {
14 | func testOAuthTokenRequest() throws {
15 | // given
16 | let authManager = NetworkAuthManager(username: "", password: "")
17 | let apiClient = JamfProAPIClient(serverUrlString: "https://something", tokenManager: authManager)
18 |
19 | // when
20 | let request = try apiClient.oauthTokenRequest(clientId: "mine&yours", clientSecret: "foo bar")
21 |
22 | // then
23 | let body = try XCTUnwrap(request.httpBody)
24 | let bodyString = String(data: body, encoding: .utf8)
25 | XCTAssertEqual(bodyString, "grant_type=client_credentials&client_id=mine%26yours&client_secret=foo%20bar")
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/NetworkingTests/NetworkAuthManagerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkAuthManagerTests.swift
3 | // PPPC UtilityTests
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2022 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 | import XCTest
30 |
31 | @testable import PPPC_Utility
32 |
33 | /// Fake networking class for testing. No actual network was used in the making of this test.
34 | class MockNetworking: Networking {
35 | var errorToThrow: Error?
36 |
37 | init(tokenManager: NetworkAuthManager) {
38 | super.init(serverUrlString: "https://example.com", tokenManager: tokenManager)
39 | }
40 |
41 | override func getBearerToken(authInfo: AuthenticationInfo) async throws -> Token {
42 | if let error = errorToThrow {
43 | throw error
44 | }
45 |
46 | let formatter = ISO8601DateFormatter()
47 | formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
48 | let expiration = try XCTUnwrap(formatter.date(from: "2950-06-22T22:05:58.81Z"))
49 |
50 | return Token(value: "xyz", expiresAt: expiration)
51 | }
52 | }
53 |
54 | class NetworkAuthManagerTests: XCTestCase {
55 | func testValidToken() async throws {
56 | // given
57 | let authManager = NetworkAuthManager(username: "test", password: "none")
58 | let networking = MockNetworking(tokenManager: authManager)
59 |
60 | // when
61 | let token = try await authManager.validToken(networking: networking)
62 |
63 | // then
64 | XCTAssertEqual(token.value, "xyz")
65 | XCTAssertTrue(token.isValid)
66 | }
67 |
68 | func testValidTokenNetworkFailure() async throws {
69 | // given
70 | let authManager = NetworkAuthManager(username: "test", password: "none")
71 | let networking = MockNetworking(tokenManager: authManager)
72 | networking.errorToThrow = NetworkingError.serverResponse(500, "Bad server")
73 |
74 | // when
75 | do {
76 | _ = try await authManager.validToken(networking: networking)
77 | XCTFail("Expected to throw from `validToken` call")
78 | } catch {
79 | // then
80 | XCTAssertEqual(error as? NetworkingError, NetworkingError.serverResponse(500, "Bad server"))
81 | }
82 | }
83 |
84 | func testValidTokenBearerAuthNotSupported() async throws {
85 | // given
86 | let authManager = NetworkAuthManager(username: "test", password: "none")
87 | let networking = MockNetworking(tokenManager: authManager)
88 | networking.errorToThrow = NetworkingError.serverResponse(404, "No such page")
89 |
90 | // default is that bearer auth is supported.
91 | let firstCheckBearerAuthSupported = await authManager.bearerAuthSupported()
92 | XCTAssertTrue(firstCheckBearerAuthSupported)
93 |
94 | // when
95 | do {
96 | _ = try await authManager.validToken(networking: networking)
97 | XCTFail("Expected to throw from `validToken` call")
98 | } catch {
99 | // then - should throw a `bearerAuthNotSupported` error
100 | XCTAssertEqual(error as? AuthError, AuthError.bearerAuthNotSupported)
101 | }
102 |
103 | // The authManager should now know that bearer auth is not supported
104 | let secondCheckBearerAuthSupported = await authManager.bearerAuthSupported()
105 | XCTAssertFalse(secondCheckBearerAuthSupported)
106 | }
107 |
108 | func testValidTokenInvalidUsernamePassword() async throws {
109 | // given
110 | let authManager = NetworkAuthManager(username: "test", password: "none")
111 | let networking = MockNetworking(tokenManager: authManager)
112 | networking.errorToThrow = NetworkingError.serverResponse(401, "Not authorized")
113 |
114 | // when
115 | do {
116 | _ = try await authManager.validToken(networking: networking)
117 | XCTFail("Expected to throw from `validToken` call")
118 | } catch {
119 | // then - should throw a `invalidUsernamePassword` error
120 | XCTAssertEqual(error as? AuthError, AuthError.invalidUsernamePassword)
121 | }
122 | }
123 |
124 | func testBasicAuthString() throws {
125 | // given
126 | let authManager = NetworkAuthManager(username: "test", password: "none")
127 |
128 | // when
129 | let actual = try authManager.basicAuthString()
130 |
131 | // then
132 | XCTAssertEqual(actual, "dGVzdDpub25l")
133 | }
134 |
135 | func testBasicAuthStringEmptyUsername() throws {
136 | // given
137 | let authManager = NetworkAuthManager(username: "", password: "none")
138 |
139 | // when
140 | do {
141 | _ = try authManager.basicAuthString()
142 | XCTFail("Expected to throw from `basicAuthString` call")
143 | } catch {
144 | // then - should throw a `invalidUsernamePassword` error
145 | XCTAssertEqual(error as? AuthError, AuthError.invalidUsernamePassword)
146 | }
147 | }
148 |
149 | func testBasicAuthStringEmptyPassword() throws {
150 | // given
151 | let authManager = NetworkAuthManager(username: "mine", password: "")
152 |
153 | // when
154 | do {
155 | _ = try authManager.basicAuthString()
156 | XCTFail("Expected to throw from `basicAuthString` call")
157 | } catch {
158 | // then - should throw a `invalidUsernamePassword` error
159 | XCTAssertEqual(error as? AuthError, AuthError.invalidUsernamePassword)
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/NetworkingTests/TokenTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TokenTests.swift
3 | // PPPC UtilityTests
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2022 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 | import XCTest
30 |
31 | @testable import PPPC_Utility
32 |
33 | class TokenTests: XCTestCase {
34 | func testPastIsNotValid() throws {
35 | // given
36 | let formatter = ISO8601DateFormatter()
37 | formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
38 | let expiration = try XCTUnwrap(formatter.date(from: "2021-06-22T22:05:58.81Z"))
39 | let token = Token(value: "abc", expiresAt: expiration)
40 |
41 | // when
42 | let valid = token.isValid
43 |
44 | // then
45 | XCTAssertFalse(valid)
46 | }
47 |
48 | func testFutureIsValid() throws {
49 | // given
50 | let formatter = ISO8601DateFormatter()
51 | formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
52 | let expiration = try XCTUnwrap(formatter.date(from: "2750-06-22T22:05:58.81Z"))
53 | let token = Token(value: "abc", expiresAt: expiration)
54 |
55 | // when
56 | let valid = token.isValid
57 |
58 | // then
59 | XCTAssertTrue(valid)
60 | }
61 |
62 | // MARK: - Decoding
63 |
64 | func testDecodeBasicAuthToken() throws {
65 | // given
66 | let jsonText = """
67 | {
68 | "token": "abc",
69 | "expires": "2750-06-22T22:05:58.81Z"
70 | }
71 | """
72 | let jsonData = try XCTUnwrap(jsonText.data(using: .utf8))
73 | let decoder = JSONDecoder()
74 |
75 | // when
76 | let actual = try decoder.decode(Token.self, from: jsonData)
77 |
78 | // then
79 | XCTAssertEqual(actual.value, "abc")
80 | XCTAssertNotNil(actual.expiresAt)
81 | XCTAssertTrue(actual.isValid)
82 | }
83 |
84 | func testDecodeExpiredBasicAuthToken() throws {
85 | // given
86 | let jsonText = """
87 | {
88 | "token": "abc",
89 | "expires": "1970-10-24T22:05:58.81Z"
90 | }
91 | """
92 | let jsonData = try XCTUnwrap(jsonText.data(using: .utf8))
93 | let decoder = JSONDecoder()
94 |
95 | // when
96 | let actual = try decoder.decode(Token.self, from: jsonData)
97 |
98 | // then
99 | XCTAssertEqual(actual.value, "abc")
100 | XCTAssertNotNil(actual.expiresAt)
101 | XCTAssertFalse(actual.isValid)
102 | }
103 |
104 | func testDecodeClientCredentialsAuthToken() throws {
105 | // given
106 | let jsonText = """
107 | {
108 | "access_token": "abc",
109 | "scope": "api-role:2",
110 | "token_type": "Bearer",
111 | "expires_in": 599
112 | }
113 | """
114 | let jsonData = try XCTUnwrap(jsonText.data(using: .utf8))
115 | let decoder = JSONDecoder()
116 |
117 | // when
118 | let actual = try decoder.decode(Token.self, from: jsonData)
119 |
120 | // then
121 | XCTAssertEqual(actual.value, "abc")
122 | XCTAssertNotNil(actual.expiresAt)
123 | XCTAssertTrue(actual.isValid)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TCCProfileImporterTests.swift
3 | // PPPC UtilityTests
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2019 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 | import XCTest
30 |
31 | @testable import PPPC_Utility
32 |
33 | class TCCProfileImporterTests: XCTestCase {
34 |
35 | func testBrokenSignedTCCProfile() {
36 | let tccProfileImporter = TCCProfileImporter()
37 |
38 | let resourceURL = getResourceProfile(fileName: "TestTCCProfileSigned-Broken")
39 |
40 | tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in
41 | switch tccProfileResult {
42 | case .success:
43 | XCTFail("Broken Signed Profile, it shouldn't be success")
44 | case .failure(let tccProfileError):
45 | XCTAssertTrue(tccProfileError.localizedDescription.contains("The given data was not a valid property list."))
46 | }
47 | }
48 | }
49 |
50 | func testEmptyContentTCCProfile() {
51 | let tccProfileImporter = TCCProfileImporter()
52 |
53 | let resourceURL = getResourceProfile(fileName: "TestTCCUnsignedProfile-Empty")
54 |
55 | let expectedTCCProfileError = TCCProfileImportError.invalidProfileFile(description: "PayloadContent")
56 |
57 | tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in
58 | switch tccProfileResult {
59 | case .success:
60 | XCTFail("Empty Content, it shouldn't be success")
61 | case .failure(let tccProfileError):
62 | XCTAssertEqual(tccProfileError.localizedDescription, expectedTCCProfileError.localizedDescription)
63 | }
64 | }
65 | }
66 |
67 | func testCorrectUnsignedProfileContentData() {
68 | let tccProfileImporter = TCCProfileImporter()
69 |
70 | let resourceURL = getResourceProfile(fileName: "TestTCCUnsignedProfile")
71 |
72 | tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in
73 | switch tccProfileResult {
74 | case .success(let tccProfile):
75 | XCTAssertNotNil(tccProfile.content)
76 | XCTAssertNotNil(tccProfile.content[0].services)
77 | case .failure(let tccProfileError):
78 | XCTFail("Unable to read tccProfile \(tccProfileError.localizedDescription)")
79 | }
80 | }
81 | }
82 |
83 | func testCorrectUnsignedProfileContentDataAllLowercase() {
84 | let tccProfileImporter = TCCProfileImporter()
85 |
86 | let resourceURL = getResourceProfile(fileName: "TestTCCUnsignedProfile-allLower")
87 |
88 | tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in
89 | switch tccProfileResult {
90 | case .success(let tccProfile):
91 | XCTAssertNotNil(tccProfile.content)
92 | XCTAssertNotNil(tccProfile.content[0].services)
93 | case .failure(let tccProfileError):
94 | XCTFail("Unable to read tccProfile \(tccProfileError.localizedDescription)")
95 | }
96 | }
97 | }
98 |
99 | func testBrokenUnsignedProfile() {
100 | let tccProfileImporter = TCCProfileImporter()
101 |
102 | let resourceURL = getResourceProfile(fileName: "TestTCCUnsignedProfile-Broken")
103 |
104 | let expectedTCCProfileError = TCCProfileImportError.invalidProfileFile(description: "The given data was not a valid property list.")
105 |
106 | tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in
107 | switch tccProfileResult {
108 | case .success:
109 | XCTFail("Broken Unsigned Profile, it shouldn't be success")
110 | case .failure(let tccProfileError):
111 | XCTAssertEqual(tccProfileError.localizedDescription, expectedTCCProfileError.localizedDescription)
112 | }
113 | }
114 | }
115 |
116 | private func getResourceProfile(fileName: String) -> URL {
117 | let testBundle = Bundle(for: type(of: self))
118 | guard let resourceURL = testBundle.url(forResource: fileName, withExtension: "mobileconfig") else {
119 | XCTFail("Resource file should exists")
120 | return URL(fileURLWithPath: "invalidPath")
121 | }
122 | return resourceURL
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/TCCProfileImporterTests/TCCProfileTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TCCProfileTests.swift
3 | //
4 | // MIT License
5 | //
6 | // Copyright (c) 2019 Jamf Software
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in all
16 | // copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | // SOFTWARE.
25 | //
26 | import XCTest
27 |
28 | @testable import PPPC_Utility
29 |
30 | class TCCProfileTests: XCTestCase {
31 |
32 | // MARK: - tests for serializing to and from xml
33 |
34 | func testSerializationOfComplexProfileUsingAuthorization() throws {
35 | // when we export to xml and reimport it should still have the same attributes
36 | let plistData = try TCCProfileBuilder().buildProfile(authorization: .allowStandardUserToSetSystemService).xmlData()
37 | let profile = try TCCProfile.parse(from: plistData)
38 |
39 | // then verify the config profile top level
40 | XCTAssertEqual("Configuration", profile.type)
41 | XCTAssertEqual(100, profile.version)
42 | XCTAssertEqual("the uuid", profile.uuid)
43 | XCTAssertEqual("System", profile.scope)
44 | XCTAssertEqual("Test Org", profile.organization)
45 | XCTAssertEqual("Test ID", profile.identifier)
46 | XCTAssertEqual("Test Name", profile.displayName)
47 | XCTAssertEqual("Test Desc", profile.payloadDescription)
48 |
49 | // then verify the payload content top level
50 | XCTAssertEqual(1, profile.content.count)
51 | profile.content.forEach { content in
52 | XCTAssertEqual("Content Desc 1", content.payloadDescription)
53 | XCTAssertEqual("Content Name 1", content.displayName)
54 | XCTAssertEqual("Content ID 1", content.identifier)
55 | XCTAssertEqual("Content Org 1", content.organization)
56 | XCTAssertEqual("Content type 1", content.type)
57 | XCTAssertEqual("Content UUID 1", content.uuid)
58 | XCTAssertEqual(1, content.version)
59 |
60 | // then verify the services key
61 | XCTAssertEqual(2, content.services.count)
62 | let allFiles = content.services["SystemPolicyAllFiles"]
63 | XCTAssertEqual(1, allFiles?.count)
64 | allFiles?.forEach { policy in
65 | XCTAssertEqual("policy id", policy.identifier)
66 | XCTAssertEqual("policy id type", policy.identifierType)
67 | XCTAssertEqual("policy code req", policy.codeRequirement)
68 | XCTAssertNil(policy.allowed)
69 | XCTAssertEqual(TCCPolicyAuthorizationValue.allowStandardUserToSetSystemService, policy.authorization)
70 | XCTAssertEqual("policy comment", policy.comment)
71 | XCTAssertEqual("policy receiver id", policy.receiverIdentifier)
72 | XCTAssertEqual("policy receiver id type", policy.receiverIdentifierType)
73 | XCTAssertEqual("policy receiver code req", policy.receiverCodeRequirement)
74 | }
75 | }
76 | }
77 |
78 | func testSerializationOfProfileUsingLegacyAllowedKey() throws {
79 | // when we export to xml and reimport it should still have the same attributes
80 | let plistData = try TCCProfileBuilder().buildProfile(allowed: true).xmlData()
81 | let profile = try TCCProfile.parse(from: plistData)
82 |
83 | // then verify the config profile top level
84 | XCTAssertEqual("Configuration", profile.type)
85 | XCTAssertEqual(100, profile.version)
86 | XCTAssertEqual("the uuid", profile.uuid)
87 | XCTAssertEqual("System", profile.scope)
88 | XCTAssertEqual("Test Org", profile.organization)
89 | XCTAssertEqual("Test ID", profile.identifier)
90 | XCTAssertEqual("Test Name", profile.displayName)
91 | XCTAssertEqual("Test Desc", profile.payloadDescription)
92 |
93 | // then verify the payload content top level
94 | XCTAssertEqual(1, profile.content.count)
95 | profile.content.forEach { content in
96 | XCTAssertEqual("Content Desc 1", content.payloadDescription)
97 | XCTAssertEqual("Content Name 1", content.displayName)
98 | XCTAssertEqual("Content ID 1", content.identifier)
99 | XCTAssertEqual("Content Org 1", content.organization)
100 | XCTAssertEqual("Content type 1", content.type)
101 | XCTAssertEqual("Content UUID 1", content.uuid)
102 | XCTAssertEqual(1, content.version)
103 |
104 | // then verify the services key
105 | XCTAssertEqual(2, content.services.count)
106 | let allFiles = content.services["SystemPolicyAllFiles"]
107 | XCTAssertEqual(1, allFiles?.count)
108 | allFiles?.forEach { policy in
109 | XCTAssertEqual("policy id", policy.identifier)
110 | XCTAssertEqual("policy id type", policy.identifierType)
111 | XCTAssertEqual("policy code req", policy.codeRequirement)
112 | XCTAssertEqual(true, policy.allowed)
113 | XCTAssertNil(policy.authorization)
114 | XCTAssertEqual("policy comment", policy.comment)
115 | XCTAssertEqual("policy receiver id", policy.receiverIdentifier)
116 | XCTAssertEqual("policy receiver id type", policy.receiverIdentifierType)
117 | XCTAssertEqual("policy receiver code req", policy.receiverCodeRequirement)
118 | }
119 | }
120 | }
121 |
122 | func testSerializationOfProfileWhenBothAllowedAndAuthorizationUsed() throws {
123 | // when we export to xml and reimport it should still have the same attributes
124 | let plistData = try TCCProfileBuilder().buildProfile(allowed: false, authorization: .allow).xmlData()
125 | let profile = try TCCProfile.parse(from: plistData)
126 |
127 | // then verify the config profile top level
128 | XCTAssertEqual("Configuration", profile.type)
129 |
130 | // then verify the payload content top level
131 | XCTAssertEqual(1, profile.content.count)
132 | profile.content.forEach { content in
133 | XCTAssertEqual("Content UUID 1", content.uuid)
134 | XCTAssertEqual(1, content.version)
135 |
136 | // then verify the services key
137 | XCTAssertEqual(2, content.services.count)
138 | let allFiles = content.services["SystemPolicyAllFiles"]
139 | XCTAssertEqual(1, allFiles?.count)
140 | allFiles?.forEach { policy in
141 | XCTAssertEqual(false, policy.allowed)
142 | XCTAssertEqual(policy.authorization, TCCPolicyAuthorizationValue.allow)
143 | }
144 | }
145 | }
146 |
147 | // unit tests for handling both Auth and allowed keys should fail?
148 |
149 | func testSettingLegacyAllowValueNullifiesAuthorization() throws {
150 | // given
151 | var tccPolicy = TCCPolicy(identifier: "id", codeRequirement: "req", receiverIdentifier: "recId", receiverCodeRequirement: "recreq")
152 | tccPolicy.authorization = .allow
153 |
154 | // when
155 | tccPolicy.allowed = true
156 |
157 | // then
158 | XCTAssertNil(tccPolicy.authorization)
159 | XCTAssertTrue(try XCTUnwrap(tccPolicy.allowed))
160 | }
161 |
162 | func testSettingAuthorizationValueDoesNotNullifyAllowed() {
163 | // given
164 | var tccPolicy = TCCPolicy(identifier: "id", codeRequirement: "req", receiverIdentifier: "recId", receiverCodeRequirement: "recreq")
165 | tccPolicy.allowed = false
166 |
167 | // when
168 | tccPolicy.authorization = .allowStandardUserToSetSystemService
169 |
170 | // then
171 | XCTAssertEqual(tccPolicy.allowed, false, "we don't have to nil this out because we use authorization by default if present")
172 | XCTAssertEqual(tccPolicy.authorization, TCCPolicyAuthorizationValue.allowStandardUserToSetSystemService)
173 | }
174 |
175 | func testJamfProAPIData() throws {
176 | // given - build the test profile
177 | let tccProfile = TCCProfileBuilder().buildProfile(allowed: false, authorization: .allow)
178 | let expected = try loadTextFile(fileName: "TestTCCProfileForJamfProAPI").trimmingCharacters(in: .whitespacesAndNewlines)
179 |
180 | // when - wrap in Jamf Pro API xml
181 | let data = try tccProfile.jamfProAPIData(signingIdentity: nil, site: nil)
182 |
183 | // then
184 | let xmlString = String(data: data, encoding: .utf8)
185 | XCTAssertEqual(xmlString, expected)
186 | }
187 |
188 | private func loadTextFile(fileName: String) throws -> String {
189 | let testBundle = Bundle(for: type(of: self))
190 | guard let resourceURL = testBundle.url(forResource: fileName, withExtension: "txt") else {
191 | XCTFail("Resource file should exists")
192 | return ""
193 | }
194 | return try String(contentsOf: resourceURL)
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/TCCProfileImporterTests/TestTCCProfileForJamfProAPI.txt:
--------------------------------------------------------------------------------
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 | <plist version="1.0">
4 | <dict>
5 | <key>PayloadContent</key>
6 | <array>
7 | <dict>
8 | <key>PayloadDescription</key>
9 | <string>Content Desc 1</string>
10 | <key>PayloadDisplayName</key>
11 | <string>Content Name 1</string>
12 | <key>PayloadIdentifier</key>
13 | <string>Content ID 1</string>
14 | <key>PayloadOrganization</key>
15 | <string>Content Org 1</string>
16 | <key>PayloadType</key>
17 | <string>Content type 1</string>
18 | <key>PayloadUUID</key>
19 | <string>Content UUID 1</string>
20 | <key>PayloadVersion</key>
21 | <integer>1</integer>
22 | <key>Services</key>
23 | <dict>
24 | <key>AppleEvents</key>
25 | <array>
26 | <dict>
27 | <key>AEReceiverCodeRequirement</key>
28 | <string>policy receiver code req</string>
29 | <key>AEReceiverIdentifier</key>
30 | <string>policy receiver id</string>
31 | <key>AEReceiverIdentifierType</key>
32 | <string>policy receiver id type</string>
33 | <key>Allowed</key>
34 | <false/>
35 | <key>Authorization</key>
36 | <string>Allow</string>
37 | <key>CodeRequirement</key>
38 | <string>policy code req</string>
39 | <key>Comment</key>
40 | <string>policy comment</string>
41 | <key>Identifier</key>
42 | <string>policy id</string>
43 | <key>IdentifierType</key>
44 | <string>policy id type</string>
45 | </dict>
46 | </array>
47 | <key>SystemPolicyAllFiles</key>
48 | <array>
49 | <dict>
50 | <key>AEReceiverCodeRequirement</key>
51 | <string>policy receiver code req</string>
52 | <key>AEReceiverIdentifier</key>
53 | <string>policy receiver id</string>
54 | <key>AEReceiverIdentifierType</key>
55 | <string>policy receiver id type</string>
56 | <key>Allowed</key>
57 | <false/>
58 | <key>Authorization</key>
59 | <string>Allow</string>
60 | <key>CodeRequirement</key>
61 | <string>policy code req</string>
62 | <key>Comment</key>
63 | <string>policy comment</string>
64 | <key>Identifier</key>
65 | <string>policy id</string>
66 | <key>IdentifierType</key>
67 | <string>policy id type</string>
68 | </dict>
69 | </array>
70 | </dict>
71 | </dict>
72 | </array>
73 | <key>PayloadDescription</key>
74 | <string>Test Desc</string>
75 | <key>PayloadDisplayName</key>
76 | <string>Test Name</string>
77 | <key>PayloadIdentifier</key>
78 | <string>Test ID</string>
79 | <key>PayloadOrganization</key>
80 | <string>Test Org</string>
81 | <key>PayloadScope</key>
82 | <string>System</string>
83 | <key>PayloadType</key>
84 | <string>Configuration</string>
85 | <key>PayloadUUID</key>
86 | <string>the uuid</string>
87 | <key>PayloadVersion</key>
88 | <integer>100</integer>
89 | </dict>
90 | </plist>
91 | Test NameTest Desc
--------------------------------------------------------------------------------
/PPPC UtilityTests/TCCProfileImporterTests/TestTCCProfileSigned-Broken.mobileconfig:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/PPPC UtilityTests/TCCProfileImporterTests/TestTCCProfileSigned-Broken.mobileconfig
--------------------------------------------------------------------------------
/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile-Broken.mobileconfig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PayloadDescription
6 | Test Unsigned Profile Empty
7 | PayloadDisplayName
8 | TestUnsignedProfile Empty
9 | PayloadIdentifier
10 | 3A4EDE0C-A189-4372-953F-304ECA0B6489
11 | PayloadOrganization
12 | Jamf
13 | PayloadType
14 | com.apple.TCC.configuration-profile-policy
15 | PayloadUUID
16 | 3FF5028C-CAA0-4B2F-A7BF-2A3539074424
17 | PayloadVersion
18 | 1
19 | PayloadScope
20 | system
21 |
22 |
--------------------------------------------------------------------------------
/PPPC UtilityTests/TCCProfileImporterTests/TestTCCUnsignedProfile-Empty.mobileconfig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PayloadDescription
6 | Test Unsigned Profile Empty
7 | PayloadDisplayName
8 | TestUnsignedProfile Empty
9 | PayloadIdentifier
10 | 3A4EDE0C-A189-4372-953F-304ECA0B6489
11 | PayloadOrganization
12 | Jamf
13 | PayloadType
14 | com.apple.TCC.configuration-profile-policy
15 | PayloadUUID
16 | 3FF5028C-CAA0-4B2F-A7BF-2A3539074424
17 | PayloadVersion
18 | 1
19 | PayloadScope
20 | system
21 |
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ![PPPC Utility logo][logo] Privacy Preferences Policy Control (PPPC) Utility
2 |
3 | [logo]: /Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32%402x.png "PPPC Utility"
4 |
5 | PPPC Utility is a macOS (10.15 and newer) application for creating configuration profiles containing the Privacy Preferences Policy Control payload for macOS. The profiles can be saved locally, signed or unsigned. Profiles can also be uploaded directly to a Jamf Pro server.
6 |
7 | All changes to the application are tracked in [the changelog](https://github.com/jamf/PPPC-Utility/blob/master/CHANGELOG.md).
8 |
9 | ## Installation
10 |
11 | Download the latest version from [the Releases page](https://github.com/jamf/PPPC-Utility/releases).
12 |
13 | ## Building profile
14 |
15 | Start by adding the bundles/executables for the payload by using drag-and-drop or by selecting the add (+) button in the left corner.
16 |
17 | 
18 |
19 | ## Saving
20 |
21 | Profiles can be saved locally either signed or unsigned.
22 |
23 | 
24 |
25 | 
26 |
27 | ## Upload to Jamf Pro
28 |
29 | PPPC Utility can use bearer token authentication (or basic authentication as a fallback for versions of Jamf Pro older than v10.34) to any supported
30 | Jamf Pro version using the username and password of a Jamf Pro user account. The user account at minimum needs the two privileges indicated below.
31 |
32 | Jamf Pro 10.49 and higher can use OAuth client credentials to access the API. The client ID and client secret generated by Jamf Pro in the
33 | "API Roles and clients" settings are used during the PPPC Utility upload process. When setting up the API Role, these are the permissions that
34 | PPPC Utility requires to upload the profiles.
35 |
36 | #### Required API Permissions
37 |
38 | - "Create macOS Configuration Profiles" - primary permission to upload profiles; each upload from PPPC Utility creates a new profile.
39 | - "Read Activation Code" - needed to retrieve the organization name that is placed in the profile.
40 |
41 | ### Jamf Pro 10.7.1 and newer
42 |
43 | Starting in Jamf Pro 10.7.1 the Privacy Preferences Policy Control Payload can be uploaded to the API without being signed before uploading.
44 |
45 | 
46 |
47 | ### Jamf Pro 10.7.0 and below
48 |
49 | To upload the Privacy Preferences Policy Control Payload to Jamf Pro 10.7.0 and below, the profile will need to be signed before uploading.
50 |
51 | 
52 |
53 | ## Importing
54 |
55 | Signed and unsigned profiles can be imported.
56 |
57 | 
58 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "PPPC_Logo_16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "PPPC_Logo_16@2x.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "PPPC_Logo_32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "PPPC_Logo_32@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "PPPC_Logo_128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "PPPC_Logo_128@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "PPPC_Logo_256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "PPPC_Logo_256@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "PPPC_Logo_512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "PPPC_Logo_512@2x.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_128.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_128@2x.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_16.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_16@2x.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_256.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_256@2x.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32@2x.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_512.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamf/PPPC-Utility/82249b9ce054342c671cf9956dba0a95e6b4bfb6/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_512@2x.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Resources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleVersion
22 | 3
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | Copyright © 2018 Jamf. All rights reserved.
27 | NSMainStoryboardFile
28 | Main
29 | NSPrincipalClass
30 | NSApplication
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Resources/PPPC Utility.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-write
8 |
9 | com.apple.security.network.client
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Resources/PPPCServices.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "mdmKey": "Accessibility",
4 | "englishName": "Accessibility",
5 | "englishDescription": "Allows specified apps to control the Mac via Accessibility APIs."
6 | },
7 | {
8 | "mdmKey": "AppleEvents",
9 | "englishName": "AppleEvents",
10 | "englishDescription": "Allows specified apps to send a restricted AppleEvent to another process."
11 | },
12 | {
13 | "mdmKey": "Calendar",
14 | "englishName": "Calendars",
15 | "englishDescription": "Allows specified apps access to event information managed by Calendar.",
16 | "entitlements":
17 | [
18 | "com.apple.security.personal-information.calendars"
19 | ]
20 | },
21 | {
22 | "mdmKey": "Camera",
23 | "englishName": "Camera",
24 | "englishDescription": "Use to deny specified apps access to the camera.",
25 | "entitlements":
26 | [
27 | "com.apple.developer.avfoundation.multitasking-camera-access",
28 | "com.apple.security.device.camera"
29 | ],
30 | "denyOnly": true
31 | },
32 | {
33 | "mdmKey": "AddressBook",
34 | "englishName": "Contacts",
35 | "englishDescription": "Allows specified apps access to contact information managed by Contacts.",
36 | "entitlements":
37 | [
38 | "com.apple.developer.contacts.notes",
39 | "com.apple.security.personal-information.addressbook"
40 | ]
41 | },
42 | {
43 | "mdmKey": "SystemPolicyDesktopFolder",
44 | "englishName": "Desktop Folder",
45 | "englishDescription": "Allows specified apps access to the Desktop folder.",
46 | "entitlements":
47 | [
48 | "com.apple.security.files.user-selected.read-only",
49 | "com.apple.security.files.user-selected.read-write"
50 | ]
51 | },
52 | {
53 | "mdmKey": "SystemPolicyDocumentsFolder",
54 | "englishName": "Documents Folder",
55 | "englishDescription": "Allows specified apps access to the Documents folder.",
56 | "entitlements":
57 | [
58 | "com.apple.security.files.user-selected.read-only",
59 | "com.apple.security.files.user-selected.read-write"
60 | ]
61 | },
62 | {
63 | "mdmKey": "SystemPolicyDownloadsFolder",
64 | "englishName": "Downloads Folder",
65 | "englishDescription": "Allows specified apps access to the Downloads folder.",
66 | "entitlements":
67 | [
68 | "com.apple.security.files.downloads.read-only",
69 | "com.apple.security.files.downloads.read-write",
70 | "com.apple.security.files.user-selected.read-only",
71 | "com.apple.security.files.user-selected.read-write"
72 | ]
73 | },
74 | {
75 | "mdmKey": "FileProviderPresence",
76 | "englishName": "File Provider presence",
77 | "englishDescription": "Allows specified File Provider apps access to know when the user is using files managed by the File Provider.",
78 | "entitlements":
79 | [
80 | "com.apple.developer.fileprovider.testing-mode"
81 | ]
82 | },
83 | {
84 | "mdmKey": "ListenEvent",
85 | "englishName": "Input Monitoring",
86 | "englishDescription": "Set which approved apps have specified access to input devices (mouse, keyboard, trackpad).",
87 | "entitlements":
88 | [
89 | "com.apple.security.device.usb",
90 | "com.apple.vm.device-access"
91 | ],
92 | "denyOnly": true,
93 | "allowStandardUsersMacOS11Plus": true
94 | },
95 | {
96 | "mdmKey": "MediaLibrary",
97 | "englishName": "Media & Apple Music",
98 | "englishDescription": "Allows specified apps access to access Apple Music, music and video activity, and the media library.",
99 | "entitlements":
100 | [
101 | "com.apple.security.assets.music.read-only",
102 | "com.apple.security.assets.music.read-write",
103 | "com.apple.security.files.user-selected.read-only",
104 | "com.apple.security.files.user-selected.read-write",
105 | "com.apple.security.assets.movies.read-only",
106 | "com.apple.security.assets.movies.read-write"
107 | ]
108 | },
109 | {
110 | "mdmKey": "Microphone",
111 | "englishName": "Microphone",
112 | "englishDescription": "Deny specified apps access to the microphone.",
113 | "entitlements":
114 | [
115 | "com.apple.security.device.microphone"
116 | ],
117 | "denyOnly": true
118 | },
119 | {
120 | "mdmKey": "SystemPolicyNetworkVolumes",
121 | "englishName": "Network Volumes",
122 | "englishDescription": "Allows specified apps access to files on network volumes.",
123 | "entitlements":
124 | [
125 | "com.apple.security.files.user-selected.read-only",
126 | "com.apple.security.files.user-selected.read-write"
127 | ]
128 | },
129 | {
130 | "mdmKey": "Photos",
131 | "englishName": "Photos",
132 | "englishDescription": "Allows specified apps access to images managed by the Photos app in: /Users/username/Pictures/Photos Library Note: If the user put their photo library somewhere else, it won’t be protected from apps.",
133 | "entitlements":
134 | [
135 | "com.apple.security.assets.pictures.read-only",
136 | "com.apple.security.assets.pictures.read-write",
137 | "com.apple.security.files.user-selected.read-only",
138 | "com.apple.security.files.user-selected.read-write"
139 | ]
140 | },
141 | {
142 | "mdmKey": "PostEvent",
143 | "englishName": "Post Event",
144 | "englishDescription": "Allows specified apps to use CoreGraphics APIs to send CGEvents to the system event stream."
145 | },
146 | {
147 | "mdmKey": "Reminders",
148 | "englishName": "Reminders",
149 | "englishDescription": "Allows specified apps access to information managed by Reminders."
150 | },
151 | {
152 | "mdmKey": "SystemPolicyRemovableVolumes",
153 | "englishName": "Removable Volumes",
154 | "englishDescription": "Allows specified apps access to files on removable volumes.",
155 | "entitlements":
156 | [
157 | "com.apple.security.files.user-selected.read-only",
158 | "com.apple.security.files.user-selected.read-write"
159 | ]
160 | },
161 | {
162 | "mdmKey": "ScreenCapture",
163 | "englishName": "Screen Recording",
164 | "englishDescription": "Deny specified apps access to capture (read) the contents of the system display.",
165 | "denyOnly": true,
166 | "allowStandardUsersMacOS11Plus": true
167 | },
168 | {
169 | "mdmKey": "SpeechRecognition",
170 | "englishName": "Speech Recognition",
171 | "englishDescription": "Allows specified apps to use the system Speech Recognition feature and to send speech data to Apple."
172 | },
173 | {
174 | "mdmKey": "SystemPolicyAllFiles",
175 | "englishName": "Full Disk Access",
176 | "englishDescription": "Allows specified apps access to data like Mail, Messages, Safari, Home, Time Machine backups, and certain administrative settings for all users on the Mac.",
177 | "entitlements":
178 | [
179 | "com.apple.security.files.all",
180 | "com.apple.security.files.user-selected.read-only",
181 | "com.apple.security.files.user-selected.read-write"
182 | ]
183 | },
184 | {
185 | "mdmKey": "SystemPolicySysAdminFiles",
186 | "englishName": "Administrator Files",
187 | "englishDescription": "Allows specified apps access to some files used by system administrators.",
188 | "entitlements":
189 | [
190 | "com.apple.security.files.user-selected.read-only",
191 | "com.apple.security.files.user-selected.read-write"
192 | ]
193 | }
194 | ]
195 |
--------------------------------------------------------------------------------
/Source/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2018 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Cocoa
29 |
30 | @NSApplicationMain
31 | class AppDelegate: NSObject, NSApplicationDelegate {
32 |
33 | func applicationDidFinishLaunching(_ aNotification: Notification) {}
34 |
35 | func applicationWillTerminate(_ aNotification: Notification) {}
36 |
37 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
38 | return true
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/Source/Extensions/ArrayExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArrayExtensions.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2018 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 | extension Array where Element: Equatable {
30 | mutating func appendIfNew(_ item: Element) {
31 | if !contains(item) {
32 | append(item)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Source/Extensions/LoggerExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoggerExtensions.swift
3 | // PPPC Utility
4 | //
5 | // Created by Skyler Godfrey on 10/21/24.
6 | // Copyright © 2024 Jamf. All rights reserved.
7 | //
8 |
9 | // This extension simplifies the logger instance creation by calling the bundle Id
10 | // and pre-declaring categories. Currently the predefined categories match the
11 | // class name.
12 |
13 | import OSLog
14 |
15 | extension Logger {
16 | static let subsystem = Bundle.main.bundleIdentifier!
17 | static let TCCProfileViewController = Logger(subsystem: subsystem, category: "TCCProfileViewController")
18 | static let PPPCServicesManager = Logger(subsystem: subsystem, category: "PPPCServicesManager")
19 | static let Model = Logger(subsystem: subsystem, category: "Model")
20 | static let SaveViewController = Logger(subsystem: subsystem, category: "SaveViewController")
21 | static let UploadInfoView = Logger(subsystem: subsystem, category: "UploadInfoView")
22 | static let UploadManager = Logger(subsystem: subsystem, category: "UploadManager")
23 | }
24 |
--------------------------------------------------------------------------------
/Source/Extensions/TCCProfileExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TCCProfileExtensions.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2020 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 |
30 | public extension TCCProfile {
31 |
32 | enum ParseError: Error {
33 | case failedToCreateDecoder
34 | }
35 |
36 | /// Create a ``TCCProfile`` object from a `Data` containing a provisioning profile.
37 | /// - Parameter profileData: The raw profile data (generally from a file read operation). This may be CMS encoded.
38 | /// - Returns: A ``TCCProfile`` instance.
39 | /// - Throws: Either a ``TCCProfile.ParseError`` or a `DecodingError`.
40 | static func parse(from profileData: Data) throws -> TCCProfile {
41 | // The profile may be CMS encoded; let's try to decode it.
42 | guard let cmsDecoder = SwiftyCMSDecoder() else {
43 | throw ParseError.failedToCreateDecoder
44 | }
45 | cmsDecoder.updateMessage(data: profileData as NSData)
46 | cmsDecoder.finaliseMessage()
47 |
48 | var plistData: Data
49 | if let decodedData = cmsDecoder.data {
50 | // Use the decoded data if CMS decoding worked.
51 | plistData = decodedData
52 | } else {
53 | // Assume it failed because it's not encrypted and move on to deserialize with original data.
54 | plistData = profileData
55 | }
56 |
57 | // Adjust letter case of required keys to be more flexible during import.
58 | plistData = fixLetterCase(of: plistData)
59 |
60 | return try PropertyListDecoder().decode(TCCProfile.self, from: plistData)
61 | }
62 |
63 | /// Adjust the letter case of required profile keys to meet the standard during import.
64 | /// - Parameter original: The original data from the file
65 | /// - Returns: (Possibly) updated data with all of the required profile keys meeting the standard letter case.
66 | private static func fixLetterCase(of original: Data) -> Data {
67 | guard let originalString = String(data: original, encoding: .utf8) else {
68 | return original
69 | }
70 | var newString = originalString
71 |
72 | // This block looks for the given coding key within a property list XML string and
73 | // converts it to the standard letter case.
74 | let conversionBlock = { (codingKey: CodingKey) in
75 | let requiredString = ">\(codingKey.stringValue)<"
76 | newString = newString.replacingOccurrences(of: requiredString,
77 | with: requiredString,
78 | options: .caseInsensitive)
79 | }
80 |
81 | // Currently there are three model structs that are used to decode the profile.
82 | TCCProfile.CodingKeys.allCases.forEach(conversionBlock)
83 | TCCProfile.Content.CodingKeys.allCases.forEach(conversionBlock)
84 | TCCPolicy.CodingKeys.allCases.forEach(conversionBlock)
85 |
86 | // Convert the String back to Data
87 | guard let newData = newString.data(using: .utf8) else {
88 | return original
89 | }
90 |
91 | return newData
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Source/Model/AppleEventRule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppleEventRule.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2018 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Cocoa
29 |
30 | class AppleEventRule: NSObject {
31 |
32 | @objc dynamic var source: Executable!
33 | @objc dynamic var destination: Executable!
34 | @objc dynamic var valueString: String! = TCCProfileDisplayValue.allow.rawValue
35 |
36 | var value: Bool { return valueString == TCCProfileDisplayValue.allow.rawValue }
37 |
38 | init(source: Executable, destination: Executable, value: Bool) {
39 | self.source = source
40 | self.destination = destination
41 | self.valueString = value ? TCCProfileDisplayValue.allow.rawValue : TCCProfileDisplayValue.deny.rawValue
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Source/Model/Executable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Executable.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2018 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Cocoa
29 |
30 | class Executable: NSObject {
31 |
32 | @objc dynamic var iconPath: String!
33 |
34 | @objc dynamic var displayName: String!
35 | @objc dynamic var identifier: String!
36 | @objc dynamic var codeRequirement: String!
37 |
38 | @objc dynamic var policy: Policy = Policy()
39 | @objc dynamic var appleEvents: [AppleEventRule] = []
40 |
41 | override init() {
42 | super.init()
43 | }
44 |
45 | init(identifier: String, codeRequirement: String, _ displayName: String? = nil) {
46 | super.init()
47 |
48 | self.identifier = identifier
49 | self.codeRequirement = codeRequirement
50 | if displayName != nil {
51 | self.displayName = displayName
52 | } else {
53 | self.displayName = generateDisplayName(identifier: identifier)
54 | }
55 | self.iconPath = generateIconPath(identifier: identifier)
56 | }
57 |
58 | func generateDisplayName(identifier: String) -> String {
59 | var separatedBy = "."
60 | if identifier.contains("/") {
61 | separatedBy = "/"
62 | }
63 | let partNames = identifier.components(separatedBy: separatedBy)
64 |
65 | return partNames.last ?? identifier
66 | }
67 |
68 | func generateIconPath(identifier: String) -> String {
69 | if identifier.contains("/") {
70 | return IconFilePath.binary
71 | } else {
72 | return IconFilePath.application
73 | }
74 | }
75 | }
76 |
77 | class Policy: NSObject {
78 | // swiftlint:disable identifier_name
79 | @objc dynamic var AddressBook: String = "-"
80 | @objc dynamic var Calendar: String = "-"
81 | @objc dynamic var Reminders: String = "-"
82 | @objc dynamic var Photos: String = "-"
83 | @objc dynamic var Camera: String = "-"
84 | @objc dynamic var Microphone: String = "-"
85 | @objc dynamic var Accessibility: String = "-"
86 | @objc dynamic var PostEvent: String = "-"
87 | @objc dynamic var SystemPolicyAllFiles: String = "-"
88 | @objc dynamic var SystemPolicySysAdminFiles: String = "-"
89 | @objc dynamic var FileProviderPresence: String = "-"
90 | @objc dynamic var ListenEvent: String = "-"
91 | @objc dynamic var MediaLibrary: String = "-"
92 | @objc dynamic var ScreenCapture: String = "-"
93 | @objc dynamic var SpeechRecognition: String = "-"
94 | @objc dynamic var SystemPolicyDesktopFolder: String = "-"
95 | @objc dynamic var SystemPolicyDocumentsFolder: String = "-"
96 | @objc dynamic var SystemPolicyDownloadsFolder: String = "-"
97 | @objc dynamic var SystemPolicyNetworkVolumes: String = "-"
98 | @objc dynamic var SystemPolicyRemovableVolumes: String = "-"
99 | // swiftlint:enable identifier_name
100 |
101 | func allPolicyValues() -> [String] {
102 | let mirror = Mirror(reflecting: self)
103 | return mirror.children.compactMap { _, value in
104 | return value as? String
105 | }
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/Source/Model/LoadExecutableError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoadExecutableError.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2019 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 | public enum LoadExecutableError: Error {
30 | case identifierNotFound
31 | case resourceURLNotFound
32 | case codeRequirementError(description: String)
33 | case executableNotFound
34 | case executableAlreadyExists
35 | }
36 |
37 | extension LoadExecutableError: LocalizedError {
38 | public var errorDescription: String? {
39 | switch self {
40 | case .identifierNotFound:
41 | return "Bundle identifier could not be found."
42 | case .resourceURLNotFound:
43 | return "Resource URL could not be found."
44 | case .codeRequirementError(let description):
45 | return "Failed to get designated code requirement. The executable may not be signed. Error: \(description)"
46 | case .executableNotFound:
47 | return "Could not find executable from url path"
48 | case .executableAlreadyExists:
49 | return "The executable is already loaded."
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Source/Model/Model.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Model.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2018 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Cocoa
29 | import OSLog
30 |
31 | @objc class Model: NSObject {
32 |
33 | var usingLegacyAllowKey = true
34 |
35 | @objc dynamic var current: Executable?
36 | @objc dynamic static let shared = Model()
37 | @objc dynamic var identities: [SigningIdentity] = []
38 | @objc dynamic var selectedExecutables: [Executable] = []
39 |
40 | let logger = Logger.Model
41 |
42 | func getAppleEventChoices(executable: Executable) -> [Executable] {
43 | var executables: [Executable] = []
44 |
45 | loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/System Events.app")) { result in
46 | switch result {
47 | case .success(let executable):
48 | executables.append(executable)
49 | case .failure(let error):
50 | self.logger.error("\(error)")
51 | }
52 | }
53 |
54 | loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/SystemUIServer.app")) { result in
55 | switch result {
56 | case .success(let executable):
57 | executables.append(executable)
58 | case .failure(let error):
59 | self.logger.error("\(error)")
60 | }
61 | }
62 |
63 | loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/Finder.app")) { result in
64 | switch result {
65 | case .success(let executable):
66 | executables.append(executable)
67 | case .failure(let error):
68 | self.logger.error("\(error)")
69 | }
70 | }
71 |
72 | let others = store.values.filter { $0 != executable && !Set(executables).contains($0) }
73 | executables.append(contentsOf: others)
74 |
75 | return executables
76 | }
77 |
78 | var store: [String: Executable] = [:]
79 | public var importedTCCProfile: TCCProfile?
80 | }
81 |
82 | // MARK: Loading executable
83 |
84 | struct IconFilePath {
85 | static let binary = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/ExecutableBinaryIcon.icns"
86 | static let application = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns"
87 | static let kext = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/KEXT.icns"
88 | static let unknown = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericQuestionMarkIcon.icns"
89 | }
90 |
91 | typealias LoadExecutableResult = Result
92 | typealias LoadExecutableCompletion = ((LoadExecutableResult) -> Void)
93 |
94 | extension Model {
95 |
96 | func requiresAuthorizationKey() -> Bool {
97 | return selectedExecutables.contains { exe -> Bool in
98 | return exe.policy.allPolicyValues().contains { value -> Bool in
99 | return value == TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue
100 | }
101 | }
102 | }
103 |
104 | /// Will convert any Authorization key values to the legacy Allowed key
105 | func changeToUseLegacyAllowKey() {
106 | usingLegacyAllowKey = true
107 | selectedExecutables.forEach { exe in
108 | if exe.policy.ListenEvent == TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue {
109 | exe.policy.ListenEvent = "-"
110 | }
111 | if exe.policy.ScreenCapture == TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue {
112 | exe.policy.ScreenCapture = "-"
113 | }
114 | }
115 | }
116 |
117 | // TODO - refactor this method so it isn't so complex
118 | // swiftlint:disable:next cyclomatic_complexity
119 | func loadExecutable(url: URL, completion: @escaping LoadExecutableCompletion) {
120 | let executable = Executable()
121 |
122 | if let bundle = Bundle(url: url) {
123 | guard let identifier = bundle.bundleIdentifier else {
124 | return completion(.failure(.identifierNotFound))
125 | }
126 | executable.identifier = identifier
127 | let info = bundle.infoDictionary
128 | executable.displayName = (info?["CFBundleName"] as? String) ?? executable.identifier
129 | if let resourcesURL = bundle.resourceURL {
130 | if let definedIconFile = info?["CFBundleIconFile"] as? String {
131 | var iconURL = resourcesURL.appendingPathComponent(definedIconFile)
132 | if iconURL.pathExtension.isEmpty {
133 | iconURL.appendPathExtension("icns")
134 | }
135 | executable.iconPath = iconURL.path
136 | } else {
137 | executable.iconPath = resourcesURL.appendingPathComponent("DefaultAppIcon.icns").path
138 | }
139 |
140 | if !FileManager.default.fileExists(atPath: executable.iconPath) {
141 | switch url.pathExtension {
142 | case "app":
143 | executable.iconPath = IconFilePath.application
144 | case "bundle":
145 | executable.iconPath = IconFilePath.kext
146 | case "xpc":
147 | executable.iconPath = IconFilePath.kext
148 | default:
149 | executable.iconPath = IconFilePath.unknown
150 | }
151 | }
152 | } else {
153 | return completion(.failure(.resourceURLNotFound))
154 | }
155 | } else {
156 | executable.identifier = url.path
157 | executable.displayName = url.lastPathComponent
158 | executable.iconPath = IconFilePath.binary
159 | }
160 |
161 | if let alreadyFoundExecutable = store[executable.identifier] {
162 | return completion(.success(alreadyFoundExecutable))
163 | }
164 |
165 | do {
166 | executable.codeRequirement = try SecurityWrapper.copyDesignatedRequirement(url: url)
167 | store[executable.identifier] = executable
168 | return completion(.success(executable))
169 | } catch {
170 | return completion(.failure(.codeRequirementError(description: error.localizedDescription)))
171 | }
172 | }
173 | }
174 |
175 | // MARK: Exporting Profile
176 |
177 | extension Model {
178 |
179 | func exportProfile(organization: String, identifier: String, displayName: String, payloadDescription: String) -> TCCProfile {
180 | var services = [String: [TCCPolicy]]()
181 |
182 | selectedExecutables.forEach { executable in
183 |
184 | let mirroredServices = Mirror(reflecting: executable.policy)
185 |
186 | for attr in mirroredServices.children {
187 | if let key = attr.label, let value = attr.value as? String {
188 | if let policyToAppend = policyFromString(executable: executable, value: value) {
189 | services[key] = services[key] ?? []
190 | services[key]?.append(policyToAppend)
191 | }
192 | }
193 | }
194 |
195 | executable.appleEvents.forEach { event in
196 | let policy = policyFromString(executable: executable, value: event.valueString, event: event)
197 | if let policy = policy {
198 | let appleEventsKey = ServicesKeys.appleEvents.rawValue
199 | services[appleEventsKey] = services[appleEventsKey] ?? []
200 | services[appleEventsKey]?.append(policy)
201 | }
202 | }
203 | }
204 |
205 | return TCCProfile(organization: organization,
206 | identifier: identifier,
207 | displayName: displayName,
208 | payloadDescription: payloadDescription,
209 | services: services)
210 | }
211 |
212 | func importProfile(tccProfile: TCCProfile) {
213 | if let content = tccProfile.content.first {
214 | self.cleanUpAndRemoveDependencies()
215 |
216 | self.importedTCCProfile = tccProfile
217 |
218 | for (key, policies) in content.services {
219 | getExecutablesFromAllPolicies(policies: policies)
220 |
221 | for policy in policies {
222 | let executable = getExecutableFromSelectedExecutables(bundleIdentifier: policy.identifier)
223 | if key == ServicesKeys.appleEvents.rawValue {
224 | if let source = executable,
225 | let rIdentifier = policy.receiverIdentifier,
226 | let rCodeRequirement = policy.receiverCodeRequirement {
227 | let destination = getExecutableFrom(identifier: rIdentifier, codeRequirement: rCodeRequirement)
228 | let allowed: Bool = (policy.allowed == true || policy.authorization == TCCPolicyAuthorizationValue.allow)
229 | let appleEvent = AppleEventRule(source: source, destination: destination, value: allowed)
230 | executable?.appleEvents.appendIfNew(appleEvent)
231 | }
232 | } else {
233 | if policy.authorization == .allow || policy.allowed == true {
234 | executable?.policy.setValue(TCCProfileDisplayValue.allow.rawValue, forKey: key)
235 | } else if policy.authorization == .allowStandardUserToSetSystemService {
236 | executable?.policy.setValue(TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue, forKey: key)
237 | } else {
238 | executable?.policy.setValue(TCCProfileDisplayValue.deny.rawValue, forKey: key)
239 | }
240 | }
241 | }
242 | }
243 | }
244 | }
245 |
246 | func policyFromString(executable: Executable, value: String, event: AppleEventRule? = nil) -> TCCPolicy? {
247 | var policy = TCCPolicy(identifier: executable.identifier,
248 | codeRequirement: executable.codeRequirement,
249 | receiverIdentifier: event?.destination.identifier,
250 | receiverCodeRequirement: event?.destination.codeRequirement)
251 | if usingLegacyAllowKey {
252 | switch value {
253 | case TCCProfileDisplayValue.allow.rawValue:
254 | policy.allowed = true
255 | case TCCProfileDisplayValue.deny.rawValue:
256 | policy.allowed = false
257 | default:
258 | return nil
259 | }
260 | } else {
261 | switch value {
262 | case TCCProfileDisplayValue.allow.rawValue:
263 | policy.authorization = .allow
264 | case TCCProfileDisplayValue.deny.rawValue:
265 | policy.authorization = .deny
266 | case TCCProfileDisplayValue.allowStandardUsersToApprove.rawValue:
267 | policy.authorization = .allowStandardUserToSetSystemService
268 | default:
269 | return nil
270 | }
271 | }
272 | return policy
273 | }
274 |
275 | func getExecutablesFromAllPolicies(policies: [TCCPolicy]) {
276 | for tccPolicy in policies where getExecutableFromSelectedExecutables(bundleIdentifier: tccPolicy.identifier) == nil {
277 | let executable = getExecutableFrom(identifier: tccPolicy.identifier, codeRequirement: tccPolicy.codeRequirement)
278 | self.selectedExecutables.append(executable)
279 | }
280 | }
281 |
282 | func getExecutableFromSelectedExecutables(bundleIdentifier: String) -> Executable? {
283 | for executable in selectedExecutables where executable.identifier == bundleIdentifier {
284 | return executable
285 | }
286 | return nil
287 | }
288 |
289 | func getExecutableFrom(identifier: String, codeRequirement: String) -> Executable {
290 | var executable = Executable(identifier: identifier, codeRequirement: codeRequirement)
291 | findExecutableOnComputerUsing(bundleIdentifier: identifier) { result in
292 | switch result {
293 | case .success(let goodExecutable):
294 | executable = goodExecutable
295 | case .failure(let error):
296 | self.logger.error("\(error)")
297 | }
298 | }
299 |
300 | return executable
301 | }
302 |
303 | private func findExecutableOnComputerUsing(bundleIdentifier: String, completion: @escaping LoadExecutableCompletion) {
304 | var urlToLoad: URL?
305 | if bundleIdentifier.contains("/") {
306 | urlToLoad = URL(string: "file://\(bundleIdentifier)")
307 | } else {
308 | urlToLoad = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier)
309 | }
310 |
311 | if let fileURL = urlToLoad {
312 | self.loadExecutable(url: fileURL) { result in
313 | switch result {
314 | case .success(let executable):
315 | return completion(.success(executable))
316 | case .failure(let error):
317 | return completion(.failure(error))
318 | }
319 | }
320 | }
321 | return completion(.failure(.executableNotFound))
322 | }
323 |
324 | private func cleanUpAndRemoveDependencies() {
325 | for executable in self.selectedExecutables {
326 | executable.appleEvents = []
327 | executable.policy = Policy()
328 | }
329 | self.selectedExecutables = []
330 | self.current = nil
331 | self.store.removeAll()
332 | self.importedTCCProfile = nil
333 | }
334 | }
335 |
--------------------------------------------------------------------------------
/Source/Model/PPPCServiceInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PPPCServiceInfo.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2022 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 |
30 | /// Holds the information about a single PPPC Service provided by Apple.
31 | struct PPPCServiceInfo: Decodable {
32 | let mdmKey: String
33 | let englishName: String
34 | let englishDescription: String
35 | let entitlements: [String]?
36 | let denyOnly: Bool?
37 | let allowStandardUsersMacOS11Plus: Bool?
38 |
39 | var userHelp: String {
40 | if let entitlements = entitlements {
41 | return "\(englishDescription)\n\nMDM Key: \(mdmKey)\nRelated entitlements: \(entitlements)"
42 | } else {
43 | return "\(englishDescription)\n\nMDM Key: \(mdmKey)"
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Source/Model/PPPCServicesManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PPPCServicesManager.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2022 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 | import OSLog
30 |
31 | class PPPCServicesManager {
32 |
33 | typealias MDMServiceKey = String
34 |
35 | let logger = Logger.PPPCServicesManager
36 |
37 | static let shared = PPPCServicesManager()
38 |
39 | let allServices: [MDMServiceKey: PPPCServiceInfo]
40 |
41 | init() {
42 | var hashed = [MDMServiceKey: PPPCServiceInfo]()
43 |
44 | do {
45 | guard let dataURL = Bundle.main.url(forResource: "PPPCServices", withExtension: "json") else {
46 | throw CocoaError(.fileNoSuchFile)
47 | }
48 |
49 | let data = try Data(contentsOf: dataURL)
50 | let decoder = JSONDecoder()
51 | let loadedServices = try decoder.decode([PPPCServiceInfo].self, from: data)
52 |
53 | loadedServices.forEach { service in
54 | hashed[service.mdmKey] = service
55 | }
56 | } catch {
57 | logger.error("Error loading PPPCServices.json: \(error)")
58 | }
59 |
60 | allServices = hashed
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Source/Model/SemanticVersion.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SemanticVersion.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2022 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | /// A simple struct defining a semantic version number.
29 | struct SemanticVersion: Comparable {
30 | let major: Int
31 | let minor: Int
32 | let patch: Int
33 |
34 | static func < (left: SemanticVersion, right: SemanticVersion) -> Bool {
35 | if left.major < right.major {
36 | return true
37 | }
38 |
39 | if left.major == right.major {
40 | if left.minor < right.minor {
41 | return true
42 | }
43 |
44 | if left.minor == right.minor && left.patch < right.patch {
45 | return true
46 | }
47 | }
48 |
49 | return false
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Source/Model/SigningIdentity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SigningIdentity.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2018 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Cocoa
29 |
30 | class SigningIdentity: NSObject {
31 |
32 | @objc dynamic var displayName: String
33 | var reference: SecIdentity?
34 |
35 | init(name: String, reference: SecIdentity?) {
36 | displayName = name
37 | super.init()
38 | self.reference = reference
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Source/Model/TCCProfile.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TCCProfile.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2018 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 |
30 | typealias TCCPolicyIdentifierType = String
31 | typealias TCCPolicyAuthorizationValue = String
32 |
33 | extension TCCPolicyIdentifierType {
34 | static let bundleID = "bundleID"
35 | static let path = "path"
36 | }
37 |
38 | extension TCCPolicyAuthorizationValue {
39 | static let allow = "Allow"
40 | static let deny = "Deny"
41 | static let allowStandardUserToSetSystemService = "AllowStandardUserToSetSystemService"
42 | }
43 |
44 | struct TCCPolicy: Codable {
45 | var comment: String
46 | var identifier: String
47 | var identifierType: TCCPolicyIdentifierType
48 | var codeRequirement: String
49 |
50 | /// legacy value to allow or deny a service. When setting this value, the authorization will be set to nil
51 | /// as the values are mutually exclusive. If authorization is present it will always be used, so we have no
52 | /// need to nil out this value if authorization is set.
53 | var allowed: Bool? {
54 | didSet {
55 | authorization = nil
56 | }
57 | }
58 | var authorization: TCCPolicyAuthorizationValue?
59 | var receiverIdentifier: String?
60 | var receiverIdentifierType: TCCPolicyIdentifierType?
61 | var receiverCodeRequirement: String?
62 |
63 | enum CodingKeys: String, CodingKey, CaseIterable {
64 | case identifier = "Identifier"
65 | case identifierType = "IdentifierType"
66 | case allowed = "Allowed"
67 | case authorization = "Authorization"
68 | case codeRequirement = "CodeRequirement"
69 | case comment = "Comment"
70 | case receiverIdentifier = "AEReceiverIdentifier"
71 | case receiverIdentifierType = "AEReceiverIdentifierType"
72 | case receiverCodeRequirement = "AEReceiverCodeRequirement"
73 | }
74 |
75 | init(identifier: String, codeRequirement: String, receiverIdentifier: String? = nil, receiverCodeRequirement: String? = nil) {
76 | self.comment = ""
77 | self.identifier = identifier
78 | self.identifierType = identifier.contains("/") ? .path : .bundleID
79 | self.codeRequirement = codeRequirement
80 | self.receiverIdentifier = receiverIdentifier
81 | if let otherIdentifier = receiverIdentifier {
82 | self.receiverIdentifierType = otherIdentifier.contains("/") ? .path : .bundleID
83 | }
84 | self.receiverCodeRequirement = receiverCodeRequirement
85 | }
86 | }
87 |
88 | public struct TCCProfile: Codable {
89 | struct Content: Codable {
90 | var payloadDescription: String
91 | var displayName: String
92 | var identifier: String
93 | var organization: String
94 | var type: String
95 | var uuid: String
96 | var version: Int
97 | var services: [String: [TCCPolicy]]
98 |
99 | // swiftlint:disable:next nesting
100 | enum CodingKeys: String, CodingKey, CaseIterable {
101 | case payloadDescription = "PayloadDescription"
102 | case displayName = "PayloadDisplayName"
103 | case identifier = "PayloadIdentifier"
104 | case organization = "PayloadOrganization"
105 | case type = "PayloadType"
106 | case uuid = "PayloadUUID"
107 | case version = "PayloadVersion"
108 | case services = "Services"
109 | }
110 | }
111 |
112 | var version: Int
113 | var uuid: String
114 | var type: String
115 | var scope: String
116 | var organization: String
117 | var identifier: String
118 | var displayName: String
119 | var payloadDescription: String
120 | var content: [Content]
121 | enum CodingKeys: String, CodingKey, CaseIterable {
122 | case payloadDescription = "PayloadDescription"
123 | case displayName = "PayloadDisplayName"
124 | case identifier = "PayloadIdentifier"
125 | case organization = "PayloadOrganization"
126 | case scope = "PayloadScope"
127 | case type = "PayloadType"
128 | case uuid = "PayloadUUID"
129 | case version = "PayloadVersion"
130 | case content = "PayloadContent"
131 | }
132 | init(organization: String, identifier: String, displayName: String, payloadDescription: String, services: [String: [TCCPolicy]]) {
133 | let content = Content(payloadDescription: payloadDescription,
134 | displayName: displayName,
135 | identifier: identifier,
136 | organization: organization,
137 | type: "com.apple.TCC.configuration-profile-policy",
138 | uuid: UUID().uuidString,
139 | version: 1,
140 | services: services)
141 | self.version = 1
142 | self.uuid = UUID().uuidString
143 | self.type = "Configuration"
144 | self.scope = "System"
145 | self.organization = content.organization
146 | self.identifier = content.identifier
147 | self.displayName = content.displayName
148 | self.payloadDescription = content.payloadDescription
149 | self.content = [content]
150 | }
151 |
152 | func xmlData() throws -> Data {
153 | let encoder = PropertyListEncoder()
154 | encoder.outputFormat = .xml
155 | return try encoder.encode(self)
156 | }
157 |
158 | /// Wraps the ``TCCProfile`` in the XML tags for upload to the Jamf Pro API.
159 | /// - Parameters:
160 | /// - signingIdentity: A signing identity; can be nil to leave the profile unsigned.
161 | /// - site: A Jamf Pro site
162 | /// - Returns: XML data for use with the Jamf Pro API.
163 | func jamfProAPIData(signingIdentity: SecIdentity?, site: (String, String)?) throws -> Data {
164 | var profileText: String
165 | var profileData = try xmlData()
166 | if let identity = signingIdentity {
167 | profileData = try SecurityWrapper.sign(data: profileData, using: identity)
168 | }
169 | profileText = String(data: profileData, encoding: .utf8) ?? ""
170 |
171 | let root = XMLElement(name: "os_x_configuration_profile")
172 | let general = XMLElement(name: "general")
173 | root.addChild(general)
174 |
175 | let payloads = XMLElement(name: "payloads", stringValue: profileText)
176 |
177 | general.addChild(payloads)
178 |
179 | if let site = site {
180 | let sites = XMLElement(name: "site")
181 | let siteId = XMLElement(name: "id", stringValue: site.0)
182 | let siteName = XMLElement(name: "name", stringValue: site.1)
183 | sites.addChild(siteId)
184 | sites.addChild(siteName)
185 | general.addChild(sites)
186 | }
187 |
188 | general.addChild(XMLElement(name: "name", stringValue: displayName))
189 | general.addChild(XMLElement(name: "description", stringValue: payloadDescription))
190 |
191 | let xml = XMLDocument(rootElement: root)
192 | return xml.xmlData
193 | }
194 | }
195 |
196 | enum ServicesKeys: String {
197 | case addressBook = "AddressBook"
198 | case calendar = "Calendar"
199 | case reminders = "Reminders"
200 | case photos = "Photos"
201 | case camera = "Camera"
202 | case microphone = "Microphone"
203 | case accessibility = "Accessibility"
204 | case postEvent = "PostEvent"
205 | case allFiles = "SystemPolicyAllFiles"
206 | case adminFiles = "SystemPolicySysAdminFiles"
207 | case fileProviderPresence = "FileProviderPresence"
208 | case listenEvent = "ListenEvent"
209 | case mediaLibrary = "MediaLibrary"
210 | case screenCapture = "ScreenCapture"
211 | case speechRecognition = "SpeechRecognition"
212 | case desktopFolder = "SystemPolicyDesktopFolder"
213 | case documentsFolder = "SystemPolicyDocumentsFolder"
214 | case downloadsFolder = "SystemPolicyDownloadsFolder"
215 | case networkVolumes = "SystemPolicyNetworkVolumes"
216 | case removableVolumes = "SystemPolicyRemovableVolumes"
217 | case appleEvents = "AppleEvents"
218 | }
219 |
--------------------------------------------------------------------------------
/Source/Networking/JamfProAPIClient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JamfProAPIClient.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2023 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 |
30 | class JamfProAPIClient: Networking {
31 | let applicationJson = "application/json"
32 |
33 | override func getBearerToken(authInfo: AuthenticationInfo) async throws -> Token {
34 | switch authInfo {
35 | case .basicAuth:
36 | let endpoint = "api/v1/auth/token"
37 | var request = try url(forEndpoint: endpoint)
38 |
39 | request.httpMethod = "POST"
40 | request.setValue(applicationJson, forHTTPHeaderField: "Accept")
41 |
42 | return try await loadBasicAuthorized(request: request)
43 | case .clientCreds(let id, let secret):
44 | let request = try oauthTokenRequest(clientId: id, clientSecret: secret)
45 |
46 | return try await loadPreAuthorized(request: request)
47 | }
48 | }
49 |
50 | /// Creates the OAuth client credentials token request
51 | /// - Parameters:
52 | /// - clientId: The client ID
53 | /// - clientSecret: The client secret
54 | /// - Returns: A `URLRequest` that is ready to send to acquire an OAuth token.
55 | func oauthTokenRequest(clientId: String, clientSecret: String) throws -> URLRequest {
56 | let endpoint = "api/oauth/token"
57 | var request = try url(forEndpoint: endpoint)
58 |
59 | request.httpMethod = "POST"
60 | request.setValue(applicationJson, forHTTPHeaderField: "Accept")
61 |
62 | var components = URLComponents()
63 | components.queryItems = [URLQueryItem(name: "grant_type", value: "client_credentials"),
64 | URLQueryItem(name: "client_id", value: clientId),
65 | URLQueryItem(name: "client_secret", value: clientSecret)]
66 |
67 | request.httpBody = components.percentEncodedQuery?.data(using: .utf8)
68 |
69 | return request
70 | }
71 |
72 | // MARK: - Requests with fallback auth
73 |
74 | /// Make a network request and decode the response using bearer auth if possible, falling back to basic auth if needed.
75 | /// - Parameter request: The `URLRequest` to make
76 | /// - Returns: The decoded response.
77 | func load(request: URLRequest) async throws -> T {
78 | let result: T
79 |
80 | if await authManager.bearerAuthSupported() {
81 | do {
82 | result = try await loadBearerAuthorized(request: request)
83 | } catch AuthError.bearerAuthNotSupported {
84 | // Note that `authManager` will automatically return false from future calls to `bearerAuthSupported()`
85 | // Possibly we're talking to Jamf Pro 10.34.x or lower and we can retry with Basic Auth
86 | result = try await loadBasicAuthorized(request: request)
87 | }
88 | } else {
89 | result = try await loadBasicAuthorized(request: request)
90 | }
91 |
92 | return result
93 | }
94 |
95 | /// Send a network request and return the response using bearer auth if possible, falling back to basic auth if needed.
96 | /// - Parameter request: The `URLRequest` to make
97 | /// - Returns: The raw data of a successful network response.
98 | func send(request: URLRequest) async throws -> Data {
99 | let result: Data
100 |
101 | if await authManager.bearerAuthSupported() {
102 | do {
103 | result = try await sendBearerAuthorized(request: request)
104 | } catch AuthError.bearerAuthNotSupported {
105 | // Note that authManager will automatically return false from future calls to ``bearerAuthSupported()``
106 | // Possibly we're talking to Jamf Pro 10.34.x or lower and we can retry with Basic Auth
107 | result = try await sendBasicAuthorized(request: request)
108 | }
109 | } else {
110 | result = try await sendBasicAuthorized(request: request)
111 | }
112 |
113 | return result
114 | }
115 |
116 | // MARK: - Useful API endpoints
117 |
118 | /// Reads the Jamf Pro organization name
119 | ///
120 | /// Requires "Read Activation Code" permission in the API
121 | /// - Parameter profileData: The prepared profile data
122 | func getOrganizationName() async throws -> String {
123 | let endpoint = "JSSResource/activationcode"
124 | var request = try url(forEndpoint: endpoint)
125 |
126 | request.httpMethod = "GET"
127 | request.setValue(applicationJson, forHTTPHeaderField: "Accept")
128 |
129 | let info: ActivationCode
130 | info = try await load(request: request)
131 |
132 | return info.activationCode.organizationName
133 | }
134 |
135 | /// Gets the Jamf Pro version
136 | ///
137 | /// No specific permissions required.
138 | /// - Returns: The Jamf Pro server version
139 | func getJamfProVersion() async throws -> JamfProVersion {
140 | let endpoint = "api/v1/jamf-pro-version"
141 | var request = try url(forEndpoint: endpoint)
142 |
143 | request.httpMethod = "GET"
144 | request.setValue(applicationJson, forHTTPHeaderField: "Accept")
145 |
146 | let info: JamfProVersion
147 | do {
148 | info = try await load(request: request)
149 | } catch is NetworkingError {
150 | // Possibly we are talking to Jamf Pro v10.22 or lower and we can grab the version from a meta tag on the login page.
151 | let simpleRequest = try url(forEndpoint: "")
152 | let (data, _) = try await URLSession.shared.data(for: simpleRequest)
153 | info = try JamfProVersion(fromHTMLString: String(data: data, encoding: .utf8))
154 | }
155 |
156 | return info
157 | }
158 |
159 | /// Uploads a computer configuration profile
160 | ///
161 | /// Requires "Create macOS Configuration Profiles" permission in the API
162 | /// - Parameter profileData: The prepared profile data
163 | func upload(computerConfigProfile profileData: Data) async throws {
164 | let endpoint = "JSSResource/osxconfigurationprofiles"
165 | var request = try url(forEndpoint: endpoint)
166 |
167 | request.httpMethod = "POST"
168 | request.httpBody = profileData
169 | request.setValue("text/xml", forHTTPHeaderField: "Content-Type")
170 | request.setValue("text/xml", forHTTPHeaderField: "Accept")
171 |
172 | _ = try await send(request: request)
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/Source/Networking/JamfProAPITypes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JamfProAPITypes.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2022 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 |
30 | struct JamfProVersion: Decodable {
31 | let version: String
32 |
33 | func mainVersionInfo() -> String {
34 | return String(version.prefix { character in
35 | return (character.isNumber || character == ".")
36 | })
37 | }
38 |
39 | func semantic() -> SemanticVersion {
40 | let components = mainVersionInfo().split(separator: ".")
41 |
42 | return SemanticVersion(major: Int(components[0]) ?? 0, minor: Int(components[1]) ?? 0, patch: Int(components[2]) ?? 0)
43 | }
44 |
45 | /// Versions of Jamf Pro less than 10.23 included the version number in a meta tag on the login page.
46 | /// - Parameter text: The HTML contents of the login page.
47 | init(fromHTMLString text: String?) throws {
48 | // we take version from HTML response body
49 | if let text = text,
50 | let startRange = text.range(of: "?
55 |
56 | private var supportsBearerAuth = true
57 |
58 | init(username: String, password: String) {
59 | authInfo = .basicAuth(username: username, password: password)
60 | }
61 |
62 | init(clientId: String, clientSecret: String) {
63 | authInfo = .clientCreds(id: clientId, secret: clientSecret)
64 | }
65 |
66 | func validToken(networking: Networking) async throws -> Token {
67 | if let task = refreshTask {
68 | // A refresh is already running; we'll use those results when ready.
69 | return try await task.value
70 | }
71 |
72 | if let token = currentToken,
73 | token.isValid {
74 | return token
75 | }
76 |
77 | return try await refreshToken(networking: networking)
78 | }
79 |
80 | func refreshToken(networking: Networking) async throws -> Token {
81 | if let task = refreshTask {
82 | // A refresh is already running; we'll use those results when ready.
83 | return try await task.value
84 | }
85 |
86 | // Initiate a refresh.
87 | let task = Task { () throws -> Token in
88 | defer { refreshTask = nil }
89 |
90 | do {
91 | let newToken = try await networking.getBearerToken(authInfo: authInfo)
92 | currentToken = newToken
93 | return newToken
94 | } catch NetworkingError.serverResponse(let responseCode, _) where responseCode == 404 {
95 | // If we got a 404 while trying to get a bearer token the server doesn't support bearer tokens.
96 | supportsBearerAuth = false
97 | throw AuthError.bearerAuthNotSupported
98 | } catch NetworkingError.serverResponse(let responseCode, _) where responseCode == 401 {
99 | // If we got a 401 while trying to get a bearer token the username/password was bad.
100 | throw AuthError.invalidUsernamePassword
101 | }
102 | }
103 |
104 | refreshTask = task
105 |
106 | return try await task.value
107 | }
108 |
109 | /// If bearer authentication is not actually supported, after the first network call trying to use bearer auth this will return false.
110 | ///
111 | /// The default is that bearer authentication is supported. After the first network call attempting to use bearer auth, if the
112 | /// server does not actually support it this will return false.
113 | /// - Returns: True if bearer auth is supported.
114 | func bearerAuthSupported() async -> Bool {
115 | return supportsBearerAuth
116 | }
117 |
118 | /// Properly encodes the username and password for use in Basic authentication.
119 | ///
120 | /// This doesn't mutate any state and only accesses `let` constants so it doesn't need to be actor isolated.
121 | /// - Returns: The encoded data string for use with Basic Auth.
122 | nonisolated func basicAuthString() throws -> String {
123 | guard case .basicAuth(let username, let password) = authInfo,
124 | !username.isEmpty && !password.isEmpty else {
125 | throw AuthError.invalidUsernamePassword
126 | }
127 | return Data("\(username):\(password)".utf8).base64EncodedString()
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/Source/Networking/Networking.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Networking.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2023 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 |
30 | /// Problems at the networking error throw this type of error.
31 | enum NetworkingError: Error, Equatable {
32 | /// The server URL cannot be converted into a standard URL.
33 | ///
34 | /// The associated value is the given server URL.
35 | case badServerUrl(String)
36 |
37 | /// If the server returns anything outside of 200...299 this is the error thrown.
38 | ///
39 | /// The first associated value is the HTTP response code. The second associated value is the full URL that was attempted.
40 | case serverResponse(Int, String)
41 |
42 | /// This is thrown if a subclass does not implement ``getBearerToken(authInfo:)`` and then attempts to use bearer tokens.
43 | case unimplemented
44 | }
45 |
46 | class Networking {
47 | let authManager: NetworkAuthManager
48 | let serverUrlString: String
49 |
50 | init(serverUrlString: String, tokenManager: NetworkAuthManager) {
51 | self.serverUrlString = serverUrlString
52 | self.authManager = tokenManager
53 | }
54 |
55 | /// Subclasses must override this to do a network call to return a bearer token.
56 | /// - Returns: A token
57 | func getBearerToken(authInfo: AuthenticationInfo) async throws -> Token {
58 | throw NetworkingError.unimplemented
59 | }
60 |
61 | /// Create a `URLRequest` for the endpoint based on the `serverUrlString`
62 | /// - Parameter endpoint: The endpoint URL path
63 | /// - Returns: A `URLRequest` representing the endpoint on the server
64 | /// - Throws: May throw a `NetworkingError.badServerUrl` error
65 | func url(forEndpoint endpoint: String) throws -> URLRequest {
66 | guard let serverURL = URL(string: serverUrlString) else {
67 | throw NetworkingError.badServerUrl(serverUrlString)
68 | }
69 | let url = serverURL.appendingPathComponent(endpoint)
70 |
71 | return URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 45.0)
72 | }
73 |
74 | /// Sends a `URLRequest` and decodes a response to an endpoint.
75 | /// - Parameter request: A request that already has authorization info.
76 | /// - Returns: The result.
77 | func loadPreAuthorized(request: URLRequest) async throws -> T {
78 | let (data, urlResponse) = try await URLSession.shared.data(for: request)
79 |
80 | if let httpResponse = urlResponse as? HTTPURLResponse {
81 | if httpResponse.statusCode == 401 {
82 | throw AuthError.invalidUsernamePassword
83 | } else if !(200...299).contains(httpResponse.statusCode) {
84 | throw NetworkingError.serverResponse(httpResponse.statusCode, request.url?.absoluteString ?? "")
85 | }
86 | }
87 |
88 | let decoder = JSONDecoder()
89 | let response = try decoder.decode(T.self, from: data)
90 |
91 | return response
92 | }
93 |
94 | /// Sends a `URLRequest` and decodes a response to a Basic Auth protected endpoint.
95 | /// - Parameter request: A request that does not yet include authorization info.
96 | /// - Returns: The result.
97 | func loadBasicAuthorized(request: URLRequest) async throws -> T {
98 | let authorizedRequest = try await authorizeBasic(request: request)
99 | let (data, urlResponse) = try await URLSession.shared.data(for: authorizedRequest)
100 |
101 | if let httpResponse = urlResponse as? HTTPURLResponse {
102 | if httpResponse.statusCode == 401 {
103 | throw AuthError.invalidUsernamePassword
104 | } else if !(200...299).contains(httpResponse.statusCode) {
105 | throw NetworkingError.serverResponse(httpResponse.statusCode, request.url?.absoluteString ?? "")
106 | }
107 | }
108 |
109 | let decoder = JSONDecoder()
110 | let response = try decoder.decode(T.self, from: data)
111 |
112 | return response
113 | }
114 |
115 | /// Sends a `URLRequest` and decodes a response to a Bearer Auth protected endpoint.
116 | /// - Parameter request: A request that does not yet include authorization info.
117 | /// - Returns: The result.
118 | func loadBearerAuthorized(request: URLRequest, allowRetryForAuth: Bool = true) async throws -> T {
119 | let authorizedRequest = try await authorizeBearer(request: request)
120 | let (data, urlResponse) = try await URLSession.shared.data(for: authorizedRequest)
121 |
122 | if let httpResponse = urlResponse as? HTTPURLResponse {
123 | if httpResponse.statusCode == 401 {
124 | if allowRetryForAuth {
125 | _ = try await authManager.refreshToken(networking: self)
126 | return try await loadBearerAuthorized(request: request, allowRetryForAuth: false)
127 | }
128 |
129 | throw AuthError.invalidToken
130 | } else if !(200...299).contains(httpResponse.statusCode) {
131 | throw NetworkingError.serverResponse(httpResponse.statusCode, request.url?.absoluteString ?? "")
132 | }
133 | }
134 |
135 | let decoder = JSONDecoder()
136 | let response = try decoder.decode(T.self, from: data)
137 |
138 | return response
139 | }
140 |
141 | /// Sends a `URLRequest` to a Bearer Auth protected endpoint and returns any response.
142 | /// - Parameter request: A request that does not yet include authorization info.
143 | /// - Returns: The result.
144 | func sendBearerAuthorized(request: URLRequest, allowRetryForAuth: Bool = true) async throws -> Data {
145 | let authorizedRequest = try await authorizeBearer(request: request)
146 | let (data, urlResponse) = try await URLSession.shared.data(for: authorizedRequest)
147 |
148 | if let httpResponse = urlResponse as? HTTPURLResponse {
149 | if httpResponse.statusCode == 401 {
150 | if allowRetryForAuth {
151 | _ = try await authManager.refreshToken(networking: self)
152 | return try await loadBearerAuthorized(request: request, allowRetryForAuth: false)
153 | }
154 |
155 | throw AuthError.invalidToken
156 | } else if !(200...299).contains(httpResponse.statusCode) {
157 | throw NetworkingError.serverResponse(httpResponse.statusCode, request.url?.absoluteString ?? "")
158 | }
159 | }
160 |
161 | return data
162 | }
163 |
164 | /// Sends a `URLRequest` to a Basic Auth protected endpoint and returns any response.
165 | /// - Parameter request: A request that does not yet include authorization info.
166 | /// - Returns: The result.
167 | func sendBasicAuthorized(request: URLRequest) async throws -> Data {
168 | let authorizedRequest = try await authorizeBasic(request: request)
169 | let (data, urlResponse) = try await URLSession.shared.data(for: authorizedRequest)
170 |
171 | if let httpResponse = urlResponse as? HTTPURLResponse {
172 | if httpResponse.statusCode == 401 {
173 | throw AuthError.invalidUsernamePassword
174 | } else if !(200...299).contains(httpResponse.statusCode) {
175 | throw NetworkingError.serverResponse(httpResponse.statusCode, request.url?.absoluteString ?? "")
176 | }
177 | }
178 |
179 | return data
180 | }
181 |
182 | /// Given a `URLRequest`, adds a bearer token from the ``authManager``
183 | /// - Parameter request: A URLRequest
184 | /// - Returns: The request with the bearer token added
185 | private func authorizeBearer(request: URLRequest) async throws -> URLRequest {
186 | let token = try await authManager.validToken(networking: self)
187 | var newRequest = request
188 | newRequest.setValue("Bearer \(token.value)", forHTTPHeaderField: "Authorization")
189 | return newRequest
190 | }
191 |
192 | /// Given a `URLRequest`, adds a basic authorization header with info from the ``authManager``
193 | /// - Parameter request: A URLRequest
194 | /// - Returns: The request with the bearer token added
195 | private func authorizeBasic(request: URLRequest) async throws -> URLRequest {
196 | let basicValue = try authManager.basicAuthString()
197 | var newRequest = request
198 | newRequest.setValue("Basic \(basicValue)", forHTTPHeaderField: "Authorization")
199 | return newRequest
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/Source/Networking/Token.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Token.swift
3 | // PPPC Utility
4 | //
5 | // SPDX-License-Identifier: MIT
6 | // Copyright (c) 2023 Jamf Software
7 |
8 | import Foundation
9 |
10 | /// Network authentication token for Jamf Pro connection.
11 | ///
12 | /// Decodes network response for authentication tokens from Jamf Pro for both the newer OAuth client credentials flow
13 | /// and the older basic-auth-based flow.
14 | struct Token: Decodable {
15 | let value: String
16 | let expiresAt: Date?
17 |
18 | var isValid: Bool {
19 | if let expiration = expiresAt {
20 | return expiration > Date()
21 | }
22 |
23 | return true
24 | }
25 |
26 | enum OAuthTokenCodingKeys: String, CodingKey {
27 | case value = "access_token"
28 | case expire = "expires_in"
29 | }
30 |
31 | enum BasicAuthCodingKeys: String, CodingKey {
32 | case value = "token"
33 | case expireTime = "expires"
34 | }
35 |
36 | init(from decoder: Decoder) throws {
37 | // First try to decode with oauth client credentials token response
38 | let container = try decoder.container(keyedBy: OAuthTokenCodingKeys.self)
39 | let possibleValue = try? container.decode(String.self, forKey: .value)
40 | if let value = possibleValue {
41 | self.value = value
42 | let expireIn = try container.decode(Double.self, forKey: .expire)
43 | self.expiresAt = Date().addingTimeInterval(expireIn)
44 | return
45 | }
46 |
47 | // If that fails try to decode with basic auth token response
48 | let container1 = try decoder.container(keyedBy: BasicAuthCodingKeys.self)
49 | self.value = try container1.decode(String.self, forKey: .value)
50 | let expireTime = try container1.decode(String.self, forKey: .expireTime)
51 |
52 | let formatter = ISO8601DateFormatter()
53 | formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
54 | self.expiresAt = formatter.date(from: expireTime)
55 | }
56 |
57 | init(value: String, expiresAt: Date) {
58 | self.value = value
59 | self.expiresAt = expiresAt
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Source/Networking/URLSessionAsyncCompatibility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLSessionAsyncCompatibility.swift
3 | // Based on AsyncCompatibilityKit's URLSession+Async.swift which is
4 | // Copyright (c) John Sundell 2021
5 | // MIT license, see LICENSE.md file for details
6 | //
7 | // Change from AsyncCompatibilityKit: Modified the `@available` line to be deprecated in macOS 12 instead of iOS 15.
8 | //
9 |
10 | import Foundation
11 |
12 | @available(macOS, deprecated: 12.0, message: "AsyncCompatibilityKit is only useful when targeting macOS versions earlier than 12")
13 | public extension URLSession {
14 | /// Start a data task with a URL using async/await.
15 | /// - parameter url: The URL to send a request to.
16 | /// - returns: A tuple containing the binary `Data` that was downloaded,
17 | /// as well as a `URLResponse` representing the server's response.
18 | /// - throws: Any error encountered while performing the data task.
19 | func data(from url: URL) async throws -> (Data, URLResponse) {
20 | try await data(for: URLRequest(url: url))
21 | }
22 |
23 | /// Start a data task with a `URLRequest` using async/await.
24 | /// - parameter request: The `URLRequest` that the data task should perform.
25 | /// - returns: A tuple containing the binary `Data` that was downloaded,
26 | /// as well as a `URLResponse` representing the server's response.
27 | /// - throws: Any error encountered while performing the data task.
28 | func data(for request: URLRequest) async throws -> (Data, URLResponse) {
29 | var dataTask: URLSessionDataTask?
30 | let onCancel = { dataTask?.cancel() }
31 |
32 | return try await withTaskCancellationHandler(
33 | operation: {
34 | try await withCheckedThrowingContinuation { continuation in
35 | dataTask = self.dataTask(with: request) { data, response, error in
36 | guard let data = data, let response = response else {
37 | let error = error ?? URLError(.badServerResponse)
38 | return continuation.resume(throwing: error)
39 | }
40 |
41 | continuation.resume(returning: (data, response))
42 | }
43 |
44 | dataTask?.resume()
45 | }
46 | },
47 | onCancel: {
48 | onCancel()
49 | }
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Source/Networking/UploadManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadManager.swift
3 | // PPPC Utility
4 | //
5 | // Created by Kyle Hammond on 11/3/23.
6 | // Copyright © 2023 Jamf. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import OSLog
11 |
12 | struct UploadManager {
13 | let serverURL: String
14 |
15 | let logger = Logger.UploadManager
16 |
17 | struct VerificationInfo {
18 | let mustSign: Bool
19 | let organization: String
20 | }
21 |
22 | enum VerificationError: Error {
23 | case anyError(String)
24 | }
25 |
26 | func verifyConnection(authManager: NetworkAuthManager, completionHandler: @escaping (Result) -> Void) {
27 | logger.info("Checking connection to Jamf Pro server")
28 |
29 | Task {
30 | let networking = JamfProAPIClient(serverUrlString: serverURL, tokenManager: authManager)
31 | let result: Result
32 |
33 | do {
34 | let version = try await networking.getJamfProVersion()
35 |
36 | // Must sign if Jamf Pro is less than v10.7.1
37 | let mustSign = (version.semantic() < SemanticVersion(major: 10, minor: 7, patch: 1))
38 |
39 | let orgName = try await networking.getOrganizationName()
40 |
41 | result = .success(VerificationInfo(mustSign: mustSign, organization: orgName))
42 | } catch is AuthError {
43 | logger.error("Invalid credentials.")
44 | result = .failure(VerificationError.anyError("Invalid credentials."))
45 | } catch {
46 | logger.error("Jamf Pro server is unavailable.")
47 | result = .failure(VerificationError.anyError("Jamf Pro server is unavailable."))
48 | }
49 |
50 | completionHandler(result)
51 | }
52 | }
53 |
54 | func upload(profile: TCCProfile, authMgr: NetworkAuthManager, siteInfo: (String, String)?, signingIdentity: SigningIdentity?, completionHandler: @escaping (Error?) -> Void) {
55 | logger.info("Uploading profile: \(profile.displayName, privacy: .public)")
56 |
57 | let networking = JamfProAPIClient(serverUrlString: serverURL, tokenManager: authMgr)
58 | Task {
59 | let success: Error?
60 | var identity: SecIdentity?
61 | if let signingIdentity = signingIdentity {
62 | logger.info("Signing profile with \(signingIdentity.displayName)")
63 | identity = signingIdentity.reference
64 | }
65 |
66 | do {
67 | let profileData = try profile.jamfProAPIData(signingIdentity: identity, site: siteInfo)
68 |
69 | _ = try await networking.upload(computerConfigProfile: profileData)
70 |
71 | success = nil
72 | logger.info("Uploaded successfully")
73 | } catch {
74 | logger.error("Error creating or uploading profile: \(error.localizedDescription)")
75 | success = error
76 | }
77 |
78 | DispatchQueue.main.async {
79 | completionHandler(success)
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Source/SecurityWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SecurityWrapper.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2023 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 | import Haversack
30 |
31 | struct SecurityWrapper {
32 |
33 | static func execute(block: () -> (OSStatus)) throws {
34 | let status = block()
35 | if status != 0 {
36 | throw NSError(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: nil)
37 | }
38 | }
39 |
40 | static func saveCredentials(username: String, password: String, server: String) throws {
41 | let haversack = Haversack()
42 | let item = InternetPasswordEntity()
43 | item.server = server
44 | item.account = username
45 | item.passwordData = password.data(using: .utf8)
46 |
47 | try haversack.save(item, itemSecurity: .standard, updateExisting: true)
48 | }
49 |
50 | static func removeCredentials(server: String, username: String) throws {
51 | let haversack = Haversack()
52 | let query = InternetPasswordQuery(server: server)
53 | .matching(account: username)
54 |
55 | try haversack.delete(where: query, treatNotFoundAsSuccess: true)
56 | }
57 |
58 | static func loadCredentials(server: String) throws -> (username: String, password: String)? {
59 | let haversack = Haversack()
60 | let query = InternetPasswordQuery(server: server)
61 | .returning([.attributes, .data])
62 |
63 | if let item = try? haversack.first(where: query),
64 | let username = item.account,
65 | let passwordData = item.passwordData,
66 | let password = String(data: passwordData, encoding: .utf8) {
67 | return (username: username, password: password)
68 | }
69 |
70 | return nil
71 | }
72 |
73 | static func copyDesignatedRequirement(url: URL) throws -> String {
74 | let flags = SecCSFlags(rawValue: 0)
75 | var staticCode: SecStaticCode?
76 | var requirement: SecRequirement?
77 | var text: CFString?
78 |
79 | try execute { SecStaticCodeCreateWithPath(url as CFURL, flags, &staticCode) }
80 | try execute { SecCodeCopyDesignatedRequirement(staticCode!, flags, &requirement) }
81 | try execute { SecRequirementCopyString(requirement!, flags, &text) }
82 |
83 | return text! as String
84 | }
85 |
86 | static func sign(data: Data, using identity: SecIdentity) throws -> Data {
87 |
88 | var outputData: CFData?
89 | var encoder: CMSEncoder?
90 | try execute { CMSEncoderCreate(&encoder) }
91 | try execute { CMSEncoderAddSigners(encoder!, identity) }
92 | try execute { CMSEncoderAddSignedAttributes(encoder!, .attrSmimeCapabilities) }
93 | try execute { CMSEncoderUpdateContent(encoder!, (data as NSData).bytes, data.count) }
94 | try execute { CMSEncoderCopyEncodedContent(encoder!, &outputData) }
95 |
96 | return outputData! as Data
97 | }
98 |
99 | static func loadSigningIdentities() throws -> [SigningIdentity] {
100 | let haversack = Haversack()
101 | let query = IdentityQuery().matching(mustBeValidOnDate: Date()).returning(.reference)
102 |
103 | let identities = try haversack.search(where: query)
104 |
105 | return identities.compactMap {
106 | guard let secIdentity = $0.reference else {
107 | return nil
108 | }
109 |
110 | let name = try? getCertificateCommonName(for: secIdentity)
111 | return SigningIdentity(name: name ?? "Unknown \(secIdentity.hashValue)",
112 | reference: secIdentity)
113 | }
114 | }
115 |
116 | static func getCertificateCommonName(for identity: SecIdentity) throws -> String {
117 | var certificate: SecCertificate?
118 | var commonName: CFString?
119 | try execute { SecIdentityCopyCertificate(identity, &certificate) }
120 | try execute { SecCertificateCopyCommonName(certificate!, &commonName) }
121 | return commonName! as String
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Source/SwiftUI/UploadInfoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UploadInfoView.swift
3 | // PPPC Utility
4 | //
5 | // SPDX-License-Identifier: MIT
6 | // Copyright (c) 2023 Jamf Software
7 |
8 | import OSLog
9 | import SwiftUI
10 |
11 | struct UploadInfoView: View {
12 | /// The signing identities available to be used.
13 | let signingIdentities: [SigningIdentity]
14 | /// Function to call when this view needs to be removed
15 | let dismissAction: (() -> Void)?
16 |
17 | // Communicate this info to the user
18 | @State private var warningInfo: String?
19 | @State private var networkOperationInfo: String?
20 | /// Must sign the profile if Jamf Pro is less than v10.7.1
21 | @State private var mustSign = false
22 | /// The hash of connection info that has been verified with a succesful connection
23 | @State private var verifiedConnectionHash: Int = 0
24 |
25 | // MARK: User entry fields
26 | @AppStorage("jamfProServer") private var serverURL = "https://"
27 | @AppStorage("organization") private var organization = ""
28 | @AppStorage("authType") private var authType = AuthenticationType.clientCredentials
29 |
30 | @State private var username = ""
31 | @State private var password = ""
32 | @State private var saveToKeychain: Bool = true
33 | @State private var payloadName = ""
34 | @State private var payloadId = UUID().uuidString
35 | @State private var payloadDescription = ""
36 | @State private var signingId: SigningIdentity?
37 | @State private var useSite: Bool = false
38 | @State private var siteId: Int = -1
39 | @State private var siteName: String = ""
40 |
41 | let logger = Logger.UploadInfoView
42 |
43 | /// The type of authentication the user wants to use.
44 | ///
45 | /// `String` type so it can be saved with `@AppStorage` above
46 | enum AuthenticationType: String {
47 | case basicAuth
48 | case clientCredentials
49 | }
50 |
51 | let intFormatter: NumberFormatter = {
52 | let formatter = NumberFormatter()
53 | formatter.numberStyle = .none
54 | return formatter
55 | }()
56 |
57 | var body: some View {
58 | VStack {
59 | Form {
60 | TextField("Jamf Pro Server *:", text: $serverURL)
61 | Picker("Authorization Type:", selection: $authType) {
62 | Text("Basic/Bearer Auth").tag(AuthenticationType.basicAuth)
63 | Text("Client Credentials (v10.49+):").tag(AuthenticationType.clientCredentials)
64 | }
65 | TextField(authType == .basicAuth ? "Username *:" : "Client ID *:", text: $username)
66 | SecureField(authType == .basicAuth ? "Password *:" : "Client Secret *:", text: $password)
67 |
68 | HStack {
69 | Toggle("Save in Keychain", isOn: $saveToKeychain)
70 | .help("Store the username & password or client id & secret in the login keychain")
71 | if verifiedConnection {
72 | Spacer()
73 | Text("✔️ Verified")
74 | .font(.footnote)
75 | }
76 | }
77 | Divider()
78 | .padding(.vertical)
79 |
80 | TextField("Organization *:", text: $organization)
81 | TextField("Payload Name *:", text: $payloadName)
82 | TextField("Payload Identifier *:", text: $payloadId)
83 | TextField("Payload Description:", text: $payloadDescription)
84 | Picker("Signing Identity:", selection: $signingId) {
85 | Text("Profile signed by server").tag(nil as SigningIdentity?)
86 | ForEach(signingIdentities, id: \.self) { identity in
87 | Text(identity.displayName).tag(identity)
88 | }
89 | }
90 | .disabled(!mustSign)
91 | Toggle("Use Site", isOn: $useSite)
92 | TextField("Site ID", value: $siteId, formatter: intFormatter)
93 | .disabled(!useSite)
94 | TextField("Site Name", text: $siteName)
95 | .disabled(!useSite)
96 | }
97 | .padding(.bottom)
98 |
99 | if let warning = warningInfo {
100 | Text(warning)
101 | .font(.headline)
102 | .foregroundColor(.red)
103 | }
104 | if let networkInfo = networkOperationInfo {
105 | HStack {
106 | Text(networkInfo)
107 | .font(.headline)
108 | ProgressView()
109 | .padding(.leading)
110 | }
111 | }
112 |
113 | HStack {
114 | Spacer()
115 |
116 | Button("Cancel") {
117 | dismissView()
118 | }
119 | .keyboardShortcut(.cancelAction)
120 |
121 | Button(verifiedConnection ? "Upload" : "Check connection") {
122 | if verifiedConnection {
123 | performUpload()
124 | } else {
125 | verifyConnection()
126 | }
127 | }
128 | .keyboardShortcut(.defaultAction)
129 | .disabled(!buttonEnabled())
130 | }
131 | }
132 | .padding()
133 | .frame(minWidth: 450)
134 | .background(Color(.windowBackgroundColor))
135 | .onAppear {
136 | // Load keychain values
137 | if let creds = try? SecurityWrapper.loadCredentials(server: serverURL) {
138 | username = creds.username
139 | password = creds.password
140 | }
141 |
142 | // Use model payload values if it was imported
143 | if let tccProfile = Model.shared.importedTCCProfile {
144 | organization = tccProfile.organization
145 | payloadName = tccProfile.displayName
146 | payloadDescription = tccProfile.payloadDescription
147 | payloadId = tccProfile.identifier
148 | }
149 | }
150 | }
151 |
152 | /// Creates a hash of the currently entered connection info
153 | var hashOfConnectionInfo: Int {
154 | var hasher = Hasher()
155 | hasher.combine(serverURL)
156 | hasher.combine(username)
157 | hasher.combine(password)
158 | hasher.combine(authType)
159 | return hasher.finalize()
160 | }
161 |
162 | /// Compare the last verified connection hash with the current hash of connection info
163 | var verifiedConnection: Bool {
164 | verifiedConnectionHash == hashOfConnectionInfo
165 | }
166 |
167 | func buttonEnabled() -> Bool {
168 | if verifiedConnection {
169 | return payloadInfoPassesValidation()
170 | }
171 | return connectionInfoPassesValidation()
172 | }
173 |
174 | private func warning(_ info: StaticString, shouldDisplay: Bool) {
175 | if shouldDisplay {
176 | logger.info("\(info)")
177 | warningInfo = "\(info)"
178 | }
179 | }
180 |
181 | /// Does some simple validation of the user-entered connection info
182 | ///
183 | /// The `setWarningInfo` parameter is optional, and should only be set to `true` during
184 | /// actions triggered by the user. This function can be called with `false` (or no parameters)
185 | /// from SwiftUI's `body` function to enable/disable controls.
186 | /// - Parameter setWarningInfo: Whether to set the warning text so the user knows something needs to be updated. Default is `false`.
187 | /// - Returns: True if the user entered connection info passes simple local validation
188 | func connectionInfoPassesValidation(setWarningInfo: Bool = false) -> Bool {
189 | guard !serverURL.isEmpty else {
190 | warning("Server URL not set", shouldDisplay: setWarningInfo)
191 | // Future on macOS 12+: focus on serverURL field
192 | return false
193 | }
194 |
195 | guard let url = URL(string: serverURL),
196 | url.scheme == "http" || url.scheme == "https" else {
197 | warning("Invalid Jamf Pro Server URL", shouldDisplay: setWarningInfo)
198 | // Future on macOS 12+: focus on serverURL field
199 | return false
200 | }
201 |
202 | if authType == .basicAuth {
203 | guard !username.isEmpty, !password.isEmpty else {
204 | warning("Username or password not set", shouldDisplay: setWarningInfo)
205 | // Future on macOS 12+: focus on username or password field
206 | return false
207 | }
208 |
209 | guard username.firstIndex(of: ":") == nil else {
210 | warning("Username cannot contain a colon", shouldDisplay: setWarningInfo)
211 | // Future on macOS 12+: focus on username field
212 | return false
213 | }
214 | } else {
215 | guard !username.isEmpty, !password.isEmpty else {
216 | warning("Client ID or secret not set", shouldDisplay: setWarningInfo)
217 | // Future on macOS 12+: focus on username or password field
218 | return false
219 | }
220 | }
221 |
222 | if setWarningInfo {
223 | warningInfo = nil
224 | }
225 | return true
226 | }
227 |
228 | /// Does some simple validation of the user-entered payload info
229 | ///
230 | /// The `setWarningInfo` parameter is optional, and should only be set to `true` during
231 | /// actions triggered by the user. This function can be called with `false` (or no parameters)
232 | /// from SwiftUI's `body` function to enable/disable controls.
233 | /// - Parameter setWarningInfo: Whether to set the warning text so the user knows something needs to be updated. Default is `false`.
234 | /// - Returns: True if the user entered payload info passes simple local validation
235 | func payloadInfoPassesValidation(setWarningInfo: Bool = false) -> Bool {
236 | guard !organization.isEmpty else {
237 | warning("Must provide an organization name", shouldDisplay: setWarningInfo)
238 | // Future on macOS 12+: focus on organization field
239 | return false
240 | }
241 |
242 | guard !payloadId.isEmpty else {
243 | warning("Must provide a payload identifier", shouldDisplay: setWarningInfo)
244 | // Future on macOS 12+: focus on payload ID field
245 | return false
246 | }
247 |
248 | guard !payloadName.isEmpty else {
249 | warning("Must provide a payload name", shouldDisplay: setWarningInfo)
250 | // Future on macOS 12+: focus on payloadName field
251 | return false
252 | }
253 |
254 | guard useSite == false || (useSite == true && siteId != -1 && !siteName.isEmpty) else {
255 | warning("Must provide both an ID and name for the site", shouldDisplay: setWarningInfo)
256 | // Future on macOS 12+: focus on siteId or siteName field
257 | return false
258 | }
259 |
260 | if setWarningInfo {
261 | warningInfo = nil
262 | }
263 | return true
264 | }
265 |
266 | func makeAuthManager() -> NetworkAuthManager {
267 | if authType == .basicAuth {
268 | return NetworkAuthManager(username: username, password: password)
269 | }
270 |
271 | return NetworkAuthManager(clientId: username, clientSecret: password)
272 | }
273 |
274 | func verifyConnection() {
275 | guard connectionInfoPassesValidation(setWarningInfo: true) else {
276 | return
277 | }
278 |
279 | networkOperationInfo = "Checking Jamf Pro server"
280 |
281 | let uploadMgr = UploadManager(serverURL: serverURL)
282 | uploadMgr.verifyConnection(authManager: makeAuthManager()) { result in
283 | if case .success(let success) = result {
284 | mustSign = success.mustSign
285 | organization = success.organization
286 | verifiedConnectionHash = hashOfConnectionInfo
287 | if saveToKeychain {
288 | do {
289 | try SecurityWrapper.saveCredentials(username: username,
290 | password: password,
291 | server: serverURL)
292 | } catch {
293 | logger.error("Failed to save credentials with error: \(error.localizedDescription)")
294 | }
295 | }
296 | // Future on macOS 12+: focus on Payload Name field
297 | } else if case .failure(let failure) = result,
298 | case .anyError(let errorString) = failure {
299 | warningInfo = errorString
300 | verifiedConnectionHash = 0
301 | }
302 |
303 | networkOperationInfo = nil
304 | }
305 | }
306 |
307 | private func dismissView() {
308 | if !saveToKeychain {
309 | try? SecurityWrapper.removeCredentials(server: serverURL, username: username)
310 | }
311 |
312 | if let dismiss = dismissAction {
313 | dismiss()
314 | }
315 | }
316 |
317 | func performUpload() {
318 | guard connectionInfoPassesValidation(setWarningInfo: true) else {
319 | return
320 | }
321 |
322 | guard payloadInfoPassesValidation(setWarningInfo: true) else {
323 | return
324 | }
325 |
326 | let profile = Model.shared.exportProfile(organization: organization,
327 | identifier: payloadId,
328 | displayName: payloadName,
329 | payloadDescription: payloadDescription)
330 |
331 | networkOperationInfo = "Uploading '\(profile.displayName)'..."
332 |
333 | var siteIdAndName: (String, String)?
334 | if useSite {
335 | if siteId != -1 && !siteName.isEmpty {
336 | siteIdAndName = ("\(siteId)", siteName)
337 | }
338 | }
339 |
340 | let uploadMgr = UploadManager(serverURL: serverURL)
341 | uploadMgr.upload(profile: profile,
342 | authMgr: makeAuthManager(),
343 | siteInfo: siteIdAndName,
344 | signingIdentity: mustSign ? signingId : nil) { possibleError in
345 | if let error = possibleError {
346 | warningInfo = error.localizedDescription
347 | } else {
348 | Alert().display(header: "Success", message: "Profile uploaded succesfully")
349 | dismissView()
350 | }
351 | networkOperationInfo = nil
352 | }
353 | }
354 | }
355 |
356 | #Preview {
357 | UploadInfoView(signingIdentities: [],
358 | dismissAction: nil)
359 | }
360 |
--------------------------------------------------------------------------------
/Source/TCCProfileImporter/TCCProfileConfigurationPanel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TCCProfileViewController.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2018 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import AppKit
29 | import Foundation
30 |
31 | class TCCProfileConfigurationPanel {
32 | /// Load TCC Profile data from file
33 | ///
34 | /// - Parameter completion: TCCProfileImportCompletion - success with TCCProfile or failure with TCCProfileImport Error
35 | func loadTCCProfileFromFile(importer: TCCProfileImporter, window: NSWindow, _ completion: @escaping TCCProfileImportCompletion) {
36 | let openPanel = NSOpenPanel.init()
37 | openPanel.allowedFileTypes = ["mobileconfig", "plist"]
38 | openPanel.allowsMultipleSelection = false
39 | openPanel.canChooseDirectories = false
40 | openPanel.canCreateDirectories = false
41 | openPanel.canChooseFiles = true
42 | openPanel.title = "Open TCCProfile File"
43 |
44 | openPanel.beginSheetModal(for: window) { (response) in
45 | if response != .OK {
46 | completion(.failure(.cancelled))
47 | } else {
48 | if let result = openPanel.url {
49 | importer.decodeTCCProfile(fileUrl: result) { tccProfileResult in
50 | return completion(tccProfileResult)
51 | }
52 | } else {
53 | completion(.failure(TCCProfileImportError.unableToOpenFile))
54 | }
55 | }
56 | }
57 |
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Source/TCCProfileImporter/TCCProfileImportError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConfigProfileImportError.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2019 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 | public enum TCCProfileImportError: Error {
30 | case cancelled
31 | case unableToOpenFile
32 | case decodeProfileError
33 | case invalidProfileFile(description: String)
34 | case emptyFields(description: String)
35 | }
36 |
37 | extension TCCProfileImportError: LocalizedError {
38 | public var errorDescription: String? {
39 | switch self {
40 | case .cancelled:
41 | return "Cancelled the import."
42 | case .unableToOpenFile:
43 | return "Unable to open file. Please make sure that file is correct and try again."
44 | case .decodeProfileError:
45 | return "Unable to read configuration profile. Please make sure the file is correct and try again."
46 | case .invalidProfileFile(let description):
47 | return "Invalid TCC Profile. Please make sure that required keys are inside profile: \(description)"
48 | case .emptyFields(let description):
49 | return "Unable to proceed. The following fields are required: \(description)"
50 | }
51 | }
52 |
53 | var isCancelled: Bool {
54 | switch self {
55 | case .cancelled:
56 | return true
57 | default:
58 | return false
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Source/TCCProfileImporter/TCCProfileImporter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TCCProfileImporter.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2019 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Foundation
29 |
30 | typealias TCCProfileImportResult = Result
31 | typealias TCCProfileImportCompletion = ((TCCProfileImportResult) -> Void)
32 |
33 | /// Load tcc profiles
34 | public class TCCProfileImporter {
35 |
36 | // MARK: Load TCCProfile
37 |
38 | /// Mapping & Decoding tcc profile
39 | ///
40 | /// - Parameter fileUrl: path with a file to load, completion: TCCProfileImportCompletion - success with TCCProfile or failure with TCCProfileImport Error
41 | func decodeTCCProfile(data: Data, _ completion: @escaping TCCProfileImportCompletion) {
42 | do {
43 | // Note that parse will ignore the signing portion of the data
44 | let tccProfile = try TCCProfile.parse(from: data)
45 | return completion(.success(tccProfile))
46 | } catch TCCProfile.ParseError.failedToCreateDecoder {
47 | return completion(.failure(.decodeProfileError))
48 | } catch let DecodingError.keyNotFound(codingKey, _) {
49 | return completion(TCCProfileImportResult.failure(.invalidProfileFile(description: codingKey.stringValue)))
50 | } catch let DecodingError.typeMismatch(type, context) {
51 | let errorDescription = "Type \(type) mismatch: \(context.debugDescription) codingPath: \(context.codingPath)"
52 | return completion(.failure(.invalidProfileFile(description: errorDescription)))
53 | } catch let error as NSError {
54 | let errorDescription = error.userInfo["NSDebugDescription"] as? String
55 | return completion(.failure(.invalidProfileFile(description: errorDescription ?? error.localizedDescription)))
56 | }
57 | }
58 |
59 | /// Mapping & Decoding tcc profile
60 | ///
61 | /// - Parameter fileUrl: path with a file to load, completion: TCCProfileImportCompletion - success with TCCProfile or failure with TCCProfileImport Error
62 | func decodeTCCProfile(fileUrl: URL, _ completion: @escaping TCCProfileImportCompletion) {
63 | let data: Data
64 | do {
65 | data = try Data(contentsOf: fileUrl)
66 | return decodeTCCProfile(data: data, completion)
67 | } catch {
68 | return completion(.failure(.unableToOpenFile))
69 | }
70 |
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Source/View Controllers/OpenViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OpenViewController.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2018 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Cocoa
29 |
30 | class OpenViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
31 |
32 | var completionBlock: (([LoadExecutableResult]) -> Void)?
33 |
34 | var observers: [NSKeyValueObservation] = []
35 |
36 | @objc dynamic var current: Executable?
37 | @objc dynamic var choices: [Executable] = []
38 |
39 | @IBOutlet var choicesAC: NSArrayController!
40 |
41 | override func viewWillAppear() {
42 | super.viewWillAppear()
43 | // Reload executables
44 | current = Model.shared.current
45 | if let value = current {
46 | choices = Model.shared.getAppleEventChoices(executable: value)
47 | }
48 | }
49 |
50 | func tableView(_ tableView: NSTableView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet {
51 | DispatchQueue.main.async {
52 | guard let index = proposedSelectionIndexes.first else { return }
53 | self.completionBlock?([.success(self.choices[index])])
54 | self.dismiss(self)
55 | }
56 | return proposedSelectionIndexes
57 | }
58 |
59 | @IBAction func prompt(_ sender: NSButton) {
60 | let block = completionBlock
61 | let panel = NSOpenPanel()
62 | panel.allowsMultipleSelection = true
63 | panel.allowedFileTypes = [ kUTTypeBundle, kUTTypeUnixExecutable ] as [String]
64 | panel.directoryURL = URL(fileURLWithPath: "/Applications", isDirectory: true)
65 | panel.begin { response in
66 | if response == .OK {
67 | var selections: [LoadExecutableResult] = []
68 | panel.urls.forEach {
69 | Model.shared.loadExecutable(url: $0) { result in
70 | selections.append(result)
71 | }
72 | }
73 | block?(selections)
74 | }
75 | }
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/Source/View Controllers/SaveViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SaveViewController.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2018 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Cocoa
29 | import OSLog
30 |
31 | class SaveViewController: NSViewController {
32 |
33 | private static var saveProfileKVOContext = 0
34 |
35 | @objc dynamic var isReadyToSave: Bool = false
36 |
37 | @objc dynamic var payloadName: String! {
38 | didSet {
39 | updateIsReadyToSave()
40 | }
41 | }
42 |
43 | @objc dynamic var payloadIdentifier: String! {
44 | didSet {
45 | updateIsReadyToSave()
46 | }
47 | }
48 |
49 | @objc dynamic var payloadDescription: String! {
50 | didSet {
51 | updateIsReadyToSave()
52 | }
53 | }
54 |
55 | @IBOutlet weak var payloadNameLabel: NSTextField!
56 |
57 | @IBOutlet weak var organizationLabel: NSTextField!
58 | @IBOutlet weak var identitiesPopUp: NSPopUpButton!
59 | @IBOutlet var identitiesPopUpAC: NSArrayController!
60 | @IBOutlet weak var saveButton: NSButton!
61 |
62 | let logger = Logger.SaveViewController
63 |
64 | var defaultsController = NSUserDefaultsController.shared
65 |
66 | func updateIsReadyToSave() {
67 | guard isReadyToSave != (
68 | !organizationLabel.stringValue.isEmpty
69 | && (payloadName != nil)
70 | && !payloadName.isEmpty
71 | && (payloadIdentifier != nil)
72 | && !payloadIdentifier.isEmpty ) else { return }
73 | isReadyToSave = !isReadyToSave
74 | }
75 |
76 | @IBAction func savePressed(_ sender: NSButton) {
77 | let panel = NSSavePanel()
78 | panel.allowedFileTypes = ["mobileconfig"]
79 | panel.nameFieldStringValue = payloadName
80 | if let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
81 | panel.directoryURL = URL(fileURLWithPath: path, isDirectory: true)
82 | }
83 |
84 | panel.begin { response in
85 | if response == .OK {
86 | // Let the save panel fully close itself before doing any work that may require keychain access.
87 | DispatchQueue.main.async {
88 | self.saveTo(url: panel.url!)
89 | }
90 | }
91 | }
92 | }
93 |
94 | override func viewDidLoad() {
95 | super.viewDidLoad()
96 | payloadIdentifier = UUID().uuidString
97 | do {
98 | var identities = try SecurityWrapper.loadSigningIdentities()
99 | identities.insert(SigningIdentity(name: "Not signed", reference: nil), at: 0)
100 | identitiesPopUpAC.add(contentsOf: identities)
101 | } catch {
102 | logger.error("Error loading identities: \(error)")
103 | }
104 |
105 | loadImportedTCCProfileInfo()
106 | }
107 |
108 | override func viewWillAppear() {
109 | super.viewWillAppear()
110 | defaultsController.addObserver(self, forKeyPath: "values.organization", options: [.new], context: &SaveViewController.saveProfileKVOContext)
111 | if !organizationLabel.stringValue.isEmpty {
112 | payloadNameLabel.becomeFirstResponder()
113 | }
114 | }
115 |
116 | override func viewWillDisappear() {
117 | super.viewWillDisappear()
118 | defaultsController.removeObserver(self, forKeyPath: "values.organization", context: &SaveViewController.saveProfileKVOContext)
119 | }
120 |
121 | // swiftlint:disable:next block_based_kvo
122 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
123 | if context == &SaveViewController.saveProfileKVOContext {
124 | updateIsReadyToSave()
125 | } else {
126 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
127 | }
128 | }
129 |
130 | func saveTo(url: URL) {
131 | logger.info("Saving to \(url, privacy: .public)")
132 | let model = Model.shared
133 | let profile = model.exportProfile(organization: organizationLabel.stringValue,
134 | identifier: payloadIdentifier,
135 | displayName: payloadName,
136 | payloadDescription: payloadDescription ?? payloadName)
137 | do {
138 | var outputData = try profile.xmlData()
139 | if let identity = identitiesPopUpAC.selectedObjects.first as? SigningIdentity, let ref = identity.reference {
140 | logger.info("Signing profile with \(identity.displayName)")
141 | outputData = try SecurityWrapper.sign(data: outputData, using: ref)
142 | }
143 | try outputData.write(to: url)
144 | logger.info("Saved successfully")
145 | } catch {
146 | logger.error("Error: \(error)")
147 | }
148 | self.dismiss(nil)
149 | }
150 |
151 | func loadImportedTCCProfileInfo() {
152 | let model = Model.shared
153 |
154 | if let tccProfile = model.importedTCCProfile {
155 | organizationLabel.stringValue = tccProfile.organization
156 | payloadName = tccProfile.displayName
157 | payloadDescription = tccProfile.payloadDescription
158 | payloadIdentifier = tccProfile.identifier
159 | }
160 | }
161 |
162 | }
163 |
--------------------------------------------------------------------------------
/Source/Views/Alert.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Alert.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2020 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Cocoa
29 |
30 | class Alert: NSObject {
31 | func display(header: String, message: String) {
32 | DispatchQueue.main.async {
33 | let dialog: NSAlert = NSAlert()
34 | dialog.messageText = header
35 | dialog.informativeText = message
36 | dialog.alertStyle = NSAlert.Style.warning
37 | dialog.addButton(withTitle: "OK")
38 | dialog.runModal()
39 | }
40 | }
41 |
42 | /// Displays a message with a cancel button and returns true if OK was pressed
43 | /// Assumes this method is called from the main queue.
44 | ///
45 | /// - Parameters:
46 | /// - header: The header message
47 | /// - message: The message body
48 | /// - Returns: True if the ok button was pressed
49 | func displayWithCancel(header: String, message: String) -> Bool {
50 | let dialog: NSAlert = NSAlert()
51 | dialog.messageText = header
52 | dialog.informativeText = message
53 | dialog.alertStyle = NSAlert.Style.warning
54 | dialog.addButton(withTitle: "OK")
55 | dialog.addButton(withTitle: "Cancel")
56 | let response = dialog.runModal()
57 | let okPressed = (response.rawValue == 1000)
58 | return okPressed
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/Source/Views/FlippedClipView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FlippedClipView.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2019 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Cocoa
29 |
30 | class FlippedClipView: NSClipView {
31 | override var isFlipped: Bool {
32 | return true
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Source/Views/InfoButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FlippedClipView.swift
3 | // PPPC Utility
4 | //
5 | // MIT License
6 | //
7 | // Copyright (c) 2019 Jamf Software
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in all
17 | // copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | // SOFTWARE.
26 | //
27 |
28 | import Cocoa
29 |
30 | class InfoButton: NSButton {
31 | private var helpMessage: String = ""
32 |
33 | func setHelpMessage(_ message: String?) {
34 | self.helpMessage = message ?? ""
35 | }
36 |
37 | func showHelpMessage() {
38 | NSHelpManager.shared.setContextHelp(NSAttributedString(string: helpMessage), for: self)
39 | NSHelpManager.shared.showContextHelp(for: self, locationHint: NSEvent.mouseLocation)
40 | NSHelpManager.shared.removeContextHelp(for: self)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------