読者です 読者をやめる 読者になる 読者になる

アトラシエの開発ブログ

株式会社アトラシエのブログです

default_scopeよりaround_actionとscopingを使おう

default_scope is evil は浸透してきたが...

qiita.com

最近では Railsでdefault_scope使うのはやめよう! という意見が強くなりましたね。 default_scope

default_scope -> { where(removed: false) }

こんなふうに論理削除を実装するときに便利です。 しかし管理画面では論理削除されたものも含めて集計したいなど、細かい欲求が出てくると意外とグローバルにスコープがセットされるのは都合が悪い。しかもdefault_scopeはmodelの初期値をセットしてしまう(これだとremoved = false)とか、解除するためにunscopedを使うとリレーションからのチェーンができない(@user.blogs.unscopedでuserのscopeごと消える)など残念な点も多いです。

しかしながら、エンドユーザー向けの画面では論理削除させたものを間違っても絶対に出したくない という強い意志があるとき、たしかにdefault_scopeを使いたくなる気持ちにも分はあるのです。「default_scopeを使わないなら、じゃあ毎回 Lesson.active.find(x) とか User.active.find(x)をつけなきゃいけないの? というのもごもっともだと思います。

around_actionとscopingを使おう

こういうときにaround_actionscopingが便利です。 scopingはRailsであまり活用されていないが便利なメソッドで、次のようにブロックの中限定でdefault_scope的な挙動を実現できます。

Comment.where(post_id: 1).scoping do
  Comment.first
end
# => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1

around_actionbefore_actionに比べると使い所がわかりづらいフィルタですが、scopingを組み合わせるとこのようなコードが書けます。

around_action :set_scope

def set_scope
  Blog.where(removed: false).scoping do
    yield
  end
end

非表示フラグが立ったPostモデルで考える

ブログがあり、一つ一つの記事がPostというモデルだとします。ここにhiddenというフラグがあり、trueの場合は非公開の記事だとします。 Post.visiblehidden=falseのPostを返すものとします。

このとき、よくやってしまうのが

def index
  @posts = Post.visible.order(created_at: :desc).limit(10) #=> ちゃんとvisibleをつけている
end

def show
  @post = Post.find(params[:id]) #=> visibleをつけていない!
end

のように、うっかりフラグの確認を抜かしてしまうミスです。これはやりがちなうえ、秘匿性の高い情報をあっさり公開してしまう大事故につながるので絶対に避けたいところです。default_scopeを使ってhidden=falseとしたくなるのも気持ちは分かります。

こういうとき、上の方法を使いましょう。 controllerを

├─application_controller.rb
├─front_controller.rb (ブログ閲覧者用の画面)
├─front
│  └─posts_controller.rb
├─admin_controller.rb (ブログ管理者用の画面)
├─admin
│  └─posts_controller.rb

のような構造にします。こうするとFront::PostsControllerAdmin::PostsControllerができます。 これだとFrontのほうでは常にhidden=falseが保証されればいいので、以下のようにすればいいのです。

app/views/front_controller.rb

class FrontController < ApplicationController
  around_action :set_scope

  def set_scope
    Post.visible.scoping do
      yield
    end
end

これでFront側でhidden=trueな記事が誤って露出する心配はありません。

スコープや閲覧権限の確認は入念に。

セキュリティの話でXSSやSQLインジェクションはコードだけで判別できるのですが、アプリケーションの仕様通りの閲覧権限・スコープになっているかはビジネスロジックなので、入念に確認しないとバグや脆弱性がわかりづらいです。上の方法はある程度有効なのですが、最終的には個々のactionごとに手を抜かず確認しましょう。