Jamf Pro

Download 31
Last updated Jan 10, 2025

Permission Check Script

If commands or loggers fail with a permission error (INVALID_PRIVILEGE) and the cause is hard to find, use the check scripts to call the Jamf Pro API directly with the same settings as the Sonar connect profile. The scripts show the privileges the account actually has and whether the APIs the app uses succeed.

  • The scripts only read data and do not change Jamf Pro or Sonar settings. Each check reads only one item, and the issued token is invalidated as soon as the check ends.
  • Output is in Korean by default. Use the -l en option for English output.

Scripts

ScriptWhere to runWhat it does
sonar_profile_export.shA server that can reach the Sonar web console (API)Saves the connect profile settings (API endpoint, account, HTTP proxy) to a file
jamf_permission_check.shA server that can reach Jamf ProCalls the Jamf Pro API with the saved file and the account password, and checks the privileges

If the Sonar server can reach Jamf Pro directly, you can run both scripts on the same server.

Prerequisites

  • bash and curl on a Linux server (no extra installation besides basic commands such as awk and sed)
  • Sonar API key: issue one in the Sonar web console at Users > Users > (user) > API Key. The user must be able to see the connect profile to check.
  • Password of the connect profile account: Sonar does not return passwords through its API for security reasons, so enter it during the check.

Step 1: Create the script files

Save the two scripts under Full scripts at the bottom of this page as files with the same names. To avoid line break changes when pasting, we recommend saving them with an editor directly on the Linux server.

Step 2: Export the connect profiles

Run on a server that can reach Sonar. The type of Jamf Pro connect profiles is jamf-pro. To export all Jamf Pro connect profiles at once, use -t jamf-pro.

sh sonar_profile_export.sh -s https://(Sonar address) -t jamf-pro -l en

To export specific connect profiles only, give identifiers with -n instead of -t, separated by commas.

sh sonar_profile_export.sh -s https://(Sonar address) -n (identifier) -l en

The script asks for the Sonar API key. The key is not shown on screen, and you can also set it in the SONAR_API_KEY environment variable. Because Sonar usually uses a private certificate, the script does not verify the certificate when connecting to Sonar.

A file named profile_(identifier)_(time).txt is created for each connect profile. The file does not contain protected values such as the password, and only the account that ran the script can read it.

Step 3: Run the permission check

Copy the file from step 2 to a server that can reach Jamf Pro, and run the check. The script asks for the account password. The password is not shown on screen and is not saved in the result file. If there are several connect profiles, run it for each file.

sh jamf_permission_check.sh -f (file exported in step 2) -l en
OptionRequiredDescription
-fRequiredFile exported in step 2. The API endpoint and account are read from this file
-pOptionalIP:PORT of an HTTP proxy if this server needs one to reach Jamf Pro. The proxy in the connect profile is for the Sonar server and is not used automatically
-lOptionalOutput language. en for English

Interpreting the results

StepNameWhat it checks
[1]TokenWhether the account and password can log in, and the Jamf Pro version
[2]Privileges the account actually hasAccess level (accessLevel), privilege set (privilegeSet), computer related privileges, and whether Read Computers is included
[3]API call per app featureComputer inventory used by jamf-pro-computers, and computer list and history used by the audit logger

Check these values in [2].

ItemExpected value
Access level (accessLevel)FullAccess
Privilege set (privilegeSet)AUDITOR (as in the install guide) or ADMINISTRATOR
Computer related privilegesIncludes Read Computers

The Summary at the end shows the problems found and the actions to take.

Summary example (the account does not have Read Computers):

Summary: problems 1

  1. The account does not have 'Read Computers' (failed: jamf-pro-computers, audit logger)
     -> Action: In Jamf Pro > Settings > System > User accounts and groups > (logpresso),
              set Privilege Set to Auditor on the Account tab as in the install guide, or
              select Read for Computers under Privileges tab > Jamf Pro Server Objects, then Save.

Sending the result

The result is shown on screen and saved as jamf_check_result_(time).txt in the current folder. If a call failed, the raw Jamf Pro response is saved in the appendix at the end of the file.

If the problem remains after taking action, send this result file to Logpresso technical support. The result file does not contain the password or the token, but it does contain the Jamf Pro address and the account name.

Full scripts

sonar_profile_export.sh

#!/usr/bin/env bash
#
# 로그프레소 소나 접속 프로파일 추출 스크립트 / Logpresso Sonar connect profile export script
#
# 소나 API로 접속 프로파일의 설정값을 읽어 프로파일마다 파일로 저장합니다. 저장한 파일은 외부 통신이 되는
# 서버로 옮겨 앱별 점검 스크립트의 -f 옵션으로 사용합니다. 암호, 서비스 키 같은 보호 항목은 소나가 API로
# 돌려주지 않으므로 파일에 포함되지 않습니다.
#
# 요구 사항 / Requirements: bash, curl, awk (소나 API에 접속할 수 있는 서버에서 실행)
#
# 사용법 / Usage:
#   sh sonar_profile_export.sh -s https://SONAR -t TYPE         유형의 모든 프로파일 저장 / export all of a type
#   sh sonar_profile_export.sh -s https://SONAR -n PROFILE[,..] 지정한 프로파일 저장 / export profiles
#
#   -s  소나 주소 / Sonar URL (e.g. https://192.0.2.10:443)
#   -n  접속 프로파일 식별자 또는 이름, 쉼표로 여러 개 / profile identifier or name, comma separated
#   -t  접속 프로파일 유형 (앱 설치 매뉴얼에 표기) / profile type (see the app install guide)
#   -l  출력 언어 ko(기본) 또는 en / output language ko (default) or en
#
# 소나 API 키(계정 > 계정 > 해당 계정 > API 키)는 실행 중에 입력받습니다 (화면에 표시되지 않음).
# SONAR_API_KEY 환경 변수로도 줄 수 있습니다. / The Sonar API key is prompted, or set SONAR_API_KEY.
# 결과 파일 / Output: profile_<식별자>_<실행시간>.txt (소유자만 읽기 가능 / owner-only)
# 소나는 보통 사설 인증서를 쓰므로 소나 접속 시 인증서 검증은 하지 않습니다.

