Skip to content

feat: add authenticated streaming group export with self-service read tokens#199

Merged
jgmontoya merged 2 commits into
masterfrom
feat/authenticated-group-export
Jul 8, 2026
Merged

feat: add authenticated streaming group export with self-service read tokens#199
jgmontoya merged 2 commits into
masterfrom
feat/authenticated-group-export

Conversation

@jgmontoya

@jgmontoya jgmontoya commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Open in Stage

Summary by CodeRabbit

  • New Features
    • Added read-only personal access tokens (profile management: create, list, revoke) plus a CLI command to mint tokens.
    • Introduced an authenticated NDJSON “Group Export (streaming)” endpoint for token-based reads.
  • Bug Fixes
    • Streaming exports are fail-closed: mid-stream failures now end with an in-band error marker instead of a proper EOF.
    • Revoked/expired tokens (and tokens owned by deactivated users) no longer authenticate; token types remain non-interchangeable.
  • Documentation
    • Updated API and deployment notes with the NDJSON contract and the GOGGLES_EXPORTS_ENABLED kill-switch.
  • Chores
    • Updated Gunicorn settings for threaded long-running streaming and improved request recycling.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: caf1074f-18bc-4428-8f7e-cde6dd843390

📥 Commits

Reviewing files that changed from the base of the PR and between 441a15e and 209bf45.

📒 Files selected for processing (5)
  • config/settings.py
  • docs/api-v1.md
  • docs/deployment.md
  • forensics/tests.py
  • forensics/views.py
✅ Files skipped from review due to trivial changes (2)
  • docs/deployment.md
  • docs/api-v1.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • forensics/views.py
  • forensics/tests.py

Walkthrough

Adds shared bearer-token crypto, a new PersonalAccessToken model and management flow, NDJSON group export streaming, export auth/route changes, deployment/config toggles, and expanded tests and docs.

Changes

Personal access tokens and streaming group export

Layer / File(s) Summary
Shared token crypto module
forensics/token_crypto.py
Adds raw token generation, hashing, legacy key migration, expiry, and authentication helpers for bearer tokens.
UploadToken refactor and PersonalAccessToken model
forensics/models.py, forensics/migrations/0013_personalaccesstoken.py, forensics/admin.py, AGENTS.md
Refactors UploadToken to delegate to token_crypto and adds the PersonalAccessToken model, migration, admin registration, and lifecycle documentation.
Token issuance CLI and profile UI
forensics/management/commands/create_access_token.py, forensics/views.py, forensics/templates/forensics/profile.html, forensics/urls.py
Adds the create_access_token command and profile-page routes, view logic, and template UI for creating and revoking personal access tokens.
NDJSON streaming helper
forensics/streaming.py
Adds the reusable NDJSON export helper that emits manifest, section records, and eof or error termination lines.
Group export streaming API endpoint
forensics/views.py, forensics/urls.py, forensics/analysis.py, config/settings.py, Dockerfile, docker-compose.yml, docs/api-v1.md, docs/deployment.md
Adds bearer auth helpers, the group export streaming endpoint, the delivery identity SQL rewrite, export schema constants, and related routing plus deployment and docs updates.
Test coverage for tokens and streaming export
forensics/tests.py
Adds test coverage for token lifecycle, NDJSON streaming, identity indexing, export access control, profile token UI, and the create_access_token command.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: an authenticated streaming group export plus self-service read tokens.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/authenticated-group-export

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

@stage-review

stage-review Bot commented Jul 6, 2026

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
Dockerfile (1)

33-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Gunicorn flags duplicated across three files risk drifting.

The same --workers 3 --threads 4 --timeout 300 --max-requests 500 --max-requests-jitter 50 string is hardcoded here, in docker-compose.yml, and described in docs/deployment.md. A future tuning change is likely to update one and miss the others.

Consider centralizing these in a single source (e.g., a gunicorn.conf.py, an entrypoint script, or env-var-driven GUNICORN_CMD_ARGS) referenced by both the Dockerfile and compose file.

🤖 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 `@Dockerfile` around lines 33 - 35, The Gunicorn startup options are duplicated
in the Dockerfile, compose setup, and deployment docs, so update the service to
read them from one shared source instead of hardcoding the same flags in
multiple places. Centralize the configuration in a single location such as a
gunicorn.conf.py, entrypoint script, or GUNICORN_CMD_ARGS, and have both the
Dockerfile CMD and docker-compose.yml reference that shared source while keeping
docs aligned with the chosen approach.
🤖 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.

