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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +57 -5
  3. data/lib/castle.rb +5 -3
  4. data/lib/castle/api.rb +12 -6
  5. data/lib/castle/api/request.rb +15 -10
  6. data/lib/castle/api/session.rb +39 -0
  7. data/lib/castle/client.rb +23 -18
  8. data/lib/castle/configuration.rb +49 -6
  9. data/lib/castle/context/default.rb +36 -18
  10. data/lib/castle/context/sanitizer.rb +1 -0
  11. data/lib/castle/events.rb +49 -0
  12. data/lib/castle/extractors/client_id.rb +7 -3
  13. data/lib/castle/extractors/headers.rb +24 -30
  14. data/lib/castle/extractors/ip.rb +69 -5
  15. data/lib/castle/headers_filter.rb +35 -0
  16. data/lib/castle/headers_formatter.rb +22 -0
  17. data/lib/castle/validators/not_supported.rb +1 -0
  18. data/lib/castle/validators/present.rb +1 -0
  19. data/lib/castle/version.rb +1 -1
  20. data/spec/integration/rails/rails_spec.rb +61 -0
  21. data/spec/integration/rails/support/all.rb +6 -0
  22. data/spec/integration/rails/support/application.rb +15 -0
  23. data/spec/integration/rails/support/home_controller.rb +21 -0
  24. data/spec/lib/castle/api/request_spec.rb +43 -30
  25. data/spec/lib/castle/api/session_spec.rb +47 -0
  26. data/spec/lib/castle/api_spec.rb +4 -4
  27. data/spec/lib/castle/client_spec.rb +5 -3
  28. data/spec/lib/castle/commands/authenticate_spec.rb +1 -0
  29. data/spec/lib/castle/commands/identify_spec.rb +1 -0
  30. data/spec/lib/castle/commands/impersonate_spec.rb +1 -0
  31. data/spec/lib/castle/commands/track_spec.rb +1 -0
  32. data/spec/lib/castle/configuration_spec.rb +21 -4
  33. data/spec/lib/castle/context/default_spec.rb +13 -13
  34. data/spec/lib/castle/events_spec.rb +5 -0
  35. data/spec/lib/castle/extractors/client_id_spec.rb +2 -1
  36. data/spec/lib/castle/extractors/headers_spec.rb +67 -51
  37. data/spec/lib/castle/extractors/ip_spec.rb +89 -12
  38. data/spec/lib/castle/headers_filter_spec.rb +38 -0
  39. data/spec/lib/castle/{header_formatter_spec.rb → headers_formatter_spec.rb} +3 -3
  40. data/spec/lib/castle/utils/cloner_spec.rb +1 -0
  41. data/spec/lib/castle/utils/timestamp_spec.rb +3 -4
  42. data/spec/lib/castle/utils_spec.rb +1 -1
  43. data/spec/lib/castle/validators/not_supported_spec.rb +1 -3
  44. data/spec/spec_helper.rb +1 -2
  45. metadata +38 -10
  46. data/lib/castle/api/request/build.rb +0 -27
  47. data/lib/castle/header_formatter.rb +0 -9
  48. 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: 01fa92f200806e9649d71afa71dff0e8ff37db950a74e122134fdbda5413ef8a
4
- data.tar.gz: f3a11d66711788d68c228ff63b647326e5446fff0ddb1be4e33e4b400d4587a8
3
+ metadata.gz: 0dd544ffa6fc2660fc67c058011df5b9d83a8078b48506ab611809166960f501
4
+ data.tar.gz: 1720630a7f1925ba1142208114198cf176a8a3fec5c04be480aa4d2384e03de2
5
5
  SHA512:
6
- metadata.gz: 5329a71bd213ee88665b68ab4a79f6eca7987355d839d9e26b4d132ea3d4b7986714d23c3b6f97e7743ce774d969438365ea004fdcdd99eb7643f5c07c43d7b9
7
- data.tar.gz: 89d8241793e59288651ac45b8d799c545a9b60aa25e6ae8d0092cda6183e9ecdf3b9d5b8ee68e0184b8021dc8633f9f949c987c6f0cd3abc1db6f7f2b47bda99
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: '$login.succeeded',
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: '$login.succeeded',
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/impersonation
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. You can also choose to catch a more [finegrained error](https://github.com/castle/castle-ruby/blob/master/lib/castle/errors.rb).
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
 
@@ -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/header_formatter
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/request/build
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
- @configuration ||= Castle::Configuration.new
57
+ Configuration.instance
56
58
  end
57
59
 
58
60
  def api_secret=(api_secret)
@@ -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::Response.call(
25
- Castle::API::Request.call(
26
- command,
27
- Castle.config.api_secret,
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
@@ -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
- http.request(
17
- Castle::API::Request::Build.call(
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 http
26
- http = Net::HTTP.new(Castle.config.host, Castle.config.port)
27
- http.read_timeout = Castle.config.request_timeout / 1000.0
28
- if Castle.config.port == 443
29
- http.use_ssl = true
30
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
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
- http
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
@@ -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
- if tracked?
42
- add_timestamp_if_necessary(options)
43
- command = Castle::Commands::Authenticate.new(@context).build(options)
44
- begin
45
- Castle::API.request(command).merge(failover: false, failover_reason: nil)
46
- rescue Castle::RequestError, Castle::InternalServerError => e
47
- self.class.failover_response_or_raise(
48
- FailoverAuthResponse.new(options[:user_id], reason: e.to_s), e
49
- )
50
- end
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.request(command)
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.request(command)
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.request(command).tap do |response|
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
@@ -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::HeaderFormatter.new
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 = ENV.fetch('CASTLE_API_SECRET', value).to_s
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