Skip to content

fix: gate review LiveView with selfhosted+OAuth auth requirement#145

Merged
tomasz-tomczyk merged 2 commits intomainfrom
fix-review-live-auth-gate
Apr 30, 2026
Merged

fix: gate review LiveView with selfhosted+OAuth auth requirement#145
tomasz-tomczyk merged 2 commits intomainfrom
fix-review-live-auth-gate

Conversation

@tomasz-tomczyk
Copy link
Copy Markdown
Owner

@tomasz-tomczyk tomasz-tomczyk commented Apr 30, 2026

Summary

  • The /r/:token review LiveView did not enforce auth in selfhosted+OAuth mode, even though ApiAuth already gated the JSON API under the same condition. An existing inline template gate masked the leak — mount/3 still subscribed to PubSub, called touch_last_activity, and push_event "init" with full comments+files to unauthenticated clients.
  • New :require_review_auth on_mount hook redirects unauthenticated visitors to /auth/login?return_to=... before mount/3 runs in selfhosted+OAuth mode.
  • New Crit.Config.selfhosted_oauth?/0 is now the single source of truth for the predicate (replaces three duplicated checks).
  • Moves dev/test-only seed routes (/api/test/seed-user etc.) out of the :api pipeline (where ApiAuth blocks them in selfhosted mode) into a :device_api scope still gated by Mix.env() in [:test, :dev].
  • Adds scripts/start-selfhosted.sh for booting a SELFHOSTED instance on :4001 with a separate DB (used by the companion crit/ integration tests).

Hosted (non-SELFHOSTED) behavior — unchanged

selfhosted_oauth?/0 requires both selfhosted == true AND oauth_provider != nil. Hosted instances with only oauth_provider set return false → all gates short-circuit. Anonymous reviewers and OAuth-logged-in users continue to work exactly as before.

Review

  • Code review: passed (3 elixir-expert passes — initial + suggestions + final)
  • Parity audit: N/A (backend-only)

Test plan

  • New test/crit_web/live/review_live_auth_gate_test.exs asserts the redirect tuple AND that last_activity_at is unchanged + no PubSub rebroadcast — proves mount/3 did not fire.
  • mix precommit clean: 471 tests, 0 failures, sobelow + deps.audit clean.
  • End-to-end coverage in test: add selfhosted+OAuth integration test suite crit#400 via new make test-share-sync-selfhosted.

🤖 Generated with Claude Code

The /r/:token review LiveView did not enforce auth in selfhosted+OAuth
mode, even though ApiAuth already gated the JSON API under the same
condition. Worse, an existing inline template gate masked the leak —
mount/3 still subscribed to PubSub, called touch_last_activity, and
push_event "init" with full comments+files to unauthenticated clients.

Closes the gap with a new :require_review_auth on_mount hook (mirrors
ApiAuth's enforcement condition via a new Crit.Config.selfhosted_oauth?/0
helper that's now the single source of truth for the predicate). On
selfhosted+OAuth, unauthenticated visitors are redirected to
/auth/login?return_to=<request_path> before mount/3 runs. Public/hosted
mode (no SELFHOSTED env) is unchanged — anonymous reviewers and OAuth
sign-in continue to work exactly as before.

Also moves dev/test-only seed routes (/api/test/seed-user etc.) from
the :api pipeline to a separate dev/test-only :device_api scope, so
they're reachable for integration tests against an enforcing instance.
Production unaffected (these routes only exist in :test/:dev envs).

Adds scripts/start-selfhosted.sh for booting a SELFHOSTED+OAuth
instance on :4001 with a separate DB, used by the companion crit/
selfhosted integration test suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 77.23%. Comparing base (97f82fc) to head (bf7abf6).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
lib/crit_web/live/review_live.ex 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #145      +/-   ##
==========================================
- Coverage   77.25%   77.23%   -0.03%     
==========================================
  Files          52       53       +1     
  Lines        1574     1577       +3     
==========================================
+ Hits         1216     1218       +2     
- Misses        358      359       +1     
Flag Coverage Δ
unit 77.23% <94.11%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Previously, the owner check (`current_user && (is_nil(review.user_id) ||
review.user_id == current_user.id)`) treated anonymous reviews
(`user_id == nil`) as owned by any logged-in user — both in the
LiveView UI gate and in the server-side `Reviews.delete_review/2`
guard. This let any authenticated user delete any anonymous review.

Anonymous reviews can only be deleted by their `delete_token` (via
`Reviews.delete_by_delete_token/1`), never by an authenticated user.
User-owned reviews can only be deleted by the matching user.

Also drop the redundant `enforced?/1` wrapper in `ApiAuth` and call
`Crit.Config.selfhosted_oauth?/0` directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tomasz-tomczyk tomasz-tomczyk merged commit 31ce594 into main Apr 30, 2026
4 checks passed
@tomasz-tomczyk tomasz-tomczyk deleted the fix-review-live-auth-gate branch April 30, 2026 12:39
tomasz-tomczyk added a commit that referenced this pull request Apr 30, 2026
Post-rebase adjustments after pulling main:

- review_live.ex: drop unused current_user binding in mount_review/4
  (current_user lookup is no longer needed locally — auth gating runs in
  the :require_review_scope on_mount hook).
- reviews_test.exs: adapt the three tests #145 added that called
  delete_review(id, owner_id: ...) to the scope-first signature
  delete_review(scope, id). Also flip the "legacy/anonymous review can
  be deleted by any authed user" test — main's #145 tightened that
  guard so anonymous reviews can only be removed via delete_token, and
  the new scope-side guard matches that semantics (review.user_id !=
  scope.user.id rejects nil user_id).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tomasz-tomczyk added a commit that referenced this pull request Apr 30, 2026
…ew (#143)

* feat: add Crit.Accounts.Scope struct

* feat: add CritWeb.UserAuth plug and on_mount hooks

* refactor: thread Crit.Accounts.Scope through Reviews and LiveView

* docs: add scope pattern reference

Project-local rules for the Phoenix 1.8 Scope pattern adopted in
Crit.Reviews + LiveView + controllers. Documents argument-order
convention, when functions take scope, the display_name rule, the
mutual-exclusion invariant, and the migration path for new code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: address scope-refactor code review

- Inline `load_user/1` into `Scope.for_session/1` (single call site).
- Refactor `resolve_comment` to two simple Repo lookups + private
  `can_resolve_comment?(scope, comment, review)` helper; drop the
  joined Repo.one + projection map.
- Make `comment_owned_by?/2` take `(scope, comment)` instead of
  `(comment, identity, user_id)`.
- Normalise `update_display_name/2` to return `:ok` for both anon and
  auth scopes (was asymmetric).
- Only persist `display_name` to session for anonymous scopes in
  `ReviewController.set_name`.
- `Layouts.site_header` and `Layouts.dashboard_header` take
  `current_scope` attr instead of `current_user`; matches phx.gen.auth
  1.8 generator output and the local CLAUDE.md scope-pattern rules.
- Sweep all 12 header call sites to pass `current_scope={@current_scope}`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: harmonize scope refactor with main's review-LV auth gate (#145)

Post-rebase adjustments after pulling main:

- review_live.ex: drop unused current_user binding in mount_review/4
  (current_user lookup is no longer needed locally — auth gating runs in
  the :require_review_scope on_mount hook).
- reviews_test.exs: adapt the three tests #145 added that called
  delete_review(id, owner_id: ...) to the scope-first signature
  delete_review(scope, id). Also flip the "legacy/anonymous review can
  be deleted by any authed user" test — main's #145 tightened that
  guard so anonymous reviews can only be removed via delete_token, and
  the new scope-side guard matches that semantics (review.user_id !=
  scope.user.id rejects nil user_id).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: destructure current_scope in set_display_name handler

Matches the convention used by every other handler in this module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant