Strong parameters limit which submitted attributes may reach model assignment. They do not identify the caller, verify the caller's permission for a record, or decide whether a state transition is legal. A Rails controller should derive identity from its authenticated server session, scope the requested record to that identity, and pass a narrow command to a service. A route ID is an address, not a grant. Browser CSRF protection checks whether a credentialed mutation carries the expected authenticity token; it does not replace case-level permission. The response deserves a separate projection so an authorized view does not accidentally disclose every model column or association.
Rails Controller Parameters and Object Permission
Working case
Reviewer 47 opens permit 62, while reviewer 81 guesses its URL. Both have valid accounts. A controller calls PermitCase.find(params[:id]), permits the submitted status field, and renders the model as JSON. Reviewer 81 can read a private district note; a later forged form can set status directly because the controller confuses a permitted field with an allowed action. A navigation menu filtered by reviewer hid the case, but it never protected the route. The repaired controller loads case 62 from a reviewer-scoped relation, sends only operation ID and reason to an approval service, and returns a deliberate case ID and status. The service checks current assignment again when it writes.
Implementation boundary
class PermitsController < ApplicationController
before_action :require_reviewer
def show
permit = PermitCase.joins(:reviewer_assignments)
.where(reviewer_assignments: { reviewer_id: Current.reviewer.id })
.find(params[:id])
render json: { case_id: permit.id, title: permit.title, status: permit.status }
end
private
def approval_params
params.require(:approval).permit(:operation_id, :expected_revision, :reason)
end
endApply an authentication guard to private actions and keep request forgery protection active for cookie-backed browser mutations. Require the expected command envelope and permit only its defined keys; reject missing or malformed values before using them. Do not permit reviewer_id, case_id, or status from the browser for an approval action. Load detail and export metadata through the same record scope, then render an explicit output hash or serializer with approved fields. For a private case, choose whether forbidden and nonexistent IDs share a missing-style response so ID guessing reveals no existence signal. Passing a scoped relation into a service is safer than fetching globally and relying on every downstream caller to remember a second filter.
Cost and boundaries
An assignment-scoped detail lookup adds a join or existence predicate and needs indexes on case and reviewer IDs. This is a small, measurable price for preventing cross-account reads. A policy implemented by lazy-loading an assignment on each item of a 47-case list adds many queries; list authorization belongs in SQL before pagination. Explicit projections reduce transferred bytes and keep private attributes out of serialization even after permission succeeds. CSRF verification and parameter shape checks are cheap compared with database work, and they can reject hostile browser commands before locks are taken. Track query count, response width, forbidden/missing timing, and rejected command classes. A 404 policy should not hide security failures from internal incident logs.
Failure trace
Make requests anonymously, as reviewer 47, as reviewer 81, and with an unknown permit ID. Confirm the forbidden and unknown responses match the product's disclosure rule. Put an obvious private_note on permit 62 and assert it never enters detail or export JSON. Submit a form with a valid authenticity token but reviewer 81's session; it must still fail the case permission check. Submit an invalid token while signed in as reviewer 47 and verify the controller does not reach the service. Try mass assignment with status, reviewer_id, and an internal flag in the approval body; none should enter the command. Revoke reviewer 47's assignment between GET and POST and confirm the write is rejected.
Verification
- A signed-in reviewer is still scoped to the exact permit.
- Only named command fields can enter the approval service.
- The public response omits private model attributes.
Practice drill
Create a private permit controller for reviewer 47 and reviewer 81. Seed case 62 with one owner, one permitted reviewer, and a private note. The show action should scope by current reviewer and project only public title, case ID, and status. The approval action should accept a stable operation ID, expected revision, and bounded reason; it must never accept status or reviewer identity from params. Wire a browser form with the authenticity token and keep the non-GET protection active. Write request tests for guessed IDs, a missing record, missing token, extra attributes, and assignment revocation. Inspect actual SQL from the scoped relation and add indexes that match the join used by the route.
Decision note
Strong parameters define a command shape; server identity, record scope, and current transition rules decide permission.
Common Mistakes
- Treating permitted parameters as permission to approve.
- Authorizing a menu while leaving a detail route unscoped.
- Rendering an entire model after an access check.
Related lessons
Rails Request, Record, and Job Boundaries; Rails Active Record Scope, Eager Load, and Page Cost; Rails Locking, Transaction, and Command Replay; Rails Active Job After-Commit and Outbox Recovery; Authorization and Tenant Boundaries; Django Middleware, Sessions, and Object Permissions; Laravel Route Binding, Policy, and Private Resource Scope.
Apply and check
Build Project: Rails permit review workflow and review Web Development: Rails request, record, and job quiz.
