castle-rb 5.0.0 → 7.1.1

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 +113 -39
  3. data/lib/castle.rb +49 -29
  4. data/lib/castle/api.rb +20 -16
  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 +74 -68
  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 +22 -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 +22 -0
  25. data/lib/castle/commands/report_device.rb +17 -0
  26. data/lib/castle/commands/risk.rb +22 -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 +31 -23
  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 +28 -0
  40. data/lib/castle/failover/strategy.rb +23 -0
  41. data/lib/castle/{extractors/headers.rb → headers/extract.rb} +8 -6
  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} +11 -7
  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 +110 -92
  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 +72 -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 +73 -0
  87. data/spec/lib/castle/commands/report_device_spec.rb +24 -0
  88. data/spec/lib/castle/commands/risk_spec.rb +73 -0
  89. data/spec/lib/castle/commands/{impersonate_spec.rb → start_impersonation_spec.rb} +13 -41
  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/{api/request_spec.rb → core/send_request_spec.rb} +16 -37
  100. data/spec/lib/castle/failover/strategy_spec.rb +12 -0
  101. data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +7 -9
  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} +5 -14
  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 +155 -0
  120. data/spec/support/shared_examples/configuration.rb +101 -0
  121. metadata +144 -67
  122. data/lib/castle/api/connection.rb +0 -24
  123. data/lib/castle/api/request.rb +0 -42
  124. data/lib/castle/api/session.rb +0 -20
  125. data/lib/castle/commands/identify.rb +0 -23
  126. data/lib/castle/commands/impersonate.rb +0 -26
  127. data/lib/castle/commands/review.rb +0 -14
  128. data/lib/castle/events.rb +0 -49
  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/connection_spec.rb +0 -59
  137. data/spec/lib/castle/api/session_spec.rb +0 -86
  138. data/spec/lib/castle/commands/identify_spec.rb +0 -88
  139. data/spec/lib/castle/events_spec.rb +0 -5
  140. data/spec/lib/castle/headers_filter_spec.rb +0 -38
  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
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- module API
5
- # this module returns a new configured Net::HTTP object
6
- module Connection
7
- HTTPS_SCHEME = 'https'
8
-
9
- class << self
10
- def call
11
- http = Net::HTTP.new(Castle.config.url.host, Castle.config.url.port)
12
- http.read_timeout = Castle.config.request_timeout / 1000.0
13
-
14
- if Castle.config.url.scheme == HTTPS_SCHEME
15
- http.use_ssl = true
16
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
17
- end
18
-
19
- http
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- module API
5
- # this class is responsible for making requests to api
6
- module Request
7
- # Default headers that we add to passed ones
8
- DEFAULT_HEADERS = {
9
- 'Content-Type' => 'application/json'
10
- }.freeze
11
-
12
- private_constant :DEFAULT_HEADERS
13
-
14
- class << self
15
- def call(command, api_secret, headers, http = nil)
16
- (http || Castle::API::Connection.call).request(
17
- build(
18
- command,
19
- headers.merge(DEFAULT_HEADERS),
20
- api_secret
21
- )
22
- )
23
- end
24
-
25
- def build(command, headers, api_secret)
26
- request_obj = Net::HTTP.const_get(
27
- command.method.to_s.capitalize
28
- ).new("#{Castle.config.url.path}/#{command.path}", headers)
29
-
30
- unless command.method == :get
31
- request_obj.body = ::Castle::Utils.replace_invalid_characters(
32
- command.data
33
- ).to_json
34
- end
35
-
36
- request_obj.basic_auth('', api_secret)
37
- request_obj
38
- end
39
- end
40
- end
41
- end
42
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- module API
5
- # this module uses the Connection object
6
- # and provides start method for persistent connection usage
7
- # when there is a need of sending multiple requests at once
8
- module Session
9
- HTTPS_SCHEME = 'https'
10
-
11
- class << self
12
- def call(&block)
13
- return unless block_given?
14
-
15
- Connection.call.start(&block)
16
- end
17
- end
18
- end
19
- end
20
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- module Commands
5
- class Identify
6
- def initialize(context)
7
- @context = context
8
- end
9
-
10
- def build(options = {})
11
- Castle::Validators::NotSupported.call(options, %i[properties])
12
- context = Castle::Context::Merger.call(@context, options[:context])
13
- context = Castle::Context::Sanitizer.call(context)
14
-
15
- Castle::Command.new(
16
- 'identify',
17
- options.merge(context: context, sent_at: Castle::Utils::Timestamp.call),
18
- :post
19
- )
20
- end
21
- end
22
- end
23
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- module Commands
5
- # builder for impersonate command
6
- class Impersonate
7
- def initialize(context)
8
- @context = context
9
- end
10
-
11
- def build(options = {})
12
- Castle::Validators::Present.call(options, %i[user_id])
13
- context = Castle::Context::Merger.call(@context, options[:context])
14
- context = Castle::Context::Sanitizer.call(context)
15
-
16
- Castle::Validators::Present.call(context, %i[user_agent ip])
17
-
18
- Castle::Command.new(
19
- 'impersonate',
20
- options.merge(context: context, sent_at: Castle::Utils::Timestamp.call),
21
- options[:reset] ? :delete : :post
22
- )
23
- end
24
- end
25
- end
26
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- module Commands
5
- class Review
6
- class << self
7
- def build(review_id)
8
- Castle::Validators::Present.call({ review_id: review_id }, %i[review_id])
9
- Castle::Command.new("reviews/#{review_id}", nil, :get)
10
- end
11
- end
12
- end
13
- end
14
- end
data/lib/castle/events.rb DELETED
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- # list of events based on https://docs.castle.io/api_reference/#list-of-recognized-events
5
- module Events
6
- # Record when a user succesfully logs in.
7
- LOGIN_SUCCEEDED = '$login.succeeded'
8
- # Record when a user failed to log in.
9
- LOGIN_FAILED = '$login.failed'
10
- # Record when a user logs out.
11
- LOGOUT_SUCCEEDED = '$logout.succeeded'
12
- # Record when a user updated their profile (including password, email, phone, etc).
13
- PROFILE_UPDATE_SUCCEEDED = '$profile_update.succeeded'
14
- # Record errors when updating profile.
15
- PROFILE_UPDATE_FAILED = '$profile_update.failed'
16
- # Capture account creation, both when a user signs up as well as when created manually
17
- # by an administrator.
18
- REGISTRATION_SUCCEEDED = '$registration.succeeded'
19
- # Record when an account failed to be created.
20
- REGISTRATION_FAILED = '$registration.failed'
21
- # The user completed all of the steps in the password reset process and the password was
22
- # successfully reset.Password resets do not required knowledge of the current password.
23
- PASSWORD_RESET_SUCCEEDED = '$password_reset.succeeded'
24
- # Use to record when a user failed to reset their password.
25
- PASSWORD_RESET_FAILED = '$password_reset.failed'
26
- # The user successfully requested a password reset.
27
- PASSWORD_RESET_REQUEST_SUCCCEEDED = '$password_reset_request.succeeded'
28
- # The user failed to request a password reset.
29
- PASSWORD_RESET_REQUEST_FAILED = '$password_reset_request.failed'
30
- # User account has been reset.
31
- INCIDENT_MITIGATED = '$incident.mitigated'
32
- # User confirmed malicious activity.
33
- REVIEW_ESCALATED = '$review.escalated'
34
- # User confirmed safe activity.
35
- REVIEW_RESOLVED = '$review.resolved'
36
- # Record when a user is prompted with additional verification, such as two-factor
37
- # authentication or a captcha.
38
- CHALLENGE_REQUESTED = '$challenge.requested'
39
- # Record when additional verification was successful.
40
- CHALLENGE_SUCCEEDED = '$challenge.succeeded'
41
- # Record when additional verification failed.
42
- CHALLENGE_FAILED = '$challenge.failed'
43
- # Record when a user attempts an in-app transaction, such as a purchase or withdrawal.
44
- TRANSACTION_ATTEMPTED = '$transaction.attempted'
45
- # Record when a user session is extended, or use any time you want
46
- # to re-authenticate a user mid-session.
47
- SESSION_EXTENDED = '$session.extended'
48
- end
49
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- # generate failover authentication response
5
- class FailoverAuthResponse
6
- def initialize(user_id, strategy: Castle.config.failover_strategy, reason:)
7
- @strategy = strategy
8
- @reason = reason
9
- @user_id = user_id
10
- end
11
-
12
- def generate
13
- {
14
- action: @strategy.to_s,
15
- user_id: @user_id,
16
- failover: true,
17
- failover_reason: @reason
18
- }
19
- end
20
- end
21
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- # used for preparing valuable headers list
5
- class HeadersFilter
6
- # headers filter
7
- # HTTP_ - this is how Rack prefixes incoming HTTP headers
8
- # CONTENT_LENGTH - for responses without Content-Length or Transfer-Encoding header
9
- # REMOTE_ADDR - ip address header returned by web server
10
- VALUABLE_HEADERS = /^
11
- HTTP(?:_|-).*|
12
- CONTENT(?:_|-)LENGTH|
13
- REMOTE(?:_|-)ADDR
14
- $/xi.freeze
15
-
16
- private_constant :VALUABLE_HEADERS
17
-
18
- # @param request [Rack::Request]
19
- def initialize(request)
20
- @request_env = request.env
21
- @formatter = HeadersFormatter
22
- end
23
-
24
- # Serialize HTTP headers
25
- # @return [Hash]
26
- def call
27
- @request_env.keys.each_with_object({}) do |header_name, acc|
28
- next unless header_name.match(VALUABLE_HEADERS)
29
-
30
- formatted_name = @formatter.call(header_name)
31
- acc[formatted_name] = @request_env[header_name]
32
- end
33
- end
34
- end
35
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- # formats header name
5
- class HeadersFormatter
6
- class << self
7
- # @param header [String]
8
- # @return [String]
9
- def call(header)
10
- format(header.to_s.gsub(/^HTTP(?:_|-)/i, ''))
11
- end
12
-
13
- private
14
-
15
- # @param header [String]
16
- # @return [String]
17
- def format(header)
18
- header.split(/_|-/).map(&:capitalize).join('-')
19
- end
20
- end
21
- end
22
- end
data/lib/castle/review.rb DELETED
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- class Review
5
- def self.retrieve(review_id)
6
- Castle::API.request(
7
- Castle::Commands::Review.build(review_id)
8
- )
9
- end
10
- end
11
- end
data/lib/castle/utils.rb DELETED
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- module Utils
5
- class << self
6
- # Returns a new hash with all keys converted to symbols, as long as
7
- # they respond to +to_sym+. This includes the keys from the root hash
8
- # and from all nested hashes and arrays.
9
- #
10
- # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
11
- #
12
- # Castle::Hash.deep_symbolize_keys(hash)
13
- # # => {:person=>{:name=>"Rob", :age=>"28"}}
14
- def deep_symbolize_keys(object, &block)
15
- case object
16
- when Hash
17
- object.each_with_object({}) do |(key, value), result|
18
- result[key.to_sym] = deep_symbolize_keys(value, &block)
19
- end
20
- when Array
21
- object.map { |e| deep_symbolize_keys(e, &block) }
22
- else
23
- object
24
- end
25
- end
26
-
27
- def deep_symbolize_keys!(object, &block)
28
- case object
29
- when Hash
30
- object.keys.each do |key|
31
- value = object.delete(key)
32
- object[key.to_sym] = deep_symbolize_keys!(value, &block)
33
- end
34
- object
35
- when Array
36
- object.map! { |e| deep_symbolize_keys!(e, &block) }
37
- else
38
- object
39
- end
40
- end
41
-
42
- def replace_invalid_characters(arg)
43
- if arg.is_a?(::String)
44
- arg.encode('UTF-8', invalid: :replace, undef: :replace)
45
- elsif arg.is_a?(::Hash)
46
- arg.each_with_object({}) { |(k, v), h| h[k] = replace_invalid_characters(v) }
47
- elsif arg.is_a?(::Array)
48
- arg.map(&method(:replace_invalid_characters))
49
- else
50
- arg
51
- end
52
- end
53
- end
54
- end
55
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- module Utils
5
- class Cloner
6
- def self.call(object)
7
- Marshal.load(Marshal.dump(object))
8
- end
9
- end
10
- end
11
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- module Utils
5
- # generates proper timestamp
6
- class Timestamp
7
- def self.call
8
- Time.now.utc.iso8601(3)
9
- end
10
- end
11
- end
12
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::API::Connection do
4
- describe '.call' do
5
- subject(:class_call) { described_class.call }
6
-
7
- context 'when ssl false' do
8
- let(:localhost) { 'localhost' }
9
- let(:port) { 3002 }
10
- let(:api_url) { '/test' }
11
-
12
- before do
13
- Castle.config.url = 'http://localhost:3002'
14
-
15
- allow(Net::HTTP)
16
- .to receive(:new)
17
- .with(localhost, port)
18
- .and_call_original
19
- end
20
-
21
- it do
22
- class_call
23
-
24
- expect(Net::HTTP)
25
- .to have_received(:new)
26
- .with(localhost, port)
27
- end
28
-
29
- it do
30
- expect(class_call).to be_an_instance_of(Net::HTTP)
31
- end
32
- end
33
-
34
- context 'when ssl true' do
35
- let(:localhost) { 'localhost' }
36
- let(:port) { 443 }
37
-
38
- before do
39
- Castle.config.url = 'https://localhost'
40
- end
41
-
42
- context 'with block' do
43
- let(:api_url) { '/test' }
44
- let(:request) { Net::HTTP::Get.new(api_url) }
45
-
46
- before do
47
- allow(Net::HTTP)
48
- .to receive(:new)
49
- .with(localhost, port)
50
- .and_call_original
51
- end
52
-
53
- it do
54
- expect(class_call).to be_an_instance_of(Net::HTTP)
55
- end
56
- end
57
- end
58
- end
59
- end