# KenOAuth + JWT 迁移总计划 - 状态:执行中 - 涉及仓库:`C:\工程\KenApi`(GitData)、`C:\工程\KenOAuth`、`C:\工程\KenAdmin` - 目标:GitData 的「用户认证」「会话」「space 身份/加入流程」由 KenOAuth 唯一持有;GitData 用 JWT 验签,不再维护任何服务端会话存储 - 前置结论:**空库对接**,不做存量数据迁移、不兼容旧口令 - 本文取代旧版计划,是唯一执行依据 ## 0. 执行进度(2026-10-24) | 批次 | 状态 | 已落地内容 | | --- | --- | --- | | B0.5 | ✅ | 删除 `app/adminhost`/`lib/admin`/`lib/admin-rpc`;`[internal] service_key` + `x-gitdata-service-key` 守卫 | | B0 | ✅ | 新增 `lib/kenauth`(proto + 客户端 + 拦截器);`[oauth]` 配置;compose `nats`/`oauth`;Helm `oauth.yaml`;Dockerfile 清理 | | B1 | ✅ | `users.uid` 及全部用户外键改为 `i64`;`jwt::Session::uid() -> i64`;OpenAPI 与 web 类型重新生成 | | B2.0 | ✅ | `lib/jwt`:EdDSA + JWKS 缓存、access/service claims、Session 提取器、cookie、guard、share 兼容层 | | B2.1 | ✅ | webapi 注入 `JwtVerifier` + 全局 `revalidate`(Redis 60s → `GetSession`) | | B2.2 | ✅ | `GET /auth/login`、`GET /auth/callback`、`POST /auth/refresh`(Redis 单飞锁)、`POST /auth/logout` | | B2.3 | ✅ | 357 个文件 `session_layer` → `jwt`;删除 8 处 `impl SessionGuard`;`api_error!` 改 `::jwt::SessionError` | | B2.4 | ✅ | 删除旧 register/login/2FA/PoW/邮箱/口令/AccessKey/SSHKey/设备/注销的 handler 与 service | | B2.5 | ✅ | 删除 `lib/session`、`lib/session-layer`、`[session]` 配置与依赖;`SameSite` 并入 `[oauth]` | | B2.6 | ✅ | `app/im` 改 KenOAuth service token(`TokenService`)+ `IntrospectToken`;`/auth/jwt-request` 改 `IssueServiceToken`;删除 `export::jwt` 与 `auth.chat_jwt_sec` | | B2.7 | ✅ | 前端 401 `token_expired` 单飞 refresh + 登录跳转;删除 6 个认证页面与旧 PoW wasm | | B2.8 | ✅ | Helm/compose/NOTES/RUST_LOG 已改;`/readyz` 增加 KenOAuth gRPC 健康探针;JWKS 拉取/刷新失败改为 `error!`(target `gitdata::jwks`)供告警 | | B3 | ✅ | `verify_access_key`、SSH 公钥校验改 gRPC;删除 `user_access_keys`/`user_ssh_keys` 及会话/设备/恢复码/注销/邮箱验证码模型 | | K1 | ✅ | KenOAuth `users.bot/system`、`EnsureBotUser`;Principal/UserPublic/AuthorityUser 透出 bot/system | | K4 | ✅ | 表结构 + JoinRule/JoinRequest RPC + `EnsureBotMember`(支持 role_keys/role_ids);`CreateOrganization/UpdateOrganization` 支持 join 字段;邀请支持 email 绑定与多角色(`role_ids`) | | B4 | ✅ | space/member/ban/group/invite/join-rule/join-request 全部写透 KenOAuth;`apply_space_join_rules` 经 `EnsureBotMember(role_ids)` 投影;权限判定改 `CheckPermissions`(300s Redis 投影) | | B5 | ✅ | `/internal` 服务密钥守卫复核;未使用依赖清理;`openapi.json` + web 客户端重新生成;`openapi --check`、web build/lint 通过;全仓代码无 `session_layer`/`lib/session`/`admin_rpc` 引用 | > 当前 `cargo check --workspace`、KenOAuth `cargo check` 与 `cargo run -p openapi -- --check` 均通过;`bun run --cwd web build` 通过。 > > 已知待收尾: > - B4.3 的物理删表改为「保留本地表作为投影缓存」:读路径与 `dir` 覆盖仍依赖这些表,事实源已全部在 KenOAuth。 > - K2/K3/K5(协议接受、MFA 注册/恢复码、账号删除)与 §9 中标注「代理」的自助端点(2FA、session、access key、ssh key、deletion)尚未落地;前端对应设置分区暂不可用,属于后续批次。 > - 邮件邀请:KenOAuth 负责 email 绑定与兑换校验,邮件投递仍由 GitData 发送(链接指向 GitData SPA)。 > - `user_emails` 投影表暂留(单邮箱模型待 K5 完成后删除)。 --- ## 1. 目标与非目标 ### 1.1 目标 1. KenOAuth 成为唯一认证源:账号、口令、邮箱、TOTP、会话、Access Token、SSH 公钥、协议接受。 2. KenOAuth 成为唯一组织事实源:组织、成员、角色、角色权限、邀请、封禁、加入规则、加入申请。 3. GitData 浏览器侧改为 OIDC 授权码 + PKCE,cookie 中只放 KenOAuth 颁发的 **access token**。 4. 删除 `lib/session` 与 `lib/session-layer`,新 `lib/jwt` 只做 EdDSA/JWKS 验签。 5. GitData 用户主键统一为 `i64`,与 `auth.common.v1.Principal.user_id` 对齐。 6. KenShell(Git 数据面)通过 gRPC 校验 Access Key / SSH 公钥。 7. 删除 GitData 自带管理后台,admin 用户与 RBAC 完全由 `C:\工程\KenAdmin` 管理。 8. `/api/v1/internal/*` 改用服务密钥鉴权。 ### 1.2 非目标 1. 不迁移存量用户、口令、会话、Access Key、space 数据。 2. 不把 `rooms`、`dir`、目录覆盖、space 图片、`space_warn` 迁入 KenOAuth。 3. 不把仓库、issue、PR、chat 消息迁入 KenOAuth。 4. GitData 已移除 `mails` 服务,非认证邮件(issue/PR/chat/space 邀请)改走 KenOAuth 内部 `EmailService.SendEmail`。 5. 不在本期实现 WebAuthn / SMS 多因子(KenOAuth 表结构已就绪)。 6. 不在本期把 GitData 的资源管理能力迁入 KenAdmin(见 §16 能力缺口)。 --- ## 2. 决策记录 | 编号 | 决策 | 结论 | 影响 | | --- | --- | --- | --- | | D1 | 用户主键 | GitData 全量 `Uuid → i64` | 87 个 model、全部用户外键、OpenAPI、前端类型 | | D2 | 浏览器认证 | OIDC 授权码 + PKCE;cookie 存 KenOAuth access token | 删除 GitData 登录/注册/重置/2FA 页面 | | D3 | Space 归属 | 身份事实 + 加入流程 → KenOAuth | 新建 2 张表 + 10 个 RPC | | D4 | 数据库 | 独立 `auth` 库 | GitData 摘掉跨库外键,保留投影表 | | D5 | 存量数据 | 空库对接 | 不做迁移脚本、不做口令兼容 | | D6 | 邮件邀请 | KenOAuth 生成 token 并直接发信 | invite 增 `email`、新增模板 | | D7 | Bot 账号 | KenOAuth 建 bot 账号 | users 增 `bot/system`、新增 2 个 RPC | | D8 | 多邮箱 | 改为单邮箱 | 删除 `user_emails` 及 6 个接口 | | D9 | 组织 ID | 保持 UUID,与 `spaces.id` 1:1 | 资源表外键不变 | | D10 | 权限判定 | 角色权限同名映射 + `CheckPermission`(300s 缓存) | 删除 `space_group_permissions` 等 4 张表 | | D11 | 会话实现 | 删除 `lib/session` / `lib/session-layer`,新建 `lib/jwt` | 412 个文件 import 改名 | | D12 | 令牌校验 | 只验签 KenOAuth 令牌(EdDSA + JWKS),GitData 不自签 | 不再需要会话签名密钥 | | D13 | 续期 | refresh token 放独立 HttpOnly cookie | 新增 `/auth/refresh` + Redis 单飞锁 | | D14 | access TTL | 保持 KenOAuth 默认 600s | 约每 9 分钟刷新一次 | | D15 | Admin | 完全由 `C:\工程\KenAdmin` 管理 | 删除 `adminhost` / `lib/admin` / `lib/admin-rpc` | | D16 | `/internal` | 服务密钥头 `x-gitdata-service-key` | 新增 `[internal] service_key` | | D17 | 聊天令牌 | KenOAuth `TokenService.IssueServiceToken`(audience=CHAT) | 删除 `auth.chat_jwt_sec` 与 `export::jwt` | --- ## 3. 目标架构 ### 3.1 组件图 ``` ┌──────────────────────────────┐ │ Browser SPA │ │ (web, Vite/React) │ └──────────┬───────────────────┘ │ cookie: gitdata_session / gitdata_refresh ▼ ┌──────────────────────────────┐ │ KenOAuth (oauth) │ │ HTTP 18081 / gRPC 18082 │ │ 账号 / 会话 / 令牌 / 组织 │ │ PostgreSQL(auth) + Redis │ │ + NATS + SMTP │ └──────▲──────────────▲────────┘ OIDC code/token │ │ gRPC + x-rpc-key │ │ ┌───────────────────────────┴──┐ ┌───────┴───────────────────────┐ │ apihost (:3000 / dev 4081) │ │ KenShell (gRPC 50091) │ │ - OIDC RP / refresh │ │ - smart HTTP / SSH │ │ - lib/jwt 验签 │ └───────────────────────────────┘ │ - 业务 REST / SPA │ │ - 本地投影读写 │ └──────────┬───────────────────┘ │ 本地投影:users / spaces / space_members ▼ PostgreSQL(gitdata) + Redis + RustFS ▲ │ im ``` ### 3.2 令牌流转 ``` 登录: 浏览器 → /api/v1/auth/login → 302 KenOAuth /api/v1/oauth/authorize (PKCE) → /api/v1/auth/callback?code → 后端 POST /api/v1/oauth/token → 验 id_token(JWKS + nonce) → Set-Cookie: gitdata_session = access_token (600s, Path=/) gitdata_refresh = refresh_token (30d, Path=/api/v1/auth) 请求: cookie → lib/jwt 验签(EdDSA + JWKS)→ AccessClaims{sub, sid, sv, username} → revalidate(Redis 60s → AuthorityService.GetSession) → handler require_uid 过期: API 401 token_expired → POST /api/v1/auth/refresh(Redis 单飞锁) → POST /api/v1/oauth/token (refresh_token, 轮换) → 覆盖两个 cookie → 重放原请求 ``` ### 3.3 服务边界 | 关注点 | 归属 | 访问方式 | | --- | --- | --- | | 凭据是否正确 | KenOAuth | `IdentityService.Authenticate` | | 账号是否可用 | KenOAuth | `IdentityService.GetUserStatus` / `AuthorityService.GetUser` | | 会话是否存活 | KenOAuth | `AuthorityService.GetSession` | | 组织内能否做某动作 | KenOAuth | `OrganizationService.CheckPermission` | | 令牌签名 | KenOAuth | `/.well-known/jwks.json` | | 仓库/房间/工单准入 | GitData | 本地表 + `space_members` 投影 | | 平台管理 | KenAdmin | 独立系统,与 GitData 无用户体系耦合 | --- ## 4. 数据归属 ### 4.1 KenOAuth 唯一持有 `users`、`user_sessions`、`user_access_tokens`、`user_ssh_keys`、`user_mfa`、`user_mfa_backup_codes`、`user_verification_tokens`、`user_events`、`oauth_clients`、`oauth_consents`、`oauth_refresh_tokens`、`service_token_revocations`、`organizations`、`organization_members`、`organization_roles`、`organization_role_permissions`、`organization_member_roles`、`organization_member_permissions`、`organization_invites`、`organization_invite_redemptions`、`organization_join_rules`(新增)、`organization_join_requests`(新增)、`user_agreement_acceptances`(新增)。 ### 4.2 GitData 只读投影 | 表 | 关键字段 | 来源 | | --- | --- | --- | | `users` | `uid i64`、`username`、`lower_name`、`display_name`、`avatar_url`、`is_active`、`is_restricted`、`prohibit_login`、`bot`、`system`、`visibility`、`version` | OIDC 回调 + `GetUserPublic` / `BatchGetUserPublic` + 对账 | | `spaces` | `id uuid`(= organization id)、`lower_name`、`name`、`description`、`owner_id i64`、`avatar_url`、`is_private`、`allow_join`、`allow_join_level`、`member_must_enable_2fa`、`welcome_*`、`banner_*`、`nums_members`、`version` | `OrganizationService` 写成功后同请求双写 + 对账 | | `space_members` | `(space_id, user_id)`、`alias_name`、`alias_avatar_url`、`status`、`joined_at`、`left_at`、`banned_at`、`ban_reason`、`ban_expires_at`、`ban_allow_*`、`message_notifications`、`muted`、`mute_until`、`mobile_push`、`suppress_*`、`hide_muted_channels`、`last_active_at` | `OrganizationService` 写成功后同请求双写 + 对账 | > 投影表只允许 GitData 内部写入,禁止绕过 gRPC 直接改。写入口集中在 `lib/space/src/sync.rs` 与 `lib/kenauth` 调用点。 ### 4.3 GitData 完全自有 `user_profile`、`user_preferences`、`user_notifications`、`user_saved_message`、`user_recent_mention`、`user_reports`、`user_push_subscription`、`user_blocks`、`user_followers`、`user_favorite_meme`、`user_suspensions`、`rooms`、`room_*`、`dir`、`dir_overwrite_group`、`dir_overwrite_user`、`space_warn`、`repos`、`issues`、`pull_request*`、`chat_*`、`ci_*`、`agent*`、`search_*`。 ### 4.4 删除的数据 `user_sessions`、`user_access_key`、`user_ssh_key`、`user_email`、`user_email_change_ticket`、`user_email_verification_codes`、`user_recovery_codes`、`user_agreement_acceptances`、`user_devices`、`space_bans`、`space_groups`、`space_group_permissions`、`space_member_groups`、`space_invite`、`space_invite_groups`、`space_join_rule`、`space_join_request`。 --- ## 5. `lib/jwt` 设计 ### 5.1 定位 `lib/jwt` **只验签,不签发**: - 验证 KenOAuth **access token**(浏览器 cookie)。 - 验证 KenOAuth **service token**(`app/im` 聊天握手)。 - 不持有任何签名私钥,不依赖 `models` / `sea-orm` / `redis` / `kenauth`。 ### 5.2 结构 ``` lib/jwt/ Cargo.toml src/lib.rs src/verifier.rs # JWKS 缓存 + EdDSA 验签 src/access.rs # AccessClaims src/service.rs # ServiceClaims src/extract.rs # Session(FromRequestParts) src/user.rs # SessionUser trait(仅 uid()) src/cookie.rs # session / refresh cookie src/guard.rs # require_uid / session_guard src/error.rs # SessionError src/share.rs # 兼容路径 ``` 依赖:`axum`、`serde`、`serde_json`、`jsonwebtoken`、`reqwest`、`tokio`、`time`、`tracing`、`config`、`export`。 ### 5.3 Claims ```rust #[derive(Clone, Deserialize)] pub struct AccessClaims { pub iss: String, #[serde(deserialize_with = "subject::deserialize")] pub sub: i64, #[serde(rename = "client_id", alias = "app_id")] pub client_id: String, pub aud: String, pub jti: String, pub iat: i64, pub exp: i64, pub security_version: i32, pub sid: Option, #[serde(default, with = "scopes")] pub scope: Option>, pub username: Option, pub token_type: String, } #[derive(Clone, Deserialize)] pub struct ServiceClaims { pub iss: String, #[serde(deserialize_with = "subject::deserialize")] pub sub: i64, pub aud: String, pub jti: String, pub iat: i64, pub exp: i64, pub security_version: i32, #[serde(default)] pub scopes: Vec, pub token_type: String, } ``` `sub` 在 KenOAuth 中序列化为字符串,必须自定义反序列化;`scope` 支持空格分隔字符串与数组两种形态。 ### 5.4 验签规则 | 检查 | access | service | | --- | --- | --- | | 算法 | `EdDSA` | `EdDSA` | | 签名键 | JWKS 按 `kid` 选取 | 同 | | `iss` | `[oauth] issuer` | 同 | | `aud` | `[oauth] audience`(默认 `gitdata.ai`) | `chat.gitdata.ai` | | `client_id` | 必须等于 `gitdata-web` | 不适用 | | `token_type` | `"access"` | `"service"` | | JOSE `typ` | `at+jwt` | `JWT` | | `exp` / `iat` | `leeway_secs`(默认 30) | 同 | | 未知 `kid` | 限速刷新 JWKS(≥30s) | 同 | | JWKS 不可用 | 有缓存用缓存;无缓存返回 `Verifier` | 同 | ### 5.5 `Session` 提取器 ```rust enum SessionState { Anonymous, Expired, Valid(AccessClaims) } pub struct Session { state: SessionState, raw: Option, } impl Session { pub fn uid(&self) -> Option; pub fn username(&self) -> Option<&str>; pub fn session_id(&self) -> Option<&str>; pub fn security_version(&self) -> Option; pub fn scopes(&self) -> &[String]; pub fn token(&self) -> Option<&str>; pub fn is_authenticated(&self) -> bool; pub fn is_expired(&self) -> bool; } ``` - 来源:cookie `gitdata_session`,回退 `Authorization: Bearer `。 - 无 cookie / 验签失败 → `Anonymous`。 - 签名有效但 `exp` 已过 → `Expired`。 - 提取器永不返回错误;鉴权由 `require_uid` / 中间件决定。 ### 5.6 `SessionUser` 与 `require_uid` ```rust pub trait SessionUser { fn uid(&self) -> Option; } impl SessionUser for Session { ... } pub fn require_uid(session: &Session) -> Result; ``` 全仓 17 处 `SessionUser` 导入、204 处 `require_uid` 调用保持原样,仅 import 路径改为 `jwt`。 ### 5.7 Cookie | Cookie | 值 | HttpOnly | SameSite | Path | Max-Age | | --- | --- | --- | --- | --- | --- | | `gitdata_session` | KenOAuth access token | 是 | Lax | `/` | token 剩余 TTL | | `gitdata_refresh` | KenOAuth refresh token | 是 | Strict | `/api/v1/auth` | 30 天 | - `Secure` 由 `[oauth] cookie_secure` 控制;生产必须为 true。 - `Domain` 由 `[oauth] cookie_domain` 控制,默认空(host-only)。 ### 5.8 错误 ```rust pub enum SessionError { Unauthorized, // 401 unauthenticated Expired, // 401 token_expired Verifier(String), // 401 invalid_token } ``` `export::api_error!` 宏中的 `::session_layer::SessionError` 改为 `::jwt::SessionError`,删除 `Database` 分支。 ### 5.9 兼容层 ```rust pub mod share { pub mod session { pub use crate::require_uid; } pub mod guard { pub use crate::guard::session_guard; } } ``` 迁移时只需把 412 个文件中的 `session_layer` 替换为 `jwt`。 ### 5.10 JWKS 缓存 - `RwLock` + `tokio::sync::Mutex` 串行刷新。 - 正常 TTL `jwks_cache_secs`(3600)。 - 遇到未知 `kid` 时立即刷新,但两次刷新间隔不小于 `jwks_min_refresh_secs`(30)。 - 启动时尝试加载;失败只告警,首个请求再重试(开发环境容忍 KenOAuth 未启动)。 --- ## 6. 认证流程 ### 6.1 OIDC 登录 ``` GET /api/v1/auth/login?redirect=/app 1. 校验 redirect 为站内相对路径 2. 生成 state / nonce / code_verifier,写 Redis 600s 3. 302 {issuer}/api/v1/oauth/authorize ?response_type=code &client_id=gitdata-web &redirect_uri={redirect_uri} &scope=openid profile email &state=&nonce=&code_challenge_method=S256&code_challenge= GET /api/v1/auth/callback?code&state 1. state 校验并删除 2. POST {issuer}/api/v1/oauth/token (authorization_code + code_verifier) 3. 验 id_token:JWKS、iss、aud、exp、nonce、at_hash 4. 验 access_token:JWKS、iss、aud、client_id、typ、exp 5. sub 解析为 i64 6. Set-Cookie session + refresh 7. 302 redirect 或 /app ``` 异常处理: | 情况 | 响应 | | --- | --- | | state 缺失/过期 | 400 | | token 端点 `invalid_grant` | 400 | | token 端点 5xx | 502 | | id_token 验签失败 / nonce 不匹配 | 401 | | `sub` 非整数 | 401 | | 用户被禁用 / 已删除 | 403 | ### 6.2 刷新 ``` POST /api/v1/auth/refresh 1. 读 gitdata_refresh;缺失 → 401 unauthenticated 2. 从 access token 读 sid(签名有效即可,忽略 exp) 3. Redis SET oauth:refresh:{sid} NX EX 10 4. POST {issuer}/api/v1/oauth/token (grant_type=refresh_token) 5. 覆盖 gitdata_session / gitdata_refresh 6. 204 - invalid_grant → 清两个 cookie → 401 refresh_expired - 拿不到锁 → 等待 ≤2s 后重试一次 ``` 前端策略: - 401 `token_expired` → 单飞调用 `/auth/refresh` → 重放原请求。 - 401 `refresh_expired` / `unauthenticated` → 跳 `/api/v1/auth/login?silent=1`。 - `silent=1` 时授权请求带 `prompt=none`;`login_required` → 清 cookie → 交互登录。 ### 6.3 吊销复核 全局中间件 `lib/auth/src/oauth/revalidate.rs`: - 跳过路径:`/healthz`、`/readyz`、`/api/v1/auth/*`、静态资源、SPA fallback。 - Redis key `oauth:recheck:{sid}:{sv}`,TTL 60s。 - 未命中 → `AuthorityService.GetSession(user_id, version, sid)`。 - 失败 → 清 cookie,返回 401 `session_revoked`。 ### 6.4 登出 ``` POST /api/v1/auth/logout 1. 读 claims.sid 2. SessionService.RevokeSession(uid, sid, "user_logout") 3. 清 gitdata_session + gitdata_refresh 4. 302 / ``` ### 6.5 敏感操作 MFA - 改口令、增删 Access Token、增删 SSH Key、管理类操作:额外调用 `AuthorityService.GetSession`,检查 `mfa_verified`。 - 未通过 → 403 `mfa_required`。 ### 6.6 聊天令牌 ``` apihost GET /api/v1/auth/jwt-request?appid=chat → TokenService.IssueServiceToken( audience = TOKEN_AUDIENCE_CHAT, subject_user_id = uid, scopes = [event, guild], ttl_seconds = 900) → { jwt, expires_in_secs } im 握手: lib/jwt::service::verify(token, aud="chat.gitdata.ai") → consume_jti(claims.jti)(Redis 单次) → TokenService.IntrospectToken(token) → handle_socket(uid) ``` 删除 `lib/export/src/jwt.rs` 与 `auth.chat_jwt_sec`。 --- ## 7. KenOAuth 改造(K 系列) > 约定:所有新增 RPC 仍走 `auth.oauth.rpc_key`(`x-rpc-key`),mutation 需 `gitdata-request-id`。 ### K0 配置与客户端注册(0.5 人日) - 新增 `gitdata-web` public 客户端(PKCE)。 - 新增 `auth.oauth.invite_accept_url`。 - 新增组织邀请邮件模板与 i18n 文案。 ```yaml auth: oauth: public_url: "http://127.0.0.1:18081" rpc_key: "<与 GitData oauth.rpc_key 一致,>=32 字符>" invite_accept_url: "http://localhost:5173/spaces/invites/accept?token={token}" clients: - client_id: gitdata-web name: "GitData Web" kind: public redirect_uris: - "http://localhost:5173/api/v1/auth/callback" post_logout_redirect_uris: - "http://localhost:5173/" scopes: [openid, profile, email, user_name, guild, event] ``` 验收:`GET /.well-known/openid-configuration` 正常;未注册 client 返回 `invalid_client`。 ### K1 用户模型与 Bot 账号(2 人日) 表变更: - `users` 新增 `bot bool default false`、`system bool default false`。 proto: ```proto message Principal { // ... bool bot = 14; bool system = 15; } message UserPublic { // ... bool bot = 8; bool system = 9; } message AuthorityUser { // ... bool bot = 19; bool system = 20; } service IdentityService { rpc EnsureBotUser(EnsureBotUserRequest) returns (EnsureBotUserResponse); } message EnsureBotUserRequest { string username = 1; string displayname = 2; bool system = 3; } message EnsureBotUserResponse { auth.common.v1.UserPublic user = 1; bool created = 2; } ``` 验收:连续两次 `EnsureBotUser("service-bot")` 返回同一 `user_id`。 ### K2 协议接受(1 人日) 新增表 `user_agreement_acceptances`(user_id、key、version、accepted_at,唯一键 `(user_id, key)`)。 ```proto service AgreementService { rpc AcceptAgreement(AcceptAgreementRequest) returns (AcceptAgreementResponse); } message AcceptAgreementRequest { int64 user_id = 1; string key = 2; string version = 3; } message AcceptAgreementResponse { bool accepted = 1; google.protobuf.Timestamp accepted_at = 2; } ``` 验收:接受后 `CheckAgreementsAccepted` 返回 `accepted=true`;重启后仍为 true。 ### K3 MFA 注册与恢复码(3 人日) ```proto service MfaService { rpc EnrollTotp(EnrollTotpRequest) returns (EnrollTotpResponse); rpc ConfirmTotp(ConfirmTotpRequest) returns (ConfirmTotpResponse); rpc DisableTotp(DisableTotpRequest) returns (DisableTotpResponse); rpc RegenerateRecoveryCodes(RegenerateRecoveryCodesRequest) returns (RegenerateRecoveryCodesResponse); } message EnrollTotpRequest { int64 user_id = 1; } message EnrollTotpResponse { string factor_id = 1; string secret_base32 = 2; string otpauth_url = 3; google.protobuf.Timestamp expires_at = 4; } message ConfirmTotpRequest { int64 user_id = 1; string factor_id = 2; string code = 3; } message ConfirmTotpResponse { bool enabled = 1; repeated string recovery_codes = 2; } message DisableTotpRequest { int64 user_id = 1; string code = 2; } message DisableTotpResponse { bool disabled = 1; } message RegenerateRecoveryCodesRequest { int64 user_id = 1; string code = 2; } message RegenerateRecoveryCodesResponse { repeated string recovery_codes = 1; } ``` 要点:secret 用 `auth.totp_encryption_key` 加密落库;恢复码只返回一次;关闭与重生成需校验动态码并推进 `security_version`。 ### K4 组织扩展(6 人日) #### K4.1 表变更 ```sql ALTER TABLE organizations ADD COLUMN join_policy text NOT NULL DEFAULT 'invite_only'; ALTER TABLE organizations ADD COLUMN allow_join boolean NOT NULL DEFAULT false; ALTER TABLE organizations ADD COLUMN require_email_verified boolean NOT NULL DEFAULT true; ALTER TABLE organizations ADD COLUMN require_mfa boolean NOT NULL DEFAULT false; ALTER TABLE organizations ADD COLUMN welcome_message text; ALTER TABLE organizations ADD COLUMN member_must_enable_2fa boolean NOT NULL DEFAULT false; ALTER TABLE organization_roles ADD COLUMN color text; ALTER TABLE organization_roles ADD COLUMN emoji text; ALTER TABLE organization_roles ADD COLUMN icon_hash text; ALTER TABLE organization_roles ADD COLUMN position bigint NOT NULL DEFAULT 0; ALTER TABLE organization_members ADD COLUMN last_active_at timestamptz; ALTER TABLE organization_invites ADD COLUMN email text; ``` `join_policy`:`open` / `approval` / `invite_only` / `private`。 #### K4.2 新增表 - `organization_join_rules`:id、organization_id、role_id、trigger、matcher、enabled、timestamps。 - `organization_join_requests`:id、organization_id、user_id、status、note、decided_by、decided_at、timestamps。 #### K4.3 新增 RPC ```proto service OrganizationService { rpc CreateJoinRule(CreateJoinRuleRequest) returns (CreateJoinRuleResponse); rpc UpdateJoinRule(UpdateJoinRuleRequest) returns (UpdateJoinRuleResponse); rpc DeleteJoinRule(DeleteJoinRuleRequest) returns (DeleteJoinRuleResponse); rpc ListJoinRules(ListJoinRulesRequest) returns (ListJoinRulesResponse); rpc CreateJoinRequest(CreateJoinRequestRequest) returns (CreateJoinRequestResponse); rpc ListJoinRequests(ListJoinRequestsRequest) returns (ListJoinRequestsResponse); rpc WithdrawJoinRequest(WithdrawJoinRequestRequest) returns (WithdrawJoinRequestResponse); rpc ApproveJoinRequest(ApproveJoinRequestRequest) returns (ApproveJoinRequestResponse); rpc RejectJoinRequest(RejectJoinRequestRequest) returns (RejectJoinRequestResponse); rpc EnsureBotMember(EnsureBotMemberRequest) returns (EnsureBotMemberResponse); } ``` `ApproveJoinRequest` 与 `AddMember` 共用同一事务函数;`EnsureBotMember` 跳过 actor 权限判定。 #### K4.4 邮件邀请 - `CreateInviteRequest` 增 `string email = 8`。 - `OrganizationJoinMethod` 增 `ORGANIZATION_JOIN_METHOD_INVITE_EMAIL = 4`。 - `CreateInvite` 当 `method=INVITE_EMAIL` 时:生成一次性 token(只存哈希)、落库、通过内嵌 `EmailService` 发送 `mail_organization_invite`。 - 链接来自 `auth.oauth.invite_accept_url`。 验收:建组织 → `join_policy=approval` → 申请 → 批准 → 成员列表出现;邮件邀请可兑换。 ### K5 账号自助(4 人日) | 能力 | 新增 | | --- | --- | | 登录后改口令 | `POST /auth/password/change` + `ChangePassword` RPC | | 改邮箱 | `POST /auth/email/change`、`/auth/email/change/confirm`、`DELETE /auth/email/change/{ticket_id}` | | 注销账号 | `POST/DELETE /auth/deletion`、`GET /auth/deletion` + 延迟清理 worker | | Access Token 更新/轮换 | `PATCH /auth/access-tokens/{id}`、`POST /auth/access-tokens/{id}/rotate` | | 会话列表/吊销 HTTP | `GET /auth/sessions`、`DELETE /auth/sessions/{sid}`、`DELETE /auth/sessions` | 要点:改口令后 `security_version + 1` 并吊销其他会话;改邮箱复用 `VerificationPurpose::EmailChange`;注销软删 + 宽限期。 --- ## 8. GitData 改造(B 系列) ### B0 通道与基础设施(3 人日) 新增 `lib/kenauth`: ``` lib/kenauth/ Cargo.toml build.rs proto/** # 从 KenOAuth 复制 src/lib.rs src/client.rs # identity/authority/session/org/token/access_key/ssh_key src/error.rs # ErrorDetail -> ApiError src/convert.rs # Principal / UserPublic -> 本地 DTO ``` 要点: - `tonic_prost_build` 编译 proto,只生成 client。 - 拦截器注入 `x-rpc-key`、`gitdata-request-id`。 - 超时:读 300ms,写 2s。 - `AppState` 增加 `kenauth: Option>`(`kenauth` 不依赖 `export`)。 基础设施: - `docker-compose.dev.yml` 增加 `nats`、`oauth`。 - `charts/core` 增加 `oauth.yaml`(Deployment/Service/ConfigMap/Secret)。 - `Dockerfile` 增加 oauth 镜像阶段。 - `scripts/dev.*`、`justfile` 增加 oauth。 - `lib/health` 增加 oauth 探测。 验收:`grpcurl -H 'x-rpc-key: ...' 127.0.0.1:18082 auth.v1.IdentityService/GetUserStatus` 可用。 ### B0.5 删除管理后台(2 人日) 删除: | 路径 | 说明 | | --- | --- | | `lib/admin/` | ~7.5k 行 | | `lib/admin-rpc/` | ~2.5k 行 + proto | | `app/adminhost/` | 73 行 | | `charts/core/templates/adminhost.yaml` | — | | `charts/core/values.yaml` 的 `adminhost` 段 | — | | `charts/prod/values/prod.yaml` 的 `adminhost` 段 | — | | `lib/config/src/admin.rs` | — | | `config/default.toml` 的 `[admin]` | — | | `apihost.admin_uids` | — | | `lib/auth/src/share/guard.rs::admin_guard` | — | | `scripts/dev.ps1`、`scripts/dev.sh` 中 adminhost | — | | `justfile` urls 中 adminhost | — | | `Dockerfile` 中 adminhost 二进制 | — | | `Cargo.toml` 的 `admin` / `admin-rpc` workspace 依赖 | — | 新增 `[internal] service_key`,把 `/api/v1/internal/*` 改为服务密钥守卫: - 请求头 `x-gitdata-service-key`。 - 常量时间比较。 - 仅内网可达。 - 实施时审计路由集合,删除无消费者的 admin-only 端点。 验收:全仓无 `adminhost` / `admin_rpc` 引用;`/api/v1/internal/*` 无密钥 401。 ### B1 用户 ID 全面 i64(8 人日) 原则:只改用户标识,不改 `space/dir/repo/issue/room` 等资源主键。 model 层: | 文件 | 改动 | | --- | --- | | `lib/models/src/users/user.rs` | `uid: Uuid → i64`;删除 `passwd_hash`、`passwd_version`、`password_last_changed_at`、`totp_*` | | `lib/models/src/**`(87 个文件) | 用户外键 `Uuid → i64` | | `lib/models/src/space/space_members.rs` | `user_id: i64` | 非 model 层: | 文件 | 改动 | | --- | --- | | `lib/export/src/authz.rs` | `OnceLock`、`HashSet` | | `lib/export/src/space_token.rs` | `bot_uid: i64` | | `lib/session-layer/src/user.rs` | 随 B2 删除 | | `lib/auth/src/share/guard.rs` | 随 B2 删除 | | `lib/config/src/apihost.rs` | 删除 `admin_uids` | | `lib/models/src/**` | 同步索引与关系 | 前端:`just openapi` + `bun run --cwd web gen:api`;把 `user_id` 当字符串处理的地方改为 number。 执行方式:先改 `users.uid`,`cargo check` 按编译错误逐个修复。 验收:`cargo check --workspace` 全绿;`openapi-check` 通过。 ### B2 `lib/jwt` 与认证替换(21.5 人日) #### B2.0 `lib/jwt` 实现(4 人日) 按 §5 实现。 #### B2.1 `webapi` 接入(1.5 人日) ```rust let verifier = Arc::new(JwtVerifier::new(&state.config.oauth).await?); ... .layer(Extension(verifier)) .layer(from_fn_with_state(state.clone(), auth::oauth::revalidate)) ``` 删除 `session_layer::{SessionLayer, store}` 与 `store::connect`。 #### B2.2 OIDC 回调 / refresh / logout(4 人日) 新增: ``` lib/auth/src/oauth/ mod.rs state.rs # Redis: oauth:state:{state} jwks.rs # 由 lib/jwt 提供 callback.rs refresh.rs logout.rs revalidate.rs ``` #### B2.3 全局替换(1.5 人日) - 412 个文件 `session_layer` → `jwt`。 - 8 个 `share/guard.rs` 保留,`session_guard` 改为非泛型或空泛型。 - 删除 8 个 `impl SessionGuard` 块。 - `app/im` 的 `SessionGuard` 引用改为 `lib/jwt::service`。 - `lib/export/src/api_error.rs` 宏引用 `::jwt::SessionError`。 #### B2.4 删除旧登录链路(4 人日) 删除: - `lib/auth/src/handlers/*` 中 register/login/login-2fa/logout/email/password/2fa/agreements/access-keys/ssh-keys/jwt-request。 - `lib/auth/src/services/{login,register,password,two_factor,email,sessions,devices,agreements,access_keys,ssh_keys,pow,jwt}.rs`。 - `lib/auth/src/pow/**`、`lib/auth/src/templates/**`、`lib/auth/src/share/{crypto,totp,rate}.rs`。 - `lib/pow-solver`。 - `lib/user/src/services/{emails,sessions}.rs` 及对应 handlers。 #### B2.5 删除两个 crate(1 人日) - 删除 `lib/session/`、`lib/session-layer/`。 - `Cargo.toml` 移除 `session`、`session-layer`。 - 14 个 crate 的 `Cargo.toml`:`session-layer` → `jwt`。 - `lib/config`:`session.rs` → 删除;`[session]` → `[oauth]`。 - `lib/webapi/Cargo.toml` 移除 `session`。 #### B2.6 `app/im` 改造(2 人日) - `GET /api/v1/auth/jwt-request` 改为 `TokenService.IssueServiceToken`。 - `app/im/src/gateway.rs` 改为 `lib/jwt::service::verify` + `consume_jti` + `IntrospectToken`。 - 删除 `lib/export/src/jwt.rs`。 #### B2.7 前端(2 人日) - `web/src/shared/api/createApis.ts`:401 拦截 + 单飞 refresh。 - `web/src/features/auth/model/session.ts`:401 跳转逻辑。 - 删除登录/注册/重置/2FA 页面与 `features/auth` 中相关逻辑。 - `web/src/app/routes/router.tsx` 去掉 6 个路由。 - `web/src/shared/lib/pow-solver` 删除。 #### B2.8 配置 / Helm(1.5 人日) 见 §12。 验收: - 登录 → 回调 → cookie 写入 → `/me` 正确 - 401 → refresh → 重放成功 - 吊销会话 60s 内 401 - 全仓无 `session_layer` ### B3 Git 凭据与令牌(5 人日) | 文件 | 改动 | | --- | --- | | `lib/git/src/share/access.rs::verify_access_key` | 改为 `AccessKeyService.VerifyAccessKey` | | `lib/git/src/ssh/handler/auth.rs` | 改为 `SshKeyService.LookupSshKey` | | `lib/git/src/share/access.rs::verify_space_bot_token` | `bot_uid: i64` | | `lib/models/src/users/{user_access_keys,user_ssh_keys}.rs` | 删除 | | `app/im` | 已由 B2.6 覆盖 | scope 映射:KenOAuth `repo_read/repo_write` → 本地 `allows(write)`。 ### B4 Space 投影与写入穿透(15 人日) #### B4.1 同步层 新增 `lib/space/src/sync.rs`: - `upsert_space` / `remove_space` - `upsert_member` / `remove_member` - `bump_members` 映射: | OrganizationInfo | spaces | | --- | --- | | `id` | `id` | | `slug` | `lower_name` | | `name` | `name` | | `description` | `description` | | `avatar_url` | `avatar_url` | | `owner_user_id` | `owner_id` | | `member_count` | `nums_members` | | `version` | `version` | | OrganizationMemberInfo | space_members | | --- | --- | | `user_id` | `user_id` | | `role` / `left_at` / `banned_at` | `status` | | `alias_*` | 同名 | | `ban_*` | 同名 | | 通知偏好字段 | 同名 | | `last_active_at` | 同名 | #### B4.2 服务改造 | 文件 | 改动 | | --- | --- | | `lib/space/src/services/spaces.rs` | 创建/更新/归档/删除 → OrganizationService | | `lib/space/src/services/members.rs` | → 成员 RPC + 投影 | | `lib/space/src/services/bans.rs` | → BanMember/UnbanMember/UpdateBan | | `lib/space/src/services/groups.rs` | → Role RPC | | `lib/space/src/services/invites.rs` | → Invite RPC | | `lib/space/src/services/join_rules.rs` | → K4 RPC | | `lib/space/src/services/join_requests.rs` | → K4 RPC | | `lib/space/src/share/authz.rs` | owner/system 短路 + `CheckPermission`(300s)+ 投影 | | `lib/space/src/services/rooms.rs`、`dir.rs`、`images.rs` | 保留本地 | #### B4.3 删除表 `space_bans`、`space_groups`、`space_group_permissions`、`space_member_groups`、`space_invite`、`space_invite_groups`、`space_join_rule`、`space_join_request`。 #### B4.4 权限映射 | 角色 | key | 权限 | | --- | --- | --- | | OWNER | `owner` | 全部(短路) | | ADMIN | `admin` | `view_space, read, message, manage_members, manage_invites, manage_bans, manage_channels, manage_overwrites` | | MEMBER | `member` | `view_space, read, message` | 自定义角色 = 原 `space_groups`;`self_assignable` = 原 `join_group_self`。 验收:建 space → 成员/角色/封禁/邀请/加入申请全链路;24 个跨域 `space_members` 查询正常。 ### B5 收尾(4 人日) 1. `lib/webapi/src/lib.rs` 路由收敛;`/internal` 服务密钥守卫。 2. `lib/export/src/authz.rs` 清理。 3. `lib/user`、`lib/auth` 死代码清理。 4. `openapi.json` + `web/src/shared/api` 最终生成;`openapi-check` 通过。 5. Helm/compose/justfile 最终校验。 6. 全链路冒烟:注册 → 登录 → 建组织 → 邀请 → 建仓库 → push → issue → chat。 --- ## 9. 端点处置表 | 原端点 | 处置 | | --- | --- | | `GET /auth/pow/challenge` | 删除 | | `POST /auth/register` | 删除 | | `POST /auth/login` | 改为 `GET` 302 | | `POST /auth/login/2fa` | 删除 | | `POST /auth/logout` | 改为本地登出 + 302 | | `POST /auth/email/verify`、`/resend`、`/reclaim` | 删除 | | `POST /auth/password/forgot`、`/reset`、`/change` | 删除 | | `GET /auth/agreements` | 保留,代理 `AgreementService.ListAgreements` | | `PUT /me/agreements/{key}` | 改为代理 `AcceptAgreement` | | `POST /auth/email/change*` | 改为代理 KenOAuth | | `GET /auth/2fa/status` | 代理 `MfaService.GetMfaStatus` | | `POST /auth/2fa/setup|enable|disable` | 代理 K3 RPC | | `GET/PUT /auth/2fa/recovery-codes` | 代理 K3 RPC | | `GET/POST/DELETE/PATCH /auth/access-keys*` | 代理 KenOAuth HTTP | | `GET/POST/DELETE /auth/ssh-keys*` | 代理 KenOAuth HTTP | | `GET /auth/jwt-request` | 改为 `TokenService.IssueServiceToken` | | `GET/PATCH /me` | 保留;账号字段写 `AuthorityService.UpdateUser` | | `PUT /me/username` | 代理 `AuthorityService.UpdateUser` | | `GET/PATCH /me/preferences` | 保留本地 | | `GET/DELETE /me/sessions*` | 代理 `SessionService` | | `GET/POST/DELETE /me/emails*` | 删除(单邮箱) | | `GET/PATCH/DELETE /me/devices*` | 删除 | | `GET/POST/DELETE /me/deletion` | 改为代理 K5 | | `GET /users*` | 保留,读投影 | --- ## 10. 数据模型变更(GitData) | 表 | 操作 | | --- | --- | | `users` | `uid BIGINT`;删凭据列;保留 `bot/system` | | `user_sessions` | 删除 | | `user_access_key` | 删除 | | `user_ssh_key` | 删除 | | `user_email*`、`user_email_change_ticket`、`user_email_verification_codes` | 删除 | | `user_recovery_codes` | 删除 | | `user_devices` | 删除 | | `user_agreement_acceptances` | 删除 | | `space_bans` | 删除 | | `space_groups` | 删除 | | `space_group_permissions` | 删除 | | `space_member_groups` | 删除 | | `space_invite`、`space_invite_groups` | 删除 | | `space_join_rule`、`space_join_request` | 删除 | | `spaces` | 保留;补 `join_policy`、`require_email_verified`、`require_mfa`、`version` | | `space_members` | 保留;补 `banned_at`、`ban_reason`、`ban_expires_at`、`ban_allow_*`、`last_active_at` | | 其余 model | 用户外键 `UUID → BIGINT` | 建表仍由 `models::migrate::sync_schema` 完成;空库无需手写迁移 SQL。 --- ## 11. 前端改造 | 区域 | 改动 | | --- | --- | | 认证页面 | 删除 6 个页面与对应路由 | | `features/auth` | 保留会话读取与错误翻译;删除登录/注册/POW | | `features/user-settings` | 密码/2FA/邮箱/会话/Access Token/SSH Key 改调代理端点 | | `features/space-management` | `group_id → role_id`;封禁字段;加入规则/申请字段 | | `entities/*` | 用户 id 由 string 改 number | | `shared/api` | `globals.d.ts`、`apiDefinitions.ts` 重新生成 | | `createApis.ts` | 401 拦截 + 单飞 refresh | | `shared/storage` | `setCacheOwner` 参数改 number | | i18n | 删除废弃文案,补新错误码 | --- ## 12. 配置与基础设施 ### 12.1 GitData 配置 ```toml [oauth] enabled = true issuer = "http://127.0.0.1:18081" client_id = "gitdata-web" client_secret = "" redirect_uri = "http://localhost:5173/api/v1/auth/callback" token_endpoint = "http://127.0.0.1:18081/api/v1/oauth/token" jwks_url = "http://127.0.0.1:18081/.well-known/jwks.json" audience = "gitdata.ai" grpc_endpoint = "http://127.0.0.1:18082" rpc_key = "" session_cookie = "gitdata_session" refresh_cookie = "gitdata_refresh" cookie_domain = "" cookie_path = "/" cookie_secure = false cookie_same_site = "lax" access_ttl_secs = 600 refresh_cookie_max_age_secs = 2592000 recheck_secs = 60 leeway_secs = 30 jwks_cache_secs = 3600 jwks_min_refresh_secs = 30 http_timeout_ms = 2000 [internal] service_key = "" ``` 删除:`[session]`、`[admin]`、`apihost.admin_uids`、`auth.chat_jwt_sec`。 ### 12.2 本地开发 ```yaml # docker-compose.dev.yml nats: image: nats:2-alpine ports: ["127.0.0.1:4222:4222", "127.0.0.1:8222:8222"] command: ["-js", "-m", "8222"] oauth: image: harbor.gitdata.me/app/kenoauth:latest network_mode: host volumes: - ./config/oauth.yaml:/app/config/config.yaml:ro - ./config/api/keys:/app/config/api/keys:ro depends_on: [postgres, redis, nats] ``` `config/local.toml` 增 `[oauth]`、`[internal]`;`config/oauth.yaml` 单独维护(gitignore)。 ### 12.3 Helm - 新增 `charts/core/templates/oauth.yaml`。 - 删除 `adminhost.yaml` 与 `adminhost` values。 - 删除 `APP_SESSION__*`、`APP_SESSION__KEY`、`APP_ADMIN__*`。 - 新增 `APP_OAUTH__ISSUER`、`APP_OAUTH__AUDIENCE`、`APP_OAUTH__CLIENT_ID`、`APP_OAUTH__GRPC_ENDPOINT`、`APP_OAUTH__RPC_KEY`、`APP_INTERNAL__SERVICE_KEY`。 - `RUST_LOG` 去掉 `session::session_store`。 ### 12.4 监控 - `lib/kenauth` 客户端指标:请求量、错误率、P99。 - JWKS 拉取失败告警。 - `oauth unavailable` 持续 30s 告警。 - 健康检查失败不清空本地 cookie。 --- ## 13. 批次、依赖、工期 ### 13.1 KenOAuth | 批次 | 内容 | 人日 | | --- | --- | --- | | K0 | 配置 + `gitdata-web` 客户端 | 0.5 | | K1 | bot 字段 + `EnsureBotUser` | 2 | | K2 | 协议接受 | 1 | | K3 | MFA 注册 | 3 | | K4 | 组织扩展 | 6 | | K5 | 账号自助 | 4 | | 小计 | | **16.5** | ### 13.2 GitData | 批次 | 内容 | 依赖 | 人日 | | --- | --- | --- | --- | | B0 | `lib/kenauth` + 基础设施 | K0 | 3 | | B0.5 | 删除管理后台 + `/internal` 服务密钥 | — | 2 | | B1 | 用户 ID 全面 i64 | — | 8 | | B2.0 | `lib/jwt` | B0 | 4 | | B2.1 | `webapi` 接入 | B2.0 | 1.5 | | B2.2 | OIDC / refresh / logout | K5/B2.1 | 4 | | B2.3 | 全局替换 + 删 `SessionGuard` | B2.1/B1 | 1.5 | | B2.4 | 删除旧登录链路 | K1/K2/K3 | 4 | | B2.5 | 删除两个 crate | B2.3/B2.4 | 1 | | B2.6 | `app/im` 服务令牌 | B2.0 | 2 | | B2.7 | 前端 | B2.2 | 2 | | B2.8 | 配置 / Helm | B2.5 | 1.5 | | B3 | Git 凭据 | B2 | 5 | | B4 | Space 投影 | K4/B1/B2 | 15 | | B5 | 收尾 | 全部 | 4 | | 小计 | | | **58.5** | ### 13.3 总计 **约 75 人日**。 关键路径: ``` B0.5 ──► B0 ──► B1 ──► B2.0 ──► B2.1 ──► B2.2 ──► B2.3 ──► B2.4 ──► B2.5 ──► B3/B4 ──► B5 ▲ K0 ──► K5 ───────────────────────────┘ K4 ─────────────────────────────────────────► B4 ``` 可并行: - B0.5 与 K 系列完全独立,可最先做。 - B1 与 K 系列并行。 - B3 与 B4 并行。 --- ## 14. 验收标准 ### 14.1 功能 - [ ] 注册(KenOAuth 页面)→ 回跳 GitData → `/me` 正确 - [ ] 登录 / 登出 / 找回口令 / 改口令 / 改邮箱 - [ ] TOTP 注册、登录验证、恢复码、关闭 - [ ] 协议列表、接受、未接受时拦截 - [ ] Access Token 创建/列出/吊销/轮换;SSH Key 添加/删除 - [ ] `git clone` HTTPS + SSH、`git push` - [ ] 建组织、改资料、归档/恢复/删除、转移所有权 - [ ] 成员增删、角色增删改、权限授予、自助角色加入/退出 - [ ] 邀请链接、邮件邀请、定向邀请、撤销、兑换 - [ ] 加入规则、加入申请(提交/撤回/批准/拒绝) - [ ] 封禁 / 解封 / 更新封禁 - [ ] MCP internal key 与 space token ### 14.2 JWT / 会话 - [ ] cookie 中是 EdDSA access token,验签通过 - [ ] `client_id != gitdata-web` 的令牌被拒绝 - [ ] access token 过期后 refresh 成功,原请求重放成功 - [ ] refresh 轮换后旧 token 重放被拒绝 - [ ] 并发 10 个刷新只触发一次 `/api/v1/oauth/token` - [ ] 吊销会话后 60s 内 401 - [ ] `im` 用 service token 握手成功,`jti` 重放被拒 ### 14.3 管理后台删除 - [ ] `adminhost` 进程、端口、Helm 资源、脚本条目全部消失 - [ ] `/api/v1/internal/*` 无服务密钥 401,有密钥放行 - [ ] 全仓无 `session_layer` / `lib/session` / `admin_rpc` ### 14.4 非功能 - [ ] `cargo check --workspace` 全绿 - [ ] `cargo clippy --workspace --all-targets` 无新增告警 - [ ] `just openapi-check` 通过 - [ ] `just web-lint` 通过 - [ ] 认证 P99 < 50ms(不含 OIDC 外部调用) - [ ] KenOAuth 不可用时已登录用户可读,写操作 503 --- ## 15. 风险与回滚 | 风险 | 等级 | 缓解 | | --- | --- | --- | | i64 改造遗漏 | 高 | 编译器驱动 + 独立批次 | | refresh 并发触发轮换撤销 | 高 | Redis 锁 + 前端单飞 + 失败重登 | | JWKS 拉取失败 | 中 | 缓存 + 限速重试 + 启动告警 | | 删除 admin 后管理能力缺口 | 中 | 列为后续独立项目 | | 删除 `lib/session` 后遗漏 | 中 | 编译器 + 全仓 grep | | 投影漂移 | 中 | 同请求双写 + 每小时对账 + 每日全量 | | NATS 新依赖 | 中 | 部署前置检查 + 健康探针 | | service key 泄露 | 中 | ≥32 字符、仅内网、可轮换 | | 令牌在 cookie 中被 XSS 读取 | 中 | HttpOnly + SameSite + CSP | 回滚: - 空库期:回滚部署 + 恢复数据库快照。 - 灰度期:`[oauth] enabled` 临时开关,关闭后回到旧会话校验;B5 删除。 --- ## 16. 已知退化与能力缺口 | 项 | 说明 | | --- | --- | | 多邮箱 | 改为单邮箱 | | 信任设备 | `user_devices` 删除 | | 手机验证 | `allow_join_level` 的 phone 档位降级为邮箱验证 | | GitData 资源管理 | 删除 adminhost 后,用户/仓库/chat 审核/系统状态均无管理入口;KenAdmin 目前只管自身用户与 KenOAuth | | 聊天 JWT | 改为 KenOAuth service token,TTL 900s | | 邀请统计 | `invite_sort_url_hash` 随表删除 | --- ## 17. 附录 ### 17.1 错误码映射(KenOAuth Code → GitData HTTP) | Code | HTTP | 前端处理 | | --- | --- | --- | | `INVALID_ARGUMENT` / `MALFORMED_REQUEST` | 400 | 展示 message | | `INVALID_CREDENTIALS` / `UNAUTHENTICATED` / `TOKEN_*` | 401 | 跳登录 | | `SECURITY_VERSION_STALE` | 401 | 清会话 + 跳登录 | | `FORBIDDEN` / `LOGIN_FORBIDDEN` / `USER_DISABLED` | 403 | 展示 message | | `MFA_REQUIRED` | 403 | 跳二次验证 | | `AGREEMENT_NOT_ACCEPTED` | 403 | 跳协议页 | | `NOT_FOUND` / `USER_NOT_FOUND` / `ORGANIZATION_*_NOT_FOUND` | 404 | 展示 message | | `CONFLICT` / `*_TAKEN` / `*_EXISTS` | 409 | 展示 message | | `RATE_LIMITED` / `EMAIL_CODE_ATTEMPTS_EXCEEDED` | 429 | 展示 retry_after | | `UNAVAILABLE` | 503 | 提示稍后重试 | | `INTERNAL` / `DATABASE_ERROR` / `CACHE_ERROR` | 500 | 通用错误 | ### 17.2 权限键 ``` view_space read message manage_members manage_roles manage_invites manage_bans manage_channels manage_overwrites ``` ### 17.3 GitData 配置模板 见 §12.1。 ### 17.4 KenOAuth 关键文件索引 | 功能 | 文件 | | --- | --- | | HTTP 路由 | `src/auth/router.rs` | | gRPC 注册 | `src/auth/grpc/mod.rs` | | 组织 | `src/auth/grpc/organization.rs`、`src/auth/organization/**` | | 身份 | `src/auth/grpc/identity.rs` | | 会话 | `src/auth/grpc/session.rs`、`src/session/**` | | 令牌 | `src/auth/grpc/token.rs`、`src/jwt/**` | | 协议 | `src/auth/grpc/agreement.rs` | | MFA | `src/auth/grpc/mfa.rs`、`src/auth/share/mfa.rs` | | 邮件 | `src/email/**` | | 配置 | `src/config/**` | | 建表 | `src/auth/migrate.rs`、`src/db/migrate.rs` | | 前端 | `view/src/**` | ### 17.5 GitData 关键文件索引 | 功能 | 文件 | | --- | --- | | 路由聚合 | `lib/webapi/src/lib.rs` | | JWT 验签 | `lib/jwt/**` | | 认证 | `lib/auth/src/**` | | 用户 | `lib/user/src/**` | | 空间 | `lib/space/src/**` | | 鉴权工具 | `lib/export/src/authz.rs`、`lib/export/src/space_token.rs` | | Git 凭据 | `lib/git/src/share/access.rs`、`lib/git/src/ssh/handler/auth.rs` | | 模型 | `lib/models/src/**` | | 配置 | `lib/config/src/**` | | 前端 | `web/src/**` | | 已删除 | `lib/session/**`、`lib/session-layer/**`、`lib/admin/**`、`lib/admin-rpc/**`、`app/adminhost/**` |