castle-rb 6.0.1 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -8
  3. data/lib/castle.rb +7 -11
  4. data/lib/castle/api.rb +7 -12
  5. data/lib/castle/api/approve_device.rb +1 -6
  6. data/lib/castle/api/authenticate.rb +10 -7
  7. data/lib/castle/api/end_impersonation.rb +3 -8
  8. data/lib/castle/api/filter.rb +37 -0
  9. data/lib/castle/api/get_device.rb +1 -6
  10. data/lib/castle/api/get_devices_for_user.rb +1 -6
  11. data/lib/castle/api/log.rb +37 -0
  12. data/lib/castle/api/report_device.rb +1 -6
  13. data/lib/castle/api/risk.rb +37 -0
  14. data/lib/castle/api/start_impersonation.rb +3 -8
  15. data/lib/castle/api/track.rb +1 -6
  16. data/lib/castle/client.rb +36 -16
  17. data/lib/castle/commands/approve_device.rb +1 -5
  18. data/lib/castle/commands/end_impersonation.rb +1 -1
  19. data/lib/castle/commands/filter.rb +23 -0
  20. data/lib/castle/commands/get_device.rb +1 -5
  21. data/lib/castle/commands/get_devices_for_user.rb +1 -5
  22. data/lib/castle/commands/{identify.rb → log.rb} +4 -3
  23. data/lib/castle/commands/report_device.rb +1 -5
  24. data/lib/castle/commands/risk.rb +23 -0
  25. data/lib/castle/commands/start_impersonation.rb +1 -1
  26. data/lib/castle/configuration.rb +18 -8
  27. data/lib/castle/core/get_connection.rb +3 -1
  28. data/lib/castle/core/process_response.rb +5 -2
  29. data/lib/castle/core/process_webhook.rb +10 -5
  30. data/lib/castle/core/send_request.rb +8 -16
  31. data/lib/castle/errors.rb +37 -13
  32. data/lib/castle/failover/prepare_response.rb +2 -7
  33. data/lib/castle/failover/strategy.rb +3 -0
  34. data/lib/castle/headers/extract.rb +4 -4
  35. data/lib/castle/headers/filter.rb +9 -6
  36. data/lib/castle/ips/extract.rb +4 -2
  37. data/lib/castle/logger.rb +3 -3
  38. data/lib/castle/payload/prepare.rb +3 -4
  39. data/lib/castle/secure_mode.rb +3 -2
  40. data/lib/castle/support/hanami.rb +2 -6
  41. data/lib/castle/support/rails.rb +1 -3
  42. data/lib/castle/utils/clean_invalid_chars.rb +1 -3
  43. data/lib/castle/verdict.rb +2 -0
  44. data/lib/castle/version.rb +1 -1
  45. data/lib/castle/webhooks/verify.rb +9 -7
  46. data/spec/integration/rails/rails_spec.rb +9 -7
  47. data/spec/integration/rails/support/home_controller.rb +26 -24
  48. data/spec/lib/castle/api/approve_device_spec.rb +3 -3
  49. data/spec/lib/castle/api/authenticate_spec.rb +20 -24
  50. data/spec/lib/castle/api/end_impersonation_spec.rb +11 -5
  51. data/spec/lib/castle/api/filter_spec.rb +5 -0
  52. data/spec/lib/castle/api/get_device_spec.rb +3 -3
  53. data/spec/lib/castle/api/get_devices_for_user_spec.rb +3 -3
  54. data/spec/lib/castle/api/log_spec.rb +5 -0
  55. data/spec/lib/castle/api/report_device_spec.rb +3 -3
  56. data/spec/lib/castle/api/risk_spec.rb +5 -0
  57. data/spec/lib/castle/api/start_impersonation_spec.rb +11 -5
  58. data/spec/lib/castle/api/track_spec.rb +11 -7
  59. data/spec/lib/castle/api_spec.rb +4 -20
  60. data/spec/lib/castle/client_id/extract_spec.rb +4 -13
  61. data/spec/lib/castle/client_spec.rb +81 -84
  62. data/spec/lib/castle/commands/authenticate_spec.rb +8 -15
  63. data/spec/lib/castle/commands/end_impersonation_spec.rb +6 -9
  64. data/spec/lib/castle/commands/{identify_spec.rb → filter_spec.rb} +41 -19
  65. data/spec/lib/castle/commands/log_spec.rb +100 -0
  66. data/spec/lib/castle/commands/risk_spec.rb +100 -0
  67. data/spec/lib/castle/commands/start_impersonation_spec.rb +6 -9
  68. data/spec/lib/castle/commands/track_spec.rb +9 -18
  69. data/spec/lib/castle/configuration_spec.rb +2 -6
  70. data/spec/lib/castle/context/get_default_spec.rb +8 -8
  71. data/spec/lib/castle/context/prepare_spec.rb +6 -7
  72. data/spec/lib/castle/core/get_connection_spec.rb +6 -22
  73. data/spec/lib/castle/core/process_response_spec.rb +1 -8
  74. data/spec/lib/castle/core/send_request_spec.rb +4 -29
  75. data/spec/lib/castle/headers/extract_spec.rb +1 -3
  76. data/spec/lib/castle/headers/filter_spec.rb +12 -11
  77. data/spec/lib/castle/ips/extract_spec.rb +4 -13
  78. data/spec/lib/castle/logger_spec.rb +2 -6
  79. data/spec/lib/castle/payload/prepare_spec.rb +5 -4
  80. data/spec/lib/castle/session_spec.rb +13 -36
  81. data/spec/lib/castle/singleton_configuration_spec.rb +2 -6
  82. data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +2 -2
  83. data/spec/lib/castle/utils/merge_spec.rb +3 -1
  84. data/spec/lib/castle/validators/present_spec.rb +5 -6
  85. data/spec/lib/castle/webhooks/verify_spec.rb +8 -24
  86. data/spec/lib/castle_spec.rb +4 -10
  87. data/spec/spec_helper.rb +1 -3
  88. data/spec/support/shared_examples/action_request.rb +152 -0
  89. data/spec/support/shared_examples/configuration.rb +14 -42
  90. metadata +23 -18
  91. data/lib/castle/api/identify.rb +0 -26
  92. data/lib/castle/api/review.rb +0 -24
  93. data/lib/castle/commands/review.rb +0 -17
  94. data/lib/castle/events.rb +0 -49
  95. data/spec/lib/castle/api/identify_spec.rb +0 -68
  96. data/spec/lib/castle/api/review_spec.rb +0 -19
  97. data/spec/lib/castle/commands/review_spec.rb +0 -24
  98. data/spec/lib/castle/events_spec.rb +0 -5
@@ -7,14 +7,16 @@ module Castle
7
7
  class Extract
8
8
  # ordered list of ip headers for ip extraction
9
9
  DEFAULT = %w[X-Forwarded-For Remote-Addr].freeze
10
+
10
11
  # list of header which are used with proxy depth setting
11
12
  DEPTH_RELATED = %w[X-Forwarded-For].freeze
12
13
 
13
14
  private_constant :DEFAULT
14
15
 
15
16
  # @param headers [Hash]
16
- # @param config [Castle::Configuration, Castle::SingletonConfiguration]
17
- def initialize(headers, config = Castle.config)
17
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
18
+ def initialize(headers, config = nil)
19
+ config ||= Castle.config
18
20
  @headers = headers
19
21
  @ip_headers = config.ip_headers.empty? ? DEFAULT : config.ip_headers
20
22
  @proxies = config.trusted_proxies + Castle::Configuration::TRUSTED_PROXIES
data/lib/castle/logger.rb CHANGED
@@ -6,9 +6,9 @@ module Castle
6
6
  class << self
7
7
  # @param message [String]
8
8
  # @param data [String]
9
- # @param config [Castle::Configuration, Castle::SingletonConfiguration]
10
- def call(message, data = nil, config = Castle.config)
11
- logger = config.logger
9
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
10
+ def call(message, data = nil, config = nil)
11
+ logger = (config || Castle.config).logger
12
12
 
13
13
  return unless logger
14
14
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Castle
4
4
  module Payload
5
- # this prepare payload based on the request
5
+ # prepares payload based on the request
6
6
  module Prepare
7
7
  class << self
8
8
  # @param payload_options [Hash]
@@ -12,9 +12,8 @@ module Castle
12
12
  def call(payload_options, request, options = {})
13
13
  context = Castle::Context::Prepare.call(request, payload_options.merge(options))
14
14
 
15
- payload = Castle::Utils::DeepSymbolizeKeys.call(
16
- payload_options || {}
17
- ).merge(context: context)
15
+ payload =
16
+ Castle::Utils::DeepSymbolizeKeys.call(payload_options || {}).merge(context: context)
18
17
  payload[:timestamp] ||= Castle::Utils::GetTimestamp.call
19
18
 
20
19
  warn '[DEPRECATION] use user_traits instead of traits key' if payload.key?(:traits)
@@ -6,8 +6,9 @@ module Castle
6
6
  module SecureMode
7
7
  class << self
8
8
  # @param user_id [String]
9
- # @param config [Castle::Configuration, Castle::SingletonConfiguration]
10
- def signature(user_id, config = Castle.config)
9
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
10
+ def signature(user_id, config = nil)
11
+ config ||= Castle.config
11
12
  OpenSSL::HMAC.hexdigest('sha256', config.api_secret, user_id.to_s)
12
13
  end
13
14
  end
@@ -4,16 +4,12 @@ module Castle
4
4
  module Hanami
5
5
  module Action
6
6
  def castle
7
- @castle ||= ::Castle::Client.from_request(request, cookies: (cookies if defined? cookies))
7
+ @castle ||= ::Castle::Client.from_request(request, cookies: (cookies if defined?(cookies)))
8
8
  end
9
9
  end
10
10
 
11
11
  def self.included(base)
12
- base.configure do
13
- controller.prepare do
14
- include Castle::Hanami::Action
15
- end
16
- end
12
+ base.configure { controller.prepare { include Castle::Hanami::Action } }
17
13
  end
18
14
  end
19
15
  end
@@ -7,7 +7,5 @@ module Castle
7
7
  end
8
8
  end
9
9
 
10
- ActiveSupport.on_load(:action_controller) do
11
- include CastleClient
12
- end
10
+ ActiveSupport.on_load(:action_controller) { include CastleClient }
13
11
  end
@@ -9,9 +9,7 @@ module Castle
9
9
  when ::String
10
10
  arg.encode('UTF-8', invalid: :replace, undef: :replace)
11
11
  when ::Hash
12
- arg.transform_values do |v|
13
- Castle::Utils::CleanInvalidChars.call(v)
14
- end
12
+ arg.transform_values { |v| Castle::Utils::CleanInvalidChars.call(v) }
15
13
  when ::Array
16
14
  arg.map { |el| Castle::Utils::CleanInvalidChars.call(el) }
17
15
  else
@@ -5,8 +5,10 @@ module Castle
5
5
  module Verdict
6
6
  # allow
7
7
  ALLOW = 'allow'
8
+
8
9
  # deny
9
10
  DENY = 'deny'
11
+
10
12
  # challenge
11
13
  CHALLENGE = 'challenge'
12
14
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castle
4
- VERSION = '6.0.1'
4
+ VERSION = '7.0.0'
5
5
  end
@@ -7,9 +7,10 @@ module Castle
7
7
  class << self
8
8
  # Checks if webhook is valid
9
9
  # @param webhook [Request]
