drillbit 0.0.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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/LICENSE.txt +19 -0
  5. data/README.md +2 -0
  6. data/Rakefile +2 -0
  7. data/lib/drillbit.rb +19 -0
  8. data/lib/drillbit/accept_header.rb +50 -0
  9. data/lib/drillbit/authorizable_resource.rb +160 -0
  10. data/lib/drillbit/authorizers/parameters.rb +24 -0
  11. data/lib/drillbit/authorizers/parameters/filtering.rb +50 -0
  12. data/lib/drillbit/authorizers/parameters/resource.rb +11 -0
  13. data/lib/drillbit/authorizers/query.rb +40 -0
  14. data/lib/drillbit/authorizers/scope.rb +30 -0
  15. data/lib/drillbit/configuration.rb +36 -0
  16. data/lib/drillbit/errors/invalid_api_request.rb +29 -0
  17. data/lib/drillbit/errors/invalid_subdomain.rb +29 -0
  18. data/lib/drillbit/errors/invalid_token.rb +22 -0
  19. data/lib/drillbit/matchers/accept_header.rb +16 -0
  20. data/lib/drillbit/matchers/generic.rb +30 -0
  21. data/lib/drillbit/matchers/subdomain.rb +31 -0
  22. data/lib/drillbit/matchers/version.rb +30 -0
  23. data/lib/drillbit/middleware/api_request.rb +49 -0
  24. data/lib/drillbit/parameters.rb +22 -0
  25. data/lib/drillbit/parameters/filter.rb +57 -0
  26. data/lib/drillbit/parameters/index.rb +31 -0
  27. data/lib/drillbit/parameters/page.rb +28 -0
  28. data/lib/drillbit/parameters/sort.rb +32 -0
  29. data/lib/drillbit/requests/base.rb +114 -0
  30. data/lib/drillbit/requests/rack.rb +50 -0
  31. data/lib/drillbit/requests/rails.rb +44 -0
  32. data/lib/drillbit/resource.rb +14 -0
  33. data/lib/drillbit/resource/model.rb +41 -0
  34. data/lib/drillbit/resource/naming.rb +33 -0
  35. data/lib/drillbit/resource/processors/filtering.rb +66 -0
  36. data/lib/drillbit/resource/processors/indexing.rb +40 -0
  37. data/lib/drillbit/resource/processors/paging.rb +46 -0
  38. data/lib/drillbit/resource/processors/sorting.rb +42 -0
  39. data/lib/drillbit/responses/invalid_api_request.rb +18 -0
  40. data/lib/drillbit/responses/invalid_subdomain.rb +18 -0
  41. data/lib/drillbit/responses/invalid_token.rb +20 -0
  42. data/lib/drillbit/serializers/json_api.rb +10 -0
  43. data/lib/drillbit/tokens/base64.rb +45 -0
  44. data/lib/drillbit/tokens/base64s/invalid.rb +14 -0
  45. data/lib/drillbit/tokens/base64s/null.rb +14 -0
  46. data/lib/drillbit/tokens/invalid.rb +26 -0
  47. data/lib/drillbit/tokens/json_web_token.rb +112 -0
  48. data/lib/drillbit/tokens/json_web_tokens/invalid.rb +14 -0
  49. data/lib/drillbit/tokens/json_web_tokens/null.rb +14 -0
  50. data/lib/drillbit/tokens/null.rb +26 -0
  51. data/lib/drillbit/version.rb +4 -0
  52. data/spec/drillbit/accept_header_spec.rb +112 -0
  53. data/spec/drillbit/authorizers/parameters/filtering_spec.rb +71 -0
  54. data/spec/drillbit/authorizers/parameters/resource_spec.rb +12 -0
  55. data/spec/drillbit/authorizers/parameters_spec.rb +17 -0
  56. data/spec/drillbit/authorizers/query_spec.rb +21 -0
  57. data/spec/drillbit/authorizers/scope_spec.rb +20 -0
  58. data/spec/drillbit/errors/invalid_api_request_spec.rb +31 -0
  59. data/spec/drillbit/errors/invalid_subdomain_spec.rb +31 -0
  60. data/spec/drillbit/errors/invalid_token_spec.rb +24 -0
  61. data/spec/drillbit/invalid_subdomain_spec.rb +46 -0
  62. data/spec/drillbit/invalid_token_spec.rb +44 -0
  63. data/spec/drillbit/matchers/accept_header_spec.rb +114 -0
  64. data/spec/drillbit/matchers/subdomain_spec.rb +78 -0
  65. data/spec/drillbit/matchers/version_spec.rb +86 -0
  66. data/spec/drillbit/middleware/api_request_spec.rb +220 -0
  67. data/spec/drillbit/parameters_spec.rb +49 -0
  68. data/spec/drillbit/requests/base_spec.rb +37 -0
  69. data/spec/drillbit/requests/rack_spec.rb +253 -0
  70. data/spec/drillbit/requests/rails_spec.rb +264 -0
  71. data/spec/drillbit/resource/model_spec.rb +64 -0
  72. data/spec/drillbit/resource/processors/filtering_spec.rb +106 -0
  73. data/spec/drillbit/resource/processors/indexing_spec.rb +46 -0
  74. data/spec/drillbit/resource/processors/paging_spec.rb +74 -0
  75. data/spec/drillbit/resource/processors/sorting_spec.rb +66 -0
  76. data/spec/drillbit/tokens/base64_spec.rb +44 -0
  77. data/spec/drillbit/tokens/json_web_token_spec.rb +135 -0
  78. data/spec/fixtures/test_rsa_key +27 -0
  79. data/spec/fixtures/test_rsa_key.pub +9 -0
  80. data/spec/spec_helper.rb +4 -0
  81. data/spec/support/private_keys.rb +42 -0
  82. metadata +244 -0
  83. metadata.gz.sig +0 -0
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum'
3
+
4
+ module Drillbit
5
+ module Errors
6
+ class InvalidApiRequest < RuntimeError
7
+ include Erratum::Error
8
+
9
+ attr_accessor :accept_header
10
+
11
+ def http_status
12
+ 400
13
+ end
14
+
15
+ def title
16
+ 'Invalid API Request'
17
+ end
18
+
19
+ def detail
20
+ 'The accept header that you passed in the request cannot be parsed, ' \
21
+ 'please refer to the documentation to verify.'
22
+ end
23
+
24
+ def source
25
+ { accept_header: accept_header }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum'
3
+
4
+ module Drillbit
5
+ module Errors
6
+ class InvalidSubdomain < RuntimeError
7
+ include Erratum::Error
8
+
9
+ attr_accessor :http_host
10
+
11
+ def http_status
12
+ 404
13
+ end
14
+
15
+ def title
16
+ 'Invalid Subdomain'
17
+ end
18
+
19
+ def detail
20
+ 'The resource you attempted to access is either not authorized for the ' \
21
+ 'authenticated user or does not exist.'
22
+ end
23
+
24
+ def source
25
+ { http_host: http_host }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ require 'erratum'
3
+
4
+ module Drillbit
5
+ module Errors
6
+ class InvalidToken < RuntimeError
7
+ include Erratum::Error
8
+
9
+ def http_status
10
+ 401
11
+ end
12
+
13
+ def title
14
+ 'Invalid or Unauthorized Token'
15
+ end
16
+
17
+ def detail
18
+ 'Either the token you passed is invalid or is not allowed to perform this action.'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ require 'drillbit/matchers/generic'
3
+
4
+ module Drillbit
5
+ module Matchers
6
+ class AcceptHeader
7
+ include Generic
8
+
9
+ def matches?(request)
10
+ super
11
+
12
+ accept_header.valid?
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require 'drillbit/requests/base'
3
+
4
+ module Drillbit
5
+ module Matchers
6
+ module Generic
7
+ attr_accessor :application,
8
+ :accept_header,
9
+ :request
10
+
11
+ def initialize(**args)
12
+ args.each do |variable, value|
13
+ __send__("#{variable}=", value)
14
+ end
15
+ end
16
+
17
+ def matches?(request)
18
+ self.request = Requests::Base.resolve(request)
19
+ end
20
+
21
+ def application
22
+ request.application_name
23
+ end
24
+
25
+ def accept_header
26
+ request.accept_header
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ module Drillbit
3
+ module Matchers
4
+ class Subdomain
5
+ def initialize(allowed_subdomains: Drillbit.configuration.allowed_subdomains,
6
+ allowed_api_subdomains: Drillbit.configuration.allowed_api_subdomains)
7
+
8
+ self.allowed_subdomains = Array(allowed_subdomains)
9
+ self.allowed_api_subdomains = Array(allowed_api_subdomains)
10
+ end
11
+
12
+ def matches?(request)
13
+ self.request = Requests::Base.resolve(request)
14
+
15
+ allowed_subdomains.include? request.subdomain
16
+ end
17
+
18
+ def matches_api_subdomain?(request)
19
+ self.request = Requests::Base.resolve(request)
20
+
21
+ allowed_api_subdomains.include? request.subdomain
22
+ end
23
+
24
+ protected
25
+
26
+ attr_accessor :allowed_subdomains,
27
+ :allowed_api_subdomains,
28
+ :request
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require 'drillbit/configuration'
3
+ require 'drillbit/matchers/generic'
4
+
5
+ module Drillbit
6
+ module Matchers
7
+ class Version
8
+ include Generic
9
+
10
+ attr_accessor :version_constraint,
11
+ :default_version
12
+
13
+ def matches?(request)
14
+ super
15
+
16
+ requested_version == version_constraint
17
+ end
18
+
19
+ private
20
+
21
+ def requested_version
22
+ accept_header.version || default_version
23
+ end
24
+
25
+ def default_version
26
+ @default_version || Drillbit.configuration.default_api_version
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ require 'drillbit/configuration'
3
+ require 'drillbit/parameters'
4
+ require 'drillbit/matchers/subdomain'
5
+ require 'drillbit/matchers/accept_header'
6
+ require 'drillbit/requests/base'
7
+ require 'drillbit/responses/invalid_api_request'
8
+ require 'drillbit/responses/invalid_subdomain'
9
+ require 'drillbit/responses/invalid_token'
10
+
11
+ module Drillbit
12
+ module Middleware
13
+ class ApiRequest
14
+ JSON_API_MIME_TYPE_PATTERN = %r{application/vnd\.api\+json(?=\z|;)}
15
+
16
+ def initialize(app)
17
+ @app = app
18
+ end
19
+
20
+ # rubocop:disable Metrics/LineLength
21
+ # :reek:FeatureEnvy
22
+ def call(env)
23
+ env['HTTP_X_APPLICATION_NAME'] = Drillbit.configuration.application_name
24
+
25
+ request = Requests::Base.resolve(env)
26
+ subdomain_matcher = Matchers::Subdomain.new
27
+ accept_header_matcher = Matchers::AcceptHeader.new
28
+ token = request.authorization_token
29
+
30
+ return Responses::InvalidSubdomain.call(env) unless subdomain_matcher.matches?(request)
31
+ return Responses::InvalidApiRequest.call(env) unless !subdomain_matcher.matches_api_subdomain?(request) ||
32
+ accept_header_matcher.matches?(request)
33
+ return Responses::InvalidToken.call(env,
34
+ application_name: request.application_name) \
35
+ unless token.valid?
36
+
37
+ env['X_DECRYPTED_JSON_WEB_TOKEN'] = token.to_h
38
+ env['QUERY_STRING'] = Parameters.process(env['QUERY_STRING'])
39
+ env['CONTENT_TYPE'] = env['CONTENT_TYPE'].
40
+ to_s.
41
+ gsub JSON_API_MIME_TYPE_PATTERN,
42
+ 'application/json'
43
+
44
+ @app.call(env)
45
+ end
46
+ # rubocop:enable Metrics/LineLength
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ module Drillbit
3
+ class Parameters
4
+ attr_accessor :query_string
5
+
6
+ def initialize(query_string)
7
+ self.query_string = query_string
8
+ end
9
+
10
+ def self.process(query_string)
11
+ new(query_string).process
12
+ end
13
+
14
+ def process
15
+ return query_string unless query_string.respond_to? :gsub
16
+
17
+ query_string.gsub(/(?<=\A|&|\?)[^=&]+/) do |match|
18
+ match.tr('-', '_')
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ module Drillbit
3
+ class Parameters
4
+ class Filter
5
+ NUMERICAL = /[\d_\.]+?/
6
+ NUMERICAL_RANGE = /\A(#{NUMERICAL})\.\.\.?(#{NUMERICAL}|Infinity)\z/
7
+
8
+ attr_accessor :raw_parameters
9
+
10
+ def initialize(raw_parameters)
11
+ self.raw_parameters = raw_parameters || {}
12
+ end
13
+
14
+ def present?
15
+ compacted_parameters.any?
16
+ end
17
+
18
+ def each_with_object(memoized)
19
+ compacted_parameters.each do |name, value|
20
+ memoized = yield name, format_value(value), memoized
21
+ end
22
+
23
+ memoized
24
+ end
25
+
26
+ private
27
+
28
+ def compacted_parameters
29
+ @compacted_parameters ||= raw_parameters.reject do |name, value|
30
+ name == 'query' ||
31
+ value == '' ||
32
+ value.nil?
33
+ end
34
+ end
35
+
36
+ # rubocop:disable Lint/AssignmentInCondition
37
+ def format_value(value)
38
+ return value unless value.is_a?(String)
39
+
40
+ if range_points = value.match(NUMERICAL_RANGE)
41
+ exclusive = value.include? '...'
42
+ starting_point = range_points[1].to_f
43
+ ending_point = if range_points[2] == 'Infinity'
44
+ 9_999_999
45
+ else
46
+ range_points[2].to_f
47
+ end
48
+
49
+ Range.new(starting_point, ending_point, exclusive)
50
+ else
51
+ value
52
+ end
53
+ end
54
+ # rubocop:enable Lint/AssignmentInCondition
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ module Drillbit
3
+ class Parameters
4
+ class Index
5
+ DEFAULT_QUERY = '*'
6
+
7
+ attr_accessor :raw_parameters
8
+
9
+ def initialize(raw_parameters)
10
+ self.raw_parameters = raw_parameters || {}
11
+ end
12
+
13
+ def present?
14
+ query
15
+ end
16
+
17
+ def query
18
+ compacted_parameters['query'] || compacted_parameters['q']
19
+ end
20
+
21
+ private
22
+
23
+ def compacted_parameters
24
+ @compacted_parameters ||= raw_parameters.reject do |_name, value|
25
+ value == '' ||
26
+ value.nil?
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ module Drillbit
3
+ class Parameters
4
+ class Page
5
+ PAGING_PARAMETERS = %w{number size limit offset cursor}.freeze
6
+ DEFAULT_STARTING_PAGE = 1
7
+ DEFAULT_PAGE_SIZE = 25
8
+
9
+ attr_accessor :raw_parameters
10
+
11
+ def initialize(raw_parameters)
12
+ self.raw_parameters = raw_parameters || {}
13
+ end
14
+
15
+ def present?
16
+ (raw_parameters.keys & PAGING_PARAMETERS).any?
17
+ end
18
+
19
+ def page_number
20
+ raw_parameters['number'] || DEFAULT_STARTING_PAGE
21
+ end
22
+
23
+ def per_page
24
+ raw_parameters['size'] || DEFAULT_PAGE_SIZE
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ module Drillbit
3
+ class Parameters
4
+ class Sort
5
+ DESCENDING_PREFIX = '-'
6
+
7
+ attr_accessor :raw_parameters
8
+
9
+ def initialize(raw_parameters)
10
+ self.raw_parameters = raw_parameters ? raw_parameters.split(',') : []
11
+ end
12
+
13
+ def present?
14
+ raw_parameters.any?
15
+ end
16
+
17
+ def to_h
18
+ @to_h ||= Hash[to_a]
19
+ end
20
+
21
+ def to_a
22
+ @to_a ||= raw_parameters.map do |raw_parameter|
23
+ if raw_parameter.start_with?(DESCENDING_PREFIX)
24
+ [raw_parameter[1..-1], 'desc']
25
+ else
26
+ [raw_parameter, 'asc']
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ require 'drillbit/tokens/json_web_tokens/invalid'
3
+ require 'drillbit/tokens/json_web_token'
4
+
5
+ module Drillbit
6
+ module Requests
7
+ class Base
8
+ BASE64_PATTERN = %r{[A-Za-z0-9_/\+\=\-\.]}
9
+ BASE64_TOKEN_PARAM_NAME = 'token_b64'
10
+ JSON_WEB_TOKEN_PARAM_NAME = 'token_jwt'
11
+ JSON_WEB_TOKEN_PATTERN = /(#{BASE64_PATTERN}+?\.){4}#{BASE64_PATTERN}+?/
12
+ BASE64_TOKEN_HEADER_PATTERN = /\A(?:Basic|Bearer)\s+(.*)\z/
13
+ JSON_WEB_TOKEN_HEADER_PATTERN = /\AToken\s+(.*)\z/
14
+
15
+ attr_accessor :token_private_key,
16
+ :request
17
+
18
+ def initialize(token_private_key: Drillbit.configuration.token_private_key,
19
+ request:)
20
+
21
+ self.token_private_key = token_private_key
22
+ self.request = request
23
+ end
24
+
25
+ def accept_header
26
+ if accept_header_from_header.valid? ||
27
+ accept_header_from_params.invalid?
28
+
29
+ accept_header_from_header
30
+ else
31
+ accept_header_from_params
32
+ end
33
+ end
34
+
35
+ def authorization_token
36
+ if (
37
+ !authorization_token_from_header.blank? &&
38
+ authorization_token_from_header.valid?
39
+ ) ||
40
+ (
41
+ authorization_token_from_params.blank? ||
42
+ !authorization_token_from_params.valid?
43
+ )
44
+
45
+ authorization_token_from_header
46
+ else
47
+ authorization_token_from_params
48
+ end
49
+ end
50
+
51
+ def application_name
52
+ raw_request_application_name || Drillbit.configuration.application_name
53
+ end
54
+
55
+ def subdomain
56
+ @subdomain ||= raw_host[/\A([a-z\-]+)/i, 1]
57
+ end
58
+
59
+ def self.resolve(original_request)
60
+ if original_request.is_a? self
61
+ original_request
62
+ elsif original_request.respond_to? :headers
63
+ rails_request_class.new(request: original_request)
64
+ else
65
+ rack_request_class.new(request: original_request)
66
+ end
67
+ end
68
+
69
+ def self.rails_request_class
70
+ require 'drillbit/requests/rails'
71
+
72
+ Object.const_get('Drillbit::Requests::Rails')
73
+ end
74
+
75
+ def self.rack_request_class
76
+ require 'drillbit/requests/rack'
77
+
78
+ Object.const_get('Drillbit::Requests::Rack')
79
+ end
80
+
81
+ private
82
+
83
+ def accept_header_from_header
84
+ AcceptHeader.new(application: application_name,
85
+ header: raw_accept_header_from_header || '')
86
+ end
87
+
88
+ def accept_header_from_params
89
+ AcceptHeader.new(application: application_name,
90
+ header: raw_accept_header_from_params || '')
91
+ end
92
+
93
+ def authorization_token_from_header
94
+ case raw_authorization_header
95
+ when JSON_WEB_TOKEN_HEADER_PATTERN
96
+ Tokens::JsonWebToken.from_jwe(
97
+ raw_authorization_header[JSON_WEB_TOKEN_HEADER_PATTERN, 1],
98
+ private_key: token_private_key,
99
+ )
100
+ when BASE64_TOKEN_HEADER_PATTERN
101
+ Tokens::Base64.convert(
102
+ raw_token: raw_authorization_header[BASE64_TOKEN_HEADER_PATTERN, 1],
103
+ )
104
+ else
105
+ Tokens::Null.instance
106
+ end
107
+ end
108
+
109
+ def raw_host
110
+ request.fetch('HTTP_HOST', '')
111
+ end
112
+ end
113
+ end
114
+ end