chore: initialize GSREA website project

This commit is contained in:
zlwu committed 2026-10-30 11:05:55 +08:00
commit d3c5461de5
1842 files changed
+246140

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
"""Create the application database when it does not exist. Connection comes from the environment."""
from urllib.parse import unquote, urlparse
import pymysql
from app.core.config import get_settings
def main() -> None:
parsed = urlparse(get_settings().database_url)
name = parsed.path.lstrip("/")
if not name or not parsed.hostname:
raise SystemExit("DATABASE_URL 未配置")
connection = pymysql.connect(
host=parsed.hostname,
port=parsed.port or 3306,
user=unquote(parsed.username or ""),
password=unquote(parsed.password or ""),
charset="utf8mb4",
connect_timeout=15,
)
try:
with connection.cursor() as cursor:
cursor.execute(
f"CREATE DATABASE IF NOT EXISTS `{name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
)
connection.commit()
finally:
connection.close()
print("database ready")
if __name__ == "__main__":
main()
+365
View File
@@ -0,0 +1,365 @@
"""Walk the first-batch acceptance against the running API. Reads secrets from the environment."""
import os
import uuid
from datetime import datetime, timedelta, timezone
import httpx
from dotenv import load_dotenv
from sqlalchemy import delete, select
load_dotenv()
from app.core.config import get_settings
from app.db import SessionLocal, get_engine
from app.models.rbac import AdminRole, AdminRoleResource, AdminUser, AdminUserRole
from app.models.security import AdminAuditLog, AdminIpBlacklist
from app.services.redis_client import get_redis
BASE = os.environ.get("ACCEPTANCE_BASE", "http://127.0.0.1:18000")
def fail(message: str) -> None:
raise SystemExit(message)
def main() -> None:
get_settings()
username = os.environ["BOOTSTRAP_ADMIN_USERNAME"]
password = os.environ["BOOTSTRAP_ADMIN_PASSWORD"]
client = httpx.Client(base_url=BASE, timeout=30)
get_engine()
def captcha_code() -> tuple[str, str]:
issued = client.get("/api/admin/auth/captcha")
if issued.status_code != 200:
fail(f"captcha {issued.status_code}")
captcha_id = issued.json()["captchaId"]
stored = get_redis().get(f"gsfdc:captcha:{captcha_id}")
if not stored:
fail("captcha missing in redis")
return captcha_id, stored
def login(name: str, secret: str) -> httpx.Response:
captcha_id, code = captcha_code()
return client.post(
"/api/admin/auth/login",
json={"username": name, "password": secret, "captchaId": captcha_id, "captchaCode": code},
)
missing = client.get("/api/admin/users")
if missing.status_code != 401:
fail(f"expected 401, got {missing.status_code}")
captcha_id, code = captcha_code()
first = client.post(
"/api/admin/auth/login",
json={"username": username, "password": password, "captchaId": captcha_id, "captchaCode": code},
)
if first.status_code != 200:
fail(f"admin login {first.status_code} {first.text[:200]}")
second = client.post(
"/api/admin/auth/login",
json={"username": username, "password": password, "captchaId": captcha_id, "captchaCode": code},
)
if second.status_code != 401 or second.json().get("detail") != "用户名或密码错误":
fail(f"captcha reuse {second.status_code} {second.text[:200]}")
no_captcha = client.post(
"/api/admin/auth/login",
json={"username": username, "password": password, "captchaId": "", "captchaCode": ""},
)
if no_captcha.status_code != 401:
fail("login without captcha was accepted")
admin = login(username, password)
if admin.status_code != 200:
fail(f"admin login retry {admin.status_code}")
body = admin.json()
token = body["token"]
titles = [node["title"] for node in body["menus"]]
if "系统管理" not in titles:
fail(f"menus {titles}")
child_titles = {item["title"] for item in body["menus"][0]["children"]}
expected = {"用户管理", "角色管理", "菜单管理", "操作日志", "登录安全", "附件", "站点配置", "敏感词"}
if not expected <= child_titles:
fail(f"missing pages {expected - child_titles}")
if any(node["type"] == "button" for node in body["menus"]):
fail("button leaked into login menu")
headers = {"Authorization": f"Bearer {token}"}
resources = client.get("/api/admin/resources", headers=headers)
if resources.status_code != 200:
fail(f"resources {resources.status_code}")
tree = resources.json()
def walk(nodes):
for node in nodes:
yield node
yield from walk(node.get("children") or [])
by_name = {node["name"]: node for node in walk(tree)}
system_id = by_name["System"]["id"]
user_page = by_name["User"]["id"]
add_button = next(node["id"] for node in walk(tree) if node.get("permission") == "user:add")
role_code = f"R_VIEW_{uuid.uuid4().hex[:8]}"
created_role = client.post(
"/api/admin/roles",
headers=headers,
json={"code": role_code, "name": "内容查看", "description": "验收"},
)
if created_role.status_code != 200:
fail(f"create role {created_role.status_code} {created_role.text[:200]}")
role_id = created_role.json()["id"]
granted = client.put(
f"/api/admin/roles/{role_id}/resources",
headers=headers,
json={"resourceIds": [system_id, user_page]},
)
if granted.status_code != 200:
fail(f"grant {granted.status_code} {granted.text[:200]}")
viewer_name = f"viewer_{uuid.uuid4().hex[:8]}"
viewer_password = "View1234"
created_user = client.post(
"/api/admin/users",
headers=headers,
json={
"username": viewer_name,
"password": viewer_password,
"displayName": "内容查看员",
"mobile": "13800138000",
"roleIds": [role_id],
},
)
if created_user.status_code != 200:
fail(f"create user {created_user.status_code} {created_user.text[:300]}")
created_body = created_user.json()
if created_body.get("mobileMask") != "138****8000":
fail(f"mask {created_body}")
if "password" in created_body or "mobileCipher" in created_body:
fail("secret leaked in user response")
viewer_id = created_body["id"]
viewer_login = login(viewer_name, viewer_password)
if viewer_login.status_code != 200:
fail(f"viewer login {viewer_login.status_code} {viewer_login.text[:200]}")
viewer_body = viewer_login.json()
pages = [node["title"] for node in viewer_body["menus"][0]["children"]]
if pages != ["用户管理"]:
fail(f"viewer pages {pages}")
if viewer_body["permissions"]:
fail(f"viewer permissions {viewer_body['permissions']}")
granted_add = client.put(
f"/api/admin/roles/{role_id}/resources",
headers=headers,
json={"resourceIds": [system_id, user_page, add_button]},
)
if granted_add.status_code != 200:
fail(f"grant add {granted_add.status_code}")
viewer_again = login(viewer_name, viewer_password)
if viewer_again.json().get("permissions") != ["user:add"]:
fail(f"permissions after grant {viewer_again.json().get('permissions')}")
disabled = client.put(
f"/api/admin/users/{viewer_id}",
headers=headers,
json={"displayName": "内容查看员", "mobile": "13800138000", "status": "disabled", "roleIds": [role_id]},
)
if disabled.status_code != 200:
fail(f"disable {disabled.status_code} {disabled.text[:200]}")
if login(viewer_name, viewer_password).status_code != 401:
fail("disabled user logged in")
enabled = client.put(
f"/api/admin/users/{viewer_id}",
headers=headers,
json={"displayName": "内容查看员", "status": "active", "roleIds": [role_id]},
)
if enabled.status_code != 200 or login(viewer_name, viewer_password).status_code != 200:
fail("reenabled user could not login")
cancelled = client.put(
f"/api/admin/users/{viewer_id}",
headers=headers,
json={"displayName": "内容查看员", "status": "cancelled", "roleIds": [role_id]},
)
if cancelled.status_code != 200:
fail(f"cancel {cancelled.status_code} {cancelled.text[:200]}")
if login(viewer_name, viewer_password).status_code != 401:
fail("cancelled user logged in")
listed = client.get("/api/admin/users", headers=headers, params={"username": viewer_name})
if listed.status_code != 200 or not listed.json()["records"]:
fail("cancelled user missing from list")
weak = client.post(
"/api/admin/users",
headers=headers,
json={"username": f"weak_{uuid.uuid4().hex[:6]}", "password": "abcdefgh", "displayName": "弱", "roleIds": []},
)
if weak.status_code != 400:
fail("weak password accepted")
policy = client.put(
"/api/admin/login-security",
headers=headers,
json={"loginMaxFailures": 2, "loginLockMinutes": 15},
)
if policy.status_code != 200:
fail(f"policy {policy.status_code} {policy.text[:200]}")
lock_name = f"lock_{uuid.uuid4().hex[:6]}"
lock_password = "Lock1234"
lock_user = client.post(
"/api/admin/users",
headers=headers,
json={"username": lock_name, "password": lock_password, "displayName": "锁定", "roleIds": []},
)
if lock_user.status_code != 200:
fail(f"lock user {lock_user.status_code}")
for _ in range(2):
bad_id, _bad_code = captcha_code()
client.post(
"/api/admin/auth/login",
json={"username": lock_name, "password": "Wrong123", "captchaId": bad_id, "captchaCode": "ZZZZ"},
)
locked = login(lock_name, lock_password)
if locked.status_code != 401:
fail("locked account accepted the right password")
db = SessionLocal()
try:
row = db.scalar(select(AdminUser).where(AdminUser.username == lock_name))
row.locked_until = datetime.now(timezone.utc) - timedelta(seconds=5)
db.commit()
finally:
db.close()
if login(lock_name, lock_password).status_code != 200:
fail("login failed after lock expired")
client.put(
"/api/admin/login-security",
headers=headers,
json={"loginMaxFailures": 5, "loginLockMinutes": 15},
)
blocked_ip = "127.0.0.1"
added = client.post("/api/admin/ip-blacklist", headers=headers, json={"ip": blocked_ip, "remark": "acceptance"})
if added.status_code != 200:
fail(f"blacklist add {added.status_code} {added.text[:200]}")
blacklist_id = added.json()["id"]
try:
blocked = client.get("/api/admin/users", headers=headers)
if blocked.status_code != 403:
fail(f"blacklisted request was not rejected: {blocked.status_code}")
finally:
db = SessionLocal()
try:
db.execute(delete(AdminIpBlacklist).where(AdminIpBlacklist.id == blacklist_id))
db.commit()
finally:
db.close()
restored = client.get("/api/admin/users", headers=headers)
if restored.status_code != 200:
fail(f"request still blocked after removal: {restored.status_code}")
logs = client.get("/api/admin/audit-logs", headers=headers, params={"username": username, "current": 1, "size": 20})
if logs.status_code != 200 or logs.json()["total"] < 1:
fail(f"audit {logs.status_code} {logs.text[:200]}")
raw_logs = logs.text
if password in raw_logs or "captchaCode" in raw_logs:
fail("audit response contains a secret")
over = client.post(
"/api/admin/files",
headers=headers,
data={"bucket": "public"},
files={"file": ("big.bin", b"x" * (10 * 1024 * 1024 + 1), "application/octet-stream")},
)
if over.status_code != 400:
fail(f"oversize {over.status_code}")
public = client.post(
"/api/admin/files",
headers=headers,
data={"bucket": "public"},
files={"file": ("ok.txt", b"hello", "text/plain")},
)
if public.status_code != 200:
fail(f"public upload {public.status_code} {public.text[:200]}")
public_url = public.json()["url"]
fetched = httpx.get(public_url, timeout=20)
if fetched.status_code != 200 or fetched.content != b"hello":
fail(f"public fetch {fetched.status_code}")
private = client.post(
"/api/admin/files",
headers=headers,
data={"bucket": "private"},
files={"file": ("secret.txt", b"secret", "text/plain")},
)
if private.status_code != 200 or not private.json().get("expiresAt"):
fail(f"private upload {private.status_code} {private.text[:200]}")
site = client.put(
"/api/admin/site-settings",
headers=headers,
json={
"site_name": "验收站点",
"icp_text": "陇ICP备19001227号",
"link_cirea": "https://www.cirea.org.cn/",
},
)
if site.status_code != 200:
fail(f"site {site.status_code} {site.text[:200]}")
public_site = client.get("/api/site/settings")
public_body = public_site.json()
if public_body.get("siteName") != "验收站点" or public_body.get("linkCirea") != "https://www.cirea.org.cn/":
fail(f"public site {public_body}")
if "loginMaxFailures" in public_body or "login_max_failures" in public_body:
fail("public site leaked lock settings")
probe_word = f"验收敏感词{uuid.uuid4().hex[:6]}"
word = client.post("/api/admin/sensitive-words", headers=headers, json={"word": probe_word})
if word.status_code != 200:
fail(f"word {word.status_code} {word.text[:200]}")
preview = client.post(
"/api/admin/content/preview-filter",
headers=headers,
json={"html": f'<p onclick="alert(1)">{probe_word}</p><script>alert(1)</script>'},
)
if preview.status_code != 200:
fail(f"preview {preview.status_code}")
preview_body = preview.json()
if "script" in preview_body["html"].lower() or "onclick" in preview_body["html"].lower():
fail(f"filter kept active content {preview_body['html']}")
if probe_word not in preview_body["hits"]:
fail(f"hits {preview_body['hits']}")
articles = client.get("/api/site/settings")
if probe_word in articles.text:
fail("preview became public content")
client.delete(f"/api/admin/sensitive-words/{word.json()['id']}", headers=headers)
client.delete(f"/api/admin/files/{public.json()['id']}", headers=headers)
client.delete(f"/api/admin/files/{private.json()['id']}", headers=headers)
client.put(
"/api/admin/site-settings",
headers=headers,
json={"site_name": "甘肃省房地产价格评估行业协会", "link_cirea": ""},
)
db = SessionLocal()
try:
user_ids = [
row.id
for row in db.scalars(select(AdminUser).where(AdminUser.username.in_([viewer_name, lock_name]))).all()
]
if user_ids:
db.execute(delete(AdminUserRole).where(AdminUserRole.user_id.in_(user_ids)))
db.execute(delete(AdminUser).where(AdminUser.id.in_(user_ids)))
db.execute(delete(AdminRoleResource).where(AdminRoleResource.role_id == role_id))
db.execute(delete(AdminRole).where(AdminRole.id == role_id))
db.execute(delete(AdminIpBlacklist).where(AdminIpBlacklist.ip == blocked_ip))
db.commit()
finally:
db.close()
print("live acceptance passed")
if __name__ == "__main__":
main()