optiaudit-rails 0.1.0 → 0.1.1
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 +16 -1
- data/README.md +24 -8
- data/lib/optiaudit/event.rb +33 -4
- data/lib/optiaudit/request_data.rb +5 -4
- data/lib/optiaudit/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2da47ba93a09717845045e65800eebf3f280a2261402a2bb7f7bf7a2db72a7a2
|
|
4
|
+
data.tar.gz: a5c6b63dfdba71cfaf7843f3ba0daedffbd1e5d76834cb4a74bb4174ace02ac7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6578b9531cbdefe3dcb5c05268d6ebaf9c6bc45c6d7773df090cd2822f325d910f76edad3b5b367752845439b1cb924a3c5f6a0546c8ec41076c7cccb2e1709b
|
|
7
|
+
data.tar.gz: eae50f9a7632ca5a6087292e9b3d2e3c99faffa37619a3e1d86dff713c027ae22243b71de72d6ba18e05689ded14c5ca380ddfe86e85c8433ce72960628a37b3
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.1] - 2026-10-22
|
|
11
|
+
|
|
12
|
+
Correcciones de la verificación del README contra OptiAudit real, con la 0.1.0 publicada.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **IP falsificada.** Cuando `Client-IP` y `X-Forwarded-For` no coinciden (`IpSpoofAttackError`), la 0.1.0 mandaba `request.ip`, que es el valor que eligió el cliente. Ahora el evento sale sin `ip_address`, con un warning.
|
|
17
|
+
- **`ip_address` inválido** pasado a mano: OptiAudit lo acepta con 202 y después su worker pierde el evento sin avisar. Ahora la gema lo omite.
|
|
18
|
+
- **`client_timestamp` que no es ISO 8601** pasado a mano: OptiAudit rechazaba el evento con 400. Ahora se reemplaza por la hora actual, con un warning.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- README: con el adaptador `:thread`, OptiAudit pisa el `metadata.request_id` de cada evento con el id del POST del batch. El spoofing de IP ocurre en Rails 5.2 a 7.0, y en Rails 5.2 con log `info` es Rails, no la gema, el que responde 500.
|
|
23
|
+
|
|
10
24
|
## [0.1.0] - 2026-10-22
|
|
11
25
|
|
|
12
26
|
Primera versión: el equivalente para Rails de `@theoptimalpartner/nestjs-optiaudit` 0.1.8 (SC25-2703).
|
|
@@ -23,5 +37,6 @@ Primera versión: el equivalente para Rails de `@theoptimalpartner/nestjs-optiau
|
|
|
23
37
|
- **`Optiaudit::Testing`** para que las apps verifiquen sus eventos en tests sin HTTP.
|
|
24
38
|
- Compatibilidad probada en CI: Ruby 2.7 + Rails 5.2 + Sidekiq 6.3, Ruby 3.0 + Rails 6.1 + Sidekiq 6.5 y Ruby 3.2 + Rails 7.0 + Sidekiq 7.2.
|
|
25
39
|
|
|
26
|
-
[Unreleased]: https://bitbucket.org/RoutingUC/optiaudit-rails/branches/compare/main%0Dv0.1.
|
|
40
|
+
[Unreleased]: https://bitbucket.org/RoutingUC/optiaudit-rails/branches/compare/main%0Dv0.1.1
|
|
41
|
+
[0.1.1]: https://bitbucket.org/RoutingUC/optiaudit-rails/src/v0.1.1/
|
|
27
42
|
[0.1.0]: https://bitbucket.org/RoutingUC/optiaudit-rails/src/v0.1.0/
|
data/README.md
CHANGED
|
@@ -166,7 +166,7 @@ Request data, also filled in by `optiaudit_track`:
|
|
|
166
166
|
| `ip_address` | `request.remote_ip` (see below) |
|
|
167
167
|
| `user_agent` | `request.user_agent` |
|
|
168
168
|
| `session_id` | SHA-256 of the session id. The raw id is a credential and never leaves the app. Only sent when the action already loaded the session. |
|
|
169
|
-
| `metadata.request_id` | `request.request_id` (`X-Request-ID`), also sent as the `X-Request-ID` header to OptiAudit |
|
|
169
|
+
| `metadata.request_id` | `request.request_id` (`X-Request-ID`), also sent as the `X-Request-ID` header to OptiAudit. With the `:thread` adapter OptiAudit replaces it with the id of the batch request (see [Delivery](#delivery)). |
|
|
170
170
|
| `metadata.module` | the module given to the macro |
|
|
171
171
|
|
|
172
172
|
## Client IP Resolution
|
|
@@ -179,15 +179,18 @@ rule as the NestJS package since `v0.1.8`: values a client injects on the left a
|
|
|
179
179
|
If your proxies use public addresses (a CDN in front of the load balancer), add them to
|
|
180
180
|
`config.action_dispatch.trusted_proxies`, or `remote_ip` will return the proxy.
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
The gem
|
|
182
|
+
When `Client-IP` and `X-Forwarded-For` disagree, `remote_ip` raises `IpSpoofAttackError` (Rails 5.2 to 7.0).
|
|
183
|
+
The gem logs a warning and sends the event without `ip_address`: the other sources of the IP, like
|
|
184
|
+
`request.ip`, return a value the client chose. The gem never raises, but Rails itself may: in Rails 5.2
|
|
185
|
+
the request logger calls `remote_ip` at log level `info`, so with those headers the request fails with
|
|
186
|
+
500 before reaching the controller, with or without this gem.
|
|
184
187
|
|
|
185
188
|
## Delivery
|
|
186
189
|
|
|
187
190
|
| Adapter | When | How |
|
|
188
191
|
|---------|------|-----|
|
|
189
192
|
| `:sidekiq` | Default when Sidekiq is loaded | One job per event in the `optiaudit` queue. Survives deploys. Retried with Sidekiq's backoff (5 times by default) on 5xx, 429, timeouts and connection errors. Other 4xx are dropped with a warning: retrying would not fix them. |
|
|
190
|
-
| `:thread` | Default without Sidekiq | In-memory buffer sent to `POST /events/batch` (up to 100 events) every 5 seconds or when full, from a background thread. Works with forking servers (Puma, Unicorn): each process starts its own thread. Pending events are flushed at exit. Events still in memory are lost if the process is killed, and failed batches are not retried. |
|
|
193
|
+
| `:thread` | Default without Sidekiq | In-memory buffer sent to `POST /events/batch` (up to 100 events) every 5 seconds or when full, from a background thread. Works with forking servers (Puma, Unicorn): each process starts its own thread. Pending events are flushed at exit. Events still in memory are lost if the process is killed, and failed batches are not retried. OptiAudit sets `metadata.request_id` of every event in a batch to the id of the batch request, so the request id of each event is lost. |
|
|
191
194
|
| `:inline` | Console, scripts, debugging | Synchronous `POST /events` in the caller's thread. Adds up to `timeout` seconds to a request. |
|
|
192
195
|
|
|
193
196
|
Sidekiq jobs receive the event as a hash of string keys and JSON values, so they work with Sidekiq 7's
|
|
@@ -271,11 +274,11 @@ Fields accepted by `Optiaudit.track` and `optiaudit_track`, with the limits of O
|
|
|
271
274
|
| `resource_type` | string | Truncated to 100. |
|
|
272
275
|
| `resource_id` | string | Truncated to 100. |
|
|
273
276
|
| `resource_path` | string | Truncated to 500. |
|
|
274
|
-
| `ip_address` | string |
|
|
277
|
+
| `ip_address` | string | IPv4 or IPv6. Omitted if it is not an IP (OptiAudit would accept it and then lose the event). |
|
|
275
278
|
| `user_agent` | string | |
|
|
276
279
|
| `session_id` | string | Truncated to 255. `optiaudit_track` sends a SHA-256. |
|
|
277
280
|
| `metadata` | hash | Free-form. Converted to JSON types. |
|
|
278
|
-
| `client_timestamp` | Time or ISO 8601 string | Default: when `track` is called, not when the event is sent. |
|
|
281
|
+
| `client_timestamp` | Time or ISO 8601 string | Default: when `track` is called, not when the event is sent. A string that is not ISO 8601 is replaced by the current time, with a warning. |
|
|
279
282
|
|
|
280
283
|
OptiAudit rejects the whole event when one field is invalid, and in a batch, the whole batch. That is why
|
|
281
284
|
the gem truncates or omits fields instead of sending them as they are.
|
|
@@ -289,7 +292,9 @@ the gem truncates or omits fields instead of sending them as they are.
|
|
|
289
292
|
| No actor | Event not sent |
|
|
290
293
|
| Action raised or response 4xx/5xx | Event not sent |
|
|
291
294
|
| Unknown `event_type` | Event not sent, logs warning |
|
|
292
|
-
| Invalid `actor_email` / `organization_id` | Field omitted, event sent |
|
|
295
|
+
| Invalid `actor_email` / `organization_id` / `ip_address` | Field omitted, event sent |
|
|
296
|
+
| Invalid `client_timestamp` | Replaced by the current time, logs warning, event sent |
|
|
297
|
+
| `Client-IP` and `X-Forwarded-For` disagree (IP spoofing) | Event sent without `ip_address`, logs warning |
|
|
293
298
|
| HTTP 5xx, 429, timeout (3s), connection error | Logs warning, doesn't raise. Sidekiq retries. |
|
|
294
299
|
| HTTP 4xx (other) | Logs warning, doesn't raise, no retry |
|
|
295
300
|
| HTTP 207 on a batch | Logs warning with the rejected events |
|
|
@@ -312,7 +317,18 @@ To try the gem end to end against a real OptiAudit (e.g. development), `examples
|
|
|
312
317
|
OPTIAUDIT_URL=https://... OPTIAUDIT_WRITE_KEY=oat-... bundle exec ruby examples/smoke.rb
|
|
313
318
|
```
|
|
314
319
|
|
|
315
|
-
|
|
320
|
+
## Releasing
|
|
321
|
+
|
|
322
|
+
The gem is owned by the `routinguc` account on rubygems.org and requires MFA (`rubygems_mfa_required`),
|
|
323
|
+
so every push asks for a one-time password and releases are published by hand, not from CI:
|
|
324
|
+
|
|
325
|
+
1. Update `lib/optiaudit/version.rb` and `CHANGELOG.md`, commit and wait for the CI matrix to pass.
|
|
326
|
+
2. Tag the release: `git tag v<version> && git push origin v<version>`.
|
|
327
|
+
3. Build and push with an API key that has the "Push rubygem" scope (rubygems.org → Settings → API keys):
|
|
328
|
+
```bash
|
|
329
|
+
gem build optiaudit-rails.gemspec
|
|
330
|
+
GEM_HOST_API_KEY=<api-key> gem push optiaudit-rails-<version>.gem # asks for the OTP
|
|
331
|
+
```
|
|
316
332
|
|
|
317
333
|
## License
|
|
318
334
|
|
data/lib/optiaudit/event.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ipaddr"
|
|
3
4
|
require "json"
|
|
4
5
|
require "time"
|
|
5
6
|
|
|
@@ -9,8 +10,9 @@ module Optiaudit
|
|
|
9
10
|
# OptiAudit validates every field and rejects the whole event (and, in a batch,
|
|
10
11
|
# the whole batch) when one is invalid. So instead of sending something that
|
|
11
12
|
# will come back as 400/422, fields are fixed here: strings are truncated to
|
|
12
|
-
# the API limits
|
|
13
|
-
# organization_id
|
|
13
|
+
# the API limits; an invalid actor_email or ip_address and a non-integer
|
|
14
|
+
# organization_id are omitted; an invalid client_timestamp becomes the
|
|
15
|
+
# current time.
|
|
14
16
|
module Event
|
|
15
17
|
# Limits from OptiAudit's CreateEventDto.
|
|
16
18
|
MAX_LENGTHS = {
|
|
@@ -59,6 +61,7 @@ module Optiaudit
|
|
|
59
61
|
end
|
|
60
62
|
event["actor_type"] ||= DEFAULT_ACTOR_TYPE
|
|
61
63
|
event.delete("actor_email") unless email?(event["actor_email"])
|
|
64
|
+
event.delete("ip_address") unless ip?(event["ip_address"])
|
|
62
65
|
|
|
63
66
|
organization_id = integer(params["organization_id"])
|
|
64
67
|
event["organization_id"] = organization_id if organization_id
|
|
@@ -99,6 +102,17 @@ module Optiaudit
|
|
|
99
102
|
labels.size >= 2 && labels.all? { |label| label.match?(DOMAIN_LABEL) } && labels.last.match?(TOP_LEVEL_DOMAIN)
|
|
100
103
|
end
|
|
101
104
|
|
|
105
|
+
# OptiAudit stores it in an INET column: anything else is accepted by the
|
|
106
|
+
# API and then lost by its worker.
|
|
107
|
+
def ip?(value)
|
|
108
|
+
return false if value.nil?
|
|
109
|
+
|
|
110
|
+
IPAddr.new(value)
|
|
111
|
+
true
|
|
112
|
+
rescue IPAddr::Error
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
|
|
102
116
|
def integer(value)
|
|
103
117
|
return value if value.is_a?(Integer)
|
|
104
118
|
|
|
@@ -107,10 +121,25 @@ module Optiaudit
|
|
|
107
121
|
|
|
108
122
|
def timestamp(value)
|
|
109
123
|
case value
|
|
110
|
-
when nil then
|
|
124
|
+
when nil then current_timestamp
|
|
111
125
|
when Time, DateTime then value.to_time.utc.iso8601(3)
|
|
112
|
-
else
|
|
126
|
+
else
|
|
127
|
+
return value.to_s if iso8601?(value.to_s)
|
|
128
|
+
|
|
129
|
+
Optiaudit.log_warning("client_timestamp #{value.to_s.inspect} is not ISO 8601, using the current time")
|
|
130
|
+
current_timestamp
|
|
113
131
|
end
|
|
114
132
|
end
|
|
133
|
+
|
|
134
|
+
def iso8601?(value)
|
|
135
|
+
Time.iso8601(value)
|
|
136
|
+
true
|
|
137
|
+
rescue ArgumentError
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def current_timestamp
|
|
142
|
+
Time.now.utc.iso8601(3)
|
|
143
|
+
end
|
|
115
144
|
end
|
|
116
145
|
end
|
|
@@ -17,13 +17,14 @@ module Optiaudit
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# remote_ip skips trusted proxies (private ranges by default), so behind a
|
|
20
|
-
# load balancer it returns the client's public IP.
|
|
21
|
-
#
|
|
20
|
+
# load balancer it returns the client's public IP. It raises when
|
|
21
|
+
# Client-IP and X-Forwarded-For disagree; then the IP is omitted: the
|
|
22
|
+
# alternatives (request.ip) return a value the client chose.
|
|
22
23
|
def client_ip(request)
|
|
23
24
|
request.remote_ip
|
|
24
25
|
rescue ActionDispatch::RemoteIp::IpSpoofAttackError => e
|
|
25
|
-
Optiaudit.log_warning("remote_ip rejected (#{e.message}),
|
|
26
|
-
|
|
26
|
+
Optiaudit.log_warning("remote_ip rejected (#{e.message}), event sent without ip_address")
|
|
27
|
+
nil
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
# Hashed: the raw session id is a credential and never leaves the app.
|
data/lib/optiaudit/version.rb
CHANGED