fix(release): close fixed-code mobile auth gaps
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user