Skip to content

Commit ae502b2

Browse files
committed
feat: allow if and unless options for skipping authorize and load
1 parent 7c99c59 commit ae502b2

6 files changed

Lines changed: 122 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* [#653](https://github.com/CanCanCommunity/cancancan/pull/653): Add support for using an nil relation as a condition. ([@ghiculescu][])
44
* [#702](https://github.com/CanCanCommunity/cancancan/pull/702): Support scopes of STI classes as ability conditions. ([@honigc][])
55
* [#798](https://github.com/CanCanCommunity/cancancan/pull/798): Allow disabling of rules compressor via `CanCan.rules_compressor_enabled = false`. ([@coorasse][])
6+
* [#808](https://github.com/CanCanCommunity/cancancan/pull/808): Add `if` and `unless` options controller helpers for skipping. ([@ammarghaus][])
67

78
## 3.4.0
89

docs/changing_defaults.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ class ProductsController < ActionController::Base
146146
skip_authorize_resource only: :new
147147
end
148148
```
149+
Both `skip_authorize_resource` and `skip_load_resource` support `:if` and `:unless` options. Either one takes a method name as a symbol or a lambda/proc. This option will be called to determine if the authorization/loading will be performed.
149150

150151
### Custom class name
151152

lib/cancan/controller_additions.rb

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,31 +196,69 @@ def skip_load_and_authorize_resource(*args)
196196
end
197197

198198
# Skip the loading behavior of CanCan. This is useful when using +load_and_authorize_resource+ but want to
199-
# only do authorization on certain actions. You can pass :only and :except options to specify which actions to
200-
# skip the effects on. It will apply to all actions by default.
199+
# only do authorization on certain actions. It will apply to all actions by default.
201200
#
202201
# class ProjectsController < ApplicationController
203202
# load_and_authorize_resource
204203
# skip_load_resource :only => :index
205204
# end
206205
#
207206
# You can also pass the resource name as the first argument to skip that resource.
207+
#
208+
# Options:
209+
# [:+only+]
210+
# Only applies to given actions.
211+
#
212+
# [:+except+]
213+
# Does not apply to given actions.
214+
#
215+
# [:+if+]
216+
# Supply the name of a controller method or a lambda/proc to be called.
217+
# The loading is only skipped if this returns true.
218+
#
219+
# skip_load_resource :if => :admin_controller?
220+
#
221+
# [:+unless+]
222+
# Supply the name of a controller method to be called.
223+
# The loading is skipped if this returns false.
224+
#
225+
# skip_load_resource :unless => :devise_controller?
226+
#
208227
def skip_load_resource(*args)
209228
options = args.extract_options!
210229
name = args.first
211230
cancan_skipper[:load][name] = options
212231
end
213232

214233
# Skip the authorization behavior of CanCan. This is useful when using +load_and_authorize_resource+ but want to
215-
# only do loading on certain actions. You can pass :only and :except options to specify which actions to
216-
# skip the effects on. It will apply to all actions by default.
234+
# only do loading on certain actions. It will apply to all actions by default.
217235
#
218236
# class ProjectsController < ApplicationController
219237
# load_and_authorize_resource
220238
# skip_authorize_resource :only => :index
221239
# end
222240
#
223241
# You can also pass the resource name as the first argument to skip that resource.
242+
#
243+
# Options:
244+
# [:+only+]
245+
# Only applies to given actions.
246+
#
247+
# [:+except+]
248+
# Does not apply to given actions.
249+
#
250+
# [:+if+]
251+
# Supply the name of a controller method or a lambda/proc to be called.
252+
# The authorization is only skipped if this returns true.
253+
#
254+
# skip_authorize_resource :if => :admin_controller?
255+
#
256+
# [:+unless+]
257+
# Supply the name of a controller method to be called.
258+
# The authorization is skipped if this returns false.
259+
#
260+
# skip_authorize_resource :unless => :devise_controller?
261+
#
224262
def skip_authorize_resource(*args)
225263
options = args.extract_options!
226264
name = args.first

lib/cancan/controller_resource.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module CanCan
55
# Handle the load and authorization controller logic
66
# so we don't clutter up all controllers with non-interface methods.
77
# This class is used internally, so you do not need to call methods directly on it.
8-
class ControllerResource # :nodoc:
8+
class ControllerResource # :nodoc: # rubocop:disable Metrics/ClassLength
99
include ControllerResourceLoader
1010

1111
def self.add_before_action(controller_class, method, *args)
@@ -47,9 +47,7 @@ def parent?
4747
def skip?(behavior)
4848
return false unless (options = @controller.class.cancan_skipper[behavior][@name])
4949

50-
options == {} ||
51-
options[:except] && !action_exists_in?(options[:except]) ||
52-
action_exists_in?(options[:only])
50+
options == {} || evaluate_options(options)
5351
end
5452

5553
protected
@@ -130,6 +128,18 @@ def action_exists_in?(options)
130128
Array(options).include?(@params[:action].to_sym)
131129
end
132130

131+
def evaluate_callable(option)
132+
return option.call if option.respond_to?(:call)
133+
return @controller.send(option) if option && defined?(option)
134+
end
135+
136+
def evaluate_options(options)
137+
options[:except] && !action_exists_in?(options[:except]) ||
138+
options[:unless] && !evaluate_callable(options[:unless]) ||
139+
action_exists_in?(options[:only]) ||
140+
evaluate_callable(options[:if])
141+
end
142+
133143
def adapter
134144
ModelAdapters::AbstractAdapter.adapter_class(resource_class)
135145
end

spec/cancan/controller_additions_spec.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@
130130
expect(@controller_class.cancan_skipper[:authorize][nil]).to eq(only: %i[index show])
131131
@controller_class.skip_authorize_resource(:article)
132132
expect(@controller_class.cancan_skipper[:authorize][:article]).to eq({})
133+
@controller_class.skip_authorize_resource(:article, if: -> {})
134+
expect(@controller_class.cancan_skipper[:authorize][:article]).to have_key(:if)
135+
@controller_class.skip_authorize_resource(:article, unless: -> {})
136+
expect(@controller_class.cancan_skipper[:authorize][:article]).to have_key(:unless)
133137
end
134138

135139
it 'skip_load_resource adds itself to the cancan skipper with given model name and options' do
@@ -139,6 +143,10 @@
139143
expect(@controller_class.cancan_skipper[:load][nil]).to eq(only: %i[index show])
140144
@controller_class.skip_load_resource(:article)
141145
expect(@controller_class.cancan_skipper[:load][:article]).to eq({})
146+
@controller_class.skip_load_resource(:article, if: -> {})
147+
expect(@controller_class.cancan_skipper[:load][:article]).to have_key(:if)
148+
@controller_class.skip_load_resource(:article, unless: -> {})
149+
expect(@controller_class.cancan_skipper[:load][:article]).to have_key(:unless)
142150
end
143151

144152
it 'skip_load_and_authorize_resource adds itself to the cancan skipper with given model name and options' do

spec/cancan/controller_resource_spec.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,62 @@ class Section; end
645645
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be(true)
646646
end
647647

648+
it 'skips resource behavior :if method returns true' do
649+
allow(controller).to receive(:should_skip?) { params[:action] == 'index' }
650+
allow(controller_class).to receive(:cancan_skipper) { { authorize: { model: { if: :should_skip? } } } }
651+
652+
params[:action] = 'index'
653+
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be_truthy
654+
params[:action] = 'other_action'
655+
expect(CanCan::ControllerResource.new(controller).skip?(:authorize)).to be(false)
656+
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be(false)
657+
end
658+
659+
it 'skips resource behavior :if lambda returns true' do
660+
allow(controller_class).to receive(:cancan_skipper) {
661+
{
662+
authorize: {
663+
model: {
664+
if: -> { params[:action] == 'index' }
665+
}
666+
}
667+
}
668+
}
669+
params[:action] = 'index'
670+
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be_truthy
671+
params[:action] = 'other_action'
672+
expect(CanCan::ControllerResource.new(controller).skip?(:authorize)).to be(false)
673+
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be(false)
674+
end
675+
676+
it 'skips resource behavior :unless method returns true' do
677+
allow(controller).to receive(:should_not_skip?) { params[:action] == 'index' }
678+
allow(controller_class).to receive(:cancan_skipper) { { authorize: { model: { unless: :should_not_skip? } } } }
679+
680+
params[:action] = 'index'
681+
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be_falsey
682+
params[:action] = 'other_action'
683+
expect(CanCan::ControllerResource.new(controller).skip?(:authorize)).to be(false)
684+
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be(true)
685+
end
686+
687+
it 'skips resource behavior :unless lambda returns true' do
688+
allow(controller_class).to receive(:cancan_skipper) {
689+
{
690+
authorize: {
691+
model: {
692+
unless: -> { params[:action] == 'index' }
693+
}
694+
}
695+
}
696+
}
697+
params[:action] = 'index'
698+
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be_falsey
699+
params[:action] = 'other_action'
700+
expect(CanCan::ControllerResource.new(controller).skip?(:authorize)).to be(false)
701+
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be(true)
702+
end
703+
648704
it 'skips loading and authorization' do
649705
allow(controller_class).to receive(:cancan_skipper) { { authorize: { nil => {} }, load: { nil => {} } } }
650706
params[:action] = 'new'

0 commit comments

Comments
 (0)