--- type: process universe: live status: verified consumes: ["../objects/identity/user.md", "../objects/trading/stock.md"] produces: [] --- # 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](../objects/identity/user.md) 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](../objects/identity/user.md), [stock](../objects/trading/stock.md) - Source: `app/controllers/application_controller.rb`, `app/controllers/admin/base_controller.rb`, `app/policies/`