Nitpick comments:
In `@Dockerfile`:
- Around line 33-35: The Gunicorn startup options are duplicated in the
Dockerfile, compose setup, and deployment docs, so update the service to read
them from one shared source instead of hardcoding the same flags in multiple
places. Centralize the configuration in a single location such as a
gunicorn.conf.py, entrypoint script, or GUNICORN_CMD_ARGS, and have both the
Dockerfile CMD and docker-compose.yml reference that shared source while keeping
docs aligned with the chosen approach.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df956539-5b76-45bc-8e6d-7716d5a26a39

📥 Commits

Reviewing files that changed from the base of the PR and between e7134bf and d4a80fd.

📒 Files selected for processing (17)
  • AGENTS.md
  • Dockerfile
  • config/settings.py
  • docker-compose.yml
  • docs/api-v1.md
  • docs/deployment.md
  • forensics/admin.py
  • forensics/analysis.py
  • forensics/management/commands/create_access_token.py
  • forensics/migrations/0013_personalaccesstoken.py
  • forensics/models.py
  • forensics/streaming.py
  • forensics/templates/forensics/profile.html
  • forensics/tests.py
  • forensics/token_crypto.py
  • forensics/urls.py
  • forensics/views.py

@erskingardner erskingardner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two findings from an adversarial pass. The first one is the merge-blocker in my view; the second is a smaller fail-closed cleanup for token expiry handling.

Comment thread forensics/views.py
Comment thread forensics/views.py Outdated
@jgmontoya
jgmontoya force-pushed the feat/authenticated-group-export branch from d4a80fd to b1f2b94 Compare July 7, 2026 14:46
@jgmontoya
jgmontoya requested a review from erskingardner July 7, 2026 14:47

@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 (1)
forensics/models.py (1)

29-141: 📐 Maintainability & Code Quality | 🔵 Trivial

Consider extracting shared token fields/behavior into an abstract base model.

UploadToken and PersonalAccessToken duplicate the entire field set (token_prefix, token_hash, is_active, created_at, expires_at, last_used_at) plus identical mark_used() bodies and nearly-identical is_expired()/authenticate() wrappers. An abstract base model (token_crypto-backed mixin) would keep the two credential types in sync as the crypto/lifecycle logic evolves, and would also resolve the asymmetry where only UploadToken exposes a public hash_secret() wrapper.

Not blocking, since the delegation to token_crypto already centralizes the actual crypto logic — this is purely about reducing duplicated field/method boilerplate between the two models.

