Skip to content

Delete remaining reached-path filters; resolve the publish-time chain walk #813

Closed as not planned

Description

@bencap

Everything #811 and #812 do not cover, plus the one item in this epic whose failure mode is a silent wrong answer rather than an absence.

Loop filters — delete

Site Form
lib/score_sets.py:356 for score_set in score_sets: if not has_permission(...): continue
routers/score_sets.py:726 same, over score sets with mapping metadata
routers/score_sets.py:800 same, in the experiment score-set listing
routers/score_calibrations.py:140, 180 list comprehension over calibrations
routers/mapped_variant.py:246 comprehension over item.variant.score_set
routers/experiments.py:100 comprehension over experiments

Narrowing assignments — delete

These become unnecessary rather than merely redundant.

Site Form Why RLS handles it
routers/score_sets.py:604-605 item.superseding_score_set = None An invisible related row does not load; the attribute is already None
routers/score_sets.py:804-805 same, in the listing loop as above
routers/score_sets.py:607 item.score_calibrations = [...] The collection arrives filtered
routers/experiment_sets.py:68 item.experiments[:] = [...] as above

lazy="raise" does not block scalar assignment, so nothing structurally prevented the scalar form. RLS removes the need for it entirely.

Boundary checks — move to #816

routers/target_genes.py:132 — if not item or not has_permission(...) is a named-path 404, not a filter.

The supersession chain walk

lib/score_sets.py:508, 515, 518, 538, 549 are not read filters. They are the supersession chain walkers, where permission acts as the chain terminator and the action is parameterized rather than always READ.

find_publish_or_private_superseded_score_set_tail runs at publish time and must see private score sets. Under RLS a filtered walk terminates early and publishes against the wrong tail. This is a silent wrong answer, not a missing row, which makes it the highest-risk change in the epic.

Resolve in this order:

  1. Establish whether the acting user always owns every score set in the chain. If so, the owner disjunct already admits them and no special handling is needed. This is the likely answer and the cheapest outcome, but it must be established, not assumed.
  2. Otherwise route the walk through a SECURITY DEFINER helper or the owner role.

Either way this needs a test that publishes over a private tail. Do not resolve it by inspection.

Carve-out, not a deletion

routers/permissions.py:80 — permission = has_permission(user_data, item, action).permitted — is the endpoint that reports permission. Under RLS a filtered lookup would make it answer "no" for every entity the caller cannot see, which is both wrong and a behavior change. It needs the owner role or a SECURITY DEFINER lookup.

Also fold in: the two primary-calibration checks (one walking item.score_set.score_calibrations in the router, one querying directly in the lib) collapse into a single SECURITY DEFINER helper. The business invariant must see rows the caller cannot.

Do not begin until the policy-gap counter in #835 has read zero for a full release cycle.

Acceptance criteria

  • Every loop filter and narrowing assignment listed above is removed.
  • The codebase contains no has_permission call used as a read filter.
  • Test: publishing a superseding score set over a private tail resolves to the correct tail.
  • Test: GET /permissions/... still reports correctly for entities the caller cannot read.
  • Primary-calibration detection has exactly one call site.
  • The chain-walk resolution is written down here, with whichever of the two options was taken and why.

Activity

  1. added theissue type on Aug 7, 2026
  2. changed the issue type fromtoon Aug 7, 2026
  3. changed the title [-]Convert remaining reached-path filter sites to visibility clauses[/-] [+]Delete remaining reached-path filters; resolve the publish-time chain walk[/+] on Aug 7, 2026
  4. bencap commented on Sep 28, 2026

    @bencap
    CollaboratorAuthor

    Merged into #811, which now covers every read-filter deletion.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    app: backendTask implementation touches the backend

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions