castle-rb 4.2.1 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +160 -45
  3. data/lib/castle.rb +49 -28
  4. data/lib/castle/api.rb +21 -14
  5. data/lib/castle/api/approve_device.rb +20 -0
  6. data/lib/castle/api/authenticate.rb +37 -0
  7. data/lib/castle/api/end_impersonation.rb +24 -0
  8. data/lib/castle/api/filter.rb +37 -0
  9. data/lib/castle/api/get_device.rb +20 -0
  10. data/lib/castle/api/get_devices_for_user.rb +20 -0
  11. data/lib/castle/api/log.rb +37 -0
  12. data/lib/castle/api/report_device.rb +20 -0
  13. data/lib/castle/api/risk.rb +37 -0
  14. data/lib/castle/api/start_impersonation.rb +24 -0
  15. data/lib/castle/api/track.rb +21 -0
  16. data/lib/castle/client.rb +78 -51
  17. data/lib/castle/{extractors/client_id.rb → client_id/extract.rb} +2 -2
  18. data/lib/castle/commands/approve_device.rb +17 -0
  19. data/lib/castle/commands/authenticate.rb +13 -13
  20. data/lib/castle/commands/end_impersonation.rb +25 -0
  21. data/lib/castle/commands/filter.rb +23 -0
  22. data/lib/castle/commands/get_device.rb +17 -0
  23. data/lib/castle/commands/get_devices_for_user.rb +17 -0
  24. data/lib/castle/commands/log.rb +23 -0
  25. data/lib/castle/commands/report_device.rb +17 -0
  26. data/lib/castle/commands/risk.rb +23 -0
  27. data/lib/castle/commands/start_impersonation.rb +25 -0
  28. data/lib/castle/commands/track.rb +12 -13
  29. data/lib/castle/configuration.rb +57 -32
  30. data/lib/castle/context/{default.rb → get_default.rb} +5 -6
  31. data/lib/castle/context/{merger.rb → merge.rb} +3 -3
  32. data/lib/castle/context/prepare.rb +18 -0
  33. data/lib/castle/context/{sanitizer.rb → sanitize.rb} +1 -1
  34. data/lib/castle/core/get_connection.rb +27 -0
  35. data/lib/castle/{api/response.rb → core/process_response.rb} +8 -3
  36. data/lib/castle/core/process_webhook.rb +25 -0
  37. data/lib/castle/core/send_request.rb +42 -0
  38. data/lib/castle/errors.rb +38 -12
  39. data/lib/castle/failover/prepare_response.rb +18 -0
  40. data/lib/castle/failover/strategy.rb +23 -0
  41. data/lib/castle/headers/extract.rb +47 -0
  42. data/lib/castle/headers/filter.rb +40 -0
  43. data/lib/castle/headers/format.rb +24 -0
  44. data/lib/castle/{extractors/ip.rb → ips/extract.rb} +31 -9
  45. data/lib/castle/logger.rb +19 -0
  46. data/lib/castle/payload/prepare.rb +26 -0
  47. data/lib/castle/secure_mode.rb +7 -2
  48. data/lib/castle/session.rb +18 -0
  49. data/lib/castle/singleton_configuration.rb +9 -0
  50. data/lib/castle/support/hanami.rb +2 -6
  51. data/lib/castle/support/rails.rb +1 -3
  52. data/lib/castle/utils/clean_invalid_chars.rb +22 -0
  53. data/lib/castle/utils/clone.rb +15 -0
  54. data/lib/castle/utils/deep_symbolize_keys.rb +45 -0
  55. data/lib/castle/utils/get_timestamp.rb +15 -0
  56. data/lib/castle/utils/{merger.rb → merge.rb} +3 -3
  57. data/lib/castle/utils/secure_compare.rb +22 -0
  58. data/lib/castle/validators/not_supported.rb +1 -0
  59. data/lib/castle/validators/present.rb +1 -0
  60. data/lib/castle/verdict.rb +15 -0
  61. data/lib/castle/version.rb +1 -1
  62. data/lib/castle/webhooks/verify.rb +45 -0
  63. data/spec/integration/rails/rails_spec.rb +42 -14
  64. data/spec/integration/rails/support/application.rb +3 -1
  65. data/spec/integration/rails/support/home_controller.rb +50 -6
  66. data/spec/lib/castle/api/approve_device_spec.rb +21 -0
  67. data/spec/lib/castle/api/authenticate_spec.rb +136 -0
  68. data/spec/lib/castle/api/end_impersonation_spec.rb +65 -0
  69. data/spec/lib/castle/api/filter_spec.rb +5 -0
  70. data/spec/lib/castle/api/get_device_spec.rb +19 -0
  71. data/spec/lib/castle/api/get_devices_for_user_spec.rb +19 -0
  72. data/spec/lib/castle/api/log_spec.rb +5 -0
  73. data/spec/lib/castle/api/report_device_spec.rb +21 -0
  74. data/spec/lib/castle/api/risk_spec.rb +5 -0
  75. data/spec/lib/castle/api/start_impersonation_spec.rb +65 -0
  76. data/spec/lib/castle/api/track_spec.rb +72 -0
  77. data/spec/lib/castle/api_spec.rb +14 -15
  78. data/spec/lib/castle/{extractors/client_id_spec.rb → client_id/extract_spec.rb} +6 -15
  79. data/spec/lib/castle/client_spec.rb +108 -93
  80. data/spec/lib/castle/commands/approve_device_spec.rb +24 -0
  81. data/spec/lib/castle/commands/authenticate_spec.rb +15 -31
  82. data/spec/lib/castle/commands/end_impersonation_spec.rb +79 -0
  83. data/spec/lib/castle/commands/filter_spec.rb +99 -0
  84. data/spec/lib/castle/commands/get_device_spec.rb +24 -0
  85. data/spec/lib/castle/commands/{review_spec.rb → get_devices_for_user_spec.rb} +7 -7
  86. data/spec/lib/castle/commands/log_spec.rb +100 -0
  87. data/spec/lib/castle/commands/report_device_spec.rb +24 -0
  88. data/spec/lib/castle/commands/risk_spec.rb +100 -0
  89. data/spec/lib/castle/commands/start_impersonation_spec.rb +79 -0
  90. data/spec/lib/castle/commands/track_spec.rb +14 -34
  91. data/spec/lib/castle/configuration_spec.rb +8 -141
  92. data/spec/lib/castle/context/{default_spec.rb → get_default_spec.rb} +9 -10
  93. data/spec/lib/castle/context/{merger_spec.rb → merge_spec.rb} +1 -1
  94. data/spec/lib/castle/context/prepare_spec.rb +43 -0
  95. data/spec/lib/castle/context/{sanitizer_spec.rb → sanitize_spec.rb} +1 -1
  96. data/spec/lib/castle/core/get_connection_spec.rb +43 -0
  97. data/spec/lib/castle/{api/response_spec.rb → core/process_response_spec.rb} +49 -1
  98. data/spec/lib/castle/core/process_webhook_spec.rb +46 -0
  99. data/spec/lib/castle/core/send_request_spec.rb +77 -0
  100. data/spec/lib/castle/failover/strategy_spec.rb +12 -0
  101. data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +18 -20
  102. data/spec/lib/castle/headers/filter_spec.rb +39 -0
  103. data/spec/lib/castle/headers/format_spec.rb +25 -0
  104. data/spec/lib/castle/{extractors/ip_spec.rb → ips/extract_spec.rb} +27 -8
  105. data/spec/lib/castle/logger_spec.rb +38 -0
  106. data/spec/lib/castle/payload/prepare_spec.rb +55 -0
  107. data/spec/lib/castle/session_spec.rb +65 -0
  108. data/spec/lib/castle/singleton_configuration_spec.rb +14 -0
  109. data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +69 -0
  110. data/spec/lib/castle/utils/{cloner_spec.rb → clone_spec.rb} +3 -3
  111. data/spec/lib/castle/utils/deep_symbolize_keys_spec.rb +50 -0
  112. data/spec/lib/castle/utils/{timestamp_spec.rb → get_timestamp_spec.rb} +1 -1
  113. data/spec/lib/castle/utils/merge_spec.rb +15 -0
  114. data/spec/lib/castle/validators/present_spec.rb +5 -6
  115. data/spec/lib/castle/verdict_spec.rb +9 -0
  116. data/spec/lib/castle/webhooks/verify_spec.rb +53 -0
  117. data/spec/lib/castle_spec.rb +4 -10
  118. data/spec/spec_helper.rb +3 -3
  119. data/spec/support/shared_examples/action_request.rb +152 -0
  120. data/spec/support/shared_examples/configuration.rb +101 -0
  121. metadata +146 -64
  122. data/lib/castle/api/request.rb +0 -42
  123. data/lib/castle/api/session.rb +0 -39
  124. data/lib/castle/commands/identify.rb +0 -23
  125. data/lib/castle/commands/impersonate.rb +0 -26
  126. data/lib/castle/commands/review.rb +0 -14
  127. data/lib/castle/events.rb +0 -49
  128. data/lib/castle/extractors/headers.rb +0 -45
  129. data/lib/castle/failover_auth_response.rb +0 -21
  130. data/lib/castle/headers_filter.rb +0 -35
  131. data/lib/castle/headers_formatter.rb +0 -22
  132. data/lib/castle/review.rb +0 -11
  133. data/lib/castle/utils.rb +0 -55
  134. data/lib/castle/utils/cloner.rb +0 -11
  135. data/lib/castle/utils/timestamp.rb +0 -12
  136. data/spec/lib/castle/api/request_spec.rb +0 -72
  137. data/spec/lib/castle/commands/identify_spec.rb +0 -88
  138. data/spec/lib/castle/commands/impersonate_spec.rb +0 -107
  139. data/spec/lib/castle/events_spec.rb +0 -5
  140. data/spec/lib/castle/headers_filter_spec.rb +0 -37
  141. data/spec/lib/castle/headers_formatter_spec.rb +0 -25
  142. data/spec/lib/castle/review_spec.rb +0 -19
  143. data/spec/lib/castle/utils/merger_spec.rb +0 -13
  144. data/spec/lib/castle/utils_spec.rb +0 -156
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ # module for logger handling
5
+ module Logger
6
+ class << self
7
+ # @param message [String]
8
+ # @param data [String]
9
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
10
+ def call(message, data = nil, config = nil)
11
+ logger = (config || Castle.config).logger
12
+
13
+ return unless logger
14
+
15
+ logger.info("[CASTLE] #{message} #{data}".strip)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Payload
5
+ # prepares payload based on the request
6
+ module Prepare
7
+ class << self
8
+ # @param payload_options [Hash]
9
+ # @param request [Request]
10
+ # @param options [Hash] required for context preparation
11
+ # @return [Hash]
12
+ def call(payload_options, request, options = {})
13
+ context = Castle::Context::Prepare.call(request, payload_options.merge(options))
14
+
15
+ payload =
16
+ Castle::Utils::DeepSymbolizeKeys.call(payload_options || {}).merge(context: context)
17
+ payload[:timestamp] ||= Castle::Utils::GetTimestamp.call
18
+
19
+ warn '[DEPRECATION] use user_traits instead of traits key' if payload.key?(:traits)
20
+
21
+ payload
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -4,8 +4,13 @@ require 'openssl'
4
4
 
