Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Another canable update - options hash on able methods #6

Open
wants to merge 10 commits into from

2 participants

@jopotts

Hi again John,

Sorry to fire another pull request to you so soon, but hopefully you'll like this one enough too to include it. Thanks for including the last one btw.

So, this latest update came about from me needing to pass things to the able methods. I've made the changes *mostly backwards compatible with 0.3.0, but had to go down the method missing route on the able class to cope with the potential operator overload. I've updated the readme with an example.

  • The only breaking change is that overloaded methods on the enforcer class will need to have an extra hash parameter.

Have a look and let me know what you think. It definitely adds to the flexibility. No more updates after this for a while I promise.

Cheers,
Jo

@jopotts

Sorry. Last thing (I think). Just added another commit to the pull request - the last commit.
It's to be able to change the default result of the able calls.

README.rdoc
((19 lines not shown))
+ enforce_index_permission(Article, :domain => get_domain)
+ @articles = Article.all
+ end
+ end
+
+And in the views the can methods can also pass the options.
+
+ can_index?(Article, :domain => get_domain)
+
+This example used a class able method, but options can equally be used on instance able methods.
+
+== Changing the Default Able
+
+You can easily change the default on the able methods from true to false to lock down security more. Simply add the following to your canable initializer file (./config/initializers/canable.rb):
+
+ Canable.default_canability = false
@jnunemaker Owner

Thinking this seems kind of long. Wondering if Canable.can_default or something would be enough?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
README.rdoc
((27 lines not shown))
+
+This example used a class able method, but options can equally be used on instance able methods.
+
+== Changing the Default Able
+
+You can easily change the default on the able methods from true to false to lock down security more. Simply add the following to your canable initializer file (./config/initializers/canable.rb):
+
+ Canable.default_canability = false
+
+This will change the global default, but you can also set it on a class level by overriding default_canability on the class.
+
+ class Article
+ include Canable::Ables
+
+ # Only allow viewable by default
+ def self.default_canability(able_name)
@jnunemaker Owner

Same thought here as the comment above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jnunemaker
Owner

Instead of method missing, can we just check the method arity before calling? Or just make a backwards compat break?

@jnunemaker

Instead of @klass = klass, just change klass to @klass in line 46.

@jnunemaker

I think this should be default to true.

@jnunemaker

This should be true too.

@jnunemaker
Owner

I think those are all my thoughts on this. Lets talk more about it if you disagree or feel free to fix them and I'll pull. Thanks for all the work. Making a lot of sense.

@jopotts

I'm not sure why it needs this. Fails test without.

Off the top of my head: probably need to ensure that able and can defaults are set to correct default in the setup of all tests.

@jopotts

Hi John,

Thanks for the comments on the last pull request. I agreed with them all and thanks for taking the time to look into it in detail.

I fixed everything you said, with the main thing being the removal of method_missing which is definitely a good thing. I've also made use of the arity check to make it backwards compatible, and it's also nicer like that to be able to have single arg able methods.

Sorry to bundle the addition of the new able_check method and the refactoring into the same commit. Hope you like this one. Comments welcome of course.

Cheers, Jo.

@jopotts

I had to update this class to use a real (as opposed to mocked) resource due to the check on the arity of the able methods.

@jopotts

Now backwards compatible.

@jnunemaker

Personally I believe in forcing an API rather than supporting all kinds of arguments. I might tweak this after pulling. We'll see.

@jopotts

Hi. Were you waiting for me to do anything else with this? No rush. Just wondering. Cheers.

@jnunemaker
Owner
@jnunemaker
Owner

Ok, finally looked this over. I'm confused. I see able_default but can_default seems gone. Now there is an able_check method?

Lets take a step back. What is the goal of this pull request?

  • To be able to pass options to can_...? methods?
  • Ability to set the default able return value?

Anything else? Also, why is the Ables module changed? What was wrong with the way it was doing things before. I think this is part of of what is confusing me.

@jnunemaker
Owner

Sometimes it takes me longer to understand others code, so please be patient! :)

@jopotts

No worries about the questions. Best to get it right.

You're right about the goals: Sending options (to class and instance methods of able_by? methods), and setting a default.

The able_check and default_able class methods were added simply for flexibility's sake (to make the gem more attractive).

The can_default change to able_default was simply a name change that seemed like a good idea at the time. Perhaps can_default is better? Sorry for the confusion on that one.

I think you get it already, but have a look at the explanations added to the readme under the titles; Passing Additional Arguments to Ables, and Changing the Default Able. They give some simple examples.

Code-wise, the reason the adding of the #{able}_by methods has been moved to where it is, is because I'm using class_eval to add class methods which wouldn't be possible to do using module_eval like before.

Hope that helps! :)

@jopotts

Just a quick reminder in case I catch you with a moment to spare! I'm using this on a couple of sites, so it would be great to get it pulled to your gem at some point. No pressure.

@jopotts jopotts Pass instance through to able_check.
When an able check on an instance of an object falls through to the catch-all able_check class method, pass through the object's instance in the option hash.
3912986
@jopotts

Hi John. Have you had a chance to look at this again yet?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 8, 2011
  1. @jopotts
  2. @jopotts
  3. @jopotts
  4. @jopotts
  5. @jopotts
Commits on Sep 17, 2011
  1. @jopotts
  2. @jopotts
Commits on Sep 19, 2011
  1. @jopotts
Commits on Sep 22, 2011
  1. @jopotts
Commits on Feb 25, 2012
  1. @jopotts

    Pass instance through to able_check.

    jopotts authored
    When an able check on an instance of an object falls through to the catch-all able_check class method, pass through the object's instance in the option hash.
