【Rails】Alias, 罠 of CanCanCan
こんにちは!ラブグラフ開発インターンの arawi です!
Rails の CanCanCan gem を利用していたときに陥った罠についてお話します!
TL;DR
- CanCanCan では、デフォルトでアクションのエイリアスが設定されている
- それを理解しておかないと予期しない挙動をすることがある
- また、CanCanCan のルールはあとに書いたものが優先される
発端
あるリソースに対して、show
は限られた id のものに絞って、 index
はできないという権限を作りたいときがありました。
ラブグラフでは権限管理に CanCanCan を利用しているので、CanCanCan の Ability
モジュールを include
した Ability
クラスに、以下のような記述を追加しました。
class Ability
include CanCan::Ability
def initialize(staff)
cannot :index, Resource
can :read, Resource, id: some_ids
end
end
これによって、
-
some_ids
に含まれる id の/resources/:id
は見れる -
some_ids
に含まれない id の/resources/:id
は見れない -
/resources/
は見れない
といった結果を得ることを期待しました。しかし、実際は
-
some_ids
に含まれる id の/resources/:id
は見れる⭕️ -
some_ids
に含まれない id の/resources/:id
は見れない⭕️ -
/resources/
が見れてしまう❌
という結果になりました。
cannot
で明示的に否定しているのに index
が見れてしまって、不可解に感じました。
Ability
のインスタンスを見てみる
調査のため、Rails コンソールで Ability
のインスタンスの中身をみてみました。
pry(dev)> staff = Staff.last
pry(dev)> ability = Ability.new(staff)
pry(dev)> ability.can?(:index, Resource)
=> true # ここでもちゃんと true になっていて、バグではないことがわかる
pry(dev)> ability
=> #<Ability:0x0000aaaae4c07990
@aliased_actions=
{:read=>[:index, :show],
:create=>[:new],
:update=>[:edit]},
@expanded_actions=
{[:index]=>[:index],
[:index, :show]=>[:index, :show],
[:read]=>[:read, :index, :show]},
@rules=
[#<CanCan::Rule:0x0000aaaae4bdc5d8
@actions=[:index],
@base_behavior=false,
@block=nil,
@conditions={},
@expanded_actions=[:index],
@match_all=false,
@subjects=
[Resource(id: integer, ... , created_at: datetime, updated_at: datetime)]>,
#<CanCan::Rule:0x0000aaaae4763ba0
@actions=[:read],
@base_behavior=true,
@block=nil,
@conditions={:order_id=>[1]},
@expanded_actions=[:read, :index, :show],
@match_all=false,
@subjects=
...
aliased_actions
とか expanded_actions
とかいう怪しい存在がいます!!こいつらか!!
我々は docs の奥地へと向かった・・・
CanCanCan のドキュメントを見ると、次のようなことが書いてありました。
CanCanCan offers four aliases: :read, :create, :update, :destroy for the actions. These aren't the same as the seven Restful actions in Rails. CanCanCan automatically adds some convenient aliases for mapping the controller actions.
read: [:index, :show]
create: [:new, :create]
update: [:edit, :update]
destroy: [:destroy]
要約すると、便宜のためデフォルトで read
は index
, show
のエイリアスになっているようです、これにより、can :read, Resource
と記述するだけで index
と show
ができるようになります。
リソース全体に対して can
するときにはそれでいいんですが、今回のケースのように対象のリソースを id で絞りたいようなときでも、もれなく index
がついてきてしまい、結局 index
できてしまいます。このようにして、冒頭の問題は引き起こされていたのでした。
ちなみに、上にもある通り create
は new
と create
のエイリアスになっています。これが原因で create
を許可するともれなく new
もついて来てしまうので、結構困ってる人もいるようです。
その issue: https://github.com/CanCanCommunity/cancancan/issues/832
cannot
も書いたやん
でも しかし今回は明示的に cannot :index, Resource
を書いたので、それが通用しそうな感じがします。なぜこの cannot
は効いてないのでしょうか?
結論から言えば、あとに書いたルールが優先されるためです。
こちらのメソッド relevant_rules
は与えられたアクションと対象に対して関連のあるルール[1]を抽出してくるメソッドです。
内部のコードを見ると、
-
relevant
を取ってくるところまでは定義順でルールが並べられます。 - その後、
reverse!
が呼ばれることで、順番が入れ替わります。 - このメソッドの外側で
relevant_rules
に対してdetect
メソッドが呼ばれ[2]、前方からルールが取られる
ので、結果的に定義が遅いほどルールが優先されるということになります。
どうするべ
さて、原因がわかったところで、問題を解決するためにはどうしたらよいでしょうか?
私は以下の2つの解決策を考えました。
-
cannot :index
をcan :read
のあとに書く。-
cannot
が優先され、期待通りの挙動になります。
-
-
read
を使わず、show
とindex
に分けてルールを書く。- これは以下のようになります。
class Ability
include CanCan::Ability
def initialize(staff)
cannot :index, Resource
- can :read, Resource, id: some_ids
+ can :show, Resource, id: some_ids
end
end
個人的には、2 の書き方が明示的で良いと思います。
おわりに
とりあえず can :read, Resource, id: ...
には気をつけろ
本当に理由がわからず結構沼ったので、原因がわかってよかったです。
また、 binding.pry を使ったり、 GitHub にソースコードを読みに行ったりして、ちゃんと理解できたのも成長につながったと感じています!
以上です!ここまで読んでいただきありがとうございました!
Discussion