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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44ee00e821c324db9b3aba699c39e04eff9b471b1d1dfd5779deae13c6b2f293
4
- data.tar.gz: cc49b0ca5595307c1461067dc06467a1f056c998b8e795a35beb29510aeb44d7
3
+ metadata.gz: 2da47ba93a09717845045e65800eebf3f280a2261402a2bb7f7bf7a2db72a7a2
4
+ data.tar.gz: a5c6b63dfdba71cfaf7843f3ba0daedffbd1e5d76834cb4a74bb4174ace02ac7
5
5
  SHA512:
6
- metadata.gz: 3b50e99331b77e089addca33877bf5ba22c38b7f37f3303fb911593382c48200b74b36eaeb321ace3baa748e1a47ac9f5c7c622e98609656f6b067d54bb38e10
7
- data.tar.gz: 8559093f201881f42ab2482949b5128c9778f095e0e394fa74187df8b1d6ff48120d8a0093b4032f03feefd47a24d3fbc76552a5bd103f839dac1f301cf032a9
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.0
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
- In Rails 5.2, `remote_ip` raises `IpSpoofAttackError` when `Client-IP` and `X-Forwarded-For` disagree.
183
- The gem then logs a warning and uses `request.ip`, so the event is still sent and the request is not affected.
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 | Truncated to 45. |
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
- Releases are published to rubygems.org from Bitbucket Pipelines with the manual `custom: publish` pipeline.
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
 
@@ -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, an invalid actor_email is omitted and a non-integer
13
- # organization_id is omitted.
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 Time.now.utc.iso8601(3)
124
+ when nil then current_timestamp
111
125
  when Time, DateTime then value.to_time.utc.iso8601(3)
112
- else value.to_s
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. In Rails 5.2 it raises
21
- # when Client-IP and X-Forwarded-For disagree.
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}), using request.ip")
26
- request.ip
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Optiaudit
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optiaudit-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Optimal Partner