Swift API Example

jtratta
New Contributor III

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

15 REPLIES 15

BrysonTyrrell
Contributor II

Nice work! How are you liking Swift?

jtratta
New Contributor III

I'm really digging it. I thought I would stick to Obj-C forever...but it's really winning me over.

adamnewman
New Contributor II

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

boberito
Valued Contributor

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.

adamnewman
New Contributor II

Thanks boberito. I will check them out.

adamnewman
New Contributor II

Amazing work really good stuff.

adamnewman
New Contributor II

@boberito

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

boberito
Valued Contributor

@adamnewman Honestly I'd have to see your code. I have no idea. It probably has to do with extension attributes and such.

adamnewman
New Contributor II

@boberito

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") ?? ""

}
}

boberito
Valued Contributor

You may wanna edit your post so it's posted more like code or quoted. The >_ will let you post scripts/code

adamnewman
New Contributor II
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") ?? ""

    }
}

boberito
Valued Contributor

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.

amosdeane
New Contributor III

@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.

eaititig
New Contributor III

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.