refactor(BA-6922): model the fragment scoped search as a scope action#13032
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Refactors app-config fragment scoped search into a single-scope action with RBAC validation.
Changes:
- Replaces bulk multi-scope search with one typed search scope.
- Adds scope-level RBAC processor validation.
- Updates repository/service tests for single-scope behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
changes/13032.fix.md |
Documents the RBAC security fix. |
src/ai/backend/manager/repositories/app_config_fragment/db_source/db_source.py |
Queries one scope. |
src/ai/backend/manager/repositories/app_config_fragment/repository.py |
Exposes the single-scope interface. |
src/ai/backend/manager/repositories/app_config_fragment/types.py |
Unifies fragment search scope types. |
src/ai/backend/manager/services/app_config_fragment/actions/scoped_search.py |
Models search as a scope action. |
src/ai/backend/manager/services/app_config_fragment/processors.py |
Adds scope RBAC validation. |
src/ai/backend/manager/services/app_config_fragment/service.py |
Passes the scope through to the repository. |
tests/unit/manager/repositories/app_config_fragment/test_repository.py |
Tests domain, user, public, and unknown scopes. |
tests/unit/manager/services/app_config_fragment/test_service.py |
Tests scope propagation and result metadata. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
jopemachine
marked this pull request as draft
July 22, 2026 07:41
jopemachine
marked this pull request as ready for review
July 22, 2026 07:53
jopemachine
commented
Jul 22, 2026
jopemachine
commented
Jul 22, 2026
jopemachine
marked this pull request as draft
July 22, 2026 07:58
3 tasks
jopemachine
marked this pull request as ready for review
July 22, 2026 08:14
`ScopedSearchAppConfigFragmentAction` was a `BaseBulkAction[SearchableActionTarget]` holding a list of domain/user targets that the repository OR-combined into one query. A search is not a bulk mutation over entities: it acts at one RBAC scope and returns whatever lives under it. `BaseBulkAction` exists to authorize an operation per entity across a batch, which is why `bulk_update` / `bulk_purge` carry the bulk RBAC validator — the scoped search carried none at all, so once an API reaches it any authenticated caller could read any domain's or user's fragments by naming them in the request. Model it the way the sibling `CreateAppConfigFragmentAction` already models a write: one `BaseScopeAction` at the single scope named by the request, deriving its RBAC facets through `AppConfigScopeType.to_rbac_*`, wired through `ScopeActionProcessor` with the scope RBAC validator. The fragment row is `(scope_type, scope_id)`, so one scope expresses the query exactly: `DomainAppConfigFragmentSearchScope` and `UserAppConfigFragmentSearchScope` collapse into `AppConfigFragmentSearchScope`, and `scoped_search(querier, scope)` takes a single scope. Searching several scopes in one request is gone — it was the only thing the list bought, and it cannot be authorized as a single scope action. Nothing consumed the removed `queried_refs` / `element_refs()` on the result. Tests are parametrized over the scope instead of one near-identical test per kind; the OR-across-scopes case is dropped with the capability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Narrowing `scoped_search` to a single scope pinned the read path to what its one caller happens to need today. The ops layer underneath (`batch_query_with_scopes`, `BatchQuerier.scopes`) is defined over a sequence and ORs them, and `audit_log` already exposes that arity, so the repository mirrors it rather than re-deciding it. The action stays a single-scope `BaseScopeAction` — that is what the scope RBAC validator authorizes — and the service wraps it as `[action.scope]` at the call. The repository test for OR-combined scopes comes back with the arity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`AppConfigFragmentSearchScope.existence_checks` returned `()` on the grounds that RBAC already gated scope reachability. It did not: the scoped search carried no RBAC validator at all until this branch added one, and even now `ScopeActionRBACValidator` returns early for superadmins and when RBAC enforcement is disabled. A search at a domain or user that does not exist therefore came back as an empty page, making "no fragments here" and "no such domain" indistinguishable. Check the scope owner the way every other scope does — `auth` checks `UserRow.uuid`, `scheduling_history` checks `KernelRow.id`, `group` checks `DomainRow.name` — so a missing owner is a 404. `public` keeps an empty check: the global scope has no owner row. The checks are aggregated across scopes and validated in one query by `execute_batch_querier`, so this costs no extra round trip. The repository tests gain the domain and user rows the checks look for, and a parametrized case asserting a missing owner raises rather than returning empty. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The scope_owners fixture imported create_test_password_info from tests.unit.manager.repositories.test_utils, which this test package does not depend on, so the module was collected without it and the import failed in CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The admin search spans every scope, so it belongs to no RBAC scope: it was a scope action carrying an empty GLOBAL scope and no validator, leaving the API-layer superadmin gate as its only check. It is now a BaseGlobalAction run through GlobalActionProcessor, whose SUPERADMIN gate authorizes it directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The scope is not part of what the search returns — it is carried only so the result can report the RBAC scope it was authorized at. Name it _scope so it does not read as a returned item. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The public branch names the fragment element while domain / user name their owner, which reads backwards until you know target_element is the permission chain's entry point rather than the entity being acted on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jopemachine
force-pushed
the
refactor/fragment-scoped-search-scope-action
branch
from
July 22, 2026 10:00
5f7426d to
3cd924e
Compare
fregataa
approved these changes
Jul 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related: #12929 (BA-6922)
Summary
ScopedSearchAppConfigFragmentActionwas aBaseBulkAction[SearchableActionTarget]holding a list of domain/user targets that the repository OR-combined into one query. It is now aBaseScopeActionacting at the single scope the request names, deriving its RBAC facets throughAppConfigScopeType.to_rbac_*exactly as the siblingCreateAppConfigFragmentActiondoes for a write.ScopeActionProcessorwithvalidators.rbac.scope. As a bulk action it reached the processor with no RBAC validator at all —bulk_update/bulk_purgepassvalidators.rbac.bulk,scoped_searchpassed nothing — so once an API reaches it, any authenticated caller could read any domain's or user's fragments by naming them in the request.(scope_type, scope_id), so one scope expresses the query exactly:DomainAppConfigFragmentSearchScope/UserAppConfigFragmentSearchScopecollapse intoAppConfigFragmentSearchScopecarryingscope_type+scope_id.AppConfigFragmentSearchScope.existence_checksno longer returns(). A search at a domain or user that does not exist came back as an empty page, making "no fragments here" indistinguishable from "no such domain". It now checks the scope owner the way every other scope does, so a missing owner is a 404.publickeeps an empty check — the global scope has no owner row.Why not a bulk action:
BaseBulkActionexists to authorize an operation per entity across a batch. A scoped search has no per-entity target — it acts at one scope and returns whatever lives under it.Repository arity is unchanged.
scoped_search(querier, scopes: Sequence[SearchScope])still takes a sequence, mirroringbatch_query_with_scopes/BatchQuerier.scopesunderneath it and matchingaudit_log. The single-scope action wraps its scope at the call site; the read path is not pinned to one scope by its only caller.Tradeoff: searching several scopes in one request is gone at the action level — it cannot be authorized as a single scope action. Nothing consumed the removed
queried_refs/element_refs()on the result.Scope of this PR: manager-side only (repository scope, action, service, processor). No REST or GraphQL surface reaches the scoped search on
mainyet, so this is self-contained. #13020 adds that surface and will build on this shape.Review notes
DomainRow,UserRowand their resource-policy / keypair dependencies into the repository test'swith_tablesset, plus ascope_ownersfixture. That weight is the reasonaudit_logleaves its checks empty; the judgement here is that a superadmin typo'ing a scope id deserves a 404, sinceScopeActionRBACValidatorreturns early for superadmins and when RBAC enforcement is disabled.execute_batch_querier, so this adds no round trip.Test plan
DomainNotFound/UserNotFoundqueried_refs/element_refs()removal verified to have no consumer anywhere in the tree🤖 Generated with Claude Code