module Authz module Controllers module AuthorizationManager extend ActiveSupport::Concern # Errors # =========================================================================== # Error that will be raised if a controller action has not called the # `authorize` or `skip_authorization` methods. class AuthorizationNotPerformedError < StandardError attr_reader :controller, :action def initialize(options = {}) @controller = options.fetch :controller @action = options.fetch :action message = "#{controller}##{action} is missing authorization." super(message) end end # Error that will be raised if the authorized method is not provided a # scoping instance and the skip_scoping option is not used class MissingScopingInstance < StandardError attr_reader :controller, :action def initialize(options = {}) @controller = options.fetch :controller @action = options.fetch :action message = "#{controller}##{action}. Provide an instance to " \ 'perform authorization or use the skip_scoping option' super(message) end end # Error that will be raised if a user is not authorized class NotAuthorized < StandardError attr_reader :rolable, :controller, :action, :instance def initialize(options = {}) @rolable = options.fetch :rolable @controller = options.fetch :controller @action = options.fetch :action @instance = options.fetch(:instance, nil) message = "#{rolable.class} #{rolable.id} " \ 'does not have a role that allows him to ' \ "#{controller}##{action}" if instance.present? message += " on #{instance}." end super(message) end end # @public api # =========================================================================== protected # 1. Check if the user is correctly skipping scoping # 2. Asks PermissionManager to check for user permission # 3. Asks ScopingManager to verify the user's access to the instance # Managers should handle their own exceptions if a problem is found # # @param [using: Object] the instance that will determine # access in the ScopingManager # @param [skip_scoping: true] to explicitly skip scoping # @return [void] def authorize(using: nil, skip_scoping: false) @_authorization_performed = true authorized = authorized?(controller: params[:controller], action: params[:action], using: using, skip_scoping: skip_scoping) return using if authorized raise NotAuthorized, rolable: authz_user, controller: params[:controller], action: params[:action], instance: using end # Determines if a user is authorized to perform a certain controller action # on a given instance # @param controller: name of the controller # @param action: name of the controller action # @param using: the instance used to determine scope access # @param skip_scoping: option for ignoring scoping during verification def authorized?(controller:, action:, using: nil, skip_scoping: false) # 1. Check if the user is correctly skipping scoping skip_scoping = skip_scoping == true if using.blank? && !skip_scoping raise MissingScopingInstance, controller: controller, action: action end # 2. At least one of the user's roles have both Permission and Scope usr = authz_user usr.roles.each do |role| # a. Check authorization on controller action auth_on_action = PermissionManager.has_permission?(role, controller, action) next unless auth_on_action # b. Check authorization on scoping privileges auth_on_scope = skip_scoping || ScopingManager.has_access_to_instance?(role, using, usr) # c. If a rule is fully authorized, return return true if auth_on_action && auth_on_scope end # 3. After searching all roles, no authorization found return false end # Allow this action not to perform authorization. # @return [void] def skip_authorization @_authorization_performed = true end # Hook method to allow customization of user used in the authorization # process def authz_user send(Authz.current_user_method) end # Raises an error if authorization has not been performed. # `around_action` filter and transaction rollbacks changes in db # if authorization was not performed. # http://guides.rubyonrails.org/action_controller_overview.html#after-filters-and-around-filters # @raise [AuthorizationNotPerformedError] if authorization has not been performed # @return [void] def verify_authorized # Yield gets replaced by the controller action performed: E.g. #show # http://stackoverflow.com/questions/27932270/how-does-an-around-action-callback-work-an-explanation-is-needed ActiveRecord::Base.transaction do yield unless authorization_performed? raise AuthorizationNotPerformedError, controller: self.class, action: self.action_name end end end # Find authz_user and forward to apply scoping rules # # @param on: collection or class on top of which # the user's scoping rules will be applied # @return [Collection] resulting collection from applying all # user's roles scoping rules def apply_authz_scopes(on:) ScopingManager.apply_scopes_for_user(on, authz_user) end # @public api =============================================================== private # @return [Boolean] whether authorization has been performed, i.e. whether # one {#authorize} or {#skip_authorization} has been called def authorization_performed? !!@_authorization_performed end # Returns true if the user has permission for the path # and :using instance given as arguments # # @param path: path or url that will be checked # @param method: of the path or url # @param using: instance that will be used to determine authorization # @param skip_scoping: option to skip scoping validation # @return [Boolean] def authorized_path?(path, method: :get, using: nil, skip_scoping: false) recognized_ca = Rails.application.routes.recognize_path path, method: method controller_name = recognized_ca[:controller] action_name = recognized_ca[:action] authorized?(controller: controller_name, action: action_name, using: using, skip_scoping: skip_scoping) end included do |includer| includer.helper_method :authorized_path? includer.helper_method :apply_authz_scopes includer.helper Authz::Helpers::ViewHelpers includer.helper_method :authz_user end end end end