- 版本控制
- 后端
【免费下载链接】lore
Lore is a next-generation, open source version control system
本篇围绕 Lore 的架构决策记录 ADR-00003(docs/developing/decisions/00003-auth-tokens-for-sub-repos.md)展开:当一个 Lore 仓库由多个拥有独立访问控制的子仓库(sub-repository)组成时,认证数据(auth data)应当以什么粒度表示。读完本文,你能理解该决策的三种候选方案及其取舍逻辑,并能在当前代码库中找到与之对应的实现证据——客户端的令牌交换流程、JWT 声明校验,以及服务端"认证与授权分离"的 gRPC 拦截器结构。
背景与问题陈述
Lore 仓库为了控制对受限内容的访问,可以由多个子仓库构成,每个子仓库拥有自己的访问控制(access controls)。在着手设计认证实现细节时,团队必须决定:认证数据如何表示一个仓库及其所有子仓库的访问关系?这正是 ADR-00003 要回答的问题(见文档 "Context and Problem Statement" 一节)。
决策驱动因素(Decision Drivers)有四个:
- 确保与所有必要的网络传输(network transports)兼容;
- 最小化客户端复杂度;
- 最小化服务端复杂度;
- 最小化授权验证(authorization verification)的开销。
这四个约束共同指向一个核心矛盾:令牌该"大而全",还是"小而多"。
三种候选方案
ADR 完整评估了三种表示认证数据的方式,本文完整继承其分析并展开。
方案一:单个令牌承载整个仓库的全部授权数据
在该模型中,客户端识别出 CLI 操作会涉及的所有仓库,并将"标识用户的令牌"交换为一个 JWT,该 JWT 描述用户对全部仓库的访问权限。ADR 给出了一个关键的容量估算:
- 每个仓库约需 18 字节授权数据(16 字节仓库 ID + 读、写各 1 字节标志位);
- 进入 JWT claim 后,base64 编码会使数据膨胀到每仓库 24 字节;
- 外推到一条命令影响 300 个仓库的退化场景,JWT 体量可进入7KB 量级;
- 而 HTTP 头部长度的普遍接受上限是8KiB,因此未来存在逼近该限制的现实风险。
该方案的利弊(引自 ADR):
- 优点:客户端逻辑简单,无需请求或切换多个令牌;
- 优点:QUIC 服务端逻辑简单,服务端收到单个令牌即可为连接上的所有流授权;
- 优点:gRPC 服务端逻辑简单,令牌作为每个 gRPC 请求的 metadata 携带并用于授权;
- 中性:服务端解析数百个仓库授权并高效检索存在一定开销;
- 缺点:令牌大小可能逼近或超出 HTTP 头部限制。
方案二:每个仓库一个令牌,客户端按需切换(选定方案)
客户端同样先识别出 CLI 操作涉及的所有仓库,但不请求覆盖全部仓库的单一 JWT,而是为每个仓库各请求一个 JWT。发送 QUIC 请求时,客户端会为每个仓库(也就是每个令牌)建立一条连接,但连接内仍可使用多个流(streams)。
该方案的利弊(引自 ADR):
- 优点:QUIC 服务端逻辑简单,单个令牌即可为连接上的所有流授权;
- 优点:gRPC 服务端逻辑简单,令牌作为每个 gRPC 请求的 metadata 携带并用于授权;
- 优点:令牌大小有界(bounded);
- 优点:服务端授权一个请求时,无需在未定长的仓库授权列表中检索;
- 缺点:客户端必须根据操作涉及的仓库数量管理多个令牌与多条连接。
方案三:令牌仅标识用户,服务端独立查授权
客户端完全不把令牌交换为仓库级令牌;每个被操作的仓库的授权,由服务端基于令牌所标识的用户独立验证。
- 优点:令牌大小有界;
- 优点:客户端逻辑简单;
- 缺点:服务端必须为每条建立连接执行授权逻辑,而这可能需要与外部服务协调或以其他形式做 IO 来确认"该认证用户被允许做什么"。
决策结果及其后果
ADR 最终选择方案二:"One auth token per repository, client swaps between auth tokens as necessary"。选择理由原文表述为:该方式既能处理认证而不破坏 HTTP 用例,又能最小化技术栈其他地方的复杂度。
决策带来的后果(Consequences):
| 方向 | 后果 |
|---|---|
| Good | 不存在生成大到超出标准 HTTP 头部限制的认证令牌的风险 |
| Good | 服务端围绕连接认证的逻辑无需改动 |
| Good | 服务端验证认证的开销极小 |
| Bad | 客户端需要为命令涉及的每个子仓库各换取一个令牌;在 QUIC 场景下还需为每个令牌各建立一条连接 |
换句话说,Lore 把复杂度有意压到了客户端(多令牌、多连接的管理),以换取服务端在认证热路径上的极简与令牌体量的恒定上界。
当前代码库中的实现证据
以下实现细节来自当前仓库源码,用于印证 ADR 中"令牌作为 gRPC 请求 metadata 携带""QUIC 连接按令牌建立"等设计点如何落地。
客户端:令牌交换与 JWT 声明校验
客户端认证入口在 lore-revision/src/auth/login.rs。其中exchange_token函数将外部令牌交换为 URC 认证令牌,随后立即执行verify_jwt_usage_for_remote,把解码后的 JWT claims 与目标远程(remote)的域名做匹配,再写入加密令牌存储(token_store::store_user_token)。with_token则根据token_type区分两条路径:lore类型令牌直接校验存储,其余类型走交换流程。交互式登录(interactive)通过clientState+ 轮询(poll_interactive_session,默认 30 次、间隔 5 秒)完成浏览器登录流。
令牌"该发给谁"的约束由 audience 声明决定。lore-credential/src/jwt.rs 中:
JWTUserInfo解析iss、sub、aud、exp等标准声明,其中aud被定义为"根域名列表";acceptable_root_domains返回"签发者域名 + audience 中的根域名"合集;domain_in_root_domains强制标签边界匹配而非裸后缀匹配——ADR 关联的威胁模型注释(见 lore-revision/src/auth.rs 顶部 "General Notes for Auth token handling")指出:攻击者可以搭一个把 Epic 受控 URC Auth 服务声明为认证提供方的恶意仓库,诱骗用户把令牌发给自己的服务器;因此"令牌只应给到令牌 audience 字段列出的域名"。domain_in_root_domains通过要求.epicgames.net形式的边界匹配,保证evilepicgames.net这类仿冒域名无法通过校验;verify_jwt_usage_for_remote在域名不匹配时直接拒绝并记录 "forbidding JWT leak"。
这套机制与 ADR 方案二"每个仓库一个有界令牌"相配合:客户端持有的是面向特定仓库域名的令牌,audience 校验则防止令牌被跨域滥用。
服务端:认证拦截器只做认证,授权交给下游
ADR 提到"gRPC 服务端逻辑简单,令牌作为每个 gRPC 请求的 metadata 携带"。当前实现中,lore-server/src/auth/jwt_interceptor.rs 的JWTInterceptor实现了 tonic 的Interceptor:从 metadata 中提取Bearer令牌(extract_bearer_token),用JwtVerifier验证签名(缓存命中走同步热路径,block_in_place兜底),然后把原始令牌(RawToken)与解析后的声明(AuthorizationToken)作为 extensions 插入请求,交给下游处理器。
该文件内的测试用例明确固化了 ADR 隐含的认证/授权两阶段分离:
an_authenticated_token_with_no_partition_grant_passes_the_interceptor:即使令牌对某分区(partition,即仓库)没有任何授权(grant),拦截器也放行——"分区决策是下游 authorizer 的事";a_request_with_no_bearer_token_is_unauthenticated:无令牌返回Unauthenticated;a_token_failing_on_its_own_claims_is_refused:声明自证失败的令牌(如过期)被拒绝,且错误细节不外泄(统一返回 "Not allowed",原因只进日志)。
真正的仓库级授权发生在authnz层,见 lore-server/src/authnz/:repository_authorizer.rs、resource_grants_authorizer.rs、global_grants_authorizer.rs等按分区/资源维度检查用户授权。这与 ADR 方案二的"授权无需在未定长列表中检索"一致——每次请求只对应一个分区,授权判断是针对单仓库的。
gRPC 请求如何标注"针对哪个仓库"
方案二要求每个请求/连接对应单一仓库,代码中这一"分区"语义由 metadata 键承载。lore-transport/src/grpc/mod.rs 定义了:
pub const PARTITION_ID_KEY: &str = "lore-partition-bin";存储客户端在发起请求时将该键的二进制值(16 字节RepositoryId/Context)追加到 metadata 中(见 lore-transport/src/grpc/storage_client.rs 中append_bin(PARTITION_ID_KEY, ...)的用法)。因此 gRPC 路径上的"令牌 + 分区"组合,正是 ADR 所描述的"令牌作为每个 gRPC 请求的 metadata 携带并用于授权"的具体形态:令牌负责认证身份,lore-partition-bin负责划定该次操作的仓库边界,authnz层再做单仓库授权判定。
小结
ADR-00003 的核心权衡可以概括为一句话:用客户端的多令牌/多连接管理成本,换取令牌的恒定体量上界与服务端认证热路径的极简。它否定了"超大 JWT"方案(7KB 逼近 8KiB 头部上限的容量风险)和"服务端逐次查授权"方案(每条连接引入外部 IO)。结合当前源码可以看到,该决策落地为三层结构:客户端侧的令牌交换与 audience 域名校验(lore-revision/src/auth/login.rs、lore-credential/src/jwt.rs),gRPC 请求上的lore-partition-bin分区 metadata(lore-transport/src/grpc/mod.rs),以及服务端认证拦截器与authnz授权层的职责切分(lore-server/src/auth/jwt_interceptor.rs、lore-server/src/authnz/)。研究 Lore 的认证体系时,这条 ADR 是理解"为什么令牌这么小、为什么认证和授权分开、为什么每个请求都带分区"的关键入口。
- 版本控制
- 后端
【免费下载链接】lore
Lore is a next-generation, open source version control system
相关推荐
Gitea Actions 令牌权限体系设计:多级钳制(Clamping)、跨仓库访问控制与令牌生命周期解析
Gitea Actions 令牌权限体系设计:多级钳制(Clamping)、跨仓库访问控制与令牌生命周期解析 本文基于 Gitea 仓库中的设计文档 token
后端代码托管研发协作CI/CDpnpr OCI 仓库级作用域令牌认证:`oci.bearerAuth` 配置完全指南
pnpr OCI 仓库级作用域令牌认证: oci.bearerAuth 配置完全指南 导读 本指南围绕 pnpr(pnpm 仓库自带的 OCI 镜像分发服务)新
包管理器开发工具CLI如何在Proxmox VE Helper-Scripts中实现API认证令牌的细粒度权限控制:完整指南
如何在Proxmox VE Helper Scripts中实现API认证令牌的细粒度权限控制:完整指南 Proxmox VE Helper Scripts(Co
运维虚拟化DevOps
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考