Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow if and unless options for skipping authorize and load, similar to check_authorization #808

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
16 changes: 13 additions & 3 deletions lib/cancan/controller_resource.rb
Original file line number Diff line number Diff line change
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