#!/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 # 18. CONTROL-CHARACTER FAIL-CLOSE (#865 round-9 blocker 1): PyYAML's Reader # scans the ENTIRE raw document stream (not merely scalar contents, and NOT # scoped by quoting) for bytes outside its printable set and raises # ReaderError -- a WHOLE-DOCUMENT reject -- the instant one is found, # regardless of where it sits: an unrelated field's plain scalar, inside a # double- or single-quoted scalar, or a comment. Verified empirically against # real installed PyYAML 6.0.3 (see detect-platform.sh's _FORBIDDEN_CONTROL # comment): every C0 control byte {0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F} plus DEL # (0x7F) rejects in ALL THREE contexts (plain / double-quoted / single-quoted); # only TAB(0x09), LF(0x0A), CR(0x0D) are accepted among the low byte range # (TAB has its own narrower, position-aware coverage in section 17 above; LF/CR # are line separators). A prior fallback ONLY guarded tabs and emitted the # login token from documents PyYAML rejects over an UNRELATED field's control # byte -- a dangerous fail-open (credential emission from a document PyYAML # refuses). write_control_char_fixture writes the raw byte directly via # printf's octal escape (never through a bash string/variable, which cannot # hold an embedded NUL) so 0x00 is exercised faithfully alongside the rest. write_control_char_fixture() { local octal="$1" quote="${2:-}" { if [[ -n "$quote" ]]; then printf 'bad: %sx' "$quote" # shellcheck disable=SC2059 # deliberate: $octal supplies printf's # own \NNN octal escape so the raw control byte reaches the file # directly, never passing through a bash string (which truncates # at an embedded NUL and so cannot represent byte 0x00 otherwise). printf "\\${octal}" printf 'y%s\n' "$quote" else printf 'bad: x' # shellcheck disable=SC2059 # deliberate: $octal supplies printf's # own \NNN octal escape so the raw control byte reaches the file # directly, never passing through a bash string (which truncates # at an embedded NUL and so cannot represent byte 0x00 otherwise). printf "\\${octal}" printf 'y\n' fi printf 'logins:\n - name: primary\n url: https://git.example\n token: TOK_PLAIN\n' } > "$FIXTURE_XDG/tea/config.yml" } # Plain (unquoted) unrelated-field placement: the full empirically-confirmed # forbidden C0/DEL set. for octal in 000 001 002 003 004 005 006 007 010 013 014 \ 016 017 020 021 022 023 024 025 026 027 \ 030 031 032 033 034 035 036 037 177; do write_control_char_fixture "$octal" assert_both_fail_closed "control byte \\$octal in unrelated plain field fails closed" primary git.example done # Inside-quote variants (double and single) for the six bytes called out # explicitly in the round-9 blocker report: 0x00,0x01,0x07,0x0e,0x1f,0x7f. for octal in 000 001 007 016 037 177; do write_control_char_fixture "$octal" '"' assert_both_fail_closed "control byte \\$octal inside double-quoted unrelated field fails closed" primary git.example write_control_char_fixture "$octal" "'" assert_both_fail_closed "control byte \\$octal inside single-quoted unrelated field fails closed" primary git.example done # Same forbidden byte inside a comment line -- PyYAML's Reader check is # stream-wide, so it rejects here too, not merely inside live scalar content. { printf '# note' printf '\007' printf 'here\nlogins:\n - name: primary\n url: https://git.example\n token: TOK_PLAIN\n' } > "$FIXTURE_XDG/tea/config.yml" assert_both_fail_closed "control byte in a comment line fails closed" primary git.example # NOT over-broad: TAB/LF/CR remain accepted where PyYAML already accepts them # (covered by section 17's tab fixtures and the ordinary newline-delimited # fixtures used throughout this file), so no additional assertion is needed # here beyond confirming the forbidden-control guard does not fire on them. # 19. UNSIGNED-EXPONENT FLOAT OVER-REJECTION (#865 round-9 blocker 2): PyYAML # 6.0.3's implicit float resolver requires an EXPLICIT SIGN on the exponent # ([eE][-+][0-9]+); an unsigned exponent is NOT matched, so PyYAML resolves # the scalar as a plain STRING, not a float. A prior fallback's float # recognizer accepted an OPTIONAL sign ([eE][-+]?[0-9]+), over-matching these # spellings as floats and dropping the token PyYAML would emit verbatim # (over-rejection). Verified empirically against real PyYAML 6.0.3. for form in '1.0e10' '+1.0e10' '-1.0e10' '1.0E10' '.5e10' '4.e8'; do write_fixture "logins: - name: primary url: https://git.example token: ${form} " assert_token "unsigned-exponent form '$form' is a PyYAML string, token resolves" "$form" primary git.example done # Parity guard: a genuine SIGNED-exponent float is still typed as a non-string # float by PyYAML and must still fail closed (not regress into over-acceptance). write_fixture 'logins: - name: primary url: https://git.example token: 1.0e+10 ' assert_token "signed-exponent genuine float still fails closed" "" primary git.example write_fixture 'logins: - name: primary url: https://git.example token: 1.0e-10 ' assert_token "signed-exponent (negative) genuine float still fails closed" "" primary git.example # 20. RESIDUAL OVER-REJECTION found via round-9 differential fuzzing (folded into # this round, not split off): a plain scalar starting with "?" NOT followed by # whitespace (e.g. "?x") is a valid PyYAML string -- only a bare "?" or "? " # (question mark followed by space/EOL) opens a complex mapping key and is # illegal in a value position. A prior blanket-reject set treated EVERY # leading "?" as illegal, over-rejecting a token PyYAML accepts verbatim. write_fixture 'logins: - name: primary url: https://git.example token: ?x ' assert_token "question-mark-not-space is a plain string, token resolves" "?x" primary git.example # 21. PRINTABLE-BOUNDARY FAIL-CLOSE (#865 round-10 blocker 1): the round-9 guard # used a hand-rolled C0/DEL subset that MISSED code points PyYAML's Reader # also rejects -- the C1 block (U+0080-0084, U+0086-009F) and the BMP # noncharacters U+FFFE/U+FFFF -- so the fallback still emitted the token from # documents PyYAML rejects whole (fail-open). The guard now uses PyYAML # 6.0.3's EXACT Reader.NON_PRINTABLE character class (see detect-platform.sh # _FORBIDDEN_CONTROL). Verified empirically against real PyYAML 6.0.3: # PRINTABLE = {0x09,0x0A,0x0D, 0x20-0x7E, 0x85(NEL), 0xA0-0xD7FF, # 0xE000-0xFFFD, 0x10000-0x10FFFF}; everything else fails the whole document # closed. write_codepoint_fixture emits a chosen Unicode code point's real # UTF-8 bytes (via python3, since bash strings cannot faithfully carry many # of these) into a selectable position, then the login block follows. write_codepoint_fixture() { # $1 = hex code point (e.g. 0x80); $2 = position: field|comment|token|nelterm CP_HEX="$1" CP_POS="$2" python3 - "$FIXTURE_XDG/tea/config.yml" <<'PY' import sys cp = int(__import__("os").environ["CP_HEX"], 16) pos = __import__("os").environ["CP_POS"] ch = chr(cp) head = "logins:\n - name: primary\n url: https://git.example\n token: TOK_PLAIN\n" if pos == "field": doc = head + "other: x" + ch + "y\n" elif pos == "comment": doc = head + "# note x" + ch + "y here\n" elif pos == "token": doc = "logins:\n - name: primary\n url: https://git.example\n token: T" + ch + "K\n" elif pos == "nelterm": # NEL (U+0085) used as the line terminator throughout: PyYAML treats it as a # line break (printable, NOT a ReaderError) and resolves the token; the # fallback's splitlines() splits on NEL identically -> parity, token resolves. doc = ("logins:" + ch + " - name: primary" + ch + " url: https://git.example" + ch + " token: TOK_NEL" + ch) else: raise SystemExit("bad pos") with open(sys.argv[1], "w", encoding="utf-8") as f: f.write(doc) PY } # C1-block + BMP-noncharacter code points fail the WHOLE document closed in an # unrelated field and in a comment, exactly as PyYAML's ReaderError does. for cphex in 0x80 0x81 0x84 0x86 0x9f 0xfffe 0xffff; do write_codepoint_fixture "$cphex" field assert_both_fail_closed "code point $cphex in unrelated field fails closed" primary git.example write_codepoint_fixture "$cphex" comment assert_both_fail_closed "code point $cphex in a comment fails closed" primary git.example done # NOT over-broad: printable code points PyYAML ACCEPTS must still resolve the # token in BOTH paths -- NEL(0x85) as a line separator, U+00A0 (NBSP) inside a # value, and an astral code point (U+1F600) inside the token value. write_codepoint_fixture 0x85 nelterm assert_token "NEL (U+0085) line-terminator resolves token" "TOK_NEL" primary git.example write_codepoint_fixture 0xa0 field assert_token "U+00A0 in unrelated value still resolves token" "TOK_PLAIN" primary git.example write_codepoint_fixture 0x1f600 token assert_token "astral U+1F600 inside token resolves verbatim" "$(printf 'T\360\237\230\200K')" primary git.example # 22. INTERNAL-INDICATOR OVER-REJECTION (#865 round-10 blocker 2): the round-9 # recognizer blanket-rejected any plain scalar CONTAINING a flow indicator # ([]{}*&!), but in BLOCK context PyYAML treats ',[]{}' as ordinary content # and treats '!&*#...' as significant ONLY at the FIRST non-space char. So an # INTERNAL indicator is legal plain-string content and PyYAML emits the token # verbatim; the fallback dropped it (over-rejection). Verified empirically # against real PyYAML 6.0.3. The fix removes the blanket internal scan while # the leading-char guard and the ' #'/': '/trailing-':' guards keep the # fail-OPEN direction shut. for tv in 'a!b' 'a,b' 'a[b' 'a]b' 'a{b' 'a}b' 'a&b' 'a*b' 'a[b]c' 'a{b}c' 'a,b,c' 'a#b' 'a:b'; do write_fixture "logins: - name: primary url: https://git.example token: ${tv} " assert_token "internal-indicator token '$tv' resolves verbatim" "$tv" primary git.example done # Fail-OPEN direction stays shut: a LEADING indicator, a ' #' comment tail, an # internal ': ' (colon-space) inline map, and a trailing ':' each make PyYAML # resolve NO usable string token (tag/flow/anchor reject or None, comment strip, # or a mapping), so BOTH must fail closed. (Leading tag/anchor/flow are the # documented, round-9-approved structural fail-closed class; kept intact here.) write_fixture 'logins: - name: primary url: https://git.example token: !x ' assert_both_fail_closed "leading '!' tag token fails closed" primary git.example write_fixture 'logins: - name: primary url: https://git.example token: [a] ' assert_both_fail_closed "leading '[' flow-seq token fails closed" primary git.example write_fixture 'logins: - name: primary url: https://git.example token: a # trailing comment ' # ' #' comment tail: PyYAML strips the comment -> token is the string 'a', which # still resolves. This is the NOT-over-broad boundary partner of the guard. assert_token "space-hash comment tail strips to plain token" "a" primary git.example write_fixture 'logins: - name: primary url: https://git.example token: a: b ' assert_both_fail_closed "internal colon-space (inline map) token fails closed" primary git.example write_fixture 'logins: - name: primary url: https://git.example token: ab: ' assert_both_fail_closed "trailing colon (map indicator) token fails closed" primary git.example echo "Gitea login resolution regression harness passed"