lhc 13.0.0 → 15.0.0

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubocop.yml +15 -0
  3. data/.github/workflows/test.yml +15 -0
  4. data/.rubocop.yml +344 -19
  5. data/.ruby-version +1 -1
  6. data/README.md +89 -0
  7. data/Rakefile +3 -3
  8. data/lhc.gemspec +3 -1
  9. data/lib/lhc.rb +70 -59
  10. data/lib/lhc/concerns/lhc/fix_invalid_encoding_concern.rb +1 -0
  11. data/lib/lhc/config.rb +16 -0
  12. data/lib/lhc/endpoint.rb +3 -0
  13. data/lib/lhc/error.rb +7 -4
  14. data/lib/lhc/interceptor.rb +4 -0
  15. data/lib/lhc/interceptors.rb +1 -0
  16. data/lib/lhc/interceptors/auth.rb +10 -5
  17. data/lib/lhc/interceptors/caching.rb +14 -3
  18. data/lib/lhc/interceptors/logging.rb +4 -2
  19. data/lib/lhc/interceptors/monitoring.rb +46 -11
  20. data/lib/lhc/interceptors/retry.rb +2 -0
  21. data/lib/lhc/interceptors/rollbar.rb +3 -2
  22. data/lib/lhc/interceptors/throttle.rb +7 -2
  23. data/lib/lhc/interceptors/zipkin.rb +2 -0
  24. data/lib/lhc/request.rb +37 -4
  25. data/lib/lhc/response.rb +1 -0
  26. data/lib/lhc/response/data.rb +1 -1
  27. data/lib/lhc/scrubber.rb +45 -0
  28. data/lib/lhc/scrubbers/auth_scrubber.rb +33 -0
  29. data/lib/lhc/scrubbers/body_scrubber.rb +28 -0
  30. data/lib/lhc/scrubbers/headers_scrubber.rb +38 -0
  31. data/lib/lhc/scrubbers/params_scrubber.rb +14 -0
  32. data/lib/lhc/version.rb +1 -1
  33. data/spec/config/scrubs_spec.rb +108 -0
  34. data/spec/error/to_s_spec.rb +13 -8
  35. data/spec/formats/multipart_spec.rb +2 -2
  36. data/spec/formats/plain_spec.rb +1 -1
  37. data/spec/interceptors/after_response_spec.rb +1 -1
  38. data/spec/interceptors/caching/main_spec.rb +2 -2
  39. data/spec/interceptors/caching/multilevel_cache_spec.rb +2 -1
  40. data/spec/interceptors/define_spec.rb +1 -0
  41. data/spec/interceptors/logging/main_spec.rb +21 -1
  42. data/spec/interceptors/monitoring/caching_spec.rb +66 -0
  43. data/spec/interceptors/response_competition_spec.rb +2 -2
  44. data/spec/interceptors/return_response_spec.rb +2 -2
  45. data/spec/interceptors/rollbar/main_spec.rb +27 -15
  46. data/spec/request/scrubbed_headers_spec.rb +101 -0
  47. data/spec/request/scrubbed_options_spec.rb +194 -0
  48. data/spec/request/scrubbed_params_spec.rb +35 -0
  49. data/spec/response/data_spec.rb +2 -2
  50. data/spec/support/zipkin_mock.rb +1 -0
  51. metadata +40 -21
  52. data/.rubocop.localch.yml +0 -325
  53. data/cider-ci.yml +0 -5
  54. data/cider-ci/bin/bundle +0 -51
  55. data/cider-ci/bin/ruby_install +0 -8
  56. data/cider-ci/bin/ruby_version +0 -25
  57. data/cider-ci/jobs/rspec-activesupport-5.yml +0 -27
  58. data/cider-ci/jobs/rspec-activesupport-6.yml +0 -28
  59. data/cider-ci/jobs/rubocop.yml +0 -18
  60. data/cider-ci/task_components/bundle.yml +0 -22
  61. data/cider-ci/task_components/rspec.yml +0 -36
  62. data/cider-ci/task_components/rubocop.yml +0 -29
  63. data/cider-ci/task_components/ruby.yml +0 -15
data/lib/lhc/response.rb CHANGED
@@ -50,6 +50,7 @@ class LHC::Response
50
50
 
51
51
  def format
52
52
  return LHC::Formats::JSON.new if request.nil?
53
+
53
54
  request.format
54
55
  end
55
56
 
@@ -18,7 +18,7 @@ class LHC::Response::Data
18
18
  end
19
19
  end
20
20
 
21
- def method_missing(method, *args, &block) # rubocop:disable Style/MethodMissingSuper
21
+ def method_missing(method, *args, &block)
22
22
  @base.send(method, *args, &block)
