Your Internal Beta Test Program: Opt-in / Opt-out via Self Service

dan-snelson
Valued Contributor II

5dc90ee44524414c89d60596f03cf7d4

Background

Inspired by @elliotjordan's plea to obtain user feedback, we've been using a pop-up menu Computer Extension Attribute called "Testing Level" which has three options:

  • Alpha (i.e., bleeding-edge test machines)
  • Beta (i.e., direct team members)
  • Gamma (i.e., opt-in testers from various teams)

9aff463b713a4848b966d645e041eefd

We then have Smart Computer Groups for each of the three levels and a fourth for "none" so we can more easily scope policies.

26c8e1591a1d434b83b12c0dfc0d7ccb

This has been working well, but has required a JSS administrator to manually edit each computer record and specify the desired Testing Level.

After being challenged by @mike.paul and @kenglish to leverage the API, a search revealed @seansb's Updating Pop-Up Extension Attribute Value via API post and @mm2270's reply about Results of single extension attribute via API we had exactly what we needed.


API Permissions for Computer Extension Attributes

In my rather frustated testing, the API read / write account needs (at least) the following JSS Objects "Read" and "Update" permissions:

  • Computer Extension Attributes
  • Computers
  • User Extension Attributes
  • Users

Script: Update Extension Attribute

You'll need to update the "apiURL" in the following script which leverages parameters 4 though 7 for:

  • API Username
  • API Password
  • EA Name (i.e., "Testing Level")
  • EA Value (i.e., "Gamma" or "None")

87be3bebb83c4ed3b86ce0b624b49f1b

#!/bin/sh
####################################################################################################
#
# ABOUT
#
#   Set a computer's Extension Attribute via the API
#
####################################################################################################
#
# HISTORY
#
#   Version 1.0, 30-Jul-2016, Dan K. Snelson
#
####################################################################################################
# Import logging functions
source /path/to/logging/script/logging.sh
####################################################################################################


ScriptLog "--- Set a computer's Extension Attribute via the API ---"


### Variables
apiURL="https://jss.company.com"  # JSS URL without trailing forward slash
apiUsername="${4}"                    # API Username
apiPassword="${5}"                    # API Password
eaName="${6}"                     # Name of Extension Attribute (i.e., "Testing Level")
eaValue="${7}"                        # Value for Extension Attribute (i.e., "Gamma" or "None")
computerUDID=$(/usr/sbin/system_profiler SPHardwareDataType | /usr/bin/awk '/Hardware UUID:/ { print $3 }')



# Validate a value has been specified for Parameter 4  ...
if [ ! -z "${apiUsername}" ] && [ ! -z "${apiPassword}" ] && [ ! -z "${eaName}" ] && [ ! -z "${eaValue}" ]; then
    # All script parameters have been specified, proceeding ...
    ScriptLog "* All script parameters have been specified, proceeding ..."
    ScriptLog "* Extension Attribute Name: ${eaName}"
    ScriptLog "* Extension Attribute New Value: ${eaValue}"

    if [ ${eaValue} == "None" ]; then
        ScriptLog "* Extension Attribute Value is 'None'; remove value ${eaName}"
        eaValue=""
    fi

    # Read current value ...
    apiRead=`curl -H "Accept: text/xml" -sfku ${apiUsername}:${apiPassword} ${apiURL}/JSSResource/computers/udid/${computerUDID}/subset/extension_attributes | xmllint --format - | grep -A3 "<name>${eaName}</name>" | awk -F'>|<' '/value/{print $3}'`

    ScriptLog "* Extension Attribute ${eaName}'s Current Value: ${apiRead}"

    # Construct the API data ...
    apiData="<computer><extension_attributes><extension_attribute><name>${eaName}</name><value>${eaValue}</value></extension_attribute></extension_attributes></computer>"

    apiPost=`curl -H "Content-Type: text/xml" -sfu ${apiUsername}:${apiPassword} ${apiURL}/JSSResource/computers/udid/${computerUDID} -d "${apiData}" -X PUT` 

    /bin/echo ${apiPost}    

    # Read the new value ...
    apiRead=`curl -H "Accept: text/xml" -sfku ${apiUsername}:${apiPassword} ${apiURL}/JSSResource/computers/udid/${computerUDID}/subset/extension_attributes | xmllint --format - | grep -A3 "<name>${eaName}</name>" | awk -F'>|<' '/value/{print $3}'`

    ScriptLog "* Extension Attribute ${eaName}'s New Value: ${apiRead}"

    ScriptLog "--- Completed setting a computer's Extension Attribute via the API ---"

    # Re-direct logging to the JSS
    exec 1>&3 2>&4
    /bin/echo >&1 "${eaName} changed to ${eaValue}"

