【Rails】cancancanのソースコードを読んでみた（authorize!メソッド）

ふと思いつきで、普段使っている[cancancan](https://github.com/CanCanCommunity/cancancan/)のgemのソースコードを読んでみようと思いました。
今回はcontrollerに`before_action`で`authorize!`メソッドが書かれたときの流れを追ってみました。

## バージョン

cancancan 3.5.0

## 処理の流れ

まず、以下の`authorize!`メソッドで`current_ability`の設定（Abilityファイルの読み込み）を行います。

```ruby:gems/cancancan-3.5.0/lib/cancan/controller_additions.rb
def authorize!(*args)
  @_authorized = true
  current_ability.authorize!(*args)
end
```

`current_ability`メソッドで`Ability.new`を行うようになっています。

```ruby:gems/cancancan-3.5.0/lib/cancan/controller_additions.rb
def current_ability
  @current_ability ||= ::Ability.new(current_user)
end
```

`can :xxx, XXX`の記述があった場合に、それらを`add_rule`として収集していきます。

```ruby:gems/cancancan-3.5.0/lib/cancan/ability.rb
def can(action = nil, subject = nil, *attributes_and_conditions, &block)
  add_rule(Rule.new(true, action, subject, *attributes_and_conditions, &block))
end
```

```ruby:gems/cancancan-3.5.0/lib/cancan/ability/rules.rb
def add_rule(rule)
  rules << rule
  add_rule_to_index(rule, rules.size - 1)
end
```

その後、`ability`インスタンスのメソッドの`authorize!`メソッドが呼ばれます。
`action`には`:index`など操作したい内容が、`subject`には権限チェックの対象が入っています。

その`subject`に対して権限がない（`cannnot?`が`true`）場合にはエラーをraiseするようになっています。

```ruby:gems/cancancan-3.5.0/lib/cancan/ability.rb
def authorize!(action, subject, *args)
  message = args.last.is_a?(Hash) && args.last.key?(:message) ? args.pop[:message] : nil
  if cannot?(action, subject, *args)
    message ||= unauthorized_message(action, subject)
    raise AccessDenied.new(message, action, subject, args)
  end
  subject
end
```

`cannnot?`メソッドは`can?`メソッドの結果を反転させているだけでした。

```ruby:gems/cancancan-3.5.0/lib/cancan/ability.rb
def cannot?(*args)
  !can?(*args)
end
```

`extract_subjects(subject)`では権限チェックの対象を配列に整えています。
`relevant_rules_for_match(action, a_subject)`で、事前にユーザーが`ability.rb`で記載している権限とマッチするものを探して配列にしています。

```ruby:gems/cancancan-3.5.0/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
```

権限チェック対象の渡し方に応じて配列の整え方を変えています。
参考： [【Rails/cancancan】authorize!で、複数のリソースのいずれかに対する権限があれば操作を許可したいときの方法](https://qiita.com/m-ito27/items/92f2813d3cb2bc396e24)

```ruby:gems/cancancan-3.5.0/lib/cancan/ability.rb
# It translates to an array the subject or the hash with multiple subjects given to can?.
def extract_subjects(subject)
  if subject.is_a?(Hash) && subject.key?(:any)
    subject[:any]
  else
    [subject]
  end
end
```

`relevant_rules_for_match(action, a_subject)`で、事前にユーザーが`ability.rb`で記載している権限とマッチするものを探して配列にしています。

```ruby:gems/cancancan-3.5.0/lib/cancan/ability/rules.rb

def relevant_rules_for_match(action, subject)
  relevant_rules(action, subject).each do |rule|
    next unless rule.only_raw_sql?

    raise Error,
          "The can? and cannot? call cannot be used with a raw sql 'can' definition. " \
          "The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
  end
end
```

`relevant_rules(action, subject)`にて、`@rules`には、最初に`add_rules`メソッドが呼ばれたときの、ユーザーが記載した権限が格納されています。

```ruby:gems/cancancan-3.5.0/lib/cancan/ability/rules.rb
def relevant_rules(action, subject)
  return [] unless @rules

  relevant = possible_relevant_rules(subject).select do |rule|
    rule.expanded_actions = expand_actions(rule.actions)
    rule.relevant? action, subject
  end
  relevant.reverse!.uniq!
  optimize_order! relevant
  relevant
end
```

`possible_relevant_rules`メソッドでは、継承等含めて関連のリソースを拾ってきていました。
参考： [【Rails】cancancanでは継承元のclassを指定してまとめて権限設定することが可能](https://qiita.com/m-ito27/items/21c523f65d845bf891f3)

```ruby:
def possible_relevant_rules(subject)
  if subject.is_a?(Hash)
    rules
  else
    positions = @rules_index.values_at(subject, *alternative_subjects(subject))
    positions.compact!
    positions.flatten!
    positions.sort!
    positions.map { |i| @rules[i] }
  end
end
```

以上です。
このソースコードを読んだことをきっかけでcancancanの知らない機能を知れたりできてよかったです。