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