fix: stabilize console scrolling and protect administrator login
This commit is contained in:
68
scripts/test-console-login.py
Normal file
68
scripts/test-console-login.py
Normal 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')
|
||||
Reference in New Issue
Block a user