ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Bytebase MCP resolveDatabase 权限模型修复:从工作区级查询到项目级查询的工程实践

Bytebase MCP resolveDatabase 权限模型修复:从工作区级查询到项目级查询的工程实践 Bytebase MCP resolveDatabase 权限模型修复从工作区级查询到项目级查询的工程实践【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase导读本文基于 Bytebase 仓库中的设计计划文档 docs/plans/2026-04-02-fix-resolve-database-permissions.md剖析 MCPquery_database工具中resolveDatabase数据库解析逻辑的权限模型缺陷与完整修复方案。修复的核心思路是放弃需要工作区级bb.databases.list权限的workspaces/-批量查询改为先列出用户可见项目再按项目逐个查询数据库的两步走策略从而让默认的WorkspaceMember角色以及通过 OAuth 新建的用户也能正常使用query_database工具。读完本文你将掌握 Bytebase MCP 内部 API 调用的完整链路、CEL 过滤器的组合方式、以及如何为基于项目作用域的数据库解析编写可回归的 mock 测试。问题背景为什么workspaces/-通配符查询会失败Bytebase 的 MCP Server 暴露了query_database工具允许 AgentLLM直接对数据库执行 SQL 查询。该工具在执行查询前需要通过resolveDatabase把用户传入的数据库名称解析为唯一的数据库资源instances/{instance}/databases/{database}或项目实例的完整规范名。原始实现通过DatabaseService/ListDatabases接口、以parent: workspaces/-一次性拉取整个工作区的数据库列表。根据计划文档的分析这种做法存在三个层面的问题通配符不合法workspaces/-中的-并不能匹配真实的 workspace ID它不是被支持的资源通配符权限不满足即便换成真实的工作区 IDWorkspaceMember角色新建用户与 OAuth 用户的默认角色在工作区作用域下并不具备bb.databases.list权限访问模型错配绝大多数用户是通过项目成员关系访问数据库的而不是通过工作区级权限工作区级查询与实际的 RBAC 访问模型相悖。这一点可以从仓库中的权限常量得到印证在 backend/common/permission/permission.go 中定义了DatabasesList bb.databases.list第 35 行与ProjectsList bb.projects.list第 82 行而 MCP 内部使用的 OpenAPI 定义 backend/api/mcp/gen/openapi.yaml 中对ListDatabases的parent参数权限矩阵描述得非常清楚projects/{project}列出项目内数据库需要bb.projects.get权限workspaces/{id}列出工作区数据库需要bb.databases.list权限instances/{instance}列出实例内数据库需要bb.instances.get权限。可见项目作用域的数据库列举权限门槛bb.projects.get远低于工作区作用域bb.databases.list这正是本次修复的底层依据。解决方案N1 两步查询架构计划的解决方案是把一次工作区级查询替换为两步查询调用ProjectService/ListProjects获取当前用户有权访问的所有项目无需特殊权限接口按成员关系返回项目对每个项目调用DatabaseService/ListDatabasesparent使用projects/{id}并沿用既有的 CEL 过滤器。这是一个 N1 次的调用模式但计划文档明确指出其代价可控N项目数量通常很小且服务端过滤器能把响应体压缩到最小。如果用户显式提供了project输入参数则可以跳过第一步直接查询该指定项目。这套方案在架构上与 Bytebase 的 RBAC 模型完全对齐大多数用户拥有的是项目级访问权限而不是工作区级bb.databases.list。修复前后的对比维度修复前修复后查询入口单次ListDatabases(parentworkspaces/-)ListProjects 逐项目ListDatabases(parentprojects/{id})所需权限bb.databases.list工作区级bb.projects.get项目级默认角色通常已具备指定项目时的路径依赖过滤器中的project 子句直接通过parent字段按项目查询可跳过第一步调用次数1 次N1 次N 为可见项目数通常很小权限拒绝处理直接失败并提示申请bb.databases.list单个项目拒绝不致命跳过继续Task 1将 resolveDatabase 重构为项目作用域查询计划文档将主实现修改定位在 backend/api/mcp/tool_query.go。需要注意的是当前仓库中resolveDatabase的实际实现位于 backend/api/mcp/tool_resolve.go第 219–225 行其签名是resolveDatabase(ctx, database, instance, project string)内部调用s.listDatabases(ctx, buildDatabaseFilter(...))再进入matchDatabases做分级匹配。计划文档给出了目标形态的完整代码分三步落地。Step 1新增listProjects辅助方法// listProjects returns project resource names the user has access to. func (s *Server) listProjects(ctx context.Context) ([]string, error) { resp, err : s.apiRequest(ctx, /bytebase.v1.ProjectService/ListProjects, map[string]any{}) if err ! nil { return nil, errors.Wrap(err, failed to list projects) } if resp.Status 400 { return nil, errors.Errorf(failed to list projects: %s, parseError(resp.Body)) } var result struct { Projects []struct { Name string json:name } json:projects } if err : json.Unmarshal(resp.Body, result); err ! nil { return nil, errors.Wrap(err, failed to parse project list) } names : make([]string, 0, len(result.Projects)) for _, p : range result.Projects { names append(names, p.Name) } return names, nil }该方法返回的是项目的完整资源名形如projects/hr-system后续可直接用作ListDatabases的parent。OpenAPI 定义中ListProjects的响应结构为bytebase.v1.ListProjectsResponse见 backend/api/mcp/gen/openapi.yaml 中第 3992 行起的端点定义字段名projects、元素字段name与上面的解析结构一致。Step 2新增listDatabasesInProject辅助方法// listDatabasesInProject returns databases matching the filter in a project. func (s *Server) listDatabasesInProject(ctx context.Context, project, filter string) ([]databaseEntry, error) { body : map[string]any{ parent: project, filter: filter, pageSize: 1000, } resp, err : s.apiRequest(ctx, /bytebase.v1.DatabaseService/ListDatabases, body) if err ! nil { return nil, err } // Permission denied on a project is not fatal — skip it. if resp.Status 400 { return nil, nil } var listResp listDatabasesResponse if err : json.Unmarshal(resp.Body, listResp); err ! nil { return nil, err } return listResp.Databases, nil }这里有一个非常关键的容错设计单个项目上的权限拒绝HTTP ≥ 400不视为致命错误而是直接跳过该项目继续查询。用户可能对部分项目只有有限权限如果某个项目的查询失败就整体失败会退化为全有或全无的糟糕体验。databaseEntry、listDatabasesResponse等类型定义在 backend/api/mcp/tool_resolve.go第 42–66 行可以直接复用。Step 3重构resolveDatabase主体func (s *Server) resolveDatabase(ctx context.Context, input QueryInput) (*resolvedDatabase, error) { filter : buildDatabaseFilter(input) var allDatabases []databaseEntry if input.Project ! { // User specified a project — query it directly. databases, err : s.listDatabasesInProject(ctx, projects/input.Project, filter) if err ! nil { return nil, errors.Wrap(err, failed to list databases) } allDatabases databases } else { // List users projects, then query each for matching databases. projects, err : s.listProjects(ctx) if err ! nil { return nil, err } for _, project : range projects { databases, err : s.listDatabasesInProject(ctx, project, filter) if err ! nil { return nil, errors.Wrap(err, failed to list databases) } allDatabases append(allDatabases, databases...) } } // ... rest of tiered matching unchanged ... }重构后的控制流非常清晰用户指定了project直接以projects/{id}作为parent发起单次查询走捷径分支未指定project先listProjects拿到可见项目列表再逐项目聚合查询结果。聚合完成后的分级匹配tiered matching逻辑保持不变——即先精确匹配matchExact、再大小写不敏感匹配matchCaseInsensitive、最后子串匹配matchSubstring这三段实现均位于 backend/api/mcp/tool_resolve.go第 396–429 行。多项目聚合天然放大了同名数据库可能存在于多个项目/实例的歧义概率而这些歧义由既有的formatAmbiguousResultAMBIGUOUS_TARGET与 MCP 的 input request 交互elicitDatabaseChoice兜底处理不需要额外改动。Step 4从buildDatabaseFilter中移除project子句由于项目过滤已经从过滤器迁移到了parent字段按项目逐个查询buildDatabaseFilter中不再需要生成project projects/xxx子句只保留instance过滤器。当前仓库中 backend/api/mcp/tool_resolve.go 第 68–79 行的buildDatabaseFilter(database, instance, project string)实现为func buildDatabaseFilter(database, instance, project string) string { // name.contains does substring matching server-side. filter : fmt.Sprintf(name.contains(%q), database) if instance ! { filter fmt.Sprintf( instance %q, formatInstanceFilter(instance)) } if project ! { filter fmt.Sprintf( project %q, formatProjectFilter(project)) } return filter }对应地backend/api/mcp/tool_query_test.go 中的TestBuildDatabaseFilter第 182–227 行目前仍断言project参数会生成project projects/hr-system子句改造时这些用例需要同步更新。注意formatInstanceFilter的兼容性细节backend/api/mcp/tool_resolve.go 第 84–89 行裸 ID如prod-pg会被格式化为instances/prod-pg这种工作区实例简写用于向后兼容而规范的项目实例名projects/hr-system/instances/prod-pg会被原样保留不能用作工作区实例寻址。这与query_database工具描述中instance 可以是工作区实例 ID/名称也可以是项目实例的完整规范名的语义一致见 backend/api/mcp/tool_query.go 第 46–63 行的工具描述。Step 5测试、Lint 与验证计划文档给出的验证命令go test -count1 ./backend/api/mcp/... golangci-lint run --allow-parallel-runners ./backend/api/mcp/...预期结果是测试会失败因为 mock server 还没有处理ListProjects端点——这正是 Task 2 要修复的内容。Task 2更新测试 mock支持项目作用域解析Step 1mockListDatabases同时服务两个端点backend/api/mcp/tool_query_test.go 中的mockListDatabases第 49–81 行当前只处理ListDatabases。计划要求它按 URL 路径路由同时响应ListProjects与ListDatabasesfunc mockListDatabases(databases []map[string]any) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set(Content-Type, application/json) switch r.URL.Path { case /bytebase.v1.ProjectService/ListProjects: _ json.NewEncoder(w).Encode(map[string]any{ projects: []map[string]any{{name: projects/test-project}}, }) case /bytebase.v1.DatabaseService/ListDatabases: // existing filter logic unchanged default: w.WriteHeader(http.StatusNotFound) } }) }值得指出的是当前仓库中的mockListDatabases已经具备按parent作用域过滤的能力workspaces/前缀的请求返回全部数据库项目作用域的请求则按db[project] reqBody.Parent过滤第 59–71 行。这意味着重构后mock 中projects/test-project下的数据库需要有对应的project: projects/test-project字段才能被正确命中现有测试数据如makeDatabase构造的projects/hr-system只需保持 project 字段与 ListProjects 返回的项目名一致即可。同时applyMockFilter第 85–140 行支持name.contains(...)、instance ...与project ...三种子句的简化过滤重构后若过滤器不再携带project 子句该分支保留也不会产生副作用。Step 2mockQueryServer自动生效mockQueryServer第 169–180 行对非SQLService/Query路径的请求统一委托给mockListDatabases因此只要mockListDatabases更新完成mockQueryServer无需任何改动即可自动获得ListProjects的响应能力。Step 3全量测试与 Lintgo test -v -count1 ./backend/api/mcp/... golangci-lint run --allow-parallel-runners ./backend/api/mcp/...预期结果全部测试通过0 个 lint 问题。测试架构补充内存内部传输newTestServerWithMockbackend/api/mcp/tool_http_test.go 第 20–27 行创建的测试 Server 使用newInternalAPIClient(handler)把内部传输指向内存中的 handler——这与生产环境完全同构。生产环境中apiRequestbackend/api/mcp/tool_http.go 第 39–102 行通过internalRoundTripper在进程内直接调度到内部 handler 链请求以 Connect-RPC POST 形式发送并携带在/mcp边界铸造的**委托凭证delegated credential**而非入站 bearer token。也就是说MCP 工具发起的每一次内部 API 调用其身份验证、审计User-Agent: bytebase-mcp/internal、X-Real-IP溯源都走完整的内部门禁这也是本次权限修复要在调用什么接口层面做文章的根本原因——权限校验发生在内部 API 链路上MCP 工具自身无法绕过。Task 3提交与 CI 验证计划文档给出了提交信息模板fix(mcp): use project-scoped queries for database resolution The default WorkspaceMember role doesnt have bb.databases.list at workspace scope. List users projects first, then query databases per project with the existing CEL filter.提交后推送远端并确认 CI 全部通过golangci-lint、SonarCloud、go-tests。变更总结动作文件修改backend/api/mcp/tool_query.go— 新增listProjects、listDatabasesInProject重构resolveDatabase更新buildDatabaseFilter修改backend/api/mcp/tool_query_test.go— 更新 mock 以处理ListProjects端点深入源码本方案依赖的底层机制1.apiRequest与内部 API 调用链计划中所有辅助方法都基于s.apiRequest(ctx, path, body)发起调用。它的实现要点backend/api/mcp/tool_http.go使用internalAPIBaseURL https://mcp-internal.bytebase.invalid作为名义地址配合内存传输请求不会触达真实网络每次调用都会为当前委托身份铸造一次性内部凭证auth.GenerateInternalMCPToken入站 bearer 在/mcp边界即被隔离每次调用受internalAPITimeout 30s上下文超时约束resolveTarget外层还有resolveTimeout 30s的解析超时backend/api/mcp/tool_resolve.go 第 17 行。因此listProjects 逐项目listDatabasesInProject的 N1 模式在最坏情况下总耗时受单次internalAPITimeout限制每步调用串行、各自有超时兜底不会出现无界等待。2. 权限常量与 OpenAPI 权限矩阵的对应计划文档中默认 WorkspaceMember 角色没有bb.databases.list的论断与 OpenAPI 中ListDatabases的parent权限描述一致见上文且bb.databases.list/bb.projects.list常量均定义于 backend/common/permission/permission.go。从源码结构可以推断项目级parentprojects/{project}只要求bb.projects.get而项目成员WorkspaceMember在加入项目后即具备项目级读权限因此项目作用域查询能让默认角色的 MCP 用户顺利解析数据库。3. 修复后仍需保留的防御逻辑重构只改如何列举数据库不改查询与掩码链路。以下既有行为在修复后依然生效数据源选择selectDataSource优先选择READ_ONLY数据源无只读源时才回退ADMINbackend/api/mcp/tool_resolve.go 第 449–461 行掩码数据保护掩码列读回为******包含该占位符的语句会被SQLService/Query拒绝工具描述与TestQueryDatabase_PolicyDenialGetsNoRoleAdvice用例backend/api/mcp/tool_query_test.go 第 670–700 行策略拒绝不引导越权当 403 来自 MCP 执行链IsPolicyRefusal判定backend/api/mcp/tool_query.go 第 341–354 行时错误提示不会给出申请项目 SQL Editor 角色之类的建议因为项目角色无法解除工作区级设置分页处理原listDatabases按NextPageToken循环拉取backend/api/mcp/tool_resolve.go 第 100–151 行项目作用域改造后同样应保留分页循环逻辑计划代码以pageSize: 1000单页为主若项目内数据库超过 1000需沿用 token 翻页。总结resolveDatabase的权限模型修复本质上是让 MCP 工具的数据库发现逻辑与 Bytebase 的 RBAC 访问模型对齐从假设用户拥有工作区级bb.databases.list转为尊重用户通过项目成员关系获得的项目级访问。修复带来的收益默认WorkspaceMember角色与 OAuth 新建用户可以正常使用query_database工具用户显式指定project时走捷径分支只发起一次查询体验更优单项目权限拒绝被降级为可跳过的非致命错误多项目环境下鲁棒性更强分级匹配、歧义澄清AMBIGUOUS_TARGET/ input request、掩码保护、策略拒绝等既有能力全部原样保留改动面可控。从实现与测试的闭环看listProjects/listDatabasesInProject两个辅助方法与apiRequest内存传输、mock 按 URL 路由的测试基建相互配合形成了权限对齐、容错跳过、测试可回归的完整方案。相关实现细节可进一步查阅 backend/api/mcp/tool_query.go、backend/api/mcp/tool_resolve.go、backend/api/mcp/tool_query_test.go 与 backend/api/mcp/tool_http.go。【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表