みなさんこんばんは。クラウドワークス Advent Calendar 2018 5日目担当のじゅんてつです。
Rails5、使ってますか?クラウドワークスではまだ使えていない現状がありまして、絶賛Railsの更新中でございます。この記事は、Rails5に更新するためMassAssignmentを撲滅することになったお話をします。
どうやってRails5更新しようとしてるのか?
Rails5に更新をするためには、何はともあれ変更点を把握しないと始まらないので変更点を洗い、次にRails4の状態で更新できるものは最新にし、最後にRails5にあげて動くように整えてあげます。これだけです。シンプルでわかりやすいですね。
- 変更点の洗い出し
- Gemの削除と更新
- MassAssingmentの撲滅 (←いまこの辺)
- Gemの追加、Rails5.x分
- 後方互換が廃止されたコードの置き換え
- 非推奨となったコードの置き換え
- Rails5サーバの本番稼働インフラの整備
- 受け入れテスト
いまやってることは、MassAssingmentで実装されてる箇所の撲滅です。コントローラーをStrongParameterで置き換えて、モデルの attr_accessible
と attr_protected
外して回ることをしています。簡単そうでしょ?それがそうでもなかったんです。Rails4以前で開発されたプロダクトを触ってる方は、同じような経験をする方もいるのではと思っています。そんなとき、この記事がなんらかの助力になればと思っています。
なぜMassAssignmentを撲滅するのか?
Rails4で非推奨だった attr_accsessible
attr_protected
が、Rails5ではついに使えなくなります。昔からMassAssignmentの脆弱性が指摘されていたからしかたないですね。パラメータでのレコード更新は、モデル層でセーフティを掛けていましたが、今後はコントローラ層でセーフティを掛けるような仕組みにしなければなりません。
対応規模はどれくらいか?
弊社はまだStrongParameterへの移行が終わっていないコントローラーが残っています。Rails4にするときの対応で、 params.permit!
を全コントローラに書いて回っていて、「日頃の業務で少しずつ気づいたら直していってね」ってやってたところほとんど直されていませんでした\(^o^)/ 対象を洗い出してみたら、なんと200本弱のコントローラーで対応が必要なことがわかりました。これを最初に知ったとき、心理的に結構辛かったです。
どう撲滅するのか?
辛いのはわかった。でも千里の道も一歩からなので、地道に頑張ることにしました。調べて結果、どうやら以下の対応をすることで撲滅することができそうです。
- コントローラーをStrongParameterで実装する
- コントローラーでレコード更新している
params[:hoge]
を StrongParameter に置き換える
- コントローラーでレコード更新している
- モデルから
attr_accessible
attr_protected
を消して回る - Gemfileから
gem protected_attributes
を消す - CI通しつつ念の為動作確認する
最終的には、勇気があれば何でも出来ます。
コントローラーをStrongParameterで実装する
どうやらコントローラーで受け取る params
をStrongParameterにしてあげれば良いことが分かりました。params.require(:hoge).permit(:fuga)
に置き換えて終わりでしょ?そんなの楽勝じゃん?余裕余裕!とはじめのうちは思っていました。
置き換えを進めていくうちに、パラメータの構造によって permit
の仕方がいくつかあることがわかりました。また、業務ロジックを考慮しつつ、あとからStrongParameterの実装しようとするとそこそこ大変なことにも気づきました。正直なめてました、スミマセン。
StrongParameterの実装パターン集
反省を踏まえ、実装パターンを共有します。だいたいはこれでカバーしきれるので、今後対応する方の参考になればと思います。いま見えている範囲では、これらのパターンと勇気だけで撲滅できそうな感じがしています。
指定したカラムだけ取得(基本形)
params.require(:article).permit(:title, :content)
ネストしたパラメータを取得
params.require(:article).permit(:title, :content, comments: [:user_id, :content])
キーがない場合にエラーを出さないようにする
params.fetch(:article, {}).permit(:title, :content)
対象のキーそのものが飛んでこない場合はfetchを使います。知らずに require
で実装しちゃうとエラー吐くので気をつけてください。
配列が入ってくるパラメータを受け取りたい
例えばこんな形の params があるとして
{
user: {
occupation_ids: [1, 2],
job_category_ids:[1, 2]
}
}
occupation_ids
や job_category_ids
を受け取りたいという場合。
以下の方法だと上手くいかない。
# 上手くいかないパターン; {} が返ってくる
params.require(:user).permit(:occupation_ids, :job_category_ids)
それぞれ、ネストした値として []
を指定しておけば期待通りに動く。
以下のようにするのが正解。
params.require(:user).permit(occupation_ids: [], job_category_ids: [])
モデルから attr_accessible
attr_protected
を消して回る
勇気を持って attr_accessible
attr_protected
を消してまわります。基本的にはコントローラー側がStrongParameterに置き換わったら消せるようになっているので、CI通ることを確認しつつ、念のため動作確認します。あとは祈ってリリースするだけです。信仰心が試されます。
attr_*
を消して回っていたら気付いたこと
さて、いくつかのモデル側の対応を進めていたら、以下のような対応が必要だと気が付きました。
- モデルのカラムでは無い項目が
attr_accessible
に含まれていたら- その項目のバリデーションが有る場合、
attr_accessible
から削除しても大丈夫 - その項目のバリデーションが無い場合、エラーを吐くのでバリデーションを実装
- その項目のバリデーションが有る場合、
こちらもサンプルとしてコードを晒しておきます。
モデルのカラムでは無い項目が attr_accessible
に含まれていたら
例えばですが、コードはこんな感じ。
class HogeHogeApplication < ActiveRecord::Base
attr_accessible :course, :agreement
validates :agreement, acceptance: true
...
end
これをこうするとちゃんと動きます。
class HogeHogeApplication < ActiveRecord::Base
# attr_accessible を全削除
validates :agreement, acceptance: true
...
end
バリデーションを消すと
class WelfareIijApplication < ActiveRecord::Base
# attr_accessible を全削除
# validates :agreement, acceptance: true
...
end
「agreement とか知らんし」と怒られる(´・ω・`)
# rails consolet
> HogeHogeApplication.new(course: "a", agreement: "1")
# => ActiveRecord::UnknownAttributeError: unknown attribute 'agreement' for HogeHogeApplication.
まとめ
- コントローラー側の対応はStrongParameter
- いろんな
permit
の仕方があるがサンプル集のどれかのパターンになる - 業務ロジック知らないと後付が難しいのでちまちま直しておいたほうがベター
- いろんな
- モデル側の対応は
attr_accessible
attr_protected
を削除- 基本的に消すだけ
- モデルのカラムに存在しない項目が含まれている場合、バリデーション掛けると動く
- CI回しつつ念の為動作確認。
最後に
Rails5更新を進めていますが、まだ序盤のため本記事ではミクロな話題をピックしました。今後様々な課題に直面するはずです。どの工程が一番つかったかとか、思ったほどじゃなかったなーとか、こんな伏兵がいた!等などをプロジェクトが終わったらアウトプットしたいと思っています。
次回は、wonda-tea-coffeeが等間隔に並ぶ素数を見つけるそうです。引き続き クラウドワークス Advent Calendar 2018をよろしくおねがいします。