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