Skip to content

refactor(BA-6922): model the fragment scoped search as a scope action#13032

Merged
fregataa merged 11 commits into
mainfrom
refactor/fragment-scoped-search-scope-action
Jul 22, 2026
Merged

refactor(BA-6922): model the fragment scoped search as a scope action#13032
fregataa merged 11 commits into
mainfrom
refactor/fragment-scoped-search-scope-action

Conversation

@jopemachine

@jopemachine jopemachine commented Jul 22, 2026

Copy link
Copy Markdown
Member

Related: #12929 (BA-6922)

Summary

  • ScopedSearchAppConfigFragmentAction was a BaseBulkAction[SearchableActionTarget] holding a list of domain/user targets that the repository OR-combined into one query. It is now a BaseScopeAction acting at the single scope the request names, deriving its RBAC facets through AppConfigScopeType.to_rbac_* exactly as the sibling CreateAppConfigFragmentAction does for a write.
  • Wire it through ScopeActionProcessor with validators.rbac.scope. As a bulk action it reached the processor with no RBAC validator at allbulk_update / bulk_purge pass validators.rbac.bulk, scoped_search passed 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.
  • The fragment row is (scope_type, scope_id), so one scope expresses the query exactly: DomainAppConfigFragmentSearchScope / UserAppConfigFragmentSearchScope collapse into AppConfigFragmentSearchScope carrying scope_type + scope_id.
  • AppConfigFragmentSearchScope.existence_checks no 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. public keeps an empty check — the global scope has no owner row.

Why not a bulk action: BaseBulkAction exists 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, mirroring batch_query_with_scopes / BatchQuerier.scopes underneath it and matching audit_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 main yet, so this is self-contained. #13020 adds that surface and will build on this shape.

Review notes

  • The existence checks pull DomainRow, UserRow and their resource-policy / keypair dependencies into the repository test's with_tables set, plus a scope_owners fixture. That weight is the reason audit_log leaves its checks empty; the judgement here is that a superadmin typo'ing a scope id deserves a 404, since ScopeActionRBACValidator returns early for superadmins and when RBAC enforcement is disabled.
  • Checks are aggregated across scopes and validated in a single query by execute_batch_querier, so this adds no round trip.

Test plan

  • Formatter, linter, visibility and mypy clean over the touched service, repository and test packages
  • Repository scoped-search cases parametrized over the scope (domain / user / public), plus OR-combined scopes and a parametrized missing-owner case asserting DomainNotFound / UserNotFound
  • Service scoped-search cases parametrized over the scope, asserting the scope is passed through and the result reports the RBAC scope it was authorized at
  • queried_refs / element_refs() removal verified to have no consumer anywhere in the tree
  • CI verification

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 22, 2026 07:27
@jopemachine
jopemachine requested a review from a team as a code owner July 22, 2026 07:27
jopemachine added a commit that referenced this pull request Jul 22, 2026
@github-actions github-actions Bot added the size:L 100~500 LoC label Jul 22, 2026
@github-actions github-actions Bot added the comp:manager Related to Manager component label Jul 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/ai/backend/manager/services/app_config_fragment/processors.py
@jopemachine
jopemachine marked this pull request as draft July 22, 2026 07:41
@jopemachine
jopemachine marked this pull request as ready for review July 22, 2026 07:53
Comment thread changes/13032.enhance.md
Comment thread src/ai/backend/manager/services/app_config_fragment/processors.py Outdated
jopemachine and others added 11 commits July 22, 2026 18:59
`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
jopemachine force-pushed the refactor/fragment-scoped-search-scope-action branch from 5f7426d to 3cd924e Compare July 22, 2026 10:00
@fregataa
fregataa merged commit 7950105 into main Jul 22, 2026
34 checks passed
@fregataa
fregataa deleted the refactor/fragment-scoped-search-scope-action branch July 22, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:manager Related to Manager component size:L 100~500 LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants