heimdallr 0.0.2 → 1.0.0.RC2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -19,17 +19,19 @@ class Article < ActiveRecord::Base
19
19
  restrict do |user|
20
20
  if user.admin? || user == self.owner
21
21
  # Administrator or owner can do everything
22
- can :fetch
23
- can [:view, :create, :update, :destroy]
22
+ scope :fetch
23
+ scope :destroy
24
+ can [:view, :create, :update]
24
25
  else
25
26
  # Other users can view only non-classified articles...
26
- can :fetch, -> { where('secrecy_level < ?', 5) }
27
+ scope :fetch, -> { where('secrecy_level < ?', 5) }
27
28
 
28
29
  # ... and see all fields except the actual security level...
29
30
  can :view
30
31
  cannot :view, [:secrecy_level]
31
32
 
32
33
  # ... and can create them with certain restrictions.
34
+ can :create, %w(content)
33
35
  can [:create, :update], {
34
36
  owner: user,
35
37
  secrecy_level: { inclusion: { in: 0..4 } }
@@ -52,12 +54,17 @@ secure = Article.restrict(johndoe)
52
54
  # Use any ARel methods:
53
55
  secure.pluck(:content)
54
56
  # => ["Nothing happens", "Hello World"]
55
- secure.find(1).secrecy_level
56
- # => nil
57
57
 
58
58
  # Everything should be permitted explicitly:
59
59
  secure.first.delete
60
60
  # ! Heimdallr::PermissionError is raised
61
+ secure.find(1).secrecy_level
62
+ # ! Heimdallr::PermissionError is raised
63
+
64
+ # There is a helper for views to be easily written:
65
+ view_passed = secure.first.implicit
66
+ view_passed.secrecy_level
67
+ # => nil
61
68
 
62
69
  # If only a single value is possible, it is inferred automatically:
63
70
  secure.create! content: "My second article"
@@ -65,7 +72,7 @@ secure.create! content: "My second article"
65
72
 
66
73
  # ... and cannot be changed:
67
74
  secure.create! owner: admin, content: "I'm a haxx0r"
68
- # ! ActiveRecord::RecordInvalid is raised
75
+ # ! Heimdallr::PermissionError is raised
69
76
 
70
77
  # You can use any valid ActiveRecord validators, too:
71
78
  secure.create! content: "Top Secret", secrecy_level: 10
@@ -78,26 +85,17 @@ secure.find 2
78
85
  # -- No, it is not.
79
86
  ```
80
87
 
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.
88
+ The DSL is described in documentation for [Heimdallr::Model](http://rubydoc.info/gems/heimdallr/1.0.0.RC2/Heimdallr/Model).
92
89
 
93
- REST interface
94
- --------------
90
+ Ideology
91
+ --------
95
92
 
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.
93
+ Heimdallr aims to make security explicit, but nevertheless convenient. It does not allow one to call any
94
+ implicit operations which may be used maliciously; instead, it forces you to explicitly call `#insecure`
95
+ method which returns the underlying object. This single point of entry is easily recognizable with code.
99
96
 
100
- The interface is described in documentation for [Heimdallr::Resource](http://rubydoc.info/gems/heimdallr/0.0.2/Heimdallr/Resource).
97
+ Heimdallr would raise exceptions in all cases of forbidden or potentially unsecure access except for attribute
98
+ reading to allow for writing uncrufted code in templates (particularly [JBuilder](http://github.com/rails/jbuilder) ones).
101
99
 
102
100
  Compatibility
103
101
  -------------
@@ -19,17 +19,19 @@ class Article < ActiveRecord::Base
19
19
  restrict do |user|
20
20
  if user.admin? || user == self.owner
21
21
  # Administrator or owner can do everything
22
- can :fetch
23
- can [:view, :create, :update, :destroy]
22
+ scope :fetch
23
+ scope :destroy
24
+ can [:view, :create, :update]
24
25
  else
25
26
  # Other users can view only non-classified articles...
26
- can :fetch, -> { where('secrecy_level < ?', 5) }
27
+ scope :fetch, -> { where('secrecy_level < ?', 5) }
27
28
 
28
29
  # ... and see all fields except the actual security level...
29
30
  can :view
30
31
  cannot :view, [:secrecy_level]
31
32
 
32
33
  # ... and can create them with certain restrictions.
34
+ can :create, %w(content)
33
35
  can [:create, :update], {
34
36
  owner: user,
35
37
  secrecy_level: { inclusion: { in: 0..4 } }
@@ -52,12 +54,17 @@ secure = Article.restrict(johndoe)
52
54
  # Use any ARel methods:
53
55
  secure.pluck(:content)
54
56
  # => ["Nothing happens", "Hello World"]
55
- secure.find(1).secrecy_level
56
- # => nil
57
57
 
58
58
  # Everything should be permitted explicitly:
59
59
  secure.first.delete
60
60
  # ! Heimdallr::PermissionError is raised
61
+ secure.find(1).secrecy_level
62
+ # ! Heimdallr::PermissionError is raised
63
+
64
+ # There is a helper for views to be easily written:
65
+ view_passed = secure.first.implicit
66
+ view_passed.secrecy_level
67
+ # => nil
61
68
 
62
69
  # If only a single value is possible, it is inferred automatically:
63
70
  secure.create! content: "My second article"
@@ -65,7 +72,7 @@ secure.create! content: "My second article"
65
72
 
66
73
  # ... and cannot be changed:
67
74
  secure.create! owner: admin, content: "I'm a haxx0r"
68
- # ! ActiveRecord::RecordInvalid is raised
75
+ # ! Heimdallr::PermissionError is raised
69
76
 
70
77
  # You can use any valid ActiveRecord validators, too:
71
78
  secure.create! content: "Top Secret", secrecy_level: 10
@@ -80,24 +87,15 @@ secure.find 2
80
87
 
81
88
  The DSL is described in documentation for {Heimdallr::Model}.
82
89
 
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
- --------------
90
+ Ideology
91
+ --------
95
92
 
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.
93
+ Heimdallr aims to make security explicit, but nevertheless convenient. It does not allow one to call any
94
+ implicit operations which may be used maliciously; instead, it forces you to explicitly call `#insecure`
95
+ method which returns the underlying object. This single point of entry is easily recognizable with code.
99
96
 
100
- The interface is described in documentation for {Heimdallr::Resource}.
97
+ Heimdallr would raise exceptions in all cases of forbidden or potentially unsecure access except for attribute
98
+ reading to allow for writing uncrufted code in templates (particularly [JBuilder](http://github.com/rails/jbuilder) ones).
101
99
 
102
100
  Compatibility
103
101
  -------------
@@ -3,17 +3,15 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "heimdallr"
6
- s.version = "0.0.2"
7
- s.authors = ["Peter Zotov"]
8
- s.email = ["whitequark@whitequark.org"]
6
+ s.version = "1.0.0.RC2"
7
+ s.authors = ["Peter Zotov", "Boris Staal"]
8
+ s.email = ["whitequark@whitequark.org", "boris@roundlake.ru"]
9
9
  s.homepage = "http://github.com/roundlake/heimdallr"
10
10
  s.summary = %q{Heimdallr is an ActiveModel extension which provides object- and field-level access control.}
11
11
  s.description = %q{Heimdallr aims to provide an easy to configure and efficient object- and field-level access
12
12
  control solution, reusing proven patterns from gems like CanCan and allowing one to manage permissions in a very
13
13
  fine-grained manner.}
14
14
 
15
- s.rubyforge_project = "heimdallr"
16
-
17
15
  s.files = `git ls-files`.split("\n")
18
16
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -1,15 +1,7 @@
1
1
  require "active_support"
2
+ require "active_support/core_ext/module/delegation"
2
3
  require "active_model"
3
4
 
4
- require "heimdallr/version"
5
-
6
- require "heimdallr/proxy/collection"
7
- require "heimdallr/proxy/record"
8
- require "heimdallr/validator"
9
- require "heimdallr/evaluator"
10
- require "heimdallr/model"
11
- require "heimdallr/resource"
12
-
13
5
  # See {file:README.yard}.
14
6
  module Heimdallr
15
7
  class << self
@@ -33,9 +25,10 @@ module Heimdallr
33
25
  #
34
26
  # @return [Boolean]
35
27
  attr_accessor :allow_insecure_associations
36
- self.allow_insecure_associations = false
37
28
  end
38
29
 
30
+ self.allow_insecure_associations = false
31
+
39
32
  # {PermissionError} is raised when a security policy prevents
40
33
  # a called operation from being executed.
41
34
  class PermissionError < StandardError; end
@@ -43,4 +36,14 @@ module Heimdallr
43
36
  # {InsecureOperationError} is raised when a potentially unsafe
44
37
  # operation is about to be executed.
45
38
  class InsecureOperationError < StandardError; end
46
- end
39
+
40
+ # Heimdallr uses proxies to control access to restricted scopes and collections.
41
+ module Proxy; end
42
+ end
43
+
44
+ require "heimdallr/proxy/collection"
45
+ require "heimdallr/proxy/record"
46
+ require "heimdallr/validator"
47
+ require "heimdallr/evaluator"
48
+ require "heimdallr/model"
49
+ require "heimdallr/legacy_resource"
@@ -6,18 +6,20 @@ module Heimdallr
6
6
  # is whitelisting safe actions, not blacklisting unsafe ones. This is by design
7
7
  # and is not going to change.
8
8
  #
9
+ # The field +#id+ is whitelisted by default.
10
+ #
9
11
  # The DSL consists of three functions: {#scope}, {#can} and {#cannot}.
10
12
  class Evaluator
11
13
  attr_reader :allowed_fields, :fixtures, :validators
12
14
 
13
- # Create a new Evaluator for the ActiveModel-descending class +model_class+,
15
+ # Create a new Evaluator for the +ActiveRecord+-derived class +model_class+,
14
16
  # and use +block+ to infer restrictions for any security context passed.
15
17
  def initialize(model_class, block)
16
18
  @model_class, @block = model_class, block
17
19
 
18
20
  @scopes = {}
19
21
  @allowed_fields = {}
20
- @validations = {}
22
+ @validators = {}
21
23
  @fixtures = {}
22
24
  end
23
25
 
@@ -36,7 +38,7 @@ module Heimdallr
36
38
  # This form accepts an implicit lambda.
37
39
  #
38
40
  # @example
39
- # scope :fetch do
41
+ # scope :fetch do |user|
40
42
  # if user.manager?
41
43
  # scoped
42
44
  # else
@@ -72,15 +74,18 @@ module Heimdallr
72
74
  # @param [Symbol, Array<Symbol>] actions one or more action names
73
75
  # @param [Hash<Hash, Object>] fields field restrictions
74
76
  def can(actions, fields=@model_class.attribute_names)
75
- Array(actions).each do |action|
77
+ Array(actions).map(&:to_sym).each do |action|
76
78
  case fields
77
79
  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)
80
+ @allowed_fields[action] += fields.keys.map(&:to_sym)
81
+ @validators[action] += create_validators(fields)
82
+
83
+ fixtures = extract_fixtures(fields)
84
+ @fixtures[action] = @fixtures[action].merge fixtures
85
+ @allowed_fields[action] -= fixtures.keys
81
86
 
82
87
  else # an array or a field name
83
- @allowed_fields[action] += Array(fields)
88
+ @allowed_fields[action] += Array(fields).map(&:to_sym)
84
89
  end
85
90
  end
86
91
  end
@@ -91,8 +96,8 @@ module Heimdallr
91
96
  # @param [Symbol, Array<Symbol>] actions one or more action names
92
97
  # @param [Array<Symbol>] fields field list
93
98
  def cannot(actions, fields)
94
- Array(actions).each do |action|
95
- @allowed_fields[action] -= fields
99
+ Array(actions).map(&:to_sym).each do |action|
100
+ @allowed_fields[action] -= fields.map(&:to_sym)
96
101
  @fixtures.delete_at *fields
97
102
  end
98
103
  end
@@ -104,25 +109,59 @@ module Heimdallr
104
109
  # @param scope name of the scope
105
110
  # @param basic_scope the scope to which scope +name+ will be applied. Defaults to +:fetch+.
106
111
  #
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])
112
+ # @return +ActiveRecord+ scope.
113
+ #
114
+ # @raise [RuntimeError] if the scope is not defined
115
+ def request_scope(name=:fetch, basic_scope=nil)
116
+ unless @scopes.has_key?(name)
117
+ raise RuntimeError, "The #{name.inspect} scope does not exist"
118
+ end
119
+
120
+ if name == :fetch && basic_scope.nil?
121
+ @model_class.instance_exec(&@scopes[:fetch])
111
122
  else
112
- basic_scope.instance_exec(&@scopes[name])
123
+ (basic_scope || request_scope(:fetch)).instance_exec(&@scopes[name])
113
124
  end
114
125
  end
115
126
 
127
+ # Check if any explicit restrictions were defined for +action+.
128
+ # +can :create, []+ _is_ an explicit restriction for action +:create+.
129
+ #
130
+ # @return Boolean
131
+ def can?(action)
132
+ @allowed_fields.include? action
133
+ end
134
+
135
+ # Return a Hash to be mixed in in +reflect_on_security+ methods of {Proxy::Collection}
136
+ # and {Proxy::Record}.
137
+ def reflection
138
+ {
139
+ operations: [ :view, :create, :update ].select { |op| can? op }
140
+ }
141
+ end
142
+
116
143
  # Compute the restrictions for a given +context+. Invokes a +block+ passed to the
117
144
  # +initialize+ once.
145
+ #
146
+ # @raise [RuntimeError] if the evaluated block did not define a set of valid restrictions
118
147
  def evaluate(context)
119
148
  if context != @last_context
120
149
  @scopes = {}
121
150
  @allowed_fields = Hash.new { [] }
122
151
  @validators = Hash.new { [] }
123
- @fixtures = Hash.new { [] }
152
+ @fixtures = Hash.new { {} }
153
+
154
+ @allowed_fields[:view] += [ :id ]
124
155
 
125
- instance_exec context, &block
156
+ instance_exec context, &@block
157
+
158
+ unless @scopes[:fetch]
159
+ raise RuntimeError, "A :fetch scope must be defined"
160
+ end
161
+
162
+ @allowed_fields.each do |action, fields|
163
+ fields.uniq!
164
+ end
126
165
 
127
166
  [@scopes, @allowed_fields, @validators, @fixtures].
128
167
  map(&:freeze)
@@ -139,7 +178,7 @@ module Heimdallr
139
178
  #
140
179
  # @return [Array<ActiveModel::Validator>]
141
180
  def create_validators(fields)
142
- validators = {}
181
+ validators = []
143
182
 
144
183
  fields.each do |attribute, validations|
145
184
  next unless validations.is_a? Hash
@@ -153,7 +192,7 @@ module Heimdallr
153
192
  raise ArgumentError, "Unknown validator: '#{key}'"
154
193
  end
155
194
 
156
- validators[attribute] = validator.new(_parse_validates_options(options).merge(:attributes => [ attribute ]))
195
+ validators << validator.new(_parse_validates_options(options).merge(:attributes => [ attribute ]))
157
196
  end
158
197
  end
159
198
 
@@ -167,7 +206,7 @@ module Heimdallr
167
206
  fields.each do |attribute, options|
168
207
  next if options.is_a? Hash
169
208
 
170
- fixtures[attribute] = options
209
+ fixtures[attribute.to_sym] = options
171
210
  end
172
211
 
173
212
  fixtures
@@ -1,5 +1,8 @@
1
1
  module Heimdallr
2
- # Heimdallr {Resource} is a boilerplate for simple creation of REST endpoints, most of which are
2
+ #
3
+ # @deprecated Will be removed ASAP. Please don't use it in favour of http://github.com/roundlake/heimdallr-resource/
4
+ #
5
+ # Heimdallr {LegacyResource} is a boilerplate for simple creation of REST endpoints, most of which are
3
6
  # quite similar and thus may share a lot of code.
4
7
  #
5
8
  # The minimal controller possible would be:
@@ -28,19 +31,21 @@ module Heimdallr
28
31
  # Resource only works with ActiveRecord.
29
32
  #
30
33
  # See also {Resource::ClassMethods}.
31
- module Resource
34
+ module LegacyResource
32
35
  # @group Actions
33
36
 
34
37
  # +GET /resources+
35
38
  #
36
39
  # This action does nothing by itself, but it has a +load_all_resources+ filter attached.
37
40
  def index
41
+ render_data
38
42
  end
39
43
 
40
44
  # +GET /resource/1+
41
45
  #
42
46
  # This action does nothing by itself, but it has a +load_one_resource+ filter attached.
43
47
  def show
48
+ render_data
44
49
  end
45
50
 
46
51
  # +GET /resources/new+
@@ -61,16 +66,15 @@ module Heimdallr
61
66
  # This action creates one or more records from the passed parameters.
62
67
  # It can accept both arrays of attribute hashes and single attribute hashes.
63
68
  #
64
- # After the creation, it calls {#render_resources}.
69
+ # After the creation, it calls {#render_data}.
65
70
  #
66
71
  # See also {#load_referenced_resources} and {#with_objects_from_params}.
67
72
  def create
68
- with_objects_from_params do |attributes, index|
69
- scoped_model.restrict(security_context).
70
- create!(attributes)
73
+ with_objects_from_params(replace: true) do |object, attributes|
74
+ restricted_model.create(attributes)
71
75
  end
72
76
 
73
- render_resources
77
+ render_data verify: true
74
78
  end
75
79
 
76
80
  # +GET /resources/1/edit+
@@ -79,7 +83,7 @@ module Heimdallr
79
83
  # See also {#new}.
80
84
  def edit
81
85
  render :json => {
82
- :fields => model.restrict(security_context).allowed_fields[:update]
86
+ :fields => model.restrictions(security_context).allowed_fields[:update]
83
87
  }
84
88
  end
85
89
 
@@ -90,15 +94,15 @@ module Heimdallr
90
94
  # and expects them to be in the order corresponding to the order of actual
91
95
  # attribute hashes.
92
96
  #
93
- # After the updating, it calls {#render_resources}.
97
+ # After the updating, it calls {#render_data}.
94
98
  #
95
99
  # See also {#load_referenced_resources} and {#with_objects_from_params}.
96
100
  def update
97
- with_objects_from_params do |attributes, index|
98
- @resources[index].update_attributes! attributes
101
+ with_objects_from_params do |object, attributes|
102
+ object.update_attributes attributes
99
103
  end
100
104
 
101
- render_resources
105
+ render_data verify: true
102
106
  end
103
107
 
104
108
  # +DELETE /resources/1,2+
@@ -108,8 +112,8 @@ module Heimdallr
108
112
  #
109
113
  # See also {#load_referenced_resources}.
110
114
  def destroy
111
- model.transaction do
112
- @resources.each &:destroy
115
+ with_objects_from_params do |object, attributes|
116
+ object.destroy
113
117
  end
114
118
 
115
119
  render :json => {}, :status => :ok
@@ -158,34 +162,50 @@ module Heimdallr
158
162
  self.model.scoped
159
163
  end
160
164
 
165
+ # Return the scoped and restricted model. By default this method
166
+ # restricts the result of {#scoped_model} with +security_context+,
167
+ # which is expected to be defined on this class or its ancestors.
168
+ def restricted_model
169
+ scoped_model.restrict(security_context, implicit: true)
170
+ end
171
+
161
172
  # Loads all resources in the current scope to +@resources+.
162
173
  #
163
174
  # Is automatically applied to {#index}.
164
175
  def load_all_resources
165
- @resources = scoped_model
166
- end
167
-
168
- # Loads one resource from the current scope, referenced by <code>params[:id]</code>,
169
- # to +@resource+.
170
- #
171
- # Is automatically applied to {#show}.
172
- def load_one_resource
173
- @resource = scoped_model.find(params[:id])
176
+ @multiple_resources = true
177
+ @resources = restricted_model
174
178
  end
175
179
 
176
180
  # Loads several resources from the current scope, referenced by <code>params[:id]</code>
177
181
  # with a comma-separated string like "1,2,3", to +@resources+.
178
182
  #
179
- # Is automatically applied to {#update} and {#destroy}.
183
+ # Is automatically applied to {#show}, {#update} and {#destroy}.
180
184
  def load_referenced_resources
181
- @resources = scoped_model.find(params[:id].split(','))
185
+ if params[:id][0] == '*'
186
+ @multiple_resources = true
187
+ @resources = restricted_model.find(params[:id][1..-1].split(','))
188
+ else
189
+ @multiple_resources = false
190
+ @resource = restricted_model.find(params[:id])
191
+ end
182
192
  end
183
193
 
184
194
  # Render a modified collection in {#create}, {#update} and similar actions.
185
- #
186
- # By default, it invokes a template for +index+.
187
- def render_resources
188
- render :action => :index
195
+ def render_data(options={})
196
+ if @multiple_resources
197
+ if options[:verify] && @resources.any?(&:invalid?)
198
+ render :json => { errors: @resources.map(&:errors) }, :status => :unprocessable_entity
199
+ else
200
+ render :action => :index
201
+ end
202
+ else
203
+ if options[:verify] && @resource.invalid?
204
+ render :json => @resource.errors, :status => :unprocessable_entity
205
+ else
206
+ render :action => :show
207
+ end
208
+ end
189
209
  end
190
210
 
191
211
  # Fetch one or several objects passed in +params+ and yield them to a block,
@@ -194,14 +214,28 @@ module Heimdallr
194
214
  # @yield [attributes, index]
195
215
  # @yieldparam [Hash] attributes
196
216
  # @yieldparam [Integer] index
197
- def with_objects_from_params
217
+ def with_objects_from_params(options={})
198
218
  model.transaction do
199
- if params.has_key? model.name.underscore
200
- yield params[model.name.underscore], 0
219
+ if @multiple_resources
220
+ begin
221
+ name = model.name.underscore.pluralize
222
+ if params[name].is_a? Hash
223
+ enumerator = params[name].keys.each
224
+ else
225
+ enumerator = params[name].each_index
226
+ end
227
+
228
+ result = enumerator.map do |index|
229
+ yield(@resources[index.to_i], params[name][index])
230
+ end
231
+ ensure
232
+ @resources = result if options[:replace]
233
+ end
201
234
  else
202
- params[model.name.underscore.pluralize].
203
- each_with_index do |attributes, index|
204
- yield attributes, index
235
+ begin
236
+ result = yield(@resource, params[model.name.underscore])
237
+ ensure
238
+ @resource = result if options[:replace]
205
239
  end
206
240
  end
207
241
  end
@@ -232,7 +266,7 @@ module Heimdallr
232
266
  # For convenience, you can pass additional actions to register with default filters in
233
267
  # +options+. It is also possible to use +append_before_filter+.
234
268
  #
235
- # @param [Class] model An ActiveModel or ActiveRecord model class.
269
+ # @param [Class] model an +ActiveRecord+-derived model class
236
270
  # @option options [Array<Symbol>] :index
237
271
  # Additional actions to be covered by {Heimdallr::Resource#load_all_resources}.
238
272
  # @option options [Array<Symbol>] :member
@@ -242,9 +276,8 @@ module Heimdallr
242
276
  def resource_for(model, options={})
243
277
  @model = model.to_s.camelize.constantize
244
278
 
245
- before_filter :load_all_resources, only: [ :index ].concat(options[:all] || [])
246
- before_filter :load_one_resource, only: [ :show ].concat(options[:member] || [])
247
- before_filter :load_referenced_resources, only: [ :update, :destroy ].concat(options[:collection] || [])
279
+ before_filter :load_all_resources, only: [ :index ].concat(options[:all] || [])
280
+ before_filter :load_referenced_resources, only: [ :show, :update, :destroy ].concat(options[:referenced] || [])
248
281
  end
249
282
  end
250
283
  end
@@ -10,6 +10,9 @@ module Heimdallr
10
10
  # end
11
11
  # end
12
12
  #
13
+ # {Heimdallr::Model} should be included prior to any other modules, as it may omit
14
+ # some named scopes defined by those if it is not.
15
+ #
13
16
  # @todo Improve description
14
17
  module Model
15
18
  extend ActiveSupport::Concern
@@ -28,11 +31,11 @@ module Heimdallr
28
31
  # @param [Object] context security context
29
32
  # @param [Symbol] action kind of actions which will be performed
30
33
  # @return [Proxy::Collection]
31
- def restrict(context=nil, &block)
34
+ def restrict(context=nil, options={}, &block)
32
35
  if block
33
- @restrictions = Evaluator.new(self, &block)
36
+ @restrictions = Evaluator.new(self, block)
34
37
  else
35
- Proxy::Collection.new(context, restrictions(context).request_scope)
38
+ Proxy::Collection.new(context, restrictions(context).request_scope, options)
36
39
  end
37
40
  end
38
41
 
@@ -42,13 +45,40 @@ module Heimdallr
42
45
  def restrictions(context)
43
46
  @restrictions.evaluate(context)
44
47
  end
48
+
49
+ # @api private
50
+ #
51
+ # An internal attribute to store the list of user-defined name scopes.
52
+ # It is required because ActiveRecord does not provide any introspection for
53
+ # named scopes.
54
+ attr_accessor :heimdallr_scopes
55
+
56
+ # An interceptor for named scopes which adds them to {#heimdallr_scopes} list.
57
+ def scope(name, *args)
58
+ self.heimdallr_scopes ||= []
59
+ self.heimdallr_scopes.push name
60
+
61
+ super
62
+ end
63
+
64
+ # @api private
65
+ #
66
+ # An internal attribute to store the list of user-defined relation-like methods
67
+ # which return ActiveRecord family objects and can be automatically restricted.
68
+ attr_accessor :heimdallr_relations
69
+
70
+ # A DSL method for defining relation-like methods.
71
+ def heimdallr_relation(*methods)
72
+ self.heimdallr_relations ||= []
73
+ self.heimdallr_relations += methods.map(&:to_sym)
74
+ end
45
75
  end
46
76
 
47
77
  # Return a secure proxy object for this record.
48
78
  #
49
79
  # @return [Record::Proxy]
50
- def restrict(context, action)
51
- Record::Proxy.new(context, self)
80
+ def restrict(context, options={})
81
+ Proxy::Record.new(context, self, options)
52
82
  end
53
83
 
54
84
  # @api private
@@ -57,9 +87,16 @@ module Heimdallr
57
87
  # the context in which this object is currently being saved.
58
88
  attr_accessor :heimdallr_validators
59
89
 
90
+ # @api private
91
+ #
92
+ # An internal method to run Heimdallr security validators, when applicable.
93
+ def heimdallr_validations
94
+ validates_with Heimdallr::Validator
95
+ end
96
+
60
97
  def self.included(klass)
61
- klass.const_eval do
62
- validates_with Heimdallr::Validator
98
+ klass.class_eval do
99
+ validate :heimdallr_validations
63
100
  end
64
101
  end
65
102
  end
@@ -2,27 +2,225 @@ module Heimdallr
2
2
  # A security-aware proxy for +ActiveRecord+ scopes. This class validates all the
3
3
  # method calls and either forwards them to the encapsulated scope or raises
4
4
  # an exception.
5
+ #
6
+ # There are two kinds of collection proxies, explicit and implicit, which instantiate
7
+ # the corresponding types of record proxies. See also {Proxy::Record}.
5
8
  class Proxy::Collection
9
+ include Enumerable
10
+
6
11
  # Create a collection proxy.
7
- # @param context security context
8
- # @param object proxified scope
9
- def initialize(context, scope)
10
- @context, @scope = context, scope
12
+ #
13
+ # The +scope+ is expected to be already restricted with +:fetch+ scope.
14
+ #
15
+ # @param context security context
16
+ # @param scope proxified scope
17
+ # @option options [Boolean] implicit proxy type
18
+ def initialize(context, scope, options={})
19
+ @context, @scope, @options = context, scope, options
11
20
 
12
- @restrictions = @object.class.restrictions(context)
21
+ @restrictions = @scope.restrictions(context)
13
22
  end
14
23
 
15
24
  # Collections cannot be restricted twice.
16
25
  #
17
26
  # @raise [RuntimeError]
18
- def restrict(context)
27
+ def restrict(*args)
19
28
  raise RuntimeError, "Collections cannot be restricted twice"
20
29
  end
21
30
 
22
- # Dummy method_missing.
23
- # @todo Write some actual dispatching logic.
24
- def method_missing(method, *args, &block)
25
- @scope.send method, *args
31
+ # @private
32
+ # @macro [attach] delegate_as_constructor
33
+ # A proxy for +$1+ method which adds fixtures to the attribute list and
34
+ # returns a restricted record.
35
+ def self.delegate_as_constructor(name, method)
36
+ class_eval(<<-EOM, __FILE__, __LINE__)
37
+ def #{name}(attributes={})
38
+ record = @restrictions.request_scope(:fetch).new.restrict(@context, @options)
39
+ record.#{method}(attributes.merge(@restrictions.fixtures[:create]))
40
+ record
41
+ end
42
+ EOM
43
+ end
44
+
45
+ # @private
46
+ # @macro [attach] delegate_as_scope
47
+ # A proxy for +$1+ method which returns a restricted scope.
48
+ def self.delegate_as_scope(name)
49
+ class_eval(<<-EOM, __FILE__, __LINE__)
50
+ def #{name}(*args)
51
+ Proxy::Collection.new(@context, @scope.#{name}(*args), @options)
52
+ end
53
+ EOM
54
+ end
55
+
56
+ # @private
57
+ # @macro [attach] delegate_as_destroyer
58
+ # A proxy for +$1+ method which works on a +:delete+ scope.
59
+ def self.delegate_as_destroyer(name)
60
+ class_eval(<<-EOM, __FILE__, __LINE__)
61
+ def #{name}(*args)
62
+ @restrictions.request_scope(:delete, @scope).#{name}(*args)
63
+ end
64
+ EOM
65
+ end
66
+
67
+ # @private
68
+ # @macro [attach] delegate_as_record
69
+ # A proxy for +$1+ method which returns a restricted record.
70
+ def self.delegate_as_record(name)
71
+ class_eval(<<-EOM, __FILE__, __LINE__)
72
+ def #{name}(*args)
73
+ @scope.#{name}(*args).restrict(@context, @options)
74
+ end
75
+ EOM
76
+ end
77
+
78
+ # @private
79
+ # @macro [attach] delegate_as_records
80
+ # A proxy for +$1+ method which returns an array of restricted records.
81
+ def self.delegate_as_records(name)
82
+ class_eval(<<-EOM, __FILE__, __LINE__)
83
+ def #{name}(*args)
84
+ @scope.#{name}(*args).map do |element|
85
+ element.restrict(@context, @options)
86
+ end
87
+ end
88
+ EOM
89
+ end
90
+
91
+ # @private
92
+ # @macro [attach] delegate_as_value
93
+ # A proxy for +$1+ method which returns a raw value.
94
+ def self.delegate_as_value(name)
95
+ class_eval(<<-EOM, __FILE__, __LINE__)
96
+ def #{name}(*args)
97
+ @scope.#{name}(*args)
98
+ end
99
+ EOM
100
+ end
101
+
102
+ delegate_as_constructor :build, :assign_attributes
103
+ delegate_as_constructor :new, :assign_attributes
104
+ delegate_as_constructor :create, :update_attributes
105
+ delegate_as_constructor :create!, :update_attributes!
106
+
107
+ delegate_as_scope :scoped
108
+ delegate_as_scope :uniq
109
+ delegate_as_scope :where
110
+ delegate_as_scope :joins
111
+ delegate_as_scope :includes
112
+ delegate_as_scope :eager_load
113
+ delegate_as_scope :preload
114
+ delegate_as_scope :lock
115
+ delegate_as_scope :limit
116
+ delegate_as_scope :offset
117
+ delegate_as_scope :order
118
+ delegate_as_scope :reorder
119
+ delegate_as_scope :reverse_order
120
+ delegate_as_scope :extending
121
+
122
+ delegate_as_value :empty?
123
+ delegate_as_value :any?
124
+ delegate_as_value :many?
125
+ delegate_as_value :include?
126
+ delegate_as_value :exists?
127
+ delegate_as_value :size
128
+ delegate_as_value :length
129
+
130
+ delegate_as_value :calculate
131
+ delegate_as_value :count
132
+ delegate_as_value :average
133
+ delegate_as_value :sum
134
+ delegate_as_value :maximum
135
+ delegate_as_value :minimum
136
+ delegate_as_value :pluck
137
+
138
+ delegate_as_destroyer :delete
139
+ delegate_as_destroyer :delete_all
140
+ delegate_as_destroyer :destroy
141
+ delegate_as_destroyer :destroy_all
142
+
143
+ delegate_as_record :first
144
+ delegate_as_record :first!
145
+ delegate_as_record :last
146
+ delegate_as_record :last!
147
+
148
+ delegate_as_records :all
149
+ delegate_as_records :to_a
150
+ delegate_as_records :to_ary
151
+
152
+ # A proxy for +find+ which restricts the returned record or records.
153
+ #
154
+ # @return [Proxy::Record, Array<Proxy::Record>]
155
+ def find(*args)
156
+ result = @scope.find(*args)
157
+
158
+ if result.is_a? Enumerable
159
+ result.map do |element|
160
+ element.restrict(@context, @options)
161
+ end
162
+ else
163
+ result.restrict(@context, @options)
164
+ end
165
+ end
166
+
167
+ # A proxy for +each+ which restricts the yielded records.
168
+ #
169
+ # @yield [record]
170
+ # @yieldparam [Proxy::Record] record
171
+ def each
172
+ @scope.each do |record|
173
+ yield record.restrict(@context, @options)
174
+ end
175
+ end
176
+
177
+ # Wraps a scope or a record in a corresponding proxy.
178
+ def method_missing(method, *args)
179
+ if method =~ /^find_all_by/
180
+ @scope.send(method, *args).map do |element|
181
+ element.restrict(@context, @options)
182
+ end
183
+ elsif method =~ /^find_by/
184
+ @scope.send(method, *args).restrict(@context, @options)
185
+ elsif @scope.heimdallr_scopes && @scope.heimdallr_scopes.include?(method)
186
+ Proxy::Collection.new(@context, @scope.send(method, *args), @options)
187
+ elsif @scope.respond_to? method
188
+ raise InsecureOperationError,
189
+ "Potentially insecure method #{method} was called"
190
+ else
191
+ super
192
+ end
193
+ end
194
+
195
+ # Return the underlying scope.
196
+ #
197
+ # @return ActiveRecord scope
198
+ def insecure
199
+ @scope
200
+ end
201
+
202
+ # Describes the proxy and proxified scope.
203
+ #
204
+ # @return [String]
205
+ def inspect
206
+ "#<Heimdallr::Proxy::Collection: #{@scope.to_sql}>"
207
+ end
208
+
209
+ # Return the associated security metadata. The returned hash will contain keys
210
+ # +:context+, +:scope+ and +:options+, corresponding to the parameters in
211
+ # {#initialize}, and +:model+, representing the model class.
212
+ #
213
+ # Such a name was deliberately selected for this method in order to reduce namespace
214
+ # pollution.
215
+ #
216
+ # @return [Hash]
217
+ def reflect_on_security
218
+ {
219
+ model: @scope,
220
+ context: @context,
221
+ scope: @scope,
222
+ options: @options
223
+ }.merge(@restrictions.reflection)
26
224
  end
27
225
  end
28
226
  end
@@ -5,12 +5,18 @@ module Heimdallr
5
5
  #
6
6
  # The #touch method call isn't considered a security threat and as such, it is
7
7
  # forwarded to the underlying object directly.
8
+ #
9
+ # Record proxies can be of two types, implicit and explicit. Implicit proxies
10
+ # return +nil+ on access to methods forbidden by the current security context;
11
+ # explicit proxies raise an {Heimdallr::PermissionError} instead.
8
12
  class Proxy::Record
9
13
  # Create a record proxy.
10
- # @param context security context
11
- # @param object proxified record
12
- def initialize(context, record)
13
- @context, @record = context, record
14
+ #
15
+ # @param context security context
16
+ # @param object proxified record
17
+ # @option options [Boolean] implicit proxy type
18
+ def initialize(context, record, options={})
19
+ @context, @record, @options = context, record, options
14
20
 
15
21
  @restrictions = @record.class.restrictions(context)
16
22
  end
@@ -37,28 +43,34 @@ module Heimdallr
37
43
  # A proxy for +attributes+ method which removes all attributes
38
44
  # without +:view+ permission.
39
45
  def attributes
40
- @restrictions.filter_attributes(:view, @record.attributes)
46
+ @record.attributes.tap do |attributes|
47
+ attributes.keys.each do |key|
48
+ unless @restrictions.allowed_fields[:view].include? key.to_sym
49
+ attributes[key] = nil
50
+ end
51
+ end
52
+ end
41
53
  end
42
54
 
43
- # A proxy for +update_attributes+ method which removes all attributes
44
- # without +:update+ permission and invokes +#save+.
55
+ # A proxy for +update_attributes+ method.
56
+ # See also {#save}.
45
57
  #
46
58
  # @raise [Heimdallr::PermissionError]
47
59
  def update_attributes(attributes, options={})
48
60
  @record.with_transaction_returning_status do
49
61
  @record.assign_attributes(attributes, options)
50
- self.save
62
+ save
51
63
  end
52
64
  end
53
65
 
54
- # A proxy for +update_attributes!+ method which removes all attributes
55
- # without +:update+ permission and invokes +#save!+.
66
+ # A proxy for +update_attributes!+ method.
67
+ # See also {#save!}.
56
68
  #
57
69
  # @raise [Heimdallr::PermissionError]
58
- def update_attributes(attributes, options={})
70
+ def update_attributes!(attributes, options={})
59
71
  @record.with_transaction_returning_status do
60
72
  @record.assign_attributes(attributes, options)
61
- self.save!
73
+ save!
62
74
  end
63
75
  end
64
76
 
@@ -93,13 +105,35 @@ module Heimdallr
93
105
  class_eval(<<-EOM, __FILE__, __LINE__)
94
106
  def #{method}
95
107
  scope = @restrictions.request_scope(:delete)
96
- if scope.where({ @record.primary_key => @record.to_key }).count != 0
108
+ if scope.where({ @record.class.primary_key => @record.to_key }).count != 0
97
109
  @record.#{method}
98
110
  end
99
111
  end
100
112
  EOM
101
113
  end
102
114
 
115
+ # @method valid?
116
+ # @macro delegate
117
+ delegate :valid?, :to => :@record
118
+
119
+ # @method invalid?
120
+ # @macro delegate
121
+ delegate :invalid?, :to => :@record
122
+
123
+ # @method errors
124
+ # @macro delegate
125
+ delegate :errors, :to => :@record
126
+
127
+ # @method assign_attributes
128
+ # @macro delegate
129
+ delegate :assign_attributes, :to => :@record
130
+
131
+ # Class name of the underlying model.
132
+ # @return [String]
133
+ def class_name
134
+ @record.class.name
135
+ end
136
+
103
137
  # Records cannot be restricted twice.
104
138
  #
105
139
  # @raise [RuntimeError]
@@ -131,28 +165,36 @@ module Heimdallr
131
165
  suffix = nil
132
166
  end
133
167
 
134
- if defined?(ActiveRecord) && @record.is_a?(ActiveRecord::Base) &&
135
- association = @record.class.reflect_on_association(method)
168
+ if (defined?(ActiveRecord) && @record.is_a?(ActiveRecord::Reflection) &&
169
+ association = @record.class.reflect_on_association(method)) ||
170
+ (!@record.class.heimdallr_relations.nil? &&
171
+ @record.class.heimdallr_relations.include?(normalized_method))
136
172
  referenced = @record.send(method, *args)
137
173
 
138
- if referenced.respond_to? :restrict
139
- referenced.restrict(@context)
174
+ if referenced.nil?
175
+ nil
176
+ elsif referenced.respond_to? :restrict
177
+ referenced.restrict(@context, @options)
140
178
  elsif Heimdallr.allow_insecure_associations
141
179
  referenced
142
180
  else
143
181
  raise Heimdallr::InsecureOperationError,
144
- "Attempt to fetch insecure association #{method}. Try #insecure."
182
+ "Attempt to fetch insecure association #{method}. Try #insecure"
145
183
  end
146
184
  elsif @record.respond_to? method
147
- if [nil, '?'].include?(suffix) &&
148
- @restrictions.allowed_fields[:view].include?(normalized_method)
149
- # Reading an attribute
150
- @record.send method, *args, &block
185
+ if [nil, '?'].include?(suffix)
186
+ if @restrictions.allowed_fields[:view].include?(normalized_method)
187
+ @record.send method, *args, &block
188
+ elsif @options[:implicit]
189
+ nil
190
+ else
191
+ raise Heimdallr::PermissionError, "Attempt to fetch non-whitelisted attribute #{method}"
192
+ end
151
193
  elsif suffix == '='
152
194
  @record.send method, *args
153
195
  else
154
196
  raise Heimdallr::PermissionError,
155
- "Non-whitelisted method #{method} is called for #{@record.inspect} on #{@action}."
197
+ "Non-whitelisted method #{method} is called for #{@record.inspect} "
156
198
  end
157
199
  else
158
200
  super
@@ -166,16 +208,30 @@ module Heimdallr
166
208
  @record
167
209
  end
168
210
 
211
+ # Return an implicit variant of this proxy.
212
+ #
213
+ # @return [Heimdallr::Proxy::Record]
214
+ def implicit
215
+ Proxy::Record.new(@context, @record, @options.merge(implicit: true))
216
+ end
217
+
218
+ # Return an explicit variant of this proxy.
219
+ #
220
+ # @return [Heimdallr::Proxy::Record]
221
+ def explicit
222
+ Proxy::Record.new(@context, @record, @options.merge(implicit: false))
223
+ end
224
+
169
225
  # Describes the proxy and proxified object.
170
226
  #
171
227
  # @return [String]
172
228
  def inspect
173
- "#<Heimdallr::Proxy(#{@action}): #{@record.inspect}>"
229
+ "#<Heimdallr::Proxy::Record: #{@record.inspect}>"
174
230
  end
175
231
 
176
232
  # Return the associated security metadata. The returned hash will contain keys
177
- # +:context+ and +:object+, corresponding to the parameters in
178
- # {#initialize}.
233
+ # +:context+, +:record+, +:options+, corresponding to the parameters in
234
+ # {#initialize}, and +:model+, representing the model class.
179
235
  #
180
236
  # Such a name was deliberately selected for this method in order to reduce namespace
181
237
  # pollution.
@@ -183,9 +239,11 @@ module Heimdallr
183
239
  # @return [Hash]
184
240
  def reflect_on_security
185
241
  {
242
+ model: @record.class,
186
243
  context: @context,
187
- object: @record
188
- }
244
+ record: @record,
245
+ options: @options
246
+ }.merge(@restrictions.reflection)
189
247
  end
190
248
 
191
249
  protected
@@ -203,10 +261,11 @@ module Heimdallr
203
261
  action = :update
204
262
  end
205
263
 
206
- fixtures = @restrictions.fixtures[action]
207
- validators = @restrictions.validators[action]
264
+ allowed_fields = @restrictions.allowed_fields[action]
265
+ fixtures = @restrictions.fixtures[action]
266
+ validators = @restrictions.validators[action]
208
267
 
209
- @record.changed.each do |attribute|
268
+ @record.changed.map(&:to_sym).each do |attribute|
210
269
  value = @record.send attribute
211
270
 
212
271
  if fixtures.has_key? attribute
@@ -214,6 +273,9 @@ module Heimdallr
214
273
  raise Heimdallr::PermissionError,
215
274
  "Attribute #{attribute} value (#{value}) is not equal to a fixture (#{fixtures[attribute]})"
216
275
  end
276
+ elsif !allowed_fields.include? attribute
277
+ raise Heimdallr::PermissionError,
278
+ "Attribute #{attribute} is not allowed to change"
217
279
  end
218
280
  end
219
281
 
@@ -231,6 +293,18 @@ module Heimdallr
231
293
  raise Heimdallr::InsecureOperationError,
232
294
  "Saving while omitting validation would omit security validations too"
233
295
  end
296
+
297
+ if @record.new_record?
298
+ unless @restrictions.can? :create
299
+ raise Heimdallr::InsecureOperationError,
300
+ "Creating was not explicitly allowed"
301
+ end
302
+ else
303
+ unless @restrictions.can? :update
304
+ raise Heimdallr::InsecureOperationError,
305
+ "Updating was not explicitly allowed"
306
+ end
307
+ end
234
308
  end
235
309
  end
236
310
  end
metadata CHANGED
@@ -1,19 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heimdallr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
5
- prerelease:
4
+ version: 1.0.0.RC2
5
+ prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Peter Zotov
9
+ - Boris Staal
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2012-02-06 00:00:00.000000000 Z
13
+ date: 2012-04-02 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: activesupport
16
- requirement: &83064660 !ruby/object:Gem::Requirement
17
+ requirement: &76679020 !ruby/object:Gem::Requirement
17
18
  none: false
18
19
  requirements:
19
20
  - - ! '>='
@@ -21,10 +22,10 @@ dependencies:
21
22
  version: 3.0.0
22
23
  type: :runtime
23
24
  prerelease: false
24
- version_requirements: *83064660
25
+ version_requirements: *76679020
25
26
  - !ruby/object:Gem::Dependency
26
27
  name: activemodel
27
- requirement: &83064350 !ruby/object:Gem::Requirement
28
+ requirement: &76678260 !ruby/object:Gem::Requirement
28
29
  none: false
29
30
  requirements:
30
31
  - - ! '>='
@@ -32,10 +33,10 @@ dependencies:
32
33
  version: 3.0.0
33
34
  type: :runtime
34
35
  prerelease: false
35
- version_requirements: *83064350
36
+ version_requirements: *76678260
36
37
  - !ruby/object:Gem::Dependency
37
38
  name: rspec
38
- requirement: &83064110 !ruby/object:Gem::Requirement
39
+ requirement: &76677800 !ruby/object:Gem::Requirement
39
40
  none: false
40
41
  requirements:
41
42
  - - ! '>='
@@ -43,10 +44,10 @@ dependencies:
43
44
  version: '0'
44
45
  type: :development
45
46
  prerelease: false
46
- version_requirements: *83064110
47
+ version_requirements: *76677800
47
48
  - !ruby/object:Gem::Dependency
48
49
  name: activerecord
49
- requirement: &83063840 !ruby/object:Gem::Requirement
50
+ requirement: &79365140 !ruby/object:Gem::Requirement
50
51
  none: false
51
52
  requirements:
52
53
  - - ! '>='
@@ -54,12 +55,13 @@ dependencies:
54
55
  version: '0'
55
56
  type: :development
56
57
  prerelease: false
57
- version_requirements: *83063840
58
+ version_requirements: *79365140
58
59
  description: ! "Heimdallr aims to provide an easy to configure and efficient object-
59
60
  and field-level access\n control solution, reusing proven patterns from gems like
60
61
  CanCan and allowing one to manage permissions in a very\n fine-grained manner."
61
62
  email:
62
63
  - whitequark@whitequark.org
64
+ - boris@roundlake.ru
63
65
  executables: []
64
66
  extensions: []
65
67
  extra_rdoc_files: []
@@ -75,10 +77,10 @@ files:
75
77
  - heimdallr.gemspec
76
78
  - lib/heimdallr.rb
77
79
  - lib/heimdallr/evaluator.rb
80
+ - lib/heimdallr/legacy_resource.rb
78
81
  - lib/heimdallr/model.rb
79
82
  - lib/heimdallr/proxy/collection.rb
80
83
  - lib/heimdallr/proxy/record.rb
81
- - lib/heimdallr/resource.rb
82
84
  - lib/heimdallr/validator.rb
83
85
  - spec/proxy_spec.rb
84
86
  - spec/spec_helper.rb
@@ -97,15 +99,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
97
99
  required_rubygems_version: !ruby/object:Gem::Requirement
98
100
  none: false
99
101
  requirements:
100
- - - ! '>='
102
+ - - ! '>'
101
103
  - !ruby/object:Gem::Version
102
- version: '0'
104
+ version: 1.3.1
103
105
  requirements: []
104
- rubyforge_project: heimdallr
105
- rubygems_version: 1.8.10
106
+ rubyforge_project:
107
+ rubygems_version: 1.8.17
106
108
  signing_key:
107
109
  specification_version: 3
108
110
  summary: Heimdallr is an ActiveModel extension which provides object- and field-level
109
111
  access control.
110
112
  test_files: []
111
- has_rdoc: