fix(git-tools): unify PR edit identity proof (#1080)
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
This commit is contained in:
@@ -21,7 +21,16 @@ AUTH_CONFIG=""
|
|||||||
cleanup() {
|
cleanup() {
|
||||||
[[ -z "$AUTH_CONFIG" ]] || rm -f -- "$AUTH_CONFIG"
|
[[ -z "$AUTH_CONFIG" ]] || rm -f -- "$AUTH_CONFIG"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT HUP INT TERM
|
terminate() {
|
||||||
|
local signal="$1"
|
||||||
|
trap - "$signal"
|
||||||
|
cleanup
|
||||||
|
kill -s "$signal" "$$"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap 'terminate HUP' HUP
|
||||||
|
trap 'terminate INT' INT
|
||||||
|
trap 'terminate TERM' TERM
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
@@ -129,24 +138,29 @@ case "$PLATFORM" in
|
|||||||
echo "Error: login '$GITEA_LOGIN_NAME' is not configured for target host '$HOST'" >&2
|
echo "Error: login '$GITEA_LOGIN_NAME' is not configured for target host '$HOST'" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
AUTHENTICATED_USER=$(get_gitea_authenticated_user "$GITEA_LOGIN_NAME") || {
|
|
||||||
echo "Error: could not authenticate Gitea login '$GITEA_LOGIN_NAME'" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
[[ "$AUTHENTICATED_USER" == "$IDENTITY" ]] || {
|
|
||||||
echo "Error: Gitea login '$GITEA_LOGIN_NAME' authenticates as '$AUTHENTICATED_USER', not MOSAIC_GIT_IDENTITY '$IDENTITY'" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
AUTH_CONFIG=$(gitea_write_auth_config "$TOKEN") || {
|
AUTH_CONFIG=$(gitea_write_auth_config "$TOKEN") || {
|
||||||
echo "Error: could not stage private Gitea authentication" >&2
|
echo "Error: could not stage private Gitea authentication" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
unset TOKEN
|
unset TOKEN
|
||||||
|
|
||||||
API_BASE="https://${HOST}/api/v1/repos/${REPO_SLUG}"
|
API_BASE="https://${HOST}/api/v1"
|
||||||
# Preflight the explicit host/repo pair before any mutation. This prevents
|
# Resolve identity through the SAME private curl config used for the
|
||||||
# a slug inferred from one checkout being combined with another host.
|
# mutation. Tea login names are globally scoped and can be duplicated
|
||||||
curl -fsS --config "$AUTH_CONFIG" -H "User-Agent: mosaic-pr-edit" "$API_BASE" >/dev/null || {
|
# across hosts; a separate `tea api --login NAME` could validate another
|
||||||
|
# credential than this host-bound token.
|
||||||
|
AUTHENTICATED_USER=$(curl -fsS --config "$AUTH_CONFIG" -H "User-Agent: mosaic-pr-edit" "$API_BASE/user" \
|
||||||
|
| python3 -c 'import json,sys; value=json.load(sys.stdin).get("login"); print(value) if isinstance(value,str) and value else sys.exit(1)') || {
|
||||||
|
echo "Error: could not authenticate the host-bound credential for '$GITEA_LOGIN_NAME'" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$AUTHENTICATED_USER" == "$IDENTITY" ]] || {
|
||||||
|
echo "Error: host-bound credential authenticates as '$AUTHENTICATED_USER', not MOSAIC_GIT_IDENTITY '$IDENTITY'" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
REPO_API="$API_BASE/repos/${REPO_SLUG}"
|
||||||
|
curl -fsS --config "$AUTH_CONFIG" -H "User-Agent: mosaic-pr-edit" "$REPO_API" >/dev/null || {
|
||||||
echo "Error: target repository preflight failed for https://${HOST}/${REPO_SLUG}" >&2
|
echo "Error: target repository preflight failed for https://${HOST}/${REPO_SLUG}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -164,7 +178,7 @@ PY
|
|||||||
)
|
)
|
||||||
curl -fsS --config "$AUTH_CONFIG" -X PATCH \
|
curl -fsS --config "$AUTH_CONFIG" -X PATCH \
|
||||||
-H "User-Agent: mosaic-pr-edit" -H "Content-Type: application/json" \
|
-H "User-Agent: mosaic-pr-edit" -H "Content-Type: application/json" \
|
||||||
-d "$PAYLOAD" "$API_BASE/pulls/${PR_NUMBER}"
|
-d "$PAYLOAD" "$REPO_API/pulls/${PR_NUMBER}"
|
||||||
echo "Updated Gitea pull request #$PR_NUMBER as '$AUTHENTICATED_USER'" >&2
|
echo "Updated Gitea pull request #$PR_NUMBER as '$AUTHENTICATED_USER'" >&2
|
||||||
;;
|
;;
|
||||||
*) echo "Error: Could not detect git platform" >&2; exit 1 ;;
|
*) echo "Error: Could not detect git platform" >&2; exit 1 ;;
|
||||||
|
|||||||
@@ -28,16 +28,24 @@ YAML
|
|||||||
cat > "$BIN_DIR/tea" <<'SH'
|
cat > "$BIN_DIR/tea" <<'SH'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
[[ "$*" == "api --login usc-coder3 /user" ]] && { printf '{"login":"coder3"}\n'; exit 0; }
|
# Deliberately misleading duplicate-name response: the wrapper must never use
|
||||||
[[ "$*" == "api --login same-host-other /user" ]] && { printf '{"login":"other"}\n'; exit 0; }
|
# tea for identity validation because its name lookup is not host-bound.
|
||||||
[[ "$*" == "api --login mosaic-coder3 /user" ]] && { printf '{"login":"coder3"}\n'; exit 0; }
|
[[ "$*" == "api --login duplicate /user" ]] && { printf '{"login":"coder3"}\n'; exit 0; }
|
||||||
exit 1
|
exit 1
|
||||||
SH
|
SH
|
||||||
cat > "$BIN_DIR/curl" <<'SH'
|
cat > "$BIN_DIR/curl" <<'SH'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
printf 'curl' >> "$MOSAIC_TEST_LOG"; printf ' <%s>' "$@" >> "$MOSAIC_TEST_LOG"; printf '\n' >> "$MOSAIC_TEST_LOG"
|
printf 'curl' >> "$MOSAIC_TEST_LOG"; printf ' <%s>' "$@" >> "$MOSAIC_TEST_LOG"; printf '\n' >> "$MOSAIC_TEST_LOG"
|
||||||
[[ " $* " == *" -X PATCH "* ]] && printf '{"number":42,"draft":false}\n' || printf '{"name":"repo"}\n'
|
if [[ "${*: -1}" == */user ]]; then
|
||||||
|
printf '{"login":"%s"}\n' "${MOSAIC_STUB_AUTH_USER:-coder3}"
|
||||||
|
elif [[ "${*: -1}" == */repos/* && " $* " != *" -X PATCH "* ]]; then
|
||||||
|
[[ "${MOSAIC_STUB_SIGNAL:-}" == "TERM" ]] && { kill -TERM "$PPID"; sleep 1; }
|
||||||
|
[[ "${MOSAIC_STUB_SIGNAL:-}" == "INT" ]] && { kill -INT "$PPID"; sleep 1; }
|
||||||
|
printf '{"name":"repo"}\n'
|
||||||
|
else
|
||||||
|
printf '{"number":42,"draft":false}\n'
|
||||||
|
fi
|
||||||
SH
|
SH
|
||||||
cat > "$BIN_DIR/gh" <<'SH'
|
cat > "$BIN_DIR/gh" <<'SH'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@@ -62,12 +70,13 @@ MOSAIC_GIT_IDENTITY=coder3 run_wrapper -n 42 --login mosaic-coder3 -r mosaicstac
|
|||||||
python3 - "$LOG_FILE" <<'PY'
|
python3 - "$LOG_FILE" <<'PY'
|
||||||
import json, pathlib, sys
|
import json, pathlib, sys
|
||||||
lines = pathlib.Path(sys.argv[1]).read_text().splitlines()
|
lines = pathlib.Path(sys.argv[1]).read_text().splitlines()
|
||||||
assert len(lines) == 2, lines
|
assert len(lines) == 3, lines
|
||||||
assert "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack" in lines[0], lines
|
assert "https://git.mosaicstack.dev/api/v1/user" in lines[0], lines
|
||||||
assert "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/pulls/42" in lines[1], lines
|
assert "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack" in lines[1], lines
|
||||||
assert "--config" in lines[0] and "--config" in lines[1], lines
|
assert "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/pulls/42" in lines[2], lines
|
||||||
|
assert all("--config" in line for line in lines), lines
|
||||||
assert "Authorization:" not in "\n".join(lines), lines
|
assert "Authorization:" not in "\n".join(lines), lines
|
||||||
payload = lines[1].split(" <-d> <", 1)[1].split("> <https://", 1)[0]
|
payload = lines[2].split(" <-d> <", 1)[1].split("> <https://", 1)[0]
|
||||||
assert json.loads(payload) == {"title":"New title","body":"Body with `literal` bytes","base":"develop","draft":True}
|
assert json.loads(payload) == {"title":"New title","body":"Body with `literal` bytes","base":"develop","draft":True}
|
||||||
PY
|
PY
|
||||||
assert_no_secret
|
assert_no_secret
|
||||||
@@ -82,7 +91,8 @@ grep -q '"draft": false' "$LOG_FILE"; assert_no_secret
|
|||||||
if run_wrapper -n 42 --login usc-coder3 --draft >/dev/null 2>&1; then echo "Unset identity wrote" >&2; exit 1; fi
|
if run_wrapper -n 42 --login usc-coder3 --draft >/dev/null 2>&1; then echo "Unset identity wrote" >&2; exit 1; fi
|
||||||
[[ ! -s "$LOG_FILE" ]] || { echo "Unset identity reached curl" >&2; exit 1; }
|
[[ ! -s "$LOG_FILE" ]] || { echo "Unset identity reached curl" >&2; exit 1; }
|
||||||
|
|
||||||
# Explicit and ambient same-host wrong principals both refuse before preflight/write.
|
# Explicit and ambient same-host wrong principals both refuse after identity
|
||||||
|
# lookup but before repo preflight/PATCH. The /user read is expected curl #1.
|
||||||
for mode in explicit ambient; do
|
for mode in explicit ambient; do
|
||||||
: > "$LOG_FILE"
|
: > "$LOG_FILE"
|
||||||
if [[ "$mode" == explicit ]]; then
|
if [[ "$mode" == explicit ]]; then
|
||||||
@@ -90,11 +100,12 @@ for mode in explicit ambient; do
|
|||||||
else
|
else
|
||||||
cmd=(); export GITEA_LOGIN=same-host-other
|
cmd=(); export GITEA_LOGIN=same-host-other
|
||||||
fi
|
fi
|
||||||
if MOSAIC_GIT_IDENTITY=coder3 run_wrapper -n 42 "${cmd[@]}" -r USC/uconnect -H git.uscllc.com --draft >/dev/null 2>&1; then
|
if MOSAIC_STUB_AUTH_USER=other MOSAIC_GIT_IDENTITY=coder3 run_wrapper -n 42 "${cmd[@]}" -r USC/uconnect -H git.uscllc.com --draft >/dev/null 2>&1; then
|
||||||
echo "$mode wrong identity wrote" >&2; exit 1
|
echo "$mode wrong identity wrote" >&2; exit 1
|
||||||
fi
|
fi
|
||||||
unset GITEA_LOGIN
|
unset GITEA_LOGIN
|
||||||
[[ ! -s "$LOG_FILE" ]] || { echo "$mode wrong identity reached curl" >&2; exit 1; }
|
[[ "$(wc -l < "$LOG_FILE")" -eq 1 ]] || { echo "$mode wrong identity passed identity lookup" >&2; exit 1; }
|
||||||
|
! grep -q '/repos/' "$LOG_FILE" || { echo "$mode wrong identity reached repo preflight/PATCH" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
|
|
||||||
# Set identity with no explicit/ambient login refuses rather than selecting first host login.
|
# Set identity with no explicit/ambient login refuses rather than selecting first host login.
|
||||||
@@ -104,6 +115,32 @@ if MOSAIC_GIT_IDENTITY=coder3 run_wrapper -n 42 -r USC/uconnect -H git.uscllc.co
|
|||||||
fi
|
fi
|
||||||
[[ ! -s "$LOG_FILE" ]] || { echo "Missing login reached curl" >&2; exit 1; }
|
[[ ! -s "$LOG_FILE" ]] || { echo "Missing login reached curl" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Split-credential probe for the duplicate-name cross-host seam: tea's
|
||||||
|
# name-only /user would report coder3, while the selected host-bound curl token
|
||||||
|
# reports other. The wrapper must trust only the latter handle used by PATCH.
|
||||||
|
: > "$LOG_FILE"
|
||||||
|
if MOSAIC_STUB_AUTH_USER=other MOSAIC_GIT_IDENTITY=coder3 run_wrapper -n 42 --login mosaic-coder3 \
|
||||||
|
-r mosaicstack/stack -H git.mosaicstack.dev --draft >/dev/null 2>&1; then
|
||||||
|
echo "Duplicate-name split credential reached PATCH" >&2; exit 1
|
||||||
|
fi
|
||||||
|
[[ "$(wc -l < "$LOG_FILE")" -eq 1 ]] || { echo "Duplicate-name identity mismatch passed /user" >&2; cat "$LOG_FILE" >&2; exit 1; }
|
||||||
|
! grep -q -- '-X> <PATCH' "$LOG_FILE" || { echo "Duplicate-name mismatch mutated" >&2; exit 1; }
|
||||||
|
|
||||||
|
# TERM and INT during repo preflight clean up, do not mutate, and return the
|
||||||
|
# signal status rather than swallowing termination into success.
|
||||||
|
for sig in TERM INT; do
|
||||||
|
: > "$LOG_FILE"
|
||||||
|
set +e
|
||||||
|
MOSAIC_STUB_SIGNAL="$sig" MOSAIC_GIT_IDENTITY=coder3 run_wrapper -n 42 --login usc-coder3 \
|
||||||
|
-r USC/uconnect -H git.uscllc.com --draft >/dev/null 2>&1
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
[[ "$rc" -ne 0 ]] || { echo "$sig was swallowed into success" >&2; exit 1; }
|
||||||
|
[[ "$rc" -eq 143 || "$rc" -eq 130 ]] || { echo "$sig returned unexpected status $rc" >&2; exit 1; }
|
||||||
|
! grep -q -- '-X> <PATCH' "$LOG_FILE" || { echo "$sig continued into PATCH" >&2; exit 1; }
|
||||||
|
assert_no_secret
|
||||||
|
done
|
||||||
|
|
||||||
# Cross-host credential fails before curl; explicit target preflight failure blocks PATCH.
|
# Cross-host credential fails before curl; explicit target preflight failure blocks PATCH.
|
||||||
: > "$LOG_FILE"
|
: > "$LOG_FILE"
|
||||||
if MOSAIC_GIT_IDENTITY=coder3 run_wrapper -n 42 --login mosaic-coder3 -r USC/uconnect -H git.uscllc.com --draft >/dev/null 2>&1; then
|
if MOSAIC_GIT_IDENTITY=coder3 run_wrapper -n 42 --login mosaic-coder3 -r USC/uconnect -H git.uscllc.com --draft >/dev/null 2>&1; then
|
||||||
|
|||||||
Reference in New Issue
Block a user