Files
project-work/worker-toolkit-stocks-in-the-future/repo/docs/map/processes/authenticate-authorize.md

4.1 KiB

type, universe, status, consumes, produces
type universe status consumes produces
process live verified
../objects/identity/user.md
../objects/trading/stock.md

authenticate-authorize

Every request proves who you are with Devise, then proves you may act with Pundit.

Verified 2026-08-16 against commit 63732df.

Input → Movement → Output

A request arrives with a session cookie. ApplicationController authenticates it by username, loads the navbar's stock list, and the controller action asks a Pundit policy whether this user may proceed. The action runs, or a rescue_from redirects the user somewhere they are allowed to be.

Why this shape

Authorization is opt-in, per action. Pundit's verify_authorized after-action is not enabled anywhere in the app — a controller that never calls authorize is simply not authorized, and nothing complains. Two consequences are live today; see Gaps below.

The admin area does not use Pundit at all. Admin::BaseController has its own before_action :authenticate_admin that redirects unless current_user&.admin? (app/controllers/admin/base_controller.rb:9,13-15). So /admin is guarded by one line, not by policies, and adding a policy will not protect an admin controller.

The rescue_from sends users somewhere sensible instead of a 403 — students go to their own portfolio, everyone else to root (app/controllers/application_controller.rb:31-40). That is why an authorization failure often looks like a redirect loop rather than an error.

Steps

  1. before_action :authenticate_user! on every controller (app/controllers/application_controller.rb:6). Devise matches on username, not email (config/initializers/devise.rb:49).
  2. before_action :set_navbar_stocks runs policy_scope(Stock).active on every request (app/controllers/application_controller.rb:8,22-24) — StockPolicy::Scope returns scope.all (app/policies/stock_policy.rb:52-56).
  3. Under /admin, authenticate_admin redirects non-admins (app/controllers/admin/base_controller.rb:13-15).
  4. Elsewhere, the action calls authorize record or policy_scope(Model). Role helpers live on the base policy (app/policies/application_policy.rb:39-53).
  5. On Pundit::NotAuthorizedError, redirect by role (app/controllers/application_controller.rb:31-40).

Gaps worth knowing

Stated as found, not as a recommendation:

  • OrdersController#edit and #update never authorize. set_order is an unscoped Order.find (app/controllers/orders_controller.rb:4,66-68) and only cancel calls authorize (:50). OrderPolicy defines update? (app/policies/order_policy.rb:12-14), but nothing invokes it.
  • GradeBookPolicy#finalize? is user.admin? (app/policies/grade_book_policy.rb:12-14). Teachers may show and update a gradebook but cannot finalize it — only admins release earnings.
  • ClassroomPolicy::Scope does not inherit ApplicationPolicy::Scope (app/policies/classroom_policy.rb:40-57) and returns a bare [] rather than scope.none for non-teachers — an Array where callers expect a relation.
  • OrdersController#destroy is defined below private (:59,105-112), so the routed DELETE /orders/:id cannot dispatch to it. unauthorized_response (:83-88) is never called.

If you change this

  • Hits: every controller — this is the one movement with no local blast radius; ApplicationController, Admin::BaseController, all six policies; user if you touch the auth key.
  • Does not hit: the four scheduled jobs. OrderExecutionJob, StockPricesUpdateJob, StockAttributeUpdateJob and MonthlyPortfolioSnapshotJob run with no current_user and never consult a policy — tightening authorization cannot break them, and cannot protect them either.

Surfaces

Surface Role
every request authenticated
/admin/* admin boolean gate, not Pundit
Solid Queue jobs bypass entirely

See

  • Objects: user, stock
  • Source: app/controllers/application_controller.rb, app/controllers/admin/base_controller.rb, app/policies/