5
5
  module Castle
6
6
  module SecureMode
7
- def self.signature(user_id)
8
- OpenSSL::HMAC.hexdigest('sha256', Castle.config.api_secret, user_id.to_s)
7
+ class << self
8
+ # @param user_id [String]
9
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
10
+ def signature(user_id, config = nil)
11
+ config ||= Castle.config
12
+ OpenSSL::HMAC.hexdigest('sha256', config.api_secret, user_id.to_s)
13
+ end
9
14
  end
10
15
  end
11
16
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ # this module uses the Connection object
5
+ # and provides start method for persistent connection usage
6
+ # when there is a need of sending multiple requests at once
7
+ module Session
8
+ HTTPS_SCHEME = 'https'
9
+
10
+ class << self
11
+ def call(&block)
12
+ return unless block_given?
13
+
14
+ Castle::Core::GetConnection.call.start(&block)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Castle
6
+ class SingletonConfiguration < Configuration
7
+ include Singleton
8
+ end
9
+ 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
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ module CleanInvalidChars
6
+ class << self
7
+ def call(arg)
8
+ case arg
9
+ when ::String
10
+ arg.encode('UTF-8', invalid: :replace, undef: :replace)
11
+ when ::Hash
12
+ arg.transform_values { |v| Castle::Utils::CleanInvalidChars.call(v) }
13
+ when ::Array
14
+ arg.map { |el| Castle::Utils::CleanInvalidChars.call(el) }
15
+ else
16
+ arg
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ # Clones any object
6
+ class Clone
7
+ class << self
8
+ # Returns a cloned object of any type
9
+ def call(object)
10
+ Marshal.load(Marshal.dump(object))
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ module DeepSymbolizeKeys
6
+ class << self
7
+ # Returns a new hash with all keys converted to symbols, as long as
8
+ # they respond to +to_sym+. This includes the keys from the root hash
9
+ # and from all nested hashes and arrays.
10
+ #
11
+ # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
12
+ #
13
+ # Castle::Hash.deep_symbolize_keys(hash)
14
+ # # => {:person=>{:name=>"Rob", :age=>"28"}}
15
+ def call(object, &block)
16
+ case object
17
+ when Hash
18
+ object.each_with_object({}) do |(key, value), result|
19
+ result[key.to_sym] = Castle::Utils::DeepSymbolizeKeys.call(value, &block)
20
+ end
21
+ when Array
22
+ object.map { |e| Castle::Utils::DeepSymbolizeKeys.call(e, &block) }
23
+ else
24
+ object
25
+ end
26
+ end
27
+
28
+ def call!(object, &block)
29
+ case object
30
+ when Hash
31
+ object.each_key do |key|
32
+ value = object.delete(key)
33
+ object[key.to_sym] = Castle::Utils::DeepSymbolizeKeys.call!(value, &block)
34
+ end
35
+ object
36
+ when Array
37
+ object.map! { |e| Castle::Utils::DeepSymbolizeKeys.call!(e, &block) }
38
+ else
39
+ object
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ # Generates a timestamp
6
+ class GetTimestamp
7
+ class << self
8
+ # Returns current time as ISO8601 formatted string
9
+ def call
10
+ Time.now.utc.iso8601(3)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Castle
4
4
  module Utils
5
- class Merger
5
+ class Merge
6
6
  def self.call(base, extra)
7
- base_s = Castle::Utils.deep_symbolize_keys(base)
8
- extra_s = Castle::Utils.deep_symbolize_keys(extra)
7
+ base_s = Castle::Utils::DeepSymbolizeKeys.call(base)
8
+ extra_s = Castle::Utils::DeepSymbolizeKeys.call(extra)
9
9
 
10
10
  extra_s.each do |name, value|
11
11
  if value.nil?
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ # Code borrowed from ActiveSupport
6
+ class SecureCompare
7
+ class << self
8
+ # @param str_a [String] first string to be compared
9
+ # @param str_b [String] second string to be compared
10
+ def call(str_a, str_b)
11
+ return false unless str_a.bytesize == str_b.bytesize
12
+
13
+ l = str_a.unpack "C#{str_a.bytesize}"
14
+
15
+ res = 0
16
+ str_b.each_byte { |byte| res |= byte ^ l.shift }
17
+ res.zero?
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Castle
4
4
  module Validators
5
+ # Checks if required keys are supported
5
6
  class NotSupported
6
7
  class << self
7
8
  def call(options, keys)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Castle
4
4
  module Validators
5
+ # Checks if required keys are present
5
6
  class Present
6
7
  class << self
