heimdallr 0.0.2 → 1.0.0.RC2

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.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: