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
@@ -9,11 +9,7 @@ module Castle
9
9
  # @return [Castle::Command]
10
10
  def build(options = {})
11
11
  Castle::Validators::Present.call(options, %i[device_token])
12
- Castle::Command.new(
13
- "devices/#{options[:device_token]}/approve",
14
- nil,
15
- :put
16
- )
12
+ Castle::Command.new("devices/#{options[:device_token]}/approve", nil, :put)
17
13
  end
18
14
  end
19
15
  end
@@ -19,7 +19,7 @@ module Castle
19
19
  :delete
20
20
  )
21
21
  end
22
- end
22
+ end
23
23
  end
24
24
  end
25
25
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Commands
5
+ # Generates the payload for the filter request
6
+ class Filter
7
+ class << self
8
+ # @param options [Hash]
9
+ # @return [Castle::Command]
10
+ def build(options = {})
11
+ Castle::Validators::Present.call(options, %i[event])
12
+ context = Castle::Context::Sanitize.call(options[:context])
13
+
14
+ Castle::Command.new(
15
+ 'filter',
16
+ options.merge(context: context, sent_at: Castle::Utils::GetTimestamp.call),
17
+ :post
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -9,11 +9,7 @@ module Castle
9
9
  # @return [Castle::Command]
10
10
  def build(options = {})
11
11
  Castle::Validators::Present.call(options, %i[device_token])
12
- Castle::Command.new(
13
- "devices/#{options[:device_token]}",
14
- nil,
15
- :get
16
- )
12
+ Castle::Command.new("devices/#{options[:device_token]}", nil, :get)
17
13
  end
18
14
  end
19
15
  end
@@ -9,11 +9,7 @@ module Castle
9
9
  # @return [Castle::Command]
10
10
  def build(options = {})
11
11
  Castle::Validators::Present.call(options, %i[user_id])
12
- Castle::Command.new(
13
- "users/#{options[:user_id]}/devices",
14
- nil,
15
- :get
16
- )
12
+ Castle::Command.new("users/#{options[:user_id]}/devices", nil, :get)
17
13
  end
18
14
  end
19
15
  end
@@ -2,16 +2,17 @@
2
2
 
3
3
  module Castle
4
4
  module Commands
5
- class Identify
5
+ # Generates the payload for the log request
6
+ class Log
6
7
  class << self
7
8
  # @param options [Hash]
8
9
  # @return [Castle::Command]
9
10
  def build(options = {})
10
- Castle::Validators::NotSupported.call(options, %i[properties])
11
+ Castle::Validators::Present.call(options, %i[event])
11
12
  context = Castle::Context::Sanitize.call(options[:context])
12
13
 
13
14
  Castle::Command.new(
14
- 'identify',
15
+ 'log',
15
16
  options.merge(context: context, sent_at: Castle::Utils::GetTimestamp.call),
16
17
  :post
17
18
  )
@@ -9,11 +9,7 @@ module Castle
9
9
  # @return [Castle::Command]
10
10
  def build(options = {})
11
11
  Castle::Validators::Present.call(options, %i[device_token])
12
- Castle::Command.new(
13
- "devices/#{options[:device_token]}/report",
14
- nil,
15
- :put
16
- )
12
+ Castle::Command.new("devices/#{options[:device_token]}/report", nil, :put)
17
13
  end
18
14
  end
19
15
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Commands
5
+ # Generates the payload for the risk request
6
+ class Risk
7
+ class << self
8
+ # @param options [Hash]
9
+ # @return [Castle::Command]
10
+ def build(options = {})
11
+ Castle::Validators::Present.call(options, %i[event])
12
+ context = Castle::Context::Sanitize.call(options[:context])
13
+
14
+ Castle::Command.new(
15
+ 'risk',
16
+ options.merge(context: context, sent_at: Castle::Utils::GetTimestamp.call),
17
+ :post
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -19,7 +19,7 @@ module Castle
19
19
  :post
20
20
  )
21
21
  end
22
- end
22
+ end
23
23
  end
24
24
  end
25
25
  end
@@ -8,15 +8,18 @@ module Castle
8
8
  # API endpoint
9
9
  BASE_URL = 'https://api.castle.io/v1'
10
10
  REQUEST_TIMEOUT = 1000 # in milliseconds
11
+
11
12
  # regexp of trusted proxies which is always appended to the trusted proxy list
12
- TRUSTED_PROXIES = [/
13
+ TRUSTED_PROXIES = [
14
+ /
13
15
  \A127\.0\.0\.1\Z|
14
16
  \A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|
15
17
  \A::1\Z|\Afd[0-9a-f]{2}:.+|
16
18
  \Alocalhost\Z|
17
19
  \Aunix\Z|
18
20
  \Aunix:
19
- /ix].freeze
21
+ /ix
22
+ ].freeze
20
23
 
21
24
  # @note this value is not assigned as we don't recommend using a allowlist. If you need to use
22
25
  # one, this constant is provided as a good default.
@@ -47,8 +50,14 @@ module Castle
47
50
  ].freeze
48
51
 
49
52
  attr_accessor :request_timeout, :trust_proxy_chain, :logger
50
- attr_reader :api_secret, :allowlisted, :denylisted, :failover_strategy, :ip_headers,
51
- :trusted_proxies, :trusted_proxy_depth, :base_url
53
+ attr_reader :api_secret,
54
+ :allowlisted,
55
+ :denylisted,
56
+ :failover_strategy,
57
+ :ip_headers,
58
+ :trusted_proxies,
59
+ :trusted_proxy_depth,
60
+ :base_url
52
61
 
53
62
  def initialize
54
63
  @header_format = Castle::Headers::Format
@@ -96,7 +105,9 @@ module Castle
96
105
  # sets trusted proxies
97
106
  # @param value [Array<String,Regexp>]
98
107
  def trusted_proxies=(value)
99
- raise Castle::ConfigurationError, 'trusted proxies must be an Array' unless value.is_a?(Array)
108
+ unless value.is_a?(Array)
109
+ raise Castle::ConfigurationError, 'trusted proxies must be an Array'
110
+ end
100
111
 
101
112
  @trusted_proxies = value
102
113
  end
@@ -111,9 +122,8 @@ module Castle
111
122
  end
112
123
 
113
124
  def failover_strategy=(value)
114
- @failover_strategy = Castle::Failover::STRATEGIES.detect do |strategy|
115
- strategy == value.to_sym
116
- end
125
+ @failover_strategy =
126
+ Castle::Failover::STRATEGIES.detect { |strategy| strategy == value.to_sym }
117
127
  raise Castle::ConfigurationError, 'unrecognized failover strategy' if @failover_strategy.nil?
118
128
  end
119
129
 
@@ -8,7 +8,9 @@ module Castle
8
8
 
9
9
  class << self
10
10
  # @param config [Castle::Configuration, Castle::SingletonConfiguration]
11
- def call(config = Castle.config)
11
+ # @return [Net::HTTP]
12
+ def call(config = nil)
13
+ config ||= Castle.config
12
14
  http = Net::HTTP.new(config.base_url.host, config.base_url.port)
13
15
  http.read_timeout = config.request_timeout / 1000.0
14
16
 
@@ -14,10 +14,13 @@ module Castle
14
14
  }.freeze
15
15
 
16
16
  class << self
17
- def call(response)
17
+ # @param response [Response]
18
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
19
+ # @return [Hash]
20
+ def call(response, config = nil)
18
21
  verify!(response)
19
22
 
20
- Castle::Logger.call('response:', response.body.to_s)
23
+ Castle::Logger.call('response:', response.body.to_s, config)
21
24
 
22
25
  return {} if response.body.nil? || response.body.empty?
23
26
 
@@ -7,12 +7,17 @@ module Castle
7
7
  class << self
8
8
  # Checks if webhook is valid
9
9
  # @param webhook [Request]
