Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions The MUT.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions The MUT.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions The MUT/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,5 +181,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}
}

func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

48 changes: 24 additions & 24 deletions The MUT/Base.lproj/Main.storyboard

Large diffs are not rendered by default.

23 changes: 11 additions & 12 deletions The MUT/KeychainHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import Foundation

class KeyChainHelper {

class func save(username: String, password: String, server: String) throws {
let passData = password.data(using: String.Encoding.utf8)!
class func save(clientId: String, clientSecret: String, server: String) throws {
let secretData = clientSecret.data(using: String.Encoding.utf8)!

// When deleting old credentials, only care if it's another MUT password.
let deleteQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
Expand All @@ -20,12 +20,12 @@ class KeyChainHelper {

// When saving, care about everything.
let saveQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrAccount as String: clientId,
kSecAttrServer as String: server,
kSecAttrComment as String: "Server: \(server)",
kSecAttrLabel as String: KeyVars.label,
kSecAttrApplicationTag as String: KeyVars.tag,
kSecValueData as String: passData]
kSecValueData as String: secretData]

// Delete old credentials before re-saving new, valid credentials.
SecItemDelete(deleteQuery as CFDictionary)
Expand All @@ -50,16 +50,16 @@ class KeyChainHelper {
guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) }

guard let existingItem = item as? [String : Any],
let passwordData = existingItem[kSecValueData as String] as? Data,
let password = String(data: passwordData, encoding: String.Encoding.utf8),
let username = existingItem[kSecAttrAccount as String] as? String,
let secretData = existingItem[kSecValueData as String] as? Data,
let clientSecret = String(data: secretData, encoding: String.Encoding.utf8),
let clientId = existingItem[kSecAttrAccount as String] as? String,
let server = existingItem[kSecAttrServer as String] as? String
else {
throw KeychainError.unexpectedPasswordData
}
Credentials.server = server
Credentials.password = password
Credentials.username = username
Credentials.clientSecret = clientSecret
Credentials.clientId = clientId
}