🤖 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/models.py` around lines 29 - 141, Extract the duplicated token
fields and lifecycle helpers from UploadToken and PersonalAccessToken into a
shared abstract base model or token_crypto-backed mixin. Move the common fields
(token_prefix, token_hash, is_active, created_at, expires_at, last_used_at) and
shared methods like mark_used(), is_expired(), and the common authenticate/hash
wrapper behavior into the base, then keep only model-specific pieces such as
PersonalAccessToken.user and its owner-active check in authenticate(). This
keeps both models aligned and removes the current asymmetry where only
UploadToken exposes hash_secret().
🤖 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 2871-2877: Guard the expiry parsing in the shared token-expiration
logic so extreme `expires_in_days` values cannot reach `timezone.now() +
timedelta(days=days)` and raise `OverflowError`; in the function that currently
converts `value` with `int()` and returns the timedelta, add an upper-bound
validation and reject too-large inputs with a `ValueError` before the addition.
Apply the same protection in `create_access_token` so both the POST path and the
CLI command use the same safe validation behavior.

---

Nitpick comments:
In `@forensics/models.py`:
- Around line 29-141: Extract the duplicated token fields and lifecycle helpers
from UploadToken and PersonalAccessToken into a shared abstract base model or
token_crypto-backed mixin. Move the common fields (token_prefix, token_hash,
is_active, created_at, expires_at, last_used_at) and shared methods like
mark_used(), is_expired(), and the common authenticate/hash wrapper behavior
into the base, then keep only model-specific pieces such as
PersonalAccessToken.user and its owner-active check in authenticate(). This
keeps both models aligned and removes the current asymmetry where only
UploadToken exposes hash_secret().
🪄 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

Run ID: 614663a0-689e-46a1-ae37-17280dd69049

📥 Commits

Reviewing files that changed from the base of the PR and between d4a80fd and b1f2b94.

📒 Files selected for processing (17)
  • AGENTS.md
  • Dockerfile
  • config/settings.py
  • docker-compose.yml
  • docs/api-v1.md
  • docs/deployment.md
  • forensics/admin.py
  • forensics/analysis.py
  • forensics/management/commands/create_access_token.py
  • forensics/migrations/0013_personalaccesstoken.py
  • forensics/models.py
  • forensics/streaming.py
  • forensics/templates/forensics/profile.html
  • forensics/tests.py
  • forensics/token_crypto.py
  • forensics/urls.py
  • forensics/views.py
✅ Files skipped from review due to trivial changes (3)
  • AGENTS.md
  • docs/deployment.md
  • docker-compose.yml
🚧 Files skipped from review as they are similar to previous changes (8)
  • forensics/management/commands/create_access_token.py
  • Dockerfile
  • config/settings.py
  • forensics/analysis.py
  • forensics/urls.py
  • forensics/admin.py
  • forensics/token_crypto.py
  • forensics/templates/forensics/profile.html

Comment thread forensics/views.py Outdated
@jgmontoya
jgmontoya force-pushed the feat/authenticated-group-export branch from b1f2b94 to d3ffcab Compare July 7, 2026 16:02

@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/token_crypto.py`:
- Line 42: The lookup prefix generated in token_crypto is too short and can
collide during row creation. Update the lookup prefix generation used for
token_prefix to use a wider value in the same spot where secrets.token_hex(4) is
called, and keep it within the existing max_length constraints by switching to a
longer hex token. This change should be applied in the token generation flow
that feeds both token models so creation does not fail on avoidable collisions.
🪄 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

Run ID: 33de30af-9353-4889-b841-d1a8b39288a4

📥 Commits

Reviewing files that changed from the base of the PR and between b1f2b94 and d3ffcab.

📒 Files selected for processing (18)
  • AGENTS.md
  • Dockerfile
  • config/settings.py
  • docker-compose.yml
  • docs/api-v1.md
  • docs/deployment.md
  • forensics/admin.py
  • forensics/analysis.py
  • forensics/management/commands/create_access_token.py
  • forensics/management/commands/create_upload_token.py
  • forensics/migrations/0013_personalaccesstoken.py
  • forensics/models.py
  • forensics/streaming.py
  • forensics/templates/forensics/profile.html
  • forensics/tests.py
  • forensics/token_crypto.py
  • forensics/urls.py
  • forensics/views.py
✅ Files skipped from review due to trivial changes (2)
  • AGENTS.md
  • docs/deployment.md
🚧 Files skipped from review as they are similar to previous changes (9)
  • docker-compose.yml
  • forensics/urls.py
  • forensics/admin.py
  • forensics/templates/forensics/profile.html
  • forensics/analysis.py
  • config/settings.py
  • forensics/management/commands/create_access_token.py
  • Dockerfile
  • forensics/tests.py

Comment thread forensics/token_crypto.py Outdated
@jgmontoya
jgmontoya force-pushed the feat/authenticated-group-export branch from d3ffcab to c5e0e95 Compare July 7, 2026 16:57

@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: 3

🤖 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 13-19: The personal access token description is too broad and
should be scoped to the streaming group export only. Update the wording in the
export docs so the PAT text near the Authorization header and the `gpat_…`
mention states it can read the streaming group export, not general forensic data
or other forensic APIs. Keep the distinction from `goggles_…` upload tokens, and
adjust the `docs/api-v1.md` copy accordingly.
- Around line 125-136: The fenced NDJSON example in the API v1 docs is missing
an explicit language tag, which triggers the markdown lint warning. Update the
code block that contains the manifest/source/event/.../eof examples to use a
text fence instead of an unlabeled block, keeping the mixed NDJSON-and-comment
content readable and lint-compliant.

