heimdallr 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,4 +1,6 @@
1
1
  *.gem
2
2
  .bundle
3
+ .yardoc
3
4
  Gemfile.lock
4
5
  pkg/*
6
+ doc/*
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private --protected -r README.yard.md --exclude README.md - LICENSE
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2012 Peter Zotov <whitequark@whitequark.org>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,128 @@
1
+ Heimdallr
2
+ =========
3
+
4
+ Heimdallr is a gem for managing security restrictions for ActiveRecord objects on field level; think
5
+ of it as a supercharged [CanCan](https://github.com/ryanb/cancan). Heimdallr favors whitelisting over blacklisting,
6
+ convention over configuration and is duck-type compatible with most of existing code.
7
+
8
+ ``` ruby
9
+ # Define a typical set of models.
10
+ class User < ActiveRecord::Base
11
+ has_many :articles
12
+ end
13
+
14
+ class Article < ActiveRecord::Base
15
+ include Heimdallr::Model
16
+
17
+ belongs_to :owner, :class_name => 'User'
18
+
19
+ restrict do |user|
20
+ if user.admin? || user == self.owner
21
+ # Administrator or owner can do everything
22
+ can :fetch
23
+ can [:view, :create, :update, :destroy]
24
+ else
25
+ # Other users can view only non-classified articles...
26
+ can :fetch, -> { where('secrecy_level < ?', 5) }
27
+
28
+ # ... and see all fields except the actual security level...
29
+ can :view
30
+ cannot :view, [:secrecy_level]
31
+
32
+ # ... and can create them with certain restrictions.
33
+ can [:create, :update], {
34
+ owner: user,
35
+ secrecy_level: { inclusion: { in: 0..4 } }
36
+ }
37
+ end
38
+ end
39
+ end
40
+
41
+ # Create some fictional data.
42
+ admin = User.create admin: true
43
+ johndoe = User.create admin: false
44
+
45
+ Article.create id: 1, owner: admin, content: "Nothing happens", secrecy_level: 0
46
+ Article.create id: 2, owner: admin, content: "This is a secret", secrecy_level: 10
47
+ Article.create id: 3, owner: johndoe, content: "Hello World"
48
+
49
+ # Get a restricted scope for the user.
50
+ secure = Article.restrict(johndoe)
51
+
52
+ # Use any ARel methods:
53
+ secure.pluck(:content)
54
+ # => ["Nothing happens", "Hello World"]
55
+ secure.find(1).secrecy_level
56
+ # => nil
57
+
58
+ # Everything should be permitted explicitly:
59
+ secure.first.delete
60
+ # ! Heimdallr::PermissionError is raised
61
+
62
+ # If only a single value is possible, it is inferred automatically:
63
+ secure.create! content: "My second article"
64
+ # => Article(id: 4, owner: johndoe, content: "My second article", security_level: 0)
65
+
66
+ # ... and cannot be changed:
67
+ secure.create! owner: admin, content: "I'm a haxx0r"
68
+ # ! ActiveRecord::RecordInvalid is raised
69
+
70
+ # You can use any valid ActiveRecord validators, too:
71
+ secure.create! content: "Top Secret", secrecy_level: 10
72
+ # ! ActiveRecord::RecordInvalid is raised
73
+
74
+ # John Doe would not see what he is not permitted to, ever:
75
+ # -- I know that you have this classified material! It's in folder #2.
76
+ secure.find 2
77
+ # ! ActiveRecord::RecordNotFound is raised
78
+ # -- No, it is not.
79
+ ```
80
+
81
+ The DSL is described in documentation for [Heimdallr::Model](http://rubydoc.info/gems/heimdallr/0.0.2/Heimdallr/Model).
82
+
83
+ Note that Heimdallr is designed with three goals in mind, in the following order:
84
+
85
+ * Preventing malicious modifications
86
+ * Preventing information leaks
87
+ * Being convenient to use
88
+
89
+ Due to the last one, not all methods will raise an exception on invalid access; some will silently drop the offending
90
+ attribute or simply return `nil`. This is clearly described in the documentation, done intentionally and isn't
91
+ going to change.
92
+
93
+ REST interface
94
+ --------------
95
+
96
+ Heimdallr also favors REST pattern; while its use is not mandated, a Heimdallr::Resource module is provided, which
97
+ implements all standard REST actions with the extension of allowing to pass multiple models at once, and also enables
98
+ one to introspect all writable fields with `new` and `edit` actions.
99
+
100
+ The interface is described in documentation for [Heimdallr::Resource](http://rubydoc.info/gems/heimdallr/0.0.2/Heimdallr/Resource).
101
+
102
+ Compatibility
103
+ -------------
104
+
105
+ Ruby 1.8 and ActiveRecord versions prior to 3.0 are not supported.
106
+
107
+ Licensing
108
+ ---------
109
+
110
+ Copyright (C) 2012 Peter Zotov <whitequark@whitequark.org>
111
+
112
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
113
+ this software and associated documentation files (the "Software"), to deal in
114
+ the Software without restriction, including without limitation the rights to
115
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
116
+ of the Software, and to permit persons to whom the Software is furnished to do
117
+ so, subject to the following conditions:
118
+
119
+ The above copyright notice and this permission notice shall be included in all
120
+ copies or substantial portions of the Software.
121
+
122
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
123
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
124
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
125
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
126
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
127
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
128
+ SOFTWARE.
data/README.yard.md ADDED
@@ -0,0 +1,128 @@
1
+ Heimdallr
2
+ =========
3
+
4
+ Heimdallr is a gem for managing security restrictions for ActiveRecord objects on field level; think
5
+ of it as a supercharged [CanCan](https://github.com/ryanb/cancan). Heimdallr favors whitelisting over blacklisting,
6
+ convention over configuration and is duck-type compatible with most of existing code.
7
+
8
+ ``` ruby
9
+ # Define a typical set of models.
10
+ class User < ActiveRecord::Base
11
+ has_many :articles
12
+ end
13
+
14
+ class Article < ActiveRecord::Base
15
+ include Heimdallr::Model
16
+
17
+ belongs_to :owner, :class_name => 'User'
18
+
19
+ restrict do |user|
20
+ if user.admin? || user == self.owner
21
+ # Administrator or owner can do everything
22
+ can :fetch
23
+ can [:view, :create, :update, :destroy]
24
+ else
25
+ # Other users can view only non-classified articles...
26
+ can :fetch, -> { where('secrecy_level < ?', 5) }
27
+
28
+ # ... and see all fields except the actual security level...
29
+ can :view
30
+ cannot :view, [:secrecy_level]
31
+
32
+ # ... and can create them with certain restrictions.
33
+ can [:create, :update], {
34
+ owner: user,
35
+ secrecy_level: { inclusion: { in: 0..4 } }
36
+ }
37
+ end
38
+ end
39
+ end
40
+
41
+ # Create some fictional data.
42
+ admin = User.create admin: true
43
+ johndoe = User.create admin: false
44
+
45
+ Article.create id: 1, owner: admin, content: "Nothing happens", secrecy_level: 0
46
+ Article.create id: 2, owner: admin, content: "This is a secret", secrecy_level: 10
47
+ Article.create id: 3, owner: johndoe, content: "Hello World"
48
+
49
+ # Get a restricted scope for the user.
50
+ secure = Article.restrict(johndoe)
51
+
52
+ # Use any ARel methods:
53
+ secure.pluck(:content)
54
+ # => ["Nothing happens", "Hello World"]
55
+ secure.find(1).secrecy_level
56
+ # => nil
57
+
58
+ # Everything should be permitted explicitly:
59
+ secure.first.delete
60
+ # ! Heimdallr::PermissionError is raised
61
+
62
+ # If only a single value is possible, it is inferred automatically:
63
+ secure.create! content: "My second article"
64
+ # => Article(id: 4, owner: johndoe, content: "My second article", security_level: 0)
65
+
66
+ # ... and cannot be changed:
67
+ secure.create! owner: admin, content: "I'm a haxx0r"
68
+ # ! ActiveRecord::RecordInvalid is raised
69
+
70
+ # You can use any valid ActiveRecord validators, too:
71
+ secure.create! content: "Top Secret", secrecy_level: 10
72
+ # ! ActiveRecord::RecordInvalid is raised
73
+
74
+ # John Doe would not see what he is not permitted to, ever:
75
+ # -- I know that you have this classified material! It's in folder #2.
76
+ secure.find 2
77
+ # ! ActiveRecord::RecordNotFound is raised
78
+ # -- No, it is not.
79
+ ```
80
+
81
+ The DSL is described in documentation for {Heimdallr::Model}.
82
+
83
+ Note that Heimdallr is designed with three goals in mind, in the following order:
84
+
85
+ * Preventing malicious modifications
86
+ * Preventing information leaks
87
+ * Being convenient to use
88
+
89
+ Due to the last one, not all methods will raise an exception on invalid access; some will silently drop the offending
90
+ attribute or simply return `nil`. This is clearly described in the documentation, done intentionally and isn't
91
+ going to change.
92
+
93
+ REST interface
94
+ --------------
95
+
96
+ Heimdallr also favors REST pattern; while its use is not mandated, a Heimdallr::Resource module is provided, which
97
+ implements all standard REST actions with the extension of allowing to pass multiple models at once, and also enables
98
+ one to introspect all writable fields with `new` and `edit` actions.
99
+
100
+ The interface is described in documentation for {Heimdallr::Resource}.
101
+
102
+ Compatibility
103
+ -------------
104
+
105
+ Ruby 1.8 and ActiveRecord versions prior to 3.0 are not supported.
106
+
107
+ Licensing
108
+ ---------
109
+
110
+ Copyright (C) 2012 Peter Zotov <whitequark@whitequark.org>
111
+
112
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
113
+ this software and associated documentation files (the "Software"), to deal in
114
+ the Software without restriction, including without limitation the rights to
115
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
116
+ of the Software, and to permit persons to whom the Software is furnished to do
117
+ so, subject to the following conditions:
118
+
119
+ The above copyright notice and this permission notice shall be included in all
120
+ copies or substantial portions of the Software.
121
+
122
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
123
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
124
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
125
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
126
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
127
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
128
+ SOFTWARE.
data/Rakefile CHANGED
@@ -1 +1,21 @@
1
1
  require "bundler/gem_tasks"
2
+
3
+ Dir.chdir File.dirname(__FILE__)
4
+
5
+ gemspec = Bundler.load_gemspec Dir["{,*}.gemspec"].first
6
+
7
+ task :release => :prepare_for_release
8
+
9
+ task :prepare_for_release do
10
+ readme = File.read('README.yard.md')
11
+ readme.gsub! /{([[:alnum:]:]+)}/i do |match|
12
+ %Q|[#{$1}](http://rubydoc.info/gems/#{gemspec.name}/#{gemspec.version}/#{$1.gsub '::', '/'})|
13
+ end
14
+
15
+ File.open('README.md', 'w') do |f|
16
+ f.write readme
17
+ end
18
+
19
+ %x|git add README.md|
20
+ #%x|git commit -m "Bump version to #{gemspec.version}."|
21
+ end
data/heimdallr.gemspec CHANGED
@@ -1,16 +1,15 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
- require "heimdallr/version"
4
3
 
5
4
  Gem::Specification.new do |s|
6
5
  s.name = "heimdallr"
7
- s.version = Heimdallr::VERSION
6
+ s.version = "0.0.2"
8
7
  s.authors = ["Peter Zotov"]
9
8
  s.email = ["whitequark@whitequark.org"]
10
9
  s.homepage = "http://github.com/roundlake/heimdallr"
11
10
  s.summary = %q{Heimdallr is an ActiveModel extension which provides object- and field-level access control.}
12
11
  s.description = %q{Heimdallr aims to provide an easy to configure and efficient object- and field-level access
13
- control solution, reusing proven patterns from gems like CanCan and allowing one to control permissions in a very
12
+ control solution, reusing proven patterns from gems like CanCan and allowing one to manage permissions in a very
14
13
  fine-grained manner.}
15
14
 
16
15
  s.rubyforge_project = "heimdallr"
data/lib/heimdallr.rb CHANGED
@@ -3,7 +3,44 @@ require "active_model"
3
3
 
4
4
  require "heimdallr/version"
5
5
 
6
+ require "heimdallr/proxy/collection"
7
+ require "heimdallr/proxy/record"
8
+ require "heimdallr/validator"
6
9
  require "heimdallr/evaluator"
7
- require "heimdallr/proxy"
8
10
  require "heimdallr/model"
9
- require "heimdallr/resource"
11
+ require "heimdallr/resource"
12
+
13
+ # See {file:README.yard}.
14
+ module Heimdallr
15
+ class << self
16
+ # Allow implicit insecure association access. Consider this code:
17
+ #
18
+ # class User < ActiveRecord::Base
19
+ # include Heimdallr::Model
20
+ #
21
+ # has_many :articles
22
+ # end
23
+ #
24
+ # class Article < ActiveRecord::Base
25
+ # # No Heimdallr::Model!
26
+ # end
27
+ #
28
+ # If the +allow_insecure_associations+ setting is +false+ (the default),
29
+ # then +user.restrict(context).articles+ fetch would cause an
30
+ # {InsecureOperationError}. This may be undesirable in some environments;
31
+ # setting +allow_insecure_associations+ to +true+ will prevent the error
32
+ # from being raised.
33
+ #
34
+ # @return [Boolean]
35
+ attr_accessor :allow_insecure_associations
36
+ self.allow_insecure_associations = false
37
+ end
38
+
39
+ # {PermissionError} is raised when a security policy prevents
40
+ # a called operation from being executed.
41
+ class PermissionError < StandardError; end
42
+
43
+ # {InsecureOperationError} is raised when a potentially unsafe
44
+ # operation is about to be executed.
45
+ class InsecureOperationError < StandardError; end
46
+ end
@@ -1,67 +1,149 @@
1
1
  module Heimdallr
2
+ # Evaluator is a DSL for managing permissions on records with the field granularity.
3
+ # It works by evaluating a block of code within a given <em>security context</em>.
4
+ #
5
+ # The default resolution is to forbid everything--that is, Heimdallr security policy
6
+ # is whitelisting safe actions, not blacklisting unsafe ones. This is by design
7
+ # and is not going to change.
8
+ #
9
+ # The DSL consists of three functions: {#scope}, {#can} and {#cannot}.
2
10
  class Evaluator
3
- attr_reader :whitelist, :validations
11
+ attr_reader :allowed_fields, :fixtures, :validators
4
12
 
5
- def initialize(model_class, &block)
13
+ # Create a new Evaluator for the ActiveModel-descending class +model_class+,
14
+ # and use +block+ to infer restrictions for any security context passed.
15
+ def initialize(model_class, block)
6
16
  @model_class, @block = model_class, block
7
17
 
8
- @whitelist = @validations = nil
9
- @last_context = nil
18
+ @scopes = {}
19
+ @allowed_fields = {}
20
+ @validations = {}
21
+ @fixtures = {}
10
22
  end
11
23
 
12
- def evaluate(context)
13
- if context != @last_context
14
- @whitelist = Hash.new { [] }
15
- @validations = Hash.new { [] }
16
-
17
- instance_exec context, &block
18
-
19
- @whitelist.freeze
20
- @validations.freeze
24
+ # @group DSL
25
+
26
+ # Define a scope. A special +:fetch+ scope is applied to any other scope
27
+ # automatically.
28
+ #
29
+ # @overload scope(name, block)
30
+ # This form accepts an explicit lambda.
31
+ #
32
+ # @example
33
+ # scope :fetch, -> { where(:protected => false) }
34
+ #
35
+ # @overload scope(name)
36
+ # This form accepts an implicit lambda.
37
+ #
38
+ # @example
39
+ # scope :fetch do
40
+ # if user.manager?
41
+ # scoped
42
+ # else
43
+ # where(:invisible => false)
44
+ # end
45
+ # end
46
+ def scope(name, explicit_block, &implicit_block)
47
+ @scopes[name] = explicit_block || implicit_block
48
+ end
21
49
 
22
- @last_context = context
50
+ # Define allowed operations for action(s).
51
+ #
52
+ # The +fields+ parameter accepts both Arrays and Hashes.
53
+ # * If an +Array+ is passed, then all fields present in the array are whitelised.
54
+ # * If a +Hash+ is passed, then all fields present as hash keys are whitelisted, and:
55
+ # 1. If a corresponding value is a +Hash+, it will be processed as a security
56
+ # validator. Security validators make records invalid when they are saved through
57
+ # a {Proxy::Record}.
58
+ # 2. If the corresponding value is any other object, it will be added as a security
59
+ # fixture. Fixtures are merged when objects are created through restricted scopes,
60
+ # and cause exceptions to be raised when a record is saved, even through the +#save+
61
+ # method.
62
+ #
63
+ # @example Array of fields
64
+ # can :view, [:title, :content]
65
+ #
66
+ # @example Fixtures
67
+ # can :create, { owner: current_user }
68
+ #
69
+ # @example Validations
70
+ # can [:create, :update], { priority: { inclusion: 1..10 } }
71
+ #
72
+ # @param [Symbol, Array<Symbol>] actions one or more action names
73
+ # @param [Hash<Hash, Object>] fields field restrictions
74
+ def can(actions, fields=@model_class.attribute_names)
75
+ Array(actions).each do |action|
76
+ case fields
77
+ when Hash # a list of validations
78
+ @allowed_fields[action] += fields.keys
79
+ @validations[action] += create_validators(fields)
80
+ @fixtures[action].merge extract_fixtures(fields)
81
+
82
+ else # an array or a field name
83
+ @allowed_fields[action] += Array(fields)
84
+ end
23
85
  end
24
-
25
- self
26
86
  end
27
87
 
28
- def validate(action, record)
29
- @validations[action].each do |validator|
30
- validator.validate(record)
88
+ # Revoke a permission on fields.
89
+ #
90
+ # @todo Revoke validating restrictions.
91
+ # @param [Symbol, Array<Symbol>] actions one or more action names
92
+ # @param [Array<Symbol>] fields field list
93
+ def cannot(actions, fields)
94
+ Array(actions).each do |action|
95
+ @allowed_fields[action] -= fields
96
+ @fixtures.delete_at *fields
31
97
  end
32
98
  end
33
99
 
34
- def can(actions, fields=@model_class.attribute_names)
35
- actions = Array(actions)
36
-
37
- case fields
38
- when Hash # a list of validations
39
- actions.each do |action|
40
- @whitelist[action] += fields.keys
41
- @validations[action] += make_validators(fields)
42
- end
43
-
44
- else # an array or a field name
45
- actions.each do |action|
46
- @whitelist[action] += Array(fields)
47
- end
100
+ # @endgroup
101
+
102
+ # Request a scope.
103
+ #
104
+ # @param scope name of the scope
105
+ # @param basic_scope the scope to which scope +name+ will be applied. Defaults to +:fetch+.
106
+ #
107
+ # @return ActiveRecord scope
108
+ def request_scope(name=:fetch, basic_scope=request_scope(:fetch))
109
+ if name == :fetch || !@scopes.has_key?(name)
110
+ fetch_scope = @model_class.instance_exec(&@scopes[:fetch])
111
+ else
112
+ basic_scope.instance_exec(&@scopes[name])
48
113
  end
49
114
  end
50
115
 
51
- def cannot(actions, fields)
52
- actions = Array(actions)
116
+ # Compute the restrictions for a given +context+. Invokes a +block+ passed to the
117
+ # +initialize+ once.
118
+ def evaluate(context)
119
+ if context != @last_context
120
+ @scopes = {}
121
+ @allowed_fields = Hash.new { [] }
122
+ @validators = Hash.new { [] }
123
+ @fixtures = Hash.new { [] }
124
+
125
+ instance_exec context, &block
53
126
 
54
- actions.each do |action|
55
- @whitelist[action] -= fields
127
+ [@scopes, @allowed_fields, @validators, @fixtures].
128
+ map(&:freeze)
129
+
130
+ @last_context = context
56
131
  end
132
+
133
+ self
57
134
  end
58
135
 
59
136
  protected
60
137
 
61
- def make_validators(fields)
62
- validators = []
138
+ # Create validators for +fields+ in +ActiveModel::Validations+-like way.
139
+ #
140
+ # @return [Array<ActiveModel::Validator>]
141
+ def create_validators(fields)
142
+ validators = {}
63
143
 
64
144
  fields.each do |attribute, validations|
145
+ next unless validations.is_a? Hash
146
+
65
147
  validations.each do |key, options|
66
148
  key = "#{key.to_s.camelize}Validator"
67
149
 
@@ -71,14 +153,30 @@ module Heimdallr
71
153
  raise ArgumentError, "Unknown validator: '#{key}'"
72
154
  end
73
155
 
74
- validators << validator.new(_parse_validates_options(options).merge(:attributes => [ attribute ]))
156
+ validators[attribute] = validator.new(_parse_validates_options(options).merge(:attributes => [ attribute ]))
75
157
  end
76
158
  end
77
159
 
78
160
  validators
79
161
  end
80
162
 
81
- def _parse_validates_options(options) #:nodoc:
163
+ # Collects fixtures from the +fields+ definition.
164
+ def extract_fixtures(fields)
165
+ fixtures = {}
166
+
167
+ fields.each do |attribute, options|
168
+ next if options.is_a? Hash
169
+
170
+ fixtures[attribute] = options
171
+ end
172
+
173
+ fixtures
174
+ end
175
+
176
+ private
177
+
178
+ # Monkey-copied from ActiveRecord.
179
+ def _parse_validates_options(options)
82
180
  case options
83
181
  when TrueClass
84
182
  {}