class func delete() throws {
Expand Down Expand Up @@ -89,10 +89,9 @@ public struct KeyVars {
}

public struct Credentials {
static var username: String?
static var password: String?
static var clientId: String?
static var clientSecret: String?
static var server: String?
static var base64Encoded: String?
}

public struct Token {
Expand Down
8 changes: 0 additions & 8 deletions The MUT/dataPreparation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,6 @@ public class dataPreparation {
// Functions to encode/decode data can be found here
// ******************************************

public func base64Credentials(user: String, password: String) -> String {
// Concatenate the credentials and base64 encode the resulting string
let concatCredentials = "\(user):\(password)"
let utf8Credentials = concatCredentials.data(using: String.Encoding.utf8)
let base64Credentials = utf8Credentials?.base64EncodedString() ?? "nil"
return base64Credentials
}

public func expectedColumns(endpoint: String) -> Int {
switch endpoint {
case "users":
Expand Down
71 changes: 31 additions & 40 deletions The MUT/loginWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ class loginWindow: NSViewController {

// Declare outlets for use in the interface
@IBOutlet weak var txtURLOutlet: NSTextField!
@IBOutlet weak var txtUserOutlet: NSTextField!
@IBOutlet weak var txtPassOutlet: NSSecureTextField!
@IBOutlet weak var txtClientIdOutlet: NSTextField!
@IBOutlet weak var txtClientSecretOutlet: NSSecureTextField!

@IBOutlet weak var spinProgress: NSProgressIndicator!

Expand Down Expand Up @@ -47,8 +47,8 @@ class loginWindow: NSViewController {
do {
try KeyChainHelper.load()
self.txtURLOutlet.stringValue = Credentials.server!
self.txtUserOutlet.stringValue = Credentials.username!
self.txtPassOutlet.stringValue = Credentials.password!
self.txtClientIdOutlet.stringValue = Credentials.clientId!
self.txtClientSecretOutlet.stringValue = Credentials.clientSecret!
if loginDefaults.bool(forKey: "AutoLogin") {
self.logMan.writeLog(level: .info, logString: "Found credentials stored in KeyChain. Attempting login.")
keyChainLogin = true
Expand Down Expand Up @@ -85,8 +85,8 @@ class loginWindow: NSViewController {
self.txtURLOutlet.stringValue = loginDefaults.string(forKey: "InstanceURL")!
}

if loginDefaults.string(forKey: "UserName") != nil {
self.txtUserOutlet.stringValue = loginDefaults.string(forKey: "UserName")!
if loginDefaults.string(forKey: "ClientId") != nil {
self.txtClientIdOutlet.stringValue = loginDefaults.string(forKey: "ClientId")!
}
}

Expand All @@ -100,34 +100,32 @@ class loginWindow: NSViewController {

// Clean up whitespace at the beginning and end of the fields, in case of faulty copy/paste
txtURLOutlet.stringValue = txtURLOutlet.stringValue.trimmingCharacters(in: CharacterSet.whitespaces)
txtUserOutlet.stringValue = txtUserOutlet.stringValue.trimmingCharacters(in: CharacterSet.whitespaces)
txtClientIdOutlet.stringValue = txtClientIdOutlet.stringValue.trimmingCharacters(in: CharacterSet.whitespaces)

// Warn the user if they have failed to enter an instancename AND prem URL
if txtURLOutlet.stringValue == "" {
_ = popPrompt().noServer()
}

// Warn the user if they have failed to enter a username
if txtUserOutlet.stringValue == "" {
_ = popPrompt().noUser()
// Warn the user if they have failed to enter a client ID
if txtClientIdOutlet.stringValue == "" {
_ = popPrompt().noClientId()
}

// Warn the user if they have failed to enter a password
if txtPassOutlet.stringValue == "" {
_ = popPrompt().noPass()
// Warn the user if they have failed to enter a client secret
if txtClientSecretOutlet.stringValue == "" {
_ = popPrompt().noClientSecret()
}

// Store the credentials information for later use
Credentials.username = txtUserOutlet.stringValue
Credentials.password = txtPassOutlet.stringValue
Credentials.clientId = txtClientIdOutlet.stringValue
Credentials.clientSecret = txtClientSecretOutlet.stringValue
Credentials.server = txtURLOutlet.stringValue
Credentials.base64Encoded = self.dataPrep.base64Credentials(user: self.txtUserOutlet.stringValue,
password: self.txtPassOutlet.stringValue)

// Move forward with verification
if txtURLOutlet.stringValue != ""
&& txtPassOutlet.stringValue != ""
&& txtUserOutlet.stringValue != "" {
&& txtClientSecretOutlet.stringValue != ""
&& txtClientIdOutlet.stringValue != "" {

// Change the UI to a running state
guiRunning()
Expand All @@ -143,36 +141,29 @@ class loginWindow: NSViewController {
self.guiReset()
} else {
// No error found leads you here:
if String(decoding: Token.data!, as: UTF8.self).contains("token") {
// Good credentials here, as told by there being a token
if String(decoding: Token.data!, as: UTF8.self).contains("access_token") {
// Good credentials here, as told by there being an access_token
do {
// Parse the JSON to return token and Expiry
let newJson = try JSON(data: Token.data!)
Token.value = newJson["token"].stringValue
Token.value = newJson["access_token"].stringValue

// Get the expiry and attempt to convert to epoch
let expireString = newJson["expires"].stringValue
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

// If we can convert successfully, store it to the Token object. Otherwise throw an error.
if let date = dateFormatter.date(from: expireString) {
Token.expiration = Int(date.timeIntervalSince1970 * 1000)
} else {
self.logMan.writeLog(level: .error, logString: "Failed to convert token expiry to epoch. Received \(expireString).")
}
// OAuth returns expires_in as seconds from now
let expiresIn = newJson["expires_in"].intValue
let currentTime = Int(Date().timeIntervalSince1970 * 1000)
Token.expiration = currentTime + (expiresIn * 1000)

self.dismiss(self)
} catch let error as NSError {
self.logMan.writeLog(level: .error, logString: "Failed to load: \(error.localizedDescription)")
}

// Store the username if we should
if self.loginDefaults.bool(forKey: "StoreUsername"){
self.loginDefaults.set(self.txtUserOutlet.stringValue, forKey: "UserName")
// Store the client ID if we should
if self.loginDefaults.bool(forKey: "StoreClientId"){
self.loginDefaults.set(self.txtClientIdOutlet.stringValue, forKey: "ClientId")
self.loginDefaults.synchronize()
} else {
self.loginDefaults.removeObject(forKey: "UserName")
self.loginDefaults.removeObject(forKey: "ClientId")
}

// Store the URL if we should
Expand All @@ -183,15 +174,15 @@ class loginWindow: NSViewController {
self.loginDefaults.removeObject(forKey: "InstanceURL")
}

// Store username if button pressed
// Store credentials if button pressed
if self.loginDefaults.bool(forKey: "Remember") {

// Attempt to save the information in keychain
self.logMan.writeLog(level: .info, logString: "Remember Me checkbox checked. Storing credentials in KeyChain for later use.")
DispatchQueue.global(qos: .background).async {
do {
try KeyChainHelper.save(username: Credentials.username!,
password: Credentials.password!,
try KeyChainHelper.save(clientId: Credentials.clientId!,
clientSecret: Credentials.clientSecret!,
server: Credentials.server!)
} catch {
self.logMan.writeLog(level: .error, logString: "Error writing credentials to keychain. \(error)")
Expand Down
20 changes: 10 additions & 10 deletions The MUT/popPrompt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public class popPrompt {
public func invalidCredentials() -> Bool {
let myPopup: NSAlert = NSAlert()
myPopup.messageText = "Invalid Credentials"
myPopup.informativeText = "The credentials you entered do not seem to have sufficient permissions. This could be due to an incorrect user/password, or possibly from insufficient permissions.\n\nMUT tests this against the user's ability to generate a token for the new JPAPI/UAPI. This token is now required for some tasks that MUT performs."
myPopup.informativeText = "The API Client credentials you entered do not seem to have sufficient permissions. This could be due to an incorrect Client ID or Client Secret, or possibly from insufficient API role privileges.\n\nMUT tests this against the API Client's ability to generate an OAuth token for the Jamf Pro API. This token is required for all tasks that MUT performs.\n\nPlease verify your Client ID and Client Secret, and ensure the API Client has appropriate permissions in Jamf Pro."
myPopup.alertStyle = NSAlert.Style.warning
myPopup.addButton(withTitle: "OK")
return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn
Expand All @@ -95,7 +95,7 @@ public class popPrompt {
public func invalidKeychain() -> Bool {
let myPopup: NSAlert = NSAlert()
myPopup.messageText = "Invalid Keychain Info"
myPopup.informativeText = "The credentials stored in your keychain do not seem to have sufficient permissions. This could be due to an incorrect user/password, or possibly from insufficient permissions.\n\nMUT tests this against the user's ability to generate a token for the new JPAPI/UAPI. This token is now required for some tasks that MUT performs.\n\nMUT will now remove the stored keychain info."
myPopup.informativeText = "The API Client credentials stored in your keychain do not seem to have sufficient permissions. This could be due to an incorrect Client ID or Client Secret, or possibly from insufficient API role privileges.\n\nMUT tests this against the API Client's ability to generate an OAuth token for the Jamf Pro API. This token is required for all tasks that MUT performs.\n\nMUT will now remove the stored keychain info. You will need to re-enter valid API Client credentials."
myPopup.alertStyle = NSAlert.Style.warning
myPopup.addButton(withTitle: "OK")
return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn
Expand All @@ -110,19 +110,19 @@ public class popPrompt {
return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn
}

public func noUser() -> Bool {
public func noClientId() -> Bool {
let myPopup: NSAlert = NSAlert()
myPopup.messageText = "No Username Found"
myPopup.informativeText = "It appears that you have not entered a username for MUT to use while accessing Jamf Pro. Please enter your username and password, and try again."
myPopup.messageText = "No Client ID Found"
myPopup.informativeText = "It appears that you have not entered a Client ID for MUT to use while accessing Jamf Pro. Please enter your API Client credentials and try again.\n\nYou can create API Clients in Jamf Pro under Settings > System > API Roles and Clients."
myPopup.alertStyle = NSAlert.Style.warning
myPopup.addButton(withTitle: "OK")
return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn
}

public func noPass() -> Bool {
public func noClientSecret() -> Bool {
let myPopup: NSAlert = NSAlert()
myPopup.messageText = "No Password Found"
myPopup.informativeText = "It appears that you have not entered a password for MUT to use while accessing Jamf Pro. Please enter your username and password, and try again."
myPopup.messageText = "No Client Secret Found"
myPopup.informativeText = "It appears that you have not entered a Client Secret for MUT to use while accessing Jamf Pro. Please enter your API Client credentials and try again.\n\nYou can create API Clients in Jamf Pro under Settings > System > API Roles and Clients."
myPopup.alertStyle = NSAlert.Style.warning
myPopup.addButton(withTitle: "OK")
return myPopup.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn
Expand All @@ -140,7 +140,7 @@ public class popPrompt {
public func clearKeychain() -> Bool {
let myPopup: NSAlert = NSAlert()
myPopup.messageText = "Clear Keychain?"
myPopup.informativeText = "This will remove your MUT credentials from keychain. You will need to re-enter your credentials when you next log in."
myPopup.informativeText = "This will remove your MUT API Client credentials from keychain. You will need to re-enter your credentials when you next log in."
myPopup.alertStyle = NSAlert.Style.warning
myPopup.addButton(withTitle: "Make it so")
myPopup.addButton(withTitle: "Cancel")
Expand All @@ -150,7 +150,7 @@ public class popPrompt {
public func hardReset() -> Bool {
let myPopup: NSAlert = NSAlert()
myPopup.messageText = "Perform Hard Reset?"
myPopup.informativeText = "If you are experiencing odd behavior with MUT it may help to perform a hard reset.\n\n This will remove all stored settings, including MUT crednetials stored in keychain, and quit the application immediately."
myPopup.informativeText = "If you are experiencing odd behavior with MUT it may help to perform a hard reset.\n\n This will remove all stored settings, including MUT API Client credentials stored in keychain, and quit the application immediately."
myPopup.alertStyle = NSAlert.Style.warning
myPopup.addButton(withTitle: "Make it so")
myPopup.addButton(withTitle: "Cancel")
Expand Down
Loading