diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 284bab7c..2bd01917 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -1,3 +1,22 @@ +1.4.0 (not yet released) + +* Adding check_authorization and skip_authorization controller class methods to ensure authorization is performed (thanks justinko) - see issue #135 + +* Setting initial attributes based on ability conditions in new/create actions - see issue #114 + +* Check parent attributes for nested association in index action - see issue #121 + +* Supporting nesting in can? method using hash - see issue #121 + +* Adding I18n support for Access Denied messages (thanks EppO) - see issue #103 + +* Passing no arguments to +can+ definition will pass action, class, and object to block - see issue #129 + +* Don't pass action to block in +can+ definition when using :+manage+ option - see issue #129 + +* No longer calling block in +can+ definition when checking on class - see issue #116 + + 1.3.4 (August 31, 2010) * Don't stop at +cannot+ with hash conditions when checking class (thanks tamoya) - see issue #131 diff --git a/Rakefile b/Rakefile index 7b6cfe6f..863ae7a7 100644 --- a/Rakefile +++ b/Rakefile @@ -10,4 +10,4 @@ Spec::Rake::SpecTask.new do |t| t.spec_opts = ["-c"] end -task :default => :spec \ No newline at end of file +task :default => :spec diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index 7b74fef6..118c8273 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -16,7 +16,7 @@ module CanCan # end # module Ability - # Use to check if the user has permission to perform a given action on an object. + # Check if the user has permission to perform a given action on an object. # # can? :destroy, @project # @@ -24,6 +24,11 @@ module Ability # # can? :create, Project # + # Nested resources can be passed through a hash, this way conditions which are + # dependent upon the association will work when using a class. + # + # can? :create, @category => Project + # # Any additional arguments will be passed into the "can" block definition. This # can be used to pass more information about the user's request for example. # @@ -69,53 +74,53 @@ def cannot?(*args) # can :update, Article # # You can pass an array for either of these parameters to match any one. + # Here the user has the ability to update or destroy both articles and comments. # # can [:update, :destroy], [Article, Comment] # - # In this case the user has the ability to update or destroy both articles and comments. + # You can pass :all to match any object and :manage to match any action. Here are some examples. + # + # can :manage, :all + # can :update, :all + # can :manage, Project # - # You can pass a hash of conditions as the third argument. + # You can pass a hash of conditions as the third argument. Here the user can only see active projects which he owns. # # can :read, Project, :active => true, :user_id => user.id # - # Here the user can only see active projects which he owns. See ActiveRecordAdditions#accessible_by - # for how to use this in database queries. + # See ActiveRecordAdditions#accessible_by for how to use this in database queries. These conditions + # are also used for initial attributes when building a record in ControllerAdditions#load_resource. # - # If the conditions hash does not give you enough control over defining abilities, you can use a block to - # write any Ruby code you want. + # If the conditions hash does not give you enough control over defining abilities, you can use a block + # along with any Ruby code you want. # # can :update, Project do |project| - # project && project.groups.include?(user.group) + # project.groups.include?(user.group) # end # # If the block returns true then the user has that :update ability for that project, otherwise he - # will be denied access. It's possible for the passed in model to be nil if one isn't specified, - # so be sure to take that into consideration. + # will be denied access. The downside to using a block is that it cannot be used to generate + # conditions for database queries. # - # The downside to using a block is that it cannot be used to generate conditions for database queries. + # You can pass custom objects into this "can" method, this is usually done with a symbol + # and is useful if a class isn't available to define permissions on. # - # You can pass :all to reference every type of object. In this case the object type will be passed - # into the block as well (just in case object is nil). + # can :read, :stats + # can? :read, :stats # => true # - # can :read, :all do |object_class, object| - # object_class != Order - # end + # IMPORTANT: Neither a hash of conditions or a block will be used when checking permission on a class. # - # Here the user has permission to read all objects except orders. + # can :update, Project, :priority => 3 + # can? :update, Project # => true # - # You can also pass :manage as the action which will match any action. In this case the action is - # passed to the block. + # If you pass no arguments to +can+, the action, class, and object will be passed to the block and the + # block will always be executed. This allows you to override the full behavior if the permissions are + # defined in an external source such as the database. # - # can :manage, Comment do |action, comment| - # action != :destroy + # can do |action, object_class, object| + # # check the database and return true/false # end # - # You can pass custom objects into this "can" method, this is usually done through a symbol - # and is useful if a class isn't available to define permissions on. - # - # can :read, :stats - # can? :read, :stats # => true - # def can(action = nil, subject = nil, conditions = nil, &block) can_definitions << CanDefinition.new(true, action, subject, conditions, block) end diff --git a/lib/cancan/controller_additions.rb b/lib/cancan/controller_additions.rb index f86e77a4..d099a8bf 100644 --- a/lib/cancan/controller_additions.rb +++ b/lib/cancan/controller_additions.rb @@ -21,6 +21,10 @@ def load_and_authorize_resource(*args) # Article.new(params[:article]) depending upon the action. It does nothing for the "index" # action. # + # If a conditions hash is used in the Ability, the +new+ and +create+ actions will set + # the initial attributes based on these conditions. This way these actions will satisfy + # the ability restrictions. + # # Call this method directly on the controller class. # # class BooksController < ApplicationController @@ -148,23 +152,44 @@ def load_resource(*args) # [:+instance_name+] # The name of the instance variable for this resource. # + # [:+through+] + # Authorize conditions on this parent resource when instance isn't available. + # def authorize_resource(*args) ControllerResource.add_before_filter(self, :authorize_resource, *args) end - def skip_authorization(*args) - self.before_filter(*args) do |controller| - controller.instance_variable_set(:@_authorized, true) - end - end - + # Add this to a controller to ensure it performs authorization through +authorized+! or +authorize_resource+ call. + # If neither of these authorization methods are called, a CanCan::AuthorizationNotPerformed exception will be raised. + # This is normally added to the ApplicationController to ensure all controller actions do authorization. + # + # class ApplicationController < ActionController::Base + # check_authorization + # end + # + # Any arguments are passed to the +after_filter+ it triggers. + # + # See skip_authorization to bypass this check on specific controller actions. def check_authorization(*args) self.after_filter(*args) do |controller| unless controller.instance_variable_defined?(:@_authorized) - raise AuthorizationNotPerformed, "This action does not authorize the user. Add authorize! or authorize_resource to the controller." + raise AuthorizationNotPerformed, "This action failed the check_authorization because it does not authorize_resource. Add skip_authorization to bypass this check." end end end + + # Call this in the class of a controller to skip the check_authorization behavior on the actions. + # + # class HomeController < ApplicationController + # skip_authorization :only => :index + # end + # + # Any arguments are passed to the +before_filter+ it triggers. + def skip_authorization(*args) + self.before_filter(*args) do |controller| + controller.instance_variable_set(:@_authorized, true) + end + end end def self.included(base) @@ -185,6 +210,16 @@ def self.included(base) # # authorize! :read, @article, :message => "Not authorized to read #{@article.name}" # + # You can also use I18n to customize the message. Action aliases defined in Ability work here. + # + # en: + # unauthorized: + # manage: + # all: "Not authorized to perform that action." + # user: "Not allowed to manage other user accounts." + # update: + # project: "Not allowed to update this project." + # # You can rescue from the exception in the controller to customize how unauthorized # access is displayed to the user. # @@ -234,6 +269,13 @@ def current_ability # <%= link_to "New Project", new_project_path %> # <% end %> # + # If it's a nested resource, you can pass the parent instance in a hash. This way it will + # check conditions which reach through that association. + # + # <% if can? :create, @category => Project %> + # <%= link_to "New Project", new_project_path %> + # <% end %> + # # This simply calls "can?" on the current_ability. See Ability#can?. def can?(*args) current_ability.can?(*args) diff --git a/lib/cancan/exceptions.rb b/lib/cancan/exceptions.rb index c9deb1f3..073e3ac8 100644 --- a/lib/cancan/exceptions.rb +++ b/lib/cancan/exceptions.rb @@ -27,7 +27,8 @@ class AuthorizationNotPerformed < Error; end # exception.default_message = "Default error message" # exception.message # => "Default error message" # - # See ControllerAdditions#authorized! for more information on rescuing from this exception. + # See ControllerAdditions#authorized! for more information on rescuing from this exception + # and customizing the message using I18n. class AccessDenied < Error attr_reader :action, :subject attr_writer :default_message