Skip to content

feat: allow access tokens to enumerate groups - #332

Open
agent-p1p wants to merge 5 commits into
masterfrom
pip/goggles-331
Open

feat: allow access tokens to enumerate groups#332
agent-p1p wants to merge 5 commits into
masterfrom
pip/goggles-331

Conversation

@agent-p1p

@agent-p1p agent-p1p commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #331

Summary

  • authenticate the group metadata index with either a session or personal access token
  • share the reader-scoped group queryset with streaming exports so denied groups remain not-found
  • add signed keyset pagination, a traversal-only polling watermark, cursor-bound updated_since, a periodic full-rescan contract for delayed commits, and a matching composite index
  • document the watchdog polling contract and cover token lifecycle/scope behavior

Verification

  • uv sync --frozen
  • DJANGO_DEBUG=1 just check (276 passed; 1 PostgreSQL-only test skipped)
  • DJANGO_DEBUG=1 just test-postgres (276 passed)
  • DJANGO_DEBUG=1 uv run python manage.py check
  • uv run ruff check .
  • uv run ruff format --check .
  • DJANGO_DEBUG=1 uv run python manage.py makemigrations --check --dry-run

Sensitive paths

  • forensics/views.py: reader authentication principal and shared object-level group scope
  • forensics/group_list_cursor.py: signed cursor uses a non-sensitive internal-ID tie-breaker instead of serializing group slugs
  • forensics/models.py: polling index on group metadata
  • forensics/migrations/0014_auditgroup_updated_at_id_index.py: database index migration

Release hygiene

  • Changelog: none present
  • Version bump: none

Summary by CodeRabbit

  • New Features
    • Added GET /api/v1/groups/ with cursor-based pagination, stable ordering, and updated_since polling via a signed cursor.
    • Reader-based access now drives both group listing and streaming export, with newly created groups appearing on subsequent polls.
  • Bug Fixes
    • Unauthorized, invalid, or malformed credentials return clear JSON 401 (no HTML redirects).
    • Upload tokens can’t list groups; non-readable groups are omitted from listing and return 404 for export.
  • Documentation
    • Updated API auth and endpoint behavior details, including pagination semantics and watermark rules.
  • Tests
    • Added extensive coverage for auth, pagination/cursors, watermark behavior, and race conditions.
  • Performance
    • Added database indexing to speed group list ordering.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change enables session and personal-access-token access to a paginated group index, adds readable-group scoping to exports, supports updated_since polling, adds an ordering index, updates documentation, and introduces comprehensive API tests.

Changes

Readable group index API

