heimdallr-resource 1.0.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. data/.rspec +1 -2
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +9 -5
  4. data/heimdallr-resource.gemspec +4 -3
  5. data/lib/heimdallr-resource.rb +2 -0
  6. data/lib/heimdallr/resource.rb +21 -191
  7. data/lib/heimdallr/resource_implementation.rb +190 -0
  8. data/spec/{resource_spec.rb → controllers/entities_controller_spec.rb} +25 -7
  9. data/spec/controllers/fluffies_controller_spec.rb +20 -0
  10. data/spec/controllers/things_controller_spec.rb +31 -0
  11. data/spec/dummy/app/controllers/{entity_controller.rb → entities_controller.rb} +6 -2
  12. data/spec/dummy/app/controllers/things_controller.rb +29 -0
  13. data/spec/dummy/app/models/entity.rb +2 -0
  14. data/spec/dummy/app/models/thing.rb +16 -0
  15. data/spec/dummy/config/application.rb +1 -1
  16. data/spec/dummy/config/routes.rb +6 -5
  17. data/spec/dummy/db/schema.rb +5 -0
  18. data/spec/models/resource_implementation_spec.rb +382 -0
  19. data/spec/models/resource_spec.rb +91 -0
  20. data/spec/spec_helper.rb +1 -0
  21. metadata +44 -48
  22. data/spec/dummy/Rakefile +0 -7
  23. data/spec/dummy/app/helpers/application_helper.rb +0 -2
  24. data/spec/dummy/config/environments/development.rb +0 -23
  25. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
  26. data/spec/dummy/config/initializers/inflections.rb +0 -10
  27. data/spec/dummy/config/initializers/mime_types.rb +0 -5
  28. data/spec/dummy/config/initializers/secret_token.rb +0 -7
  29. data/spec/dummy/config/initializers/session_store.rb +0 -8
  30. data/spec/dummy/config/locales/en.yml +0 -5
  31. data/spec/dummy/public/404.html +0 -26
  32. data/spec/dummy/public/422.html +0 -26
  33. data/spec/dummy/public/500.html +0 -26
  34. data/spec/dummy/public/favicon.ico +0 -0
  35. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  36. data/spec/dummy/script/rails +0 -6
  37. data/spec/fluffies_spec.rb +0 -22
data/.rspec CHANGED
@@ -1,2 +1 @@
1
- --color
2
- --format progress
1
+ --tty --color --format progress
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0
4
+
5
+ * Numerous fixes [@alerticus][], [@voidseeker][]
6
+ * :instance_name option [@voidseeker][]
7
+ * :through_association option [@voidseeker][]
8
+ * Improved specs [@alerticus][], [@voidseeker][]
9
+
3
10
  ## 1.0.2
4
11
 
5
12
  * Additional methods resources assignation [@whitequark][]
@@ -8,5 +15,7 @@
8
15
 
9
16
  * load_and_authorize_resource accepts just one hash parameter from now [@_inossidabile][]
10
17
 
18
+ [@voidseeker]: https://github.com/voidseeker
19
+ [@alerticus]: http://github.com/AlexanderPavlenko
11
20
  [@_inossidabile]: http://twitter.com/#!/_inossidabile
12
21
  [@whitequark]: http://twitter.com/#!/whitequark
