convert pdf to md, add OVERVIEW and docs

This commit is contained in:
2026-08-17 22:30:18 +00:00
parent 95d5787868
commit 4df62d2609
44 changed files with 3307 additions and 1 deletions

View File

@@ -0,0 +1,93 @@
---
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/`