Skip to content

Latest commit

 

History

History
115 lines (88 loc) · 4.34 KB

Exception-Handling.md

File metadata and controls

115 lines (88 loc) · 4.34 KB

The CanCan::AccessDenied exception is raised when calling authorize! in the controller and the user is not able to perform the given action. A message can optionally be provided.

authorize! :read, Article, :message => "Unable to read this article."

This exception can also be raised manually if you want more custom behavior.

raise CanCan::AccessDenied.new("Not authorized!", :read, Article)

The message can also be customized through internationalization.

# in config/locales/en.yml
en:
  unauthorized:
    manage:
      all: "Not authorized to %{action} %{subject}."
      user: "Not allowed to manage other user accounts."
    update:
      project: "Not allowed to update this project."

Notice manage and all can be used to generalize the subject and actions. Also %{action} and %{subject} can be used as variables in the message.

You can catch the exception and modify its behavior in the ApplicationController. The behavior may vary depending on the request format. For example here we set the error message to a flash and redirect to the home page for HTML requests and return 403 Forbidden for JSON requests.

class ApplicationController < ActionController::Base
  rescue_from CanCan::AccessDenied do |exception|
    respond_to do |format|
      format.json { head :forbidden }
      format.html { redirect_to main_app.root_url, :alert => exception.message }
    end
  end
end

The action and subject can be retrieved through the exception to customize the behavior further.

exception.action # => :read
exception.subject # => Article

The default error message can also be customized through the exception. This will be used if no message was provided.

exception.default_message = "Default error message"
exception.message # => "Default error message"

If you prefer to return the 403 Forbidden HTTP code, create a public/403.html file and write a rescue_from statement like this example in ApplicationController:

class ApplicationController < ActionController::Base
  rescue_from CanCan::AccessDenied do |exception|
    render :file => "#{Rails.root}/public/403.html", :status => 403, :layout => false
    ## to avoid deprecation warnings with Rails 3.2.x (and incidentally using Ruby 1.9.3 hash syntax)
    ## this render call should be:
    # render file: "#{Rails.root}/public/403", formats: [:html], status: 403, layout: false
  end
end 

403.html must be pure HTML, CSS, and JavaScript--not a template. The fields of the exception are not available to it.

If you are getting unexpected behavior when rescuing from the exception it is best to add some logging . See [[Debugging Abilities]] for details.

Rescuing exceptions for XML responses

If your web application provides a web service which returns XML or JSON responses then you will likely want to handle Authorization properly with a 403 response. You can do so by rendering a response when rescuing from the exception.

rescue_from CanCan::AccessDenied do |exception|
  respond_to do |format|
    format.json { render nothing: true, status: :forbidden }
    format.xml { render xml: '...', status: :forbidden }
    format.html { redirect_to main_app.root_url, alert: exception.message }
  end
end

Danger of exposing sensible information

Please read this thread for more information.

In a Rails application, if a record is not found during load_and_authorize_resource it raises ActiveRecord::NotFound before it checks authentication in the authorize step.

This means that secured routes can have their resources discovered without even being signed in:

$ curl -I https://app.example.com/restricted_resource/does-not-exist
HTTP/1.1 404 Not Found

$ curl -I https://app.example.com/restricted_resource/does-exist-but-not-permitted
HTTP/1.1 302 Found
Location: https://app.example.com/sessions/new

A more secure approach is to always return a 404 status instead of 302:

class ApplicationController < ActionController::Base
  rescue_from CanCan::AccessDenied do |exception|
    respond_to do |format|
      format.json { render nothing: true, status: :not_found }
      format.html { redirect_to main_app.root_url, notice: exception.message, status: :not_found }
      format.js   { render nothing: true, status: :not_found }
    end
  end
end