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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/LICENSE.txt +19 -0
- data/README.md +2 -0
- data/Rakefile +2 -0
- data/lib/drillbit.rb +19 -0
- data/lib/drillbit/accept_header.rb +50 -0
- data/lib/drillbit/authorizable_resource.rb +160 -0
- data/lib/drillbit/authorizers/parameters.rb +24 -0
- data/lib/drillbit/authorizers/parameters/filtering.rb +50 -0
- data/lib/drillbit/authorizers/parameters/resource.rb +11 -0
- data/lib/drillbit/authorizers/query.rb +40 -0
- data/lib/drillbit/authorizers/scope.rb +30 -0
- data/lib/drillbit/configuration.rb +36 -0
- data/lib/drillbit/errors/invalid_api_request.rb +29 -0
- data/lib/drillbit/errors/invalid_subdomain.rb +29 -0
- data/lib/drillbit/errors/invalid_token.rb +22 -0
- data/lib/drillbit/matchers/accept_header.rb +16 -0
- data/lib/drillbit/matchers/generic.rb +30 -0
- data/lib/drillbit/matchers/subdomain.rb +31 -0
- data/lib/drillbit/matchers/version.rb +30 -0
- data/lib/drillbit/middleware/api_request.rb +49 -0
- data/lib/drillbit/parameters.rb +22 -0
- data/lib/drillbit/parameters/filter.rb +57 -0
- data/lib/drillbit/parameters/index.rb +31 -0
- data/lib/drillbit/parameters/page.rb +28 -0
- data/lib/drillbit/parameters/sort.rb +32 -0
- data/lib/drillbit/requests/base.rb +114 -0
- data/lib/drillbit/requests/rack.rb +50 -0
- data/lib/drillbit/requests/rails.rb +44 -0
- data/lib/drillbit/resource.rb +14 -0
- data/lib/drillbit/resource/model.rb +41 -0
- data/lib/drillbit/resource/naming.rb +33 -0
- data/lib/drillbit/resource/processors/filtering.rb +66 -0
- data/lib/drillbit/resource/processors/indexing.rb +40 -0
- data/lib/drillbit/resource/processors/paging.rb +46 -0
- data/lib/drillbit/resource/processors/sorting.rb +42 -0
- data/lib/drillbit/responses/invalid_api_request.rb +18 -0
- data/lib/drillbit/responses/invalid_subdomain.rb +18 -0
- data/lib/drillbit/responses/invalid_token.rb +20 -0
- data/lib/drillbit/serializers/json_api.rb +10 -0
- data/lib/drillbit/tokens/base64.rb +45 -0
- data/lib/drillbit/tokens/base64s/invalid.rb +14 -0
- data/lib/drillbit/tokens/base64s/null.rb +14 -0
- data/lib/drillbit/tokens/invalid.rb +26 -0
- data/lib/drillbit/tokens/json_web_token.rb +112 -0
- data/lib/drillbit/tokens/json_web_tokens/invalid.rb +14 -0
- data/lib/drillbit/tokens/json_web_tokens/null.rb +14 -0
- data/lib/drillbit/tokens/null.rb +26 -0
- data/lib/drillbit/version.rb +4 -0
- data/spec/drillbit/accept_header_spec.rb +112 -0
- data/spec/drillbit/authorizers/parameters/filtering_spec.rb +71 -0
- data/spec/drillbit/authorizers/parameters/resource_spec.rb +12 -0
- data/spec/drillbit/authorizers/parameters_spec.rb +17 -0
- data/spec/drillbit/authorizers/query_spec.rb +21 -0
- data/spec/drillbit/authorizers/scope_spec.rb +20 -0
- data/spec/drillbit/errors/invalid_api_request_spec.rb +31 -0
- data/spec/drillbit/errors/invalid_subdomain_spec.rb +31 -0
- data/spec/drillbit/errors/invalid_token_spec.rb +24 -0
- data/spec/drillbit/invalid_subdomain_spec.rb +46 -0
- data/spec/drillbit/invalid_token_spec.rb +44 -0
- data/spec/drillbit/matchers/accept_header_spec.rb +114 -0
- data/spec/drillbit/matchers/subdomain_spec.rb +78 -0
- data/spec/drillbit/matchers/version_spec.rb +86 -0
- data/spec/drillbit/middleware/api_request_spec.rb +220 -0
- data/spec/drillbit/parameters_spec.rb +49 -0
- data/spec/drillbit/requests/base_spec.rb +37 -0
- data/spec/drillbit/requests/rack_spec.rb +253 -0
- data/spec/drillbit/requests/rails_spec.rb +264 -0
- data/spec/drillbit/resource/model_spec.rb +64 -0
- data/spec/drillbit/resource/processors/filtering_spec.rb +106 -0
- data/spec/drillbit/resource/processors/indexing_spec.rb +46 -0
- data/spec/drillbit/resource/processors/paging_spec.rb +74 -0
- data/spec/drillbit/resource/processors/sorting_spec.rb +66 -0
- data/spec/drillbit/tokens/base64_spec.rb +44 -0
- data/spec/drillbit/tokens/json_web_token_spec.rb +135 -0
- data/spec/fixtures/test_rsa_key +27 -0
- data/spec/fixtures/test_rsa_key.pub +9 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/private_keys.rb +42 -0
- metadata +244 -0
- metadata.gz.sig +0 -0
@@ -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,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,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,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
|