├── Gemfile ├── lib ├── strongly_typed_parameters │ ├── version.rb │ ├── boolean.rb │ └── railtie.rb ├── strongly_typed_parameters.rb ├── active_model │ └── forbidden_attributes_protection.rb ├── generators │ └── rails │ │ ├── strong_parameters_controller_generator.rb │ │ ├── USAGE │ │ └── templates │ │ └── controller.rb └── action_controller │ └── parameters.rb ├── .gitignore ├── test ├── gemfiles │ ├── Gemfile.rails-3.0.x │ ├── Gemfile.rails-3.1.x │ ├── Gemfile.rails-3.2.x │ └── Gemfile.rails-3.0.x.lock ├── parameters_require_test.rb ├── action_controller_tainted_params_test.rb ├── active_model_smart_type_defaulting_test.rb ├── test_helper.rb ├── action_controller_required_params_test.rb ├── raise_on_unpermitted_params_test.rb ├── active_model_mass_assignment_taint_protection_test.rb ├── controller_generator_test.rb ├── log_on_unpermitted_params_test.rb ├── multi_parameter_attributes_test.rb ├── parameters_taint_test.rb └── parameters_permit_test.rb ├── .travis.yml ├── Rakefile ├── strongly_typed_parameters.gemspec ├── MIT-LICENSE ├── Gemfile.lock └── README.rdoc /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | 4 | gem 'rdoc' 5 | -------------------------------------------------------------------------------- /lib/strongly_typed_parameters/version.rb: -------------------------------------------------------------------------------- 1 | module StronglyTypedParameters 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/log/*.log 6 | test/dummy/tmp/ 7 | test/dummy/.sass-cache 8 | tmp/ 9 | -------------------------------------------------------------------------------- /test/gemfiles/Gemfile.rails-3.0.x: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec :path => "./../.." 3 | 4 | gem "actionpack", "~> 3.0.0" 5 | gem "railties", "~> 3.0.0" 6 | gem "activemodel", "~> 3.0.0" 7 | -------------------------------------------------------------------------------- /test/gemfiles/Gemfile.rails-3.1.x: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec :path => "./../.." 3 | 4 | gem "actionpack", "~> 3.1.0" 5 | gem "railties", "~> 3.1.0" 6 | gem "activemodel", "~> 3.1.0" 7 | -------------------------------------------------------------------------------- /test/gemfiles/Gemfile.rails-3.2.x: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec :path => "./../.." 3 | 4 | gem "actionpack", "~> 3.2.0" 5 | gem "railties", "~> 3.2.0" 6 | gem "activemodel", "~> 3.2.0" 7 | -------------------------------------------------------------------------------- /lib/strongly_typed_parameters.rb: -------------------------------------------------------------------------------- 1 | require 'action_controller/parameters' 2 | require 'active_model/forbidden_attributes_protection' 3 | require 'strongly_typed_parameters/railtie' 4 | require 'strongly_typed_parameters/boolean' 5 | -------------------------------------------------------------------------------- /lib/strongly_typed_parameters/boolean.rb: -------------------------------------------------------------------------------- 1 | #marker module to identify boolean objects. 2 | 3 | module Boolean 4 | end 5 | 6 | class TrueClass 7 | include Boolean 8 | end 9 | 10 | class FalseClass 11 | include Boolean 12 | end 13 | -------------------------------------------------------------------------------- /test/parameters_require_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'action_controller/parameters' 3 | 4 | class ParametersRequireTest < ActiveSupport::TestCase 5 | test "required parameters must be present not merely not nil" do 6 | assert_raises(ActionController::ParameterMissing) do 7 | ActionController::Parameters.new(:person => {}).require(:person) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 1.8.7 5 | - 1.9.2 6 | - 1.9.3 7 | 8 | gemfile: 9 | - test/gemfiles/Gemfile.rails-3.0.x 10 | - test/gemfiles/Gemfile.rails-3.1.x 11 | - test/gemfiles/Gemfile.rails-3.2.x 12 | 13 | notifications: 14 | email: false 15 | irc: 16 | on_success: change 17 | on_failure: always 18 | channels: 19 | - "irc.freenode.org#rails-contrib" 20 | -------------------------------------------------------------------------------- /lib/active_model/forbidden_attributes_protection.rb: -------------------------------------------------------------------------------- 1 | module ActiveModel 2 | class ForbiddenAttributes < StandardError 3 | end 4 | 5 | module ForbiddenAttributesProtection 6 | def sanitize_for_mass_assignment(*options) 7 | new_attributes = options.first 8 | if !new_attributes.respond_to?(:permitted?) || new_attributes.permitted? 9 | super 10 | else 11 | raise ActiveModel::ForbiddenAttributes 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/generators/rails/strong_parameters_controller_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/version' 2 | require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' 3 | 4 | module Rails 5 | module Generators 6 | class StrongParametersControllerGenerator < ScaffoldControllerGenerator 7 | argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" 8 | source_root File.expand_path("../templates", __FILE__) 9 | 10 | if ::Rails::VERSION::STRING < '3.1' 11 | def module_namespacing 12 | yield if block_given? 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/rails/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Stubs out a scaffolded controller and its views. Different from rails 3 | scaffold_controller, it uses strongly_typed_parameters to whitelist permissible 4 | attributes in a private method. 5 | Pass the model name, either CamelCased or under_scored. The controller 6 | name is retrieved as a pluralized version of the model name. 7 | 8 | To create a controller within a module, specify the model name as a 9 | path like 'parent_module/controller_name'. 10 | 11 | This generates a controller class in app/controllers and invokes helper, 12 | template engine and test framework generators. 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | begin 3 | require 'bundler/setup' 4 | require 'bundler/gem_tasks' 5 | rescue LoadError 6 | raise 'You must `gem install bundler` and `bundle install` to run rake tasks' 7 | end 8 | 9 | require 'rdoc/task' 10 | 11 | RDoc::Task.new(:rdoc) do |rdoc| 12 | rdoc.rdoc_dir = 'rdoc' 13 | rdoc.title = 'StrongParameters' 14 | rdoc.options << '--line-numbers' 15 | rdoc.rdoc_files.include('README.rdoc') 16 | rdoc.rdoc_files.include('lib/**/*.rb') 17 | end 18 | 19 | require 'rake/testtask' 20 | 21 | Rake::TestTask.new(:test) do |t| 22 | t.libs << 'lib' 23 | t.libs << 'test' 24 | t.pattern = 'test/**/*_test.rb' 25 | t.verbose = false 26 | end 27 | 28 | task :default => :test 29 | -------------------------------------------------------------------------------- /lib/strongly_typed_parameters/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'rails/railtie' 2 | 3 | module StronglyTypedParameters 4 | class Railtie < ::Rails::Railtie 5 | if config.respond_to?(:app_generators) 6 | config.app_generators.scaffold_controller = :strongly_typed_parameters_controller 7 | else 8 | config.generators.scaffold_controller = :strongly_typed_parameters_controller 9 | end 10 | 11 | initializer "strong_parameters.config", :before => "action_controller.set_configs" do |app| 12 | ActionController::Parameters.action_on_unpermitted_parameters = app.config.action_controller.delete(:action_on_unpermitted_parameters) do 13 | (Rails.env.test? || Rails.env.development?) ? :log : false 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/action_controller_tainted_params_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PeopleController < ActionController::Base 4 | def create 5 | render :text => params[:person].permitted? ? "untainted" : "tainted" 6 | end 7 | 8 | def create_with_permit 9 | render :text => params[:person].permit(:name).permitted? ? "untainted" : "tainted" 10 | end 11 | end 12 | 13 | class ActionControllerTaintedParamsTest < ActionController::TestCase 14 | tests PeopleController 15 | 16 | test "parameters are tainted" do 17 | post :create, { :person => { :name => "Mjallo!" } } 18 | assert_equal "tainted", response.body 19 | end 20 | 21 | test "parameters can be permitted and are then not tainted" do 22 | post :create_with_permit, { :person => { :name => "Mjallo!" } } 23 | assert_equal "untainted", response.body 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/active_model_smart_type_defaulting_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'action_controller/parameters' 3 | 4 | class Column 5 | attr_accessor :name 6 | attr_accessor :klass 7 | def initialize(hsh) 8 | hsh.each do |name, klass| 9 | @name = name 10 | @klass = klass 11 | end 12 | end 13 | end 14 | 15 | class User 16 | def self.columns 17 | [Column.new("id" => Fixnum), Column.new("name" => String)] 18 | end 19 | end 20 | 21 | class ActiveModelSmartTypeDefaultingTest < ActiveSupport::TestCase 22 | test "if no types are given but the parent object shares a name with a model, attribute types are used" do 23 | params = ActionController::Parameters.new(:user => [:id => 1234]) 24 | permitted = params.permit(:user => [:id, :name]) 25 | assert_equal permitted[:user][0][:id], 1234 26 | assert_nil permitted[:user][0][:name] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require 'test/unit' 5 | require 'rails' 6 | 7 | class FakeApplication < Rails::Application; end 8 | 9 | Rails.application = FakeApplication 10 | Rails.configuration.action_controller = ActiveSupport::OrderedOptions.new 11 | 12 | require 'strongly_typed_parameters' 13 | require 'mocha' 14 | 15 | module ActionController 16 | SharedTestRoutes = ActionDispatch::Routing::RouteSet.new 17 | SharedTestRoutes.draw do 18 | match ':controller(/:action)' 19 | end 20 | 21 | class Base 22 | include ActionController::Testing 23 | include SharedTestRoutes.url_helpers 24 | end 25 | 26 | class ActionController::TestCase 27 | setup do 28 | @routes = SharedTestRoutes 29 | end 30 | end 31 | end 32 | 33 | ActionController::Parameters.action_on_unpermitted_parameters = false 34 | 35 | # Load support files 36 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 37 | -------------------------------------------------------------------------------- /test/action_controller_required_params_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BooksController < ActionController::Base 4 | def create 5 | params.require(:book).require(:name) 6 | head :ok 7 | end 8 | end 9 | 10 | class ActionControllerRequiredParamsTest < ActionController::TestCase 11 | tests BooksController 12 | 13 | test "missing required parameters will raise exception" do 14 | post :create, { :magazine => { :name => "Mjallo!" } } 15 | assert_response :bad_request 16 | 17 | post :create, { :book => { :title => "Mjallo!" } } 18 | assert_response :bad_request 19 | end 20 | 21 | test "required parameters that are present will not raise" do 22 | post :create, { :book => { :name => "Mjallo!" } } 23 | assert_response :ok 24 | end 25 | 26 | test "missing parameters will be mentioned in the return" do 27 | post :create, { :magazine => { :name => "Mjallo!" } } 28 | assert_equal "Required parameter missing: book", response.body 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /strongly_typed_parameters.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "strongly_typed_parameters/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "strongly_typed_parameters" 9 | s.version = StronglyTypedParameters::VERSION 10 | s.authors = ["David Heinemeier Hansson", "Aaron Weiner"] 11 | s.email = ["aweiner@mdsol.com"] 12 | s.summary = "Whitelist and typecheck your parameters at the controller level" 13 | s.homepage = "https://github.com/mdsol/strong_parameters" 14 | 15 | s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.rdoc"] 16 | s.test_files = Dir["test/**/*"] 17 | 18 | s.add_dependency "actionpack", "~> 3.0" 19 | s.add_dependency "activemodel", "~> 3.0" 20 | s.add_dependency "railties", "~> 3.0" 21 | 22 | s.add_development_dependency "rake" 23 | s.add_development_dependency "mocha", "~> 0.12.0" 24 | 25 | end 26 | -------------------------------------------------------------------------------- /test/raise_on_unpermitted_params_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'action_controller/parameters' 3 | 4 | class RaiseOnUnpermittedParamsTest < ActiveSupport::TestCase 5 | def setup 6 | ActionController::Parameters.action_on_unpermitted_parameters = :raise 7 | end 8 | 9 | def teardown 10 | ActionController::Parameters.action_on_unpermitted_parameters = false 11 | end 12 | 13 | test "raises on unexpected params" do 14 | params = ActionController::Parameters.new({ 15 | :book => { :pages => 65 }, 16 | :fishing => "Turnips" 17 | }) 18 | 19 | assert_raises(ActionController::UnpermittedParameters) do 20 | params.permit(:book => [:pages]) 21 | end 22 | end 23 | 24 | test "raises on unexpected nested params" do 25 | params = ActionController::Parameters.new({ 26 | :book => { :pages => 65, :title => "Green Cats and where to find then." } 27 | }) 28 | 29 | assert_raises(ActionController::UnpermittedParameters) do 30 | params.permit(:book => [:pages]) 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /test/active_model_mass_assignment_taint_protection_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Person 4 | include ActiveModel::MassAssignmentSecurity 5 | include ActiveModel::ForbiddenAttributesProtection 6 | 7 | public :sanitize_for_mass_assignment 8 | end 9 | 10 | class ActiveModelMassUpdateProtectionTest < ActiveSupport::TestCase 11 | test "forbidden attributes cannot be used for mass updating" do 12 | assert_raises(ActiveModel::ForbiddenAttributes) do 13 | Person.new.sanitize_for_mass_assignment(ActionController::Parameters.new(:a => "b")) 14 | end 15 | end 16 | 17 | test "permitted attributes can be used for mass updating" do 18 | assert_nothing_raised do 19 | assert_equal({ "a" => "b" }, 20 | Person.new.sanitize_for_mass_assignment(ActionController::Parameters.new(:a => "b").permit(:a))) 21 | end 22 | end 23 | 24 | test "regular attributes should still be allowed" do 25 | assert_nothing_raised do 26 | assert_equal({ :a => "b" }, 27 | Person.new.sanitize_for_mass_assignment(:a => "b")) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 David Heinemeier Hansson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/controller_generator_test.rb: -------------------------------------------------------------------------------- 1 | #FIXME: This errors due to Mocha. 2 | 3 | =begin 4 | 5 | require 'rails/generators/test_case' 6 | require 'generators/rails/strong_parameters_controller_generator' 7 | 8 | class StrongParametersControllerGeneratorTest < Rails::Generators::TestCase 9 | tests Rails::Generators::StrongParametersControllerGenerator 10 | arguments %w(User name:string age:integer --orm=none) 11 | destination File.expand_path("../tmp", File.dirname(__FILE__)) 12 | setup :prepare_destination 13 | 14 | def test_controller_content 15 | Rails.stubs(:application).returns(nil) 16 | run_generator 17 | 18 | assert_file "app/controllers/users_controller.rb" do |content| 19 | 20 | assert_instance_method :create, content do |m| 21 | assert_match '@user = User.new(user_params)', m 22 | assert_match '@user.save', m 23 | assert_match '@user.errors', m 24 | end 25 | 26 | assert_instance_method :update, content do |m| 27 | assert_match '@user = User.find(params[:id])', m 28 | assert_match '@user.update_attributes(user_params)', m 29 | assert_match '@user.errors', m 30 | end 31 | 32 | assert_match 'def user_params', content 33 | assert_match 'params.require(:user).permit(:age, :name)', content 34 | end 35 | end 36 | end 37 | 38 | =end 39 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | strongly_typed_parameters (0.0.1) 5 | actionpack (~> 3.0) 6 | activemodel (~> 3.0) 7 | railties (~> 3.0) 8 | 9 | GEM 10 | remote: http://rubygems.org/ 11 | specs: 12 | abstract (1.0.0) 13 | actionpack (3.0.8) 14 | activemodel (= 3.0.8) 15 | activesupport (= 3.0.8) 16 | builder (~> 2.1.2) 17 | erubis (~> 2.6.6) 18 | i18n (~> 0.5.0) 19 | rack (~> 1.2.1) 20 | rack-mount (~> 0.6.14) 21 | rack-test (~> 0.5.7) 22 | tzinfo (~> 0.3.23) 23 | activemodel (3.0.8) 24 | activesupport (= 3.0.8) 25 | builder (~> 2.1.2) 26 | i18n (~> 0.5.0) 27 | activesupport (3.0.8) 28 | builder (2.1.2) 29 | erubis (2.6.6) 30 | abstract (>= 1.0.0) 31 | i18n (0.5.0) 32 | json (1.8.1) 33 | metaclass (0.0.1) 34 | mocha (0.12.10) 35 | metaclass (~> 0.0.1) 36 | rack (1.2.8) 37 | rack-mount (0.6.14) 38 | rack (>= 1.0.0) 39 | rack-test (0.5.7) 40 | rack (>= 1.0) 41 | railties (3.0.8) 42 | actionpack (= 3.0.8) 43 | activesupport (= 3.0.8) 44 | rake (>= 0.8.7) 45 | thor (~> 0.14.4) 46 | rake (10.1.0) 47 | rdoc (4.0.1) 48 | json (~> 1.4) 49 | thor (0.14.6) 50 | tzinfo (0.3.38) 51 | 52 | PLATFORMS 53 | ruby 54 | 55 | DEPENDENCIES 56 | mocha (~> 0.12.0) 57 | rake 58 | rdoc 59 | strongly_typed_parameters! 60 | -------------------------------------------------------------------------------- /test/log_on_unpermitted_params_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'action_controller/parameters' 3 | 4 | class LogOnUnpermittedParamsTest < ActiveSupport::TestCase 5 | def setup 6 | ActionController::Parameters.action_on_unpermitted_parameters = :log 7 | end 8 | 9 | def teardown 10 | ActionController::Parameters.action_on_unpermitted_parameters = false 11 | end 12 | 13 | test "logs on unexpected params" do 14 | params = ActionController::Parameters.new({ 15 | :book => { :pages => 65 }, 16 | :fishing => "Turnips" 17 | }) 18 | 19 | assert_logged("Unpermitted parameters: fishing") do 20 | params.permit(:book => [:pages]) 21 | end 22 | end 23 | 24 | test "logs on unexpected nested params" do 25 | params = ActionController::Parameters.new({ 26 | :book => { :pages => 65, :title => "Green Cats and where to find then." } 27 | }) 28 | 29 | assert_logged("Unpermitted parameters: title") do 30 | params.permit(:book => [:pages => Numeric]) 31 | end 32 | end 33 | 34 | private 35 | 36 | def assert_logged(message) 37 | old_logger = ActionController::Base.logger 38 | log = StringIO.new 39 | ActionController::Base.logger = Logger.new(log) 40 | 41 | begin 42 | yield 43 | 44 | log.rewind 45 | assert_match message, log.read 46 | ensure 47 | ActionController::Base.logger = old_logger 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/multi_parameter_attributes_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'action_controller/parameters' 3 | 4 | class MultiParameterAttributesTest < ActiveSupport::TestCase 5 | test "permitted multi-parameter attribute keys" do 6 | params = ActionController::Parameters.new({ 7 | :book => { 8 | "shipped_at(1i)" => "2012", 9 | "shipped_at(2i)" => "3", 10 | "shipped_at(3i)" => "25", 11 | "shipped_at(4i)" => "10", 12 | "shipped_at(5i)" => "15", 13 | "published_at(1i)" => "1999", 14 | "published_at(2i)" => "2", 15 | "published_at(3i)" => "5", 16 | "price(1)" => "R$", 17 | "price(2f)" => "2.02" 18 | } 19 | }) 20 | permitted = params.permit :book => [ :shipped_at, :price ] 21 | 22 | assert permitted.permitted? 23 | 24 | assert_equal "2012", permitted[:book]["shipped_at(1i)"] 25 | assert_equal "3", permitted[:book]["shipped_at(2i)"] 26 | assert_equal "25", permitted[:book]["shipped_at(3i)"] 27 | assert_equal "10", permitted[:book]["shipped_at(4i)"] 28 | assert_equal "15", permitted[:book]["shipped_at(5i)"] 29 | 30 | assert_equal "R$", permitted[:book]["price(1)"] 31 | assert_equal "2.02", permitted[:book]["price(2f)"] 32 | 33 | assert_nil permitted[:book]["published_at(1i)"] 34 | assert_nil permitted[:book]["published_at(2i)"] 35 | assert_nil permitted[:book]["published_at(3i)"] 36 | end 37 | end 38 | 39 | -------------------------------------------------------------------------------- /test/gemfiles/Gemfile.rails-3.0.x.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: /Users/mgrosser/code/tools/strong_parameters 3 | specs: 4 | strong_parameters (0.1.6.dev) 5 | actionpack (~> 3.0) 6 | activemodel (~> 3.0) 7 | railties (~> 3.0) 8 | 9 | GEM 10 | remote: http://rubygems.org/ 11 | specs: 12 | abstract (1.0.0) 13 | actionpack (3.0.17) 14 | activemodel (= 3.0.17) 15 | activesupport (= 3.0.17) 16 | builder (~> 2.1.2) 17 | erubis (~> 2.6.6) 18 | i18n (~> 0.5.0) 19 | rack (~> 1.2.5) 20 | rack-mount (~> 0.6.14) 21 | rack-test (~> 0.5.7) 22 | tzinfo (~> 0.3.23) 23 | activemodel (3.0.17) 24 | activesupport (= 3.0.17) 25 | builder (~> 2.1.2) 26 | i18n (~> 0.5.0) 27 | activesupport (3.0.17) 28 | builder (2.1.2) 29 | erubis (2.6.6) 30 | abstract (>= 1.0.0) 31 | i18n (0.5.0) 32 | json (1.7.5) 33 | metaclass (0.0.1) 34 | mocha (0.12.7) 35 | metaclass (~> 0.0.1) 36 | rack (1.2.5) 37 | rack-mount (0.6.14) 38 | rack (>= 1.0.0) 39 | rack-test (0.5.7) 40 | rack (>= 1.0) 41 | railties (3.0.17) 42 | actionpack (= 3.0.17) 43 | activesupport (= 3.0.17) 44 | rake (>= 0.8.7) 45 | rdoc (~> 3.4) 46 | thor (~> 0.14.4) 47 | rake (10.0.1) 48 | rdoc (3.12) 49 | json (~> 1.4) 50 | thor (0.14.6) 51 | tzinfo (0.3.35) 52 | 53 | PLATFORMS 54 | ruby 55 | 56 | DEPENDENCIES 57 | actionpack (~> 3.0.0) 58 | activemodel (~> 3.0.0) 59 | mocha (~> 0.12.0) 60 | railties (~> 3.0.0) 61 | rake 62 | strong_parameters! 63 | -------------------------------------------------------------------------------- /test/parameters_taint_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'action_controller/parameters' 3 | 4 | class ParametersTaintTest < ActiveSupport::TestCase 5 | setup do 6 | @params = ActionController::Parameters.new({ :person => { 7 | :age => "32", :name => { :first => "David", :last => "Heinemeier Hansson" } 8 | }}) 9 | end 10 | 11 | test "fetch raises ParameterMissing exception" do 12 | e = assert_raises(ActionController::ParameterMissing) do 13 | @params.fetch :foo 14 | end 15 | assert_equal :foo, e.param 16 | end 17 | 18 | test "fetch doesnt raise ParameterMissing exception if there is a default" do 19 | assert_nothing_raised do 20 | assert_equal "monkey", @params.fetch(:foo, "monkey") 21 | assert_equal "monkey", @params.fetch(:foo) { "monkey" } 22 | end 23 | end 24 | 25 | test "not permitted is sticky on accessors" do 26 | assert !@params.slice(:person).permitted? 27 | assert !@params[:person][:name].permitted? 28 | assert !@params[:person].except(:name).permitted? 29 | 30 | @params.each { |key, value| assert(!value.permitted?) if key == "person" } 31 | 32 | assert !@params.fetch(:person).permitted? 33 | 34 | assert !@params.values_at(:person).first.permitted? 35 | end 36 | 37 | test "permitted is sticky on accessors" do 38 | @params.permit! 39 | assert @params.slice(:person).permitted? 40 | assert @params[:person][:name].permitted? 41 | assert @params[:person].except(:name).permitted? 42 | 43 | @params.each { |key, value| assert(value.permitted?) if key == "person" } 44 | 45 | assert @params.fetch(:person).permitted? 46 | 47 | assert @params.values_at(:person).first.permitted? 48 | end 49 | 50 | test "not permitted is sticky on mutators" do 51 | assert !@params.delete_if { |k, v| k == "person" }.permitted? 52 | assert !@params.keep_if { |k, v| k == "person" }.permitted? if @params.respond_to?(:keep_if) 53 | end 54 | 55 | test "permitted is sticky on mutators" do 56 | @params.permit! 57 | assert @params.delete_if { |k, v| k == "person" }.permitted? 58 | assert @params.keep_if { |k, v| k == "person" }.permitted? if @params.respond_to?(:keep_if) 59 | end 60 | 61 | test "not permitted is sticky beyond merges" do 62 | assert !@params.merge(:a => "b").permitted? 63 | end 64 | 65 | test "permitted is sticky beyond merges" do 66 | @params.permit! 67 | assert @params.merge(:a => "b").permitted? 68 | end 69 | 70 | test "modifying the parameters" do 71 | @params[:person][:hometown] = "Chicago" 72 | @params[:person][:family] = { :brother => "Jonas" } 73 | 74 | assert_equal "Chicago", @params[:person][:hometown] 75 | assert_equal "Jonas", @params[:person][:family][:brother] 76 | end 77 | 78 | test "permitting parameters that are not there should not include the keys" do 79 | assert !@params.permit(:person, :funky).has_key?(:funky) 80 | end 81 | 82 | test "permit state is kept on a dup" do 83 | @params.permit! 84 | assert_equal @params.permitted?, @params.dup.permitted? 85 | end 86 | 87 | test "permit is recursive" do 88 | @params.permit! 89 | assert @params.permitted? 90 | assert @params[:person].permitted? 91 | assert @params[:person][:name].permitted? 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/generators/rails/templates/controller.rb: -------------------------------------------------------------------------------- 1 | <% module_namespacing do -%> 2 | class <%= controller_class_name %>Controller < ApplicationController 3 | # GET <%= route_url %> 4 | # GET <%= route_url %>.json 5 | def index 6 | @<%= plural_table_name %> = <%= orm_class.all(class_name) %> 7 | 8 | respond_to do |format| 9 | format.html # index.html.erb 10 | format.json { render json: <%= "@#{plural_table_name}" %> } 11 | end 12 | end 13 | 14 | # GET <%= route_url %>/1 15 | # GET <%= route_url %>/1.json 16 | def show 17 | @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> 18 | 19 | respond_to do |format| 20 | format.html # show.html.erb 21 | format.json { render json: <%= "@#{singular_table_name}" %> } 22 | end 23 | end 24 | 25 | # GET <%= route_url %>/new 26 | # GET <%= route_url %>/new.json 27 | def new 28 | @<%= singular_table_name %> = <%= orm_class.build(class_name) %> 29 | 30 | respond_to do |format| 31 | format.html # new.html.erb 32 | format.json { render json: <%= "@#{singular_table_name}" %> } 33 | end 34 | end 35 | 36 | # GET <%= route_url %>/1/edit 37 | def edit 38 | @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> 39 | end 40 | 41 | # POST <%= route_url %> 42 | # POST <%= route_url %>.json 43 | def create 44 | @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> 45 | 46 | respond_to do |format| 47 | if @<%= orm_instance.save %> 48 | format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %> } 49 | format.json { render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %> } 50 | else 51 | format.html { render action: "new" } 52 | format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity } 53 | end 54 | end 55 | end 56 | 57 | # PATCH/PUT <%= route_url %>/1 58 | # PATCH/PUT <%= route_url %>/1.json 59 | def update 60 | @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> 61 | 62 | respond_to do |format| 63 | if @<%= orm_instance.update_attributes("#{singular_table_name}_params") %> 64 | format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> } 65 | format.json { head :no_content } 66 | else 67 | format.html { render action: "edit" } 68 | format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity } 69 | end 70 | end 71 | end 72 | 73 | # DELETE <%= route_url %>/1 74 | # DELETE <%= route_url %>/1.json 75 | def destroy 76 | @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> 77 | @<%= orm_instance.destroy %> 78 | 79 | respond_to do |format| 80 | format.html { redirect_to <%= index_helper %>_url } 81 | format.json { head :no_content } 82 | end 83 | end 84 | 85 | private 86 | 87 | # Use this method to whitelist the permissible parameters. Example: 88 | # params.require(:person).permit(:name, :age) 89 | # Also, you can specialize this method with per-user checking of permissible attributes. 90 | def <%= "#{singular_table_name}_params" %> 91 | params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes.map {|a| ":#{a.name}" }.sort.join(', ') %>) 92 | end 93 | end 94 | <% end -%> 95 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Strong(ly typed) Parameters 2 | 3 | With this plugin Action Controller parameters are forbidden to be used in Active Model mass assignments until they have been whitelisted. This means you'll have to make a conscious choice about which attributes to allow for mass updating and thus prevent accidentally exposing that which shouldn't be exposed. 4 | In this fork, the type of each parameter is also validated to avoid unexpected behavior with implicit casting. 5 | 6 | In addition, parameters can be marked as required and flow through a predefined raise/rescue flow to end up as a 400 Bad Request with no effort. 7 | 8 | class PeopleController < ActionController::Base 9 | # This will raise an ActiveModel::ForbiddenAttributes exception because it's using mass assignment 10 | # without an explicit permit step. 11 | def create 12 | Person.create(params[:person]) 13 | end 14 | 15 | # This will pass with flying colors as long as there's a person key in the parameters, otherwise 16 | # it'll raise a ActionController::MissingParameter exception, which will get caught by 17 | # ActionController::Base and turned into that 400 Bad Request reply. 18 | def update 19 | person = current_account.people.find(params[:id]) 20 | person.update_attributes!(person_params) 21 | redirect_to person 22 | end 23 | 24 | private 25 | # Using a private method to encapsulate the permissible parameters is just a good pattern 26 | # since you'll be able to reuse the same permit list between create and update. Also, you 27 | # can specialize this method with per-user checking of permissible attributes. 28 | def person_params 29 | params.require(:person).permit(:name, :age) 30 | end 31 | end 32 | 33 | == Permitted Types 34 | 35 | Given 36 | 37 | params.permit(:id) 38 | 39 | the key +:id+ will pass the whitelisting if it appears in +params+ and is a String. Otherwise the key is going to be filtered out, so arrays, hashes, or any other objects cannot be injected. 40 | 41 | If instead the argument is given as 42 | 43 | params.permit(:id => Numeric) 44 | 45 | the +:id+ value must be a number. Any class or module can be given here. The marker module Boolean is included in TrueClass and FalseClass. 46 | 47 | To declare that the value in +params+ must be an array of values of a certain type, wrap the type constant in an Array: 48 | 49 | params.permit(:id => [Numeric]) 50 | 51 | == Defaults with ActiveRecord 52 | 53 | If a parameter shares a name with an ActiveRecord model, the default types for its attributes are those of that model, rather than String. 54 | 55 | == Nested Parameters 56 | 57 | You can also use permit on nested parameters, like: 58 | 59 | params.permit(:name, {:emails => [String]}, :friends => [ :name, { :family => [ :name ] }]) 60 | 61 | Thanks to Nick Kallen for the permit idea! 62 | 63 | == Handling of Unpermitted Keys 64 | 65 | By default parameter keys that are not explicitly permitted will be logged in the development and test environment. In other environments these parameters will simply be filtered out and ignored. 66 | 67 | Additionally, this behaviour can be changed by changing the +config.action_controller.action_on_unpermitted_parameters+ property in your environment files. If set to +:log+ the unpermitted attributes will be logged, if set to +:raise+ an exception will be raised. 68 | 69 | == Installation 70 | 71 | In Gemfile: 72 | 73 | gem 'strongly_typed_parameters' 74 | 75 | and then run `bundle`. To activate the strong parameters, you need to include this module in 76 | every model you want protected. 77 | 78 | class Post < ActiveRecord::Base 79 | include ActiveModel::ForbiddenAttributesProtection 80 | end 81 | 82 | If you want to now disable the default whitelisting that occurs in later versions of Rails, change the +config.active_record.whitelist_attributes+ property in your +config/application.rb+: 83 | 84 | config.active_record.whitelist_attributes = false 85 | 86 | This will allow you to remove / not have to use +attr_accessible+ and do mass assignment inside your code and tests. 87 | 88 | == Compatibility 89 | 90 | This plugin is only fully compatible with Rails versions 3.0, 3.1 and 3.2 but not 4.0+, as the non-typechecking version is part of Rails Core in 4.0. 91 | -------------------------------------------------------------------------------- /lib/action_controller/parameters.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | require 'bigdecimal' 3 | require 'stringio' 4 | 5 | require 'active_support/concern' 6 | require 'active_support/core_ext/hash/indifferent_access' 7 | require 'action_controller' 8 | require 'action_dispatch/http/upload' 9 | 10 | module ActionController 11 | class ParameterMissing < IndexError 12 | attr_reader :param 13 | 14 | def initialize(param) 15 | @param = param 16 | super("key not found: #{param}") 17 | end 18 | end 19 | 20 | class UnpermittedParameters < IndexError 21 | attr_reader :params 22 | 23 | def initialize(params) 24 | @params = params 25 | super("found unpermitted parameters: #{params.join(", ")}") 26 | end 27 | end 28 | 29 | class Parameters < ActiveSupport::HashWithIndifferentAccess 30 | attr_accessor :permitted 31 | alias :permitted? :permitted 32 | attr_accessor :klass 33 | 34 | cattr_accessor :action_on_unpermitted_parameters, :instance_accessor => false 35 | 36 | # Never raise an UnpermittedParameters exception because of these params 37 | # are present. They are added by Rails and it's of no concern. 38 | NEVER_UNPERMITTED_PARAMS = %w( controller action ) 39 | 40 | def initialize(attributes = nil, klass = String) 41 | super(attributes) 42 | @permitted = false 43 | @klass = klass 44 | end 45 | 46 | def permit! 47 | each_pair do |key, value| 48 | convert_hashes_to_parameters(key, value) 49 | self[key].permit! if self[key].respond_to? :permit! 50 | end 51 | 52 | @permitted = true 53 | self 54 | end 55 | 56 | def require(key) 57 | self[key].presence || raise(ActionController::ParameterMissing.new(key)) 58 | end 59 | 60 | alias :required :require 61 | 62 | def permit(*filters) 63 | params = self.class.new 64 | filters.each do |filter| 65 | rule = filter.is_a?(Hash) ? filter : default_rule(filter) 66 | if rule.values.one? && rule.values.first.is_a?(Class) 67 | permitted_scalar_filter(params, rule.keys.first, rule.values.first) 68 | else 69 | apply_filter(params, rule) 70 | end 71 | end 72 | 73 | unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters 74 | 75 | params.permit! 76 | end 77 | 78 | def [](key) 79 | convert_hashes_to_parameters(key, super) 80 | end 81 | 82 | def fetch(key, *args) 83 | convert_hashes_to_parameters(key, super) 84 | rescue KeyError, IndexError 85 | raise ActionController::ParameterMissing.new(key) 86 | end 87 | 88 | def slice(*keys) 89 | self.class.new(super).tap do |new_instance| 90 | new_instance.instance_variable_set :@permitted, @permitted 91 | end 92 | end 93 | 94 | def dup 95 | self.class.new(self).tap do |duplicate| 96 | duplicate.default = default 97 | duplicate.instance_variable_set :@permitted, @permitted 98 | end 99 | end 100 | 101 | protected 102 | def convert_value(value) 103 | if value.class == Hash 104 | self.class.new_from_hash_copying_default(value) 105 | elsif value.is_a?(Array) 106 | value.dup.replace(value.map { |e| convert_value(e) }) 107 | else 108 | value 109 | end 110 | end 111 | 112 | private 113 | 114 | def convert_hashes_to_parameters(key, value) 115 | if value.is_a?(Parameters) || !value.is_a?(Hash) 116 | value 117 | else 118 | # Convert to Parameters on first access 119 | self[key] = self.class.new(value) 120 | end 121 | end 122 | 123 | def permitted_scalar?(value, klass) 124 | value.is_a?(klass) 125 | end 126 | 127 | def array_of_permitted_scalars?(value,klass) 128 | if value.is_a?(Array) 129 | value.all? {|element| permitted_scalar?(element,klass)} 130 | end 131 | end 132 | 133 | def permitted_scalar_filter(params, key, klass) 134 | if has_key?(key) && permitted_scalar?(self[key],klass) 135 | params[key] = self[key] 136 | end 137 | 138 | keys.grep(/\A#{Regexp.escape(key.to_s)}\(\d+[if]?\)\z/).each do |key| 139 | if permitted_scalar?(self[key],klass) 140 | params[key] = self[key] 141 | end 142 | end 143 | end 144 | 145 | def array_of_permitted_scalars_filter(params, key, rule) 146 | raise ArgumentError unless rule.one? 147 | if has_key?(key) && array_of_permitted_scalars?(self[key],rule.first) 148 | params[key] = self[key] 149 | end 150 | end 151 | 152 | def apply_filter(params, filter) 153 | filter = filter.with_indifferent_access 154 | 155 | # Slicing filters out non-declared keys. 156 | slice(*filter.keys).each do |key, value| 157 | 158 | rule = filter[key] 159 | 160 | # Declaration {:favorite_numbers => [Numeric]} 161 | if rule.is_a?(Array) && rule.first.is_a?(Class) 162 | array_of_permitted_scalars_filter(params, key, rule) 163 | # Declaration {:favorite_number => Numeric} or :uuid [=> String] 164 | elsif rule.is_a?(Class) || rule.is_a?(Module) 165 | permitted_scalar_filter(params, key, rule) 166 | else 167 | # Declaration {:user => :name} or {:user => [:name, :age, {:address => ...}]} 168 | raise ArgumentError if rule.empty? 169 | params[key] = each_element(value) do |element| 170 | if element.is_a?(Hash) 171 | element = self.class.new(element) unless element.respond_to?(:permit) 172 | element.klass = key.camelize.constantize rescue String 173 | element.permit(*Array.wrap(rule)) 174 | end 175 | end 176 | end 177 | end 178 | end 179 | 180 | def default_rule(filter) 181 | if @klass.respond_to?(:columns) && (type = @klass.columns.find { |attr| attr.name == filter.to_s }) 182 | {filter => type.klass} 183 | else 184 | {filter => String} 185 | end 186 | end 187 | 188 | def each_element(value) 189 | if value.is_a?(Array) 190 | value.map { |el| yield el }.compact 191 | # fields_for on an array of records uses numeric hash keys. 192 | elsif value.is_a?(Hash) && value.keys.all? { |k| k =~ /\A-?\d+\z/ } 193 | hash = value.class.new 194 | value.each { |k,v| hash[k] = yield v } 195 | hash 196 | else 197 | yield value 198 | end 199 | end 200 | 201 | def unpermitted_parameters!(params) 202 | return unless self.class.action_on_unpermitted_parameters 203 | 204 | unpermitted_keys = unpermitted_keys(params) 205 | 206 | if unpermitted_keys.any? 207 | case self.class.action_on_unpermitted_parameters 208 | when :log 209 | ActionController::Base.logger.debug "Unpermitted parameters: #{unpermitted_keys.join(", ")}" 210 | when :raise 211 | raise ActionController::UnpermittedParameters.new(unpermitted_keys) 212 | end 213 | end 214 | end 215 | 216 | def unpermitted_keys(params) 217 | self.keys - params.keys - NEVER_UNPERMITTED_PARAMS 218 | end 219 | end 220 | 221 | module StrongParameters 222 | extend ActiveSupport::Concern 223 | 224 | included do 225 | rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception| 226 | render :text => "Required parameter missing: #{parameter_missing_exception.param}", :status => :bad_request 227 | end 228 | end 229 | 230 | def params 231 | @_params ||= Parameters.new(request.parameters) 232 | end 233 | 234 | def params=(val) 235 | @_params = val.is_a?(Hash) ? Parameters.new(val) : val 236 | end 237 | end 238 | end 239 | 240 | ActionController::Base.send :include, ActionController::StrongParameters 241 | -------------------------------------------------------------------------------- /test/parameters_permit_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'action_controller/parameters' 3 | require 'action_dispatch/http/upload' 4 | 5 | class NestedParametersTest < ActiveSupport::TestCase 6 | def assert_filtered_out(params, key) 7 | assert !params.has_key?(key), "key #{key.inspect} has not been filtered out" 8 | end 9 | 10 | # 11 | # --- Basic interface -------------------------------------------------------- 12 | # 13 | 14 | # --- nothing ---------------------------------------------------------------- 15 | 16 | test 'if nothing is permitted, the hash becomes empty' do 17 | params = ActionController::Parameters.new(:id => '1234') 18 | permitted = params.permit 19 | permitted.permitted? 20 | permitted.empty? 21 | end 22 | 23 | # --- key -------------------------------------------------------------------- 24 | 25 | test 'key: unexpected types are filtered out' do 26 | params = ActionController::Parameters.new(:id => 1234, :token => 0) 27 | permitted = params.permit(:id => Numeric, :token => String) 28 | assert_equal 1234, permitted[:id] 29 | assert_filtered_out permitted, :token 30 | end 31 | 32 | test 'key: unknown keys are filtered out' do 33 | params = ActionController::Parameters.new(:id => '1234', :injected => 'injected') 34 | permitted = params.permit(:id) 35 | assert_equal '1234', permitted[:id] 36 | assert_filtered_out permitted, :injected 37 | end 38 | 39 | test 'key: arrays are filtered out' do 40 | [[], [1], ['1']].each do |array| 41 | params = ActionController::Parameters.new(:id => array) 42 | permitted = params.permit(:id) 43 | assert_filtered_out permitted, :id 44 | 45 | %w(i f).each do |suffix| 46 | params = ActionController::Parameters.new("foo(000#{suffix})" => array) 47 | permitted = params.permit(:foo) 48 | assert_filtered_out permitted, "foo(000#{suffix})" 49 | end 50 | end 51 | end 52 | 53 | test 'key: hashes are filtered out' do 54 | [{}, {:foo => 1}, {:foo => 'bar'}].each do |hash| 55 | params = ActionController::Parameters.new(:id => hash) 56 | permitted = params.permit(:id) 57 | assert_filtered_out permitted, :id 58 | 59 | %w(i f).each do |suffix| 60 | params = ActionController::Parameters.new("foo(000#{suffix})" => hash) 61 | permitted = params.permit(:foo) 62 | assert_filtered_out permitted, "foo(000#{suffix})" 63 | end 64 | end 65 | end 66 | 67 | test 'key: non-permitted scalar values are filtered out' do 68 | params = ActionController::Parameters.new(:id => Object.new) 69 | permitted = params.permit(:id) 70 | assert_filtered_out permitted, :id 71 | 72 | %w(i f).each do |suffix| 73 | params = ActionController::Parameters.new("foo(000#{suffix})" => Object.new) 74 | permitted = params.permit(:foo) 75 | assert_filtered_out permitted, "foo(000#{suffix})" 76 | end 77 | end 78 | 79 | test 'key: Boolean matches only true and false' do 80 | params = ActionController::Parameters.new(:happy => true) 81 | permitted = params.permit(:happy => Boolean) 82 | assert_equal true, permitted[:happy] 83 | 84 | params = ActionController::Parameters.new(:happy => false) 85 | permitted = params.permit(:happy => Boolean) 86 | assert_equal false, permitted[:happy] 87 | 88 | params = ActionController::Parameters.new(:happy => Object.new) 89 | permitted = params.permit(:happy => Boolean) 90 | assert_filtered_out permitted, :happy 91 | 92 | end 93 | 94 | test 'key: it is not assigned if not present in params' do 95 | params = ActionController::Parameters.new(:name => 'Joe') 96 | permitted = params.permit(:id) 97 | assert !permitted.has_key?(:id) 98 | end 99 | 100 | # 101 | # --- Nesting ---------------------------------------------------------------- 102 | # 103 | 104 | test "permitted nested parameters" do 105 | params = ActionController::Parameters.new({ 106 | :book => { 107 | :title => "Romeo and Juliet", 108 | :authors => [{ 109 | :name => "William Shakespeare", 110 | :born => "1564-04-26" 111 | }, { 112 | :name => "Christopher Marlowe" 113 | }, { 114 | :name => %w(malicious injected names) 115 | }], 116 | :details => { 117 | :pages => 200, 118 | :genre => "Tragedy" 119 | } 120 | }, 121 | :magazine => "Mjallo!" 122 | }) 123 | 124 | permitted = params.permit :book => [ :title, { :authors => [ :name ] }, { :details => {:pages => Numeric} } ] 125 | 126 | assert permitted.permitted? 127 | assert_equal "Romeo and Juliet", permitted[:book][:title] 128 | assert_equal "William Shakespeare", permitted[:book][:authors][0][:name] 129 | assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name] 130 | assert_equal 200, permitted[:book][:details][:pages] 131 | 132 | assert_filtered_out permitted[:book][:authors][2], :name 133 | 134 | assert_filtered_out permitted, :magazine 135 | assert_filtered_out permitted[:book][:details], :genre 136 | assert_filtered_out permitted[:book][:authors][0], :born 137 | end 138 | 139 | test "permitted nested parameters with a string or a symbol as a key" do 140 | params = ActionController::Parameters.new({ 141 | :book => { 142 | 'authors' => [ 143 | { :name => "William Shakespeare", :born => "1564-04-26" }, 144 | { :name => "Christopher Marlowe" } 145 | ] 146 | } 147 | }) 148 | 149 | permitted = params.permit :book => [ { 'authors' => [ :name ] } ] 150 | 151 | assert_equal "William Shakespeare", permitted[:book]['authors'][0][:name] 152 | assert_equal "William Shakespeare", permitted[:book][:authors][0][:name] 153 | assert_equal "Christopher Marlowe", permitted[:book]['authors'][1][:name] 154 | assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name] 155 | 156 | permitted = params.permit :book => [ { :authors => [ :name ] } ] 157 | 158 | assert_equal "William Shakespeare", permitted[:book]['authors'][0][:name] 159 | assert_equal "William Shakespeare", permitted[:book][:authors][0][:name] 160 | assert_equal "Christopher Marlowe", permitted[:book]['authors'][1][:name] 161 | assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name] 162 | end 163 | 164 | test "nested arrays with strings" do 165 | params = ActionController::Parameters.new({ 166 | :book => { 167 | :genres => ["Tragedy"] 168 | } 169 | }) 170 | 171 | permitted = params.permit :book => {:genres => [String]} 172 | assert_equal ["Tragedy"], permitted[:book][:genres] 173 | end 174 | 175 | test "permit may specify symbols or strings" do 176 | params = ActionController::Parameters.new({ 177 | :book => { 178 | :title => "Romeo and Juliet", 179 | :author => "William Shakespeare" 180 | }, 181 | :magazine => "Shakespeare Today" 182 | }) 183 | 184 | permitted = params.permit({ :book => ["title", :author] }, "magazine") 185 | assert_equal "Romeo and Juliet", permitted[:book][:title] 186 | assert_equal "William Shakespeare", permitted[:book][:author] 187 | assert_equal "Shakespeare Today", permitted[:magazine] 188 | end 189 | 190 | test "nested array with strings that should be hashes" do 191 | params = ActionController::Parameters.new({ 192 | :book => { 193 | :genres => ["Tragedy"] 194 | } 195 | }) 196 | 197 | permitted = params.permit :book => { :genres => :type } 198 | assert permitted[:book][:genres].empty? 199 | end 200 | 201 | test "nested array with strings that should be hashes and additional values" do 202 | params = ActionController::Parameters.new({ 203 | :book => { 204 | :title => "Romeo and Juliet", 205 | :genres => ["Tragedy"] 206 | } 207 | }) 208 | 209 | permitted = params.permit :book => [ :title, { :genres => :type } ] 210 | assert_equal "Romeo and Juliet", permitted[:book][:title] 211 | assert permitted[:book][:genres].empty? 212 | end 213 | 214 | test "nested string that should be a hash" do 215 | params = ActionController::Parameters.new({ 216 | :book => { 217 | :genre => "Tragedy" 218 | } 219 | }) 220 | 221 | permitted = params.permit :book => { :genre => :type } 222 | assert_nil permitted[:book][:genre] 223 | end 224 | 225 | test "fields_for_style_nested_params" do 226 | params = ActionController::Parameters.new({ 227 | :book => { 228 | :authors_attributes => { 229 | :'0' => { :name => 'William Shakespeare', :age_of_death => '52' }, 230 | :'1' => { :name => 'Unattributed Assistant' }, 231 | :'2' => { :name => %w(injected names)} 232 | } 233 | } 234 | }) 235 | permitted = params.permit :book => { :authors_attributes => [ :name ] } 236 | 237 | assert_not_nil permitted[:book][:authors_attributes]['0'] 238 | assert_not_nil permitted[:book][:authors_attributes]['1'] 239 | assert permitted[:book][:authors_attributes]['2'].empty? 240 | assert_equal 'William Shakespeare', permitted[:book][:authors_attributes]['0'][:name] 241 | assert_equal 'Unattributed Assistant', permitted[:book][:authors_attributes]['1'][:name] 242 | 243 | assert_filtered_out permitted[:book][:authors_attributes]['0'], :age_of_death 244 | end 245 | 246 | test "fields_for_style_nested_params with negative numbers" do 247 | params = ActionController::Parameters.new({ 248 | :book => { 249 | :authors_attributes => { 250 | :'-1' => { :name => 'William Shakespeare', :age_of_death => '52' }, 251 | :'-2' => { :name => 'Unattributed Assistant' } 252 | } 253 | } 254 | }) 255 | permitted = params.permit :book => { :authors_attributes => [:name] } 256 | 257 | assert_not_nil permitted[:book][:authors_attributes]['-1'] 258 | assert_not_nil permitted[:book][:authors_attributes]['-2'] 259 | assert_equal 'William Shakespeare', permitted[:book][:authors_attributes]['-1'][:name] 260 | assert_equal 'Unattributed Assistant', permitted[:book][:authors_attributes]['-2'][:name] 261 | 262 | assert_filtered_out permitted[:book][:authors_attributes]['-1'], :age_of_death 263 | end 264 | end 265 | --------------------------------------------------------------------------------