10
- def call(webhook)
11
- webhook.body.read.tap do |result|
12
- raise Castle::ApiError, 'Invalid webhook from Castle API' if result.blank?
10
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
11
+ # @return [String]
12
+ def call(webhook, config = nil)
13
+ webhook
14
+ .body
15
+ .read
16
+ .tap do |result|
17
+ raise Castle::ApiError, 'Invalid webhook from Castle API' if result.blank?
13
18
 
14
- Castle::Logger.call('webhook:', result.to_s)
15
- end
19
+ Castle::Logger.call('webhook:', result.to_s, config)
20
+ end
16
21
  end
17
22
  end
18
23
  end
@@ -5,9 +5,7 @@ module Castle
5
5
  # this class is responsible for making requests to api
6
6
  module SendRequest
7
7
  # Default headers that we add to passed ones
8
- DEFAULT_HEADERS = {
9
- 'Content-Type' => 'application/json'
10
- }.freeze
8
+ DEFAULT_HEADERS = { 'Content-Type' => 'application/json' }.freeze
11
9
 
12
10
  private_constant :DEFAULT_HEADERS
13
11
 
@@ -15,31 +13,25 @@ module Castle
15
13
  # @param command [String]
16
14
  # @param headers [Hash]
17
15
  # @param http [Net::HTTP]
18
- # @param config [Castle::Configuration, Castle::SingletonConfiguration]
19
- def call(command, headers, http = nil, config = Castle.config)
16
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
17
+ def call(command, headers, http = nil, config = nil)
20
18
  (http || Castle::Core::GetConnection.call).request(
21
- build(
22
- command,
23
- headers.merge(DEFAULT_HEADERS),
24
- config
25
- )
19
+ build(command, headers.merge(DEFAULT_HEADERS), config)
26
20
  )
27
21
  end
28
22
 
29
23
  # @param command [String]
30
24
  # @param headers [Hash]
31
- # @param config [Castle::Configuration, Castle::SingletonConfiguration]
32
- def build(command, headers, config = Castle.config)
25
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
26
+ def build(command, headers, config)
33
27
  url = "#{config.base_url.path}/#{command.path}"
34
28
  request_obj = Net::HTTP.const_get(command.method.to_s.capitalize).new(url, headers)
35
29
 
36
30
  unless command.method == :get
37
- request_obj.body = ::Castle::Utils::CleanInvalidChars.call(
38
- command.data
39
- ).to_json
31
+ request_obj.body = ::Castle::Utils::CleanInvalidChars.call(command.data).to_json
40
32
  end
41
33
 
42
- Castle::Logger.call("#{url}:", request_obj.body)
34
+ Castle::Logger.call("#{url}:", request_obj.body, config)
43
35
 
44
36
  request_obj.basic_auth('', config.api_secret)
45
37
  request_obj
data/lib/castle/errors.rb CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  module Castle
4
4
  # general error
5
- class Error < RuntimeError; end
5
+ class Error < RuntimeError
6
+ end
7
+
6
8
  # Raised when anything is wrong with the request (any unhappy path)
7
9
  # This error indicates that either we would wait too long for a response or something
8
10
  # else happened somewhere in the middle and we weren't able to get the results
@@ -14,30 +16,52 @@ module Castle
14
16
  @reason = reason
15
17
  end
16
18
  end
19
+
17
20
  # security error
18
- class SecurityError < Castle::Error; end
21
+ class SecurityError < Castle::Error
22
+ end
23
+
19
24
  # wrong configuration error
20
- class ConfigurationError < Castle::Error; end
25
+ class ConfigurationError < Castle::Error
26
+ end
27
+
21
28
  # error returned by api
22
- class ApiError < Castle::Error; end
29
+ class ApiError < Castle::Error
30
+ end
31
+
23
32
  # webhook signature verification error
24
- class WebhookVerificationError < Castle::Error; end
33
+ class WebhookVerificationError < Castle::Error
34
+ end
25
35
 
26
36
  # api error bad request 400
27
- class BadRequestError < Castle::ApiError; end
37
+ class BadRequestError < Castle::ApiError
38
+ end
39
+
28
40
  # api error forbidden 403
29
- class ForbiddenError < Castle::ApiError; end
41
+ class ForbiddenError < Castle::ApiError
42
+ end
43
+
30
44
  # api error not found 404
