# frozen_string_literal: true module Rails module Auth class ACL # Rules for a particular route class Resource attr_reader :http_methods, :path, :host, :matchers # Valid HTTP methods HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK).freeze # Options allowed for resource matchers VALID_OPTIONS = %w(method path host).freeze # @option :options [String] :method HTTP method allowed ("ALL" for all methods) # @option :options [String] :path path to the resource (regex syntax allowed) # @param [Hash] :matchers which matchers are used for this resource # def initialize(options, matchers) raise TypeError, "expected Hash for options" unless options.is_a?(Hash) raise TypeError, "expected Hash for matchers" unless matchers.is_a?(Hash) unless (extra_keys = options.keys - VALID_OPTIONS).empty? raise ParseError, "unrecognized key in ACL resource: #{extra_keys.first}" end methods = options["method"] || raise(ParseError, "no 'method' key in resource: #{options.inspect}") path = options["path"] || raise(ParseError, "no 'path' key in resource: #{options.inspect}") @http_methods = extract_methods(methods) @path = /\A#{path}\z/ @matchers = matchers.freeze # Unlike method and path, host is optional host = options["host"] @host = /\A#{host}\z/ if host end # Match this resource against the given Rack environment, checking all # matchers to ensure at least one of them matches # # @param [Hash] :env Rack environment # # @return [String, nil] name of the matcher which matched, or nil if none matched # def match(env) return nil unless match!(env) name, = @matchers.find { |_name, matcher| matcher.match(env) } name end # Match *only* the request method/path/host against the given Rack environment. # matchers are NOT checked. # # @param [Hash] :env Rack environment # # @return [Boolean] method and path *only* match the given environment # def match!(env) return false unless @http_methods.include?(env["REQUEST_METHOD".freeze]) return false unless @path =~ env["PATH_INFO".freeze] return false unless @host.nil? || @host =~ env["HTTP_HOST".freeze] true end private def extract_methods(methods) methods = Array(methods) return HTTP_METHODS if methods == ["ALL"] raise ParseError, "method 'ALL' cannot be used with other methods" if methods.include?("ALL") methods.each do |method| raise ParseError, "invalid HTTP method: #{method}" unless HTTP_METHODS.include?(method) end methods.freeze end end end end end