castle-rb 3.6.2 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +57 -5
  3. data/lib/castle.rb +5 -3
  4. data/lib/castle/api.rb +12 -6
  5. data/lib/castle/api/request.rb +15 -10
  6. data/lib/castle/api/session.rb +39 -0
  7. data/lib/castle/client.rb +23 -18
  8. data/lib/castle/configuration.rb +49 -6
  9. data/lib/castle/context/default.rb +36 -18
  10. data/lib/castle/context/sanitizer.rb +1 -0
  11. data/lib/castle/events.rb +49 -0
  12. data/lib/castle/extractors/client_id.rb +7 -3
  13. data/lib/castle/extractors/headers.rb +24 -30
  14. data/lib/castle/extractors/ip.rb +69 -5
  15. data/lib/castle/headers_filter.rb +35 -0
  16. data/lib/castle/headers_formatter.rb +22 -0
  17. data/lib/castle/validators/not_supported.rb +1 -0
  18. data/lib/castle/validators/present.rb +1 -0
  19. data/lib/castle/version.rb +1 -1
  20. data/spec/integration/rails/rails_spec.rb +61 -0
  21. data/spec/integration/rails/support/all.rb +6 -0
  22. data/spec/integration/rails/support/application.rb +15 -0
  23. data/spec/integration/rails/support/home_controller.rb +21 -0
  24. data/spec/lib/castle/api/request_spec.rb +43 -30
  25. data/spec/lib/castle/api/session_spec.rb +47 -0
  26. data/spec/lib/castle/api_spec.rb +4 -4
  27. data/spec/lib/castle/client_spec.rb +5 -3
  28. data/spec/lib/castle/commands/authenticate_spec.rb +1 -0
  29. data/spec/lib/castle/commands/identify_spec.rb +1 -0
  30. data/spec/lib/castle/commands/impersonate_spec.rb +1 -0
  31. data/spec/lib/castle/commands/track_spec.rb +1 -0
  32. data/spec/lib/castle/configuration_spec.rb +21 -4
  33. data/spec/lib/castle/context/default_spec.rb +13 -13
  34. data/spec/lib/castle/events_spec.rb +5 -0
  35. data/spec/lib/castle/extractors/client_id_spec.rb +2 -1
  36. data/spec/lib/castle/extractors/headers_spec.rb +67 -51
  37. data/spec/lib/castle/extractors/ip_spec.rb +89 -12
  38. data/spec/lib/castle/headers_filter_spec.rb +38 -0
  39. data/spec/lib/castle/{header_formatter_spec.rb → headers_formatter_spec.rb} +3 -3
  40. data/spec/lib/castle/utils/cloner_spec.rb +1 -0
  41. data/spec/lib/castle/utils/timestamp_spec.rb +3 -4
  42. data/spec/lib/castle/utils_spec.rb +1 -1
  43. data/spec/lib/castle/validators/not_supported_spec.rb +1 -3
  44. data/spec/spec_helper.rb +1 -2
  45. metadata +38 -10
  46. data/lib/castle/api/request/build.rb +0 -27
  47. data/lib/castle/header_formatter.rb +0 -9
  48. data/spec/lib/castle/api/request/build_spec.rb +0 -44
@@ -1,27 +1,104 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  describe Castle::Extractors::IP do
4
- subject(:extractor) { described_class.new(request) }
5
-
6
- let(:request) { Rack::Request.new(env) }
4
+ subject(:extractor) { described_class.new(headers) }
7
5
 
8
6
  describe 'ip' do
7
+ after do
8
+ Castle.config.ip_headers = []
9
+ Castle.config.trusted_proxies = []
10
+ end
11
+
9
12
  context 'when regular ip' do
10
- let(:env) { Rack::MockRequest.env_for('/', 'HTTP_X_FORWARDED_FOR' => '1.2.3.5') }
13
+ let(:headers) { { 'X-Forwarded-For' => '1.2.3.5' } }
11
14
 
12
15
  it { expect(extractor.call).to eql('1.2.3.5') }
13
16
  end
14
17
 
