-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathBootstraptoken_Escrow_Fix_LAPS.jss.zsh
More file actions
executable file
·334 lines (271 loc) · 10.6 KB
/
Bootstraptoken_Escrow_Fix_LAPS.jss.zsh
File metadata and controls
executable file
·334 lines (271 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
#!/bin/zsh --no-rcs
# Jason Filice
# jfilice@csumb.edu
# Technology Support Services in IT
# California State University, Monterey Bay
# https://csumb.edu/it
# Run it with 6 arguments.:
# PARAMETER 4: url
# PARAMETER 5: API client id
# PARAMETER 6: API client secret
# PARAMETER 7: jamfProID_ManagedPrefDomain
# PARAMETER 8: computerLocalAdminUsername
# PARAMETER 9: LAPSpasswordFallback
#
# Use as script in Jamf JSS.
#
SCRIPTNAME=$(/usr/bin/basename "$0")
SCRIPTDIR=$(/usr/bin/dirname "$0")
# Jamf JSS Parameters 1 through 3 are predefined as mount point, computer name, and username
pathToScript=$0
if [[ -n "${1}" ]]; then
mountPoint="${1}"
fi
if [[ -n "${2}" ]]; then
computerName="${2}"
fi
if [[ -n "${3}" ]]; then
userName="${3}"
fi
echo "pathToScript=$pathToScript"
echo "mountPoint=$mountPoint"
echo "computerName=$computerName"
echo "userName=$userName"
url=${4:=""}
client_id=${5:=""}
client_secret=${6:=""}
jamfProID_ManagedPrefDomain=${7:=""}
computerLocalAdminUsername=${8:=""}
LAPSpasswordFallback=${9:=""}
# ##### Debugging flags #####
# debug script by enabling verbose “-v” option
# set -v
# debug script using noexec (Test for syntaxt errors)
# set -n
# identify the unset variables while debugging script
# set -u
# debug script using xtrace
# set -x
# Enable tracing without trace output
# { set -x; } 2>/dev/null
# Disable tracing without trace output
# { set +x; } 2>/dev/null
# Token state
ACCESS_TOKEN=""
TOKEN_EXPIRATION_EPOCH=0
# ########## FUNCTIONS ##########
function getAccessToken() {
local response
response=$(curl --silent --location --request POST "${url}/api/oauth/token" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=${client_id}" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_secret=${client_secret}")
# Try to extract access_token and expires_in
ACCESS_TOKEN=$(echo "$response" | /usr/bin/plutil -extract access_token raw - 2>/dev/null || echo "")
token_expires_in=$(echo "$response" | /usr/bin/plutil -extract expires_in raw - 2>/dev/null || echo "")
# Fallback parsing if plutil failed (attempt simple grep/awk)
if [[ -z "$ACCESS_TOKEN" ]]; then
ACCESS_TOKEN=$(echo "$response" | /usr/bin/awk -F'"' '/access_token/{print $4; exit}')
fi
if [[ -z "$token_expires_in" ]]; then
token_expires_in=$(echo "$response" | /usr/bin/awk -F'"' '/expires_in/{print $4; exit}')
fi
if [[ -z "$ACCESS_TOKEN" || -z "$token_expires_in" ]]; then
echo "Failed to obtain access token." >&2
return 1
fi
current_epoch=$(date +%s)
TOKEN_EXPIRATION_EPOCH=$((current_epoch + token_expires_in - 1))
return 0
}
function checkTokenExpiration() {
current_epoch=$(date +%s)
if [[ ${TOKEN_EXPIRATION_EPOCH} -gt ${current_epoch} && -n "${ACCESS_TOKEN}" ]]; then
echo "Token valid until epoch: ${TOKEN_EXPIRATION_EPOCH}"
else
echo "No valid token available (or expired), getting new token"
getAccessToken
fi
}
function invalidateToken() {
if [[ -z "${ACCESS_TOKEN}" ]]; then
echo "No token to invalidate"
return 0
fi
responseCode=$(curl -w "%{http_code}" -H "Authorization: Bearer ${ACCESS_TOKEN}" "${url}/api/v1/auth/invalidate-token" -X POST -s -o /dev/null)
if [[ ${responseCode} == 204 ]]; then
echo "Token successfully invalidated"
ACCESS_TOKEN=""
TOKEN_EXPIRATION_EPOCH=0
elif [[ ${responseCode} == 401 ]]; then
echo "Token already invalid"
else
echo "An unknown error occurred invalidating the token: HTTP ${responseCode}"
fi
}
function checkResponseCode() {
# arg is a concatenation of body and three-digit http code appended by curl
local full="$1"
local http="${full: -3}"
case "${http}" in
200) echo "200 Request successful";;
201) echo "201 Request to create or update object successful";;
400) echo "400 Bad request";;
401) echo "401 Authentication failed";;
403) echo "403 Invalid permissions";;
404) echo "404 Object/resource not found";;
409) echo "409 Conflict";;
500) echo "500 Internal server error";;
000) echo "000 No HTTP code received";;
*) echo "${http} Unknown HTTP code";;
esac
}
function apiGET() {
# usage: apiGET "Header-Name: value" "https://..."
local extraHeader="$1"
local urlToGet="$2"
checkTokenExpiration
# Get response body and HTTP code appended
local apiGetResponse
apiGetResponse=$( /usr/bin/curl \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "${extraHeader}" \
--request GET \
--silent \
--url "${urlToGet}" \
--write-out "%{http_code}" )
local codeCheck
codeCheck=$( checkResponseCode "${apiGetResponse}" )
# if first char of http code is 2 => success
local httpCode="${apiGetResponse: -3}"
if [[ "${httpCode:0:1}" != "2" ]]; then
echo "Error while attempting to retrieve: ${codeCheck}" >&2
return 1
else
# return body (strip last 3 chars)
echo "${apiGetResponse:: -3}"
fi
}
function setLAPSpassword() {
for managementID in "${managementIDs[@]}"; do
echo "Running simpleLAPS for management ID: $managementID"
local accountName=$1
checkTokenExpiration
curl -X 'PUT' \
"$url/api/v2/local-admin-password/$managementID/set-password" \
--header 'accept: application/json' \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header 'Content-Type: application/json' \
-d '{
"lapsUserPasswordList": [
{
"username": "'$accountName'",
"password": "Strong-ish_Password"
}
]
}'
echo -e
done
}
function GETcomputerManagementID() {
echo "Requesting oauth token."
# Get bearer token for API transactions
checkTokenExpiration || { echo "Unable to obtain token"; exit 1; }
# Get local s/n
readonly serialNumber=$( /usr/sbin/system_profiler SPHardwareDataType 2>/dev/null | /usr/bin/awk -F": " '/Serial Number/{ print $2 }' )
echo "Computer serial number: $serialNumber"
# get computer Jamf Pro ID from local plist (if present)
jamfProComputerID=""
if [[ -f "/Library/Managed Preferences/${jamfProID_ManagedPrefDomain}.plist" ]]; then
jamfProComputerID=$(/usr/bin/defaults read "/Library/Managed Preferences/${jamfProID_ManagedPrefDomain}.plist" JamfProID 2>/dev/null || echo "")
fi
if [[ -z "${jamfProComputerID}" ]]; then
# fallback to API (note: older JSS endpoints)
computerGeneralXML=$( apiGET "Accept: text/xml" "${url}/JSSResource/computers/serialnumber/${serialNumber}" ) || { echo "Failed to fetch computer by serial"; exit 1; }
jamfProComputerID=$( /usr/bin/xpath -e "/computer/general/id/text()" 2>/dev/null <<< "$computerGeneralXML" )
# /api/v2/computers-inventory?section=GENERAL&filter=hardware.serialNumber%3D%3D${serialNumber}
fi
echo "jamfProComputerID: $jamfProComputerID"
# get computer management ID (using v2 inventory)
computerGeneralJson=$( apiGET "Accept: application/json" "${url}/api/v2/computers-inventory/${jamfProComputerID}?section=GENERAL" ) || { echo "Failed to fetch computer inventory"; exit 1; }
computerManagementID=$( /usr/bin/awk -F "\"" '/managementId/{ print $4; exit }' <<< "$computerGeneralJson" )
echo "Jamf Pro management ID: $computerManagementID"
return 0
}
function GetLAPSpassword() {
local computerLocalAdminUsername=$1
LAPSpassword=""
checkTokenExpiration
if [[ -z ${computerManagementID} ]]; then
GETcomputerManagementID
fi
# get computer local admin password
# API get LAPS password
# /v2/local-admin-password/{clientManagementId}/account/{username}/password
# LAPSpasswordJson=$( apiGET "Accept: application/json" "${url}/api/v2/local-admin-password/${computerManagementID}/account/${computerLocalAdminUsername}/password" ) || { echo "Failed to get password"; exit 1; }
LAPSpasswordJson=$( apiGET "Accept: application/json" "${url}/api/v2/local-admin-password/${computerManagementID}/account/${computerLocalAdminUsername}/password" ) \
&& LAPSpassword=$( /usr/bin/awk -F "\"" '/password/{ print $4; exit }' <<< "$LAPSpasswordJson" ) \
|| { echo "Failed to get password"; echo "Using fallback static password"; LAPSpassword="${LAPSpasswordFallback}"; }
}
function GetLAPSusernames() {
# get computer local admin username list
checkTokenExpiration
if [[ -z ${computerManagementID} ]]; then
GETcomputerManagementID
fi
computerLocalAdminUsernameJson=$( apiGET "Accept: application/json" "${url}/api/v2/local-admin-password/${computerManagementID}/accounts" ) || { echo "Failed to get accounts"; exit 1; }
computerLAPSUSERNAMES=$( /usr/bin/awk -F "\"" '/username/{ print $4 }' <<< "$computerLocalAdminUsernameJson" )
echo "LAPS username(s):"
echo "$computerLAPSUSERNAMES"
}
# ########## END FUNCTIONS ##########
# Add parameter validation
if [[ -z "${computerLocalAdminUsername}" ]] ; then
echo "Error: LAPS account is required"
exit 1
fi
GetLAPSpassword "${computerLocalAdminUsername}"
# Do not echo sensitive info in logs in production. These are printed here for debugging only.
echo "LAPS user name: ${computerLocalAdminUsername}"
echo "LAPS LAPSpassword: (redacted)"
# Add parameter validation
if [[ -z "${computerLocalAdminUsername}" ]] || [[ -z "${LAPSpassword}" ]]; then
echo "Error: Volume owner account and password are required"
exit 1
fi
# Used to verify the password
# authenticate the account without actually logging into anything
# account authenticates in any way it will have a SecureToken enabled on the account
if /usr/bin/dscl . authonly "${computerLocalAdminUsername}" "${LAPSpassword}"; then
echo "Authentication test passed"
else
echo "Error: Authentication test failed"
exit 1
fi
# Show secure token status for additional info
/usr/sbin/sysadminctl -secureTokenStatus "${computerLocalAdminUsername}"
echo "Check Bootstrap Token status..."
bootstrap=$(/usr/bin/profiles status -type bootstraptoken)
echo ${bootstrap}
if [[ $bootstrap == *"supported on server: YES"* ]]; then
if [[ $bootstrap == *"escrowed to server: YES"* ]]; then
echo "Bootstrap escrowed."
echo "Updating the Bootstrap Token APFS record and escrowing to the MDM server..."
else
echo "Bootstrap not escrowed."
echo "Creating the Bootstrap Token APFS record and escrowing to the MDM server..."
fi
sleep 1
# Add error handling for profiles command
if ! /usr/bin/profiles install -type bootstraptoken -user "${computerLocalAdminUsername}" -password "${LAPSpassword}" -verbose; then
echo "Error: Failed to install bootstrap token"
exit 1
fi
sleep 1
/usr/bin/profiles status -type bootstraptoken
else
echo "Bootstrap token not supported on server"
result="NOT SUPPORTED"
fi
exit 0