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,10 @@
1
+ # frozen_string_literal: true
2
+ module Drillbit
3
+ module Serializers
4
+ module JsonApi
5
+ def json_api_type
6
+ object.class.name.demodulize.tableize.dasherize
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ require 'base64'
3
+ require 'drillbit/tokens/base64s/null'
4
+ require 'drillbit/tokens/base64s/invalid'
5
+
6
+ module Drillbit
7
+ module Tokens
8
+ class Base64
9
+ attr_accessor :token
10
+
11
+ def initialize(token:)
12
+ self.token = token
13
+ end
14
+
15
+ def valid?
16
+ true
17
+ end
18
+
19
+ def blank?
20
+ false
21
+ end
22
+
23
+ def to_h
24
+ [
25
+ {
26
+ 'token' => token,
27
+ },
28
+ {
29
+ 'typ' => 'base64',
30
+ },
31
+ ]
32
+ end
33
+
34
+ def self.convert(raw_token:)
35
+ return Base64s::Null.instance if raw_token.to_s == ''
36
+
37
+ ::Base64.strict_decode64(raw_token)
38
+
39
+ new(token: raw_token)
40
+ rescue ArgumentError
41
+ Base64s::Invalid.instance
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ require 'drillbit/tokens/invalid'
3
+
4
+ module Drillbit
5
+ module Tokens
6
+ module Base64s
7
+ class Invalid < Tokens::Invalid
8
+ def to_h
9
+ [{}, {}]
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ require 'drillbit/tokens/null'
3
+
4
+ module Drillbit
5
+ module Tokens
6
+ module Base64s
7
+ class Null < Tokens::Null
8
+ def to_h
9
+ [{}, {}]
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ require 'singleton'
3
+
4
+ module Drillbit
5
+ module Tokens
6
+ class Invalid
7
+ include Singleton
8
+
9
+ def valid?
10
+ false
11
+ end
12
+
13
+ def blank?
14
+ false
15
+ end
16
+
17
+ def to_h
18
+ {}
19
+ end
20
+
21
+ def to_s
22
+ ''
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+ require 'jwt'
3
+ require 'json/jwt'
4
+ require 'drillbit/tokens/json_web_tokens/invalid'
5
+ require 'drillbit/tokens/json_web_tokens/null'
6
+
7
+ module Drillbit
8
+ module Tokens
9
+ class JsonWebToken
10
+ TRANSFORMATION_EXCEPTIONS = [
11
+ JSON::JWT::Exception,
12
+ JSON::JWT::InvalidFormat,
13
+ JSON::JWT::VerificationFailed,
14
+ JSON::JWT::UnexpectedAlgorithm,
15
+ JWT::DecodeError,
16
+ JWT::VerificationError,
17
+ JWT::ExpiredSignature,
18
+ JWT::IncorrectAlgorithm,
19
+ JWT::ImmatureSignature,
20
+ JWT::InvalidIssuerError,
21
+ JWT::InvalidIatError,
22
+ JWT::InvalidAudError,
23
+ JWT::InvalidSubError,
24
+ JWT::InvalidJtiError,
25
+ OpenSSL::PKey::RSAError,
26
+ OpenSSL::Cipher::CipherError,
27
+ ].freeze
28
+
29
+ attr_accessor :data,
30
+ :private_key
31
+
32
+ def initialize(data:,
33
+ private_key: Drillbit.configuration.token_private_key)
34
+
35
+ self.data = data
36
+ self.private_key = private_key
37
+ end
38
+
39
+ def valid?
40
+ true
41
+ end
42
+
43
+ def blank?
44
+ false
45
+ end
46
+
47
+ def to_h
48
+ data
49
+ end
50
+
51
+ def to_jwt
52
+ @jwt ||= JSON::JWT.new(data)
53
+ end
54
+
55
+ def to_jwt_s
56
+ @jwt_s ||= to_jwt.to_s
57
+ end
58
+
59
+ def to_jws
60
+ @jws ||= to_jwt.sign(private_key, 'RS256')
61
+ end
62
+
63
+ def to_jws_s
64
+ @jws_s ||= to_jws.to_s
65
+ end
66
+
67
+ def to_jwe
68
+ @jwe ||= to_jws.encrypt(private_key, 'RSA-OAEP', 'A256GCM')
69
+ end
70
+
71
+ def to_jwe_s
72
+ @jwe_s ||= to_jwe.to_s
73
+ end
74
+
75
+ def self.from_jwe(encrypted_token,
76
+ private_key: Drillbit.configuration.token_private_key)
77
+
78
+ return JsonWebTokens::Null.instance if encrypted_token.to_s == ''
79
+
80
+ decrypted_token = JSON::JWT.
81
+ decode(encrypted_token, private_key).
82
+ plain_text
83
+
84
+ from_jws(decrypted_token, private_key: private_key)
85
+ rescue *TRANSFORMATION_EXCEPTIONS
86
+ JsonWebTokens::Invalid.instance
87
+ end
88
+
89
+ def self.from_jws(signed_token,
90
+ private_key: Drillbit.configuration.token_private_key)
91
+
92
+ return JsonWebTokens::Null.instance if signed_token.to_s == ''
93
+
94
+ data = JWT.decode(
95
+ signed_token,
96
+ private_key,
97
+ true,
98
+ algorithm: 'RS256',
99
+ verify_expiration: true,
100
+ verify_not_before: true,
101
+ verify_iat: true,
102
+ leeway: 5,
103
+ )
104
+
105
+ new(data: data,
106
+ private_key: private_key)
107
+ rescue *TRANSFORMATION_EXCEPTIONS
108
+ JsonWebTokens::Invalid.instance
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ require 'drillbit/tokens/invalid'
3
+
4
+ module Drillbit
5
+ module Tokens
6
+ module JsonWebTokens
7
+ class Invalid < Tokens::Invalid
8
+ def to_h
9
+ [{}, {}]
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ require 'drillbit/tokens/null'
3
+
4
+ module Drillbit
5
+ module Tokens
6
+ module JsonWebTokens
7
+ class Null < Tokens::Null
8
+ def to_h
9
+ [{}, {}]
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ require 'singleton'
3
+
4
+ module Drillbit
5
+ module Tokens
6
+ class Null
7
+ include Singleton
8
+
9
+ def valid?
10
+ true
11
+ end
12
+
13
+ def blank?
14
+ true
15
+ end
16
+
17
+ def to_h
18
+ [{}, {}]
19
+ end
20
+
21
+ def to_s
22
+ ''
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Drillbit
3
+ VERSION = '0.0.1'
4
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'drillbit/accept_header'
4
+
5
+ # rubocop:disable Metrics/LineLength
6
+ module Drillbit
7
+ RSpec.describe AcceptHeader do
8
+ it 'can validate an accept header with all the pieces of information' do
9
+ header = AcceptHeader.new(application: 'westeros',
10
+ header: 'application/vnd.westeros+redkeep;version=1.0.0')
11
+
12
+ expect(header).to be_valid
13
+ end
14
+
15
+ it 'does not validate an accept header without passing an application' do
16
+ header = AcceptHeader.new(application: '',
17
+ header: 'application/vnd.westeros+redkeep;version=1.0.0')
18
+
19
+ expect(header).not_to be_valid
20
+ end
21
+
22
+ it 'does not validate an accept header if it is not passed in' do
23
+ header = AcceptHeader.new(application: '',
24
+ header: '')
25
+
26
+ expect(header).not_to be_valid
27
+
28
+ header = AcceptHeader.new(application: '',
29
+ header: nil)
30
+
31
+ expect(header).not_to be_valid
32
+ end
33
+
34
+ it 'does not validate an accept header without an application in the header' do
35
+ header = AcceptHeader.new(application: 'westeros',
36
+ header: 'application/vnd.+redkeep;version=1.0.0')
37
+
38
+ expect(header).not_to be_valid
39
+
40
+ header = AcceptHeader.new(application: 'westeros',
41
+ header: 'application/+redkeep;version=1.0.0')
42
+
43
+ expect(header).not_to be_valid
44
+
45
+ header = AcceptHeader.new(application: 'westeros',
46
+ header: 'application/westeros+redkeep;version=1.0.0')
47
+
48
+ expect(header).not_to be_valid
49
+ end
50
+
51
+ it 'does not validate an accept header with an invalid version' do
52
+ header = AcceptHeader.new(application: 'westeros',
53
+ header: 'application/vnd.westeros+redkeep;version=10..0')
54
+
55
+ expect(header).not_to be_valid
56
+
57
+ header = AcceptHeader.new(application: 'westeros',
58
+ header: 'application/vnd.westeros+redkeep;version=neo')
59
+
60
+ expect(header).not_to be_valid
61
+
62
+ header = AcceptHeader.new(application: 'westeros',
63
+ header: 'application/vnd.westeros+redkeep;version=')
64
+
65
+ expect(header).not_to be_valid
66
+
67
+ header = AcceptHeader.new(application: 'westeros',
68
+ header: 'application/vnd.westeros+redkeep;10.0')
69
+
70
+ expect(header).not_to be_valid
71
+ end
72
+
73
+ it 'does validate an accept header even with a missing content type' do
74
+ header = AcceptHeader.new(application: 'westeros',
75
+ header: 'application/vnd.westeros;version=10.0')
76
+
77
+ expect(header).to be_valid
78
+ end
79
+
80
+ it 'does validate an accept header with only the minimal information' do
81
+ header = AcceptHeader.new(application: 'westeros',
82
+ header: 'application/vnd.westeros')
83
+
84
+ expect(header).to be_valid
85
+ end
86
+
87
+ it 'does validate an accept header with only a content type but no version' do
88
+ header = AcceptHeader.new(application: 'westeros',
89
+ header: 'application/vnd.westeros+redkeep')
90
+
91
+ expect(header).to be_valid
92
+ end
93
+
94
+ it 'can extract version information from an accept header' do
95
+ header = AcceptHeader.new(
96
+ application: 'westeros',
97
+ header: 'application/vnd.westeros+redkeep;version=10.0.0beta1',
98
+ )
99
+
100
+ expect(header.version).to eql '10.0.0beta1'
101
+ end
102
+
103
+ it 'can extract the content type from an accept header' do
104
+ header = AcceptHeader.new(
105
+ application: 'westeros',
106
+ header: 'application/vnd.westeros+redkeep;version=10.0.0beta1',
107
+ )
108
+
109
+ expect(header.content_type).to eql 'redkeep'
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'drillbit/authorizers/parameters/filtering'
4
+
5
+ module Drillbit
6
+ module Authorizers
7
+ class Parameters
8
+ RSpec.describe Filtering do
9
+ let(:params) { { filter: { name: 'Bill', age: 26 } } }
10
+
11
+ it 'can authorize new filter parameters', verify: false do
12
+ filter_params = Filtering.new(token: '1234',
13
+ user: '1234',
14
+ params: params)
15
+
16
+ allow(params).to receive(:permit)
17
+
18
+ filter_params.send(:add_filterable_parameters, :name, :age)
19
+ filter_params.call
20
+
21
+ expect(params).to have_received(:permit).
22
+ with(:sort, include(filter: include(:name, :age)))
23
+ end
24
+
25
+ it 'can authorize parameters if they come in as arrays', verify: false do
26
+ params = {
27
+ filter: {
28
+ name: 'Bill',
29
+ ary: %w{hello},
30
+ },
31
+ }
32
+ filter_params = Filtering.new(token: '1234',
33
+ user: '1234',
34
+ params: params)
35
+
36
+ allow(params).to receive(:permit)
37
+
38
+ filter_params.send(:add_filterable_parameters, :name, :ary)
39
+ filter_params.call
40
+
41
+ expect(params).to have_received(:permit).
42
+ with(:sort, include(filter: include(:name, ary: [])))
43
+ end
44
+
45
+ it 'has default authorized parameters', verify: false do
46
+ filter_params = Filtering.new(token: '1234',
47
+ user: '1234',
48
+ params: params)
49
+
50
+ allow(params).to receive(:permit)
51
+
52
+ filter_params.call
53
+
54
+ expect(params).to have_received(:permit).
55
+ with(:sort,
56
+ page: %i{
57
+ number
58
+ size
59
+ offset
60
+ limit
61
+ cursor
62
+ },
63
+ filter: [
64
+ :query,
65
+ {},
66
+ ])
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end