Layer / File(s) Summary
Reader authentication and export scoping
forensics/views.py
Reader authentication returns a structured principal, validates personal access tokens, tracks token usage, and applies readable-group checks to listing and exports.
Cursor pagination and polling
forensics/views.py, forensics/group_list_cursor.py, forensics/analysis.py, forensics/models.py, forensics/migrations/...
GET /api/v1/groups/ supports bounded signed-cursor pagination, deterministic ordering, updated_since, polling watermarks, and scoped metadata enrichment backed by a matching database index.
API contract and validation
forensics/tests.py, docs/api-v1.md, AGENTS.md
Tests cover credential handling, visibility filtering, pagination, change detection, commit timing, and upload-token rejection; documentation defines the credential and polling contracts.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies the issue by adding token-authenticated group listing, scope checks, pagination/change detection, docs, and tests.
Out of Scope Changes check ✅ Passed The added model, migration, cursor, view, test, and docs changes all directly support token-based group enumeration.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: enabling access tokens to enumerate groups.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pip/goggles-331

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@forensics/views.py`:
- Around line 874-881: Replace offset-based pagination in forensics/views.py
lines 874-881 with an opaque keyset cursor tied to a server-provided polling
snapshot or equivalent fixed watermark, while preserving efficient polling and
change detection. Add a regression test in forensics/tests.py lines 5900-5955
that updates a later-page group between requests and verifies it remains
returned. Update docs/api-v1.md lines 130-135 to document the snapshot/cursor
contract and remove any claim that live offset pagination is stable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 32d24c3f-989b-491b-8d60-53b3ea1014d0

📥 Commits

Reviewing files that changed from the base of the PR and between 480857b and 093fa92.

📒 Files selected for processing (7)
  • AGENTS.md
  • docs/api-v1.md
  • forensics/analysis.py
  • forensics/migrations/0014_auditgroup_updated_at_slug_index.py
  • forensics/models.py
  • forensics/tests.py
  • forensics/views.py

Comment thread forensics/views.py Outdated
@agent-p1p

Copy link
Copy Markdown
Contributor Author

Pip adversarial review

Verdict: BLOCKING (1 blocking finding, 0 suggestions, 0 nitpicks)

Blocking

  1. forensics/views.py:874-881 — live offset pagination can permanently omit an authorized group while the documented watermark advances past it.

    The queryset is mutable between requests, but the next page is selected by numeric offset. I reproduced this against the exact PR head with four groups A/B/C/D ordered by updated_at and limit=2: page 1 returned A,B; deleting A (the same shift occurs when a future scope filter removes it) before page 2 made offset=2 return only D. C was never returned. Advancing updated_since to the greatest timestamp already seen then returned nothing, so C remained permanently invisible to that poller.

    This violates the complete readable-group enumeration and future group-scoped visibility requirements. It also makes the stability claim in docs/api-v1.md:130-136 false. Replace the live offset with keyset/cursor pagination over (updated_at, slug), carrying a fixed polling upper watermark or an equivalent contract that cannot skip rows when earlier rows disappear or move. Add a regression that mutates/removes an earlier row (or changes its visibility) between pages and proves every remaining readable group is either returned in that traversal or unambiguously deferred to the next watermark.

    Independent reproduction: isolated Django test DB printed pages_seen=[A,B,D], skipped=C, next_poll=[]. CodeRabbit flagged the same root pagination race; this reproduction confirms a permanent-loss case rather than only duplication or one-cycle delay.

Verification

  • Exact head: 093fa92
  • Focused GroupListApiTests + GroupExportStreamTests: 28/28 passed
  • just check: 269 tests passed; Django check, Ruff lint/format, and migration drift all passed
  • GitHub CI: Django tests/checks and pip-audit passed

Sensitive paths reviewed: forensics/views.py, forensics/models.py, and forensics/migrations/0014_auditgroup_updated_at_slug_index.py.

Replace mutable offsets with a signed keyset cursor bound to a fixed polling watermark and the original change filter.

Fixes #331
@agent-p1p

Copy link
Copy Markdown
Contributor Author

Addressed the blocking pagination finding in b6eab0d. Mutable offsets are replaced with a signed keyset cursor, fixed polling watermark, and cursor-bound updated_since. Regression coverage includes earlier-row deletion, post-watermark update recovery, filter propagation, and invalid cursors. Local SQLite/Postgres suites pass 273 tests; Ruff, Django checks, formatting, and migration drift are green.

@agent-p1p
agent-p1p marked this pull request as ready for review July 29, 2026 09:46
@agent-p1p

Copy link
Copy Markdown
Contributor Author

Pip adversarial re-review

Verdict: BLOCKING (the prior page-shift blocker is resolved; 1 remaining blocking finding)

Prior blocker

The deletion/scope-removal case from the prior review is fixed at b6eab0d: continuation now uses a signed (updated_at DESC, slug ASC) keyset cursor, carries the original updated_since, and holds a fixed upper watermark. The deletion, filter-propagation, post-watermark update, and tamper regressions all pass independently.

Blocking

  1. forensics/views.py:887-898, docs/api-v1.md:159-162 — the wall-clock watermark is not a commit-safe change boundary, so a newly committed group can still be permanently omitted.

    The first page captures timezone.now() and the documented client contract advances the next strict updated_since to that value. But group timestamps are assigned before commit inside upload transactions (forensics/ingest.py:249-289). A transaction can insert/update a group, remain uncommitted (and therefore invisible to the page traversal), then commit after page 1 with updated_at <= polling_watermark. If that key sorts before the continuation cursor, page 2 excludes it; the next poll also excludes it because the filter is updated_at > polling_watermark.

    Independent API reproduction at the exact head: page 1 returned [g0, g1]; a row made visible after page 1 with updated_at = polling_watermark - 1s sorted before the continuation boundary; page 2 returned [g2, g3]; the documented next poll returned []; commit_delayed_seen=False. This is the state produced by an insert whose timestamp is assigned inside a transaction before the poll but whose commit becomes visible afterward.

    Use a commit-safe change boundary, or retain a conservative overlap/full-rescan contract with client deduplication rather than treating application wall time as a strict exclusive checkpoint. Add a regression that makes a pre-watermark row visible after page 1 (preferably with a second Postgres connection/transaction) and proves a documented poll sequence eventually returns it.

Verification

  • Exact head: b6eab0d2092889b447487dd537d3de87b725d93b (GitHub signature verified)
  • Four focused cursor regressions: 4/4 passed
  • DJANGO_DEBUG=1 just check: 273 tests passed; Django check, Ruff lint/format, and migration drift passed
  • GitHub CI: Django tests/checks, dependency audit, and CodeRabbit status are green

Sensitive paths reviewed: forensics/views.py, forensics/group_list_cursor.py, forensics/models.py, and forensics/migrations/0014_auditgroup_updated_at_slug_index.py.

Require periodic full index scans with slug deduplication instead of treating a wall-clock watermark as a strict checkpoint. Cover the real delayed-commit race with a second PostgreSQL transaction.

Fixes #331
@agent-p1p

Copy link
Copy Markdown
Contributor Author

Addressed the remaining commit-order polling blocker in signed commit 8f917b1.

  • Preserved the signed keyset cursor and fixed traversal watermark.
  • Stopped treating application wall time as a commit-safe checkpoint: updated_since is documented as a best-effort optimization, while periodic full index scans without updated_since plus client deduplication by slug guarantee eventual discovery. Finite overlap alone is not claimed as sufficient.
  • Added a real PostgreSQL regression using a second psycopg transaction: the row is inserted before page 1, remains uncommitted through page 1, commits with a pre-watermark updated_at, is absent from the continuation and strict incremental poll, then appears in the documented full rescan.

Verification:

  • DJANGO_DEBUG=1 just check — 275 passed, 1 PostgreSQL-only test skipped; Django check, Ruff lint/format, and migration drift passed.
  • DJANGO_DEBUG=1 just test-postgres — 275 passed.
  • Final focused group-list tests passed on SQLite (21, 1 skipped) and PostgreSQL (21).

Sensitive scope: forensics/tests.py exercises PAT-authenticated group enumeration; docs/api-v1.md defines the authenticated polling contract. No production/authentication code changed in this follow-up.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
forensics/group_list_cursor.py (2)

51-69: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Specific validation messages are swallowed by the broad except.

InvalidGroupListCursor subclasses ValueError, so the targeted raises inside the try (bad timestamp, bad slug) are caught at Line 68 and re-raised as the generic "cursor is invalid". Harmless for the API contract (all map to 400), but the finer-grained messages are unreachable — consider re-raising InvalidGroupListCursor untouched if those messages are meant for logs.

♻️ Preserve specific messages
+    except InvalidGroupListCursor:
+        raise
     except (BadSignature, TypeError, json.JSONDecodeError, ValueError) as exc:
         raise InvalidGroupListCursor("cursor is invalid") from exc
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@forensics/group_list_cursor.py` around lines 51 - 69, Update
decode_group_list_cursor so InvalidGroupListCursor exceptions raised for invalid
timestamps or slug validation pass through unchanged instead of being converted
to the generic “cursor is invalid” message. Add the specific exception type to
the appropriate handling order while preserving generic wrapping for
BadSignature, malformed JSON, TypeError, and other ValueError failures.