31
- class NotFoundError < Castle::ApiError; end
45
+ class NotFoundError < Castle::ApiError
46
+ end
47
+
32
48
  # api error user unauthorized 419
33
- class UserUnauthorizedError < Castle::ApiError; end
49
+ class UserUnauthorizedError < Castle::ApiError
50
+ end
51
+
34
52
  # api error invalid param 422
35
- class InvalidParametersError < Castle::ApiError; end
53
+ class InvalidParametersError < Castle::ApiError
54
+ end
55
+
36
56
  # api error unauthorized 401
37
- class UnauthorizedError < Castle::ApiError; end
57
+ class UnauthorizedError < Castle::ApiError
58
+ end
59
+
38
60
  # all internal server errors
39
- class InternalServerError < Castle::ApiError; end
61
+ class InternalServerError < Castle::ApiError
62
+ end
40
63
 
41
64
  # impersonation command failed
42
- class ImpersonationFailed < Castle::ApiError; end
65
+ class ImpersonationFailed < Castle::ApiError
66
+ end
43
67
  end
@@ -4,19 +4,14 @@ module Castle
4
4
  module Failover
5
5
  # generate failover authentication response
6
6
  class PrepareResponse
7
- def initialize(user_id, reason:, strategy: Castle.config.failover_strategy)
7
+ def initialize(user_id, reason:, strategy:)
8
8
  @strategy = strategy
9
9
  @reason = reason
10
10
  @user_id = user_id
11
11
  end
12
12
 
13
13
  def call
14
- {
15
- action: @strategy.to_s,
16
- user_id: @user_id,
17
- failover: true,
18
- failover_reason: @reason
19
- }
14
+ { action: @strategy.to_s, user_id: @user_id, failover: true, failover_reason: @reason }
20
15
  end
21
16
  end
22
17
  end
@@ -6,10 +6,13 @@ module Castle
6
6
  module Strategy
7
7
  # allow
8
8
  ALLOW = :allow
9
+
9
10
  # deny
10
11
  DENY = :deny
12
+
11
13
  # challenge
12
14
  CHALLENGE = :challenge
15
+
13
16
  # throw an error
14
17
  THROW = :throw
15
18
  end
@@ -13,11 +13,11 @@ module Castle
13
13
  private_constant :ALWAYS_ALLOWLISTED, :ALWAYS_DENYLISTED
14
14
 
15
15
  # @param headers [Hash]
16
- # @param config [Castle::Configuration, Castle::SingletonConfiguration]
17
- def initialize(headers, config = Castle.config)
16
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
17
+ def initialize(headers, config = nil)
18
18
  @headers = headers
19
- @config = config
20
- @no_allowlist = config.allowlisted.empty?
19
+ @config = config || Castle.config
20
+ @no_allowlist = @config.allowlisted.empty?
21
21
  end
22
22
 
23
23
  # Serialize HTTP headers
@@ -12,7 +12,8 @@ module Castle
12
12
  HTTP(?:_|-).*|
13
13
  CONTENT(?:_|-)LENGTH|
14
14
  REMOTE(?:_|-)ADDR
15
- $/xi.freeze
15
+ $/xi
16
+ .freeze
16
17
 
17
18
  private_constant :VALUABLE_HEADERS
18
19
 
@@ -25,12 +26,14 @@ module Castle
25
26
  # Serialize HTTP headers
26
27
  # @return [Hash]
27
28
  def call
28
- @request_env.keys.each_with_object({}) do |header_name, acc|
29
- next unless header_name.match(VALUABLE_HEADERS)
29
+ @request_env
30
+ .keys
31
+ .each_with_object({}) do |header_name, acc|
32
+ next unless header_name.match(VALUABLE_HEADERS)
30
33
 
31
- formatted_name = @header_format.call(header_name)
32
- acc[formatted_name] = @request_env[header_name]
33
- end
34
+ formatted_name = @header_format.call(header_name)
35
+ acc[formatted_name] = @request_env[header_name]
36
+ end
34
37
  end
35
38
  end
36
39
  end