drillbit 0.0.1

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