23
23
  end
24
24
 
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LHC::Scrubber
4
+ attr_accessor :scrubbed
5
+
6
+ SCRUB_DISPLAY = '[FILTERED]'
7
+
8
+ def initialize(data)
9
+ @scrubbed = data
10
+ end
11
+
12
+ private
13
+
14
+ def scrub_auth_elements
15
+ LHC.config.scrubs.dig(:auth)
16
+ end
17
+
18
+ def scrub!
19
+ return if scrub_elements.blank?
20
+ return if scrubbed.blank?
21
+
22
+ LHC::Scrubber.scrub_hash!(scrub_elements, scrubbed) if scrubbed.is_a?(Hash)
23
+ LHC::Scrubber.scrub_array!(scrub_elements, scrubbed) if scrubbed.is_a?(Array)
24
+ end
25
+
26
+ def self.scrub_array!(scrub_elements, scrubbed)
27
+ scrubbed.each do |scrubbed_hash|
28
+ LHC::Scrubber.scrub_hash!(scrub_elements, scrubbed_hash)
29
+ end
30
+ end
31
+
32
+ def self.scrub_hash!(scrub_elements, scrubbed)
33
+ scrub_elements.each do |scrub_element|
34
+ if scrubbed.key?(scrub_element.to_s)
35
+ key = scrub_element.to_s
36
+ elsif scrubbed.key?(scrub_element.to_sym)
37
+ key = scrub_element.to_sym
38
+ end
39
+ next if key.blank? || scrubbed[key].blank?
40
+
41
+ scrubbed[key] = SCRUB_DISPLAY
42
+ end
43
+ scrubbed.values.each { |v| LHC::Scrubber.scrub_hash!(scrub_elements, v) if v.instance_of?(Hash) }
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LHC::AuthScrubber < LHC::Scrubber
4
+ def initialize(data)
5
+ super(data)
6
+ scrub_auth_options!
7
+ end
8
+
9
+ private
10
+
11
+ def scrub_auth_options!
12
+ return if scrubbed.blank?
13
+ return if scrub_auth_elements.blank?
14
+
15
+ scrub_basic_auth_options! if scrub_auth_elements.include?(:basic)
16
+ scrub_bearer_auth_options! if scrub_auth_elements.include?(:bearer)
17
+ end
18
+
19
+ def scrub_basic_auth_options!
20
+ return if scrubbed[:basic].blank?
21
+
22
+ scrubbed[:basic][:username] = SCRUB_DISPLAY
23
+ scrubbed[:basic][:password] = SCRUB_DISPLAY
24
+ scrubbed[:basic][:base_64_encoded_credentials] = SCRUB_DISPLAY
25
+ end
26
+
27
+ def scrub_bearer_auth_options!
28
+ return if scrubbed[:bearer].blank?
29
+
30
+ scrubbed[:bearer] = SCRUB_DISPLAY if scrubbed[:bearer].is_a?(String)
31
+ scrubbed[:bearer_token] = SCRUB_DISPLAY
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LHC::BodyScrubber < LHC::Scrubber
4
+ def initialize(data)
5
+ super(data)
6
+ parse!
7
+ scrub!
8
+ end
9
+
10
+ private
11
+
12
+ def scrub_elements
13
+ LHC.config.scrubs[:body]
14
+ end
15
+
16
+ def parse!
17
+ return if scrubbed.nil? || scrubbed.is_a?(Hash) || scrubbed.is_a?(Array)
18
+
19
+ if scrubbed.is_a?(String)
20
+ json = scrubbed
21
+ else
22
+ json = scrubbed.to_json
23
+ end
24
+
25
+ parsed = JSON.parse(json)
26
+ self.scrubbed = parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LHC::HeadersScrubber < LHC::Scrubber
4
+ def initialize(data, auth_options)
5
+ super(data)
6
+ @auth_options = auth_options
7
+ scrub!
8
+ scrub_auth_headers!
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :auth_options
14
+
15
+ def scrub_elements
16
+ LHC.config.scrubs[:headers]
17
+ end
18
+
19
+ def scrub_auth_headers!
20
+ return if scrub_auth_elements.blank?
21
+ return if auth_options.blank?
22
+
23
+ scrub_basic_authentication_headers! if scrub_auth_elements.include?(:basic)
24
+ scrub_bearer_authentication_headers! if scrub_auth_elements.include?(:bearer)
25
+ end
26
+
27
+ def scrub_basic_authentication_headers!
28
+ return if auth_options[:basic].blank? || scrubbed['Authorization'].blank?
29
+
30
+ scrubbed['Authorization'].gsub!(auth_options[:basic][:base_64_encoded_credentials], SCRUB_DISPLAY)
31
+ end
32
+
33
+ def scrub_bearer_authentication_headers!
34
+ return if auth_options[:bearer].blank? || scrubbed['Authorization'].blank?
35
+
36
+ scrubbed['Authorization'].gsub!(auth_options[:bearer_token], SCRUB_DISPLAY)
37
+ end
38
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LHC::ParamsScrubber < LHC::Scrubber
4
+ def initialize(data)
5
+ super(data)
6
+ scrub!
7
+ end
8
+
9
+ private
10
+
11
+ def scrub_elements
12
+ LHC.config.scrubs[:params]
13
+ end
14
+ end
data/lib/lhc/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LHC
4
- VERSION ||= '13.0.0'
4
+ VERSION ||= '15.0.0'
5
5
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ describe LHC do
6
+ it 'has a default value for scrubs' do
7
+ expect(LHC.config.scrubs[:auth]).to eq [:bearer, :basic]
8
+ expect(LHC.config.scrubs[:params]).to eq []
9
+ expect(LHC.config.scrubs[:headers]).to eq []
10
+ expect(LHC.config.scrubs[:body]).to eq ['password', 'password_confirmation']
11
+ end
12
+
13
+ describe 'auth' do
14
+ context 'when only bearer auth should get scrubbed' do
15
+ before(:each) do
16
+ LHC.configure do |c|
17
+ c.scrubs[:auth] = [:bearer]
18
+ end
19
+ end
20
+
21
+ it 'has only bearer auth in scrubs' do
22
+ expect(LHC.config.scrubs[:auth]).to eq([:bearer])
23
+ expect(LHC.config.scrubs[:params]).to eq []
24
+ expect(LHC.config.scrubs[:headers]).to eq []
25
+ expect(LHC.config.scrubs[:body]).to eq ['password', 'password_confirmation']
26
+ end
27
+ end
28
+ end
29
+
30
+ context 'params' do
31
+ context 'when additional param "api_key" should be scrubbed' do
32
+ before(:each) do
33
+ LHC.configure do |c|
34
+ c.scrubs[:params] << 'api_key'
35
+ end
36
+ end
37
+
38
+ it 'has "api_key" in scrubs' do
39
+ expect(LHC.config.scrubs[:auth]).to eq [:bearer, :basic]
40
+ expect(LHC.config.scrubs[:params]).to eq ['api_key']
41
+ expect(LHC.config.scrubs[:headers]).to eq []
42
+ expect(LHC.config.scrubs[:body]).to eq ['password', 'password_confirmation']
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'headers' do
48
+ context 'when additional header "private_key" should be scrubbed' do
49
+ before(:each) do
50
+ LHC.configure do |c|
51
+ c.scrubs[:headers] << 'private_key'
52
+ end
53
+ end
54
+
55
+ it 'has "private_key" in scrubs' do
56
+ expect(LHC.config.scrubs[:auth]).to eq [:bearer, :basic]
57
+ expect(LHC.config.scrubs[:params]).to eq []
58
+ expect(LHC.config.scrubs[:headers]).to eq ['private_key']
59
+ expect(LHC.config.scrubs[:body]).to eq ['password', 'password_confirmation']
60
+ end
61
+ end
62
+ end
63
+
64
+ context 'body' do
65
+ context 'when only password should get scrubbed' do
66
+ before(:each) do
67
+ LHC.configure do |c|
68
+ c.scrubs[:body] = ['password']
69
+ end
70
+ end
71
+
72
+ it 'has password in scrubs' do
73
+ expect(LHC.config.scrubs[:auth]).to eq [:bearer, :basic]
74
+ expect(LHC.config.scrubs[:params]).to eq []
75
+ expect(LHC.config.scrubs[:headers]).to eq []
76
+ expect(LHC.config.scrubs[:body]).to eq(['password'])
77
+ end
78
+ end
79
+
80
+ context 'when "user_token" should be scrubbed' do
81
+ before(:each) do
82
+ LHC.configure do |c|
83
+ c.scrubs[:body] << 'user_token'
84
+ end
85
+ end
86
+
87
+ it 'has user_token in scrubs' do
88
+ expect(LHC.config.scrubs[:auth]).to eq [:bearer, :basic]
89
+ expect(LHC.config.scrubs[:params]).to eq []
90
+ expect(LHC.config.scrubs[:headers]).to eq []
91
+ expect(LHC.config.scrubs[:body]).to eq(['password', 'password_confirmation', 'user_token'])
92
+ end
93
+ end
94
+ end
95
+
96
+ context 'when nothing should be scrubbed' do
97
+ before(:each) do
98
+ LHC.configure do |c|
99
+ c.scrubs = {}
100
+ end
101
+ end
102
+
103
+ it 'does not have scrubs' do
104
+ expect(LHC.config.scrubs.blank?).to be true
105
+ expect(LHC.config.scrubs[:auth]).to be nil
106
+ end
107
+ end
108
+ end
@@ -45,17 +45,17 @@ describe LHC::Error do
45
45
 
