Skip to content

Fix IP assignment audit pagination against the API sort contract #208

Description

@Kerrycek

Priority

P2 — the audit can repeat rows and make older assignment history unreachable.

Problem

/admin/networking/ip-address-assignments does not have a cursor compatible with the API ordering:

  • the UI requests order=newest|oldest and sends from_id;
  • the UI always derives the next cursor with cursorFromDescendingPage, i.e. the minimum ID on the current page;
  • the upstream IpAddressAssignment::Index sorts by from_date DESC for newest or from_date ASC for oldest, then calls with_pagination;
  • the HaveAPI implementation deployed on the test server aliases with_pagination to ascending pagination and applies primary-key id > from_id.

Filtering by ID while ordering by an independent timestamp does not define a lossless keyset cursor. With a typical newest page containing IDs 100 through 51, the UI sends from_id=51; the backend can return IDs 100 through 52 again, and records at or below 51 remain unreachable. Non-monotonic IDs and from_date values can also cause missing rows in either sort direction.

There is a second, independent end-of-list defect: canNext = Boolean(nextCursor) enables Next for every nonempty page, including a final partial page.

Evidence

At main commit 23c71925c9d7ec314534201396ade99bd904c936:

  • src/pages/app/admin/networking/IpAssignmentsPage.tsx:54-75 selects the time order, sends fromId, derives the minimum-ID cursor, and enables Next for any nonempty result.
  • src/pages/app/admin/networking/IpAssignmentsPage.tsx:113 wires that cursor and state to the paginator.
  • src/lib/lockIndex.ts:65-75 defines the descending helper as the minimum ID.
  • upstream api/lib/vpsadmin/api/resources/ip_address_assignment.rb:110-120 orders by from_date and calls with_pagination.
  • deployed HaveAPI 0.27.3 active_record.rb:44-60 implements with_pagination as id > from_id.
  • e2e/specs/admin/ip_address_assignment_exact_filter.spec.ts verifies only the first request and canonicalization. It neither traverses a second page nor asserts final-page Next state.

The underlying server-ordering class is already tracked in #189, but that issue currently enumerates dataset/snapshot/download endpoints and does not cover this page-specific cursor direction or false Next state.

Backend-contract blocker

There is no frontend-only cursor choice that can guarantee complete traversal while the server filters on ID and sorts on from_date. Changing only min ID to max ID can move forward, but can still skip an unseen lower-ID row; keeping min ID can repeat pages indefinitely. Fetching limit + 1 fixes only the false Next indicator, not completeness.

The upstream repository is read-only from this project, so do not paper over the contract in WebUI.

Recommended resolution

  1. Define a deterministic upstream cursor matching the selected order, preferably a stable composite (from_date, id) cursor with both fields in the WHERE predicate and ORDER BY, or explicitly make ordering/direction ID-based if that is the intended product semantics.
  2. Deploy and document that API contract.
  3. Update the frontend to consume the matching cursor and request one-row lookahead so Next is enabled only when another row exists.
  4. Add integration coverage with more than two pages and deliberately non-monotonic IDs/timestamps for both newest and oldest.
  5. Add Playwright coverage that clicks Next, proves no duplicates or omissions, and verifies a final partial page disables Next.

Acceptance criteria

  • Traversing all pages in newest and oldest order produces each matching assignment exactly once.
  • Non-monotonic ID/timestamp fixtures do not skip or repeat rows.
  • The final page has Next disabled.
  • Exact ip_addr, user, VPS, and active filters retain cursor correctness.
  • Tests exercise actual second-page requests instead of only first-page URL canonicalization.

Related: #189, #200, #201.

Activity

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

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions