castle-rb 4.1.0 → 6.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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +158 -43
  3. data/lib/castle.rb +46 -21
  4. data/lib/castle/api.rb +24 -12
  5. data/lib/castle/api/approve_device.rb +25 -0
  6. data/lib/castle/api/authenticate.rb +34 -0
  7. data/lib/castle/api/end_impersonation.rb +29 -0
  8. data/lib/castle/api/get_device.rb +25 -0
  9. data/lib/castle/api/get_devices_for_user.rb +25 -0
  10. data/lib/castle/api/identify.rb +26 -0
  11. data/lib/castle/api/report_device.rb +25 -0
  12. data/lib/castle/api/review.rb +24 -0
  13. data/lib/castle/api/start_impersonation.rb +29 -0
  14. data/lib/castle/api/track.rb +26 -0
  15. data/lib/castle/client.rb +52 -45
  16. data/lib/castle/{extractors/client_id.rb → client_id/extract.rb} +2 -2
  17. data/lib/castle/commands/approve_device.rb +21 -0
  18. data/lib/castle/commands/authenticate.rb +13 -13
  19. data/lib/castle/commands/end_impersonation.rb +25 -0
  20. data/lib/castle/commands/get_device.rb +21 -0
  21. data/lib/castle/commands/get_devices_for_user.rb +21 -0
  22. data/lib/castle/commands/identify.rb +12 -13
  23. data/lib/castle/commands/report_device.rb +21 -0
  24. data/lib/castle/commands/review.rb +6 -3
  25. data/lib/castle/commands/start_impersonation.rb +25 -0
  26. data/lib/castle/commands/track.rb +12 -13
  27. data/lib/castle/configuration.rb +45 -28
  28. data/lib/castle/context/{default.rb → get_default.rb} +5 -6
  29. data/lib/castle/context/{merger.rb → merge.rb} +3 -3
  30. data/lib/castle/context/prepare.rb +18 -0
  31. data/lib/castle/context/{sanitizer.rb → sanitize.rb} +1 -1
  32. data/lib/castle/core/get_connection.rb +25 -0
  33. data/lib/castle/{api/response.rb → core/process_response.rb} +4 -2
  34. data/lib/castle/core/process_webhook.rb +20 -0
  35. data/lib/castle/core/send_request.rb +50 -0
  36. data/lib/castle/errors.rb +2 -0
  37. data/lib/castle/events.rb +1 -1
  38. data/lib/castle/failover/prepare_response.rb +23 -0
  39. data/lib/castle/failover/strategy.rb +20 -0
  40. data/lib/castle/headers/extract.rb +47 -0
  41. data/lib/castle/headers/filter.rb +37 -0
  42. data/lib/castle/headers/format.rb +24 -0
  43. data/lib/castle/ip/extract.rb +83 -0
  44. data/lib/castle/logger.rb +19 -0
  45. data/lib/castle/payload/prepare.rb +27 -0
  46. data/lib/castle/secure_mode.rb +6 -2
  47. data/lib/castle/session.rb +18 -0
  48. data/lib/castle/singleton_configuration.rb +9 -0
  49. data/lib/castle/utils/clean_invalid_chars.rb +24 -0
  50. data/lib/castle/utils/clone.rb +15 -0
  51. data/lib/castle/utils/deep_symbolize_keys.rb +45 -0
  52. data/lib/castle/utils/get_timestamp.rb +15 -0
  53. data/lib/castle/utils/{merger.rb → merge.rb} +3 -3
  54. data/lib/castle/utils/secure_compare.rb +22 -0
  55. data/lib/castle/validators/not_supported.rb +1 -0
  56. data/lib/castle/validators/present.rb +1 -0
  57. data/lib/castle/verdict.rb +13 -0
  58. data/lib/castle/version.rb +1 -1
  59. data/lib/castle/webhooks/verify.rb +43 -0
  60. data/spec/integration/rails/rails_spec.rb +33 -7
  61. data/spec/integration/rails/support/application.rb +3 -1
  62. data/spec/integration/rails/support/home_controller.rb +47 -5
  63. data/spec/lib/castle/api/approve_device_spec.rb +21 -0
  64. data/spec/lib/castle/api/authenticate_spec.rb +140 -0
  65. data/spec/lib/castle/api/end_impersonation_spec.rb +59 -0
  66. data/spec/lib/castle/api/get_device_spec.rb +19 -0
  67. data/spec/lib/castle/api/get_devices_for_user_spec.rb +19 -0
  68. data/spec/lib/castle/api/identify_spec.rb +68 -0
  69. data/spec/lib/castle/api/report_device_spec.rb +21 -0
  70. data/spec/lib/castle/{review_spec.rb → api/review_spec.rb} +3 -3
  71. data/spec/lib/castle/api/start_impersonation_spec.rb +59 -0
  72. data/spec/lib/castle/api/track_spec.rb +68 -0
  73. data/spec/lib/castle/api_spec.rb +16 -1
  74. data/spec/lib/castle/{extractors/client_id_spec.rb → client_id/extract_spec.rb} +2 -2
  75. data/spec/lib/castle/client_spec.rb +41 -23
  76. data/spec/lib/castle/commands/approve_device_spec.rb +24 -0
  77. data/spec/lib/castle/commands/authenticate_spec.rb +7 -16
  78. data/spec/lib/castle/commands/end_impersonation_spec.rb +82 -0
  79. data/spec/lib/castle/commands/get_device_spec.rb +24 -0
  80. data/spec/lib/castle/commands/get_devices_for_user_spec.rb +24 -0
  81. data/spec/lib/castle/commands/identify_spec.rb +5 -16
  82. data/spec/lib/castle/commands/report_device_spec.rb +24 -0
  83. data/spec/lib/castle/commands/review_spec.rb +1 -1
  84. data/spec/lib/castle/commands/{impersonate_spec.rb → start_impersonation_spec.rb} +9 -34
  85. data/spec/lib/castle/commands/track_spec.rb +5 -16
  86. data/spec/lib/castle/configuration_spec.rb +9 -138
  87. data/spec/lib/castle/context/{default_spec.rb → get_default_spec.rb} +1 -2
  88. data/spec/lib/castle/context/{merger_spec.rb → merge_spec.rb} +1 -1
  89. data/spec/lib/castle/context/prepare_spec.rb +44 -0
  90. data/spec/lib/castle/context/{sanitizer_spec.rb → sanitize_spec.rb} +1 -1
  91. data/spec/lib/castle/core/get_connection_spec.rb +59 -0
  92. data/spec/lib/castle/{api/response_spec.rb → core/process_response_spec.rb} +56 -1
  93. data/spec/lib/castle/core/process_webhook_spec.rb +46 -0
  94. data/spec/lib/castle/core/send_request_spec.rb +102 -0
  95. data/spec/lib/castle/failover/strategy_spec.rb +12 -0
  96. data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +18 -18
  97. data/spec/lib/castle/{headers_filter_spec.rb → headers/filter_spec.rb} +6 -5
  98. data/spec/lib/castle/headers/format_spec.rb +25 -0
  99. data/spec/lib/castle/{extractors/ip_spec.rb → ip/extract_spec.rb} +35 -7
  100. data/spec/lib/castle/logger_spec.rb +42 -0
  101. data/spec/lib/castle/payload/prepare_spec.rb +54 -0
  102. data/spec/lib/castle/session_spec.rb +88 -0
  103. data/spec/lib/castle/singleton_configuration_spec.rb +18 -0
  104. data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +69 -0
  105. data/spec/lib/castle/utils/{cloner_spec.rb → clone_spec.rb} +3 -3
  106. data/spec/lib/castle/utils/deep_symbolize_keys_spec.rb +50 -0
  107. data/spec/lib/castle/utils/{timestamp_spec.rb → get_timestamp_spec.rb} +1 -1
  108. data/spec/lib/castle/utils/{merger_spec.rb → merge_spec.rb} +3 -3
  109. data/spec/lib/castle/verdict_spec.rb +9 -0
  110. data/spec/lib/castle/webhooks/verify_spec.rb +69 -0
  111. data/spec/spec_helper.rb +2 -0
  112. data/spec/support/shared_examples/configuration.rb +129 -0
  113. metadata +133 -56
  114. data/lib/castle/api/request.rb +0 -42
  115. data/lib/castle/api/session.rb +0 -39
  116. data/lib/castle/commands/impersonate.rb +0 -26
  117. data/lib/castle/extractors/headers.rb +0 -45
  118. data/lib/castle/extractors/ip.rb +0 -68
  119. data/lib/castle/failover_auth_response.rb +0 -21
  120. data/lib/castle/headers_filter.rb +0 -35
  121. data/lib/castle/headers_formatter.rb +0 -22
  122. data/lib/castle/review.rb +0 -11
  123. data/lib/castle/utils.rb +0 -55
  124. data/lib/castle/utils/cloner.rb +0 -11
  125. data/lib/castle/utils/timestamp.rb +0 -12
  126. data/spec/lib/castle/api/request_spec.rb +0 -72
  127. data/spec/lib/castle/headers_formatter_spec.rb +0 -25
  128. data/spec/lib/castle/utils_spec.rb +0 -156
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ module Authenticate
6
+ class << self
7
+ # @param options [Hash]
8
+ # return [Hash]
9
+ def call(options = {})
10
+ unless options[:no_symbolize]
11
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
12
+ end
13
+ options.delete(:no_symbolize)
14
+ http = options.delete(:http)
15
+ config = options.delete(:config) || Castle.config
16
+
17
+ response = Castle::API.call(
18
+ Castle::Commands::Authenticate.build(options),
19
+ {},
20
+ http,
21
+ config
22
+ )
23
+ response.merge(failover: false, failover_reason: nil)
24
+ rescue Castle::RequestError, Castle::InternalServerError => e
25
+ unless config.failover_strategy == :throw
26
+ return Castle::Failover::PrepareResponse.new(options[:user_id], reason: e.to_s).call
27
+ end
28
+
29
+ raise e
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ # Sends DELETE impersonate request
6
+ module EndImpersonation
7
+ class << self
8
+ # @param options [Hash]
9
+ def call(options = {})
10
+ unless options[:no_symbolize]
11
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
12
+ end
13
+ options.delete(:no_symbolize)
14
+ http = options.delete(:http)
15
+ config = options.delete(:config) || Castle.config
16
+
17
+ Castle::API.call(
18
+ Castle::Commands::EndImpersonation.build(options),
19
+ {},
20
+ http,
21
+ config
22
+ ).tap do |response|
23
+ raise Castle::ImpersonationFailed unless response[:success]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ # Sends GET devices/#{device_token} request
6
+ module GetDevice
7
+ class << self
8
+ # @param options [Hash]
9
+ # return [Hash]
10
+ def call(options = {})
11
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
12
+ http = options.delete(:http)
13
+ config = options.delete(:config) || Castle.config
14
+
15
+ Castle::API.call(
16
+ Castle::Commands::GetDevice.build(options),
17
+ {},
18
+ http,
19
+ config
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ # Sends GET users/#{user_id}/devices request
6
+ module GetDevicesForUser
7
+ class << self
8
+ # @param options [Hash]
9
+ # return [Hash]
10
+ def call(options = {})
11
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
12
+ http = options.delete(:http)
13
+ config = options.delete(:config) || Castle.config
14
+
15
+ Castle::API.call(
16
+ Castle::Commands::GetDevicesForUser.build(options),
17
+ {},
18
+ http,
19
+ config
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ module Identify
6
+ class << self
7
+ # @param options [Hash]
8
+ def call(options = {})
9
+ unless options[:no_symbolize]
10
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
11
+ end
12
+ options.delete(:no_symbolize)
13
+ http = options.delete(:http)
14
+ config = options.delete(:config) || Castle.config
15
+
16
+ Castle::API.call(
17
+ Castle::Commands::Identify.build(options),
18
+ {},
19
+ http,
20
+ config
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ # Sends PUT devices/#{device_token}/report request
6
+ module ReportDevice
7
+ class << self
8
+ # @param options [Hash]
9
+ # return [Hash]
10
+ def call(options = {})
11
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
12
+ http = options.delete(:http)
13
+ config = options.delete(:config) || Castle.config
14
+
15
+ Castle::API.call(
16
+ Castle::Commands::ReportDevice.build(options),
17
+ {},
18
+ http,
19
+ config
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ module Review
6
+ class << self
7
+ # @param options [Hash]
8
+ # return [Hash]
9
+ def call(options = {})
10
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
11
+ http = options.delete(:http)
12
+ config = options.delete(:config) || Castle.config
13
+
14
+ Castle::API.call(
15
+ Castle::Commands::Review.build(options),
16
+ {},
17
+ http,
18
+ config
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ # Sends POST impersonate request
6
+ module StartImpersonation
7
+ class << self
8
+ # @param options [Hash]
9
+ def call(options = {})
10
+ unless options[:no_symbolize]
11
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
12
+ end
13
+ options.delete(:no_symbolize)
14
+ http = options.delete(:http)
15
+ config = options.delete(:config) || Castle.config
16
+
17
+ Castle::API.call(
18
+ Castle::Commands::StartImpersonation.build(options),
19
+ {},
20
+ http,
21
+ config
22
+ ).tap do |response|
23
+ raise Castle::ImpersonationFailed unless response[:success]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ module Track
6
+ class << self
7
+ # @param options [Hash]
8
+ def call(options = {})
9
+ unless options[:no_symbolize]
10
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
11
+ end
12
+ options.delete(:no_symbolize)
13
+ http = options.delete(:http)
14
+ config = options.delete(:config) || Castle.config
15
+
16
+ Castle::API.call(
17
+ Castle::Commands::Track.build(options),
18
+ {},
19
+ http,
20
+ config
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -5,82 +5,87 @@ module Castle
5
5
  class << self
6
6
  def from_request(request, options = {})
7
7
  new(
8
- to_context(request, options),
9
- to_options(options)
8
+ options.merge(context: Castle::Context::Prepare.call(request, options))
10
9
  )
11
10
  end
12
-
13
- def to_context(request, options = {})
14
- default_context = Castle::Context::Default.new(request, options[:cookies]).call
15
- Castle::Context::Merger.call(default_context, options[:context])
16
- end
17
-
18
- def to_options(options = {})
19
- options[:timestamp] ||= Castle::Utils::Timestamp.call
20
- warn '[DEPRECATION] use user_traits instead of traits key' if options.key?(:traits)
21
- options
22
- end
23
-
24
- def failover_response_or_raise(failover_response, error)
25
- return failover_response.generate unless Castle.config.failover_strategy == :throw
26
-
27
- raise error
28
- end
29
11
  end
30
12
 
31
13
  attr_accessor :context
32
14
 
33
- def initialize(context, options = {})
15
+ # @param options [Hash]
16
+ def initialize(options = {})
17
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
34
18
  @do_not_track = options.fetch(:do_not_track, false)
35
- @timestamp = options[:timestamp]
36
- @context = context
19
+ @timestamp = options.fetch(:timestamp) { Castle::Utils::GetTimestamp.call }
20
+ @context = options.fetch(:context) { {} }
37
21
  end
38
22
 
23
+ # @param options [Hash]
39
24
  def authenticate(options = {})
40
- options = Castle::Utils.deep_symbolize_keys(options || {})
25
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
41
26
 
42
27
  return generate_do_not_track_response(options[:user_id]) unless tracked?
43
28
 
44
29
  add_timestamp_if_necessary(options)
45
- command = Castle::Commands::Authenticate.new(@context).build(options)
46
- begin
47
- Castle::API.call(command).merge(failover: false, failover_reason: nil)
48
- rescue Castle::RequestError, Castle::InternalServerError => e
49
- self.class.failover_response_or_raise(
50
- FailoverAuthResponse.new(options[:user_id], reason: e.to_s), e
51
- )
52
- end
30
+
31
+ new_context = Castle::Context::Merge.call(@context, options[:context])
32
+
33
+ Castle::API::Authenticate.call(options.merge(context: new_context, no_symbolize: true))
53
34
  end
54
35
 
36
+ # @param options [Hash]
55
37
  def identify(options = {})
56
- options = Castle::Utils.deep_symbolize_keys(options || {})
38
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
57
39
 
58
40
  return unless tracked?
59
41
 
60
42
  add_timestamp_if_necessary(options)
61
43
 
62
- command = Castle::Commands::Identify.new(@context).build(options)
63
- Castle::API.call(command)
44
+ new_context = Castle::Context::Merge.call(@context, options[:context])
45
+
46
+ Castle::API::Identify.call(options.merge(context: new_context, no_symbolize: true))
64
47
  end
65
48
 
49
+ # @param options [Hash]
66
50
  def track(options = {})
67
- options = Castle::Utils.deep_symbolize_keys(options || {})
51
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
68
52
 
69
53
  return unless tracked?
70
54
 
71
55
  add_timestamp_if_necessary(options)
72
56
 
73
- command = Castle::Commands::Track.new(@context).build(options)
74
- Castle::API.call(command)
57
+ new_context = Castle::Context::Merge.call(@context, options[:context])
58
+
59
+ Castle::API::Track.call(options.merge(context: new_context, no_symbolize: true))
60
+ end
61
+
62
+ # @param options [Hash]
63
+ def start_impersonation(options = {})
64
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
65
+
66
+ add_timestamp_if_necessary(options)
67
+
68
+ new_context = Castle::Context::Merge.call(@context, options[:context])
69
+
70
+ Castle::API::StartImpersonation.call(options.merge(context: new_context, no_symbolize: true))
75
71
  end
76
72
 
77
- def impersonate(options = {})
78
- options = Castle::Utils.deep_symbolize_keys(options || {})
73
+ # @param options [Hash]
74
+ def end_impersonation(options = {})
75
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
76
+
79
77
  add_timestamp_if_necessary(options)
80
- command = Castle::Commands::Impersonate.new(@context).build(options)
81
- Castle::API.call(command).tap do |response|
82
- raise Castle::ImpersonationFailed unless response[:success]
83
- end
78
+
79
+ new_context = Castle::Context::Merge.call(@context, options[:context])
80
+
81
+ Castle::API::EndImpersonation.call(options.merge(context: new_context, no_symbolize: true))
82
+ end
83
+
84
+ # @param options [Hash]
85
+ def review(options = {})
86
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
87
+
88
+ Castle::API::Review.call(options.merge(no_symbolize: true))
84
89
  end
85
90
 
86
91
  def disable_tracking
@@ -97,13 +102,15 @@ module Castle
97
102
 
98
103
  private
99
104
 
105
+ # @param user_id [String|Boolean]
100
106
  def generate_do_not_track_response(user_id)
101
- FailoverAuthResponse.new(
107
+ Castle::Failover::PrepareResponse.new(
102
108
  user_id,
103
109
  strategy: :allow, reason: 'Castle is set to do not track.'
104
- ).generate
110
+ ).call
105
111
  end
106
112
 
113
+ # @param options [Hash]
107
114
  def add_timestamp_if_necessary(options)
108
115
  options[:timestamp] ||= @timestamp if @timestamp
109
116
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castle
4
- module Extractors
4
+ module ClientId
5
5
  # used for extraction of cookies and headers from the request
6
- class ClientId
6
+ class Extract
7
7
  # @param headers [Hash]
8
8
  # @param cookies [NilClass|Hash]
9
9
  def initialize(headers, cookies)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Commands
5
+ # Generates the payload for the PUT devices/#{device_token}/approve request
6
+ class ApproveDevice
7
+ class << self
8
+ # @param options [Hash]
9
+ # @return [Castle::Command]
10
+ def build(options = {})
11
+ Castle::Validators::Present.call(options, %i[device_token])
12
+ Castle::Command.new(
13
+ "devices/#{options[:device_token]}/approve",
14
+ nil,
15
+ :put
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,21 +2,21 @@
2
2
 
3
3
  module Castle
4
4
  module Commands
5
+ # Generates the payload for the authenticate request
5
6
  class Authenticate
6
- def initialize(context)
7
- @context = context
8
- end
9
-
10
- def build(options = {})
11
- Castle::Validators::Present.call(options, %i[event])
12
- context = Castle::Context::Merger.call(@context, options[:context])
13
- context = Castle::Context::Sanitizer.call(context)
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])
14
13
 
15
- Castle::Command.new(
16
- 'authenticate',
17
- options.merge(context: context, sent_at: Castle::Utils::Timestamp.call),
18
- :post
19
- )
14
+ Castle::Command.new(
15
+ 'authenticate',
16
+ options.merge(context: context, sent_at: Castle::Utils::GetTimestamp.call),
17
+ :post
18
+ )
19
+ end
20
20
  end
21
21
  end
22
22
  end