10
- # @param config [Castle::Configuration, Castle::SingletonConfiguration]
11
- def call(webhook, config = Castle.config)
12
- expected_signature = compute_signature(webhook, config.api_secret)
10
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
11
+ def call(webhook, config = nil)
12
+ config ||= Castle.config
13
+ expected_signature = compute_signature(webhook, config)
13
14
  signature = webhook.env['HTTP_X_CASTLE_SIGNATURE']
14
15
  verify_signature(signature, expected_signature)
15
16
  end
@@ -18,13 +19,14 @@ module Castle
18
19
 
19
20
  # Computes a webhook signature using provided user_id
20
21
  # @param webhook [Request]
21
- # @param api_secret [String]
22
- def compute_signature(webhook, api_secret)
22
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration]
23
+ # @return [String]
24
+ def compute_signature(webhook, config)
23
25
  Base64.encode64(
24
26
  OpenSSL::HMAC.digest(
25
27
  OpenSSL::Digest.new('sha256'),
26
- api_secret,
27
- Castle::Core::ProcessWebhook.call(webhook)
28
+ config.api_secret,
29
+ Castle::Core::ProcessWebhook.call(webhook, config)
28
30
  )
29
31
  ).strip
30
32
  end
@@ -9,15 +9,20 @@ RSpec.describe HomeController, type: :request do
9
9
  {
10
10
  'event' => '$login.succeeded',
11
11
  'user_id' => '123',
12
- 'properties' => { 'key' => 'value' },
13
- 'user_traits' => { 'key' => 'value' },
12
+ 'properties' => {
13
+ 'key' => 'value'
14
+ },
15
+ 'user_traits' => {
16
+ 'key' => 'value'
17
+ },
14
18
  'timestamp' => now.utc.iso8601(3),
15
19
  'sent_at' => now.utc.iso8601(3),
16
20
  'context' => {
17
21
  'client_id' => '',
18
22
  'active' => true,
19
23
  'headers' => {
20
- 'Accept' => 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
24
+ 'Accept' =>
25
+ 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
21
26
  'Authorization' => true,
22
27
  'Content-Length' => '0',
23
28
  'Cookie' => true,
@@ -35,10 +40,7 @@ RSpec.describe HomeController, type: :request do
35
40
  end
36
41
  let(:now) { Time.now }
37
42
  let(:headers) do
38
- {
39
- 'HTTP_AUTHORIZATION' => 'Basic 123',
40
- 'HTTP_X_FORWARDED_FOR' => '5.5.5.5, 1.2.3.4'
41
- }
43
+ { 'HTTP_AUTHORIZATION' => 'Basic 123', 'HTTP_X_FORWARDED_FOR' => '5.5.5.5, 1.2.3.4' }
42
44
  end
43
45
 
44
46
  before do
@@ -22,19 +22,20 @@ class HomeController < ActionController::Base
22
22
 
23
23
  # prepare payload and calling track with client example
24
24
  def index2
25
- payload = ::Castle::Payload::Prepare.call(
26
- {
27
- event: '$login.succeeded',
28
- user_id: '123',
29
- properties: {
30
- key: 'value'
25
+ payload =
26
+ ::Castle::Payload::Prepare.call(
27
+ {
28
+ event: '$login.succeeded',
29
+ user_id: '123',
30
+ properties: {
31
+ key: 'value'
32
+ },
33
+ user_traits: {
34
+ key: 'value'
35
+ }
31
36
  },
32
- user_traits: {
33
- key: 'value'
34
- }
35
- },
36
- request
37
- )
37
+ request
38
+ )
38
39
  client = ::Castle::Client.new
39
40
  client.track(payload)
40
41
 
@@ -43,19 +44,20 @@ class HomeController < ActionController::Base
43
44
 
44
45
  # prepare payload and calling track with direct API::Track service
45
46
  def index3
46
- payload = ::Castle::Payload::Prepare.call(
47
- {
48
- event: '$login.succeeded',
49
- user_id: '123',
50
- properties: {
51
- key: 'value'
47
+ payload =
48
+ ::Castle::Payload::Prepare.call(
49
+ {
50
+ event: '$login.succeeded',
51
+ user_id: '123',
52
+ properties: {
53
+ key: 'value'
54
+ },
55
+ user_traits: {
56
+ key: 'value'
57
+ }
52
58
  },
53
- user_traits: {
54
- key: 'value'
55
- }
56
- },
57
- request
58
- )
59
+ request
60
+ )
59
61
  Castle::API::Track.call(payload)
60
62
 
61
63
  render inline: 'hello'
@@ -2,9 +2,9 @@
2
2
 
3
3
  describe Castle::API::ApproveDevice do
4
4
  before do
5
- stub_request(:any, /api.castle.io/).with(
6
- basic_auth: ['', 'secret']
7
- ).to_return(status: 200, body: '{}', headers: {})
5
+ stub_request(:any, /api.castle.io/)
6
+ .with(basic_auth: ['', 'secret'])
7
+ .to_return(status: 200, body: '{}', headers: {})
8
8
  end
9
9
 
10
10
  describe '.call' do
@@ -30,15 +30,14 @@ describe Castle::API::Authenticate do
30
30
 
31
31
  describe '.call' do
32
32
  let(:request_body) do
33
- { event: '$login.succeeded', context: context, user_id: '1234',
34
- sent_at: time_auto }
33
+ { event: '$login.succeeded', context: context, user_id: '1234', sent_at: time_auto }
35
34
  end
36
35
 
37
36
  context 'when used with symbol keys' do
38
37
  before do
39
- stub_request(:any, /api.castle.io/).with(
40
- basic_auth: ['', 'secret']
41
- ).to_return(status: 200, body: response_body, headers: {})
38
+ stub_request(:any, /api.castle.io/)
39
+ .with(basic_auth: ['', 'secret'])
40
+ .to_return(status: 200, body: response_body, headers: {})
42
41
  call_subject
43
42
  end
44
43
 
@@ -55,8 +54,13 @@ describe Castle::API::Authenticate do
55
54
  { event: '$login.succeeded', user_id: '1234', timestamp: time_user, context: context }
56
55
  end
57
56
  let(:request_body) do
58
- { event: '$login.succeeded', user_id: '1234', context: context,
59
- timestamp: time_user, sent_at: time_auto }
57
+ {
58
+ event: '$login.succeeded',
59
+ user_id: '1234',
60
+ context: context,
61
+ timestamp: time_user,
62
+ sent_at: time_auto
63
+ }
60
64
  end
61
65
 
62
66
  it do
@@ -75,20 +79,14 @@ describe Castle::API::Authenticate do
75
79
  context 'when denied without any risk policy' do
76
80
  let(:response_body) { deny_response_without_rp.to_json }
77
81
  let(:deny_response_without_rp) do
78
- {
79
- action: 'deny',
80
- user_id: '12345',
81
- device_token: 'abcdefg1234'
82
- }
83
- end
84
- let(:deny_without_rp_failover_result) do
85
- deny_response_without_rp.merge(failover_appendix)
82
+ { action: 'deny', user_id: '12345', device_token: 'abcdefg1234' }
86
83
  end
84
+ let(:deny_without_rp_failover_result) { deny_response_without_rp.merge(failover_appendix) }
87
85
 
88
86
  before do
89
- stub_request(:any, /api.castle.io/).with(
90
- basic_auth: ['', 'secret']
91
- ).to_return(status: 200, body: deny_response_without_rp.to_json, headers: {})
87
+ stub_request(:any, /api.castle.io/)
88
+ .with(basic_auth: ['', 'secret'])
89
+ .to_return(status: 200, body: deny_response_without_rp.to_json, headers: {})
92
90
  call_subject
93
91
  end
94
92
 
@@ -116,14 +114,12 @@ describe Castle::API::Authenticate do
116
114
  }
117
115
  end
118
116
  let(:response_body) { deny_response_with_rp.to_json }
119
- let(:deny_with_rp_failover_result) do
120
- deny_response_with_rp.merge(failover_appendix)
121
- end
117
+ let(:deny_with_rp_failover_result) { deny_response_with_rp.merge(failover_appendix) }
122
118
 
123
119
  before do
124
- stub_request(:any, /api.castle.io/).with(
125
- basic_auth: ['', 'secret']
126
- ).to_return(status: 200, body: deny_response_with_rp.to_json, headers: {})
120
+ stub_request(:any, /api.castle.io/)
121
+ .with(basic_auth: ['', 'secret'])
122
+ .to_return(status: 200, body: deny_response_with_rp.to_json, headers: {})
127
123
  call_subject
128
124
  end
129
125
 
@@ -22,9 +22,9 @@ describe Castle::API::EndImpersonation do
22
22
  before do
23
23
  Timecop.freeze(time_now)
24
24
  stub_const('Castle::VERSION', '2.2.0')
25
- stub_request(:any, /api.castle.io/).with(
26
- basic_auth: ['', 'secret']
27
- ).to_return(status: 200, body: response_body, headers: {})
25
+ stub_request(:any, /api.castle.io/)
26
+ .with(basic_auth: ['', 'secret'])
27
+ .to_return(status: 200, body: response_body, headers: {})
28
28
  end
29
29
 
30
30
  after { Timecop.return }
@@ -32,8 +32,14 @@ describe Castle::API::EndImpersonation do
32
32
  describe 'call' do
33
33
  let(:impersonator) { 'test@castle.io' }
34
34
  let(:request_body) do
35
- { user_id: '1234', sent_at: time_auto,
36
- properties: { impersonator: impersonator }, context: context }
35
+ {
36
+ user_id: '1234',
37
+ sent_at: time_auto,
38
+ properties: {
39
+ impersonator: impersonator
40
+ },
41
+ context: context
42
+ }
37
43
  end
38
44
  let(:response_body) { { success: true }.to_json }
39
45
  let(:options) do
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::API::Filter do
4
+ pending
5
+ end
@@ -2,9 +2,9 @@
2
2
 
3
3
  describe Castle::API::GetDevice do
4
4
  before do
5
- stub_request(:any, /api.castle.io/).with(
6
- basic_auth: ['', 'secret']
7
- ).to_return(status: 200, body: '{}', headers: {})
5
+ stub_request(:any, /api.castle.io/)
6
+ .with(basic_auth: ['', 'secret'])
7
+ .to_return(status: 200, body: '{}', headers: {})
8
8
  end
9
9
 
10
10
  describe '.call' do
@@ -2,9 +2,9 @@
2
2
 
3
3
  describe Castle::API::GetDevicesForUser do
4
4
  before do
5
- stub_request(:any, /api.castle.io/).with(
6
- basic_auth: ['', 'secret']
7
- ).to_return(status: 200, body: '{}', headers: {})
5
+ stub_request(:any, /api.castle.io/)
6
+ .with(basic_auth: ['', 'secret'])
7
+ .to_return(status: 200, body: '{}', headers: {})
8
8
  end
9
9
 
10
10
  describe '.call' do
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::API::Log do
4
+ pending
5
+ end
@@ -2,9 +2,9 @@
2
2
 
3
3
  describe Castle::API::ReportDevice do
4
4
  before do
5
- stub_request(:any, /api.castle.io/).with(
6
- basic_auth: ['', 'secret']
7
- ).to_return(status: 200, body: '{}', headers: {})
5
+ stub_request(:any, /api.castle.io/)
6
+ .with(basic_auth: ['', 'secret'])
7
+ .to_return(status: 200, body: '{}', headers: {})
8
8
  end
9
9
 
10
10
  describe '.call' do