4.1 KiB
type, universe, status, consumes, produces
| type | universe | status | consumes | produces | ||
|---|---|---|---|---|---|---|
| process | live | verified |
|
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
before_action :authenticate_user!on every controller (app/controllers/application_controller.rb:6). Devise matches onusername, not email (config/initializers/devise.rb:49).before_action :set_navbar_stocksrunspolicy_scope(Stock).activeon every request (app/controllers/application_controller.rb:8,22-24) —StockPolicy::Scopereturnsscope.all(app/policies/stock_policy.rb:52-56).- Under
/admin,authenticate_adminredirects non-admins (app/controllers/admin/base_controller.rb:13-15). - Elsewhere, the action calls
authorize recordorpolicy_scope(Model). Role helpers live on the base policy (app/policies/application_policy.rb:39-53). - On
Pundit::NotAuthorizedError, redirect by role (app/controllers/application_controller.rb:31-40).
Gaps worth knowing
Stated as found, not as a recommendation:
OrdersController#editand#updatenever authorize.set_orderis an unscopedOrder.find(app/controllers/orders_controller.rb:4,66-68) and onlycancelcallsauthorize(:50).OrderPolicydefinesupdate?(app/policies/order_policy.rb:12-14), but nothing invokes it.GradeBookPolicy#finalize?isuser.admin?(app/policies/grade_book_policy.rb:12-14). Teachers mayshowandupdatea gradebook but cannot finalize it — only admins release earnings.ClassroomPolicy::Scopedoes not inheritApplicationPolicy::Scope(app/policies/classroom_policy.rb:40-57) and returns a bare[]rather thanscope.nonefor non-teachers — an Array where callers expect a relation.OrdersController#destroyis defined belowprivate(:59,105-112), so the routedDELETE /orders/:idcannot 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,StockAttributeUpdateJobandMonthlyPortfolioSnapshotJobrun with nocurrent_userand 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 |