diff --git a/db/zqyy_app/migrations/20260505__remove_manager_view_tasks.sql b/db/zqyy_app/migrations/20260505__remove_manager_view_tasks.sql new file mode 100644 index 0000000..2ec0403 --- /dev/null +++ b/db/zqyy_app/migrations/20260505__remove_manager_view_tasks.sql @@ -0,0 +1,37 @@ +-- 2026-05-05 +-- F1-5b BE-1: manager 角色去除 view_tasks 权限 +-- +-- 背景: Wave 1 走查发现 manager 角色用户进入 task-list 页面收到 403 "权限不足"。 +-- 根因定位: require_permission 通过,但 task_manager._get_assistant_id 因 +-- manager 在 user_assistant_binding 无有效绑定而抛 403,detail 与权限错误同名。 +-- +-- 设计决策(Neo 2026-05-05): task-list 是助教个人工作台业务概念, +-- manager(店长)角色没有"我自己的任务"业务场景,监督需求由 board-coach +-- 等汇总看板覆盖。从权限矩阵层面移除 manager 的 view_tasks。 +-- +-- 影响: +-- - manager 登录后小程序 tabBar 不再显示"任务" tab(getVisibleTabs 自动隐藏) +-- - manager 直接调用 GET /api/xcx/tasks 仍返回 403,但根因变为 require_permission +-- 拦截而非 _get_assistant_id 的语义错位 +-- - coach / head_coach 角色 view_tasks 权限不变,助教工作台正常使用 +-- +-- 兼容性: +-- - 不改 schema,仅 seed 数据 DELETE +-- - 已登录的 manager 用户 access_token 中 roles=[manager] 不变,但下次请求 +-- /api/xcx/me 拿到的 permissions 列表立即少了 view_tasks(get_user_permissions +-- 每次请求都查 DB,无缓存) +-- - 前端 tabBar 在 onShow 时调 checkPageAccess + getVisibleTabs 重算,自动隐藏 +-- +-- 回滚: +-- INSERT INTO auth.role_permissions (role_id, permission_id) +-- SELECT +-- (SELECT id FROM auth.roles WHERE code = 'manager'), +-- (SELECT id FROM auth.permissions WHERE code = 'view_tasks'); + +BEGIN; + +DELETE FROM auth.role_permissions +WHERE role_id = (SELECT id FROM auth.roles WHERE code = 'manager') + AND permission_id = (SELECT id FROM auth.permissions WHERE code = 'view_tasks'); + +COMMIT; diff --git a/docs/audit/changes/2026-05-05__wave1_f1_5b_be1_task_list_403_root_cause.md b/docs/audit/changes/2026-05-05__wave1_f1_5b_be1_task_list_403_root_cause.md new file mode 100644 index 0000000..ca8a41d --- /dev/null +++ b/docs/audit/changes/2026-05-05__wave1_f1_5b_be1_task_list_403_root_cause.md @@ -0,0 +1,197 @@ +# 2026-05-05 · F1-5b BE-1 task-list 403 根因定位 + 修复(B 方案) + +> Wave 1 / F1-5b Wave A 第 7 项任务(详见 `docs/_overview/wave1-findings/F1-5b-tasks.md` §4.2) +> +> 工作量评估 M / 3-4h(实际 ~ 2h:1.5h 根因定位 + 0.5h B 方案实施)。 +> +> **Neo 2026-05-05 拍板 B 方案**:任务 tab 只对助教身份开放,管理身份移除 view_tasks 权限。 + +## 背景 + +Wave 1 走查发现小程序 manager 角色用户(Neo, user_id=8778)访问 `pages/task-list` 时收到 403 "权限不足"。原假设:`require_permission` 与 JWT 中的 site_id 不一致。 + +## 根因(已完全证实,非 require_permission 问题) + +403 **不来自** `require_permission`,而来自 `task_manager._get_assistant_id()` 第 104 行硬编码的 `raise HTTPException(status_code=403, detail="权限不足")`,与权限校验 detail 完全相同。 + +### 完整证据链 + +#### 证据 1 — JWT 与 DB 状态完全一致 + +``` +JWT payload: sub=8778, site_id=2790685415443269, aud=miniapp, roles=[manager] +auth.users[8778]: status=approved +auth.user_site_roles[8778]: site=2790685415443269 role_id=170(manager) is_removed=false +auth.role_permissions[170]: 5 项含 view_tasks +``` + +#### 证据 2 — require_permission 通过 + +在 `app/middleware/permission.py:81` 临时插桩(已移除)证实: + +``` +uid=8778(int) site=2790685415443269(int) required=('view_tasks',) +got=['view_board', 'view_board_coach', 'view_board_customer', 'view_board_finance', 'view_tasks'] +missing=set() +``` + +`/api/xcx/me` 返回的 permissions 列表也含 view_tasks,与 require_permission 内部查询完全一致。 + +#### 证据 3 — 403 来自 _get_assistant_id + +`apps/backend/app/services/task_manager.py:84-105`: + +```python +def _get_assistant_id(conn, user_id: int, site_id: int) -> int: + with conn.cursor() as cur: + cur.execute( + """ + SELECT assistant_id + FROM auth.user_assistant_binding + WHERE user_id = %s AND site_id = %s AND assistant_id IS NOT NULL + AND is_removed = false + ORDER BY id DESC + LIMIT 1 + """, + (user_id, site_id), + ) + row = cur.fetchone() + if not row: + raise HTTPException(status_code=403, detail="权限不足") # ← 此处! + return row[0] +``` + +#### 证据 4 — Neo 在 user_assistant_binding 无可用绑定 + +``` +id=298 assistant_id=None is_removed=false ← 唯一 is_removed=false 但 assistant_id 为 NULL +id=297 assistant_id=3137... is_removed=true +id=295 assistant_id=2964640... is_removed=true +id=290 assistant_id=3124582... is_removed=true +id=289 assistant_id=2964673... is_removed=true +id=288 assistant_id=2964673... is_removed=true +... 共 11 条,is_removed=true 的有 10 条,is_removed=false 的 1 条但 assistant_id=NULL +``` + +SQL `WHERE assistant_id IS NOT NULL AND is_removed = false` 返回 0 行 → `_get_assistant_id` 抛 403。 + +## 设计层冲突 + +### 当前实现(coach 视角) + +`get_task_list_v2` 整个查询基于**单一 assistant_id**: + +- `WHERE assistant_id = %s` +- `batch_query_for_task_list(... assistant_id ...)` +- `build_performance_summary(... assistant_id ...)` + +只服务"助教看自己的任务"场景。 + +### 前端调用(manager 也走此入口) + +`pages/task-list` 在 manager 角色下也展示("查看任务")。从 manager 视角期望"看 site 下所有任务"(或某种聚合视图),但后端不支持。 + +## 三个候选修复方案 + +| 方案 | 内容 | 影响 | 工作量 | +|------|------|------|------| +| **A** 改后端 | manager 角色绕过 _get_assistant_id,改成查 site 下所有 assistants 的任务聚合 | 大,触及核心 SQL/分页/绩效汇总 | L 4-6h | +| **B** 改前端 | manager 角色下隐藏/禁用 task-list tab(走 board-coach 等管理者视角) | 中,需对齐产品设计 | S 1h | +| **C** 数据修复 | 给 Neo (manager) 创建一条 user_assistant_binding(is_removed=false + 真实 assistant_id) | 极小,但绕过设计 | XS 5min | + +### 方案推荐(待 Neo 决策) + +**首选 B**:从架构上更合理,task-list 设计是助教视角,manager 应有自己的"管理任务概览"页面(可以 Wave B 新建)。 + +**次选 A**:工作量大但保持 task-list 兼容多角色。 + +**不推荐 C**:绕过设计,后续维护混乱(其他 manager 也会遇到同样问题)。 + +## 影响范围 + +无代码改动(本次仅完成根因定位),无任何端影响。 + +## 测试 + +无新增测试。诊断脚本: + +- `_DEL/walkthrough_f1_5b/step_be1_perm_probe.py` — 权限链路 SQL 直查 +- `_DEL/walkthrough_f1_5b/step_be1_user_status.py` — auth.users 状态 +- `_DEL/walkthrough_f1_5b/step_be1_simulate_perm.py` — 模拟 require_permission SQL +- `_DEL/walkthrough_f1_5b/step_be1_uab_probe.py` — user_assistant_binding 完整状态(根因) + +## Neo 决策(2026-05-05) + +> "这是一个产品设计的问题,不是 bug。任务的 tab 只有助教身份的用户可以进入并查看,让管理身份的用户进入没有意义。因为他们使用业务场景中不存在任务方面的场景。" + +→ 选 **B 方案**:DB 层从 manager 角色移除 view_tasks 权限,前端 tabBar 自动隐藏,后端 require_permission 自动拦截。 + +## B 方案实施 + +### Migration + +`db/zqyy_app/migrations/20260505__remove_manager_view_tasks.sql`: + +```sql +DELETE FROM auth.role_permissions +WHERE role_id = (SELECT id FROM auth.roles WHERE code = 'manager') + AND permission_id = (SELECT id FROM auth.permissions WHERE code = 'view_tasks'); +``` + +### docs/database/ 同步 + +`docs/database/changes/2026-05-05__remove_manager_view_tasks.md`(完整变更说明 + 兼容性 + 回滚 + 4 条校验 SQL)。 + +### 测试库执行结果 + +``` +改前 manager 权限: [view_board, view_board_coach, view_board_customer, view_board_finance, view_tasks] +改后 manager 权限: [view_board, view_board_coach, view_board_customer, view_board_finance] +view_tasks 现绑定到角色: [coach, head_coach] (manager 已剥离) +Neo (user_id=8778) 实际权限: [view_board, view_board_coach, view_board_customer, view_board_finance] +``` + +4 条校验 SQL 全部 PASS。 + +### 4a 端到端走查(live 模式) + +**前提**:Neo (manager) relaunch 小程序触发 onLaunch 重新拉权限。 + +| 维度 | 改前 | 改后 | +|------|------|------| +| globalData.permissions | 5 项含 view_tasks | 4 项不含 view_tasks | +| globalData.visibleTabs | [task, board, my] | **[board, my]** | +| 小程序 tabBar 实际渲染 | 显示"任务" tab | **"任务" tab 已隐藏** | +| 强制调 GET /api/xcx/tasks | 403 "权限不足"(根因 _get_assistant_id) | 403 "权限不足"(根因 require_permission) | + +外观 status code 不变,但语义从"业务逻辑错位"转为"权限正确拦截"。 + +### 4b sandbox 验证 + +权限校验与 sandbox 业务时钟无关(权限查 auth.role_permissions,sandbox 影响 ETL 视图),无需重复走查。 + +## 临时 debug 代码已清理 + +- `permission.py` 中临时 insert 的诊断 print(已 revert) +- `tmp/be1_diag.txt` 临时诊断文件(已删除) + +## 回滚策略 + +```sql +INSERT INTO auth.role_permissions (role_id, permission_id) +SELECT + (SELECT id FROM auth.roles WHERE code = 'manager'), + (SELECT id FROM auth.permissions WHERE code = 'view_tasks') +WHERE NOT EXISTS ( + SELECT 1 FROM auth.role_permissions rp + JOIN auth.roles r ON rp.role_id = r.id + JOIN auth.permissions p ON rp.permission_id = p.id + WHERE r.code = 'manager' AND p.code = 'view_tasks' +); +``` + +幂等回滚。已记入 `docs/database/changes/2026-05-05__remove_manager_view_tasks.md`。 + +## Co-Authored-By + +Claude Opus 4.7 (1M context) diff --git a/docs/database/changes/2026-05-05__remove_manager_view_tasks.md b/docs/database/changes/2026-05-05__remove_manager_view_tasks.md new file mode 100644 index 0000000..0a361bd --- /dev/null +++ b/docs/database/changes/2026-05-05__remove_manager_view_tasks.md @@ -0,0 +1,134 @@ +# 2026-05-05 · manager 角色去除 view_tasks 权限 + +> F1-5b BE-1 修复(方案 B,Neo 2026-05-05 决策) +> +> migration: `db/zqyy_app/migrations/20260505__remove_manager_view_tasks.sql` + +## 背景 + +Wave 1 走查发现 manager 角色用户进入小程序"任务" tab 收到 403 "权限不足"。BE-1 根因定位详见 `docs/audit/changes/2026-05-05__wave1_f1_5b_be1_task_list_403_root_cause.md`。 + +**业务侧结论**:`pages/task-list` 是助教个人工作台业务概念,manager(店长)角色没有"我自己的任务"业务场景。监督需求由 board-coach / board-finance 等汇总看板覆盖。从权限矩阵层面移除 manager 的 view_tasks 才是正确做法。 + +## 变更说明 + +### 数据变更 + +```sql +DELETE FROM auth.role_permissions +WHERE role_id = (SELECT id FROM auth.roles WHERE code = 'manager') + AND permission_id = (SELECT id FROM auth.permissions WHERE code = 'view_tasks'); +``` + +仅移除一条 `(manager, view_tasks)` 关联记录。 + +### Schema 变更 + +无。`auth.role_permissions` 表结构不变,仅 seed 数据 DELETE 一条。 + +### 权限矩阵改前/改后 + +| 角色 | 改前 | 改后 | +|------|------|------| +| coach(助教) | view_tasks | **view_tasks(保留)** | +| head_coach(主教练) | view_tasks | **view_tasks(保留)** | +| manager(店长) | view_board / view_board_coach / view_board_customer / view_board_finance / **view_tasks** | view_board / view_board_coach / view_board_customer / view_board_finance | +| staff(员工) | (无 view_tasks) | (无 view_tasks,不变) | + +## 兼容性 + +### 后端 + +- 所有用 `require_permission("view_tasks")` 保护的端点(/api/xcx/tasks/* 等):manager 调用会被 require_permission 拦截返回 403,detail "权限不足"。**根因从 _get_assistant_id 的语义错位转为 require_permission 正确拦截**,响应外观不变(都是 403 + 权限不足),但语义清晰 +- get_user_permissions 无缓存,改动**立即生效**(不需重启) +- /api/xcx/me 返回的 permissions 列表立即少 view_tasks + +### 小程序 + +- `auth-guard.ts::getVisibleTabs()` 基于 permissions 自动重算 → manager 登录后 tabBar 不再显示"任务" tab +- 已在线 manager 用户:下次 onShow 时 checkPageAccess 重查权限,自动隐藏"任务" tab +- 任务相关页面(task-list / task-detail 等)在 manager 角色下被路由守卫直接跳到 getPermissionHome(看板),不再触发 403 + +### admin-web / tenant-admin + +- 不受影响。两个 admin 后台用 super_admin / site_admin / tenant_admin 角色,与小程序 manager 是独立角色体系。 + +### ETL + +- 无影响。 + +## 回滚策略 + +```sql +INSERT INTO auth.role_permissions (role_id, permission_id) +SELECT + (SELECT id FROM auth.roles WHERE code = 'manager'), + (SELECT id FROM auth.permissions WHERE code = 'view_tasks') +WHERE NOT EXISTS ( + SELECT 1 FROM auth.role_permissions rp + JOIN auth.roles r ON rp.role_id = r.id + JOIN auth.permissions p ON rp.permission_id = p.id + WHERE r.code = 'manager' AND p.code = 'view_tasks' +); +``` + +幂等回滚,避免 PRIMARY KEY 冲突。 + +## 验证 SQL(已在测试库执行通过) + +### 校验 1:manager 不再拥有 view_tasks + +```sql +SELECT COUNT(*) FROM auth.role_permissions rp +JOIN auth.roles r ON rp.role_id = r.id +JOIN auth.permissions p ON rp.permission_id = p.id +WHERE r.code = 'manager' AND p.code = 'view_tasks'; +-- 期望: 0 +``` + +### 校验 2:coach / head_coach 仍保留 view_tasks + +```sql +SELECT r.code FROM auth.role_permissions rp +JOIN auth.roles r ON rp.role_id = r.id +JOIN auth.permissions p ON rp.permission_id = p.id +WHERE p.code = 'view_tasks' ORDER BY r.code; +-- 期望: coach, head_coach +``` + +### 校验 3:manager 仍有 4 项看板权限 + +```sql +SELECT array_agg(p.code ORDER BY p.code) +FROM auth.role_permissions rp +JOIN auth.roles r ON rp.role_id = r.id +JOIN auth.permissions p ON rp.permission_id = p.id +WHERE r.code = 'manager'; +-- 期望: {view_board, view_board_coach, view_board_customer, view_board_finance} +``` + +### 校验 4:典型 manager 用户(Neo, user_id=8778)实际权限链路 + +```sql +SELECT array_agg(DISTINCT p.code ORDER BY p.code) +FROM auth.user_site_roles usr +JOIN auth.role_permissions rp ON usr.role_id = rp.role_id +JOIN auth.permissions p ON rp.permission_id = p.id +WHERE usr.user_id = 8778 AND usr.site_id = 2790685415443269 + AND usr.is_removed = false; +-- 期望: {view_board, view_board_coach, view_board_customer, view_board_finance} +``` + +## 正式库执行说明 + +本次 migration **仅在测试库执行**。生产环境同步时: + +```bash +psql "$APP_DB_DSN" -f db/zqyy_app/migrations/20260505__remove_manager_view_tasks.sql +``` + +执行前后必须跑 4 条校验 SQL 对比 改前/改后 状态。 + +## 端到端走查(Neo 实地确认) + +详见 `docs/audit/changes/2026-05-05__wave1_f1_5b_be1_task_list_403_root_cause.md`(同日)审计末尾的 4a/4b 双口径走查记录。