data/README.md CHANGED
@@ -5,15 +5,17 @@ Heimdallr Resource is a gem which provides CanCan-like interface for writing sec
5
5
  controllers on top of [Heimdallr](http://github.com/roundlake/heimdallr)-protected
6
6
  models.
7
7
 
8
+ ![Travis CI](https://secure.travis-ci.org/roundlake/heimdallr-resource.png)
9
+
8
10
  Overview
9
11
  --------
10
12
 
11
- API of Heimdallr Resource basically consists of two methods, `load_resource` and `authorize_resource`.
13
+ API of Heimdallr Resource basically consists of two methods, `load_resource` and `load_and_authorize_resource`.
12
14
  Both work by adding a filter in standard Rails filter chain and obey the `:only` and `:except` options.
13
15
 
14
16
  `load_resource` loads a record or scope and wraps it in a Heimadllr proxy. For `index` action, a scope is loaded. For `show`, `new`, `create`, `edit`, `update` and `destroy` a record is loaded. No further action is performed by Heimdallr Resource.
15
17
 
16
- `authorize_resource` verifies if the current security context allows for creating or updating the records. The checks are performed for `new`, `create`, `edit` and `update` actions. `index` will simply follow the defined `:fetch` scope.
18
+ `load_and_authorize_resource` loads a record and verifies if the current security context allows for creating, updating or destroying the records. The checks are performed for `new`, `create`, `edit`, `update` and `destroy` actions. `index` and `show` will simply follow the defined `:fetch` scope.
17
19
 
18
20
  ```ruby
19
21
  class CricketController < ApplicationController
@@ -70,7 +72,7 @@ Custom methods (besides CRUD)
70
72
  By default Heimdallr Resource will consider non-CRUD methods a `:record` methods (like `show`). So it will try to find entity using `params[:id]`. To modify this behavior to make it work like `index` or `create`, you can explicitly define the way it should handle the methods.
71
73
 
72
74
  ```ruby
73
- load_and_authorize :collection => [:search], :new_record => [:special_create], :record => [:attack]
75
+ load_and_authorize :collection => [:search], :new_record => [:special_create]
74
76
  ```
75
77
 
76
78
  Inlined resources
@@ -106,8 +108,10 @@ Credits
106
108
 
107
109
  <img src="http://roundlake.ru/assets/logo.png" align="right" />
108
110
 
109
- * Peter Zotov ([@whitequark](http://twitter.com/#!/whitequark))
110
- * Boris Staal ([@_inossidabile](http://twitter.com/#!/_inossidabile))
111
+ * Peter Zotov ([@whitequark](http://twitter.com/whitequark))
112
+ * Boris Staal ([@_inossidabile](http://twitter.com/_inossidabile))
113
+ * Alexander Pavlenko ([@alerticus](https://twitter.com/alerticus))
114
+ * Shamil Fattakhov ([@voidseeker](https://github.com/voidseeker))
111
115
 
112
116
  LICENSE
113
117
  -------
@@ -3,9 +3,9 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "heimdallr-resource"
6
- s.version = "1.0.3"
7
- s.authors = ["Peter Zotov", "Boris Staal"]
8
- s.email = ["whitequark@whitequark.org", "boris@roundlake.ru"]
6
+ s.version = "1.2.0"
7
+ s.authors = ["Peter Zotov", "Boris Staal", "Alexander Pavlenko", "Shamil Fattakhov"]
8
+ s.email = ["whitequark@whitequark.org", "boris@roundlake.ru", "a.pavlenko@roundlake.ru"]
9
9
  s.homepage = "http://github.com/roundlake/heimdallr-resource"
10
10
  s.summary = %q{Heimdallr-Resource provides CanCan-like interface for Heimdallr-secured objects.}
11
11
  s.description = s.summary
@@ -16,6 +16,7 @@ Gem::Specification.new do |s|
16
16
  s.require_paths = ["lib"]
17
17
 
18
18
  s.add_development_dependency "rspec-rails"
19
+ s.add_development_dependency "rr"
19
20
  s.add_development_dependency "activerecord"
20
21
  s.add_development_dependency "sqlite3"
21
22
  s.add_development_dependency "tzinfo"
@@ -0,0 +1,2 @@
1
+ require 'heimdallr/resource'
2
+ require 'heimdallr/resource_implementation'
@@ -1,210 +1,40 @@
1
1
  module Heimdallr
2
- # {AccessDenied} exception is to be raised when access is denied to an action.
3
- class AccessDenied < StandardError; end
4
-
5
- module ResourceImplementation
6
- class << self
7
- def prepare_options(klass, options)
8
- options = options.merge :resource => (options[:resource] || klass.name.sub(/Controller$/, '').underscore).to_s
9
-
10
- filter_options = {}
11
- filter_options[:only] = options.delete(:only) if options.has_key?(:only)
12
- filter_options[:except] = options.delete(:except) if options.has_key?(:except)
13
-
14
- [ options, filter_options ]
15
- end
16
-
17
- def load(controller, options)
18
- unless controller.instance_variable_defined?(ivar_name(controller, options))
19
- if options.has_key? :through
20
- target = load_target(controller, options)
21
-
22
- if target
23
- if options[:singleton]
24
- scope = target.send(:"#{variable_name(options)}")
25
- else
26
- scope = target.send(:"#{variable_name(options).pluralize}")
27
- end
28
- elsif options[:shallow]
29
- scope = class_name(options).constantize.scoped
30
- else
31
- raise "Cannot fetch #{options[:resource]} via #{options[:through]}"
32
- end
33
- else
34
- scope = class_name(options).constantize.scoped
35
- end
36
-
37
- loaders = {
38
- collection: -> {
39
- controller.instance_variable_set(ivar_name(controller, options), scope)
40
- },
41
-
42
- new_record: -> {
43
- controller.instance_variable_set(
44
- ivar_name(controller, options),
45
- scope.new(controller.params[params_key_name(options)] || {})
46
- )
47
- },
48
-
49
- record: -> {
50
- controller.instance_variable_set(
51
- ivar_name(controller, options),
52
- scope.find([:"#{params_key_name(options)}_id", :id].map{|key| controller.params[key] }.reject(&:blank?)[0])
53
- )
54
- },
55
-
56
- related_record: -> {
57
- unless controller.params[:"#{params_key_name(options)}_id"].blank?
58
- controller.instance_variable_set(
59
- ivar_name(controller, options),
60
- scope.find(controller.params[:"#{params_key_name(options)}_id"])
61
- )
62
- end
63
- }
64
- }
65
-
66
- loaders[action_type(controller.params[:action], options)].()
67
- end
68
- end
69
-
70
- def authorize(controller, options)
71
- value = controller.instance_variable_get(ivar_name(controller, options))
72
- return unless value
73
-
74
- controller.instance_variable_set(ivar_name(controller, options.merge(:insecure => true)), value)
75
-
76
- value = value.restrict(controller.security_context)
77
- controller.instance_variable_set(ivar_name(controller, options), value)
78
-
79
- case controller.params[:action]
80
- when 'new', 'create'
81
- value.assign_attributes(value.reflect_on_security[:restrictions].fixtures[:create])
82
-
83
- unless value.reflect_on_security[:operations].include? :create
84
- raise Heimdallr::AccessDenied, "Cannot create model"
85
- end
86
-
87
- when 'edit', 'update'
88
- value.assign_attributes(value.reflect_on_security[:restrictions].fixtures[:update])
89
-
90
- unless value.reflect_on_security[:operations].include? :update
91
- raise Heimdallr::AccessDenied, "Cannot update model"
92
- end
93
-
94
- when 'destroy'
95
- unless value.destroyable?
96
- raise Heimdallr::AccessDenied, "Cannot delete model"
97
- end
98
- end unless options[:related]
99
- end
100
-
101
- def load_target(controller, options)
102
- Array.wrap(options[:through]).map do |parent|
103
- loaded = controller.instance_variable_get(:"@#{variable_name parent}")
104
- unless loaded
105
- load(controller, :resource => parent.to_s, :related => true)
106
- loaded = controller.instance_variable_get(:"@#{variable_name parent}")
107
- end
108
- if loaded && options[:authorize_chain]
109
- authorize(controller, :resource => parent.to_s, :related => true)
110
- end
111
- controller.instance_variable_get(:"@#{variable_name parent}")
112
- end.reject(&:nil?).first
113
- end
114
-
115
- def ivar_name(controller, options)
116
- if action_type(controller.params[:action], options) == :collection
117
- :"@#{variable_name(options).pluralize}"
118
- else
119
- :"@#{variable_name(options)}"
120
- end
121
- end
122
-
123
- def action_type(action, options)
124
- if options[:related]
125
- :related_record
126
- else
127
- action = action.to_sym
128
- case action
129
- when :index
130
- :collection
131
- when :new, :create
132
- :new_record
133
- when :show, :edit, :update, :destroy
134
- :record
135
- else
136
- if options[:collection] && options[:collection].include?(action)
137
- :collection
138
- elsif options[:new] && options[:new].include?(action)
139
- :new_record
140
- else
141
- :record
142
- end
143
- end
144
- end
145
- end
146
-
147
- def variable_name(options)
148
- if options.kind_of? Hash
149
- options[:resource]
150
- else
151
- options.to_s
152
- end.parameterize('_')
153
- end
154
-
155
- def class_name(options)
156
- if options.kind_of? Hash
157
- options[:resource]
158
- else
159
- options.to_s
160
- end.classify
161
- end
162
-
163
- def params_key_name(options)
164
- if options.kind_of? Hash
165
- options[:resource]
166
- else
167
- options.to_s
168
- end.split('/').last
169
- end
170
- end
171
- end
172
2
 
173
3
  # {Resource} is a mixin providing CanCan-like interface for Rails controllers.
174
4
  module Resource
175
5
  extend ActiveSupport::Concern
176
6
 
177
7
  module ClassMethods
178
- def load_and_authorize_resource(options={})
179
- options[:authorize_chain] = true
180
- load_resource(options)
181
- authorize_resource(options)
8
+ def load_resource(options = {})
9
+ Heimdallr::ResourceImplementation.add_before_filter self, :load_resource, options
182
10
  end
183
11
 
184
- def load_resource(options={})
185
- options, filter_options = Heimdallr::ResourceImplementation.prepare_options(self, options)
186
- self.own_heimdallr_options = options
187
-
188
- before_filter filter_options do |controller|
189
- Heimdallr::ResourceImplementation.load(controller, options)
190
- end
12
+ def load_and_authorize_resource(options = {})
13
+ Heimdallr::ResourceImplementation.add_before_filter self, :load_and_authorize_resource, options
191
14
  end
192
15
 
193
- def authorize_resource(options={})
194
- options, filter_options = Heimdallr::ResourceImplementation.prepare_options(self, options)
195
- self.own_heimdallr_options = options
196
-
197
- before_filter filter_options do |controller|
198
- Heimdallr::ResourceImplementation.authorize(controller, options)
16
+ def skip_authorization_check(options = {})
17
+ prepend_before_filter options do |controller|
18
+ controller.instance_variable_set :@_skip_authorization_check, true
199
19
  end
200
20
  end
201
21
 
202
- protected
203
-
22
+ protected
204
23
  def own_heimdallr_options=(options)
205
- cattr_accessor :heimdallr_options
24
+ class << self
25
+ attr_accessor :heimdallr_options
26
+ end
206
27
  self.heimdallr_options = options
207
28
  end
208
29
  end
30
+
31
+ protected
32
+ def skip_authorization_check?
33
+ @_skip_authorization_check
34
+ end
209
35
  end
210
- end
36
+
37
+ # {AccessDenied} exception is to be raised when access is denied to an action.
38
+ class AccessDenied < StandardError; end
39
+
40
+ end
@@ -0,0 +1,190 @@
1
+ module Heimdallr
2
+ class ResourceImplementation
3
+ def self.add_before_filter(controller_class, method, options)
4
+ options, filter_options = Heimdallr::ResourceImplementation.prepare_options controller_class, options
5
+ controller_class.send :own_heimdallr_options=, options
6
+
7
+ controller_class.class_eval do
8
+ before_filter filter_options do |controller|
9
+ Heimdallr::ResourceImplementation.new(controller, options).send method
10
+ end
11
+ end
12
+ end
13
+
14
+ def self.prepare_options(controller_class, options)
15
+ options = options.dup
16
+ options[:resource] ||= controller_class.name.sub(/Controller$/, '').singularize.underscore
17
+
18
+ filter_keys = [:only, :except]
19
+
20
+ filter_options = filter_keys.inject({}) do |hash, key|
21
+ hash[key] = options.delete key if options.has_key? key
22
+ hash
23
+ end
24
+
25
+ [options, filter_options]
26
+ end
27
+
28
+ def initialize(controller, options)
29
+ @controller = controller
30
+ @options = options
31
+ end
32
+
33
+ def load_resource
34
+ ResourceLoader.new(@controller, @options).load
35
+ end
36
+
37
+ def load_and_authorize_resource
38
+ resource = ResourceLoader.new(@controller, @options, :restricted => true).load
39
+ authorize_resource resource unless @controller.send :skip_authorization_check?
40
+ resource
41
+ end
42
+
43
+ def authorize_resource(resource)
44
+ case @controller.params[:action]
45
+ when 'new', 'create'
46
+ raise Heimdallr::AccessDenied, "Cannot create model" unless resource.creatable?
47
+ resource.assign_attributes resource.reflect_on_security[:restrictions].fixtures[:create]
48
+
49
+ when 'edit', 'update'
50
+ raise Heimdallr::AccessDenied, "Cannot update model" unless resource.modifiable?
51
+ resource.assign_attributes resource.reflect_on_security[:restrictions].fixtures[:update]
52
+
53
+ when 'destroy'
54
+ raise Heimdallr::AccessDenied, "Cannot delete model" unless resource.destroyable?
55
+ end
56
+ end
57
+
58
+ class ResourceLoader
59
+ def initialize(controller, options, loader_options = {})
60
+ @restricted = loader_options[:restricted]
61
+ @parent = loader_options[:parent]
62
+ @controller = controller
63
+ @options = options
64
+ @params = controller.params
65
+ end
66
+
67
+ def load
68
+ return @controller.instance_variable_get(ivar_name) if @controller.instance_variable_defined? ivar_name
69
+
70
+ if !@parent && @options.has_key?(:through)
71
+ parent_resource = load_parent
72
+ raise "Cannot fetch #{@options[:resource]} through #{@options[:through]}" unless parent_resource || @options[:shallow]
73
+ else
74
+ parent_resource = nil
75
+ end
76
+
77
+ resource = self.send action_type, resource_scope(parent_resource), parent_resource
78
+ @controller.instance_variable_set ivar_name, resource unless resource.nil?
79
+ resource
80
+ end
81
+
82
+ def load_parent
83
+ Array.wrap(@options[:through]).map { |parent|
84
+ ResourceLoader.new(@controller, {:resource => parent}, :restricted => @restricted, :parent => true).load
85
+ }.reject(&:nil?).first
86
+ end
87
+
88
+ def resource_scope(parent_resource)
89
+ if parent_resource
90
+ if @options[:through_association]
91
+ parent_resource.send @options[:through_association]
92
+ elsif @options[:singleton]
93
+ parent_resource.send :"#{variable_name}"
94
+ else
95
+ parent_resource.send :"#{variable_name.pluralize}"
96
+ end
97
+ else
98
+ if @restricted
99
+ class_name.constantize.restrict(@controller.security_context)
100
+ else
101
+ class_name.constantize.scoped
102
+ end
103
+ end
104
+ end
105
+
106
+ def collection(scope, parent_resource)
107
+ scope
108
+ end
109
+
110
+ def new_resource(scope, parent_resource)
111
+ attributes = @params[params_key_name] || {}
112
+
113
+ if @options[:singleton] && parent_resource
114
+ if scope.nil?
115
+ parent_resource.send singleton_builder_name, attributes
116
+ else
117
+ scope.assign_attributes attributes
118
+ scope
119
+ end
120
+ else
121
+ scope.new attributes
122
+ end
123
+ end
124
+
125
+ def resource(scope, parent_resource)
126
+ if @options[:singleton] && parent_resource
127
+ scope
128
+ else
129
+ key = [:"#{params_key_name}_id", :id].map{|key| @params[key] }.find &:present?
130
+ scope.send(@options[:finder] || :find, key)
131
+ end
132
+ end
133
+
134
+ def parent_resource(scope, parent_resource)
135
+ key = @params[:"#{params_key_name}_id"]
136
+ return if key.blank?
137
+ scope.send(@options[:finder] || :find, key)
138
+ end
139
+
140
+ def action_type
141
+ if @parent
142
+ :parent_resource
143
+ else
144
+ case action = @params[:action].to_sym
145
+ when :index
146
+ :collection
147
+ when :new, :create
148
+ :new_resource
149
+ when :show, :edit, :update, :destroy
150
+ :resource
151
+ else
152
+ if @options[:collection] && @options[:collection].include?(action)
153
+ :collection
154
+ elsif @options[:new_record] && @options[:new_record].include?(action)
155
+ :new_resource
156
+ else
157
+ :resource
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ def ivar_name
164
+ name = (@options[:instance_name] || variable_name).to_s
165
+ name = name.pluralize if action_type == :collection
166
+ :"@#{name}"
167
+ end
168
+
169
+ def resource_name
170
+ @options[:resource].to_s
171
+ end
172
+
173
+ def variable_name
174
+ resource_name.parameterize('_')
175
+ end
176
+
177
+ def class_name
178
+ resource_name.classify
179
+ end
180
+
181
+ def params_key_name
182
+ resource_name.split('/').last
183
+ end
184
+
185
+ def singleton_builder_name
186
+ :"build_#{@options[:through_association] || variable_name}"
187
+ end
188
+ end
189
+ end
190
+ end