chore(tools): add read-only ADFS context collector for closed-contour debugging
This commit is contained in:
129
tools/mock-adfs/collect-context.sh
Normal file
129
tools/mock-adfs/collect-context.sh
Normal file
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
# collect-context.sh — read-only сбор контекста о реальном ADFS ВНУТРИ закрытого контура.
|
||||
#
|
||||
# Ничего не меняет в контуре: только GET discovery/jwks и локальное декодирование
|
||||
# токена БЕЗ проверки подписи. Результат — каталог, который можно вынести наружу
|
||||
# и положить в репозиторий для настройки tools/mock-adfs.
|
||||
#
|
||||
# Использование:
|
||||
# ./collect-context.sh --metadata-url https://<adfs>/adfs/.well-known/openid-configuration
|
||||
# ./collect-context.sh --metadata-url "$ADFS_METADATA_URL" --token-file /tmp/id_token
|
||||
# ADFS_ID_TOKEN=<jwt> ./collect-context.sh --metadata-url "$ADFS_METADATA_URL"
|
||||
#
|
||||
# Опции:
|
||||
# --metadata-url URL URL OIDC discovery (или env ADFS_METADATA_URL)
|
||||
# --token-file FILE файл с реальным id_token (можно взять из DevTools/логов backend)
|
||||
# --ca FILE дополнительный CA (если контур с внутренним PKI)
|
||||
# --out DIR каталог вывода (по умолчанию ./adfs-context)
|
||||
# -h, --help
|
||||
#
|
||||
# Что НЕ собирать и не выносить: ADFS_CLIENT_SECRET, любые пароли, полные логи с токенами.
|
||||
set -euo pipefail
|
||||
|
||||
OUT="${OUT:-adfs-context}"
|
||||
METADATA_URL="${ADFS_METADATA_URL:-}"
|
||||
TOKEN_FILE="${TOKEN_FILE:-}"
|
||||
EXTRA_CA="${EXTRA_CA:-}"
|
||||
|
||||
usage() {
|
||||
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--metadata-url) METADATA_URL="${2:-}"; shift 2 ;;
|
||||
--token-file) TOKEN_FILE="${2:-}"; shift 2 ;;
|
||||
--ca) EXTRA_CA="${2:-}"; shift 2 ;;
|
||||
--out) OUT="${2:-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) printf 'Unknown argument: %s\n' "$1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "${METADATA_URL}" ]; then
|
||||
printf 'ADFS_METADATA_URL or --metadata-url is required\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
command -v python3 >/dev/null 2>&1 || { printf 'python3 not found\n' >&2; exit 1; }
|
||||
command -v curl >/dev/null 2>&1 || { printf 'curl not found\n' >&2; exit 1; }
|
||||
|
||||
mkdir -p "${OUT}"
|
||||
|
||||
CURL=(curl -fsS --max-time 30)
|
||||
if [ -n "${EXTRA_CA}" ]; then
|
||||
CURL+=(--cacert "${EXTRA_CA}")
|
||||
fi
|
||||
|
||||
printf '[*] Discovery: %s\n' "${METADATA_URL}"
|
||||
"${CURL[@]}" "${METADATA_URL}" -o "${OUT}/discovery.json"
|
||||
|
||||
JWKS_URI="$(python3 - "${OUT}/discovery.json" <<'PY'
|
||||
import json, sys
|
||||
meta = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
for key in ("jwks_uri", "userinfo_endpoint", "issuer"):
|
||||
print(f"{key}={meta.get(key, '')}")
|
||||
PY
|
||||
)"
|
||||
|
||||
printf '%s\n' "${JWKS_URI}"
|
||||
JWKS_URL="$(printf '%s\n' "${JWKS_URI}" | sed -n 's/^jwks_uri=//p')"
|
||||
|
||||
if [ -n "${JWKS_URL}" ]; then
|
||||
printf '[*] JWKS: %s\n' "${JWKS_URL}"
|
||||
"${CURL[@]}" "${JWKS_URL}" -o "${OUT}/jwks.json"
|
||||
else
|
||||
printf '[!] jwks_uri отсутствует в discovery — возможно, это не OIDC (SAML/WS-Fed?)\n' >&2
|
||||
fi
|
||||
|
||||
TOKEN="${ADFS_ID_TOKEN:-}"
|
||||
if [ -n "${TOKEN_FILE}" ]; then
|
||||
TOKEN="$(tr -d '\r\n' < "${TOKEN_FILE}")"
|
||||
fi
|
||||
|
||||
if [ -n "${TOKEN}" ]; then
|
||||
TMP_TOKEN="$(mktemp)"
|
||||
trap 'rm -f "${TMP_TOKEN}"' EXIT
|
||||
printf '%s' "${TOKEN}" > "${TMP_TOKEN}"
|
||||
python3 - "${TMP_TOKEN}" "${OUT}/token" <<'PY'
|
||||
import base64, json, sys
|
||||
|
||||
raw = open(sys.argv[1], encoding="utf-8").read().strip()
|
||||
parts = raw.split(".")
|
||||
if len(parts) < 2:
|
||||
sys.exit("Provided value is not a JWT (need header.payload[.signature])")
|
||||
|
||||
def b64url(segment: str) -> bytes:
|
||||
return base64.urlsafe_b64decode(segment + "=" * (-len(segment) % 4))
|
||||
|
||||
header = json.loads(b64url(parts[0]))
|
||||
payload = json.loads(b64url(parts[1]))
|
||||
prefix = sys.argv[2]
|
||||
open(prefix + ".header.json", "w", encoding="utf-8").write(json.dumps(header, indent=2, ensure_ascii=False))
|
||||
|
||||
# Оставляем только значимые для интеграции claim'ы — PII вроде sub/email можно вычистить вручную.
|
||||
interesting = ("upn", "email", "name", "preferred_username", "groups", "group", "role", "roles", "scp", "aud", "iss", "tid")
|
||||
summary = {k: payload[k] for k in interesting if k in payload}
|
||||
open(prefix + ".payload.json", "w", encoding="utf-8").write(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||
open(prefix + ".claims-summary.json", "w", encoding="utf-8").write(json.dumps(summary, indent=2, ensure_ascii=False))
|
||||
print("[*] Decoded id_token. Header alg/kid:", header.get("alg"), header.get("kid"))
|
||||
print("[*] Claim keys:", ", ".join(sorted(payload.keys())))
|
||||
groups = payload.get("groups") or payload.get("group") or []
|
||||
print("[*] groups claim present:", bool(groups), "| sample type:", type(groups[0]).__name__ if groups else "n/a")
|
||||
PY
|
||||
else
|
||||
printf '[*] id_token не передан (--token-file / ADFS_ID_TOKEN). groups-claim не проверен.\n'
|
||||
fi
|
||||
|
||||
cat > "${OUT}/MANIFEST.txt" <<'EOF'
|
||||
Собрано read-only (GET discovery/jwks + локальное decode токена без проверки подписи).
|
||||
Проверьте перед выносом:
|
||||
- discovery.json, jwks.json — публичные, безопасны.
|
||||
- token.payload.json — может содержать PII (sub, email). Вычистите при необходимости.
|
||||
- token.claims-summary.json — только upn/email/name/groups/aud/iss.
|
||||
НЕ выносить: ADFS_CLIENT_SECRET, пароли, сырые подписанные токены, полные логи.
|
||||
Что важно для mock: iss/aud, alg/kid, формат groups (names | SIDs | DNs), endpoint'ы.
|
||||
EOF
|
||||
|
||||
printf '\n[*] Готово: %s\n' "$(cd "${OUT}" && pwd)"
|
||||
printf '[*] Для repo: %s/discovery.json, %s/jwks.json, %s/token.claims-summary.json\n' "${OUT}" "${OUT}" "${OUT}"
|
||||
Reference in New Issue
Block a user