14-14: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Cursor payload is signed but human-readable; the group slug travels in the query string as cleartext.

Signer.sign only appends a signature — the JSON (including slug) stays readable in the cursor, which is passed as a URL query parameter and therefore lands in web-server/proxy access logs. Group references are sensitive data, and docs/api-v1.md describes the cursor as "opaque". django.core.signing.dumps/loads gives a base64 payload plus a timestamp, so you also get optional max_age expiry for stale cursors.

♻️ Suggested change to opaque, expiring cursors
-from django.core.signing import BadSignature, Signer
+from django.core import signing
+from django.core.signing import BadSignature
@@
-CURSOR_SIGNER = Signer(salt="goggles-groups-v1-cursor")
+CURSOR_SALT = "goggles-groups-v1-cursor"
+CURSOR_MAX_AGE_SECONDS = 24 * 60 * 60
@@
-    return CURSOR_SIGNER.sign(json.dumps(payload, separators=(",", ":")))
+    return signing.dumps(payload, salt=CURSOR_SALT, compress=True)

And in decode_group_list_cursor:

payload = signing.loads(raw_cursor, salt=CURSOR_SALT, max_age=CURSOR_MAX_AGE_SECONDS)

signing.SignatureExpired subclasses BadSignature, so the existing except clause still maps it to 400 invalid cursor.

