feat: 管理员跨租户管理 + 注册开关

- 管理员可管理任意用户笔记(读取/修改/删除/回收站/标签/图谱/FTS 全平台)
- 普通用户仍数据隔离, 越权返回404
- 新增注册开关: 管理员后台⚙设置可开/关, 支持REGISTRATION_ENABLED环境变量
- 注册关闭时前台/登录页隐藏注册入口, 注册接口返回400
- 冒烟测试扩展到91用例全过
This commit is contained in:
Your Name
2026-08-11 13:07:56 +08:00
parent d9793300f9
commit 13c53fea0a
12 changed files with 741 additions and 89 deletions
+73 -12
View File
@@ -359,24 +359,31 @@ report("合并后标签去重", "已重命名" in tag_str and "保留" not in ta
st, d = req("DELETE", "/admin/api/tags", {"name": "已重命名"})
report("删除标签", st == 200, f"(got {st})")
# ── 多租户数据隔离 ──
print("\n═══ 12b. 多租户数据隔离 ═══")
# 记住当前测试用户可见的笔记(应只有本脚本创建的 + 自己空间的
st, d = req("GET", "/admin/api/tree")
own_ids = {x["id"] for x in d.get("data", [])}
# ═══ 12b. 多租户数据隔离 + 管理员跨租户管理 ═══
print("\n═══ 12b. 多租户数据隔离 + 管理员跨租户管理 ═══")
# 平台管理员账号(首个注册的 adminprod 已存在 admin/admin123
ADMINU = "admin"
ADMINP = "admin123"
# 注册第二个用户
st, d = req("POST", "/api/auth/register", {"username": "iso_" + _uuid.uuid4().hex[:8], "password": "iso12345"})
# 切到管理员
st, d = req("POST", "/api/auth/login", {"username": ADMINU, "password": ADMINP})
admin_ok = st == 200 and d.get("code") == 0
report("管理员账号可登录", admin_ok, f"(got {st} {d})")
if not admin_ok:
# 若无 admin 账号,则首个用户即管理员;这里直接注册一个新管理员作为降级
st, d = req("POST", "/api/auth/register", {"username": "_adm_" + _uuid.uuid4().hex[:6], "password": "adm12345"})
report("(降级)注册新管理员", st == 200 and d.get("code") == 0, f"(got {st} {d})")
# 注册第二个普通用户
ISOUSER = "iso_" + _uuid.uuid4().hex[:8]
st, d = req("POST", "/api/auth/register", {"username": ISOUSER, "password": "iso12345"})
report("第二个用户注册成功", st == 200 and d.get("code") == 0, f"(got {st})")
# 第二个用户的树必须是空的(看不到第一个用户的笔记)
# 第二个用户的树必须是空的(看不到别人的笔记)
st, d = req("GET", "/admin/api/tree")
other_ids = {x["id"] for x in d.get("data", [])}
report("第二用户树为空(数据隔离)", st == 200 and len(other_ids) == 0, f"(got {other_ids})")
# 两用户笔记 ID 无交集
report("两用户数据无交集", len(own_ids & other_ids) == 0, f"(own={own_ids} other={other_ids})")
# 第二用户无权读取第一用户创建的笔记
st, d = req("GET", f"/admin/api/notes/{note_id}")
report("越权读取他人笔记被拒(404)", st == 404, f"(got {st})")
@@ -391,12 +398,66 @@ st, d = req("GET", f"/api/notes/search?{q}")
other_hits = d.get("data") or []
report("第二用户搜不到他人笔记", len(other_hits) == 0, f"(got {len(other_hits)})")
# 第二用户创建一篇自己的私有笔记(供管理员跨租户操作)
st, d = req("POST", "/admin/api/notes", {"title": "第二用户的私有笔记", "content": "only visible to owner + admin", "is_public": False})
iso_note_id = d.get("data", {}).get("id")
report("第二用户创建私有笔记", st == 200 and iso_note_id, f"(got {st} {d})")
# 管理员跨租户管理:管理员应能读取第二用户的私有笔记
st, d = req("POST", "/api/auth/login", {"username": ADMINU, "password": ADMINP})
report("切回管理员成功", st == 200 and d.get("code") == 0, f"(got {st})")
st, d = req("GET", f"/admin/api/notes/{iso_note_id}")
report("管理员可读取他人(第二用户)笔记", st == 200 and d.get("code") == 0 and d.get("data", {}).get("title") == "第二用户的私有笔记", f"(got {st} {d})")
# 管理员可更新他人笔记(收藏)
st, d = req("PUT", f"/admin/api/notes/{iso_note_id}", {"is_favorite": True})
report("管理员可修改他人笔记", st == 200 and d.get("code") == 0, f"(got {st})")
# 管理员树包含第二用户的笔记(全平台视角)
st, d = req("GET", "/admin/api/tree")
admin_all = {x["id"] for x in d.get("data", [])}
report("管理员树含第二用户笔记(全平台)", iso_note_id in admin_all, f"(got {iso_note_id} in {admin_all})")
# 游客公开接口能读取公开笔记(跨租户展示,需登出为游客)
req("POST", "/api/auth/logout")
st, d = req("GET", f"/api/notes/{note_id}")
report("游客可读公开笔记", st == 200 and d.get("code") == 0, f"(got {st})")
# 重新登录回第一个测试用户(后续清理需要)
# ═══ 12c. 注册开关 ═══
print("\n═══ 12c. 注册开关 ═══")
# 切回管理员
st, d = req("POST", "/api/auth/login", {"username": ADMINU, "password": ADMINP})
report("切回管理员(开关注册测试)", st == 200 and d.get("code") == 0, f"(got {st})")
# 普通用户无权限修改注册开关
st, d = req("POST", "/api/auth/login", {"username": ISOUSER, "password": "iso12345"})
report("切到普通用户", st == 200 and d.get("code") == 0, f"(got {st})")
st, d = req("PUT", "/admin/api/settings/registration", {"enabled": False})
report("普通用户无权改注册开关(403)", st == 403, f"(got {st})")
# 管理员关闭注册
st, d = req("POST", "/api/auth/login", {"username": ADMINU, "password": ADMINP})
st, d = req("PUT", "/admin/api/settings/registration", {"enabled": False})
report("管理员关闭注册", st == 200 and d.get("data", {}).get("registration_enabled") == False, f"(got {st} {d})")
# 关闭后再注册应被拒
st, d = req("POST", "/api/auth/register", {"username": "blocked_" + _uuid.uuid4().hex[:6], "password": "pass12345"})
report("注册关闭后被拒绝", st == 400, f"(got {st} {d})")
# 管理员重新开启注册
st, d = req("PUT", "/admin/api/settings/registration", {"enabled": True})
report("管理员开启注册", st == 200 and d.get("data", {}).get("registration_enabled") == True, f"(got {st} {d})")
# 重新切回第一个测试用户用于清理并删除第二用户的笔记
st, d = req("POST", "/api/auth/login", {"username": TUSER, "password": "smoke123"})
report("切回测试用户", st == 200 and d.get("code") == 0, f"(got {st})")
# 管理员清理第二用户笔记前,先切回管理员
st, d = req("POST", "/api/auth/login", {"username": ADMINU, "password": ADMINP})
st, d = req("POST", f"/admin/api/purge/{iso_note_id}")
report("清理第二用户笔记", st in (200, 500), f"(got {st})")
# 切回第一个测试用户(后续清理需要)
st, d = req("POST", "/api/auth/login", {"username": TUSER, "password": "smoke123"})
report("切回测试用户成功", st == 200 and d.get("code") == 0, f"(got {st})")