api_valve 0.6.1 → 0.7.0

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: 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