All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
PyYAML raises a ScannerError on a tab used anywhere outside a quoted
scalar (leading/trailing/embedded in a plain value, immediately before or
after a key colon, or as indentation) and yields no token, accepting tabs
ONLY inside single/double-quoted scalars. The conservative block-YAML
fallback in get_gitea_token_for_login normalized those tabs away -- via
_scalar's top-level .strip(), _KEY_RE's [ \t] separators, _Parser.__init__'s
body.rstrip(), parse_seq's body[1:].strip(), and the inline-map emptiness
checks -- and still emitted the login token: a credential fail-open in the
dangerous direction (less conservative than PyYAML).
Extend the whole-document "only ever more conservative than PyYAML, never
less" invariant to tab/scanner parity:
- _KEY_RE now uses SPACE-only separators (` *:` / `[ ](.*)`), so a tab in a
key/value separator makes the line fail to match and the caller fails closed.
- Every whitespace-normalization site strips SPACES only (strip(" ")/rstrip(" "))
so a tab survives to a fail-closed guard instead of being silently removed:
_scalar top-level strip, quoted-trailing strip, post-comment strip,
_Parser.__init__ body rstrip, parse_seq item strip, and the three inline
emptiness checks.
- _scalar fails closed on any tab remaining in a plain scalar.
Tabs strictly inside quoted scalars are preserved verbatim (unchanged parity),
matching exactly what PyYAML accepts.
Verified empirically against PyYAML 6.0.3: ScannerError for each rejected
tab position; string-preserved for quoted inner tabs. Differential fuzz with
tab re-included: 0 fail-opens over ~6000 inputs; 0 over-rejection across 220
PyYAML-accepted quoted-tab cases. Adds section 17 to the regression harness
(fail-close fixtures for trailing/leading/embedded/after-colon/indentation
tabs; parity fixtures for double- and single-quoted inner tabs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
780 lines
31 KiB
Bash
Executable File
780 lines
31 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Regression harness for host-specific Gitea tea login resolution.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/gitea-login-resolution}"
|
|
REPO_DIR="$WORK_DIR/repo"
|
|
BIN_DIR="$WORK_DIR/bin"
|
|
LOG_FILE="$WORK_DIR/calls.log"
|
|
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
|
|
|
rm -rf "$WORK_DIR"
|
|
mkdir -p "$REPO_DIR" "$BIN_DIR"
|
|
|
|
git -C "$REPO_DIR" init -q
|
|
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
|
|
|
|
cat > "$CREDENTIALS_FILE" <<'JSON'
|
|
{
|
|
"gitea": {
|
|
"mosaicstack": {
|
|
"url": "https://git.mosaicstack.dev",
|
|
"token": "mosaic-token"
|
|
},
|
|
"usc": {
|
|
"url": "https://git.uscllc.com",
|
|
"token": "usc-token"
|
|
}
|
|
}
|
|
}
|
|
JSON
|
|
|
|
cat > "$BIN_DIR/tea" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
if [[ "$*" == "login list --output json" ]]; then
|
|
cat <<'JSON'
|
|
[
|
|
{"name":"evil-usc","url":"https://evilgit.uscllc.com","user":"bad.actor"},
|
|
{"name":"usc","url":"https://git.uscllc.com","user":"ci-bot"}
|
|
]
|
|
JSON
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "${1:-}" == "api" ]]; then
|
|
printf '%s\n' '{"login":"ci-bot"}'
|
|
exit 0
|
|
fi
|
|
|
|
printf 'tea %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
|
if [[ "${MOSAIC_TEA_FAIL_PR_CREATE:-}" == "1" && "$*" == pr\ create* ]]; then
|
|
echo 'GetUserByName: simulated stale login failure' >&2
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
SH
|
|
|
|
cat > "$BIN_DIR/curl" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
printf 'curl %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
|
url="${*: -1}"
|
|
case "$url" in
|
|
*/pulls/*.diff)
|
|
printf 'diff --git a/file b/file\n'
|
|
;;
|
|
*/pulls/*)
|
|
printf '{"head":{"sha":"abc123"}}'
|
|
;;
|
|
*/commits/*/status)
|
|
printf '{"state":"success","statuses":[{"context":"ci/mock","status":"success"}]}'
|
|
;;
|
|
*)
|
|
printf '{}'
|
|
;;
|
|
esac
|
|
SH
|
|
|
|
chmod +x "$BIN_DIR/tea" "$BIN_DIR/curl"
|
|
|
|
run_in_repo() {
|
|
(
|
|
cd "$REPO_DIR"
|
|
PATH="$BIN_DIR:$PATH" \
|
|
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
|
MOSAIC_TEST_LOG="$LOG_FILE" \
|
|
"$@"
|
|
)
|
|
}
|
|
|
|
usc_login=$(run_in_repo bash -c '
|
|
export GITEA_LOGIN=mosaicstack
|
|
export GITEA_URL=https://git.mosaicstack.dev
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_login
|
|
')
|
|
if [[ "$usc_login" != "usc" ]]; then
|
|
echo "Expected USC host to resolve tea login 'usc' despite stale mosaicstack env; got '$usc_login'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
usc_login_with_usc_url=$(run_in_repo bash -c '
|
|
export GITEA_LOGIN=mosaicstack
|
|
export GITEA_URL=https://git.uscllc.com
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_login
|
|
')
|
|
if [[ "$usc_login_with_usc_url" != "usc" ]]; then
|
|
echo "Expected USC host to reject stale GITEA_LOGIN even when GITEA_URL matches USC; got '$usc_login_with_usc_url'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
usc_login_without_url=$(run_in_repo bash -c '
|
|
export GITEA_LOGIN=mosaicstack
|
|
unset GITEA_URL
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_login
|
|
')
|
|
if [[ "$usc_login_without_url" != "usc" ]]; then
|
|
echo "Expected USC host to ignore unmatched GITEA_LOGIN without URL; got '$usc_login_without_url'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
git -C "$REPO_DIR" remote set-url origin https://hermes:token@git.uscllc.com/USC/uconnect.git
|
|
embedded_host=$(run_in_repo bash -c '
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_remote_host
|
|
')
|
|
if [[ "$embedded_host" != "git.uscllc.com" ]]; then
|
|
echo "Expected credential-bearing remote host to strip userinfo; got '$embedded_host'" >&2
|
|
exit 1
|
|
fi
|
|
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
|
|
|
override_login=$(run_in_repo bash -c '
|
|
export GITEA_LOGIN=usc
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_login_for_repo_override
|
|
')
|
|
if [[ "$override_login" != "usc" ]]; then
|
|
echo "Expected --repo override path to honor explicit GITEA_LOGIN; got '$override_login'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
: > "$LOG_FILE"
|
|
run_in_repo env GITEA_LOGIN=usc "$SCRIPT_DIR/issue-list.sh" --repo USC/uconnect -n 1
|
|
grep -q -- 'tea issues list --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo "$SCRIPT_DIR/issue-close.sh" -i 42
|
|
grep -q -- 'tea issue close 42 --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
if grep -q -- '--login mosaicstack' "$LOG_FILE"; then
|
|
echo "issue-close.sh used hardcoded mosaicstack login on USC host" >&2
|
|
exit 1
|
|
fi
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo "$SCRIPT_DIR/milestone-list.sh"
|
|
grep -q -- 'tea milestone list --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo "$SCRIPT_DIR/milestone-create.sh" -t "0.2.0" -d "USC milestone"
|
|
grep -q -- 'tea milestones create --title 0.2.0 --description USC milestone --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo "$SCRIPT_DIR/milestone-close.sh" -t "0.2.0"
|
|
grep -q -- 'tea milestone close 0.2.0 --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
if command -v pwsh >/dev/null 2>&1; then
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/issue-list.ps1" -Limit 1
|
|
grep -q -- 'tea issues list --state open --limit 1 --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/issue-create.ps1" -Title "PowerShell issue"
|
|
grep -q -- 'tea issue create --title PowerShell issue --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/pr-list.ps1" -Limit 1
|
|
grep -q -- 'tea pr list --state open --limit 1 --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/pr-create.ps1" -Title "PowerShell PR"
|
|
grep -q -- 'tea pr create --title PowerShell PR --head master --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/pr-merge.ps1" -Number 42 -SkipQueueGuard
|
|
grep -q -- 'tea pr merge 42 --style squash --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo pwsh -NoProfile -File "$SCRIPT_DIR/milestone-create.ps1" -List
|
|
grep -q -- 'tea milestones list --repo USC/uconnect --login usc' "$LOG_FILE"
|
|
fi
|
|
|
|
: > "$LOG_FILE"
|
|
if run_in_repo "$SCRIPT_DIR/pr-diff.sh" --repo USC/uconnect -n 7 >/dev/null 2>&1; then
|
|
echo "Expected pr-diff.sh --repo without host to fail loud" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q -- 'git.mosaicstack.dev/api/v1/repos/USC/uconnect' "$LOG_FILE"; then
|
|
echo "pr-diff.sh --repo defaulted API host to git.mosaicstack.dev" >&2
|
|
exit 1
|
|
fi
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo env GITEA_URL=https://git.uscllc.com "$SCRIPT_DIR/pr-diff.sh" --repo USC/uconnect -n 7 >/dev/null
|
|
grep -q -- 'curl .*https://git.uscllc.com/api/v1/repos/USC/uconnect/pulls/7.diff' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo "$SCRIPT_DIR/pr-ci-wait.sh" --repo USC/uconnect --host git.uscllc.com -n 9 -t 2 -i 1
|
|
grep -q -- 'curl .*https://git.uscllc.com/api/v1/repos/USC/uconnect/pulls/9' "$LOG_FILE"
|
|
grep -q -- 'curl .*https://git.uscllc.com/api/v1/repos/USC/uconnect/commits/abc123/status' "$LOG_FILE"
|
|
|
|
: > "$LOG_FILE"
|
|
run_in_repo env MOSAIC_TEA_FAIL_PR_CREATE=1 GITEA_TOKEN=usc-token GITEA_URL=https://git.uscllc.com "$SCRIPT_DIR/pr-create.sh" -t "USC API fallback" -H feature/pr-create
|
|
grep -q -- 'tea pr create --repo USC/uconnect --login usc --title USC API fallback --head feature/pr-create' "$LOG_FILE"
|
|
grep -q -- 'curl .*Authorization: token usc-token .*https://git.uscllc.com/api/v1/repos/USC/uconnect/pulls' "$LOG_FILE"
|
|
if grep -q -- 'git.mosaicstack.dev/api/v1/repos/USC/uconnect/pulls' "$LOG_FILE"; then
|
|
echo "pr-create.sh API fallback defaulted USC repo to git.mosaicstack.dev" >&2
|
|
exit 1
|
|
fi
|
|
|
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
: > "$LOG_FILE"
|
|
run_in_repo env GITEA_TOKEN=mosaic-token GITEA_URL=https://git.mosaicstack.dev "$SCRIPT_DIR/issue-close.sh" -i 536
|
|
grep -q -- 'curl .*https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/issues/536' "$LOG_FILE"
|
|
if grep -q -- 'tea issue close 536 .*--login mosaicstack' "$LOG_FILE"; then
|
|
echo "issue-close.sh invented a mosaicstack tea login instead of using API fallback" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# #560: loud diagnostic + host-derived login for BOTH instances + override-wins
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Loud diagnostic: a host with no matching tea login must emit an actionable
|
|
# error to stderr (the previous behavior was a SILENT failure). The original
|
|
# mock defines only usc/evil-usc logins, so mosaicstack resolution fails here.
|
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
diag_stderr=$(run_in_repo bash -c '
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_login_for_host git.mosaicstack.dev
|
|
' 2>&1 1>/dev/null || true)
|
|
if ! grep -q "no Gitea tea login matches host 'git.mosaicstack.dev'" <<<"$diag_stderr"; then
|
|
echo "Expected loud diagnostic naming the unresolved host; got: $diag_stderr" >&2
|
|
exit 1
|
|
fi
|
|
if ! grep -q "Available tea logins:" <<<"$diag_stderr"; then
|
|
echo "Expected diagnostic to list available tea logins; got: $diag_stderr" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Both-instance host derivation + override-wins, using a mock that DOES define a
|
|
# mosaicstack login. Scoped to this section so the API-fallback assertions above
|
|
# (which rely on mosaicstack having NO tea login) remain valid.
|
|
BIN_DIR2="$WORK_DIR/bin2"
|
|
mkdir -p "$BIN_DIR2"
|
|
cp "$BIN_DIR/curl" "$BIN_DIR2/curl"
|
|
cat > "$BIN_DIR2/tea" <<'SH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "$*" == "login list --output json" ]]; then
|
|
cat <<'JSON'
|
|
[
|
|
{"name":"mosaicstack","url":"https://git.mosaicstack.dev","user":"ci-bot"},
|
|
{"name":"usc","url":"https://git.uscllc.com","user":"ci-bot"}
|
|
]
|
|
JSON
|
|
exit 0
|
|
fi
|
|
printf 'tea %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
|
exit 0
|
|
SH
|
|
chmod +x "$BIN_DIR2/tea"
|
|
|
|
run_in_repo2() {
|
|
(
|
|
cd "$REPO_DIR"
|
|
PATH="$BIN_DIR2:$PATH" \
|
|
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
|
MOSAIC_TEST_LOG="$LOG_FILE" \
|
|
"$@"
|
|
)
|
|
}
|
|
|
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
mosaic_login=$(run_in_repo2 bash -c 'source "'"$SCRIPT_DIR"'/detect-platform.sh"; get_gitea_login')
|
|
if [[ "$mosaic_login" != "mosaicstack" ]]; then
|
|
echo "Expected mosaicstack origin to derive login 'mosaicstack'; got '$mosaic_login'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
|
usc_login_derived=$(run_in_repo2 bash -c 'source "'"$SCRIPT_DIR"'/detect-platform.sh"; get_gitea_login')
|
|
if [[ "$usc_login_derived" != "usc" ]]; then
|
|
echo "Expected usc origin to derive login 'usc'; got '$usc_login_derived'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Explicit GITEA_LOGIN override is honored when it matches the host.
|
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
|
override_wins=$(run_in_repo2 bash -c 'export GITEA_LOGIN=mosaicstack; source "'"$SCRIPT_DIR"'/detect-platform.sh"; get_gitea_login')
|
|
if [[ "$override_wins" != "mosaicstack" ]]; then
|
|
echo "Expected valid GITEA_LOGIN override to win on mosaicstack host; got '$override_wins'" >&2
|
|
exit 1
|
|
fi
|
|
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# #865 Blocker 1 & 2: get_gitea_token_for_login must resolve the SAME token as
|
|
# PyYAML would (or fail closed identically) even when PyYAML is ABSENT, and must
|
|
# bind the credential to the repo host's scheme + host + EFFECTIVE PORT — not the
|
|
# hostname alone. These fixtures probe the ImportError-dispatched line-parser
|
|
# fallback under FORCED PyYAML absence with adversarial YAML shapes, asserting it
|
|
# NEVER misattributes a token from a nested sub-map or a mis-indented line, strips
|
|
# inline comments like PyYAML, fails closed where PyYAML errors, and rejects a
|
|
# port mismatch while accepting an exact / default-port match. When PyYAML is
|
|
# available the same fixtures also assert the PyYAML path agrees (equivalence).
|
|
# ---------------------------------------------------------------------------
|
|
FIXTURE_XDG="$WORK_DIR/tokenfix"
|
|
NOYAML_DIR="$WORK_DIR/noyaml"
|
|
mkdir -p "$FIXTURE_XDG/tea" "$NOYAML_DIR"
|
|
# A shadow `yaml` module that raises ImportError, forcing the fallback path.
|
|
printf 'raise ImportError("forced-absent for #865 fallback regression")\n' > "$NOYAML_DIR/yaml.py"
|
|
if python3 -c 'import yaml' >/dev/null 2>&1; then HAVE_PYYAML=true; else HAVE_PYYAML=false; fi
|
|
# Confirm the shim really does force ImportError, so the fallback is exercised.
|
|
if python3 -c 'import yaml' >/dev/null 2>&1; then
|
|
if PYTHONPATH="$NOYAML_DIR" python3 -c 'import yaml' >/dev/null 2>&1; then
|
|
echo "FAIL: PyYAML-absence shim did not force ImportError (fallback not exercised)" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
write_fixture() { printf '%s' "$1" > "$FIXTURE_XDG/tea/config.yml"; }
|
|
|
|
# Resolve a token via the FORCED-fallback path (PyYAML shimmed to ImportError).
|
|
token_fallback() {
|
|
(
|
|
cd "$REPO_DIR"
|
|
XDG_CONFIG_HOME="$FIXTURE_XDG" PYTHONPATH="$NOYAML_DIR" bash -c '
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_token_for_login "$1" "$2"
|
|
' _ "$1" "$2"
|
|
) 2>/dev/null || true
|
|
}
|
|
|
|
# Resolve a token via the normal path (uses PyYAML when installed).
|
|
token_pyyaml() {
|
|
(
|
|
cd "$REPO_DIR"
|
|
XDG_CONFIG_HOME="$FIXTURE_XDG" bash -c '
|
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
|
get_gitea_token_for_login "$1" "$2"
|
|
' _ "$1" "$2"
|
|
) 2>/dev/null || true
|
|
}
|
|
|
|
assert_token() {
|
|
local desc="$1" expected="$2" login="$3" host="$4" got
|
|
got=$(token_fallback "$login" "$host")
|
|
if [[ "$got" != "$expected" ]]; then
|
|
echo "FAIL fallback [$desc]: expected [$expected] got [$got]" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$HAVE_PYYAML" == true ]]; then
|
|
got=$(token_pyyaml "$login" "$host")
|
|
if [[ "$got" != "$expected" ]]; then
|
|
echo "FAIL pyyaml [$desc]: expected [$expected] got [$got]" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# 1. Plain, well-formed entry resolves its token.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
'
|
|
assert_token "plain scalar" "TOK_PLAIN" primary git.example
|
|
|
|
# 2. A token nested inside a deeper SUB-MAP must NOT attach to the entry — PyYAML
|
|
# resolves the entry's own token to None here, so the fallback must too.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
extra:
|
|
token: TOK_NESTED_ATTACKER
|
|
- name: other
|
|
url: https://git.example
|
|
token: TOK_OTHER
|
|
'
|
|
assert_token "nested sub-map token is not attributed" "" primary git.example
|
|
assert_token "sibling entry still resolves its own token" "TOK_OTHER" other git.example
|
|
|
|
# 3. A MIS-INDENTED token line (deeper than the entry's fields) must not attach;
|
|
# PyYAML errors on this shape, so both fail closed.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_MISINDENT
|
|
'
|
|
assert_token "mis-indented token fails closed" "" primary git.example
|
|
|
|
# 4. A trailing inline comment on a scalar is stripped, exactly as PyYAML does.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_INLINE # trailing note
|
|
'
|
|
assert_token "inline comment stripped" "TOK_INLINE" primary git.example
|
|
|
|
# 5. A PyYAML-fail-closed case: tab indentation. PyYAML raises a scanner error;
|
|
# the fallback resolves no token. Both fail closed identically.
|
|
write_fixture "$(printf 'logins:\n - name: primary\n url: https://git.example\n\ttoken: TOK_TAB\n')"
|
|
assert_token "tab-indent fails closed like PyYAML" "" primary git.example
|
|
|
|
# 6. Host binding is scheme + host + EFFECTIVE PORT, not hostname alone.
|
|
write_fixture 'logins:
|
|
- name: ported
|
|
url: https://git.example:8443
|
|
token: TOK_PORTED
|
|
'
|
|
assert_token "explicit port exact match accepted" "TOK_PORTED" ported git.example:8443
|
|
assert_token "portless repo host rejects :8443 login" "" ported git.example
|
|
assert_token "wrong explicit port rejected" "" ported git.example:9443
|
|
|
|
# 7. An implicit (portless) login URL equals the scheme's explicit default port.
|
|
write_fixture 'logins:
|
|
- name: defported
|
|
url: https://git.example
|
|
token: TOK_DEFPORT
|
|
'
|
|
assert_token "implicit https vs explicit :443 match" "TOK_DEFPORT" defported git.example:443
|
|
assert_token "implicit https vs :8443 rejected" "" defported git.example:8443
|
|
|
|
# 8. An UNQUOTED token whose raw text PyYAML's implicit resolver types as a
|
|
# NON-string (int / null / bool / float) must fail closed: PyYAML yields a
|
|
# non-str value that _accept rejects, so the fallback must NOT surface the
|
|
# stringified scalar as a credential. Each raw form fails closed IDENTICALLY
|
|
# to PyYAML (a prior residual emitted "12345"/"null"/"true"/etc. here).
|
|
assert_nonstring_token_fails_closed() {
|
|
local desc="$1" raw="$2"
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ${raw}
|
|
"
|
|
assert_token "$desc" "" primary git.example
|
|
}
|
|
assert_nonstring_token_fails_closed "unquoted int token fails closed" "12345"
|
|
assert_nonstring_token_fails_closed "unquoted null token fails closed" "null"
|
|
assert_nonstring_token_fails_closed "unquoted tilde-null token fails closed" "~"
|
|
assert_nonstring_token_fails_closed "unquoted yes(bool) token fails closed" "yes"
|
|
assert_nonstring_token_fails_closed "unquoted true(bool) token fails closed" "true"
|
|
assert_nonstring_token_fails_closed "unquoted float token fails closed" "3.14"
|
|
|
|
# 9. A QUOTED scalar is ALWAYS a string, even when its contents look like a
|
|
# non-string implicit form. The quotes force str typing in PyYAML, so the
|
|
# fallback must accept the literal (quote-stripped) contents as the token.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: "12345"
|
|
'
|
|
assert_token "double-quoted digit token is a literal string" "12345" primary git.example
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: 'abc'
|
|
"
|
|
assert_token "single-quoted token is a literal string" "abc" primary git.example
|
|
|
|
# assert_fallback_fails_closed: the forced-fallback path MUST resolve no token
|
|
# (fail closed). Used for STRUCTURAL cases where PyYAML would resolve a DIFFERENT
|
|
# token (e.g. duplicate-key last-wins) — the fallback must never surface the
|
|
# wrong/stale token, so it fails closed instead; when PyYAML is present we also
|
|
# confirm it really does resolve a (divergent) token, proving the fallback is the
|
|
# strictly-more-conservative side and the case is a genuine fail-open guard.
|
|
assert_fallback_fails_closed() {
|
|
local desc="$1" login="$2" host="$3" got
|
|
got=$(token_fallback "$login" "$host")
|
|
if [[ -n "$got" ]]; then
|
|
echo "FAIL fallback [$desc]: expected fail-closed, got a token" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$HAVE_PYYAML" == true ]]; then
|
|
got=$(token_pyyaml "$login" "$host")
|
|
if [[ -z "$got" ]]; then
|
|
echo "FAIL [$desc]: expected PyYAML to resolve a divergent token" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# 10. tea's REAL on-disk shape: the `logins:` block SEQUENCE items sit at the
|
|
# SAME indentation as the key (dash at column 0), with extra scalar fields.
|
|
# The recognizer must resolve this exactly like PyYAML (regression guard so
|
|
# the stricter whole-document recognizer does not fail closed on real input).
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_REAL
|
|
default: false
|
|
ssh_host: ""
|
|
- name: other
|
|
url: https://other.example
|
|
token: TOK_REAL_OTHER
|
|
preferences:
|
|
editor: false
|
|
flags: null
|
|
'
|
|
assert_token "tea dash-at-column-0 real shape resolves" "TOK_REAL" primary git.example
|
|
assert_token "tea real shape sibling resolves own token" "TOK_REAL_OTHER" other other.example
|
|
|
|
# 11. NESTED-SHADOW: a nested `logins:` (NOT at root scope) must not be mistaken
|
|
# for the real root logins. The recognizer parses whole-document structure,
|
|
# so it selects the ROOT logins token exactly as PyYAML does — never the
|
|
# nested attacker token. (A prior line scan matched the FIRST logins at ANY
|
|
# indent and returned ATTACKER.)
|
|
write_fixture 'outer:
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ATTACKER_NESTED
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ROOT_TOK
|
|
'
|
|
assert_token "nested logins shadow selects ROOT token" "ROOT_TOK" primary git.example
|
|
|
|
# 12. BLOCK-SCALAR-SHADOW: text inside a YAML literal/folded block ( | or > ) is
|
|
# an OPAQUE scalar to PyYAML (so `logins` is a string, not a list) and must
|
|
# not be scanned as live logins entries. Both fail closed.
|
|
write_fixture 'logins: |
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ATTACKER_BLOCK
|
|
'
|
|
assert_token "block-scalar logins value fails closed" "" primary git.example
|
|
# A folded/literal block scalar anywhere is outside the recognizer's subset, so
|
|
# the fallback fails closed (conservative) even though PyYAML can still resolve
|
|
# the real root token past the opaque scalar. Fail-closed is the safe side.
|
|
write_fixture 'note: >
|
|
logins:
|
|
- name: primary
|
|
token: ATTACKER_FOLDED
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ROOT_OK
|
|
'
|
|
assert_fallback_fails_closed "folded block scalar present fails closed" primary git.example
|
|
|
|
# 13. DUPLICATE-ROOT / DUPLICATE-FIELD: a duplicated `logins:` root key (PyYAML
|
|
# last-wins) or a duplicated field within a login must fail closed rather
|
|
# than take the FIRST (stale) value. PyYAML resolves the LAST; the fallback
|
|
# refuses to guess.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: FIRST_DUP
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: LAST_DUP
|
|
'
|
|
assert_fallback_fails_closed "duplicate root logins key fails closed" primary git.example
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: FIRST_FIELD
|
|
token: SECOND_FIELD
|
|
'
|
|
assert_fallback_fails_closed "duplicate token field fails closed" primary git.example
|
|
|
|
# 14. MALFORMED-AFTER-VALID: a syntax error LATER in the file makes PyYAML reject
|
|
# the WHOLE document; the recognizer must too (not emit the earlier token).
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
broken: a: b: c
|
|
'
|
|
assert_token "malformed line after valid login fails closed" "" primary git.example
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
broken: [unclosed
|
|
'
|
|
assert_token "unclosed flow after valid login fails closed" "" primary git.example
|
|
|
|
# 15. EXTRA-DOCUMENT: a multi-document file (--- separator, or ... end marker)
|
|
# makes PyYAML safe_load reject multi-document input; the recognizer fails
|
|
# closed on ANY document marker rather than emit the first doc's token.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
---
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: SECOND_DOC
|
|
'
|
|
assert_token "second document (--- separator) fails closed" "" primary git.example
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
...
|
|
trailing: 1
|
|
'
|
|
assert_token "end marker then more content fails closed" "" primary git.example
|
|
|
|
# 16. CONSTRUCTOR-VALIDITY / INVALID-INDICATOR: a plain scalar can match a typed
|
|
# implicit resolver (int/float/timestamp) yet be NON-constructible, or begin
|
|
# with an indicator a plain scalar may not start with. PyYAML then RAISES on
|
|
# the WHOLE document (constructor error / scanner error) and yields NO token,
|
|
# so the fallback must ALSO fail closed for the whole document -- even though
|
|
# the (unrelated) malformed key sits alongside an otherwise-valid logins
|
|
# block whose token is itself well-formed. A prior residual proved STRUCTURE
|
|
# and implicit TYPE but not constructor validity, so it ignored the malformed
|
|
# key and still emitted the valid login token (fail-open in the dangerous
|
|
# direction). assert_both_fail_closed asserts fallback == PyYAML == no token.
|
|
assert_both_fail_closed() {
|
|
local desc="$1" login="$2" host="$3" got
|
|
got=$(token_fallback "$login" "$host")
|
|
if [[ -n "$got" ]]; then
|
|
echo "FAIL fallback [$desc]: expected fail-closed, got a token" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$HAVE_PYYAML" == true ]]; then
|
|
got=$(token_pyyaml "$login" "$host")
|
|
if [[ -n "$got" ]]; then
|
|
echo "FAIL pyyaml [$desc]: expected PyYAML to also fail closed (raise/no token), got a token" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# write_bad_key_fixture: an unrelated root key carrying $1 as its plain scalar,
|
|
# followed by an otherwise-valid logins block whose token is well-formed.
|
|
write_bad_key_fixture() {
|
|
write_fixture "bad: $1
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
"
|
|
}
|
|
|
|
# Non-constructible TIMESTAMP-tagged scalars: match the resolver, but the
|
|
# calendar field is out of range so PyYAML's datetime construction raises.
|
|
write_bad_key_fixture '2023-99-99' # month 99 / day 99 invalid
|
|
assert_both_fail_closed "bad-date 2023-99-99 fails closed like PyYAML" primary git.example
|
|
write_bad_key_fixture '2023-13-01' # month 13 invalid
|
|
assert_both_fail_closed "bad-month 2023-13-01 fails closed like PyYAML" primary git.example
|
|
write_bad_key_fixture '2023-01-15T25:00:00' # hour 25 invalid
|
|
assert_both_fail_closed "bad-hour timestamp fails closed like PyYAML" primary git.example
|
|
|
|
# Non-constructible INT-tagged scalars: match the int resolver, but the radix
|
|
# body is empty after underscore removal so int(base) raises.
|
|
write_bad_key_fixture '0b_'
|
|
assert_both_fail_closed "empty-binary 0b_ fails closed like PyYAML" primary git.example
|
|
write_bad_key_fixture '0x_'
|
|
assert_both_fail_closed "empty-hex 0x_ fails closed like PyYAML" primary git.example
|
|
write_bad_key_fixture '0x__'
|
|
assert_both_fail_closed "empty-hex 0x__ (multi-underscore) fails closed" primary git.example
|
|
|
|
# Invalid plain-scalar INDICATOR forms: a plain scalar may not begin with '%'
|
|
# (directive) or ',' (flow) -- PyYAML raises a scanner/parser error on the whole
|
|
# document, so the fallback fails closed on the leading indicator.
|
|
write_bad_key_fixture '%broken'
|
|
assert_both_fail_closed "leading-%% directive indicator fails closed" primary git.example
|
|
write_bad_key_fixture ',bad'
|
|
assert_both_fail_closed "leading-comma flow indicator fails closed" primary git.example
|
|
# Bare block indicators in a value position ('-'/'- ', '?'/'? ', ':'/': '):
|
|
# PyYAML raises a scanner error on the whole document, so the fallback must fail
|
|
# closed rather than accept the indicator as a plain-scalar string.
|
|
write_bad_key_fixture '-'
|
|
assert_both_fail_closed "bare dash (seq indicator) fails closed" primary git.example
|
|
write_bad_key_fixture '- x'
|
|
assert_both_fail_closed "dash-space (seq entry) fails closed" primary git.example
|
|
write_bad_key_fixture '? key'
|
|
assert_both_fail_closed "question-space (complex key) fails closed" primary git.example
|
|
# ...but an indicator NOT followed by whitespace is a valid plain scalar string,
|
|
# so the token still resolves (no over-broad fail-close).
|
|
write_bad_key_fixture '-x'
|
|
assert_token "dash-not-space is a plain string, token resolves" "TOK_PLAIN" primary git.example
|
|
write_bad_key_fixture ':x'
|
|
assert_token "colon-not-space is a plain string, token resolves" "TOK_PLAIN" primary git.example
|
|
|
|
# NOT over-broad: a genuinely CONSTRUCTIBLE typed scalar (or a look-alike PyYAML
|
|
# keeps as a plain string) leaves the document valid, so BOTH still resolve the
|
|
# login token -- the fix must not fail closed on these.
|
|
write_bad_key_fixture '2023-01-15'
|
|
assert_token "valid date unrelated key still resolves token" "TOK_PLAIN" primary git.example
|
|
write_bad_key_fixture '2023-01-15 10:00:00'
|
|
assert_token "valid datetime unrelated key still resolves token" "TOK_PLAIN" primary git.example
|
|
# '0o_' is NOT matched by PyYAML's int resolver (YAML 1.1 octal is 0[0-7]+, not
|
|
# 0o...), so PyYAML keeps it a STRING and resolves the token; the fallback must
|
|
# agree (no spurious fail-close).
|
|
write_bad_key_fixture '0o_'
|
|
assert_token "0o_ is a plain string in PyYAML, token still resolves" "TOK_PLAIN" primary git.example
|
|
# '4.e8' matches the fallback's (superset) float pattern but PyYAML keeps it a
|
|
# string; either way it is constructible, so the token still resolves in both.
|
|
write_bad_key_fixture '4.e8'
|
|
assert_token "4.e8 float look-alike still resolves token" "TOK_PLAIN" primary git.example
|
|
# A valid radix int as an unrelated key must not fail closed.
|
|
write_bad_key_fixture '0x1f'
|
|
assert_token "valid hex int unrelated key still resolves token" "TOK_PLAIN" primary git.example
|
|
|
|
# 17. TAB / SCANNER PARITY: PyYAML raises a ScannerError on a tab used anywhere
|
|
# outside a quoted scalar -- leading, trailing, or embedded in a plain value,
|
|
# immediately after a key colon, before a key colon, or as indentation -- and
|
|
# yields NO token, accepting tabs ONLY inside single/double-quoted scalars
|
|
# (where the tab is preserved as string content). A prior fallback swallowed
|
|
# those tabs (via .strip()/.rstrip() normalization and [ \t] key separators)
|
|
# and still emitted the login token -- a fail-open in the dangerous direction.
|
|
# The recognizer now fails CLOSED for the whole document on any tab PyYAML
|
|
# rejects, while preserving the tabs PyYAML keeps (inside quotes). All tab
|
|
# positions were verified empirically against PyYAML 6.0.3 (ScannerError for
|
|
# each rejected position; string-preserved for quoted inner tabs).
|
|
TAB=$'\t'
|
|
# Fail-close: a tab in a plain value position (trailing / leading / embedded).
|
|
write_bad_key_fixture "l4o${TAB}"
|
|
assert_both_fail_closed "trailing tab in plain value fails closed" primary git.example
|
|
write_bad_key_fixture "${TAB}9"
|
|
assert_both_fail_closed "leading tab in plain value fails closed" primary git.example
|
|
write_bad_key_fixture "a${TAB}b"
|
|
assert_both_fail_closed "embedded tab in plain value fails closed" primary git.example
|
|
# Fail-close: a tab immediately after the key colon (no separating space).
|
|
write_fixture "bad:${TAB}9
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
"
|
|
assert_both_fail_closed "tab immediately after key colon fails closed" primary git.example
|
|
# Fail-close: a tab used as indentation (before a sequence dash).
|
|
write_fixture "logins:
|
|
${TAB}- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN
|
|
"
|
|
assert_both_fail_closed "tab used as indentation fails closed" primary git.example
|
|
# Fail-close: a tab trailing a sequence-mapping field value.
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_PLAIN${TAB}
|
|
"
|
|
assert_both_fail_closed "tab trailing a seq field value fails closed" primary git.example
|
|
# NOT over-broad: a tab strictly INSIDE a quoted scalar is valid YAML (PyYAML
|
|
# keeps it as string content), so the document parses and the login token still
|
|
# resolves in BOTH paths -- double-quoted and single-quoted.
|
|
write_bad_key_fixture "\"a${TAB}b\""
|
|
assert_token "tab inside a double-quoted value still resolves token" "TOK_PLAIN" primary git.example
|
|
write_bad_key_fixture "'a${TAB}b'"
|
|
assert_token "tab inside a single-quoted value still resolves token" "TOK_PLAIN" primary git.example
|
|
# ...and a quoted token value carrying an inner tab resolves to the exact string
|
|
# (tab preserved), identical to PyYAML's construction.
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: \"T${TAB}OK\"
|
|
"
|
|
assert_token "quoted token with inner tab resolves verbatim" "T${TAB}OK" primary git.example
|
|
|
|
echo "Gitea login resolution regression harness passed"
|