feat: 管理员跨租户管理 + 注册开关
- 管理员可管理任意用户笔记(读取/修改/删除/回收站/标签/图谱/FTS 全平台) - 普通用户仍数据隔离, 越权返回404 - 新增注册开关: 管理员后台⚙设置可开/关, 支持REGISTRATION_ENABLED环境变量 - 注册关闭时前台/登录页隐藏注册入口, 注册接口返回400 - 冒烟测试扩展到91用例全过
This commit is contained in:
+73
-12
@@ -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. 多租户数据隔离 + 管理员跨租户管理 ═══")
|
||||
# 平台管理员账号(首个注册的 admin,prod 已存在 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})")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user