castle-rb 3.4.0 → 3.4.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -24
  3. data/lib/castle.rb +5 -3
  4. data/lib/castle/client.rb +21 -18
  5. data/lib/castle/commands/authenticate.rb +8 -16
  6. data/lib/castle/commands/identify.rb +9 -21
  7. data/lib/castle/commands/impersonate.rb +9 -23
  8. data/lib/castle/commands/review.rb +5 -4
  9. data/lib/castle/commands/track.rb +8 -16
  10. data/lib/castle/configuration.rb +1 -2
  11. data/lib/castle/context/default.rb +40 -0
  12. data/lib/castle/context/merger.rb +14 -0
  13. data/lib/castle/context/sanitizer.rb +23 -0
  14. data/lib/castle/review.rb +1 -1
  15. data/lib/castle/utils/merger.rb +9 -9
  16. data/lib/castle/validators/not_supported.rb +16 -0
  17. data/lib/castle/validators/present.rb +16 -0
  18. data/lib/castle/version.rb +1 -1
  19. data/spec/lib/castle/client_spec.rb +3 -2
  20. data/spec/lib/castle/commands/authenticate_spec.rb +21 -21
  21. data/spec/lib/castle/commands/identify_spec.rb +17 -17
  22. data/spec/lib/castle/commands/impersonate_spec.rb +1 -1
  23. data/spec/lib/castle/commands/review_spec.rb +1 -1
  24. data/spec/lib/castle/commands/track_spec.rb +23 -23
  25. data/spec/lib/castle/configuration_spec.rb +13 -13
  26. data/spec/lib/castle/{default_context_spec.rb → context/default_spec.rb} +1 -1
  27. data/spec/lib/castle/{context_merger_spec.rb → context/merger_spec.rb} +4 -4
  28. data/spec/lib/castle/{context_sanitizer_spec.rb → context/sanitizer_spec.rb} +1 -1
  29. data/spec/lib/castle/extractors/client_id_spec.rb +1 -1
  30. data/spec/lib/castle/request_spec.rb +2 -2
  31. data/spec/lib/castle/response_spec.rb +4 -4
  32. data/spec/lib/castle/review_spec.rb +1 -1
  33. data/spec/lib/castle/utils_spec.rb +14 -14
  34. data/spec/lib/castle/validators/not_supported_spec.rb +26 -0
  35. data/spec/lib/castle/validators/present_spec.rb +33 -0
  36. data/spec/lib/castle_spec.rb +3 -3
  37. metadata +19 -13
  38. data/lib/castle/context_merger.rb +0 -12
  39. data/lib/castle/context_sanitizer.rb +0 -20
  40. data/lib/castle/default_context.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70b2d5182f8c36fca5ab6db47879bd9173451451d8e226561aa03d5f4d13f2d4
4
- data.tar.gz: 821adef78792ecb690d29d220d09cda4aae770a240c5258e818acd88aa07b494
3
+ metadata.gz: 688cb3645a056cf00e31610ad8a89b8a0e8183775ba6bf71bbadf7334d73b0d8
4
+ data.tar.gz: 9624e2f4feb4a78ef5f4eb8376688c1c839dae357d220fb9f9ddca76d277b3b7
5
5
  SHA512:
