module Walruz
#
# This module provides pretty handy methods to do compositions of basic policies to create
# more complex ones.
#
# === Using a policies file to manage complex policies
#
# It's always a good idea to keep the policy composition in one place, you may place a file in your
# project where you manage all the authorization policies, and then you use them on your models.
#
# Say for example you have a lib/policies.rb in your project where you manage the composition of policies,
# and a lib/policies folder where you create your custom policies.
#
# The lib/policies.rb file could be something like this:
#
# module Policies
# include Walruz::Utils
#
# # Requiring basic Walruz::Policy classes
# BASE = File.join(File.dirname(__FILE__), "policies") unless defined?(BASE)
# require File.join(BASE, "actor_is_admin")
# require File.join(BASE, "user_is_owner")
# require File.join(BASE, "user_is_friend")
#
# #####
# # User Policies
# #
# UserCreatePolicy = ActorIsAdmin
# UserReadPolicy = any(UserIsOwner, UserIsFriend, ActorIsAdmin)
# UserUpdatePolicy = any(UserIsOwner, ActorIsAdmin)
# UserDestroyPolicy = ActorIsAdmin
#
# end
#
# Using a policies file on your project, keeps all your authorization logic just in one place
# that way, when you change the authorizations you just have to go to one place only.
#
module Utils
module PolicyCompositionHelper # :nodoc: all
#-- NOTE: Not using cattr_accessor to avoid dependencies with ActiveSupport
def policies=(policies)
@policies = policies
end
def policies
@policies
end
def policy=(policy)
@policy = policy
end
def policy
@policy
end
def set_params(params = {})
@params ||= {}
@params.merge!(params)
end
def params
@params
end
end
#
# Generates a new policy that merges together different policies by an OR association.
# As soon as one of the policies succeed, the parameters of that policy will be returned.
#
# @param [Array] A set of policies that will be merged by an OR association.
# @return [Walruz::Policy] A new policy class that will execute each policy until one of them succeeds.
#
# @example
# UserReadPolicy = any(UserIsOwner, UserIsAdmin)
#
def any(*policies)
clazz = Class.new(Walruz::Policy) do # :nodoc:
extend PolicyCompositionHelper
def authorized?(actor, subject) # :nodoc:
result = nil
self.class.policies.detect do |policy|
result = policy.new.set_params(params).safe_authorized?(actor, subject)
result[0]
end
result[0] ? result : result[0]
end
end
clazz.policies = policies
clazz
end
#
# Generates a new policy that merges together different policies by an AND association.
# This will execute every policy on the list, if all of them return true then the policy will
# succeed. This process will merge the parameters of each policy, so you may be able to use the parameters
# of previous policies, and at the end it will return all the parameters from every policy.
#
# @param [Array] A set of policies that will be merged by an AND association.
# @return [Walruz::Policy] A new policy class that will check true for each policy.
#
def all(*policies)
clazz = Class.new(Walruz::Policy) do # :nodoc:
extend PolicyCompositionHelper
def authorized?(actor, subject) # :nodoc:
acum = [true, self.params || {}]
self.class.policies.each do |policy|
break unless acum[0]
result = policy.new.set_params(acum[1]).safe_authorized?(actor, subject)
acum[0] &&= result[0]
acum[1].merge!(result[1])
end
acum[0] ? acum : acum[0]
end
def self.policy_keyword # :nodoc:
(self.policies.map { |p| p.policy_keyword.to_s[0..-2] }.join('_and_') + "?").to_sym
end
end
clazz.policies = policies
clazz
end
#
# Generates a new policy that negates the result of the given policy.
# @param [Walruz::Policy] The policy which result is going to be negated.
# @param [Walruz::Policy] A new policy that will negate the result of the given policy.
#
def negate(policy)
clazz = Class.new(Walruz::Policy) do # :nodoc:
extend PolicyCompositionHelper
# :nodoc:
def authorized?(actor, subject)
result = self.class.policy.new.set_params(params).safe_authorized?(actor, subject)
result[0] = !result[0]
result
end
def self.policy_keyword # :nodoc:
keyword = self.policy.policy_keyword.to_s[0..-2]
:"not(#{keyword})?"
end
end
clazz.policy = policy
clazz
end
module_function(:any, :all, :negate)
end
end