castle-rb 3.6.2 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +57 -5
- data/lib/castle.rb +5 -3
- data/lib/castle/api.rb +12 -6
- data/lib/castle/api/request.rb +15 -10
- data/lib/castle/api/session.rb +39 -0
- data/lib/castle/client.rb +23 -18
- data/lib/castle/configuration.rb +49 -6
- data/lib/castle/context/default.rb +36 -18
- data/lib/castle/context/sanitizer.rb +1 -0
- data/lib/castle/events.rb +49 -0
- data/lib/castle/extractors/client_id.rb +7 -3
- data/lib/castle/extractors/headers.rb +24 -30
- data/lib/castle/extractors/ip.rb +69 -5
- data/lib/castle/headers_filter.rb +35 -0
- data/lib/castle/headers_formatter.rb +22 -0
- data/lib/castle/validators/not_supported.rb +1 -0
- data/lib/castle/validators/present.rb +1 -0
- data/lib/castle/version.rb +1 -1
- data/spec/integration/rails/rails_spec.rb +61 -0
- data/spec/integration/rails/support/all.rb +6 -0
- data/spec/integration/rails/support/application.rb +15 -0
- data/spec/integration/rails/support/home_controller.rb +21 -0
- data/spec/lib/castle/api/request_spec.rb +43 -30
- data/spec/lib/castle/api/session_spec.rb +47 -0
- data/spec/lib/castle/api_spec.rb +4 -4
- data/spec/lib/castle/client_spec.rb +5 -3
- data/spec/lib/castle/commands/authenticate_spec.rb +1 -0
- data/spec/lib/castle/commands/identify_spec.rb +1 -0
- data/spec/lib/castle/commands/impersonate_spec.rb +1 -0
- data/spec/lib/castle/commands/track_spec.rb +1 -0
- data/spec/lib/castle/configuration_spec.rb +21 -4
- data/spec/lib/castle/context/default_spec.rb +13 -13
- data/spec/lib/castle/events_spec.rb +5 -0
- data/spec/lib/castle/extractors/client_id_spec.rb +2 -1
- data/spec/lib/castle/extractors/headers_spec.rb +67 -51
- data/spec/lib/castle/extractors/ip_spec.rb +89 -12
- data/spec/lib/castle/headers_filter_spec.rb +38 -0
- data/spec/lib/castle/{header_formatter_spec.rb → headers_formatter_spec.rb} +3 -3
- data/spec/lib/castle/utils/cloner_spec.rb +1 -0
- data/spec/lib/castle/utils/timestamp_spec.rb +3 -4
- data/spec/lib/castle/utils_spec.rb +1 -1
- data/spec/lib/castle/validators/not_supported_spec.rb +1 -3
- data/spec/spec_helper.rb +1 -2
- metadata +38 -10
- data/lib/castle/api/request/build.rb +0 -27
- data/lib/castle/header_formatter.rb +0 -9
- data/spec/lib/castle/api/request/build_spec.rb +0 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0dd544ffa6fc2660fc67c058011df5b9d83a8078b48506ab611809166960f501
|
4
|
+
data.tar.gz: 1720630a7f1925ba1142208114198cf176a8a3fec5c04be480aa4d2384e03de2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 71320c8ff0a5dd2a137723efc76c5530449bdf4635eed38f4c5fcad8bcb9253c5b2557b9113071cd606528eb58d3d66921fa1a728e6ea123d65ab2173e71ad59
|
7
|
+
data.tar.gz: d2f57e05bd976a294385808757b148248f01b2319cd99e88277bc18e104d4a2fabbcd2d1aa55057d7fc5a12e4ab7e3ec66086502c74ba6d8e5f6b8e51acfba01
|
data/README.md
CHANGED
@@ -85,20 +85,67 @@ Castle.configure do |config|
|
|
85
85
|
# We always blacklist Cookie and Authentication headers. If you use any other headers that
|
86
86
|
# might contain sensitive information, you should blacklist them.
|
87
87
|
config.blacklisted = ['HTTP-X-header']
|
88
|
+
|
89
|
+
# Castle needs the original IP of the client, not the IP of your proxy or load balancer.
|
90
|
+
# The SDK will only trust the proxy chain as defined in the configuration.
|
91
|
+
# We try to fetch the client IP based on X-Forwarded-For or Remote-Addr headers in that order,
|
92
|
+
# but sometimes the client IP may be stored in a different header or order.
|
93
|
+
# The SDK can be configured to look for the client IP address in headers that you specify.
|
94
|
+
# If the specified header or X-Forwarded-For default contains a proxy chain with public IP addresses,
|
95
|
+
# then one of the following must be set
|
96
|
+
# 1. The trusted_proxies value must match the known proxy IP's
|
97
|
+
# 2. The trusted_proxy_depth value must be set to the number of known trusted proxies in the chain (see below)
|
98
|
+
configuration.ip_headers = []
|
99
|
+
|
100
|
+
# Additionally to make X-Forwarded-For and other headers work better discovering client ip address,
|
101
|
+
# and not the address of a reverse proxy server, you can define trusted proxies
|
102
|
+
# which will help to fetch proper ip from those headers
|
103
|
+
|
104
|
+
# In order to extract the client IP of the X-Forwarded-For header
|
105
|
+
# and not the address of a reverse proxy server, you must define all trusted public proxies
|
106
|
+
# you can achieve this by listing all the proxies ip defined by string or regular expressions
|
107
|
+
# in trusted_proxies setting
|
108
|
+
configuration.trusted_proxies = []
|
109
|
+
# or by providing number of trusted proxies used in the chain
|
110
|
+
configuration.trusted_proxy_depth = 0
|
111
|
+
|
112
|
+
# If there is no possibility to define options above and there is no other header which can have client ip
|
113
|
+
# then you may set trust_proxy_chain = true to trust all of the proxy IP's in X-Forwarded-For
|
114
|
+
configuration.trust_proxy_chain = false
|
115
|
+
|
116
|
+
# *Note: default list of proxies which is always marked as trusted: Castle::Configuration::TRUSTED_PROXIES
|
88
117
|
end
|
89
118
|
```
|
90
119
|
|
120
|
+
## Event Context
|
121
|
+
|
91
122
|
The client will automatically configure the context for each request.
|
92
123
|
|
124
|
+
### Overriding Default Context Properties
|
125
|
+
|
126
|
+
If you need to modify the event context properties or if you desire to add additional properties such as user traits to the context, you can pass the properties in as options to the method of interest. An example:
|
127
|
+
```ruby
|
128
|
+
request_context = ::Castle::Client.to_context(request)
|
129
|
+
track_options = ::Castle::Client.to_options({
|
130
|
+
event: ::Castle::Events::LOGIN_SUCCEEDED,
|
131
|
+
user_id: user.id,
|
132
|
+
properties: {
|
133
|
+
key: 'value'
|
134
|
+
},
|
135
|
+
user_traits: {
|
136
|
+
key: 'value'
|
137
|
+
}
|
138
|
+
})
|
139
|
+
```
|
140
|
+
|
93
141
|
## Tracking
|
94
142
|
|
95
143
|
Here is a simple example of a track event.
|
96
144
|
|
97
|
-
|
98
145
|
```ruby
|
99
146
|
begin
|
100
147
|
castle.track(
|
101
|
-
event:
|
148
|
+
event: ::Castle::Events::LOGIN_SUCCEEDED,
|
102
149
|
user_id: user.id
|
103
150
|
)
|
104
151
|
rescue Castle::Error => e
|
@@ -132,7 +179,7 @@ end
|
|
132
179
|
```ruby
|
133
180
|
request_context = ::Castle::Client.to_context(request)
|
134
181
|
track_options = ::Castle::Client.to_options({
|
135
|
-
event:
|
182
|
+
event: ::Castle::Events::LOGIN_SUCCEEDED,
|
136
183
|
user_id: user.id,
|
137
184
|
properties: {
|
138
185
|
key: 'value'
|
@@ -144,13 +191,18 @@ track_options = ::Castle::Client.to_options({
|
|
144
191
|
CastleTrackingWorker.perform_async(request_context, track_options)
|
145
192
|
```
|
146
193
|
|
194
|
+
## Events
|
195
|
+
|
196
|
+
List of Recognized Events can be found [here](https://github.com/castle/castle-ruby/tree/master/lib/castle/events.rb) or in the [docs](https://docs.castle.io/api_reference/#list-of-recognized-events)
|
197
|
+
|
147
198
|
## Impersonation mode
|
148
199
|
|
149
|
-
https://castle.io/docs/
|
200
|
+
https://castle.io/docs/impersonation_mode
|
150
201
|
|
151
202
|
## Exceptions
|
152
203
|
|
153
|
-
`Castle::Error` will be thrown if the Castle API returns a 400 or a 500 level HTTP response.
|
204
|
+
`Castle::Error` will be thrown if the Castle API returns a 400 or a 500 level HTTP response.
|
205
|
+
You can also choose to catch a more [finegrained error](https://github.com/castle/castle-ruby/blob/master/lib/castle/errors.rb).
|
154
206
|
|
155
207
|
## Documentation
|
156
208
|
|
data/lib/castle.rb
CHANGED
@@ -9,6 +9,7 @@
|
|
9
9
|
|
10
10
|
%w[
|
11
11
|
castle/version
|
12
|
+
castle/events
|
12
13
|
castle/errors
|
13
14
|
castle/command
|
14
15
|
castle/utils
|
@@ -28,14 +29,15 @@
|
|
28
29
|
castle/configuration
|
29
30
|
castle/failover_auth_response
|
30
31
|
castle/client
|
31
|
-
castle/
|
32
|
+
castle/headers_filter
|
33
|
+
castle/headers_formatter
|
32
34
|
castle/secure_mode
|
33
35
|
castle/extractors/client_id
|
34
36
|
castle/extractors/headers
|
35
37
|
castle/extractors/ip
|
36
38
|
castle/api/response
|
37
39
|
castle/api/request
|
38
|
-
castle/api/
|
40
|
+
castle/api/session
|
39
41
|
castle/review
|
40
42
|
castle/api
|
41
43
|
].each(&method(:require))
|
@@ -52,7 +54,7 @@ module Castle
|
|
52
54
|
end
|
53
55
|
|
54
56
|
def config
|
55
|
-
|
57
|
+
Configuration.instance
|
56
58
|
end
|
57
59
|
|
58
60
|
def api_secret=(api_secret)
|
data/lib/castle/api.rb
CHANGED
@@ -17,16 +17,16 @@ module Castle
|
|
17
17
|
private_constant :HANDLED_ERRORS
|
18
18
|
|
19
19
|
class << self
|
20
|
+
# @param command [String]
|
21
|
+
# @param headers [Hash]
|
20
22
|
def request(command, headers = {})
|
21
23
|
raise Castle::ConfigurationError, 'configuration is not valid' unless Castle.config.valid?
|
22
24
|
|
23
25
|
begin
|
24
|
-
Castle::API::
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
headers
|
29
|
-
)
|
26
|
+
Castle::API::Request.call(
|
27
|
+
command,
|
28
|
+
Castle.config.api_secret,
|
29
|
+
headers
|
30
30
|
)
|
31
31
|
rescue *HANDLED_ERRORS => e
|
32
32
|
# @note We need to initialize the error, as the original error is a cause for this
|
@@ -35,6 +35,12 @@ module Castle
|
|
35
35
|
raise Castle::RequestError.new(e) # rubocop:disable Style/RaiseArgs
|
36
36
|
end
|
37
37
|
end
|
38
|
+
|
39
|
+
# @param command [String]
|
40
|
+
# @param headers [Hash]
|
41
|
+
def call(command, headers = {})
|
42
|
+
Castle::API::Response.call(request(command, headers))
|
43
|
+
end
|
38
44
|
end
|
39
45
|
end
|
40
46
|
end
|
data/lib/castle/api/request.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Castle
|
4
|
-
# this class is responsible for making requests to api
|
5
4
|
module API
|
5
|
+
# this class is responsible for making requests to api
|
6
6
|
module Request
|
7
7
|
# Default headers that we add to passed ones
|
8
8
|
DEFAULT_HEADERS = {
|
@@ -13,8 +13,8 @@ module Castle
|
|
13
13
|
|
14
14
|
class << self
|
15
15
|
def call(command, api_secret, headers)
|
16
|
-
|
17
|
-
|
16
|
+
Castle::API::Session.get.request(
|
17
|
+
build(
|
18
18
|
command,
|
19
19
|
headers.merge(DEFAULT_HEADERS),
|
20
20
|
api_secret
|
@@ -22,14 +22,19 @@ module Castle
|
|
22
22
|
)
|
23
23
|
end
|
24
24
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
def build(command, headers, api_secret)
|
26
|
+
request_obj = Net::HTTP.const_get(
|
27
|
+
command.method.to_s.capitalize
|
28
|
+
).new("#{Castle.config.url_prefix}/#{command.path}", headers)
|
29
|
+
|
30
|
+
unless command.method == :get
|
31
|
+
request_obj.body = ::Castle::Utils.replace_invalid_characters(
|
32
|
+
command.data
|
33
|
+
).to_json
|
31
34
|
end
|
32
|
-
|
35
|
+
|
36
|
+
request_obj.basic_auth('', api_secret)
|
37
|
+
request_obj
|
33
38
|
end
|
34
39
|
end
|
35
40
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
|
5
|
+
module Castle
|
6
|
+
module API
|
7
|
+
# this class keeps http config object
|
8
|
+
# and provides start/finish methods for persistent connection usage
|
9
|
+
# when there is a need of sending multiple requests at once
|
10
|
+
class Session
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
attr_accessor :http
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
reset
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset
|
20
|
+
@http = Net::HTTP.new(Castle.config.host, Castle.config.port)
|
21
|
+
@http.read_timeout = Castle.config.request_timeout / 1000.0
|
22
|
+
|
23
|
+
if Castle.config.port == 443
|
24
|
+
@http.use_ssl = true
|
25
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
26
|
+
end
|
27
|
+
|
28
|
+
@http
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
# @return [Net::HTTP]
|
33
|
+
def get
|
34
|
+
instance.http
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/castle/client.rb
CHANGED
@@ -23,6 +23,7 @@ module Castle
|
|
23
23
|
|
24
24
|
def failover_response_or_raise(failover_response, error)
|
25
25
|
return failover_response.generate unless Castle.config.failover_strategy == :throw
|
26
|
+
|
26
27
|
raise error
|
27
28
|
end
|
28
29
|
end
|
@@ -38,21 +39,16 @@ module Castle
|
|
38
39
|
def authenticate(options = {})
|
39
40
|
options = Castle::Utils.deep_symbolize_keys(options || {})
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
)
|
50
|
-
|
51
|
-
else
|
52
|
-
FailoverAuthResponse.new(
|
53
|
-
options[:user_id],
|
54
|
-
strategy: :allow, reason: 'Castle set to do not track.'
|
55
|
-
).generate
|
42
|
+
return generate_do_not_track_response(options[:user_id]) unless tracked?
|
43
|
+
|
44
|
+
add_timestamp_if_necessary(options)
|
45
|
+
command = Castle::Commands::Authenticate.new(@context).build(options)
|
46
|
+
begin
|
47
|
+
Castle::API.call(command).merge(failover: false, failover_reason: nil)
|
48
|
+
rescue Castle::RequestError, Castle::InternalServerError => e
|
49
|
+
self.class.failover_response_or_raise(
|
50
|
+
FailoverAuthResponse.new(options[:user_id], reason: e.to_s), e
|
51
|
+
)
|
56
52
|
end
|
57
53
|
end
|
58
54
|
|
@@ -60,27 +56,29 @@ module Castle
|
|
60
56
|
options = Castle::Utils.deep_symbolize_keys(options || {})
|
61
57
|
|
62
58
|
return unless tracked?
|
59
|
+
|
63
60
|
add_timestamp_if_necessary(options)
|
64
61
|
|
65
62
|
command = Castle::Commands::Identify.new(@context).build(options)
|
66
|
-
Castle::API.
|
63
|
+
Castle::API.call(command)
|
67
64
|
end
|
68
65
|
|
69
66
|
def track(options = {})
|
70
67
|
options = Castle::Utils.deep_symbolize_keys(options || {})
|
71
68
|
|
72
69
|
return unless tracked?
|
70
|
+
|
73
71
|
add_timestamp_if_necessary(options)
|
74
72
|
|
75
73
|
command = Castle::Commands::Track.new(@context).build(options)
|
76
|
-
Castle::API.
|
74
|
+
Castle::API.call(command)
|
77
75
|
end
|
78
76
|
|
79
77
|
def impersonate(options = {})
|
80
78
|
options = Castle::Utils.deep_symbolize_keys(options || {})
|
81
79
|
add_timestamp_if_necessary(options)
|
82
80
|
command = Castle::Commands::Impersonate.new(@context).build(options)
|
83
|
-
Castle::API.
|
81
|
+
Castle::API.call(command).tap do |response|
|
84
82
|
raise Castle::ImpersonationFailed unless response[:success]
|
85
83
|
end
|
86
84
|
end
|
@@ -99,6 +97,13 @@ module Castle
|
|
99
97
|
|
100
98
|
private
|
101
99
|
|
100
|
+
def generate_do_not_track_response(user_id)
|
101
|
+
FailoverAuthResponse.new(
|
102
|
+
user_id,
|
103
|
+
strategy: :allow, reason: 'Castle is set to do not track.'
|
104
|
+
).generate
|
105
|
+
end
|
106
|
+
|
102
107
|
def add_timestamp_if_necessary(options)
|
103
108
|
options[:timestamp] ||= @timestamp if @timestamp
|
104
109
|
end
|
data/lib/castle/configuration.rb
CHANGED
@@ -1,14 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'singleton'
|
4
|
+
|
3
5
|
module Castle
|
4
6
|
# manages configuration variables
|
5
7
|
class Configuration
|
8
|
+
include Singleton
|
9
|
+
|
6
10
|
HOST = 'api.castle.io'
|
7
11
|
PORT = 443
|
8
|
-
URL_PREFIX = 'v1'
|
12
|
+
URL_PREFIX = '/v1'
|
9
13
|
FAILOVER_STRATEGY = :allow
|
10
14
|
REQUEST_TIMEOUT = 500 # in milliseconds
|
11
15
|
FAILOVER_STRATEGIES = %i[allow deny challenge throw].freeze
|
16
|
+
# regexp of trusted proxies which is always appended to the trusted proxy list
|
17
|
+
TRUSTED_PROXIES = [/
|
18
|
+
\A127\.0\.0\.1\Z|
|
19
|
+
\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|
|
20
|
+
\A::1\Z|\Afd[0-9a-f]{2}:.+|
|
21
|
+
\Alocalhost\Z|
|
22
|
+
\Aunix\Z|
|
23
|
+
\Aunix:
|
24
|
+
/ix].freeze
|
12
25
|
|
13
26
|
# @note this value is not assigned as we don't recommend using a whitelist. If you need to use
|
14
27
|
# one, this constant is provided as a good default.
|
@@ -31,23 +44,32 @@ module Castle
|
|
31
44
|
X-Castle-Client-Id
|
32
45
|
].freeze
|
33
46
|
|
34
|
-
attr_accessor :host, :port, :request_timeout, :url_prefix
|
35
|
-
attr_reader :api_secret, :whitelisted, :blacklisted, :failover_strategy
|
47
|
+
attr_accessor :host, :port, :request_timeout, :url_prefix, :trust_proxy_chain
|
48
|
+
attr_reader :api_secret, :whitelisted, :blacklisted, :failover_strategy, :ip_headers,
|
49
|
+
:trusted_proxies, :trusted_proxy_depth
|
36
50
|
|
37
51
|
def initialize
|
38
|
-
@formatter = Castle::
|
52
|
+
@formatter = Castle::HeadersFormatter
|
39
53
|
@request_timeout = REQUEST_TIMEOUT
|
54
|
+
reset
|
55
|
+
end
|
56
|
+
|
57
|
+
def reset
|
40
58
|
self.failover_strategy = FAILOVER_STRATEGY
|
41
59
|
self.host = HOST
|
42
60
|
self.port = PORT
|
43
61
|
self.url_prefix = URL_PREFIX
|
44
62
|
self.whitelisted = [].freeze
|
45
63
|
self.blacklisted = [].freeze
|
46
|
-
self.api_secret = ''
|
64
|
+
self.api_secret = ENV.fetch('CASTLE_API_SECRET', '')
|
65
|
+
self.ip_headers = [].freeze
|
66
|
+
self.trusted_proxies = [].freeze
|
67
|
+
self.trust_proxy_chain = false
|
68
|
+
self.trusted_proxy_depth = nil
|
47
69
|
end
|
48
70
|
|
49
71
|
def api_secret=(value)
|
50
|
-
@api_secret =
|
72
|
+
@api_secret = value.to_s
|
51
73
|
end
|
52
74
|
|
53
75
|
def whitelisted=(value)
|
@@ -58,6 +80,27 @@ module Castle
|
|
58
80
|
@blacklisted = (value ? value.map { |header| @formatter.call(header) } : []).freeze
|
59
81
|
end
|
60
82
|
|
83
|
+
# sets ip headers
|
84
|
+
# @param value [Array<String>]
|
85
|
+
def ip_headers=(value)
|
86
|
+
raise Castle::ConfigurationError, 'ip headers must be an Array' unless value.is_a?(Array)
|
87
|
+
|
88
|
+
@ip_headers = value.map { |header| @formatter.call(header) }.freeze
|
89
|
+
end
|
90
|
+
|
91
|
+
# sets trusted proxies
|
92
|
+
# @param value [Array<String,Regexp>]
|
93
|
+
def trusted_proxies=(value)
|
94
|
+
raise Castle::ConfigurationError, 'trusted proxies must be an Array' unless value.is_a?(Array)
|
95
|
+
|
96
|
+
@trusted_proxies = value
|
97
|
+
end
|
98
|
+
|
99
|
+
# @param value [String,Number,NilClass]
|
100
|
+
def trusted_proxy_depth=(value)
|
101
|
+
@trusted_proxy_depth = value.to_i
|
102
|
+
end
|
103
|
+
|
61
104
|
def valid?
|
62
105
|
!api_secret.to_s.empty? && !host.to_s.empty? && !port.to_s.empty?
|
63
106
|
end
|