Repository navigation
Conversation
query_database, get_schema and propose_database_change find a database by listing at the workspace, which needs workspace-wide bb.databases.list. Project roles don't grant it, so a user whose access comes only from project roles got PERMISSION_DENIED from all three tools. A caller refused at the workspace is now listed through the projects it can read: the project it named, or each project SearchProjects returns. A caller with workspace-wide access still lists once, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arch A named project is now listed under that project for every caller, so a project-only caller who names one makes one call instead of a refused workspace call first. The server's answer for that project comes through: PERMISSION_DENIED naming the project if the caller holds no role there, DATABASE_NOT_FOUND if it does not exist. Without a named project, a project the caller loses access to between the search and its listing now contributes nothing, as a project deleted in that window already did, instead of failing the whole resolve. Tests pin the search path across two projects, the filter on that path, a project refused after the search, the named-project answers against a live server, and a project developer proposing a change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Avoid the per-project database-list fallback
For a caller without workspace-wide bb.databases.list, this path first receives a denied workspace request, then paginates SearchProjects and makes a sequential ListDatabases request for every readable project. That can exceed the resolver's 30-second deadline in a workspace with many projects, and even a caller with no project access must scan the project list before receiving PERMISSION_DENIED.
Could we use a different API shape here? In particular, consider extending ListDatabases to accept parent = "projects/-" and return only databases from projects where the caller holds bb.projects.get. The resolver could then fall back from the denied workspace request to one wildcard request, rather than enumerating projects and issuing one database-list request per project.
…ct-role lookups to #21551 The recheck found the in-result advice firing for writes and EXPLAIN, which no grant lifts: a grant's statement must be read-only, and a granted target still needs bb.sql.explain. The advice now needs bb.sql.select or bb.sql.info; other refusals pass through alone. The resolver fallback duplicated open #21551, whose reviewer asked for ListDatabases to accept projects/- instead of a per-project loop, so it is removed here. The account line now opens any existing server instructions instead of replacing them, for #21552. The service-account row runs in a workspace that admits all users, so only the principal type keeps it out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Ships in the same release as #21552, which removes the MCP
queryskill andget_skilland adds server instructions.query_database,get_schemaandpropose_database_changefind a database by name. To do that, they list databases at the workspace, and that listing needsbb.databases.listat the workspace level. Project roles don't grant it, and neither does Workspace member. So a user whose access comes only from a project role, such as a project developer or a SQL Editor user, got PERMISSION_DENIED from all three tools, even for a database in their own project.After this change:
SearchProjectsreturns for the caller. A database in a project the caller has no role in stays DATABASE_NOT_FOUND, the same answer as for a name that matches nothing. A caller who can read no project at all gets PERMISSION_DENIED for every name, because it can list no databases.ListDatabasesanswers 404 for a missing project before it checks permission, andcall_apialready shows the same caller the same 403 and 404.The server checks
bb.projects.getin the project for each project listing, and the console's project Databases page makes the same call. So the tools resolve exactly the databases the caller can list through the API, and nothing more. A caller with workspace-levelbb.databases.list(workspace admin, DBA) still makes one listing call. A refusal by the MCP access policy comes back as before, with no fallback.This ships with that PR, which deletes the
queryskill. Until this fix, a project-only user could query through MCP only withcall_apiand a full database name.Why the project loop comes back
#19823 replaced a
SearchProjectsloop with the single workspace call, at ecmadao's request: the loop had been working around a "workspace mismatch" 403 that the workspace's full name fixes. The workspace call also needs workspace-levelbb.databases.list, so project-only users lost access; Codex's last P1 on #19823 named exactly this. Here the loop runs only as the fallback after the caller's permissions refuse the workspace call. Workspace-level callers keep the single call.A cleaner shape would remove the loop: let
ListDatabasesaccept the AIP-159 wildcardprojects/-and return the databases in the projects the caller can read, asSearchIssuesalready does withgetProjectIDsSearchFilter. Then every resolve is one call. That changes the documented parent forms ofListDatabasesRequestinproto/, so it isn't in this PR.What a resolve costs
ListDatabasescall.ListDatabasescall at the workspace, oneSearchProjectscall per 1,000 projects in the workspace, and oneListDatabasescall per project the caller can read.The refused workspace call is a permission refusal like any other.
ListDatabasesisn't an audited method, so no audit row is stored. When the stdout audit stream is on, the refusal is streamed as a WARNING line.SearchProjectsfilters each page after reading it, so a page can be empty and still have a next one; the resolver follows the page token. A project that becomes unreadable or is deleted between the search and its listing contributes nothing.Not in this change
bb.databases.listwithoutbb.projects.getused to resolve a database in a named project through the workspace listing. It now gets PERMISSION_DENIED for that project, asListDatabasesgives it for that parent; it can still leaveprojectout. No predefined role has that shape. On feat(mcp): simplify resolveDatabase with workspace-scoped ListDatabases #19823 this split was judged a pathological custom role.bb.instances.getwithoutbb.databases.listcan list databases underinstances/{id}. The resolver doesn't try instance parents. No predefined role has that shape.bb.projects.getwithoutbb.databases.listgets every project fromSearchProjects, so its unnamed resolve makes one listing call per project in the workspace. No predefined role has that shape either.Tests
TestMCPResolvesDatabasesThroughProjectRoles(backend/tests) uses a live server, real MCP sessions and real IAM. It creates a SQL Editor user and a project developer, each with a role in one project only, and a second database in another project.query_databasefails:PERMISSION_DENIED: you don't have permission to list databases in this workspace.bb.sql.select, reads the schema and gets DATABASE_NOT_FOUND for the other project's database. Under a Read-write policy the developer proposes a change, and the issue lands in their project. The workspace admin resolves the other project's database, as the control.tool_resolve_test.gouse a mock that answers the way the server answers a project-only caller:TestResolve_ProjectRoleListsThroughReadableProjects: lists through the searched projects, including a search page that is empty but has a next page.TestResolve_ProjectRoleCombinesProjectsAndKeepsTheFilter: two readable projects give one candidate list, and an instance filter, workspace or project instance, still narrows the fallback.TestResolve_ProjectRoleSkipsAProjectRefusedAfterTheSearch: a project refused after the search is skipped, not fatal.TestResolve_ProjectRoleWithNamedProject: a named project is listed with one call. Its cases are readable, refused (PERMISSION_DENIED naming the project) and missing (DATABASE_NOT_FOUND).TestResolve_WorkspaceCallerListsOnce: a workspace-level caller makes exactly one call. It passes on main and on this branch.TestQueryDatabase_WorkspacePermissionDeniednow expects the project-role advice. The old advice, "grant bb.databases.list", was the wrong ask for most of these users.TestMCPReadOnlyRoleDowngradeBitesTheNextRequestandTestResolve_PolicyDenialGetsNoRoleAdvice: comments no longer describe the old listing or the old advice. No behavior change.Receipts
backend/api/mcp/tool_resolve.go:113-120on mainresolveTargetattool_query.go:90,tool_schema.go:243,tool_change.go:197bb.databases.list; a project parent needsbb.projects.getin the project, after a 404 for a missing projectbackend/api/v1/database_service.go:307-333frontend/src/routes/project/ProjectDatabasesPage.tsx:826-828backend/store/predefined_roles.go:319-344backend/component/iam/manager.go:52-97SearchProjectsfilters after pagingbackend/api/v1/project_service.go:169-198SearchIssuesservesprojects/-backend/api/v1/issue_service.go:368-377; helperplan_service.go:136-166backend/api/v1/audit.go:82-83workspaces/{id}instead of loop the projects"); Codex's last P1 there, "Resolve databases with project-scoped ListDatabases"🤖 Generated with Claude Code