Unmanaging macOS devices through API

rcoleman
New Contributor III

Hi folks, struggling to find an answer for this.

We have a number of machines unable to run Big Sur, so I wish to deploy a script on all of these devices that remove these devices from support. I've achieved similar in the past where I can actually remove all the local jamf components, licensed apps etc.. and also delete the JSS record for the specific device using an API call. However, looking forward, I'd like to keep the JSS record available as they may contain encryption keys that we want to keep a hold of.

Basically, I'm looking for a way to "Unmanage" these devices by unticking the "Allow Jamf Pro to perform management tasks" checkbox for each machine through the API, as I believe this will free up licenses:

Screenshot 2023-01-30 at 15.39.47.png

I've had a look online, and found some instances that I believe provide a resolution but I can't get anything to de-select this checkbox. For example :

https://community.jamf.com/t5/jamf-pro/script-to-remove-management-through-api/m-p/233611

https://community.jamf.com/t5/jamf-pro/help-sending-post-command-to-jss-api/td-p/192968

But the above are older posts and don't seem to be using tokens for authentication, and I just don't seem to be able to get the proper syntax when attempting a POST or PUT.

I can obtain the bearer tokens fine (using encrypted JSS variables with a dedicated API account) and send other GET API calls with no problems, so don't believe this to be an authorisation issue. The account I'm using for testing purposes has full access to perform all API calls.

The script I'm attempting to create, grabs the serial from the local device, uses the serial to grab the specific computer ID in the JSS using an API call (this also works fine), and then uses the ID to attempt to unmanage the device (this below is just a stand alone script for testing purposes):

 

#!/bin/bash

echo "Enter JSS username:"
read USERNAME
echo "Enter JSS password:"
read -s PASSWORD

TOKEN_EXPIRATION_EPOCH="0"

function getBearerToken() {
    RESPONSE=$(curl -s -u "$USERNAME":"$PASSWORD" "https://<our_server>.jamfcloud.com/api/v1/auth/token" -X POST)
    BEARER_TOKEN=$(echo "$RESPONSE" | plutil -extract token raw -)
	TOKEN_EXPIRATION=$(echo "$RESPONSE" | plutil -extract expires raw - | awk -F . '{print $1}')
	TOKEN_EXPIRATION_EPOCH=$(date -j -f "%Y-%m-%dT%T" "$TOKEN_EXPIRATION" +"%s")
}

function checkTokenExpiration() {
    NOW_EPOCH_UTC=$(date -j -f "%Y-%m-%dT%T" "$(date -u +"%Y-%m-%dT%T")" +"%s")
    if [[ TOKEN_EXPIRATION_EPOCH -gt NOW_EPOCH_UTC ]]
    then
        echo "Token valid until the following epoch time: " "$TOKEN_EXPIRATION_EPOCH"
    else
        echo "No valid token available, getting new token"
        getBearerToken
    fi
}

function invalidateToken() {
	RESPONSE_CODE=$(curl -w "%{http_code}" -H "Authorization: Bearer ${BEARER_TOKEN}" "https://<our_server>.jamfcloud.com/api/v1/auth/invalidate-token" -X POST -s -o /dev/null)
	if [[ ${RESPONSE_CODE} == 204 ]]
	then
		echo "Token successfully invalidated"
		BEARER_TOKEN=""
		TOKEN_EXPIRATION_EPOCH="0"
	elif [[ ${RESPONSE_CODE} == 401 ]]
	then
		echo "Token already invalid"
	else
		echo "An unknown error occurred invalidating the token"
	fi
}

echo "Getting API token..."
checkTokenExpiration

# Get serial number
SERIAL=$(system_profiler SPHardwareDataType | awk '/Serial/ {print $4}')
/bin/echo "Serial number is $SERIAL"

# Get JAMF ID of device
JAMF_ID=$(curl -X GET "https://<our_server>.jamfcloud.com/JSSResource/computers/serialnumber/$SERIAL" -H "accept: application/xml" -H "Authorization: Bearer $BEARER_TOKEN" | xmllint --xpath '/computer/general/id/text()' -)
/bin/echo "JAMF ID for $SERIAL is $JAMF_ID"

