From ee3f4a843ffd97765920080d560351a21742a86b Mon Sep 17 00:00:00 2001 From: ila Date: Fri, 21 Aug 2026 00:44:51 +0800 Subject: [PATCH] docs: record portal authentication boundaries (#11) --- docs/02-architecture-and-code-map.md | 16 +++++++++++--- docs/04-local-development-and-verification.md | 21 +++++++++++++++++-- docs/10-deployment-and-operations.md | 17 ++++++++++++--- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/docs/02-architecture-and-code-map.md b/docs/02-architecture-and-code-map.md index f2b205d..7793c20 100644 --- a/docs/02-architecture-and-code-map.md +++ b/docs/02-architecture-and-code-map.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Architecture-and-Code-Map wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Architecture-and-Code-Map.- -wiki_revision: 47686aa767849c6ecffc5db37a76a24564feb5e6 -synchronized_at: 2026-08-20T16:14:54Z +wiki_revision: f9f5d67d06d86694948f5812ce8a565e30b4f266 +synchronized_at: 2026-08-20T16:41:50Z # 架构与代码地图 @@ -56,7 +56,7 @@ MVP-0 第一张上传图自动作为 `primary`,其余为 `reference`;不提 - `migrations/000001_mvp0_base` 建立 `users`、`providers`、`provider_models`、`prompt_templates`、`generations`、`generation_inputs`、`generation_outputs`,生产启动不调用 AutoMigrate。 - 迁移显式定义用户幂等唯一键、队列/历史索引、5 个外键和删除规则,以及状态、租约、错误、attempt 数组和输出内容检查约束。 - `internal/seed/defaults.json` 保存已确认 Prompt 和 mock ProviderModel 数据;`cmd/chorus-seed` 在事务中幂等 upsert 构造用户、mock Provider/Model 和 Prompt,不保存或输出明文凭据。 -- `portal` 的 HTTP 路由仍待 #11;`portal/worker` 已在 #10 落地 Provider 选择、输入读取、上游调用、输出保存和最终 CAS,不得把 worker 完成误写成用户 API 已完成。 +- `portal/handler`、`service`、`auth` 和 `session` 已在 #11 落地用户 API 与安全边界;最终页面与静态资源仍属于 #12。`portal/worker` 负责异步上游调用,提交 handler 没有 Provider 依赖。 ### #7 已落地的核心契约 @@ -96,6 +96,16 @@ MVP-0 第一张上传图自动作为 `primary`,其余为 `reference`;不提 - 429/5xx/超时/连接错误以及 400/401/内容策略拒绝均映射为固定脱敏错误;MVP-0 记录 retryable 分类但不换 Provider、不自动重试。 - `internal/platform/mockprovider` 和 `cmd/chorus-mock-provider` 提供本地协议 fixture;自动测试通过注入受控 DNS/dial 连接它,不放宽生产对回环/私网地址的 SSRF 拦截。 - MySQL 8 集成测试分别完成一次 chat 和 images_edits,断言 rendered_prompt、attempt ProviderModel/latency、输出记录、原图和缩略图;全程不访问真实 Provider。 +### #11 已落地的 portal 认证与用户 API + +- `portal/session` 使用服务端内存会话和 HMAC 不透明 Cookie;登录成功与退出都会轮换 session ID 和 CSRF token。Cookie 为 HttpOnly/SameSite=Lax,生产启用 Secure;写请求只接受 `X-CSRF-Token`,避免 multipart 在正文限流前被隐式解析。 +- MVP-0 会话仅支持单 portal 进程,重启后统一失效;会话数量有内存上限并在创建时清理过期项。多实例共享会话不在 MVP-0 范围。 +- `internal/platform/password` 固定 `bcrypt:v1:` 版本化编码;未知版本失败关闭。登录对未知账号、错误密码和禁用账号执行同类密码校验并返回同一错误,按远端地址做内存窗口节流。 +- `portal/service` 统一处理 prompt 渲染、用户作用域幂等、上传数量/单文件/总量、声明与实际 MIME、扩展名、解码和像素校验;第一张图为 primary,其余为 reference。 +- `CreateIdempotentPrepared` 在事务插入 generation 后才执行 inputs 回调,因而文件 metadata 可使用真实 generation ID;幂等重放不执行回调,回调或数据库失败只补偿删除本次保存文件。 +- 同步提交只创建 pending、inputs 和 rendered_prompt,不 import 或调用 Provider/worker,也不读取点数。JSON 与 HTMX 查询共享同一用户作用域服务,认证过期返回 401;HTMX 额外返回登录跳转提示,不修改 generation 状态。 +- 任务、原图、生成图和缩略图查询先用 generation.user_id 过滤,再核对文件 metadata 的 owner/generation;响应只给受控 URL、内容和安全文件名,不暴露 storage key 或文件系统路径。 +- Gin 1.12.0 与用户指定 go-admin 基线一致。portal 不注册公开文件目录、注册、找回密码、管理员登录、点数或同步上游路由。 ## 代码地图 | 想改什么 | 从哪里开始读 | 必要验证 | diff --git a/docs/04-local-development-and-verification.md b/docs/04-local-development-and-verification.md index cde4431..9381eab 100644 --- a/docs/04-local-development-and-verification.md +++ b/docs/04-local-development-and-verification.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Local-Development-and-Verification wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Local-Development-and-Verification.- -wiki_revision: 0f7d68926b23995be4481f05218484be1301a780 -synchronized_at: 2026-08-20T16:15:02Z +wiki_revision: 075e82a07b58c105c46d7fbcfb22d8ed6d8f97f8 +synchronized_at: 2026-08-20T16:45:23Z # 本地开发与验证 @@ -45,6 +45,13 @@ synchronized_at: 2026-08-20T16:15:02Z | `CHORUS_SESSION_KEY` | portal 会话,必须与主密钥分离 | | `CHORUS_STORAGE_ROOT` | 受保护生成物目录 | | `CHORUS_ENV` | `development/test/production`,生产强制安全开关 | +| `CHORUS_SESSION_TTL_MINUTES` | 会话有效分钟数;生产必填 | +| `CHORUS_LOGIN_ATTEMPTS` / `CHORUS_LOGIN_WINDOW_SECONDS` | 单远端地址登录窗口限制;生产必填 | +| `CHORUS_MAX_PROMPT_BYTES` | prompt UTF-8 字节上限;生产必填 | +| `CHORUS_MAX_IMAGES` | 单任务图片数量上限;生产必填 | +| `CHORUS_MAX_IMAGE_BYTES` / `CHORUS_MAX_UPLOAD_BYTES` | 单图/整次 multipart 字节上限;生产必填 | +| `CHORUS_MAX_IMAGE_PIXELS` | 单图解码像素上限;生产必填 | +| `CHORUS_HISTORY_LIMIT` | 单次历史查询上限;生产必填 | | `GITEA_TOKEN` | Wiki/工单操作,不影响产品服务 | ## 第一次运行 @@ -144,6 +151,8 @@ go run ./cmd/chorus-mock-provider 生产安全 Client 始终拒绝回环/私网 Provider URL。自动化端到端测试使用构造公共域名解析和受控 `DialContext` 把请求送到进程内 mock,不应为了让 portal 直连本机 mock 而放宽 SSRF 规则。 +种子密码从 #11 起保存为 `bcrypt:v1:`。已有无版本构造哈希不会被登录兼容;使用同一构造邮箱和本次选择的密码重新运行 `go run ./cmd/chorus-seed`,会更新密码哈希而不新增用户。开发/测试未提供 session key 时进程随机生成临时 key,重启后会话失效;生产必须显式提供至少 32 字节且与 Provider 主密钥分离的 key。 + ### 5. 启动 portal(MVP-0) ```powershell @@ -196,6 +205,14 @@ $env:CHORUS_TEST_DSN = $null ``` - Provider:全部 retryable 类别使用 mock,不消耗真实额度。 +- Portal:真实 MySQL 8 覆盖登录/退出、session/CSRF 轮换、用户作用域幂等、pending 提交、上传边界、任务与文件跨用户授权、终态和 HTMX 401;测试结束 generation/input/output 和构造用户必须为 0。 + +```powershell +$env:CHORUS_TEST_DSN = $env:CHORUS_DSN +go test -v -count=1 ./portal/handler/... ./portal/service/... ./portal/session/... ./portal/auth/... +$env:CHORUS_TEST_DSN = $null +``` + - SSRF:IPv4/IPv6 私网、DNS rebinding、redirect 链、环境代理、结果 URL。 - 安全:密码/session/CSRF、登录节流、跨用户任务与文件、错误脱敏、密钥轮换。 - 浏览器:HTMX 动态状态、终态停止、认证过期、网络错误;375/768/1024,无主区域横向滚动,键盘和 reduced-motion。 diff --git a/docs/10-deployment-and-operations.md b/docs/10-deployment-and-operations.md index 0abb3c5..6a886bc 100644 --- a/docs/10-deployment-and-operations.md +++ b/docs/10-deployment-and-operations.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Deployment-and-Operations wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Deployment-and-Operations.- -wiki_revision: 40b115d8da2f8b7d4e01783eb47a73659cc4d67f -synchronized_at: 2026-08-20T16:15:29Z +wiki_revision: efd73fb39c69cb94a9da7e2157b765b4f8da2431 +synchronized_at: 2026-08-20T16:42:26Z # 部署与运维 @@ -54,7 +54,11 @@ portal → /var/lib/chorus/storage(或后续对象存储适配) | `CHORUS_SESSION_KEY` | portal 会话 | 是 | | `CHORUS_STORAGE_ROOT` | 生成物目录 | 否 | | 上游/存储超时、lease、worker 数 | 执行与优雅退出 | 否 | -| 上传和限流阈值 | 数量、大小、MIME、像素、请求频率 | 否 | +| `CHORUS_SESSION_TTL_MINUTES` | 服务端内存会话有效期 | 否 | +| `CHORUS_LOGIN_ATTEMPTS` / `CHORUS_LOGIN_WINDOW_SECONDS` | 登录节流窗口 | 否 | +| `CHORUS_MAX_PROMPT_BYTES` | prompt 字节上限 | 否 | +| `CHORUS_MAX_IMAGES` / `CHORUS_MAX_IMAGE_BYTES` / `CHORUS_MAX_UPLOAD_BYTES` / `CHORUS_MAX_IMAGE_PIXELS` | 上传边界 | 否 | +| `CHORUS_HISTORY_LIMIT` | 历史查询上限 | 否 | | 保留期与磁盘告警 | 清理和容量保护 | 否 | 生产启动必须校验必需配置、密钥长度/key_id、目录权限、超时与 lease 关系;缺失或不安全时 fail closed。`CHORUS_ENV=production` 下若检测到 AutoMigrate 或 dev-tools 注册必须拒绝启动。 @@ -105,6 +109,13 @@ journalctl -u chorus-portal -n 100 --no-pager MVP-1 同样检查 admin 8081。外部检查经 TLS/nginx 访问,不把内部端口暴露公网。 +## portal 会话与请求边界 + +MVP-0 的终端用户会话保存在单个 portal 进程内存中,Cookie 只携带签名后的不透明 session ID。部署必须保持单实例;portal 重启或切换版本会让全部终端用户重新登录。不要在负载均衡后启动多个 portal 实例,除非先建立新的共享会话设计和工单。 + +生产 Cookie 必须为 Secure/HttpOnly/SameSite=Lax。反向代理不得记录 Cookie 或 CSRF header;请求日志和 GORM SQL 使用参数化输出。JSON 正文、multipart 总量、单图字节/像素/数量和历史查询都有显式生产配置,缺少任一阈值时启动失败。 + +上传先在应用层校验,再在 generation 事务回调中保存带 owner/generation metadata 的文件。事务失败只清理本请求保存的 key;存储根目录不能作为 nginx/static 目录暴露。 ## worker 优雅退出 - 收到 TERM 后先停止认领新任务;