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 行删除
+26 -9
查看文件
@@ -44,14 +44,16 @@ WB_DATA_DIR=$PWD/data/demo python manage.py serve --port 8849 --no-scheduler
## 三、改动前请先跑一遍验证
项目有一层层递进的验证,**代价从低到高**,改完至少跑到第 2 层:
项目有一层层递进的验证,**代价从低到高**,改完至少跑到第 2 层;
**只要动过 `*.md`,第 1.5 层必须跑**:
| # | 命令 | 覆盖什么 | 需要什么 |
|---|---|---|---|
| 1 | `python -m compileall -q workbuddy_portal manage.py tools` | 语法 | — |
| 2 | `python tools/smoke.py` | **165 项**离线断言:全页面只读渲染、模板残留检测、多用户隔离与凭证保密、注册与验证码、历史缺陷防回归、静态资源、CSS 类名对账 | 无(用 Flask test_client,不启服务) |
| 3 | `python tools/check_live.py --base http://127.0.0.1:8848` | **83 项**真实 HTTP 断言,含登录/CSRF/开放重定向/验证码/安全响应头 | 一个运行中的服务 |
| 4 | `python tools/shots.py --base http://127.0.0.1:8849` | 登录后逐页截图并收集 `console`/`pageerror` | Playwright + Chromium |
| 1.5 | `python tools/check_docs.py` | **文档自检**:内部链接与跨文件锚点、图片引用、**绝对路径泄漏**、版本一致性、产品名硬编码。改过任何 md 都跑,否则章节重排造成的**锚点静默失效**会一路漏到线上。文档清单自动发现,不写死文件名 | — |
| 2 | `python tools/smoke.py` | **215 项**离线断言:全页面只读渲染、模板残留检测、多用户隔离与凭证保密、注册与验证码、**非管理员越权面全关死**、**全局键必须落在实例级**、历史缺陷防回归、静态资源、CSS 类名对账 | 无(用 Flask test_client,不启服务) |
| 3 | `python tools/check_live.py --base http://127.0.0.1:8848 --as demo:admin123` | **122 项**真实 HTTP 断言,含登录/CSRF/开放重定向/验证码/安全响应头,`--as` 追加普通账号越权验收 | 一个运行中的服务 |
| 4 | `python tools/shots.py --base http://127.0.0.1:8849` | 登录后逐页截图并收集 `console`/`pageerror`,**含普通账号只读视角**与越权面探测 | Playwright + Chromium |
| 5 | `docker compose up -d --build && docker compose ps` | 容器化路径 | Docker |
> `tools/shots.py` 是**最有价值的一层**:项目曾经出过「大屏整页全白」的 bug,
@@ -79,7 +81,7 @@ WB_DATA_DIR=$PWD/data/demo python manage.py serve --port 8849 --no-scheduler
6. **`.gitignore` 不支持行尾注释**——规则后跟 `# 注释` 会让整行失效。注释必须单独占一行,
改完用 `git check-ignore -v <file>` 逐条确认命中。
### 多用户相关的四条(v1.2.0 起)
### 多用户与权限相关的六条(v1.2.0 起,v1.3.0 扩充)
7. **`uid` 必须是 `conn` 之后的第一个位置参数,且不给默认值。**
这是防越权的核心机制:漏传就直接 `TypeError`,而不是静默返回所有人的数据。
@@ -90,24 +92,39 @@ WB_DATA_DIR=$PWD/data/demo python manage.py serve --port 8849 --no-scheduler
9. **验证码答案只能放服务端。** 不要图省事塞进 `session`——Flask 的 session 是
「签名 + base64」而非加密,客户端能直接解开读到答案。下发给浏览器的只有随机 `captcha_id`;
且校验时**先删后判**(一次性)。同理,验证码图不要用 SVG 渲染,那玩意是文本。
10. **改主键的迁移必须「删索引 → 改名 → 建新表 → 回填 → 删旧表」。**
10. **写权限只有一个入口:`config.writable_by(key, is_admin)`。**(v1.3.0)
页面上的置灰 / 隐藏只是「不给误导性按钮」,真正的闸门是服务端判断 ——
所以别在模板里另写一套「哪些键只读」的条件,那必然会和接口判断漂移。
新增一个可配置项时,先决定它的归属:`GLOBAL_KEYS`(实例级、仅管理员)
还是 `USER_EDITABLE_KEYS`(个人级、人人可写本人那份),然后只改这一处。
11. **「键存哪一级」和「谁能写」必须对齐。**(v1.3.0)
反例:把采集参数只写进管理员自己的 `user_id`,其它账号读取时会回落到 `DEFAULTS`,
于是**管理员改的值对别人完全不生效** —— 不报错、不进日志,是个纯粹的静默 bug。
所以实例级策略(调度、采集参数、注册策略)一律存 `user_id=0`,
并由 `db.set_setting()` 强制重定向(`slot:*` 这类**个人簿记键**除外,它们本来就该是个人级)。
12. **`user_id=0` 不是孤儿行。**(v1.3.0)
实例级配置挂在一个不对应任何真实账号的 `user_id=0` 上。任何「清理孤儿行」的语句
都必须排除它 —— `DELETE FROM settings WHERE user_id NOT IN (SELECT id FROM users)`
会把整片实例级配置删掉(历史缺陷:实测把 19 个实例级键清到只剩 1 行)。
现在 `tools/smoke.py` 里有「跑完整轮 smoke 后实例级配置一条不少」的防回归断言,别删。
13. **改主键的迁移必须「删索引 → 改名 → 建新表 → 回填 → 删旧表」。**
`ALTER TABLE … RENAME TO` 会**把索引一起带走**,后续 `CREATE INDEX IF NOT EXISTS`
就变成空操作,新表会零索引。本项目在迁移前先调 `_drop_all_user_indexes()`,
并把 `ALTER TABLE … ADD COLUMN` 放在 `executescript` 之前。
### 加密与验证码这两块(零依赖约束)
11. **`crypto.py` 与 `captcha.py` 只能用标准库。** 项目的硬约束是「只要 Flask / waitress / openpyxl」
14. **`crypto.py` 与 `captcha.py` 只能用标准库。** 项目的硬约束是「只要 Flask / waitress / openpyxl」
—— 所以 ChaCha20、HMAC、PNG 编码、点阵字模都是手写的。想引 `cryptography` 或 `Pillow`
之前先想清楚:这会让「下载即跑」的卖点消失。
12. **解密失败必须显式报错,不能「失败就返回原值」。** `crypto.decrypt()` 对非 `v1.` 前缀
15. **解密失败必须显式报错,不能「失败就返回原值」。** `crypto.decrypt()` 对非 `v1.` 前缀
原样返回(兼容历史明文),但**校验不过就抛 `DecryptError`**。静默降级会让加密形同虚设。
## 五、代码风格
- 遵循 PEP 8;行宽 100。
- **注释与文档字符串用中文**,说明「为什么这么做」而不是「这行在做什么」。
- 提交前用 `python -m compileall` 与 `python tools/smoke.py` 自查。
- 提交前用 `python -m compileall` 与 `python tools/smoke.py` 自查;**动过文档再加一条 `python tools/check_docs.py`**。
- 不要引入新的第三方依赖,除非有充分理由并在 PR 里说明——这个项目的卖点之一就是依赖少。
如果确实新增了,请同步登记到 [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)。