# Unmanage the device
/bin/echo "Attempting to remove device from Jamf management..."
curl -X POST "https://<our_server>.jamfcloud.com/JSSResource/computercommands/command/UnmanageDevice/id/$JAMF_ID" -H "accept: application/xml" -H "Authorization: Bearer $BEARER_TOKEN"

# Bin the token
/bin/echo "Invalidating API token..."
invalidateToken

/bin/echo "Done."
exit 0;

 

Strangely, when I run the above, there does appear to be an "Unenroll" command sent to the JSS:

Screenshot 2023-01-30 at 16.05.54.png

However, nothing seems to be changing.

Apologies if I'm missing something which is fairly straight forward, but I'm just going round in circles.

Anyone any ideas?

Many thanks

 

47 REPLIES 47

rcoleman
New Contributor III

Ok, think I may be looking at this wrong. I've just noticed that on my test device, all profiles have now gone, and that there is no option in the JSS Record for the device for issuing Management commands, so it looks like it may actually be "unmanaging" the device anyway. 

Does anyone know of anyway to use the API to de-select the ""Allow Jamf Pro to perform management tasks" checkbox anyway? 

rcoleman
New Contributor III

I feel like I'm narrowing it down. Looking here: https://developer.jamf.com/jamf-pro/reference/updatecomputerbyid

I've tried the following:

curl --request PUT --url "https://<our_server>.jamfcloud.com/JSSResource/computers/id/$JAMF_ID" -H "Content-Type: application/xml" -H "Accept: application/xml" -H "Authorization: Bearer $BEARER_TOKEN" '<computer><general><remote_management><managed>false</managed></remote_management></general></computer>'

But now getting an error:

curl: (6) Could not resolve host: <computer><general><remote_management><managed>false<

Any ideas what I'm missing?

rcoleman
New Contributor III

Ok figured it out. The exact command required is:

curl --request PUT --url "https://<our_server>.jamfcloud.com/JSSResource/computers/id/$JAMF_ID" -H "Content-Type: application/xml" -H "Accept: application/xml" -H "Authorization: Bearer $BEARER_TOKEN" -d '<computer><general><remote_management><managed>false</managed></remote_management></general></computer>'

 

Mikael_lofgren
New Contributor II

@rcoleman I trying to get the Unmanaged Command to work, do you know what minimum permissions that is needed? I have Jamf Pro Server Actions -> Send Computer Unmanage Command and Computer Read
Thanks!

@Mikael_lofgren I actually just ran into this issue. We use a dedicated API account with encrypted details for performing API operations and on my last test with this it complained about permissions. During my testing, I used my own account which has full permissions and it worked fine. Unfortunately I don't have time just now to investigate this further however on first look I believe for the account being used to perform API operations you'll need to make sure "Send Computer Unmanage Command" is selected. If you manage to try this let me know if it works:

Screenshot 2023-02-01 at 16.00.40.png

EDIT - Sorry, I've just realised that you've mentioned this is already selected! In that case I'm not sure what will be required. We also use the account for enabling remote management using MDM command and that works fine. When I get time to investigate further and if I come up with a solution then I'll make sure to post.