Note: the ast-grep use-jsonify hint on Line 47 is a Flask-specific rule and does not apply here.

Also applies to: 42-48

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@forensics/group_list_cursor.py` at line 14, Replace the human-readable
CURSOR_SIGNER encoding with django.core.signing.dumps/loads for group list
cursors. Update decode_group_list_cursor to load using the cursor salt and an
expiry max age, preserving the existing BadSignature handling so expired or
invalid cursors return 400 invalid cursor. Ensure the serialized payload,
including slug, is opaque in the query string.

Sources: Coding guidelines, Linters/SAST tools

forensics/tests.py (1)

6169-6206: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Second connection ignores OPTIONS and has no connect timeout; the raw INSERT column list is brittle.

Two small robustness gaps in the PostgreSQL-only race test:

  • _postgres_connection drops settings_dict["OPTIONS"] (sslmode, service, unix socket, etc.), so it can fail or connect differently from Django's own connection in CI. No connect_timeout means a stuck server hangs the suite instead of failing.
  • The hardcoded column list will break the moment AuditGroup gains a NOT NULL field without a DB default, and the failure will look like an unrelated test error.
♻️ Suggested hardening
     def _postgres_connection(self):
         import psycopg
 
         settings = connection.settings_dict
+        options = {
+            key: value
+            for key, value in (settings.get("OPTIONS") or {}).items()
+            if key != "pool"
+        }
         return psycopg.connect(
             host=settings["HOST"],
             port=settings["PORT"],
             dbname=settings["NAME"],
             user=settings["USER"],
             password=settings["PASSWORD"],
+            connect_timeout=10,
+            **options,
         )

For the INSERT, deriving the column list from AuditGroup._meta (or asserting the concrete field set once) keeps the failure mode obvious if the model changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@forensics/tests.py` around lines 6169 - 6206, Harden _postgres_connection by
passing the relevant connection settings from settings["OPTIONS"] through to
psycopg.connect and enforcing a finite connect timeout, while preserving
Django’s host, port, database, and credential behavior. In
test_delayed_upload_commit_is_discovered_by_full_rescan_not_incremental_watermark,
replace the brittle hardcoded INSERT column list with columns derived from
AuditGroup metadata (or an equivalent explicit model-field assertion), ensuring
every required non-default field is supplied and future model changes fail
clearly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/api-v1.md`:
- Around line 127-131: Promote the “Polling contract” body text near the polling
guidance into a Markdown heading so it generates the `#polling-contract` anchor
referenced by updated_since. Preserve the existing wording and section placement
while ensuring the heading level matches the surrounding document structure.

---

Nitpick comments:
In `@forensics/group_list_cursor.py`:
- Around line 51-69: Update decode_group_list_cursor so InvalidGroupListCursor
exceptions raised for invalid timestamps or slug validation pass through
unchanged instead of being converted to the generic “cursor is invalid” message.
Add the specific exception type to the appropriate handling order while
preserving generic wrapping for BadSignature, malformed JSON, TypeError, and
other ValueError failures.
- Line 14: Replace the human-readable CURSOR_SIGNER encoding with
django.core.signing.dumps/loads for group list cursors. Update
decode_group_list_cursor to load using the cursor salt and an expiry max age,
preserving the existing BadSignature handling so expired or invalid cursors
return 400 invalid cursor. Ensure the serialized payload, including slug, is
opaque in the query string.

In `@forensics/tests.py`:
- Around line 6169-6206: Harden _postgres_connection by passing the relevant
connection settings from settings["OPTIONS"] through to psycopg.connect and
enforcing a finite connect timeout, while preserving Django’s host, port,
database, and credential behavior. In
test_delayed_upload_commit_is_discovered_by_full_rescan_not_incremental_watermark,
replace the brittle hardcoded INSERT column list with columns derived from
AuditGroup metadata (or an equivalent explicit model-field assertion), ensuring
every required non-default field is supplied and future model changes fail
clearly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 103e62a1-ff46-433c-9ad8-57ed259e99a7

