Skip to content

Commit

Permalink
feat: allow if and unless options for skipping authorize and load
Browse files Browse the repository at this point in the history
  • Loading branch information
ammarghaus committed Dec 2, 2022
1 parent 7c99c59 commit ae502b2
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* [#653](https://github.com/CanCanCommunity/cancancan/pull/653): Add support for using an nil relation as a condition. ([@ghiculescu][])
* [#702](https://github.com/CanCanCommunity/cancancan/pull/702): Support scopes of STI classes as ability conditions. ([@honigc][])
* [#798](https://github.com/CanCanCommunity/cancancan/pull/798): Allow disabling of rules compressor via `CanCan.rules_compressor_enabled = false`. ([@coorasse][])
* [#808](https://github.com/CanCanCommunity/cancancan/pull/808): Add `if` and `unless` options controller helpers for skipping. ([@ammarghaus][])

## 3.4.0

Expand Down
1 change: 1 addition & 0 deletions docs/changing_defaults.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class ProductsController < ActionController::Base
skip_authorize_resource only: :new
end
```
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.

### Custom class name

Expand Down
46 changes: 42 additions & 4 deletions lib/cancan/controller_additions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,31 +196,69 @@ def skip_load_and_authorize_resource(*args)
end

# Skip the loading behavior of CanCan. This is useful when using +load_and_authorize_resource+ but want to
# only do authorization on certain actions. You can pass :only and :except options to specify which actions to
# skip the effects on. It will apply to all actions by default.
# only do authorization on certain actions. It will apply to all actions by default.
#
# class ProjectsController < ApplicationController
# load_and_authorize_resource
# skip_load_resource :only => :index
# end
#
# You can also pass the resource name as the first argument to skip that resource.
#
# Options:
# [:+only+]
# Only applies to given actions.
#
# [:+except+]
# Does not apply to given actions.
#
# [:+if+]
# Supply the name of a controller method or a lambda/proc to be called.
# The loading is only skipped if this returns true.
#
# skip_load_resource :if => :admin_controller?
#
# [:+unless+]
# Supply the name of a controller method to be called.
# The loading is skipped if this returns false.
#
# skip_load_resource :unless => :devise_controller?
#
def skip_load_resource(*args)
options = args.extract_options!
name = args.first
cancan_skipper[:load][name] = options
end

# Skip the authorization behavior of CanCan. This is useful when using +load_and_authorize_resource+ but want to
# only do loading on certain actions. You can pass :only and :except options to specify which actions to
# skip the effects on. It will apply to all actions by default.
# only do loading on certain actions. It will apply to all actions by default.
#
# class ProjectsController < ApplicationController
# load_and_authorize_resource
# skip_authorize_resource :only => :index
# end
#
# You can also pass the resource name as the first argument to skip that resource.
#
# Options:
# [:+only+]
# Only applies to given actions.
#
# [:+except+]
# Does not apply to given actions.
#
# [:+if+]
# Supply the name of a controller method or a lambda/proc to be called.
# The authorization is only skipped if this returns true.
#
# skip_authorize_resource :if => :admin_controller?
#
# [:+unless+]
# Supply the name of a controller method to be called.
# The authorization is skipped if this returns false.
#
# skip_authorize_resource :unless => :devise_controller?
#
def skip_authorize_resource(*args)
options = args.extract_options!
name = args.first
Expand Down
18 changes: 14 additions & 4 deletions lib/cancan/controller_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module CanCan
# Handle the load and authorization controller logic
# so we don't clutter up all controllers with non-interface methods.
# This class is used internally, so you do not need to call methods directly on it.
class ControllerResource # :nodoc:
class ControllerResource # :nodoc: # rubocop:disable Metrics/ClassLength
include ControllerResourceLoader

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

options == {} ||
options[:except] && !action_exists_in?(options[:except]) ||
action_exists_in?(options[:only])
options == {} || evaluate_options(options)
end

protected
Expand Down Expand Up @@ -130,6 +128,18 @@ def action_exists_in?(options)
Array(options).include?(@params[:action].to_sym)
end

def evaluate_callable(option)
return option.call if option.respond_to?(:call)
return @controller.send(option) if option && defined?(option)
end

def evaluate_options(options)
options[:except] && !action_exists_in?(options[:except]) ||
options[:unless] && !evaluate_callable(options[:unless]) ||
action_exists_in?(options[:only]) ||
evaluate_callable(options[:if])
end

def adapter
ModelAdapters::AbstractAdapter.adapter_class(resource_class)
end
Expand Down
8 changes: 8 additions & 0 deletions spec/cancan/controller_additions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@
expect(@controller_class.cancan_skipper[:authorize][nil]).to eq(only: %i[index show])
@controller_class.skip_authorize_resource(:article)
expect(@controller_class.cancan_skipper[:authorize][:article]).to eq({})
@controller_class.skip_authorize_resource(:article, if: -> {})
expect(@controller_class.cancan_skipper[:authorize][:article]).to have_key(:if)
@controller_class.skip_authorize_resource(:article, unless: -> {})
expect(@controller_class.cancan_skipper[:authorize][:article]).to have_key(:unless)
end

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

it 'skip_load_and_authorize_resource adds itself to the cancan skipper with given model name and options' do
Expand Down
56 changes: 56 additions & 0 deletions spec/cancan/controller_resource_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,62 @@ class Section; end
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be(true)
end

it 'skips resource behavior :if method returns true' do
allow(controller).to receive(:should_skip?) { params[:action] == 'index' }
allow(controller_class).to receive(:cancan_skipper) { { authorize: { model: { if: :should_skip? } } } }

params[:action] = 'index'
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be_truthy
params[:action] = 'other_action'
expect(CanCan::ControllerResource.new(controller).skip?(:authorize)).to be(false)
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be(false)
end

it 'skips resource behavior :if lambda returns true' do
allow(controller_class).to receive(:cancan_skipper) {
{
authorize: {
model: {
if: -> { params[:action] == 'index' }
}
}
}
}
params[:action] = 'index'
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be_truthy
params[:action] = 'other_action'
expect(CanCan::ControllerResource.new(controller).skip?(:authorize)).to be(false)
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be(false)
end

it 'skips resource behavior :unless method returns true' do
allow(controller).to receive(:should_not_skip?) { params[:action] == 'index' }
allow(controller_class).to receive(:cancan_skipper) { { authorize: { model: { unless: :should_not_skip? } } } }

params[:action] = 'index'
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be_falsey
params[:action] = 'other_action'
expect(CanCan::ControllerResource.new(controller).skip?(:authorize)).to be(false)
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be(true)
end

it 'skips resource behavior :unless lambda returns true' do
allow(controller_class).to receive(:cancan_skipper) {
{
authorize: {
model: {
unless: -> { params[:action] == 'index' }
}
}
}
}
params[:action] = 'index'
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be_falsey
params[:action] = 'other_action'
expect(CanCan::ControllerResource.new(controller).skip?(:authorize)).to be(false)
expect(CanCan::ControllerResource.new(controller, :model).skip?(:authorize)).to be(true)
end

it 'skips loading and authorization' do
allow(controller_class).to receive(:cancan_skipper) { { authorize: { nil => {} }, load: { nil => {} } } }
params[:action] = 'new'
Expand Down

0 comments on commit ae502b2

Please sign in to comment.