46
46
  context 'some mocked response' do
47
47
  let(:request) do
48
- double('request',
48
+ double('LHC::Request',
49
49
  method: 'GET',
50
50
  url: 'http://example.com/sessions',
51
- headers: { 'Bearer Token' => "aaaaaaaa-bbbb-cccc-dddd-eeee" },
52
- options: { followlocation: true,
53
- auth: { bearer: "aaaaaaaa-bbbb-cccc-dddd-eeee" },
54
- params: { limit: 20 }, url: "http://example.com/sessions" })
51
+ scrubbed_headers: { 'Bearer Token' => LHC::Scrubber::SCRUB_DISPLAY },
52
+ scrubbed_options: { followlocation: true,
53
+ auth: { bearer: LHC::Scrubber::SCRUB_DISPLAY },
54
+ params: { limit: 20 }, url: "http://example.com/sessions" })
55
55
  end
56
56
 
57
57
  let(:response) do
58
- double('response',
58
+ double('LHC::Response',
59
59
  request: request,
60
60
  code: 500,
61
61
  options: { return_code: :internal_error, response_headers: "" },
@@ -64,11 +64,16 @@ describe LHC::Error do
64
64
 
65
65
  subject { LHC::Error.new('The error message', response) }
66
66
 
67
+ before do
68
+ allow(request).to receive(:is_a?).with(LHC::Request).and_return(true)
69
+ allow(response).to receive(:is_a?).with(LHC::Response).and_return(true)
70
+ end
71
+
67
72
  it 'produces correct debug output' do
68
73
  expect(subject.to_s.split("\n")).to eq(<<-MSG.strip_heredoc.split("\n"))
69
74
  GET http://example.com/sessions
70
- Options: {:followlocation=>true, :auth=>{:bearer=>"aaaaaaaa-bbbb-cccc-dddd-eeee"}, :params=>{:limit=>20}, :url=>"http://example.com/sessions"}
71
- Headers: {"Bearer Token"=>"aaaaaaaa-bbbb-cccc-dddd-eeee"}
75
+ Options: {:followlocation=>true, :auth=>{:bearer=>"#{LHC::Scrubber::SCRUB_DISPLAY}"}, :params=>{:limit=>20}, :url=>"http://example.com/sessions"}
76
+ Headers: {"Bearer Token"=>"#{LHC::Scrubber::SCRUB_DISPLAY}"}
72
77
  Response Code: 500 (internal_error)
73
78
  Response Options: {:return_code=>:internal_error, :response_headers=>""}
74
79
  {"status":500,"message":"undefined"}
@@ -6,14 +6,14 @@ describe LHC do
6
6
  include ActionDispatch::TestProcess
7
7
 
8
8
  context 'multipart' do
9
- let(:file) { fixture_file_upload(Tempfile.new, 'image/jpeg') }
9
+ let(:file) { Rack::Test::UploadedFile.new(Tempfile.new) }
10
10
  let(:body) { { size: 2231 }.to_json }
11
11
  let(:location) { 'http://local.ch/uploads/image.jpg' }
12
12
 
13
13
  it 'formats requests to be multipart/form-data' do
14
14
  stub_request(:post, 'http://local.ch/') do |request|
15
15
  raise 'Content-Type header wrong' unless request.headers['Content-Type'] == 'multipart/form-data'
16
- raise 'Body wrongly formatted' unless request.body.match(/file=%23%3CActionDispatch%3A%3AHttp%3A%3AUploadedFile%3A.*%3E&type=Image/)
16
+ raise 'Body wrongly formatted' unless request.body.match?(/file=%23%3CActionDispatch%3A%3AHttp%3A%3AUploadedFile%3A.*%3E&type=Image/)
17
17
  end.to_return(status: 200, body: body, headers: { 'Location' => location })
18
18
  response = LHC.multipart.post(
19
19
  'http://local.ch',
@@ -6,7 +6,7 @@ describe LHC do
6
6
  include ActionDispatch::TestProcess
7
7
 
8
8
  context 'plain' do
9
- let(:file) { fixture_file_upload(Tempfile.new, 'image/jpeg') }
9
+ let(:file) { Rack::Test::UploadedFile.new(Tempfile.new) }
10
10
 
11
11
  it 'leaves plains requests unformatted' do
12
12
  stub_request(:post, 'http://local.ch/')
@@ -14,7 +14,7 @@ describe LHC do
14
14
  uri = URI.parse(response.request.url)
15
15
  path = [
16
16
  'web',
17
- Rails.application.class.parent_name,
17
+ ((ActiveSupport.gem_version >= Gem::Version.new('6.0.0')) ? Rails.application.class.module_parent_name : Rails.application.class.parent_name).underscore,
18
18
  Rails.env,
19
19
  response.request.method,
20
20
  uri.scheme,
@@ -47,7 +47,7 @@ describe LHC::Caching do
47
47
 
48
48
  it 'lets you configure the cache key that will be used' do
49
49
  LHC.config.endpoint(:local, 'http://local.ch', cache: { key: 'STATICKEY' })
50
- expect(Rails.cache).to receive(:fetch).with("LHC_CACHE(v#{LHC::Caching::CACHE_VERSION}): STATICKEY").and_call_original
50
+ expect(Rails.cache).to receive(:fetch).at_least(:once).with("LHC_CACHE(v#{LHC::Caching::CACHE_VERSION}): STATICKEY").and_call_original
51
51
  expect(Rails.cache).to receive(:write).with("LHC_CACHE(v#{LHC::Caching::CACHE_VERSION}): STATICKEY", anything, anything).and_call_original
52
52
  stub
53
53
  LHC.get(:local)
@@ -66,8 +66,8 @@ describe LHC::Caching do
66
66
  stub
67
67
  LHC.config.endpoint(:local, 'http://local.ch', cache: true)
68
68
  original_response = LHC.get(:local)
69
- cached_response = LHC.get(:local)
70
69
  expect(original_response.from_cache?).to eq false
70
+ cached_response = LHC.get(:local)
71
71
  expect(cached_response.from_cache?).to eq true
72
72
  end
73
73
  end
@@ -62,7 +62,8 @@ describe LHC::Caching do
62
62
 
63
63
  context 'found in central cache' do
64
64
  it 'serves it from central cache if found there' do
65
- expect(redis_cache).to receive(:fetch).and_return(nil, body: '<h1>Hi there</h1>', code: 200, headers: nil, return_code: nil, mock: :webmock)
65
+ expect(redis_cache).to receive(:fetch).and_return(nil,
66
+ body: '<h1>Hi there</h1>', code: 200, headers: nil, return_code: nil, mock: :webmock)
66
67
  expect(redis_cache).to receive(:write).and_return(true)
67
68
  expect(Rails.cache).to receive(:fetch).and_call_original
68
69
  expect(Rails.cache).to receive(:write).and_call_original
@@ -7,6 +7,7 @@ describe LHC do
7
7
  before(:each) do
8
8
  class SomeInterceptor < LHC::Interceptor
9
9
  end
10
+
10
11
  class AnotherInterceptor < LHC::Interceptor
11
12
  end
12
13
  end
@@ -8,7 +8,7 @@ describe LHC::Logging do
8
8
  before(:each) do
9
9
  LHC.config.interceptors = [LHC::Logging]
10
10
  LHC::Logging.logger = logger
11
- stub_request(:get, 'http://local.ch').to_return(status: 200)
11
+ stub_request(:get, /http:\/\/local.ch.*/).to_return(status: 200)
12
12
  end
13
13
 
14
14
  it 'does log information before and after every request made with LHC' do
@@ -34,4 +34,24 @@ describe LHC::Logging do
34
34
  )
35
35
  end
36
36
  end
37
+
38
+ context 'sensitive data' do
39
+ before :each do
40
+ LHC.config.scrubs[:params] << 'api_key'
41
+ LHC.config.scrubs[:headers] << 'private_key'
42
+ LHC.get('http://local.ch', params: { api_key: '123-abc' }, headers: { private_key: 'abc-123' })
43
+ end
44
+
45
+ it 'does not log sensitive params information' do
46
+ expect(logger).to have_received(:info).once.with(
47
+ a_string_including("Params={:api_key=>\"#{LHC::Scrubber::SCRUB_DISPLAY}\"}")
48
+ )
49
+ end
50
+
51
+ it 'does not log sensitive header information' do
52
+ expect(logger).to have_received(:info).once.with(
53
+ a_string_including(":private_key=>\"#{LHC::Scrubber::SCRUB_DISPLAY}\"")
54
+ )
55
+ end
56
+ end
37
57
  end