From bbb43a210f4eee3926e6c9436b1349c43c2d8aaf Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:38:16 +0800 Subject: [PATCH] docs(docs): sync 004 with portal-shell data layer and GraphQL hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 §11.7 portal-shell 前端数据访问层 + GraphQL 安全栈: - §11.7.1 四层数据访问分层(Widget → API → Operations → Hook) - §11.7.2 GraphQL 安全栈(APQ + PQ Manifest + 深度/成本限制) - §11.7.3 Resolver 权限守卫审计与补齐 新增 §16.5 portal-shell 数据抽象与 GraphQL 加固子阶段(M1-M4 完成) 新增 ADR-042(前端数据访问四层分层)、ADR-043(PQ Manifest + APQ 安全加固) 更新 §16.4 关联 spec 文档列表 --- .../004_architecture_impact_map.md | 175 +++++++++++++++++- 1 file changed, 169 insertions(+), 6 deletions(-) diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index f03a39a..741f059 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -1622,6 +1622,146 @@ GraphQL 响应中,错误通过 `errors[].extensions` 携带 ActionState 字段 } ``` +### 11.7 portal-shell 前端数据访问层 + GraphQL 安全栈(v2.1 M3 新增) + +> 来源:[portal-shell 数据抽象与 GraphQL 加固 spec](../superpowers/specs/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening-design.md) v1.0 / [实施 plan](../superpowers/plans/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening.md) +> 关联 ADR:ADR-023(Apollo Federation 替代 BFF)、ADR-036(Router-Authorization)、ADR-042(前端数据访问四层分层)、ADR-043(PQ Manifest + APQ 安全加固) +> 实施阶段:v2.1 M1(数据抽象层)+ M3(GraphQL 安全加固)+ M4(resolver 守卫补齐) + +#### 11.7.1 四层数据访问分层(Widget → API → Operations → Hook) + +> v2.1 变更:废弃 widget 内联 `gql\`...\``字面量模式,统一抽取到`lib/api/` 四层架构。原 31 个 widget 全部迁移,0 处内联 gql 残留。 + +```mermaid +graph TD + subgraph Widget["Widget 层(UI)"] + W[widgets/*.tsx
只关心渲染] + end + subgraph API["API 层(语义化函数)"] + A1[lib/api/parent.ts] + A2[lib/api/teacher.ts] + A3[lib/api/admin.ts] + A4[lib/api/student.ts] + A5[lib/api/universal.ts] + A6[lib/api/sidebar.ts] + A7[lib/api/topbar.ts] + end + subgraph Ops["Operations 层(gql 文档集中)"] + O[lib/api/operations/*.graphql.ts
51 个 DocumentNode 常量] + end + subgraph Hook["Hook 层(Apollo 封装)"] + H1[useWidgetQuery] + H2[useWidgetMutation] + end + subgraph Codegen["类型生成"] + C[graphql-codegen
7 子图 schema.graphql → types.ts] + end + W --> A1 & A2 & A3 & A4 & A5 & A6 & A7 + A1 & A2 & A3 & A4 & A5 & A6 & A7 --> O + A1 & A2 & A3 & A4 & A5 & A6 & A7 --> H1 & H2 + O --> C +``` + +**层职责矩阵**: + +| 层 | 位置 | 职责 | 禁止 | +| ---------- | --------------------------------------------- | ------------------------------------------------------- | ----------------------------------------- | +| Widget | `src/widgets/*.tsx` | UI 渲染、用户交互 | 内联 gql 字面量、直接 import Apollo hooks | +| API | `src/lib/api/.ts` | 语义化函数(`useMyChildren()` / `saveLessonPlan()` 等) | 写 gql 字符串、直接操作 Apollo cache | +| Operations | `src/lib/api/operations/*.graphql.ts` | gql DocumentNode 常量集中存放(51 个 query/mutation) | 包含业务逻辑 | +| Hook | `src/lib/useWidgetQuery/useWidgetMutation.ts` | Apollo useQuery/useMutation 封装 + ApiError 归一化 | 引用具体 domain | +| Types | `src/lib/api/types.ts` + codegen 生成 | TS 类型(Widget 友好形) | 手写 | + +**7 个 domain API 文件**: + +| 文件 | Widget 数 | 主要场景 | +| -------------- | --------- | ---------------------------------------------------------------------------------- | +| `universal.ts` | 7 | 公告 / 通知 / 课表 / 作业 / 考试 / 成绩 / 考勤(多角色复用) | +| `sidebar.ts` | 3 | 当前用户 / 我的班级 / 学期列表(侧边栏) | +| `topbar.ts` | 2 | 搜索 / 通知铃铛(顶栏,`useNotificationBell` 限 N 条) | +| `teacher.ts` | 5 | 教材 / 教案 / 题目 / 课表规则 / 教案保存 / 课表更新 | +| `student.ts` | 5 | AI Tutor / 选修课 / 错题本 / 学习路径 / 错题掌握标记 | +| `parent.ts` | 3 | 我的子女 / 请假审批 / 请假驳回 | +| `admin.ts` | 11 | 用户 / 角色 / 权限 / 学校 / 插件注册 / 角色插件映射 / 布局模板 / 邀请码 / 审计日志 | + +**类型生成(graphql-codegen)**: + +- 配置:`apps/portal-shell/codegen.yml` +- schema 来源:7 个子图的 `services//src/graphql/generated/schema.graphql` +- 生成产物:`src/lib/api/operations/types.ts`(仅类型,无运行时代码) +- 关键配置:`skipDocumentsValidation: true`(避免子图未启动时 codegen 失败) +- schema 归一化:`scripts/normalize-schema.ts` 移除 federation 指令(`@key` / `@requires` / `@extends`)防止 codegen 误解析 + +#### 11.7.2 GraphQL 安全栈(APQ + PQ Manifest + 深度/成本限制) + +> v2.1 M3 安全加固:解决"前端 gql 字面量暴露 schema,攻击者可构造任意查询探测"风险。 + +```mermaid +graph LR + subgraph FE["portal-shell(前端)"] + APQ[createPersistedQueryLink
sha256 query → hash] + end + subgraph Router["apollo-router :3000"] + Manifest[pq-manifest.json
hash → query 白名单] + Limits[limits.max_depth=10
max_cost=1000
max_batch_size=5] + Intro[introspection
环境变量控制] + end + subgraph Sub["子图 /graphql"] + Guard[RouterAuthGuard
+ @RequirePermission] + end + APQ -->|只发 hash| Manifest + Manifest -->|未知 hash 拒绝| APQ + Manifest --> Limits + Limits --> Intro + Intro --> Guard +``` + +**安全机制矩阵**: + +| 机制 | 位置 | 防御目标 | 配置 | +| ---------------------------------- | -------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------- | +| APQ(Automatic Persisted Queries) | `apps/portal-shell/src/lib/apollo-client.ts` | 前端只发 query hash,不发明文 query | `createPersistedQueryLink({ sha256 })`,env `NEXT_PUBLIC_APOLLO_APQ=false` 关闭 | +| PQ Manifest | `apps/portal-shell/public/pq-manifest.json` | Router 仅解析白名单 hash,拒绝未知 hash 任意查询 | 51 个 query 的 `sha256 → query` 映射,由 `scripts/generate-pq-manifest.ts` 生成 | +| Router 强制 manifest | `infra/apollo-router/router.yaml` | 生产模式(`require_manifest: true`)拒绝未注册查询 | env `APOLLO_REQUIRE_PQ_MANIFEST=true`,entrypoint.sh 启动前检查文件存在性 | +| 深度限制 | `router.yaml` `limits.max_depth=10` | 防止深度嵌套查询 DoS | 11 层嵌套被 router 拒绝(`QUERY_DEPTH_EXCEEDED`) | +| 成本限制 | `router.yaml` `limits.max_cost=1000` | 防止高成本查询 DoS | 按字段复杂度评分累加 | +| 批量限制 | `router.yaml` `limits.max_batch_size=5` | 防止批量查询 DoS | 单次请求最多 5 个 query | +| Introspection 控制 | `router.yaml` `supergraph.introspection` | 生产关闭 introspection 防止 schema 泄露 | env `APOLLO_ROUTER_INTROSPECTION=false`(开发默认 true) | +| Router-Authorization 信任 | 子图 `RouterAuthGuard`(见 §5.5) | 防止绕过 Router 直接访问子图 | 子图校验 `Router-Authorization` header,拒绝非 Router 请求(ADR-036) | +| Resolver 字段级权限 | 子图 `@RequirePermission()` 装饰器 | 防止越权访问字段 | 每个 Resolver 必须声明权限点(见 §3.8) | + +**PQ Manifest 生成流程**: + +1. `pnpm --filter @edu/portal-shell run codegen` → 从 7 子图 schema 生成 TS 类型 +2. `pnpm --filter @edu/portal-shell run generate-pq-manifest` → 遍历 `lib/api/operations/index.ts` 中所有 DocumentNode,`print(doc)` 后 `sha256(query)` 生成映射,写入 `public/pq-manifest.json` +3. `prebuild` 钩子自动串联 codegen + generate-pq-manifest +4. Docker compose 挂载 `pq-manifest.json` 到 apollo-router `/etc/apollo-router/pq-manifest.json:ro` +5. apollo-router entrypoint.sh 启动前校验 manifest 存在性(`require_manifest=true` 时缺失即 exit 1) + +**安全栈测试覆盖**(`apps/portal-shell/src/lib/api/__tests__/security.test.ts`): + +- PQ Manifest 完整性(6 cases):DocumentNode 校验 / sha256 稳定性 / 确定性 / 唯一性 / manifest 文件有效性 / hash 一致性 +- Query depth limit(2 cases):11 层嵌套构造 / 合法查询构造(实际拒绝由 router 执行) +- APQ behavior(2 cases):默认启用 / `NEXT_PUBLIC_APOLLO_APQ=false` 关闭 + +#### 11.7.3 Resolver 权限守卫(@RequirePermission 字段级) + +> v2.1 M4:补齐 19 个 TS resolver 文件的 `@RequirePermission()` 装饰器。Python 子图(data-ana/ai)权限基础设施待补,见 [审计报告](../security/graphql-auth-audit-2026-07.md) follow-up。 + +**已审计范围**:50 个 resolver(35 TS + 15 Python) + +| 状态 | 数量 | 说明 | +| ------------------------- | ---- | -------------------------------------------------------------------------------------- | +| 已有 `@RequirePermission` | 37 | 18 原有 + 19 M4 补齐 | +| 缺失守卫 | 13 | 4 个 TS(`/graphql` 路径未覆盖,由 RouterAuthGuard 兜底)+ 9 个 Python(待补基础设施) | + +**TS 审计 follow-up**(不阻断 M3 验收): + +- 4 个 TS 子图(iam / core-edu / content / msg)的 `AuthMiddleware` 仅覆盖 REST 路径,未覆盖 `/graphql`(由 `RouterAuthGuard` 兜底,仍建议补齐字段级守卫) +- Python 子图(data-ana / ai)缺 `@RequirePermission` 基础设施,需补 Strawberry / Ariadne 中间件 + +详见:`docs/security/graphql-auth-audit-2026-07.md` + --- ## 12. 架构约束 @@ -1728,6 +1868,8 @@ GraphQL 响应中,错误通过 `errors[].extensions` 携带 ActionState 字段 | ADR-039 | 乐观锁版本号回传 | CQRS 读后一致性:写返回 version,读携带 expectedVersion,ES 落后则穿透读 MySQL | ✅ 已采纳(v2.1) | | ADR-040 | Redis Pub/Sub 推送背板 | 边缘网关不挂 Kafka,msg worker → Redis Pub/Sub → realtime-gateway 订阅,轻量 + 按需订阅 | ✅ 已采纳(v2.1) | | ADR-041 | ScopeToken 优化大规模 ID 列表 | 不传全量数组,传极短 token,子图从 Redis SMEMBERS 获取,降低 HTTP payload 开销 | ✅ 已采纳(v2.1) | +| ADR-042 | portal-shell 前端数据访问四层分层(2026-07-17) | Widget → API → Operations → Hook,废弃 widget 内联 gql 字面量,集中管理 GraphQL 文档 | ✅ 已采纳(v2.1,lib/api/ 7 domain + 51 operations) | +| ADR-043 | PQ Manifest + APQ 安全加固(2026-07-17) | 前端只发 query hash,Router 仅解析白名单 manifest,叠加深度/成本/introspection 限制 | ✅ 已采纳(v2.1,pq-manifest.json + router.yaml limits) | --- @@ -1994,12 +2136,33 @@ graph LR ### 16.4 v2.1 关联 spec 文档 -| 文档 | 用途 | -| --------------------------------------------------------------------------- | ---------------------------------------------------------- | -| `docs/superpowers/specs/2026-07-14-architecture-v2-redesign-design.md` | v2.1 架构重设计主 spec(ADR-023~041 决策源) | -| `docs/superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md` | portal-shell 插件化仪表盘设计 spec | -| `infra/port-allocation.md` | 端口分配唯一源(含 apollo-router/config-service/Temporal) | +| 文档 | 用途 | +| ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| `docs/superpowers/specs/2026-07-14-architecture-v2-redesign-design.md` | v2.1 架构重设计主 spec(ADR-023~041 决策源) | +| `docs/superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md` | portal-shell 插件化仪表盘设计 spec | +| `docs/superpowers/specs/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening-design.md` | portal-shell 前端数据访问层 + GraphQL 安全栈加固 spec(ADR-042/043) | +| `docs/superpowers/plans/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening.md` | portal-shell 数据抽象与 GraphQL 加固 20-task 实施 plan | +| `docs/security/graphql-auth-audit-2026-07.md` | 50 resolver @auth 审计报告(35 TS + 15 Python) | +| `infra/port-allocation.md` | 端口分配唯一源(含 apollo-router/config-service/Temporal) | + +### 16.5 portal-shell 数据抽象与 GraphQL 加固子阶段(2026-07-17 新增) + +> 来源:[spec v1.0](../superpowers/specs/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening-design.md) + [plan](../superpowers/plans/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening.md) +> 关联 ADR:ADR-042(前端数据访问四层分层)、ADR-043(PQ Manifest + APQ 安全加固) + +| 子阶段 | 内容 | 退出标准 | 状态 | +| ------ | ------------------------------------------------------------ | --------------------------------------------------------- | ------- | +| M1 | lib/api 四层架构 + 7 domain 迁移 + 31 widget 全量切换 | 0 处内联 gql 字面量、95/95 测试通过 | ✅ 完成 | +| M2 | 迁移完整性验证(typecheck + lint + test 全绿) | 0 error / 0 warning / 85 测试通过(M1 收尾) | ✅ 完成 | +| M3 | GraphQL 安全加固(APQ + PQ Manifest + router limits + 测试) | apollo-router 启用 manifest + 深度/成本限制 + 10 安全测试 | ✅ 完成 | +| M4 | Resolver @RequirePermission 审计 + 补齐 | 50 resolver 审计完成、19 TS resolver 补齐守卫 | ✅ 完成 | + +**Follow-up(不阻断 M3 验收)**: + +- 4 个 TS 子图 `AuthMiddleware` 覆盖 `/graphql` 路径(当前由 `RouterAuthGuard` 兜底) +- Python 子图(data-ana / ai)补 `@RequirePermission` 基础设施(Strawberry / Ariadne 中间件) +- 生产环境部署前将 `APOLLO_REQUIRE_PQ_MANIFEST=true` + `APOLLO_ROUTER_INTROSPECTION=false` 写入部署 env --- -> **本文件 v2.1 已基于代码现状(2026-07-15)全面校准,反映 Apollo Federation + portal-shell 架构重设计(M0-M11 全部完成)。后续代码变更须按 [项目规则 §1](../../.trae/rules/project_rules.md) 同步更新本文件 + 运行 `pnpm run arch:scan`。** +> **本文件 v2.1 已基于代码现状(2026-07-15)全面校准,反映 Apollo Federation + portal-shell 架构重设计(M0-M11 全部完成)+ 2026-07-17 portal-shell 数据抽象与 GraphQL 加固(M1-M4 完成)。后续代码变更须按 [项目规则 §1](../../.trae/rules/project_rules.md) 同步更新本文件 + 运行 `pnpm run arch:scan`。**