else

    ScriptLog "Error: Parameters 4, 5, 6 and 7 not populated; exiting."

    # Re-direct logging to the JSS
    exec 1>&3 2>&4
    /bin/echo >&1 "Error: Parameters 4, 5, 6 and 7 not populated; exiting."
    exit 3
fi


exit 0

Script: Logging

The following script is installed client-side and is then source'd by the above script.

#!/bin/sh
####################################################################################################
#
# ABOUT
#
#   Standard logging functions which are imported into other scripts
#
####################################################################################################
#
# HISTORY
#
#   Version 1.0, 22-Nov-2014, Dan K. Snelson
#   Version 2.0, 13-Oct-2015, Dan K. Snelson
#
####################################################################################################

# LOGGING

# Logging variables
    logFile="/var/log/com.company.log"

# Check for / create logFile
    if [ ! -f "${logFile}" ]; then
        # logFile not found; Create logFile
        /usr/bin/touch "${logFile}"
    fi

# Save standard output and standard error
    exec 3>&1 4>&2
# Redirect standard output to logFile
    exec 1>>"${logFile}"
# Redirect standard error to logFile
    exec 2>>"${logFile}"

# Enable all logging from this point forward
#   set -xv; exec 1>>"${logFile}" 2>&1


# Logging Function inspired by rtrouton
ScriptLog(){

    DATE=`date +%Y-%m-%d %H:%M:%S`
    /bin/echo "$DATE" " $1" >> ${logFile}
}

####################################################################################################

Opt-in Beta Test Program Self Service Policy

Create an ongoing Self Sevice policy, scoped to "Testing: None" which includes a single Scripts option of "Update Extension Attribute" and specify:

  • API Username (Read / Write)
  • API Password (Read / Write)
  • EA Name (i.e., "Testing Level")
  • EA Value (i.e., "Gamma" or "None")

Opt-out Beta Test Program Self Service Policy

Clone your Opt-in policy and change EA Value to "None" to unset a computer's Testing Level; scope to your testing groups.


--
Dan
7 REPLIES 7

elliotjordan
Contributor II

Very impressive! Nice work.

Have you had any issues with special characters not escaping properly in the API password? Is the password visible in any logs on the client or on the JSS?

Are those the minimum necessary API permissions? Seems like "Create" shouldn't be necessary, but it sounds like you've tested extensively.

Are there still any features you'd like help implementing? If you post it to GitHub, you might get some pull requests.

dan-snelson
Valued Contributor II

@elliotjordan Thanks for the feedback.

Special Characters: No, I haven't seen any issue with special characters not escaping properly. As password such as:

9A0.ERkuAftTSei)P7k

works fine, except now I've got to change my API password πŸ˜‰

Password Visibility: The password is not visible client-side in logs or temporary JAMF directories thanks to the Script Parameters, but is visible in the JSS policy. (We use Sites extensively and few JSS admins can see Policies assigned to a Site of "None.")

API Permissions: Nice catch; fixed above.

To Dos: I believe I saw where @seansb was working on hashing the API password and I've only tested this with text-flavored EAs; it'd be nice to make that more robust.


--
Dan

Josh_Smith
Contributor III

Very nice, thanks for sharing.

dan-snelson
Valued Contributor II

@mike.paul Points out:
"You could leverage the project that our IT and Security team setup for this exact use:"
https://github.com/jamfit/Encrypted-Script-Parameters

Here's an updated version:

#!/bin/sh
####################################################################################################
#
# ABOUT
#
#   Set a computer's Extension Attribute via the API
#
####################################################################################################
#
# HISTORY
#
#   Version 1.0, 30-Jul-2016, Dan K. Snelson
#       Original
#   Version 1.1, 17-Oct-2016, Dan K. Snelson
#       Updated to leverage an encyrpted API password
#
####################################################################################################
# Import functions
source /path/to/logging/script/functions.sh
####################################################################################################


ScriptLog "--- Set a computer's Extension Attribute via the API ---"


### Variables
apiURL="https://jss.company.com/"             # JSS URL without trailing forward slash
apiUsername="${4}"                            # API Username
apiPasswordEncrypted="${5}"                   # API Encrypted Password
eaName="${6}"                             # Name of Extension Attribute (i.e., "Testing Level")
eaValue="${7}"                                # Value for Extension Attribute (i.e., "Gamma" or "None")
Salt="saltOutputGoesHere"                 # Salt (generated from Encrypt Password)
Passphrase="passphraseOutputGoesHere"     # Passphrase (generated from Encrypt Password)
computerUDID=$(/usr/sbin/system_profiler SPHardwareDataType | /usr/bin/awk '/Hardware UUID:/ { print $3 }')


# Validate a value has been specified for Parameter 4  ...
if [ ! -z "${apiUsername}" ] && [ ! -z "${apiPasswordEncrypted}" ] && [ ! -z "${eaName}" ] && [ ! -z "${eaValue}" ]; then
    # All script parameters have been specified, proceeding ...
    ScriptLog "* All script parameters have been specified, proceeding ..."
    apiPassword=$(decryptPassword ${apiPasswordEncrypted} ${Salt} ${Passphrase})
    ScriptLog "* Extension Attribute Name: ${eaName}"
    ScriptLog "* Extension Attribute New Value: ${eaValue}"

    if [ ${eaValue} == "None" ]; then
        ScriptLog "* Extension Attribute Value is 'None'; remove value ${eaName}"
        eaValue=""
    fi

    # Read current value ...
    apiRead=`curl -H "Accept: text/xml" -sfu ${apiUsername}:${apiPassword} ${apiURL}/JSSResource/computers/udid/${computerUDID}/subset/extension_attributes | xmllint --format - | grep -A3 "<name>${eaName}</name>" | awk -F'>|<' '/value/{print $3}'`

    ScriptLog "* Extension Attribute ${eaName}'s Current Value: ${apiRead}"

    # Construct the API data ...
    apiData="<computer><extension_attributes><extension_attribute><name>${eaName}</name><value>${eaValue}</value></extension_attribute></extension_attributes></computer>"

    apiPost=`curl -H "Content-Type: text/xml" -sfu ${apiUsername}:${apiPassword} ${apiURL}/JSSResource/computers/udid/${computerUDID} -d "${apiData}" -X PUT` 

    /bin/echo ${apiPost}    

    # Read the new value ...
    apiRead=`curl -H "Accept: text/xml" -sfu ${apiUsername}:${apiPassword} ${apiURL}/JSSResource/computers/udid/${computerUDID}/subset/extension_attributes | xmllint --format - | grep -A3 "<name>${eaName}</name>" | awk -F'>|<' '/value/{print $3}'`

    ScriptLog "* Extension Attribute ${eaName}'s New Value: ${apiRead}"

    ScriptLog "--- Completed setting a computer's Extension Attribute via the API ---"

    # Re-direct logging to the JSS
    exec 1>&3 2>&4
    /bin/echo >&1 "${eaName} changed to ${eaValue}"

else

    ScriptLog "Error: Parameters 4, 5, 6 and 7 not populated; exiting."

    # Re-direct logging to the JSS
    exec 1>&3 2>&4
    /bin/echo >&1 "Error: Parameters 4, 5, 6 and 7 not populated; exiting."
    exit 3
fi


exit 0

--
Dan

dan-snelson
Valued Contributor II

@elliotjordan Finally took your advice and posted this on GitHub.


--
Dan

dan-snelson
Valued Contributor II

The GitHub page includes a link to the JNUC 2019 Keynote file and YouTube presentation.


--
Dan

dan-snelson
Valued Contributor II

Tip: Refresh Self Service when a user opts-in / opts-out.


--
Dan