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.
- checksums.yaml +4 -4
- data/README.md +36 -24
- data/lib/castle.rb +5 -3
- data/lib/castle/client.rb +21 -18
- data/lib/castle/commands/authenticate.rb +8 -16
- data/lib/castle/commands/identify.rb +9 -21
- data/lib/castle/commands/impersonate.rb +9 -23
- data/lib/castle/commands/review.rb +5 -4
- data/lib/castle/commands/track.rb +8 -16
- data/lib/castle/configuration.rb +1 -2
- data/lib/castle/context/default.rb +40 -0
- data/lib/castle/context/merger.rb +14 -0
- data/lib/castle/context/sanitizer.rb +23 -0
- data/lib/castle/review.rb +1 -1
- data/lib/castle/utils/merger.rb +9 -9
- data/lib/castle/validators/not_supported.rb +16 -0
- data/lib/castle/validators/present.rb +16 -0
- data/lib/castle/version.rb +1 -1
- data/spec/lib/castle/client_spec.rb +3 -2
- data/spec/lib/castle/commands/authenticate_spec.rb +21 -21
- data/spec/lib/castle/commands/identify_spec.rb +17 -17
- data/spec/lib/castle/commands/impersonate_spec.rb +1 -1
- data/spec/lib/castle/commands/review_spec.rb +1 -1
- data/spec/lib/castle/commands/track_spec.rb +23 -23
- data/spec/lib/castle/configuration_spec.rb +13 -13
- data/spec/lib/castle/{default_context_spec.rb → context/default_spec.rb} +1 -1
- data/spec/lib/castle/{context_merger_spec.rb → context/merger_spec.rb} +4 -4
- data/spec/lib/castle/{context_sanitizer_spec.rb → context/sanitizer_spec.rb} +1 -1
- data/spec/lib/castle/extractors/client_id_spec.rb +1 -1
- data/spec/lib/castle/request_spec.rb +2 -2
- data/spec/lib/castle/response_spec.rb +4 -4
- data/spec/lib/castle/review_spec.rb +1 -1
- data/spec/lib/castle/utils_spec.rb +14 -14
- data/spec/lib/castle/validators/not_supported_spec.rb +26 -0
- data/spec/lib/castle/validators/present_spec.rb +33 -0
- data/spec/lib/castle_spec.rb +3 -3
- metadata +19 -13
- data/lib/castle/context_merger.rb +0 -12
- data/lib/castle/context_sanitizer.rb +0 -20
- data/lib/castle/default_context.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 688cb3645a056cf00e31610ad8a89b8a0e8183775ba6bf71bbadf7334d73b0d8
|
4
|
+
data.tar.gz: 9624e2f4feb4a78ef5f4eb8376688c1c839dae357d220fb9f9ddca76d277b3b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b9343297d9d09ace02b9ed5f40f6836023ceb86329163c2e9eb5d957dc249a40a7b3af63a0c201ca759f29b7eb7deec0f37f2dedd3c3879ab7a77781e30e66a
|
7
|
+
data.tar.gz: e040d6020f980521f0f4f5771bd07ac9b504db9e58e3baa8b22aa51f46e620f308ee1b8d716566cd657fccc2fdc4ebe79d2a59bb21097fe6d6909ae11d97ba7c
|
data/README.md
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
[](https://badge.fury.io/rb/castle-rb)
|
6
6
|
[](https://gemnasium.com/github.com/castle/castle-ruby)
|
7
7
|
|
8
|
-
**[Castle](https://castle.io)
|
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
|
-
|
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
|
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)
|
data/lib/castle.rb
CHANGED
@@ -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/
|
16
|
-
require 'castle/
|
17
|
-
require 'castle/
|
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'
|
data/lib/castle/client.rb
CHANGED
@@ -11,20 +11,25 @@ module Castle
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def to_context(request, options = {})
|
14
|
-
default_context = Castle::
|
15
|
-
Castle::
|
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
|
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
|
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(
|
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(
|
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
|
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
|
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
|
92
|
-
|
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
|
-
|
12
|
-
context =
|
13
|
-
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(
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
12
|
-
|
13
|
-
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(
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
13
|
-
context =
|
14
|
-
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
|
-
|
16
|
+
Castle::Validators::Present.call(context, %i[user_agent ip])
|
17
17
|
|
18
|
-
Castle::Command.new(
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
12
|
-
context =
|
13
|
-
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(
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
data/lib/castle/configuration.rb
CHANGED
@@ -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, :
|
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
|