heimdallr 0.0.1 → 0.0.2

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 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
  {}