castle-rb 5.0.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +107 -33
  3. data/lib/castle.rb +46 -22
  4. data/lib/castle/api.rb +22 -13
  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 +48 -62
  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 +17 -19
  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/{extractors/headers.rb → headers/extract.rb} +8 -6
  41. data/lib/castle/headers/filter.rb +37 -0
  42. data/lib/castle/headers/format.rb +24 -0
  43. data/lib/castle/{extractors/ip.rb → ip/extract.rb} +8 -7
  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 +39 -21
  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} +7 -32
  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/{api/connection_spec.rb → core/get_connection_spec.rb} +3 -3
  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/{api/request_spec.rb → core/send_request_spec.rb} +20 -16
  95. data/spec/lib/castle/failover/strategy_spec.rb +12 -0
  96. data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +7 -7
  97. data/spec/lib/castle/{headers_filter_spec.rb → headers/filter_spec.rb} +3 -3
  98. data/spec/lib/castle/headers/format_spec.rb +25 -0
  99. data/spec/lib/castle/{extractors/ip_spec.rb → ip/extract_spec.rb} +1 -1
  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/{api/session_spec.rb → session_spec.rb} +6 -4
  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 +129 -57
  114. data/lib/castle/api/connection.rb +0 -24
  115. data/lib/castle/api/request.rb +0 -42
  116. data/lib/castle/api/session.rb +0 -20
  117. data/lib/castle/commands/impersonate.rb +0 -26
  118. data/lib/castle/failover_auth_response.rb +0 -21
  119. data/lib/castle/headers_filter.rb +0 -35
  120. data/lib/castle/headers_formatter.rb +0 -22
  121. data/lib/castle/review.rb +0 -11
  122. data/lib/castle/utils.rb +0 -55
  123. data/lib/castle/utils/cloner.rb +0 -11
  124. data/lib/castle/utils/timestamp.rb +0 -12
  125. data/spec/lib/castle/headers_formatter_spec.rb +0 -25
  126. data/spec/lib/castle/utils_spec.rb +0 -156
@@ -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,83 +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
30
 
46
- begin
47
- Castle::API
48
- .call(authenticate_command(options), {}, options[:http])
49
- .merge(failover: false, failover_reason: nil)
50
- rescue Castle::RequestError, Castle::InternalServerError => e
51
- self.class.failover_response_or_raise(
52
- FailoverAuthResponse.new(options[:user_id], reason: e.to_s), e
53
- )
54
- end
31
+ new_context = Castle::Context::Merge.call(@context, options[:context])
32
+
33
+ Castle::API::Authenticate.call(options.merge(context: new_context, no_symbolize: true))
55
34
  end
56
35
 
36
+ # @param options [Hash]
57
37
  def identify(options = {})
58
- options = Castle::Utils.deep_symbolize_keys(options || {})
38
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
59
39
 
60
40
  return unless tracked?
61
41
 
62
42
  add_timestamp_if_necessary(options)
63
43
 
64
- Castle::API.call(identify_command(options), {}, options[:http])
44
+ new_context = Castle::Context::Merge.call(@context, options[:context])
45
+
46
+ Castle::API::Identify.call(options.merge(context: new_context, no_symbolize: true))
65
47
  end
66
48
 
49
+ # @param options [Hash]
67
50
  def track(options = {})
68
- options = Castle::Utils.deep_symbolize_keys(options || {})
51
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
69
52
 
70
53
  return unless tracked?
71
54
 
72
55
  add_timestamp_if_necessary(options)
73
56
 
74
- Castle::API.call(track_command(options), {}, options[:http])
57
+ new_context = Castle::Context::Merge.call(@context, options[:context])
58
+
59
+ Castle::API::Track.call(options.merge(context: new_context, no_symbolize: true))
75
60
  end
76
61
 
77
- def impersonate(options = {})
78
- options = Castle::Utils.deep_symbolize_keys(options || {})
62
+ # @param options [Hash]
63
+ def start_impersonation(options = {})
64
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
79
65
 
80
66
  add_timestamp_if_necessary(options)
81
67
 
82
- Castle::API.call(impersonate_command(options), {}, options[:http]).tap do |response|
83
- raise Castle::ImpersonationFailed unless response[:success]
84
- end
68
+ new_context = Castle::Context::Merge.call(@context, options[:context])
69
+
70
+ Castle::API::StartImpersonation.call(options.merge(context: new_context, no_symbolize: true))
71
+ end
72
+
73
+ # @param options [Hash]
74
+ def end_impersonation(options = {})
75
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
76
+
77
+ add_timestamp_if_necessary(options)
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))
85
89
  end
86
90
 
87
91
  def disable_tracking
@@ -98,33 +102,15 @@ module Castle
98
102
 
99
103
  private
100
104
 
105
+ # @param user_id [String|Boolean]
101
106
  def generate_do_not_track_response(user_id)
102
- FailoverAuthResponse.new(
107
+ Castle::Failover::PrepareResponse.new(
103
108
  user_id,
104
109
  strategy: :allow, reason: 'Castle is set to do not track.'
105
- ).generate
106
- end
107
-
108
- # @param options [Hash]
109
- def authenticate_command(options)
110
- Castle::Commands::Authenticate.new(@context).build(options)
111
- end
112
-
113
- # @param options [Hash]
114
- def identify_command(options)
115
- Castle::Commands::Identify.new(@context).build(options)
110
+ ).call
116
111
  end
117
112
 
118
113
  # @param options [Hash]
119
- def impersonate_command(options)
120
- Castle::Commands::Impersonate.new(@context).build(options)
121
- end
122
-
123
- # @param options [Hash]
124
- def track_command(options)
125
- Castle::Commands::Track.new(@context).build(options)
126
- end
127
-
128
114
  def add_timestamp_if_necessary(options)
129
115
  options[:timestamp] ||= @timestamp if @timestamp
130
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
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Commands
5
+ # builder for impersonate command
6
+ class EndImpersonation
7
+ class << self
8
+ # @param options [Hash]
9
+ # @return [Castle::Command]
10
+ def build(options = {})
11
+ Castle::Validators::Present.call(options, %i[user_id])
12
+ context = Castle::Context::Sanitize.call(options[:context])
13
+
14
+ Castle::Validators::Present.call(context, %i[user_agent ip])
15
+
16
+ Castle::Command.new(
17
+ 'impersonate',
18
+ options.merge(context: context, sent_at: Castle::Utils::GetTimestamp.call),
19
+ :delete
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end