【Rails】StrongParameterの使用は必須ですか？

# 概要
StrongParameterはrails4から導入されたマスアサインメントを防ぐために導入された仕組みですが、使用が前提になっていると思っている方がいるようですが実際には必須ではありません。例えば、以下のようにStrongParameterからHashにして入力しても使うことができます。なぜこのように動作するかについてrailsのソースコードを見ながら説明します。

```ruby
class Dog < ActiveRecord
end

params = ActionController::Parameters.new(name: 'tama', status:"sleepy")

// new with strong parameter
Dog.new(params.permit(:name, :status))

// new without strong parameter (hash)
Dog.new(name: params[:name], status: params[:status])

// new without strong parameter (hash)
Dog.new( {:name =>  params[:name], :status => params[:status]})
```

ソースは、タグv6.1.4.1を参照します。

# ActiveRecordを初期化するとどこへ行くの?

ActiveModelのクラスをnewするとactive_record/core.rbで以下の初期化処理が走ります。 普段渡しているStrongParameterはここではattributesという変数で扱われ、assign_attibutesを呼び出すようになっています。

```ruby
def initialize(attributes = nil)
  @new_record = true
  @attributes = self.class._default_attributes.deep_dup

  init_internals
  initialize_internals_callback

  assign_attributes(attributes) if attributes

  yield self if block_given?
  _run_initialize_callbacks
end

```
https://github.com/rails/rails/blob/v6.1.4.1/activerecord/lib/active_record/core.rb#L503

# assign_attibutesって何してるの？

assign_attributesは、active_model/attribute_assignment.rbに定義されています。その名前の通り、ActiveModelの属性を設定するための処理が書かれています。

respond_to?はObjectクラスのメソッドで、指定した名前のメソッドがあるかどうかを返却します。また引数で指定しているstringify_keysは、active_suportのhashで定義されており、`{ name: 'Rob', age: '28' }.stringify_keys`とすると、`{"name"=>"Rob", "age"=>"28"}`のようにkeyを文字列にしたHashオブジェクトを返却します。 ここでは、new_attributesがstringify_keysメソッドが無ければArgumentErrorとし、有れば呼び出して、sanitize_for_mass_assignmentに結果を渡すという処理をやっています。

```ruby
def assign_attributes(new_attributes)
  if !new_attributes.respond_to?(:stringify_keys)
    raise ArgumentError, "When assigning attributes, you must pass a hash as an argument, #{new_attributes.class} passed."
  end
  return if new_attributes.empty?
  
  attributes = new_attributes.stringify_keys
  _assign_attributes(sanitize_for_mass_assignment(attributes))
end
```

assign_attributesの定義
https://github.com/rails/rails/blob/v6.1.4.1/activemodel/lib/active_model/attribute_assignment.rb#L28

stringify_keysの定義
https://github.com/rails/rails/blob/v6.1.4.1/activesupport/lib/active_support/core_ext/hash/keys.rb#L10



# sanitize_for_mass_assignmentって何をやっているの？

attributesにpermitted?メソッドが存在するかどうかをrespond_to?を使って確認し、呼び出して許可されていなければForbiddenAttributesErrorとしています。そしてこれがマスアサインメントを防ぐための仕組みになります。 では、permitted?メソッドが存在しない場合はどうなるかというと、elseに書かれているようにattributesをそのまま返すようになっています。 要するに、前述したstringify_keysメソッドが定義されていることが満たされていれば、サニタイズは実行されませんので、**表題の通りstrong parameterは必須ではない**ということになります。

```ruby
module ForbiddenAttributesProtection # :nodoc:
  private
  def sanitize_for_mass_assignment(attributes)
    if attributes.respond_to?(:permitted?)
      raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
      attributes.to_h
    else
      attributes
    end
  end
  alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment
end
```
sanitize_for_mass_assignmentの定義
https://github.com/rails/rails/blob/v6.1.4.1/activemodel/lib/active_model/forbidden_attributes_protection.rb

# この処理の流れだとpermitted?メソッドって呼び出せないんじゃないの？

