Files
stack/packages/mosaic/framework/tools/git/test-grant-reviewer.sh
T
veronica deb4760b11
ci/woodpecker/pr/ci Pipeline failed
feat(tools/git): add grant-reviewer.sh org-team reviewer grant with fail-closed read-back (#1415)
Port the operator-tree grant-reviewer tool into the framework suite.
grant-reviewer.sh -u <user> [-r <owner>/<repo>] [-t <team>] grants a
reviewer code-read plus issues/pulls-write on an org-owned Gitea
repository via an org team (default fleet-reviewers). Gitea only; on a
GitHub remote it refuses with a clear error.

Idempotent: exact-name team lookup before create (permission read,
units_map: code read, issues write, pulls write); member and repo
additions are idempotent puts. Fail-closed read-back: after the writes,
the single member resource and the single team-repo resource are both
fetched back and must confirm the grant or the run fails — a success
status is an exit code, not evidence of a durable write (#865 defect
class). HTTP 403 on any step surfaces as 'org admin required on <org>',
never a silent partial grant.

Identity follows issue-comment.sh: GITEA_LOGIN names a tea login whose
host-matched token MUST resolve (fail closed, no downgrade to the host
default); otherwise the detect-platform.sh identity ladder applies. The
bearer token travels via a curl config file, never argv.

Documented limitation (field finding, usc/infrastructure PR 273): under
branch protection with required approvals, reviews from a
read-permission team are official=false and do not count toward the
required total; whitelisting the team on the protected branch is an
operator review-policy decision this script deliberately does not
automate, and official is computed at review submission.

Hermetic suite test-grant-reviewer.sh models a real server with
persistent on-disk state, exact-payload validation, a substring-named
decoy team, write-without-persist sabotage modes, identity-per-request
assertions, and token-not-in-argv plus temp-leak checks; enumerated in
test:framework-shell.
2026-08-24 20:45:43 -05:00

617 lines
22 KiB
Bash
Executable File

#!/usr/bin/env bash
# Regression harness for grant-reviewer.sh (#1415): org-team reviewer grant
# with fail-closed read-back verification.
#
# This harness models a REAL server: the curl stub keeps persistent team/
# member/repo state on disk, the POST actually CREATES and PERSISTS the team,
# the member/repo PUTs persist (except in the sabotage modes), and the
# read-back GETs answer from that same state. There is no fabricated record
# for the wrapper to "find" — verification passes only if the PUTs genuinely
# persisted what the read-back retrieves. It proves the wrapper:
# 1. creates the team with the EXACT reviewer payload (permission: read,
# units_map {repo.code: read, repo.issues: write, repo.pulls: write}) —
# the stub rejects any other payload;
# 2. is idempotent: an existing team is found by EXACT name (a decoy team
# whose name merely CONTAINS the wanted name is listed first and must
# not be matched) and no create POST is issued;
# 3. refuses to run against a GitHub-remoted repo (Gitea only);
# 4. refuses when the owner is not an organization;
# 5. maps HTTP 403 to "org admin required on <org>" and stops before any
# partial grant;
# 6. fails closed when the member PUT returns 204 without persisting (the
# #865 defect class: an exit code is not evidence of a durable write);
# 7. fails closed when the repo PUT returns 204 without persisting;
# 8. with GITEA_LOGIN set, performs EVERY request under that login's token
# (never the host default), and with an UNRESOLVABLE GITEA_LOGIN fails
# closed with ZERO API calls instead of downgrading;
# 9. never lets the bearer token ride in curl argv (curl --config only);
# 10. leaves no temp files behind on success or failure paths.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/grant-reviewer}"
REPO_DIR="$WORK_DIR/repo"
GH_REPO_DIR="$WORK_DIR/gh-repo"
BIN_DIR="$WORK_DIR/bin"
XDG_DIR="$WORK_DIR/xdg"
TEA_LOG="$WORK_DIR/tea.log"
CURL_LOG="$WORK_DIR/curl.log"
# Full curl argv per invocation — proves the bearer token never rides in argv.
CURL_ARGV_LOG="$WORK_DIR/curl-argv.log"
AUTH_LOG="$WORK_DIR/auth.log"
OUTPUT_FILE="$WORK_DIR/output.log"
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
STATE_FILE="$WORK_DIR/grants.json"
PAYLOAD_VIOLATION_FILE="$WORK_DIR/payload-violation"
TMP_SCRATCH="$WORK_DIR/scratch"
HOME_DIR="$WORK_DIR/home"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
mkdir -p "$REPO_DIR" "$GH_REPO_DIR" "$BIN_DIR" "$XDG_DIR" "$TMP_SCRATCH" "$HOME_DIR"
git -C "$REPO_DIR" init -q
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
git -C "$GH_REPO_DIR" init -q
git -C "$GH_REPO_DIR" remote add origin https://github.com/someorg/somerepo.git
# HERMETICITY (#1007): get_gitea_token() step 0 resolves a per-agent identity
# from `git config --get mosaic.gitIdentity`, which on a provisioned seat is
# set GLOBALLY and leaks into this fresh repo, after which a REAL per-slot
# token is read from $HOME and the fixture credential is silently ignored. An
# empty repo-local value shadows the global one and reads back empty at rc=0.
# (The env-var route does NOT neutralize step 0's git-config read — but the
# run env below still pins MOSAIC_GIT_IDENTITY= empty so the ENV rung of the
# ladder cannot resolve either: `${MOSAIC_GIT_IDENTITY:-}` treats set-but-empty
# as unset.)
git -C "$REPO_DIR" config mosaic.gitIdentity ""
git -C "$GH_REPO_DIR" config mosaic.gitIdentity ""
ORG="mosaicstack"
REPO_SLUG="mosaicstack/stack"
API_ROOT="https://git.mosaicstack.dev/api/v1"
REVIEWER="rev-user"
TEAM_NAME="fleet-reviewers"
TEAM_ID=42
DECOY_TEAM_ID=99
DEFAULT_TOKEN="test-only-placeholder"
DEFAULT_IDENTITY="seat-default"
OVERRIDE_LOGIN="granter"
OVERRIDE_TOKEN="override-token-placeholder"
# tea config: the GITEA_LOGIN override login has its own host-bound token here.
mkdir -p "$XDG_DIR/tea"
OVERRIDE_LOGIN="$OVERRIDE_LOGIN" OVERRIDE_TOKEN="$OVERRIDE_TOKEN" \
python3 - "$XDG_DIR/tea/config.yml" <<'PY'
import os
import sys
with open(sys.argv[1], "w", encoding="utf-8") as handle:
handle.write("logins:\n")
handle.write(f" - name: {os.environ['OVERRIDE_LOGIN']}\n")
handle.write(" url: https://git.mosaicstack.dev\n")
handle.write(f" token: {os.environ['OVERRIDE_TOKEN']}\n")
PY
CONFIGURED_GITEA_URL="https://git.mosaicstack.dev" python3 - "$CREDENTIALS_FILE" <<'PY'
import json
import os
import sys
with open(sys.argv[1], "w", encoding="utf-8") as credentials:
json.dump({
"gitea": {
"mosaicstack": {
"url": os.environ["CONFIGURED_GITEA_URL"],
"token": "test-only-placeholder",
}
}
}, credentials)
PY
# tea stub: grant-reviewer.sh must never shell out to tea at all.
cat > "$BIN_DIR/tea" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >> "$GRANT_REVIEWER_TEA_LOG"
echo "Unexpected tea command (grant-reviewer must not use tea): $*" >&2
exit 92
SH
chmod +x "$BIN_DIR/tea"
# curl stub: a small REST server backed by persistent on-disk grant state.
# GET /orgs/{org} -> org existence (404 in not-an-org mode)
# GET /orgs/{org}/teams/search -> teams from state (decoy always listed FIRST)
# POST /orgs/{org}/teams -> validate EXACT payload, CREATE + PERSIST
# PUT /teams/{id}/members/{user} -> 204; persists unless member-put-noop
# PUT /teams/{id}/repos/{org}/{repo} -> 204; persists unless repo-put-noop
# GET /teams/{id}/members/{user} -> answers from persisted state only
# GET /teams/{id}/repos/{org}/{repo} -> answers from persisted state only
cat > "$BIN_DIR/curl" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
# Record the FULL argv exactly as spawned, before consumption. The bearer token
# must NOT appear here — it is delivered via a curl --config file, so only the
# config file PATH may show up.
printf '%s\n' "$*" >> "$GRANT_REVIEWER_CURL_ARGV_LOG"
output_file=""
method="GET"
url=""
data=""
auth_token=""
config_file=""
while [[ $# -gt 0 ]]; do
case "$1" in
-o) output_file="$2"; shift 2 ;;
-H)
[[ "$2" == Authorization:* ]] && auth_token="${2##* }"
shift 2 ;;
-K|--config) config_file="$2"; shift 2 ;;
-w) shift 2 ;;
-X) method="$2"; shift 2 ;;
-d|--data) data="$2"; shift 2 ;;
-s|-S|-sS) shift ;;
http://*|https://*) url="$1"; shift ;;
*) shift ;;
esac
done
# Resolve the bearer token from the curl --config file (its real, secure
# source). The config line is `header = "Authorization: token <value>"`.
if [[ -z "$auth_token" && -n "$config_file" && -f "$config_file" ]]; then
config_hdr="$(grep -i 'Authorization' "$config_file" 2>/dev/null || true)"
if [[ "$config_hdr" == *"token "* ]]; then
auth_token="${config_hdr##*token }"
auth_token="${auth_token%\"}"
fi
fi
path="${url%%\?*}"
printf '%s %s\n' "$method" "$url" >> "$GRANT_REVIEWER_CURL_LOG"
# Map the presented bearer token to the identity it authenticates as. Every
# request the wrapper makes must carry the SAME credential, so the identity
# recorded here reveals which credential actually performed each request.
acting_identity=""
case "$auth_token" in
"$GRANT_REVIEWER_DEFAULT_TOKEN") acting_identity="$GRANT_REVIEWER_DEFAULT_IDENTITY" ;;
"$GRANT_REVIEWER_OVERRIDE_TOKEN") acting_identity="$GRANT_REVIEWER_OVERRIDE_LOGIN" ;;
esac
printf '%s %s %s\n' "$method" "$path" "${acting_identity:-<unauthenticated>}" >> "$GRANT_REVIEWER_AUTH_LOG"
write_response() {
local status="$1" body="$2"
[[ -n "$output_file" ]] || exit 96
printf '%s' "$body" > "$output_file"
printf '%s' "$status"
}
[[ -n "$acting_identity" ]] || { write_response 401 '{"message":"unauthenticated"}'; exit 0; }
mode="$GRANT_REVIEWER_TEST_MODE"
org="$GRANT_REVIEWER_ORG"
api="$GRANT_REVIEWER_API_ROOT"
if [[ "$method" == "GET" && "$path" == "$api/orgs/$org" ]]; then
if [[ "$mode" == "not-an-org" ]]; then
write_response 404 '{"message":"not found"}'
else
write_response 200 "{\"username\":\"$org\"}"
fi
elif [[ "$method" == "GET" && "$path" == "$api/orgs/$org/teams/search" ]]; then
result=$(python3 - "$GRANT_REVIEWER_STATE" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
state = json.load(handle)
print(json.dumps({"ok": True, "data": state["teams"]}))
PY
)
write_response 200 "$result"
elif [[ "$method" == "POST" && "$path" == "$api/orgs/$org/teams" ]]; then
if [[ "$mode" == "create-403" ]]; then
write_response 403 '{"message":"forbidden"}'
exit 0
fi
result=$(GRANT_REVIEWER_DATA="$data" python3 - "$GRANT_REVIEWER_STATE" <<'PY'
import json
import os
import sys
payload = json.loads(os.environ["GRANT_REVIEWER_DATA"])
expected = {
"name": os.environ["GRANT_REVIEWER_TEAM_NAME"],
"description": "review seats: code read + issues/pulls write",
"permission": "read",
"includes_all_repositories": False,
"can_create_org_repo": False,
"units_map": {
"repo.code": "read",
"repo.issues": "write",
"repo.pulls": "write",
},
}
if payload != expected:
with open(os.environ["GRANT_REVIEWER_PAYLOAD_VIOLATION"], "w", encoding="utf-8") as handle:
json.dump({"got": payload, "expected": expected}, handle, indent=2)
print("422")
print(json.dumps({"message": "payload mismatch"}))
raise SystemExit(0)
state_path = sys.argv[1]
with open(state_path, encoding="utf-8") as handle:
state = json.load(handle)
team = {"id": int(os.environ["GRANT_REVIEWER_TEAM_ID"]), "name": payload["name"]}
state["teams"].append(team)
with open(state_path, "w", encoding="utf-8") as handle:
json.dump(state, handle)
print("201")
print(json.dumps(team))
PY
)
response_status="${result%%$'\n'*}"
response_body="${result#*$'\n'}"
write_response "$response_status" "$response_body"
elif [[ "$method" == "PUT" && "$path" == "$api/teams/$GRANT_REVIEWER_TEAM_ID/members/$GRANT_REVIEWER_REVIEWER" ]]; then
# Sabotage mode member-put-noop: 204 WITHOUT persisting — the exit-code lie.
if [[ "$mode" != "member-put-noop" ]]; then
python3 - "$GRANT_REVIEWER_STATE" <<'PY'
import json
import os
import sys
state_path = sys.argv[1]
with open(state_path, encoding="utf-8") as handle:
state = json.load(handle)
member = os.environ["GRANT_REVIEWER_REVIEWER"]
if member not in state["members"]:
state["members"].append(member)
with open(state_path, "w", encoding="utf-8") as handle:
json.dump(state, handle)
PY
fi
write_response 204 ''
elif [[ "$method" == "PUT" && "$path" == "$api/teams/$GRANT_REVIEWER_TEAM_ID/repos/$GRANT_REVIEWER_REPO_SLUG" ]]; then
# Sabotage mode repo-put-noop: 204 WITHOUT persisting.
if [[ "$mode" != "repo-put-noop" ]]; then
python3 - "$GRANT_REVIEWER_STATE" <<'PY'
import json
import os
import sys
state_path = sys.argv[1]
with open(state_path, encoding="utf-8") as handle:
state = json.load(handle)
slug = os.environ["GRANT_REVIEWER_REPO_SLUG"]
if slug not in state["repos"]:
state["repos"].append(slug)
with open(state_path, "w", encoding="utf-8") as handle:
json.dump(state, handle)
PY
fi
write_response 204 ''
elif [[ "$method" == "GET" && "$path" == "$api/teams/$GRANT_REVIEWER_TEAM_ID/members/$GRANT_REVIEWER_REVIEWER" ]]; then
if python3 - "$GRANT_REVIEWER_STATE" <<'PY'
import json
import os
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
state = json.load(handle)
raise SystemExit(0 if os.environ["GRANT_REVIEWER_REVIEWER"] in state["members"] else 1)
PY
then
write_response 200 "{\"login\":\"$GRANT_REVIEWER_REVIEWER\"}"
else
write_response 404 '{"message":"not a member"}'
fi
elif [[ "$method" == "GET" && "$path" == "$api/teams/$GRANT_REVIEWER_TEAM_ID/repos/$GRANT_REVIEWER_REPO_SLUG" ]]; then
if python3 - "$GRANT_REVIEWER_STATE" <<'PY'
import json
import os
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
state = json.load(handle)
raise SystemExit(0 if os.environ["GRANT_REVIEWER_REPO_SLUG"] in state["repos"] else 1)
PY
then
write_response 200 "{\"full_name\":\"$GRANT_REVIEWER_REPO_SLUG\"}"
else
write_response 404 '{"message":"repo not on team"}'
fi
else
echo "Unexpected curl request: $method $url" >&2
exit 97
fi
SH
chmod +x "$BIN_DIR/curl"
# Seed persistent server state for a mode: fresh (no team yet) or a pre-seeded
# team. The DECOY team — whose name CONTAINS the wanted name — is always listed
# FIRST, so a first-result or substring match would grab the wrong team.
seed_state() {
local seeded_team="$1"
GRANT_REVIEWER_SEEDED_TEAM="$seeded_team" GRANT_REVIEWER_TEAM_NAME="$TEAM_NAME" \
GRANT_REVIEWER_TEAM_ID="$TEAM_ID" GRANT_REVIEWER_DECOY_TEAM_ID="$DECOY_TEAM_ID" \
python3 - "$STATE_FILE" <<'PY'
import json
import os
import sys
wanted = os.environ["GRANT_REVIEWER_TEAM_NAME"]
teams = [{"id": int(os.environ["GRANT_REVIEWER_DECOY_TEAM_ID"]), "name": wanted + "-archive"}]
if os.environ["GRANT_REVIEWER_SEEDED_TEAM"] == "yes":
teams.append({"id": int(os.environ["GRANT_REVIEWER_TEAM_ID"]), "name": wanted})
with open(sys.argv[1], "w", encoding="utf-8") as handle:
json.dump({"teams": teams, "members": [], "repos": []}, handle)
PY
}
# run_grant <mode> <seeded-team yes|no> [extra env VAR=value ...] -- [wrapper args ...]
run_grant() {
local mode="$1" seeded="$2"
shift 2
local -a extra_env=()
while [[ $# -gt 0 && "$1" != "--" ]]; do
extra_env+=("$1")
shift
done
[[ $# -gt 0 ]] && shift
: > "$TEA_LOG"
: > "$CURL_LOG"
: > "$CURL_ARGV_LOG"
: > "$AUTH_LOG"
: > "$OUTPUT_FILE"
rm -f "$PAYLOAD_VIOLATION_FILE"
seed_state "$seeded"
(
cd "$RUN_REPO_DIR"
env \
PATH="$BIN_DIR:$PATH" \
TMPDIR="$TMP_SCRATCH" \
HOME="$HOME_DIR" \
XDG_CONFIG_HOME="$XDG_DIR" \
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
MOSAIC_BRAIN_HOME="$HOME_DIR/.mosaic" \
MOSAIC_GIT_IDENTITY= \
GITEA_LOGIN= \
GITEA_TOKEN= \
GITEA_URL= \
GRANT_REVIEWER_TEA_LOG="$TEA_LOG" \
GRANT_REVIEWER_CURL_LOG="$CURL_LOG" \
GRANT_REVIEWER_CURL_ARGV_LOG="$CURL_ARGV_LOG" \
GRANT_REVIEWER_AUTH_LOG="$AUTH_LOG" \
GRANT_REVIEWER_STATE="$STATE_FILE" \
GRANT_REVIEWER_TEST_MODE="$mode" \
GRANT_REVIEWER_ORG="$ORG" \
GRANT_REVIEWER_API_ROOT="$API_ROOT" \
GRANT_REVIEWER_TEAM_NAME="$TEAM_NAME" \
GRANT_REVIEWER_TEAM_ID="$TEAM_ID" \
GRANT_REVIEWER_REVIEWER="$REVIEWER" \
GRANT_REVIEWER_REPO_SLUG="$REPO_SLUG" \
GRANT_REVIEWER_DEFAULT_TOKEN="$DEFAULT_TOKEN" \
GRANT_REVIEWER_DEFAULT_IDENTITY="$DEFAULT_IDENTITY" \
GRANT_REVIEWER_OVERRIDE_LOGIN="$OVERRIDE_LOGIN" \
GRANT_REVIEWER_OVERRIDE_TOKEN="$OVERRIDE_TOKEN" \
GRANT_REVIEWER_PAYLOAD_VIOLATION="$PAYLOAD_VIOLATION_FILE" \
"${extra_env[@]}" \
"$SCRIPT_DIR/grant-reviewer.sh" -u "$REVIEWER" "$@"
) > "$OUTPUT_FILE" 2>&1
}
assert_no_temp_leak() {
local context="$1" leaked
# Includes the curl auth-config files (mosaic-gitea-auth-*), which carry the
# bearer token and must be unlinked on every exit path.
leaked=$(find "$TMP_SCRATCH" -type f \( -name 'mosaic-grant-reviewer-*' -o -name 'mosaic-gitea-auth-*' \) 2>/dev/null || true)
if [[ -n "$leaked" ]]; then
echo "FAIL: grant-reviewer temp files leaked ($context):" >&2
printf '%s\n' "$leaked" >&2
exit 1
fi
}
assert_token_not_in_argv() {
local context="$1"
if grep -qF -e "$DEFAULT_TOKEN" -e "$OVERRIDE_TOKEN" "$CURL_ARGV_LOG"; then
echo "FAIL: a Gitea bearer token leaked into curl argv ($context)" >&2
exit 1
fi
if ! grep -q -- '--config' "$CURL_ARGV_LOG"; then
echo "FAIL: curl was not invoked with --config file auth ($context)" >&2
exit 1
fi
}
assert_no_payload_violation() {
local context="$1"
if [[ -f "$PAYLOAD_VIOLATION_FILE" ]]; then
echo "FAIL: team create payload deviated from the reviewer contract ($context):" >&2
cat "$PAYLOAD_VIOLATION_FILE" >&2
exit 1
fi
}
RUN_REPO_DIR="$REPO_DIR"
# Case 1: fresh grant — team absent, created with the exact reviewer payload,
# member + repo PUTs persist, both read-backs verify against server state.
run_grant normal no -- || {
echo "FAIL: fresh grant exited nonzero" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
grep -q "Created team '$TEAM_NAME' (id $TEAM_ID) on org '$ORG'" "$OUTPUT_FILE" || {
echo "FAIL: fresh grant did not create the team" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
grep -q "Granted: '$REVIEWER' is a member of team '$TEAM_NAME' (id $TEAM_ID) with access to '$REPO_SLUG'" "$OUTPUT_FILE" || {
echo "FAIL: fresh grant did not report a verified grant" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
assert_no_payload_violation "fresh"
assert_token_not_in_argv "fresh"
assert_no_temp_leak "fresh"
# The default path must have acted as the host-default identity on EVERY request.
if grep -qv " $DEFAULT_IDENTITY\$" "$AUTH_LOG"; then
echo "FAIL: fresh grant made a request under an unexpected identity" >&2
cat "$AUTH_LOG" >&2
exit 1
fi
# grant-reviewer must never shell out to tea.
if [[ -s "$TEA_LOG" ]]; then
echo "FAIL: grant-reviewer invoked tea" >&2
cat "$TEA_LOG" >&2
exit 1
fi
# Case 2: idempotent — the team already exists. It must be found by EXACT name
# (the decoy is listed first), no create POST issued, and the decoy team must
# never be touched.
run_grant normal yes -- || {
echo "FAIL: idempotent grant exited nonzero" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
grep -q "Found existing team '$TEAM_NAME' (id $TEAM_ID) on org '$ORG'" "$OUTPUT_FILE" || {
echo "FAIL: idempotent grant did not find the existing team" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
grep -q "Granted: '$REVIEWER'" "$OUTPUT_FILE" || {
echo "FAIL: idempotent grant did not report a verified grant" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
if grep -q "^POST " "$CURL_LOG"; then
echo "FAIL: idempotent grant issued a create POST for an existing team" >&2
cat "$CURL_LOG" >&2
exit 1
fi
if grep -q "/teams/$DECOY_TEAM_ID/" "$CURL_LOG"; then
echo "FAIL: substring-named decoy team was operated on" >&2
cat "$CURL_LOG" >&2
exit 1
fi
assert_no_temp_leak "idempotent"
# Case 3: GITEA_LOGIN override — every request must carry the override login's
# token, never the host default credential.
run_grant normal no GITEA_LOGIN="$OVERRIDE_LOGIN" -- || {
echo "FAIL: GITEA_LOGIN override grant exited nonzero" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
grep -q "Granted: '$REVIEWER'" "$OUTPUT_FILE" || {
echo "FAIL: GITEA_LOGIN override grant did not succeed" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
if grep -qv " $OVERRIDE_LOGIN\$" "$AUTH_LOG"; then
echo "FAIL: GITEA_LOGIN override made a request under a different identity" >&2
cat "$AUTH_LOG" >&2
exit 1
fi
assert_token_not_in_argv "override"
assert_no_temp_leak "override"
# Case 4: unresolvable GITEA_LOGIN — fail closed BEFORE any API call; no
# downgrade to the host default identity.
if run_grant normal no GITEA_LOGIN="no-such-login" --; then
echo "FAIL: unresolvable GITEA_LOGIN did not fail" >&2
cat "$OUTPUT_FILE" >&2
exit 1
fi
grep -q "refusing to fall back to the host default identity" "$OUTPUT_FILE" || {
echo "FAIL: unresolvable GITEA_LOGIN missing the fail-closed message" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
if [[ -s "$CURL_LOG" ]]; then
echo "FAIL: unresolvable GITEA_LOGIN still made API calls" >&2
cat "$CURL_LOG" >&2
exit 1
fi
assert_no_temp_leak "unresolvable-login"
# Case 5: GitHub-remoted repo — refuse before any API call.
RUN_REPO_DIR="$GH_REPO_DIR"
if run_grant normal no --; then
echo "FAIL: GitHub repo was not refused" >&2
cat "$OUTPUT_FILE" >&2
exit 1
fi
grep -q "Gitea only" "$OUTPUT_FILE" || {
echo "FAIL: GitHub refusal missing the 'Gitea only' message" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
if [[ -s "$CURL_LOG" ]]; then
echo "FAIL: GitHub refusal still made API calls" >&2
cat "$CURL_LOG" >&2
exit 1
fi
RUN_REPO_DIR="$REPO_DIR"
# Case 6: owner is not an organization — clear refusal.
if run_grant not-an-org no --; then
echo "FAIL: non-org owner was not refused" >&2
cat "$OUTPUT_FILE" >&2
exit 1
fi
grep -q "is not an organization" "$OUTPUT_FILE" || {
echo "FAIL: non-org refusal missing its message" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
assert_no_temp_leak "not-an-org"
# Case 7: HTTP 403 on team create — reported as an org-admin requirement, and
# the run stops before any member/repo PUT (no partial grant).
if run_grant create-403 no --; then
echo "FAIL: 403 on team create did not fail the run" >&2
cat "$OUTPUT_FILE" >&2
exit 1
fi
grep -q "org admin required on '$ORG'" "$OUTPUT_FILE" || {
echo "FAIL: 403 was not mapped to the org-admin message" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
if grep -q "^PUT " "$CURL_LOG"; then
echo "FAIL: run continued into PUTs after a 403 (partial grant)" >&2
cat "$CURL_LOG" >&2
exit 1
fi
assert_no_temp_leak "create-403"
# Cases 8-9: the exit-code lie — a PUT answers 204 without persisting. The
# read-back must fail closed; no success line may appear.
for noop_mode in member-put-noop repo-put-noop; do
if run_grant "$noop_mode" no --; then
echo "FAIL: $noop_mode was reported as success" >&2
cat "$OUTPUT_FILE" >&2
exit 1
fi
grep -q "NOT verified" "$OUTPUT_FILE" || {
echo "FAIL: $noop_mode missing the fail-closed verification message" >&2
cat "$OUTPUT_FILE" >&2
exit 1
}
if grep -q "^Granted:" "$OUTPUT_FILE"; then
echo "FAIL: $noop_mode still printed the success line" >&2
exit 1
fi
assert_no_temp_leak "$noop_mode"
done
echo "grant-reviewer.sh org-team grant + fail-closed read-back regression passed"