accessible_for 0.1.0
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/README.markdown +101 -0
- data/Rakefile +14 -0
- data/accessible_for.gemspec +16 -0
- data/lib/accessible_for.rb +24 -0
- data/lib/mass_assignment_backport.rb +36 -0
- data/mass_assignment_backport.gemspec +16 -0
- data/test/accessible_for_test.rb +49 -0
- data/test/mass_assignment_test.rb +49 -0
- metadata +56 -0
data/README.markdown
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# AccessibleFor: key-based hash sanitizer for Ruby
|
2
|
+
|
3
|
+
This is a simple mass-assignment security module loosely based on
|
4
|
+
[ActiveModel::MassAssignmentSecurity][1]. It attempts to steal the good ideas
|
5
|
+
and some of the API while being compatible with Rails 2.3-based applications.
|
6
|
+
|
7
|
+
Only attr_accessible (or its equivalent, keep reading) is implemented, because
|
8
|
+
attr_protected is just a bad ActiveRecord API that hung around for some reason,
|
9
|
+
and we don't want it stinking up the place.
|
10
|
+
|
11
|
+
There are actually two available APIs, the ActiveModel-workalike and a new one
|
12
|
+
called accessible_for. They provide identical functionality.
|
13
|
+
|
14
|
+
# Usage
|
15
|
+
|
16
|
+
## ActiveModel-workalike API
|
17
|
+
|
18
|
+
class TacoShop < Controller
|
19
|
+
include MassAssignmentBackport
|
20
|
+
|
21
|
+
# when no role is specified, :default is used
|
22
|
+
attr_accessible :rating
|
23
|
+
|
24
|
+
# you can specify multiple roles
|
25
|
+
attr_accessible :filling, :topping, :as => [:default, :manager]
|
26
|
+
|
27
|
+
# and add to existing roles
|
28
|
+
attr_accessible :price, :as => :manager
|
29
|
+
|
30
|
+
def update
|
31
|
+
Taco.find(params[:id]).update_attributes!(taco_params)
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def taco_params
|
37
|
+
# use sanitize_for_mass_assignment to build a safe hash given a role.
|
38
|
+
# when nothing/nil is passed for the role, :default is used
|
39
|
+
sanitize_for_mass_assignment params[:taco], current_user.manager? ? :manager : nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
## accessible_for API
|
44
|
+
|
45
|
+
class TacoShop < Controller
|
46
|
+
include AccessibleFor
|
47
|
+
|
48
|
+
# there are no implicit roles and you can declare only one group at a time
|
49
|
+
accessible_for :default => [ :filling, :topping, :rating ]
|
50
|
+
accessible_for :manager => [ :filling, :topping, :price ]
|
51
|
+
|
52
|
+
def update
|
53
|
+
Taco.find(params[:id]).update_attributes!(taco_params)
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
def taco_params
|
59
|
+
# use sanitize_for(role, params) to build a safe hash
|
60
|
+
# again, there is no implicit role
|
61
|
+
if current_user.manager?
|
62
|
+
sanitize_for :manager, params[:taco]
|
63
|
+
else
|
64
|
+
sanitize_for :default, params[:taco]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Rationale
|
70
|
+
|
71
|
+
There are two things I've never liked about ActiveRecord's attr_* API:
|
72
|
+
|
73
|
+
It's model-level when the resources I am trying to protect are controller-level.
|
74
|
+
This actually gets in our way when we're just trying to test/manipulate our own
|
75
|
+
models outside of a controller context, making it harder to work with
|
76
|
+
our own data for no good reason. I feel this phenomenon could have the effect of
|
77
|
+
discouraging developers from using it.
|
78
|
+
|
79
|
+
Another problem with ActiveRecord is that it provides attr_protected.
|
80
|
+
Blacklisting instead of whitelisting is just a bad idea, and I see no reason
|
81
|
+
to allow/support it when security is the primary goal.
|
82
|
+
|
83
|
+
So once we address those two things we have something that looks a bit like
|
84
|
+
ActiveModel's implementation minus attr_protected, which is the purpose of the
|
85
|
+
ActiveModel-workalike API. However there are problems with this API as well:
|
86
|
+
|
87
|
+
The role is optional, leading to lack of clarity. Sometimes you need to
|
88
|
+
specify :default, sometimes it's implicit. I think an API designed for
|
89
|
+
hardening should be more transparent.
|
90
|
+
|
91
|
+
The way the role is specified is also suboptimal. It's at the end of the
|
92
|
+
declaration so you have to hunt for it. It uses the key :as implying a
|
93
|
+
user-based access role, but the fact is this value is really just a scope and
|
94
|
+
can mean anything.
|
95
|
+
|
96
|
+
# Author
|
97
|
+
|
98
|
+
Zack Hobson (zack@zackhobson.com)
|
99
|
+
|
100
|
+
[1]: http://api.rubyonrails.org/classes/ActiveModel/MassAssignmentSecurity.html
|
101
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
desc 'run tests'
|
2
|
+
task :test do
|
3
|
+
# This should work in my opinion, but it only runs the last loaded test.
|
4
|
+
#sh 'ruby -Ilib -rminitest/autorun test/*_test.rb'
|
5
|
+
require 'minitest/unit'
|
6
|
+
$:.unshift("./lib")
|
7
|
+
Dir['test/*_test.rb'].each {|t| load t}
|
8
|
+
MiniTest::Unit.autorun
|
9
|
+
end
|
10
|
+
|
11
|
+
desc 'build the gem'
|
12
|
+
task :gem do
|
13
|
+
sh 'gem build accessible_for.gemspec'
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
$:.unshift File.expand_path("./lib")
|
2
|
+
require 'accessible_for'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "accessible_for"
|
6
|
+
s.version = AccessibleFor::VERSION
|
7
|
+
s.summary = 'Simple API for sanitizing hashes by input key'
|
8
|
+
s.description = <<-EOD
|
9
|
+
This is a simple mass-assignment security module loosely based on
|
10
|
+
ActiveModel::MassAssignmentSecurity. It attempts to steal the good ideas
|
11
|
+
and some of the API while being compatible with Rails 2.3-based applications.
|
12
|
+
EOD
|
13
|
+
s.authors = ['Zack Hobson']
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'mass_assignment_backport'
|
2
|
+
|
3
|
+
module AccessibleFor
|
4
|
+
VERSION = "0.1.0"
|
5
|
+
|
6
|
+
def self.included(mod)
|
7
|
+
mod.send :include, MassAssignmentBackport
|
8
|
+
mod.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def accessible_for params
|
13
|
+
params.each do |role, attrs|
|
14
|
+
attr_accessible *([attrs].flatten.push(:as => role))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def sanitize_for role, values
|
20
|
+
sanitize_for_mass_assignment values, role
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module MassAssignmentBackport
|
2
|
+
VERSION = "0.1.0"
|
3
|
+
|
4
|
+
def self.included(mod)
|
5
|
+
mod.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
attr_accessor :_accessible_attributes
|
10
|
+
|
11
|
+
def attr_accessible *args
|
12
|
+
options = args.last.kind_of?(Hash) ? args.pop : {}
|
13
|
+
role = options[:as] || :default
|
14
|
+
self._accessible_attributes ||= {}
|
15
|
+
[role].flatten.each do |name|
|
16
|
+
self._accessible_attributes[name] = accessible_attributes(name) + args
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def accessible_attributes role=:default
|
21
|
+
_accessible_attributes[role] || []
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def sanitize_for_mass_assignment values, role=:default
|
26
|
+
{}.tap do |result|
|
27
|
+
values.each do |k, v|
|
28
|
+
if self.class._accessible_attributes[role].include?(k.to_sym)
|
29
|
+
yield k, v if block_given?
|
30
|
+
result[k] = v
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
$:.unshift File.expand_path("./lib")
|
2
|
+
require 'mass_assignment_backport'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "mass_assignment_backport"
|
6
|
+
s.version = MassAssignmentBackport::VERSION
|
7
|
+
s.summary = 'Simple API for sanitizing hashes by input key'
|
8
|
+
s.description = <<-EOD
|
9
|
+
This is a simple mass-assignment security module loosely based on
|
10
|
+
ActiveModel::MassAssignmentSecurity. It attempts to steal the good ideas
|
11
|
+
and some of the API while being compatible with Rails 2.3-based applications.
|
12
|
+
EOD
|
13
|
+
s.authors = ['Zack Hobson']
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
16
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'accessible_for'
|
2
|
+
|
3
|
+
class AccessibleForTest < MiniTest::Unit::TestCase
|
4
|
+
include AccessibleFor
|
5
|
+
accessible_for :default => :topping
|
6
|
+
accessible_for :manager => [:price, :topping]
|
7
|
+
|
8
|
+
def test_accessible_default
|
9
|
+
default = sanitize_for :default, :topping => 'salsa', :price => 123, :extra => 'foo'
|
10
|
+
assert default.has_key?(:topping), "default gets accessible key"
|
11
|
+
assert !default.has_key?(:price), "default does not get inaccessible key"
|
12
|
+
assert !default.has_key?(:extra), "default does not get extra key"
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_accessible_role
|
16
|
+
manager = sanitize_for :manager, :topping => 'salsa', :price => 123, :extra => 'foo'
|
17
|
+
assert manager.has_key?(:topping), "role gets accessible key"
|
18
|
+
assert manager.has_key?(:price), "role gets second accessible key"
|
19
|
+
assert !manager.has_key?(:extra), "role does not get extra key"
|
20
|
+
end
|
21
|
+
|
22
|
+
class SubTest
|
23
|
+
include AccessibleFor
|
24
|
+
accessible_for :default => :toasted
|
25
|
+
accessible_for :manager => :free
|
26
|
+
end
|
27
|
+
class SubclassTest < SubTest
|
28
|
+
accessible_for :default => :rating
|
29
|
+
accessible_for :manager => :toasted
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_ignores_parent_settings_default
|
33
|
+
sub = SubclassTest.new
|
34
|
+
default = sub.sanitize_for :default, :toasted => true, :rating => 'good'
|
35
|
+
assert !default.has_key?(:toasted), "inherited default does not get parent accessible key"
|
36
|
+
assert default.has_key?(:rating), "inherited default gets accessible key"
|
37
|
+
assert !default.has_key?(:free), "inherited default does not get inaccessible key"
|
38
|
+
assert !default.has_key?(:extra), "inherited default does not get extra key"
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_ignores_parent_settings_role
|
42
|
+
sub = SubclassTest.new
|
43
|
+
manager = sub.sanitize_for :manager, :toasted => true, :free => true, :rating => 'good'
|
44
|
+
assert !manager.has_key?(:free), "inherited role does not get parent accessible key"
|
45
|
+
assert manager.has_key?(:toasted), "inherited role gets accessible key"
|
46
|
+
assert !manager.has_key?(:rating), "inherited role does not get inaccessible key"
|
47
|
+
assert !manager.has_key?(:extra), "inherited role does not get extra key"
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'mass_assignment_backport'
|
2
|
+
|
3
|
+
class MassAssignmentTest < MiniTest::Unit::TestCase
|
4
|
+
include MassAssignmentBackport
|
5
|
+
attr_accessible :topping, :as => [:default, :manager]
|
6
|
+
attr_accessible :price, :as => :manager
|
7
|
+
|
8
|
+
def test_accessible_default
|
9
|
+
default = sanitize_for_mass_assignment :topping => 'salsa', :price => 123, :extra => 'foo'
|
10
|
+
assert default.has_key?(:topping), "default gets accessible key"
|
11
|
+
assert !default.has_key?(:price), "default does not get inaccessible key"
|
12
|
+
assert !default.has_key?(:extra), "default does not get extra key"
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_accessible_role
|
16
|
+
manager = sanitize_for_mass_assignment({ :topping => 'salsa', :price => 123, :extra => 'foo' }, :manager)
|
17
|
+
assert manager.has_key?(:topping), "role gets accessible key"
|
18
|
+
assert manager.has_key?(:price), "role gets second accessible key"
|
19
|
+
assert !manager.has_key?(:extra), "role does not get extra key"
|
20
|
+
end
|
21
|
+
|
22
|
+
class SubTest
|
23
|
+
include MassAssignmentBackport
|
24
|
+
attr_accessible :toasted
|
25
|
+
attr_accessible :free, :as => :manager
|
26
|
+
end
|
27
|
+
class SubclassTest < SubTest
|
28
|
+
attr_accessible :rating
|
29
|
+
attr_accessible :toasted, :as => :manager
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_ignores_parent_settings_default
|
33
|
+
sub = SubclassTest.new
|
34
|
+
default = sub.sanitize_for_mass_assignment :toasted => true, :rating => 'good'
|
35
|
+
assert !default.has_key?(:toasted), "inherited default does not get parent accessible key"
|
36
|
+
assert default.has_key?(:rating), "inherited default gets accessible key"
|
37
|
+
assert !default.has_key?(:free), "inherited default does not get inaccessible key"
|
38
|
+
assert !default.has_key?(:extra), "inherited default does not get extra key"
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_ignores_parent_settings_role
|
42
|
+
sub = SubclassTest.new
|
43
|
+
manager = sub.sanitize_for_mass_assignment({ :toasted => true, :free => true, :rating => 'good' }, :manager)
|
44
|
+
assert !manager.has_key?(:free), "inherited role does not get parent accessible key"
|
45
|
+
assert manager.has_key?(:toasted), "inherited role gets accessible key"
|
46
|
+
assert !manager.has_key?(:rating), "inherited role does not get inaccessible key"
|
47
|
+
assert !manager.has_key?(:extra), "inherited role does not get extra key"
|
48
|
+
end
|
49
|
+
end
|
metadata
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: accessible_for
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Zack Hobson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-06 00:00:00.000000000Z
|
13
|
+
dependencies: []
|
14
|
+
description: ! " This is a simple mass-assignment security module loosely based
|
15
|
+
on\n ActiveModel::MassAssignmentSecurity. It attempts to steal the good ideas\n
|
16
|
+
\ and some of the API while being compatible with Rails 2.3-based applications.\n"
|
17
|
+
email:
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- README.markdown
|
23
|
+
- Rakefile
|
24
|
+
- accessible_for.gemspec
|
25
|
+
- lib/accessible_for.rb
|
26
|
+
- lib/mass_assignment_backport.rb
|
27
|
+
- mass_assignment_backport.gemspec
|
28
|
+
- test/accessible_for_test.rb
|
29
|
+
- test/mass_assignment_test.rb
|
30
|
+
homepage:
|
31
|
+
licenses: []
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
require_paths:
|
35
|
+
- lib
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubyforge_project:
|
50
|
+
rubygems_version: 1.8.10
|
51
|
+
signing_key:
|
52
|
+
specification_version: 3
|
53
|
+
summary: Simple API for sanitizing hashes by input key
|
54
|
+
test_files:
|
55
|
+
- test/accessible_for_test.rb
|
56
|
+
- test/mass_assignment_test.rb
|