# sh(dash 등)로 실행해도 bash 로 다시 실행
if [ -z "$BASH_VERSION" ] || case ":$SHELLOPTS:" in *:posix:*) true ;; *) false ;; esac; then
	exec bash "$0" "$@"
fi

# 만드는 파일은 소유자만 읽을 수 있게
umask 077

SONAR=""
NAMES=""
TYPE=""
LANG_OPT=ko

usage() {
	sed -n '11,18p' "$0" | sed 's/^# \{0,1\}//'
	exit 2
}

while getopts "s:n:t:l:h" opt; do
	case "$opt" in
		s) SONAR="${OPTARG%/}" ;;
		n) NAMES="$OPTARG" ;;
		t) TYPE="$OPTARG" ;;
		l) LANG_OPT="$OPTARG" ;;
		*) usage ;;
	esac
done
[ -z "$SONAR" ] && usage
[ -z "$NAMES" ] && [ -z "$TYPE" ] && usage

# 언어별 문구 선택 / pick message by language
t() {
	if [ "$LANG_OPT" = "en" ]; then printf '%s' "$2"; else printf '%s' "$1"; fi
}

if ! command -v curl >/dev/null 2>&1; then
	t "curl 명령이 필요합니다." "curl is required."; echo
	exit 2
fi

API_KEY="${SONAR_API_KEY:-}"
if [ -z "$API_KEY" ]; then
	printf '%s: ' "$(t "소나 API 키" "Sonar API key")"
	read -r -s API_KEY
	echo ""
fi
[ -z "$API_KEY" ] && usage

WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT

# curl 설정 파일 값의 따옴표·역슬래시 처리
cfg_esc() {
	printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

# 프로파일 배열 원소를 한 줄에 하나씩 출력 (문자열 안의 괄호는 무시)
split_profiles() {
	tr -d '\n\r' | awk '
	{
		s = $0; n = length(s); depth = 0; instr = 0; esc = 0; start = 0; arr = 0
		p = index(s, "\"profiles\"")
		if (p == 0) exit
		for (i = p; i <= n; i++) {
			c = substr(s, i, 1)
			if (instr) {
				if (esc) esc = 0
				else if (c == "\\") esc = 1
				else if (c == "\"") instr = 0
				continue
			}
			if (c == "\"") { instr = 1; continue }
			if (!arr) { if (c == "[") arr = 1; continue }
			if (c == "{") { if (depth == 0) start = i; depth++ }
			else if (c == "}") { depth--; if (depth == 0) print substr(s, start, i - start + 1) }
			else if (c == "]" && depth == 0) exit
		}
	}'
}

# 프로파일 객체의 최상위 문자열 필드 값 (code, name, type)
top_str() {
	awk -v key="$1" '
	{
		s = $0; n = length(s); depth = 0; instr = 0; esc = 0; buf = ""; want = 0
		for (i = 1; i <= n; i++) {
			c = substr(s, i, 1)
			if (instr) {
				if (esc) { esc = 0; buf = buf c; continue }
				if (c == "\\") { esc = 1; continue }
				if (c == "\"") {
					instr = 0
					if (want) { print buf; exit }
					if (depth == 1) last = buf
					continue
				}
				buf = buf c
				continue
			}
			if (c == "\"") { instr = 1; buf = ""; continue }
			if (c == "{" || c == "[") depth++
			else if (c == "}" || c == "]") depth--
			else if (c == ":" && depth == 1 && last == key) {
				j = i + 1
				while (substr(s, j, 1) == " ") j++
				if (substr(s, j, 1) == "\"") want = 1
				else exit
			} else if (c == ",") last = ""
		}
	}'
}

# configs 객체를 "키<TAB>값" 줄로 출력. null은 출력하지 않음
configs_lines() {
	awk '
	{
		s = $0; n = length(s)
		p = index(s, "\"configs\"")
		if (p == 0) exit
		i = p + 9
		while (i <= n && substr(s, i, 1) != "{") i++
		i++
		while (i <= n) {
			while (i <= n && index(" ,\t", substr(s, i, 1))) i++
			c = substr(s, i, 1)
			if (c == "}" || i > n) break
			# 키
			i++; key = ""
			while (i <= n) {
				c = substr(s, i, 1)
				if (c == "\\") { key = key substr(s, i + 1, 1); i += 2; continue }
				if (c == "\"") { i++; break }
				key = key c; i++
			}
			while (i <= n && substr(s, i, 1) != ":") i++
			i++
			while (i <= n && substr(s, i, 1) == " ") i++
			c = substr(s, i, 1)
			if (c == "\"") {
				i++; val = ""
				while (i <= n) {
					c = substr(s, i, 1)
					if (c == "\\") {
						d = substr(s, i + 1, 1)
						if (d == "n") val = val " "
						else if (d == "t") val = val " "
						else val = val d
						i += 2; continue
					}
					if (c == "\"") { i++; break }
					val = val c; i++
				}
				print key "\t" val
			} else if (c == "{" || c == "[") {
				depth = 0; st = i
				while (i <= n) {
					c = substr(s, i, 1)
					if (c == "{" || c == "[") depth++
					else if (c == "}" || c == "]") { depth--; if (depth == 0) { i++; break } }
					i++
				}
				print key "\t" substr(s, st, i - st)
			} else {
				st = i
				while (i <= n && !index(",}", substr(s, i, 1))) i++
				val = substr(s, st, i - st)
				gsub(/ +$/, "", val)
				if (val != "null") print key "\t" val
			}
		}
	}'
}

# protected_keys 배열의 값 목록
protected_keys() {
	grep -o '"protected_keys"[[:space:]]*:[[:space:]]*\[[^]]*\]' | grep -o '"[^"]*"' | sed -n '2,$p' | tr -d '"'
}

# 소나 API에서 필요한 프로파일만 조회 (-t: 유형 필터, -n: 이름마다 검색어 필터)
fetch() {
	# API 키가 프로세스 목록에 보이지 않도록 curl 설정을 표준 입력으로 전달
	STATUS=$(printf 'header = "Authorization: Bearer %s"\n' "$(cfg_esc "$API_KEY")" | \
		curl -s -S -k --max-time 30 -K - -o "$WORK_DIR/profiles" -w '%{http_code}' -G \
		--data-urlencode "$1" "$SONAR/api/sonar/connect-profiles" 2>"$WORK_DIR/curl_err")
	if [ "$STATUS" = "000" ] || [ -z "$STATUS" ]; then
		echo "$(t "[실패] 소나에 접속하지 못했습니다" "[FAIL] Cannot connect to Sonar"): $(head -c 300 "$WORK_DIR/curl_err")"
		exit 1
	elif [ "$STATUS" = "401" ] || [ "$STATUS" = "403" ]; then
		echo "[$(t "실패" "FAIL")] HTTP $STATUS - $(t "소나 API 키가 틀렸거나 접속 프로파일 조회 권한이 없습니다." "Invalid Sonar API key or no permission to read connect profiles.")"
		exit 1
	elif [ "$STATUS" != "200" ]; then
		echo "[$(t "실패" "FAIL")] HTTP $STATUS $(head -c 300 "$WORK_DIR/profiles")"
		exit 1
	fi
	split_profiles < "$WORK_DIR/profiles" >> "$WORK_DIR/lines"
}

: > "$WORK_DIR/lines"
[ -n "$TYPE" ] && fetch "type=$TYPE"
if [ -n "$NAMES" ]; then
	IFS=',' read -r -a wanted <<< "$NAMES"
	for w in "${wanted[@]}"; do
		fetch "keywords=$w"
	done
fi
# 여러 조회에 같은 프로파일이 나오면 한 번만 처리
sort -u "$WORK_DIR/lines" -o "$WORK_DIR/lines"

TS=$(date +%Y%m%d_%H%M%S)
COUNT=0
FOUND_NAMES=","
while IFS= read -r line; do
	code=$(printf '%s' "$line" | top_str code)
	pname=$(printf '%s' "$line" | top_str name)
	ptype=$(printf '%s' "$line" | top_str type)
	match=0
	if [ -n "$TYPE" ] && [ "$ptype" = "$TYPE" ]; then
		match=1
	fi
	if [ -n "$NAMES" ]; then
		IFS=',' read -r -a wanted <<< "$NAMES"
		for w in "${wanted[@]}"; do
			if [ "$w" = "$code" ] || [ "$w" = "$pname" ]; then
				match=1
				FOUND_NAMES="$FOUND_NAMES$w,"
			fi
		done
	fi
	[ "$match" -eq 1 ] || continue

	# 파일 이름에는 영문·숫자·_·- 만 사용 (경로 문자 차단)
	safe=$(printf '%s' "$code" | tr -c 'A-Za-z0-9_-' '_')
	out="$(pwd)/profile_${safe}_${TS}.txt"
	{
		echo "# $(t "로그프레소 소나 접속 프로파일" "Logpresso Sonar connect profile") (sonar_profile_export.sh, $(date '+%Y-%m-%d %H:%M:%S'))"
		echo "sonar=$SONAR"
		echo "profile_code=$code"
		echo "profile_name=$pname"
		echo "profile_type=$ptype"
		prot=$(printf '%s' "$line" | protected_keys | paste -sd, -)
		echo "protected_keys=$prot"
		# 보호 항목은 소나가 빈 값으로 주므로 저장하지 않음 (빈 값과 헷갈리지 않게)
		printf '%s' "$line" | configs_lines | while IFS="$(printf '\t')" read -r k v; do
			case ",$prot," in
				*",$k,"*) continue ;;
			esac
			echo "config.$k=$(printf '%s' "$v" | tr -s ' ')"
		done
	} > "$out"
	COUNT=$((COUNT + 1))

	echo ""
	echo "[$COUNT] $(t "식별자" "Identifier"): $code"
	echo "    $(t "이름" "Name"): $pname"
	echo "    $(t "유형" "Type"): $ptype"
	echo "    $(t "설정" "Settings"):"
	grep '^config\.' "$out" | sed 's/^config\./      /'
	echo "    $(t "저장 파일" "Saved file"): $out"
