castle-rb 3.6.2 → 4.3.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 (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