CENSUS CORRECTION: FOUR suites, not the three my own #1007 audit named. The fourth (test-pr-metadata-gitea.sh) was outside the candidate set that audit worked from and was found only by sweeping the discriminator across all 16 tools/git/test-*.sh suites. Recording that as a correction to my finding, not as part of the original claim. THE DEFECT. get_gitea_token() (detect-platform.sh:502-599) resolves a per-agent identity at STEP 0, from `git config --get mosaic.gitIdentity`, BEFORE both the Mosaic credential loader (step 1) and the GITEA_TOKEN env check (step 2). On a provisioned agent seat that value is set GLOBALLY in ~/.gitconfig and is inherited by any freshly-`git init`ed repo, so step 0 reads a REAL per-slot token out of $HOME and returns it without ever consulting the suite's own MOSAIC_CREDENTIALS_FILE / GITEA_TOKEN fixtures. The suites were running against production credentials, and the fixture credential each one carefully constructs was inert. THE FIX: an empty repo-local `mosaic.gitIdentity`. An empty local value shadows the global one and reads back empty at rc=0, so step 0 declines. The env route does NOT work: detect-platform.sh reads "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset. OPERATIVE vs CONTAINMENT — the two mechanisms are not interchangeable and the comment in each suite says so. The pin is operative: it prevents the resolution. The sandboxed HOME each suite now also gets is containment: it bounds a failure the pin should already have prevented. Conflating them is how this class stays invisible, because a decoy HOME REMOVES the trigger (~/.gitconfig is where the global identity lives), so any suite audited under one reads clean however vulnerable it is. To MEASURE, replicate a seat: a decoy HOME whose .gitconfig sets mosaic.gitIdentity with no per-slot token, so step 0 reaches its fail-loud branch. That note is in each file for the next auditor. SECOND, INDEPENDENT DEFECT in test-pr-metadata-gitea.sh. Applying the pin alone turned that suite RED — and a control at baseline826a8b3under a plain HOME reproduced the same failure, so it is pre-existing, not introduced. Its `GITEA_TOKEN="stub-token"` / `GITEA_URL="https://git.example.test"` pair can never satisfy step 2, because step 2 accepts GITEA_TOKEN only when GITEA_URL matches the remote host and this repo's origin is git.uscllc.com. The suite had therefore only ever passed by resolving a REAL credential — step 0 on a seat, or step 1 from the operator's own credentials.json. A MOSAIC_CREDENTIALS_FILE fixture is added rather than leaning on the sandboxed HOME making step 1 find nothing: a test that passes because production configuration is ABSENT fails the moment it is present. Shipping the pin without this would have moved the failure rather than removed it. NO CI ARM. .woodpecker/ci.yml does not run these suites; packages/mosaic/ package.json:28 (test:framework-shell) runs an ENUMERATED list that excludes all four. They run only by hand — i.e. exclusively on a provisioned seat, the one environment where the defect is live. "Passes in CI, fails on a seat" does not apply here; there is no CI observation at all. VERIFICATION (seat replica = decoy HOME with mosaic.gitIdentity set, no per-slot token; canary = same plus a marked non-credential at both per-slot paths; plain = empty HOME; real = ordinary invocation): - bash -n clean on all four. - Sweep of all 16 suites at baseline826a8b3under the seat replica: test-gitea-login-resolution rc=1 REACHES-STEP0; test-issue-create- interactive-auth rc=1 REACHES-STEP0; test-pr-merge-gitea-empty-uid rc=1 REACHES-STEP0; test-pr-metadata-gitea rc=1 REACHES-STEP0. - Same sweep after: every row rc=0 with step0 absent. - test-gitea-token-identity flags REACHES-STEP0 in BOTH arms and is NOT a defect: it runs under `env -i HOME="$FAKE_HOME"` (line 77) and its hit is its own deliberate assert_failloud fixtures (lines 158-171). The fail-loud grep matches the intended behaviour as well as the defect, so it needs the second discriminator; recorded here so the next sweep does not re-file it. - Durable-argv assay (a PATH shim that tees argv out of each suite's own mock curl, because test-pr-merge-gitea-empty-uid truncates its log between phases and its EXIT trap removes the sandbox — a post-hoc read of that suite is a non-measurement, and "no trace" there is not a clearance): test-pr-merge-gitea-empty-uid before: canary token in argv, fixture never used. after: fixture token in argv, canary absent. 5 curl calls both arms. test-pr-metadata-gitea before: canary in argv. after: both calls carry the fixture token against git.uscllc.com. - test-pr-metadata-gitea across seat/canary/plain HOMEs after the fix: rc=0, rc=0, rc=0. - All four under the real HOME: rc=0. No regression to ordinary invocation. The comment block is duplicated across the four files rather than pointing at a shared note. Deliberate, and matching the merged #1006 precedent (test-pr-review-gitea-comment.sh:87-95): the reader who needs it is auditing one file. TWO FINDINGS DELIBERATELY NOT FIXED HERE (out of this branch's scope, to be filed): 1. pr-metadata.sh:89-92 — the anonymous curl fallback does not check ^2, so an HTTP 200 carrying valid JSON is reported as "unknown API error" at rc=1. 2. test-issue-comment-readback.sh exits 1 with ZERO bytes on stdout AND stderr, dying at its first seed_state python3 heredoc. Reproduces at baseline826a8b3under both a seat replica and the real HOME. Silently red at main for everyone; unrelated to #1007. Refs #1007
1241 lines
54 KiB
Bash
Executable File
1241 lines
54 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"
|
|
HOME_DIR="$WORK_DIR/home"
|
|
LOG_FILE="$WORK_DIR/calls.log"
|
|
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
|
|
|
rm -rf "$WORK_DIR"
|
|
mkdir -p "$REPO_DIR" "$BIN_DIR" "$HOME_DIR"
|
|
|
|
git -C "$REPO_DIR" init -q
|
|
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
|
|
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
|
|
#
|
|
# OPERATIVE: the empty repo-local `mosaic.gitIdentity` below. get_gitea_token()
|
|
# step 0 resolves a per-agent identity from `git config --get mosaic.gitIdentity`,
|
|
# which on a provisioned agent seat is set GLOBALLY and so leaks into this fresh
|
|
# repo. It then reads a REAL per-slot token from $HOME and returns it WITHOUT ever
|
|
# consulting MOSAIC_CREDENTIALS_FILE, so the fixture credentials below are silently
|
|
# ignored. This suite is the one where the consequence is not subtle: it FAILS
|
|
# outright on a provisioned seat (rc=1 bare, rc=0 with $HOME sandboxed, one
|
|
# variable changed) and passes everywhere else, including CI, which has no
|
|
# per-agent token to leak.
|
|
#
|
|
# CONTAINMENT: the sandboxed HOME in the four run helpers below. It only has to
|
|
# bound a failure that the pin should already have prevented.
|
|
#
|
|
# NOTE FOR ANYONE AUDITING THIS SUITE: the sandboxed HOME is containment, NOT an
|
|
# assay. Running a suite under a decoy HOME to test for this defect REMOVES the
|
|
# trigger — ~/.gitconfig is where the global identity lives, so step 0 is skipped
|
|
# by construction and every suite reads clean however vulnerable it is. To measure,
|
|
# REPLICATE a seat (a decoy HOME whose .gitconfig sets mosaic.gitIdentity, with no
|
|
# per-slot token) so step 0 reaches its fail-loud branch.
|
|
#
|
|
# Note the env-var route does NOT work: detect-platform.sh reads
|
|
# "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset.
|
|
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
|
|
|
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" \
|
|
HOME="$HOME_DIR" \
|
|
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:[email protected]/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" \
|
|
HOME="$HOME_DIR" \
|
|
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"
|
|
HOME="$HOME_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"
|
|
HOME="$HOME_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 _split_logical_lines splits on NEL identically OUTSIDE a quote
|
|
# -> parity, token resolves. (Round 23 covers NEL/LS/PS INSIDE a quote, where
|
|
# PyYAML folds rather than breaks and a naive splitlines() would over-split.)
|
|
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
|
|
|
|
# 23. EMBEDDED LINE-BREAK INSIDE A QUOTED SCALAR (#865 round-11 blocker A): the
|
|
# round-10 fallback split the raw document with str.splitlines(), which breaks
|
|
# at NEL(U+0085), LS(U+2028) and PS(U+2029) -- code points that are PRINTABLE
|
|
# to PyYAML's Reader. Inside a flow (quoted) scalar PyYAML does NOT break at
|
|
# these: it LINE-FOLDS a double/single-quoted scalar (NEL/LF/CR -> a single
|
|
# space; LS/PS -> the char verbatim), so it resolves ONE token, while
|
|
# splitlines() cut the value mid-quote and failed the whole document closed
|
|
# (over-rejection). The fallback now uses _split_logical_lines, which
|
|
# reproduces PyYAML's flow-folding. Verified empirically vs real PyYAML 6.0.3.
|
|
# write_break_fixture emits a chosen break code point in a selectable context.
|
|
write_break_fixture() {
|
|
# $1 = hex code point of the break; $2 = context: dq|sq|plain|comment|dq2
|
|
CP_HEX="$1" Q_STYLE="$2" python3 - "$FIXTURE_XDG/tea/config.yml" <<'PY'
|
|
import sys, os
|
|
cp = int(os.environ["CP_HEX"], 16)
|
|
q = os.environ["Q_STYLE"]
|
|
ch = chr(cp)
|
|
head = "logins:\n - name: primary\n url: https://git.example\n token: "
|
|
if q == "dq":
|
|
doc = head + '"tok' + ch + 'en"'
|
|
elif q == "sq":
|
|
doc = head + "'tok" + ch + "en'"
|
|
elif q == "plain":
|
|
doc = head + "tok" + ch + "en"
|
|
elif q == "dq2":
|
|
# blank line inside a quoted scalar: PyYAML folds a two-break run to a literal
|
|
# newline, which the recognizer's key regex cannot carry -> endorsed
|
|
# fail-closed over-reject (see assert_fallback_fails_closed below).
|
|
doc = head + '"tok' + ch + ch + 'en"'
|
|
elif q == "comment":
|
|
doc = ("logins:\n - name: primary\n url: https://git.example\n"
|
|
" token: TOK_PLAIN\n# c" + ch + "x")
|
|
else:
|
|
raise SystemExit("bad q")
|
|
with open(sys.argv[1], "w", encoding="utf-8") as f:
|
|
f.write(doc + "\n")
|
|
PY
|
|
}
|
|
|
|
# NEL folds to a single space inside double- AND single-quoted scalars: the token
|
|
# resolves identically in both paths (fallback no longer over-splits).
|
|
write_break_fixture 0x85 dq
|
|
assert_token "NEL inside double-quote folds to space, token resolves" "tok en" primary git.example
|
|
write_break_fixture 0x85 sq
|
|
assert_token "NEL inside single-quote folds to space, token resolves" "tok en" primary git.example
|
|
# LS(U+2028)/PS(U+2029) are preserved VERBATIM by PyYAML's flow fold (they are
|
|
# not \n-class breaks); the fallback must surface them byte-for-byte.
|
|
write_break_fixture 0x2028 dq
|
|
assert_token "LS inside double-quote is verbatim" "$(printf 'tok\342\200\250en')" primary git.example
|
|
write_break_fixture 0x2028 sq
|
|
assert_token "LS inside single-quote is verbatim" "$(printf 'tok\342\200\250en')" primary git.example
|
|
write_break_fixture 0x2029 dq
|
|
assert_token "PS inside double-quote is verbatim" "$(printf 'tok\342\200\251en')" primary git.example
|
|
|
|
# Direction-sensitivity: the SAME code points UNQUOTED (a plain scalar) or in a
|
|
# COMMENT make PyYAML raise a scanner error, so both paths must fail closed. The
|
|
# fold rule applies ONLY inside a quoted scalar.
|
|
write_break_fixture 0x85 plain
|
|
assert_token "NEL in an unquoted plain scalar fails closed" "" primary git.example
|
|
write_break_fixture 0x2028 plain
|
|
assert_token "LS in an unquoted plain scalar fails closed" "" primary git.example
|
|
write_break_fixture 0x85 comment
|
|
assert_token "NEL in a comment fails closed" "" primary git.example
|
|
|
|
# Endorsed fail-closed over-reject: a blank line inside a quoted scalar folds to a
|
|
# literal newline that the recognizer cannot carry -- PyYAML resolves a
|
|
# (newline-bearing) token, the fallback fails closed (strictly safer).
|
|
write_break_fixture 0x85 dq2
|
|
assert_fallback_fails_closed "blank-line-in-quote (NEL run) fails closed" primary git.example
|
|
|
|
# 24. LEADING NON-SPECIFIC TAG / ANCHOR PROPERTY (#865 round-11 blocker B, revised
|
|
# in round 12): the round-10 recognizer blanket-rejected any scalar beginning
|
|
# with '!' or '&'. Round 11 taught it to strip a transparent NON-SPECIFIC tag
|
|
# ('! ' bang + SPACE) and a transparent plain ANCHOR ('&name ') so '! x' /
|
|
# '&a x' resolve the STRING 'x', matching PyYAML. Round 12 discovered that the
|
|
# anchor half of that was a HIGH fail-open: PyYAML's Composer tracks anchor
|
|
# NAMES in a document-scoped registry and raises ComposerError ("found
|
|
# duplicate anchor") the instant the SAME name is declared on a SECOND node
|
|
# ANYWHERE in the document (even an unrelated one) -- the fallback's
|
|
# per-scalar-only view has no such registry and would emit the later token.
|
|
# Round 12's fix: reject EVERY '&'-anchor property, unconditionally. The
|
|
# transparent NON-SPECIFIC TAG behavior ('! x' -> 'x') is unchanged and still
|
|
# verified below; only the anchor half now fails closed (deliberate
|
|
# conservative over-reject, verified safe both ways against real PyYAML 6.0.3).
|
|
for pair in '! x=x' '! x y=x y' '! "q"=q' "! 'q'=q"; do
|
|
tv="${pair%%=*}"; want="${pair#*=}"
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ${tv}
|
|
"
|
|
assert_token "tag property token '$tv' resolves node string" "$want" primary git.example
|
|
done
|
|
|
|
# Fail-OPEN direction stays shut. '!x' (bang + NON-space) is a tag HANDLE ->
|
|
# ConstructorError; '!foo x'/'* a'/'! !x' raise; a property over a NON-string node
|
|
# ('! 123'/'! true'/'! null') types non-str -> no usable token. All fail closed in
|
|
# BOTH paths.
|
|
for tv in '!x' '!foo x' '* a' '! !x' '! 123' '! true' '! null' '&a &b x'; do
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ${tv}
|
|
"
|
|
assert_token "non-resolving property token '$tv' fails closed" "" primary git.example
|
|
done
|
|
|
|
# Round 12: a SINGLE, non-duplicated '&a x' is valid YAML that real PyYAML
|
|
# resolves to the string 'x' (round-11 behavior, and still true of the oracle).
|
|
# The fallback now rejects it anyway -- a deliberate, endorsed CONSERVATIVE
|
|
# over-reject (see the round-12 comment block above): fail-closed can only cost
|
|
# an emitted token PyYAML would have allowed, never emit one PyYAML rejects, and
|
|
# a per-scalar recognizer cannot safely prove document-wide anchor-name
|
|
# uniqueness. assert_fallback_fails_closed also confirms PyYAML really does
|
|
# resolve a token here, proving this is a genuine (safe-direction) divergence
|
|
# and not an accidental parity loss.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: &a x
|
|
'
|
|
assert_fallback_fails_closed "round-12: single non-duplicated anchor '&a x' now fails closed (conservative over-reject; PyYAML resolves x)" primary git.example
|
|
# Same over-reject for the combined tag+anchor forms round 11 used to resolve.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ! &b x
|
|
'
|
|
assert_fallback_fails_closed "round-12: '! &b x' (tag+anchor) now fails closed (conservative over-reject)" primary git.example
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: &b ! x
|
|
'
|
|
assert_fallback_fails_closed "round-12: '&b ! x' (anchor+tag) now fails closed (conservative over-reject)" primary git.example
|
|
|
|
# Endorsed fail-closed over-reject: an EXPLICIT tag ('!!str x', verbose
|
|
# '!<tag:yaml.org,2002:str> x') forces a string PyYAML resolves, but the fallback
|
|
# recognizes only the transparent non-specific tag and fails closed (safer).
|
|
for tv in '!!str x' '!<tag:yaml.org,2002:str> x'; do
|
|
write_fixture "logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ${tv}
|
|
"
|
|
assert_fallback_fails_closed "explicit-tag token '$tv' fails closed" primary git.example
|
|
done
|
|
|
|
# 25. #865 round 12 HIGH fail-open closure: DUPLICATE ANCHOR NAME across separate
|
|
# nodes. Real PyYAML's Composer tracks anchor names in a DOCUMENT-SCOPED
|
|
# registry and raises ComposerError ("found duplicate anchor ... first
|
|
# occurrence") the instant the SAME anchor name is declared a second time
|
|
# ANYWHERE in the document -- failing the WHOLE document closed, no token,
|
|
# regardless of how far the duplicate sits from the logins block. The round-11
|
|
# fallback tracked anchors only WITHIN a single scalar's `_strip_properties`
|
|
# call, so it had no visibility into a duplicate declared on an unrelated
|
|
# node and would still emit the (later) token: fail-open. The round-12 fix
|
|
# (reject every '&'-anchor property, unconditionally -- see section 24 above)
|
|
# closes this as a strict superset: since NO anchor is ever accepted, a
|
|
# duplicate anchor can never slip through. These cases exercise that
|
|
# document-wide duplicate-anchor invariant specifically (as opposed to
|
|
# section 24's single-anchor-on-the-token-field cases) and pair each fallback
|
|
# assertion with confirmation that real PyYAML also fails closed here (via
|
|
# ComposerError), proving this was a genuine fail-open, not a hypothetical.
|
|
write_fixture 'first: &same one
|
|
second: &same two
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_DUP_ROOT
|
|
'
|
|
assert_both_fail_closed "round-12: duplicate anchor name on two unrelated root nodes fails closed" primary git.example
|
|
|
|
write_fixture 'outer:
|
|
nested: &dup x
|
|
dup_root: &dup y
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: TOK_DUP_NESTED
|
|
'
|
|
assert_both_fail_closed "round-12: duplicate anchor name across a nested node and a root node fails closed" primary git.example
|
|
|
|
write_fixture 'first: &dup one
|
|
logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: &dup TOK_DUP_TOKEN_NODE
|
|
'
|
|
assert_both_fail_closed "round-12: anchor name declared earlier and repeated on the token-bearing node fails closed" primary git.example
|
|
|
|
# Regression guard: the non-specific TAG half of section 24 ('! x' -> 'x') is
|
|
# UNCHANGED by the round-12 anchor fix and must still resolve.
|
|
write_fixture 'logins:
|
|
- name: primary
|
|
url: https://git.example
|
|
token: ! x
|
|
'
|
|
assert_token "round-12 regression: '! x' (tag, no anchor) still resolves 'x'" "x" primary git.example
|
|
|
|
echo "Gitea login resolution regression harness passed"
|