Permalink
Fetching contributors…
Cannot retrieve contributors at this time
197 lines (168 sloc) 7.86 KB
module Authority
# Gets included into the app's controllers automatically by the railtie
module Controller
extend ActiveSupport::Concern
include ActiveSupport::Rescuable unless defined?(Rails)
def self.security_violation_callback
Proc.new do |exception|
# Through the magic of `instance_exec` `ActionController::Base#rescue_from`
# can call this proc and make `self` the actual controller instance
self.send(Authority.configuration.security_violation_handler, exception)
end
end
included do
rescue_from(Authority::SecurityViolation, :with => Authority::Controller.security_violation_callback)
class_attribute :authority_resource, :instance_reader => false
class_attribute :authority_arguments, :instance_writer => false
end
attr_writer :authorization_performed
def authorization_performed?
!!@authorization_performed
end
def ensure_authorization_performed(options = {})
return if authorization_performed?
return if options[:if] && !send(options[:if])
return if options[:unless] && send(options[:unless])
raise AuthorizationNotPerformed, "No authorization was performed for #{self.class.to_s}##{self.action_name}"
end
module ClassMethods
# Sets up before_filter to ensure user is allowed to perform a given controller action
#
# @param [Class OR Symbol] resource_or_finder - class whose authorizer
# should be consulted, or instance method on the controller which will
# determine that class when the request is made
# @param [Hash] options - can contain :actions to
# be merged with existing
# ones and any other options applicable to a before_filter,
# and can contain an array of :opts to pass to the authorizer
def authorize_actions_for(resource_or_finder, options = {})
self.authority_resource = resource_or_finder
add_actions(options.fetch(:actions, {}))
force_action(options[:all_actions]) if options[:all_actions]
# Capture custom authorization options
self.authority_arguments = options.delete(:args)
if respond_to? :before_action
before_action :run_authorization_check, options
else
before_filter :run_authorization_check, options
end
end
# Allows defining and overriding a controller's map of its actions to the model's authorizer methods
#
# @param [Hash] action_map - controller actions and methods, to be merged with existing action_map
def authority_actions(action_map)
forced_action = action_map.delete(:all_actions)
add_actions(action_map)
force_action(forced_action) if forced_action
end
def authority_action(action_map)
Authority.logger.warn "Authority's `authority_action` method has been renamed \
to `authority_actions` (plural) to reflect the fact that you can \
set multiple actions in one shot. Please update your controllers \
accordingly. (called from #{caller.first})".squeeze(' ')
authority_actions(action_map)
end
# Convenience wrapper for instance method
def ensure_authorization_performed(options = {})
if respond_to? :after_action
after_action(options.slice(:only, :except)) do |controller_instance|
controller_instance.ensure_authorization_performed(options)
end
else
after_filter(options.slice(:only, :except)) do |controller_instance|
controller_instance.ensure_authorization_performed(options)
end
end
end
# The controller action to authority action map used for determining
# which Rails actions map to which authority actions (ex: index to read)
#
# @return [Hash] A duplicated copy of the configured controller_action_map
def authority_action_map
@authority_action_map ||= Authority.configuration.controller_action_map.dup
end
# Adds the passed in actions to the current action map.
#
# @param [Hash] action_map - controller actions and methods to be merged
# with the existing action map
def add_actions(action_map)
authority_action_map.merge!(action_map)
end
# Updates the current action map to use the forced action for all of it's
# actions.
#
# @param [String OR Symbol] forced_action - the authority action to use
# for all Rails actions in the action map
def force_action(forced_action)
add_actions(
Hash[authority_action_map.map {|key, _| [key, forced_action] }]
)
end
end
protected
# To be run in a `before_filter`; ensure this controller action is allowed for the user
# Can be used directly within a controller action as well, given an instance or class with or
# without options to delegate to the authorizer.
#
# @param [Class] authority_resource, the model class associated with this controller
# @param [Hash] options, arbitrary options hash to forward up the chain to the authorizer
# @raise [MissingAction] if controller action isn't a key in `config.controller_action_map`
def authorize_action_for(authority_resource, *options)
# `action_name` comes from ActionController
authority_action = self.class.authority_action_map[action_name.to_sym]
if authority_action.nil?
raise MissingAction.new("No authority action defined for #{action_name}")
end
Authority.enforce(authority_action, authority_resource, authority_user, *options)
# This method is always invoked, but will only log if it's overriden
authority_success(authority_user, authority_action, authority_resource)
self.authorization_performed = true
end
# Renders a static file to minimize the chances of further errors.
#
# @param [Exception] error, an error that indicates the user tried to perform a forbidden action.
def authority_forbidden(error)
Authority.logger.warn(error.message)
render :file => Rails.root.join('public', '403.html'), :status => 403, :layout => false
end
# This method can be overloaded inside the application controller, similar to authority_forbidden.
def authority_success(user, action, resource)
# Do nothing by default, but provide the option for users to override if they will.
end
private
# The `before_filter` that will be setup to run when the class method
# `authorize_actions_for` is called
def run_authorization_check
if instance_authority_resource.is_a?(Array)
# Array includes options; pass as separate args
authorize_action_for(*instance_authority_resource, *authority_arguments)
else
# *resource would be interpreted as resource.to_a, which is wrong and
# actually triggers a query if it's a Sequel model
authorize_action_for(instance_authority_resource, *authority_arguments)
end
end
def instance_authority_resource
case self.class.authority_resource
when Class then self.class.authority_resource
when String, Symbol then send(self.class.authority_resource)
end
rescue NoMethodError
raise MissingResource.new(
"Trying to authorize actions for '#{self.class.authority_resource}', but can't. \
Must be either a resource class OR the name of a controller instance method that \
returns one.".squeeze(' ')
)
end
# Convenience wrapper for sending configured `user_method` to extract the
# request's current user
#
# @return [Object] the user object returned from sending the user_method
def authority_user
send(Authority.configuration.user_method)
end
class MissingAction < StandardError ; end
class MissingResource < StandardError ; end
class AuthorizationNotPerformed < StandardError ; end
end
end