done < "$WORK_DIR/lines"

if [ -n "$NAMES" ]; then
	IFS=',' read -r -a wanted <<< "$NAMES"
	for w in "${wanted[@]}"; do
		case "$FOUND_NAMES" in
			*",$w,"*) ;;
			*) echo ""; echo "[$(t "실패" "FAIL")] $(t "식별자나 이름이 '$w'인 접속 프로파일이 없습니다." "No connect profile with identifier or name '$w'.")" ;;
		esac
	done
fi

echo ""
if [ "$COUNT" -eq 0 ]; then
	echo "$(t "저장한 프로파일이 없습니다. 식별자나 유형을 확인하세요." "No profile exported. Check the identifier or type.")"
	exit 1
fi
echo "$(t "저장한 프로파일" "Exported profiles"): $COUNT"
echo "$(t "보호 항목(암호, 서비스 키 등)은 소나가 제공하지 않아 파일에 포함되지 않습니다." "Protected values (password, service key, etc.) are not provided by Sonar and are not included.")"
echo "$(t "파일을 외부 통신이 되는 서버로 옮겨 앱별 점검 스크립트의 -f 옵션으로 사용하세요." "Copy the files to a server with internet access and pass them to the app check script with -f.")"

jamf_permission_check.sh

#!/usr/bin/env bash
#
# Jamf Pro 앱 권한 점검 스크립트 / Jamf Pro app permission check script
#
# 로그프레소 Jamf Pro 앱과 같은 방식(계정/암호로 토큰 발급 후 API 호출)으로 Jamf Pro API를 호출해 보고,
# 계정이 가진 권한과 어느 단계에서 권한 오류(403 INVALID_PRIVILEGE)가 나는지 알려줍니다.
# 데이터를 변경하는 호출은 하지 않으며, 조회는 1건만 요청합니다.
#
# 요구 사항 / Requirements: Linux, bash, curl (Jamf Pro에 접속할 수 있는 서버에서 실행)
#
# 사용법 / Usage:
#   sh jamf_permission_check.sh -f profile_FILE.txt [-p IP:PORT] [-l en]
#
#   -f  sonar_profile_export.sh 가 만든 프로파일 파일 (API 주소, 계정을 읽음) / profile file
#   -p  이 서버에서 Jamf Pro에 접속할 때 쓸 HTTP 프록시 / HTTP proxy for this server
#   -l  출력 언어 ko(기본) 또는 en / output language ko (default) or en
#
# 암호는 실행 중에 입력받습니다 (화면에 표시되지 않음). JAMF_PASSWORD 환경 변수로도 줄 수 있습니다.
# 결과는 화면과 함께 jamf_check_result_<실행시간>.txt 파일에 저장됩니다 (소유자만 읽기 가능).
# 암호와 토큰 값은 출력하거나 저장하지 않습니다.

# sh(dash 등)로 실행해도 bash 로 다시 실행
if [ -z "$BASH_VERSION" ] || case ":$SHELLOPTS:" in *:posix:*) true ;; *) false ;; esac; then
	exec bash "$0" "$@"
fi

# 만드는 파일은 소유자만 읽을 수 있게
umask 077

PROFILE_FILE=""
ENDPOINT=""
JAMF_USER=""
PROXY=""
LANG_OPT=ko

usage() {
	sed -n '11,16p' "$0" | sed 's/^# \{0,1\}//'
	exit 2
}

while getopts "f:p:l:h" opt; do
	case "$opt" in
		f) PROFILE_FILE="$OPTARG" ;;
		p) PROXY="$OPTARG" ;;
		l) LANG_OPT="$OPTARG" ;;
		*) usage ;;
	esac
done

# 언어별 문구 선택 / pick message by language
t() {
	if [ "$LANG_OPT" = "en" ]; then printf '%s' "$2"; else printf '%s' "$1"; fi
}

# 프로파일 파일에서 API 주소, 계정을 읽음
[ -z "$PROFILE_FILE" ] && usage
if [ -n "$PROFILE_FILE" ]; then
	if [ ! -r "$PROFILE_FILE" ]; then
		echo "$(t "프로파일 파일을 읽을 수 없습니다" "Cannot read profile file"): $PROFILE_FILE"
		exit 2
	fi
	pf_get() {
		grep "^$1=" "$PROFILE_FILE" | head -n 1 | cut -d= -f2-
	}
	F_ENDPOINT=$(pf_get config.endpoint)
	F_USER=$(pf_get config.username)
	F_PROXY=$(pf_get config.http_proxy)
	ENDPOINT="${F_ENDPOINT%/}"
	JAMF_USER="$F_USER"
fi
if [ -z "$ENDPOINT" ] || [ -z "$JAMF_USER" ]; then
	echo "$(t "프로파일 파일에 API 주소(config.endpoint) 또는 계정(config.username)이 없습니다" "The profile file has no API endpoint (config.endpoint) or account (config.username)"): $PROFILE_FILE"
	exit 2
fi

