api_valve 0.0.1.alpha → 0.0.1.beta.1

Sign up to get free protection for your applications and to get access to all the features.
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