feat(权限): 收敛普通账号写权限至本人凭证

将调度时刻、采集参数等实例级配置收归管理员,普通账号仅可维护本人 Cookie 与 User-Agent。
新增 config.writable_by 作为唯一写权限入口,set_setting 强制全局键落到 user_id=0,
消除「管理员改了只有自己生效」的静默缺陷。新增 tools/check_docs.py 文档自检,
smoke 断言扩至 215 项、check_live 扩至 122 项并支持普通账号越权验收,
忽略 backups/、data/*.bak* 与 legacy-v1/,版本升至 v1.3.0。
这个提交包含在:
2026-09-18 08:46:00 +08:00
父节点 df7db3582e
当前提交 1bf961f6b3
共修改 42 个文件,包含 2615 行新增和 854 行删除
+48 -10
查看文件
@@ -28,8 +28,11 @@ EXPORT_DIR = os.path.join(DATA_DIR, "exports")
APP_LOG = os.path.join(LOG_DIR, "app.log")
INSTANCE_FILE = os.path.join(DATA_DIR, "instance.json")
# 旧版脚本项目的存档(迁移用;--migrate-csv 默认读这里)
# 旧版脚本项目的存档(迁移用;--migrate-csv 默认读这里)。
# v1.3.0 起旧版被收进工作区级的 legacy-v1/ 目录,所以第一个候选是新位置,
# 后面两个保留以兼容「还没挪走」的部署。
LEGACY_CSV_CANDIDATES = [
os.path.join(os.path.dirname(BASE_DIR), "legacy-v1", "data", "usage_records.csv"),
os.path.join(os.path.dirname(BASE_DIR), "data", "usage_records.csv"),
os.path.join(BASE_DIR, "data", "usage_records.csv"),
]
@@ -50,7 +53,12 @@ DEFAULTS = {
"register_max_per_ip": "3", # 同一 IP 每天最多注册几个账号
"captcha_policy": "always", # always | adaptive | off(见 CAPTCHA_POLICIES)
"captcha_length": "4", # 验证码字符数 4~6
# ---- 个人级:采集参数 ----
# ---- 实例级:采集调度(全实例统一,见 GLOBAL_KEYS)----
"schedule_enabled": "1",
"schedule_times": "09:00,17:00", # 每天固定时刻(逗号分隔,本地时区)
"catch_up": "1", # 启动时补跑当天已错过且未执行的槽位
"catch_up_grace_hours": "12", # 超过该小时数就不再补跑
# ---- 实例级:采集参数 ----
"page_size": "200",
"rewind_minutes": "2", # 断点回退分钟数
"drift_tolerance_minutes": "5", # 云端比本地早超过该值才告警
@@ -58,25 +66,55 @@ DEFAULTS = {
"verify_days": "0", # 每次采集后做整日完整性校验的天数
"timeout": "30",
"ssl_verify": "1", # 校验云端 HTTPS 证书(cookie 是凭证,不该裸奔)
# ---- 个人级:调度 ----
"schedule_enabled": "1",
"schedule_times": "09:00,17:00", # 每天固定时刻(逗号分隔,本地时区)
"catch_up": "1", # 启动时补跑当天已错过且未执行的槽位
"catch_up_grace_hours": "12", # 超过该小时数就不再补跑
# ---- 个人级:凭证(每个账号自己的,Cookie 静态加密后入库)----
"cookie": "",
"user_agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36"),
}
# 实例级配置:所有账号共用一份,只有管理员能改。
# 其余键(采集参数 / 调度 / 凭证)都是个人级 —— 这正是「多用户」的核心:
# 每个人填自己的 Cookie、收自己的数据、定自己的采集时刻。
# ---------------- 配置的作用域与写权限(改这里之前先读完整段)----------------
#
# 三级回落:**个人级(user_id=n) -> 实例级(user_id=0) -> config.DEFAULTS**。
# 但「谁能写哪个键」和「键存在哪一级」是两件事,本项目把两者对齐成一条规则:
#
# 普通用户只能写 USER_EDITABLE_KEYS(本人凭证);
# 其余所有键都归管理员,且一律存在**实例级**(user_id=0)。
#
# 为什么采集参数 / 调度也要放到实例级,而不是「个人级但只有管理员能写」:
# 如果它们只在管理员自己的 user_id 下,其它账号读取时会回落到
# DEFAULTS,管理员改的值对别人**完全不生效** —— 那才是真正的坑。
# 统一放实例级,语义是「一台部署一套采集与调度策略」,读起来也简单。
GLOBAL_KEYS = {
# 云端接口
"api_base", "api_path",
# 开放注册与防攻击策略
"allow_register", "register_max_per_ip", "captcha_policy", "captcha_length",
# 采集调度(v1.3.0 起为实例级:普通用户只读,不能设置频率)
"schedule_enabled", "schedule_times", "catch_up", "catch_up_grace_hours",
# 采集参数(同理:允许普通用户调 page_size/关 ssl_verify 都是越权)
"page_size", "rewind_minutes", "drift_tolerance_minutes", "max_prompt",
"verify_days", "timeout", "ssl_verify",
}
# 普通用户**唯一**可写的两个键:本人账号的云端凭证。
# cookie —— 静态加密后入库,页面/接口只回掩码
# user_agent —— 必须与拿 Cookie 的那次请求同源,所以和 Cookie 归在一起
# 这两个键是个人级、且不参与实例级回落(见 db.NO_FALLBACK_KEYS):
# 回落等于「用别人的身份采集」,是最严重的一类越权。
USER_EDITABLE_KEYS = {"cookie", "user_agent"}
def is_user_key(key):
"""是否属于「个人凭证」类配置。"""
return key in USER_EDITABLE_KEYS
def writable_by(key, is_admin):
"""当前角色能否写这个键 —— 前后端与测试都走这一个判断,避免两处规则漂移。"""
if key in USER_EDITABLE_KEYS:
return True
return bool(is_admin)
# 验证码策略
CAPTCHA_POLICIES = {
"always": "始终要求(默认,最安全)",