assign_attributesメソッドでは、`stringify_keys`を呼び出してHashに変換してから`sanitize_for_mass_assignment`を呼び出しています。Hashオブジェクトにはpermitted?メソッドが定義されていないから、結果的にサニタイズの処理が走らないんじゃないのか？ と思えるかもしれません。

```ruby
attributes = new_attributes.stringify_keys # ここで、ActionController::ParametersがHashになるのでは？
_assign_attributes(sanitize_for_mass_assignment(attributes))
```

Hashにはpermitted?は定義されてないので、何も対応しないと想像の通りの処理になってしまいます。そこで、StrongParameter(実体はActionController::Parameters)では、以下のようにstringify_keysを定義して自身のオブジェクトのシャローコピーを返すようにしています。これによって先ほどの行では、StrongParameter(ActionController::Parameters)が入力された時は、Hashには変換されずにActionController::Parametersが返却されるという仕組みとなっています。

```ruby
    # This is required by ActiveModel attribute assignment, so that user can
    # pass +Parameters+ to a mass assignment methods in a model. It should not
    # matter as we are using +HashWithIndifferentAccess+ internally.
    def stringify_keys # :nodoc:
      dup
    end
```

strong_parameters.rb
https://github.com/rails/rails/blob/v6.1.4.1/actionpack/lib/action_controller/metal/strong_parameters.rb#L853



# ブラックリスト方式のMyStrongParameterを作ってみる

permitted?とstringify_keysを定義すれば、StrongParameterとして動作させることができるとわかりましたので、独自にStrongParameterを作ってみました。既存のものはホワイトリストで、許可したいkeyのみを指定する方式なので、逆にブラックリスト方式にしてみます。


```ruby
    class MyStrongParameter < Hash

      BLACK_LIST = [
          :id,
          :created_at,
          :updated_at
      ]

      def initialize(constructor = {})
        if constructor.respond_to?(:to_hash)
          super()
          update(constructor)

          hash = constructor.to_hash
          self.default = hash.default if hash.default
          self.default_proc = hash.default_proc if hash.default_proc
        else
          super(constructor)
        end
      end

      def stringify_keys
        dup
      end

      def permitted?
        !keys.any? {|key| BLACK_LIST.include? key }
      end
    end

```

実際のStrongParameterはHashWithInDifferentAccessを内包するクラスですが、ここでは簡単にするためサブクラスとして実装します。stringify_keysは既存のものと同様に自身のシャローコピーを返却するようにして、permitted?ではブラックリストに1つでも含まれていれば許可しないようにロジックを書きます。 これを実行すると以下のようになります。


```ruby
irb(main):213:0> MyStrongParameter.new(name: "kuro", status: "sleepy").permitted?
=> true
irb(main):214:0> MyStrongParameter.new("name": "kuro", "status": "sleepy").permitted?
=> true
irb(main):215:0> MyStrongParameter.new(id: "kuro", status: "sleepy").permitted?
=> false
irb(main):216:0> MyStrongParameter.new(id: 1, name: "kuro", status: "sleepy").permitted?
=> false
irb(main):217:0> MyStrongParameter.new(name: "kuro", status: "sleepy", created_at: Time.now).permitted?
=> false
```

このクラスで実装したオブジェクトを使って、実際にマスアサインメントでサニタイズされるかどうかを確認します。

```ruby
# ActiveModel(Dog)を定義
class Dog
      include ActiveModel::Model
      attr_accessor :name, :status
end

# ブラックリストに登録されているkeyは使ってないので、permitted?はtrueとなり正常に犬(タマ)ができる
dog.assign_attributes(MyStrongParameter.new(name: "tama", status: "sleepy"))
=> {:name=>"tama", :status=>"sleepy"}


# idはブラックリストに登録されているためpermitted?はfalseとなりサニタイズされる(ForbiddenAttributesErrorが出る)
dog.assign_attributes(MyStrongParameter.new(id: 1, name: "tama", status: "sleepy"))
Traceback (most recent call last):
        1: from (irb):261
ActiveModel::ForbiddenAttributesError (ActiveModel::ForbiddenAttributesError)
```

良い感じに動作しています。


# まとめ
StrongParameterは必須ではないので使わないという選択もできます。 
ActiveModelの内部ではStrongParameterのインスタンスであるかというチェックをしているわけでは無いので拡張も可能です。