7
8
  def call(options, keys)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ # handles verdict consts
5
+ module Verdict
6
+ # allow
7
+ ALLOW = 'allow'
8
+
9
+ # deny
10
+ DENY = 'deny'
11
+
12
+ # challenge
13
+ CHALLENGE = 'challenge'
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castle
4
- VERSION = '4.2.1'
4
+ VERSION = '7.0.0'
5
5
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Webhooks
5
+ # Verify a webhook
6
+ class Verify
7
+ class << self
8
+ # Checks if webhook is valid
9
+ # @param webhook [Request]
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)
14
+ signature = webhook.env['HTTP_X_CASTLE_SIGNATURE']
15
+ verify_signature(signature, expected_signature)
16
+ end
17
+
18
+ private
19
+
20
+ # Computes a webhook signature using provided user_id
21
+ # @param webhook [Request]
22
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration]
23
+ # @return [String]
24
+ def compute_signature(webhook, config)
25
+ Base64.encode64(
26
+ OpenSSL::HMAC.digest(
27
+ OpenSSL::Digest.new('sha256'),
28
+ config.api_secret,
29
+ Castle::Core::ProcessWebhook.call(webhook, config)
30
+ )
31
+ ).strip
32
+ end
33
+
34
+ # Check if the signatures are matching
35
+ # @param signature [String] first signature to be compared
36
+ # @param expected_signature [String] second signature to be compared
37
+ def verify_signature(signature, expected_signature)
38
+ return if Castle::Utils::SecureCompare.call(signature, expected_signature)
39
+
40
+ raise Castle::WebhookVerificationError, 'Signature not matching the expected signature'
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -4,21 +4,25 @@ require 'spec_helper'
4
4
  require_relative 'support/all'
5
5
 
6
6
  RSpec.describe HomeController, type: :request do
7
- describe '#index' do
7
+ context 'with index pages' do
8
8
  let(: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
- 'origin' => 'web',
20
23
  'headers' => {
21
- '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',
22
26
  'Authorization' => true,
23
27
  'Content-Length' => '0',
24
28
  'Cookie' => true,
@@ -36,26 +40,50 @@ RSpec.describe HomeController, type: :request do
36
40
  end
37
41
  let(:now) { Time.now }
38
42
  let(:headers) do
39
- {
40
- 'HTTP_AUTHORIZATION' => 'Basic 123',
41
- 'HTTP_X_FORWARDED_FOR' => '5.5.5.5, 1.2.3.4'
42
- }
43
+ { 'HTTP_AUTHORIZATION' => 'Basic 123', 'HTTP_X_FORWARDED_FOR' => '5.5.5.5, 1.2.3.4' }
43
44
  end
44
45
 
45
46
  before do
46
47
  Timecop.freeze(now)
47
48
  stub_request(:post, 'https://api.castle.io/v1/track')
48
- get '/', headers: headers
49
49
  end
50
50
 
51
51
  after { Timecop.return }
52
52
 
53
- it do
54
- assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
55
- JSON.parse(req.body) == request
53
+ describe '#index1' do
54
+ before { get '/index1', headers: headers }
55
+
56
+ it do
57
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
58
+ JSON.parse(req.body) == request
59
+ end
56
60
  end
61
+
62
+ it { expect(response).to be_successful }
57
63
  end
58
64
 
59
- it { expect(response).to be_successful }
65
+ describe '#index2' do
66
+ before { get '/index2', headers: headers }
67
+
68
+ it do
69
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
70
+ JSON.parse(req.body) == request
71
+ end
72
+ end
73
+
74
+ it { expect(response).to be_successful }
75
+ end
76
+
77
+ describe '#index3' do
78
+ before { get '/index3', headers: headers }
79
+
80
+ it do
81
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
82
+ JSON.parse(req.body) == request
83
+ end
84
+ end
85
+
86
+ it { expect(response).to be_successful }
87
+ end
60
88
  end
61
89
  end