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,12 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'drillbit/authorizers/parameters/resource'
4
+
5
+ module Drillbit
6
+ module Authorizers
7
+ class Parameters
8
+ RSpec.describe Resource do
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'drillbit/authorizers/parameters'
4
+
5
+ module Drillbit
6
+ module Authorizers
7
+ RSpec.describe Parameters do
8
+ it 'defaults to nothing' do
9
+ parameters = Parameters.new(token: '123',
10
+ user: 'my_user',
11
+ params: { foo: 'bar' })
12
+
13
+ expect(parameters.call).to eql(foo: 'bar')
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'drillbit/authorizers/query'
4
+
5
+ module Drillbit
6
+ module Authorizers
7
+ RSpec.describe Query do
8
+ it 'does not authorize the resource by default' do
9
+ authorizer = Query.new(token: '123',
10
+ user: 'my_user',
11
+ resource: 'my_resource')
12
+
13
+ expect(authorizer).to be_able_to_index
14
+ expect(authorizer).not_to be_able_to_show
15
+ expect(authorizer).not_to be_able_to_create
16
+ expect(authorizer).not_to be_able_to_update
17
+ expect(authorizer).not_to be_able_to_destroy
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ require 'rspeckled'
3
+ require 'ostruct'
4
+ require 'drillbit/authorizers/scope'
5
+
6
+ module Drillbit
7
+ module Authorizers
8
+ RSpec.describe Scope do
9
+ it 'defaults to nothing' do
10
+ scope = Scope.new(token: '123',
11
+ user: 'my_user',
12
+ scoped_user_id: '456',
13
+ params: {},
14
+ scope_root: OpenStruct.new(none: []))
15
+
16
+ expect(scope.call).to be_empty
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'drillbit/errors/invalid_api_request'
4
+
5
+ module Drillbit
6
+ module Errors
7
+ RSpec.describe InvalidApiRequest do
8
+ let(:error) { InvalidApiRequest.new }
9
+
10
+ it 'has a status of 400' do
11
+ expect(error.http_status).to eql 400
12
+ end
13
+
14
+ it 'has a code' do
15
+ expect(error.code).to eql 'errors.invalid_api_request'
16
+ end
17
+
18
+ it 'can output the detail' do
19
+ expect(error.detail).to eql 'The accept header that you passed in the ' \
20
+ 'request cannot be parsed, please refer to ' \
21
+ 'the documentation to verify.'
22
+ end
23
+
24
+ it 'can output the source' do
25
+ error = InvalidApiRequest.new accept_header: 'foo'
26
+
27
+ expect(error.source).to eql(accept_header: 'foo')
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'drillbit/errors/invalid_subdomain'
4
+
5
+ module Drillbit
6
+ module Errors
7
+ RSpec.describe InvalidSubdomain do
8
+ let(:error) { InvalidSubdomain.new }
9
+
10
+ it 'has a status of 404' do
11
+ expect(error.http_status).to eql 404
12
+ end
13
+
14
+ it 'has a code' do
15
+ expect(error.code).to eql 'errors.invalid_subdomain'
16
+ end
17
+
18
+ it 'can output the detail' do
19
+ expect(error.detail).to eql \
20
+ 'The resource you attempted to access is either not authorized for the ' \
21
+ 'authenticated user or does not exist.'
22
+ end
23
+
24
+ it 'can output the source' do
25
+ error = InvalidSubdomain.new http_host: 'foo'
26
+
27
+ expect(error.source).to eql(http_host: 'foo')
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'drillbit/errors/invalid_token'
4
+
5
+ module Drillbit
6
+ module Errors
7
+ RSpec.describe InvalidToken do
8
+ let(:error) { InvalidToken.new }
9
+
10
+ it 'has a status of 401' do
11
+ expect(error.http_status).to eql 401
12
+ end
13
+
14
+ it 'has a code' do
15
+ expect(error.code).to eql 'errors.invalid_token'
16
+ end
17
+
18
+ it 'can output the detail' do
19
+ expect(error.detail).to eql \
20
+ 'Either the token you passed is invalid or is not allowed to perform this action.'
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'drillbit/responses/invalid_subdomain'
4
+
5
+ # rubocop:disable Metrics/LineLength
6
+ module Drillbit
7
+ module Responses
8
+ RSpec.describe InvalidSubdomain, singletons: Erratum::Configuration do
9
+ it 'returns the proper response' do
10
+ Erratum.configuration.url_mappings = {
11
+ 'external_documentation_urls' => {
12
+ 'errors.responses.invalid_subdomain' => 'http://example.com/foo',
13
+ },
14
+ 'developer_documentation_urls' => {
15
+ 'errors.responses.invalid_subdomain' => 'http://example.com/foo',
16
+ },
17
+ }
18
+
19
+ request = { 'HTTP_HOST' => 'api.example.com' }
20
+ status, headers, response = InvalidSubdomain.call(request)
21
+
22
+ expect(status).to eql 404
23
+ expect(headers).to eql({})
24
+ expect(JSON.load(response[0])).to include(
25
+ 'errors' => [
26
+ include(
27
+ 'id' => match(/[a-f0-9\-]+/),
28
+ 'links' => {
29
+ 'about' => nil,
30
+ 'documentation' => nil,
31
+ },
32
+ 'status' => 404,
33
+ 'code' => 'errors.invalid_subdomain',
34
+ 'title' => 'Invalid Subdomain',
35
+ 'detail' => 'The resource you attempted to access is either not authorized ' \
36
+ 'for the authenticated user or does not exist.',
37
+ 'source' => {
38
+ 'http_host' => 'api.example.com',
39
+ },
40
+ ),
41
+ ],
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'drillbit/responses/invalid_token'
4
+
5
+ # rubocop:disable Metrics/LineLength
6
+ module Drillbit
7
+ module Responses
8
+ RSpec.describe InvalidToken, singletons: Erratum::Configuration do
9
+ it 'returns the proper response' do
10
+ Erratum.configuration.url_mappings = {
11
+ 'external_documentation_urls' => {
12
+ 'errors.responses.invalid_token' => 'http://example.com/foo',
13
+ },
14
+ 'developer_documentation_urls' => {
15
+ 'errors.responses.invalid_token' => 'http://example.com/foo',
16
+ },
17
+ }
18
+
19
+ request = { 'HTTP_HOST' => 'api.example.com' }
20
+ status, headers, response = InvalidToken.call(request, application_name: 'my_app')
21
+
22
+ expect(status).to eql 401
23
+ expect(headers).to eql('WWW-Authenticate' => 'Token realm="my_app"')
24
+ expect(JSON.load(response[0])).to include(
25
+ 'errors' => [
26
+ include(
27
+ 'id' => match(/[a-f0-9\-]+/),
28
+ 'links' => {
29
+ 'about' => nil,
30
+ 'documentation' => nil,
31
+ },
32
+ 'status' => 401,
33
+ 'code' => 'errors.invalid_token',
34
+ 'title' => 'Invalid or Unauthorized Token',
35
+ 'detail' => 'Either the token you passed is invalid or is not allowed to ' \
36
+ 'perform this action.',
37
+ 'source' => {},
38
+ ),
39
+ ],
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'drillbit/requests/base'
4
+ require 'drillbit/matchers/accept_header'
5
+
6
+ # rubocop:disable Metrics/LineLength
7
+ module Drillbit
8
+ module Matchers
9
+ RSpec.describe AcceptHeader do
10
+ it 'matches if the subdomain is API and the accept header is valid' do
11
+ env = {
12
+ 'HTTP_ACCEPT' => 'application/vnd.westeros+redkeep;version=1.0.0',
13
+ 'HTTP_X_APPLICATION_NAME' => 'westeros',
14
+ }
15
+ request = Requests::Base.resolve(env)
16
+
17
+ matcher = AcceptHeader.new
18
+
19
+ expect(matcher.matches?(request)).to be_a TrueClass
20
+ end
21
+
22
+ it 'matches if the subdomain is API and the accept header is passed in as ' \
23
+ 'a parameter' do
24
+
25
+ env = {
26
+ 'QUERY_STRING' => 'accept=application/vnd.westeros+redkeep;version=1.0.0',
27
+ 'HTTP_X_APPLICATION_NAME' => 'westeros',
28
+ }
29
+ request = Requests::Base.resolve(env)
30
+
31
+ matcher = AcceptHeader.new
32
+
33
+ expect(matcher.matches?(request)).to be_a TrueClass
34
+ end
35
+
36
+ it 'matches if the subdomain is API and the accept header is passed in as a ' \
37
+ 'secondary parameter' do
38
+
39
+ env = {
40
+ 'QUERY_STRING' => 'first=my_param&accept=application/vnd.westeros+redkeep;' \
41
+ 'version=1.0.0',
42
+ 'HTTP_X_APPLICATION_NAME' => 'westeros',
43
+ }
44
+ request = Requests::Base.resolve(env)
45
+
46
+ matcher = AcceptHeader.new
47
+
48
+ expect(matcher.matches?(request)).to be_a TrueClass
49
+ end
50
+
51
+ it 'matches the header accept header if the subdomain is API and the accept header ' \
52
+ 'is passed both as a valid header and as a parameter' do
53
+
54
+ env = {
55
+ 'HTTP_ACCEPT' => 'application/vnd.westeros+redkeep;version=1.0.0',
56
+ 'QUERY_STRING' => 'accept=application/vnd.westeros+redkeep;version=2.0.0',
57
+ 'HTTP_X_APPLICATION_NAME' => 'westeros',
58
+ }
59
+ request = Requests::Base.resolve(env)
60
+
61
+ matcher = AcceptHeader.new
62
+ matcher.matches?(request)
63
+
64
+ expect(matcher.accept_header.version).to eql '1.0.0'
65
+ end
66
+
67
+ it 'matches the accept header parameter if the subdomain is API and the accept ' \
68
+ 'header is passed both as an invalid header as well as as a parameter' do
69
+
70
+ env = {
71
+ 'HTTP_ACCEPT' => 'application/vndwesteros+redkeep;version=1.0.0',
72
+ 'QUERY_STRING' => 'accept=application/vnd.westeros+redkeep;version=2.0.0',
73
+ 'HTTP_X_APPLICATION_NAME' => 'westeros',
74
+ }
75
+ request = Requests::Base.resolve(env)
76
+
77
+ matcher = AcceptHeader.new
78
+ matcher.matches?(request)
79
+
80
+ expect(matcher.accept_header.version).to eql '2.0.0'
81
+ end
82
+
83
+ it 'matches the accept header parameter if the subdomain is API and the accept ' \
84
+ 'header is passed both as an invalid header as well as as a parameter' do
85
+
86
+ env = {
87
+ 'HTTP_ACCEPT' => 'application/vndwesteros+redkeep;version=1.0.0',
88
+ 'QUERY_STRING' => '',
89
+ 'HTTP_X_APPLICATION_NAME' => 'westeros',
90
+ }
91
+ request = Requests::Base.resolve(env)
92
+
93
+ matcher = AcceptHeader.new
94
+ matcher.matches?(request)
95
+
96
+ expect(matcher.accept_header.raw_accept_header).to eql \
97
+ 'application/vndwesteros+redkeep;version=1.0.0'
98
+ end
99
+
100
+ it 'does not match if the subdomain is API but the accept header is invalid' do
101
+ env = {
102
+ 'HTTP_ACCEPT' => 'application/vndwesteros+redkeep;version=1.0.0',
103
+ 'QUERY_STRING' => '',
104
+ 'HTTP_X_APPLICATION_NAME' => 'westeros',
105
+ }
106
+ request = Requests::Base.resolve(env)
107
+
108
+ matcher = AcceptHeader.new
109
+
110
+ expect(matcher.matches?(request)).to be_a FalseClass
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'drillbit/requests/base'
4
+ require 'drillbit/matchers/subdomain'
5
+ require 'drillbit/configuration'
6
+
7
+ module Drillbit
8
+ module Matchers
9
+ RSpec.describe Subdomain do
10
+ before(:each) do
11
+ Drillbit.configuration.allowed_subdomains = %w{api}
12
+ Drillbit.configuration.allowed_api_subdomains = %w{api}
13
+ end
14
+
15
+ it 'matches if the subdomain is API' do
16
+ env = { 'HTTP_HOST' => 'api.example.com' }
17
+ request = Requests::Base.resolve(env)
18
+ matcher = Subdomain.new
19
+
20
+ expect(matcher.matches?(request)).to be_a TrueClass
21
+ end
22
+
23
+ it 'matches if the first subdomain is API' do
24
+ env = { 'HTTP_HOST' => 'api.westeros.example.com' }
25
+ request = Requests::Base.resolve(env)
26
+ matcher = Subdomain.new
27
+
28
+ expect(matcher.matches?(request)).to be_a TrueClass
29
+ end
30
+
31
+ it 'does not match if the first subdomain is not API' do
32
+ env = { 'HTTP_HOST' => 'westeros.example.com' }
33
+ request = Requests::Base.resolve(env)
34
+ matcher = Subdomain.new
35
+
36
+ expect(matcher.matches?(request)).to be_a FalseClass
37
+ end
38
+
39
+ it 'allows the matched subdomain to be specified' do
40
+ env = { 'HTTP_HOST' => 'westeros.example.com' }
41
+ request = Requests::Base.resolve(env)
42
+ matcher = Subdomain.new(allowed_subdomains: 'westeros')
43
+
44
+ expect(matcher.matches?(request)).to be_a TrueClass
45
+ end
46
+
47
+ it 'allows more than one subdomain to be matched' do
48
+ env = { 'HTTP_HOST' => 'westeros.example.com' }
49
+ request = Requests::Base.resolve(env)
50
+ matcher = Subdomain.new(allowed_subdomains: %w{api westeros})
51
+
52
+ expect(matcher.matches?(request)).to be_a TrueClass
53
+
54
+ env = { 'HTTP_HOST' => 'api.example.com' }
55
+ request = Requests::Base.resolve(env)
56
+ matcher = Subdomain.new(allowed_subdomains: %w{api westeros})
57
+
58
+ expect(matcher.matches?(request)).to be_a TrueClass
59
+ end
60
+
61
+ it 'can match only the api subdomain' do
62
+ env = { 'HTTP_HOST' => 'westeros.example.com' }
63
+ request = Requests::Base.resolve(env)
64
+ matcher = Subdomain.new(allowed_api_subdomains: %w{westeros})
65
+
66
+ expect(matcher.matches_api_subdomain?(request)).to be_a TrueClass
67
+ end
68
+
69
+ it 'matches "api" as an api subdomain by default' do
70
+ env = { 'HTTP_HOST' => 'api.example.com' }
71
+ request = Requests::Base.resolve(env)
72
+ matcher = Subdomain.new
73
+
74
+ expect(matcher.matches_api_subdomain?(request)).to be_a TrueClass
75
+ end
76
+ end
77
+ end
78
+ end