castle-rb 8.0.0 → 9.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/CHANGELOG.md +317 -0
- data/LICENSE +21 -0
- data/README.md +290 -112
- data/lib/castle/api/filter.rb +3 -1
- data/lib/castle/api/list_items/archive.rb +23 -0
- data/lib/castle/api/list_items/count.rb +22 -0
- data/lib/castle/api/list_items/create.rb +22 -0
- data/lib/castle/api/list_items/create_batch.rb +22 -0
- data/lib/castle/api/list_items/get.rb +22 -0
- data/lib/castle/api/list_items/query.rb +22 -0
- data/lib/castle/api/list_items/unarchive.rb +22 -0
- data/lib/castle/api/list_items/update.rb +22 -0
- data/lib/castle/api/lists/create.rb +23 -0
- data/lib/castle/api/lists/delete.rb +22 -0
- data/lib/castle/api/lists/get.rb +22 -0
- data/lib/castle/api/lists/get_all.rb +22 -0
- data/lib/castle/api/lists/query.rb +22 -0
- data/lib/castle/api/lists/update.rb +22 -0
- data/lib/castle/api/log.rb +2 -1
- data/lib/castle/api/privacy/delete_data.rb +23 -0
- data/lib/castle/api/privacy/request_data.rb +23 -0
- data/lib/castle/api/risk.rb +2 -1
- data/lib/castle/client.rb +15 -52
- data/lib/castle/client_actions/list_items.rb +49 -0
- data/lib/castle/client_actions/lists.rb +39 -0
- data/lib/castle/client_actions/privacy.rb +20 -0
- data/lib/castle/commands/list_items/archive.rb +24 -0
- data/lib/castle/commands/list_items/count.rb +23 -0
- data/lib/castle/commands/list_items/create.rb +22 -0
- data/lib/castle/commands/list_items/create_batch.rb +22 -0
- data/lib/castle/commands/list_items/get.rb +23 -0
- data/lib/castle/commands/list_items/query.rb +24 -0
- data/lib/castle/commands/list_items/unarchive.rb +23 -0
- data/lib/castle/commands/list_items/update.rb +22 -0
- data/lib/castle/commands/lists/create.rb +21 -0
- data/lib/castle/commands/lists/delete.rb +20 -0
- data/lib/castle/commands/lists/get.rb +20 -0
- data/lib/castle/commands/lists/get_all.rb +17 -0
- data/lib/castle/commands/lists/query.rb +21 -0
- data/lib/castle/commands/lists/update.rb +22 -0
- data/lib/castle/commands/privacy/delete_data.rb +20 -0
- data/lib/castle/commands/privacy/request_data.rb +20 -0
- data/lib/castle/core/get_connection.rb +5 -1
- data/lib/castle/core/process_response.rb +1 -0
- data/lib/castle/core/process_webhook.rb +1 -1
- data/lib/castle/core/send_request.rb +2 -1
- data/lib/castle/errors.rb +1 -5
- data/lib/castle/headers/filter.rb +1 -1
- data/lib/castle/version.rb +1 -1
- data/lib/castle.rb +36 -17
- metadata +62 -162
- data/lib/castle/api/approve_device.rb +0 -20
- data/lib/castle/api/authenticate.rb +0 -28
- data/lib/castle/api/end_impersonation.rb +0 -22
- data/lib/castle/api/get_device.rb +0 -20
- data/lib/castle/api/get_devices_for_user.rb +0 -20
- data/lib/castle/api/report_device.rb +0 -20
- data/lib/castle/api/start_impersonation.rb +0 -22
- data/lib/castle/api/track.rb +0 -19
- data/lib/castle/commands/approve_device.rb +0 -17
- data/lib/castle/commands/authenticate.rb +0 -23
- data/lib/castle/commands/end_impersonation.rb +0 -25
- data/lib/castle/commands/get_device.rb +0 -17
- data/lib/castle/commands/get_devices_for_user.rb +0 -17
- data/lib/castle/commands/report_device.rb +0 -17
- data/lib/castle/commands/start_impersonation.rb +0 -25
- data/lib/castle/commands/track.rb +0 -22
- data/lib/castle/support/hanami.rb +0 -15
- data/lib/castle/support/padrino.rb +0 -19
- data/spec/integration/rails/rails_spec.rb +0 -95
- data/spec/integration/rails/support/all.rb +0 -6
- data/spec/integration/rails/support/application.rb +0 -17
- data/spec/integration/rails/support/home_controller.rb +0 -39
- data/spec/lib/castle/api/approve_device_spec.rb +0 -17
- data/spec/lib/castle/api/authenticate_spec.rb +0 -133
- data/spec/lib/castle/api/end_impersonation_spec.rb +0 -59
- data/spec/lib/castle/api/filter_spec.rb +0 -5
- data/spec/lib/castle/api/get_device_spec.rb +0 -17
- data/spec/lib/castle/api/get_devices_for_user_spec.rb +0 -17
- data/spec/lib/castle/api/log_spec.rb +0 -5
- data/spec/lib/castle/api/report_device_spec.rb +0 -17
- data/spec/lib/castle/api/risk_spec.rb +0 -5
- data/spec/lib/castle/api/start_impersonation_spec.rb +0 -59
- data/spec/lib/castle/api/track_spec.rb +0 -65
- data/spec/lib/castle/api_spec.rb +0 -36
- data/spec/lib/castle/client_id/extract_spec.rb +0 -47
- data/spec/lib/castle/client_spec.rb +0 -337
- data/spec/lib/castle/command_spec.rb +0 -9
- data/spec/lib/castle/commands/approve_device_spec.rb +0 -24
- data/spec/lib/castle/commands/authenticate_spec.rb +0 -86
- data/spec/lib/castle/commands/end_impersonation_spec.rb +0 -72
- data/spec/lib/castle/commands/filter_spec.rb +0 -72
- data/spec/lib/castle/commands/get_device_spec.rb +0 -24
- data/spec/lib/castle/commands/get_devices_for_user_spec.rb +0 -24
- data/spec/lib/castle/commands/log_spec.rb +0 -73
- data/spec/lib/castle/commands/report_device_spec.rb +0 -24
- data/spec/lib/castle/commands/risk_spec.rb +0 -73
- data/spec/lib/castle/commands/start_impersonation_spec.rb +0 -72
- data/spec/lib/castle/commands/track_spec.rb +0 -89
- data/spec/lib/castle/configuration_spec.rb +0 -14
- data/spec/lib/castle/context/get_default_spec.rb +0 -41
- data/spec/lib/castle/context/merge_spec.rb +0 -23
- data/spec/lib/castle/context/prepare_spec.rb +0 -42
- data/spec/lib/castle/context/sanitize_spec.rb +0 -27
- data/spec/lib/castle/core/get_connection_spec.rb +0 -43
- data/spec/lib/castle/core/process_response_spec.rb +0 -103
- data/spec/lib/castle/core/process_webhook_spec.rb +0 -52
- data/spec/lib/castle/core/send_request_spec.rb +0 -97
- data/spec/lib/castle/failover/strategy_spec.rb +0 -12
- data/spec/lib/castle/headers/extract_spec.rb +0 -103
- data/spec/lib/castle/headers/filter_spec.rb +0 -42
- data/spec/lib/castle/headers/format_spec.rb +0 -25
- data/spec/lib/castle/ips/extract_spec.rb +0 -91
- data/spec/lib/castle/logger_spec.rb +0 -39
- data/spec/lib/castle/payload/prepare_spec.rb +0 -52
- data/spec/lib/castle/secure_mode_spec.rb +0 -7
- data/spec/lib/castle/session_spec.rb +0 -61
- data/spec/lib/castle/singleton_configuration_spec.rb +0 -14
- data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +0 -69
- data/spec/lib/castle/utils/clone_spec.rb +0 -19
- data/spec/lib/castle/utils/deep_symbolize_keys_spec.rb +0 -50
- data/spec/lib/castle/utils/get_timestamp_spec.rb +0 -16
- data/spec/lib/castle/utils/merge_spec.rb +0 -13
- data/spec/lib/castle/validators/not_supported_spec.rb +0 -19
- data/spec/lib/castle/validators/present_spec.rb +0 -25
- data/spec/lib/castle/verdict_spec.rb +0 -9
- data/spec/lib/castle/version_spec.rb +0 -5
- data/spec/lib/castle/webhooks/verify_spec.rb +0 -59
- data/spec/lib/castle_spec.rb +0 -58
- data/spec/spec_helper.rb +0 -24
- data/spec/support/shared_examples/action_request.rb +0 -167
- data/spec/support/shared_examples/configuration.rb +0 -99
data/README.md
CHANGED
|
@@ -1,163 +1,341 @@
|
|
|
1
|
-
# Ruby SDK
|
|
1
|
+
# Castle Ruby SDK
|
|
2
2
|
|
|
3
|
-
[](https://github.com/castle/castle-ruby/actions/workflows/specs.yml)
|
|
4
|
+
[](https://github.com/castle/castle-ruby/actions/workflows/lint.yml)
|
|
5
5
|
[](https://badge.fury.io/rb/castle-rb)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The official Ruby SDK for [Castle](https://castle.io). Castle analyzes user behavior in web and mobile apps to stop fraud before it happens.
|
|
8
|
+
|
|
9
|
+
This gem is a thin, dependency-light wrapper around the [Castle HTTP API](https://reference.castle.io). It exposes:
|
|
10
|
+
|
|
11
|
+
- **Risk Assessment** — `POST /v1/risk`, `POST /v1/filter`
|
|
12
|
+
- **Event logging** — `POST /v1/log` (fire-and-forget, no verdict)
|
|
13
|
+
- **Lists & List Items** — full CRUD + search + batch
|
|
14
|
+
- **Privacy / GDPR** — `POST` and `DELETE /v1/privacy/users` (Article 15 & 17)
|
|
15
|
+
- **Webhook signature verification**
|
|
16
|
+
|
|
17
|
+
A full list of supported events and the JSON shape of every payload is documented at <https://reference.castle.io>.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Ruby `>= 3.2`
|
|
22
|
+
- A [Castle](https://dashboard.castle.io) API secret
|
|
8
23
|
|
|
9
24
|
## Installation
|
|
10
25
|
|
|
11
|
-
Add the
|
|
26
|
+
Add the gem to your `Gemfile`:
|
|
12
27
|
|
|
13
28
|
```ruby
|
|
14
29
|
gem 'castle-rb'
|
|
15
30
|
```
|
|
16
31
|
|
|
17
|
-
|
|
32
|
+
Then:
|
|
18
33
|
|
|
19
|
-
|
|
34
|
+
```sh
|
|
35
|
+
bundle install
|
|
36
|
+
```
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
## Quick start
|
|
22
39
|
|
|
23
40
|
```ruby
|
|
24
|
-
|
|
41
|
+
require 'castle'
|
|
42
|
+
|
|
43
|
+
Castle.api_secret = ENV.fetch('CASTLE_API_SECRET')
|
|
44
|
+
|
|
45
|
+
verdict = Castle::API::Risk.call(
|
|
46
|
+
type: '$login',
|
|
47
|
+
status: '$succeeded',
|
|
48
|
+
request_token: params[:castle_request_token],
|
|
49
|
+
user: { id: '12345', email: 'user@example.com' },
|
|
50
|
+
context: Castle::Context::Prepare.call(request)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
case verdict[:policy][:action]
|
|
54
|
+
when 'deny' then # block the user
|
|
55
|
+
when 'challenge' then # send 2FA / additional verification
|
|
56
|
+
else # allow
|
|
57
|
+
end
|
|
25
58
|
```
|
|
26
59
|
|
|
27
|
-
|
|
60
|
+
`Castle::Context::Prepare.call(request)` extracts the IP and the headers Castle needs from a Rack-compatible `request` object. See [Advanced configuration](#advanced-configuration) for how header allow/deny lists and proxy chains are resolved.
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
28
63
|
|
|
29
|
-
|
|
64
|
+
The minimal, recommended setup:
|
|
30
65
|
|
|
31
|
-
|
|
66
|
+
```ruby
|
|
67
|
+
Castle.configure do |config|
|
|
68
|
+
# Same as `Castle.api_secret = ...`
|
|
69
|
+
config.api_secret = ENV.fetch('CASTLE_API_SECRET')
|
|
32
70
|
|
|
33
|
-
|
|
71
|
+
# Behavior when Castle's API is unreachable or returns a 5xx.
|
|
72
|
+
# One of: :allow (default), :deny, :challenge, :throw
|
|
73
|
+
config.failover_strategy = :allow
|
|
74
|
+
|
|
75
|
+
# Request timeout in milliseconds (default: 1000).
|
|
76
|
+
# `Castle::RequestError` is raised on timeout.
|
|
77
|
+
config.request_timeout = 1000
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Logging
|
|
34
82
|
|
|
35
83
|
```ruby
|
|
36
|
-
|
|
84
|
+
Castle.configure do |config|
|
|
85
|
+
config.logger = Logger.new($stdout)
|
|
86
|
+
end
|
|
87
|
+
```
|
|
37
88
|
|
|
38
|
-
|
|
39
|
-
|
|
89
|
+
The logger only needs to respond to `#info`. Each request and response (with sensitive values stripped) will be logged.
|
|
90
|
+
|
|
91
|
+
### Multi-environment / multi-tenant
|
|
92
|
+
|
|
93
|
+
Most apps only need one global config, but you can also build standalone `Castle::Configuration` objects and pass them per call:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
config = Castle::Configuration.new.tap do |c|
|
|
97
|
+
c.api_secret = ENV.fetch('CASTLE_API_SECRET_TENANT_A')
|
|
40
98
|
end
|
|
99
|
+
|
|
100
|
+
Castle::API::Risk.call(payload.merge(config: config))
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Usage
|
|
104
|
+
|
|
105
|
+
All endpoints are exposed as `Castle::API::<Endpoint>.call(payload)` and return a parsed `Hash`. The same payloads can be sent through `Castle::Client` (created from a Rack request), which automatically attaches request context and a do-not-track flag.
|
|
106
|
+
|
|
107
|
+
### Risk
|
|
108
|
+
|
|
109
|
+
Used for evaluating high-risk events such as logins, registrations, password resets, and transactions. Returns a verdict (`policy[:action]`) plus risk scores and signals.
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
Castle::API::Risk.call(
|
|
113
|
+
type: '$login',
|
|
114
|
+
status: '$succeeded',
|
|
115
|
+
request_token: params[:castle_request_token],
|
|
116
|
+
user: { id: '12345', email: 'user@example.com' },
|
|
117
|
+
context: Castle::Context::Prepare.call(request)
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Filter
|
|
122
|
+
|
|
123
|
+
Used to block bots and bad traffic early in the chain (typically registration). Same response shape as Risk.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
Castle::API::Filter.call(
|
|
127
|
+
type: '$registration',
|
|
128
|
+
status: '$attempted',
|
|
129
|
+
request_token: params[:castle_request_token],
|
|
130
|
+
params: { email: 'user@example.com' },
|
|
131
|
+
context: Castle::Context::Prepare.call(request)
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Log
|
|
136
|
+
|
|
137
|
+
Fire-and-forget event logging; no verdict is returned. Useful for events that should be visible in the Castle dashboard but don't need a real-time decision.
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
Castle::API::Log.call(
|
|
141
|
+
type: '$profile_update',
|
|
142
|
+
status: '$succeeded',
|
|
143
|
+
user: { id: '12345' },
|
|
144
|
+
context: Castle::Context::Prepare.call(request)
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Lists & List Items
|
|
149
|
+
|
|
150
|
+
Lists let you organize users, IPs, transactions, or any custom property and use them in policies as allow/deny lists. The SDK mirrors the [Lists API](https://reference.castle.io#tag/Lists):
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
list = Castle::API::Lists::Create.call(
|
|
154
|
+
name: 'Trusted IPs',
|
|
155
|
+
color: 'green',
|
|
156
|
+
primary_field: 'ip.address'
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
Castle::API::ListItems::Create.call(
|
|
160
|
+
list_id: list[:id],
|
|
161
|
+
primary_value: '1.2.3.4'
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
Castle::API::ListItems::Query.call(
|
|
165
|
+
list_id: list[:id],
|
|
166
|
+
filters: { primary_value: '1.2.3.4' }
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Available namespaces:
|
|
171
|
+
|
|
172
|
+
- `Castle::API::Lists::{Create, GetAll, Get, Update, Delete, Query}`
|
|
173
|
+
- `Castle::API::ListItems::{Create, CreateBatch, Get, Query, Count, Update, Archive, Unarchive}`
|
|
174
|
+
|
|
175
|
+
`CreateBatch` accepts up to ~1000 items per call and returns processing counts:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
Castle::API::ListItems::CreateBatch.call(
|
|
179
|
+
list_id: list[:id],
|
|
180
|
+
items: [
|
|
181
|
+
{ primary_value: '1.2.3.4', author: { type: '$other', identifier: 'me' } },
|
|
182
|
+
{ primary_value: '5.6.7.8', author: { type: '$other', identifier: 'me' } }
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
# => { total_received: 2, total_processed: 2, created: 2, ... }
|
|
41
186
|
```
|
|
42
187
|
|
|
43
|
-
|
|
188
|
+
### Privacy (GDPR)
|
|
189
|
+
|
|
190
|
+
To support GDPR Articles 15 (right of access) and 17 (right to be forgotten), the SDK exposes the current `/v1/privacy/users` endpoints. Both take a JSON body with `identifier` and `identifier_type` (`$id` or `$email`):
|
|
44
191
|
|
|
45
192
|
```ruby
|
|
46
|
-
|
|
193
|
+
Castle::API::Privacy::RequestData.call(
|
|
194
|
+
identifier: 'rhea@example.org',
|
|
195
|
+
identifier_type: '$email'
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
Castle::API::Privacy::DeleteData.call(
|
|
199
|
+
identifier: 'user_42',
|
|
200
|
+
identifier_type: '$id'
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
For the request flow, Castle compiles the user's data and emails a download link to the privacy address configured in the dashboard. Configure that address before calling `RequestData` for the first time.
|
|
47
205
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
206
|
+
> The deprecated path-based variants (`POST/DELETE /v1/privacy/users/{id}`) are intentionally not exposed by the SDK.
|
|
207
|
+
|
|
208
|
+
### Webhook signature verification
|
|
209
|
+
|
|
210
|
+
Castle signs every webhook with `X-Castle-Signature`. Verify it before trusting the payload:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
post '/castle/webhooks' do
|
|
214
|
+
Castle::Webhooks::Verify.call(request)
|
|
215
|
+
# signature is valid; proceed
|
|
216
|
+
rescue Castle::WebhookVerificationError
|
|
217
|
+
halt 400
|
|
52
218
|
end
|
|
53
219
|
```
|
|
54
220
|
|
|
55
|
-
###
|
|
221
|
+
### Framework helpers
|
|
222
|
+
|
|
223
|
+
Drop-in helpers expose a request-scoped `castle` client:
|
|
56
224
|
|
|
57
225
|
```ruby
|
|
58
|
-
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# For authenticate method you can set failover strategies: allow(default), deny, challenge, throw
|
|
63
|
-
config.failover_strategy = :deny
|
|
64
|
-
|
|
65
|
-
# Castle::RequestError is raised when timing out in milliseconds (default: 1000 milliseconds)
|
|
66
|
-
config.request_timeout = 1500
|
|
67
|
-
|
|
68
|
-
# Base Castle API url
|
|
69
|
-
# config.base_url = "https://api.castle.io/v1"
|
|
70
|
-
|
|
71
|
-
# Logger (need to respond to info method) - logs Castle API requests and responses
|
|
72
|
-
# config.logger = Logger.new(STDOUT)
|
|
73
|
-
|
|
74
|
-
# Allowlisted and Denylisted headers are case insensitive and allow to use _ and - as a separator, http prefixes are removed
|
|
75
|
-
# Allowlisted headers
|
|
76
|
-
# By default, the SDK sends all HTTP headers, except for Cookie and Authorization.
|
|
77
|
-
# If you decide to use a allowlist, the SDK will:
|
|
78
|
-
# - always send the User-Agent header
|
|
79
|
-
# - send scrubbed values of non-allowlisted headers
|
|
80
|
-
# - send proper values of allowlisted headers.
|
|
81
|
-
# @example
|
|
82
|
-
# config.allowlisted = ['X_HEADER']
|
|
83
|
-
# # will send { 'User-Agent' => 'Chrome', 'X_HEADER' => 'proper value', 'Any-Other-Header' => true }
|
|
84
|
-
#
|
|
85
|
-
# We highly suggest using denylist instead of allowlist, so that Castle can use as many data points
|
|
86
|
-
# as possible to secure your users. If you want to use the allowlist, this is the minimal
|
|
87
|
-
# amount of headers we recommend:
|
|
88
|
-
config.allowlisted = Castle::Configuration::DEFAULT_ALLOWLIST
|
|
226
|
+
require 'castle/support/rails' # `castle` available in controllers
|
|
227
|
+
require 'castle/support/sinatra' # `register Sinatra::Castle` for modular apps
|
|
228
|
+
```
|
|
89
229
|
|
|
90
|
-
|
|
91
|
-
# We always denylist Cookie and Authentication headers. If you use any other headers that
|
|
92
|
-
# might contain sensitive information, you should denylist them.
|
|
93
|
-
config.denylisted = ['HTTP-X-header']
|
|
94
|
-
|
|
95
|
-
# Castle needs the original IP of the client, not the IP of your proxy or load balancer.
|
|
96
|
-
# The SDK will only trust the proxy chain as defined in the configuration.
|
|
97
|
-
# We try to fetch the client IP based on X-Forwarded-For or Remote-Addr headers in that order,
|
|
98
|
-
# but sometimes the client IP may be stored in a different header or order.
|
|
99
|
-
# The SDK can be configured to look for the client IP address in headers that you specify.
|
|
100
|
-
|
|
101
|
-
# Sometimes, Cloud providers do not use consistent IP addresses to proxy requests.
|
|
102
|
-
# In this case, the client IP is usually preserved in a custom header. Example:
|
|
103
|
-
# Cloudflare preserves the client request in the 'Cf-Connecting-Ip' header.
|
|
104
|
-
# It would be used like so: config.ip_headers=['Cf-Connecting-Ip']
|
|
105
|
-
config.ip_headers = []
|
|
106
|
-
|
|
107
|
-
# If the specified header or X-Forwarded-For default contains a proxy chain with public IP addresses,
|
|
108
|
-
# then you must choose only one of the following (but not both):
|
|
109
|
-
# 1. The trusted_proxies value must match the known proxy IPs. This option is preferable if the IP is static.
|
|
110
|
-
# 2. The trusted_proxy_depth value must be set to the number of known trusted proxies in the chain (see below).
|
|
111
|
-
# This option is preferable if the IPs are ephemeral, but the depth is consistent.
|
|
112
|
-
|
|
113
|
-
# Additionally to make X-Forwarded-For and other headers work better discovering client ip address,
|
|
114
|
-
# and not the address of a reverse proxy server, you can define trusted proxies
|
|
115
|
-
# which will help to fetch proper ip from those headers
|
|
116
|
-
|
|
117
|
-
# In order to extract the client IP of the X-Forwarded-For header
|
|
118
|
-
# and not the address of a reverse proxy server, you must define all trusted public proxies
|
|
119
|
-
# you can achieve this by listing all the proxies ip defined by string or regular expressions
|
|
120
|
-
# in the trusted_proxies setting
|
|
121
|
-
config.trusted_proxies = []
|
|
122
|
-
|
|
123
|
-
# or by providing number of trusted proxies used in the chain
|
|
124
|
-
config.trusted_proxy_depth = 0
|
|
125
|
-
|
|
126
|
-
# note that you must pick one approach over the other.
|
|
127
|
-
|
|
128
|
-
# If there is no possibility to define options above and there is no other header that holds the client IP,
|
|
129
|
-
# then you may set trust_proxy_chain = true to trust all of the proxy IPs in X-Forwarded-For
|
|
130
|
-
config.trust_proxy_chain = false
|
|
131
|
-
# *Warning*: this mode is highly promiscuous and could lead to wrongly trusting a spoofed IP if the request passes through a malicious proxy
|
|
230
|
+
Each helper memoizes `Castle::Client.from_request(request)` on first access.
|
|
132
231
|
|
|
133
|
-
|
|
232
|
+
For any other framework you can wire it up yourself in one line:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
def castle
|
|
236
|
+
@castle ||= Castle::Client.from_request(request)
|
|
134
237
|
end
|
|
135
238
|
```
|
|
136
239
|
|
|
137
|
-
|
|
240
|
+
## Advanced configuration
|
|
241
|
+
|
|
242
|
+
The defaults are good for most deployments. The options below only matter if you have a non-trivial proxy chain or strict header policies.
|
|
138
243
|
|
|
139
|
-
|
|
244
|
+
### Header allow/deny lists
|
|
245
|
+
|
|
246
|
+
By default the SDK sends every HTTP header except `Cookie` and `Authorization`. Castle uses these headers to fingerprint the request, so the broader the better.
|
|
140
247
|
|
|
141
248
|
```ruby
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
249
|
+
Castle.configure do |config|
|
|
250
|
+
# Always-blocked headers (in addition to Cookie/Authorization).
|
|
251
|
+
config.denylisted = ['HTTP-X-Internal-Header']
|
|
252
|
+
|
|
253
|
+
# Strict allow-list mode. Headers outside the list are sent with
|
|
254
|
+
# scrubbed values, except for User-Agent which is always preserved.
|
|
255
|
+
# We recommend the curated default if you have to use an allow list:
|
|
256
|
+
config.allowlisted = Castle::Configuration::DEFAULT_ALLOWLIST
|
|
257
|
+
end
|
|
148
258
|
```
|
|
149
259
|
|
|
150
|
-
|
|
260
|
+
Header names are case-insensitive and accept both `_` and `-` as separators. A leading `HTTP_` prefix is stripped automatically.
|
|
261
|
+
|
|
262
|
+
### Client IP detection
|
|
263
|
+
|
|
264
|
+
Castle needs the original client IP, not the IP of your proxy or load balancer. The SDK reads `X-Forwarded-For` and `Remote-Addr` by default; pick **one** of the strategies below depending on your infrastructure:
|
|
151
265
|
|
|
152
266
|
```ruby
|
|
153
|
-
|
|
267
|
+
Castle.configure do |config|
|
|
268
|
+
# 1. Custom header (e.g. Cloudflare's Cf-Connecting-Ip).
|
|
269
|
+
config.ip_headers = ['Cf-Connecting-Ip']
|
|
270
|
+
|
|
271
|
+
# 2. Static, known proxy IPs (strings or regexes).
|
|
272
|
+
config.trusted_proxies = ['10.0.0.1', /\A192\.168\./]
|
|
273
|
+
|
|
274
|
+
# 3. Ephemeral proxies but known chain depth.
|
|
275
|
+
config.trusted_proxy_depth = 2
|
|
276
|
+
|
|
277
|
+
# 4. Last resort: trust the entire X-Forwarded-For chain.
|
|
278
|
+
# Warning: vulnerable to header spoofing if a malicious proxy is in path.
|
|
279
|
+
config.trust_proxy_chain = false
|
|
280
|
+
end
|
|
154
281
|
```
|
|
155
282
|
|
|
156
|
-
|
|
283
|
+
Pick **either** `trusted_proxies` **or** `trusted_proxy_depth`, never both. Private/loopback ranges in `Castle::Configuration::TRUSTED_PROXIES` are always considered trusted.
|
|
284
|
+
|
|
285
|
+
## Errors
|
|
286
|
+
|
|
287
|
+
All exceptions inherit from `Castle::Error`. The most useful ones:
|
|
288
|
+
|
|
289
|
+
| Class | Raised when |
|
|
290
|
+
| ---------------------------------- | ------------------------------------------------------------- |
|
|
291
|
+
| `Castle::ConfigurationError` | The SDK is misconfigured (missing API secret, bad URL, etc.). |
|
|
292
|
+
| `Castle::RequestError` | Network failure or timeout reaching Castle. |
|
|
293
|
+
| `Castle::InvalidRequestTokenError` | The `request_token` is missing or invalid. |
|
|
294
|
+
| `Castle::InvalidParametersError` | 422 response with validation details. |
|
|
295
|
+
| `Castle::RateLimitError` | 429 response — back off and retry. |
|
|
296
|
+
| `Castle::UnauthorizedError` | 401 — bad API secret. |
|
|
297
|
+
| `Castle::InternalServerError` | 5xx response from Castle. |
|
|
298
|
+
| `Castle::WebhookVerificationError` | Webhook signature did not match. |
|
|
299
|
+
|
|
300
|
+
The full list lives in [`lib/castle/errors.rb`](lib/castle/errors.rb).
|
|
157
301
|
|
|
158
|
-
|
|
302
|
+
## Upgrading to 9.0
|
|
303
|
+
|
|
304
|
+
`9.0` removes a number of legacy endpoints and DSL methods. If you're upgrading from 8.x:
|
|
305
|
+
|
|
306
|
+
| Removed | Replacement |
|
|
307
|
+
| ------------------------------------------------------------------------ | ---------------------------------------- |
|
|
308
|
+
| `Castle::API::Track` / `Castle::Client#track` | `Castle::API::Log` or `Castle::API::Risk` |
|
|
309
|
+
| `Castle::API::Authenticate` / `Castle::Client#authenticate` | `Castle::API::Risk` |
|
|
310
|
+
| `Castle::API::ApproveDevice` / `GetDevice` / `GetDevicesForUser` / `ReportDevice` | No direct replacement — contact support |
|
|
311
|
+
| `Castle::API::StartImpersonation` / `EndImpersonation` | No direct replacement — contact support |
|
|
312
|
+
| `Castle::ImpersonationFailed` | Removed |
|
|
313
|
+
|
|
314
|
+
New in 9.0:
|
|
315
|
+
|
|
316
|
+
- `Castle::API::ListItems::CreateBatch` (`POST /v1/lists/{id}/items/batch`)
|
|
317
|
+
- `Castle::API::Privacy::{RequestData, DeleteData}` (`POST` / `DELETE /v1/privacy/users`) — closes [#261](https://github.com/castle/castle-ruby/issues/261)
|
|
318
|
+
- Failover handlers in `Risk`, `Filter`, and `Log` no longer crash when `options[:user]` is missing — closes [#279](https://github.com/castle/castle-ruby/issues/279)
|
|
319
|
+
|
|
320
|
+
Minimum supported Ruby is now `3.2`. See [`CHANGELOG.md`](CHANGELOG.md) for the full list.
|
|
321
|
+
|
|
322
|
+
## Contributing
|
|
323
|
+
|
|
324
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/castle/castle-ruby).
|
|
325
|
+
|
|
326
|
+
```sh
|
|
327
|
+
bundle install
|
|
328
|
+
bundle exec rspec # run the test suite
|
|
329
|
+
bin/lint # run RuboCop and Prettier
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
To test against a specific Rails version, set `BUNDLE_GEMFILE` to one of the files in [`gemfiles/`](gemfiles):
|
|
333
|
+
|
|
334
|
+
```sh
|
|
335
|
+
BUNDLE_GEMFILE=gemfiles/rails_8.1.gemfile bundle install
|
|
336
|
+
BUNDLE_GEMFILE=gemfiles/rails_8.1.gemfile bundle exec rspec
|
|
337
|
+
```
|
|
159
338
|
|
|
160
|
-
##
|
|
339
|
+
## License
|
|
161
340
|
|
|
162
|
-
|
|
163
|
-
You can also choose to catch a more [finegrained error](https://github.com/castle/castle-ruby/blob/master/lib/castle/errors.rb).
|
|
341
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
data/lib/castle/api/filter.rb
CHANGED
|
@@ -18,7 +18,9 @@ module Castle
|
|
|
18
18
|
rescue Castle::RequestError, Castle::InternalServerError => e
|
|
19
19
|
unless config.failover_strategy == :throw
|
|
20
20
|
strategy = (config || Castle.config).failover_strategy
|
|
21
|
-
|
|
21
|
+
# `user` is optional on /v1/filter (#279) — fall back to `matching_user_id` then nil.
|
|
22
|
+
user_id = options.dig(:user, :id) || options[:matching_user_id]
|
|
23
|
+
return Castle::Failover::PrepareResponse.new(user_id, reason: e.to_s, strategy: strategy).call
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
raise e
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Castle
|
|
4
|
+
module API
|
|
5
|
+
# Namespace for the list items API ednpoints
|
|
6
|
+
module ListItems
|
|
7
|
+
# Sends DELETE /lists/:list_id/items/:item_id request
|
|
8
|
+
module Archive
|
|
9
|
+
class << self
|
|
10
|
+
# @param options [Hash]
|
|
11
|
+
# @return [Hash]
|
|
12
|
+
def call(options = {})
|
|
13
|
+
options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
|
|
14
|
+
http = options.delete(:http)
|
|
15
|
+
config = options.delete(:config) || Castle.config
|
|
16
|
+
|
|
17
|
+
Castle::API.call(Castle::Commands::ListItems::Archive.build(options), {}, http, config)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Castle
|
|
4
|
+
module API
|
|
5
|
+
module ListItems
|
|
6
|
+
# Sends POST /lists/:list_id/items request
|
|
7
|
+
module Count
|
|
8
|
+
class << self
|
|
9
|
+
# @param options [Hash]
|
|
10
|
+
# @return [Hash]
|
|
11
|
+
def call(options = {})
|
|
12
|
+
options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
|
|
13
|
+
http = options.delete(:http)
|
|
14
|
+
config = options.delete(:config) || Castle.config
|
|
15
|
+
|
|
16
|
+
Castle::API.call(Castle::Commands::ListItems::Count.build(options), {}, http, config)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Castle
|
|
4
|
+
module API
|
|
5
|
+
module ListItems
|
|
6
|
+
# Sends POST /lists/:list_id/items request
|
|
7
|
+
module Create
|
|
8
|
+
class << self
|
|
9
|
+
# @param options [Hash]
|
|
10
|
+
# @return [Hash]
|
|
11
|
+
def call(options = {})
|
|
12
|
+
options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
|
|
13
|
+
http = options.delete(:http)
|
|
14
|
+
config = options.delete(:config) || Castle.config
|
|
15
|
+
|
|
16
|
+
Castle::API.call(Castle::Commands::ListItems::Create.build(options), {}, http, config)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Castle
|
|
4
|
+
module API
|
|
5
|
+
module ListItems
|
|
6
|
+
# Sends POST /lists/{list_id}/items/batch request
|
|
7
|
+
module CreateBatch
|
|
8
|
+
class << self
|
|
9
|
+
# @param options [Hash]
|
|
10
|
+
# @return [Hash]
|
|
11
|
+
def call(options = {})
|
|
12
|
+
options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
|
|
13
|
+
http = options.delete(:http)
|
|
14
|
+
config = options.delete(:config) || Castle.config
|
|
15
|
+
|
|
16
|
+
Castle::API.call(Castle::Commands::ListItems::CreateBatch.build(options), {}, http, config)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Castle
|
|
4
|
+
module API
|
|
5
|
+
module ListItems
|
|
6
|
+
# Sends GET /lists/:list_id/items/:item_id request
|
|
7
|
+
module Get
|
|
8
|
+
class << self
|
|
9
|
+
# @param options [Hash]
|
|
10
|
+
# @return [Hash]
|
|
11
|
+
def call(options = {})
|
|
12
|
+
options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
|
|
13
|
+
http = options.delete(:http)
|
|
14
|
+
config = options.delete(:config) || Castle.config
|
|
15
|
+
|
|
16
|
+
Castle::API.call(Castle::Commands::ListItems::Get.build(options), {}, http, config)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Castle
|
|
4
|
+
module API
|
|
5
|
+
module ListItems
|
|
6
|
+
# Sends POST /lists/:list_id/items/query request
|
|
7
|
+
module Query
|
|
8
|
+
class << self
|
|
9
|
+
# @param options [Hash]
|
|
10
|
+
# @return [Hash]
|
|
11
|
+
def call(options = {})
|
|
12
|
+
options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
|
|
13
|
+
http = options.delete(:http)
|
|
14
|
+
config = options.delete(:config) || Castle.config
|
|
15
|
+
|
|
16
|
+
Castle::API.call(Castle::Commands::ListItems::Query.build(options), {}, http, config)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Castle
|
|
4
|
+
module API
|
|
5
|
+
module ListItems
|
|
6
|
+
# Sends PUT /lists/:list_id/items/:item_id/unarchive request
|
|
7
|
+
module Unarchive
|
|
8
|
+
class << self
|
|
9
|
+
# @param options [Hash]
|
|
10
|
+
# @return [Hash]
|
|
11
|
+
def call(options = {})
|
|
12
|
+
options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
|
|
13
|
+
http = options.delete(:http)
|
|
14
|
+
config = options.delete(:config) || Castle.config
|
|
15
|
+
|
|
16
|
+
Castle::API.call(Castle::Commands::ListItems::Unarchive.build(options), {}, http, config)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Castle
|
|
4
|
+
module API
|
|
5
|
+
module ListItems
|
|
6
|
+
# Sends PUT /lists/:list_id/items/:item_id request
|
|
7
|
+
module Update
|
|
8
|
+
class << self
|
|
9
|
+
# @param options [Hash]
|
|
10
|
+
# @return [Hash]
|
|
11
|
+
def call(options = {})
|
|
12
|
+
options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
|
|
13
|
+
http = options.delete(:http)
|
|
14
|
+
config = options.delete(:config) || Castle.config
|
|
15
|
+
|
|
16
|
+
Castle::API.call(Castle::Commands::ListItems::Update.build(options), {}, http, config)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|