Posted on 02-25-2015 08:59 AM
Hi all,
I just wanted to share a example project for anyone interested. This is a pure Swift application for OS X that queries and displays the number of computers enrolled in Casper. It demonstrates how to leverage the API in a Swift environment. I will update the repo with a posting example in the future. This can also be used to easily start an iOS project as well.
Hopefully this is helpful for someone.
https://github.com/jason-tratta/SwiftJSS
Posted on 02-25-2015 10:12 AM
Nice work! How are you liking Swift?
Posted on 02-25-2015 10:21 AM
I'm really digging it. I thought I would stick to Obj-C forever...but it's really winning me over.
Posted on 03-10-2019 08:52 AM
As this was posted in 2015 is there any additional steps to set this up. I have got the app to build in Xcode 10 but it seems to do nothing when entering the credentials. Does this still work if JAMF is on the cloud and what about 2FA.
thanks
Posted on 03-10-2019 08:28 PM
Still works awesome. I recently built a few apps that interacted with the API.
This would definitely work with Jamf in the cloud. I don't believe Jamf supports 2FA at all but does support kerberos and all that single sign on fun but the API doesn't use that, it uses your username/password.
https://github.com/boberito/swift-things - here's my GitHub. You can see the two little apps I've built and maybe how they're built will help you. You just use a URLSession to interact with the API. In fact Postman (an API app) will write you some Swift code if you want it to.
Posted on 03-12-2019 08:44 AM
Thanks boberito. I will check them out.
Posted on 03-12-2019 08:51 AM
Amazing work really good stuff.
Posted on 03-15-2019 03:30 PM
Hi boberito
Thanks again for the example apps you created. I have been playing with them trying to gain an understanding of getting what I need to work. I seem to have fallen at the first hurdle. I am just trying to convert what you did in the SMARTCARD UTILITY to GET data for mobile devices as this is what my current project is working towards. I have changed the URL to mobile devices and most of the settings to point to mobile devices and checked as you suggested with postman and it GETS the data however when i change the setting in your app just to see if I can get results I get an "UNKNOWN ERROR" then the app crashes. My thinking is it could be to do with the extension attributes.
After debugging yours compared to mine its does seem the data it gets in bytes in very low as if it's not getting the data.
Any help or pointers would be very appreciated.
Thanks
Adam
Posted on 03-17-2019 08:05 AM
@adamnewman Honestly I'd have to see your code. I have no idea. It probably has to do with extension attributes and such.
Posted on 03-17-2019 08:18 AM
If you could have a quick look. I just want to see if I can pull mobile devices in. I do think i'm going to have to start from the beginning and build my code from scratch. I'm trying to use yours to get a better understanding of how it works.
See code below with my edits.
import Cocoa
//login class
class jamfInfo {
var server: String?
var id: String?
var name: String?
var username: String = ""
var password: String = ""
}
struct userSearch: Decodable { let user: listOfUsers
struct listOfUsers: Decodable { let name: String let full_name: String let email: String let phone_number: String let links: listOfmobiledevices
struct listOfmobiledevices: Decodable { let mobiledevices: [mobiledevices]
struct mobiledevices: Decodable { let name: String let id: Int
}
}
}
}
struct extensionAttribute: Decodable { let mobiledevices: mobiledevicesEA
struct mobiledevicesEA: Decodable { let extension_attributes: [EA]
struct EA: Decodable { let id: Int let name: String let value: String }
}
}
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
@IBOutlet var userInfo: NSTextField! @IBOutlet var tableView: NSTableView! @IBOutlet var JamfPassword: NSSecureTextField! @IBOutlet var jamfUser: NSTextField! @IBOutlet var userNameField: NSTextFieldCell! @IBOutlet var toggleButton: NSButtonCell!
var listOfmobiledevices = [String]() var listOfmobiledevicesIDs = [Int]() var listOfEAs = [String]()
var prefs = jamfInfo()
override func prepare(for segue: NSStoryboardSegue, sender: Any?) { let second = segue.destinationController as! PrefController readPlist() prefs.username = jamfUser.stringValue prefs.password = JamfPassword.stringValue second.representedObject = prefs listOfmobiledevices.removeAll() tableView.reloadData()
}
override func viewDidLoad() { super.viewDidLoad()
readPlist() if prefs.server != nil { let keychainVar = try? keychainlogin(server: prefs.server!) if keychainVar != nil { prefs.username = keychainVar!.KCUsername prefs.password = keychainVar!.KCPassword jamfUser.stringValue = prefs.username JamfPassword.stringValue = prefs.password } }
toggleButton.isEnabled = false // Do any additional setup after loading the view. }
@IBAction func preferencesMenuItemSelected(_ sender: Any) { self.performSegue(withIdentifier: "prefSegue", sender: self) }
@IBAction func doSomething(_ sender: Any) { readPlist() getTableView() }
func numberOfRows(in tableView: NSTableView) -> Int { return listOfmobiledevices.count }
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard let vw = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as? NSTableCellView else { return nil }
if tableColumn?.title == "Computer List" { vw.textField?.stringValue = listOfmobiledevices[row]
} else { var eaOutput: Data? readPlist() eaOutput = getJamfData(url: "(prefs.server!)JSSResource/mobiledevices/id/(listOfmobiledevicesIDs[row])/subset/ExtensionAttributes").jamfDataOutput
let EAdecoder = JSONDecoder() let EADetails = try! EAdecoder.decode(extensionAttribute.self, from: eaOutput!)
for EAs in EADetails.mobiledevices.extension_attributes { if EAs.id == Int(prefs.id!) { vw.textField?.stringValue = EAs.value listOfEAs.insert(EAs.value, at: 0) break; } else { vw.textField?.stringValue = "" listOfEAs.insert("", at: 0) } }
} return vw }
func tableViewSelectionDidChange(_ notification: Notification) {
if tableView.selectedRow != -1 { toggleButton.isEnabled = true } else { toggleButton.isEnabled = false }
}
func getJamfData(url: String) -> (jamfDataOutput: Data, jamfResponse: Int) { let loginData = "(jamfUser.stringValue):(JamfPassword.stringValue)".data(using: String.Encoding.utf8) let base64LoginString = loginData!.base64EncodedString() let headers = ["Accept": "application/json", "Authorization": "Basic (String(describing: base64LoginString))"]
let request = NSMutableURLRequest(url: NSURL(string: url)! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) request.httpMethod = "GET" request.allHTTPHeaderFields = headers
var dataReturn: Data? var ResponseCode: HTTPURLResponse?
let dispatchGroup = DispatchGroup() dispatchGroup.enter()
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in if let APIdata = data { dataReturn = APIdata ResponseCode = response as? HTTPURLResponse
} dispatchGroup.leave() }) dataTask.resume() dispatchGroup.wait() if dataReturn == nil { dataReturn = "".data(using: String.Encoding.utf8)
} var ResponseStatusCode: Int if ResponseCode == nil { ResponseStatusCode = 1 } else { ResponseStatusCode = (ResponseCode?.statusCode)! } return(dataReturn!, ResponseStatusCode)
}
@IBAction func toggle(_ sender: Any) { readPlist() guard tableView.selectedRow != -1 else { return } var action: String = ""
if listOfEAs[tableView.selectedRow] != "Enabled" { action = "Enabled" } else { action = "Disabled" } var xmldata: String
let requestURL = "(prefs.server!)JSSResource/mobiledevices/id/(listOfmobiledevicesIDs[tableView.selectedRow])"
xmldata = "<?xml version="1.0" encoding="UTF-8" standalone="no"?><mobiledevices><extension_attributes><extension_attribute><id>" prefs.id! "</id><name>" prefs.name! "</name><type>String</type><value>" action "</value></extension_attribute></extension_attributes></mobiledevice>"
let loginData = "(jamfUser.stringValue):(JamfPassword.stringValue)".data(using: String.Encoding.utf8) let base64LoginString = loginData!.base64EncodedString() let postData = NSData(data: xmldata.data(using: String.Encoding.utf8)!) let headers = ["Content-Type": "text/xml", "Authorization": "Basic (String(describing: base64LoginString))"] let request = NSMutableURLRequest(url: NSURL(string: requestURL)! as URL,cachePolicy: .useProtocolCachePolicy,timeoutInterval: 10.0) request.httpMethod = "PUT" request.allHTTPHeaderFields = headers request.httpBody = postData as Data
let dispatchGroup = DispatchGroup() dispatchGroup.enter()
let session = URLSession.shared let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
dispatchGroup.leave() } )
dataTask.resume()
dispatchGroup.wait()
getTableView() toggleButton.isEnabled = true
}
func errorOccured(typeOfError: String){ let alert = NSAlert() alert.messageText = "Error" alert.informativeText = typeOfError alert.runModal() }
func getTableView(){ listOfmobiledevices.removeAll() tableView.reloadData()
let myData = getJamfData(url: "(prefs.server!)JSSResource/users/name/(userNameField.stringValue)")
let decoder = JSONDecoder() do { switch myData.jamfResponse { case 401: errorOccured(typeOfError: "Login Incorrect.") case 400: errorOccured(typeOfError: "Bad Request: You sent a request that this server could not understand.") case 404: errorOccured(typeOfError: "User not found.") case 1: errorOccured(typeOfError: "Could not connect to Server") case 200: let userdetails = try decoder.decode(userSearch.self, from: myData.jamfDataOutput)
let userInfoString = """ (userdetails.user.full_name) (userdetails.user.email) (userdetails.user.phone_number) """ let emailStringLength = userdetails.user.email.count + 1 let attributedString = NSMutableAttributedString(string: userInfoString) attributedString.addAttribute(.link, value: "mailto: (userdetails.user.email)", range: NSRange(location: userdetails.user.full_name.count, length: emailStringLength ))
userInfo.attributedStringValue = attributedString
for entries in userdetails.user.links.mobiledevices { listOfmobiledevicesIDs.insert(entries.id as Int, at: 0) listOfmobiledevices.insert(entries.name as String, at: 0) tableView.insertRows(at: IndexSet(integer: 0)) } default: errorOccured(typeOfError: "A connection error occured. Sorry") //NSApplication.shared.terminate(self) }
} catch { errorOccured(typeOfError: "Unknown error occured. Sorry") //NSApplication.shared.terminate(self) } }
func keychainlogin(server: String) throws -> (KCUsername: String, KCPassword: String) { let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, kSecAttrServer as String: server, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnAttributes as String: true, kSecReturnData as String: true]
var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) guard status != errSecItemNotFound else { throw KeychainError.noPassword } 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 keychainpassword = String(data: passwordData, encoding: String.Encoding.utf8), let keychainaccount = existingItem[kSecAttrAccount as String] as? String else { throw KeychainError.unexpectedPasswordData }
return(keychainaccount, keychainpassword) }
func readPlist(){
prefs.server = UserDefaults.standard.string(forKey: "jss_URL") ?? "" prefs.name = UserDefaults.standard.string(forKey: "EA_NAME") ?? "" prefs.id = UserDefaults.standard.string(forKey: "EA_ID") ?? ""
}
}
Posted on 03-17-2019 02:51 PM
You may wanna edit your post so it's posted more like code or quoted. The >_ will let you post scripts/code
Posted on 03-17-2019 03:31 PM
import Cocoa
//login class
class jamfInfo {
var server: String?
var id: String?
var name: String?
var username: String = ""
var password: String = ""
}
struct userSearch: Decodable {
let user: listOfUsers
struct listOfUsers: Decodable {
let name: String
let full_name: String
let email: String
let phone_number: String
let links: listOfmobiledevices
struct listOfmobiledevices: Decodable {
let mobiledevices: [mobiledevices]
struct mobiledevices: Decodable {
let name: String
let id: Int
}
}
}
}
struct extensionAttribute: Decodable {
let mobiledevices: mobiledevicesEA
struct mobiledevicesEA: Decodable {
let extension_attributes: [EA]
struct EA: Decodable {
let id: Int
let name: String
let value: String
}
}
}
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
@IBOutlet var userInfo: NSTextField!
@IBOutlet var tableView: NSTableView!
@IBOutlet var JamfPassword: NSSecureTextField!
@IBOutlet var jamfUser: NSTextField!
@IBOutlet var userNameField: NSTextFieldCell!
@IBOutlet var toggleButton: NSButtonCell!
var listOfmobiledevices = [String]()
var listOfmobiledevicesIDs = [Int]()
var listOfEAs = [String]()
var prefs = jamfInfo()
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
let second = segue.destinationController as! PrefController
readPlist()
prefs.username = jamfUser.stringValue
prefs.password = JamfPassword.stringValue
second.representedObject = prefs
listOfmobiledevices.removeAll()
tableView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
readPlist()
if prefs.server != nil {
let keychainVar = try? keychainlogin(server: prefs.server!)
if keychainVar != nil {
prefs.username = keychainVar!.KCUsername
prefs.password = keychainVar!.KCPassword
jamfUser.stringValue = prefs.username
JamfPassword.stringValue = prefs.password
}
}
toggleButton.isEnabled = false
// Do any additional setup after loading the view.
}
@IBAction func preferencesMenuItemSelected(_ sender: Any) {
self.performSegue(withIdentifier: "prefSegue", sender: self)
}
@IBAction func doSomething(_ sender: Any) {
readPlist()
getTableView()
}
func numberOfRows(in tableView: NSTableView) -> Int {
return listOfmobiledevices.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard let vw = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as? NSTableCellView else { return nil }
if tableColumn?.title == "Computer List" {
vw.textField?.stringValue = listOfmobiledevices[row]
} else {
var eaOutput: Data?
readPlist()
eaOutput = getJamfData(url: "(prefs.server!)JSSResource/mobiledevices/id/(listOfmobiledevicesIDs[row])/subset/ExtensionAttributes").jamfDataOutput
let EAdecoder = JSONDecoder()
let EADetails = try! EAdecoder.decode(extensionAttribute.self, from: eaOutput!)
for EAs in EADetails.mobiledevices.extension_attributes {
if EAs.id == Int(prefs.id!) {
vw.textField?.stringValue = EAs.value
listOfEAs.insert(EAs.value, at: 0)
break;
} else {
vw.textField?.stringValue = ""
listOfEAs.insert("", at: 0)
}
}
}
return vw
}
func tableViewSelectionDidChange(_ notification: Notification) {
if tableView.selectedRow != -1 {
toggleButton.isEnabled = true
} else {
toggleButton.isEnabled = false
}
}
func getJamfData(url: String) -> (jamfDataOutput: Data, jamfResponse: Int) {
let loginData = "(jamfUser.stringValue):(JamfPassword.stringValue)".data(using: String.Encoding.utf8)
let base64LoginString = loginData!.base64EncodedString()
let headers = ["Accept": "application/json",
"Authorization": "Basic (String(describing: base64LoginString))"]
let request = NSMutableURLRequest(url: NSURL(string: url)! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
var dataReturn: Data?
var ResponseCode: HTTPURLResponse?
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if let APIdata = data {
dataReturn = APIdata
ResponseCode = response as? HTTPURLResponse
}
dispatchGroup.leave()
})
dataTask.resume()
dispatchGroup.wait()
if dataReturn == nil {
dataReturn = "".data(using: String.Encoding.utf8)
}
var ResponseStatusCode: Int
if ResponseCode == nil {
ResponseStatusCode = 1
} else {
ResponseStatusCode = (ResponseCode?.statusCode)!
}
return(dataReturn!, ResponseStatusCode)
}
@IBAction func toggle(_ sender: Any) {
readPlist()
guard tableView.selectedRow != -1 else { return }
var action: String = ""
if listOfEAs[tableView.selectedRow] != "Enabled" {
action = "Enabled"
} else {
action = "Disabled"
}
var xmldata: String
let requestURL = "(prefs.server!)JSSResource/mobiledevices/id/(listOfmobiledevicesIDs[tableView.selectedRow])"
xmldata = "<?xml version="1.0" encoding="UTF-8" standalone="no"?><mobiledevices><extension_attributes><extension_attribute><id>" + prefs.id! + "</id><name>" + prefs.name! + "</name><type>String</type><value>" + action + "</value></extension_attribute></extension_attributes></mobiledevice>"
let loginData = "(jamfUser.stringValue):(JamfPassword.stringValue)".data(using: String.Encoding.utf8)
let base64LoginString = loginData!.base64EncodedString()
let postData = NSData(data: xmldata.data(using: String.Encoding.utf8)!)
let headers = ["Content-Type": "text/xml", "Authorization": "Basic (String(describing: base64LoginString))"]
let request = NSMutableURLRequest(url: NSURL(string: requestURL)! as URL,cachePolicy: .useProtocolCachePolicy,timeoutInterval: 10.0)
request.httpMethod = "PUT"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
dispatchGroup.leave()
}
)
dataTask.resume()
dispatchGroup.wait()
getTableView()
toggleButton.isEnabled = true
}
func errorOccured(typeOfError: String){
let alert = NSAlert()
alert.messageText = "Error"
alert.informativeText = typeOfError
alert.runModal()
}
func getTableView(){
listOfmobiledevices.removeAll()
tableView.reloadData()
let myData = getJamfData(url: "(prefs.server!)JSSResource/users/name/(userNameField.stringValue)")
let decoder = JSONDecoder()
do {
switch myData.jamfResponse {
case 401:
errorOccured(typeOfError: "Login Incorrect.")
case 400:
errorOccured(typeOfError: "Bad Request: You sent a request that this server could not understand.")
case 404:
errorOccured(typeOfError: "User not found.")
case 1:
errorOccured(typeOfError: "Could not connect to Server")
case 200:
let userdetails = try decoder.decode(userSearch.self, from: myData.jamfDataOutput)
let userInfoString = """
(userdetails.user.full_name)
(userdetails.user.email)
(userdetails.user.phone_number)
"""
let emailStringLength = userdetails.user.email.count + 1
let attributedString = NSMutableAttributedString(string: userInfoString)
attributedString.addAttribute(.link, value: "mailto: (userdetails.user.email)", range: NSRange(location: userdetails.user.full_name.count, length: emailStringLength ))
userInfo.attributedStringValue = attributedString
for entries in userdetails.user.links.mobiledevices {
listOfmobiledevicesIDs.insert(entries.id as Int, at: 0)
listOfmobiledevices.insert(entries.name as String, at: 0)
tableView.insertRows(at: IndexSet(integer: 0))
}
default:
errorOccured(typeOfError: "A connection error occured. Sorry")
//NSApplication.shared.terminate(self)
}
} catch {
errorOccured(typeOfError: "Unknown error occured. Sorry")
//NSApplication.shared.terminate(self)
}
}
func keychainlogin(server: String) throws -> (KCUsername: String, KCPassword: String) {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecReturnData as String: true]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else { throw KeychainError.noPassword }
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 keychainpassword = String(data: passwordData, encoding: String.Encoding.utf8),
let keychainaccount = existingItem[kSecAttrAccount as String] as? String
else {
throw KeychainError.unexpectedPasswordData
}
return(keychainaccount, keychainpassword)
}
func readPlist(){
prefs.server = UserDefaults.standard.string(forKey: "jss_URL") ?? ""
prefs.name = UserDefaults.standard.string(forKey: "EA_NAME") ?? ""
prefs.id = UserDefaults.standard.string(forKey: "EA_ID") ?? ""
}
}
Posted on 03-17-2019 06:35 PM
Without running it against your Jamf server and everything it's hard to tell. But it sounds like you're potentially receiving some error from the request. So that's why you see data get returned, just a very small amount.
Posted on 04-17-2020 05:36 PM
Posted on 04-01-2021 10:25 AM
@adamnewman I was wondering if you completed your project and/or if it's available on Github anywhere? I'm grappling with something similar and it looks like your project would answer a few questions.
Posted on 06-16-2021 09:36 PM
Any chances someone could update this with working code? Those git links are dead and the ones I've been able to find are written in old Swift 2 or 3 code which no longer works. I can use PHP to access the API no worries but when using Swift, I just keep getting
CredStore - performQuery - Error copying matching creds. Error=-25300, query={ class = inet; "m_Limit" = "m_LimitAll"; ptcl = htps; "r_Attributes" = 1; sdmn = "Restful JSS Access -- Please supply your credentials"; srvr = "<URLREMOVED>"; sync = syna; }
even though I'm sending the Basic Auth in the HTTP headers.