In `@forensics/views.py`:
- Around line 2918-2925: The one-time PAT creation response in the view that
issues PersonalAccessToken should be marked non-cacheable because it renders the
raw token once and must not be retained by caches. Update the return path that
renders "forensics/profile.html" with profile_context(..., new_token=raw_token)
to attach a Cache-Control: no-store header, following the same pattern used by
the export response, so the sensitive token is never cached.
🪄 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

Run ID: d019dd89-4af2-4bda-9dd8-fff3d4ce3c43

📥 Commits

Reviewing files that changed from the base of the PR and between d3ffcab and c5e0e95.

📒 Files selected for processing (18)
  • AGENTS.md
  • Dockerfile
  • config/settings.py
  • docker-compose.yml
  • docs/api-v1.md
  • docs/deployment.md
  • forensics/admin.py
  • forensics/analysis.py
  • forensics/management/commands/create_access_token.py
  • forensics/management/commands/create_upload_token.py
  • forensics/migrations/0013_personalaccesstoken.py
  • forensics/models.py
  • forensics/streaming.py
  • forensics/templates/forensics/profile.html
  • forensics/tests.py
  • forensics/token_crypto.py
  • forensics/urls.py
  • forensics/views.py
✅ Files skipped from review due to trivial changes (3)
  • docs/deployment.md
  • AGENTS.md
  • config/settings.py
🚧 Files skipped from review as they are similar to previous changes (10)
  • forensics/management/commands/create_upload_token.py
  • forensics/analysis.py
  • forensics/urls.py
  • docker-compose.yml
  • Dockerfile
  • forensics/admin.py
  • forensics/templates/forensics/profile.html
  • forensics/management/commands/create_access_token.py
  • forensics/token_crypto.py
  • forensics/tests.py

Comment thread docs/api-v1.md Outdated
Comment thread docs/api-v1.md Outdated
Comment thread forensics/views.py Outdated
@jgmontoya
jgmontoya force-pushed the feat/authenticated-group-export branch from c5e0e95 to 441a15e Compare July 7, 2026 17:15
@jgmontoya

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

@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.

🧹 Nitpick comments (1)
forensics/views.py (1)

2185-2186: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Clear ordering before DISTINCT in distinct_values().

AuditEvent has default ordering, so this query can carry ordering columns into DISTINCT and return extra rows for the same identity. Clearing ordering keeps the dedupe at the database level.

♻️ Proposed change
     def distinct_values(queryset, field: str) -> set[str]:
