fix: sites 标量 domains 防御/_to_bool/403 锁定/注释
- _norm_domains 入口类型守卫:domains 传数字曾 500、字符串曾逐字符 迭代成单字符域名静默落库,统一走 invalid_domains 400 - bool() 强转换 _to_bool:"false" 字符串曾是 True,对 allow_no_origin 这类放宽开关方向不利,改按字面解析(其余未知值仍 False) - 补 member DELETE 不存在 pk → 403(先于 404)锁定测试 - SiteDetailView 注释删去 GET 字样(无 detail GET,前端走列表+Dialog) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,10 @@ from tracker.models import Site
|
||||
|
||||
def _norm_domains(raw) -> list[str]:
|
||||
"""域名归一:去协议/端口/路径、转小写、去空去重。"""
|
||||
if isinstance(raw, str) or not isinstance(raw, (list, tuple)):
|
||||
return [] # 标量/字符串(会逐字符迭代成单字符域名)→ 走 invalid_domains 400
|
||||
out = []
|
||||
for d in raw or []:
|
||||
for d in raw:
|
||||
d = str(d).strip().lower()
|
||||
for pre in ("https://", "http://"):
|
||||
if d.startswith(pre):
|
||||
@@ -20,6 +22,16 @@ def _norm_domains(raw) -> list[str]:
|
||||
return out
|
||||
|
||||
|
||||
def _to_bool(v) -> bool:
|
||||
"""宽松布尔解析:bool 直通,"true"/"false" 字符串按字面(bool("false") 是 True,
|
||||
对 allow_no_origin 这类放宽开关方向不利),其余(数字/未知串/None)一律 False。"""
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
if isinstance(v, str) and v.lower() in ("true", "false"):
|
||||
return v.lower() == "true"
|
||||
return False
|
||||
|
||||
|
||||
def _serialize(site: Site) -> dict:
|
||||
return {"id": site.pk, "name": site.name, "site_id": site.site_id,
|
||||
"domains": site.domains, "allow_no_origin": site.allow_no_origin,
|
||||
@@ -39,13 +51,13 @@ class SiteListView(EnvelopeMixin, APIView):
|
||||
if not domains:
|
||||
return Response({"code": 400, "data": None, "message": "invalid_domains"}, status=400)
|
||||
site = Site.objects.create(name=name.strip()[:100], domains=domains,
|
||||
allow_no_origin=bool(data.get("allow_no_origin")))
|
||||
allow_no_origin=_to_bool(data.get("allow_no_origin")))
|
||||
return ok(_serialize(site), "created")
|
||||
|
||||
|
||||
class SiteDetailView(EnvelopeMixin, APIView):
|
||||
def get_permissions(self):
|
||||
# DELETE(软删)仅 admin;GET/PATCH 登录成员即可(settings 默认 IsAuthenticated)。
|
||||
# DELETE(软删)仅 admin;PATCH 登录成员即可(settings 默认 IsAuthenticated;无 detail GET,前端走列表)。
|
||||
# 在 initial() 阶段判权限:403 先于 404,且不泄露站点存在性。
|
||||
if self.request.method == "DELETE":
|
||||
return [IsAdminUser()]
|
||||
@@ -68,9 +80,9 @@ class SiteDetailView(EnvelopeMixin, APIView):
|
||||
return Response({"code": 400, "data": None, "message": "invalid_domains"}, status=400)
|
||||
site.domains = domains
|
||||
if "allow_no_origin" in data:
|
||||
site.allow_no_origin = bool(data["allow_no_origin"])
|
||||
site.allow_no_origin = _to_bool(data["allow_no_origin"])
|
||||
if "is_active" in data:
|
||||
site.is_active = bool(data["is_active"])
|
||||
site.is_active = _to_bool(data["is_active"])
|
||||
site.save()
|
||||
return ok(_serialize(site))
|
||||
|
||||
|
||||
@@ -107,3 +107,37 @@ def test_delete_soft_keeps_collect_rejects(client, admin_user):
|
||||
"session_id": "s1", "events": []}),
|
||||
content_type="text/plain", HTTP_ORIGIN="https://a.com")
|
||||
assert r.status_code == 403 and r.json()["message"] == "not_allowed"
|
||||
|
||||
|
||||
# ---- 审查修复锁定测试 ----
|
||||
|
||||
def test_scalar_domains_400_not_500(client, member_user):
|
||||
"""domains 传标量:数字曾直接 500(for d in 123)、字符串曾逐字符迭代成单字符域名静默落库——统一 400。"""
|
||||
_auth(client, member_user)
|
||||
assert client.post("/api/admin/sites",
|
||||
{"name": "x", "domains": "example.com"}, format="json").status_code == 400
|
||||
assert client.post("/api/admin/sites",
|
||||
{"name": "x", "domains": 5}, format="json").status_code == 400
|
||||
|
||||
|
||||
def test_bool_string_parsed_by_literal(client, member_user):
|
||||
"""bool("false") 是 True,对 allow_no_origin 这类放宽开关方向不利——"false" 字符串须按字面落库 False。"""
|
||||
_auth(client, member_user)
|
||||
r = client.post("/api/admin/sites",
|
||||
{"name": "x", "domains": ["a.com"], "allow_no_origin": "false"}, format="json")
|
||||
assert r.status_code == 200
|
||||
site = Site.objects.get(pk=r.json()["data"]["id"])
|
||||
assert site.allow_no_origin is False
|
||||
client.patch(f"/api/admin/sites/{site.pk}", {"is_active": "false"}, format="json")
|
||||
site.refresh_from_db()
|
||||
assert site.is_active is False
|
||||
client.patch(f"/api/admin/sites/{site.pk}", {"allow_no_origin": "true"}, format="json")
|
||||
site.refresh_from_db()
|
||||
assert site.allow_no_origin is True
|
||||
|
||||
|
||||
def test_delete_member_403_before_404(client, member_user):
|
||||
"""权限先于存在性:member DELETE 不存在的 pk 也是 403——不泄露站点是否存在。"""
|
||||
_auth(client, member_user)
|
||||
r = client.delete("/api/admin/sites/99999")
|
||||
assert r.status_code == 403 and r.json()["code"] == 403
|
||||
|
||||
Reference in New Issue
Block a user