fix: stabilize console scrolling and protect administrator login

This commit is contained in:
2026-09-30 00:09:01 +08:00
parent 9a15406f76
commit f56f233704
32 changed files with 1308 additions and 86 deletions

View File

@@ -8,13 +8,14 @@ E2E_IMAGE=${ORBIT_E2E_IMAGE:-orbit:e2e}
E2E_SUFFIX=$$
E2E_MYSQL="orbit-e2e-mysql-$E2E_SUFFIX"
E2E_ORBIT="orbit-e2e-server-$E2E_SUFFIX"
E2E_ORBIT_SECOND="orbit-e2e-login-peer-$E2E_SUFFIX"
E2E_DIR=$(mktemp -d "${TMPDIR:-/tmp}/orbit-console-e2e.XXXXXX")
E2E_PASSWORD_FILE="$E2E_DIR/console-password"
E2E_CREDENTIALS_FILE="$E2E_DIR/credentials.json"
cleanup() {
"$E2E_DOCKER" stop "$E2E_ORBIT" "$E2E_MYSQL" >/dev/null 2>&1 || true
"$E2E_DOCKER" container rm "$E2E_ORBIT" "$E2E_MYSQL" >/dev/null 2>&1 || true
"$E2E_DOCKER" stop "$E2E_ORBIT_SECOND" "$E2E_ORBIT" "$E2E_MYSQL" >/dev/null 2>&1 || true
"$E2E_DOCKER" container rm "$E2E_ORBIT_SECOND" "$E2E_ORBIT" "$E2E_MYSQL" >/dev/null 2>&1 || true
if [ -d "$E2E_DIR/archive-job" ]; then
for e2e_artifact in "$E2E_DIR/archive-job"/.lock "$E2E_DIR/archive-job"/manifest.json "$E2E_DIR/archive-job"/batch-000001.json; do
if [ -f "$e2e_artifact" ]; then unlink "$e2e_artifact"; fi
@@ -115,6 +116,9 @@ printf '%s\n' "[4/8] start Orbit container"
--env ORBIT_MYSQL_DSN="$E2E_DSN" \
--env ORBIT_HTTP_ADDR=0.0.0.0:8080 \
--env ORBIT_CONSOLE_QUEUE_ID="$E2E_QUEUE" \
--env ORBIT_CONSOLE_LOGIN_FAILURE_LIMIT=3 \
--env ORBIT_CONSOLE_LOGIN_LOCK_DURATION=10s \
--env ORBIT_CONSOLE_LOGIN_RATE_WINDOW=2s \
--env ORBIT_CONSOLE_APPLICATION_ADMIN="$E2E_ADMIN" \
--env ORBIT_CREDENTIAL_KEY_FILE=/run/credential-keys.json \
--volume "$E2E_DIR/credential-keys.json:/run/credential-keys.json:ro" \
@@ -199,6 +203,22 @@ assert status == 200, status
print("console_login=ok protected_page=ok")
PY
printf '%s\n' "[login] cross-node rejection, restart and recovery"
"$E2E_DOCKER" run -d --name "$E2E_ORBIT_SECOND" --network bridge --link "$E2E_MYSQL:mysql" \
--env ORBIT_MYSQL_DSN="$E2E_DSN" --env ORBIT_HTTP_ADDR=0.0.0.0:8080 \
--env ORBIT_CONSOLE_APPLICATION_ADMIN="$E2E_ADMIN" \
--env ORBIT_CREDENTIAL_KEY_FILE=/run/credential-keys.json \
--env ORBIT_CONSOLE_LOGIN_FAILURE_LIMIT=3 \
--env ORBIT_CONSOLE_LOGIN_LOCK_DURATION=10s \
--env ORBIT_CONSOLE_LOGIN_RATE_WINDOW=2s \
--volume "$E2E_DIR/credential-keys.json:/run/credential-keys.json:ro" \
--publish 127.0.0.1::8080 "$E2E_IMAGE" >/dev/null
E2E_SECOND_PORT=$("$E2E_DOCKER" port "$E2E_ORBIT_SECOND" 8080/tcp | sed -n 's/.*:\([0-9][0-9]*\)$/\1/p' | head -n 1)
ORBIT_E2E_URL="$E2E_URL" ORBIT_E2E_SECOND_URL="http://127.0.0.1:$E2E_SECOND_PORT" \
ORBIT_E2E_SECOND_CONTAINER="$E2E_ORBIT_SECOND" ORBIT_E2E_USER="$E2E_ADMIN" \
ORBIT_E2E_PASSWORD_FILE="$E2E_PASSWORD_FILE" DOCKER="$E2E_DOCKER" python3 scripts/test-console-login.py
"$E2E_DOCKER" stop "$E2E_ORBIT_SECOND" >/dev/null
printf '%s\n' "[6/8] WebSocket Worker task execution"
(cd examples && ORBIT_URL="$E2E_URL" ORBIT_CREDENTIALS_FILE="$E2E_CREDENTIALS_FILE" \
"$E2E_GO" run ./worker) | tee "$E2E_DIR/worker.log"

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""由 console-e2e 调用,在一次性双节点环境核验登录保护;不输出密码或 Cookie。"""
import http.cookiejar
import json
import os
import pathlib
import subprocess
import time
import urllib.error
import urllib.request
base = os.environ['ORBIT_E2E_URL']
peer = os.environ['ORBIT_E2E_SECOND_URL']
username = os.environ['ORBIT_E2E_USER']
password = pathlib.Path(os.environ['ORBIT_E2E_PASSWORD_FILE']).read_text().strip()
jar = http.cookiejar.CookieJar()
client = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
def call(url, path, body=None, headers=None):
req = urllib.request.Request(url + path, data=None if body is None else json.dumps(body).encode(), headers={'Content-Type': 'application/json', **(headers or {})})
try:
with client.open(req, timeout=8) as response:
return response.status, json.load(response), response.headers
except urllib.error.HTTPError as response:
return response.code, json.load(response), response.headers
def ready(url):
for _ in range(30):
try:
if call(url, '/health/ready')[0] == 200:
return
except (OSError, ValueError):
pass
time.sleep(.2)
raise AssertionError('test peer not ready')
ready(peer)
assert call(base, '/console/auth/login', {'username': username, 'password': password})[0] == 200
for url in (base, peer):
status, data, headers = call(url, '/console/auth/login', {'username': username, 'password': 'wrong-test-only'})
assert status == 401 and not headers.get('Set-Cookie'), status
status, data, headers = call(base, '/console/auth/login', {'username': username, 'password': 'wrong-test-only'})
assert status == 429 and data['error']['code'] == 'LOGIN_LOCKED' and int(headers['Retry-After']) > 0
assert not headers.get('Set-Cookie')
assert call(peer, '/console/auth/session')[1]['authenticated'] is True, 'lock killed existing session'
status, body, _ = call(peer, '/console/auth/audit')
assert status == 200 and any(e['outcome'] == 'LOCKED' for e in body['data']['items'])
until = next(e['retry_at'] for e in body['data']['items'] if e['outcome'] == 'LOCKED')
subprocess.run([os.environ.get('DOCKER', 'docker'), 'restart', os.environ['ORBIT_E2E_SECOND_CONTAINER']], check=True, stdout=subprocess.DEVNULL)
# 随机发布端口在 Docker 重启后可能重新分配,读取实际端口再验证同一节点。
published = subprocess.check_output([os.environ.get('DOCKER', 'docker'), 'port', os.environ['ORBIT_E2E_SECOND_CONTAINER'], '8080/tcp'], text=True).strip().splitlines()[0]
peer = 'http://127.0.0.1:' + published.rsplit(':', 1)[1]
ready(peer)
status, data, headers = call(peer, '/console/auth/login', {'username': username, 'password': password}, {'X-Forwarded-For': '203.0.113.88'})
assert status == 429 and data['error']['code'] == 'LOGIN_LOCKED', 'restart bypassed lock'
body = call(base, '/console/auth/audit')[1]
latest = body['data']['items'][0]
assert latest['retry_at'] == until and latest['remote_ip'] != '203.0.113.88', 'lock extended or untrusted header accepted'
# 仅按真实返回的截止等待;不修改数据库时间或删除锁定记录。
time.sleep(int(headers['Retry-After']) + .1)
assert call(peer, '/console/auth/login', {'username': username, 'password': password})[0] == 200
body = call(base, '/console/auth/audit')[1]
assert body['data']['items'][0]['outcome'] == 'SUCCESS'
assert password not in json.dumps(body)
print('console_login_security=passed cross_node=locked restart=locked existing_session=valid forged_ip=ignored expiry=success audit=confirmed')