😊

【Rails/CanCanCan】can?のクラスとインスタンスに対する権限判定の違い

2024/12/20に公開

目次

  1. はじめに
  2. Ability クラスでの権限設定
  3. Abilityインスタンスの生成
  4. 権限判定
    a. クラスに対する権限判定
    b. インスタンスに対する権限判定
  5. ソースコードから読み取れる権限判定の違い
  6. まとめ

はじめに

CanCanCan は Ruby on Railsの認可ライブラリで、特定のユーザーがアクセスできるリソースを制限できる機能があります。
https://github.com/CanCanCommunity/cancancan

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_userArticleモデルに対して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

アクションとクラスが一致するルールが存在することを確認し、その上でarticleanother_user_articleが条件を満たすかどうかで判定を行います。(追加条件の設定がない場合、Abilityの設定がcanならtrue, cannotならfalseが返されます)

ここでは、current_userが持つそれぞれのインスタンスに対するupdate権限の有無が確認できました。

ソースコードから読み取れる権限判定の違い

can?によるクラスとインスタンスに対する権限判定の分岐について、ソースコードの該当部分を紹介します。
https://github.com/CanCanCommunity/cancancan/blob/develop/lib/cancan/ability.rb

    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にはクラスやインスタンスが入り、attributeextra_argsに追加の条件が入ります。
relevant_rules_for_matchactionsubjectが一致するようなルールを取り出し、最初に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_behaviorRuleクラスのインスタンス変数で、設定がcanの時true, cannotの時falseが設定される
  • インスタンスのマッチングではmatches_conditions_hash?(subject)が呼ばれる

    • matches_conditions_hash?(subject)では、条件のkeyに対応する、インスタンスのvalue(article.user_id)と設定のvalue(user.id)が等しいか確認

まとめ

  • クラスに対する権限判定:アクションとクラスが一致するルールを検索して判定
  • インスタンスに対する権限判定:アクションとクラスが一致するルールを検索し、設定した条件とインスタンスを比較して判定

Discussion