Bytebase Store 层 WorkloadIdentityConfig 持久化验证:protojson 与 PostgreSQL JSONB 的零改动兼容机制
【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase
导读
本文围绕 Bytebase 后端 Store 层的WorkloadIdentityConfig验证任务展开,核心结论是:借助 Protocol Buffers 的protojson序列化机制与 PostgreSQL JSONB 列的自描述特性,Store 层对 WORKLOAD_IDENTITY 类型主体(principal)的配置读写无需任何代码改动即可自动兼容。读完本文,你将掌握 Bytebaseprincipal.profileJSONB 列的读写调用链、UserMessage结构体的数据流、以及如何用测试验证一个 proto 新字段在既有存储路径上的端到端生效。
任务背景:WORKLOAD_IDENTITY 主体类型的引入
Bytebase 将访问主体(principal)划分为三类,定义于 proto/store/store/user.proto:
END_USER:人类用户;WORKLOAD_IDENTITY:外部 CI/CD 工作负载身份(如 GitHub Actions、GitLab CI 通过 OIDC 换取临时凭证);SERVICE_ACCOUNT:调用 Bytebase OpenAPI 的外部服务。
为支持 WORKLOAD_IDENTITY 主体,需要为其携带一份 OIDC 配置(签发者、受众、subject 匹配规则等),即WorkloadIdentityConfig。Task 6 的核心问题非常聚焦:在不改动 Store 层任何业务代码的前提下,这份配置能否随既有用户档案(UserProfile)读写机制自动持久化?验证结果是肯定的。
验证结论总览:四个关键检查点全部通过
关联设计文档 task6-store-layer-verification.md 记录了 Store 层四个关键位置的逐一核验,全部标记为"无需改动":
| 检查点 | 位置 | 结论 |
|---|---|---|
UserMessage结构体 | Profile *storepb.UserProfile字段 | ✅ 无需改动 |
listUserImpl读取函数 | 扫描profile列并用 protojson 反序列化 | ✅ 无需改动 |
CreateUser创建函数 | nil 初始化后 protojson 序列化入库 | ✅ 无需改动 |
UpdateUser更新函数 | profile 非 nil 时整体序列化覆盖 | ✅ 无需改动 |
核心机制:protojson 与 JSONB 列的双向自动编解码
整个验证成立的根基,是 Bytebase Store 层统一采用Protocol Buffers 的 JSON 映射(protojson)来读写 PostgreSQL 的 JSONB 列,而不是手写逐字段的 JSON 转换。这条约定贯穿了所有 principal 相关读写路径。
读取路径:listUserImpl 与 scanPrincipalRow
在 backend/store/principal.go 的listUserImpl中,SQL 查询将principal.profile列扫描进profileBytes,随后:
profile := storepb.UserProfile{} if err := common.ProtojsonUnmarshaler.Unmarshal(profileBytes, &profile); err != nil { return nil, err } userMessage.Profile = &profile同样,scanPrincipalRow(backend/store/principal.go)作为UpdateUser、UpdateUserMFAConfigIfPending等路径共用的行扫描器,也以完全相同的方式反序列化 profile。这意味着:只要UserProfileproto 消息新增了字段,反序列化结果就会自动携带该字段,读取侧零改动。
写入路径:CreateUser 与 UpdateUser
创建用户时(backend/store/principal.go),先做 nil 兜底初始化,再整体序列化:
if create.Profile == nil { create.Profile = &storepb.UserProfile{} } profileBytes, err := protojson.Marshal(create.Profile)随后INSERT INTO principal (..., profile) VALUES (..., ?)以 JSONB 形式落库。因此只要调用方在create.Profile.WorkloadIdentityConfig中填好配置,序列化时自动包含,无需 Store 层感知。
更新用户时(backend/store/principal.go),profile 采用整体覆盖策略:
if v := patch.Profile; v != nil { profileBytes, err := protojson.Marshal(v) if err != nil { return nil, err } set.Comma("profile = ?", profileBytes) }即更新 WorkloadIdentityConfig 完全复用既有的 profile 更新通道——调用方构造携带新配置的UserProfile补丁即可,Store 层只负责"序列化整块 JSONB 并覆盖"。
UserProfile 与 WorkloadIdentityConfig 的消息定义
验证所依据的 proto 定义位于 proto/store/store/user.proto。UserProfile承载用户档案:
message UserProfile { google.protobuf.Timestamp last_login_time = 1; google.protobuf.Timestamp last_change_password_time = 2; // The source indicates where the user comes from. For now we support Entra ID SCIM sync, so the source could be Entra ID. string source = 3; reserved 4; // The workspace resource ID the user last logged into. string last_login_workspace = 5; }WorkloadIdentityConfig是独立的 OIDC 配置消息,也是验证的核心数据载体:
message WorkloadIdentityConfig { enum ProviderType { PROVIDER_TYPE_UNSPECIFIED = 0; GITHUB = 1; GITLAB = 2; OIDC = 3; } ProviderType provider_type = 1; string issuer_url = 2; // OIDC issuer URL repeated string allowed_audiences = 3; // 令牌校验的允许受众 string subject_pattern = 4; // 匹配令牌 subject 声明 string jwks_url = 5; // 可选 JWKS 端点,为空时用 issuer_url 做 OIDC discovery }说明:原验证文档描述的
UserProfile中内嵌workload_identity_config = 4字段属于该任务进行时的中间状态;从当前仓库源码看,该字段位置已标记为reserved 4,WorkloadIdentityConfig演进为独立消息,且工作负载身份已拆分到独立数据表(见下文演进章节)。这恰好从侧面印证了 protojson 机制的健壮性——无论字段如何迁移,JSON 映射始终自描述。
数据库存储格式:JSONB 中的 camelCase 细节
WorkloadIdentityConfig落库于principal.profileJSONB 列,文档给出了完整存储样例:
{ "lastLoginTime": "2024-12-11T10:00:00Z", "lastChangePasswordTime": "2024-12-11T09:00:00Z", "source": "", "workloadIdentityConfig": { "providerType": "PROVIDER_GITHUB", "issuerUrl": "https://token.actions.githubusercontent.com", "allowedAudiences": ["https://github.com/myorg"], "subjectPattern": "repo:myorg/myrepo:ref:refs/heads/main" } }一个必须牢记的实操细节:protojson 对字段名使用 camelCase 而非 proto 中的 snake_case。例如workload_identity_config在 JSONB 中写作workloadIdentityConfig,provider_type写作providerType。如果你直接在 SQL 或脚本中按 snake_case 键名查询/修改 JSONB,将无法命中数据——这也是 Store 层始终用 protojson 而不是手写 JSON 的根本原因。仓库中ConsumeRecoveryCode(backend/store/principal.go)等函数在 SQL 层直接操作mfa_config->'recoveryCodes'时,同样显式注释了"protojson 的拼写"这一约束,可见该约定是 Store 层的全局规则。
测试验证:字段可访问性与 CRUD 通道
原验证文档新增了三个测试并全部通过:
- TestWorkloadIdentityConfigInUserMessage:验证 WorkloadIdentityConfig 可通过
UserMessage.Profile访问; - TestCreateUserMessageWithWorkloadIdentityConfig:验证
CreateUser能携带 WorkloadIdentityConfig; - TestUpdateUserMessageWithWorkloadIdentityConfig:验证
UpdateUser能完成配置更新。
在当前仓库中,对应的持久化测试演进为 backend/store/workload_identity_test.go,覆盖WorkloadIdentityMessage、CreateWorkloadIdentityMessage、UpdateWorkloadIdentityMessage三者的 Config 字段,例如:
config := &storepb.WorkloadIdentityConfig{ ProviderType: storepb.WorkloadIdentityConfig_GITHUB, IssuerUrl: "https://token.actions.githubusercontent.com", AllowedAudiences: []string{"https://github.com/myorg"}, SubjectPattern: "repo:myorg/myrepo:ref:refs/heads/main", }运行验证方式(在仓库根目录执行):
go test ./backend/store/ -run TestWorkloadIdentity -v原文档给出的通过输出形态为ok github.com/bytebase/bytebase/backend/store,配合 gofmt 与 golangci-lint 零告警,共同构成了验证任务的完成判据。
从验证到落地:当前仓库的存储演进
从当前仓库源码结构看,该验证任务的结论已被后续任务继承并进一步演进——工作负载身份不再仅作为 END_USER 档案的内嵌 JSONB 段,而是拥有独立的表结构与 Store 层实现:
- 建表:迁移脚本 backend/migrator/migration/3.16/0000##split_principal_table.sql 将 principal 表拆分为 principal(仅 END_USER)、service_account、workload_identity 三张表,其中
workload_identity表以config jsonb NOT NULL DEFAULT '{}'存储WorkloadIdentityConfig,并附注释标明其对应 proto/store/store/user.proto 中的消息; - Store 层 CRUD:backend/store/workload_identity.go 提供
WorkloadIdentityMessage、GetWorkloadIdentityByEmail、ListWorkloadIdentities、CreateWorkloadIdentity、UpdateWorkloadIdentity、DeleteWorkloadIdentity(软删除)等完整能力,其中邮箱格式约束为{name}@{project-id}.workload.bytebase.com或{name}@workload.bytebase.com; - 编解码延续:独立表的读写依然复用了同一套 protojson 机制——
CreateWorkloadIdentity用protojson.Marshal(create.Config)写入,读取时用common.ProtojsonUnmarshaler.Unmarshal(configBytes, &config)还原,与 Task 6 验证的机制完全同源。
API 层的配置校验闭环
Store 层只负责持久化,配置合法性由 API 层把关。在 backend/api/v1/workload_identity_service.go 的validateWorkloadIdentityConfig中:
provider_type必须为 GITHUB / GITLAB / OIDC 之一;issuer_url必填并通过wif.ValidateIssuerURL校验;jwks_url可选,提供时需通过wif.ValidateJWKSURL;allowed_audiences非空且不允许空字符串;subject_pattern通过wif.ValidateSubjectPattern校验。
该函数与 backend/plugin/idp/wif/wif.go 中的校验实现共同构成了"API 校验 → Store 持久化 → 登录换取凭证"的完整链路,与 Task 6 的 Store 层验证结果衔接。
结论与后续步骤
Task 6 的验证给出了一个可复用的工程结论:在 protojson + JSONB 的持久化架构下,为 proto 消息新增字段不需要改动任何存储层代码,只需保证读写两侧使用同一套 proto 定义与编解码器。验证通过判据为四条:代码编译无错误、全部测试通过、golangci-lint 零问题、gofmt 已格式化。
原文档规划的后续步骤(Task 8:UserService 层 workload identity CRUD API)在仓库中亦已落地,体现在 backend/api/v1/workload_identity_service.go 的CreateWorkloadIdentity/GetWorkloadIdentity/ListWorkloadIdentities/UpdateWorkloadIdentity/DeleteWorkloadIdentity/UndeleteWorkloadIdentity六个 RPC 以及 backend/tests/login_audit_test.go、backend/api/v1/workload_identity_service_test.go 等集成测试。对后续开发者而言,若要新增类似的可配置主体字段,照搬这套"proto 字段扩展 + protojson 编解码 + JSONB 落库 + API 校验"的模式即可。
【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考