if ! command -v curl >/dev/null 2>&1; then
	echo "curl $(t "명령이 필요합니다." "is required.")"
	exit 2
fi

JAMF_PASS="${JAMF_PASSWORD:-}"
if [ -z "$JAMF_PASS" ]; then
	printf '%s: ' "$(t "$JAMF_USER 계정의 암호" "Password of $JAMF_USER")"
	read -r -s JAMF_PASS
	echo ""
fi

RESULT_FILE="$(pwd)/jamf_check_result_$(date +%Y%m%d_%H%M%S).txt"
: > "$RESULT_FILE"
WORK_DIR="$(mktemp -d)"
chmod 700 "$WORK_DIR"
TOKEN=""

cleanup() {
	# 발급받은 토큰은 점검이 끝나면 바로 무효화
	if [ -n "$TOKEN" ]; then
		auth_cfg "$TOKEN" | curl "${CURL_OPTS[@]}" -K - -o /dev/null -X POST \
			"$ENDPOINT/api/v1/auth/invalidate-token" 2>/dev/null
	fi
	rm -rf "$WORK_DIR"
}
trap cleanup EXIT

PROBLEMS=()
FIXES=()

log() {
	echo "$*"
	echo "$*" >> "$RESULT_FILE"
}

add_problem() {
	PROBLEMS+=("$1")
	FIXES+=("$2")
}

# 실패한 호출의 응답 원문을 결과 파일 부록용으로 모음. $1=제목, $2=요청 설명
save_raw() {
	{
		echo ""
		echo "### $1"
		echo "$(t "요청" "Request"): $2"
		echo "HTTP: $HTTP_STATUS"
		head -c 3000 "$WORK_DIR/body"
		echo ""
	} >> "$WORK_DIR/appendix"
}

