【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