castle-rb 6.0.1 → 7.0.0
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 +8 -8
- data/lib/castle.rb +7 -11
- data/lib/castle/api.rb +7 -12
- data/lib/castle/api/approve_device.rb +1 -6
- data/lib/castle/api/authenticate.rb +10 -7
- data/lib/castle/api/end_impersonation.rb +3 -8
- data/lib/castle/api/filter.rb +37 -0
- data/lib/castle/api/get_device.rb +1 -6
- data/lib/castle/api/get_devices_for_user.rb +1 -6
- data/lib/castle/api/log.rb +37 -0
- data/lib/castle/api/report_device.rb +1 -6
- data/lib/castle/api/risk.rb +37 -0
- data/lib/castle/api/start_impersonation.rb +3 -8
- data/lib/castle/api/track.rb +1 -6
- data/lib/castle/client.rb +36 -16
- data/lib/castle/commands/approve_device.rb +1 -5
- data/lib/castle/commands/end_impersonation.rb +1 -1
- data/lib/castle/commands/filter.rb +23 -0
- data/lib/castle/commands/get_device.rb +1 -5
- data/lib/castle/commands/get_devices_for_user.rb +1 -5
- data/lib/castle/commands/{identify.rb → log.rb} +4 -3
- data/lib/castle/commands/report_device.rb +1 -5
- data/lib/castle/commands/risk.rb +23 -0
- data/lib/castle/commands/start_impersonation.rb +1 -1
- data/lib/castle/configuration.rb +18 -8
- data/lib/castle/core/get_connection.rb +3 -1
- data/lib/castle/core/process_response.rb +5 -2
- data/lib/castle/core/process_webhook.rb +10 -5
- data/lib/castle/core/send_request.rb +8 -16
- data/lib/castle/errors.rb +37 -13
- data/lib/castle/failover/prepare_response.rb +2 -7
- data/lib/castle/failover/strategy.rb +3 -0
- data/lib/castle/headers/extract.rb +4 -4
- data/lib/castle/headers/filter.rb +9 -6
- data/lib/castle/ips/extract.rb +4 -2
- data/lib/castle/logger.rb +3 -3
- data/lib/castle/payload/prepare.rb +3 -4
- data/lib/castle/secure_mode.rb +3 -2
- data/lib/castle/support/hanami.rb +2 -6
- data/lib/castle/support/rails.rb +1 -3
- data/lib/castle/utils/clean_invalid_chars.rb +1 -3
- data/lib/castle/verdict.rb +2 -0
- data/lib/castle/version.rb +1 -1
- data/lib/castle/webhooks/verify.rb +9 -7
- data/spec/integration/rails/rails_spec.rb +9 -7
- data/spec/integration/rails/support/home_controller.rb +26 -24
- data/spec/lib/castle/api/approve_device_spec.rb +3 -3
- data/spec/lib/castle/api/authenticate_spec.rb +20 -24
- data/spec/lib/castle/api/end_impersonation_spec.rb +11 -5
- data/spec/lib/castle/api/filter_spec.rb +5 -0
- data/spec/lib/castle/api/get_device_spec.rb +3 -3
- data/spec/lib/castle/api/get_devices_for_user_spec.rb +3 -3
- data/spec/lib/castle/api/log_spec.rb +5 -0
- data/spec/lib/castle/api/report_device_spec.rb +3 -3
- data/spec/lib/castle/api/risk_spec.rb +5 -0
- data/spec/lib/castle/api/start_impersonation_spec.rb +11 -5
- data/spec/lib/castle/api/track_spec.rb +11 -7
- data/spec/lib/castle/api_spec.rb +4 -20
- data/spec/lib/castle/client_id/extract_spec.rb +4 -13
- data/spec/lib/castle/client_spec.rb +81 -84
- data/spec/lib/castle/commands/authenticate_spec.rb +8 -15
- data/spec/lib/castle/commands/end_impersonation_spec.rb +6 -9
- data/spec/lib/castle/commands/{identify_spec.rb → filter_spec.rb} +41 -19
- data/spec/lib/castle/commands/log_spec.rb +100 -0
- data/spec/lib/castle/commands/risk_spec.rb +100 -0
- data/spec/lib/castle/commands/start_impersonation_spec.rb +6 -9
- data/spec/lib/castle/commands/track_spec.rb +9 -18
- data/spec/lib/castle/configuration_spec.rb +2 -6
- data/spec/lib/castle/context/get_default_spec.rb +8 -8
- data/spec/lib/castle/context/prepare_spec.rb +6 -7
- data/spec/lib/castle/core/get_connection_spec.rb +6 -22
- data/spec/lib/castle/core/process_response_spec.rb +1 -8
- data/spec/lib/castle/core/send_request_spec.rb +4 -29
- data/spec/lib/castle/headers/extract_spec.rb +1 -3
- data/spec/lib/castle/headers/filter_spec.rb +12 -11
- data/spec/lib/castle/ips/extract_spec.rb +4 -13
- data/spec/lib/castle/logger_spec.rb +2 -6
- data/spec/lib/castle/payload/prepare_spec.rb +5 -4
- data/spec/lib/castle/session_spec.rb +13 -36
- data/spec/lib/castle/singleton_configuration_spec.rb +2 -6
- data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +2 -2
- data/spec/lib/castle/utils/merge_spec.rb +3 -1
- data/spec/lib/castle/validators/present_spec.rb +5 -6
- data/spec/lib/castle/webhooks/verify_spec.rb +8 -24
- data/spec/lib/castle_spec.rb +4 -10
- data/spec/spec_helper.rb +1 -3
- data/spec/support/shared_examples/action_request.rb +152 -0
- data/spec/support/shared_examples/configuration.rb +14 -42
- metadata +23 -18
- data/lib/castle/api/identify.rb +0 -26
- data/lib/castle/api/review.rb +0 -24
- data/lib/castle/commands/review.rb +0 -17
- data/lib/castle/events.rb +0 -49
- data/spec/lib/castle/api/identify_spec.rb +0 -68
- data/spec/lib/castle/api/review_spec.rb +0 -19
- data/spec/lib/castle/commands/review_spec.rb +0 -24
- data/spec/lib/castle/events_spec.rb +0 -5
data/lib/castle/ips/extract.rb
CHANGED
@@ -7,14 +7,16 @@ module Castle
|
|
7
7
|
class Extract
|
8
8
|
# ordered list of ip headers for ip extraction
|
9
9
|
DEFAULT = %w[X-Forwarded-For Remote-Addr].freeze
|
10
|
+
|
10
11
|
# list of header which are used with proxy depth setting
|
11
12
|
DEPTH_RELATED = %w[X-Forwarded-For].freeze
|
12
13
|
|
13
14
|
private_constant :DEFAULT
|
14
15
|
|
15
16
|
# @param headers [Hash]
|
16
|
-
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
17
|
-
def initialize(headers, config =
|
17
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
|
18
|
+
def initialize(headers, config = nil)
|
19
|
+
config ||= Castle.config
|
18
20
|
@headers = headers
|
19
21
|
@ip_headers = config.ip_headers.empty? ? DEFAULT : config.ip_headers
|
20
22
|
@proxies = config.trusted_proxies + Castle::Configuration::TRUSTED_PROXIES
|
data/lib/castle/logger.rb
CHANGED
@@ -6,9 +6,9 @@ module Castle
|
|
6
6
|
class << self
|
7
7
|
# @param message [String]
|
8
8
|
# @param data [String]
|
9
|
-
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
10
|
-
def call(message, data = nil, config =
|
11
|
-
logger = config.logger
|
9
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
|
10
|
+
def call(message, data = nil, config = nil)
|
11
|
+
logger = (config || Castle.config).logger
|
12
12
|
|
13
13
|
return unless logger
|
14
14
|
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Castle
|
4
4
|
module Payload
|
5
|
-
#
|
5
|
+
# prepares payload based on the request
|
6
6
|
module Prepare
|
7
7
|
class << self
|
8
8
|
# @param payload_options [Hash]
|
@@ -12,9 +12,8 @@ module Castle
|
|
12
12
|
def call(payload_options, request, options = {})
|
13
13
|
context = Castle::Context::Prepare.call(request, payload_options.merge(options))
|
14
14
|
|
15
|
-
payload =
|
16
|
-
payload_options || {}
|
17
|
-
).merge(context: context)
|
15
|
+
payload =
|
16
|
+
Castle::Utils::DeepSymbolizeKeys.call(payload_options || {}).merge(context: context)
|
18
17
|
payload[:timestamp] ||= Castle::Utils::GetTimestamp.call
|
19
18
|
|
20
19
|
warn '[DEPRECATION] use user_traits instead of traits key' if payload.key?(:traits)
|
data/lib/castle/secure_mode.rb
CHANGED
@@ -6,8 +6,9 @@ module Castle
|
|
6
6
|
module SecureMode
|
7
7
|
class << self
|
8
8
|
# @param user_id [String]
|
9
|
-
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
10
|
-
def signature(user_id, config =
|
9
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
|
10
|
+
def signature(user_id, config = nil)
|
11
|
+
config ||= Castle.config
|
11
12
|
OpenSSL::HMAC.hexdigest('sha256', config.api_secret, user_id.to_s)
|
12
13
|
end
|
13
14
|
end
|
@@ -4,16 +4,12 @@ module Castle
|
|
4
4
|
module Hanami
|
5
5
|
module Action
|
6
6
|
def castle
|
7
|
-
@castle ||= ::Castle::Client.from_request(request, cookies: (cookies if defined?
|
7
|
+
@castle ||= ::Castle::Client.from_request(request, cookies: (cookies if defined?(cookies)))
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
11
|
def self.included(base)
|
12
|
-
base.configure
|
13
|
-
controller.prepare do
|
14
|
-
include Castle::Hanami::Action
|
15
|
-
end
|
16
|
-
end
|
12
|
+
base.configure { controller.prepare { include Castle::Hanami::Action } }
|
17
13
|
end
|
18
14
|
end
|
19
15
|
end
|
data/lib/castle/support/rails.rb
CHANGED
@@ -9,9 +9,7 @@ module Castle
|
|
9
9
|
when ::String
|
10
10
|
arg.encode('UTF-8', invalid: :replace, undef: :replace)
|
11
11
|
when ::Hash
|
12
|
-
arg.transform_values
|
13
|
-
Castle::Utils::CleanInvalidChars.call(v)
|
14
|
-
end
|
12
|
+
arg.transform_values { |v| Castle::Utils::CleanInvalidChars.call(v) }
|
15
13
|
when ::Array
|
16
14
|
arg.map { |el| Castle::Utils::CleanInvalidChars.call(el) }
|
17
15
|
else
|
data/lib/castle/verdict.rb
CHANGED
data/lib/castle/version.rb
CHANGED
@@ -7,9 +7,10 @@ module Castle
|
|
7
7
|
class << self
|
8
8
|
# Checks if webhook is valid
|
9
9
|
# @param webhook [Request]
|
10
|
-
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
11
|
-
def call(webhook, config =
|
12
|
-
|
10
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration, nil]
|
11
|
+
def call(webhook, config = nil)
|
12
|
+
config ||= Castle.config
|
13
|
+
expected_signature = compute_signature(webhook, config)
|
13
14
|
signature = webhook.env['HTTP_X_CASTLE_SIGNATURE']
|
14
15
|
verify_signature(signature, expected_signature)
|
15
16
|
end
|
@@ -18,13 +19,14 @@ module Castle
|
|
18
19
|
|
19
20
|
# Computes a webhook signature using provided user_id
|
20
21
|
# @param webhook [Request]
|
21
|
-
# @param
|
22
|
-
|
22
|
+
# @param config [Castle::Configuration, Castle::SingletonConfiguration]
|
23
|
+
# @return [String]
|
24
|
+
def compute_signature(webhook, config)
|
23
25
|
Base64.encode64(
|
24
26
|
OpenSSL::HMAC.digest(
|
25
27
|
OpenSSL::Digest.new('sha256'),
|
26
|
-
api_secret,
|
27
|
-
Castle::Core::ProcessWebhook.call(webhook)
|
28
|
+
config.api_secret,
|
29
|
+
Castle::Core::ProcessWebhook.call(webhook, config)
|
28
30
|
)
|
29
31
|
).strip
|
30
32
|
end
|
@@ -9,15 +9,20 @@ RSpec.describe HomeController, type: :request do
|
|
9
9
|
{
|
10
10
|
'event' => '$login.succeeded',
|
11
11
|
'user_id' => '123',
|
12
|
-
'properties' => {
|
13
|
-
|
12
|
+
'properties' => {
|
13
|
+
'key' => 'value'
|
14
|
+
},
|
15
|
+
'user_traits' => {
|
16
|
+
'key' => 'value'
|
17
|
+
},
|
14
18
|
'timestamp' => now.utc.iso8601(3),
|
15
19
|
'sent_at' => now.utc.iso8601(3),
|
16
20
|
'context' => {
|
17
21
|
'client_id' => '',
|
18
22
|
'active' => true,
|
19
23
|
'headers' => {
|
20
|
-
'Accept' =>
|
24
|
+
'Accept' =>
|
25
|
+
'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
|
21
26
|
'Authorization' => true,
|
22
27
|
'Content-Length' => '0',
|
23
28
|
'Cookie' => true,
|
@@ -35,10 +40,7 @@ RSpec.describe HomeController, type: :request do
|
|
35
40
|
end
|
36
41
|
let(:now) { Time.now }
|
37
42
|
let(:headers) do
|
38
|
-
{
|
39
|
-
'HTTP_AUTHORIZATION' => 'Basic 123',
|
40
|
-
'HTTP_X_FORWARDED_FOR' => '5.5.5.5, 1.2.3.4'
|
41
|
-
}
|
43
|
+
{ 'HTTP_AUTHORIZATION' => 'Basic 123', 'HTTP_X_FORWARDED_FOR' => '5.5.5.5, 1.2.3.4' }
|
42
44
|
end
|
43
45
|
|
44
46
|
before do
|
@@ -22,19 +22,20 @@ class HomeController < ActionController::Base
|
|
22
22
|
|
23
23
|
# prepare payload and calling track with client example
|
24
24
|
def index2
|
25
|
-
payload =
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
payload =
|
26
|
+
::Castle::Payload::Prepare.call(
|
27
|
+
{
|
28
|
+
event: '$login.succeeded',
|
29
|
+
user_id: '123',
|
30
|
+
properties: {
|
31
|
+
key: 'value'
|
32
|
+
},
|
33
|
+
user_traits: {
|
34
|
+
key: 'value'
|
35
|
+
}
|
31
36
|
},
|
32
|
-
|
33
|
-
|
34
|
-
}
|
35
|
-
},
|
36
|
-
request
|
37
|
-
)
|
37
|
+
request
|
38
|
+
)
|
38
39
|
client = ::Castle::Client.new
|
39
40
|
client.track(payload)
|
40
41
|
|
@@ -43,19 +44,20 @@ class HomeController < ActionController::Base
|
|
43
44
|
|
44
45
|
# prepare payload and calling track with direct API::Track service
|
45
46
|
def index3
|
46
|
-
payload =
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
47
|
+
payload =
|
48
|
+
::Castle::Payload::Prepare.call(
|
49
|
+
{
|
50
|
+
event: '$login.succeeded',
|
51
|
+
user_id: '123',
|
52
|
+
properties: {
|
53
|
+
key: 'value'
|
54
|
+
},
|
55
|
+
user_traits: {
|
56
|
+
key: 'value'
|
57
|
+
}
|
52
58
|
},
|
53
|
-
|
54
|
-
|
55
|
-
}
|
56
|
-
},
|
57
|
-
request
|
58
|
-
)
|
59
|
+
request
|
60
|
+
)
|
59
61
|
Castle::API::Track.call(payload)
|
60
62
|
|
61
63
|
render inline: 'hello'
|
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
describe Castle::API::ApproveDevice do
|
4
4
|
before do
|
5
|
-
stub_request(:any, /api.castle.io/)
|
6
|
-
basic_auth: ['', 'secret']
|
7
|
-
|
5
|
+
stub_request(:any, /api.castle.io/)
|
6
|
+
.with(basic_auth: ['', 'secret'])
|
7
|
+
.to_return(status: 200, body: '{}', headers: {})
|
8
8
|
end
|
9
9
|
|
10
10
|
describe '.call' do
|
@@ -30,15 +30,14 @@ describe Castle::API::Authenticate do
|
|
30
30
|
|
31
31
|
describe '.call' do
|
32
32
|
let(:request_body) do
|
33
|
-
{ event: '$login.succeeded', context: context, user_id: '1234',
|
34
|
-
sent_at: time_auto }
|
33
|
+
{ event: '$login.succeeded', context: context, user_id: '1234', sent_at: time_auto }
|
35
34
|
end
|
36
35
|
|
37
36
|
context 'when used with symbol keys' do
|
38
37
|
before do
|
39
|
-
stub_request(:any, /api.castle.io/)
|
40
|
-
basic_auth: ['', 'secret']
|
41
|
-
|
38
|
+
stub_request(:any, /api.castle.io/)
|
39
|
+
.with(basic_auth: ['', 'secret'])
|
40
|
+
.to_return(status: 200, body: response_body, headers: {})
|
42
41
|
call_subject
|
43
42
|
end
|
44
43
|
|
@@ -55,8 +54,13 @@ describe Castle::API::Authenticate do
|
|
55
54
|
{ event: '$login.succeeded', user_id: '1234', timestamp: time_user, context: context }
|
56
55
|
end
|
57
56
|
let(:request_body) do
|
58
|
-
{
|
59
|
-
|
57
|
+
{
|
58
|
+
event: '$login.succeeded',
|
59
|
+
user_id: '1234',
|
60
|
+
context: context,
|
61
|
+
timestamp: time_user,
|
62
|
+
sent_at: time_auto
|
63
|
+
}
|
60
64
|
end
|
61
65
|
|
62
66
|
it do
|
@@ -75,20 +79,14 @@ describe Castle::API::Authenticate do
|
|
75
79
|
context 'when denied without any risk policy' do
|
76
80
|
let(:response_body) { deny_response_without_rp.to_json }
|
77
81
|
let(:deny_response_without_rp) do
|
78
|
-
{
|
79
|
-
action: 'deny',
|
80
|
-
user_id: '12345',
|
81
|
-
device_token: 'abcdefg1234'
|
82
|
-
}
|
83
|
-
end
|
84
|
-
let(:deny_without_rp_failover_result) do
|
85
|
-
deny_response_without_rp.merge(failover_appendix)
|
82
|
+
{ action: 'deny', user_id: '12345', device_token: 'abcdefg1234' }
|
86
83
|
end
|
84
|
+
let(:deny_without_rp_failover_result) { deny_response_without_rp.merge(failover_appendix) }
|
87
85
|
|
88
86
|
before do
|
89
|
-
stub_request(:any, /api.castle.io/)
|
90
|
-
basic_auth: ['', 'secret']
|
91
|
-
|
87
|
+
stub_request(:any, /api.castle.io/)
|
88
|
+
.with(basic_auth: ['', 'secret'])
|
89
|
+
.to_return(status: 200, body: deny_response_without_rp.to_json, headers: {})
|
92
90
|
call_subject
|
93
91
|
end
|
94
92
|
|
@@ -116,14 +114,12 @@ describe Castle::API::Authenticate do
|
|
116
114
|
}
|
117
115
|
end
|
118
116
|
let(:response_body) { deny_response_with_rp.to_json }
|
119
|
-
let(:deny_with_rp_failover_result)
|
120
|
-
deny_response_with_rp.merge(failover_appendix)
|
121
|
-
end
|
117
|
+
let(:deny_with_rp_failover_result) { deny_response_with_rp.merge(failover_appendix) }
|
122
118
|
|
123
119
|
before do
|
124
|
-
stub_request(:any, /api.castle.io/)
|
125
|
-
basic_auth: ['', 'secret']
|
126
|
-
|
120
|
+
stub_request(:any, /api.castle.io/)
|
121
|
+
.with(basic_auth: ['', 'secret'])
|
122
|
+
.to_return(status: 200, body: deny_response_with_rp.to_json, headers: {})
|
127
123
|
call_subject
|
128
124
|
end
|
129
125
|
|
@@ -22,9 +22,9 @@ describe Castle::API::EndImpersonation do
|
|
22
22
|
before do
|
23
23
|
Timecop.freeze(time_now)
|
24
24
|
stub_const('Castle::VERSION', '2.2.0')
|
25
|
-
stub_request(:any, /api.castle.io/)
|
26
|
-
basic_auth: ['', 'secret']
|
27
|
-
|
25
|
+
stub_request(:any, /api.castle.io/)
|
26
|
+
.with(basic_auth: ['', 'secret'])
|
27
|
+
.to_return(status: 200, body: response_body, headers: {})
|
28
28
|
end
|
29
29
|
|
30
30
|
after { Timecop.return }
|
@@ -32,8 +32,14 @@ describe Castle::API::EndImpersonation do
|
|
32
32
|
describe 'call' do
|
33
33
|
let(:impersonator) { 'test@castle.io' }
|
34
34
|
let(:request_body) do
|
35
|
-
{
|
36
|
-
|
35
|
+
{
|
36
|
+
user_id: '1234',
|
37
|
+
sent_at: time_auto,
|
38
|
+
properties: {
|
39
|
+
impersonator: impersonator
|
40
|
+
},
|
41
|
+
context: context
|
42
|
+
}
|
37
43
|
end
|
38
44
|
let(:response_body) { { success: true }.to_json }
|
39
45
|
let(:options) do
|
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
describe Castle::API::GetDevice do
|
4
4
|
before do
|
5
|
-
stub_request(:any, /api.castle.io/)
|
6
|
-
basic_auth: ['', 'secret']
|
7
|
-
|
5
|
+
stub_request(:any, /api.castle.io/)
|
6
|
+
.with(basic_auth: ['', 'secret'])
|
7
|
+
.to_return(status: 200, body: '{}', headers: {})
|
8
8
|
end
|
9
9
|
|
10
10
|
describe '.call' do
|
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
describe Castle::API::GetDevicesForUser do
|
4
4
|
before do
|
5
|
-
stub_request(:any, /api.castle.io/)
|
6
|
-
basic_auth: ['', 'secret']
|
7
|
-
|
5
|
+
stub_request(:any, /api.castle.io/)
|
6
|
+
.with(basic_auth: ['', 'secret'])
|
7
|
+
.to_return(status: 200, body: '{}', headers: {})
|
8
8
|
end
|
9
9
|
|
10
10
|
describe '.call' do
|
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
describe Castle::API::ReportDevice do
|
4
4
|
before do
|
5
|
-
stub_request(:any, /api.castle.io/)
|
6
|
-
basic_auth: ['', 'secret']
|
7
|
-
|
5
|
+
stub_request(:any, /api.castle.io/)
|
6
|
+
.with(basic_auth: ['', 'secret'])
|
7
|
+
.to_return(status: 200, body: '{}', headers: {})
|
8
8
|
end
|
9
9
|
|
10
10
|
describe '.call' do
|