fix(release): close fixed-code mobile auth gaps

This commit is contained in:
key
2026-07-29 11:59:30 +08:00
parent 3c92edd23d
commit 70e5516ff3
21 changed files with 546 additions and 65 deletions
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SCRIPT="$ROOT/scripts/verify-aug1-authenticated-production.sh"
contains() {
grep -Fq -- "$2" "$1" || {
echo "FAIL: expected '$2' in $1" >&2
exit 1
}
}
not_contains() {
if grep -Fq -- "$2" "$1"; then
echo "FAIL: forbidden '$2' in $1" >&2
exit 1
fi
}
contains "$SCRIPT" '[[ "${1:-}" != "--execute" ]]'
contains "$SCRIPT" 'AIHR_SMS_PROD_FIXED_CODE_ENABLED'
contains "$SCRIPT" "u.user_type = 'app_user'"
contains "$SCRIPT" 'HAVING COUNT(DISTINCT o.ext_party_id) = 1'
contains "$SCRIPT" 'FIXED_SMS_NO_REAL_SEND_LOG=PASS'
contains "$SCRIPT" 'DIRECT_CHANNEL_COUNT'
contains "$SCRIPT" 'LOGOUT_TOKEN_REJECTED'
contains "$SCRIPT" 'time.sleep(60)'
contains "$SCRIPT" 'RELOGIN_FIXED_SMS_REQUEST'
contains "$SCRIPT" '"MOBILE_RELOGIN"'
contains "$SCRIPT" 'MOBILE_RELOGIN_ME'
contains "$SCRIPT" 'fixed_log_before + 2'
not_contains "$SCRIPT" 'echo "$phone"'
not_contains "$SCRIPT" 'echo "$fixed_code"'
not_contains "$SCRIPT" 'echo "$token"'
not_contains "$SCRIPT" '/api/aihr/direct/feedback",'
not_contains "$SCRIPT" '/api/aihr/agent/messages",'
not_contains "$SCRIPT" '/api/ai/asr",'
not_contains "$SCRIPT" '/api/ai/tts",'
echo "PASS: authenticated production smoke is fixed-code-only, redacted and non-business-writing"
@@ -0,0 +1,247 @@
#!/usr/bin/env bash
set -euo pipefail
REMOTE_SSH="${AIHR_AUG1_REMOTE_SSH:-YCWY}"
REMOTE_SERVICE="${AIHR_AUG1_REMOTE_SERVICE:-wygj-aihr.service}"
REMOTE_DB="${AIHR_AUG1_REMOTE_DB:-ry-vue}"
REMOTE_BASE_URL="${AIHR_AUG1_REMOTE_BASE_URL:-https://peilian.njzhmj.top/dev-api}"
if [[ "${1:-}" != "--execute" ]]; then
cat <<'USAGE'
Usage: ./scripts/verify-aug1-authenticated-production.sh --execute
Runs an authenticated August 1 production smoke test. The remote service must
have both fixed-code settings enabled. The script selects an existing active
APP user with a unique organization identity, never prints the phone, code, or
token, performs no registration and submits no business records.
USAGE
exit 2
fi
[[ "$REMOTE_DB" =~ ^[A-Za-z0-9_-]+$ ]] || {
echo "FAIL: invalid remote database name" >&2
exit 3
}
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" bash -s -- \
"$REMOTE_SERVICE" "$REMOTE_DB" "$REMOTE_BASE_URL" <<'REMOTE'
set -euo pipefail
service="$1"
db="$2"
base="$3"
pid="$(systemctl show -p MainPID --value "$service")"
[[ -n "$pid" && "$pid" != "0" ]] || {
echo "FAIL: production service is not running" >&2
exit 10
}
fixed_code=""
fixed_enabled="false"
while IFS='=' read -r key value; do
case "$key" in
AIHR_SMS_DEV_FIXED_CODE) fixed_code="$value" ;;
AIHR_SMS_PROD_FIXED_CODE_ENABLED) fixed_enabled="$value" ;;
esac
done < <(tr '\0' '\n' < "/proc/$pid/environ")
[[ -n "$fixed_code" && "$fixed_enabled" == "true" ]] || {
echo "FAIL: production fixed-code mode is not explicitly enabled" >&2
exit 11
}
phone="$(mysql --batch --skip-column-names --connect-timeout=5 "$db" -e "
SELECT o.person_phone
FROM aihr_org_snapshot o
JOIN sys_user u
ON u.tenant_id = o.tenant_id
AND BINARY u.phonenumber = BINARY o.person_phone
AND u.user_type = 'app_user'
AND u.status = '0'
AND u.del_flag = '0'
WHERE o.tenant_id = '000000'
AND o.employment_status = 'active'
AND o.person_phone REGEXP '^1[3-9][0-9]{9}$'
AND NOT EXISTS (
SELECT 1
FROM sys_user su
WHERE su.tenant_id = o.tenant_id
AND BINARY su.phonenumber = BINARY o.person_phone
AND (su.user_type IS NULL OR su.user_type <> 'app_user')
)
AND NOT EXISTS (
SELECT 1
FROM aihr_org_snapshot x
WHERE x.tenant_id = o.tenant_id
AND x.employment_status = 'active'
AND x.ext_party_id = o.ext_party_id
AND NULLIF(x.person_phone, '') IS NOT NULL
AND BINARY x.person_phone <> BINARY o.person_phone
)
GROUP BY o.person_phone
HAVING COUNT(DISTINCT o.ext_party_id) = 1
ORDER BY MAX(CASE WHEN o.position_name = '生活顾问' THEN 1 ELSE 0 END) DESC,
MAX(o.snapshot_date) DESC
LIMIT 1
")"
[[ "$phone" =~ ^1[3-9][0-9]{9}$ ]] || {
echo "FAIL: no existing active APP user has a unique organization identity" >&2
exit 12
}
count_fixed_log() {
local total=0 root matches
for root in /opt/wygj/logs /opt/wygj/app/logs /var/log/wygj; do
[[ -d "$root" ]] || continue
matches="$(grep -R -F -h --include='*.log' --include='*.out' \
'短信验证码走固定码,未真实发送短信' "$root" 2>/dev/null | wc -l)"
total=$((total + matches))
done
printf '%s\n' "$total"
}
fixed_log_before="$(count_fixed_log)"
AIHR_TEST_BASE="$base" \
AIHR_TEST_PHONE="$phone" \
AIHR_TEST_CODE="$fixed_code" \
python3 - <<'PY'
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
base = os.environ["AIHR_TEST_BASE"].rstrip("/")
phone = os.environ["AIHR_TEST_PHONE"]
code = os.environ["AIHR_TEST_CODE"]
def call(method, path, body=None, token=None, client=None):
data = None if body is None else json.dumps(body, ensure_ascii=False).encode("utf-8")
headers = {"Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
if token:
headers["Authorization"] = f"Bearer {token}"
if client:
headers["clientid"] = client
request = urllib.request.Request(base + path, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=45) as response:
raw = response.read().decode("utf-8", "replace")
return response.status, json.loads(raw or "{}")
except urllib.error.HTTPError as error:
raw = error.read().decode("utf-8", "replace")
try:
payload = json.loads(raw or "{}")
except Exception:
payload = {}
return error.code, payload
def require_ok(label, result):
status, payload = result
business_code = payload.get("code") if isinstance(payload, dict) else None
passed = status == 200 and business_code == 200
print(f"{label}={'PASS' if passed else 'FAIL'} http={status} biz={business_code}")
if not passed:
raise SystemExit(20)
return payload.get("data")
sms_path = "/resource/sms/code?phonenumber=" + urllib.parse.quote(phone)
require_ok("FIXED_SMS_REQUEST", call("GET", sms_path))
login = require_ok(
"MOBILE_LOGIN",
call("POST", "/auth/mobile/sms-login", {"phonenumber": phone, "smsCode": code}),
)
if not isinstance(login, dict) or not login.get("access_token") or not login.get("client_id"):
print("MOBILE_LOGIN_TOKEN=FAIL")
raise SystemExit(21)
token = login["access_token"]
client = login["client_id"]
identity = require_ok(
"MOBILE_ME",
call("GET", "/api/aihr/mobile/me", token=token, client=client),
)
role = identity.get("role") if isinstance(identity, dict) else None
projects = identity.get("projects") if isinstance(identity, dict) else []
print("MOBILE_ROLE_VALID=" + ("PASS" if role in ("user", "supervisor") else "FAIL"))
print("MOBILE_PROJECT_SCOPE=" + ("NONEMPTY" if isinstance(projects, list) and projects else "EMPTY"))
if role not in ("user", "supervisor") or not isinstance(projects, list) or not projects:
raise SystemExit(22)
route_role = "supervisor" if role == "supervisor" else "user"
read_checks = [
("MOBILE_HOME_AUTHENTICATED", f"/api/aihr/mobile/home/{route_role}"),
("PRACTICE_HISTORY_READ", "/api/aihr/mobile/practice/history"),
("PRACTICE_PROFILE_READ", "/api/aihr/mobile/profile"),
("DIRECT_MINE_READ", "/api/aihr/direct/mine?pageNum=1&pageSize=10"),
("BROADCAST_READ", "/api/aihr/broadcast/messages?pageNum=1&pageSize=10&scope=all"),
("EXAM_READ", "/api/aihr/mobile/exams"),
("CASE_CAPABILITIES_READ", "/api/knowledge/case/capabilities"),
("WEB_AI_CAPABILITIES_READ", "/api/aihr/web-ai/capabilities"),
]
for label, path in read_checks:
require_ok(label, call("GET", path, token=token, client=client))
channels = require_ok(
"DIRECT_CHANNELS_READ",
call("GET", "/api/aihr/direct/channels", token=token, client=client),
)
channel_count = len(channels) if isinstance(channels, list) else -1
print(f"DIRECT_CHANNEL_COUNT={channel_count}")
if channel_count != 5:
raise SystemExit(23)
require_ok(
"MOBILE_LOGOUT",
call("POST", "/auth/logout", {}, token=token, client=client),
)
status, payload = call("GET", "/api/aihr/mobile/me", token=token, client=client)
business_code = payload.get("code") if isinstance(payload, dict) else None
expired = status == 401 or business_code == 401
print(f"LOGOUT_TOKEN_REJECTED={'PASS' if expired else 'FAIL'} http={status} biz={business_code}")
if not expired:
raise SystemExit(24)
time.sleep(60)
require_ok("RELOGIN_FIXED_SMS_REQUEST", call("GET", sms_path))
relogin = require_ok(
"MOBILE_RELOGIN",
call("POST", "/auth/mobile/sms-login", {"phonenumber": phone, "smsCode": code}),
)
if not isinstance(relogin, dict) or not relogin.get("access_token") or not relogin.get("client_id"):
print("MOBILE_RELOGIN_TOKEN=FAIL")
raise SystemExit(25)
relogin_token = relogin["access_token"]
relogin_client = relogin["client_id"]
require_ok(
"MOBILE_RELOGIN_ME",
call("GET", "/api/aihr/mobile/me", token=relogin_token, client=relogin_client),
)
require_ok(
"MOBILE_RELOGIN_LOGOUT",
call("POST", "/auth/logout", {}, token=relogin_token, client=relogin_client),
)
PY
fixed_log_after="$fixed_log_before"
fixed_log_expected=$((fixed_log_before + 2))
for _ in 1 2 3 4 5; do
fixed_log_after="$(count_fixed_log)"
(( fixed_log_after >= fixed_log_expected )) && break
sleep 1
done
if (( fixed_log_after < fixed_log_expected )); then
echo "FIXED_SMS_NO_REAL_SEND_LOG=FAIL"
exit 13
fi
echo "FIXED_SMS_NO_REAL_SEND_LOG=PASS"
REMOTE