trusted_keys 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE +22 -0
- data/README.md +35 -0
- data/Rakefile +18 -0
- data/example/controller.rb +120 -0
- data/lib/trusted_keys.rb +54 -0
- data/lib/trusted_keys/error/not_trusted.rb +40 -0
- data/lib/trusted_keys/error/usage.rb +16 -0
- data/lib/trusted_keys/trustable.rb +114 -0
- data/lib/trusted_keys/version.rb +3 -0
- data/spec/minitest_helper.rb +8 -0
- data/spec/trusted_keys/trustable_spec.rb +274 -0
- data/spec/trusted_keys_spec.rb +186 -0
- data/trusted_keys.gemspec +25 -0
- metadata +144 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Anders Törnqvist
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# TrustedKeys
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'trusted_keys'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install trusted_keys
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Other mass assignment protection gems for in controller
|
24
|
+
* https://github.com/topdan/param_accessible
|
25
|
+
* https://github.com/ryanb/trusted-params
|
26
|
+
* https://github.com/elabs/trusted_attributes
|
27
|
+
* https://github.com/rails/strong_parameters
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
1. Fork it
|
32
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
33
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
34
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
35
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
Rake::TestTask.new(:spec) do |t|
|
6
|
+
t.libs.push "lib"
|
7
|
+
t.libs.push "spec"
|
8
|
+
t.test_files = FileList['spec/**/*_spec.rb']
|
9
|
+
t.verbose = true
|
10
|
+
end
|
11
|
+
|
12
|
+
task :test => :spec
|
13
|
+
task :default => :test
|
14
|
+
|
15
|
+
desc "open console (require 'trusted_keys')"
|
16
|
+
task :c do
|
17
|
+
system "irb -I lib -r trusted_keys"
|
18
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
$LOAD_PATH << File.dirname(__FILE__) + "../../lib"
|
3
|
+
require 'trusted_keys'
|
4
|
+
|
5
|
+
# An exception is raised if untrusted keys are submitted when Rails.env
|
6
|
+
# equals 'development' or 'test'. When Rail.env is something else, it will
|
7
|
+
# silently remove the untrusted keys.
|
8
|
+
Rails.env = "development"
|
9
|
+
Rails.env = "test"
|
10
|
+
|
11
|
+
#Rails.env = "other"
|
12
|
+
|
13
|
+
module Trusted
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
include TrustedKeys
|
16
|
+
extend self
|
17
|
+
|
18
|
+
def params
|
19
|
+
{ "utf8"=>"✓", "authenticity_token"=>"zIE/nwLd",
|
20
|
+
"event"=> { "title"=>"",
|
21
|
+
"location"=>"not trusted",
|
22
|
+
"start(1i)"=>"2012", "start(2i)"=>"3", "start(3i)"=>"5",
|
23
|
+
"description"=>"",
|
24
|
+
"attendees"=>"",
|
25
|
+
"slug_attributes" => { "slug" => "dddd",
|
26
|
+
"dangerous" => "not trusted" },
|
27
|
+
"comment_attributes" => { "body" => "I am body",
|
28
|
+
"evil" => "not trusted",
|
29
|
+
"slug3_attributes" => { "slug3" => "is cool",
|
30
|
+
"cruel" => "not trusted" } } } }
|
31
|
+
end
|
32
|
+
|
33
|
+
def puts
|
34
|
+
p trusted_attributes
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
puts "params => "
|
39
|
+
p Trusted.params
|
40
|
+
|
41
|
+
class ApplicationController
|
42
|
+
include Trusted
|
43
|
+
end
|
44
|
+
|
45
|
+
#ApplicationController.new.trusted_attributes # raises NoMethodError
|
46
|
+
#ApplicationController.new.puts # raises UsageError
|
47
|
+
|
48
|
+
class ApplicationController
|
49
|
+
trust :utf8
|
50
|
+
end
|
51
|
+
ApplicationController.new.puts # {"utf8"=>"✓" }
|
52
|
+
|
53
|
+
class ApplicationControllerX
|
54
|
+
include Trusted
|
55
|
+
trust :event, :utf8
|
56
|
+
end
|
57
|
+
Rails.env = "fff"
|
58
|
+
ApplicationControllerX.new.puts # {"utf8"=>"✓" "event" => "" }
|
59
|
+
|
60
|
+
# raises TrustedKeys::NotTrustedError if test och development environment.
|
61
|
+
#Rails.env = "test"
|
62
|
+
ApplicationControllerX.new.puts
|
63
|
+
|
64
|
+
|
65
|
+
#Rails.env = "test"
|
66
|
+
class ApplicationController2
|
67
|
+
include Trusted
|
68
|
+
trust :title, :start, :description, :attendees, :slug_attributes, for: :event
|
69
|
+
end
|
70
|
+
ApplicationController2.new.puts
|
71
|
+
# =>
|
72
|
+
# { "title"=>"", "start(1i)"=>"2012", "start(2i)"=>"3", "start(3i)"=>"5",
|
73
|
+
# "description"=>"", "attendees"=>"", "slug_attributes"=>""}
|
74
|
+
|
75
|
+
#Rails.env = "test"
|
76
|
+
class ApplicationController3
|
77
|
+
include Trusted
|
78
|
+
trust :title, :start, :description, :attendees, :slug_attributes,
|
79
|
+
:location, :comment_attributes, for: :event
|
80
|
+
trust :slug, for: "event.slug_attributes"
|
81
|
+
end
|
82
|
+
ApplicationController3.new.puts
|
83
|
+
# =>
|
84
|
+
# { "title"=>"", "start(1i)"=>"2012", "start(2i)"=>"3", "start(3i)"=>"5",
|
85
|
+
# "description"=>"", "attendees"=>"",
|
86
|
+
# "slug_attributes"=>{ "slug"=>"dddd"}}
|
87
|
+
|
88
|
+
#Rails.env = "development"
|
89
|
+
class ApplicationController4
|
90
|
+
include Trusted
|
91
|
+
trust :title, :start, :description, :attendees, :slug_attributes,
|
92
|
+
:comment_attributes, :location, for: :event
|
93
|
+
trust :slug, for: "event.slug_attributes"
|
94
|
+
trust "body", :slug3_attributes, for: "event.comment_attributes"
|
95
|
+
end
|
96
|
+
ApplicationController4.new.puts
|
97
|
+
# =>
|
98
|
+
# { "title"=>"", "start(1i)"=>"2012", "start(2i)"=>"3", "start(3i)"=>"5",
|
99
|
+
# "description"=>"", "attendees"=>"", "location" => "not trusted"
|
100
|
+
# "slug_attributes"=>{"slug"=>"dddd"},
|
101
|
+
# "comment_attributes"=>{ "body"=>"I am body",
|
102
|
+
# "slug3_attributes"=>""}}
|
103
|
+
#
|
104
|
+
|
105
|
+
#Rails.env = "test"
|
106
|
+
class ApplicationController5
|
107
|
+
include Trusted
|
108
|
+
trust :title, :start, :description, :attendees, :slug_attributes,
|
109
|
+
:comment_attributes, for: :event
|
110
|
+
trust :slug, for: "event.slug_attributes"
|
111
|
+
trust "body", :slug3_attributes, for: "event.comment_attributes"
|
112
|
+
trust "slug3", for: "event.comment_attributes.slug3_attributes"
|
113
|
+
end
|
114
|
+
ApplicationController5.new.puts
|
115
|
+
# =>
|
116
|
+
# { "title"=>"", "start(1i)"=>"2012", "start(2i)"=>"3", "start(3i)"=>"5",
|
117
|
+
# "description"=>"", "attendees"=>"",
|
118
|
+
# "slug_attributes"=>{"slug"=>"dddd"},
|
119
|
+
# "comment_attributes"=>{ "body"=>"I am body",
|
120
|
+
# "slug3_attributes"=>{"slug3"=>"is cool"}}}
|
data/lib/trusted_keys.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'trusted_keys/version'
|
2
|
+
require 'rails'
|
3
|
+
require 'trusted_keys/trustable'
|
4
|
+
require 'trusted_keys/error/usage'
|
5
|
+
require 'trusted_keys/error/not_trusted'
|
6
|
+
|
7
|
+
module TrustedKeys
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def trust(*args)
|
12
|
+
options = args.extract_options!
|
13
|
+
scope = options.fetch(:for, "").to_s.split '.'
|
14
|
+
env = options[:env] || Rails.env
|
15
|
+
nested = options.fetch(:nested, true)
|
16
|
+
|
17
|
+
klass = Class.new do
|
18
|
+
include Trustable
|
19
|
+
end
|
20
|
+
|
21
|
+
@_trusted_keys ||= []
|
22
|
+
@_trusted_keys << klass.new(:scope => scope,
|
23
|
+
:trusted_keys => @_trusted_keys,
|
24
|
+
:untrusted => Error::NotTrusted.new(env),
|
25
|
+
:nested => nested,
|
26
|
+
:keys => args)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def trusted_attributes
|
33
|
+
trusted_keys = self.class.instance_variable_get("@_trusted_keys")
|
34
|
+
raise Error::Usage.new(params) unless trusted_keys
|
35
|
+
|
36
|
+
sorted_keys = trusted_keys.sort
|
37
|
+
|
38
|
+
attributes = sorted_keys.first.attributes(params)
|
39
|
+
|
40
|
+
sorted_keys.drop(1).each do |trusted|
|
41
|
+
current_attributes = trusted.on_scope(attributes)
|
42
|
+
|
43
|
+
if trusted.parent_nested?(current_attributes)
|
44
|
+
current_attributes.each do |key, hash|
|
45
|
+
hash[trusted.key] = trusted.attributes(hash)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
current_attributes[trusted.key] = trusted.attributes(current_attributes)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
attributes
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module TrustedKeys
|
2
|
+
module Error
|
3
|
+
class NotTrusted < StandardError
|
4
|
+
def initialize(env)
|
5
|
+
@keys = {}
|
6
|
+
@production = not(env.development? or env.test?)
|
7
|
+
end
|
8
|
+
|
9
|
+
def message
|
10
|
+
usage = @keys.map do |scope, keys|
|
11
|
+
"`trust #{keys}, for: '#{scope}'`"
|
12
|
+
end.join("\n")
|
13
|
+
|
14
|
+
"\n\nError: There are keys in the params hash that are not trusted. " +
|
15
|
+
"Set them as trusted with:\n#{usage} at the top of the controller."
|
16
|
+
end
|
17
|
+
|
18
|
+
def keys(options)
|
19
|
+
scope = options.fetch(:scope)
|
20
|
+
key = options.fetch(:key)
|
21
|
+
keys = options.fetch(:keys).flatten.map do |key|
|
22
|
+
":#{key.to_s.sub(/\(\di\)/, '')}"
|
23
|
+
end.uniq.join(', ')
|
24
|
+
|
25
|
+
scope_key = (scope + [key]).compact.join('.')
|
26
|
+
@keys[scope_key] = keys unless scope_key.empty?
|
27
|
+
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def present?
|
32
|
+
if @production
|
33
|
+
false
|
34
|
+
else
|
35
|
+
not @keys.empty?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module TrustedKeys
|
2
|
+
module Error
|
3
|
+
class Usage < StandardError
|
4
|
+
def initialize(params)
|
5
|
+
@params = params
|
6
|
+
end
|
7
|
+
|
8
|
+
def message
|
9
|
+
"\n\nparams => #{@params.inspect}\n\n" +
|
10
|
+
"Error: Before using `trusted_attributes` you must set the " +
|
11
|
+
"trusted keys in the controller, for examples: `trust :post` " +
|
12
|
+
"or `trust :title, :body, for: 'post'`"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
|
3
|
+
module TrustedKeys
|
4
|
+
module Trustable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include ActiveModel::MassAssignmentSecurity
|
7
|
+
|
8
|
+
def initialize(options)
|
9
|
+
@scope = options.fetch(:scope)
|
10
|
+
@trusted_keys = options.fetch(:trusted_keys)
|
11
|
+
@untrusted = options.fetch(:untrusted)
|
12
|
+
@nested = options.fetch(:nested, true)
|
13
|
+
keys = options.fetch(:keys)
|
14
|
+
|
15
|
+
if nested?({})
|
16
|
+
keys << "_destroy"
|
17
|
+
keys << "id"
|
18
|
+
end
|
19
|
+
|
20
|
+
self.class.send("attr_accessible", *keys)
|
21
|
+
end
|
22
|
+
|
23
|
+
def attributes(params)
|
24
|
+
params = params[key.to_sym] || params[key.to_s] if key
|
25
|
+
|
26
|
+
if nested?(params)
|
27
|
+
{}.tap do |hash|
|
28
|
+
params.each do |key, nested_hash|
|
29
|
+
hash[key] = sanitize(nested_hash)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
elsif params.is_a?(Array)
|
33
|
+
[].tap do |array|
|
34
|
+
params.each do |hash|
|
35
|
+
array << sanitize(hash)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
else
|
39
|
+
sanitize(params)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_scope(attributes)
|
44
|
+
@scope.slice(1, @scope.size - 2).reduce(attributes) do |attributes, key|
|
45
|
+
attributes[key]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def key; @key ||= @scope.last; end
|
50
|
+
def <=> (other); level <=> other.level; end
|
51
|
+
def level; @scope.size; end
|
52
|
+
|
53
|
+
def parent_nested?(params)
|
54
|
+
if level > 1
|
55
|
+
parent and parent.nested?(params)
|
56
|
+
else
|
57
|
+
false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def nested?(params)
|
64
|
+
if params.is_a?(Hash)
|
65
|
+
@nested and key.to_s[/\A.+_attributes\Z/]
|
66
|
+
else
|
67
|
+
false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def parent
|
74
|
+
key = @scope.at(@scope.size - 2)
|
75
|
+
|
76
|
+
@trusted_keys.select do |trusted|
|
77
|
+
trusted.key.to_s == key and trusted.level == (level - 1)
|
78
|
+
end.first
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def sanitize(params)
|
83
|
+
result = sanitize_for_mass_assignment(params)
|
84
|
+
|
85
|
+
keys = params.keys.map(&:to_s) - result.keys.map(&:to_s)
|
86
|
+
|
87
|
+
unless keys.empty?
|
88
|
+
untrusted = @untrusted.keys(:scope => @scope,
|
89
|
+
:key => nil,
|
90
|
+
:keys => keys)
|
91
|
+
raise untrusted if untrusted.present?
|
92
|
+
end
|
93
|
+
|
94
|
+
remove_untrusted_keys(result)
|
95
|
+
end
|
96
|
+
|
97
|
+
def remove_untrusted_keys(attributes)
|
98
|
+
trusted_keys = @trusted_keys.select do |trusted|
|
99
|
+
trusted.level == (level + 1)
|
100
|
+
end.map { |trusted| trusted.key.to_s }
|
101
|
+
|
102
|
+
attributes.each do |key, value|
|
103
|
+
if value.is_a?(Hash) and !trusted_keys.include?(key.to_s)
|
104
|
+
attributes[key] = ""
|
105
|
+
@untrusted.keys :scope => @scope, :key => key, :keys => value.keys
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
raise @untrusted if @untrusted.present?
|
110
|
+
|
111
|
+
HashWithIndifferentAccess.new(attributes)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
NotTrusted = TrustedKeys::Error::NotTrusted
|
4
|
+
Usage = TrustedKeys::Error::Usage
|
5
|
+
|
6
|
+
describe TrustedKeys::Trustable do
|
7
|
+
let(:test) { OpenStruct.new(:test? => true, :development? => false) }
|
8
|
+
|
9
|
+
let(:klass) do
|
10
|
+
Class.new do
|
11
|
+
include TrustedKeys::Trustable
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:params) do
|
16
|
+
{ "email" => "anders@email.com",
|
17
|
+
:controller => "events",
|
18
|
+
:collection => [ { "one" => "ok 1",
|
19
|
+
"two" => "remove me"},
|
20
|
+
{ "one" => "ok 2",
|
21
|
+
"two" => "remove me 2"} ],
|
22
|
+
:time => { "start_time(1i)"=>"2012",
|
23
|
+
"start_time(2i)"=>"3",
|
24
|
+
"start_time(3i)"=>"14" },
|
25
|
+
:events => {
|
26
|
+
:nested_attributes => { "0" => { "_destroy"=>"false",
|
27
|
+
"dangerous" => "remove me",
|
28
|
+
"start"=>"2012" },
|
29
|
+
"new_1331711737056" => { "_destroy"=>"false",
|
30
|
+
"start"=>"2012" } } },
|
31
|
+
:password => "secret",
|
32
|
+
:post => { :body => "I am a body",
|
33
|
+
:title => "This is my title",
|
34
|
+
:comments => { 'body' => 'My body',
|
35
|
+
:email => "an email",
|
36
|
+
:author => { :name => "anders" } } } }
|
37
|
+
end
|
38
|
+
|
39
|
+
def options(options_hash)
|
40
|
+
{ :scope => [],
|
41
|
+
:trusted_keys => [],
|
42
|
+
:untrusted => NotTrusted.new(OpenStruct.new(:test? => false,
|
43
|
+
:development? => false)),
|
44
|
+
:keys => [:email]
|
45
|
+
}.merge(options_hash)
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "attributes inside a collection" do
|
49
|
+
it "returns trusted attributes" do
|
50
|
+
t = klass.new(options(:scope => [:collection],
|
51
|
+
:keys => ["one"]))
|
52
|
+
expected = [{ "one" => "ok 1" }, { "one" => "ok 2" }]
|
53
|
+
|
54
|
+
t.attributes(params).must_equal(expected)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "datetime selects" do
|
59
|
+
it "return all datetime attributes" do
|
60
|
+
t = klass.new(options(:scope => [:time],
|
61
|
+
:trusted_keys => [],
|
62
|
+
:keys => [:start_time]))
|
63
|
+
|
64
|
+
expected = { "start_time(1i)"=>"2012",
|
65
|
+
"start_time(2i)"=>"3",
|
66
|
+
"start_time(3i)"=>"14" }
|
67
|
+
t.attributes(params).must_equal(expected)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "nested attributes" do
|
72
|
+
it "returns nested hash" do
|
73
|
+
t = klass.new(options(:scope => [:events, :nested_attributes],
|
74
|
+
:trusted_keys => [],
|
75
|
+
:keys => [ "start" ]))
|
76
|
+
|
77
|
+
expected = { "0" => { "_destroy"=>"false",
|
78
|
+
"start"=>"2012" },
|
79
|
+
"new_1331711737056" => { "_destroy"=>"false",
|
80
|
+
"start"=>"2012" } }
|
81
|
+
t.attributes(params[:events]).must_equal(expected)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "#attributes" do
|
86
|
+
describe "when in test or development environment" do
|
87
|
+
context "level 0" do
|
88
|
+
it "doesn't raise an exception if all params isn't trusted" do
|
89
|
+
t = klass.new(options(:scope => [],
|
90
|
+
:trusted_keys => [],
|
91
|
+
:untrusted => NotTrusted.new(test),
|
92
|
+
:keys => [:email]))
|
93
|
+
t.attributes(params).must_equal("email" => "anders@email.com")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "level 1" do
|
98
|
+
it "raises an exception if all params isn't trusted" do
|
99
|
+
t = klass.new(options(:scope => [:post],
|
100
|
+
:trusted_keys => [],
|
101
|
+
:untrusted => NotTrusted.new(test),
|
102
|
+
:keys => [:body]))
|
103
|
+
proc { t.attributes(params) }.must_raise NotTrusted
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "level 2" do
|
108
|
+
it "raises an exception if all params isn't trusted" do
|
109
|
+
t = klass.new(options(:scope => [:post, :comments],
|
110
|
+
:trusted_keys => [],
|
111
|
+
:untrusted => NotTrusted.new(test),
|
112
|
+
:keys => [:body]))
|
113
|
+
proc { t.attributes(params[:post]) }.must_raise NotTrusted
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context "hash on next level isn't trusted" do
|
118
|
+
it "raises an exception" do
|
119
|
+
t = klass.new(options(:scope => [],
|
120
|
+
:trusted_keys => [],
|
121
|
+
:untrusted => NotTrusted.new(test),
|
122
|
+
:keys => [:email, :password, :post]))
|
123
|
+
proc { t.attributes(params) }.must_raise NotTrusted
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context "level 0" do
|
129
|
+
it "returns trusted params for level 0" do
|
130
|
+
t = klass.new(options(:scope => [],
|
131
|
+
:trusted_keys => [],
|
132
|
+
:keys => [:email]))
|
133
|
+
t.attributes(params).must_equal("email" => 'anders@email.com')
|
134
|
+
end
|
135
|
+
|
136
|
+
it %(transform hash on next level to an empty string if its keys
|
137
|
+
aren't trusted) do
|
138
|
+
t = klass.new(options(:scope => [],
|
139
|
+
:trusted_keys => [],
|
140
|
+
:keys => [:post]))
|
141
|
+
t.attributes(params).must_equal("post" => '')
|
142
|
+
end
|
143
|
+
|
144
|
+
it "returns the hash on next level if it has trusted keys" do
|
145
|
+
level1 = klass.new(options(:scope => [:post],
|
146
|
+
:trusted_keys => [],
|
147
|
+
:keys => [:body]))
|
148
|
+
t = klass.new(options(:scope => [],
|
149
|
+
:trusted_keys => [level1],
|
150
|
+
:keys => [:post]))
|
151
|
+
|
152
|
+
expected = {"post"=> { "body"=>"I am a body",
|
153
|
+
"title"=>"This is my title",
|
154
|
+
"comments"=> { "body"=>"My body",
|
155
|
+
"email"=>"an email",
|
156
|
+
"author"=>{"name"=>"anders"}} } }
|
157
|
+
|
158
|
+
t.attributes(params).must_equal(expected)
|
159
|
+
level1.attributes(params).must_equal("body" => "I am a body")
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context "level 1" do
|
164
|
+
it "returns trusted params for level 1" do
|
165
|
+
t = klass.new(options(:scope => [:post],
|
166
|
+
:trusted_keys => [],
|
167
|
+
:keys => [:body]))
|
168
|
+
t.attributes(params).must_equal("body" => 'I am a body')
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context "level 2" do
|
173
|
+
it "returns trusted params for level 2" do
|
174
|
+
t = klass.new(options(:scope => [:post, :comments],
|
175
|
+
:trusted_keys => [],
|
176
|
+
:keys => [:body]))
|
177
|
+
t.attributes(params[:post]).must_equal("body" => 'My body')
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe "#on_scope" do
|
183
|
+
context "level 2" do
|
184
|
+
it "returns the hash for that level" do
|
185
|
+
t = klass.new(options(:scope => [:post, :comments],
|
186
|
+
:trusted_keys => [],
|
187
|
+
:keys => [:body]))
|
188
|
+
t.on_scope(params[:post]).must_equal(params[:post])
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
context "level 3" do
|
193
|
+
it "returns the hash for that level" do
|
194
|
+
t = klass.new(options(:scope => [:post, :comments, :author],
|
195
|
+
:trusted_keys => [],
|
196
|
+
:keys => [:name]))
|
197
|
+
t.on_scope(params[:post]).must_equal(params[:post][:comments])
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
context "level 1" do
|
202
|
+
it "not applicable" do
|
203
|
+
t = klass.new(options(:scope => [:post],
|
204
|
+
:trusted_keys => [],
|
205
|
+
:keys => [:body]))
|
206
|
+
proc { t.on_scope(params) }.must_raise NoMethodError
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context "level 0" do
|
211
|
+
it "not applicable" do
|
212
|
+
t = klass.new(options(:scope => [],
|
213
|
+
:trusted_keys => [],
|
214
|
+
:keys => [:body]))
|
215
|
+
proc { t.on_scope(params) }.must_raise NoMethodError
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
describe "#key" do
|
221
|
+
context "scope is empty" do
|
222
|
+
it "returns nil" do
|
223
|
+
klass.new(options(:scope => [])).key.must_equal nil
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
context "scope has 1 key" do
|
228
|
+
it "returns the key" do
|
229
|
+
klass.new(options(:scope => [:post])).key.must_equal :post
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
context "scope has 2 keys" do
|
234
|
+
it "returns the last key - deepest key" do
|
235
|
+
klass.new(options(:scope => [:post, :comment])).key.must_equal :comment
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
describe "#level" do
|
241
|
+
context "scope is empty" do
|
242
|
+
it "returns 0" do
|
243
|
+
klass.new(options(:scope => [])).level.must_equal 0
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
context "scope has 1 key" do
|
248
|
+
it "returns 1" do
|
249
|
+
klass.new(options(:scope =>[:post])).level.must_equal 1
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
context "scope has 2 keys" do
|
254
|
+
it "returns 2" do
|
255
|
+
klass.new(options(:scope => [:post, :comment])).level.must_equal 2
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
describe "#<=>" do
|
261
|
+
describe "sort an array" do
|
262
|
+
before do
|
263
|
+
@a1 = klass.new(options(:scope =>[:post, :comment]))
|
264
|
+
@a2 = klass.new(options(:scope => [:post]))
|
265
|
+
@array = [@a1, @a2]
|
266
|
+
@array.first.must_equal @a1
|
267
|
+
end
|
268
|
+
|
269
|
+
it "sort objects with the lowest level first" do
|
270
|
+
@array.sort.first.must_equal @a2
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
describe TrustedKeys do
|
4
|
+
let(:controller) do
|
5
|
+
Class.new do
|
6
|
+
include TrustedKeys
|
7
|
+
|
8
|
+
def params
|
9
|
+
{ "email" => "anders@email.com",
|
10
|
+
:time => { "start_time(1i)"=>"2012",
|
11
|
+
"start_time(2i)"=>"3",
|
12
|
+
"start_time(3i)"=>"14" },
|
13
|
+
:event => {
|
14
|
+
:password => "secret",
|
15
|
+
:collection_attributes => [ { "one" => "ok 1",
|
16
|
+
"two" => "remove me"},
|
17
|
+
{ "one" => "ok 2",
|
18
|
+
"two" => "remove me 2"} ],
|
19
|
+
:nested_attributes => {
|
20
|
+
"0" => { "_destroy"=>"false",
|
21
|
+
"start"=>"2012" },
|
22
|
+
"new_1331711737056" => { "_destroy"=>"false",
|
23
|
+
"start"=>"2012" } } },
|
24
|
+
"survey"=> {
|
25
|
+
"name"=>"survery 1",
|
26
|
+
"questions_attributes"=>
|
27
|
+
{ "0"=>{"_destroy"=>"1",
|
28
|
+
"content"=>"question2",
|
29
|
+
"dddd" => "xxxx",
|
30
|
+
"id" => "1",
|
31
|
+
"answers_attributes"=>{
|
32
|
+
"0"=>{"content"=>"answer1","_destroy"=>"","dd" => "x"},
|
33
|
+
"1"=>{"content"=>"answer 2","_destroy"=>"1","id"=>"2"}
|
34
|
+
}},
|
35
|
+
"1"=>{"_destroy"=>"1",
|
36
|
+
"content"=>"",
|
37
|
+
"answers_attributes"=>{
|
38
|
+
"1"=>{"content"=>"", "_destroy"=>""}}}}},
|
39
|
+
|
40
|
+
:controller => "events",
|
41
|
+
:password => "secret",
|
42
|
+
:post => { :body => "I am a body",
|
43
|
+
:title => "This is my title",
|
44
|
+
:comments => { 'body' => 'My body',
|
45
|
+
:email => "an email",
|
46
|
+
:author => { :name => "anders" } } } }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
let(:env) { OpenStruct.new(:test? => false, :development? => false) }
|
52
|
+
|
53
|
+
describe ".trust" do
|
54
|
+
describe "datetime selects" do
|
55
|
+
it "return all datetime attributes" do
|
56
|
+
controller.trust :start_time, :for => :time, :env => env
|
57
|
+
trusted_attributes = controller.new.send(:trusted_attributes)
|
58
|
+
|
59
|
+
expected = { "start_time(1i)"=>"2012",
|
60
|
+
"start_time(2i)"=>"3",
|
61
|
+
"start_time(3i)"=>"14" }
|
62
|
+
trusted_attributes.must_equal(expected)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "hashes inside a collection" do
|
67
|
+
it "returns trusted attributes" do
|
68
|
+
controller.trust :collection_attributes, "password", :for => :event,
|
69
|
+
:env => env
|
70
|
+
controller.trust :one, :for => 'event.collection_attributes',
|
71
|
+
:env => env
|
72
|
+
trusted_attributes = controller.new.send(:trusted_attributes)
|
73
|
+
|
74
|
+
expected = { "password" => "secret",
|
75
|
+
"collection_attributes" => [{ "one" => "ok 1" },
|
76
|
+
{ "one" => "ok 2" }] }
|
77
|
+
trusted_attributes.must_equal(expected)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "nested attributes" do
|
82
|
+
it "returns nested hash" do
|
83
|
+
controller.trust :questions_attributes, :name, :for => :survey,
|
84
|
+
:env => env
|
85
|
+
controller.trust "answers_attributes", "content",
|
86
|
+
:for => 'survey.questions_attributes', :env => env
|
87
|
+
|
88
|
+
controller.trust "content",
|
89
|
+
:for => 'survey.questions_attributes.answers_attributes', :env => env
|
90
|
+
|
91
|
+
trusted_attributes = controller.new.send(:trusted_attributes)
|
92
|
+
|
93
|
+
expected = {
|
94
|
+
"name"=>"survery 1",
|
95
|
+
"questions_attributes"=>
|
96
|
+
{ "0"=>{"_destroy"=>"1",
|
97
|
+
"content"=>"question2",
|
98
|
+
"id" => "1",
|
99
|
+
"answers_attributes"=>{
|
100
|
+
"0"=>{"content"=>"answer1", "_destroy"=>""},
|
101
|
+
"1"=>{"content"=>"answer 2", "_destroy"=>"1", "id"=>"2"}}},
|
102
|
+
"1"=>{"_destroy"=>"1",
|
103
|
+
"content"=>"",
|
104
|
+
"answers_attributes"=>{
|
105
|
+
"1"=>{"content"=>"", "_destroy"=>""}}}}}
|
106
|
+
|
107
|
+
trusted_attributes.must_equal(expected)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "returns nested hash" do
|
111
|
+
controller.trust :nested_attributes, :for => :event, :env => env
|
112
|
+
controller.trust "start", :for => 'event.nested_attributes', :env => env
|
113
|
+
|
114
|
+
trusted_attributes = controller.new.send(:trusted_attributes)
|
115
|
+
|
116
|
+
expected = { "nested_attributes" => {
|
117
|
+
"0" => { "_destroy"=>"false",
|
118
|
+
"start"=>"2012" },
|
119
|
+
"new_1331711737056" => { "_destroy"=>"false",
|
120
|
+
"start"=>"2012" } } }
|
121
|
+
trusted_attributes.must_equal(expected)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "no trusted keys" do
|
126
|
+
it "raises an exception" do
|
127
|
+
proc {
|
128
|
+
controller.new.send(:trusted_attributes)
|
129
|
+
}.must_raise TrustedKeys::Error::Usage
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "0 level" do
|
134
|
+
it "returns trusted keys" do
|
135
|
+
controller.trust :email , :env => env
|
136
|
+
trusted_attributes = controller.new.send(:trusted_attributes)
|
137
|
+
trusted_attributes.must_equal("email" => "anders@email.com")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context "1 level" do
|
142
|
+
it "returns trusted keys" do
|
143
|
+
controller.trust :body, :for => :post, :env => env
|
144
|
+
trusted_attributes = controller.new.send(:trusted_attributes)
|
145
|
+
|
146
|
+
expected = { "body" => "I am a body" }
|
147
|
+
trusted_attributes.must_equal(expected)
|
148
|
+
end
|
149
|
+
|
150
|
+
it "returns trusted keys" do
|
151
|
+
controller.trust :body, :title, :comments, :for => :post, :env => env
|
152
|
+
trusted_attributes = controller.new.send(:trusted_attributes)
|
153
|
+
|
154
|
+
expected = { "body" => "I am a body",
|
155
|
+
"title" => "This is my title",
|
156
|
+
"comments" => "" }
|
157
|
+
trusted_attributes.must_equal(expected)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context "2 levels" do
|
162
|
+
it "returns trusted keys" do
|
163
|
+
controller.trust :body, :comments, :for => :post, :env => env
|
164
|
+
controller.trust :author, :for => 'post.comments', :env => env
|
165
|
+
trusted_attributes = controller.new.send(:trusted_attributes)
|
166
|
+
|
167
|
+
expected = { "body" => "I am a body",
|
168
|
+
"comments" => { "author" => "" } }
|
169
|
+
trusted_attributes.must_equal(expected)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
context "3 levels" do
|
174
|
+
it "returns trusted keys" do
|
175
|
+
controller.trust :body, :comments, :for => :post, :env => env
|
176
|
+
controller.trust :author, :for => 'post.comments', :env => env
|
177
|
+
controller.trust :name, :for => 'post.comments.author', :env => env
|
178
|
+
trusted_attributes = controller.new.send(:trusted_attributes)
|
179
|
+
|
180
|
+
expected = { "body" => "I am a body",
|
181
|
+
"comments" => { "author" => { "name" => "anders" } } }
|
182
|
+
trusted_attributes.must_equal(expected)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/trusted_keys/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Anders Törnqvist"]
|
6
|
+
gem.email = ["anders.tornqvist@gmail.com"]
|
7
|
+
gem.description = %q{Mass assignment security in your controller}
|
8
|
+
gem.summary = %q{Mass assignment security in your controller}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map do |f|
|
12
|
+
File.basename(f)
|
13
|
+
end
|
14
|
+
gem.files = `git ls-files`.split("\n")
|
15
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
gem.name = "trusted_keys"
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
gem.version = TrustedKeys::VERSION
|
19
|
+
|
20
|
+
gem.add_runtime_dependency "rails", ["~> 3.0"]
|
21
|
+
gem.add_development_dependency "minitest", ["~> 2.11"]
|
22
|
+
gem.add_development_dependency "guard", ["~> 1.0"]
|
23
|
+
gem.add_development_dependency "guard-minitest", ["~> 0.5"]
|
24
|
+
gem.add_development_dependency "minitest-colorize", ["~> 0.0.4"]
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: trusted_keys
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Anders Törnqvist
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-30 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: minitest
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '2.11'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2.11'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: guard
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: guard-minitest
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0.5'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0.5'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: minitest-colorize
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.0.4
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.0.4
|
94
|
+
description: Mass assignment security in your controller
|
95
|
+
email:
|
96
|
+
- anders.tornqvist@gmail.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- .gitignore
|
102
|
+
- Gemfile
|
103
|
+
- Guardfile
|
104
|
+
- LICENSE
|
105
|
+
- README.md
|
106
|
+
- Rakefile
|
107
|
+
- example/controller.rb
|
108
|
+
- lib/trusted_keys.rb
|
109
|
+
- lib/trusted_keys/error/not_trusted.rb
|
110
|
+
- lib/trusted_keys/error/usage.rb
|
111
|
+
- lib/trusted_keys/trustable.rb
|
112
|
+
- lib/trusted_keys/version.rb
|
113
|
+
- spec/minitest_helper.rb
|
114
|
+
- spec/trusted_keys/trustable_spec.rb
|
115
|
+
- spec/trusted_keys_spec.rb
|
116
|
+
- trusted_keys.gemspec
|
117
|
+
homepage: ''
|
118
|
+
licenses: []
|
119
|
+
post_install_message:
|
120
|
+
rdoc_options: []
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
none: false
|
125
|
+
requirements:
|
126
|
+
- - ! '>='
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
requirements: []
|
136
|
+
rubyforge_project:
|
137
|
+
rubygems_version: 1.8.21
|
138
|
+
signing_key:
|
139
|
+
specification_version: 3
|
140
|
+
summary: Mass assignment security in your controller
|
141
|
+
test_files:
|
142
|
+
- spec/minitest_helper.rb
|
143
|
+
- spec/trusted_keys/trustable_spec.rb
|
144
|
+
- spec/trusted_keys_spec.rb
|