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.
- 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
|
[![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)
|
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
|