Skip to content
Clone
b7cd2b1
1256 lines 47.9 KB Markdown

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 kenapi -- 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 → i6487 个 model、全部用户外键、OpenAPI、前端类型
D2浏览器认证OIDC 授权码 + PKCE;cookie 存 KenOAuth access token删除 GitData 登录/注册/重置/2FA 页面
D3Space 归属身份事实 + 加入流程 → KenOAuth新建 2 张表 + 10 个 RPC
D4数据库独立 auth 库GitData 摘掉跨库外键,保留投影表
D5存量数据空库对接不做迁移脚本、不做口令兼容
D6邮件邀请KenOAuth 生成 token 并直接发信invite 增 email、新增模板
D7Bot 账号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/jwt412 个文件 import 改名
D12令牌校验只验签 KenOAuth 令牌(EdDSA + JWKS),GitData 不自签不再需要会话签名密钥
D13续期refresh token 放独立 HttpOnly cookie新增 /auth/refresh + Redis 单飞锁
D14access TTL保持 KenOAuth 默认 600s约每 9 分钟刷新一次
D15Admin完全由 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 服务边界

关注点归属访问方式
凭据是否正确KenOAuthIdentityService.Authenticate
账号是否可用KenOAuthIdentityService.GetUserStatus / AuthorityService.GetUser
会话是否存活KenOAuthAuthorityService.GetSession
组织内能否做某动作KenOAuthOrganizationService.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 只读投影

表关键字段来源
usersuid i64、username、lower_name、display_name、avatar_url、is_active、is_restricted、prohibit_login、bot、system、visibility、versionOIDC 回调 + GetUserPublic / BatchGetUserPublic + 对账
spacesid 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、versionOrganizationService 写成功后同请求双写 + 对账
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_atOrganizationService 写成功后同请求双写 + 对账

投影表只允许 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<S>)
  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       # 兼容路径

依赖:actix-web、serde、serde_json、jsonwebtoken、reqwest、tokio、time、tracing、config、export。

5.3 Claims

#[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<String>,
    #[serde(default, with = "scopes")]
    pub scope: Option<Vec<String>>,
    pub username: Option<String>,
    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<i32>,
    pub token_type: String,
}

sub 在 KenOAuth 中序列化为字符串,必须自定义反序列化;scope 支持空格分隔字符串与数组两种形态。

5.4 验签规则

检查accessservice
算法EdDSAEdDSA
签名键JWKS 按 kid 选取同
iss[oauth] issuer同
aud[oauth] audience(默认 gitdata.ai)chat.gitdata.ai
client_id必须等于 gitdata-web不适用
token_type"access""service"
JOSE typat+jwtJWT
exp / iatleeway_secs(默认 30)同
未知 kid限速刷新 JWKS(≥30s)同
JWKS 不可用有缓存用缓存;无缓存返回 Verifier同

5.5 Session 提取器

enum SessionState { Anonymous, Expired, Valid(AccessClaims) }

pub struct Session {
    state: SessionState,
    raw: Option<String>,
}

impl Session {
    pub fn uid(&self) -> Option<i64>;
    pub fn username(&self) -> Option<&str>;
    pub fn session_id(&self) -> Option<&str>;
    pub fn security_version(&self) -> Option<i32>;
    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 <access token>。
  • 无 cookie / 验签失败 → Anonymous。
  • 签名有效但 exp 已过 → Expired。
  • 提取器永不返回错误;鉴权由 require_uid / 中间件决定。

5.6 SessionUser 与 require_uid

pub trait SessionUser {
    fn uid(&self) -> Option<i64>;
}

impl SessionUser for Session { ... }

pub fn require_uid(session: &Session) -> Result<i64, SessionError>;

全仓 17 处 SessionUser 导入、204 处 require_uid 调用保持原样,仅 import 路径改为 jwt。

5.7 Cookie

Cookie值HttpOnlySameSitePathMax-Age
gitdata_sessionKenOAuth access token是Lax/token 剩余 TTL
gitdata_refreshKenOAuth refresh token是Strict/api/v1/auth30 天
  • Secure 由 [oauth] cookie_secure 控制;生产必须为 true。
  • Domain 由 [oauth] cookie_domain 控制,默认空(host-only)。

5.8 错误

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 兼容层

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<CachedJwks> + 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_grant400
token 端点 5xx502
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 文案。
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:

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))。

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 人日)

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 表变更

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

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
会话列表/吊销 HTTPGET /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<Arc<KenAuthClient>>(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.rsuid: Uuid → i64;删除 passwd_hash、passwd_version、password_last_changed_at、totp_*
lib/models/src/**(87 个文件)用户外键 Uuid → i64
lib/models/src/space/space_members.rsuser_id: i64

非 model 层:

文件改动
lib/export/src/authz.rsOnceLock<i64>、HashSet<i64>
lib/export/src/space_token.rsbot_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 人日)

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_tokenbot_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

映射:

OrganizationInfospaces
idid
sluglower_name
namename
descriptiondescription
avatar_urlavatar_url
owner_user_idowner_id
member_countnums_members
versionversion
OrganizationMemberInfospace_members
user_iduser_id
role / left_at / banned_atstatus
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.rsowner/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权限
OWNERowner全部(短路)
ADMINadminview_space, read, message, manage_members, manage_invites, manage_bans, manage_channels, manage_overwrites
MEMBERmemberview_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/setupenable
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)

表操作
usersuid 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-managementgroup_id → role_id;封禁字段;加入规则/申请字段
entities/*用户 id 由 string 改 number
shared/apiglobals.d.ts、apiDefinitions.ts 重新生成
createApis.ts401 拦截 + 单飞 refresh
shared/storagesetCacheOwner 参数改 number
i18n删除废弃文案,补新错误码

12. 配置与基础设施

12.1 GitData 配置

[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 本地开发

# 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
K1bot 字段 + EnsureBotUser2
K2协议接受1
K3MFA 注册3
K4组织扩展6
K5账号自助4
小计16.5

13.2 GitData

批次内容依赖人日
B0lib/kenauth + 基础设施K03
B0.5删除管理后台 + /internal 服务密钥—2
B1用户 ID 全面 i64—8
B2.0lib/jwtB04
B2.1webapi 接入B2.01.5
B2.2OIDC / refresh / logoutK5/B2.14
B2.3全局替换 + 删 SessionGuardB2.1/B11.5
B2.4删除旧登录链路K1/K2/K34
B2.5删除两个 crateB2.3/B2.41
B2.6app/im 服务令牌B2.02
B2.7前端B2.22
B2.8配置 / HelmB2.51.5
B3Git 凭据B25
B4Space 投影K4/B1/B215
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)

CodeHTTP前端处理
INVALID_ARGUMENT / MALFORMED_REQUEST400展示 message
INVALID_CREDENTIALS / UNAUTHENTICATED / TOKEN_*401跳登录
SECURITY_VERSION_STALE401清会话 + 跳登录
FORBIDDEN / LOGIN_FORBIDDEN / USER_DISABLED403展示 message
MFA_REQUIRED403跳二次验证
AGREEMENT_NOT_ACCEPTED403跳协议页
NOT_FOUND / USER_NOT_FOUND / ORGANIZATION_*_NOT_FOUND404展示 message
CONFLICT / *_TAKEN / *_EXISTS409展示 message
RATE_LIMITED / EMAIL_CODE_ATTEMPTS_EXCEEDED429展示 retry_after
UNAVAILABLE503提示稍后重试
INTERNAL / DATABASE_ERROR / CACHE_ERROR500通用错误

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
MFAsrc/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/**