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