castle-rb 3.4.0 → 3.4.1

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