📥 Commits

Reviewing files that changed from the base of the PR and between 093fa92 and 8f917b1.

📒 Files selected for processing (4)
  • docs/api-v1.md
  • forensics/group_list_cursor.py
  • forensics/tests.py
  • forensics/views.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • forensics/views.py

Comment thread docs/api-v1.md
Serialize signed cursor payloads so sensitive group slugs are not written verbatim in query strings. Repair the polling docs anchor and harden the PostgreSQL commit-race test connection.

Fixes #331
@agent-p1p

Copy link
Copy Markdown
Contributor Author

Addressed the remaining exact-head CodeRabbit feedback in signed commit 9c3eee9:

  • repaired the #polling-contract heading target;
  • switched cursor encoding from plaintext JSON plus signature to Django signed serialization, so sensitive group slugs are no longer written verbatim in query strings (serialization/opacity, not encryption);
  • preserved specific InvalidGroupListCursor validation errors;
  • reused Django PostgreSQL connection parameters with a 10-second timeout and made raw-INSERT schema drift fail explicitly.

Added a cursor-opacity regression (observed RED against Signer, then GREEN). DJANGO_DEBUG=1 just check and DJANGO_DEBUG=1 just test-postgres both pass 276 tests; final focused group-list tests pass on both backends. Existing in-flight cursors use the prior wire format and will receive 400, requiring clients to restart the traversal after deployment.

@agent-p1p

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@agent-p1p

Copy link
Copy Markdown
Contributor Author

Pip exact-head adversarial re-review

Verdict: BLOCKING (the prior commit-order omission is resolved; 1 blocking security finding remains)

Prior blocker

The revised polling contract at exact head 9c3eee915ad6c8fbf81d3e1c676c57438fa79fdf resolves the permanent-omission finding from the prior review:

  • updated_since is explicitly best-effort rather than a commit-safe checkpoint.
  • Repeated cursor-free, updated_since-free full index traversals, with client deduplication by slug, eventually discover rows that commit after an earlier traversal with a pre-watermark timestamp.
  • The real PostgreSQL regression holds an insert uncommitted on a second connection through page 1, commits it afterward, proves continuation and strict incremental polling miss it, and proves the documented full rescan finds it. I ran that regression in the full PostgreSQL suite independently.

The shared readable_groups_queryset() scope is reapplied to both list and export requests; denied exports remain not-found. Signed keyset continuation, fixed traversal watermarks, cursor-bound updated_since, earlier-row deletion, and post-watermark update behavior are otherwise correct.

Blocking

  1. forensics/group_list_cursor.py:42-48, forensics/tests.py:5941-5952 — Django signed serialization does not keep the sensitive group slug out of query logs.

    signing.dumps(..., compress=True) provides integrity and serialization, not confidentiality. The first cursor segment is URL-safe base64, optionally zlib-compressed. I generated a cursor containing a synthetic group slug and decoded that segment using only base64 plus zlib—without Django's SECRET_KEY; the decoded s field exactly matched the slug. The new regression checks only that the raw slug is not a literal substring of the token, so it passes despite the reversible disclosure.

    Because group refs are explicitly sensitive in AGENTS.md, an access/proxy/APM log that records the cursor query parameter still contains the group ref in recoverable form. Base64 is wearing a small hat; it is not encryption.

    Remove the sensitive slug from the client-visible cursor (for example, use a non-sensitive stable keyset tie-breaker with a matching index), use actual authenticated encryption, or keep cursor state server-side. Add a regression that demonstrates a logged cursor cannot recover the group ref without server-held secret/state while preserving signature/tamper rejection.

Verification

  • Exact head: 9c3eee915ad6c8fbf81d3e1c676c57438fa79fdf (GitHub signature verified; author/committer agent-p1p)
  • Focused group-list suites: 22 tests passed, 1 PostgreSQL-only test skipped on SQLite
  • DJANGO_DEBUG=1 just check: 276 passed, 1 skipped; Django check, Ruff lint/format, and migration drift passed
  • DJANGO_DEBUG=1 just test-postgres: 276 passed, including the second-connection commit-order regression
  • Exact-head GitHub Actions: Django tests/checks and dependency audit passed
  • CodeRabbit's latest exact-head status is rate-limited/hollow and was not treated as substantive review