@Mikael_lofgren Just to say this worked for myself and I only have the same permissions you listed above, so apologies but I'm not sure what's going wrong at your side :(

Mikael_lofgren
New Contributor II

@rcoleman My command seems to work when using full permissions, I have opened a support case to get this sorted, can post any findings here when done. Thanks!

rcoleman
New Contributor III

@Mikael_lofgren That would be great, many thanks. It's possible I made a change to the permissions a long time ago for our API account but just can't remember, so it certainly would be useful to know exactly what permissions are required.

Mikael_lofgren
New Contributor II

Ahh think I missed, create in Computer objects, but this is the minimum to work, response code from API when working is 201.
# Jamf Pro Server Objects > Computers Create, Read and Update
# Jamf Pro Server Actions > Send Computer Unmanage Command

MLBZ521
Contributor III

Sending the MDM Command to "Unmanage" a device requires the device to be active to receive it -- in other words, the device is not marked as "unmanaged" until it receives the command and responds back to the Jamf Pro Server.

 

The other method you used, sending the remote_management > managed > false payload does not rely on the device being active, however, it does not remove anything from the device (e.g. the Jamf Management Framework, aka the jamf binary, MDM Profile, etc.); in this scenario the device does not know it has been unmanaged and will continue to attempt to check-in, as well as apply any and all configurations that it has "cached" on the device.

 

As noted, to perform this operation, the account will need Update privileges to Computers (at minimum), but it's not uncommon that additional privileges, that seem excessive given the task, are required (such as Create and Read) in Jamf's implementation of numerous API endpoints.  There's even been occurrences where permissions for completely separate objects are required to perform operations on another object.

MatG
Contributor III

@rcoleman 
Just looking at this myself but still not getting the Mac to move into the umanaged devices in Jamf, the tick box is still ticked.

Any chance you can post your full updated script as the first script posted does a full unenrol unmanage which removes all jamf framework which is not what I think we are both trying to achieve and I can't seem to get the syntax right when adding in your updated curl setting this

<remote_management><managed>false</managed></remote_management>

 

MatG
Contributor III

I worked it out 😀

rcoleman
New Contributor III

 @MatG - Sure no problem. I've removed the "Unmanage" command and you'll need to replace the server name but this works for myself for unticking:

 

#!/bin/bash

echo "Enter JSS username:"
read USERNAME
echo "Enter JSS password:"
read -s PASSWORD

TOKEN_EXPIRATION_EPOCH="0"

function getBearerToken() {
    RESPONSE=$(curl -s -u "$USERNAME":"$PASSWORD" "https://<your_server>.jamfcloud.com/api/v1/auth/token" -X POST)
    OS_MAJOR_VERSION=$(sw_vers -buildVersion | cut -c 1-2)
    echo "OS Major Version: $OS_MAJOR_VERSION"
    if [ "$OS_MAJOR_VERSION" -lt 21 ]; then
        # Get the token info
        BEARER_TOKEN=$(echo $RESPONSE | python -c 'import json,sys;obj=json.load(sys.stdin);print obj["token"]')
        # Get the expiration date
        TOKEN_EXPIRATION=$(echo $RESPONSE | python -c 'import json,sys;obj=json.load(sys.stdin);print obj["expires"]')
    # If we are running Monterey or later then we can use plutil to parse json
    else    
        # Get the token info
        BEARER_TOKEN=$(echo "$RESPONSE" | plutil -extract token raw -)    
        # Get the token expiration date
        TOKEN_EXPIRATION=$(echo "$RESPONSE" | plutil -extract expires raw - | awk -F . '{print $1}')
    fi
	TOKEN_EXPIRATION_EPOCH=$(date -j -f "%Y-%m-%dT%T" "$TOKEN_EXPIRATION" +"%s")
}

function checkTokenExpiration() {
    NOW_EPOCH_UTC=$(date -j -f "%Y-%m-%dT%T" "$(date -u +"%Y-%m-%dT%T")" +"%s")
    if [[ TOKEN_EXPIRATION_EPOCH -gt NOW_EPOCH_UTC ]]
    then
        echo "Token valid until the following epoch time: " "$TOKEN_EXPIRATION_EPOCH"
    else
        echo "No valid token available, getting new token"
        getBearerToken
    fi
}

function invalidateToken() {
	RESPONSE_CODE=$(curl -w "%{http_code}" -H "Authorization: Bearer ${BEARER_TOKEN}" "https://uoe.jamfcloud.com/api/v1/auth/invalidate-token" -X POST -s -o /dev/null)
	if [[ ${RESPONSE_CODE} == 204 ]]
	then
		echo "Token successfully invalidated"
		BEARER_TOKEN=""
		TOKEN_EXPIRATION_EPOCH="0"
	elif [[ ${RESPONSE_CODE} == 401 ]]
	then
		echo "Token already invalid"
	else
		echo "An unknown error occurred invalidating the token"
	fi
}

echo "Getting API token..."
checkTokenExpiration

# Get serial number
SERIAL=$(system_profiler SPHardwareDataType | awk '/Serial/ {print $4}')
/bin/echo "Serial number is $SERIAL"

# Get JAMF ID of device
JAMF_ID=$(curl -X GET "https://<your_server>.jamfcloud.com/JSSResource/computers/serialnumber/$SERIAL" -H "accept: application/xml" -H "Authorization: Bearer $BEARER_TOKEN" | xmllint --xpath '/computer/general/id/text()' -)
/bin/echo "JAMF ID for $SERIAL is $JAMF_ID"

# De-select "Allow Jamf Pro to perform management tasks" in the JSS for this device
curl --request PUT --url "https://<your_server>.jamfcloud.com/JSSResource/computers/id/$JAMF_ID" -H "Content-Type: application/xml" -H "Accept: application/xml" -H "Authorization: Bearer $BEARER_TOKEN" -d '<computer><general><remote_management><managed>false</managed></remote_management></general></computer>'

# Bin the token
/bin/echo "Invalidating API token..."
invalidateToken

/bin/echo "Done."

exit 0;

EDIT - Ah, I just see you got it working - good stuff 😀

 

sk25
Contributor

Guys, 

Help me out here.. I'm in testing mode of Jamf API. So did sample test to get activation code. I do have all permission to run the API as I created a local user account in Jamf with the privilege as 'Administrator'. However, I'm getting the same below error message when I 'Try it out' as well as through 'shell script'.

Error message:

<html>

<head>

<title>Status page</title>

</head>

<body style="font-family: sans-serif;">

<p style="font-size: 1.2em;font-weight: bold;margin: 1em 0px;">Unauthorized</p>

<p>The request requires user authentication</p>

<p>You can get technical details <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2">here</a>.<br>

Please continue your visit at our <a href="/">home page</a>.

</p>

</body>

</html>% 

Kindly advice. Thanks.

MLBZ521
Contributor III

@sk25 Without knowing how you constructed the command in the script....But using the API documentation page's "Try it out" feature and still having issues...

Do you use Sites?  The Site you have selected in the GUI affects API usage.  If you're not at the "Full Jamf Pro" level, you will be limited when using the API.

As said the local user created in Jamf Pro is as an 'Administrator' privilege which means having create, read,update and delete of Jamf pro server objects, jamf pro server settings,Jamf Pro server actions

MLBZ521
Contributor III

That doesn't matter if you use and have a Site selected.  If you use Sites and are viewing a Site in the GUI, it affects what you can do/see in the API.

Trust me, we use Sites here and I have to account for it in all of the scripts I write for usage by our Site Admins.

Mikael_lofgren
New Contributor II

@sk25 Do you have the bearer token then? As the API page says:

If Basic authentication is disabled then a token must be generated to be used with the 'Try it out!' feature. Enter username and password to generate a token.

I've unchecked the 'Allow Basic Authentication in addition to Bearer Token Authentication" and ran the Bearer Token authorization from open recipe and getting the below error message. Kindly advice. Thanks.

"No valid token available, getting new token
Failed conversion of ``<stdin>: Could not extract value, error: No value at that key path or invalid key path: expires'' using format ``%Y-%m-%dT%T''
date: illegal time format
usage: date [-jnRu] [-r seconds|file] [-v[+|-]val[ymwdHMS]]
[-I[date | hours | minutes | seconds]]
[-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
{
"httpStatus" : 401,
"errors" : [ ]
}No valid token available, getting new token
Failed conversion of ``<stdin>: Could not extract value, error: No value at that key path or invalid key path: expires'' using format ``%Y-%m-%dT%T''
date: illegal time format
usage: date [-jnRu] [-r seconds|file] [-v[+|-]val[ymwdHMS]]
[-I[date | hours | minutes | seconds]]
[-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
Token already invalid
{
"httpStatus" : 401,
"errors" : [ ]"

MLBZ521
Contributor III

Without seeing the constructed command, it's hard to troubleshoot errors.  But....it looks like you may have ran the code from the Bearer Token Authorization Recipe example.  Did you simply copy and paste the code?  If so, you could have invalid character (e.g. ASCII vs Unicode) encoding in your script.  (This is a common issue when copying code from a website into a shell script and it's commonly due to the quotation marks used; e.g.  " and ' ).

For more information on review these pages:

Also, I'd recommend reviewing (if you haven't already) for working with Bearer Tokens:

All,

I'm not getting anymore error on date format on the above mentioned API. However, if I give SSO enabled Jamf username and password, getting "Http code as 401". If I give Jamf local account username and password, I'm able to see the token, token expiration information, epoch etc.,
Question here:
SSO enabled Jamf username and password won't work on API's?
Any simple code for renewal of Token as Jamf token is valid for 30 mins or so. 
Kindly advice guys. Thanks.

Mikael_lofgren
New Contributor II

We haven't got any SSO accounts to work with the API, we use local accounts for different API/scripts. Strange thing when re-tested is that API call to return the servern version seems to work (jamf-pro-version)

@Mikael_lofgren @MLBZ521 For Jamf API SSO is not supporting. We have to use local Jamf account with necessary API permission.

MLBZ521
Contributor III

I can't speak for the API usage with SSO enabled, but wouldn't be surprised...

For expiration tracking/checking, you can utilize the function sample in the Bearer Token Authorization Recipe page I referenced before.  Or, more correctly, you could use the Keep Alive endpoint to invalidate the current and create a new Bearer Token in one step.

Mikael_lofgren
New Contributor II

sk25
Contributor

Guys,

Help me out with the below script. Trying to send unmanage command to machine. However, I'm getting: Syntax Error Unexpected end of file

#! /bin/sh




# server connection information

jamfProURL="XXXX"

username="XXXX"

password="XXXX"




renewToken() {




# renew auth token

authToken=$( /usr/bin/curl \

--header "Accept: application/json" \

--header "Authorization: Bearer $token" \

--request POST \

--silent \

--url "$jamfProURL/api/v1/auth/keep-alive" )




# parse auth token

token=$( /usr/bin/plutil \

-extract token raw - <<< "$authToken" )




tokenExpiration=$( /usr/bin/plutil \

-extract expires raw - <<< "$authToken" )




localTokenExpirationEpoch=$( TZ=UTC /bin/date -j -f "%Y-%m-%dT%T" "$tokenExpiration" "+%s" 2> /dev/null )




# update the renewal time for another 25 minutes

renewalTime=$(( $localTokenExpirationEpoch - 300 ))




}




# request auth token

authToken=$( /usr/bin/curl \

--request POST \

--silent \

--url "$jamfProURL/api/v1/auth/token" \

--user "$username:$password" )




echo "$authToken"




# parse auth token

token=$( /usr/bin/plutil \

-extract token raw - <<< "$authToken" )




tokenExpiration=$( /usr/bin/plutil \

-extract expires raw - <<< "$authToken" )




localTokenExpirationEpoch=$( TZ=GMT /bin/date -j \

-f "%Y-%m-%dT%T" "$tokenExpiration" \

+"%s" 2> /dev/null )




echo Token: "$token"

echo Expiration: "$tokenExpiration"

echo Expiration epoch: "$localTokenExpirationEpoch"




# verify auth token is valid

checkToken=$( /usr/bin/curl \

--header "Authorization: Bearer $token" \

--silent \

--url "$jamfProURL/api/v1/auth" \

--write-out "%{http_code}" )




tokenStatus=${checkToken: -3}

echo Token status: "$tokenStatus"




# subtract five minutes (300 seconds) from localTokenExpirationEpoch

renewalTime=$(( $localTokenExpirationEpoch - 300 ))




while IFS= read aDevice

do

now=$( /bin/date +"%s" )




if [[ "$renewalTime" -lt "$now" ]]; then

renewToken

fi




 ## get unique identifier for machine

udid=$(system_profiler SPHardwareDataType | awk '/UUID/ { print $3; }')




## get computer ID from Jamf server

compId=$( /usr/bin/curl \

--request GET \

--url "$jamfProURL/JSSResource/computers/udid/${udid}/subset/general" \

--header "Accept: application/xml" \

--header "Authorization: Bearer $token" \

--xpath '/computer/general/id/text()' )




echo "${compID}"




## send unmanage command to machine

/usr/bin/curl \

--header "Authorization: Bearer $token" \

--request POST \

--url "$jamfProURL/JSSResource/computercommands/command/UnmanageDevice/id/${compId}"




# expire auth token

/usr/bin/curl \

--header "Authorization: Bearer $token" \

--request POST \

--silent \

--url "$jamfProURL/api/v1/auth/invalidate-token"


 

MLBZ521
Contributor III

I'm really confused on what you're doing here....as it looks like two different possibilities:

  • There's a `while` loop, which isn't terminated, nor is there something it's iterating through
    • This could be what's causing the Syntax Error Unexpected end of file
  • Second, it seems you're unmanaging the device the script is running on.....so...that means the device has to be active to run it....
    • And you're saving credentials in a script, which is bad practice

 

It's also important to note that sending the Unmanage MDM Command to a device means the device must be active to receive it and become unmanaged.  In addition, on a Mac, this command only removes the MDM Profile (and related bits), it does not remove the Jamf Management Framework which consists of the `jamf` binary and other bits, which means the device will continue to check-in to the JPS.

@MLBZ521 Using this same While loop i'm able to get the list of smart groups, but not sure why unable to send the unmanage command.

I'm passing the credential to bearer token so that no need to key in credentials each and every time.

Also, I'm running this script which already managed by the Jamf. And I would like to take your last point that it would check in back to Jamf server. Will work on it. Thanks, BTW.

@MLBZ521 My apologies MLBZ521. After I removed the 'while' loop the script executed well. I mean without any issues profiles got removed. Thanks a lot. 

MLBZ521
Contributor III

As a mentioned previously, removing the MDM Profile does not remove the Jamf Management Framework which consists of the `jamf` binary and other bits, which means the device will continue to check-in to the JPS.

MLBZ521
Contributor III

Why are you using the `while` loop to get a list of Smart Groups?

But, as mentioned, the `while` loop is not terminated, so that is going to cause the script to fail.

sk25
Contributor

Guys, anyone explain me below when I execute the unmanage script via API and Bearer token renewal in terminal I'm getting this,

Screen Shot 2023-06-05 at 2.23.17 pm.png

What's this? 

Also, I used below to delete the entry of the computer in Jamf via API, but getting the below error message and the Id having administrator privilege.

Screen Shot 2023-06-05 at 2.26.14 pm.png

 

jamf-42
Valued Contributor II

add:

set -x

to code after shebang, then you can see output to debug.

MLBZ521
Contributor III

The first question:  The image indicates that `curl` is "downloading" something, for instance an ".html" file.  This is what `curl` natively does.  What it's downloading, you'd have to check.  You can add `--output ~/Downloads/file.html` to the end of your `curl` command and see what's being transmitted.

 

For the second question:  It's hard to know exactly without seeing the full `curl` command, but to me, it almost looks like the `curl` command is attempting to interact with a web server (aka host) named `DELETE`, which would obviously be incorrect.  That should be your HTTP method, not your host.  Be sure you're passing your values to the correct parameters.  e.g.

curl --url <https://jamf.pro.url:8443> --request [GET | PUT | POST | DELETE ] [...<remaining parameters>...]

That said, I don't think the code is doing what you think it's doing...  At least, the XML payload in that second image has nothing to do with deleting a computer record...

@MLBZ521 Below is the curl command for the deletion of the computer entry in Jamf.

/usr/bin/curl \

--header "Authorization: Bearer $token"\

--request DELETE \

--url "$jamfProURL/JSSResource/computers/udid/${compId}"

Sure, will try add output parameter and check. Thanks.

MLBZ521
Contributor III

That looks correct for what I can see.  What's the value of your `$comdId` variable?

That said, that command and the screenshot for the results of that command do not match at all.

sk25
Contributor

Guys, help me with the script,

Requirements:
1. Once the jamf unenroll script executed, I would like to let the customer must get notification "system is going to reboot in 2 mins" and after 2 mins should restart automatically.

2. Another script to remove the CA certificate profile from Profile tab in system preference.

Thanks in advance.

MLBZ521
Contributor III

I believe you have a misunderstanding of how things work...

1.  Unless you're running this on the device that is being unenrolled...this isn't possible....and if you are doing that....that's a very bad idea...  Either way, why are you bothering with rebooting them?

2.  Not sure what you're referring to here...  This sounds like a custom Profile and you can't locally remove Profiles installed via MDM when the device is ADE Enrolled...

The reason to reboot the machine is our customer is moving out of jamf and getting the enrolled in Intune. After the Jamf profile removed, we were unable to enroll it intune unless we reboot the machine. 

2. As soon as we enroll the device in Jamf, unlike the other profiles, customer created a profile to CA certificate install. Normal script isn't remove the CA Profile.