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 行删除
+37 -12
查看文件
@@ -214,10 +214,19 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
"scheduler": { "enabled": true, "times": ["09:00","17:00"], "next": "2026-09-14 17:00:00" },
"running_runs": 0,
"last_run": { "id": 8, "trigger": "startup", "status": "ok", "started_at": "...", "message": "..." },
"cookie_set": true
"cookie_set": true,
"is_admin": false,
"can_edit_schedule": false,
"can_view_logs": false
}
```
> 最后三个字段是**前端显隐的依据**(v1.3.0 起)。
> 调度时刻是**实例级**的 —— 普通账号拿到 `can_edit_schedule: false`,
> 页面据此把「采集调度」渲染成只读表格,而不是给一个点了会被拒的表单。
> 大屏是拿不到 Jinja 上下文的静态页,只能靠 `/api/manifest` 的 `role`
> 或这里的字段决定要不要显示「日志管理」入口。
### GET `/api/audit`
操作审计分页。
@@ -255,9 +264,14 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
"cookie_hint": "92 字符,…c0ffee",
"cookie_broken": false,
"user_agent": "Mozilla/5.0 (...)",
"_globalKeys": ["allow_register", "api_base", "api_path",
"captcha_length", "captcha_policy", "register_max_per_ip"],
"_canEditGlobal": true
"_globalKeys": ["allow_register", "api_base", "api_path", "captcha_length",
"captcha_policy", "catch_up", "catch_up_grace_hours",
"drift_tolerance_minutes", "max_prompt", "page_size",
"register_max_per_ip", "rewind_minutes", "schedule_enabled",
"schedule_times", "ssl_verify", "timeout", "verify_days"],
"_userKeys": ["cookie", "user_agent"],
"_canEditGlobal": true,
"_role": "user"
}
```
@@ -267,8 +281,14 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
| `cookie` | **恒为空串** —— `db.get_settings()` 统一置空,明文只能经 `db.get_secret()` 取 |
| `cookie_hint` | 「N 字符,…尾 4 位」;未配置时为空串 |
| `cookie_broken` | `true` 表示密文解不开(`cookie_key` 换过),需重新粘贴 Cookie |
| `_globalKeys` | 实例级键清单(所有账号共用一份,只有管理员能改) |
| `_globalKeys` | **实例级**键清单(所有账号共用一份,只有管理员能改)。v1.3.0 起含调度与采集参数 |
| `_userKeys` | **个人级**键清单,即 `config.USER_EDITABLE_KEYS`;普通账号唯一能写的两个键 |
| `_canEditGlobal` | 当前账号能否改 `_globalKeys` 里的键 |
| `_role` | `"admin"` / `"user"` —— 前端据此决定显隐(大屏走 `/api/manifest` 的 `role`) |
> **写权限就一条规则**:`config.writable_by(key, is_admin)`。
> 页面上「哪些输入框可以改」与接口「哪些键能写」用的是同一个函数,
> 所以不会出现「界面置灰但接口还能写」的不一致。
### GET `/api/users`(管理员)
@@ -382,19 +402,24 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
|---|---|
| 全部合法 | `200 {"ok": true, "changed": ["page_size"], "ignored": []}` |
| 有非法值 | `400 {"ok": false, "error": "invalid", "errors": ["page_size 需在 20 ~ 1000 条/页 之间"]}` |
| 非管理员改实例级键 | `400 {"ok": false, "error": "invalid", "errors": ["以下为实例级配置,仅管理员可修改:api_base"]}` |
| 不可写的键(普通账号写实例级) | `400 {"ok": false, "error": "invalid", "errors": ["以下配置仅管理员可修改,本账号无法保存:api_base。普通账号可以维护的是本人凭证(Cookie / User-Agent)。"]}` |
要点:
- **写权限判断只有一处**:`config.writable_by(key, is_admin)`。
普通账号能写的**只有** `cookie` 与 `user_agent`(且只限本人这份);
其余(云端接口、注册策略、调度、采集参数)全部仅管理员。
- **越权写是「整单拒绝」而不是「部分生效」**:请求里只要含一个不可写的键,
整个请求 `400`,并在 `errors` 里**点名**是哪些键。这样调用方不会误以为
「既然 `changed` 里没有它就说明写过了」。
- `cookie` **留空 = 不修改**(不会把已有 Cookie 清掉);写 `__clear__` 或 `-` 才是清空;
- `cookie` 落库前会**自动加密**(`db.set_secret`),写进去的永远不是明文;
- **实例级键**(`_globalKeys`:`api_base` / `api_path` / `allow_register` /
`register_max_per_ip` / `captcha_policy` / `captcha_length`)非管理员**写不了**
—— 否则任意注册用户都能把大家的数据采集指向别的服务器;
- 未知键被忽略并在 `ignored` 里列出,**不会被写成任意键**;
- 内部键(`slot:*`)被忽略;
- 改了 `schedule_times` / `schedule_enabled` 会清掉**自己**的槽位标记,新时刻立即生效;
- 每次拒绝都会写一条 `settings_rejected` 审计。
- 内部键(`slot:*`)被忽略 —— 它们是调度簿记,不属于用户可配置项;
- 改 `schedule_times` 会清掉**已不存在时刻**对应的 `slot:*` 标记(所有账号一起清),
新时刻立即生效。刻意**不做全清**:全清会让所有账号在宽限期内一起重采。
- 每次拒绝都会写一条 `settings_rejected` 审计(管理员的「日志管理 → 操作审计」里能看到,
这也是排查「谁的账号在试越权」的入口)。
### POST `/api/password`