class ViewModel::AccessControl::Composed
# edit_valid_unless!(“user is on fire”) { … }
# edit_valid_if!(“logged in as specified user”) { … }
# if at least one ‘if` check and no `unless` checks succeed. For example:
# for each access check (visible, editable, edit_valid). An action is permitted
# Provides access control as a combination of `x_if!` and `x_unless!` checks
def check_delegates(env, ifs, unlesses)
def check_delegates(env, ifs, unlesses) veto, veto_error = detect_veto(env, unlesses) allow, allow_error = detect_allow(env, ifs) ComposedResult.new(allow, veto, allow_error, veto_error) end
def detect_allow(env, checkers)
def detect_allow(env, checkers) error = nil checkers.each do |checker| result = checker.check(env) next unless result if result.is_a?(StandardError) error ||= result else # short-circuit exit with success return true, nil end end error ||= NoRequiredConditionsError.new( env.view.blame_reference, checkers.map(&:name)) return false, error end
def detect_veto(env, checkers)
def detect_veto(env, checkers) checkers.each do |checker| result = checker.check(env) next unless result error = if result.is_a?(StandardError) result else checker.error_type.new('Action not permitted because: ' + checker.reason, env.view.blame_reference) end # short-circuit exit with failure return true, error end return false, nil end
def each_check(check_name, include_ancestor = nil)
def each_check(check_name, include_ancestor = nil) return enum_for(:each_check, check_name, include_ancestor) unless block_given? self.public_send(check_name).each { |x| yield x } visited = Set.new @included_checkers.each do |ancestor| next unless visited.add?(ancestor) next if include_ancestor && !include_ancestor.call(ancestor) ancestor.each_check(check_name, include_ancestor) { |x| yield x } end end
def edit_valid_if!(reason, &block)
def edit_valid_if!(reason, &block) @edit_valid_ifs << new_permission_check(reason, &block) end
def edit_valid_unless!(reason, &block)
def edit_valid_unless!(reason, &block) @edit_valid_unlesses << new_permission_check(reason, &block) end
def editable_check(traversal_env)
def editable_check(traversal_env) check_delegates(traversal_env, self.class.each_check(:editable_ifs), self.class.each_check(:editable_unlesses)) end
def editable_if!(reason, &block)
def editable_if!(reason, &block) @editable_ifs << new_permission_check(reason, &block) end
def editable_unless!(reason, &block)
def editable_unless!(reason, &block) @editable_unlesses << new_permission_check(reason, &block) end
def include_from(ancestor)
def include_from(ancestor) unless ancestor < ViewModel::AccessControl::Composed raise ArgumentError.new("Invalid ancestor: #{ancestor}") end @included_checkers << ancestor end
def inherited(subclass)
def inherited(subclass) super subclass.initialize_as_composed_access_control end
def initialize_as_composed_access_control
def initialize_as_composed_access_control @included_checkers = [] @edit_valid_ifs = [] @edit_valid_unlesses = [] @editable_ifs = [] @editable_unlesses = [] @visible_ifs = [] @visible_unlesses = [] end
def inspect
def inspect checks = inspect_checks checks << "includes checkers: #{@included_checkers.inspect}" if @included_checkers.present? super + '(' + checks.join(', ') + ')' end
def inspect_checks
def inspect_checks checks = [] checks << "visible_if: #{@visible_ifs.map(&:reason)}" if @visible_ifs.present? checks << "visible_unless: #{@visible_unlesses.map(&:reason)}" if @visible_unlesses.present? checks << "editable_if: #{@editable_ifs.map(&:reason)}" if @editable_ifs.present? checks << "editable_unless: #{@editable_unlesses.map(&:reason)}" if @editable_unlesses.present? checks << "edit_valid_if: #{@edit_valid_ifs.map(&:reason)}" if @edit_valid_ifs.present? checks << "edit_valid_unless: #{@edit_valid_unlesses.map(&:reason)}" if @edit_valid_unlesses.present? checks end
def new_permission_check(reason, error_type: ViewModel::AccessControlError, &block)
def new_permission_check(reason, error_type: ViewModel::AccessControlError, &block) PermissionsCheck.new(self.name&.demodulize, reason, error_type, block) end
def valid_edit_check(traversal_env)
def valid_edit_check(traversal_env) check_delegates(traversal_env, self.class.each_check(:edit_valid_ifs), self.class.each_check(:edit_valid_unlesses)) end
def visible_check(traversal_env)
def visible_check(traversal_env) check_delegates(traversal_env, self.class.each_check(:visible_ifs), self.class.each_check(:visible_unlesses)) end
def visible_if!(reason, &block)
def visible_if!(reason, &block) @visible_ifs << new_permission_check(reason, &block) end
def visible_unless!(reason, &block)
def visible_unless!(reason, &block) @visible_unlesses << new_permission_check(reason, &block) end