mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-14 12:43:45 +00:00
docs: document auth design and user isolation (#2913)
* docs: document auth design and user isolation * docs: align auth docs with current storage and reset behavior --------- Co-authored-by: greatmengqi <chenmengqi.0376@bytedance.com>
This commit is contained in:
parent
84f88b6610
commit
f734e14d8b
@ -62,7 +62,7 @@ async def _ensure_admin_user(app: FastAPI) -> None:
|
||||
|
||||
Subsequent boots (admin already exists):
|
||||
- Runs the one-time "no-auth → with-auth" orphan thread migration for
|
||||
existing LangGraph thread metadata that has no owner_id.
|
||||
existing LangGraph thread metadata that has no user_id.
|
||||
|
||||
No SQL persistence migration is needed: the four user_id columns
|
||||
(threads_meta, runs, run_events, feedback) only come into existence
|
||||
@ -177,7 +177,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
async with langgraph_runtime(app):
|
||||
logger.info("LangGraph runtime initialised")
|
||||
|
||||
# Ensure admin user exists (auto-create on first boot)
|
||||
# Check admin bootstrap state and migrate orphan threads after admin exists.
|
||||
# Must run AFTER langgraph_runtime so app.state.store is available for thread migration
|
||||
await _ensure_admin_user(app)
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ class User(BaseModel):
|
||||
oauth_id: str | None = Field(None, description="User ID from OAuth provider")
|
||||
|
||||
# Auth lifecycle
|
||||
needs_setup: bool = Field(default=False, description="True for auto-created admin until setup completes")
|
||||
needs_setup: bool = Field(default=False, description="True when a reset account must complete setup")
|
||||
token_version: int = Field(default=0, description="Incremented on password change to invalidate old JWTs")
|
||||
|
||||
|
||||
|
||||
@ -305,7 +305,7 @@ async def login_local(
|
||||
async def register(request: Request, response: Response, body: RegisterRequest):
|
||||
"""Register a new user account (always 'user' role).
|
||||
|
||||
Admin is auto-created on first boot. This endpoint creates regular users.
|
||||
The first admin is created explicitly through /initialize. This endpoint creates regular users.
|
||||
Auto-login by setting the session cookie.
|
||||
"""
|
||||
try:
|
||||
|
||||
@ -535,14 +535,28 @@ All APIs return errors in a consistent format:
|
||||
|
||||
## Authentication
|
||||
|
||||
Currently, DeerFlow does not implement authentication. All APIs are accessible without credentials.
|
||||
DeerFlow enforces authentication for all non-public HTTP routes. Public routes are limited to health/docs metadata and these public auth endpoints:
|
||||
|
||||
Note: This is about DeerFlow API authentication. MCP outbound connections can still use OAuth for configured HTTP/SSE MCP servers.
|
||||
- `POST /api/v1/auth/initialize` creates the first admin account when no admin exists.
|
||||
- `POST /api/v1/auth/login/local` logs in with email/password and sets an HttpOnly `access_token` cookie.
|
||||
- `POST /api/v1/auth/register` creates a regular `user` account and sets the session cookie.
|
||||
- `POST /api/v1/auth/logout` clears the session cookie.
|
||||
- `GET /api/v1/auth/setup-status` reports whether the first admin still needs to be created.
|
||||
|
||||
For production deployments, it is recommended to:
|
||||
1. Use Nginx for basic auth or OAuth integration
|
||||
2. Deploy behind a VPN or private network
|
||||
3. Implement custom authentication middleware
|
||||
The authenticated auth endpoints are:
|
||||
|
||||
- `GET /api/v1/auth/me` returns the current user.
|
||||
- `POST /api/v1/auth/change-password` changes password, optionally changes email during setup, increments `token_version`, and reissues the cookie.
|
||||
|
||||
Protected state-changing requests also require the CSRF double-submit token: send the `csrf_token` cookie value as the `X-CSRF-Token` header. Login/register/initialize/logout are bootstrap auth endpoints: they are exempt from the double-submit token but still reject hostile browser `Origin` headers.
|
||||
|
||||
User isolation is enforced from the authenticated user context:
|
||||
|
||||
- Thread metadata is scoped by `threads_meta.user_id`; search/read/write/delete APIs only expose the current user's threads.
|
||||
- Thread files live under `{base_dir}/users/{user_id}/threads/{thread_id}/user-data/` and are exposed inside the sandbox as `/mnt/user-data/`.
|
||||
- Memory and custom agents are stored under `{base_dir}/users/{user_id}/...`.
|
||||
|
||||
Note: MCP outbound connections can still use OAuth for configured HTTP/SSE MCP servers; that is separate from DeerFlow API authentication.
|
||||
|
||||
---
|
||||
|
||||
|
||||
331
backend/docs/AUTH_DESIGN.md
Normal file
331
backend/docs/AUTH_DESIGN.md
Normal file
@ -0,0 +1,331 @@
|
||||
# 用户认证与隔离设计
|
||||
|
||||
本文档描述 DeerFlow 当前内置认证模块的设计,而不是历史 RFC。它覆盖浏览器登录、API 认证、CSRF、用户隔离、首次初始化、密码重置、内部调用和升级迁移。
|
||||
|
||||
## 设计目标
|
||||
|
||||
认证模块的核心目标是把 DeerFlow 从“本地单用户工具”提升为“可多用户部署的 agent runtime”,并让用户身份贯穿 HTTP API、LangGraph-compatible runtime、文件系统、memory、自定义 agent 和反馈数据。
|
||||
|
||||
设计约束:
|
||||
|
||||
- 默认强制认证:除健康检查、文档和 auth bootstrap 端点外,HTTP 路由都必须有有效 session。
|
||||
- 服务端持有所有权:客户端 metadata 不能声明 `user_id` 或 `owner_id`。
|
||||
- 隔离默认开启:repository(仓储)、文件路径、memory、agent 配置默认按当前用户解析。
|
||||
- 旧数据可升级:无认证版本留下的 thread 可以在 admin 存在后迁移到 admin。
|
||||
- 密码不进日志:首次初始化由操作者设置密码;`reset_admin` 只写 0600 凭据文件。
|
||||
|
||||
非目标:
|
||||
|
||||
- 当前 OAuth 端点只是占位,尚未实现第三方登录。
|
||||
- 当前用户角色只有 `admin` 和 `user`,尚未实现细粒度 RBAC。
|
||||
- 当前登录限速是进程内字典,多 worker 下不是全局精确限速。
|
||||
|
||||
## 核心模型
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
classDef actor fill:#D8CFC4,stroke:#6E6259,color:#2F2A26;
|
||||
classDef api fill:#C9D7D2,stroke:#5D706A,color:#21302C;
|
||||
classDef state fill:#D7D3E8,stroke:#6B6680,color:#29263A;
|
||||
classDef data fill:#E5D2C4,stroke:#806A5B,color:#30251E;
|
||||
|
||||
Browser["Browser — access_token cookie and csrf_token cookie"]:::actor
|
||||
AuthMiddleware["AuthMiddleware — strict session gate"]:::api
|
||||
CSRFMiddleware["CSRFMiddleware — double-submit token and Origin check"]:::api
|
||||
AuthRoutes["Auth routes — initialize login register logout me change-password"]:::api
|
||||
UserContext["Current user ContextVar — request-scoped identity"]:::state
|
||||
Repositories["Repositories — AUTO resolves user_id from context"]:::state
|
||||
Files["Filesystem — users/{user_id}/threads/{thread_id}/user-data"]:::data
|
||||
Memory["Memory and agents — users/{user_id}/memory.json and agents"]:::data
|
||||
|
||||
Browser --> AuthMiddleware
|
||||
Browser --> CSRFMiddleware
|
||||
AuthMiddleware --> AuthRoutes
|
||||
AuthMiddleware --> UserContext
|
||||
UserContext --> Repositories
|
||||
UserContext --> Files
|
||||
UserContext --> Memory
|
||||
```
|
||||
|
||||
### 用户表
|
||||
|
||||
用户记录定义在 `app.gateway.auth.models.User`,持久化到 `users` 表。关键字段:
|
||||
|
||||
| 字段 | 语义 |
|
||||
|---|---|
|
||||
| `id` | 用户主键,JWT `sub` 使用该值 |
|
||||
| `email` | 唯一登录名 |
|
||||
| `password_hash` | bcrypt hash,OAuth 用户可为空 |
|
||||
| `system_role` | `admin` 或 `user` |
|
||||
| `needs_setup` | reset 后要求用户完成邮箱 / 密码设置 |
|
||||
| `token_version` | 改密码或 reset 时递增,用于废弃旧 JWT |
|
||||
|
||||
### 运行时身份
|
||||
|
||||
认证成功后,`AuthMiddleware` 把用户同时写入:
|
||||
|
||||
- `request.state.user`
|
||||
- `request.state.auth`
|
||||
- `deerflow.runtime.user_context` 的 `ContextVar`
|
||||
|
||||
`ContextVar` 是这里的核心边界。上层 Gateway 负责写入身份,下层 persistence / file path 只读取结构化的当前用户,不反向依赖 `app.gateway.auth` 具体类型。
|
||||
|
||||
可以把 repository 调用的用户参数理解成一个三态 ADT:
|
||||
|
||||
```scala
|
||||
enum UserScope:
|
||||
case AutoFromContext
|
||||
case Explicit(userId: String)
|
||||
case BypassForMigration
|
||||
```
|
||||
|
||||
对应 Python 实现是 `AUTO | str | None`:
|
||||
|
||||
- `AUTO`:从 `ContextVar` 解析当前用户;没有上下文则抛错。
|
||||
- `str`:显式指定用户,主要用于测试或管理脚本。
|
||||
- `None`:跳过用户过滤,只允许迁移脚本或 admin CLI 使用。
|
||||
|
||||
## 登录与初始化流程
|
||||
|
||||
### 首次初始化
|
||||
|
||||
首次启动时,如果没有 admin,服务不会自动创建账号,只记录日志提示访问 `/setup`。
|
||||
|
||||
流程:
|
||||
|
||||
1. 用户访问 `/setup`。
|
||||
2. 前端调用 `GET /api/v1/auth/setup-status`。
|
||||
3. 如果返回 `{"needs_setup": true}`,前端展示创建 admin 表单。
|
||||
4. 表单提交 `POST /api/v1/auth/initialize`。
|
||||
5. 服务端确认当前没有 admin,创建 `system_role="admin"`、`needs_setup=false` 的用户。
|
||||
6. 服务端设置 `access_token` HttpOnly cookie,用户进入 workspace。
|
||||
|
||||
`/api/v1/auth/initialize` 只在没有 admin 时可用。并发初始化由数据库唯一约束兜底,失败方返回 409。
|
||||
|
||||
### 普通登录
|
||||
|
||||
`POST /api/v1/auth/login/local` 使用 `OAuth2PasswordRequestForm`:
|
||||
|
||||
- `username` 是邮箱。
|
||||
- `password` 是密码。
|
||||
- 成功后签发 JWT,放入 `access_token` HttpOnly cookie。
|
||||
- 响应体只返回 `expires_in` 和 `needs_setup`,不返回 token。
|
||||
|
||||
登录失败会按客户端 IP 计数。IP 解析只在 TCP peer 属于 `AUTH_TRUSTED_PROXIES` 时信任 `X-Real-IP`,不使用 `X-Forwarded-For`。
|
||||
|
||||
### 注册
|
||||
|
||||
`POST /api/v1/auth/register` 创建普通 `user`,并自动登录。
|
||||
|
||||
当前实现允许在没有 admin 时注册普通用户,但 `setup-status` 仍会返回 `needs_setup=true`,因为 admin 仍不存在。这是当前产品策略边界:如果后续要求“必须先初始化 admin 才能注册普通用户”,需要在 `/register` 增加 admin-exists gate。
|
||||
|
||||
### 改密码与 reset setup
|
||||
|
||||
`POST /api/v1/auth/change-password` 需要当前密码和新密码:
|
||||
|
||||
- 校验当前密码。
|
||||
- 更新 bcrypt hash。
|
||||
- `token_version += 1`,使旧 JWT 立即失效。
|
||||
- 重新签发 cookie。
|
||||
- 如果 `needs_setup=true` 且传了 `new_email`,则更新邮箱并清除 `needs_setup`。
|
||||
|
||||
`python -m app.gateway.auth.reset_admin` 会:
|
||||
|
||||
- 找到 admin 或指定邮箱用户。
|
||||
- 生成随机密码。
|
||||
- 更新密码 hash。
|
||||
- `token_version += 1`。
|
||||
- 设置 `needs_setup=true`。
|
||||
- 写入 `.deer-flow/admin_initial_credentials.txt`,权限 `0600`。
|
||||
|
||||
命令行只输出凭据文件路径,不输出明文密码。
|
||||
|
||||
## HTTP 认证边界
|
||||
|
||||
`AuthMiddleware` 是 fail-closed(默认拒绝)的全局认证门。
|
||||
|
||||
公开路径:
|
||||
|
||||
- `/health`
|
||||
- `/docs`
|
||||
- `/redoc`
|
||||
- `/openapi.json`
|
||||
- `/api/v1/auth/login/local`
|
||||
- `/api/v1/auth/register`
|
||||
- `/api/v1/auth/logout`
|
||||
- `/api/v1/auth/setup-status`
|
||||
- `/api/v1/auth/initialize`
|
||||
|
||||
其余路径都要求有效 `access_token` cookie。存在 cookie 但 JWT 无效、过期、用户不存在或 `token_version` 不匹配时,直接返回 401,而不是让请求穿透到业务路由。
|
||||
|
||||
路由级别的 owner check 由 `require_permission(..., owner_check=True)` 完成:
|
||||
|
||||
- 读类请求允许旧的未追踪 legacy thread 兼容读取。
|
||||
- 写 / 删除类请求使用 `require_existing=True`,要求 thread row 存在且属于当前用户,避免删除后缺 row 导致其他用户误通过。
|
||||
|
||||
## CSRF 设计
|
||||
|
||||
DeerFlow 使用 Double Submit Cookie:
|
||||
|
||||
- 服务端设置 `csrf_token` cookie。
|
||||
- 前端 state-changing 请求发送同值 `X-CSRF-Token` header。
|
||||
- 服务端用 `secrets.compare_digest` 比较 cookie/header。
|
||||
|
||||
需要 CSRF 的方法:
|
||||
|
||||
- `POST`
|
||||
- `PUT`
|
||||
- `DELETE`
|
||||
- `PATCH`
|
||||
|
||||
auth bootstrap 端点(login/register/initialize/logout)不要求 double-submit token,因为首次调用时浏览器还没有 token;但这些端点会校验 browser `Origin`,拒绝 hostile Origin,避免 login CSRF / session fixation。
|
||||
|
||||
## 用户隔离
|
||||
|
||||
### Thread metadata
|
||||
|
||||
Thread metadata 存在 `threads_meta`,关键隔离字段是 `user_id`。
|
||||
|
||||
创建 thread 时:
|
||||
|
||||
- 客户端传入的 `metadata.user_id` 和 `metadata.owner_id` 会被剥离。
|
||||
- `ThreadMetaRepository.create(..., user_id=AUTO)` 从 `ContextVar` 解析真实用户。
|
||||
- `/api/threads/search` 默认只返回当前用户的 thread。
|
||||
|
||||
读取 / 修改 / 删除时:
|
||||
|
||||
- `get()` 默认按当前用户过滤。
|
||||
- `check_access()` 用于路由 owner check。
|
||||
- 对其他用户的 thread 返回 404,避免泄露资源存在性。
|
||||
|
||||
### 文件系统
|
||||
|
||||
当前线程文件布局:
|
||||
|
||||
```text
|
||||
{base_dir}/users/{user_id}/threads/{thread_id}/user-data/
|
||||
├── workspace/
|
||||
├── uploads/
|
||||
└── outputs/
|
||||
```
|
||||
|
||||
agent 在 sandbox 内看到统一虚拟路径:
|
||||
|
||||
```text
|
||||
/mnt/user-data/workspace
|
||||
/mnt/user-data/uploads
|
||||
/mnt/user-data/outputs
|
||||
```
|
||||
|
||||
`ThreadDataMiddleware` 使用 `get_effective_user_id()` 解析当前用户并生成线程路径。没有认证上下文时会落到 `default` 用户桶,主要用于内部调用、嵌入式 client 或无 HTTP 的本地执行路径。
|
||||
|
||||
### Memory
|
||||
|
||||
默认 memory 存储:
|
||||
|
||||
```text
|
||||
{base_dir}/users/{user_id}/memory.json
|
||||
{base_dir}/users/{user_id}/agents/{agent_name}/memory.json
|
||||
```
|
||||
|
||||
有用户上下文时,空或相对 `memory.storage_path` 都使用上述 per-user 默认路径;只有绝对 `memory.storage_path` 会视为显式 opt-out(退出) per-user isolation,所有用户共享该路径。无用户上下文的 legacy 路径仍会把相对 `storage_path` 解析到 `Paths.base_dir` 下。
|
||||
|
||||
### 自定义 agent
|
||||
|
||||
用户自定义 agent 写入:
|
||||
|
||||
```text
|
||||
{base_dir}/users/{user_id}/agents/{agent_name}/
|
||||
├── config.yaml
|
||||
├── SOUL.md
|
||||
└── memory.json
|
||||
```
|
||||
|
||||
旧布局 `{base_dir}/agents/{agent_name}/` 只作为只读兼容回退。更新或删除旧共享 agent 会要求先运行迁移脚本。
|
||||
|
||||
## 内部调用与 IM 渠道
|
||||
|
||||
IM channel worker 不是浏览器用户,不持有浏览器 cookie。它们通过 Gateway 内部认证:
|
||||
|
||||
- 请求带 `X-DeerFlow-Internal-Token`。
|
||||
- 同时带匹配的 CSRF cookie/header。
|
||||
- 服务端识别为内部用户,`id="default"`、`system_role="internal"`。
|
||||
|
||||
这意味着 channel 产生的数据默认进入 `default` 用户桶。这个选择适合“平台级 bot 身份”,但不是“每个 IM 用户单独隔离”。如果后续要做到外部 IM 用户隔离,需要把外部 platform user 映射到 DeerFlow user,并让 channel manager 设置对应的 scoped identity。
|
||||
|
||||
## LangGraph-compatible 认证
|
||||
|
||||
Gateway 内嵌 runtime 路径由 `AuthMiddleware` 和 `CSRFMiddleware` 保护。
|
||||
|
||||
仓库仍保留 `app.gateway.langgraph_auth`,用于 LangGraph Server 直连模式:
|
||||
|
||||
- `@auth.authenticate` 校验 JWT cookie、CSRF、用户存在性和 `token_version`。
|
||||
- `@auth.on` 在写入 metadata 时注入 `user_id`,并在读路径返回 `{"user_id": current_user}` 过滤条件。
|
||||
|
||||
这保证 Gateway 路由和 LangGraph-compatible 直连模式使用同一 JWT 语义。
|
||||
|
||||
## 升级与迁移
|
||||
|
||||
从无认证版本升级时,可能存在没有 `user_id` 的历史 thread。
|
||||
|
||||
当前策略:
|
||||
|
||||
1. 首次启动如果没有 admin,只提示访问 `/setup`,不迁移。
|
||||
2. 操作者创建 admin。
|
||||
3. 后续启动时,`_ensure_admin_user()` 找到 admin,并把 LangGraph store 中缺少 `metadata.user_id` 的 thread 迁移到 admin。
|
||||
|
||||
文件系统旧布局迁移由脚本处理:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
PYTHONPATH=. python scripts/migrate_user_isolation.py --dry-run
|
||||
PYTHONPATH=. python scripts/migrate_user_isolation.py --user-id <target-user-id>
|
||||
```
|
||||
|
||||
迁移脚本覆盖 legacy `memory.json`、`threads/` 和 `agents/` 到 per-user layout。
|
||||
|
||||
## 安全不变量
|
||||
|
||||
必须长期保持的不变量:
|
||||
|
||||
- JWT 只在 HttpOnly cookie 中传输,不出现在响应 JSON。
|
||||
- 任何非 public HTTP 路由都不能只靠“cookie 存在”放行,必须严格验证 JWT。
|
||||
- `token_version` 不匹配必须拒绝,保证改密码 / reset 后旧 session 失效。
|
||||
- 客户端 metadata 中的 `user_id` / `owner_id` 必须剥离。
|
||||
- repository 默认 `AUTO` 必须从当前用户上下文解析,不能静默退化成全局查询。
|
||||
- 只有迁移脚本和 admin CLI 可以显式传 `user_id=None` 绕过隔离。
|
||||
- 本地文件路径必须通过 `Paths` 和 sandbox path validation 解析,不能拼接未校验的用户输入。
|
||||
- 捕获认证、迁移、后台任务异常必须记录日志;不能空 catch。
|
||||
|
||||
## 已知边界
|
||||
|
||||
| 边界 | 当前行为 | 后续方向 |
|
||||
|---|---|---|
|
||||
| 无 admin 时注册普通用户 | 允许注册普通 `user` | 如产品要求先初始化 admin,给 `/register` 加 gate |
|
||||
| 登录限速 | 进程内 dict,单 worker 精确,多 worker 近似 | Redis / DB-backed rate limiter |
|
||||
| OAuth | 端点占位,未实现 | 接入 provider 并统一 `token_version` / role 语义 |
|
||||
| IM 用户隔离 | channel 使用 `default` 内部用户 | 建立外部用户到 DeerFlow user 的映射 |
|
||||
| 绝对 memory path | 显式共享 memory | UI / docs 明确提示 opt-out 风险 |
|
||||
|
||||
## 相关文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| `app/gateway/auth_middleware.py` | 全局认证门、JWT 严格验证、写入 user context |
|
||||
| `app/gateway/csrf_middleware.py` | CSRF double-submit 和 auth Origin 校验 |
|
||||
| `app/gateway/routers/auth.py` | initialize/login/register/logout/me/change-password |
|
||||
| `app/gateway/auth/jwt.py` | JWT 创建与解析 |
|
||||
| `app/gateway/auth/reset_admin.py` | 密码 reset CLI |
|
||||
| `app/gateway/auth/credential_file.py` | 0600 凭据文件写入 |
|
||||
| `app/gateway/authz.py` | 路由权限与 owner check |
|
||||
| `deerflow/runtime/user_context.py` | 当前用户 ContextVar 与 `AUTO` sentinel |
|
||||
| `deerflow/persistence/thread_meta/` | thread metadata owner filter |
|
||||
| `deerflow/config/paths.py` | per-user filesystem layout |
|
||||
| `deerflow/agents/middlewares/thread_data_middleware.py` | run 时解析用户线程目录 |
|
||||
| `deerflow/agents/memory/storage.py` | per-user memory storage |
|
||||
| `deerflow/config/agents_config.py` | per-user custom agents |
|
||||
| `app/channels/manager.py` | IM channel 内部认证调用 |
|
||||
| `scripts/migrate_user_isolation.py` | legacy 数据迁移到 per-user layout |
|
||||
| `.deer-flow/data/deerflow.db` | 统一 SQLite 数据库,包含 users / threads_meta / runs / feedback 等表 |
|
||||
| `.deer-flow/users/{user_id}/agents/{agent_name}/` | 用户自定义 agent 配置、SOUL 和 agent memory |
|
||||
| `.deer-flow/admin_initial_credentials.txt` | `reset_admin` 生成的新凭据文件(0600,读完应删除) |
|
||||
@ -24,11 +24,11 @@ All other test plan sections were executed against either:
|
||||
|
||||
| Case | Title | What it covers | Why not run |
|
||||
|---|---|---|---|
|
||||
| TC-DOCKER-01 | `users.db` volume persistence | Verify the `DEER_FLOW_HOME` bind mount survives container restart | needs `docker compose up` |
|
||||
| TC-DOCKER-01 | `deerflow.db` volume persistence | Verify the `DEER_FLOW_HOME` bind mount survives container restart | needs `docker compose up` |
|
||||
| TC-DOCKER-02 | Session persistence across container restart | `AUTH_JWT_SECRET` env var keeps cookies valid after `docker compose down && up` | needs `docker compose down/up` |
|
||||
| TC-DOCKER-03 | Per-worker rate limiter divergence | Confirms in-process `_login_attempts` dict doesn't share state across `gunicorn` workers (4 by default in the compose file); known limitation, documented | needs multi-worker container |
|
||||
| TC-DOCKER-04 | IM channels skip AuthMiddleware | Verify Feishu/Slack/Telegram dispatchers run in-container against `http://langgraph:2024` without going through nginx | needs `docker logs` |
|
||||
| TC-DOCKER-05 | Admin credentials surfacing | **Updated post-simplify** — was "log scrape", now "0600 credential file in `DEER_FLOW_HOME`". The file-based behavior is already validated by TC-1.1 + TC-UPG-13 on sg_dev (non-Docker), so the only Docker-specific gap is verifying the volume mount carries the file out to the host | needs container + host volume |
|
||||
| TC-DOCKER-04 | IM channels use internal Gateway auth | Verify Feishu/Slack/Telegram dispatchers attach the process-local internal auth header plus CSRF cookie/header when calling Gateway-compatible LangGraph APIs | needs `docker logs` |
|
||||
| TC-DOCKER-05 | Reset credentials surfacing | `reset_admin` writes a 0600 credential file in `DEER_FLOW_HOME` instead of logging plaintext. The file-based behavior is validated by non-Docker reset tests, so the only Docker-specific gap is verifying the volume mount carries the file out to the host | needs container + host volume |
|
||||
| TC-DOCKER-06 | Gateway-mode Docker deploy | `./scripts/deploy.sh --gateway` produces a 3-container topology (no `langgraph` container); same auth flow as standard mode | needs `docker compose --profile gateway` |
|
||||
|
||||
## Coverage already provided by non-Docker tests
|
||||
@ -41,8 +41,8 @@ the test cases that ran on sg_dev or local:
|
||||
| TC-DOCKER-01 (volume persistence) | TC-REENT-01 on sg_dev (admin row survives gateway restart) — same SQLite file, just no container layer between |
|
||||
| TC-DOCKER-02 (session persistence) | TC-API-02/03/06 (cookie roundtrip), plus TC-REENT-04 (multi-cookie) — JWT verification is process-state-free, container restart is equivalent to `pkill uvicorn && uv run uvicorn` |
|
||||
| TC-DOCKER-03 (per-worker rate limit) | TC-GW-04 + TC-REENT-09 (single-worker rate limit + 5min expiry). The cross-worker divergence is an architectural property of the in-memory dict; no auth code path differs |
|
||||
| TC-DOCKER-04 (IM channels skip auth) | Code-level only: `app/channels/manager.py` uses `langgraph_sdk` directly with no cookie handling. The langgraph_auth handler is bypassed by going through SDK, not HTTP |
|
||||
| TC-DOCKER-05 (credential surfacing) | TC-1.1 on sg_dev (file at `~/deer-flow/backend/.deer-flow/admin_initial_credentials.txt`, mode 0600, password 22 chars) — the only Docker-unique step is whether the bind mount projects this path onto the host, which is a `docker compose` config check, not a runtime behavior change |
|
||||
| TC-DOCKER-04 (IM channels use internal auth) | Code-level: `app/channels/manager.py` creates the `langgraph_sdk` client with `create_internal_auth_headers()` plus CSRF cookie/header, so channel workers do not rely on browser cookies |
|
||||
| TC-DOCKER-05 (credential surfacing) | `reset_admin` writes `.deer-flow/admin_initial_credentials.txt` with mode 0600 and logs only the path — the only Docker-unique step is whether the bind mount projects this path onto the host, which is a `docker compose` config check, not a runtime behavior change |
|
||||
| TC-DOCKER-06 (gateway-mode container) | Section 七 7.2 covered by TC-GW-01..05 + Section 二 (gateway-mode auth flow on sg_dev) — same Gateway code, container is just a packaging change |
|
||||
|
||||
## Reproduction steps when Docker becomes available
|
||||
@ -72,6 +72,6 @@ Then run TC-DOCKER-01..06 from the test plan as written.
|
||||
about *container packaging* details (bind mounts, multi-worker, log
|
||||
collection), not about whether the auth code paths work.
|
||||
- **TC-DOCKER-05 was updated in place** in `AUTH_TEST_PLAN.md` to reflect
|
||||
the post-simplify reality (credentials file → 0600 file, no log leak).
|
||||
the current reset flow (`reset_admin` → 0600 credentials file, no log leak).
|
||||
The old "grep 'Password:' in docker logs" expectation would have failed
|
||||
silently and given a false sense of coverage.
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
```bash
|
||||
# 清除已有数据
|
||||
rm -f backend/.deer-flow/users.db
|
||||
rm -f backend/.deer-flow/data/deerflow.db
|
||||
|
||||
# 选择模式启动
|
||||
make dev # 标准模式
|
||||
@ -28,10 +28,11 @@ make dev-pro # Gateway 模式
|
||||
```
|
||||
|
||||
**验证点:**
|
||||
- [ ] 控制台输出 admin 邮箱和随机密码
|
||||
- [ ] 密码格式为 `secrets.token_urlsafe(16)` 的 22 字符字符串
|
||||
- [ ] 邮箱为 `admin@deerflow.dev`
|
||||
- [ ] 提示 `Change it after login: Settings -> Account`
|
||||
- [ ] 控制台不输出 admin 邮箱或明文密码
|
||||
- [ ] 控制台提示 `First boot detected — no admin account exists.`
|
||||
- [ ] 控制台提示访问 `/setup` 完成 admin 创建
|
||||
- [ ] `GET /api/v1/auth/setup-status` 返回 `{"needs_setup": true}`
|
||||
- [ ] 前端访问 `/login` 会跳转 `/setup`
|
||||
|
||||
### 1.2 非首次启动
|
||||
|
||||
@ -42,7 +43,8 @@ make dev
|
||||
|
||||
**验证点:**
|
||||
- [ ] 控制台不输出密码
|
||||
- [ ] 如果 admin 仍 `needs_setup=True`,控制台有 warning 提示
|
||||
- [ ] `GET /api/v1/auth/setup-status` 返回 `{"needs_setup": false}`
|
||||
- [ ] 已登录用户如果 `needs_setup=True`,访问 workspace 会被引导到 `/setup` 完成改邮箱 / 改密码流程
|
||||
|
||||
### 1.3 环境变量配置
|
||||
|
||||
@ -76,19 +78,22 @@ make dev
|
||||
curl -s $BASE/api/v1/auth/setup-status | jq .
|
||||
```
|
||||
|
||||
**预期:** 返回 `{"needs_setup": false}`(admin 在启动时已自动创建,`count_users() > 0`)。仅在启动完成前的极短窗口内可能返回 `true`。
|
||||
**预期:**
|
||||
- 干净数据库且尚未初始化 admin:返回 `{"needs_setup": true}`
|
||||
- 已存在 admin:返回 `{"needs_setup": false}`
|
||||
|
||||
#### TC-API-02: Admin 首次登录
|
||||
#### TC-API-02: 首次初始化 Admin
|
||||
|
||||
```bash
|
||||
curl -s -X POST $BASE/api/v1/auth/login/local \
|
||||
-d "username=admin@deerflow.dev&password=<控制台密码>" \
|
||||
curl -s -X POST $BASE/api/v1/auth/initialize \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"admin@example.com","password":"AdminPass1!"}' \
|
||||
-c cookies.txt | jq .
|
||||
```
|
||||
|
||||
**预期:**
|
||||
- 状态码 200
|
||||
- Body: `{"expires_in": 604800, "needs_setup": true}`
|
||||
- 状态码 201
|
||||
- Body: `{"id": "...", "email": "admin@example.com", "system_role": "admin", "needs_setup": false}`
|
||||
- `cookies.txt` 包含 `access_token`(HttpOnly)和 `csrf_token`(非 HttpOnly)
|
||||
|
||||
#### TC-API-03: 获取当前用户
|
||||
@ -97,9 +102,9 @@ curl -s -X POST $BASE/api/v1/auth/login/local \
|
||||
curl -s $BASE/api/v1/auth/me -b cookies.txt | jq .
|
||||
```
|
||||
|
||||
**预期:** `{"id": "...", "email": "admin@deerflow.dev", "system_role": "admin", "needs_setup": true}`
|
||||
**预期:** `{"id": "...", "email": "admin@example.com", "system_role": "admin", "needs_setup": false}`
|
||||
|
||||
#### TC-API-04: Setup 流程(改邮箱 + 改密码)
|
||||
#### TC-API-04: 改密码流程
|
||||
|
||||
```bash
|
||||
CSRF=$(grep csrf_token cookies.txt | awk '{print $NF}')
|
||||
@ -107,13 +112,36 @@ curl -s -X POST $BASE/api/v1/auth/change-password \
|
||||
-b cookies.txt \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-CSRF-Token: $CSRF" \
|
||||
-d '{"current_password":"<控制台密码>","new_password":"NewPass123!","new_email":"admin@example.com"}' | jq .
|
||||
-d '{"current_password":"AdminPass1!","new_password":"NewPass123!"}' | jq .
|
||||
```
|
||||
|
||||
**预期:**
|
||||
- 状态码 200
|
||||
- `{"message": "Password changed successfully"}`
|
||||
- 再调 `/auth/me` 邮箱变为 `admin@example.com`,`needs_setup` 变为 `false`
|
||||
- 再调 `/auth/me` 仍为 `admin@example.com`,`needs_setup` 仍为 `false`
|
||||
|
||||
#### TC-API-04a: reset_admin 后的 Setup 流程(改邮箱 + 改密码)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python -m app.gateway.auth.reset_admin --email admin@example.com
|
||||
# 从 .deer-flow/admin_initial_credentials.txt 读取 reset 后密码
|
||||
|
||||
curl -s -X POST $BASE/api/v1/auth/login/local \
|
||||
-d "username=admin@example.com&password=<凭据文件密码>" \
|
||||
-c cookies.txt | jq .
|
||||
|
||||
CSRF=$(grep csrf_token cookies.txt | awk '{print $NF}')
|
||||
curl -s -X POST $BASE/api/v1/auth/change-password \
|
||||
-b cookies.txt \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-CSRF-Token: $CSRF" \
|
||||
-d '{"current_password":"<凭据文件密码>","new_password":"AdminPass2!","new_email":"admin2@example.com"}' | jq .
|
||||
```
|
||||
|
||||
**预期:**
|
||||
- 登录返回 `{"expires_in": 604800, "needs_setup": true}`
|
||||
- `change-password` 后 `/auth/me` 邮箱变为 `admin2@example.com`,`needs_setup` 变为 `false`
|
||||
|
||||
#### TC-API-05: 普通用户注册
|
||||
|
||||
@ -493,7 +521,7 @@ curl -s -X POST $BASE/api/v1/auth/register \
|
||||
|
||||
```bash
|
||||
# 检查数据库
|
||||
sqlite3 backend/.deer-flow/users.db "SELECT email, password_hash FROM users LIMIT 3;"
|
||||
sqlite3 backend/.deer-flow/data/deerflow.db "SELECT email, password_hash FROM users LIMIT 3;"
|
||||
```
|
||||
|
||||
**预期:** `password_hash` 以 `$2b$` 开头(bcrypt 格式)
|
||||
@ -506,24 +534,25 @@ sqlite3 backend/.deer-flow/users.db "SELECT email, password_hash FROM users LIMI
|
||||
|
||||
### 4.1 首次登录流程
|
||||
|
||||
#### TC-UI-01: 访问首页跳转登录
|
||||
#### TC-UI-01: 无 admin 时访问 workspace 跳转 setup
|
||||
|
||||
1. 打开 `http://localhost:2026/workspace`
|
||||
2. **预期:** 自动跳转到 `/login`
|
||||
2. **预期:** 自动跳转到 `/setup`
|
||||
|
||||
#### TC-UI-02: Login 页面
|
||||
#### TC-UI-02: Setup 页面创建 admin
|
||||
|
||||
1. 输入 admin 邮箱和控制台密码
|
||||
2. 点击 Login
|
||||
3. **预期:** 跳转到 `/setup`(因为 `needs_setup=true`)
|
||||
|
||||
#### TC-UI-03: Setup 页面
|
||||
|
||||
1. 输入新邮箱、控制台密码(current)、新密码、确认密码
|
||||
2. 点击 Complete Setup
|
||||
1. 输入 admin 邮箱、密码、确认密码
|
||||
2. 点击 Create Admin Account
|
||||
3. **预期:** 跳转到 `/workspace`
|
||||
4. 刷新页面不跳回 `/setup`
|
||||
|
||||
#### TC-UI-03: 已初始化后 Login 页面
|
||||
|
||||
1. 退出登录后访问 `/login`
|
||||
2. 输入 admin 邮箱和密码
|
||||
3. 点击 Login
|
||||
4. **预期:** 跳转到 `/workspace`
|
||||
|
||||
#### TC-UI-04: Setup 密码不匹配
|
||||
|
||||
1. 新密码和确认密码不一致
|
||||
@ -602,7 +631,7 @@ sqlite3 backend/.deer-flow/users.db "SELECT email, password_hash FROM users LIMI
|
||||
#### TC-UI-15: reset_admin 后重新登录
|
||||
|
||||
1. 执行 `cd backend && python -m app.gateway.auth.reset_admin`
|
||||
2. 使用新密码登录
|
||||
2. 从 `.deer-flow/admin_initial_credentials.txt` 读取新密码并登录
|
||||
3. **预期:** 跳转到 `/setup` 页面(`needs_setup` 被重置为 true)
|
||||
4. 旧 session 已失效
|
||||
|
||||
@ -645,18 +674,28 @@ make install
|
||||
make dev
|
||||
```
|
||||
|
||||
#### TC-UPG-01: 首次启动创建 admin
|
||||
#### TC-UPG-01: 首次启动等待 admin 初始化
|
||||
|
||||
**预期:**
|
||||
- [ ] 控制台输出 admin 邮箱(`admin@deerflow.dev`)和随机密码
|
||||
- [ ] 控制台不输出 admin 邮箱或随机密码
|
||||
- [ ] 访问 `/setup` 可创建第一个 admin
|
||||
- [ ] 无报错,正常启动
|
||||
|
||||
#### TC-UPG-02: 旧 Thread 迁移到 admin
|
||||
|
||||
```bash
|
||||
# 创建第一个 admin
|
||||
curl -s -X POST http://localhost:2026/api/v1/auth/initialize \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"admin@example.com","password":"AdminPass1!"}' \
|
||||
-c cookies.txt
|
||||
|
||||
# 重启一次:启动迁移只在已有 admin 的启动路径执行
|
||||
make stop && make dev
|
||||
|
||||
# 登录 admin
|
||||
curl -s -X POST http://localhost:2026/api/v1/auth/login/local \
|
||||
-d "username=admin@deerflow.dev&password=<控制台密码>" \
|
||||
-d "username=admin@example.com&password=AdminPass1!" \
|
||||
-c cookies.txt
|
||||
|
||||
# 查看 thread 列表
|
||||
@ -670,8 +709,8 @@ curl -s -X POST http://localhost:2026/api/threads/search \
|
||||
|
||||
**预期:**
|
||||
- [ ] 返回的 thread 数量 ≥ 旧版创建的数量
|
||||
- [ ] 控制台日志有 `Migrated N orphaned thread(s) to admin`
|
||||
- [ ] 每个 thread 的 `metadata.owner_id` 都已被设为 admin 的 ID
|
||||
- [ ] 控制台日志有 `Migrated N orphan LangGraph thread(s) to admin`
|
||||
- [ ] 旧 thread 只对 admin 可见
|
||||
|
||||
#### TC-UPG-03: 旧 Thread 内容完整
|
||||
|
||||
@ -683,7 +722,7 @@ curl -s http://localhost:2026/api/threads/<old-thread-id> \
|
||||
|
||||
**预期:**
|
||||
- [ ] `metadata.title` 保留原值(如 `old-thread-1`)
|
||||
- [ ] `metadata.owner_id` 已填充
|
||||
- [ ] 响应不回显服务端保留的 `user_id` / `owner_id`
|
||||
|
||||
#### TC-UPG-04: 新用户看不到旧 Thread
|
||||
|
||||
@ -706,18 +745,19 @@ curl -s -X POST http://localhost:2026/api/threads/search \
|
||||
|
||||
### 5.3 数据库 Schema 兼容
|
||||
|
||||
#### TC-UPG-05: 无 users.db 时自动创建
|
||||
#### TC-UPG-05: 无 deerflow.db 时创建 schema 但不创建默认用户
|
||||
|
||||
```bash
|
||||
ls -la backend/.deer-flow/users.db
|
||||
ls -la backend/.deer-flow/data/deerflow.db
|
||||
sqlite3 backend/.deer-flow/data/deerflow.db "SELECT COUNT(*) FROM users;"
|
||||
```
|
||||
|
||||
**预期:** 文件存在,`sqlite3` 可查到 `users` 表含 `needs_setup`、`token_version` 列
|
||||
**预期:** 文件存在,`sqlite3` 可查到 `users` 表含 `needs_setup`、`token_version` 列;未调用 `/initialize` 前用户数为 0
|
||||
|
||||
#### TC-UPG-06: users.db WAL 模式
|
||||
#### TC-UPG-06: deerflow.db WAL 模式
|
||||
|
||||
```bash
|
||||
sqlite3 backend/.deer-flow/users.db "PRAGMA journal_mode;"
|
||||
sqlite3 backend/.deer-flow/data/deerflow.db "PRAGMA journal_mode;"
|
||||
```
|
||||
|
||||
**预期:** 返回 `wal`
|
||||
@ -768,9 +808,9 @@ make dev
|
||||
```
|
||||
|
||||
**预期:**
|
||||
- [ ] 服务正常启动(忽略 `users.db`,无 auth 相关代码不报错)
|
||||
- [ ] 服务正常启动(忽略 `deerflow.db`,无 auth 相关代码不报错)
|
||||
- [ ] 旧对话数据仍然可访问
|
||||
- [ ] `users.db` 文件残留但不影响运行
|
||||
- [ ] `deerflow.db` 文件残留但不影响运行
|
||||
|
||||
#### TC-UPG-12: 再次升级到 auth 分支
|
||||
|
||||
@ -781,51 +821,47 @@ make dev
|
||||
```
|
||||
|
||||
**预期:**
|
||||
- [ ] 识别已有 `users.db`,不重新创建 admin
|
||||
- [ ] 旧的 admin 账号仍可登录(如果回退期间未删 `users.db`)
|
||||
- [ ] 识别已有 `deerflow.db`,不重新创建 admin
|
||||
- [ ] 旧的 admin 账号仍可登录(如果回退期间未删 `deerflow.db`)
|
||||
|
||||
### 5.7 休眠 Admin(初始密码未使用/未更改)
|
||||
### 5.7 Admin 初始化与 reset_admin
|
||||
|
||||
> 首次启动生成 admin + 随机密码,但运维未登录、未改密码。
|
||||
> 密码只在首次启动的控制台闪过一次,后续启动不再显示。
|
||||
> 首次启动不生成默认 admin,也不在日志输出密码。忘记密码时走 `reset_admin`,新密码写入 0600 凭据文件。
|
||||
|
||||
#### TC-UPG-13: 重启后自动重置密码并打印
|
||||
#### TC-UPG-13: 未初始化 admin 时重启不创建默认账号
|
||||
|
||||
```bash
|
||||
# 首次启动,记录密码
|
||||
rm -f backend/.deer-flow/users.db
|
||||
rm -f backend/.deer-flow/data/deerflow.db
|
||||
make dev
|
||||
# 控制台输出密码 P0,不登录
|
||||
make stop
|
||||
|
||||
# 隔了几天,再次启动
|
||||
make dev
|
||||
# 控制台输出新密码 P1
|
||||
curl -s $BASE/api/v1/auth/setup-status | jq .
|
||||
```
|
||||
|
||||
**预期:**
|
||||
- [ ] 控制台输出 `Admin account setup incomplete — password reset`
|
||||
- [ ] 输出新密码 P1(P0 已失效)
|
||||
- [ ] 用 P1 可以登录,P0 不可以
|
||||
- [ ] 登录后 `needs_setup=true`,跳转 `/setup`
|
||||
- [ ] `token_version` 递增(旧 session 如有也失效)
|
||||
- [ ] 控制台不输出密码
|
||||
- [ ] `setup-status` 仍为 `{"needs_setup": true}`
|
||||
- [ ] 访问 `/setup` 仍可创建第一个 admin
|
||||
|
||||
#### TC-UPG-14: 密码丢失 — 无需 CLI,重启即可
|
||||
#### TC-UPG-14: 密码丢失 — reset_admin 写入凭据文件
|
||||
|
||||
```bash
|
||||
# 忘记了控制台密码 → 直接重启服务
|
||||
make stop && make dev
|
||||
# 控制台自动输出新密码
|
||||
python -m app.gateway.auth.reset_admin --email admin@example.com
|
||||
ls -la backend/.deer-flow/admin_initial_credentials.txt
|
||||
cat backend/.deer-flow/admin_initial_credentials.txt
|
||||
```
|
||||
|
||||
**预期:**
|
||||
- [ ] 无需 `reset_admin`,重启服务即可拿到新密码
|
||||
- [ ] `reset_admin` CLI 仍然可用作手动备选方案
|
||||
- [ ] 命令行只输出凭据文件路径,不输出明文密码
|
||||
- [ ] 凭据文件权限为 `0600`
|
||||
- [ ] 凭据文件包含 email + password 行
|
||||
- [ ] 该用户下次登录返回 `needs_setup=true`
|
||||
|
||||
#### TC-UPG-15: 休眠 admin 期间普通用户注册
|
||||
#### TC-UPG-15: 未初始化 admin 期间普通用户注册策略边界
|
||||
|
||||
```bash
|
||||
# admin 存在但从未登录,普通用户先注册
|
||||
# admin 尚不存在,普通用户尝试注册
|
||||
curl -s -X POST $BASE/api/v1/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"earlybird@example.com","password":"EarlyPass1!"}' \
|
||||
@ -833,11 +869,11 @@ curl -s -X POST $BASE/api/v1/auth/register \
|
||||
```
|
||||
|
||||
**预期:**
|
||||
- [ ] 注册成功(201),角色为 `user`
|
||||
- [ ] 无法提权为 admin
|
||||
- [ ] 普通用户的数据与 admin 隔离
|
||||
- [ ] 当前代码允许注册普通用户并自动登录(201,角色为 `user`)
|
||||
- [ ] 但 `setup-status` 仍为 `{"needs_setup": true}`,因为 admin 仍不存在
|
||||
- [ ] 这是一个产品策略边界:若要求“必须先有 admin”,需要在 `/register` 增加 admin-exists gate
|
||||
|
||||
#### TC-UPG-16: 休眠 admin 不影响后续操作
|
||||
#### TC-UPG-16: 普通用户数据与后续 admin 隔离
|
||||
|
||||
```bash
|
||||
# 普通用户正常创建 thread、发消息
|
||||
@ -849,14 +885,13 @@ curl -s -X POST $BASE/api/threads \
|
||||
-d '{"metadata":{}}' | jq .thread_id
|
||||
```
|
||||
|
||||
**预期:** 正常创建,不受休眠 admin 影响
|
||||
**预期:** 普通用户正常创建 thread;后续 admin 创建后,搜索不到该普通用户 thread
|
||||
|
||||
#### TC-UPG-17: 休眠 admin 最终完成 Setup
|
||||
#### TC-UPG-17: reset_admin 后完成 Setup
|
||||
|
||||
```bash
|
||||
# 运维终于登录
|
||||
curl -s -X POST $BASE/api/v1/auth/login/local \
|
||||
-d "username=admin@deerflow.dev&password=<P0或P1>" \
|
||||
-d "username=admin@example.com&password=<凭据文件密码>" \
|
||||
-c admin.txt | jq .needs_setup
|
||||
# 预期: true
|
||||
|
||||
@ -866,7 +901,7 @@ curl -s -X POST $BASE/api/v1/auth/change-password \
|
||||
-b admin.txt \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-CSRF-Token: $CSRF" \
|
||||
-d '{"current_password":"<密码>","new_password":"AdminFinal1!","new_email":"admin@real.com"}' \
|
||||
-d '{"current_password":"<凭据文件密码>","new_password":"AdminFinal1!","new_email":"admin@real.com"}' \
|
||||
-c admin.txt
|
||||
|
||||
# 验证
|
||||
@ -876,7 +911,7 @@ curl -s $BASE/api/v1/auth/me -b admin.txt | jq '{email, needs_setup}'
|
||||
**预期:**
|
||||
- [ ] `email` 变为 `admin@real.com`
|
||||
- [ ] `needs_setup` 变为 `false`
|
||||
- [ ] 后续重启控制台不再有 warning
|
||||
- [ ] 后续登录使用新密码
|
||||
|
||||
#### TC-UPG-18: 长期未用后 JWT 密钥轮换
|
||||
|
||||
@ -890,8 +925,8 @@ make stop && make dev
|
||||
|
||||
**预期:**
|
||||
- [ ] 服务正常启动
|
||||
- [ ] 旧密码仍可登录(密码存在 DB,与 JWT 密钥无关)
|
||||
- [ ] 旧的 JWT token 失效(密钥变了签名不匹配)— 但因为从未登录过也没有旧 token
|
||||
- [ ] 账号密码仍可登录(密码存在 DB,与 JWT 密钥无关)
|
||||
- [ ] 旧的 JWT token 失效(密钥变了签名不匹配)
|
||||
|
||||
---
|
||||
|
||||
@ -910,7 +945,7 @@ for i in 1 2 3; do
|
||||
done
|
||||
|
||||
# 检查 admin 数量
|
||||
sqlite3 backend/.deer-flow/users.db \
|
||||
sqlite3 backend/.deer-flow/data/deerflow.db \
|
||||
"SELECT COUNT(*) FROM users WHERE system_role='admin';"
|
||||
```
|
||||
|
||||
@ -1055,7 +1090,7 @@ curl -s -X POST $BASE/api/v1/auth/register \
|
||||
wait
|
||||
|
||||
# 检查用户数
|
||||
sqlite3 backend/.deer-flow/users.db \
|
||||
sqlite3 backend/.deer-flow/data/deerflow.db \
|
||||
"SELECT COUNT(*) FROM users WHERE email='race@example.com';"
|
||||
```
|
||||
|
||||
@ -1165,13 +1200,16 @@ curl -s -w "%{http_code}" -X DELETE "$BASE/api/threads/$TID" \
|
||||
```bash
|
||||
cd backend
|
||||
python -m app.gateway.auth.reset_admin
|
||||
# 记录密码 P1
|
||||
cp .deer-flow/admin_initial_credentials.txt /tmp/deerflow-reset-p1.txt
|
||||
P1=$(awk -F': ' '/^password:/ {print $2}' /tmp/deerflow-reset-p1.txt)
|
||||
|
||||
python -m app.gateway.auth.reset_admin
|
||||
# 记录密码 P2
|
||||
cp .deer-flow/admin_initial_credentials.txt /tmp/deerflow-reset-p2.txt
|
||||
P2=$(awk -F': ' '/^password:/ {print $2}' /tmp/deerflow-reset-p2.txt)
|
||||
```
|
||||
|
||||
**预期:**
|
||||
- [ ] `.deer-flow/admin_initial_credentials.txt` 每次都会被重写,文件权限为 `0600`
|
||||
- [ ] P1 ≠ P2(每次生成新随机密码)
|
||||
- [ ] P1 不可用,只有 P2 有效
|
||||
- [ ] `token_version` 递增了 2
|
||||
@ -1324,7 +1362,8 @@ done
|
||||
```bash
|
||||
GW=http://localhost:8001
|
||||
|
||||
for path in /health /api/v1/auth/setup-status /api/v1/auth/login/local /api/v1/auth/register; do
|
||||
for path in /health /api/v1/auth/setup-status /api/v1/auth/login/local \
|
||||
/api/v1/auth/register /api/v1/auth/initialize /api/v1/auth/logout; do
|
||||
echo "$path: $(curl -s -w '%{http_code}' -o /dev/null $GW$path)"
|
||||
done
|
||||
# 预期: 200 或 405/422(方法不对但不是 401)
|
||||
@ -1399,9 +1438,9 @@ done
|
||||
>
|
||||
> 前置条件:
|
||||
> - `.env` 中设置 `AUTH_JWT_SECRET`(否则每次容器重启 session 全部失效)
|
||||
> - `DEER_FLOW_HOME` 挂载到宿主机目录(持久化 `users.db`)
|
||||
> - `DEER_FLOW_HOME` 挂载到宿主机目录(持久化 `deerflow.db`)
|
||||
|
||||
#### TC-DOCKER-01: users.db 通过 volume 持久化
|
||||
#### TC-DOCKER-01: deerflow.db 通过 volume 持久化
|
||||
|
||||
```bash
|
||||
# 启动容器
|
||||
@ -1416,13 +1455,13 @@ curl -s -X POST $BASE/api/v1/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"docker-test@example.com","password":"DockerTest1!"}' -w "\nHTTP %{http_code}"
|
||||
|
||||
# 检查宿主机上的 users.db
|
||||
ls -la ${DEER_FLOW_HOME:-backend/.deer-flow}/users.db
|
||||
sqlite3 ${DEER_FLOW_HOME:-backend/.deer-flow}/users.db \
|
||||
# 检查宿主机上的 deerflow.db
|
||||
ls -la ${DEER_FLOW_HOME:-backend/.deer-flow}/data/deerflow.db
|
||||
sqlite3 ${DEER_FLOW_HOME:-backend/.deer-flow}/data/deerflow.db \
|
||||
"SELECT email FROM users WHERE email='docker-test@example.com';"
|
||||
```
|
||||
|
||||
**预期:** users.db 在宿主机 `DEER_FLOW_HOME` 目录中,查询可见刚注册的用户。
|
||||
**预期:** deerflow.db 在宿主机 `DEER_FLOW_HOME` 目录中,查询可见刚注册的用户。
|
||||
|
||||
#### TC-DOCKER-02: 重启容器后 session 保持
|
||||
|
||||
@ -1466,22 +1505,24 @@ done
|
||||
|
||||
**已知限制:** In-process rate limiter 不跨 worker 共享。生产环境如需精确限速,需要 Redis 等外部存储。
|
||||
|
||||
#### TC-DOCKER-04: IM 渠道不经过 auth
|
||||
#### TC-DOCKER-04: IM 渠道使用内部认证
|
||||
|
||||
```bash
|
||||
# IM 渠道(Feishu/Slack/Telegram)在 gateway 容器内部通过 LangGraph SDK 通信
|
||||
# 不走 nginx,不经过 AuthMiddleware
|
||||
# IM 渠道(Feishu/Slack/Telegram)在 gateway 容器内部通过 LangGraph SDK 调 Gateway
|
||||
# 请求携带 process-local internal auth header,并带匹配的 CSRF cookie/header
|
||||
|
||||
# 验证方式:检查 gateway 日志中 channel manager 的请求不包含 auth 错误
|
||||
docker logs deer-flow-gateway 2>&1 | grep -E "ChannelManager|channel" | head -10
|
||||
```
|
||||
|
||||
**预期:** 无 auth 相关错误。渠道通过 `langgraph-sdk` 直连 LangGraph Server(`http://langgraph:2024`),不走 auth 层。
|
||||
**预期:** 无 auth 相关错误。渠道不依赖浏览器 cookie;服务端通过内部认证头把请求归入 `default` 用户桶。
|
||||
|
||||
#### TC-DOCKER-05: admin 密码写入 0600 凭证文件(不再走日志)
|
||||
#### TC-DOCKER-05: reset_admin 密码写入 0600 凭证文件(不再走日志)
|
||||
|
||||
```bash
|
||||
# 凭证文件写在挂载到宿主机的 DEER_FLOW_HOME 下
|
||||
# 首次启动不会自动生成 admin 密码。先重置已有 admin,凭据文件写在挂载到宿主机的 DEER_FLOW_HOME 下。
|
||||
docker exec deer-flow-gateway python -m app.gateway.auth.reset_admin --email docker-test@example.com
|
||||
|
||||
ls -la ${DEER_FLOW_HOME:-backend/.deer-flow}/admin_initial_credentials.txt
|
||||
# 预期文件权限: -rw------- (0600)
|
||||
|
||||
@ -1512,14 +1553,15 @@ sleep 15
|
||||
docker ps --filter name=deer-flow-langgraph --format '{{.Names}}' | wc -l
|
||||
# 预期: 0
|
||||
|
||||
# auth 流程正常
|
||||
# auth 流程正常:未登录受保护接口返回 401
|
||||
curl -s -w "%{http_code}" -o /dev/null $BASE/api/models
|
||||
# 预期: 401
|
||||
|
||||
curl -s -X POST $BASE/api/v1/auth/login/local \
|
||||
-d "username=admin@deerflow.dev&password=<日志密码>" \
|
||||
curl -s -X POST $BASE/api/v1/auth/initialize \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"admin@example.com","password":"AdminPass1!"}' \
|
||||
-c cookies.txt -w "\nHTTP %{http_code}"
|
||||
# 预期: 200
|
||||
# 预期: 201
|
||||
```
|
||||
|
||||
### 7.4 补充边界用例
|
||||
@ -1587,13 +1629,15 @@ curl -s -D - -X POST $BASE/api/v1/auth/login/local \
|
||||
#### TC-EDGE-05: HTTP 无 max_age / HTTPS 有 max_age
|
||||
|
||||
```bash
|
||||
GW=http://localhost:8001
|
||||
|
||||
# HTTP
|
||||
curl -s -D - -X POST $BASE/api/v1/auth/login/local \
|
||||
curl -s -D - -X POST $GW/api/v1/auth/login/local \
|
||||
-d "username=admin@example.com&password=正确密码" 2>/dev/null \
|
||||
| grep "access_token=" | grep -oi "max-age=[0-9]*" || echo "NO max-age (HTTP session cookie)"
|
||||
|
||||
# HTTPS
|
||||
curl -s -D - -X POST $BASE/api/v1/auth/login/local \
|
||||
# HTTPS:直连 Gateway 才能用 X-Forwarded-Proto 模拟 HTTPS;nginx 会覆盖该 header
|
||||
curl -s -D - -X POST $GW/api/v1/auth/login/local \
|
||||
-H "X-Forwarded-Proto: https" \
|
||||
-d "username=admin@example.com&password=正确密码" 2>/dev/null \
|
||||
| grep "access_token=" | grep -oi "max-age=[0-9]*"
|
||||
@ -1712,10 +1756,10 @@ curl -s -X POST $BASE/api/threads \
|
||||
-b cookies.txt \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-CSRF-Token: $CSRF" \
|
||||
-d '{"metadata":{"owner_id":"victim-user-id"}}' | jq .metadata.owner_id
|
||||
-d '{"metadata":{"owner_id":"victim-user-id","user_id":"victim-user-id"}}' | jq .metadata
|
||||
```
|
||||
|
||||
**预期:** 返回的 `metadata.owner_id` 应为当前登录用户的 ID,不是请求中注入的 `victim-user-id`。服务端应覆盖客户端提供的 `user_id`。
|
||||
**预期:** 返回的 `metadata` 不包含 `owner_id` 或 `user_id`。真实所有权写入 `threads_meta.user_id`,不从客户端 metadata 接收,也不通过 metadata 回显。
|
||||
|
||||
#### 7.5.6 HTTP Method 探测
|
||||
|
||||
@ -1796,6 +1840,6 @@ cd backend && PYTHONPATH=. uv run pytest \
|
||||
# 核心接口冒烟
|
||||
curl -s $BASE/health # 200
|
||||
curl -s $BASE/api/models # 401 (无 cookie)
|
||||
curl -s -X POST $BASE/api/v1/auth/setup-status # 200
|
||||
curl -s $BASE/api/v1/auth/setup-status # 200
|
||||
curl -s $BASE/api/v1/auth/me -b cookies.txt # 200 (有 cookie)
|
||||
```
|
||||
|
||||
@ -2,13 +2,16 @@
|
||||
|
||||
DeerFlow 内置了认证模块。本文档面向从无认证版本升级的用户。
|
||||
|
||||
完整设计见 [AUTH_DESIGN.md](AUTH_DESIGN.md)。
|
||||
|
||||
## 核心概念
|
||||
|
||||
认证模块采用**始终强制**策略:
|
||||
|
||||
- 首次启动时自动创建 admin 账号,随机密码打印到控制台日志
|
||||
- 首次启动时不会自动创建账号;首次访问 `/setup` 时由操作者创建第一个 admin 账号
|
||||
- 认证从一开始就是强制的,无竞争窗口
|
||||
- 历史对话(升级前创建的 thread)自动迁移到 admin 名下
|
||||
- 已有 admin 后,服务启动时会把历史对话(升级前创建且缺少 `user_id` 的 thread)迁移到 admin 名下
|
||||
- 新数据按用户隔离:thread、workspace/uploads/outputs、memory、自定义 agent 都归属当前用户
|
||||
|
||||
## 升级步骤
|
||||
|
||||
@ -25,39 +28,41 @@ cd backend && make install
|
||||
make dev
|
||||
```
|
||||
|
||||
控制台会输出:
|
||||
如果没有 admin 账号,控制台只会提示:
|
||||
|
||||
```
|
||||
============================================================
|
||||
Admin account created on first boot
|
||||
Email: admin@deerflow.dev
|
||||
Password: aB3xK9mN_pQ7rT2w
|
||||
Change it after login: Settings → Account
|
||||
First boot detected — no admin account exists.
|
||||
Visit /setup to complete admin account creation.
|
||||
============================================================
|
||||
```
|
||||
|
||||
如果未登录就重启了服务,不用担心——只要 setup 未完成,每次启动都会重置密码并重新打印到控制台。
|
||||
首次启动不会在日志里打印随机密码,也不会写入默认 admin。这样避免启动日志泄露凭据,也避免在操作者创建账号前出现可被猜测的默认身份。
|
||||
|
||||
### 3. 登录
|
||||
### 3. 创建 admin
|
||||
|
||||
访问 `http://localhost:2026/login`,使用控制台输出的邮箱和密码登录。
|
||||
访问 `http://localhost:2026/setup`,填写邮箱和密码创建第一个 admin 账号。创建成功后会自动登录并进入 workspace。
|
||||
|
||||
### 4. 修改密码
|
||||
如果这是从无认证版本升级,创建 admin 后重启一次服务,让启动迁移把缺少 `user_id` 的历史 thread 归属到 admin。
|
||||
|
||||
登录后进入 Settings → Account → Change Password。
|
||||
### 4. 登录
|
||||
|
||||
后续访问 `http://localhost:2026/login`,使用已创建的邮箱和密码登录。
|
||||
|
||||
### 5. 添加用户(可选)
|
||||
|
||||
其他用户通过 `/login` 页面注册,自动获得 **user** 角色。每个用户只能看到自己的对话。
|
||||
其他用户通过 `/login` 页面注册,自动获得 **user** 角色。每个用户只能看到自己的对话、上传文件、输出文件、memory 和自定义 agent。
|
||||
|
||||
## 安全机制
|
||||
|
||||
| 机制 | 说明 |
|
||||
|------|------|
|
||||
| JWT HttpOnly Cookie | Token 不暴露给 JavaScript,防止 XSS 窃取 |
|
||||
| CSRF Double Submit Cookie | 所有 POST/PUT/DELETE 请求需携带 `X-CSRF-Token` |
|
||||
| CSRF Double Submit Cookie | 受保护的 POST/PUT/PATCH/DELETE 请求需携带 `X-CSRF-Token`;登录/注册/初始化/登出走 auth 端点 Origin 校验 |
|
||||
| bcrypt 密码哈希 | 密码不以明文存储 |
|
||||
| 多租户隔离 | 用户只能访问自己的 thread |
|
||||
| Thread owner filter | `threads_meta.user_id` 由服务端认证上下文写入,搜索、读取、更新、删除默认按当前用户过滤 |
|
||||
| 文件系统隔离 | 线程数据写入 `{base_dir}/users/{user_id}/threads/{thread_id}/user-data/`,sandbox 内统一映射为 `/mnt/user-data/` |
|
||||
| Memory / agent 隔离 | 用户 memory 和自定义 agent 写入 `{base_dir}/users/{user_id}/...`;旧共享 agent 只作为只读兼容回退 |
|
||||
| HTTPS 自适应 | 检测 `x-forwarded-proto`,自动设置 `Secure` cookie 标志 |
|
||||
|
||||
## 常见操作
|
||||
@ -74,22 +79,26 @@ python -m app.gateway.auth.reset_admin
|
||||
python -m app.gateway.auth.reset_admin --email user@example.com
|
||||
```
|
||||
|
||||
会输出新的随机密码。
|
||||
会把新的随机密码写入 `.deer-flow/admin_initial_credentials.txt`,文件权限为 `0600`。命令行只输出文件路径,不输出明文密码。
|
||||
|
||||
### 完全重置
|
||||
|
||||
删除用户数据库,重启后自动创建新 admin:
|
||||
删除统一 SQLite 数据库,重启后重新访问 `/setup` 创建新 admin:
|
||||
|
||||
```bash
|
||||
rm -f backend/.deer-flow/users.db
|
||||
# 重启服务,控制台输出新密码
|
||||
rm -f backend/.deer-flow/data/deerflow.db
|
||||
# 重启服务后访问 http://localhost:2026/setup
|
||||
```
|
||||
|
||||
## 数据存储
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `.deer-flow/users.db` | SQLite 用户数据库(密码哈希、角色) |
|
||||
| `.deer-flow/data/deerflow.db` | 统一 SQLite 数据库(users、threads_meta、runs、feedback 等应用数据) |
|
||||
| `.deer-flow/users/{user_id}/threads/{thread_id}/user-data/` | 用户线程的 workspace、uploads、outputs |
|
||||
| `.deer-flow/users/{user_id}/memory.json` | 用户级 memory |
|
||||
| `.deer-flow/users/{user_id}/agents/{agent_name}/` | 用户自定义 agent 配置、SOUL 和 agent memory |
|
||||
| `.deer-flow/admin_initial_credentials.txt` | `reset_admin` 生成的新凭据文件(0600,读完应删除) |
|
||||
| `.env` 中的 `AUTH_JWT_SECRET` | JWT 签名密钥(未设置时自动生成临时密钥,重启后 session 失效) |
|
||||
|
||||
### 生产环境建议
|
||||
@ -111,19 +120,21 @@ python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
| `/api/v1/auth/me` | GET | 获取当前用户信息 |
|
||||
| `/api/v1/auth/change-password` | POST | 修改密码 |
|
||||
| `/api/v1/auth/setup-status` | GET | 检查 admin 是否存在 |
|
||||
| `/api/v1/auth/initialize` | POST | 首次初始化第一个 admin(仅无 admin 时可调用) |
|
||||
|
||||
## 兼容性
|
||||
|
||||
- **标准模式**(`make dev`):完全兼容,admin 自动创建
|
||||
- **标准模式**(`make dev`):完全兼容;无 admin 时访问 `/setup` 初始化
|
||||
- **Gateway 模式**(`make dev-pro`):完全兼容
|
||||
- **Docker 部署**:完全兼容,`.deer-flow/users.db` 需持久化卷挂载
|
||||
- **IM 渠道**(Feishu/Slack/Telegram):通过 LangGraph SDK 通信,不经过认证层
|
||||
- **Docker 部署**:完全兼容,`.deer-flow/data/deerflow.db` 需持久化卷挂载
|
||||
- **IM 渠道**(Feishu/Slack/Telegram):通过 Gateway 内部认证通信,使用 `default` 用户桶
|
||||
- **DeerFlowClient**(嵌入式):不经过 HTTP,不受认证影响
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 症状 | 原因 | 解决 |
|
||||
|------|------|------|
|
||||
| 启动后没看到密码 | admin 已存在(非首次启动) | 用 `reset_admin` 重置,或删 `users.db` |
|
||||
| 启动后没看到密码 | 当前实现不在启动日志输出密码 | 首次安装访问 `/setup`;忘记密码用 `reset_admin` |
|
||||
| `/login` 自动跳到 `/setup` | 系统还没有 admin | 在 `/setup` 创建第一个 admin |
|
||||
| 登录后 POST 返回 403 | CSRF token 缺失 | 确认前端已更新 |
|
||||
| 重启后需要重新登录 | `AUTH_JWT_SECRET` 未持久化 | 在 `.env` 中设置固定密钥 |
|
||||
|
||||
@ -8,6 +8,7 @@ This directory contains detailed documentation for the DeerFlow backend.
|
||||
|----------|-------------|
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | System architecture overview |
|
||||
| [API.md](API.md) | Complete API reference |
|
||||
| [AUTH_DESIGN.md](AUTH_DESIGN.md) | User authentication, CSRF, and per-user isolation design |
|
||||
| [CONFIGURATION.md](CONFIGURATION.md) | Configuration options |
|
||||
| [SETUP.md](SETUP.md) | Quick setup guide |
|
||||
|
||||
@ -42,6 +43,7 @@ docs/
|
||||
├── README.md # This file
|
||||
├── ARCHITECTURE.md # System architecture
|
||||
├── API.md # API reference
|
||||
├── AUTH_DESIGN.md # User authentication and isolation design
|
||||
├── CONFIGURATION.md # Configuration guide
|
||||
├── SETUP.md # Setup instructions
|
||||
├── FILE_UPLOAD.md # File upload feature
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user