【Rails/CanCanCan】can?のクラスとインスタンスに対する権限判定の違い
目次
- はじめに
Ability
クラスでの権限設定Ability
インスタンスの生成-
権限判定
a. クラスに対する権限判定
b. インスタンスに対する権限判定 - ソースコードから読み取れる権限判定の違い
- まとめ
はじめに
CanCanCan は Ruby on Railsの認可ライブラリで、特定のユーザーがアクセスできるリソースを制限できる機能があります。
CanCanCanでは、クラスやインスタンスをリソースとして権限判定が行えるのですが、これらの違いがよくわからなかったので整理しました。
具体的にはcan?(:update, Article)
とcan?(:update, article)
で行われる権限判定の違いについて簡単に説明し、ソースコードの該当部分を紹介します。
Ability
クラスでの権限設定
ユーザーに持たせる権限をAbility.rbに定義します。
今回は、誰のArticle
でも閲覧できるが本人のArticle
しか更新できないような権限設定を行います。
# Ability.rb
class Ability
include CanCan::Ability
def initialize(user)
can :read, Article
can :update, Article, { user_id: user.id }
end
end
Ability
インスタンスの生成
Ability
クラスは通常、リクエストが始まったタイミングでインスタンス化します。
@current_ability ||= Ability.new(current_user)
認可チェックで authorize! や can? が呼ばれる際、すでに生成されている Ability
インスタンスが利用され、ルールが評価されます。
権限判定
ユーザーに付与された権限はviewやcontrollerでcan?
やcannot?
を使用して確認できます。
クラスに対する権限判定
can?(:update, Article) # => true
アクションとクラスが一致するルールが存在することを確認します。
ここでは、current_user
がArticle
モデルに対してupdateを行える権限を持っていることが確認できました。
注意点
CanCanCanのメソッドでは、上記のように属性やハッシュ、ブロックでリソースに対して条件を追加できます。今回は、Ability.rbで本人のArticle
のみupdateできるような設定を行いました。
# Abilityクラスで設定したuser_idと
# can?で確認するuser_idが異なる場合
can?(:update, Article, { user_id: another_user.id }) # => true
しかし、can?
の第二引数にクラスを渡した場合、レコードについての条件は確認されず、アクションとクラスの一致のみで判定が行われるため、他ユーザーのArticle
の権限も持っているような判定になってしまいます。
そのため、特定のユーザーに関連するモデルなどの権限の有無を確認したい場合は、インスタンスに対する権限判定を行います。
インスタンスに対する権限判定
can?(:update, article) # => true
can?(:update, another_user_article) # => false
アクションとクラスが一致するルールが存在することを確認し、その上でarticle
やanother_user_article
が条件を満たすかどうかで判定を行います。(追加条件の設定がない場合、Ability
の設定がcan
ならtrue
, cannot
ならfalse
が返されます)
ここでは、current_user
が持つそれぞれのインスタンスに対するupdate権限の有無が確認できました。
ソースコードから読み取れる権限判定の違い
can?
によるクラスとインスタンスに対する権限判定の分岐について、ソースコードの該当部分を紹介します。
def can?(action, subject, attribute = nil, *extra_args)
match = extract_subjects(subject).lazy.map do |a_subject|
relevant_rules_for_match(action, a_subject).detect do |rule|
rule.matches_conditions?(action, a_subject, attribute, *extra_args) && rule.matches_attributes?(attribute)
end
end.reject(&:nil?).first
match ? match.base_behavior : false
end
subject
にはクラスやインスタンスが入り、attribute
やextra_args
に追加の条件が入ります。
relevant_rules_for_match
でaction
やsubject
が一致するようなルールを取り出し、最初にtrue
を返すルールのbase_behavior
を返します。(can
ならtrue
, cannot
ならfalse
)
今回のAbilityの設定と判定(can?(:update, Article)
とcan?(:update, article)
)の場合、クラスとインスタンスの分岐があるのは以下のメソッドになっています。
def matches_non_block_conditions(subject)
return nested_subject_matches_conditions?(subject) if subject.class == Hash
return matches_conditions_hash?(subject) unless subject_class?(subject)
# Don't stop at "cannot" definitions when there are conditions.
@base_behavior
end
-
クラスのマッチングでは
@base_behavior
が返される-
@base_behavior
はRule
クラスのインスタンス変数で、設定がcan
の時true
,cannot
の時false
が設定される
-
-
インスタンスのマッチングでは
matches_conditions_hash?(subject)
が呼ばれる-
matches_conditions_hash?(subject)
では、条件のkey
に対応する、インスタンスのvalue
(article.user_id
)と設定のvalue
(user.id
)が等しいか確認
-
まとめ
- クラスに対する権限判定:アクションとクラスが一致するルールを検索して判定
- インスタンスに対する権限判定:アクションとクラスが一致するルールを検索し、設定した条件とインスタンスを比較して判定
Discussion