api_valve 0.6.1 → 0.7.0

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: 8f4cba9301dc5313a548d0d0be717b8bbc0af686c5da846bac838547e08dd8e3
4
- data.tar.gz: 42d48117b5969d38f5125bfa4fbbde0633fbde1214b738de1efd607007cbb978
3
+ metadata.gz: 975b9612ff292d9330c8ee255ec3584f83f5daa8fe2e2a65cf3af0ed7f000eed
4
+ data.tar.gz: c3d75f806fad96a5ca9f871973a4b458e1da812e0c8bb0e4480c887dc12c77b8
5
5
  SHA512:
6
- metadata.gz: 537cfb673ad5a8eeb6ec9f0871a0a85d6b61206326e0c9f0f4731414f341f60c45b595feab392f86ab4ea00c64819f83c3b7b98c2650a40e318e22bf1e812a64
7
- data.tar.gz: f7e6538270b6d444c1bdf4b1fe06f9d86864c61ca1d9738201d21f9eb06c6349888e4141756ef5db7afdc63a9c427985a64d8d7c306d783d9740ad04d8934743
6
+ metadata.gz: f4454093eb9d930218a252d6e05c6bcfa573172660c34c1a7bfba91ad085df92bc6ed8fc2d02714fb39a32960a493c30518609c6bb3a3d77efc8b2e44daf0d08
7
+ data.tar.gz: 8a7c8e8ba52346c4c65b2a6dcaa0f95592d880de6c68745b1a4259f68ab4fc2c0e16fef4f75ecb7c1f9d56dd70d0d491a8ff421622b4d7ed34e6b848e3aa5feb
@@ -12,16 +12,16 @@ require 'multi_json'
12
12
  require 'logger'
13
13
 
14
14
  module ApiValve
15
- autoload :Benchmarking, 'api_valve/benchmarking'
16
- autoload :Cascade, 'api_valve/cascade'
17
- autoload :Error, 'api_valve/error'
18
- autoload :ErrorResponder, 'api_valve/error_responder'
19
- autoload :Forwarder, 'api_valve/forwarder'
20
- autoload :Middleware, 'api_valve/middleware'
21
- autoload :Logger, 'api_valve/logger'
22
- autoload :Proxy, 'api_valve/proxy'
23
- autoload :RouteSet, 'api_valve/route_set'
24
- autoload :Runner, 'api_valve/runner'
15
+ autoload :Benchmarking, 'api_valve/benchmarking'
16
+ autoload :Cascade, 'api_valve/cascade'
17
+ autoload :Error, 'api_valve/error'
18
+ autoload :ErrorResponder, 'api_valve/error_responder'
19
+ autoload :Forwarder, 'api_valve/forwarder'
20
+ autoload :Middleware, 'api_valve/middleware'
21
+ autoload :Logger, 'api_valve/logger'
22
+ autoload :PermissionHandler, 'api_valve/permission_handler'
23
+ autoload :Proxy, 'api_valve/proxy'
24
+ autoload :RouteSet, 'api_valve/route_set'
25
25
 
26
26
  include ActiveSupport::Configurable
27
27
 
@@ -4,9 +4,8 @@ module ApiValve
4
4
  # options, and called from the router.
5
5
 
6
6
  class Forwarder
7
- autoload :PermissionHandler, 'api_valve/forwarder/permission_handler'
8
- autoload :Request, 'api_valve/forwarder/request'
9
- autoload :Response, 'api_valve/forwarder/response'
7
+ autoload :Request, 'api_valve/forwarder/request'
8
+ autoload :Response, 'api_valve/forwarder/response'
10
9
 
11
10
  include Benchmarking
12
11
 
@@ -25,7 +24,6 @@ module ApiValve
25
24
  # request and response.
26
25
  def call(original_request, local_options = {})
27
26
  request = build_request(original_request, request_options.deep_merge(local_options))
28
- request.check_permissions!
29
27
  response = build_response(original_request, run_request(request), response_options)
30
28
  response.rack_response
31
29
  end
@@ -50,13 +48,11 @@ module ApiValve
50
48
  end
51
49
 
52
50
  def request_options
53
- # integrate permission handler options as it is instantiated in the request
54
- (@options[:request] || {}).merge(@options.slice(:permission_handler))
51
+ (@options[:request] || {})
55
52
  end
56
53
 
57
54
  def response_options
58
- # integrate permission handler options as it is instantiated in the response
59
- (@options[:response] || {}).merge(@options.slice(:permission_handler) || {})
55
+ (@options[:response] || {})
60
56
  end
61
57
 
62
58
  def run_request(request)
@@ -5,8 +5,6 @@ module ApiValve
5
5
  # request is forwarded
6
6
 
7
7
  class Forwarder::Request
8
- include Forwarder::PermissionHandler::RequestIntegration
9
-
10
8
  attr_reader :original_request, :options
11
9
 
12
10
  WHITELISTED_HEADERS = %w(
@@ -25,9 +23,6 @@ module ApiValve
25
23
  @options = options.with_indifferent_access
26
24
  end
27
25
 
28
- # Called by forwarder before actual request is executed
29
- delegate :check_permissions!, to: :permission_handler
30
-
31
26
  # HTTP method to use when forwarding. Must return sym.
32
27
  # Returns original request method
33
28
  def method
@@ -72,6 +67,12 @@ module ApiValve
72
67
  @url_params ||= Rack::Utils.parse_nested_query(original_request.query_string)
73
68
  end
74
69
 
70
+ protected
71
+
72
+ def permission_handler
73
+ original_request.env['permission_handler']
74
+ end
75
+
75
76
  private
76
77
 
77
78
  def forwarded_headers
@@ -9,8 +9,6 @@ module ApiValve
9
9
  # to the original caller
10
10
 
11
11
  class Forwarder::Response
12
- include Forwarder::PermissionHandler::RequestIntegration
13
-
14
12
  attr_reader :original_request, :original_response, :options
15
13
 
16
14
  WHITELISTED_HEADERS = %w(
@@ -31,6 +29,12 @@ module ApiValve
31
29
  [status, headers, [body]]
32
30
  end
33
31
 
32
+ protected
33
+
34
+ def permission_handler
35
+ original_request.env['permission_handler']
36
+ end
37
+
34
38
  private
35
39
 
36
40
  def status
@@ -1,8 +1,9 @@
1
1
  module ApiValve
2
2
  class Middleware
3
- autoload :ErrorHandling, 'api_valve/middleware/error_handling'
4
- autoload :Logging, 'api_valve/middleware/logging'
5
- autoload :Router, 'api_valve/middleware/router'
3
+ autoload :ErrorHandling, 'api_valve/middleware/error_handling'
4
+ autoload :Logging, 'api_valve/middleware/logging'
5
+ autoload :PermissionCheck, 'api_valve/middleware/permission_check'
6
+ autoload :Router, 'api_valve/middleware/router'
6
7
 
7
8
  Item = Struct.new(:klass, :proc)
8
9
 
@@ -0,0 +1,36 @@
1
+ class ApiValve::Middleware
2
+ class PermissionCheck
3
+ def initialize(app, options = {})
4
+ @app = app
5
+ @options = options
6
+ end
7
+
8
+ def call(env)
9
+ env['permission_handler'] = @handler
10
+ if handler(env).allowed?
11
+ @app.call(env)
12
+ else
13
+ message = handler(env).message
14
+ ApiValve.logger.info { message }
15
+ render_error ApiValve::Error::Forbidden.new message
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def handler(env)
22
+ env['permission_handler'] ||= handler_klass.new(
23
+ env,
24
+ @options.merge(env['api_valve.router.route'].options[:permission_handler] || {})
25
+ )
26
+ end
27
+
28
+ def handler_klass
29
+ @options[:klass] || ApiValve::PermissionHandler
30
+ end
31
+
32
+ def render_error(error)
33
+ self.class.const_get(ApiValve.error_responder).new(error).call
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ module ApiValve
2
+ class PermissionHandler
3
+ def initialize(env, options = {})
4
+ @env = env
5
+ @options = options.with_indifferent_access
6
+ end
7
+
8
+ # Run permission checks
9
+ # Simple implementation is always true. Override in your implementation.
10
+ def allowed?
11
+ true
12
+ end
13
+
14
+ # Returns string message why access was denied
15
+ # Rendered on the API.
16
+ # Override in your implementation.
17
+ def message
18
+ 'Insufficient permissions'
19
+ end
20
+
21
+ protected
22
+
23
+ attr_reader :env, :options
24
+ end
25
+ end
@@ -1,69 +1,17 @@
1
1
  module ApiValve
2
2
  class Proxy
3
- include ActiveSupport::Callbacks
3
+ autoload :Builder, 'api_valve/proxy/builder'
4
+ autoload :Runner, 'api_valve/proxy/runner'
4
5
 
5
- FORWARDER_OPTIONS = %w(endpoint request response permission_handler).freeze
6
+ extend Builder
7
+
8
+ FORWARDER_OPTIONS = %w(endpoint request response).freeze
6
9
 
7
10
  class_attribute :permission_handler, :request, :response
8
- self.permission_handler = Forwarder::PermissionHandler
11
+ self.permission_handler = PermissionHandler
9
12
  self.request = Forwarder::Request
10
13
  self.response = Forwarder::Response
11
14
 
12
- class << self
13
- # Creates a n instance from a config hash and takes optional block
14
- # which is executed in scope of the proxy
15
- def build(config)
16
- block = Proc.new if block_given? # capture the yield
17
- from_hash(config).tap do |proxy|
18
- proxy.instance_eval(&block) if block
19
- end
20
- end
21
-
22
- def from_config(file_name = nil)
23
- file_name ||= name.underscore
24
- path = find_config(file_name)
25
- raise "Config not found for #{name.underscore}(.yml|.yml.erb) in #{ApiValve.config_paths.inspect}" unless path
26
-
27
- yaml = File.read(path)
28
- yaml = ERB.new(yaml, nil, '-').result if path.fnmatch? '*.erb'
29
- from_yaml yaml
30
- end
31
-
32
- def from_yaml(string)
33
- from_hash YAML.load(string) # rubocop:disable Security/YAMLLoad
34
- end
35
-
36
- def from_hash(config)
37
- config = config.with_indifferent_access
38
- forwarder = Forwarder.new(forwarder_config(config))
39
- new(forwarder).tap do |proxy|
40
- Array.wrap(config[:use]).each { |mw| proxy.use mw }
41
- proxy.build_routes_from_config config[:routes]
42
- end
43
- end
44
-
45
- private
46
-
47
- def find_config(file_name)
48
- ApiValve.config_paths.each do |dir|
49
- path = dir.join("#{file_name}.yml")
50
- return path if path.exist?
51
-
52
- path = dir.join("#{file_name}.yml.erb")
53
- return path if path.exist?
54
- end
55
- nil
56
- end
57
-
58
- def forwarder_config(config)
59
- {
60
- permission_handler: {klass: permission_handler},
61
- request: {klass: request},
62
- response: {klass: response}
63
- }.with_indifferent_access.deep_merge config.slice(*FORWARDER_OPTIONS)
64
- end
65
- end
66
-
67
15
  attr_reader :request, :forwarder, :middleware, :route_set
68
16
 
69
17
  alias router route_set
@@ -84,38 +32,19 @@ module ApiValve
84
32
  delegate :add_route, to: :route_set
85
33
  delegate :use, to: :middleware
86
34
 
87
- def build_routes_from_config(routes_config)
88
- return forward_all unless routes_config
89
-
90
- routes_config.each do |route_config|
91
- method, path_regexp, request_override = *route_config.values_at('method', 'path', 'request')
92
- method ||= 'any' # no method defined means all methods
93
- if route_config['raise']
94
- deny method, path_regexp, with: route_config['raise']
95
- else
96
- forward method, path_regexp, request_override
97
- end
98
- end
99
- end
100
-
101
- def forward(methods, path_regexp = nil, request_override = {})
102
- Array.wrap(methods).each do |method|
103
- route_set.public_send(method, path_regexp, proc { |request, match_data|
104
- forwarder.call request, {'match_data' => match_data}.merge(request_override || {})
105
- })
106
- end
35
+ def forward(methods, path_regexp = nil, options = {})
36
+ options = options.with_indifferent_access
37
+ route_set.append(methods, path_regexp, options.except(:request), proc { |request, match_data|
38
+ forwarder.call request, {'match_data' => match_data}.merge(options[:request] || {}).with_indifferent_access
39
+ })
107
40
  end
108
41
 
109
- def forward_all
110
- route_set.any do |request, match_data|
111
- forwarder.call request, 'match_data' => match_data
112
- end
42
+ def forward_all(options = {})
43
+ forward(RouteSet::METHODS, nil, options)
113
44
  end
114
45
 
115
46
  def deny(methods, path_regexp = nil, with: 'Error::Forbidden')
116
- Array.wrap(methods).each do |method|
117
- route_set.public_send(method, path_regexp, ->(*_args) { raise ApiValve.const_get(with) })
118
- end
47
+ route_set.append(methods, path_regexp, {}, ->(*_args) { raise ApiValve.const_get(with) })
119
48
  end
120
49
 
121
50
  protected
@@ -0,0 +1,72 @@
1
+ module ApiValve
2
+ class Proxy
3
+ module Builder
4
+ # Creates a n instance from a config hash and takes optional block
5
+ # which is executed in scope of the proxy
6
+ def build(config)
7
+ block = Proc.new if block_given? # capture the yield
8
+ from_hash(config).tap do |proxy|
9
+ proxy.instance_eval(&block) if block
10
+ end
11
+ end
12
+
13
+ def from_config(file_name = nil)
14
+ file_name ||= name.underscore
15
+ path = find_config(file_name)
16
+ raise "Config not found for #{name.underscore}(.yml|.yml.erb) in #{ApiValve.config_paths.inspect}" unless path
17
+
18
+ yaml = File.read(path)
19
+ yaml = ERB.new(yaml, nil, '-').result if path.fnmatch? '*.erb'
20
+ from_yaml yaml
21
+ end
22
+
23
+ def from_yaml(string)
24
+ from_hash YAML.load(string) # rubocop:disable Security/YAMLLoad
25
+ end
26
+
27
+ def from_hash(config)
28
+ config = config.with_indifferent_access
29
+ forwarder = Forwarder.new(forwarder_config(config))
30
+ new(forwarder).tap do |proxy|
31
+ Array.wrap(config[:use]).each { |mw| proxy.use mw }
32
+ add_routes_from_config proxy, config[:routes]
33
+ proxy.use Middleware::PermissionCheck, config[:permission_handler] if config[:permission_handler]
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def add_routes_from_config(proxy, routes_config)
40
+ return proxy.forward_all unless routes_config
41
+
42
+ routes_config.each do |route_config|
43
+ method, path_regexp = *route_config.values_at('method', 'path')
44
+ method ||= 'any' # no method defined means all methods
45
+ if route_config['raise']
46
+ proxy.deny method, path_regexp, with: route_config['raise']
47
+ else
48
+ proxy.forward method, path_regexp, route_config.except('method', 'path')
49
+ end
50
+ end
51
+ end
52
+
53
+ def find_config(file_name)
54
+ ApiValve.config_paths.each do |dir|
55
+ path = dir.join("#{file_name}.yml")
56
+ return path if path.exist?
57
+
58
+ path = dir.join("#{file_name}.yml.erb")
59
+ return path if path.exist?
60
+ end
61
+ nil
62
+ end
63
+
64
+ def forwarder_config(config)
65
+ {
66
+ request: {klass: request},
67
+ response: {klass: response}
68
+ }.with_indifferent_access.deep_merge config.slice(*FORWARDER_OPTIONS)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,5 +1,5 @@
1
1
  module ApiValve
2
- class Runner
2
+ class Proxy::Runner
3
3
  def call(env)
4
4
  env['api_valve.router.route'].call \
5
5
  Rack::Request.new(env),
@@ -2,7 +2,7 @@ module ApiValve
2
2
  class RouteSet
3
3
  METHODS = %i(get post put patch delete head).freeze
4
4
 
5
- Route = Struct.new(:regexp, :block) do
5
+ Route = Struct.new(:regexp, :options, :block) do
6
6
  delegate :call, to: :block
7
7
 
8
8
  def match(path_info)
@@ -24,7 +24,7 @@ module ApiValve
24
24
  raise 'URL not supported' if request.path_info.include?('/../')
25
25
 
26
26
  match_data = nil
27
- route = @routes && @routes[request.request_method.downcase.to_sym].find do |r|
27
+ route = @routes && @routes[request.request_method.downcase].find do |r|
28
28
  (match_data = r.match(request.path_info))
29
29
  end
30
30
  raise Error::NotRouted, 'Endpoint not found' unless route
@@ -32,50 +32,55 @@ module ApiValve
32
32
  [route, match_data]
33
33
  end
34
34
 
35
- def delete(path = nil, prok = nil)
36
- append :delete, path, prok || Proc.new
35
+ def delete(path = nil, options = {}, prok = nil)
36
+ push :delete, path, options, prok || Proc.new
37
37
  end
38
38
 
39
- def get(path = nil, prok = nil)
40
- append :get, path, prok || Proc.new
39
+ def get(path = nil, options = {}, prok = nil)
40
+ push :get, path, options, prok || Proc.new
41
41
  end
42
42
 
43
- def head(path = nil, prok = nil)
44
- append :head, path, prok || Proc.new
43
+ def head(path = nil, options = {}, prok = nil)
44
+ push :head, path, options, prok || Proc.new
45
45
  end
46
46
 
47
- def patch(path = nil, prok = nil)
48
- append :patch, path, prok || Proc.new
47
+ def patch(path = nil, options = {}, prok = nil)
48
+ push :patch, path, options, prok || Proc.new
49
49
  end
50
50
 
51
- def post(path = nil, prok = nil)
52
- append :post, path, prok || Proc.new
51
+ def post(path = nil, options = {}, prok = nil)
52
+ push :post, path, options, prok || Proc.new
53
53
  end
54
54
 
55
- def put(path = nil, prok = nil)
56
- append :put, path, prok || Proc.new
55
+ def put(path = nil, options = {}, prok = nil)
56
+ push :put, path, options, prok || Proc.new
57
57
  end
58
58
 
59
- def any(path = nil, prok = nil)
60
- append METHODS, path, prok || Proc.new
59
+ def any(path = nil, options = {}, prok = nil)
60
+ append METHODS, path, options, prok || Proc.new
61
61
  end
62
62
 
63
- def append(methods, regexp, prok = nil)
64
- prok ||= Proc.new
65
- Array.wrap(methods).each do |method|
66
- @routes[method] << Route.new(regexp, prok)
67
- end
63
+ def push(methods, regexp, options = {}, prok = nil)
64
+ add_route :push, methods, regexp, options, prok || Proc.new
68
65
  end
69
66
 
70
- def unshift(methods, regexp = nil, prok = nil)
71
- prok ||= Proc.new
72
- Array.wrap(methods).each do |method|
73
- @routes[method].unshift Route.new(regexp, prok)
74
- end
67
+ alias append push
68
+
69
+ def unshift(methods, regexp = nil, options = {}, prok = nil)
70
+ add_route :unshift, methods, regexp, options, prok || Proc.new
75
71
  end
76
72
 
77
73
  def reset_routes
78
- @routes = Hash[METHODS.map { |v| [v, []] }].freeze
74
+ @routes = Hash[METHODS.map { |v| [v, []] }].with_indifferent_access.freeze
75
+ end
76
+
77
+ private
78
+
79
+ def add_route(how, methods, regexp, options, prok)
80
+ methods = METHODS if methods.to_s == 'any'
81
+ Array.wrap(methods).each do |method|
82
+ @routes[method].public_send how, Route.new(regexp, options, prok)
83
+ end
79
84
  end
80
85
  end
81
86
  end
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.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mkon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-18 00:00:00.000000000 Z
11
+ date: 2018-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -198,17 +198,19 @@ files:
198
198
  - lib/api_valve/error.rb
199
199
  - lib/api_valve/error_responder.rb
200
200
  - lib/api_valve/forwarder.rb
201
- - lib/api_valve/forwarder/permission_handler.rb
202
201
  - lib/api_valve/forwarder/request.rb
203
202
  - lib/api_valve/forwarder/response.rb
204
203
  - lib/api_valve/logger.rb
205
204
  - lib/api_valve/middleware.rb
206
205
  - lib/api_valve/middleware/error_handling.rb
207
206
  - lib/api_valve/middleware/logging.rb
207
+ - lib/api_valve/middleware/permission_check.rb
208
208
  - lib/api_valve/middleware/router.rb
209
+ - lib/api_valve/permission_handler.rb
209
210
  - lib/api_valve/proxy.rb
211
+ - lib/api_valve/proxy/builder.rb
212
+ - lib/api_valve/proxy/runner.rb
210
213
  - lib/api_valve/route_set.rb
211
- - lib/api_valve/runner.rb
212
214
  homepage: https://github.com/mkon/api_valve
213
215
  licenses:
214
216
  - MIT
@@ -1,47 +0,0 @@
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
- InsufficientPermissions = Class.new(Error::Forbidden)
8
-
9
- module RequestIntegration
10
- private
11
-
12
- def permission_handler
13
- permission_handler_klass.instance(original_request, permission_handler_options)
14
- end
15
-
16
- def permission_handler_klass
17
- permission_handler_options[:klass] || Forwarder::PermissionHandler
18
- end
19
-
20
- def permission_handler_options
21
- options[:permission_handler] || {}
22
- end
23
- end
24
-
25
- attr_reader :request, :options
26
-
27
- delegate :env, to: :request
28
-
29
- # Returns an instance of the PermissionHandler, cached in the request env
30
- # This allows re-use of the PermissionHandler by both Request and Response instances
31
- def self.instance(request, options)
32
- request.env['permission_handler'] ||= new(request, options)
33
- end
34
-
35
- def initialize(request, options = {})
36
- @request = request
37
- @options = options
38
- end
39
-
40
- # Run permission checks
41
- # Simple implementation is always true. Override in your implementation.
42
- # For example raise ApiValve::Error::Forbidden in certain conditions
43
- def check_permissions!
44
- true
45
- end
46
- end
47
- end