Sensitive paths reviewed: forensics/views.py, forensics/group_list_cursor.py, forensics/models.py, forensics/migrations/0014_auditgroup_updated_at_slug_index.py, forensics/tests.py, and docs/api-v1.md.

@agent-p1p

Copy link
Copy Markdown
Contributor Author

Addressed the exact-head cursor disclosure blocker in signed commit faa1261.

  • Replaced the sensitive slug keyset tie-breaker with the internal AuditGroup primary key across cursor payload, query ordering/filter, model index, and migration.
  • Strengthened the regression to base64/zlib-decode the client-visible signed payload without SECRET_KEY; the old cursor failed by exposing the slug, while the new payload contains no group ref.
  • Preserved signature/tamper rejection, readable-group scope, fixed watermark, cursor-bound updated_since, deterministic keyset pagination, and periodic full rescans.

Verification:

  • Focused SQLite GroupListApiTests: 21 passed.
  • DJANGO_DEBUG=1 just test-postgres: 276 passed, including the delayed-commit regression.
  • DJANGO_DEBUG=1 just check: 276 passed, 1 PostgreSQL-only skip; Django checks, Ruff lint/format, and migration drift passed.
  • Independent pre-commit review: PASS.

@agent-p1p

Copy link
Copy Markdown
Contributor Author

Pip exact-head adversarial re-review

Verdict: LGTM — prior BLOCKING finding cleared at exact signed head faa1261ccf34517f7e7d9e82a8a59b1bb6e6dd09.

Resolution evidence

  • forensics/group_list_cursor.py:22-47,57-70 now serializes only watermark/update timestamps, the original change filter, and the internal AuditGroup primary key. I decoded the client-visible signed segment using public URL-safe base64 plus optional zlib, without SECRET_KEY: the payload keys were a, i, u, and w; no group slug/ref was recoverable. The strengthened regression at forensics/tests.py:5956-5971 passes.
  • forensics/views.py:894-911 orders by (-updated_at, pk) and resumes with the symmetric updated_at < boundary OR (updated_at = boundary AND pk > boundary_id) filter. An independent five-row equal-timestamp traversal returned pages [1,2], [3,4], [5] with no omissions or duplicates. Earlier-row deletion and cursor-bound updated_since regressions also pass.
  • forensics/models.py:24-26 and forensics/migrations/0014_auditgroup_updated_at_id_index.py match the query exactly. sqlmigrate emits ("updated_at" DESC, "id"); migration drift is clean.
  • readable_groups_queryset() remains shared by list and export. The scoped-list/denied-export regression confirms omitted list rows and not-found-style 404 exports. PAT lifecycle rejection, upload-token rejection, and session behavior remain intact.
  • The delayed-commit PostgreSQL regression still proves strict incremental polling can miss a pre-watermark commit and that the documented cursor-free full rescan discovers it. The best-effort updated_since/periodic full-rescan contract remains explicit in docs/api-v1.md.

Verification

  • Ownership guard: author agent-p1p, branch pip/goggles-331; GitHub reports the exact head signature valid.
  • Focused list/export suites: 34 passed, 1 PostgreSQL-only skip on SQLite.
  • DJANGO_DEBUG=1 just check: 276 passed, 1 skipped; Django checks, Ruff lint/format, and migration drift passed.
  • DJANGO_DEBUG=1 just test-postgres: 276 passed, including the real second-connection commit-order regression.
  • Exact-head GitHub Actions: Django tests/checks and dependency audit passed. CodeRabbit status is green but rate-limited/hollow and was not treated as substantive review.
  • Release hygiene: no changelog exists; no version/release/lockfile bump.

No blocking findings, suggestions, or nitpicks remain.

Sensitive paths reviewed: forensics/views.py, forensics/group_list_cursor.py, forensics/models.py, forensics/migrations/0014_auditgroup_updated_at_id_index.py, forensics/tests.py, forensics/analysis.py, and docs/api-v1.md. Database index migration noted for the merge gate; this review does not merge.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow personal access tokens to enumerate readable groups for watchdogs

1 participant