diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..312d1f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output diff --git a/Images/Building.png b/Images/Building.png new file mode 100644 index 0000000..1abf505 Binary files /dev/null and b/Images/Building.png differ diff --git a/Images/SavingSigned.png b/Images/SavingSigned.png new file mode 100644 index 0000000..fa14880 Binary files /dev/null and b/Images/SavingSigned.png differ diff --git a/Images/SavingUnsigned.png b/Images/SavingUnsigned.png new file mode 100644 index 0000000..e6235f2 Binary files /dev/null and b/Images/SavingUnsigned.png differ diff --git a/Images/UploadSigned.png b/Images/UploadSigned.png new file mode 100644 index 0000000..bca7336 Binary files /dev/null and b/Images/UploadSigned.png differ diff --git a/Images/UploadUnsigned.png b/Images/UploadUnsigned.png new file mode 100644 index 0000000..222200b Binary files /dev/null and b/Images/UploadUnsigned.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..949dfab --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Jamf Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PPPC Utility.xcodeproj/project.pbxproj b/PPPC Utility.xcodeproj/project.pbxproj new file mode 100644 index 0000000..2dd2f23 --- /dev/null +++ b/PPPC Utility.xcodeproj/project.pbxproj @@ -0,0 +1,414 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 6E6216F9215321CE0043DF18 /* OpenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6216F8215321CE0043DF18 /* OpenViewController.swift */; }; + 6EB45830214FFCCB00BE5749 /* AppleEventRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EB4582F214FFCCB00BE5749 /* AppleEventRule.swift */; }; + 6EB86F692151476500FBE634 /* JamfProClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EB86F682151476500FBE634 /* JamfProClient.swift */; }; + 6EC409DE214D65BC00BE4F17 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC409DD214D65BC00BE4F17 /* AppDelegate.swift */; }; + 6EC409E2214D65BD00BE4F17 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6EC409E1214D65BD00BE4F17 /* Assets.xcassets */; }; + 6EC409E5214D65BD00BE4F17 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6EC409E3214D65BD00BE4F17 /* Main.storyboard */; }; + 6EC409F3214D8FFA00BE4F17 /* TCCProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC409F2214D8FFA00BE4F17 /* TCCProfileViewController.swift */; }; + 6EC409F5214D95D200BE4F17 /* TCCProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC409F4214D95D200BE4F17 /* TCCProfile.swift */; }; + 6EC40A10214DE3B200BE4F17 /* Executable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC40A0F214DE3B200BE4F17 /* Executable.swift */; }; + 6EC40A12214DF8FE00BE4F17 /* SecurityWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC40A11214DF8FE00BE4F17 /* SecurityWrapper.swift */; }; + 6EC40A14214DFB5800BE4F17 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC40A13214DFB5800BE4F17 /* Model.swift */; }; + 6EC40A16214ECF1E00BE4F17 /* SaveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC40A15214ECF1E00BE4F17 /* SaveViewController.swift */; }; + 6EC40A18214ECF2C00BE4F17 /* UploadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC40A17214ECF2C00BE4F17 /* UploadViewController.swift */; }; + 6EC40A1C214EF87800BE4F17 /* SigningIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC40A1B214EF87800BE4F17 /* SigningIdentity.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 6E5D5A1521541B8F00B43312 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 6E6216F8215321CE0043DF18 /* OpenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenViewController.swift; sourceTree = ""; }; + 6E95730721553B650002C30B /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + 6E957309215557870002C30B /* PPPC Utility.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "PPPC Utility.entitlements"; sourceTree = ""; }; + 6EB4582F214FFCCB00BE5749 /* AppleEventRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleEventRule.swift; sourceTree = ""; }; + 6EB86F682151476500FBE634 /* JamfProClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JamfProClient.swift; sourceTree = ""; }; + 6EC409DA214D65BC00BE4F17 /* PPPC Utility.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PPPC Utility.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6EC409DD214D65BC00BE4F17 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 6EC409E1214D65BD00BE4F17 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 6EC409E4214D65BD00BE4F17 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 6EC409E6214D65BD00BE4F17 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6EC409F2214D8FFA00BE4F17 /* TCCProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCCProfileViewController.swift; sourceTree = ""; }; + 6EC409F4214D95D200BE4F17 /* TCCProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCCProfile.swift; sourceTree = ""; }; + 6EC40A0F214DE3B200BE4F17 /* Executable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Executable.swift; sourceTree = ""; }; + 6EC40A11214DF8FE00BE4F17 /* SecurityWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityWrapper.swift; sourceTree = ""; }; + 6EC40A13214DFB5800BE4F17 /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; + 6EC40A15214ECF1E00BE4F17 /* SaveViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveViewController.swift; sourceTree = ""; }; + 6EC40A17214ECF2C00BE4F17 /* UploadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadViewController.swift; sourceTree = ""; }; + 6EC40A1B214EF87800BE4F17 /* SigningIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningIdentity.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6EC409D7214D65BC00BE4F17 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6EC409D1214D65BC00BE4F17 = { + isa = PBXGroup; + children = ( + 6E95730721553B650002C30B /* LICENSE */, + 6E5D5A1521541B8F00B43312 /* README.md */, + 6EC409DC214D65BC00BE4F17 /* Source */, + 6EC409F6214D975A00BE4F17 /* Resources */, + 6EC409DB214D65BC00BE4F17 /* Products */, + ); + sourceTree = ""; + }; + 6EC409DB214D65BC00BE4F17 /* Products */ = { + isa = PBXGroup; + children = ( + 6EC409DA214D65BC00BE4F17 /* PPPC Utility.app */, + ); + name = Products; + sourceTree = ""; + }; + 6EC409DC214D65BC00BE4F17 /* Source */ = { + isa = PBXGroup; + children = ( + 6EC409DD214D65BC00BE4F17 /* AppDelegate.swift */, + 6EB86F682151476500FBE634 /* JamfProClient.swift */, + 6EC40A11214DF8FE00BE4F17 /* SecurityWrapper.swift */, + 6EC40A1E214EF89600BE4F17 /* View Controllers */, + 6EC40A1D214EF87E00BE4F17 /* Model */, + ); + path = Source; + sourceTree = ""; + }; + 6EC409F6214D975A00BE4F17 /* Resources */ = { + isa = PBXGroup; + children = ( + 6EC409E1214D65BD00BE4F17 /* Assets.xcassets */, + 6EC409E3214D65BD00BE4F17 /* Main.storyboard */, + 6EC409E6214D65BD00BE4F17 /* Info.plist */, + 6E957309215557870002C30B /* PPPC Utility.entitlements */, + ); + path = Resources; + sourceTree = ""; + }; + 6EC40A1D214EF87E00BE4F17 /* Model */ = { + isa = PBXGroup; + children = ( + 6EC40A1B214EF87800BE4F17 /* SigningIdentity.swift */, + 6EC40A13214DFB5800BE4F17 /* Model.swift */, + 6EC40A0F214DE3B200BE4F17 /* Executable.swift */, + 6EC409F4214D95D200BE4F17 /* TCCProfile.swift */, + 6EB4582F214FFCCB00BE5749 /* AppleEventRule.swift */, + ); + path = Model; + sourceTree = ""; + }; + 6EC40A1E214EF89600BE4F17 /* View Controllers */ = { + isa = PBXGroup; + children = ( + 6EC409F2214D8FFA00BE4F17 /* TCCProfileViewController.swift */, + 6EC40A15214ECF1E00BE4F17 /* SaveViewController.swift */, + 6EC40A17214ECF2C00BE4F17 /* UploadViewController.swift */, + 6E6216F8215321CE0043DF18 /* OpenViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 6EC409D9214D65BC00BE4F17 /* PPPC Utility */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6EC409EA214D65BD00BE4F17 /* Build configuration list for PBXNativeTarget "PPPC Utility" */; + buildPhases = ( + 6EC409D6214D65BC00BE4F17 /* Sources */, + 6EC409D7214D65BC00BE4F17 /* Frameworks */, + 6EC409D8214D65BC00BE4F17 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "PPPC Utility"; + productName = TCCUtility; + productReference = 6EC409DA214D65BC00BE4F17 /* PPPC Utility.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6EC409D2214D65BC00BE4F17 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1000; + LastUpgradeCheck = 1000; + ORGANIZATIONNAME = Jamf; + TargetAttributes = { + 6EC409D9214D65BC00BE4F17 = { + CreatedOnToolsVersion = 10.0; + SystemCapabilities = { + com.apple.HardenedRuntime = { + enabled = 1; + }; + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + }; + }; + buildConfigurationList = 6EC409D5214D65BC00BE4F17 /* Build configuration list for PBXProject "PPPC Utility" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 6EC409D1214D65BC00BE4F17; + productRefGroup = 6EC409DB214D65BC00BE4F17 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6EC409D9214D65BC00BE4F17 /* PPPC Utility */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6EC409D8214D65BC00BE4F17 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6EC409E2214D65BD00BE4F17 /* Assets.xcassets in Resources */, + 6EC409E5214D65BD00BE4F17 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6EC409D6214D65BC00BE4F17 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6EB86F692151476500FBE634 /* JamfProClient.swift in Sources */, + 6EC40A14214DFB5800BE4F17 /* Model.swift in Sources */, + 6EC40A1C214EF87800BE4F17 /* SigningIdentity.swift in Sources */, + 6EC40A18214ECF2C00BE4F17 /* UploadViewController.swift in Sources */, + 6EC409F5214D95D200BE4F17 /* TCCProfile.swift in Sources */, + 6EC40A12214DF8FE00BE4F17 /* SecurityWrapper.swift in Sources */, + 6EC409DE214D65BC00BE4F17 /* AppDelegate.swift in Sources */, + 6EC409F3214D8FFA00BE4F17 /* TCCProfileViewController.swift in Sources */, + 6E6216F9215321CE0043DF18 /* OpenViewController.swift in Sources */, + 6EC40A10214DE3B200BE4F17 /* Executable.swift in Sources */, + 6EC40A16214ECF1E00BE4F17 /* SaveViewController.swift in Sources */, + 6EB45830214FFCCB00BE5749 /* AppleEventRule.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 6EC409E3214D65BD00BE4F17 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 6EC409E4214D65BD00BE4F17 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 6EC409E8214D65BD00BE4F17 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.13; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 6EC409E9214D65BD00BE4F17 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.13; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 6EC409EB214D65BD00BE4F17 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "Resources/PPPC Utility.entitlements"; + CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = Resources/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.jamf.opensource.pppcutility; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 4.2; + }; + name = Debug; + }; + 6EC409EC214D65BD00BE4F17 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "Resources/PPPC Utility.entitlements"; + CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = Resources/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.jamf.opensource.pppcutility; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 4.2; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6EC409D5214D65BC00BE4F17 /* Build configuration list for PBXProject "PPPC Utility" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6EC409E8214D65BD00BE4F17 /* Debug */, + 6EC409E9214D65BD00BE4F17 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6EC409EA214D65BD00BE4F17 /* Build configuration list for PBXNativeTarget "PPPC Utility" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6EC409EB214D65BD00BE4F17 /* Debug */, + 6EC409EC214D65BD00BE4F17 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 6EC409D2214D65BC00BE4F17 /* Project object */; +} diff --git a/PPPC Utility.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/PPPC Utility.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..ccdc494 --- /dev/null +++ b/PPPC Utility.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/PPPC Utility.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/PPPC Utility.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/PPPC Utility.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/README.md b/README.md index e517f3a..ed58cc7 100644 --- a/README.md +++ b/README.md @@ -1 +1,35 @@ -# PPPC-Utility +# ![alt text][logo] Privacy Preferences Policy Control (PPPC) Utility + +[logo]: /Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32%402x.png "PPPC Utility" + +PPPC Utility is a macOS (10.13 and newer) application for creating configuration profiles containing the +Privacy Preferences Policy Control payload for macOS. The profiles can be saved locally signed or unsigned. +Profiles can also be uploaded directly to a Jamf Pro server. + +## Installation + +#### [Download the latest version here](https://github.com/jamf/PPPC-Utility/releases) + +## Building profile +Start by adding the bundles/executables for the payload by using drag-and-drop or by selecting the add (+) +button in the left corner. +![alt text](/Images/Building.png "Building profile") + +## Saving +Profiles can be saved locally either signed or unsigned. + +![alt text](/Images/SavingUnsigned.png "Building profile") + + +## Upload to Jamf Pro + +#### Jamf Pro 10.7.1 and newer +Starting in Jamf Pro 10.7.1 the Privacy Preferences Policy Control Payload can be uploaded to the API without being signed before uploading. +![alt text](/Images/UploadUnsigned.png "Upload unsigned") + +#### Jamf Pro 10.7.0 and below +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. +![alt text](/Images/UploadSigned.png "Upload signed") + + diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..4528606 --- /dev/null +++ b/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "PPPC_Logo_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "PPPC_Logo_16@2x.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "PPPC_Logo_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "PPPC_Logo_32@2x.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "PPPC_Logo_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "PPPC_Logo_128@2x.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "PPPC_Logo_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "PPPC_Logo_256@2x.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "PPPC_Logo_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "PPPC_Logo_512@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_128.png b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_128.png new file mode 100644 index 0000000..4a795a3 Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_128.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_128@2x.png b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_128@2x.png new file mode 100644 index 0000000..4ae5fcd Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_128@2x.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_16.png b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_16.png new file mode 100644 index 0000000..9600215 Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_16.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_16@2x.png b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_16@2x.png new file mode 100644 index 0000000..76503d5 Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_16@2x.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_256.png b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_256.png new file mode 100644 index 0000000..4ae5fcd Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_256.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_256@2x.png b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_256@2x.png new file mode 100644 index 0000000..24422a1 Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_256@2x.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32.png b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32.png new file mode 100644 index 0000000..76503d5 Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32@2x.png b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32@2x.png new file mode 100644 index 0000000..1793546 Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_32@2x.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_512.png b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_512.png new file mode 100644 index 0000000..24422a1 Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_512.png differ diff --git a/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_512@2x.png b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_512@2x.png new file mode 100644 index 0000000..60b4efd Binary files /dev/null and b/Resources/Assets.xcassets/AppIcon.appiconset/PPPC_Logo_512@2x.png differ diff --git a/Resources/Assets.xcassets/Contents.json b/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Resources/Base.lproj/Main.storyboard b/Resources/Base.lproj/Main.storyboard new file mode 100644 index 0000000..b7d989f --- /dev/null +++ b/Resources/Base.lproj/Main.storyboard @@ -0,0 +1,2540 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + identifier "com.jamfsoftware.jamf" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "483DWKW443" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSIsNil + + + + + + [Required] + + + + + + + + + + + + + + + + + + + + + + + + + + + NSIsNil + + + + + + Jamf Pro Username + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + NSIsNil + + + + + + Jamf Pro Password + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSIsNil + + + + + + [Required] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSIsNil + + + + + + [Required] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSIsNil + + + + + + [Required] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSIsNil + + + + + + [Optional] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSIsNil + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Required] + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Required] + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Required] + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Optional] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/Info.plist b/Resources/Info.plist new file mode 100644 index 0000000..14099bd --- /dev/null +++ b/Resources/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2018 Jamf. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + + diff --git a/Resources/PPPC Utility.entitlements b/Resources/PPPC Utility.entitlements new file mode 100644 index 0000000..a046386 --- /dev/null +++ b/Resources/PPPC Utility.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + + diff --git a/Source/AppDelegate.swift b/Source/AppDelegate.swift new file mode 100644 index 0000000..5f32079 --- /dev/null +++ b/Source/AppDelegate.swift @@ -0,0 +1,42 @@ +// +// AppDelegate.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + + +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + + func applicationDidFinishLaunching(_ aNotification: Notification) {} + + func applicationWillTerminate(_ aNotification: Notification) {} + + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + +} diff --git a/Source/JamfProClient.swift b/Source/JamfProClient.swift new file mode 100644 index 0000000..cd38ea3 --- /dev/null +++ b/Source/JamfProClient.swift @@ -0,0 +1,154 @@ +// +// JamfProClient.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +struct JamfProClient { + + var urlString: String + var username: String + var password: String + + init(_ url: String, _ user: String, _ pass: String) { + urlString = url + password = pass + username = user + } + + func uploadProfile(_ profile: TCCProfile, signingIdentity: SecIdentity?, completionBlock: @escaping (Bool)->Void) { + var profileText: String + do { + var profileData = try profile.xmlData() + if let identity = signingIdentity { + profileData = try SecurityWrapper.sign(data: profileData, using: identity) + } + profileText = String(data: profileData, encoding: .utf8) ?? "" + } catch { + print("Error encoding profile: \(error)") + completionBlock(false) + return + } + + let root = XMLElement(name: "os_x_configuration_profile") + let general = XMLElement(name: "general") + root.addChild(general) + + let payloads = XMLElement(name: "payloads", stringValue: profileText) + general.addChild(payloads) + + general.addChild(XMLElement(name: "name", stringValue: profile.displayName)) + general.addChild(XMLElement(name: "description", stringValue: profile.payloadDescription)) + + let xml = XMLDocument(rootElement: root) + + sendRequest(endpoint: "osxconfigurationprofiles", data: xml.xmlData) { (statusCode, resultData) in + let success: Bool = (200 <= statusCode && statusCode <= 299) + if !success { + if let text = String(data: resultData, encoding: .utf8) { + print("Error (\(statusCode)):\n\(text)") + } else { + print("Unknown error: \(statusCode)") + } + + } + completionBlock(success) + } + } + + func getJamfProVersion(completionBlock: @escaping ((major: Int, minor: Int, patch: Int)?)->Void) { + sendRequest(endpoint: nil, data: nil) { (_, data) in + var result: (major: Int, minor: Int, patch: Int)? = nil + if let text = String(data: data, encoding: .utf8), + let startRange = text.range(of: "Void) { + sendRequest(endpoint: "activationcode", data: nil) { (statusCode, data) in + var orgName: String? = nil + if let doc = try? XMLDocument(data: data, options: []), + let nodes = try? doc.nodes(forXPath: "/activation_code/organization_name"), + let name = nodes.first?.stringValue { + orgName = name + } + completionBlock(statusCode,orgName) + } + } + + func sendRequest(endpoint: String?, data: Data?, completionHandler: @escaping (_ statusCode: Int, _ output: Data)->Void) { + let failureBlock: (String)->Void = { + print("\($0)") + completionHandler(0,Data()) + } + + guard let serverURL = URL(string: urlString) else { + failureBlock("Failed to create url for: \(urlString)") + return + } + var url = serverURL + var headers: [String:String] = [:] + if let apiEndpoint = endpoint { + url = serverURL.appendingPathComponent("JSSResource/\(apiEndpoint)") + let encodedText = "\(username):\(password)".data(using: .utf8)?.base64EncodedString() ?? "" + headers = [ + "Content-Type" : "text/xml", + "Accept" : "application/xml", + "Authorization" : "Basic \(encodedText)" + ] + } + var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60.0) + request.allHTTPHeaderFields = headers + + if let body = data { + request.httpMethod = "POST" + request.httpBody = body + } else { + request.httpMethod = "GET" + } + + URLSession.shared.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in + if let error = possibleError { + failureBlock("Error: \(error)") + } else if let response = possibleResponse as? HTTPURLResponse { + completionHandler(response.statusCode, possibleData ?? Data()) + } else { + failureBlock("Response was nil") + } + }.resume() + } +} diff --git a/Source/Model/AppleEventRule.swift b/Source/Model/AppleEventRule.swift new file mode 100644 index 0000000..d644d9d --- /dev/null +++ b/Source/Model/AppleEventRule.swift @@ -0,0 +1,38 @@ +// +// AppleEventRule.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Cocoa + +class AppleEventRule: NSObject { + + @objc dynamic var source: Executable! + @objc dynamic var destination: Executable! + @objc dynamic var valueString: String! = "Allow" + + var value: Bool { return valueString == "Allow" } + +} diff --git a/Source/Model/Executable.swift b/Source/Model/Executable.swift new file mode 100644 index 0000000..1705a33 --- /dev/null +++ b/Source/Model/Executable.swift @@ -0,0 +1,50 @@ +// +// Executable.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Cocoa + +class Executable: NSObject { + + @objc dynamic var iconPath: String! + + @objc dynamic var displayName: String! + @objc dynamic var identifier: String! + @objc dynamic var codeRequirement: String! + + @objc dynamic var addressBookPolicyString: String = "-" + @objc dynamic var photosPolicyString: String = "-" + @objc dynamic var remindersPolicyString: String = "-" + @objc dynamic var calendarPolicyString: String = "-" + @objc dynamic var accessibilityPolicyString: String = "-" + @objc dynamic var postEventsPolicyString: String = "-" + @objc dynamic var adminFilesPolicyString: String = "-" + @objc dynamic var allFilesPolicyString: String = "-" + @objc dynamic var cameraPolicyString: String = "-" + @objc dynamic var microphonePolicyString: String = "-" + + @objc dynamic var appleEvents: [AppleEventRule] = [] +} diff --git a/Source/Model/Model.swift b/Source/Model/Model.swift new file mode 100644 index 0000000..258e190 --- /dev/null +++ b/Source/Model/Model.swift @@ -0,0 +1,213 @@ +// +// Model.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Cocoa + +class Model : NSObject { + + @objc dynamic var current: Executable? + + @objc dynamic static let shared = Model() + @objc dynamic var identities: [SigningIdentity] = [] + @objc dynamic var selectedExecutables: [Executable] = [] + + func getAppleEventChoices(executable: Executable) -> [Executable] { + var executables: [Executable] = [] + if let executable = loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/System Events.app")) { + executables.append(executable) + } + + if let executable = loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/SystemUIServer.app")) { + executables.append(executable) + } + + if let executable = loadExecutable(url: URL(fileURLWithPath: "/System/Library/CoreServices/Finder.app")) { + executables.append(executable) + } + + let others = store.values.filter({ $0 != executable && !Set(executables).contains($0) }) + executables.append(contentsOf: others) + + return executables + } + + var store: [String:Executable] = [:] +} + +// MARK: Loading executable + +struct IconFilePath { + static let binary = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/ExecutableBinaryIcon.icns" + static let application = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns" + static let kext = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/KEXT.icns" + static let unknown = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericQuestionMarkIcon.icns" +} + +extension Model { + + func loadExecutable(url: URL) -> Executable? { + let executable = Executable() + + if let bundle = Bundle(url: url) { + guard let identifier = bundle.bundleIdentifier else { return nil } + executable.identifier = identifier + let info = bundle.infoDictionary + executable.displayName = (info?["CFBundleName"] as? String) ?? executable.identifier + if let resourcesURL = bundle.resourceURL { + if let definedIconFile = info?["CFBundleIconFile"] as? String { + var iconURL = resourcesURL.appendingPathComponent(definedIconFile) + if iconURL.pathExtension.isEmpty { + iconURL.appendPathExtension("icns") + } + executable.iconPath = iconURL.path + } else { + executable.iconPath = resourcesURL.appendingPathComponent("AppIcon.icns").path + } + + if !FileManager.default.fileExists(atPath: executable.iconPath) { + switch url.pathExtension { + case "app": executable.iconPath = IconFilePath.application + case "bundle": executable.iconPath = IconFilePath.kext + case "xpc": executable.iconPath = IconFilePath.kext + default: executable.iconPath = IconFilePath.unknown + } + } + } else { + return nil + } + } else { + executable.identifier = url.path + executable.displayName = url.lastPathComponent + executable.iconPath = IconFilePath.binary + } + + if let alreadyFoundExecutable = store[executable.identifier] { + return alreadyFoundExecutable + } + + do { + executable.codeRequirement = try SecurityWrapper.copyDesignatedRequirement(url: url) + store[executable.identifier] = executable + return executable + } catch { + print("Failed to get designated requirement with error: \(error)") + return nil + } + } +} + +// MARK: Exporting Profile + +extension Model { + + func exportProfile(organization: String, identifier: String, displayName: String, payloadDescription: String) -> TCCProfile { + var services = TCCServices() + + selectedExecutables.forEach { executable in + if let policy = policyFromString(executable: executable, value: executable.addressBookPolicyString) { + services.addressBook = services.addressBook ?? [] + services.addressBook?.append(policy) + } + + if let policy = policyFromString(executable: executable, value: executable.photosPolicyString) { + services.photos = services.photos ?? [] + services.photos?.append(policy) + } + + if let policy = policyFromString(executable: executable, value: executable.remindersPolicyString) { + services.reminders = services.reminders ?? [] + services.reminders?.append(policy) + } + + if let policy = policyFromString(executable: executable, value: executable.calendarPolicyString) { + services.calendar = services.calendar ?? [] + services.calendar?.append(policy) + } + + + if let policy = policyFromString(executable: executable, value: executable.accessibilityPolicyString) { + services.accessibility = services.accessibility ?? [] + services.accessibility?.append(policy) + } + + + if let policy = policyFromString(executable: executable, value: executable.postEventsPolicyString) { + services.postEvent = services.postEvent ?? [] + services.postEvent?.append(policy) + } + + if let policy = policyFromString(executable: executable, value: executable.adminFilesPolicyString) { + services.adminFiles = services.adminFiles ?? [] + services.postEvent?.append(policy) + } + + if let policy = policyFromString(executable: executable, value: executable.allFilesPolicyString) { + services.allFiles = services.allFiles ?? [] + services.allFiles?.append(policy) + } + + if let policy = policyFromString(executable: executable, value: executable.cameraPolicyString) { + services.camera = services.camera ?? [] + services.camera?.append(policy) + } + + if let policy = policyFromString(executable: executable, value: executable.microphonePolicyString) { + services.microphone = services.microphone ?? [] + services.microphone?.append(policy) + } + + executable.appleEvents.forEach { event in + let policy = TCCPolicy(identifier: executable.identifier, + codeRequirement: executable.codeRequirement, + allowed: event.value, + receiverIdentifier: event.destination.identifier, + receiverCodeRequirement: event.destination.codeRequirement) + services.appleEvents = services.appleEvents ?? [] + services.appleEvents?.append(policy) + + } + } + + return TCCProfile(organization: organization, + identifier: identifier, + displayName: displayName, + payloadDescription: payloadDescription, + services: services) + } + + func policyFromString(executable: Executable, value: String) -> TCCPolicy? { + let allowed: Bool + switch value { + case "Allow": allowed = true + case "Deny": allowed = false + default: return nil + } + return TCCPolicy(identifier: executable.identifier, + codeRequirement: executable.codeRequirement, + allowed: allowed) + } +} diff --git a/Source/Model/SigningIdentity.swift b/Source/Model/SigningIdentity.swift new file mode 100644 index 0000000..23bba65 --- /dev/null +++ b/Source/Model/SigningIdentity.swift @@ -0,0 +1,40 @@ +// +// SigningIdentity.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Cocoa + +class SigningIdentity: NSObject { + + @objc dynamic var displayName: String + var reference: SecIdentity? + + init(name: String, reference: SecIdentity?) { + displayName = name + super.init() + self.reference = reference + } +} diff --git a/Source/Model/TCCProfile.swift b/Source/Model/TCCProfile.swift new file mode 100644 index 0000000..6e3b751 --- /dev/null +++ b/Source/Model/TCCProfile.swift @@ -0,0 +1,164 @@ +// +// TCCProfile.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +typealias TCCPolicyIdentifierType = String + +extension TCCPolicyIdentifierType { + static let bundleID = "bundleID" + static let path = "path" +} + +struct TCCPolicy : Codable { + var comment: String + var identifier: String + var identifierType: TCCPolicyIdentifierType + var codeRequirement: String + var allowed: Bool + var receiverIdentifier: String? + var receiverIdentifierType: TCCPolicyIdentifierType? + var receiverCodeRequirement: String? + enum CodingKeys: String, CodingKey { + case identifier = "Identifier" + case identifierType = "IdentifierType" + case allowed = "Allowed" + case codeRequirement = "CodeRequirement" + case comment = "Comment" + case receiverIdentifier = "AEReceiverIdentifier" + case receiverIdentifierType = "AEReceiverIdentifierType" + case receiverCodeRequirement = "AEReceiverCodeRequirement" + } + init(identifier: String, codeRequirement: String, allowed: Bool, receiverIdentifier: String? = nil, receiverCodeRequirement: String? = nil) { + self.allowed = allowed + self.comment = "" + self.identifier = identifier + self.identifierType = identifier.contains("/") ? .path : .bundleID + self.codeRequirement = codeRequirement + self.receiverIdentifier = receiverIdentifier + if let otherIdentifier = receiverIdentifier { + self.receiverIdentifierType = otherIdentifier.contains("/") ? .path : .bundleID + } + self.receiverCodeRequirement = receiverCodeRequirement + } +} + +struct TCCServices : Codable { + var addressBook: [TCCPolicy]? + var calendar: [TCCPolicy]? + var reminders: [TCCPolicy]? + var photos: [TCCPolicy]? + var camera: [TCCPolicy]? + var microphone: [TCCPolicy]? + var accessibility: [TCCPolicy]? + var postEvent: [TCCPolicy]? + var allFiles: [TCCPolicy]? + var adminFiles: [TCCPolicy]? + var appleEvents: [TCCPolicy]? + enum CodingKeys: String, CodingKey { + case addressBook = "AddressBook" + case calendar = "Calendar" + case reminders = "Reminders" + case photos = "Photos" + case camera = "Camera" + case microphone = "Microphone" + case accessibility = "Accessibility" + case postEvent = "PostEvent" + case allFiles = "SystemPolicyAllFiles" + case adminFiles = "SystemPolicySysAdminFiles" + case appleEvents = "AppleEvents" + } +} + +struct TCCProfile : Codable { + struct Content: Codable { + var payloadDescription: String + var displayName: String + var identifier: String + var organization: String + var type: String + var uuid: String + var version: Int + var services: TCCServices + enum CodingKeys: String, CodingKey { + case payloadDescription = "PayloadDescription" + case displayName = "PayloadDisplayName" + case identifier = "PayloadIdentifier" + case organization = "PayloadOrganization" + case type = "PayloadType" + case uuid = "PayloadUUID" + case version = "PayloadVersion" + case services = "Services" + } + } + + var version: Int + var uuid: String + var type: String + var scope: String + var organization: String + var identifier: String + var displayName: String + var payloadDescription: String + var content: [Content] + enum CodingKeys: String, CodingKey { + case payloadDescription = "PayloadDescription" + case displayName = "PayloadDisplayName" + case identifier = "PayloadIdentifier" + case organization = "PayloadOrganization" + case scope = "payloadScope" + case type = "PayloadType" + case uuid = "PayloadUUID" + case version = "PayloadVersion" + case content = "PayloadContent" + } + init(organization: String, identifier: String, displayName: String, payloadDescription: String, services: TCCServices) { + let content = Content(payloadDescription: payloadDescription, + displayName: displayName, + identifier: identifier, + organization: organization, + type: "com.apple.TCC.configuration-profile-policy", + uuid: UUID().uuidString, + version: 1, + services: services) + self.version = 1 + self.uuid = UUID().uuidString + self.type = content.type + self.scope = "system" + self.organization = content.organization + self.identifier = content.identifier + self.displayName = content.displayName + self.payloadDescription = content.payloadDescription + self.content = [content] + } + + func xmlData() throws -> Data { + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + return try encoder.encode(self) + } +} diff --git a/Source/SecurityWrapper.swift b/Source/SecurityWrapper.swift new file mode 100644 index 0000000..d12c77a --- /dev/null +++ b/Source/SecurityWrapper.swift @@ -0,0 +1,153 @@ +// +// SecurityWrapper.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +struct SecurityWrapper { + + static func execute(block: ()->(OSStatus)) throws { + let status = block() + if status != 0 { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: nil) + } + } + + static func saveCredentials(username: String, password: String, server: String) throws { + + do { + let possibleResult = try loadCredentials(server: server) + if let old = possibleResult, username == old.username && password == old.password { + return + } else { + let dict = [ + kSecClass as String: kSecClassInternetPassword, + kSecAttrServer as String: server, + kSecAttrAccount as String: username + ] as CFDictionary + try execute { SecItemDelete(dict) } + } + } catch {} + + let dict = [ + kSecClass as String: kSecClassInternetPassword, + kSecAttrServer as String: server, + kSecAttrAccount as String: username, + kSecValueData as String: password + ] as CFDictionary + try execute { SecItemAdd(dict, nil) } + } + + static func removeCredentials(server: String, username: String) throws { + let dict = [ + kSecClass as String: kSecClassInternetPassword, + kSecAttrServer as String: server, + kSecAttrAccount as String: username + ] as CFDictionary + try execute { SecItemDelete(dict) } + } + + static func loadCredentials(server: String) throws -> (username: String, password: String)? { + let dict = [ + kSecClass as String: kSecClassInternetPassword, + kSecAttrServer as String: server, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecReturnAttributes as String: true, + kSecReturnData as String: true + ] as CFDictionary + + var item: CFTypeRef? + try execute { + let status = SecItemCopyMatching(dict, &item) + // Check if success or not found, thrown error is a "real" error + if status == errSecSuccess || status == errSecItemNotFound { + return errSecSuccess + } + return status + } + guard let existingItem = item as? [String : Any], + let passwordData = existingItem[kSecValueData as String] as? Data, + let password = String(data: passwordData, encoding: .utf8), + let username = existingItem[kSecAttrAccount as String] as? String + else { + return nil + } + return (username: username, password: password) + } + + static func copyDesignatedRequirement(url: URL) throws -> String { + let flags = SecCSFlags(rawValue: 0) + var staticCode: SecStaticCode? + var requirement: SecRequirement? + var text: CFString? + + try execute { SecStaticCodeCreateWithPath(url as CFURL, flags, &staticCode) } + try execute { SecCodeCopyDesignatedRequirement(staticCode!, flags, &requirement) } + try execute { SecRequirementCopyString(requirement!, flags, &text) } + + return text! as String + } + + static func sign(data: Data, using identity: SecIdentity) throws -> Data { + + var outputData: CFData? + var encoder: CMSEncoder? + try execute { CMSEncoderCreate(&encoder) } + try execute { CMSEncoderAddSigners(encoder!,identity) } + try execute { CMSEncoderAddSignedAttributes(encoder!,.attrSmimeCapabilities) } + try execute { CMSEncoderUpdateContent(encoder!,(data as NSData).bytes,data.count) } + try execute { CMSEncoderCopyEncodedContent(encoder!,&outputData) } + + return outputData! as Data + } + + static func loadSigningIdentities() throws -> [SigningIdentity] { + + let dict = [ + kSecClass as String : kSecClassIdentity, + kSecReturnRef as String : kCFBooleanTrue, + kSecMatchLimit as String : kSecMatchLimitAll + ] as CFDictionary + + var result: AnyObject? + try execute { SecItemCopyMatching(dict, &result) } + + guard let secIdentities = result as? [SecIdentity] else { return [] } + + return secIdentities.map({ + let name = try? getCertificateCommonName(for: $0) + return SigningIdentity(name: name ?? "Unknown \($0.hashValue)", reference: $0) + }) + } + + static func getCertificateCommonName(for identity: SecIdentity) throws -> String { + var certificate: SecCertificate? + var commonName: CFString? + try execute { SecIdentityCopyCertificate(identity, &certificate) } + try execute { SecCertificateCopyCommonName(certificate!, &commonName) } + return commonName! as String + } +} diff --git a/Source/View Controllers/OpenViewController.swift b/Source/View Controllers/OpenViewController.swift new file mode 100644 index 0000000..02fdef2 --- /dev/null +++ b/Source/View Controllers/OpenViewController.swift @@ -0,0 +1,77 @@ +// +// OpenViewController.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Cocoa + +class OpenViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate { + + var completionBlock: (([Executable]) -> Void)? + + var observers: [NSKeyValueObservation] = [] + + @objc dynamic var current: Executable? + @objc dynamic var choices: [Executable] = [] + + @IBOutlet var choicesAC: NSArrayController! + + override func viewWillAppear() { + super.viewWillAppear() + // Reload executables + current = Model.shared.current + if let value = current { + choices = Model.shared.getAppleEventChoices(executable: value) + } + } + + func tableView(_ tableView: NSTableView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet { + DispatchQueue.main.async { + guard let index = proposedSelectionIndexes.first else { return } + self.completionBlock?([self.choices[index]]) + self.dismiss(self) + } + return proposedSelectionIndexes + } + + @IBAction func prompt(_ sender: NSButton) { + let block = completionBlock + let panel = NSOpenPanel() + panel.allowsMultipleSelection = true + panel.allowedFileTypes = [ kUTTypeBundle, kUTTypeExecutable ] as [String] + panel.directoryURL = URL(fileURLWithPath: "/Applications", isDirectory: true) + panel.begin { response in + if response == .OK { + var selections : [Executable] = [] + panel.urls.forEach { + guard let executable = Model.shared.loadExecutable(url: $0) else { return } + selections.append(executable) + } + block?(selections) + } + } + } + +} diff --git a/Source/View Controllers/SaveViewController.swift b/Source/View Controllers/SaveViewController.swift new file mode 100644 index 0000000..aaa8f91 --- /dev/null +++ b/Source/View Controllers/SaveViewController.swift @@ -0,0 +1,142 @@ +// +// SaveViewController.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Cocoa + +class SaveViewController: NSViewController { + + private static var saveProfileKVOContext = 0 + + @objc dynamic var isReadyToSave: Bool = false + + @objc dynamic var payloadName: String! { + didSet { + updateIsReadyToSave() + } + } + + @objc dynamic var payloadIdentifier: String! { + didSet { + updateIsReadyToSave() + } + } + + @objc dynamic var payloadDescription: String! { + didSet { + updateIsReadyToSave() + } + } + + @IBOutlet weak var payloadNameLabel: NSTextField! + + @IBOutlet weak var organizationLabel: NSTextField! + @IBOutlet weak var identitiesPopUp: NSPopUpButton! + @IBOutlet var identitiesPopUpAC: NSArrayController! + @IBOutlet weak var saveButton: NSButton! + + var defaultsController = NSUserDefaultsController.shared + + func updateIsReadyToSave() { + guard isReadyToSave != ( + !organizationLabel.stringValue.isEmpty + && (payloadName != nil) + && !payloadName.isEmpty + && (payloadIdentifier != nil) + && !payloadIdentifier.isEmpty ) else { return } + isReadyToSave = !isReadyToSave + } + + @IBAction func savePressed(_ sender: NSButton) { + let panel = NSSavePanel() + panel.allowedFileTypes = ["mobileconfig"] + panel.nameFieldStringValue = payloadName + if let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first { + panel.directoryURL = URL(fileURLWithPath: path, isDirectory: true) + } + + panel.begin { response in + if response == .OK { + self.saveTo(url: panel.url!) + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + payloadIdentifier = UUID().uuidString + do { + var identities = try SecurityWrapper.loadSigningIdentities() + identities.insert(SigningIdentity(name: "Not signed", reference: nil), at: 0) + identitiesPopUpAC.add(contentsOf: identities) + } catch { + print("Error loading identities: \(error)") + } + } + + override func viewWillAppear() { + super.viewWillAppear() + defaultsController.addObserver(self, forKeyPath: "values.organization", options: [.new], context: &SaveViewController.saveProfileKVOContext) + if !organizationLabel.stringValue.isEmpty { + payloadNameLabel.becomeFirstResponder() + } + } + + override func viewWillDisappear() { + super.viewWillDisappear() + defaultsController.removeObserver(self, forKeyPath: "values.organization", context: &SaveViewController.saveProfileKVOContext) + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if context == &SaveViewController.saveProfileKVOContext { + updateIsReadyToSave() + } else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + } + } + + func saveTo(url: URL) { + print("Saving to \(url)") + let model = Model.shared + let profile = model.exportProfile(organization: organizationLabel.stringValue, + identifier: payloadIdentifier, + displayName: payloadName, + payloadDescription: payloadDescription ?? payloadName) + do { + var outputData = try profile.xmlData() + if let identity = identitiesPopUpAC.selectedObjects.first as? SigningIdentity, let ref = identity.reference { + print("Signing profile with \(identity.displayName)") + outputData = try SecurityWrapper.sign(data: outputData, using: ref) + } + try outputData.write(to: url) + print("Saved successfully") + } catch { + print("Error: \(error)") + } + self.dismiss(nil) + } + +} diff --git a/Source/View Controllers/TCCProfileViewController.swift b/Source/View Controllers/TCCProfileViewController.swift new file mode 100644 index 0000000..33da0de --- /dev/null +++ b/Source/View Controllers/TCCProfileViewController.swift @@ -0,0 +1,193 @@ +// +// TCCProfileViewController.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Cocoa + +class TCCProfileViewController: NSViewController { + + @objc dynamic var model = Model.shared + @objc dynamic var canEdit = true + + @IBOutlet weak var executablesTable: NSTableView! + @IBOutlet weak var executablesAC: NSArrayController! + + @IBOutlet weak var appleEventsTable: NSTableView! + @IBOutlet weak var appleEventsAC: NSArrayController! + + @IBOutlet weak var nameLabel: NSTextField! + @IBOutlet weak var iconView: NSImageView! + @IBOutlet weak var identifierLabel: NSTextField! + @IBOutlet weak var codeRequirementLabel: NSTextField! + + @IBOutlet weak var addressBookPopUp: NSPopUpButton! + @IBOutlet weak var photosPopUp: NSPopUpButton! + @IBOutlet weak var remindersPopUp: NSPopUpButton! + @IBOutlet weak var calendarPopUp: NSPopUpButton! + @IBOutlet weak var accessibilityPopUp: NSPopUpButton! + @IBOutlet weak var postEventsPopUp: NSPopUpButton! + @IBOutlet weak var adminFilesPopUp: NSPopUpButton! + @IBOutlet weak var allFilesPopUp: NSPopUpButton! + @IBOutlet weak var cameraPopUp: NSPopUpButton! + @IBOutlet weak var microphonePopUp: NSPopUpButton! + + @IBOutlet weak var addressBookPopUpAC: NSArrayController! + @IBOutlet weak var photosPopUpAC: NSArrayController! + @IBOutlet weak var remindersPopUpAC: NSArrayController! + @IBOutlet weak var calendarPopUpAC: NSArrayController! + @IBOutlet weak var accessibilityPopUpAC: NSArrayController! + @IBOutlet weak var postEventsPopUpAC: NSArrayController! + @IBOutlet weak var adminFilesPopUpAC: NSArrayController! + @IBOutlet weak var allFilesPopUpAC: NSArrayController! + @IBOutlet weak var cameraPopUpAC: NSArrayController! + @IBOutlet weak var microphonePopUpAC: NSArrayController! + + @IBOutlet weak var saveButton: NSButton! + @IBOutlet weak var uploadButton: NSButton! + @IBOutlet weak var addAppleEventButton: NSButton! + @IBOutlet weak var removeAppleEventButton: NSButton! + + @IBOutlet weak var recordButton: NSButton! + + @IBAction func recordPressed(_ sender: NSButton) { + canEdit = !canEdit + if canEdit { + recordButton.title = "Record" + } else { + recordButton.title = "Stop" + } + } + + @IBAction func addToProfile(_ sender: NSButton) { + promptForExecutables { + self.model.selectedExecutables.append($0) + } + } + + // Binding currently deletes at index + @IBAction func removeButtonPressed(_ sender: NSButton) { + } + + @IBAction func addToExecutable(_ sender: NSButton) { + promptForExecutables { + self.insetIntoAppleEvents($0) + } + } + + func promptForExecutables(_ block: @escaping (Executable) -> Void) { + let panel = NSOpenPanel() + panel.allowsMultipleSelection = true + panel.allowedFileTypes = [ kUTTypeBundle, kUTTypeExecutable ] as [String] + panel.directoryURL = URL(fileURLWithPath: "/Applications", isDirectory: true) + panel.begin { response in + if response == .OK { + panel.urls.forEach { + guard let executable = self.model.loadExecutable(url: $0) else { return } + block(executable) + } + } + } + } + + let pasteboardOptions: [NSPasteboard.ReadingOptionKey : Any] = [ + .urlReadingContentsConformToTypes: [ kUTTypeBundle, kUTTypeExecutable ] + ] + + override func viewDidLoad() { + super.viewDidLoad() + + // Setup policy pop up + addressBookPopUpAC.add(contentsOf: [ "-" , "Allow" , "Deny" ]) + photosPopUpAC.add(contentsOf: [ "-" , "Allow" , "Deny" ]) + remindersPopUpAC.add(contentsOf: [ "-" , "Allow" , "Deny" ]) + calendarPopUpAC.add(contentsOf: [ "-" , "Allow" , "Deny" ]) + accessibilityPopUpAC.add(contentsOf: [ "-" , "Allow" , "Deny" ]) + postEventsPopUpAC.add(contentsOf: [ "-" , "Allow" , "Deny" ]) + adminFilesPopUpAC.add(contentsOf: [ "-" , "Allow" , "Deny" ]) + allFilesPopUpAC.add(contentsOf: [ "-" , "Allow" , "Deny" ]) + cameraPopUpAC.add(contentsOf: [ "-" , "Deny" ]) + microphonePopUpAC.add(contentsOf: [ "-" , "Deny" ]) + + // Setup table views + executablesTable.registerForDraggedTypes([.fileURL]) + executablesTable.dataSource = self + appleEventsTable.registerForDraggedTypes([.fileURL]) + appleEventsTable.dataSource = self + + // Record button + } + + override func prepare(for segue: NSStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + guard let openVC = segue.destinationController as? OpenViewController else { return } + if let button = sender as? NSButton, button == addAppleEventButton { + Model.shared.current = executablesAC.selectedObjects.first as? Executable + openVC.completionBlock = { + $0.forEach { self.insetIntoAppleEvents($0) } + } + } + } + + func insetIntoAppleEvents(_ executable: Executable) { + guard let source = self.executablesAC.selectedObjects.first as? Executable else { return } + let rule = AppleEventRule() + rule.source = source + rule.destination = executable + guard self.appleEventsAC.canInsert else { return } + self.appleEventsAC.insert(rule, atArrangedObjectIndex: 0) + } + +} + +extension TCCProfileViewController : NSTableViewDataSource { + + func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation { + let accept = info.draggingPasteboard.canReadObject(forClasses: [NSURL.self], options: pasteboardOptions) + return accept ? .copy : NSDragOperation() + } + + func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool { + let pasteboard = info.draggingPasteboard + + guard let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: pasteboardOptions) as? [URL]? else { + return false + } + + guard let url = urls?.first else { return false } + + guard let newExecutable = model.loadExecutable(url: url) else { return false } + + if tableView == executablesTable { + guard executablesAC.canInsert else { return false } + executablesAC.insert(newExecutable, atArrangedObjectIndex: row) + } else { + self.insetIntoAppleEvents(newExecutable) + } + return true + } + +} + diff --git a/Source/View Controllers/UploadViewController.swift b/Source/View Controllers/UploadViewController.swift new file mode 100644 index 0000000..525f4fe --- /dev/null +++ b/Source/View Controllers/UploadViewController.swift @@ -0,0 +1,296 @@ +// +// UploadViewController.swift +// PPPC Utility +// +// MIT License +// +// Copyright (c) 2018 Jamf Software +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Cocoa +import CoreGraphics + +class UploadViewController: NSViewController { + + private static var uploadKVOContext = 0 + + @objc dynamic var networkOperationsTitle: String! = nil + @objc dynamic var mustSignForUpload: Bool = true { + didSet { + let containsNullRef = (identitiesPopUpAC.arrangedObjects as? [SigningIdentity])?.first?.reference == nil + if mustSignForUpload && containsNullRef { + identitiesPopUpAC.remove(atArrangedObjectIndex: 0) + } else if !mustSignForUpload && !containsNullRef { + let nullRef = SigningIdentity(name: "Profile signed by server", reference: nil) + identitiesPopUpAC.insert(nullRef, atArrangedObjectIndex: 0) + } + } + } + + @objc dynamic var credentialsAvailable = false + @objc dynamic var credentialsVerified = false + @objc dynamic var saveCredentials = true + @objc dynamic var readyForUpload = false + + @objc dynamic var username: String! + @objc dynamic var password: String! + @objc dynamic var payloadName: String! + @objc dynamic var payloadIdentifier: String! = UUID().uuidString + @objc dynamic var payloadDescription: String! + + @IBOutlet weak var defaultsController: NSUserDefaultsController! + + @IBOutlet weak var jamfProServerLabel: NSTextField! + @IBOutlet weak var usernameLabel: NSTextField! + @IBOutlet weak var passwordLabel: NSSecureTextField! + @IBOutlet weak var organizationLabel: NSTextField! + @IBOutlet weak var payloadNameLabel: NSTextField! + @IBOutlet weak var payloadIdentifierLabel: NSTextField! + @IBOutlet weak var payloadDescriptionLabel: NSTextField! + @IBOutlet weak var identitiesPopUp: NSPopUpButton! + @IBOutlet var identitiesPopUpAC: NSArrayController! + @IBOutlet weak var uploadButton: NSButton! + @IBOutlet weak var checkConnectionButton: NSButton! + + @IBOutlet weak var gridView: NSGridView! + + @IBAction func uploadPressed(_ sender: NSButton) { + print("Uploading profile: \(payloadName ?? "?")") + self.networkOperationsTitle = "Uploading \(payloadName ?? "profile")" + + let model = Model.shared + let profile = model.exportProfile(organization: organizationLabel.stringValue, + identifier: payloadIdentifierLabel.stringValue, + displayName: payloadNameLabel.stringValue, + payloadDescription: payloadDescriptionLabel.stringValue) + var identity: SecIdentity? + if mustSignForUpload, let signingIdentity = identitiesPopUpAC.selectedObjects.first as? SigningIdentity, signingIdentity.reference != nil { + print("Signing profile with \(signingIdentity.displayName)") + identity = signingIdentity.reference + } + + JamfProClient(jamfProServerLabel.stringValue, username, password).uploadProfile(profile, signingIdentity: identity) { (success) in + DispatchQueue.main.async { + self.handleUploadCompletion(success: success) + } + } + } + + @IBAction func checkConnectionPressed(_ sender: NSButton) { + print("Checking connection") + self.networkOperationsTitle = "Checking Jamf Pro server" + + let client = JamfProClient(jamfProServerLabel.stringValue, username, password) + + client.getJamfProVersion { (possibleVersion) in + if let version = possibleVersion { + print("Jamf Pro Server: \(version.major).\(version.minor).\(version.patch)") + let mustSign: Bool = + (version.major < 10 || + (version.major == 10 && + (version.minor < 7 || + (version.minor == 7 && version.patch == 0)))) + client.getOrganizationName { (statusCode, orgName) in + if statusCode == 401 { + print("Invalid username/password") + DispatchQueue.main.async { + self.handleCheckConnectionFailure(enforceSigning: mustSign) + } + } else if let name = orgName { + DispatchQueue.main.async { + self.handleCheckConnection(enforceSigning: mustSign, + organization: name) + } + } else { + print("Unable to read organization name") + DispatchQueue.main.async { + self.handleCheckConnectionFailure(enforceSigning: mustSign) + } + } + } + } else { + print("Jamf Pro server is unavailable") + DispatchQueue.main.async { + self.handleCheckConnectionFailure(enforceSigning: nil) + } + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + checkConnectionButton.isEnabled = false + organizationLabel.isEnabled = false + payloadNameLabel.isEnabled = false + payloadIdentifierLabel.isEnabled = false + payloadDescriptionLabel.isEnabled = false + + do { + let identities = try SecurityWrapper.loadSigningIdentities() + identitiesPopUpAC.add(contentsOf: identities) + } catch { + identitiesPopUpAC.add(contentsOf: []) + print("Error loading identities: \(error)") + } + + mustSignForUpload = UserDefaults.standard.bool(forKey: "enforceSigning") + + loadCredentials() + } + + override func viewWillAppear() { + super.viewWillAppear() + defaultsController.addObserver(self, forKeyPath: "values.jamfProServer", options: [.new], context: &UploadViewController.uploadKVOContext) + defaultsController.addObserver(self, forKeyPath: "values.organization", options: [.new], context: &UploadViewController.uploadKVOContext) + addObserver(self, forKeyPath: "username", options: [.new], context: &UploadViewController.uploadKVOContext) + addObserver(self, forKeyPath: "password", options: [.new], context: &UploadViewController.uploadKVOContext) + addObserver(self, forKeyPath: "payloadName", options: [.new], context: &UploadViewController.uploadKVOContext) + addObserver(self, forKeyPath: "payloadDescription", options: [.new], context: &UploadViewController.uploadKVOContext) + addObserver(self, forKeyPath: "payloadIdentifier", options: [.new], context: &UploadViewController.uploadKVOContext) + + if organizationLabel.stringValue.isEmpty { + organizationLabel.becomeFirstResponder() + } else if credentialsAvailable { + payloadNameLabel.becomeFirstResponder() + } else { + usernameLabel.becomeFirstResponder() + } + } + + override func viewWillDisappear() { + super.viewWillDisappear() + defaultsController.removeObserver(self, forKeyPath: "values.jamfProServer", context: &UploadViewController.uploadKVOContext) + defaultsController.removeObserver(self, forKeyPath: "values.organization", context: &UploadViewController.uploadKVOContext) + removeObserver(self, forKeyPath: "username", context: &UploadViewController.uploadKVOContext) + removeObserver(self, forKeyPath: "password", context: &UploadViewController.uploadKVOContext) + removeObserver(self, forKeyPath: "payloadName", context: &UploadViewController.uploadKVOContext) + removeObserver(self, forKeyPath: "payloadDescription", context: &UploadViewController.uploadKVOContext) + removeObserver(self, forKeyPath: "payloadIdentifier", context: &UploadViewController.uploadKVOContext) + + + // Save keychain + syncronizeCredentials() + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if context == &UploadViewController.uploadKVOContext { + updateCredentialsAvailable() + updateReadForUpload() + } else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + } + } + + func updateCredentialsAvailable() { + guard credentialsAvailable != ( + !jamfProServerLabel.stringValue.isEmpty + && (username != nil) + && !username.isEmpty + && (password != nil) + && !password.isEmpty) else { return } + credentialsAvailable = !credentialsAvailable + } + + func updateReadForUpload() { + guard readyForUpload != ( + credentialsVerified + && credentialsAvailable + && !organizationLabel.stringValue.isEmpty + && (payloadName != nil) + && !payloadName.isEmpty + && (payloadIdentifier != nil) + && !payloadIdentifier.isEmpty) else { return } + readyForUpload = !readyForUpload + } + + func handleCheckConnectionFailure(enforceSigning: Bool?) { + identitiesPopUp.isEnabled = enforceSigning ?? false + networkOperationsTitle = nil + credentialsVerified = false + updateReadForUpload() + passwordLabel.becomeFirstResponder() + } + + func handleCheckConnection(enforceSigning: Bool, organization: String) { + defaultsController.setValue(organization, forKeyPath: "values.organization") + UserDefaults.standard.set(enforceSigning, forKey: "enforceSigning") + networkOperationsTitle = nil + mustSignForUpload = enforceSigning + syncronizeCredentials() + credentialsVerified = true + payloadNameLabel.becomeFirstResponder() + updateReadForUpload() + } + + func handleUploadCompletion(success: Bool) { + guard !success else { + print("Uploaded successfully") + self.dismiss(nil) + return + } + + print("Failed to upload") + + networkOperationsTitle = nil + credentialsVerified = false + passwordLabel.becomeFirstResponder() + updateReadForUpload() + } + + func loadCredentials() { + if let server = UserDefaults.standard.string(forKey: "jamfProServer") { + do { + let possibleCredentials = try SecurityWrapper.loadCredentials(server: server) + if let credentials = possibleCredentials { + username = credentials.username + password = credentials.password + credentialsAvailable = true + credentialsVerified = true + return + } + } catch { + print("Error loading credentials: \(error)") + } + } + + username = nil + password = nil + credentialsAvailable = false + credentialsVerified = false + } + + + func syncronizeCredentials() { + if saveCredentials { + if credentialsAvailable { + do { + try SecurityWrapper.saveCredentials(username: username, + password: password, + server: jamfProServerLabel.stringValue) + } catch { + print("Failed to save credentials with error: \(error)") + } + } + } else { + try? SecurityWrapper.removeCredentials(server: jamfProServerLabel.stringValue, username: username) + } + } +}