diff --git a/app/controllers/devise/confirmations_controller.rb b/app/controllers/devise/confirmations_controller.rb index 68014c92..58802882 100644 --- a/app/controllers/devise/confirmations_controller.rb +++ b/app/controllers/devise/confirmations_controller.rb @@ -39,5 +39,4 @@ class Devise::ConfirmationsController < DeviseController def after_confirmation_path_for(resource_name, resource) after_sign_in_path_for(resource) end - end diff --git a/app/controllers/devise/registrations_controller.rb b/app/controllers/devise/registrations_controller.rb index 281979a7..c7cee32c 100644 --- a/app/controllers/devise/registrations_controller.rb +++ b/app/controllers/devise/registrations_controller.rb @@ -83,7 +83,11 @@ class Devise::RegistrationsController < DeviseController # Build a devise resource passing in the session. Useful to move # temporary session data to the newly created user. def build_resource(hash=nil) - hash ||= resource_params || {} + if request.get? + hash ||= {} + else + hash ||= resource_params || {} + end self.resource = resource_class.new_with_session(hash, session) end diff --git a/app/controllers/devise/unlocks_controller.rb b/app/controllers/devise/unlocks_controller.rb index 45f6b2c1..3b0d9f7f 100644 --- a/app/controllers/devise/unlocks_controller.rb +++ b/app/controllers/devise/unlocks_controller.rb @@ -40,5 +40,4 @@ class Devise::UnlocksController < DeviseController def after_unlock_path_for(resource) new_session_path(resource) end - end diff --git a/app/controllers/devise_controller.rb b/app/controllers/devise_controller.rb index 94359769..8c67eb2d 100644 --- a/app/controllers/devise_controller.rb +++ b/app/controllers/devise_controller.rb @@ -28,10 +28,6 @@ class DeviseController < Devise.parent_controller.constantize devise_mapping.to end - def resource_params - params[resource_name] - end - # Returns a signed in resource from session (if one exists) def signed_in_resource warden.authenticate(:scope => resource_name) @@ -96,7 +92,13 @@ MESSAGE # Build a devise resource. # Assignment bypasses attribute protection when :unsafe option is passed def build_resource(hash = nil, options = {}) - hash ||= resource_params || {} + # When building a resource, invoke strong_parameters require/permit + # steps if the params hash includes the resource name. + if params[resource_name] + hash ||= resource_params || {} + else + hash ||= {} + end if options[:unsafe] self.resource = resource_class.new.tap do |resource| @@ -181,4 +183,21 @@ MESSAGE format.any(*navigational_formats, &block) end end + + # Setup a param sanitizer to filter parameters using strong_parameters. See + # lib/devise/controllers/parameter_sanitizer.rb for more info. Override this + # method in your application controller to use your own parameter sanitizer. + def parameters_sanitizer + @parameters_sanitizer ||= Devise::ParameterSanitizer.new + end + + # Return the params to be used for mass assignment passed through the + # strong_parameters require/permit step. To customize the parameters + # permitted for a specific controller, simply prepend a before_filter and + # call #permit_devise_param or #remove_permitted_devise_param on + # parameters_sanitizer to update the default allowed lists of permitted + # parameters. + def resource_params + params.require(resource_name).permit(parameters_sanitizer.permitted_params_for(controller_name)) + end end diff --git a/lib/devise.rb b/lib/devise.rb index 87e1f307..75001585 100644 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -6,12 +6,13 @@ require 'set' require 'securerandom' module Devise - autoload :Delegator, 'devise/delegator' - autoload :FailureApp, 'devise/failure_app' - autoload :OmniAuth, 'devise/omniauth' - autoload :ParamFilter, 'devise/param_filter' - autoload :TestHelpers, 'devise/test_helpers' - autoload :TimeInflector, 'devise/time_inflector' + autoload :Delegator, 'devise/delegator' + autoload :FailureApp, 'devise/failure_app' + autoload :OmniAuth, 'devise/omniauth' + autoload :ParamFilter, 'devise/param_filter' + autoload :ParameterSanitizer, 'devise/parameter_sanitizer' + autoload :TestHelpers, 'devise/test_helpers' + autoload :TimeInflector, 'devise/time_inflector' module Controllers autoload :Helpers, 'devise/controllers/helpers' diff --git a/lib/devise/parameter_sanitizer.rb b/lib/devise/parameter_sanitizer.rb new file mode 100644 index 00000000..ce800e2d --- /dev/null +++ b/lib/devise/parameter_sanitizer.rb @@ -0,0 +1,65 @@ +module Devise + class ParameterSanitizer + attr_reader :allowed_params + + # Return a list of parameter names permitted to be mass-assigned for the + # passed controller. + def permitted_params_for(controller_name) + allowed_params.fetch(key_for_controller_name(controller_name), []) + end + + # Set up a new parameter sanitizer with a set of allowed parameters. This + # gets initialized on each request so that parameters may be augmented or + # changed as needed via before_filter. + def initialize + @allowed_params = { + :confirmations_controller => [:email], + :passwords_controller => authentication_keys + [:password, :password_confirmation, :reset_password_token], + :registrations_controller => authentication_keys + [:password, :password_confirmation, :current_password], + :sessions_controller => authentication_keys + [:password], + :unlocks_controller => [:email] + } + end + + # Allow additional parameters for a Devise controller. If the + # controller_name doesn't exist in allowed_params, it will be added to it + # as an empty array and param_name will be appended to that array. Note + # that when adding a new controller, use the full controller name + # (:confirmations_controller) and not the short names + # (:confirmation/:confirmations). + def permit_devise_param(controller_name, param_name) + @allowed_params[key_for_controller_name(controller_name)] << param_name + true + end + + # Remove specific allowed parameter for a Devise controller. If the + # controller_name doesn't exist in allowed_params, it will be added to it + # as an empty array. + def remove_permitted_devise_param(controller_name, param_name) + @allowed_params[key_for_controller_name(controller_name)].delete(param_name) + true + end + + protected + + def authentication_keys + Array(::Devise.authentication_keys) + end + + # Flexibly allow access to permitting/denying/checking parameters by + # controller name in the following key formats: :confirmations_controller, + # :confirmations, :confirmation + def key_for_controller_name(name) + if allowed_params.has_key?(name.to_sym) + name.to_sym + elsif allowed_params.has_key?(:"#{name}s_controller") + :"#{name}s_controller" + elsif allowed_params.has_key?(:"#{name}_controller") + :"#{name}_controller" + else + @allowed_params[name.to_sym] = [] + name.to_sym + end + end + end +end diff --git a/test/parameter_sanitizer_test.rb b/test/parameter_sanitizer_test.rb new file mode 100644 index 00000000..56c86193 --- /dev/null +++ b/test/parameter_sanitizer_test.rb @@ -0,0 +1,52 @@ +require 'test_helper' + +class ParameterSanitizerTest < ActiveSupport::TestCase + def sanitizer + Devise::ParameterSanitizer.new + end + + test '#permitted_params_for allows querying of allowed parameters by controller' do + assert_equal [:email], sanitizer.permitted_params_for(:confirmations_controller) + assert_equal [:email, :password, :password_confirmation, :reset_password_token], sanitizer.permitted_params_for(:password) + assert_equal [:email], sanitizer.permitted_params_for(:unlocks) + end + + test '#permitted_params_for returns an empty array for a bad key' do + assert_equal [], sanitizer.permitted_params_for(:bad_key) + end + + test '#permit_devise_param allows adding an allowed param for a specific controller' do + subject = sanitizer + + subject.permit_devise_param(:confirmations_controller, :other) + + assert_equal [:email, :other], subject.permitted_params_for(:confirmations_controller) + end + + test '#remove_permitted_devise_param allows disallowing a param for a specific controller' do + subject = sanitizer + + subject.remove_permitted_devise_param(:confirmations_controller, :email) + + assert_equal [], subject.permitted_params_for(:confirmations_controller) + end + + test '#permit_devise_param allows adding additional devise controllers' do + subject = sanitizer + + subject.permit_devise_param(:invitations_controller, :email) + + assert_equal [:email], subject.permitted_params_for(:invitations) + end + + test '#remove_permitted_devise_param fails gracefully when removing a missing param' do + subject = sanitizer + + # perform twice, just to be sure it handles it gracefully + subject.remove_permitted_devise_param(:invitations_controller, :email) + subject.remove_permitted_devise_param(:invitations_controller, :email) + + assert_equal [], subject.permitted_params_for(:invitations) + end +end +