15
- context 'when cf remote_ip' do
16
- let(:env) do
17
- Rack::MockRequest.env_for(
18
- '/',
19
- 'HTTP_CF_CONNECTING_IP' => '1.2.3.4',
20
- 'HTTP_X_FORWARDED_FOR' => '1.2.3.5'
21
- )
18
+ context 'when we need to use other ip header' do
19
+ let(:headers) do
20
+ { 'Cf-Connecting-Ip' => '1.2.3.4', 'X-Forwarded-For' => '1.1.1.1, 1.2.2.2, 1.2.3.5' }
21
+ end
22
+
23
+ context 'with uppercase format' do
24
+ before { Castle.config.ip_headers = %w[CF_CONNECTING_IP X-Forwarded-For] }
25
+
26
+ it { expect(extractor.call).to eql('1.2.3.4') }
27
+ end
28
+
29
+ context 'with regular format' do
30
+ before { Castle.config.ip_headers = %w[Cf-Connecting-Ip X-Forwarded-For] }
31
+
32
+ it { expect(extractor.call).to eql('1.2.3.4') }
33
+ end
34
+
35
+ context 'with value from trusted proxies it get seconds header' do
36
+ before do
37
+ Castle.config.ip_headers = %w[Cf-Connecting-Ip X-Forwarded-For]
38
+ Castle.config.trusted_proxies = %w[1.2.3.4]
39
+ end
40
+
41
+ it { expect(extractor.call).to eql('1.2.3.5') }
42
+ end
43
+ end
44
+
45
+ context 'with all the trusted proxies' do
46
+ let(:http_x_header) do
47
+ '127.0.0.1,10.0.0.1,172.31.0.1,192.168.0.1'
22
48
  end
23
49
 
24
- it { expect(extractor.call).to eql('1.2.3.4') }
50
+ let(:headers) { { 'Remote-Addr' => '127.0.0.1', 'X-Forwarded-For' => http_x_header } }
51
+
52
+ it 'fallbacks to first available header when all headers are marked trusted proxy' do
53
+ expect(extractor.call).to eql('127.0.0.1')
54
+ end
55
+ end
56
+
57
+ context 'with trust_proxy_chain option' do
58
+ let(:http_x_header) do
59
+ '6.6.6.6, 2.2.2.3, 6.6.6.5'
60
+ end
61
+
62
+ let(:headers) { { 'Remote-Addr' => '6.6.6.4', 'X-Forwarded-For' => http_x_header } }
63
+
64
+ before { Castle.config.trust_proxy_chain = true }
65
+
66
+ it 'selects first available header' do
67
+ expect(extractor.call).to eql('6.6.6.6')
68
+ end
69
+ end
70
+
71
+ context 'with trusted_proxy_depth option' do
72
+ let(:http_x_header) do
73
+ '6.6.6.6, 2.2.2.3, 6.6.6.5'
74
+ end
75
+
76
+ let(:headers) { { 'Remote-Addr' => '6.6.6.4', 'X-Forwarded-For' => http_x_header } }
77
+
78
+ before { Castle.config.trusted_proxy_depth = 1 }
79
+
80
+ it 'selects first available header' do
81
+ expect(extractor.call).to eql('2.2.2.3')
82
+ end
83
+ end
84
+
85
+ context 'when list of not trusted ips provided in X_FORWARDED_FOR' do
86
+ let(:headers) do
87
+ {
88
+ 'X-Forwarded-For' => '6.6.6.6, 2.2.2.3, 192.168.0.7',
89
+ 'Client-Ip' => '6.6.6.6'
90
+ }
91
+ end
92
+
93
+ it 'does not allow to spoof ip' do
94
+ expect(extractor.call).to eql('2.2.2.3')
95
+ end
96
+
97
+ context 'when marked 2.2.2.3 as trusted proxy' do
98
+ before { Castle.config.trusted_proxies = [/^2.2.2.\d$/] }
99
+
100
+ it { expect(extractor.call).to eql('6.6.6.6') }
101
+ end
25
102
  end
26
103
  end
27
104
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::HeadersFilter do
4
+ subject(:headers) { described_class.new(request).call }
5
+
6
+ let(:env) do
7
+ result = Rack::MockRequest.env_for(
8
+ '/',
9
+ 'Action-Dispatch.request.content-Type' => 'application/json',
10
+ 'HTTP_AUTHORIZATION' => 'Basic 123456',
11
+ 'HTTP_COOKIE' => '__cid=abcd;other=efgh',
12
+ 'HTTP_ACCEPT' => 'application/json',
13
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
14
+ 'HTTP_USER_AGENT' => 'Mozilla 1234',
15
+ 'TEST' => '1',
16
+ 'REMOTE_ADDR' => '1.2.3.4'
17
+ )
18
+ result[:HTTP_OK] = 'OK'
19
+ result
20
+ end
21
+ let(:filtered) do
22
+ {
23
+ 'Accept' => 'application/json',
24
+ 'Authorization' => 'Basic 123456',
25
+ 'Cookie' => '__cid=abcd;other=efgh',
26
+ 'Content-Length' => '0',
27
+ 'Ok' => 'OK',
28
+ 'User-Agent' => 'Mozilla 1234',
29
+ 'Remote-Addr' => '1.2.3.4',
30
+ 'X-Forwarded-For' => '1.2.3.4'
31
+ }
32
+ end
33
+ let(:request) { Rack::Request.new(env) }
34
+
35
+ context 'with list of header' do
36
+ it { expect(headers).to eq(filtered) }
37
+ end
38
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- describe Castle::HeaderFormatter do
4
- subject(:formatter) { described_class.new }
3
+ describe Castle::HeadersFormatter do
4
+ subject(:formatter) { described_class }
5
5
 
6
6
  it 'removes HTTP_' do
7
7
  expect(formatter.call('HTTP_X_TEST')).to be_eql('X-Test')
@@ -19,7 +19,7 @@ describe Castle::HeaderFormatter do
19
19
  expect(formatter.call('httpX_teST')).to be_eql('Httpx-Test')
20
20
  end
21
21
 
22
- it 'removes HTTP_' do
22
+ it 'capitalizes' do
23
23
  expect(formatter.call(:clearance)).to be_eql('Clearance')
24
24
  end
25
25
  end
@@ -10,6 +10,7 @@ describe Castle::Utils::Cloner do
10
10
  let(:cloned) { cloner.call(first) }
11
11
 
12
12
  before { cloned }
13
+
13
14
  it do
14
15
  nested[:test] = 'sample'
15
16
  expect(cloned).to be_eql(result)
@@ -1,17 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  describe Castle::Utils::Timestamp do
4
- subject { described_class.call }
4
+ subject(:timestamp) { described_class.call }
5
5
 
6
6
  let(:time_string) { '2018-01-10T14:14:24.407Z' }
7
7
  let(:time) { Time.parse(time_string) }
8
8
 
9
9
  before { Timecop.freeze(time) }
10
+
10
11
  after { Timecop.return }
11
12
 
12
13
  describe '#call' do
13
- it do
14
- is_expected.to eql(time_string)
15
- end
14
+ it { expect(timestamp).to eql(time_string) }
16
15
  end
17
16
  end
@@ -48,7 +48,7 @@ describe Castle::Utils do
48
48
  end
49
49
  end
50
50
 
51
- describe '#deep_symbolize_keys' do
51
+ describe '#cloner' do
52
52
  subject { described_class.deep_symbolize_keys!(Castle::Utils::Cloner.call(hash)) }
53
53
 
54
54
  context 'when nested_symbols' do
@@ -8,9 +8,7 @@ describe Castle::Validators::NotSupported do
8
8
  let(:keys) { %i[first second] }
9
9
 
10
10
  it do
