api_valve 0.0.1.alpha → 0.0.1.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6b393476b3a17c2f66a154ed51be1d1ffe1bd8425ba4bcd3758336da16a169a
4
- data.tar.gz: b320775933274fd57cd9fc1792f0f0c9b881d27b3ceb87df16328adadcf1d6cf
3
+ metadata.gz: 0db44e3a1e246e94e9c31ad856fef01d7b76626cfe665bd18dfa769223c385bc
4
+ data.tar.gz: 7a05b9b44d9606bb831da1709bacc50135115311d4517f87bdad59e17ea3340d
5
5
  SHA512:
6
- metadata.gz: 2c687fb655c0eed88ff7e197c7ea02346e92a113fc1c01940195b3df15d962f2c3224ebe6de122cb42db9c07d911f055510604013f5547d8a0637f5113407cdf
7
- data.tar.gz: 18409ae39eabc1d45aaf34e7cd8e8328d339a0c1123c823408dedc414096eaa0c4dc4aa3272706672b107308bef08e1f52a37d3d707f3fc9ac1cc1f0d6bdb038
6
+ metadata.gz: d86f709762d7cd80fe4d1d01184310ae422e38dac84c9244a968ce52cae0ede75365975d8bd86b3fc9bdc5e9dd16d34ac1e94bf3f6129d97d662cabb13ac72d8
7
+ data.tar.gz: a733954d49421ad1735f12c27b3361026c8a3f4d5378837090d5373dcfb75e8409adca6566b6d437cfb64d0512a8531b8de551a14cd65ed1c60771df4514e039
@@ -15,7 +15,9 @@ module ApiValve
15
15
  private
16
16
 
17
17
  def status
18
- Rack::Utils::SYMBOL_TO_STATUS_CODE[@error.http_status] || 500
18
+ status = @error.try(:http_status)
19
+ return status if status&.is_a?(Integer)
20
+ Rack::Utils::SYMBOL_TO_STATUS_CODE[status] || 500
19
21
  end
20
22
 
21
23
  def json_error
@@ -0,0 +1,40 @@
1
+ module ApiValve
2
+ # This class is responsible to decide if a request is allowed or not, and can
3
+ # be extended with more ACL related features, for example returning a list of
4
+ # attributes that can be read or written.
5
+
6
+ class Forwarder::PermissionHandler
7
+ module RequestIntegration
8
+ private
9
+
10
+ def permission_handler
11
+ permission_handler_klass.instance(@original_request, permission_handler_options)
12
+ end
13
+
14
+ def permission_handler_klass
15
+ permission_handler_options[:klass] || Forwarder::PermissionHandler
16
+ end
17
+
18
+ def permission_handler_options
19
+ @options[:permission_handler] || {}
20
+ end
21
+ end
22
+
23
+ # Returns an instance of the PermissionHandler, cached in the request env
24
+ # This allows re-use of the PermissionHandler by both Request and Response instances
25
+ def self.instance(request, options)
26
+ request.env['permission_handler'] ||= new(request, options)
27
+ end
28
+
29
+ def initialize(request, options = {})
30
+ @request = request
31
+ @options = options
32
+ end
33
+
34
+ # Tells the request class if the request is allowed
35
+ # Simple implementation is always true. Override in your implementation.
36
+ def request_allowed?
37
+ true
38
+ end
39
+ end
40
+ end
@@ -1,7 +1,12 @@
1
1
  module ApiValve
2
- # Wraps original request
3
- # Responsible for altering the request before it is forwarded
2
+ # This class is wraps the original request. It's methods are called by
3
+ # the Forwarder to make the actual request in the target endpoint.
4
+ # So by changing the public methods in this call, we can control how the
5
+ # request is forwarded
6
+
4
7
  class Forwarder::Request
8
+ include Forwarder::PermissionHandler::RequestIntegration
9
+
5
10
  attr_reader :original_request, :options
6
11
 
