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
@@ -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