11
- expect do
12
- call
13
- end.to raise_error(
11
+ expect { call }.to raise_error(
14
12
  Castle::InvalidParametersError,
15
13
  'first is/are not supported'
16
14
  )
@@ -16,8 +16,7 @@ WebMock.disable_net_connect!(allow_localhost: true)
16
16
 
17
17
  RSpec.configure do |config|
18
18
  config.before do
19
- Castle.instance_variable_set(:@configuration, Castle::Configuration.new)
20
-
19
+ Castle.config.reset
21
20
  Castle.configure do |cfg|
22
21
  cfg.api_secret = 'secret'
23
22
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: castle-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.2
4
+ version: 4.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johan Brissmyr
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-24 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2020-05-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: appraisal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: Castle protects your users from account compromise
14
28
  email: johan@castle.io
15
29
  executables: []
@@ -21,8 +35,8 @@ files:
21
35
  - lib/castle.rb
22
36
  - lib/castle/api.rb
23
37
  - lib/castle/api/request.rb
24
- - lib/castle/api/request/build.rb
25
38
  - lib/castle/api/response.rb
39
+ - lib/castle/api/session.rb
26
40
  - lib/castle/client.rb
27
41
  - lib/castle/command.rb
28
42
  - lib/castle/commands/authenticate.rb
@@ -35,11 +49,13 @@ files:
35
49
  - lib/castle/context/merger.rb
36
50
  - lib/castle/context/sanitizer.rb
37
51
  - lib/castle/errors.rb
52
+ - lib/castle/events.rb
38
53
  - lib/castle/extractors/client_id.rb
39
54
  - lib/castle/extractors/headers.rb
40
55
  - lib/castle/extractors/ip.rb
41
56
  - lib/castle/failover_auth_response.rb
42
- - lib/castle/header_formatter.rb
57
+ - lib/castle/headers_filter.rb
58
+ - lib/castle/headers_formatter.rb
43
59
  - lib/castle/review.rb
44
60
  - lib/castle/secure_mode.rb
45
61
  - lib/castle/support/hanami.rb
@@ -53,9 +69,13 @@ files:
53
69
  - lib/castle/validators/not_supported.rb
54
70
  - lib/castle/validators/present.rb
55
71
  - lib/castle/version.rb
56
- - spec/lib/castle/api/request/build_spec.rb
72
+ - spec/integration/rails/rails_spec.rb
73
+ - spec/integration/rails/support/all.rb
74
+ - spec/integration/rails/support/application.rb
75
+ - spec/integration/rails/support/home_controller.rb
57
76
  - spec/lib/castle/api/request_spec.rb
58
77
  - spec/lib/castle/api/response_spec.rb
78
+ - spec/lib/castle/api/session_spec.rb
59
79
  - spec/lib/castle/api_spec.rb
60
80
  - spec/lib/castle/client_spec.rb
61
81
  - spec/lib/castle/command_spec.rb
@@ -68,10 +88,12 @@ files:
68
88
  - spec/lib/castle/context/default_spec.rb
69
89
  - spec/lib/castle/context/merger_spec.rb
70
90
  - spec/lib/castle/context/sanitizer_spec.rb
91
+ - spec/lib/castle/events_spec.rb
71
92
  - spec/lib/castle/extractors/client_id_spec.rb
72
93
  - spec/lib/castle/extractors/headers_spec.rb
73
94
  - spec/lib/castle/extractors/ip_spec.rb
74
- - spec/lib/castle/header_formatter_spec.rb
95
+ - spec/lib/castle/headers_filter_spec.rb
96
+ - spec/lib/castle/headers_formatter_spec.rb
75
97
  - spec/lib/castle/review_spec.rb
76
98
  - spec/lib/castle/secure_mode_spec.rb
77
99
  - spec/lib/castle/utils/cloner_spec.rb
@@ -102,14 +124,19 @@ required_rubygems_version: !ruby/object:Gem::Requirement
102
124
  - !ruby/object:Gem::Version
103
125
  version: '0'
104
126
  requirements: []
105
- rubygems_version: 3.0.6
127
+ rubygems_version: 3.1.3
106
128
  signing_key:
107
129
  specification_version: 4
108
130
  summary: Castle
109
131
  test_files:
110
132
  - spec/spec_helper.rb
133
+ - spec/integration/rails/support/application.rb
134
+ - spec/integration/rails/support/all.rb
135
+ - spec/integration/rails/support/home_controller.rb
136
+ - spec/integration/rails/rails_spec.rb
111
137
  - spec/lib/castle_spec.rb
112
138
  - spec/lib/castle/review_spec.rb
139
+ - spec/lib/castle/headers_filter_spec.rb
113
140
  - spec/lib/castle/client_spec.rb
114
141
  - spec/lib/castle/context/default_spec.rb
115
142
  - spec/lib/castle/context/merger_spec.rb
@@ -117,14 +144,14 @@ test_files:
117
144
  - spec/lib/castle/api_spec.rb
118
145
  - spec/lib/castle/configuration_spec.rb
119
146
  - spec/lib/castle/version_spec.rb
120
- - spec/lib/castle/header_formatter_spec.rb
121
147
  - spec/lib/castle/utils/cloner_spec.rb
122
148
  - spec/lib/castle/utils/timestamp_spec.rb
123
149
  - spec/lib/castle/utils/merger_spec.rb
124
150
  - spec/lib/castle/command_spec.rb
151
+ - spec/lib/castle/headers_formatter_spec.rb
152
+ - spec/lib/castle/api/session_spec.rb
125
153
  - spec/lib/castle/api/request_spec.rb
126
154
  - spec/lib/castle/api/response_spec.rb
127
- - spec/lib/castle/api/request/build_spec.rb
128
155
  - spec/lib/castle/commands/review_spec.rb
129
156
  - spec/lib/castle/commands/authenticate_spec.rb
130
157
  - spec/lib/castle/commands/track_spec.rb
@@ -137,3 +164,4 @@ test_files:
137
164
  - spec/lib/castle/extractors/client_id_spec.rb
138
165
  - spec/lib/castle/utils_spec.rb
139
166
  - spec/lib/castle/secure_mode_spec.rb
167
+ - spec/lib/castle/events_spec.rb
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- module API
5
- # generate api request
6
- module Request
7
- module Build
8
- class << self
9
- def call(command, headers, api_secret)
10
- request = Net::HTTP.const_get(
11
- command.method.to_s.capitalize
12
- ).new("/#{Castle.config.url_prefix}/#{command.path}", headers)
13
-
14
- unless command.method == :get
15
- request.body = ::Castle::Utils.replace_invalid_characters(
16
- command.data
17
- ).to_json
18
- end
19
-
20
- request.basic_auth('', api_secret)
21
- request
22
- end
23
- end
24
- end
25
- end
26
- end
27
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- class HeaderFormatter
5
- def call(header)
6
- header.to_s.gsub(/^HTTP(?:_|-)/i, '').split(/_|-/).map(&:capitalize).join('-')
7
- end
8
- end
9
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::API::Request::Build do
4
- subject(:call) { described_class.call(command, headers, api_secret) }
5
-
6
- let(:headers) { { 'SAMPLE-HEADER' => '1' } }
7
- let(:api_secret) { 'secret' }
8
-
9
- describe 'call' do
10
- context 'when get' do
11
- let(:command) { Castle::Commands::Review.build(review_id) }
12
- let(:review_id) { SecureRandom.uuid }
13
-
14
- it { expect(call.body).to be_nil }
15
- it { expect(call.method).to eql('GET') }
16
- it { expect(call.path).to eql("/v1/#{command.path}") }
17
- it { expect(call.to_hash).to have_key('authorization') }
18
- it { expect(call.to_hash).to have_key('sample-header') }
19
- it { expect(call.to_hash['sample-header']).to eql(['1']) }
20
- end
21
-
22
- context 'when post' do
23
- let(:time) { Time.now.utc.iso8601(3) }
24
- let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded', name: "\xC4") }
25
- let(:expected_body) do
26
- {
27
- event: '$login.succeeded',
28
- name: "�",
29
- context: {},
30
- sent_at: time
31
- }
32
- end
33
-
34
- before { allow(Castle::Utils::Timestamp).to receive(:call).and_return(time) }
35
-
36
- it { expect(call.body).to be_eql(expected_body.to_json) }
37
- it { expect(call.method).to eql('POST') }
38
- it { expect(call.path).to eql("/v1/#{command.path}") }
39
- it { expect(call.to_hash).to have_key('authorization') }
40
- it { expect(call.to_hash).to have_key('sample-header') }
41
- it { expect(call.to_hash['sample-header']).to eql(['1']) }
42
- end
43
- end
44
- end