7
12
  WHITELISTED_HEADERS = %w(
@@ -21,13 +26,21 @@ module ApiValve
21
26
 
22
27
  def initialize(original_request, options = {})
23
28
  @original_request = original_request
24
- @options = default_options.merge options
29
+ @options = options.with_indifferent_access
30
+ end
31
+
32
+ # Is the request allowed? If it returns false, Forwarder will raise Error::Forbidden
33
+ def allowed?
34
+ permission_handler.request_allowed?
25
35
  end
26
36
 
37
+ # HTTP method to use when forwarding. Must return sym.
38
+ # Returns original request method
27
39
  def method
28
40
  @method ||= original_request.request_method.downcase.to_sym
29
41
  end
30
42
 
43
+ # URL path to use when forwarding
31
44
  def path
32
45
  path = options['endpoint'] || ''
33
46
  if pattern = options['path']
@@ -35,26 +48,27 @@ module ApiValve
35
48
  else
36
49
  path += original_request.path_info
37
50
  end
51
+ # we remove leading slash so we can use endpoints with deeper folder levels
38
52
  path.gsub(%r{^/}, '')
39
53
  end
40
54
 
55
+ # Returns a hash of headers to forward to the target endpoint
56
+ # Override to control the HTTP headers that will be passed through
41
57
  def headers
42
58
  whitelisted_headers.each_with_object({}) do |key, h|
43
59
  h[key] = header(key)
44
60
  end.merge('X-Request-Id' => Thread.current[:request_id]).compact
45
61
  end
46
62
 
47
- def header(name)
48
- name = "HTTP_#{name}" unless NOT_PREFIXED_HEADERS.include? name
49
- name = name.upcase.tr('-', '_')
50
- original_request.get_header(name)
51
- end
52
-
63
+ # Returns body to forward to the target endpoint
64
+ # Override to control the payload that is passed through
53
65
  def body
54
66
  return unless %i(put post patch).include? method
55
67
  original_request.body.read
56
68
  end
57
69
 
70
+ # Returns query params to forward to the target endpoint
71
+ # Override to control the query parameters that can be passed through
58
72
  def url_params
59
73
  return unless original_request.query_string.present?
60
74
  @url_params ||= Rack::Utils.parse_nested_query(original_request.query_string)
@@ -62,14 +76,14 @@ module ApiValve
62
76
 
63
77
  private
64
78
 
65
- def whitelisted_headers
66
- @options[:whitelisted_headers]
79
+ def header(name)
80
+ name = "HTTP_#{name}" unless NOT_PREFIXED_HEADERS.include? name
81
+ name = name.upcase.tr('-', '_')
82
+ original_request.get_header(name)
67
83
  end
68
84
 
69
- def default_options
70
- {
71
- whitelisted_headers: WHITELISTED_HEADERS
72
- }
85
+ def whitelisted_headers
86
+ @options[:whitelisted_headers] || WHITELISTED_HEADERS
73
87
  end
74
88
  end
75
89
  end
@@ -1,18 +1,31 @@
1
1
  module ApiValve
2
2
  # Wraps faraday response
3
3
  # Responsible for altering the response before it is returned
4
+
5
+ # This class is wraps the original response. The rack_response method
6
+ # is called by the Forwarder to build the rack response that will be
7
+ # returned by the proxy.
8
+ # By changing this class, we can control how the request is published
9
+ # to the original caller
10
+
4
11
  class Forwarder::Response
5
- attr_reader :original_response
12
+ include Forwarder::PermissionHandler::RequestIntegration
13
+
14
+ attr_reader :original_request, :original_response
6
15
 
7
16
  WHITELISTED_HEADERS = %w(
8
17
  Content-Type
9
18
  Cache-Control
19
+ Location
10
20
  ).freeze
11
21
 
12
- def initialize(original_response)
22
+ def initialize(original_request, original_response, options = {})
23
+ @original_request = original_request
13
24
  @original_response = original_response
25
+ @options = options.with_indifferent_access
14
26
  end
15
27
 
28
+ # Must return a rack compatible response array of status code, headers and body
16
29
  def rack_response
17
30
  [status, headers, [body]]
18
31
  end
@@ -24,11 +37,15 @@ module ApiValve
24
37
  end
25
38
 
26
39
  def headers
27
- WHITELISTED_HEADERS.each_with_object({}) do |k, h|
40
+ whitelisted_headers.each_with_object({}) do |k, h|
28
41
  h[k] = original_response.headers[k]
29
42
  end.compact
30
43
  end
31
44
 
45
+ def whitelisted_headers
46
+ @options[:whitelisted_headers] || WHITELISTED_HEADERS
47
+ end
48
+
32
49
  def body
33
50
  original_response.body.to_s
34
51
  end
@@ -1,35 +1,50 @@
1
1
  module ApiValve
2
+ # This class is responsible for forwarding the HTTP request to the
3
+ # designated endpoint. It is instanciated once per Proxy with relevant
4
+ # options, and called from the router.
5
+
2
6
  class Forwarder
3
- autoload :Request, 'api_valve/forwarder/request'
4
- autoload :Response, 'api_valve/forwarder/response'
7
+ autoload :PermissionHandler, 'api_valve/forwarder/permission_handler'
8
+ autoload :Request, 'api_valve/forwarder/request'
9
+ autoload :Response, 'api_valve/forwarder/response'
5
10
 
6
11
  include Benchmarking
7
12
 
8
- DEFAULT_OPTIONS = {
9
- response_klass: Response,
10
- request_klass: Request
11
- }.freeze
13
+ # Initialized with global options. Possible values are:
14
+ # request: Options for the request wrapper. See Request#new.
15
+ # response: Options for the response wrapper. See Response#new
16
+ def initialize(options = {})
17
+ @options = options.with_indifferent_access
18
+ end
12
19
 
13
- attr_accessor :response_klass, :request_klass
14
- attr_reader :endpoint
20
+ # Takes the original rack request with optional options and returns a rack response
21
+ # Instanciates the Request and Response classes and wraps them arround the original
22
+ # request and response.
23
+ def call(original_request, local_options = {})
24
+ request = request_klass.new(original_request, request_options.deep_merge(local_options))
25
+ raise Error::Forbidden unless request.allowed?
26
+ response_klass.new(original_request, run_request(request), response_options).rack_response
27
+ end
15
28
 
16
- def initialize(options = {})
17
- DEFAULT_OPTIONS.merge(options).each do |k, v|
18
- public_send("#{k}=", v)
19
- end
29
+ private
30
+
31
+ def request_klass
32
+ request_options[:klass] || Request
20
33
  end
21
34
 
22
- def call(original_request, request_options = {})
23
- request = request_klass.new(original_request, request_options)
24
- response_klass.new(run_request(request)).rack_response
35
+ def request_options
36
+ # integrate permission handler options as it is instantiated in the request
37
+ (@options[:request] || {}).merge(@options.slice(:permission_handler))
25
38
  end
26
39
 
27
- # Enforce trailing slash
28
- def endpoint=(endpoint)
29
- @endpoint = File.join(endpoint, '')
40
+ def response_klass
41
+ response_options[:klass] || Response
30
42
  end
31
43
 
32
- private
44
+ def response_options
45
+ # integrate permission handler options as it is instantiated in the response
46
+ (@options[:response] || {}).merge(@options.slice(:permission_handler) || {})
47
+ end
33
48
 
34
49
  def run_request(request)
35
50
  log_request request
@@ -40,6 +55,8 @@ module ApiValve
40
55
  end
41
56
  log_response response, elapsed_time
42
57
  response
58
+ rescue Faraday::ConnectionFailed
59
+ raise Error::ServiceUnavailable
43
60
  end
44
61
 
45
62
  def log_request(request)
@@ -71,5 +88,10 @@ module ApiValve
71
88
  config.adapter Faraday.default_adapter
72
89
  end
73
90
  end
91
+
92
+ # Enforce trailing slash
93
+ def endpoint
94
+ @endpoint ||= File.join(@options[:endpoint], '')
95
+ end
74
96
  end
75
97
  end
@@ -2,9 +2,11 @@ require 'byebug'
2
2
 
3
3
  module ApiValve
4
4
  class Proxy
5
- include ActiveSupport::Rescuable
5
+ include ActiveSupport::Callbacks
6
6
 
7
- FORWARDER_OPTIONS = %w(endpoint).freeze
7
+ FORWARDER_OPTIONS = %w(endpoint request response permission_handler).freeze
8
+
9
+ define_callbacks :call
8
10
 
9
11
  class << self
10
12
  def from_yaml(file_path)
@@ -18,15 +20,16 @@ module ApiValve
18
20
  end
19
21
  end
20
22
 
21
- attr_reader :forwarder, :router
23
+ attr_reader :request, :forwarder, :router
22
24
 
23
25
  def initialize(forwarder)
24
26
  @forwarder = forwarder
25
27
  @router = Router.new
26
28
  end
27
29
 
28
- def call(*args)
29
- @router.call(*args)
30
+ def call(env)
31
+ @request = Rack::Request.new(env)
32
+ run_callbacks(:call) { @router.call(@request) }
30
33
  rescue ApiValve::Error::Base => e
31
34
  ErrorResponder.new(e).call
32
35
  end
@@ -35,20 +38,21 @@ module ApiValve
35
38
 
36
39
  def build_routes_from_config(config)
37
40
  config['routes']&.each do |route_config|
38
- method, path_regexp, req_conf = *route_config.values_at('method', 'path', 'request')
41
+ method, path_regexp, request_override = *route_config.values_at('method', 'path', 'request')
42
+ method ||= %w(get head patch post put) # no method defined means all methods
39
43
  if route_config['raise']
40
44
  deny method, path_regexp, with: route_config['raise']
41
45
  else
42
- forward method, path_regexp, req_conf
46
+ forward method, path_regexp, request_override
43
47
  end
44
48
  end
45
49
  forward_all
46
50
  end
47
51
 
48
- def forward(methods, path_regexp = nil, config = {})
52
+ def forward(methods, path_regexp = nil, request_override = {})
49
53
  Array.wrap(methods).each do |method|
50
54
  router.public_send(method, path_regexp, proc { |request, match_data|
51
- forwarder.call request, {'match_data' => match_data}.merge(config || {})
55
+ forwarder.call request, {'match_data' => match_data}.merge(request_override || {})
52
56
  })
53
57
  end
54
58
  end
@@ -15,8 +15,8 @@ module ApiValve
15
15
  reset_routes
16
16
  end
17
17
 
18
- def call(env)
19
- match Rack::Request.new(env)
18
+ def call(request)
19
+ match request
20
20
  end
21
21
 
22
22
  def delete(path = nil, callee = nil)
data/lib/api_valve.rb CHANGED
@@ -1,7 +1,10 @@
1
+ require 'active_support/callbacks'
1
2
  require 'active_support/configurable'
2
3
  require 'active_support/core_ext/class'
3
4
  require 'active_support/core_ext/hash'
5
+ require 'active_support/core_ext/object'
4
6
  require 'active_support/core_ext/module'
7
+ require 'active_support/json'
5
8
  require 'active_support/rescuable'
6
9
  require 'benchmark'
7
10
  require 'faraday'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: api_valve
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.alpha
4
+ version: 0.0.1.beta.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - mkon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-17 00:00:00.000000000 Z
11
+ date: 2018-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -106,28 +106,28 @@ dependencies:
106
106
  requirements:
107
107
  - - '='
108
108
  - !ruby/object:Gem::Version
109
- version: 0.54.0
109
+ version: 0.56.0
110
110
  type: :development
111
111
  prerelease: false
112
112
  version_requirements: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - '='
115
115
  - !ruby/object:Gem::Version
116
- version: 0.54.0
116
+ version: 0.56.0
117
117
  - !ruby/object:Gem::Dependency
118
118
  name: rubocop-rspec
119
119
  requirement: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - '='
122
122
  - !ruby/object:Gem::Version
123
- version: 1.22.2
123
+ version: 1.25.1
124
124
  type: :development
125
125
  prerelease: false
126
126
  version_requirements: !ruby/object:Gem::Requirement
127
127
  requirements:
128
128
  - - '='
129
129
  - !ruby/object:Gem::Version
130
- version: 1.22.2
130
+ version: 1.25.1
131
131
  - !ruby/object:Gem::Dependency
132
132
  name: simplecov
133
133
  requirement: !ruby/object:Gem::Requirement
@@ -183,6 +183,7 @@ files:
183
183
  - lib/api_valve/error.rb
184
184
  - lib/api_valve/error_responder.rb
185
185
  - lib/api_valve/forwarder.rb
186
+ - lib/api_valve/forwarder/permission_handler.rb
186
187
  - lib/api_valve/forwarder/request.rb
187
188
  - lib/api_valve/forwarder/response.rb
188
189
  - lib/api_valve/middleware/error_handling.rb