ネットサーフィンをしていると「CanCan便利!」「CanCanはRailsでの権限管理デファクトスタンダードだよ」などの声をぼちぼち見かけますが、あまりまとまったCanCanの使い方に関する記事を見つけられなかったので覚書をしたためます。
ほとんどCanCanの配布元Wikiに書いてあることですので、英語が読める方はそちらを参照するほうが正確だと思われます。
内容へのツッコミや訂正大歓迎です!
CanCanとは
(by CanCan repo README - github)
CanCan is an authorization library for Ruby on Rails which restricts what resources a given user is allowed to access. All permissions are defined in a single location (the Ability class) and not duplicated across controllers, views, and database queries.
(意訳)
Railsを使ってると、このユーザーはindexにアクセスできるけど、createが出来ないとか、そういうことしたくなるよな。
よくあるけど、そういう認証処理がコントローラーやビュー、はたまたSQLクエリなんかのいろいろな場所にばら撒かれるのはCoolじゃねえ。いっちょAbilityクラスとか作ってそこでどうにかできたら素敵!
なーんていう夢を提供しているのさ!
CanCanCanとは
(by CanCanCan repo README - github)
This repo is a continuation of the dead CanCan project. Our mission is to keep CanCan alive and >moving forward, with maintenance fixes and new features. Pull Requests are welcome!
I am currently focusing on the 1.x branch for the immediate future, making sure it is up to date as well as ensuring compatibility with Rails 4+. I will take a look into the 2.x branch and try to see what improvements, reorganizations and redesigns Ryan was attempting and go forward from there.Any help is greatly appreciated, feel free to submit pull-requests or open issues.
(意訳)
...という夢を見たんだ。
どうもCanCanはメンテナンスが止まってしまった。しかしまだまだオワコンにはさせたくない。夢を見続けたいんだ。Rails4でもCanCan使ったりしたいとかね!
ってわけで勝手にCanCanのメンテナンスを続けているけど、もちろんみんなからのプルリクとか意見・要望とかウェルカムさ!
ざっくりとした解説
例えば以下のようなアプリケーションがあったとします。
- 「店頭ページ」と「倉庫ページ」があるショッピングサイトで、訪問者は店頭ページに表示された製品を購入できる。
- システム管理者はすべてのページを閲覧できる。すべての操作が可能。
- 倉庫管理者は、自分の担当する倉庫に保管された製品を閲覧できる。担当倉庫の製品を店頭に出す、あるいは店頭の製品を倉庫にしまうことができる。
- 店長はどの製品がどの倉庫にしまわれているか、一覧を見ることができる。しまわれている製品を削除することができる。任意の倉庫に新しく製品を追加できる。
店頭ページを作成すると
def index
if current_user.customer? || current_user.sys_admin? || current_user.product_manager?
# 製品一覧の表示
else
render 403 # アクセス権限エラー
end
end
と、このようになります。
同様に、倉庫ページ、製品の取り出し、製品の購入などのページでも「もしユーザーがーXXかYYなら使える。そうでないならアクセス権限ページに移動する」という処理を作ることになります。
数カ所ならば大した問題ではありませんが、アプリケーション全体でこのような処理をつど入れると、大変複雑なシステムになってしまいます。
そうではなく何処か一箇所で、
- 管理者ならなんでもできる
- 倉庫管理者なら
- Aができる
- Bができる
- 訪問者なら
- Aができる
- Bはできない
などと設定し、
can? :do, A # ログインユーザーはAできるか
cannot? :do, B # ログインユーザーはBできないか
can? :index, Product # ログインユーザーは製品の一覧を見れるか
@product = Product.find(1)
can? :show, @product # ログインユーザーはid=1の製品に関して詳細を見れるか
と、判定できれば、新しく権限が追加されたり、「倉庫管理者も製品の削除がしたいよ」となったとき、その何処か一箇所だけをいじればよく、システムを簡潔に保てます。
CanCanは、このような権限管理を目的としたRuby on Rails用のGemです。
ユーザー権限ごとのアクセス制限に関する処理や設定を、極力一箇所(Abilityクラスを作成するのが通例)に集中させることが出来ます。
CanCanCanは、開発の止まったCanCanの後続プロジェクトで、Rails4にも対応しているようです。
この記事では主に、Rails4とCanCanCanを用いてることを前提に説明しますが、だいたい同じようなことがRails3でも可能です。
この記事について
本記事の前提
ユーザーに権限をもたせる場合、システムによって
- 1ユーザーは1つの権限しか持たない
- 1ユーザーは複数の権限を持ちうる
のパターンが存在するでしょう。
それぞれの方法に長所短所が存在しますが、本記事のテーマから外れるので解説はしません。
本記事では、後者の「1ユーザーは複数の権限を持ちうる」場合を前提にします。
権限設定(Abilityクラスの作成)
CanCanは、Ruby on Rails用のGemです。
ユーザー権限ごとのアクセス制限に関する処理や設定を、極力一箇所(Abilityクラスを作成するのが通例)に集中させることを目的にしています。
コンソールでRailsプロジェクト以下に移動し
$ rails g cancan:ability
を実行することで
class Ability
include CanCan::Ability
def initialize(user)
end
end
を作成してくれます。
権限があるかはcan?
メソッドで判定することが出来ますが、このとき暗黙的に
def current_ability
@current_ability ||= Ability.new(current_user)
end
が呼び出されます。
もし、権限を判定するユーザーにcurrent_user
を使いたくない場合は、ApplicationControllerでcurrent_ability
をオーバーライドすることで、いろいろ弄くり回せます。(例えば、オペレーション権限を持つユーザーが、ログイン後に他のユーザーに成りすましてシステムを操作する場合があるなど)
基本的な使い方
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # ログインしていない場合は、からユーザーを用意し判定に用いる
# default parmission
cannot :buy, Product
if user.sys_admin?
can :manage, :all
end
if user.product_manager?
can :manage, Stockpile, owner: user # 自分がオーナーの倉庫には全権限を持つ
can :read, Stockpile # そうでなくても、読み取り権限を持つ
# 自分の倉庫にある製品に対してすべての権限を持つ
can :manage, Product, stockpile: {owner: user}
# ただし、新規登録、削除はできない
cannot [:create, :destroy], Product
end
if user.customer?
# 複数のモデルに権限を付与できる
can :read, [Stockpile, Product]
# 独自権限も作れる
can :buy, Procuct, stockpile: nil # 倉庫から出されている製品を買える
end
end
end
基本の権限には
- read ... 参照
- create ... 登録
- update ... 更新
- destroy ... 削除
- manage ... すべて
があります。注意したいのは「manage / すべて」というのは「read+crete+update+destroy」というわけではないことです。
上の例では「buy / 購入」という権限を独自に作っていますが、manage権限ではbuyも可能になります。
そこでデフォルトの権限付与で、製品の購入を禁じ、もしuser.customer? = true
ならば購入可能になるようにしています。
あるモデルのある権限にcan
とcannot
が重なって付与された場合、最後に適応されたほうが有効になるようです。
# current_userがproduct_managerの場合
can? :manage, Stockpile #=> true
can? :read, Stockpile #=> true
can? :update, Stockpile #=> true
# 1. あるモデルに権限Xを持っているか判定する
@stockpile = Stockpile.find_by(owner: user)
can? :manage, @stockpile #=> true
# 2. あるレコードに権限Xを持っているか判定する
@stockpile_2 = Stockpile.where("owner_id != ?", user.id).first
can? :manage, @stockpile_2 #=> false
can? :read, @stockpile_2 #=> true
can? :update, @stockpile_2 #=> false
# 3. あるモデルから権限を持っているレコードを取得する
Stockpile.accessible_by(current_ability).to_sql
#=> SELECT "stockpiles".*
# FROM "stockpiles"
# WHERE "stockpiles"."owner_id" = 1
ということが出来ます。
3の方法では、index権限での検索がデフォルトのようですが、
Stockpile.accessible_by(current_ability, :destroy)
Stockpile.accessible_by(current_ability, :my_permission)
などとすることで、他の権限でのレコード取得も可能です。
余談ですが、もし権限が複数あり、Abilityクラスのdef initalize(user)
が大きくなってしまうのが嫌ならば、以下のようにするといいでしょう。
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
# user.roles => ['sys_admin', 'product_manager'...]
# などと動作する User#roles があるとして
user.roles.each do |role_name|
send("#{role_name}_ability", user)
end
end
private
def sys_admin_ability(_)
can :manage, :all
end
def product_manager_ability(user)
can :manage, Stockpile, owner: user
can :read, Stockpile
... # 省略
end
def customer_ability(_)
...
end
end
注意
Hashによる設定は、レコードの検索時と判定時に利用されます。
しかしこの仕様では、例えばEnumなどでレコードの実値にたいして別名をつけているような場合に、ハマったりすることがあるかもしれません。
# gem "enumerize" で Adminのrole_typeに別名をつけている
admin = Admin.where(role_type: :sysad).first
#=> SELECT "admins".* FROM "admins" WHERE "admins"."role_type" = 1 LIMIT 1
admin.role_type #=> "sysad"
# can :read, Admin, role_type: :sysad と設定すると..
Admin.accessible_by(current_ability) #=> 「WHERE "admins"."role_type" = 'sysad'」が発行されてしまいエラー
# can :read, Admin, role_type: 1 と設定すると..
admin = Admin.where(role_type: :sysad).first
can? :read, admin #=> 「"sysad" == 1」と判定され false になる
これを回避するには、検索条件と判定条件を別々に設定する必要があります(後述)。
Modelのないコントローラーへの権限設定
もし店舗を表すShop
モデルのようなものがなく、「店舗ページへのアクセス権限」というものを作成したい場合はどうすればいいでしょうか。
1つの方法はからのShop
モデルを作成し、それに対してcan :manage, Shop
などと権限を付与することです。
しかし、実際に使わないからのモデルを作成するのは、後からプロジェクトに参加した人間に混乱を招くかもしれません。
通常は、以下のように設定すれば十分なはずです。
# models/ability.rb
can :manage, :shop
# controller/shops_controller.rb
can? :read, :shop #=> true or false
権限のAND条件
# 自身が登録した食べ物に参照権限がある
can :show, Product, type: 'food', created_by: user.id
# 一年前〜半年前に登録された食べ物の削除権限がある
can :destroy, Product, type: 'food', created_at: (1.years.ago..6.months.ago)
# どう検索されるか確認
Stockpile.accessible_by(current_ability, :show)
# WHERE "stockpiles"."type" = 'food' AND "stockpiles"."created_by" = 1
Stockpile.accessible_by(current_ability, :destroy)
# WHERE "stockpiles"."type" = 'food' AND "stockpiles"."created_at" BETWEEN '2014-12-1' AND '2014-6-1'
権限のOR条件
# 食べ物、または飲み物の閲覧権限がある
can :read, Product, type: 'food'
can :read, Product, type: 'drink'
# どう検索されるか確認
Stockpile.accessible_by(current_ability)
# WHERE "stockpiles"."type" = 'food' OR "stockpiles"."type" = 'drink'
権限判定でhas_oneな関係のレコードが必要
has_one
belongs_to
で関連を設定したレコードの場合、以下のようにして権限判定に利用することが出来ます。
# 製品が格納されている倉庫の管理者なら、製品を更新できる
can :update, Product, stockpile: {owner_id: user.id}
ModelのスコープやStringによる権限設定
権限の設定には上記であげたHashを使った指定以外にも、違うやり方があります。
仮に、「Productには、秘匿(secret)でないものだけ閲覧権限がある」という設定をしたい場合、以下のようないくつかの方法があります。
can :read, Product, secret: false
can :read, Product, "secret = false"
can :read, Product, ["secret = ?" , false]
can :read, Product, Product.where(secret: false)
# Publicモデルで`scope :not_secret, ->{ where(secret: false) }`を設定している
can :read, Product, Product.not_secret
注意
上記のようにModelのスコープやStringによって権限を設定した場合、その設定は検索条件には使われますが、判定条件には使えません。
can :read, Product, ["expired_at <= ?", Date.today]
can? :read, Product #=> true
Product.accessible_by(current_ability)
# SELECT "products".* FROM "products" WHERE "products"."expired_at" <= '2014-12-24'
product = Product.accessible_by(current_ability).first
can? :read, product
#CanCan::Error: The can? and cannot? call cannot be used with a raw sql 'can' definition. The checking code cannot be determined for :read
レコードの権限判定でエラーが起きました。
["expired_at <= ?", Date.today]
は検索条件にはなりますが、レコードを判定する場合には使えません。
判定にも使いたい場合はブロックを用いて、以下のように書く必要があります。
can :read, Product, ["expired_at <= ?", Date.today] do |record|
# レコードの権限判定
record.expired_at <= Date.today
end
ModelのスコープやStringによって権限を設定した場合、Hashを使った権限設定とのORができないという問題もあります。
can :read, Product, publish: true
can :read, Product, Product.where(secret: false)
Product.accessible_by(current_ability)
#=> CanCan::Error: Unable to merge an Active Record scope with other conditions. Instead use a hash or SQL for read Product ability.
権限認証(Controllerでの設定)
そろそろ書くのに疲れてきましたので、さくさく行きます。
authorize!
can?
がtrue / false
を返すのに対し、authorize!
は失敗時にraiseします
#hoges_controller.rb
def index
authorize! :index, Hoge
end
load_resource
controllerの各アクションに対して暗黙的に、うまいことレコードをロードしてくれます。
class HogesController < ApplicationController
load_resource
def index
# 暗黙的に @hoges 変数に Hoge.accessible_by(current_ability) を設定
end
def show
# 暗黙的に @hoge 変数に@product Hoge.find(params[:id]) を設定
end
index アクションならアクセスできるクラス
show, edit, update, destroy アクションはidで検索
new アクションでは上手いことアクセスできる権限のレコードをデフォルトで作成してくれます
詳細はドキュメント読むとなんとなくわかります。
authorize_resource
controllerの各アクションにたいする、権限を暗黙的に判定してくれます。
判定は、レコードではなくモデルへの権限なことに注意してください。
class HogesController < ApplicationController
authorize_resource
def index
#暗黙的に authorize! :index, Hoge
end
def show
#暗黙的に authorize! :show, Hoge
end
def oreore_method
#暗黙的に authorize! :oreore_method, Hoge
end
load_and_authorize_resource
load_resource
とauthorize_resource
の合わせ技をしてくれます。
class ProductsController < ActionController::Base
load_and_authorize_resource
def discontinue
# 暗黙的にレコードを検索し、その権限を判定する
# @product = Product.find(params[:id])
# authorize! :discontinue, @product
end
end
特定のアクションにだけこれらの設定をしたい / 特定のアクションでは判定をしなくない場合、以下のように設定できます。
load_and_authorize_resource :only => [:index, :show]
load_and_authorize_resource :except => [:index, :show]
終わりに
その他、テストの書き方、デバッグの仕方、コントローラーの作り方サンプル、StrongParameerとの食いあわせ、などなど
有益な情報がリポジトリのWikiに書いてあります。
一度目を通してみるのをおすすめします。そして誰か、もっと詳細な和訳解説を書いてください
おまけ
本記事では実際に使う時のイメージを優先し、「モデルに権限を付与する」「レコードの権限を判定する」などという言い回しを使っています。
しかし、正確には「クラス / インスタンスに権限を付与する」というのが正しいです。
例えば
can? :read, 'aiueo' #=> false
can? :read, Fixnum #=> false
can :manage, :all
can? :read, 'aiueo' #=> true
cannot :read, String
can? :read, 'aiueo' #=> false
can? :read, 123 #=> true
「へえ、何の意味があるの?」と問われれば「いや、何も…」となるのですが。
[修正 2015/11/27] 誤った利用法「can :read, :string」について削除