6
- metadata.gz: 58eb9e46cfcae41a04c1c3f41eb8a32b86cfc58ad254f62530040253a08ae4910333a8150304a9000c46e1257043c08942b31ac942417b0cc6a1e995497f7b77
7
- data.tar.gz: 9aa9a512116fadd5ad079bc65c95c7b9bbb638574ce9cf8c0491a76a38b88b825beb54f4491cd06ed13f00191c981202ab3bb24654970880b43efe90b1110779
6
+ metadata.gz: 7b9343297d9d09ace02b9ed5f40f6836023ceb86329163c2e9eb5d957dc249a40a7b3af63a0c201ca759f29b7eb7deec0f37f2dedd3c3879ab7a77781e30e66a
7
+ data.tar.gz: e040d6020f980521f0f4f5771bd07ac9b504db9e58e3baa8b22aa51f46e620f308ee1b8d716566cd657fccc2fdc4ebe79d2a59bb21097fe6d6909ae11d97ba7c
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Gem Version](https://badge.fury.io/rb/castle-rb.svg)](https://badge.fury.io/rb/castle-rb)
6
6
  [![Dependency Status](https://gemnasium.com/badges/github.com/castle/castle-ruby.svg)](https://gemnasium.com/github.com/castle/castle-ruby)
7
7
 
8
- **[Castle](https://castle.io) adds real-time monitoring of your authentication stack, instantly notifying you and your users on potential account hijacks.**
8
+ **[Castle](https://castle.io) analyzes device, location, and interaction patterns in your web and mobile apps and lets you stop account takeover attacks in real-time..**
9
9
 
10
10
  ## Installation
11
11
 
@@ -15,6 +15,10 @@ Add the `castle-rb` gem to your `Gemfile`
15
15
  gem 'castle-rb'
16
16
  ```
17
17
 
18
+ ## Configuration
19
+
20
+ ### Framework configuration
21
+
18
22
  Load and configure the library with your Castle API secret in an initializer or similar.
19
23
 
20
24
  ```ruby
@@ -49,28 +53,7 @@ module Web
49
53
  end
50
54
  ```
51
55
 
52
- The client will automatically configure the [request context](https://api.castle.io/docs#request-context) for each request.
53
-
54
- ## Documentation
55
-
56
- [Official Castle docs](https://castle.io/docs)
57
-
58
- ## Exceptions
59
-
60
- `Castle::Error` will be thrown if the Castle API returns a 400 or a 500 level HTTP response. You can also choose to catch a more [finegrained error](https://github.com/castle/castle-ruby/blob/master/lib/castle/errors.rb).
61
-
62
- ```ruby
63
- begin
64
- castle.track(
65
- event: '$login.succeeded',
66
- user_id: user.id
67
- )
68
- rescue Castle::Error => e
69
- puts e.message
70
- end
71
- ```
72
-
73
- ## Configuration
56
+ ### Client configuration
74
57
 
75
58
  ```ruby
76
59
  Castle.configure do |config|
@@ -80,7 +63,7 @@ Castle.configure do |config|
80
63
  # For authenticate method you can set failover strategies: allow(default), deny, challenge, throw
81
64
  config.failover_strategy = :deny
82
65
 
83
- # Castle::RequestError is raised when timing out in seconds (default: 500 milliseconds)
66
+ # Castle::RequestError is raised when timing out in milliseconds (default: 500 milliseconds)
84
67
  config.request_timeout = 2000
85
68
 
86
69
  # Whitelisted and Blacklisted headers are case insensitive and allow to use _ and - as a separator, http prefixes are removed
@@ -96,6 +79,23 @@ Castle.configure do |config|
96
79
  end
97
80
  ```
98
81
 
82
+ The client will automatically configure the context for each request.
83
+
84
+ ## Tracking
85
+
86
+ Here is a simple example of a track event.
87
+
88
+
89
+ ```ruby
90
+ begin
91
+ castle.track(
92
+ event: '$login.succeeded',
93
+ user_id: user.id
94
+ )
95
+ rescue Castle::Error => e
96
+ puts e.message
97
+ end
98
+ ```
99
99
 
100
100
  ## Signature
101
101
 
@@ -134,3 +134,15 @@ track_options = ::Castle::Client.to_options({
134
134
  })
135
135
  CastleTrackingWorker.perform_async(request_context, track_options)
136
136
  ```
137
+
138
+ ## Impersonation mode
139
+
140
+ https://castle.io/docs/impersonation
141
+
142
+ ## Exceptions
143
+
144
+ `Castle::Error` will be thrown if the Castle API returns a 400 or a 500 level HTTP response. You can also choose to catch a more [finegrained error](https://github.com/castle/castle-ruby/blob/master/lib/castle/errors.rb).
145
+
146
+ ## Documentation
147
+
148
+ [Official Castle docs](https://castle.io/docs)
@@ -12,9 +12,11 @@ require 'castle/utils'
12
12
  require 'castle/utils/merger'
13
13
  require 'castle/utils/cloner'
14
14
  require 'castle/utils/timestamp'
15
- require 'castle/context_merger'
16
- require 'castle/context_sanitizer'
17
- require 'castle/default_context'
15
+ require 'castle/validators/present'
16
+ require 'castle/validators/not_supported'
17
+ require 'castle/context/merger'
18
+ require 'castle/context/sanitizer'
19
+ require 'castle/context/default'
18
20
  require 'castle/commands/identify'
19
21
  require 'castle/commands/authenticate'
20
22
  require 'castle/commands/track'
@@ -11,20 +11,25 @@ module Castle
11
11
  end
12
12
 
13
13
  def to_context(request, options = {})
14
- default_context = Castle::DefaultContext.new(request, options[:cookies]).call
15
- Castle::ContextMerger.call(default_context, options[:context])
14
+ default_context = Castle::Context::Default.new(request, options[:cookies]).call
15
+ Castle::Context::Merger.call(default_context, options[:context])
16
16
  end
17
17
 
18
18
  def to_options(options = {})
19
19
  options[:timestamp] ||= Castle::Utils::Timestamp.call
20
20
  options
21
21
  end
22
+
23
+ def failover_response_or_raise(failover_response, error)
24
+ return failover_response.generate unless Castle.config.failover_strategy == :throw
25
+ raise error
26
+ end
22
27
  end
23
28
 
24
29
  attr_accessor :api
25
30
 
26
31
  def initialize(context, options = {})
27
- @do_not_track = options[:do_not_track]
32
+ @do_not_track = options.fetch(:do_not_track, false)
28
33
  @timestamp = options[:timestamp]
29
34
  @context = context
30
35
  @api = API.new
@@ -34,15 +39,20 @@ module Castle
34
39
  options = Castle::Utils.deep_symbolize_keys(options || {})
35
40
 
36
41
  if tracked?
37
- options[:timestamp] ||= @timestamp if @timestamp
42
+ add_timestamp_if_necessary(options)
38
43
  command = Castle::Commands::Authenticate.new(@context).build(options)
39
44
  begin
40
45
  @api.request(command).merge(failover: false, failover_reason: nil)
41
46
  rescue Castle::RequestError, Castle::InternalServerError => error
42
- failover_response_or_raise(FailoverAuthResponse.new(options[:user_id], reason: error.to_s), error)
47
+ self.class.failover_response_or_raise(
48
+ FailoverAuthResponse.new(options[:user_id], reason: error.to_s), error
49
+ )
43
50
  end
44
51
  else
45
- FailoverAuthResponse.new(options[:user_id], strategy: :allow, reason: 'Castle set to do not track.').generate
52
+ FailoverAuthResponse.new(
53
+ options[:user_id],
54
+ strategy: :allow, reason: 'Castle set to do not track.'
55
+ ).generate
46
56
  end
47
57
  end
48
58
 
@@ -50,7 +60,7 @@ module Castle
50
60
  options = Castle::Utils.deep_symbolize_keys(options || {})
51
61
 
52
62
  return unless tracked?
53
- options[:timestamp] ||= @timestamp if @timestamp
63
+ add_timestamp_if_necessary(options)
54
64
 
55
65
  command = Castle::Commands::Identify.new(@context).build(options)
56
66
  @api.request(command)
@@ -60,7 +70,7 @@ module Castle
60
70
  options = Castle::Utils.deep_symbolize_keys(options || {})
61
71
 
62
72
  return unless tracked?
63
- options[:timestamp] ||= @timestamp if @timestamp
73
+ add_timestamp_if_necessary(options)
64
74
 
65
75
  command = Castle::Commands::Track.new(@context).build(options)
66
76
  @api.request(command)
@@ -68,8 +78,7 @@ module Castle
68
78
 
69
79
  def impersonate(options = {})
70
80
  options = Castle::Utils.deep_symbolize_keys(options || {})
71
-
72
- return unless tracked?
81
+ add_timestamp_if_necessary(options)
73
82
  command = Castle::Commands::Impersonate.new(@context).build(options)
74
83
  @api.request(command)
75
84
  end
@@ -88,14 +97,8 @@ module Castle
88
97
 
89
98
  private
90
99
 
91
- def setup_context(request, cookies, additional_context)
92
- default_context = Castle::DefaultContext.new(request, cookies).call
93
- Castle::ContextMerger.call(default_context, additional_context)
94
- end
95
-
96
- def failover_response_or_raise(failover_response, error)
97
- return failover_response.generate unless Castle.config.failover_strategy == :throw
98
- raise error
100
+ def add_timestamp_if_necessary(options)
101
+ options[:timestamp] ||= @timestamp if @timestamp
99
102
  end
100
103
  end
101
104
  end
@@ -8,23 +8,15 @@ module Castle
8
8
  end
9
9
 
10
10
  def build(options = {})
11
- validate!(options)
12
- context = ContextMerger.call(@context, options[:context])
13
- context = ContextSanitizer.call(context)
11
+ Castle::Validators::Present.call(options, %i[event user_id])
12
+ context = Castle::Context::Merger.call(@context, options[:context])
13
+ context = Castle::Context::Sanitizer.call(context)
14
14
 
15
- Castle::Command.new('authenticate',
16
- options.merge(context: context,
17
- sent_at: Castle::Utils::Timestamp.call),
18
- :post)
19
- end
20
-
21
- private
22
-
23
- def validate!(options)
24
- %i[event user_id].each do |key|
25
- next unless options[key].to_s.empty?
26
- raise Castle::InvalidParametersError, "#{key} is missing or empty"
27
- end
15
+ Castle::Command.new(
16
+ 'authenticate',
17
+ options.merge(context: context, sent_at: Castle::Utils::Timestamp.call),
18
+ :post
19
+ )
28
20
  end
29
21
  end
30
22
  end
@@ -8,28 +8,16 @@ module Castle
8
8
  end
9
9
 
10
10
  def build(options = {})
11
- validate!(options)
12
- context = ContextMerger.call(@context, options[:context])
13
- context = ContextSanitizer.call(context)
11
+ Castle::Validators::Present.call(options, %i[user_id])
12
+ Castle::Validators::NotSupported.call(options, %i[properties])
13
+ context = Castle::Context::Merger.call(@context, options[:context])
14
+ context = Castle::Context::Sanitizer.call(context)
14
15
 
15
- Castle::Command.new('identify',
16
- options.merge(context: context,
17
- sent_at: Castle::Utils::Timestamp.call),
18
- :post)
19
- end
20
-
21
- private
22
-
23
- def validate!(options)
24
- %i[user_id].each do |key|
25
- next unless options[key].to_s.empty?
26
- raise Castle::InvalidParametersError, "#{key} is missing or empty"
27
- end
28
-
29
- if options[:properties]
30
- raise Castle::InvalidParametersError,
31
- 'properties are not supported in identify calls'
32
- end
16
+ Castle::Command.new(
17
+ 'identify',
18
+ options.merge(context: context, sent_at: Castle::Utils::Timestamp.call),
19
+ :post
20
+ )
33
21
  end
34
22
  end
35
23
  end
@@ -9,31 +9,17 @@ module Castle
9
9
  end
10
10
 
11
11
  def build(options = {})
12
- validate!(options)
13
- context = ContextMerger.call(@context, options[:context])
14
- context = ContextSanitizer.call(context)
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
15
 
16
- validate_context!(context)
16
+ Castle::Validators::Present.call(context, %i[user_agent ip])
17
17
 
18
- Castle::Command.new('impersonate',
19
- options.merge(context: context),
20
- :post)
21
- end
22
-
23
- private
24
-
25
- def validate!(options)
26
- %i[user_id].each do |key|
27
- next unless options[key].to_s.empty?
28
- raise Castle::InvalidParametersError, "#{key} is missing or empty"
29
- end
30
- end
31
-
32
- def validate_context!(context)
33
- %i[user_agent ip].each do |key|
34
- next unless context[key].to_s.empty?
35
- raise Castle::InvalidParametersError, "#{key} is missing or empty"
36
- end
18
+ Castle::Command.new(
19
+ 'impersonate',
20
+ options.merge(context: context, sent_at: Castle::Utils::Timestamp.call),
21
+ :post
22
+ )
37
23
  end
38
24
  end
39
25
  end
@@ -3,10 +3,11 @@
3
3
  module Castle
4
4
  module Commands
5
5
  class Review
6
- def build(review_id)
7
- raise Castle::InvalidParametersError if review_id.nil? || review_id.to_s.empty?
8
-
9
- Castle::Command.new("reviews/#{review_id}", nil, :get)
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
10
11
  end
11
12
  end
12
13
  end
@@ -8,23 +8,15 @@ module Castle
8
8
  end
9
9
 
10
10
  def build(options = {})
11
- validate!(options)
12
- context = ContextMerger.call(@context, options[:context])
13
- context = ContextSanitizer.call(context)
11
+ Castle::Validators::Present.call(options, %i[event])
12
+ context = Castle::Context::Merger.call(@context, options[:context])
13
+ context = Castle::Context::Sanitizer.call(context)
14
14
 
15
- Castle::Command.new('track',
16
- options.merge(context: context,
17
- sent_at: Castle::Utils::Timestamp.call),
18
- :post)
19
- end
20
-
21
- private
22
-
23
- def validate!(options)
24
- %i[event].each do |key|
25
- next unless options[key].to_s.empty?
26
- raise Castle::InvalidParametersError, "#{key} is missing or empty"
27
- end
15
+ Castle::Command.new(
16
+ 'track',
17
+ options.merge(context: context, sent_at: Castle::Utils::Timestamp.call),
18
+ :post
19
+ )
28
20
  end
29
21
  end
30
22
  end
@@ -24,7 +24,7 @@ module Castle
24
24
  BLACKLISTED = ['HTTP_COOKIE'].freeze
25
25
 
26
26
  attr_accessor :host, :port, :request_timeout, :url_prefix
27
- attr_reader :api_secret, :host, :port, :whitelisted, :blacklisted, :failover_strategy
27
+ attr_reader :api_secret, :whitelisted, :blacklisted, :failover_strategy
28
28
 
29
29
  def initialize
30
30
  @formatter = Castle::HeaderFormatter.new
@@ -57,7 +57,6 @@ module Castle
57
57
  def failover_strategy=(value)
58
58
  @failover_strategy = FAILOVER_STRATEGIES.detect { |strategy| strategy == value.to_sym }
59
59
  raise Castle::ConfigurationError, 'unrecognized failover strategy' if @failover_strategy.nil?
60
- @failover_strategy
61
60
  end
62
61
 
63
62
  private
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Context
5
+ class Default
6
+ def initialize(request, cookies = nil)
7
+ @client_id = Extractors::ClientId.new(request, cookies || request.cookies).call('__cid')
8
+ @headers = Extractors::Headers.new(request).call
9
+ @request_ip = Extractors::IP.new(request).call
10
+ end
11
+
12
+ def call
13
+ defaults.merge!(additional_defaults)
14
+ end
15
+
16
+ private
17
+
18
+ def defaults
19
+ {
20
+ client_id: @client_id,
21
+ active: true,
22
+ origin: 'web',
23
+ headers: @headers,
24
+ ip: @request_ip,
25
+ library: {
26
+ name: 'castle-rb',
27
+ version: Castle::VERSION
28
+ }
29
+ }
30
+ end
31
+
32
+ def additional_defaults
33
+ {}.tap do |result|
34
+ result[:locale] = @headers['Accept-Language'] if @headers['Accept-Language']
35
+ result[:user_agent] = @headers['User-Agent'] if @headers['User-Agent']
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Context
5
+ class Merger
6
+ class << self
7
+ def call(initial_context, request_context)
8
+ main_context = Castle::Utils::Cloner.call(initial_context)
9
+ Castle::Utils::Merger.call(main_context, request_context || {})
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end