Skip to content

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

Closed
jopemachine wants to merge 4 commits into
feat/BA-6922-app-config-fragment-search-rest-v2from
refactor/app-config-fragment-scoped-search-scope-action
Closed

refactor(BA-6922): model fragment scoped search as a scope action#13024
jopemachine wants to merge 4 commits into
feat/BA-6922-app-config-fragment-search-rest-v2from
refactor/app-config-fragment-scoped-search-scope-action

Conversation

@jopemachine

@jopemachine jopemachine commented Jul 22, 2026

Copy link
Copy Markdown
Member

📚 Stacked PRs

Merge in order, bottom-up. Each PR is based on the one above it, so its diff shows only its own layer.

Related: #12929 (BA-6922), resolved by #13020.

Summary

  • ScopedSearchAppConfigFragmentAction was a BaseBulkAction[SearchableActionTarget] holding a list of domain/user targets 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.
  • Wire it through ScopeActionProcessor with validators.rbac.scope. As a bulk action it reached the processor with no RBAC validator at all, so any authenticated caller could read any domain's or user's fragments by naming them in the request body.
  • Repository: DomainAppConfigFragmentSearchScope / UserAppConfigFragmentSearchScope collapse into one AppConfigFragmentSearchScope(scope_type, scope_id) — the fragment row is (scope_type, scope_id), so one scope expresses the query exactly. scoped_search(querier, scope) takes a single scope.
  • DTO: AppConfigFragmentScope becomes scope_type: AppConfigScopeType + scope_id: AppConfigScopeID, mirroring CreateAppConfigFragmentInput. The route stays POST /v2/app-config-fragments/scoped/search.

Why: a search is not a bulk mutation. BaseBulkAction exists to authorize an operation per entity across a batch (bulk_update / bulk_purge over fragment ids); a scoped search acts at one RBAC scope and returns whatever lives under it.

Tradeoff: searching several scopes in one request is gone — that was the only thing the list bought, and it cannot be authorized as a single scope action.

Open item, unchanged: the DTO rejects scope_type: public. Public maps to the GLOBAL RBAC scope, which has no scope element to authorize against, so whether a scoped search should reach public fragments still needs its own decision. The repository scope models it (scope_id=None), so lifting the restriction is a validator change.

Test plan

  • pants fix fmt lint check over the six touched packages — ruff, visibility, mypy all clean
  • openapi.json regenerated with ./backend.ai mgr api dump-openapi; final delta vs feat(BA-6922): AppConfig fragment search REST v2 API (admin + scoped) #13020 is /scoped-search/scoped/search plus the reshaped AppConfigFragmentScope
  • repository scoped-search tests parametrized over the scope (domain / user / public / unknown owner); the OR-across-scopes case dropped with the capability
  • queried_refs / element_refs() removal verified to have no consumer anywhere in the tree
  • CI test run
  • live-server verification of /scoped/search for admin and non-admin

🤖 Generated with Claude Code

The scoped fragment search was a `BaseBulkAction[SearchableActionTarget]`
carrying a list of domain/user targets 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. Modelled as a bulk action it also reached the
processor with no RBAC validator at all, so any authenticated caller could
search any domain's or user's fragments.

Follow the scoped-search pattern the rest of the codebase already uses
(`SearchVFoldersInProjectAction`, `SearchKernelScopedHistoryAction`): one
`BaseScopeAction` per scope kind, holding the repository `SearchScope`
directly, wired through `ScopeActionProcessor` with the scope RBAC validator.

- actions: `scoped_search.py` split into `domain_scoped_search.py` and
  `user_scoped_search.py`, one Action + ActionResult pair each
- repository: `scoped_search(querier, scope)` takes a single scope
- REST: `POST /scoped-search` split into `POST /domains/search` and
  `POST /users/search`, both auth + RBAC gated
- DTO: `AppConfigFragmentScope` (domain/user id lists) replaced by
  `AppConfigFragmentDomainScope` / `AppConfigFragmentUserScope`

Searching several scopes at once is no longer a single request. Nothing
consumed the removed `queried_refs` / `element_refs()` on the result.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jopemachine
jopemachine requested a review from a team as a code owner July 22, 2026 06:39
@github-actions github-actions Bot added size:XL 500~ LoC area:docs Documentations comp:manager Related to Manager component comp:common Related to Common component labels Jul 22, 2026
@jopemachine
jopemachine marked this pull request as draft July 22, 2026 06:40
Splitting the request DTO per scope kind duplicated eight order/pagination
fields to vary one submodel. The convention is one scoped-search input
carrying a scope submodel (`ScopedSearchKernelHistoriesInput.scope`), so the
scope kind belongs inside that submodel, not in the input's name.

`ScopedSearchAppConfigFragmentInput.scope` is an `AppConfigFragmentScope`
discriminated by `AppConfigFragmentSearchScopeType` (domain | user), with
`scope_id` typed as the polymorphic `AppConfigScopeID`. `POST /domains/search`
and `POST /users/search` collapse back into `POST /scoped/search`.

The actions stay split: each scope is its own `BaseScopeAction` with its own
RBAC scope validation, so the adapter matches on the scope type to pick one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size:L 100~500 LoC and removed size:XL 500~ LoC labels Jul 22, 2026
Splitting per scope kind duplicated the action, the processor, the service
method and the repository scope to vary one enum value. The fragment row is
`(scope_type, scope_id)`, so one scope expresses the query exactly — the same
shape `AppConfigFragmentCreatorSpec` already uses for writes.

- repository: `DomainAppConfigFragmentSearchScope` /
  `UserAppConfigFragmentSearchScope` become one `AppConfigFragmentSearchScope`
  carrying `scope_type` + `scope_id`
- action: one `ScopedSearchAppConfigFragmentAction` on `BaseScopeAction`,
  deriving its RBAC facets through `AppConfigScopeType.to_rbac_*` exactly as
  `CreateAppConfigFragmentAction` does
- service / processor / adapter / route: one `scoped_search` each
- DTO: `AppConfigFragmentScope` takes `AppConfigScopeType` + `AppConfigScopeID`,
  rejecting `public` until public reachability is decided

Repository scoped-search 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>
@jopemachine jopemachine changed the title refactor(BA-6922): model fragment scoped search as a scope action per scope refactor(BA-6922): model fragment scoped search as a scope action Jul 22, 2026
@jopemachine

Copy link
Copy Markdown
Member Author

Superseded by #13032, which puts the same refactor on main. The manager-side scoped search (action, service, processor, repository scope) already lives on main via #12928, so this never needed to stack on #13020 — only the REST surface does. #13020 will be updated to build on the shape #13032 lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:docs Documentations comp:common Related to Common component comp:manager Related to Manager component size:L 100~500 LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant