概要
StrongParameterはrails4から導入されたマスアサインメントを防ぐために導入された仕組みですが、使用が前提になっていると思っている方がいるようですが実際には必須ではありません。例えば、以下のようにStrongParameterからHashにして入力しても使うことができます。なぜこのように動作するかについてrailsのソースコードを見ながら説明します。
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を呼び出すようになっています。
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
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に結果を渡すという処理をやっています。
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は必須ではないということになります。
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?メソッドが定義されていないから、結果的にサニタイズの処理が走らないんじゃないのか? と思えるかもしれません。
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が返却されるという仕組みとなっています。
# 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のみを指定する方式なので、逆にブラックリスト方式にしてみます。
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つでも含まれていれば許可しないようにロジックを書きます。 これを実行すると以下のようになります。
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
このクラスで実装したオブジェクトを使って、実際にマスアサインメントでサニタイズされるかどうかを確認します。
# 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のインスタンスであるかというチェックをしているわけでは無いので拡張も可能です。