From 4b5a12d5a0a016f0b4d52f7351a367afed87a2e9 Mon Sep 17 00:00:00 2001 From: Zach Walker Date: Mon, 6 Aug 2018 11:58:29 -0700 Subject: [PATCH 1/5] zw - copy in_controller.rb to in_praxis_controller.rb --- .../in_praxis_controller.rb | 713 ++++++++++++++++++ 1 file changed, 713 insertions(+) create mode 100644 lib/declarative_authorization/in_praxis_controller.rb diff --git a/lib/declarative_authorization/in_praxis_controller.rb b/lib/declarative_authorization/in_praxis_controller.rb new file mode 100644 index 0000000..d7c4939 --- /dev/null +++ b/lib/declarative_authorization/in_praxis_controller.rb @@ -0,0 +1,713 @@ +# Authorization::AuthorizationInController +require File.dirname(__FILE__) + '/authorization.rb' + +module Authorization + module AuthorizationInController + + def self.included(base) # :nodoc: + base.extend(ClassMethods) + base.module_eval do + before_action(:filter_access_filter) if method_defined?(:filter_access_filter) + end + end + + DEFAULT_DENY = false + + # If attribute_check is set for filter_access_to, decl_auth_context will try to + # load the appropriate object from the current controller's model with + # the id from params[:id]. If that fails, a 404 Not Found is often the + # right way to handle the error. If you have additional measures in place + # that restricts the find scope, handling this error as a permission denied + # might be a better way. Set failed_auto_loading_is_not_found to false + # for the latter behavior. + @@failed_auto_loading_is_not_found = true + def self.failed_auto_loading_is_not_found? + @@failed_auto_loading_is_not_found + end + def self.failed_auto_loading_is_not_found=(new_value) + @@failed_auto_loading_is_not_found = new_value + end + + # Returns the Authorization::Engine for the current controller. + def authorization_engine + @authorization_engine ||= Authorization::Engine.instance + end + + # If the current user meets the given privilege, permitted_to? returns true + # and yields to the optional block. The attribute checks that are defined + # in the authorization rules are only evaluated if an object is given + # for context. + # + # See examples for Authorization::AuthorizationHelper #permitted_to? + # + # If no object or context is specified, the controller_name is used as + # context. + # + def permitted_to?(privilege, object_or_sym = nil, options = {}) + if authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, false)) + yield if block_given? + true + else + false + end + end + + # Works similar to the permitted_to? method, but + # throws the authorization exceptions, just like Engine#permit! + def permitted_to!(privilege, object_or_sym = nil, options = {}) + authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, true)) + end + + # While permitted_to? is used for authorization, in some cases + # content should only be shown to some users without being concerned + # with authorization. E.g. to only show the most relevant menu options + # to a certain group of users. That is what has_role? should be used for. + def has_role?(*roles) + user_roles = authorization_engine.roles_for(current_user) + result = roles.all? do |role| + user_roles.include?(role) + end + yield if result and block_given? + result + end + + # Intended to be used where you want to allow users with any single listed role to view + # the content in question + def has_any_role?(*roles) + user_roles = authorization_engine.roles_for(current_user) + result = roles.any? do |role| + user_roles.include?(role) + end + yield if result and block_given? + result + end + + # As has_role? except checks all roles included in the role hierarchy + def has_role_with_hierarchy?(*roles) + user_roles = authorization_engine.roles_with_hierarchy_for(current_user) + result = roles.all? do |role| + user_roles.include?(role) + end + yield if result and block_given? + result + end + + # As has_any_role? except checks all roles included in the role hierarchy + def has_any_role_with_hierarchy?(*roles) + user_roles = authorization_engine.roles_with_hierarchy_for(current_user) + result = roles.any? do |role| + user_roles.include?(role) + end + yield if result and block_given? + result + end + + protected + def filter_access_filter # :nodoc: + permissions = self.class.all_filter_access_permissions + all_permissions = permissions.select {|p| p.actions.include?(:all)} + matching_permissions = permissions.select {|p| p.matches?(action_name)} + allowed = false + auth_exception = nil + begin + allowed = if !matching_permissions.empty? + matching_permissions.all? {|perm| perm.permit!(self)} + elsif !all_permissions.empty? + all_permissions.all? {|perm| perm.permit!(self)} + else + !DEFAULT_DENY + end + rescue NotAuthorized => e + auth_exception = e + end + + unless allowed + if all_permissions.empty? and matching_permissions.empty? + logger.warn "Permission denied: No matching filter access " + + "rule found for #{self.class.controller_name}.#{action_name}" + elsif auth_exception + logger.info "Permission denied: #{auth_exception}" + end + if respond_to?(:permission_denied, true) + # permission_denied needs to render or redirect + send(:permission_denied) + else + send(:render, :plain => "You are not allowed to access this action.", + :status => :forbidden) + end + end + end + + def load_controller_object(context_without_namespace = nil, model = nil) # :nodoc: + instance_var = :"@#{context_without_namespace.to_s.singularize}" + model = model ? model.classify.constantize : context_without_namespace.to_s.classify.constantize + instance_variable_set(instance_var, model.find(params[:id])) + end + + def load_parent_controller_object(parent_context_without_namespace) # :nodoc: + instance_var = :"@#{parent_context_without_namespace.to_s.singularize}" + model = parent_context_without_namespace.to_s.classify.constantize + instance_variable_set(instance_var, model.find(params[:"#{parent_context_without_namespace.to_s.singularize}_id"])) + end + + def new_controller_object_from_params(context_without_namespace, parent_context_without_namespace, strong_params) # :nodoc: + model_or_proxy = parent_context_without_namespace ? + instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) : + context_without_namespace.to_s.classify.constantize + instance_var = :"@#{context_without_namespace.to_s.singularize}" + instance_variable_set(instance_var, + model_or_proxy.new(params[context_without_namespace.to_s.singularize])) + end + + def new_blank_controller_object(context_without_namespace, parent_context_without_namespace, strong_params, model) # :nodoc: + if model + model_or_proxy = model.to_s.classify.constantize + else + model_or_proxy = parent_context_without_namespace ? + instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) : + context_without_namespace.to_s.classify.constantize + end + instance_var = :"@#{context_without_namespace.to_s.singularize}" + instance_variable_set(instance_var, + model_or_proxy.new()) + end + + def new_controller_object_for_collection(context_without_namespace, parent_context_without_namespace, strong_params) # :nodoc: + model_or_proxy = parent_context_without_namespace ? + instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) : + context_without_namespace.to_s.classify.constantize + instance_var = :"@#{context_without_namespace.to_s.singularize}" + instance_variable_set(instance_var, model_or_proxy.new) + end + + def options_for_permit(object_or_sym = nil, options = {}, bang = true) + context = object = nil + if object_or_sym.nil? + context = self.class.decl_auth_context + elsif !Authorization.is_a_association_proxy?(object_or_sym) and object_or_sym.is_a?(Symbol) + context = object_or_sym + else + object = object_or_sym + end + + result = {:object => object, + :context => context, + :skip_attribute_test => object.nil?, + :bang => bang}.merge(options) + result[:user] = current_user unless result.key?(:user) + result + end + + module ClassMethods + # + # Defines a filter to be applied according to the authorization of the + # current user. Requires at least one symbol corresponding to an + # action as parameter. The special symbol :+all+ refers to all actions. + # The all :+all+ statement is only employed if no specific statement is + # present. + # class UserController < ApplicationController + # filter_access_to :index + # filter_access_to :new, :edit + # filter_access_to :all + # ... + # end + # + # The default is to allow access unconditionally if no rule matches. + # Thus, including the +filter_access_to+ :+all+ statement is a good + # idea, implementing a default-deny policy. + # + # When the access is denied, the method +permission_denied+ is called + # on the current controller, if defined. Else, a simple "you are not + # allowed" string is output. Log.info is given more information on the + # reasons of denial. + # + # def permission_denied + # flash[:error] = 'Sorry, you are not allowed to the requested page.' + # respond_to do |format| + # format.html { redirect_to(:back) rescue redirect_to('/') } + # format.xml { head :unauthorized } + # format.js { head :unauthorized } + # end + # end + # + # By default, required privileges are inferred from the action name and + # the controller name. Thus, in UserController :+edit+ requires + # :+edit+ +users+. To specify required privilege, use the option :+require+ + # filter_access_to :new, :create, :require => :create, :context => :users + # + # Without the :+attribute_check+ option, no constraints from the + # authorization rules are enforced because for some actions (collections, + # +new+, +create+), there is no object to evaluate conditions against. To + # allow attribute checks on all actions, it is a common pattern to provide + # custom objects through +before_actions+: + # class BranchesController < ApplicationController + # before_action :load_company + # before_action :new_branch_from_company_and_params, + # :only => [:index, :new, :create] + # filter_access_to :all, :attribute_check => true + # + # protected + # def new_branch_from_company_and_params + # @branch = @company.branches.new(params[:branch]) + # end + # end + # NOTE: +before_actions+ need to be defined before the first + # +filter_access_to+ call. + # + # For further customization, a custom filter expression may be formulated + # in a block, which is then evaluated in the context of the controller + # on a matching request. That is, for checking two objects, use the + # following: + # filter_access_to :merge do + # permitted_to!(:update, User.find(params[:original_id])) and + # permitted_to!(:delete, User.find(params[:id])) + # end + # The block should raise a Authorization::AuthorizationError or return + # false if the access is to be denied. + # + # Later calls to filter_access_to with overlapping actions overwrite + # previous ones for that action. + # + # All options: + # [:+require+] + # Privilege required; defaults to action_name + # [:+context+] + # The privilege's context, defaults to decl_auth_context, which consists + # of controller_name, prepended by any namespaces + # [:+attribute_check+] + # Enables the check of attributes defined in the authorization rules. + # Defaults to false. If enabled, filter_access_to will use a context + # object from one of the following sources (in that order): + # * the method from the :+load_method+ option, + # * an instance variable named after the singular of the context + # (by default from the controller name, e.g. @post for PostsController), + # * a find on the context model, using +params+[:id] as id value. + # Any of these methods will only be employed if :+attribute_check+ + # is enabled. + # [:+model+] + # The data model to load a context object from. Defaults to the + # context, singularized. + # [:+load_method+] + # Specify a method by symbol or a Proc object which should be used + # to load the object. Both should return the loaded object. + # If a Proc object is given, e.g. by way of + # +lambda+, it is called in the instance of the controller. + # Example demonstrating the default behavior: + # filter_access_to :show, :attribute_check => true, + # :load_method => lambda { User.find(params[:id]) } + # + + def filter_access_to(*args, &filter_block) + options = args.last.is_a?(Hash) ? args.pop : {} + options = { + :require => nil, + :context => nil, + :attribute_check => false, + :model => nil, + :load_method => nil, + :strong_parameters => nil + }.merge!(options) + privilege = options[:require] + context = options[:context] + actions = args.flatten + + # prevent setting filter_access_filter multiple times + skip_before_action(:filter_access_filter) if method_defined?(:filter_access_filter) + before_action :filter_access_filter + + filter_access_permissions.each do |perm| + perm.remove_actions(actions) + end + filter_access_permissions << + ControllerPermission.new(actions, privilege, context, + options[:strong_parameters], + options[:attribute_check], + options[:model], + options[:load_method], + filter_block) + end + + # Disables authorization entirely. Requires at least one symbol corresponding + # to an action as parameter. The special symbol :+all+ refers to all actions. + # The all :+all+ statement is only employed if no specific statement is + # present. + def no_filter_access_to(*args) + filter_access_to args do + true + end + end + + # Collecting all the ControllerPermission objects from the controller + # hierarchy. Permissions for actions are overwritten by calls to + # filter_access_to in child controllers with the same action. + def all_filter_access_permissions # :nodoc: + ancestors.inject([]) do |perms, mod| + if mod.respond_to?(:filter_access_permissions, true) + perms + + mod.filter_access_permissions.collect do |p1| + p1.clone.remove_actions(perms.inject(Set.new) {|actions, p2| actions + p2.actions}) + end + else + perms + end + end + end + + # To DRY up the filter_access_to statements in restful controllers, + # filter_resource_access combines typical filter_access_to and + # before_action calls, which set up the instance variables. + # + # The simplest case are top-level resource controllers with only the + # seven CRUD methods, e.g. + # class CompanyController < ApplicationController + # filter_resource_access + # + # def index... + # end + # Here, all CRUD actions are protected through a filter_access_to :all + # statement. :+attribute_check+ is enabled for all actions except for + # the collection action :+index+. To have an object for attribute checks + # available, filter_resource_access will set the instance variable + # @+company+ in before filters. For the member actions (:+show+, :+edit+, + # :+update+, :+destroy+) @company is set to Company.find(params[:id]). + # For +new+ actions (:+new+, :+create+), filter_resource_access creates + # a new object from company parameters: Company.new(params[:company]. + # + # For nested resources, the parent object may be loaded automatically. + # class BranchController < ApplicationController + # filter_resource_access :nested_in => :companies + # end + # Again, the CRUD actions are protected. Now, for all CRUD actions, + # the parent object @company is loaded from params[:company_id]. It is + # also used when creating @branch for +new+ actions. Here, attribute_check + # is enabled for the collection :+index+ as well, checking attributes on a + # @company.branches.new method. + # + # In many cases, the default seven CRUD actions are not sufficient. As in + # the resource definition for routing you may thus give additional member, + # new and collection methods. The +options+ allow you to specify the + # required privileges for each action by providing a hash or an array of + # pairs. By default, for each action the action name is taken as privilege + # (action search in the example below requires the privilege :index + # :companies). Any controller action that is not specified and does not + # belong to the seven CRUD actions is handled as a member method. + # class CompanyController < ApplicationController + # filter_resource_access :collection => [[:search, :index], :index], + # :additional_member => {:mark_as_key_company => :update} + # end + # The +additional_+* options add to the respective CRUD actions, + # the other options (:+member+, :+collection+, :+new+) replace their + # respective CRUD actions. + # filter_resource_access :member => { :toggle_open => :update } + # Would declare :toggle_open as the only member action in the controller and + # require that permission :update is granted for the current user. + # filter_resource_access :additional_member => { :toggle_open => :update } + # Would add a member action :+toggle_open+ to the default members, such as :+show+. + # + # If :+collection+ is an array of method names filter_resource_access will + # associate a permission with the method that is the same as the method + # name and no attribute checks will be performed unless + # :attribute_check => true + # is added in the options. + # + # You can override the default object loading by implementing any of the + # following instance methods on the controller. Examples are given for the + # BranchController (with +nested_in+ set to :+companies+): + # [+new_branch_from_params+] + # Used for +new+ actions. + # [+new_branch_for_collection+] + # Used for +collection+ actions if the +nested_in+ option is set. + # [+load_branch+] + # Used for +member+ actions. + # [+load_company+] + # Used for all +new+, +member+, and +collection+ actions if the + # +nested_in+ option is set. + # + # All options: + # [:+member+] + # Member methods are actions like +show+, which have an params[:id] from + # which to load the controller object and assign it to @controller_name, + # e.g. @+branch+. + # + # By default, member actions are [:+show+, :+edit+, :+update+, + # :+destroy+]. Also, any action not belonging to the seven CRUD actions + # are handled as member actions. + # + # There are three different syntax to specify member, collection and + # new actions. + # * Hash: Lets you set the required privilege for each action: + # {:+show+ => :+show+, :+mark_as_important+ => :+update+} + # * Array of actions or pairs: [:+show+, [:+mark_as_important+, :+update+]], + # with single actions requiring the privilege of the same name as the method. + # * Single method symbol: :+show+ + # [:+additional_member+] + # Allows to add additional member actions to the default resource +member+ + # actions. + # [:+collection+] + # Collection actions are like :+index+, actions without any controller object + # to check attributes of. If +nested_in+ is given, a new object is + # created from the parent object, e.g. @company.branches.new. Without + # +nested_in+, attribute check is deactivated for these actions. By + # default, collection is set to :+index+. + # [:+additional_collection+] + # Allows to add additional collection actions to the default resource +collection+ + # actions. + # [:+new+] + # +new+ methods are actions such as +new+ and +create+, which don't + # receive a params[:id] to load an object from, but + # a params[:controller_name_singular] hash with attributes for a new + # object. The attributes will be used here to create a new object and + # check the object against the authorization rules. The object is + # assigned to @controller_name_singular, e.g. @branch. + # + # If +nested_in+ is given, the new object + # is created from the parent_object.controller_name + # proxy, e.g. company.branches.new(params[:branch]). By default, + # +new+ is set to [:new, :create]. + # [:+additional_new+] + # Allows to add additional new actions to the default resource +new+ actions. + # [:+context+] + # The context is used to determine the model to load objects from for the + # before_actions and the context of privileges to use in authorization + # checks. + # [:+nested_in+] + # Specifies the parent controller if the resource is nested in another + # one. This is used to automatically load the parent object, e.g. + # @+company+ from params[:company_id] for a BranchController nested in + # a CompanyController. + # [:+shallow+] + # Only relevant when used in conjunction with +nested_in+. Specifies a nested resource + # as being a shallow nested resource, resulting in the controller not attempting to + # load a parent object for all member actions defined by +member+ and + # +additional_member+ or rather the default member actions (:+show+, :+edit+, + # :+update+, :+destroy+). + # [:+no_attribute_check+] + # Allows to set actions for which no attribute check should be performed. + # See filter_access_to on details. By default, with no +nested_in+, + # +no_attribute_check+ is set to all collections. If +nested_in+ is given + # +no_attribute_check+ is empty by default. + # [:+strong_parameters+] + # If set to true, relies on controller to provide instance variable and + # create new object in :create action. Set true if you use strong_params + # and false if you use protected_attributes. + # + def filter_resource_access(options = {}) + options = { + :new => [:new, :create], + :additional_new => nil, + :member => [:show, :edit, :update, :destroy], + :additional_member => nil, + :collection => [:index], + :additional_collection => nil, + #:new_method_for_collection => nil, # only symbol method name + #:new_method => nil, # only symbol method name + #:load_method => nil, # only symbol method name + :no_attribute_check => nil, + :context => nil, + :model => nil, + :nested_in => nil, + :strong_parameters => nil + }.merge(options) + options.merge!({ :strong_parameters => true }) if options[:strong_parameters] == nil + + new_actions = actions_from_option( options[:new] ).merge( + actions_from_option(options[:additional_new]) ) + members = actions_from_option(options[:member]).merge( + actions_from_option(options[:additional_member])) + collections = actions_from_option(options[:collection]).merge( + actions_from_option(options[:additional_collection])) + + no_attribute_check_actions = options[:strong_parameters] ? actions_from_option(options[:collection]).merge(actions_from_option([:create])) : collections + + options[:no_attribute_check] ||= no_attribute_check_actions.keys unless options[:nested_in] + + unless options[:nested_in].blank? + load_parent_method = :"load_#{options[:nested_in].to_s.singularize}" + shallow_exceptions = options[:shallow] ? {:except => members.keys} : {} + before_action shallow_exceptions do |controller| + if controller.respond_to?(load_parent_method, true) + controller.send(load_parent_method) + else + controller.send(:load_parent_controller_object, options[:nested_in]) + end + end + + new_for_collection_method = :"new_#{controller_name.singularize}_for_collection" + before_action :only => collections.keys do |controller| + # new_for_collection + if controller.respond_to?(new_for_collection_method, true) + controller.send(new_for_collection_method) + else + controller.send(:new_controller_object_for_collection, + options[:context] || controller_name, options[:nested_in], options[:strong_parameters]) + end + end + end + + unless options[:strong_parameters] + new_from_params_method = :"new_#{controller_name.singularize}_from_params" + before_action :only => new_actions.keys do |controller| + # new_from_params + if controller.respond_to?(new_from_params_method, true) + controller.send(new_from_params_method) + else + controller.send(:new_controller_object_from_params, + options[:context] || controller_name, options[:nested_in], options[:strong_parameters]) + end + end + else + new_object_method = :"new_#{controller_name.singularize}" + before_action :only => :new do |controller| + # new_from_params + if controller.respond_to?(new_object_method, true) + controller.send(new_object_method) + else + controller.send(:new_blank_controller_object, + options[:context] || controller_name, options[:nested_in], options[:strong_parameters], options[:model]) + end + end + end + + load_method = :"load_#{controller_name.singularize}" + before_action :only => members.keys do |controller| + # load controller object + if controller.respond_to?(load_method, true) + controller.send(load_method) + else + controller.send(:load_controller_object, options[:context] || controller_name, options[:model]) + end + end + filter_access_to :all, :attribute_check => true, :context => options[:context], :model => options[:model] + + members.merge(new_actions).merge(collections).each do |action, privilege| + if action != privilege or (options[:no_attribute_check] and options[:no_attribute_check].include?(action)) + filter_options = { + :strong_parameters => options[:strong_parameters], + :context => options[:context], + :attribute_check => !options[:no_attribute_check] || !options[:no_attribute_check].include?(action), + :model => options[:model] + } + filter_options[:require] = privilege if action != privilege + filter_access_to(action, filter_options) + end + end + end + + # Returns the context for authorization checks in the current controller. + # Uses the controller_name and prepends any namespaces underscored and + # joined with underscores. + # + # E.g. + # AllThosePeopleController => :all_those_people + # AnyName::Space::ThingsController => :any_name_space_things + # + def decl_auth_context + prefixes = name.split('::')[0..-2].map(&:underscore) + ((prefixes + [controller_name]) * '_').to_sym + end + + protected + def filter_access_permissions # :nodoc: + unless filter_access_permissions? + ancestors[1..-1].reverse.each do |mod| + mod.filter_access_permissions if mod.respond_to?(:filter_access_permissions, true) + end + end + class_variable_set(:@@declarative_authorization_permissions, {}) unless filter_access_permissions? + class_variable_get(:@@declarative_authorization_permissions)[self.name] ||= [] + end + + def filter_access_permissions? # :nodoc: + class_variable_defined?(:@@declarative_authorization_permissions) + end + + def actions_from_option(option) # :nodoc: + case option + when nil + {} + when Symbol, String + {option.to_sym => option.to_sym} + when Hash + option + when Enumerable + option.each_with_object({}) do |action, hash| + if action.is_a?(Array) + raise "Unexpected option format: #{option.inspect}" if action.length != 2 + hash[action.first] = action.last + else + hash[action.to_sym] = action.to_sym + end + end + end + end + end + end + + class ControllerPermission # :nodoc: + attr_reader :actions, :privilege, :context, :attribute_check, :strong_params + def initialize(actions, privilege, context, strong_params, attribute_check = false, + load_object_model = nil, load_object_method = nil, + filter_block = nil) + @actions = actions.to_set + @privilege = privilege + @context = context + @load_object_model = load_object_model + @load_object_method = load_object_method + @filter_block = filter_block + @attribute_check = attribute_check + @strong_params = strong_params + end + + def matches?(action_name) + @actions.include?(action_name.to_sym) + end + + def permit!(contr) + if @filter_block + return contr.instance_eval(&@filter_block) + end + object = @attribute_check ? load_object(contr) : nil + privilege = @privilege || :"#{contr.action_name}" + + contr.authorization_engine.permit!(privilege, + :user => contr.send(:current_user), + :object => object, + :skip_attribute_test => !@attribute_check, + :context => @context || contr.class.decl_auth_context) + end + + def remove_actions(actions) + @actions -= actions + self + end + + private + + def load_object(contr) + if @load_object_method and @load_object_method.is_a?(Symbol) + contr.send(@load_object_method) + elsif @load_object_method and @load_object_method.is_a?(Proc) + contr.instance_eval(&@load_object_method) + else + load_object_model = @load_object_model || + (@context ? @context.to_s.classify.constantize : contr.class.controller_name.classify.constantize) + load_object_model = load_object_model.classify.constantize if load_object_model.is_a?(String) + instance_var = "@#{load_object_model.name.demodulize.underscore}" + object = contr.instance_variable_get(instance_var) + unless object + begin + object = @strong_params ? load_object_model.find_or_initialize_by(:id => contr.params[:id]) : load_object_model.find(contr.params[:id]) + rescue => e + contr.logger.debug("filter_access_to tried to find " + + "#{load_object_model} from params[:id] " + + "(#{contr.params[:id].inspect}), because attribute_check is enabled " + + "and #{instance_var.to_s} isn't set, but failed: #{e.class.name}: #{e}") + raise if AuthorizationInController.failed_auto_loading_is_not_found? + end + contr.instance_variable_set(instance_var, object) + end + object + end + end + end +end From d138f310726467c956251ae1bd16fe54efa77bb8 Mon Sep 17 00:00:00 2001 From: Zach Walker Date: Mon, 6 Aug 2018 12:09:46 -0700 Subject: [PATCH 2/5] zw - adapt to praxis style controllers Removes params related to strong params which is a rails concept. Removes filter_resource_access from the praxis version as it isn't needed for our current use case and requires more work to convert over to praxis --- Gemfile | 3 + lib/declarative_authorization.rb | 1 + .../in_praxis_controller.rb | 348 ++-------------- test/praxis_controller_test.rb | 381 ++++++++++++++++++ test/praxis_dummy/app/controllers/all.rb | 25 ++ test/praxis_dummy/app/controllers/basic.rb | 52 +++ test/praxis_dummy/app/controllers/foo.rb | 5 + .../app/controllers/load_method.rb | 51 +++ .../app/controllers/name_spaced.rb | 25 ++ .../praxis_dummy/app/controllers/overwrite.rb | 25 ++ test/praxis_dummy/app/controllers/people.rb | 24 ++ test/praxis_dummy/app/models/load_method.rb | 7 + test/praxis_dummy/app/models/mock_model.rb | 40 ++ test/praxis_dummy/config/environment.rb | 31 ++ test/praxis_dummy/design/api.rb | 11 + test/praxis_dummy/design/endpoints/all.rb | 28 ++ test/praxis_dummy/design/endpoints/basic.rb | 117 ++++++ test/praxis_dummy/design/endpoints/foo.rb | 27 ++ .../design/endpoints/load_method.rb | 86 ++++ .../design/endpoints/name_spaced.rb | 28 ++ .../design/endpoints/overwrite.rb | 28 ++ test/praxis_dummy/design/endpoints/people.rb | 20 + test/praxis_test_engine.rb | 13 + test/praxis_test_helper.rb | 24 ++ test/test_helper.rb | 7 + 25 files changed, 1101 insertions(+), 306 deletions(-) create mode 100644 test/praxis_controller_test.rb create mode 100644 test/praxis_dummy/app/controllers/all.rb create mode 100644 test/praxis_dummy/app/controllers/basic.rb create mode 100644 test/praxis_dummy/app/controllers/foo.rb create mode 100644 test/praxis_dummy/app/controllers/load_method.rb create mode 100644 test/praxis_dummy/app/controllers/name_spaced.rb create mode 100644 test/praxis_dummy/app/controllers/overwrite.rb create mode 100644 test/praxis_dummy/app/controllers/people.rb create mode 100644 test/praxis_dummy/app/models/load_method.rb create mode 100644 test/praxis_dummy/app/models/mock_model.rb create mode 100644 test/praxis_dummy/config/environment.rb create mode 100644 test/praxis_dummy/design/api.rb create mode 100644 test/praxis_dummy/design/endpoints/all.rb create mode 100644 test/praxis_dummy/design/endpoints/basic.rb create mode 100644 test/praxis_dummy/design/endpoints/foo.rb create mode 100644 test/praxis_dummy/design/endpoints/load_method.rb create mode 100644 test/praxis_dummy/design/endpoints/name_spaced.rb create mode 100644 test/praxis_dummy/design/endpoints/overwrite.rb create mode 100644 test/praxis_dummy/design/endpoints/people.rb create mode 100644 test/praxis_test_engine.rb create mode 100644 test/praxis_test_helper.rb diff --git a/Gemfile b/Gemfile index f7bb4aa..1859ca9 100644 --- a/Gemfile +++ b/Gemfile @@ -5,3 +5,6 @@ gemspec gem 'appraisal', '~> 2.1' gem 'mocha', '~> 1.0', require: false gem 'sqlite3' +gem 'praxis', path: '/Users/zachwalker/Playground/praxis' +gem 'pry' +gem 'pry-byebug' diff --git a/lib/declarative_authorization.rb b/lib/declarative_authorization.rb index 5ca6373..4a03f13 100644 --- a/lib/declarative_authorization.rb +++ b/lib/declarative_authorization.rb @@ -1,5 +1,6 @@ require File.join(%w{declarative_authorization helper}) require File.join(%w{declarative_authorization in_controller}) +require File.join(%w{declarative_authorization in_praxis_controller}) if defined?(ActiveRecord) require File.join(%w{declarative_authorization in_model}) require File.join(%w{declarative_authorization obligation_scope}) diff --git a/lib/declarative_authorization/in_praxis_controller.rb b/lib/declarative_authorization/in_praxis_controller.rb index d7c4939..3e226f9 100644 --- a/lib/declarative_authorization/in_praxis_controller.rb +++ b/lib/declarative_authorization/in_praxis_controller.rb @@ -2,12 +2,14 @@ require File.dirname(__FILE__) + '/authorization.rb' module Authorization - module AuthorizationInController + module AuthorizationInPraxisController def self.included(base) # :nodoc: base.extend(ClassMethods) base.module_eval do - before_action(:filter_access_filter) if method_defined?(:filter_access_filter) + before :action do |controller| + controller.send(:filter_access_filter) + end if method_defined?(:filter_access_filter) end end @@ -102,6 +104,22 @@ def has_any_role_with_hierarchy?(*roles) result end + def controller_name + self.class.name.demodulize.underscore + end + + def action_name + self.request.action.name + end + + def params + self.request.params + end + + def logger + self.request.env['action_dispatch.logger'] + end + protected def filter_access_filter # :nodoc: permissions = self.class.all_filter_access_permissions @@ -124,7 +142,7 @@ def filter_access_filter # :nodoc: unless allowed if all_permissions.empty? and matching_permissions.empty? logger.warn "Permission denied: No matching filter access " + - "rule found for #{self.class.controller_name}.#{action_name}" + "rule found for #{self.controller_name}.#{action_name}" elsif auth_exception logger.info "Permission denied: #{auth_exception}" end @@ -132,54 +150,12 @@ def filter_access_filter # :nodoc: # permission_denied needs to render or redirect send(:permission_denied) else - send(:render, :plain => "You are not allowed to access this action.", - :status => :forbidden) + Praxis::Responses::Forbidden.new(body: "You are not allowed to access this action.") + # send(:render, :plain => "You are not allowed to access this action.", :status => :forbidden) end end end - def load_controller_object(context_without_namespace = nil, model = nil) # :nodoc: - instance_var = :"@#{context_without_namespace.to_s.singularize}" - model = model ? model.classify.constantize : context_without_namespace.to_s.classify.constantize - instance_variable_set(instance_var, model.find(params[:id])) - end - - def load_parent_controller_object(parent_context_without_namespace) # :nodoc: - instance_var = :"@#{parent_context_without_namespace.to_s.singularize}" - model = parent_context_without_namespace.to_s.classify.constantize - instance_variable_set(instance_var, model.find(params[:"#{parent_context_without_namespace.to_s.singularize}_id"])) - end - - def new_controller_object_from_params(context_without_namespace, parent_context_without_namespace, strong_params) # :nodoc: - model_or_proxy = parent_context_without_namespace ? - instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) : - context_without_namespace.to_s.classify.constantize - instance_var = :"@#{context_without_namespace.to_s.singularize}" - instance_variable_set(instance_var, - model_or_proxy.new(params[context_without_namespace.to_s.singularize])) - end - - def new_blank_controller_object(context_without_namespace, parent_context_without_namespace, strong_params, model) # :nodoc: - if model - model_or_proxy = model.to_s.classify.constantize - else - model_or_proxy = parent_context_without_namespace ? - instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) : - context_without_namespace.to_s.classify.constantize - end - instance_var = :"@#{context_without_namespace.to_s.singularize}" - instance_variable_set(instance_var, - model_or_proxy.new()) - end - - def new_controller_object_for_collection(context_without_namespace, parent_context_without_namespace, strong_params) # :nodoc: - model_or_proxy = parent_context_without_namespace ? - instance_variable_get(:"@#{parent_context_without_namespace.to_s.singularize}").send(context_without_namespace.to_sym) : - context_without_namespace.to_s.classify.constantize - instance_var = :"@#{context_without_namespace.to_s.singularize}" - instance_variable_set(instance_var, model_or_proxy.new) - end - def options_for_permit(object_or_sym = nil, options = {}, bang = true) context = object = nil if object_or_sym.nil? @@ -241,17 +217,19 @@ module ClassMethods # allow attribute checks on all actions, it is a common pattern to provide # custom objects through +before_actions+: # class BranchesController < ApplicationController - # before_action :load_company - # before_action :new_branch_from_company_and_params, - # :only => [:index, :new, :create] - # filter_access_to :all, :attribute_check => true + # before do + # load_company + # end + # before actions: [:index, :new, :create] + # new_branch_from_company_and_params + # end # # protected # def new_branch_from_company_and_params # @branch = @company.branches.new(params[:branch]) # end # end - # NOTE: +before_actions+ need to be defined before the first + # NOTE: +before actions+ need to be defined before the first # +filter_access_to+ call. # # For further customization, a custom filter expression may be formulated @@ -297,6 +275,10 @@ module ClassMethods # :load_method => lambda { User.find(params[:id]) } # + def controller_name + self.name.demodulize.underscore + end + def filter_access_to(*args, &filter_block) options = args.last.is_a?(Hash) ? args.pop : {} options = { @@ -304,23 +286,17 @@ def filter_access_to(*args, &filter_block) :context => nil, :attribute_check => false, :model => nil, - :load_method => nil, - :strong_parameters => nil + :load_method => nil }.merge!(options) privilege = options[:require] context = options[:context] actions = args.flatten - # prevent setting filter_access_filter multiple times - skip_before_action(:filter_access_filter) if method_defined?(:filter_access_filter) - before_action :filter_access_filter - filter_access_permissions.each do |perm| perm.remove_actions(actions) end filter_access_permissions << - ControllerPermission.new(actions, privilege, context, - options[:strong_parameters], + PraxisControllerPermission.new(actions, privilege, context, options[:attribute_check], options[:model], options[:load_method], @@ -353,246 +329,6 @@ def all_filter_access_permissions # :nodoc: end end - # To DRY up the filter_access_to statements in restful controllers, - # filter_resource_access combines typical filter_access_to and - # before_action calls, which set up the instance variables. - # - # The simplest case are top-level resource controllers with only the - # seven CRUD methods, e.g. - # class CompanyController < ApplicationController - # filter_resource_access - # - # def index... - # end - # Here, all CRUD actions are protected through a filter_access_to :all - # statement. :+attribute_check+ is enabled for all actions except for - # the collection action :+index+. To have an object for attribute checks - # available, filter_resource_access will set the instance variable - # @+company+ in before filters. For the member actions (:+show+, :+edit+, - # :+update+, :+destroy+) @company is set to Company.find(params[:id]). - # For +new+ actions (:+new+, :+create+), filter_resource_access creates - # a new object from company parameters: Company.new(params[:company]. - # - # For nested resources, the parent object may be loaded automatically. - # class BranchController < ApplicationController - # filter_resource_access :nested_in => :companies - # end - # Again, the CRUD actions are protected. Now, for all CRUD actions, - # the parent object @company is loaded from params[:company_id]. It is - # also used when creating @branch for +new+ actions. Here, attribute_check - # is enabled for the collection :+index+ as well, checking attributes on a - # @company.branches.new method. - # - # In many cases, the default seven CRUD actions are not sufficient. As in - # the resource definition for routing you may thus give additional member, - # new and collection methods. The +options+ allow you to specify the - # required privileges for each action by providing a hash or an array of - # pairs. By default, for each action the action name is taken as privilege - # (action search in the example below requires the privilege :index - # :companies). Any controller action that is not specified and does not - # belong to the seven CRUD actions is handled as a member method. - # class CompanyController < ApplicationController - # filter_resource_access :collection => [[:search, :index], :index], - # :additional_member => {:mark_as_key_company => :update} - # end - # The +additional_+* options add to the respective CRUD actions, - # the other options (:+member+, :+collection+, :+new+) replace their - # respective CRUD actions. - # filter_resource_access :member => { :toggle_open => :update } - # Would declare :toggle_open as the only member action in the controller and - # require that permission :update is granted for the current user. - # filter_resource_access :additional_member => { :toggle_open => :update } - # Would add a member action :+toggle_open+ to the default members, such as :+show+. - # - # If :+collection+ is an array of method names filter_resource_access will - # associate a permission with the method that is the same as the method - # name and no attribute checks will be performed unless - # :attribute_check => true - # is added in the options. - # - # You can override the default object loading by implementing any of the - # following instance methods on the controller. Examples are given for the - # BranchController (with +nested_in+ set to :+companies+): - # [+new_branch_from_params+] - # Used for +new+ actions. - # [+new_branch_for_collection+] - # Used for +collection+ actions if the +nested_in+ option is set. - # [+load_branch+] - # Used for +member+ actions. - # [+load_company+] - # Used for all +new+, +member+, and +collection+ actions if the - # +nested_in+ option is set. - # - # All options: - # [:+member+] - # Member methods are actions like +show+, which have an params[:id] from - # which to load the controller object and assign it to @controller_name, - # e.g. @+branch+. - # - # By default, member actions are [:+show+, :+edit+, :+update+, - # :+destroy+]. Also, any action not belonging to the seven CRUD actions - # are handled as member actions. - # - # There are three different syntax to specify member, collection and - # new actions. - # * Hash: Lets you set the required privilege for each action: - # {:+show+ => :+show+, :+mark_as_important+ => :+update+} - # * Array of actions or pairs: [:+show+, [:+mark_as_important+, :+update+]], - # with single actions requiring the privilege of the same name as the method. - # * Single method symbol: :+show+ - # [:+additional_member+] - # Allows to add additional member actions to the default resource +member+ - # actions. - # [:+collection+] - # Collection actions are like :+index+, actions without any controller object - # to check attributes of. If +nested_in+ is given, a new object is - # created from the parent object, e.g. @company.branches.new. Without - # +nested_in+, attribute check is deactivated for these actions. By - # default, collection is set to :+index+. - # [:+additional_collection+] - # Allows to add additional collection actions to the default resource +collection+ - # actions. - # [:+new+] - # +new+ methods are actions such as +new+ and +create+, which don't - # receive a params[:id] to load an object from, but - # a params[:controller_name_singular] hash with attributes for a new - # object. The attributes will be used here to create a new object and - # check the object against the authorization rules. The object is - # assigned to @controller_name_singular, e.g. @branch. - # - # If +nested_in+ is given, the new object - # is created from the parent_object.controller_name - # proxy, e.g. company.branches.new(params[:branch]). By default, - # +new+ is set to [:new, :create]. - # [:+additional_new+] - # Allows to add additional new actions to the default resource +new+ actions. - # [:+context+] - # The context is used to determine the model to load objects from for the - # before_actions and the context of privileges to use in authorization - # checks. - # [:+nested_in+] - # Specifies the parent controller if the resource is nested in another - # one. This is used to automatically load the parent object, e.g. - # @+company+ from params[:company_id] for a BranchController nested in - # a CompanyController. - # [:+shallow+] - # Only relevant when used in conjunction with +nested_in+. Specifies a nested resource - # as being a shallow nested resource, resulting in the controller not attempting to - # load a parent object for all member actions defined by +member+ and - # +additional_member+ or rather the default member actions (:+show+, :+edit+, - # :+update+, :+destroy+). - # [:+no_attribute_check+] - # Allows to set actions for which no attribute check should be performed. - # See filter_access_to on details. By default, with no +nested_in+, - # +no_attribute_check+ is set to all collections. If +nested_in+ is given - # +no_attribute_check+ is empty by default. - # [:+strong_parameters+] - # If set to true, relies on controller to provide instance variable and - # create new object in :create action. Set true if you use strong_params - # and false if you use protected_attributes. - # - def filter_resource_access(options = {}) - options = { - :new => [:new, :create], - :additional_new => nil, - :member => [:show, :edit, :update, :destroy], - :additional_member => nil, - :collection => [:index], - :additional_collection => nil, - #:new_method_for_collection => nil, # only symbol method name - #:new_method => nil, # only symbol method name - #:load_method => nil, # only symbol method name - :no_attribute_check => nil, - :context => nil, - :model => nil, - :nested_in => nil, - :strong_parameters => nil - }.merge(options) - options.merge!({ :strong_parameters => true }) if options[:strong_parameters] == nil - - new_actions = actions_from_option( options[:new] ).merge( - actions_from_option(options[:additional_new]) ) - members = actions_from_option(options[:member]).merge( - actions_from_option(options[:additional_member])) - collections = actions_from_option(options[:collection]).merge( - actions_from_option(options[:additional_collection])) - - no_attribute_check_actions = options[:strong_parameters] ? actions_from_option(options[:collection]).merge(actions_from_option([:create])) : collections - - options[:no_attribute_check] ||= no_attribute_check_actions.keys unless options[:nested_in] - - unless options[:nested_in].blank? - load_parent_method = :"load_#{options[:nested_in].to_s.singularize}" - shallow_exceptions = options[:shallow] ? {:except => members.keys} : {} - before_action shallow_exceptions do |controller| - if controller.respond_to?(load_parent_method, true) - controller.send(load_parent_method) - else - controller.send(:load_parent_controller_object, options[:nested_in]) - end - end - - new_for_collection_method = :"new_#{controller_name.singularize}_for_collection" - before_action :only => collections.keys do |controller| - # new_for_collection - if controller.respond_to?(new_for_collection_method, true) - controller.send(new_for_collection_method) - else - controller.send(:new_controller_object_for_collection, - options[:context] || controller_name, options[:nested_in], options[:strong_parameters]) - end - end - end - - unless options[:strong_parameters] - new_from_params_method = :"new_#{controller_name.singularize}_from_params" - before_action :only => new_actions.keys do |controller| - # new_from_params - if controller.respond_to?(new_from_params_method, true) - controller.send(new_from_params_method) - else - controller.send(:new_controller_object_from_params, - options[:context] || controller_name, options[:nested_in], options[:strong_parameters]) - end - end - else - new_object_method = :"new_#{controller_name.singularize}" - before_action :only => :new do |controller| - # new_from_params - if controller.respond_to?(new_object_method, true) - controller.send(new_object_method) - else - controller.send(:new_blank_controller_object, - options[:context] || controller_name, options[:nested_in], options[:strong_parameters], options[:model]) - end - end - end - - load_method = :"load_#{controller_name.singularize}" - before_action :only => members.keys do |controller| - # load controller object - if controller.respond_to?(load_method, true) - controller.send(load_method) - else - controller.send(:load_controller_object, options[:context] || controller_name, options[:model]) - end - end - filter_access_to :all, :attribute_check => true, :context => options[:context], :model => options[:model] - - members.merge(new_actions).merge(collections).each do |action, privilege| - if action != privilege or (options[:no_attribute_check] and options[:no_attribute_check].include?(action)) - filter_options = { - :strong_parameters => options[:strong_parameters], - :context => options[:context], - :attribute_check => !options[:no_attribute_check] || !options[:no_attribute_check].include?(action), - :model => options[:model] - } - filter_options[:require] = privilege if action != privilege - filter_access_to(action, filter_options) - end - end - end - # Returns the context for authorization checks in the current controller. # Uses the controller_name and prepends any namespaces underscored and # joined with underscores. @@ -607,6 +343,7 @@ def decl_auth_context end protected + def filter_access_permissions # :nodoc: unless filter_access_permissions? ancestors[1..-1].reverse.each do |mod| @@ -643,9 +380,9 @@ def actions_from_option(option) # :nodoc: end end - class ControllerPermission # :nodoc: - attr_reader :actions, :privilege, :context, :attribute_check, :strong_params - def initialize(actions, privilege, context, strong_params, attribute_check = false, + class PraxisControllerPermission # :nodoc: + attr_reader :actions, :privilege, :context, :attribute_check + def initialize(actions, privilege, context, attribute_check = false, load_object_model = nil, load_object_method = nil, filter_block = nil) @actions = actions.to_set @@ -655,7 +392,6 @@ def initialize(actions, privilege, context, strong_params, attribute_check = fal @load_object_method = load_object_method @filter_block = filter_block @attribute_check = attribute_check - @strong_params = strong_params end def matches?(action_name) @@ -690,13 +426,13 @@ def load_object(contr) contr.instance_eval(&@load_object_method) else load_object_model = @load_object_model || - (@context ? @context.to_s.classify.constantize : contr.class.controller_name.classify.constantize) + (@context ? @context.to_s.classify.constantize : contr.controller_name.classify.constantize) load_object_model = load_object_model.classify.constantize if load_object_model.is_a?(String) instance_var = "@#{load_object_model.name.demodulize.underscore}" object = contr.instance_variable_get(instance_var) unless object begin - object = @strong_params ? load_object_model.find_or_initialize_by(:id => contr.params[:id]) : load_object_model.find(contr.params[:id]) + object = load_object_model.find(contr.params[:id]) rescue => e contr.logger.debug("filter_access_to tried to find " + "#{load_object_model} from params[:id] " + diff --git a/test/praxis_controller_test.rb b/test/praxis_controller_test.rb new file mode 100644 index 0000000..c7bf4e7 --- /dev/null +++ b/test/praxis_controller_test.rb @@ -0,0 +1,381 @@ +require 'test_helper' + +class BasicPraxisControllerTest < PraxisTestCase + + def setup + header('X-API-Version', '1') + end + + def test_filter_access_to_receiving_an_explicit_array + reader = Authorization::Reader::DSLReader.new + + reader.parse %{ + authorization do + role :test_action_group_2 do + has_permission_on :praxis_dummy_controllers_basic, :to => :action_group_action_2 + end + end + } + + request!(PraxisDummy::Controllers::Basic, MockUser.new(:test_action_group_2), "/praxis_test_engine/basic/action_group_action_2", reader) + assert_equal 200, response.status + + request!(PraxisDummy::Controllers::Basic, MockUser.new(:test_action_group_2), "/praxis_test_engine/basic/action_group_action_1", reader) + assert_equal 403, response.status + + request!(PraxisDummy::Controllers::Basic, nil, "/praxis_test_engine/basic/action_group_action_2", reader) + assert_equal 403, response.status + end + + def test_filter_access + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :permissions, :to => :test + has_permission_on :praxis_dummy_controllers_basic, :to => :show_stuff + end + end + } + + request!(PraxisDummy::Controllers::Basic, MockUser.new(:test_role), "/praxis_test_engine/basic/test_action", reader) + assert_equal 200, response.status + + request!(PraxisDummy::Controllers::Basic, MockUser.new(:test_role), "/praxis_test_engine/basic/test_action_2", reader) + assert_equal 403, response.status + + request!(PraxisDummy::Controllers::Basic, MockUser.new(:test_role_2), "/praxis_test_engine/basic/test_action_2", reader) + assert_equal 403, response.status + + request!(PraxisDummy::Controllers::Basic, MockUser.new(:test_role), "/praxis_test_engine/basic/show_stuff/99", reader) + assert_equal 200, response.status + end + + def test_filter_access_multi_actions + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :permissions, :to => :test + end + end + } + + request!(PraxisDummy::Controllers::Basic, MockUser.new(:test_role), "/praxis_test_engine/basic/", reader, method: :post) + assert_equal 200, response.status + end + + def test_filter_access_unprotected_actions + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + end + end + } + request!(PraxisDummy::Controllers::Basic, MockUser.new(:test_role), "/praxis_test_engine/basic/unprotected_action", reader) + assert_equal 200, response.status + end + + def test_filter_access_priv_hierarchy + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + privileges do + privilege :read do + includes :list, :show_stuff + end + end + authorization do + role :test_role do + has_permission_on :praxis_dummy_controllers_basic, :to => :read + end + end + } + request!(PraxisDummy::Controllers::Basic, MockUser.new(:test_role), "/praxis_test_engine/basic/show_stuff/99", reader) + assert_equal 200, response.status + end + + def test_filter_access_skip_attribute_test + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :permissions, :to => :test do + if_attribute :id => is { user } + end + end + end + } + request!(PraxisDummy::Controllers::Basic, MockUser.new(:test_role), "/praxis_test_engine/basic/new", reader) + assert_equal 200, response.status + end + + + def test_existing_instance_var_remains_unchanged + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :permissions, :to => :test do + if_attribute :id => is { 5 } + end + end + end + } + PraxisDummy::Controllers::Basic.any_instance.expects(:instance_variable_get).once.with('@mock_model').returns(PraxisDummy::Models::MockModel.new(id: 5)) + PraxisDummy::Controllers::Basic.any_instance.expects(:instance_variable_set).with('@mock_model', anything).never + request!(PraxisDummy::Controllers::Basic, MockUser.new(:test_role), "/praxis_test_engine/basic/edit", reader) + assert_equal 200, response.status + end + + def test_permitted_to_without_context__no_current_user + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :praxis_dummy_controllers_basic, :to => :foo + end + end + } + PraxisDummy::Controllers::Basic.any_instance.stubs(:authorization_engine).returns(Authorization::Engine.new(reader)) + controller = PraxisDummy::Controllers::Basic.new({}) + assert_equal false, controller.permitted_to?(:foo) + assert_equal false, controller.permitted_to?(:bar) + end + + def test_permitted_to_without_context + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :praxis_dummy_controllers_basic, :to => :foo + end + end + } + PraxisDummy::Controllers::Basic.any_instance.stubs(:current_user).returns(MockUser.new(:test_role)) + PraxisDummy::Controllers::Basic.any_instance.stubs(:authorization_engine).returns(Authorization::Engine.new(reader)) + controller = PraxisDummy::Controllers::Basic.new({}) + assert controller.permitted_to?(:foo) + assert_equal false, controller.permitted_to?(:bar) + end +end + + +class FilterAccessToAllPraxisControllerTest < PraxisTestCase + + def setup + header('X-API-Version', '1') + end + + def test_filter_access_all + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :permissions, :to => :test + has_permission_on :praxis_dummy_controllers_all, :to => :show + end + end + } + + request!(PraxisDummy::Controllers::All, MockUser.new(:test_role), '/praxis_test_engine/all/show', reader) + assert_equal 200, response.status + + request!(PraxisDummy::Controllers::All, MockUser.new(:test_role), '/praxis_test_engine/all/view', reader) + assert_equal 200, response.status + + request!(PraxisDummy::Controllers::All, MockUser.new(:test_role_2), '/praxis_test_engine/all/show', reader) + assert_equal 403, response.status + end +end + + +class LoadMethodPraxisControllerTest < PraxisTestCase + + def setup + header('X-API-Version', '1') + end + + def teardown + Authorization::AuthorizationInController.failed_auto_loading_is_not_found = true + end + + def test_filter_access_with_object_load + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :praxis_dummy_controllers_load_method, :to => [:show, :edit] do + if_attribute :id => 1 + if_attribute :id => "1" + end + end + end + } + + request!(PraxisDummy::Controllers::LoadMethod, MockUser.new(:test_role), '/praxis_test_engine/load_method/show', reader, :id => 2) + assert_equal 403, response.status + + request!(PraxisDummy::Controllers::LoadMethod, MockUser.new(:test_role), '/praxis_test_engine/load_method/show', reader, :id => 1) + assert_equal 200, response.status + + PraxisDummy::Controllers::LoadMethod.any_instance.expects(:instance_variable_set).once.with('@load_method', ::LoadMethod.new(id: 1)) + request!(PraxisDummy::Controllers::LoadMethod, MockUser.new(:test_role), '/praxis_test_engine/load_method/edit', reader, :id => 1) + assert_equal 200, response.status + end + + def test_filter_access_object_load_without_param + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :praxis_dummy_controllers_load_method, :to => [:show_id_not_required, :edit] do + if_attribute :id => is {"1"} + end + end + end + } + + Authorization::AuthorizationInController.failed_auto_loading_is_not_found = true + request!(PraxisDummy::Controllers::LoadMethod, MockUser.new(:test_role), '/praxis_test_engine/load_method/show_id_not_required', reader) + assert_equal 500, response.status + + Authorization::AuthorizationInController.failed_auto_loading_is_not_found = false + request!(PraxisDummy::Controllers::LoadMethod, MockUser.new(:test_role), '/praxis_test_engine/load_method/show_id_not_required', reader) + assert_equal 403, response.status + end + + def test_filter_access_with_object_load_custom + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :praxis_dummy_controllers_load_method, :to => :view do + if_attribute :test => is {2} + end + has_permission_on :praxis_dummy_controllers_load_method, :to => :update do + if_attribute :test => is {1} + end + has_permission_on :praxis_dummy_controllers_load_method, :to => :delete do + if_attribute :test => is {2} + end + end + end + } + + request!(PraxisDummy::Controllers::LoadMethod, MockUser.new(:test_role), '/praxis_test_engine/load_method/delete', reader) + assert_equal 403, response.status + + request!(PraxisDummy::Controllers::LoadMethod, MockUser.new(:test_role), '/praxis_test_engine/load_method/update', reader) + assert_equal 200, response.status + + PraxisDummy::Controllers::LoadMethod.any_instance.expects(:load_method).twice.returns(PraxisDummy::Models::MockModel.new(test: 2)) + request!(PraxisDummy::Controllers::LoadMethod, MockUser.new(:test_role), '/praxis_test_engine/load_method/view', reader) + assert_equal 200, response.status + + request!(PraxisDummy::Controllers::LoadMethod, MockUser.new(:test_role_2), '/praxis_test_engine/load_method/view', reader) + assert_equal 403, response.status + end + + def test_filter_access_custom + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :praxis_dummy_controllers_load_method, :to => :edit + end + role :test_role_2 do + has_permission_on :praxis_dummy_controllers_load_method, :to => :create + end + end + } + + request!(PraxisDummy::Controllers::LoadMethod, MockUser.new(:test_role), '/praxis_test_engine/load_method/create', reader) + assert_equal 200, response.status + + request!(PraxisDummy::Controllers::LoadMethod, MockUser.new(:test_role_2), '/praxis_test_engine/load_method/create', reader) + assert_equal 403, response.status + end +end + + +class AccessOverwritePraxisControllerTest < PraxisTestCase + + def setup + header('X-API-Version', '1') + end + + def test_filter_access_overwrite + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :permissions, :to => :test + end + + role :test_role_2 do + has_permission_on :permissions_2, :to => :test + end + end + } + request!(PraxisDummy::Controllers::Overwrite, MockUser.new(:test_role), '/praxis_test_engine/overwrite/test_action_2', reader) + assert_equal 403, response.status + + request!(PraxisDummy::Controllers::Overwrite, MockUser.new(:test_role), '/praxis_test_engine/overwrite/test_action', reader) + assert_equal 200, response.status + end +end + + +class PluralizationPraxisControllerTest < PraxisTestCase + + def setup + header('X-API-Version', '1') + end + + def test_filter_access_people_controller + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :test_role do + has_permission_on :praxis_dummy_controllers_people, :to => :show + end + end + } + + request!(PraxisDummy::Controllers::People, MockUser.new(:test_role), '/praxis_test_engine/people/show', reader) + assert_equal 200, response.status + end +end + + +class NameSpacedPraxisControllerTest < PraxisTestCase + + def setup + header('X-API-Version', '1') + end + + def test_context + reader = Authorization::Reader::DSLReader.new + reader.parse %{ + authorization do + role :permitted_role do + has_permission_on :praxis_dummy_controllers_name_spaced, :to => :show + has_permission_on :name_spaced, :to => :update + end + role :prohibited_role do + has_permission_on :praxis_dummy_controllers_name_spaced, :to => :update + has_permission_on :name_spaced, :to => :show + end + end + } + request!(PraxisDummy::Controllers::NameSpaced, MockUser.new(:permitted_role), '/praxis_test_engine/name_spaced/show', reader) + assert_equal 200, response.status + request!(PraxisDummy::Controllers::NameSpaced, MockUser.new(:prohibited_role), '/praxis_test_engine/name_spaced/show', reader) + assert 403, response.status + request!(PraxisDummy::Controllers::NameSpaced, MockUser.new(:permitted_role), '/praxis_test_engine/name_spaced/update', reader) + assert_equal 200, response.status + request!(PraxisDummy::Controllers::NameSpaced, MockUser.new(:prohibited_role), '/praxis_test_engine/name_spaced/update', reader) + assert 403, response.status + end +end diff --git a/test/praxis_dummy/app/controllers/all.rb b/test/praxis_dummy/app/controllers/all.rb new file mode 100644 index 0000000..d4014ca --- /dev/null +++ b/test/praxis_dummy/app/controllers/all.rb @@ -0,0 +1,25 @@ + + +module PraxisDummy + module Controllers + class All + include Praxis::Controller + include Authorization::AuthorizationInPraxisController + + implements PraxisDummy::Endpoints::All + attr_accessor :current_user + attr_writer :authorization_engine + + filter_access_to :all + filter_access_to :view, :require => :test, :context => :permissions + + def show + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json' }) + end + + def view + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + end + end +end diff --git a/test/praxis_dummy/app/controllers/basic.rb b/test/praxis_dummy/app/controllers/basic.rb new file mode 100644 index 0000000..aa2a27b --- /dev/null +++ b/test/praxis_dummy/app/controllers/basic.rb @@ -0,0 +1,52 @@ + + +module PraxisDummy + module Controllers + class Basic + include Praxis::Controller + include Authorization::AuthorizationInPraxisController + + implements PraxisDummy::Endpoints::Basic + attr_accessor :current_user + attr_writer :authorization_engine + + filter_access_to :test_action, :require => :test, :context => :permissions + filter_access_to :test_action_2, :require => :test, :context => :permissions_2 + filter_access_to :show_stuff + filter_access_to :edit_stuff, :create, :require => :test, :context => :permissions + filter_access_to :edit, :require => :test, :context => :permissions, + :attribute_check => true, :model => PraxisDummy::Models::MockModel + filter_access_to :new, :require => :test, :context => :permissions + + filter_access_to [:action_group_action_1, :action_group_action_2] + + def action_group_action_2 + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json' }) + end + + def test_action + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + + def show_stuff + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + + def create + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + + def unprotected_action + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + + def new + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + + def edit + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + end + end +end diff --git a/test/praxis_dummy/app/controllers/foo.rb b/test/praxis_dummy/app/controllers/foo.rb new file mode 100644 index 0000000..7a21e02 --- /dev/null +++ b/test/praxis_dummy/app/controllers/foo.rb @@ -0,0 +1,5 @@ + +class Foo + include Praxis::Controller + implements Endpoints::Foo +end diff --git a/test/praxis_dummy/app/controllers/load_method.rb b/test/praxis_dummy/app/controllers/load_method.rb new file mode 100644 index 0000000..4851e28 --- /dev/null +++ b/test/praxis_dummy/app/controllers/load_method.rb @@ -0,0 +1,51 @@ + + +module PraxisDummy + module Controllers + class LoadMethod + include Praxis::Controller + include Authorization::AuthorizationInPraxisController + + implements PraxisDummy::Endpoints::LoadMethod + attr_accessor :current_user + attr_writer :authorization_engine + + filter_access_to :show, :show_id_not_required, attribute_check: true, model: PraxisDummy::Models::MockModel + filter_access_to :edit, attribute_check: true + filter_access_to :update, :delete, attribute_check: true, + load_method: proc {PraxisDummy::Models::MockModel.new(test: 1)} + filter_access_to :create do + permitted_to! :edit, :praxis_dummy_controllers_load_method + end + filter_access_to :view, :attribute_check => true, load_method: :load_method + + def show(id:) + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + + def show_id_not_required(id: nil) + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + + def edit(id:) + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + + def update + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + + def delete(id:) + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + + def create + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + + def view(id: nil) + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + end + end +end diff --git a/test/praxis_dummy/app/controllers/name_spaced.rb b/test/praxis_dummy/app/controllers/name_spaced.rb new file mode 100644 index 0000000..d9be332 --- /dev/null +++ b/test/praxis_dummy/app/controllers/name_spaced.rb @@ -0,0 +1,25 @@ + + +module PraxisDummy + module Controllers + class NameSpaced + include Praxis::Controller + include Authorization::AuthorizationInPraxisController + + implements PraxisDummy::Endpoints::NameSpaced + attr_accessor :current_user + attr_writer :authorization_engine + + filter_access_to :show + filter_access_to :update, :context => :name_spaced + + def show + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json' }) + end + + def update + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + end + end +end diff --git a/test/praxis_dummy/app/controllers/overwrite.rb b/test/praxis_dummy/app/controllers/overwrite.rb new file mode 100644 index 0000000..6e46953 --- /dev/null +++ b/test/praxis_dummy/app/controllers/overwrite.rb @@ -0,0 +1,25 @@ + + +module PraxisDummy + module Controllers + class Overwrite + include Praxis::Controller + include Authorization::AuthorizationInPraxisController + + implements PraxisDummy::Endpoints::Overwrite + attr_accessor :current_user + attr_writer :authorization_engine + + filter_access_to :test_action, :test_action_2, :require => :test, :context => :permissions_2 + filter_access_to :test_action, :require => :test, :context => :permissions + + def test_action + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json' }) + end + + def test_action_2 + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + end + end +end diff --git a/test/praxis_dummy/app/controllers/people.rb b/test/praxis_dummy/app/controllers/people.rb new file mode 100644 index 0000000..e1295dc --- /dev/null +++ b/test/praxis_dummy/app/controllers/people.rb @@ -0,0 +1,24 @@ + + +module PraxisDummy + module Controllers + class People + include Praxis::Controller + include Authorization::AuthorizationInPraxisController + + implements PraxisDummy::Endpoints::People + attr_accessor :current_user + attr_writer :authorization_engine + + filter_access_to :all + + def show + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json' }) + end + + def view + Praxis::Responses::Ok.new(headers: {'Content-Type' => 'application/json'}) + end + end + end +end diff --git a/test/praxis_dummy/app/models/load_method.rb b/test/praxis_dummy/app/models/load_method.rb new file mode 100644 index 0000000..a15ff6a --- /dev/null +++ b/test/praxis_dummy/app/models/load_method.rb @@ -0,0 +1,7 @@ +require File.expand_path(File.dirname(__FILE__) + '/mock_model') + +class LoadMethod < PraxisDummy::Models::MockModel + def self.name + "LoadMethod" + end +end diff --git a/test/praxis_dummy/app/models/mock_model.rb b/test/praxis_dummy/app/models/mock_model.rb new file mode 100644 index 0000000..4f0d88e --- /dev/null +++ b/test/praxis_dummy/app/models/mock_model.rb @@ -0,0 +1,40 @@ +module PraxisDummy + module Models + class MockModel + def initialize(attrs = {}) + attrs.each do |key, value| + instance_variable_set(:"@#{key}", value) + self.class.class_eval do + attr_reader key + end + end + end + + def self.descends_from_active_record? + true + end + + def self.table_name + name.tableize + end + + def self.name + "MockModel" + end + + def self.find(*args) + raise StandardError, "Couldn't find #{self.name} with id #{args[0].inspect}" unless args[0] + new :id => args[0] + end + + def self.find_or_initialize_by(args) + raise StandardError, "Syntax error: find_or_initialize by expects a hash: User.find_or_initialize_by(:id => @user.id)" unless args.is_a?(Hash) + new args + end + + def ==(other) + self.id == other.id + end + end + end +end diff --git a/test/praxis_dummy/config/environment.rb b/test/praxis_dummy/config/environment.rb new file mode 100644 index 0000000..207b01e --- /dev/null +++ b/test/praxis_dummy/config/environment.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'praxis/plugins/praxis_mapper_plugin' + +Praxis::Application.configure do |application| + # Use Rack::ContentLength middleware + application.middleware Rack::ContentLength + + application.bootloader.use Praxis::Plugins::PraxisMapperPlugin, + config_data: { + repositories: {}, + log_stats: 'skip' + } + + # Ensure we validate responses + application.config.praxis.validate_responses = true #if %w[development test].include?(ENV['RAILS_ENV']) + + # Configure application layout + application.layout do + map :design, 'design/' do + map :api, 'api.rb' + map :endpoints, '**/endpoints/**/*' + end + map :app, 'app/' do + map :models, '**/models/**/*' + map :controllers, '**/controllers/**/*' + end + end +end + +Praxis::Blueprint.caching_enabled = false diff --git a/test/praxis_dummy/design/api.rb b/test/praxis_dummy/design/api.rb new file mode 100644 index 0000000..5935a0e --- /dev/null +++ b/test/praxis_dummy/design/api.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +Praxis::ApiDefinition.define do |api| + api.info do + name 'praxis_dummy' + title 'praxis_dummy' + base_path '/praxis_test_engine' + consumes 'json' + produces 'json' + end +end diff --git a/test/praxis_dummy/design/endpoints/all.rb b/test/praxis_dummy/design/endpoints/all.rb new file mode 100644 index 0000000..e731afa --- /dev/null +++ b/test/praxis_dummy/design/endpoints/all.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module PraxisDummy + module Endpoints + class All + include Praxis::ResourceDefinition + version '1' + + media_type 'application/json' + + action :show do + description 'All Show Action' + routing {get '/show'} + response :ok + response :unauthorized + response :forbidden + end + + action :view do + description 'All View Action' + routing {get '/view'} + response :ok + response :unauthorized + response :forbidden + end + end + end +end diff --git a/test/praxis_dummy/design/endpoints/basic.rb b/test/praxis_dummy/design/endpoints/basic.rb new file mode 100644 index 0000000..e739861 --- /dev/null +++ b/test/praxis_dummy/design/endpoints/basic.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +module PraxisDummy + module Endpoints + class Basic + include Praxis::ResourceDefinition + version '1' + + media_type 'application/json' + + action :index do + description 'Index Test' + routing {get '/'} + response :ok + response :unauthorized + response :forbidden + end + + action :new do + description 'New Test' + routing {get '/new'} + response :ok + response :unauthorized + response :forbidden + end + + action :edit do + description 'Edit Test' + routing {get '/edit'} + response :ok + response :unauthorized + response :forbidden + end + + action :show_stuff do + description 'Show Test' + routing {get '/show_stuff/:id'} + params do + attribute :id, Integer, required: true + end + response :ok + response :unauthorized + response :forbidden + end + + action :update do + description 'Update Test' + routing {put '/:id'} + params do + attribute :id, required: true + end + response :ok + response :unauthorized + response :forbidden + end + + action :create do + description 'Create Test' + routing {post '/'} + response :ok + response :unauthorized + response :forbidden + end + + action :destroy do + description 'Destroy Test' + routing {delete '/:id'} + params do + attribute :id, required: true + end + response :ok + response :unauthorized + response :forbidden + end + + action :test_action do + description 'test_action' + routing {get '/test_action'} + response :ok + response :unauthorized + response :forbidden + end + + action :test_action_2 do + description 'test_action_2' + routing {get '/test_action_2'} + response :ok + response :unauthorized + response :forbidden + end + + action :unprotected_action do + description 'unprotected_action' + routing {get '/unprotected_action'} + response :ok + response :unauthorized + response :forbidden + end + + action :action_group_action_1 do + description 'action_group_action_1' + routing {get '/action_group_action_1'} + response :ok + response :unauthorized + response :forbidden + end + + action :action_group_action_2 do + description 'action_group_action_2' + routing {get '/action_group_action_2'} + response :ok + response :unauthorized + response :forbidden + end + end + end +end diff --git a/test/praxis_dummy/design/endpoints/foo.rb b/test/praxis_dummy/design/endpoints/foo.rb new file mode 100644 index 0000000..6f49957 --- /dev/null +++ b/test/praxis_dummy/design/endpoints/foo.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Endpoints + class Foo + include Praxis::ResourceDefinition + version '1' + + media_type 'application/json' + + action :show do + description 'Show me the Foo' + routing {get '/:id'} + params do + attribute :id, required: true + end + response :ok + response :unauthorized + end + + action :action_group_action_2 do + description 'action_group_action_2' + routing {get '/action_group_action_2'} + response :ok + response :unauthorized + end + end +end diff --git a/test/praxis_dummy/design/endpoints/load_method.rb b/test/praxis_dummy/design/endpoints/load_method.rb new file mode 100644 index 0000000..f305339 --- /dev/null +++ b/test/praxis_dummy/design/endpoints/load_method.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module PraxisDummy + module Endpoints + class LoadMethod + include Praxis::ResourceDefinition + version '1' + + media_type 'application/json' + + action :show do + description 'Load Method Show Action' + routing {get '/show'} + params do + attribute :id, Integer#, required: true + end + response :ok + response :unauthorized + response :forbidden + end + + action :show_id_not_required do + description 'Load Method Show Action' + routing {get '/show_id_not_required'} + params do + attribute :id, Integer + end + response :ok + response :unauthorized + response :forbidden + end + + action :edit do + description 'Load Method Edit Action' + routing {get '/edit'} + params do + attribute :id, Integer, required: true + end + response :ok + response :unauthorized + response :forbidden + end + + action :view do + description 'Load Method View Action' + routing {get '/view'} + params do + attribute :id, Integer#, required: true + end + response :ok + response :unauthorized + response :forbidden + end + + action :update do + description 'Load Method Update Action' + routing {get '/update'} + # params do + # attribute :id, Integer#, required: true + # end + response :ok + response :unauthorized + response :forbidden + end + + action :delete do + description 'Load Method Delete Action' + routing {get '/delete'} + params do + attribute :id, Integer#, required: true + end + response :ok + response :unauthorized + response :forbidden + end + + action :create do + description 'Load Method Create Action' + routing {get '/create'} + response :ok + response :unauthorized + response :forbidden + end + end + end +end diff --git a/test/praxis_dummy/design/endpoints/name_spaced.rb b/test/praxis_dummy/design/endpoints/name_spaced.rb new file mode 100644 index 0000000..1562f8b --- /dev/null +++ b/test/praxis_dummy/design/endpoints/name_spaced.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module PraxisDummy + module Endpoints + class NameSpaced + include Praxis::ResourceDefinition + version '1' + + media_type 'application/json' + + action :show do + description 'NameSpaced Show Action' + routing {get '/show'} + response :ok + response :unauthorized + response :forbidden + end + + action :update do + description 'NameSpaced Update Action' + routing {get '/update'} + response :ok + response :unauthorized + response :forbidden + end + end + end +end diff --git a/test/praxis_dummy/design/endpoints/overwrite.rb b/test/praxis_dummy/design/endpoints/overwrite.rb new file mode 100644 index 0000000..db5c5fe --- /dev/null +++ b/test/praxis_dummy/design/endpoints/overwrite.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module PraxisDummy + module Endpoints + class Overwrite + include Praxis::ResourceDefinition + version '1' + + media_type 'application/json' + + action :test_action do + description 'Overwrite test_action Action' + routing {get '/test_action'} + response :ok + response :unauthorized + response :forbidden + end + + action :test_action_2 do + description 'Overwrite test_action_2 Action' + routing {get '/test_action_2'} + response :ok + response :unauthorized + response :forbidden + end + end + end +end diff --git a/test/praxis_dummy/design/endpoints/people.rb b/test/praxis_dummy/design/endpoints/people.rb new file mode 100644 index 0000000..8ce3ee7 --- /dev/null +++ b/test/praxis_dummy/design/endpoints/people.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module PraxisDummy + module Endpoints + class People + include Praxis::ResourceDefinition + version '1' + + media_type 'application/json' + + action :show do + description 'People Show Action' + routing {get '/show'} + response :ok + response :unauthorized + response :forbidden + end + end + end +end diff --git a/test/praxis_test_engine.rb b/test/praxis_test_engine.rb new file mode 100644 index 0000000..3c11d13 --- /dev/null +++ b/test/praxis_test_engine.rb @@ -0,0 +1,13 @@ +require 'praxis' + +class PraxisTestEngine < Rails::Engine + initializer 'praxis_test_engine.add_middleware' do |app| + root_path = PraxisTestEngine.root + 'test/praxis_dummy' + mware = Praxis::MiddlewareApp.for(root: root_path, name: 'app-praxis-test') + app.middleware.use mware + + Rails.application.config.after_initialize do + mware.setup + end + end +end diff --git a/test/praxis_test_helper.rb b/test/praxis_test_helper.rb new file mode 100644 index 0000000..b7c525a --- /dev/null +++ b/test/praxis_test_helper.rb @@ -0,0 +1,24 @@ +require 'mocha/minitest' + +class PraxisTestCase < Minitest::Test + include Rack::Test::Methods + + APP = Rack::Builder.app do + run Rails.application + end + + def app + APP + end + + def request!(controller_class, user, url, reader, method: :get, **params) + controller_class.any_instance.stubs(:current_user).returns(user) + controller_class.any_instance.stubs(:authorization_engine).returns(Authorization::Engine.new(reader)) + send(method, url, params) + end + + def response + last_response + end +end + diff --git a/test/test_helper.rb b/test/test_helper.rb index fc6b213..1e1927b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -30,6 +30,7 @@ require DA_ROOT + File.join(%w{lib declarative_authorization authorization}) require DA_ROOT + File.join(%w{lib declarative_authorization in_controller}) +require DA_ROOT + File.join(%w{lib declarative_authorization in_praxis_controller}) require DA_ROOT + File.join(%w{lib declarative_authorization maintenance}) require DA_ROOT + File.join(%w{lib declarative_authorization test helpers}) @@ -118,6 +119,8 @@ class User < ActiveRecord::Base scope :visible_by, ->(user) { where(id: user.id) } end +require DA_ROOT + 'test/praxis_test_engine' + class TestApp class Application < ::Rails::Application config.eager_load = false @@ -129,6 +132,8 @@ class Application < ::Rails::Application end end +require DA_ROOT + 'test/praxis_test_helper' + class ApplicationController < ActionController::Base end @@ -136,6 +141,8 @@ class ApplicationController < ActionController::Base match '/name/spaced_things(/:action)' => 'name/spaced_things', via: [:get, :post, :put, :patch, :delete] match '/deep/name_spaced/things(/:action)' => 'deep/name_spaced/things', via: [:get, :post, :put, :patch, :delete] match '/:controller(/:action(/:id))', via: [:get, :post, :put, :patch, :delete] + + mount PraxisTestEngine, at: "/praxis_test_engine", as: "praxis_test_engine" end ActionController::Base.send :include, Authorization::AuthorizationInController From 23df862596f1896b9dc1ad30782ba0ee7ec26fd9 Mon Sep 17 00:00:00 2001 From: Zach Walker Date: Tue, 14 Aug 2018 10:38:39 -0700 Subject: [PATCH 3/5] zw - cleanup dependencies --- Gemfile | 3 --- declarative_authorization.gemspec | 1 + test/praxis_test_engine.rb | 6 +----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 1859ca9..f7bb4aa 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,3 @@ gemspec gem 'appraisal', '~> 2.1' gem 'mocha', '~> 1.0', require: false gem 'sqlite3' -gem 'praxis', path: '/Users/zachwalker/Playground/praxis' -gem 'pry' -gem 'pry-byebug' diff --git a/declarative_authorization.gemspec b/declarative_authorization.gemspec index 7ca8db8..48347e8 100644 --- a/declarative_authorization.gemspec +++ b/declarative_authorization.gemspec @@ -21,4 +21,5 @@ Gem::Specification.new do |s| s.add_dependency(%q, ['~> 0.5.0']) s.add_dependency(%q, ['>= 4.2.5.2', '< 6']) + s.add_dependency(%q, ['~> 0.21']) end diff --git a/test/praxis_test_engine.rb b/test/praxis_test_engine.rb index 3c11d13..4a47766 100644 --- a/test/praxis_test_engine.rb +++ b/test/praxis_test_engine.rb @@ -3,11 +3,7 @@ class PraxisTestEngine < Rails::Engine initializer 'praxis_test_engine.add_middleware' do |app| root_path = PraxisTestEngine.root + 'test/praxis_dummy' - mware = Praxis::MiddlewareApp.for(root: root_path, name: 'app-praxis-test') + mware = ::Praxis::MiddlewareApp.for(root: root_path) app.middleware.use mware - - Rails.application.config.after_initialize do - mware.setup - end end end From 582549d9580b1cc139456341516781b9575d59ef Mon Sep 17 00:00:00 2001 From: Zach Walker Date: Tue, 14 Aug 2018 10:37:51 -0700 Subject: [PATCH 4/5] zw - refactoring common code out of praxis and rails versions of authorization modules --- .../in_controller.rb | 93 +--------------- .../in_controller_common.rb | 103 ++++++++++++++++++ .../in_praxis_controller.rb | 95 +--------------- 3 files changed, 107 insertions(+), 184 deletions(-) create mode 100644 lib/declarative_authorization/in_controller_common.rb diff --git a/lib/declarative_authorization/in_controller.rb b/lib/declarative_authorization/in_controller.rb index d7c4939..6b3582e 100644 --- a/lib/declarative_authorization/in_controller.rb +++ b/lib/declarative_authorization/in_controller.rb @@ -1,8 +1,9 @@ -# Authorization::AuthorizationInController require File.dirname(__FILE__) + '/authorization.rb' +require File.dirname(__FILE__) + '/in_controller_common.rb' module Authorization module AuthorizationInController + include AuthorizationInControllerCommon def self.included(base) # :nodoc: base.extend(ClassMethods) @@ -11,96 +12,6 @@ def self.included(base) # :nodoc: end end - DEFAULT_DENY = false - - # If attribute_check is set for filter_access_to, decl_auth_context will try to - # load the appropriate object from the current controller's model with - # the id from params[:id]. If that fails, a 404 Not Found is often the - # right way to handle the error. If you have additional measures in place - # that restricts the find scope, handling this error as a permission denied - # might be a better way. Set failed_auto_loading_is_not_found to false - # for the latter behavior. - @@failed_auto_loading_is_not_found = true - def self.failed_auto_loading_is_not_found? - @@failed_auto_loading_is_not_found - end - def self.failed_auto_loading_is_not_found=(new_value) - @@failed_auto_loading_is_not_found = new_value - end - - # Returns the Authorization::Engine for the current controller. - def authorization_engine - @authorization_engine ||= Authorization::Engine.instance - end - - # If the current user meets the given privilege, permitted_to? returns true - # and yields to the optional block. The attribute checks that are defined - # in the authorization rules are only evaluated if an object is given - # for context. - # - # See examples for Authorization::AuthorizationHelper #permitted_to? - # - # If no object or context is specified, the controller_name is used as - # context. - # - def permitted_to?(privilege, object_or_sym = nil, options = {}) - if authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, false)) - yield if block_given? - true - else - false - end - end - - # Works similar to the permitted_to? method, but - # throws the authorization exceptions, just like Engine#permit! - def permitted_to!(privilege, object_or_sym = nil, options = {}) - authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, true)) - end - - # While permitted_to? is used for authorization, in some cases - # content should only be shown to some users without being concerned - # with authorization. E.g. to only show the most relevant menu options - # to a certain group of users. That is what has_role? should be used for. - def has_role?(*roles) - user_roles = authorization_engine.roles_for(current_user) - result = roles.all? do |role| - user_roles.include?(role) - end - yield if result and block_given? - result - end - - # Intended to be used where you want to allow users with any single listed role to view - # the content in question - def has_any_role?(*roles) - user_roles = authorization_engine.roles_for(current_user) - result = roles.any? do |role| - user_roles.include?(role) - end - yield if result and block_given? - result - end - - # As has_role? except checks all roles included in the role hierarchy - def has_role_with_hierarchy?(*roles) - user_roles = authorization_engine.roles_with_hierarchy_for(current_user) - result = roles.all? do |role| - user_roles.include?(role) - end - yield if result and block_given? - result - end - - # As has_any_role? except checks all roles included in the role hierarchy - def has_any_role_with_hierarchy?(*roles) - user_roles = authorization_engine.roles_with_hierarchy_for(current_user) - result = roles.any? do |role| - user_roles.include?(role) - end - yield if result and block_given? - result - end protected def filter_access_filter # :nodoc: diff --git a/lib/declarative_authorization/in_controller_common.rb b/lib/declarative_authorization/in_controller_common.rb new file mode 100644 index 0000000..2a6c3dc --- /dev/null +++ b/lib/declarative_authorization/in_controller_common.rb @@ -0,0 +1,103 @@ +# require File.dirname(__FILE__) + '/authorization.rb' + +module Authorization + module AuthorizationInControllerCommon + + DEFAULT_DENY = false + + def self.included(base) + base.module_eval do + # If attribute_check is set for filter_access_to, decl_auth_context will try to + # load the appropriate object from the current controller's model with + # the id from params[:id]. If that fails, a 404 Not Found is often the + # right way to handle the error. If you have additional measures in place + # that restricts the find scope, handling this error as a permission denied + # might be a better way. Set failed_auto_loading_is_not_found to false + # for the latter behavior. + @@failed_auto_loading_is_not_found = true + + def self.failed_auto_loading_is_not_found? + @@failed_auto_loading_is_not_found + end + + def self.failed_auto_loading_is_not_found=(new_value) + @@failed_auto_loading_is_not_found = new_value + end + end + end + + # Returns the Authorization::Engine for the current controller. + def authorization_engine + @authorization_engine ||= Authorization::Engine.instance + end + + # If the current user meets the given privilege, permitted_to? returns true + # and yields to the optional block. The attribute checks that are defined + # in the authorization rules are only evaluated if an object is given + # for context. + # + # See examples for Authorization::AuthorizationHelper #permitted_to? + # + # If no object or context is specified, the controller_name is used as + # context. + # + def permitted_to?(privilege, object_or_sym = nil, options = {}) + if authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, false)) + yield if block_given? + true + else + false + end + end + + # Works similar to the permitted_to? method, but + # throws the authorization exceptions, just like Engine#permit! + def permitted_to!(privilege, object_or_sym = nil, options = {}) + authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, true)) + end + + # While permitted_to? is used for authorization, in some cases + # content should only be shown to some users without being concerned + # with authorization. E.g. to only show the most relevant menu options + # to a certain group of users. That is what has_role? should be used for. + def has_role?(*roles) + user_roles = authorization_engine.roles_for(current_user) + result = roles.all? do |role| + user_roles.include?(role) + end + yield if result and block_given? + result + end + + # Intended to be used where you want to allow users with any single listed role to view + # the content in question + def has_any_role?(*roles) + user_roles = authorization_engine.roles_for(current_user) + result = roles.any? do |role| + user_roles.include?(role) + end + yield if result and block_given? + result + end + + # As has_role? except checks all roles included in the role hierarchy + def has_role_with_hierarchy?(*roles) + user_roles = authorization_engine.roles_with_hierarchy_for(current_user) + result = roles.all? do |role| + user_roles.include?(role) + end + yield if result and block_given? + result + end + + # As has_any_role? except checks all roles included in the role hierarchy + def has_any_role_with_hierarchy?(*roles) + user_roles = authorization_engine.roles_with_hierarchy_for(current_user) + result = roles.any? do |role| + user_roles.include?(role) + end + yield if result and block_given? + result + end + end +end diff --git a/lib/declarative_authorization/in_praxis_controller.rb b/lib/declarative_authorization/in_praxis_controller.rb index 3e226f9..cbf18b7 100644 --- a/lib/declarative_authorization/in_praxis_controller.rb +++ b/lib/declarative_authorization/in_praxis_controller.rb @@ -1,8 +1,9 @@ -# Authorization::AuthorizationInController require File.dirname(__FILE__) + '/authorization.rb' +require File.dirname(__FILE__) + '/in_controller_common.rb' module Authorization module AuthorizationInPraxisController + include AuthorizationInControllerCommon def self.included(base) # :nodoc: base.extend(ClassMethods) @@ -13,97 +14,6 @@ def self.included(base) # :nodoc: end end - DEFAULT_DENY = false - - # If attribute_check is set for filter_access_to, decl_auth_context will try to - # load the appropriate object from the current controller's model with - # the id from params[:id]. If that fails, a 404 Not Found is often the - # right way to handle the error. If you have additional measures in place - # that restricts the find scope, handling this error as a permission denied - # might be a better way. Set failed_auto_loading_is_not_found to false - # for the latter behavior. - @@failed_auto_loading_is_not_found = true - def self.failed_auto_loading_is_not_found? - @@failed_auto_loading_is_not_found - end - def self.failed_auto_loading_is_not_found=(new_value) - @@failed_auto_loading_is_not_found = new_value - end - - # Returns the Authorization::Engine for the current controller. - def authorization_engine - @authorization_engine ||= Authorization::Engine.instance - end - - # If the current user meets the given privilege, permitted_to? returns true - # and yields to the optional block. The attribute checks that are defined - # in the authorization rules are only evaluated if an object is given - # for context. - # - # See examples for Authorization::AuthorizationHelper #permitted_to? - # - # If no object or context is specified, the controller_name is used as - # context. - # - def permitted_to?(privilege, object_or_sym = nil, options = {}) - if authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, false)) - yield if block_given? - true - else - false - end - end - - # Works similar to the permitted_to? method, but - # throws the authorization exceptions, just like Engine#permit! - def permitted_to!(privilege, object_or_sym = nil, options = {}) - authorization_engine.permit!(privilege, options_for_permit(object_or_sym, options, true)) - end - - # While permitted_to? is used for authorization, in some cases - # content should only be shown to some users without being concerned - # with authorization. E.g. to only show the most relevant menu options - # to a certain group of users. That is what has_role? should be used for. - def has_role?(*roles) - user_roles = authorization_engine.roles_for(current_user) - result = roles.all? do |role| - user_roles.include?(role) - end - yield if result and block_given? - result - end - - # Intended to be used where you want to allow users with any single listed role to view - # the content in question - def has_any_role?(*roles) - user_roles = authorization_engine.roles_for(current_user) - result = roles.any? do |role| - user_roles.include?(role) - end - yield if result and block_given? - result - end - - # As has_role? except checks all roles included in the role hierarchy - def has_role_with_hierarchy?(*roles) - user_roles = authorization_engine.roles_with_hierarchy_for(current_user) - result = roles.all? do |role| - user_roles.include?(role) - end - yield if result and block_given? - result - end - - # As has_any_role? except checks all roles included in the role hierarchy - def has_any_role_with_hierarchy?(*roles) - user_roles = authorization_engine.roles_with_hierarchy_for(current_user) - result = roles.any? do |role| - user_roles.include?(role) - end - yield if result and block_given? - result - end - def controller_name self.class.name.demodulize.underscore end @@ -151,7 +61,6 @@ def filter_access_filter # :nodoc: send(:permission_denied) else Praxis::Responses::Forbidden.new(body: "You are not allowed to access this action.") - # send(:render, :plain => "You are not allowed to access this action.", :status => :forbidden) end end end From 3bef9f817abcd9a8201a1516881c7901001a3b35 Mon Sep 17 00:00:00 2001 From: Zach Walker Date: Fri, 7 Sep 2018 11:58:39 -0700 Subject: [PATCH 5/5] zw - stub instead of set the current user Setting the current user sets a value in Thread.local which may impact ther results of other tests run in the same Thread since the value will persist between test executions. Prior to this change, there were some flakey tests that flaked depending on test execution order --- test/model_test.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/model_test.rb b/test/model_test.rb index 0d6d227..dc3d7d5 100644 --- a/test/model_test.rb +++ b/test/model_test.rb @@ -1436,10 +1436,10 @@ def test_model_security_write_not_allowed_no_privilege } Authorization::Engine.instance(reader) - Authorization.current_user = MockUser.new(:test_role) + Authorization.stubs(:current_user).returns(MockUser.new(:test_role)) assert(object = TestModelSecurityModel.create) - Authorization.current_user = MockUser.new(:test_role_restricted) + Authorization.stubs(:current_user).returns(MockUser.new(:test_role_restricted)) assert_raise Authorization::NotAuthorized do object.update_attributes(:attr_2 => 2) end @@ -1503,11 +1503,11 @@ def test_model_security_with_and_without_find_restrictions } Authorization::Engine.instance(reader) - Authorization.current_user = MockUser.new(:test_role_unrestricted) + Authorization.stubs(:current_user).returns(MockUser.new(:test_role_unrestricted)) object = TestModelSecurityModel.create :attr => 2 object_with_find = TestModelSecurityModelWithFind.create :attr => 2 - Authorization.current_user = MockUser.new(:test_role) + Authorization.stubs(:current_user).returns(MockUser.new(:test_role)) assert_nothing_raised do object.class.find(object.id) end @@ -1559,9 +1559,9 @@ def test_model_security_delete_unallowed } Authorization::Engine.instance(reader) - Authorization.current_user = MockUser.new(:test_role_unrestricted) + Authorization.stubs(:current_user).returns(MockUser.new(:test_role_unrestricted)) object = TestModelSecurityModel.create :attr => 2 - Authorization.current_user = MockUser.new(:test_role) + Authorization.stubs(:current_user).returns(MockUser.new(:test_role)) assert_raise Authorization::AttributeAuthorizationError do object.destroy end