【Rails】formオブジェクトでForbiddenAttributesErrorが起こる原因と対処方法

## はじめに
formクラスにparamsを渡してsuperで初期化しようとした際にActiveModel::ForbiddenAttributesErrorになる原因をこの記事で解明していこうと思います。

## 忙しい人向けの結論
paramsに対して`.permit`していないからです。
formクラスにparamsを渡す前に、controller側でparams.permit(:〇〇)とするとエラーが起きなくなります。
よければその理由を以下の文章を通してより深く理解してみてください！

## なぜForbiddenAttributesErrorになるのか？

### コード例
コード例は以下のような状況を基に作成されています。
- 醤油ラーメンを注文しようとしている
- このラーメン屋では、醤油ラーメンと豚骨ラーメンしか売っていない
- 醤油ラーメン・豚骨ラーメン以外の注文が来ないようにバリデーションをかけたい


```
# 醤油ラーメンの注文がparametersで飛んでくる
#<ActionController::Parameters {"soup"=>"醤油"} permitted: false>
```
```order_controller.rb
class OrderController
    def order
        form = OrderForm.new(params)
        raise '醤油・豚骨以外は許さん！' if form.invalid?
        
        form.order
    end
end
```

``` order_form.rb
class OrderForm
  include ActiveModel::Model

  validates :soup, inclusion: { in: ['醤油', '豚骨'] }

  def initialize(attributes = {})
    super(attributes)
  end

  def order
      # 注文処理を行う
  end
end
```

###  エラー詳細
処理を走らせると以下のようなエラーが出ました。
```
Rendering with exception: ActiveModel::ForbiddenAttributesError
/usr/local/bundle/gems/activemodel-6.1.7.7/lib/active_model/forbidden_attributes_protection.rb:23:in `sanitize_for_mass_assignment'
web-central-1  | /usr/local/bundle/gems/activemodel-6.1.7.7/lib/active_model/attribute_assignment.rb:34:in `assign_attributes'
```
エラーをよく見ると`lib/active_model/forbidden_attributes_protection.rb`の`sanitize_for_mass_assignment`メソッド内でエラーになっていることがわかります。
以下が該当ファイルです。
```forbidden_attributes_protection.rb
module ForbiddenAttributesProtection
    private
    # 今回attributesに入っている値は　#<ActionController::Parameters {"soup"=>"醤油"} permitted: false>
      def sanitize_for_mass_assignment(attributes)
        if attributes.respond_to?(:permitted?)
        # permitted: falseだったらForbiddenAttributesErrorを起こす。
          raise ActiveModel::ForbiddenAttributesError if !attributes.permitted? 
          attributes.to_h
        else
          attributes
        end
      end
      alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment
  end
```

### エラーが起こる原因とエラーを起こしたい理由って？
エラーが起きている理由は上記コードブロック内に記載があるとおり、`permitted: false`だからですね。

ではなぜ`permitted: false`のときにエラーを起こす必要があるのでしょうか？
答えはマスアサインメントという機能にあります。

 ### マスアサインメントって？
 Railsガイド参照
> チェックされていないパラメータをまるごとモデルに保存する行為は、モデルに対する「マスアサインメント」と呼ばれています。これが発生すると、正常なデータの中に悪意のあるデータが含まれてしまう可能性があります。

この文章だけ見るとよくわからないですよね。銀行口座開設を例に出して説明します。
（イメージしやすいように極端な例にしています）

銀行口座開設をする際に以下の要素をwebサイト上で入力して送信するとします。
- 氏名
- 生年月日
- 電話番号

普通の人は上記のみを入力して送信します。

ですが、悪い人はこのように考えます。
悪い人：「もしかしたら、貯金残高10億円というパラメーターを送信すれば、貯金残高１０億円の銀行口座を作れるんじゃないか！？」

**実はこれ可能なんです。** セキュリティがガバガバであれば貯金残高１０億円の夢の銀行口座がつくれちゃいます。
このように悪意のあるデータが紛れ込んでしまうことを**マスアサインメント**といいます。

### マスアサインメントを防ぐにはどうすればいいの？
答えは　`.permit`を使用する　です。
`.permit`を使用することで不正なパラメーターを防止できます。

### `.permit`を使用したコード例
元のコードを以下のように変更しました。
```order_controller.rb
class OrderController
    def order
        # 元はform = OrderForm.new(params)
        form = OrderForm.new(params.permit(:soup)
        raise '醤油・豚骨以外は許さん！' if form.invalid?
        
        form.order
    end
end
```
変更点は`.permit(:許可したいパラメーター)`を追加したことです。
これによってパラメーターが以下のようになりました。
```
#<ActionController::Parameters {"soup"=>"醤油"} permitted: true>
```
`permitted: false`が`permitted: true`に変化しているのでエラーを起こすことなく処理を走らせることができるようになっています。

ただ、これだと`.permit`のメリットが伝わらないと思うので、不正に煮卵をトッピングしてみましょう。
```
#<ActionController::Parameters {"soup"=>"醤油", "topping"=>"boiled egg"} permitted: false>
```
上記のようなパラメーターを`order_controller`に飛ばしてみましょう。
すると`.permit`によって以下のようなパラメーターになります。
```
#<ActionController::Parameters {"soup"=>"醤油"} permitted: true>
```
 `"topping"=>"boiled egg"`というパラメーターが消ていますね。
 このように`.permit`を使用すると、指定したもの以外の**不正なパラメーターを除外**してくれ、安全に処理を実行できます
　
 これで`.permit`のメリットが理解できたと思いますので、ぜひ活用してみてください！

## 結論
- formクラスにparamsを渡してsuperで初期化しようとした際にActiveModel::ForbiddenAttributesErroとなる原因は`.permit`で飛んでくるパラメーターを許可していないから
- `.permit`を使用すると、不正なパラメーターを防いでくれるため安全に処理を実行できる