This page is out of date. Refresh to see the latest.
View
3  .gitignore
@@ -13,6 +13,9 @@ tmtags
## VIM
*.swp
+## RVM
+.rvmrc
+
## PROJECT::GENERAL
coverage
rdoc
View
55 README.rdoc
@@ -124,6 +124,61 @@ Then in the article model, add the able check as a class method:
!user.nil?
end
end
+
+== Passing Additional Arguments to Ables
+
+You can also pass additional information to the able methods through an options hash. For example:
+
+ class Article
+ include Canable::Ables
+
+ def self.indexable_by?(user, options={})
+ options[:domain] == "public"
+ end
+ end
+
+ class ArticlesController < ApplicationController
+ def index
+ enforce_index_permission(Article, :domain => get_domain)
+ @articles = Article.all
+ end
+ end
+
+And in the views the can methods can also pass the options.
+
+ can_index?(Article, :domain => get_domain)
+
+This example used a class able method, but options can equally be used on instance able methods.
+
+== Changing the Default Able
+
+You can easily change the default on the able methods from true to false to lock down security more. Simply add the following to your canable initializer file (./config/initializers/canable.rb):
+
+ Canable.able_default = false
+
+This will change the global default, but you can also set it on a class level by overriding the method default_able on the class.
+
+ class Article
+ include Canable::Ables
+
+ # Only allow viewable and updatable by default
+ def self.default_able(able)
+ [:viewable, :updatable].include?(able)
+ end
+ end
+
+As an alternative to overriding individual able methods on the able classes, you can also use the single catch-all method able_check:
+
+ class Article
+ include Canable::Ables
+
+ def self.able_check(user, able, options={})
+ # Do whatever checks you need here..
+ return false if !user.super?
+ # Then fall through to the default
+ default_able(able)
+ end
+ end
== Review
View
87 lib/canable.rb
@@ -3,7 +3,34 @@ module Canable
module Cans; end
# Module that holds all the [method]able_by? methods.
- module Ables; end
+ module Ables
+ def self.included(klass)
+ klass.instance_eval <<-EOM
+ def able_check(user, able, options={})
+ default_able(able)
+ end
+ def default_able(able=nil)
+ #{Canable.able_default}
+ end
+ EOM
+ Canable.ables.each do |able|
+ klass.instance_eval <<-EOM
+ def #{able}_by?(*args)
+ user, options = args
+ able_check(user, :#{able}, options || {})
+ end
+ EOM
+ klass.class_eval <<-EOM
+ def #{able}_by?(*args)
+ user, options = args
+ options = options || {}
+ options.merge!(:instance => self)
+ self.class.able_check(user, :#{able}, options)
+ end
+ EOM
+ end
+ end
+ end
# Module that holds all the enforce_[action]_permission methods for use in controllers.
module Enforcers
@@ -13,13 +40,24 @@ def self.included(controller)
helper_method "can_#{can}?" if controller.respond_to?(:helper_method)
hide_action "can_#{can}?" if controller.respond_to?(:hide_action)
end
+
+ private
+
+ def transgression_message(options)
+ return options if options.is_a?(String)
+ return options[:message] if options.is_a?(Hash)
+ ""
+ end
end
end
end
# Exception that gets raised when permissions are broken for whatever reason.
class Transgression < StandardError; end
-
+
+ # The default value for all able methods
+ @able_default = true
+
# Default actions to an empty hash.
@actions = {}
@@ -33,6 +71,18 @@ def self.cans
actions.keys
end
+ def self.ables
+ actions.values
+ end
+
+ def self.able_default
+ @able_default
+ end
+
+ def self.able_default=(value)
+ @able_default = value
+ end
+
# Adds an action to actions and the correct methods to can and able modules.
#
# @param [Symbol] can_method The name of the can_[action]? method.
@@ -40,37 +90,36 @@ def self.cans
def self.add(can, able)
@actions[can] = able
add_can_method(can, able)
- add_able_method(able)
add_enforcer_method(can)
end
private
+
def self.add_can_method(can, able)
Cans.module_eval <<-EOM
- def can_#{can}?(resource)
+ def can_#{can}?(*args)
+ resource, options = args
return false if resource.blank?
- resource.#{able}_by?(self)
+ resource.method(:#{able}_by?).arity == 1 ? resource.#{able}_by?(self) : resource.#{able}_by?(self, options)
end
EOM
end
-
- def self.add_able_method(able)
- Ables.module_eval <<-EOM
- def #{able}_by?(user)
- true
- end
- EOM
- end
-
+
def self.add_enforcer_method(can)
Enforcers.module_eval <<-EOM
- def can_#{can}?(resource)
- current_user && current_user.can_#{can}?(resource)
+ def can_#{can}?(*args)
+ resource, options = args
+ return false if current_user.blank?
+ current_user.can_#{can}?(resource, options)
end
-
- def enforce_#{can}_permission(resource, message="")
- raise(Canable::Transgression, message) unless can_#{can}?(resource)
+
+ def enforce_#{can}_permission(*args)
+ resource, options = args
+ unless method(:can_#{can}?).arity == 1 ? can_#{can}?(resource) : can_#{can}?(resource, options)
+ raise(Canable::Transgression, transgression_message(options))
+ end
end
+
private :enforce_#{can}_permission
EOM
end
View
113 test/test_ables.rb
@@ -3,11 +3,13 @@
class AblesTest < Test::Unit::TestCase
context "Class with Canable::Ables included" do
setup do
- klass = Doc do
+ Canable.able_default = true
+
+ @klass = Doc do
include Canable::Ables
end
- @resource = klass.new
+ @resource = @klass.new
@user = mock('user')
end
@@ -26,10 +28,91 @@ class AblesTest < Test::Unit::TestCase
should "default destroyable_by? to true" do
assert @resource.destroyable_by?(@user)
end
+
+ should "raise error if able not defined" do
+ assert_raises(NoMethodError) { @resource.publishable_by?(@user) }
+ end
+
+ should "default viewable_by? to true on the class level" do
+ assert @klass.viewable_by?(@user)
+ end
+
+ should "default viewable_by? to true on the class level and ignores options" do
+ assert @klass.viewable_by?(@user, :foo => "bar")
+ end
+
+ should "default able method ignores passed options" do
+ assert @resource.viewable_by?(@user, :foo => "bar")
+ end
+ end
+
+ context "Class with Canable::Ables included" do
+ setup do
+ Canable.able_default = false
+
+ klass = Doc do
+ include Canable::Ables
+ end
+
+ @user = mock('user')
+ @resource = klass.new
+ end
+
+ should "use the able_default setting" do
+ assert ! @resource.viewable_by?(@user)
+ end
+ end
+
+ context "Class with Canable::Ables included with overridden able_check" do
+ setup do
+ Canable.able_default = true
+
+ klass = Doc do
+ include Canable::Ables
+ def self.able_check(user, able, options={})
+ return user.name == "John" if able == :updatable
+ default_able
+ end
+ end
+
+ @resource = klass.new
+ @john = mock('user', :name => 'John')
+ @steve = mock('user', :name => 'Steve')
+ end
+
+ should "overridden able_check correctly using user" do
+ assert @resource.viewable_by?(@steve)
+ assert @resource.updatable_by?(@john)
+ assert ! @resource.updatable_by?(@steve)
+ end
+ end
+
+ context "Class with Canable::Ables included with overridden able_check" do
+ setup do
+ Canable.able_default = true
+
+ klass = Doc do
+ include Canable::Ables
+ def self.able_check(user, able, options={})
+ return false if options[:lockdown] == true
+ default_able
+ end
+ end
+
+ @resource = klass.new
+ @user = mock('user')
+ end
+
+ should "overridden able_check correctly using options" do
+ assert @resource.viewable_by?(@user, :lockdown => false)
+ assert ! @resource.viewable_by?(@user, :lockdown => true)
+ end
end
context "Class that overrides an able method" do
setup do
+ Canable.able_default = true
+
klass = Doc do
include Canable::Ables
@@ -48,4 +131,30 @@ def viewable_by?(user)
assert ! @resource.viewable_by?(@steve)
end
end
+
+ context "Class that overrides an able method and accepts options" do
+ setup do
+ Canable.able_default = true
+
+ klass = Doc do
+ include Canable::Ables
+
+ def viewable_by?(user, options={})
+ user.name == 'John' && options[:day] == "Saturday"
+ end
+ end
+
+ @resource = klass.new
+ @john = mock('user', :name => 'John')
+ end
+
+ should "use the options on the overriden method" do
+ assert @resource.viewable_by?(@john, :day => "Saturday")
+ end
+
+ should "use the options on the overriden method" do
+ assert ! @resource.viewable_by?(@john, :day => "Friday")
+ end
+
+ end
end
View
4 test/test_canable.rb
@@ -2,6 +2,10 @@
class TestCanable < Test::Unit::TestCase
context "Canable" do
+ setup do
+ Canable.able_default = true
+ end
+
should "have view action by default" do
assert_equal :viewable, Canable.actions[:view]
end
View
42 test/test_cans.rb
@@ -3,22 +3,28 @@
class CansTest < Test::Unit::TestCase
context "Class with Canable::Cans included" do
setup do
- klass = Doc do
+ Canable.able_default = true
+
+ can_class = Doc do
include Canable::Cans
end
+ able_class = Doc do
+ include Canable::Ables
+ end
- @user = klass.new(:name => 'John')
+ @resource = able_class.new
+ @user = can_class.new(:name => 'John')
end
context "can_view?" do
should "be true if resource is viewable_by?" do
- resource = mock('resource', :viewable_by? => true)
- assert @user.can_view?(resource)
+ @resource.expects(:viewable_by?).returns(true)
+ assert @user.can_view?(@resource)
end
should "be false if resource is not viewable_by?" do
- resource = mock('resource', :viewable_by? => false)
- assert ! @user.can_view?(resource)
+ @resource.expects(:viewable_by?).returns(false)
+ assert ! @user.can_view?(@resource)
end
should "be false if resource is blank" do
@@ -29,13 +35,13 @@ class CansTest < Test::Unit::TestCase
context "can_create?" do
should "be true if resource is creatable_by?" do
- resource = mock('resource', :creatable_by? => true)
- assert @user.can_create?(resource)
+ @resource.expects(:creatable_by?).returns(true)
+ assert @user.can_create?(@resource)
end
should "be false if resource is not creatable_by?" do
- resource = mock('resource', :creatable_by? => false)
- assert ! @user.can_create?(resource)
+ @resource.expects(:creatable_by?).returns(false)
+ assert ! @user.can_create?(@resource)
end
should "be false if resource is blank" do
@@ -46,13 +52,13 @@ class CansTest < Test::Unit::TestCase
context "can_update?" do
should "be true if resource is updatable_by?" do
- resource = mock('resource', :updatable_by? => true)
- assert @user.can_update?(resource)
+ @resource.expects(:updatable_by?).returns(true)
+ assert @user.can_update?(@resource)
end
should "be false if resource is not updatable_by?" do
- resource = mock('resource', :updatable_by? => false)
- assert ! @user.can_update?(resource)
+ @resource.expects(:updatable_by?).returns(false)
+ assert ! @user.can_update?(@resource)
end
should "be false if resource is blank" do
@@ -63,13 +69,13 @@ class CansTest < Test::Unit::TestCase
context "can_destroy?" do
should "be true if resource is destroyable_by?" do
- resource = mock('resource', :destroyable_by? => true)
- assert @user.can_destroy?(resource)
+ @resource.expects(:destroyable_by?).returns(true)
+ assert @user.can_destroy?(@resource)
end
should "be false if resource is not destroyable_by?" do
- resource = mock('resource', :destroyable_by? => false)
- assert ! @user.can_destroy?(resource)
+ @resource.expects(:destroyable_by?).returns(false)
+ assert ! @user.can_destroy?(@resource)
end
should "be false if resource is blank" do
View
36 test/test_enforcers.rb
@@ -3,6 +3,8 @@
class EnforcersTest < Test::Unit::TestCase
context "Including Canable::Enforcers in a class" do
setup do
+ Canable.able_default = true
+
klass = Class.new do
include Canable::Enforcers
attr_accessor :current_user, :article
@@ -12,6 +14,12 @@ def can_update?(resource)
return false if current_user && current_user.banned?
super
end
+
+ # Override that accepts options
+ def can_destroy?(resource, options={})
+ return true if options && options[:secret] == "123"
+ super
+ end
def show
enforce_view_permission(article)
@@ -20,10 +28,14 @@ def show
def update
enforce_update_permission(article)
end
-
+
def edit
enforce_update_permission(article, "You Can't Edit This")
end
+
+ def destroy(options={})
+ enforce_destroy_permission(article, options)
+ end
end
@article = mock('article')
@@ -34,12 +46,12 @@ def edit
end
should "not raise error if can" do
- @user.expects(:can_view?).with(@article).returns(true)
+ @user.expects(:can_view?).with(@article, nil).returns(true)
assert_nothing_raised { @controller.show }
end
should "raise error if cannot" do
- @user.expects(:can_view?).with(@article).returns(false)
+ @user.expects(:can_view?).with(@article, nil).returns(false)
assert_raises(Canable::Transgression) { @controller.show }
end
@@ -61,5 +73,23 @@ def edit
assert_equal e.message, "You Can't Edit This"
end
end
+
+ should "be able to define overridden can_xx? method with options" do
+ @user.expects(:can_destroy?).with(@article, {}).returns(false)
+ assert_raises(Canable::Transgression) { @controller.destroy }
+ end
+
+ should "be able to use options on overridden can_xx? method" do
+ assert_nothing_raised { @controller.destroy(:secret => "123") }
+ end
+
+ should "be able to pass a transgression message in the options hash" do
+ @controller.current_user = nil
+ begin
+ @controller.destroy(:message => "You Can't Edit This")
+ rescue Canable::Transgression => e
+ assert_equal e.message, "You Can't Edit This"
+ end
+ end
end
end
Something went wrong with that request. Please try again.