castle-rb 5.0.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
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