finish() {
	log ""
	log "======================================================================"
	if [ ${#PROBLEMS[@]} -gt 0 ]; then
		log "$(t "요약: 문제" "Summary: problems") ${#PROBLEMS[@]}$(t "건" "")"
		for i in "${!PROBLEMS[@]}"; do
			log ""
			log "  $((i + 1)). ${PROBLEMS[$i]}"
			if [ -n "${FIXES[$i]}" ]; then
				prefix="-> $(t "조치" "Action"):"
				while IFS= read -r line; do
					log "     $prefix $line"
					prefix="        "
				done <<< "${FIXES[$i]}"
			fi
		done
	else
		log "$(t "요약: 모든 점검 통과" "Summary: all checks passed")"
	fi
	log "======================================================================"
	if [ -s "$WORK_DIR/appendix" ]; then
		{
			echo ""
			echo "$(t "부록: 실패한 호출의 Jamf Pro 응답 원문 (원인 분석용, 로그프레소에 전달)" "Appendix: raw Jamf Pro responses of failed calls (send to Logpresso for analysis)")"
			echo "----------------------------------------------------------------------"
			cat "$WORK_DIR/appendix"
		} >> "$RESULT_FILE"
		echo ""
		echo "$(t "실패한 호출의 응답 원문은 결과 파일 끝 '부록'에 저장했습니다." "Raw responses of failed calls are saved in the appendix at the end of the result file.")"
	fi
	echo ""
	echo "$(t "결과 파일" "Result file"): $RESULT_FILE"
	[ ${#PROBLEMS[@]} -gt 0 ] && exit 1
	exit 0
}

# JSON 에서 첫 번째 문자열 값 추출 (jq 없이)
json_str() {
	grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -n 1 | sed "s/^\"$1\"[[:space:]]*:[[:space:]]*\"//; s/\"\$//"
}

# JSON 에서 첫 번째 숫자/불린 값 추출
json_val() {
	grep -o "\"$1\"[[:space:]]*:[[:space:]]*[-0-9a-z]*" | head -n 1 | sed 's/.*:[[:space:]]*//'
}

CURL_OPTS=(-s -S --max-time 60)
[ -n "$PROXY" ] && CURL_OPTS+=(-x "$PROXY")

# curl 설정 파일 값의 따옴표·역슬래시 처리
cfg_esc() {
	printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

# 토큰 인증 헤더를 curl 설정 줄로 출력 (토큰이 프로세스 목록에 보이지 않게 표준 입력으로 넘김)
auth_cfg() {
	printf 'header = "Authorization: Bearer %s"\n' "$(cfg_esc "$1")"
}

# 토큰으로 GET 호출
api_get() {
	HTTP_STATUS=$(auth_cfg "$TOKEN" | curl "${CURL_OPTS[@]}" -K - -o "$WORK_DIR/body" -w '%{http_code}' \
		-H "Accept: application/json" "$ENDPOINT$1" 2>"$WORK_DIR/curl_err")
	if [ "$HTTP_STATUS" = "000" ] || [ -z "$HTTP_STATUS" ]; then
		HTTP_STATUS="000"
		cp "$WORK_DIR/curl_err" "$WORK_DIR/body"
	fi
}

# Jamf Pro 오류 응답의 code/description 추출
jamf_error() {
	local code desc
	code=$(json_str code < "$WORK_DIR/body")
	desc=$(json_str description < "$WORK_DIR/body")
	if [ -n "$code$desc" ]; then
		echo "$code $desc"
	else
		head -c 200 "$WORK_DIR/body" | tr '\n' ' '
	fi
}

ACCOUNT_PATH="Jamf Pro > Settings > System > User accounts and groups > ($JAMF_USER)"

log "======================================================================"
log "$(t "Jamf Pro 앱 권한 점검" "Jamf Pro app permission check")  ($(date '+%Y-%m-%d %H:%M:%S'))"
log "======================================================================"
log ""
if [ -n "$PROFILE_FILE" ]; then
	log "  $(t "프로파일 파일" "Profile file"): $PROFILE_FILE"
	log "  $(t "식별자" "Identifier"): $(pf_get profile_code)"
	log "  $(t "이름" "Name"): $(pf_get profile_name)"
fi
log "  $(t "API 주소" "API endpoint"): $ENDPOINT"
log "  $(t "계정" "Account"): $JAMF_USER"
log "  $(t "HTTP 프록시" "HTTP proxy"): ${PROXY:-$(t "(사용 안 함)" "(none)")}"
if [ -n "$F_PROXY" ] && [ -z "$PROXY" ]; then
	log "  -> $(t "소나 서버는 프록시($F_PROXY)로 Jamf Pro에 접속합니다. 이 서버도 프록시가 필요하면 -p 옵션으로 지정하세요." "Sonar connects to Jamf Pro through proxy $F_PROXY. Use -p if this server also needs a proxy.")"
fi

# 1. 토큰 발급
log ""
log "[1] $(t "토큰 발급 (앱과 같은 방식: 계정/암호로 POST /api/v1/auth/token)" "Token (same as the app: POST /api/v1/auth/token with account/password)")"
# 암호가 프로세스 목록에 보이지 않도록 curl 설정을 표준 입력으로 전달
CRED=$(cfg_esc "$JAMF_USER:$JAMF_PASS")
HTTP_STATUS=$(printf 'user = "%s"\n' "$CRED" | curl "${CURL_OPTS[@]}" -K - \
	-o "$WORK_DIR/body" -w '%{http_code}' -X POST -H "Accept: application/json" \
	"$ENDPOINT/api/v1/auth/token" 2>"$WORK_DIR/curl_err")
JAMF_PASS=""
CRED=""
if [ "$HTTP_STATUS" = "000" ] || [ -z "$HTTP_STATUS" ]; then
	HTTP_STATUS="000"
	cp "$WORK_DIR/curl_err" "$WORK_DIR/body"
fi
TOKEN=$(json_str token < "$WORK_DIR/body")
if [ "$HTTP_STATUS" = "200" ] && [ -n "$TOKEN" ]; then
	log "  [$(t "정상" "OK")] $(t "토큰 발급 성공" "Token issued")"
else
	TOKEN=""
	log "  [$(t "실패" "FAIL")] HTTP $HTTP_STATUS $(jamf_error)"
	save_raw "$(t "토큰 발급 실패" "Token request failed")" "POST $ENDPOINT/api/v1/auth/token"
	case "$HTTP_STATUS" in
		000) add_problem "$(t "Jamf Pro에 접속하지 못함" "Cannot connect to Jamf Pro")" \
			"$(t "API 주소($ENDPOINT)가 맞는지, 이 서버에서 Jamf Pro로 HTTPS 통신이 되는지 확인하세요. 프록시가 필요하면 -p 옵션으로 지정하세요." "Check the API endpoint ($ENDPOINT) and HTTPS access from this server. Use -p if a proxy is required.")" ;;
		401) add_problem "$(t "계정 또는 암호가 틀렸거나 계정이 비활성 상태" "Wrong account or password, or the account is disabled")" \
			"$(t "$ACCOUNT_PATH 에서 계정의 Access Status 가 Enabled 인지 확인하고, 소나 접속 프로파일의 계정·암호를 다시 입력하세요." "In $ACCOUNT_PATH, check that Access Status is Enabled, and re-enter the account and password in the connect profile.")" ;;
		404) add_problem "$(t "API 주소가 Jamf Pro 주소가 아님 (HTTP 404)" "The API endpoint is not a Jamf Pro address (HTTP 404)")" \
			"$(t "API 주소를 https://TENANT.jamfcloud.com 형식(경로 없이)으로 입력했는지 확인하세요." "Use the https://TENANT.jamfcloud.com format without a path.")" ;;
		*) add_problem "$(t "토큰 발급 실패" "Token request failed") (HTTP $HTTP_STATUS)" \
			"$(t "결과 파일의 부록을 로그프레소에 전달하세요." "Send the appendix of the result file to Logpresso.")" ;;
	esac
	finish
fi

# Jamf Pro 버전 (권한 불필요)
api_get "/api/v1/jamf-pro-version"
JAMF_VERSION=$(json_str version < "$WORK_DIR/body")
log "  $(t "Jamf Pro 버전" "Jamf Pro version"): ${JAMF_VERSION:-$(t "(확인 불가)" "(unknown)")}"

# 2. 계정 권한
log ""
log "[2] $(t "계정이 실제로 가진 권한 (GET /api/v1/auth)" "Privileges the account actually has (GET /api/v1/auth)")"
api_get "/api/v1/auth"
HAS_READ_COMPUTERS=unknown
NONE=$(t "(응답에 없음)" "(not in response)")
if [ "$HTTP_STATUS" = "200" ]; then
	tr -d '\n\r' < "$WORK_DIR/body" > "$WORK_DIR/auth"
	A_USER=$(json_str username < "$WORK_DIR/auth")
	A_LEVEL=$(json_str accessLevel < "$WORK_DIR/auth")
	A_SET=$(json_str privilegeSet < "$WORK_DIR/auth")
	A_TYPE=$(json_str authenticationType < "$WORK_DIR/auth")
	log "  $(t "계정" "Account"): ${A_USER:-$NONE}"
	log "  $(t "인증 방식" "Authentication type")(authenticationType): ${A_TYPE:-$NONE}"
	log "  $(t "접근 수준" "Access level")(accessLevel): ${A_LEVEL:-$NONE}"
	log "  $(t "권한 세트" "Privilege set")(privilegeSet): ${A_SET:-$NONE}"
	# 권한 이름 중 컴퓨터 관련 항목만 표시
	grep -o '"[A-Za-z ]*Computer[A-Za-z ]*"' "$WORK_DIR/auth" | tr -d '"' | sort -u > "$WORK_DIR/computer_privs"
	if [ -s "$WORK_DIR/computer_privs" ]; then
		log "  $(t "컴퓨터 관련 권한" "Computer related privileges"):"
		while IFS= read -r line; do
			log "    - $line"
		done < "$WORK_DIR/computer_privs"
	else
		log "  $(t "컴퓨터 관련 권한: (없음)" "Computer related privileges: (none)")"
	fi
	if grep -qx "Read Computers" "$WORK_DIR/computer_privs"; then
		HAS_READ_COMPUTERS=yes
		log "  -> $(t "'Read Computers' 권한이 있습니다." "The account has 'Read Computers'.")"
	else
		HAS_READ_COMPUTERS=no
		log "  -> $(t "'Read Computers' 권한이 없습니다. jamf-pro-computers 명령과 감사 로그 수집에 필요한 권한입니다." "The account does not have 'Read Computers', which jamf-pro-computers and the audit logger need.")"
	fi
	if [ -n "$A_LEVEL" ] && [ "$A_LEVEL" != "FullAccess" ]; then
		log "  -> $(t "접근 수준이 FullAccess 가 아닙니다($A_LEVEL). 사이트/그룹 단위로 권한이 제한될 수 있습니다." "Access level is not FullAccess ($A_LEVEL). Privileges may be limited to a site or group.")"
	fi
else
	log "  [$(t "참고" "INFO")] $(t "계정 권한을 조회하지 못했습니다" "Could not read account privileges") (HTTP $HTTP_STATUS $(jamf_error)). $(t "점검은 계속합니다." "Continuing.")"
	save_raw "$(t "계정 권한 조회 실패" "Privilege lookup failed")" "GET /api/v1/auth"
fi

# 3. 앱과 같은 API 호출
log ""
log "[3] $(t "앱 기능별 API 호출 (각 1건)" "API call per app feature (1 item each)")"
N_INV=$(t "컴퓨터 인벤토리 (jamf-pro-computers)" "Computer inventory (jamf-pro-computers)")
N_LIST=$(t "컴퓨터 목록 (감사 로그 수집기, Classic API)" "Computer list (audit logger, Classic API)")
N_HIST=$(t "컴퓨터 이력 (감사 로그 수집기, Classic API)" "Computer history (audit logger, Classic API)")
INV="/api/v1/computers-inventory?section=GENERAL&section=OPERATING_SYSTEM&section=HARDWARE&section=USER_AND_LOCATION&page=0&page-size=1"
api_get "$INV"
INV_STATUS="$HTTP_STATUS"
if [ "$HTTP_STATUS" = "200" ]; then
	log "  [$(t "정상" "OK")] $N_INV"
else
	INV_ERR=$(jamf_error)
	log "  [$(t "실패" "FAIL")] $N_INV"
	log "         HTTP $HTTP_STATUS $INV_ERR"
	save_raw "$N_INV" "GET $INV"
	# 특정 섹션 때문인지 구분하기 위해 GENERAL 섹션만으로 다시 조회
	if [ "$HTTP_STATUS" = "403" ]; then
		api_get "/api/v1/computers-inventory?section=GENERAL&page=0&page-size=1"
		if [ "$HTTP_STATUS" = "200" ]; then
			INV_GENERAL_OK=yes
			log "         [$(t "비교" "COMPARE")] $(t "GENERAL 섹션만 조회하면 성공 -> 운영체제/하드웨어/사용자·위치 섹션 중 일부에 권한이 없습니다." "GENERAL section only works -> no permission for some of OS, hardware, user/location sections.")"
		else
			log "         [$(t "비교" "COMPARE")] $(t "GENERAL 섹션만 조회해도 HTTP $HTTP_STATUS -> 컴퓨터 인벤토리 자체에 권한이 없습니다." "GENERAL section only also returns HTTP $HTTP_STATUS -> no permission for computer inventory itself.")"
			save_raw "$N_INV (GENERAL)" "GET /api/v1/computers-inventory?section=GENERAL"
		fi
	fi
fi

api_get "/JSSResource/computers"
if [ "$HTTP_STATUS" = "200" ]; then
	log "  [$(t "정상" "OK")] $N_LIST"
	FIRST_ID=$(json_val id < "$WORK_DIR/body")
	if [ -n "$FIRST_ID" ]; then
		api_get "/JSSResource/computerhistory/id/$FIRST_ID"
		if [ "$HTTP_STATUS" = "200" ]; then
			log "  [$(t "정상" "OK")] $N_HIST"
		else
			log "  [$(t "실패" "FAIL")] $N_HIST"
			log "         HTTP $HTTP_STATUS $(jamf_error)"
			save_raw "$N_HIST" "GET /JSSResource/computerhistory/id/$FIRST_ID"
			add_problem "$(t "감사 로그 수집기의 컴퓨터 이력 조회 실패" "Computer history lookup of the audit logger failed") (HTTP $HTTP_STATUS)" \
				"$(t "$ACCOUNT_PATH > Privileges 탭에서 Computers의 Read 가 체크되어 있는지 확인하세요." "In $ACCOUNT_PATH > Privileges tab, check that Read is selected for Computers.")"
		fi
	else
		log "  [$(t "참고" "INFO")] $(t "등록된 컴퓨터가 없어 컴퓨터 이력 조회는 건너뜁니다." "No computers registered, computer history skipped.")"
	fi
	CLASSIC_OK=yes
else
	log "  [$(t "실패" "FAIL")] $N_LIST"
	log "         HTTP $HTTP_STATUS $(jamf_error)"
	save_raw "$N_LIST" "GET /JSSResource/computers"
	CLASSIC_OK=no
	CLASSIC_STATUS="$HTTP_STATUS"
fi

# 요약: [2] 권한 정보와 [3] 결과를 합쳐 원인을 좁힘
if [ "$INV_STATUS" = "403" ] || { [ "$CLASSIC_OK" = "no" ] && [ "$CLASSIC_STATUS" = "403" ]; }; then
	FAILED=""
	[ "$INV_STATUS" = "403" ] && FAILED="jamf-pro-computers"
	[ "$CLASSIC_OK" = "no" ] && FAILED="${FAILED:+$FAILED, }$(t "감사 로그 수집기" "audit logger")"
	if [ "$HAS_READ_COMPUTERS" = "no" ]; then
		add_problem "$(t "계정에 'Read Computers' 권한이 없음" "The account does not have 'Read Computers'") ($(t "실패" "failed"): $FAILED)" \
			"$(t "$ACCOUNT_PATH 에서" "In $ACCOUNT_PATH,")
$(t "Account 탭의 Privilege Set을 설치 매뉴얼대로 Auditor로 지정하거나," "set Privilege Set to Auditor on the Account tab as in the install guide, or")
$(t "Privileges 탭 > Jamf Pro Server Objects > Computers의 Read를 체크한 뒤 Save 하세요." "select Read for Computers under Privileges tab > Jamf Pro Server Objects, then Save.")"
	elif [ "$INV_GENERAL_OK" = "yes" ]; then
		add_problem "$(t "컴퓨터 인벤토리 일부 섹션에 권한 없음" "No permission for some computer inventory sections") ($(t "실패" "failed"): $FAILED)" \
			"$(t "Privilege Set을 Auditor로 지정하면 필요한 읽기 권한이 모두 포함됩니다" "Privilege Set Auditor includes all required read privileges"): $ACCOUNT_PATH > Account
$(t "이미 Auditor 인데도 실패하면 결과 파일을 로그프레소에 전달하세요." "If it is already Auditor, send the result file to Logpresso.")"
	elif [ "$HAS_READ_COMPUTERS" = "yes" ]; then
		add_problem "$(t "'Read Computers' 권한이 있는데도 거부됨" "Denied although the account has 'Read Computers'") ($(t "실패" "failed"): $FAILED)" \
			"$(t "접근 수준(accessLevel)이 FullAccess 가 아니면 사이트 단위로 제한될 수 있습니다" "If access level is not FullAccess, access may be limited to a site"): $ACCOUNT_PATH > Account
$(t "권한을 바꾼 직후라면 잠시 후 다시 실행하고, 같은 결과면 결과 파일을 로그프레소에 전달하세요." "If privileges were just changed, retry later; otherwise send the result file to Logpresso.")"
	else
		add_problem "$(t "컴퓨터 조회 권한 부족" "Missing computer read permission") ($(t "실패" "failed"): $FAILED)" \
			"$(t "$ACCOUNT_PATH 에서" "In $ACCOUNT_PATH, check that")
$(t "1) Account 탭의 Privilege Set 이 Auditor(또는 Administrator)인지" "1) Privilege Set on the Account tab is Auditor (or Administrator)")
$(t "2) Custom 이면 Privileges 탭 > Jamf Pro Server Objects > Computers의 Read 가 체크되어 있는지 확인하세요." "2) if Custom, Read is selected for Computers under Privileges tab > Jamf Pro Server Objects.")"
	fi
elif [ "$INV_STATUS" != "200" ]; then
	add_problem "$(t "컴퓨터 인벤토리 조회 실패" "Computer inventory lookup failed") (HTTP $INV_STATUS $INV_ERR)" \
		"$(t "결과 파일의 부록을 로그프레소에 전달하세요." "Send the appendix of the result file to Logpresso.")"
fi
if [ "$CLASSIC_OK" = "no" ] && [ "$CLASSIC_STATUS" != "403" ]; then
	add_problem "$(t "감사 로그 수집기의 컴퓨터 목록 조회 실패" "Computer list lookup of the audit logger failed") (HTTP $CLASSIC_STATUS)" \
		"$(t "결과 파일의 부록을 로그프레소에 전달하세요." "Send the appendix of the result file to Logpresso.")"
fi

finish