YOSHINO日記

プログラミングに関すること

cancancanの内部の動きを追う

はじめに

cancancan https://github.com/CanCanCommunity/cancancan

権限を一元管理できるgem。

とっても便利。

内部動作が気になったので追ってみます。

1: viewのcan?

まず、viewのcan?が読み込まれます。

- if can? :buy, @product
  %h2 Anyone can buy Product

2: Abilityのinitialize

$ rails g cancan:ability

を実行すると、以下のファイルが生成されますが、

app/models/ability.rb

そこで定義したAbility Classのinitializeが実行されます。

3: canの定義

app/models/ability.rb

の中で以下の記述があるとします。

can :buy, Product

4: Ruleオブジェクトを作成する

cancancanのAbilityクラスのadd_ruleメソッドが呼ばれます。

def can(action = nil, subject = nil, conditions = nil, &block)
  add_rule(Rule.new(true, action, subject, conditions, block))
end

引数はこんな感じです。

pry(#<Ability>)> action
=> :buy
[2] pry(#<Ability>)> subject
=> Product(id: integer, name: string, stockpile_id: integer, created_at: datetime, updated_at: datetime)
[3] pry(#<Ability>)> conditions
=> nil
[4] pry(#<Ability>)> block
=> nil

CanCan::Ability::Ruleのオブジェクトが作成され、

 @actions=[:buy],
 @base_behavior=true,
 @block=nil,
 @conditions={},
 @match_all=false,
 @subjects=
  [Product(id: integer, name: string, stockpile_id: integer, created_at: datetime, updated_at: datetime)]>

rulesという配列に格納されます。

def add_rule(rule) rules << rule add_rule_to_index(rule, rules.size - 1) end

rulesは、CanCan::Ability::Ruleのクラスインスタンス変数です。

Abilityに他にも定義されていれば、以下同様に、rulesにruleが追加されていく。 また、このRuleオブジェクトの作成とrulesへの追加はページの読み込みが行われるごとに実行される。

同様に、@rules_indexが同様に定義される。

def add_rule_to_index(rule, position)
  @rules_index ||= Hash.new { |h, k| h[k] = [] }

  subjects = rule.subjects.compact
  subjects << :all if subjects.empty?

  subjects.each do |subject|
    def add_rule_to_index(rule, position)
        binding.pry
        @rules_index ||= Hash.new { |h, k| h[k] = [] }

        subjects = rule.subjects.compact
        subjects << :all if subjects.empty?

        subjects.each do |subject|
          @rules_index[subject] << position
        end
      end[subject] << position
  end
end

例えばこんな感じに定義される。 subjectがkey、indexがvalueのハッシュが形成される。

pry(#<Ability>)> subject
=> Product(id: integer, name: string, stockpile_id: integer, created_at: datetime, updated_at: datetime)
[14] pry(#<Ability>)> position
=> 0

5: can?

viewのcan?が実行される。

- if can? :buy, @product
  %h2 Anyone can buy Product

cancancan/lib/cancan/ability.rb

def can?(action, subject, *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, extra_args)
    end
  end.reject(&:nil?).first
  match ? match.base_behavior : false
end

この時の引数はこんな感じ。

[2] pry(#<Ability>)> action
=> :buy
[3] pry(#<Ability>)> subject
=> #<Product:0x00007f2f54213588
 id: 1,
 name: "チョコレート",
 stockpile_id: 1,
 created_at: Sun, 10 Jun 2018 07:29:05 UTC +00:00,
 updated_at: Sun, 10 Jun 2018 07:29:05 UTC +00:00>
[4] pry(#<Ability>)> extra_args
=> []

can?を細かく追う

def can?(action, subject, *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, extra_args)
    end
  end.reject(&:nil?).first
  match ? match.base_behavior : false
end

まず、最初の行をみます。

extract_subjects(subject).lazy.map do |a_subject|

extract_subjects(subject)の返り値は、

[#<Product:0x00007f2f50067080
  id: 1,
  name: "チョコレート",
  stockpile_id: 1,
  created_at: Sun, 10 Jun 2018 07:29:05 UTC +00:00,
  updated_at: Sun, 10 Jun 2018 07:29:05 UTC +00:00>]

lazyについてはこちらの説明がわかりやすかったです。 無限リストを map 可能にする Enumerable#lazy

次の行。 action:buyと、Projectに関するruleを配列で取ってきています。

1つは条件付きの"manage"。 もう1つは、誰でもOKな"buy"。

relevant_rules_for_match(action, a_subject)
[#<CanCan::Rule:0x00007f2f48033a58
  @actions=[:manage],
  @base_behavior=true,
  @block=nil,
  @conditions=
   {:stockpile=>
     {:user=>
       #<User:0x00007f2f50084a90
        id: 2,
        name: "product_manager1",
        role: "product_manager",
        created_at: Sun, 10 Jun 2018 07:29:04 UTC +00:00,
        updated_at: Sun, 10 Jun 2018 07:29:04 UTC +00:00>}},
  @expanded_actions=[:manage],
  @match_all=false,
  @subjects=[Product(id: integer, name: string, stockpile_id: integer, created_at: datetime, updated_at: datetime)]>,
 #<CanCan::Rule:0x00007f2f480672b8
  @actions=[:buy],
  @base_behavior=true,
  @block=nil,
  @conditions={},
  @expanded_actions=[:buy],
  @match_all=false,
  @subjects=[Product(id: integer, name: string, stockpile_id: integer, created_at: datetime, updated_at: datetime)]>]

つぎの行で各ruleに関して、現在のユーザーが権限を与えられているかをチェックします。 返り値はtrue or falseです。

rule.matches_conditions?(action, a_subject, extra_args)

ここでは、[true, true]が返ります。

最終的に"match"したのは、

match
=> #<CanCan::Rule:0x00007f2f4e1d1c38
 @actions=[:manage],
 @base_behavior=true,
 @block=nil,
 @conditions=
  {:stockpile=>
    {:user=>
      #<User:0x00007f2f4cc7b028
       id: 2,
       name: "product_manager1",
       role: "product_manager",
       created_at: Sun, 10 Jun 2018 07:29:04 UTC +00:00,
       updated_at: Sun, 10 Jun 2018 07:29:04 UTC +00:00>}},
 @expanded_actions=[:manage],
 @match_all=false,
 @subjects=[Product(id: integer, name: string, stockpile_id: integer, created_at: datetime, updated_at: datetime)]>

でtrueが返されます。