Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: stffn/declarative_authorization
base: master
...
head fork: malis/declarative_authorization
compare: master
Checking mergeability… Don’t worry, you can still create the pull request.
  • 6 commits
  • 3 files changed
  • 0 commit comments
  • 1 contributor
View
2  declarative_authorization.gemspec
@@ -2,7 +2,7 @@
Gem::Specification.new do |s|
s.name = "declarative_authorization"
- s.version = "0.5.5"
+ s.version = "0.5.6"
s.required_ruby_version = ">= 1.8.6"
s.authors = ["Steffen Bartsch"]
View
1,614 lib/declarative_authorization/authorization.rb
@@ -1,789 +1,825 @@
-# Authorization
-require File.dirname(__FILE__) + '/reader.rb'
-require "set"
-require "forwardable"
-
-module Authorization
- # An exception raised if anything goes wrong in the Authorization realm
- class AuthorizationError < StandardError ; end
- # NotAuthorized is raised if the current user is not allowed to perform
- # the given operation possibly on a specific object.
- class NotAuthorized < AuthorizationError ; end
- # AttributeAuthorizationError is more specific than NotAuthorized, signaling
- # that the access was denied on the grounds of attribute conditions.
- class AttributeAuthorizationError < NotAuthorized ; end
- # AuthorizationUsageError is used whenever a situation is encountered
- # in which the application misused the plugin. That is, if, e.g.,
- # authorization rules may not be evaluated.
- class AuthorizationUsageError < AuthorizationError ; end
- # NilAttributeValueError is raised by Attribute#validate? when it hits a nil attribute value.
- # The exception is raised to ensure that the entire rule is invalidated.
- class NilAttributeValueError < AuthorizationError; end
-
- AUTH_DSL_FILES = [Pathname.new(Rails.root || '').join("config", "authorization_rules.rb").to_s] unless defined? AUTH_DSL_FILES
-
- # Controller-independent method for retrieving the current user.
- # Needed for model security where the current controller is not available.
- def self.current_user
- Thread.current["current_user"] || AnonymousUser.new
- end
-
- # Controller-independent method for setting the current user.
- def self.current_user=(user)
- Thread.current["current_user"] = user
- end
-
- # For use in test cases only
- def self.ignore_access_control (state = nil) # :nodoc:
- Thread.current["ignore_access_control"] = state unless state.nil?
- Thread.current["ignore_access_control"] || false
- end
-
- def self.activate_authorization_rules_browser? # :nodoc:
- ::Rails.env.development?
- end
-
- @@dot_path = "dot"
- def self.dot_path
- @@dot_path
- end
-
- def self.dot_path= (path)
- @@dot_path = path
- end
-
- @@default_role = :guest
- def self.default_role
- @@default_role
- end
-
- def self.default_role= (role)
- @@default_role = role.to_sym
- end
-
- def self.is_a_association_proxy? (object)
- if Rails.version < "3.2"
- object.respond_to?(:proxy_reflection)
- else
- object.respond_to?(:proxy_association)
- end
- end
-
- # Authorization::Engine implements the reference monitor. It may be used
- # for querying the permission and retrieving obligations under which
- # a certain privilege is granted for the current user.
- #
- class Engine
- extend Forwardable
- attr_reader :reader
-
- def_delegators :@reader, :auth_rules_reader, :privileges_reader, :load, :load!
- def_delegators :auth_rules_reader, :auth_rules, :roles, :omnipotent_roles, :role_hierarchy, :role_titles, :role_descriptions
- def_delegators :privileges_reader, :privileges, :privilege_hierarchy
-
- # If +reader+ is not given, a new one is created with the default
- # authorization configuration of +AUTH_DSL_FILES+. If given, may be either
- # a Reader object or a path to a configuration file.
- def initialize (reader = nil)
- #@auth_rules = AuthorizationRuleSet.new reader.auth_rules_reader.auth_rules
- @reader = Reader::DSLReader.factory(reader || AUTH_DSL_FILES)
- end
-
- def initialize_copy (from) # :nodoc:
- @reader = from.reader.clone
- end
-
- # {[priv, ctx] => [priv, ...]}
- def rev_priv_hierarchy
- if @rev_priv_hierarchy.nil?
- @rev_priv_hierarchy = {}
- privilege_hierarchy.each do |key, value|
- value.each do |val|
- @rev_priv_hierarchy[val] ||= []
- @rev_priv_hierarchy[val] << key
- end
- end
- end
- @rev_priv_hierarchy
- end
-
- # {[priv, ctx] => [priv, ...]}
- def rev_role_hierarchy
- if @rev_role_hierarchy.nil?
- @rev_role_hierarchy = {}
- role_hierarchy.each do |higher_role, lower_roles|
- lower_roles.each do |role|
- (@rev_role_hierarchy[role] ||= []) << higher_role
- end
- end
- end
- @rev_role_hierarchy
- end
-
- # Returns true if privilege is met by the current user. Raises
- # AuthorizationError otherwise. +privilege+ may be given with or
- # without context. In the latter case, the :+context+ option is
- # required.
- #
- # Options:
- # [:+context+]
- # The context part of the privilege.
- # Defaults either to the tableized +class_name+ of the given :+object+, if given.
- # That is, :+users+ for :+object+ of type User.
- # Raises AuthorizationUsageError if context is missing and not to be inferred.
- # [:+object+] An context object to test attribute checks against.
- # [:+skip_attribute_test+]
- # Skips those attribute checks in the
- # authorization rules. Defaults to false.
- # [:+user+]
- # The user to check the authorization for.
- # Defaults to Authorization#current_user.
- # [:+bang+]
- # Should NotAuthorized exceptions be raised
- # Defaults to true.
- #
- def permit! (privilege, options = {})
- return true if Authorization.ignore_access_control
- options = {
- :object => nil,
- :skip_attribute_test => false,
- :context => nil,
- :bang => true
- }.merge(options)
-
- # Make sure we're handling all privileges as symbols.
- privilege = privilege.is_a?( Array ) ?
- privilege.flatten.collect { |priv| priv.to_sym } :
- privilege.to_sym
-
- #
- # If the object responds to :proxy_reflection, we're probably working with
- # an association proxy. Use 'new' to leverage ActiveRecord's builder
- # functionality to obtain an object against which we can check permissions.
- #
- # Example: permit!( :edit, :object => user.posts )
- #
- if Authorization.is_a_association_proxy?(options[:object]) && options[:object].respond_to?(:new)
- options[:object] = options[:object].new
- end
-
- options[:context] ||= options[:object] && (
- options[:object].class.respond_to?(:decl_auth_context) ?
- options[:object].class.decl_auth_context :
- options[:object].class.name.tableize.to_sym
- ) rescue NoMethodError
-
- user, roles, privileges = user_roles_privleges_from_options(privilege, options)
-
- return true if roles.is_a?(Array) and not (roles & omnipotent_roles).empty?
-
- # find a authorization rule that matches for at least one of the roles and
- # at least one of the given privileges
- attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
- rules = matching_auth_rules(roles, privileges, options[:context])
-
- # Test each rule in turn to see whether any one of them is satisfied.
- rules.each do |rule|
- return true if rule.validate?(attr_validator, options[:skip_attribute_test])
- end
-
- if options[:bang]
- if rules.empty?
- raise NotAuthorized, "No matching rules found for #{privilege} for #{user.inspect} " +
- "(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
- "context #{options[:context].inspect})."
- else
- raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{(options[:object] || options[:context]).inspect}."
- end
- else
- false
- end
- end
-
- # Calls permit! but doesn't raise authorization errors. If no exception is
- # raised, permit? returns true and yields to the optional block.
- def permit? (privilege, options = {}) # :yields:
- if permit!(privilege, options.merge(:bang=> false))
- yield if block_given?
- true
- else
- false
- end
- end
-
- # Returns the obligations to be met by the current user for the given
- # privilege as an array of obligation hashes in form of
- # [{:object_attribute => obligation_value, ...}, ...]
- # where +obligation_value+ is either (recursively) another obligation hash
- # or a value spec, such as
- # [operator, literal_value]
- # The obligation hashes in the array should be OR'ed, conditions inside
- # the hashes AND'ed.
- #
- # Example
- # {:branch => {:company => [:is, 24]}, :active => [:is, true]}
- #
- # Options
- # [:+context+] See permit!
- # [:+user+] See permit!
- #
- def obligations (privilege, options = {})
- options = {:context => nil}.merge(options)
- user, roles, privileges = user_roles_privleges_from_options(privilege, options)
-
- permit!(privilege, :skip_attribute_test => true, :user => user, :context => options[:context])
-
- return [] if roles.is_a?(Array) and not (roles & omnipotent_roles).empty?
-
- attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
- matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
- rule.obligations(attr_validator)
- end.flatten
- end
-
- # Returns the description for the given role. The description may be
- # specified with the authorization rules. Returns +nil+ if none was
- # given.
- def description_for (role)
- role_descriptions[role]
- end
-
- # Returns the title for the given role. The title may be
- # specified with the authorization rules. Returns +nil+ if none was
- # given.
- def title_for (role)
- role_titles[role]
- end
-
- # Returns the role symbols of the given user.
- def roles_for (user)
- user ||= Authorization.current_user
- raise AuthorizationUsageError, "User object doesn't respond to roles (#{user.inspect})" \
- if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
-
- Rails.logger.info("The use of user.roles is deprecated. Please add a method " +
- "role_symbols to your User model.") if defined?(Rails) and Rails.respond_to?(:logger) and !user.respond_to?(:role_symbols)
-
- roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
-
- raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
- "doesn't return an Array of Symbols (#{roles.inspect})" \
- if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
-
- (roles.empty? ? [Authorization.default_role] : roles)
- end
-
- # Returns the role symbols and inherritted role symbols for the given user
- def roles_with_hierarchy_for(user)
- flatten_roles(roles_for(user))
- end
-
- def self.development_reload?
- if Rails.env.development?
- mod_time = AUTH_DSL_FILES.map { |m| File.mtime(m) rescue Time.at(0) }.flatten.max
- @@auth_dsl_last_modified ||= mod_time
- if mod_time > @@auth_dsl_last_modified
- @@auth_dsl_last_modified = mod_time
- return true
- end
- end
- end
-
- # Returns an instance of Engine, which is created if there isn't one
- # yet. If +dsl_file+ is given, it is passed on to Engine.new and
- # a new instance is always created.
- def self.instance (dsl_file = nil)
- if dsl_file or development_reload?
- @@instance = new(dsl_file)
- else
- @@instance ||= new
- end
- end
-
- class AttributeValidator # :nodoc:
- attr_reader :user, :object, :engine, :context, :privilege
- def initialize (engine, user, object = nil, privilege = nil, context = nil)
- @engine = engine
- @user = user
- @object = object
- @privilege = privilege
- @context = context
- end
-
- def evaluate (value_block)
- # TODO cache?
- instance_eval(&value_block)
- end
- end
-
- private
- def user_roles_privleges_from_options(privilege, options)
- options = {
- :user => nil,
- :context => nil,
- :user_roles => nil
- }.merge(options)
- user = options[:user] || Authorization.current_user
- privileges = privilege.is_a?(Array) ? privilege : [privilege]
-
- raise AuthorizationUsageError, "No user object given (#{user.inspect}) or " +
- "set through Authorization.current_user" unless user
-
- roles = options[:user_roles] || flatten_roles(roles_for(user))
- privileges = flatten_privileges privileges, options[:context]
- [user, roles, privileges]
- end
-
- def flatten_roles (roles, flattened_roles = Set.new)
- # TODO caching?
- roles.reject {|role| flattened_roles.include?(role)}.each do |role|
- flattened_roles << role
- flatten_roles(role_hierarchy[role], flattened_roles) if role_hierarchy[role]
- end
- flattened_roles.to_a
- end
-
- # Returns the privilege hierarchy flattened for given privileges in context.
- def flatten_privileges (privileges, context = nil, flattened_privileges = Set.new)
- # TODO caching?
- raise AuthorizationUsageError, "No context given or inferable from object" unless context
- privileges.reject {|priv| flattened_privileges.include?(priv)}.each do |priv|
- flattened_privileges << priv
- flatten_privileges(rev_priv_hierarchy[[priv, nil]], context, flattened_privileges) if rev_priv_hierarchy[[priv, nil]]
- flatten_privileges(rev_priv_hierarchy[[priv, context]], context, flattened_privileges) if rev_priv_hierarchy[[priv, context]]
- end
- flattened_privileges.to_a
- end
-
- def matching_auth_rules (roles, privileges, context)
- auth_rules.matching(roles, privileges, context)
- end
- end
-
-
- class AuthorizationRuleSet
- include Enumerable
- extend Forwardable
- def_delegators :@rules, :each, :length, :[]
-
- def initialize (rules = [])
- @rules = rules.clone
- reset!
- end
-
- def initialize_copy (source)
- @rules = @rules.collect {|rule| rule.clone}
- reset!
- end
-
- def matching(roles, privileges, context)
- roles = [roles] unless roles.is_a?(Array)
- rules = cached_auth_rules[context] || []
- rules.select do |rule|
- rule.matches? roles, privileges, context
- end
- end
- def delete rule
- @rules.delete rule
- reset!
- end
- def << rule
- @rules << rule
- reset!
- end
- def each &block
- @rules.each &block
- end
-
- private
- def reset!
- @cached_auth_rules =nil
- end
- def cached_auth_rules
- return @cached_auth_rules if @cached_auth_rules
- @cached_auth_rules = {}
- @rules.each do |rule|
- rule.contexts.each do |context|
- @cached_auth_rules[context] ||= []
- @cached_auth_rules[context] << rule
- end
- end
- @cached_auth_rules
- end
- end
- class AuthorizationRule
- attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
- :source_file, :source_line
-
- def initialize (role, privileges = [], contexts = nil, join_operator = :or,
- options = {})
- @role = role
- @privileges = Set.new(privileges)
- @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
- @join_operator = join_operator
- @attributes = []
- @source_file = options[:source_file]
- @source_line = options[:source_line]
- end
-
- def initialize_copy (from)
- @privileges = @privileges.clone
- @contexts = @contexts.clone
- @attributes = @attributes.collect {|attribute| attribute.clone }
- end
-
- def append_privileges (privs)
- @privileges.merge(privs)
- end
-
- def append_attribute (attribute)
- @attributes << attribute
- end
-
- def matches? (roles, privs, context = nil)
- roles = [roles] unless roles.is_a?(Array)
- @contexts.include?(context) and roles.include?(@role) and
- not (@privileges & privs).empty?
- end
-
- def validate? (attr_validator, skip_attribute = false)
- skip_attribute or @attributes.empty? or
- @attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
- begin
- attr.validate?(attr_validator)
- rescue NilAttributeValueError => e
- nil # Bumping up against a nil attribute value flunks the rule.
- end
- end
- end
-
- def obligations (attr_validator)
- exceptions = []
- obligations = @attributes.collect do |attr|
- begin
- attr.obligation(attr_validator)
- rescue NotAuthorized => e
- exceptions << e
- nil
- end
- end
-
- if exceptions.length > 0 and (@join_operator == :and or exceptions.length == @attributes.length)
- raise NotAuthorized, "Missing authorization in collecting obligations: #{exceptions.map(&:to_s) * ", "}"
- end
-
- if @join_operator == :and and !obligations.empty?
- # cross product of OR'ed obligations in arrays
- arrayed_obligations = obligations.map {|obligation| obligation.is_a?(Hash) ? [obligation] : obligation}
- merged_obligations = arrayed_obligations.first
- arrayed_obligations[1..-1].each do |inner_obligations|
- previous_merged_obligations = merged_obligations
- merged_obligations = inner_obligations.collect do |inner_obligation|
- previous_merged_obligations.collect do |merged_obligation|
- merged_obligation.deep_merge(inner_obligation)
- end
- end.flatten
- end
- obligations = merged_obligations
- else
- obligations = obligations.flatten.compact
- end
- obligations.empty? ? [{}] : obligations
- end
-
- def to_long_s
- attributes.collect {|attr| attr.to_long_s } * "; "
- end
- end
-
- class Attribute
- # attr_conditions_hash of form
- # { :object_attribute => [operator, value_block], ... }
- # { :object_attribute => { :attr => ... } }
- def initialize (conditions_hash)
- @conditions_hash = conditions_hash
- end
-
- def initialize_copy (from)
- @conditions_hash = deep_hash_clone(@conditions_hash)
- end
-
- def validate? (attr_validator, object = nil, hash = nil)
- object ||= attr_validator.object
- return false unless object
-
- (hash || @conditions_hash).all? do |attr, value|
- attr_value = object_attribute_value(object, attr)
- if value.is_a?(Hash)
- if attr_value.is_a?(Enumerable)
- attr_value.any? do |inner_value|
- validate?(attr_validator, inner_value, value)
- end
- elsif attr_value == nil
- raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
- else
- validate?(attr_validator, attr_value, value)
- end
- elsif value.is_a?(Array) and value.length == 2 and value.first.is_a?(Symbol)
- evaluated = if value[1].is_a?(Proc)
- attr_validator.evaluate(value[1])
- else
- value[1]
- end
- case value[0]
- when :is
- attr_value == evaluated
- when :is_not
- attr_value != evaluated
- when :contains
- begin
- attr_value.include?(evaluated)
- rescue NoMethodError => e
- raise AuthorizationUsageError, "Operator contains requires a " +
- "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
- "contains #{evaluated.inspect}: #{e}"
- end
- when :does_not_contain
- begin
- !attr_value.include?(evaluated)
- rescue NoMethodError => e
- raise AuthorizationUsageError, "Operator does_not_contain requires a " +
- "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
- "does_not_contain #{evaluated.inspect}: #{e}"
- end
- when :intersects_with
- begin
- !(evaluated.to_set & attr_value.to_set).empty?
- rescue NoMethodError => e
- raise AuthorizationUsageError, "Operator intersects_with requires " +
- "subclasses of Enumerable, got: #{attr_value.inspect} " +
- "intersects_with #{evaluated.inspect}: #{e}"
- end
- when :is_in
- begin
- evaluated.include?(attr_value)
- rescue NoMethodError => e
- raise AuthorizationUsageError, "Operator is_in requires a " +
- "subclass of Enumerable as value, got: #{attr_value.inspect} " +
- "is_in #{evaluated.inspect}: #{e}"
- end
- when :is_not_in
- begin
- !evaluated.include?(attr_value)
- rescue NoMethodError => e
- raise AuthorizationUsageError, "Operator is_not_in requires a " +
- "subclass of Enumerable as value, got: #{attr_value.inspect} " +
- "is_not_in #{evaluated.inspect}: #{e}"
- end
- when :lt
- attr_value && attr_value < evaluated
- when :lte
- attr_value && attr_value <= evaluated
- when :gt
- attr_value && attr_value > evaluated
- when :gte
- attr_value && attr_value >= evaluated
- else
- raise AuthorizationError, "Unknown operator #{value[0]}"
- end
- else
- raise AuthorizationError, "Wrong conditions hash format"
- end
- end
- end
-
- # resolves all the values in condition_hash
- def obligation (attr_validator, hash = nil)
- hash = (hash || @conditions_hash).clone
- hash.each do |attr, value|
- if value.is_a?(Hash)
- hash[attr] = obligation(attr_validator, value)
- elsif value.is_a?(Array) and value.length == 2
- hash[attr] = [value[0], attr_validator.evaluate(value[1])]
- else
- raise AuthorizationError, "Wrong conditions hash format"
- end
- end
- hash
- end
-
- def to_long_s (hash = nil)
- if hash
- hash.inject({}) do |memo, key_val|
- key, val = key_val
- memo[key] = case val
- when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
- when Hash then to_long_s(val)
- end
- memo
- end
- else
- "if_attribute #{to_long_s(@conditions_hash).inspect}"
- end
- end
-
- protected
- def object_attribute_value (object, attr)
- begin
- object.send(attr)
- rescue ArgumentError, NoMethodError => e
- raise AuthorizationUsageError, "Error occurred while validating attribute ##{attr} on #{object.inspect}: #{e}.\n" +
- "Please check your authorization rules and ensure the attribute is correctly spelled and \n" +
- "corresponds to a method on the model you are authorizing for."
- end
- end
-
- def deep_hash_clone (hash)
- hash.inject({}) do |memo, (key, val)|
- memo[key] = case val
- when Hash
- deep_hash_clone(val)
- when NilClass, Symbol
- val
- else
- val.clone
- end
- memo
- end
- end
- end
-
- # An attribute condition that uses existing rules to decide validation
- # and create obligations.
- class AttributeWithPermission < Attribute
- # E.g. privilege :read, attr_or_hash either :attribute or
- # { :attribute => :deeper_attribute }
- def initialize (privilege, attr_or_hash, context = nil)
- @privilege = privilege
- @context = context
- @attr_hash = attr_or_hash
- end
-
- def initialize_copy (from)
- @attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
- end
-
- def validate? (attr_validator, object = nil, hash_or_attr = nil)
- object ||= attr_validator.object
- hash_or_attr ||= @attr_hash
- return false unless object
-
- case hash_or_attr
- when Symbol
- attr_value = object_attribute_value(object, hash_or_attr)
- case attr_value
- when nil
- raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
- when Enumerable
- attr_value.any? do |inner_value|
- attr_validator.engine.permit? @privilege, :object => inner_value, :user => attr_validator.user
- end
- else
- attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
- end
- when Hash
- hash_or_attr.all? do |attr, sub_hash|
- attr_value = object_attribute_value(object, attr)
- if attr_value == nil
- raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
- elsif attr_value.is_a?(Enumerable)
- attr_value.any? do |inner_value|
- validate?(attr_validator, inner_value, sub_hash)
- end
- else
- validate?(attr_validator, attr_value, sub_hash)
- end
- end
- when NilClass
- attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
- else
- raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
- end
- end
-
- # may return an array of obligations to be OR'ed
- def obligation (attr_validator, hash_or_attr = nil, path = [])
- hash_or_attr ||= @attr_hash
- case hash_or_attr
- when Symbol
- @context ||= begin
- rule_model = attr_validator.context.to_s.classify.constantize
- context_reflection = self.class.reflection_for_path(rule_model, path + [hash_or_attr])
- if context_reflection.klass.respond_to?(:decl_auth_context)
- context_reflection.klass.decl_auth_context
- else
- context_reflection.klass.name.tableize.to_sym
- end
- rescue # missing model, reflections
- hash_or_attr.to_s.pluralize.to_sym
- end
-
- obligations = attr_validator.engine.obligations(@privilege,
- :context => @context,
- :user => attr_validator.user)
-
- obligations.collect {|obl| {hash_or_attr => obl} }
- when Hash
- obligations_array_attrs = []
- obligations =
- hash_or_attr.inject({}) do |all, pair|
- attr, sub_hash = pair
- all[attr] = obligation(attr_validator, sub_hash, path + [attr])
- if all[attr].length > 1
- obligations_array_attrs << attr
- else
- all[attr] = all[attr].first
- end
- all
- end
- obligations = [obligations]
- obligations_array_attrs.each do |attr|
- next_array_size = obligations.first[attr].length
- obligations = obligations.collect do |obls|
- (0...next_array_size).collect do |idx|
- obls_wo_array = obls.clone
- obls_wo_array[attr] = obls_wo_array[attr][idx]
- obls_wo_array
- end
- end.flatten
- end
- obligations
- when NilClass
- attr_validator.engine.obligations(@privilege,
- :context => attr_validator.context,
- :user => attr_validator.user)
- else
- raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
- end
- end
-
- def to_long_s
- "if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
- end
-
- private
- def self.reflection_for_path (parent_model, path)
- reflection = path.empty? ? parent_model : begin
- parent = reflection_for_path(parent_model, path[0..-2])
- if !parent.respond_to?(:proxy_reflection) and parent.respond_to?(:klass)
- parent.klass.reflect_on_association(path.last)
- else
- parent.reflect_on_association(path.last)
- end
- rescue
- parent.reflect_on_association(path.last)
- end
- raise "invalid path #{path.inspect}" if reflection.nil?
- reflection
- end
- end
-
- # Represents a pseudo-user to facilitate anonymous users in applications
- class AnonymousUser
- attr_reader :role_symbols
- def initialize (roles = [Authorization.default_role])
- @role_symbols = roles
- end
- end
-end
-
+# Authorization
+require File.dirname(__FILE__) + '/reader.rb'
+require "set"
+require "forwardable"
+
+module Authorization
+ # An exception raised if anything goes wrong in the Authorization realm
+ class AuthorizationError < StandardError ; end
+ # NotAuthorized is raised if the current user is not allowed to perform
+ # the given operation possibly on a specific object.
+ class NotAuthorized < AuthorizationError ; end
+ # AttributeAuthorizationError is more specific than NotAuthorized, signaling
+ # that the access was denied on the grounds of attribute conditions.
+ class AttributeAuthorizationError < NotAuthorized ; end
+ # AuthorizationUsageError is used whenever a situation is encountered
+ # in which the application misused the plugin. That is, if, e.g.,
+ # authorization rules may not be evaluated.
+ class AuthorizationUsageError < AuthorizationError ; end
+ # NilAttributeValueError is raised by Attribute#validate? when it hits a nil attribute value.
+ # The exception is raised to ensure that the entire rule is invalidated.
+ class NilAttributeValueError < AuthorizationError; end
+
+ AUTH_DSL_FILES = [Pathname.new(Rails.root || '').join("config", "authorization_rules.rb").to_s] unless defined? AUTH_DSL_FILES
+
+ # Controller-independent method for retrieving the current user.
+ # Needed for model security where the current controller is not available.
+ def self.current_user
+ Thread.current["current_user"] || AnonymousUser.new
+ end
+
+ # Controller-independent method for setting the current user.
+ def self.current_user=(user)
+ Thread.current["current_user"] = user
+ end
+
+ # For use in test cases only
+ def self.ignore_access_control (state = nil) # :nodoc:
+ Thread.current["ignore_access_control"] = state unless state.nil?
+ Thread.current["ignore_access_control"] || false
+ end
+
+ def self.activate_authorization_rules_browser? # :nodoc:
+ ::Rails.env.development?
+ end
+
+ @@dot_path = "dot"
+ def self.dot_path
+ @@dot_path
+ end
+
+ def self.dot_path= (path)
+ @@dot_path = path
+ end
+
+ @@default_role = :guest
+ def self.default_role
+ @@default_role
+ end
+
+ def self.default_role= (role)
+ @@default_role = role.to_sym
+ end
+
+ def self.is_a_association_proxy? (object)
+ if Rails.version < "3.2"
+ object.respond_to?(:proxy_reflection)
+ else
+ object.respond_to?(:proxy_association)
+ end
+ end
+
+ # Authorization::Engine implements the reference monitor. It may be used
+ # for querying the permission and retrieving obligations under which
+ # a certain privilege is granted for the current user.
+ #
+ class Engine
+ extend Forwardable
+ attr_reader :reader
+
+ def_delegators :@reader, :auth_rules_reader, :privileges_reader, :load, :load!
+ def_delegators :auth_rules_reader, :auth_rules, :roles, :omnipotent_roles, :role_hierarchy, :role_titles, :role_descriptions, :role_dependents, :role_privacy_sign
+ def_delegators :privileges_reader, :privileges, :privilege_hierarchy
+
+ # If +reader+ is not given, a new one is created with the default
+ # authorization configuration of +AUTH_DSL_FILES+. If given, may be either
+ # a Reader object or a path to a configuration file.
+ def initialize (reader = nil)
+ #@auth_rules = AuthorizationRuleSet.new reader.auth_rules_reader.auth_rules
+ @reader = Reader::DSLReader.factory(reader || AUTH_DSL_FILES)
+ end
+
+ def initialize_copy (from) # :nodoc:
+ @reader = from.reader.clone
+ end
+
+ # {[priv, ctx] => [priv, ...]}
+ def rev_priv_hierarchy
+ if @rev_priv_hierarchy.nil?
+ @rev_priv_hierarchy = {}
+ privilege_hierarchy.each do |key, value|
+ value.each do |val|
+ @rev_priv_hierarchy[val] ||= []
+ @rev_priv_hierarchy[val] << key
+ end
+ end
+ end
+ @rev_priv_hierarchy
+ end
+
+ # {[priv, ctx] => [priv, ...]}
+ def rev_role_hierarchy
+ if @rev_role_hierarchy.nil?
+ @rev_role_hierarchy = {}
+ role_hierarchy.each do |higher_role, lower_roles|
+ lower_roles.each do |role|
+ (@rev_role_hierarchy[role] ||= []) << higher_role
+ end
+ end
+ end
+ @rev_role_hierarchy
+ end
+
+ # Returns true if privilege is met by the current user. Raises
+ # AuthorizationError otherwise. +privilege+ may be given with or
+ # without context. In the latter case, the :+context+ option is
+ # required.
+ #
+ # Options:
+ # [:+context+]
+ # The context part of the privilege.
+ # Defaults either to the tableized +class_name+ of the given :+object+, if given.
+ # That is, :+users+ for :+object+ of type User.
+ # Raises AuthorizationUsageError if context is missing and not to be inferred.
+ # [:+object+] An context object to test attribute checks against.
+ # [:+skip_attribute_test+]
+ # Skips those attribute checks in the
+ # authorization rules. Defaults to false.
+ # [:+user+]
+ # The user to check the authorization for.
+ # Defaults to Authorization#current_user.
+ # [:+bang+]
+ # Should NotAuthorized exceptions be raised
+ # Defaults to true.
+ #
+ def permit! (privilege, options = {})
+ return true if Authorization.ignore_access_control
+ options = {
+ :object => nil,
+ :skip_attribute_test => false,
+ :context => nil,
+ :bang => true
+ }.merge(options)
+
+ # Make sure we're handling all privileges as symbols.
+ privilege = privilege.is_a?( Array ) ?
+ privilege.flatten.collect { |priv| priv.to_sym } :
+ privilege.to_sym
+
+ #
+ # If the object responds to :proxy_reflection, we're probably working with
+ # an association proxy. Use 'new' to leverage ActiveRecord's builder
+ # functionality to obtain an object against which we can check permissions.
+ #
+ # Example: permit!( :edit, :object => user.posts )
+ #
+ if Authorization.is_a_association_proxy?(options[:object]) && options[:object].respond_to?(:new)
+ options[:object] = options[:object].new
+ end
+
+ options[:context] ||= options[:object] && (
+ options[:object].class.respond_to?(:decl_auth_context) ?
+ options[:object].class.decl_auth_context :
+ options[:object].class.name.tableize.to_sym
+ ) rescue NoMethodError
+
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
+
+ return true if roles.is_a?(Array) and not (roles & omnipotent_roles).empty?
+
+ # find a authorization rule that matches for at least one of the roles and
+ # at least one of the given privileges
+ attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
+ rules = matching_auth_rules(roles, privileges, options[:context])
+
+ # Test each rule in turn to see whether any one of them is satisfied.
+ rules.each do |rule|
+ return true if rule.validate?(attr_validator, options[:skip_attribute_test])
+ end
+
+ if options[:bang]
+ if rules.empty?
+ raise NotAuthorized, "No matching rules found for #{privilege} for #{user.inspect} " +
+ "(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
+ "context #{options[:context].inspect})."
+ else
+ raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{(options[:object] || options[:context]).inspect}."
+ end
+ else
+ false
+ end
+ end
+
+ # Calls permit! but doesn't raise authorization errors. If no exception is
+ # raised, permit? returns true and yields to the optional block.
+ def permit? (privilege, options = {}) # :yields:
+ if permit!(privilege, options.merge(:bang=> false))
+ yield if block_given?
+ true
+ else
+ false
+ end
+ end
+
+ # Returns the obligations to be met by the current user for the given
+ # privilege as an array of obligation hashes in form of
+ # [{:object_attribute => obligation_value, ...}, ...]
+ # where +obligation_value+ is either (recursively) another obligation hash
+ # or a value spec, such as
+ # [operator, literal_value]
+ # The obligation hashes in the array should be OR'ed, conditions inside
+ # the hashes AND'ed.
+ #
+ # Example
+ # {:branch => {:company => [:is, 24]}, :active => [:is, true]}
+ #
+ # Options
+ # [:+context+] See permit!
+ # [:+user+] See permit!
+ #
+ def obligations (privilege, options = {})
+ options = {:context => nil}.merge(options)
+ user, roles, privileges = user_roles_privleges_from_options(privilege, options)
+
+ permit!(privilege, :skip_attribute_test => true, :user => user, :context => options[:context])
+
+ return [] if roles.is_a?(Array) and not (roles & omnipotent_roles).empty?
+
+ attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
+ matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
+ rule.obligations(attr_validator)
+ end.flatten
+ end
+
+ # Returns the dependent for the given role. The dependent may be
+ # specified with the authorization rules. Returns +nil+ if none was
+ # given.
+ def dependent_for (role)
+ role_dependents[role]
+ end
+
+ # Returns if privacy contract sign is needed for the given role. The
+ # privacy contract sign may be specified with the authorization rules.
+ # Returns +nil+ of false if none was given.
+ def privacy_sign_for (role)
+ role_privacy_sign[role]
+ end
+
+ # Returns the description for the given role. The description may be
+ # specified with the authorization rules. Returns +nil+ if none was
+ # given.
+ def description_for (role)
+ role_descriptions[role]
+ end
+
+ # Returns the title for the given role. The title may be
+ # specified with the authorization rules. Returns +nil+ if none was
+ # given.
+ def title_for (role)
+ role_titles[role]
+ end
+
+ # Returns the role symbols of the given user.
+ def roles_for (user)
+ user ||= Authorization.current_user
+ raise AuthorizationUsageError, "User object doesn't respond to roles (#{user.inspect})" \
+ if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
+
+ Rails.logger.info("The use of user.roles is deprecated. Please add a method " +
+ "role_symbols to your User model.") if defined?(Rails) and Rails.respond_to?(:logger) and !user.respond_to?(:role_symbols)
+
+ roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
+
+ raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
+ "doesn't return an Array of Symbols (#{roles.inspect})" \
+ if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
+
+ (roles.empty? ? [Authorization.default_role] : roles)
+ end
+
+ # Returns the role symbols and inherritted role symbols for the given user
+ def roles_with_hierarchy_for(user)
+ flatten_roles(roles_for(user))
+ end
+
+ def self.development_reload?
+ if Rails.env.development?
+ mod_time = AUTH_DSL_FILES.map { |m| File.mtime(m) rescue Time.at(0) }.flatten.max
+ @@auth_dsl_last_modified ||= mod_time
+ if mod_time > @@auth_dsl_last_modified
+ @@auth_dsl_last_modified = mod_time
+ return true
+ end
+ end
+ end
+
+ # Returns an instance of Engine, which is created if there isn't one
+ # yet. If +dsl_file+ is given, it is passed on to Engine.new and
+ # a new instance is always created.
+ def self.instance (dsl_file = nil)
+ if dsl_file or development_reload?
+ @@instance = new(dsl_file)
+ else
+ @@instance ||= new
+ end
+ end
+
+ class AttributeValidator # :nodoc:
+ attr_reader :user, :object, :engine, :context, :privilege
+ def initialize (engine, user, object = nil, privilege = nil, context = nil)
+ @engine = engine
+ @user = user
+ @object = object
+ @privilege = privilege
+ @context = context
+ end
+
+ def evaluate (value_block)
+ # TODO cache?
+ instance_eval(&value_block)
+ end
+ end
+
+ private
+ def user_roles_privleges_from_options(privilege, options)
+ options = {
+ :user => nil,
+ :context => nil,
+ :user_roles => nil
+ }.merge(options)
+ user = options[:user] || Authorization.current_user
+ privileges = privilege.is_a?(Array) ? privilege : [privilege]
+
+ raise AuthorizationUsageError, "No user object given (#{user.inspect}) or " +
+ "set through Authorization.current_user" unless user
+
+ roles = options[:user_roles] || flatten_roles(roles_for(user))
+ privileges = flatten_privileges privileges, options[:context]
+ [user, roles, privileges]
+ end
+
+=begin
+ def flatten_roles (roles, flattened_roles = Set.new)
+ # TODO caching?
+ roles.reject {|role| flattened_roles.include?(role)}.each do |role|
+ flattened_roles << role
+ flatten_roles(role_hierarchy[role], flattened_roles) if role_hierarchy[role]
+ end
+ flattened_roles.to_a
+ end
+=end
+ def flatten_roles (roles)
+ # TODO caching?
+ result = []
+ roles.each do |role|
+ result += flatten_role role
+ end
+ result.flatten.uniq
+ end
+
+ public
+ def flatten_role (role)
+ @flatten_role_cache ||= {}
+ return @flatten_role_cache[role] if @flatten_role_cache[role]
+ result = [role]
+ role_hierarchy[role].each do |r|
+ result += flatten_role r
+ end if role_hierarchy[role]
+ @flatten_role_cache[role] = result.flatten.uniq
+ end
+
+ private
+ # Returns the privilege hierarchy flattened for given privileges in context.
+ def flatten_privileges (privileges, context = nil, flattened_privileges = Set.new)
+ # TODO caching?
+ raise AuthorizationUsageError, "No context given or inferable from object" unless context
+ privileges.reject {|priv| flattened_privileges.include?(priv)}.each do |priv|
+ flattened_privileges << priv
+ flatten_privileges(rev_priv_hierarchy[[priv, nil]], context, flattened_privileges) if rev_priv_hierarchy[[priv, nil]]
+ flatten_privileges(rev_priv_hierarchy[[priv, context]], context, flattened_privileges) if rev_priv_hierarchy[[priv, context]]
+ end
+ flattened_privileges.to_a
+ end
+
+ def matching_auth_rules (roles, privileges, context)
+ auth_rules.matching(roles, privileges, context)
+ end
+ end
+
+
+ class AuthorizationRuleSet
+ include Enumerable
+ extend Forwardable
+ def_delegators :@rules, :each, :length, :[]
+
+ def initialize (rules = [])
+ @rules = rules.clone
+ reset!
+ end
+
+ def initialize_copy (source)
+ @rules = @rules.collect {|rule| rule.clone}
+ reset!
+ end
+
+ def matching(roles, privileges, context)
+ roles = [roles] unless roles.is_a?(Array)
+ rules = cached_auth_rules[context] || []
+ rules.select do |rule|
+ rule.matches? roles, privileges, context
+ end
+ end
+ def delete rule
+ @rules.delete rule
+ reset!
+ end
+ def << rule
+ @rules << rule
+ reset!
+ end
+ def each &block
+ @rules.each &block
+ end
+
+ private
+ def reset!
+ @cached_auth_rules =nil
+ end
+ def cached_auth_rules
+ return @cached_auth_rules if @cached_auth_rules
+ @cached_auth_rules = {}
+ @rules.each do |rule|
+ rule.contexts.each do |context|
+ @cached_auth_rules[context] ||= []
+ @cached_auth_rules[context] << rule
+ end
+ end
+ @cached_auth_rules
+ end
+ end
+ class AuthorizationRule
+ attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
+ :source_file, :source_line
+
+ def initialize (role, privileges = [], contexts = nil, join_operator = :or,
+ options = {})
+ @role = role
+ @privileges = Set.new(privileges)
+ @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
+ @join_operator = join_operator
+ @attributes = []
+ @source_file = options[:source_file]
+ @source_line = options[:source_line]
+ end
+
+ def initialize_copy (from)
+ @privileges = @privileges.clone
+ @contexts = @contexts.clone
+ @attributes = @attributes.collect {|attribute| attribute.clone }
+ end
+
+ def append_privileges (privs)
+ @privileges.merge(privs)
+ end
+
+ def append_attribute (attribute)
+ @attributes << attribute
+ end
+
+ def matches? (roles, privs, context = nil)
+ roles = [roles] unless roles.is_a?(Array)
+ @contexts.include?(context) and roles.include?(@role) and
+ not (@privileges & privs).empty?
+ end
+
+ def validate? (attr_validator, skip_attribute = false)
+ skip_attribute or @attributes.empty? or
+ @attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
+ begin
+ attr.validate?(attr_validator)
+ rescue NilAttributeValueError => e
+ nil # Bumping up against a nil attribute value flunks the rule.
+ end
+ end
+ end
+
+ def obligations (attr_validator)
+ exceptions = []
+ obligations = @attributes.collect do |attr|
+ begin
+ attr.obligation(attr_validator)
+ rescue NotAuthorized => e
+ exceptions << e
+ nil
+ end
+ end
+
+ if exceptions.length > 0 and (@join_operator == :and or exceptions.length == @attributes.length)
+ raise NotAuthorized, "Missing authorization in collecting obligations: #{exceptions.map(&:to_s) * ", "}"
+ end
+
+ if @join_operator == :and and !obligations.empty?
+ # cross product of OR'ed obligations in arrays
+ arrayed_obligations = obligations.map {|obligation| obligation.is_a?(Hash) ? [obligation] : obligation}
+ merged_obligations = arrayed_obligations.first
+ arrayed_obligations[1..-1].each do |inner_obligations|
+ previous_merged_obligations = merged_obligations
+ merged_obligations = inner_obligations.collect do |inner_obligation|
+ previous_merged_obligations.collect do |merged_obligation|
+ merged_obligation.deep_merge(inner_obligation)
+ end
+ end.flatten
+ end
+ obligations = merged_obligations
+ else
+ obligations = obligations.flatten.compact
+ end
+ obligations.empty? ? [{}] : obligations
+ end
+
+ def to_long_s
+ attributes.collect {|attr| attr.to_long_s } * "; "
+ end
+ end
+
+ class Attribute
+ # attr_conditions_hash of form
+ # { :object_attribute => [operator, value_block], ... }
+ # { :object_attribute => { :attr => ... } }
+ def initialize (conditions_hash)
+ @conditions_hash = conditions_hash
+ end
+
+ def initialize_copy (from)
+ @conditions_hash = deep_hash_clone(@conditions_hash)
+ end
+
+ def validate? (attr_validator, object = nil, hash = nil)
+ object ||= attr_validator.object
+ return false unless object
+
+ (hash || @conditions_hash).all? do |attr, value|
+ attr_value = object_attribute_value(object, attr)
+ if value.is_a?(Hash)
+ if attr_value.is_a?(Enumerable)
+ attr_value.any? do |inner_value|
+ validate?(attr_validator, inner_value, value)
+ end
+ elsif attr_value == nil
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
+ else
+ validate?(attr_validator, attr_value, value)
+ end
+ elsif value.is_a?(Array) and value.length == 2 and value.first.is_a?(Symbol)
+ evaluated = if value[1].is_a?(Proc)
+ attr_validator.evaluate(value[1])
+ else
+ value[1]
+ end
+ case value[0]
+ when :is
+ attr_value == evaluated
+ when :is_not
+ attr_value != evaluated
+ when :contains
+ begin
+ attr_value.include?(evaluated)
+ rescue NoMethodError => e
+ raise AuthorizationUsageError, "Operator contains requires a " +
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
+ "contains #{evaluated.inspect}: #{e}"
+ end
+ when :does_not_contain
+ begin
+ !attr_value.include?(evaluated)
+ rescue NoMethodError => e
+ raise AuthorizationUsageError, "Operator does_not_contain requires a " +
+ "subclass of Enumerable as attribute value, got: #{attr_value.inspect} " +
+ "does_not_contain #{evaluated.inspect}: #{e}"
+ end
+ when :intersects_with
+ begin
+ evaluated == :unlimited ? true : !(evaluated.to_set & attr_value.to_set).empty?
+ rescue NoMethodError => e
+ raise AuthorizationUsageError, "Operator intersects_with requires " +
+ "subclasses of Enumerable, got: #{attr_value.inspect} " +
+ "intersects_with #{evaluated.inspect}: #{e}"
+ end
+ when :is_in
+ begin
+ evaluated == :unlimited ? true : evaluated.include?(attr_value)
+ rescue NoMethodError => e
+ raise AuthorizationUsageError, "Operator is_in requires a " +
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
+ "is_in #{evaluated.inspect}: #{e}"
+ end
+ when :is_not_in
+ begin
+ !evaluated.include?(attr_value)
+ rescue NoMethodError => e
+ raise AuthorizationUsageError, "Operator is_not_in requires a " +
+ "subclass of Enumerable as value, got: #{attr_value.inspect} " +
+ "is_not_in #{evaluated.inspect}: #{e}"
+ end
+ when :lt
+ attr_value && attr_value < evaluated
+ when :lte
+ attr_value && attr_value <= evaluated
+ when :gt
+ attr_value && attr_value > evaluated
+ when :gte
+ attr_value && attr_value >= evaluated
+ else
+ raise AuthorizationError, "Unknown operator #{value[0]}"
+ end
+ else
+ raise AuthorizationError, "Wrong conditions hash format"
+ end
+ end
+ end
+
+ # resolves all the values in condition_hash
+ def obligation (attr_validator, hash = nil)
+ hash = (hash || @conditions_hash).clone
+ hash.each do |attr, value|
+ if value.is_a?(Hash)
+ hash[attr] = obligation(attr_validator, value)
+ elsif value.is_a?(Array) and value.length == 2
+ hash[attr] = [value[0], attr_validator.evaluate(value[1])]
+ else
+ raise AuthorizationError, "Wrong conditions hash format"
+ end
+ end
+ hash
+ end
+
+ def to_long_s (hash = nil)
+ if hash
+ hash.inject({}) do |memo, key_val|
+ key, val = key_val
+ memo[key] = case val
+ when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
+ when Hash then to_long_s(val)
+ end
+ memo
+ end
+ else
+ "if_attribute #{to_long_s(@conditions_hash).inspect}"
+ end
+ end
+
+ protected
+ def object_attribute_value (object, attr)
+ begin
+ object.send(attr)
+ rescue ArgumentError, NoMethodError => e
+ raise AuthorizationUsageError, "Error occurred while validating attribute ##{attr} on #{object.inspect}: #{e}.\n" +
+ "Please check your authorization rules and ensure the attribute is correctly spelled and \n" +
+ "corresponds to a method on the model you are authorizing for."
+ end
+ end
+
+ def deep_hash_clone (hash)
+ hash.inject({}) do |memo, (key, val)|
+ memo[key] = case val
+ when Hash
+ deep_hash_clone(val)
+ when NilClass, Symbol
+ val
+ else
+ val.clone
+ end
+ memo
+ end
+ end
+ end
+
+ # An attribute condition that uses existing rules to decide validation
+ # and create obligations.
+ class AttributeWithPermission < Attribute
+ # E.g. privilege :read, attr_or_hash either :attribute or
+ # { :attribute => :deeper_attribute }
+ def initialize (privilege, attr_or_hash, context = nil)
+ @privilege = privilege
+ @context = context
+ @attr_hash = attr_or_hash
+ end
+
+ def initialize_copy (from)
+ @attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
+ end
+
+ def validate? (attr_validator, object = nil, hash_or_attr = nil)
+ object ||= attr_validator.object
+ hash_or_attr ||= @attr_hash
+ return false unless object
+
+ case hash_or_attr
+ when Symbol
+ attr_value = object_attribute_value(object, hash_or_attr)
+ case attr_value
+ when nil
+ raise NilAttributeValueError, "Attribute #{hash_or_attr.inspect} is nil in #{object.inspect}."
+ when Enumerable
+ attr_value.any? do |inner_value|
+ attr_validator.engine.permit? @privilege, :object => inner_value, :user => attr_validator.user
+ end
+ else
+ attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
+ end
+ when Hash
+ hash_or_attr.all? do |attr, sub_hash|
+ attr_value = object_attribute_value(object, attr)
+ if attr_value == nil
+ raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
+ elsif attr_value.is_a?(Enumerable)
+ attr_value.any? do |inner_value|
+ validate?(attr_validator, inner_value, sub_hash)
+ end
+ else
+ validate?(attr_validator, attr_value, sub_hash)
+ end
+ end
+ when NilClass
+ attr_validator.engine.permit? @privilege, :object => object, :user => attr_validator.user
+ else
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
+ end
+ end
+
+ # may return an array of obligations to be OR'ed
+ def obligation (attr_validator, hash_or_attr = nil, path = [])
+ hash_or_attr ||= @attr_hash
+ case hash_or_attr
+ when Symbol
+ @context ||= begin
+ rule_model = attr_validator.context.to_s.classify.constantize
+ context_reflection = self.class.reflection_for_path(rule_model, path + [hash_or_attr])
+ if context_reflection.klass.respond_to?(:decl_auth_context)
+ context_reflection.klass.decl_auth_context
+ else
+ context_reflection.klass.name.tableize.to_sym
+ end
+ rescue # missing model, reflections
+ hash_or_attr.to_s.pluralize.to_sym
+ end
+
+ obligations = attr_validator.engine.obligations(@privilege,
+ :context => @context,
+ :user => attr_validator.user)
+
+ obligations.collect {|obl| {hash_or_attr => obl} }
+ when Hash
+ obligations_array_attrs = []
+ obligations =
+ hash_or_attr.inject({}) do |all, pair|
+ attr, sub_hash = pair
+ all[attr] = obligation(attr_validator, sub_hash, path + [attr])
+ if all[attr].length > 1
+ obligations_array_attrs << attr
+ else
+ all[attr] = all[attr].first
+ end
+ all
+ end
+ obligations = [obligations]
+ obligations_array_attrs.each do |attr|
+ next_array_size = obligations.first[attr].length
+ obligations = obligations.collect do |obls|
+ (0...next_array_size).collect do |idx|
+ obls_wo_array = obls.clone
+ obls_wo_array[attr] = obls_wo_array[attr][idx]
+ obls_wo_array
+ end
+ end.flatten
+ end
+ obligations
+ when NilClass
+ attr_validator.engine.obligations(@privilege,
+ :context => attr_validator.context,
+ :user => attr_validator.user)
+ else
+ raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
+ end
+ end
+
+ def to_long_s
+ "if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
+ end
+
+ private
+ def self.reflection_for_path (parent_model, path)
+ reflection = path.empty? ? parent_model : begin
+ parent = reflection_for_path(parent_model, path[0..-2])
+ if !parent.respond_to?(:proxy_reflection) and parent.respond_to?(:klass)
+ parent.klass.reflect_on_association(path.last)
+ else
+ parent.reflect_on_association(path.last)
+ end
+ rescue
+ parent.reflect_on_association(path.last)
+ end
+ raise "invalid path #{path.inspect}" if reflection.nil?
+ reflection
+ end
+ end
+
+ # Represents a pseudo-user to facilitate anonymous users in applications
+ class AnonymousUser
+ attr_reader :role_symbols
+ def initialize (roles = [Authorization.default_role])
+ @role_symbols = roles
+ end
+ end
+end
+
View
1,116 lib/declarative_authorization/reader.rb
@@ -1,546 +1,570 @@
-# Authorization::Reader
-
-require File.dirname(__FILE__) + '/authorization.rb'
-
-module Authorization
- # Parses an authorization configuration file in the authorization DSL and
- # constructs a data model of its contents.
- #
- # For examples and the modeled data model, see the
- # README[link:files/README_rdoc.html].
- #
- # Also, see role definition methods
- # * AuthorizationRulesReader#role,
- # * AuthorizationRulesReader#includes,
- # * AuthorizationRulesReader#title,
- # * AuthorizationRulesReader#description
- #
- # Methods for rule definition in roles
- # * AuthorizationRulesReader#has_permission_on,
- # * AuthorizationRulesReader#to,
- # * AuthorizationRulesReader#if_attribute,
- # * AuthorizationRulesReader#if_permitted_to
- #
- # Methods to be used in if_attribute statements
- # * AuthorizationRulesReader#contains,
- # * AuthorizationRulesReader#does_not_contain,
- # * AuthorizationRulesReader#intersects_with,
- # * AuthorizationRulesReader#is,
- # * AuthorizationRulesReader#is_not,
- # * AuthorizationRulesReader#is_in,
- # * AuthorizationRulesReader#is_not_in,
- # * AuthorizationRulesReader#lt,
- # * AuthorizationRulesReader#lte,
- # * AuthorizationRulesReader#gt,
- # * AuthorizationRulesReader#gte
- #
- # And privilege definition methods
- # * PrivilegesReader#privilege,
- # * PrivilegesReader#includes
- #
- module Reader
- # Signals that the specified file to load was not found.
- class DSLFileNotFoundError < Exception; end
- # Signals errors that occur while reading and parsing an authorization DSL
- class DSLError < Exception; end
- # Signals errors in the syntax of an authorization DSL.
- class DSLSyntaxError < DSLError; end
-
- # Top-level reader, parses the methods +privileges+ and +authorization+.
- # +authorization+ takes a block with authorization rules as described in
- # AuthorizationRulesReader. The block to +privileges+ defines privilege
- # hierarchies, as described in PrivilegesReader.
- #
- class DSLReader
- attr_reader :privileges_reader, :auth_rules_reader # :nodoc:
-
- def initialize ()
- @privileges_reader = PrivilegesReader.new
- @auth_rules_reader = AuthorizationRulesReader.new
- end
-
- def initialize_copy (from) # :nodoc:
- @privileges_reader = from.privileges_reader.clone
- @auth_rules_reader = from.auth_rules_reader.clone
- end
-
- # ensures you get back a DSLReader
- # if you provide a:
- # DSLReader - you will get it back.
- # String or Array - it will treat it as if you have passed a path or an array of paths and attempt to load those.
- def self.factory(obj)
- case obj
- when Reader::DSLReader
- obj
- when String, Array
- load(obj)
- end
- end
-
- # Parses a authorization DSL specification from the string given
- # in +dsl_data+. Raises DSLSyntaxError if errors occur on parsing.
- def parse (dsl_data, file_name = nil)
- if file_name
- DSLMethods.new(self).instance_eval(dsl_data, file_name)
- else
- DSLMethods.new(self).instance_eval(dsl_data)
- end
- rescue SyntaxError, NoMethodError, NameError => e
- raise DSLSyntaxError, "Illegal DSL syntax: #{e}"
- end
-
- # Load and parse a DSL from the given file name.
- def load (dsl_file)
- parse(File.read(dsl_file), dsl_file) if File.exist?(dsl_file)
- end
-
- # Load and parse a DSL from the given file name. Raises Authorization::Reader::DSLFileNotFoundError
- # if the file cannot be found.
- def load! (dsl_file)
- raise ::Authorization::Reader::DSLFileNotFoundError, "Error reading authorization rules file with path '#{dsl_file}'! Please ensure it exists and that it is accessible." unless File.exist?(dsl_file)
- load(dsl_file)
- end
-
- # Loads and parses DSL files and returns a new reader
- def self.load (dsl_files)
- # TODO cache reader in production mode?
- reader = new
- dsl_files = [dsl_files].flatten
- dsl_files.each do |file|
- reader.load(file)
- end
- reader
- end
-
- # DSL methods
- class DSLMethods # :nodoc:
- def initialize (parent)
- @parent = parent
- end
-
- def privileges (&block)
- @parent.privileges_reader.instance_eval(&block)
- end
-
- def contexts (&block)
- # Not implemented
- end
-
- def authorization (&block)
- @parent.auth_rules_reader.instance_eval(&block)
- end
- end
- end
-
- # The PrivilegeReader handles the part of the authorization DSL in
- # a +privileges+ block. Here, privilege hierarchies are defined.
- class PrivilegesReader
- # TODO handle privileges with separated context
- attr_reader :privileges, :privilege_hierarchy # :nodoc:
-
- def initialize # :nodoc:
- @current_priv = nil
- @current_context = nil
- @privileges = []
- # {priv => [[priv,ctx], ...]}
- @privilege_hierarchy = {}
- end
-
- def initialize_copy (from) # :nodoc:
- @privileges = from.privileges.clone
- @privilege_hierarchy = from.privilege_hierarchy.clone
- end
-
- def append_privilege (priv) # :nodoc:
- @privileges << priv unless @privileges.include?(priv)
- end
-
- # Defines part of a privilege hierarchy. For the given +privilege+,
- # included privileges may be defined in the block (through includes)
- # or as option :+includes+. If the optional context is given,
- # the privilege hierarchy is limited to that context.
- #
- def privilege (privilege, context = nil, options = {}, &block)
- if context.is_a?(Hash)
- options = context
- context = nil
- end
- @current_priv = privilege
- @current_context = context
- append_privilege privilege
- instance_eval(&block) if block
- includes(*options[:includes]) if options[:includes]
- ensure
- @current_priv = nil
- @current_context = nil
- end
-
- # Specifies +privileges+ that are to be assigned as lower ones. Only to
- # be used inside a privilege block.
- def includes (*privileges)
- raise DSLError, "includes only in privilege block" if @current_priv.nil?
- privileges.each do |priv|
- append_privilege priv
- @privilege_hierarchy[@current_priv] ||= []
- @privilege_hierarchy[@current_priv] << [priv, @current_context]
- end
- end
- end
-
- class AuthorizationRulesReader
- attr_reader :roles, :role_hierarchy, :auth_rules,
- :role_descriptions, :role_titles, :omnipotent_roles # :nodoc:
-
- def initialize # :nodoc:
- @current_role = nil
- @current_rule = nil
- @roles = []
- @omnipotent_roles = []
- # higher_role => [lower_roles]
- @role_hierarchy = {}
- @role_titles = {}
- @role_descriptions = {}
- @auth_rules = AuthorizationRuleSet.new
- end
-
- def initialize_copy (from) # :nodoc:
- [:roles, :role_hierarchy, :auth_rules,
- :role_descriptions, :role_titles, :omnipotent_roles].each do |attribute|
- instance_variable_set(:"@#{attribute}", from.send(attribute).clone)
- end
- end
-
- def append_role (role, options = {}) # :nodoc:
- @roles << role unless @roles.include? role
- @role_titles[role] = options[:title] if options[:title]
- @role_descriptions[role] = options[:description] if options[:description]
- end
-
- # Defines the authorization rules for the given +role+ in the
- # following block.
- # role :admin do
- # has_permissions_on ...
- # end
- #
- def role (role, options = {}, &block)
- append_role role, options
- @current_role = role
- yield
- ensure
- @current_role = nil
- end
-
- # Roles may inherit all the rights from subroles. The given +roles+
- # become subroles of the current block's role.
- # role :admin do
- # includes :user
- # has_permission_on :employees, :to => [:update, :create]
- # end
- # role :user do
- # has_permission_on :employees, :to => :read
- # end
- #
- def includes (*roles)
- raise DSLError, "includes only in role blocks" if @current_role.nil?
- @role_hierarchy[@current_role] ||= []
- @role_hierarchy[@current_role] += roles.flatten
- end
-
- # Allows the definition of privileges to be allowed for the current role,
- # either in a has_permission_on block or directly in one call.
- # role :admin
- # has_permission_on :employees, :to => :read
- # has_permission_on [:employees, :orders], :to => :read
- # has_permission_on :employees do
- # to :create
- # if_attribute ...
- # end
- # has_permission_on :employees, :to => :delete do
- # if_attribute ...
- # end
- # end
- # The block form allows to describe restrictions on the permissions
- # using if_attribute. Multiple has_permission_on statements are
- # OR'ed when evaluating the permissions. Also, multiple if_attribute
- # statements in one block are OR'ed if no :+join_by+ option is given
- # (see below). To AND conditions, either set :+join_by+ to :and or place
- # them in one if_attribute statement.
- #
- # Available options
- # [:+to+]
- # A symbol or an array of symbols representing the privileges that
- # should be granted in this statement.
- # [:+join_by+]
- # Join operator to logically connect the constraint statements inside
- # of the has_permission_on block. May be :+and+ or :+or+. Defaults to :+or+.
- #
- def has_permission_on (*args, &block)
- options = args.extract_options!
- context = args.flatten
-
- raise DSLError, "has_permission_on only allowed in role blocks" if @current_role.nil?
- options = {:to => [], :join_by => :or}.merge(options)
-
- privs = options[:to]
- privs = [privs] unless privs.is_a?(Array)
- raise DSLError, "has_permission_on either needs a block or :to option" if !block_given? and privs.empty?
-
- file, line = file_and_line_number_from_call_stack
- rule = AuthorizationRule.new(@current_role, privs, context, options[:join_by],
- :source_file => file, :source_line => line)
- @auth_rules << rule
- if block_given?
- @current_rule = rule
- yield
- raise DSLError, "has_permission_on block content specifies no privileges" if rule.privileges.empty?
- # TODO ensure?
- @current_rule = nil
- end
- end
-
- # Removes any permission checks for the current role.
- # role :admin
- # has_omnipotence
- # end
- def has_omnipotence
- raise DSLError, "has_omnipotence only allowed in role blocks" if @current_role.nil?
- @omnipotent_roles << @current_role
- end
-
- # Sets a description for the current role. E.g.
- # role :admin
- # description "To be assigned to administrative personnel"
- # has_permission_on ...
- # end
- def description (text)
- raise DSLError, "description only allowed in role blocks" if @current_role.nil?
- role_descriptions[@current_role] = text
- end
-
- # Sets a human-readable title for the current role. E.g.
- # role :admin
- # title "Administrator"
- # has_permission_on ...
- # end
- def title (text)
- raise DSLError, "title only allowed in role blocks" if @current_role.nil?
- role_titles[@current_role] = text
- end
-
- # Used in a has_permission_on block, to may be used to specify privileges
- # to be assigned to the current role under the conditions specified in
- # the current block.
- # role :admin
- # has_permission_on :employees do
- # to :create, :read, :update, :delete
- # end
- # end
- def to (*privs)
- raise DSLError, "to only allowed in has_permission_on blocks" if @current_rule.nil?
- @current_rule.append_privileges(privs.flatten)
- end
-
- # In a has_permission_on block, if_attribute specifies conditions
- # of dynamic parameters that have to be met for the user to meet the
- # privileges in this block. Conditions are evaluated on the context
- # object. Thus, the following allows CRUD for branch admins only on
- # employees that belong to the same branch as the current user.
- # role :branch_admin
- # has_permission_on :employees do
- # to :create, :read, :update, :delete
- # if_attribute :branch => is { user.branch }
- # end
- # end
- # In this case, is is the operator for evaluating the condition. Another
- # operator is contains for collections. In the block supplied to the
- # operator, +user+ specifies the current user for whom the condition
- # is evaluated.
- #
- # Conditions may be nested:
- # role :company_admin
- # has_permission_on :employees do
- # to :create, :read, :update, :delete
- # if_attribute :branch => { :company => is {user.branch.company} }
- # end
- # end
- #
- # has_many and has_many through associations may also be nested.
- # Then, at least one item in the association needs to fulfill the
- # subsequent condition:
- # if_attribute :company => { :branches => { :manager => { :last_name => is { user.last_name } } }
- # Beware of possible performance issues when using has_many associations in
- # permitted_to? checks. For
- # permitted_to? :read, object
- # a check like
- # object.company.branches.any? { |branch| branch.manager ... }
- # will be executed. with_permission_to scopes construct efficient SQL
- # joins, though.
- #
- # Multiple attributes in one :if_attribute statement are AND'ed.
- # Multiple if_attribute statements are OR'ed if the join operator for the
- # has_permission_on block isn't explicitly set. Thus, the following would
- # require the current user either to be of the same branch AND the employee
- # to be "changeable_by_coworker". OR the current user has to be the
- # employee in question.
- # has_permission_on :employees, :to => :manage do
- # if_attribute :branch => is {user.branch}, :changeable_by_coworker => true
- # if_attribute :id => is {user.id}
- # end
- # The join operator for if_attribute rules can explicitly set to AND, though.
- # See has_permission_on for details.
- #
- # Arrays and fixed values may be used directly as hash values:
- # if_attribute :id => 1
- # if_attribute :type => "special"
- # if_attribute :id => [1,2]
- #
- def if_attribute (attr_conditions_hash)
- raise DSLError, "if_attribute only in has_permission blocks" if @current_rule.nil?
- parse_attribute_conditions_hash!(attr_conditions_hash)
- @current_rule.append_attribute Attribute.new(attr_conditions_hash)
- end
-
- # if_permitted_to allows the has_permission_on block to depend on
- # permissions on associated objects. By using it, the authorization
- # rules may be a lot DRYer. E.g.:
- #
- # role :branch_manager
- # has_permission_on :branches, :to => :manage do
- # if_attribute :employees => contains { user }
- # end
- # has_permission_on :employees, :to => :read do
- # if_permitted_to :read, :branch
- # # instead of
- # # if_attribute :branch => { :employees => contains { user } }
- # end
- # end
- #
- # if_permitted_to associations may be nested as well:
- # if_permitted_to :read, :branch => :company
- #
- # You can even use has_many associations as target. Then, it is checked
- # if the current user has the required privileg