Tadas Sasnauskas Tech/Engineering Blog

Avoid ActiveRecord queries starting with model class

When writing a Rails resource controller code it’s quite common to start with querying from model class itself: Model.find, Model.find_by, Model.where, etc.. After all for simplicity sake it is what’s given in some Rails guides examples:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end

  def show
    @article = Article.find(params[:id])
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find(params[:id])
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @article = Article.find(params[:id])
    @article.destroy

    redirect_to root_path, status: :see_other
  end

  private
    def article_params
      params.require(:article).permit(:title, :body)
    end
end

What is not shown in these examples is user authorisation to view/create/update/delete the resource. As soon as we add Devise to our app it is tempting to implement article access authorization like this:

class ArticlesController < ApplicationController
  def index
    @articles = Article.where(owner: current_user)
  end

  def show
    @article = Article.find_by(id: params[:id], owner: current_user)
  end

  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)
    @article.owner = current_user

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = Article.find_by(id: params[:id], owner: current_user)
  end

  def update
    @article = Article.find_by(id: params[:id], owner: current_user)

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @article = Article.find(params[:id])
    @article.destroy

    redirect_to root_path, status: :see_other
  end

  private
    def article_params
      params.require(:article).permit(:title, :body)
    end
end

Works fine until one forgets to add owner: current_user in one of queries. Which is quite easy to miss. And as an example I made described error in the #destroy method. As a result one system user can delete articles owned by another user by “feeding” the method some random identifier.

It should be caught in code review. But can we reduce likelihood of an error in this case by changing the way we write resource queries?

My rule of thumb for this: avoid queries starting with model class and if possible load it through what is known/loaded in current context.

In our case we have Devise loaded current user instance. All we need to do is “walk” resource relationship graph until we reach the relevant resource:

class ArticlesController < ApplicationController
  def index
    @articles = current_user.articles
  end

  def show
    @article = current_user.articles.find(params[:id])
  end

  def new
    @article = current_user.articles.build
  end

  def create
    @article = current_user.articles.build(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @article = current_user.articles.find(params[:id])
  end

  def update
    @article = current_user.articles.find(params[:id])

    if @article.update(article_params)
      redirect_to @article
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @article = current_user.articles.find(params[:id])
    @article.destroy

    redirect_to root_path, status: :see_other
  end

  private
    def article_params
      params.require(:article).permit(:title, :body)
    end
end

This way looking up something without scoping to current user is pretty much impossible.

Note that it also applies to initializing/creating resource records as well. ActiveRecord relationships provide methods #build and #create which sets up relevant owner record attribute for you.