-        return {value for value in queryset.values_list(field, flat=True).distinct() if value}
+        return {
+            value
+            for value in queryset.order_by().values_list(field, flat=True).distinct()
+            if value
+        }
🤖 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/views.py` around lines 2185 - 2186, The distinct_values() helper is
relying on the queryset’s default ordering, which can leak ordering columns into
the DISTINCT and produce duplicate identities. Update distinct_values() to clear
ordering on the queryset before calling values_list(...).distinct(), preserving
the existing field filtering while ensuring deduplication happens correctly at
the database level.
🤖 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.

Nitpick comments:
In `@forensics/views.py`:
- Around line 2185-2186: The distinct_values() helper is relying on the
queryset’s default ordering, which can leak ordering columns into the DISTINCT
and produce duplicate identities. Update distinct_values() to clear ordering on
the queryset before calling values_list(...).distinct(), preserving the existing
field filtering while ensuring deduplication happens correctly at the database
level.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 027a2fdc-9bc7-408b-abf1-b42a956fbac7

📥 Commits

Reviewing files that changed from the base of the PR and between c5e0e95 and 441a15e.

📒 Files selected for processing (18)
  • AGENTS.md
  • Dockerfile
  • config/settings.py
  • docker-compose.yml
  • docs/api-v1.md
  • docs/deployment.md
  • forensics/admin.py
  • forensics/analysis.py
  • forensics/management/commands/create_access_token.py
  • forensics/management/commands/create_upload_token.py
  • forensics/migrations/0013_personalaccesstoken.py
  • forensics/models.py
  • forensics/streaming.py
  • forensics/templates/forensics/profile.html
  • forensics/tests.py
  • forensics/token_crypto.py
  • forensics/urls.py
  • forensics/views.py
✅ Files skipped from review due to trivial changes (3)
  • forensics/analysis.py
  • docs/deployment.md
  • AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (11)
  • config/settings.py
  • forensics/urls.py
  • Dockerfile
  • forensics/admin.py
  • forensics/templates/forensics/profile.html
  • forensics/management/commands/create_upload_token.py
  • forensics/management/commands/create_access_token.py
  • docker-compose.yml
  • forensics/token_crypto.py
  • docs/api-v1.md
  • forensics/tests.py

@erskingardner

Copy link
Copy Markdown
Member

I don't know if this is that helpful at this point but I ran another agent (composer 2.5) against this PR and got all this back:

Adversarial review of PR #199. CI is green and several earlier review threads (filter contract, expiry validation) look addressed. Below are the issues I’d still raise, ordered by severity.


Critical / high

1. iterator() silently drops prefetch_related — export will N+1 hard on projection sections

stream_ndjson always walks sections with .iterator(). Django explicitly ignores prefetch_related() when iterator() is used.

The export reuses querysets built for paginated APIs:

  • delivery_artifact_queryset() — heavy prefetch_related on evidence, engine observations, recipient expectations
  • convergence_run_queryset() — prefetches evidence, candidates, rule evaluations

But delivery_artifact_payload() still does .all() on related managers and touches expectation.evidence_event per row. Without prefetch, that’s multiple queries per artifact, per run, for the entire group.

Impact: A large group export can turn into thousands–millions of DB round-trips, blow past the 300s gunicorn timeout, and become both a reliability bug and a self-inflicted DoS (one export pegs Postgres).

Ask: Either:

  • add export-specific querysets with select_related/Prefetch patterns that work with iterator(), or
  • batch projection sections differently (chunked prefetch windows), or
  • at minimum document + test query count for a representative export.

Events/sources look fine (select_related("audit_file") survives iterator()).


2. No abuse controls on a new expensive, long-lived read surface

This endpoint holds a DB connection for minutes, serializes unbounded rows, and is reachable with a long-lived bearer token. Capacity is ~workers × threads (12) per instance, documented honestly — but nothing limits who can consume those slots:

  • No per-user / per-token concurrency limit
  • No rate limit (consistent with uploads, but exports are much costlier)
  • Any authenticated session or any valid PAT can hammer every group slug
  • --timeout 300 is process-wide, relaxing liveness for uploads and all other routes too

Impact: One compromised PAT, one angry script, or one misconfigured CGKA poller can exhaust worker threads + Postgres connections and degrade the whole app.

Ask: Consider at least one of: per-IP/per-token concurrency cap, require GOGGLES_EXPORTS_ENABLED + ops runbook default-off until sized, shorter PAT default TTL for service accounts, or edge rate limiting on /api/v1/groups/*/export/.


3. PAT blast radius is the entire forensic corpus

By design, Goggles is single-tenant and authenticate_reader() is not group-scoped. A PAT is equivalent to “logged-in user who can read everything” — but unlike a browser session it’s:

  • long-lived (often never-expiring)
  • easily copied into automation
  • self-service mintable from the profile page with no admin approval

Impact: Leaked gpat_… is standing bulk exfiltration of all groups, not just one. Upload tokens are least-privilege by comparison.

Ask: For service-account minting (create_access_token), consider requiring --expires-in-days by default, or documenting that non-expiring PATs are equivalent to permanent database dump credentials. Optional future: group-scoped tokens if CGKA only needs one group.


Medium

4. DISABLE_SERVER_SIDE_CURSORS is documented but not wired

docs/deployment.md tells operators to set DISABLE_SERVER_SIDE_CURSORS=1 behind PgBouncer transaction pooling, but config/settings.py never reads it. Following the doc today does nothing unless someone adds the setting manually.

Ask: Wire it in settings (Django supports this as a top-level setting) and mention it in .env.example.


5. mark_used() runs before the group exists check

In api_group_export_stream:

if not authenticate_reader(request): ...
group = get_object_or_404(AuditGroup, slug=slug)

For PAT auth, authenticate_reader() writes last_used_at before slug validation. Probing random slugs still mutates token state and creates audit noise.

Ask: Move mark_used() to after get_object_or_404, or split “authenticate” from “record usage”.


6. Auth is point-in-time only — revocation doesn’t stop an in-flight export

Revoking a PAT or deactivating the owner takes effect on the next request. An export that already passed auth can continue streaming sensitive data for minutes.

Probably acceptable for v1, but worth calling out in docs/api-v1.md so incident response doesn’t assume “revoke = immediate cut-off”.


7. Token issuance has no collision retry

token_crypto.generate_raw_token() picks a 64-bit lookup prefix and inserts with a unique constraint, but neither PersonalAccessToken.issue() nor the profile POST handler retries on IntegrityError.

Birthday collision is unlikely; concurrent double-submit on a bad day is less so. Failure mode is an unhandled 500 on “Generate token”.

Ask: Retry issuance a few times on integrity error (cheap fix).


8. .env.example missing new kill-switch

GOGGLES_EXPORTS_ENABLED is added to settings and documented in deployment docs, but not in .env.example. Operators may not discover it until they need to shed load.


Low / nits

9. In-band error line is intentionally opaque

{"t":"error","complete":false} gives consumers no failure class (DB timeout vs serialization vs disconnect). Fine for fail-closed, but CGKA will only know “discard and retry”. Consider a stable error_code enum without leaking internals.

10. No cap on tokens per user

Any logged-in user can mint unlimited PATs from the profile UI. Low risk internally, but increases credential sprawl and revocation toil.

11. Profile token creation is not idempotent

Double-click / double POST creates two tokens with the same name. Minor UX/security hygiene issue.

12. Test gap: unknown group slug

Auth paths are well tested; I don’t see a 404 test for GET …/export/not-a-real-slug/ with a valid PAT. Small addition for contract clarity.

13. Append-time consistency is honest but sharp-edged

Docs say each section is its own transaction. Adversarial consumer: they may treat eof.complete=true as snapshot isolation and miss records that landed mid-stream. Docs cover this — just ensure CGKA handles it (re-export or tolerate skew).


What looks solid (credit where due)

  • Upload vs PAT separation (goggles_ vs gpat_, cross-auth tests) is clean.
  • token_crypto extraction avoids UploadToken/PAT drift; expiry overflow handling is thoughtful.
  • Fail-closed mid-stream contract (error without eof) is the right HTTP semantics.
  • Filter inconsistency regression is fixed: export ignores query params consistently; test proves it.
  • Profile revoke is owner-scoped (get_object_or_404(..., user=request.user) + test).
  • One-time token display uses Cache-Control: no-store and avoids redirect-to-session.
  • delivery_identity_index SQL rewrite has parity tests — good catch for the hot path.
  • Test coverage is unusually thorough for a feature PR (~600 new lines).

Summary verdict

I’d not block on the token/crypto/auth design — that part is careful. I would block or strongly request changes on the N+1 + iterator/prefetch interaction before shipping to production with real group sizes; that’s the most likely “works in tests, dies in prod” failure mode. Second priority is operational abuse: concurrent unbounded exports + long-lived PATs on a connection-hungry endpoint.

Want me to draft specific inline review comments formatted for GitHub, or spike a query-count test against the PR branch locally?

@jgmontoya

Copy link
Copy Markdown
Contributor Author

This endpoint records the common query filters in the manifest, but this event section does not use them; the source section above is also unfiltered. I reproduced ?engine_id=... and got a manifest advertising that filter while the event rows still included another engine...

you're right, and I went the other way instead of completing the filtering: the export now takes no query filters at all, filters is gone from the manifest too. Applying them to only some sections misrepresents the payload (exactly what you reproduced), and filtering raw events would break the completeness contract the export exists to provide (it feeds the fail-closed replay pipeline, which needs the whole group). Narrowed views stay on the paginated projection endpoints; rationale documented in api-v1.md. Shout if you'd rather have real filtering across all sections, but I think complete-or-nothing is the more honest contract for this endpoint.

This fails open for bad expiry input: 0, negative numbers, or an unparsable value all become None, which mints a never-expiring token... Could we validate this server-side and reject non-positive/unparseable values, matching the stricter create_access_token --expires-in-days behavior?

fixed, parse_access_token_expiry now validates server-side and rejects non-positive/unparsable values with a rendered error (no token minted), matching the management command's behavior. Tests cover 0, negatives, and garbage input.

@jgmontoya
jgmontoya merged commit 95829c1 into master Jul 8, 2026
3 checks passed
@jgmontoya
jgmontoya deleted the feat/authenticated-group-export branch July 8, 2026 15:38
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.

2 participants