Scope Every Query To The Current User Without Polluting Every Method
Writing current_user.orders.find(params[:id]) everywhere works right up until someone writes Order.find(params[:id]) in a new controller and exposes another user’s data. The fix is to make the scoped path the only path.
Description
Load records through the association from the current user or account rather than through the model class. That turns an authorization check into a query condition, so a record belonging to someone else returns 404 instead of 403 — which also avoids leaking that the record exists.
Centralise it in a single scoped_resource helper in ApplicationController so every controller uses the same path and there is one place to audit.
ActiveSupport::CurrentAttributes makes the current user available to models and jobs without threading it through every method signature, and it resets between requests.
Back it with a spec that iterates your controllers and asserts a foreign record 404s.
Sample input:
GET /orders/999 # an order belonging to another account
Sample Output:
404 Not Found (not 403 — the existence of the record is not disclosed)
Answer
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :user, :account
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_current
before_action :authenticate_user!
private
def set_current
Current.user = authenticated_user
Current.account = Current.user&.account
end
# The single scoped entry point every controller uses
def scoped(model)
raise "No current account" if Current.account.nil?
Current.account.public_send(model.model_name.plural)
end
end
class OrdersController < ApplicationController
def index = @orders = scoped(Order).recent
def show = @order = scoped(Order).find(params[:id]) # 404 for others
def create
@order = scoped(Order).build(order_params)
# ...
end
end
# The spec that keeps it honest
RSpec.describe "tenant isolation", type: :request do
it "404s on another account's order" do
foreign = create(:order)
sign_in create(:user)
get "/orders/#{foreign.id}"
expect(response).to have_http_status(:not_found)
end
end
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