131 lines
7.6 KiB
Bash
Executable File
131 lines
7.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# new-api 额度池导入回归:使用假 hostname/SSH,不连接 mini-desktop、不写真实 MySQL。
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
SCRIPT="$ROOT_DIR/deploy/import-newapi-pool.sh"
|
||
TMP_DIR="$(mktemp -d)"
|
||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||
|
||
fail() { printf 'TEST_FAIL %s\n' "$*" >&2; exit 1; }
|
||
mode_of() { stat -f '%Lp' "$1" 2>/dev/null || stat -c '%a' "$1"; }
|
||
|
||
[ -x "$SCRIPT" ] || fail "缺少可执行额度池导入器"
|
||
bash -n "$SCRIPT"
|
||
rg -q "readonly MYSQL_DATABASE='ruoyi-vue-pro'" "$SCRIPT" || fail "目标数据库不是固定值"
|
||
rg -q "readonly POOL_TABLE='newapi_quota_pool'" "$SCRIPT" || fail "目标表不是固定值"
|
||
rg -q 'START TRANSACTION' "$SCRIPT" || fail "导入未显式开启事务"
|
||
rg -q 'COMMIT' "$SCRIPT" || fail "导入未显式提交事务"
|
||
|
||
cat >"$TMP_DIR/hostname" <<'SH'
|
||
#!/usr/bin/env bash
|
||
printf '%s\n' "${MOCK_HOSTNAME_VALUE:-mini-infra}"
|
||
SH
|
||
|
||
cat >"$TMP_DIR/ssh" <<'SH'
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
printf '%s\n' "$*" >>"$MOCK_SSH_ARGS"
|
||
cat >"$MOCK_SQL_CAPTURE"
|
||
rg -q 'sk-import-secret-must-not-leak' "$MOCK_SQL_CAPTURE"
|
||
rows="$(rg -c '^INSERT INTO newapi_quota_pool ' "$MOCK_SQL_CAPTURE")"
|
||
[ "$rows" = 4 ]
|
||
before="$(cat "$MOCK_IMPORT_STATE" 2>/dev/null || printf 0)"
|
||
printf 'BEFORE\t%s\t%s\t0\t0\t%s\n' "$before" "$before" "$before"
|
||
printf '%s\n' 4 >"$MOCK_IMPORT_STATE"
|
||
printf 'AFTER\t4\t4\t0\t0\t4\n'
|
||
SH
|
||
chmod +x "$TMP_DIR/hostname" "$TMP_DIR/ssh"
|
||
|
||
cat >"$TMP_DIR/pool.sql" <<'SQL'
|
||
-- 仅供本地假 SSH 回归,不接触真实数据库。
|
||
-- pool_entry username=neice_051 newapi_user_id=51
|
||
INSERT INTO newapi_quota_pool (newapi_user_id, newapi_token_id, newapi_token_key, grant_quota, quota_per_unit_snapshot, usd_rate_snapshot, status) VALUES (51, 151, 'sk-import-secret-must-not-leak-51', 6849315, 500000, 7.3, 'FREE') ON DUPLICATE KEY UPDATE newapi_token_id=VALUES(newapi_token_id), newapi_token_key=VALUES(newapi_token_key), grant_quota=VALUES(grant_quota), quota_per_unit_snapshot=VALUES(quota_per_unit_snapshot), usd_rate_snapshot=VALUES(usd_rate_snapshot);
|
||
-- pool_entry username=neice_052 newapi_user_id=52
|
||
INSERT INTO newapi_quota_pool (newapi_user_id, newapi_token_id, newapi_token_key, grant_quota, quota_per_unit_snapshot, usd_rate_snapshot, status) VALUES (52, 152, 'sk-import-secret-must-not-leak-52', 6849315, 500000, 7.3, 'FREE') ON DUPLICATE KEY UPDATE newapi_token_id=VALUES(newapi_token_id), newapi_token_key=VALUES(newapi_token_key), grant_quota=VALUES(grant_quota), quota_per_unit_snapshot=VALUES(quota_per_unit_snapshot), usd_rate_snapshot=VALUES(usd_rate_snapshot);
|
||
-- pool_entry username=neice_053 newapi_user_id=53
|
||
INSERT INTO newapi_quota_pool (newapi_user_id, newapi_token_id, newapi_token_key, grant_quota, quota_per_unit_snapshot, usd_rate_snapshot, status) VALUES (53, 153, 'sk-import-secret-must-not-leak-53', 6849315, 500000, 7.3, 'FREE') ON DUPLICATE KEY UPDATE newapi_token_id=VALUES(newapi_token_id), newapi_token_key=VALUES(newapi_token_key), grant_quota=VALUES(grant_quota), quota_per_unit_snapshot=VALUES(quota_per_unit_snapshot), usd_rate_snapshot=VALUES(usd_rate_snapshot);
|
||
-- pool_entry username=neice_054 newapi_user_id=54
|
||
INSERT INTO newapi_quota_pool (newapi_user_id, newapi_token_id, newapi_token_key, grant_quota, quota_per_unit_snapshot, usd_rate_snapshot, status) VALUES (54, 154, 'sk-import-secret-must-not-leak-54', 6849315, 500000, 7.3, 'FREE') ON DUPLICATE KEY UPDATE newapi_token_id=VALUES(newapi_token_id), newapi_token_key=VALUES(newapi_token_key), grant_quota=VALUES(grant_quota), quota_per_unit_snapshot=VALUES(quota_per_unit_snapshot), usd_rate_snapshot=VALUES(usd_rate_snapshot);
|
||
SQL
|
||
chmod 600 "$TMP_DIR/pool.sql"
|
||
|
||
run_import() {
|
||
local evidence="$1"
|
||
NEWAPI_POOL_IMPORT_ACK='IMPORT_NEWAPI_POOL_TO_STAGING' \
|
||
NEWAPI_POOL_SQL_FILE="$TMP_DIR/pool.sql" \
|
||
NEWAPI_POOL_IMPORT_EVIDENCE="$evidence" \
|
||
NEWAPI_POOL_IMPORT_HOST='root@100.64.0.7' \
|
||
HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" \
|
||
MOCK_HOSTNAME_VALUE="${MOCK_HOSTNAME_VALUE:-mini-infra}" \
|
||
MOCK_SSH_ARGS="$TMP_DIR/ssh-args" MOCK_SQL_CAPTURE="$TMP_DIR/captured.sql" \
|
||
MOCK_IMPORT_STATE="$TMP_DIR/import-state" \
|
||
"$SCRIPT"
|
||
}
|
||
|
||
run_import "$TMP_DIR/import-first.evidence" >"$TMP_DIR/first.log"
|
||
run_import "$TMP_DIR/import-second.evidence" >"$TMP_DIR/second.log"
|
||
for evidence in "$TMP_DIR/import-first.evidence" "$TMP_DIR/import-second.evidence"; do
|
||
[ "$(mode_of "$evidence")" = 400 ] || fail "脱敏证据权限不是 400:$evidence"
|
||
rg -q '^status=PASS$' "$evidence" || fail "证据未记录 PASS"
|
||
rg -q '^source_row_count=4$' "$evidence" || fail "证据未记录四条输入"
|
||
rg -q '^after_target_total=4$' "$evidence" || fail "导入后目标聚合不为四条"
|
||
rg -q '^source_sql_sha256=[0-9a-f]{64}$' "$evidence" || fail "证据缺 SQL 哈希"
|
||
! rg -q 'sk-import-secret-must-not-leak' "$evidence" || fail "证据泄漏 token"
|
||
done
|
||
rg -q '^before_target_total=4$' "$TMP_DIR/import-second.evidence" || fail "第二次导入未证明幂等"
|
||
rg -q 'ProxyCommand=none' "$TMP_DIR/ssh-args" || fail "SSH 未禁用代理"
|
||
rg -q 'root@100\.64\.0\.7' "$TMP_DIR/ssh-args" || fail "未限定 mini-desktop Tailscale 主机"
|
||
! rg -q 'sk-import-secret-must-not-leak' "$TMP_DIR/first.log" "$TMP_DIR/second.log" || fail "日志泄漏 token"
|
||
|
||
# ACK、执行宿主、远端目标和 SQL 结构均必须在 SSH 前 fail-closed。
|
||
calls_before="$(wc -l <"$TMP_DIR/ssh-args" | tr -d ' ')"
|
||
if NEWAPI_POOL_IMPORT_ACK=wrong NEWAPI_POOL_SQL_FILE="$TMP_DIR/pool.sql" \
|
||
HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" "$SCRIPT" >/dev/null 2>&1; then
|
||
fail "错误 ACK 未被拒绝"
|
||
fi
|
||
if MOCK_HOSTNAME_VALUE=mini-desktop NEWAPI_POOL_IMPORT_ACK='IMPORT_NEWAPI_POOL_TO_STAGING' \
|
||
NEWAPI_POOL_SQL_FILE="$TMP_DIR/pool.sql" HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" \
|
||
"$SCRIPT" >/dev/null 2>&1; then
|
||
fail "非 mini-infra 宿主未被拒绝"
|
||
fi
|
||
if NEWAPI_POOL_IMPORT_ACK='IMPORT_NEWAPI_POOL_TO_STAGING' NEWAPI_POOL_SQL_FILE="$TMP_DIR/pool.sql" \
|
||
NEWAPI_POOL_IMPORT_HOST='root@100.64.0.8' HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" \
|
||
"$SCRIPT" >/dev/null 2>&1; then
|
||
fail "非 mini-desktop 目标未被拒绝"
|
||
fi
|
||
cp "$TMP_DIR/pool.sql" "$TMP_DIR/bad.sql"
|
||
printf 'DROP TABLE newapi_quota_pool;\n' >>"$TMP_DIR/bad.sql"
|
||
if NEWAPI_POOL_IMPORT_ACK='IMPORT_NEWAPI_POOL_TO_STAGING' NEWAPI_POOL_SQL_FILE="$TMP_DIR/bad.sql" \
|
||
HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" "$SCRIPT" >/dev/null 2>&1; then
|
||
fail "越界 SQL 未被拒绝"
|
||
fi
|
||
python3 - "$TMP_DIR/pool.sql" "$TMP_DIR/inline-injection.sql" <<'PY'
|
||
import sys
|
||
|
||
source, target = sys.argv[1:]
|
||
lines = open(source, encoding="utf-8").read().splitlines()
|
||
for index, line in enumerate(lines):
|
||
if line.startswith("INSERT INTO newapi_quota_pool "):
|
||
lines[index] = line.replace(
|
||
" ON DUPLICATE KEY UPDATE ",
|
||
"; DROP TABLE newapi_quota_pool; -- ON DUPLICATE KEY UPDATE ",
|
||
1,
|
||
)
|
||
break
|
||
open(target, "w", encoding="utf-8").write("\n".join(lines) + "\n")
|
||
PY
|
||
chmod 600 "$TMP_DIR/inline-injection.sql"
|
||
if NEWAPI_POOL_IMPORT_ACK='IMPORT_NEWAPI_POOL_TO_STAGING' \
|
||
NEWAPI_POOL_SQL_FILE="$TMP_DIR/inline-injection.sql" \
|
||
NEWAPI_POOL_IMPORT_EVIDENCE="$TMP_DIR/inline-injection.evidence" \
|
||
HOSTNAME_BIN="$TMP_DIR/hostname" SSH_BIN="$TMP_DIR/ssh" \
|
||
MOCK_SSH_ARGS="$TMP_DIR/ssh-args" MOCK_SQL_CAPTURE="$TMP_DIR/captured-injection.sql" \
|
||
MOCK_IMPORT_STATE="$TMP_DIR/import-state" "$SCRIPT" >/dev/null 2>&1; then
|
||
fail "同一 INSERT 行内夹带额外 SQL 未被拒绝"
|
||
fi
|
||
calls_after="$(wc -l <"$TMP_DIR/ssh-args" | tr -d ' ')"
|
||
[ "$calls_before" = "$calls_after" ] || fail "本地前置校验失败后仍发生 SSH"
|
||
|
||
printf 'TEST_PASS new-api pool 导入宿主、ACK、事务、哈希、聚合、幂等与脱敏校验通过\n'
|