heimdallr-resource 1.0.3 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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