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