standard_audit 0.11.0 → 0.11.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: b89dc74bd119d2e0f1ff5080cef706439e68b61c79d2a6b653fcd5a74e265ae1
4
- data.tar.gz: 86686ea3bb611a13e8c8947ef3e0b63aad63eb47bfa1e48b16abba377a426f7f
3
+ metadata.gz: 697434bf73f4588971d9fda7503f13f53a848ab4fe058fbc05be5b567a186d6f
4
+ data.tar.gz: e5bad881dc6a5ab13ae22d13777176120967161f40356dcd6522ab4616492c1f
5
5
  SHA512:
6
- metadata.gz: b06e7ee9a3b5d79191cb47a532cf81efb95e3bb722015540be79ce4bb9412ab13b8e69985a66d368c8dcdfb099f34f1ca1d3f0d894bfc90cc409615782dde921
7
- data.tar.gz: bb1e9316eba648a8d238becb75d33f705b82f509310e8c3e0d562656accac2b44339e49551e026ae0357bf57dda8fe8251f6f1d6c7df655777d5b157c3d17b0a
6
+ metadata.gz: feb2b3354808c948aff042e0a3354ac2344c3e4ac1b03f0c375645ff7b1aaf1ec27253e87e764652ee7d52710a4c7aecd7748ba13475afa001a0c27f55a2ca79
7
+ data.tar.gz: a83422f4ef6b6ca319c29c450ae7cf82457cc5ad342345947a867e2243ef701c2bb0310a5b1d99f5cbf9c322b756f5094c42e77e53eb6ea3b828a834f94bfacc
data/CHANGELOG.md CHANGED
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.1] - 2026-09-24
11
+
12
+ ### Fixed
13
+
14
+ - **`rake standard_audit:anonymize_actor[gid]` and `standard_audit:export_actor[gid]` no longer raise `NoMethodError`.** The tasks passed the GlobalID string straight into `AuditLog.anonymize_actor!` / `export_for_actor`, which called `to_global_id` on it. Both methods now accept a record, a `GlobalID`, or a GlobalID string. A string is parsed, not located, so erasure works after the subject's row has been deleted; an invalid string raises `ArgumentError`. What gets anonymized is unchanged.
15
+ - **`rake standard_audit:cleanup` no longer deletes logs older than 90 days when `retention_days` is nil.** nil means keep forever, but the task fell back to a hard-coded 90. `cleanup` and `archive` (which always defaulted to 90) now take the days argument, else `config.retention_days`, else abort with a message saying how to set one.
16
+ - **`cleanup`/`archive` reject a days value that is not a positive integer.** `cleanup[abc]` used to become `0` days, i.e. delete every row. `0`, negatives and non-numeric values now abort. `StandardAudit::CleanupJob` is unchanged.
17
+ - **`StandardAudit::Subscriber` and `StandardAudit::EventSubscriber` report swallowed errors to `Rails.error`.** A failed audit write was only logged, so it never reached error tracking. It is now also reported with `handled: true` and context `{ <config.audit_error_context_key> => event_name, subscriber: <class name> }`. The log line is kept.
18
+
10
19
  ## [0.11.0] - 2026-07-31
11
20
 
12
21
  ### Security
data/README.md CHANGED
@@ -499,6 +499,16 @@ This:
499
499
  - Clears `ip_address`, `user_agent`, and `session_id`
500
500
  - Removes metadata keys listed in `anonymizable_metadata_keys`
501
501
 
502
+ Both `anonymize_actor!` and `export_for_actor` accept the subject as a record,
503
+ a `GlobalID`, or a GlobalID string (`"gid://myapp/User/123"`). A string is
504
+ parsed, never located, so erasure still works after the user's own row has been
505
+ deleted — the usual order for an erasure request. Anything that is not a valid
506
+ GlobalID raises `ArgumentError`.
507
+
508
+ ```ruby
509
+ StandardAudit::AuditLog.anonymize_actor!("gid://myapp/User/123")
510
+ ```
511
+
502
512
  ### Right to Access (Export)
503
513
 
504
514
  Export all audit data for a specific user:
@@ -525,7 +535,9 @@ STANDARD_AUDIT_RETENTION_DAYS=365 # keep 365 days
525
535
  ```
526
536
 
527
537
  Infinite retention (the default) is the compliance-safe behavior: nothing is
528
- ever auto-deleted. For financial/legal domains that is usually what you want;
538
+ ever auto-deleted. The `standard_audit:cleanup` and `standard_audit:archive`
539
+ rake tasks respect this: with no days argument and a nil `retention_days` they
540
+ abort rather than fall back to a default window. For financial/legal domains that is usually what you want;
529
541
  enabling a finite window is a deliberate decision.
530
542
 
531
543
  ### Production retention warning (StandardHealth)
@@ -635,10 +647,12 @@ rake standard_audit:verify
635
647
  # Record the parent digest each existing row was signed against
636
648
  rake standard_audit:relink_checksums
637
649
 
638
- # Delete logs older than N days (default: retention_days config or 90)
650
+ # Delete logs older than N days
639
651
  rake standard_audit:cleanup[180]
652
+ # ...or older than config.retention_days
653
+ rake standard_audit:cleanup
640
654
 
641
- # Archive old logs to a JSON file before deleting
655
+ # Archive old logs to a JSON file before deleting (same days rules)
642
656
  rake standard_audit:archive[90,audit_backup.json]
643
657
 
644
658
  # Show statistics
@@ -651,6 +665,15 @@ rake "standard_audit:anonymize_actor[gid://myapp/User/123]"
651
665
  rake "standard_audit:export_actor[gid://myapp/User/123,export.json]"
652
666
  ```
653
667
 
668
+ `cleanup` and `archive` take the window from the days argument, else
669
+ `config.retention_days`; if neither is set they abort (a nil `retention_days`
670
+ means keep forever, so there is no implicit 90-day default). Days must be a
671
+ positive integer — `0`, negatives and non-numeric values abort instead of
672
+ deleting everything.
673
+
674
+ `anonymize_actor` and `export_actor` take a GlobalID string and work even after
675
+ the user record has been deleted.
676
+
654
677
  ## Database Support
655
678
 
656
679
  The migration uses `json` column type by default, which works across:
@@ -92,8 +92,11 @@ module StandardAudit
92
92
 
93
93
  # -- GDPR methods --
94
94
 
95
- def self.anonymize_actor!(record)
96
- gid = record.to_global_id.to_s
95
+ # `subject` may be a record, a GlobalID, or a GlobalID string
96
+ # ("gid://app/User/1"). A string is parsed, never located, so erasure and
97
+ # export still work after the subject's own row has been deleted.
98
+ def self.anonymize_actor!(subject)
99
+ gid = subject_gid_for(subject)
97
100
  logs = where("actor_gid = ? OR target_gid = ?", gid, gid)
98
101
  count = logs.count
99
102
 
@@ -122,8 +125,8 @@ module StandardAudit
122
125
  count
123
126
  end
124
127
 
125
- def self.export_for_actor(record)
126
- gid = record.to_global_id.to_s
128
+ def self.export_for_actor(subject)
129
+ gid = subject_gid_for(subject)
127
130
  logs = where("actor_gid = ? OR target_gid = ?", gid, gid).chronological
128
131
 
129
132
  records = logs.map do |log|
@@ -149,6 +152,22 @@ module StandardAudit
149
152
  }
150
153
  end
151
154
 
155
+ # Normalises a GDPR subject to its GlobalID string. Parses rather than
156
+ # locates, so it works when the subject's row no longer exists.
157
+ def self.subject_gid_for(subject)
158
+ gid =
159
+ case subject
160
+ when GlobalID then subject
161
+ when String then GlobalID.parse(subject)
162
+ else subject.respond_to?(:to_global_id) ? subject.to_global_id : nil
163
+ end
164
+
165
+ raise ArgumentError, "expected a record, a GlobalID, or a GlobalID string (gid://app/Model/id), got #{subject.inspect}" unless gid
166
+
167
+ gid.to_s
168
+ end
169
+ private_class_method :subject_gid_for
170
+
152
171
  # Recomputes the checksum from the record's current field values and the
153
172
  # given previous checksum. Useful for verification without saving.
154
173
  def compute_checksum_value(previous_checksum: nil)
@@ -48,10 +48,27 @@ module StandardAudit
48
48
  )
49
49
  rescue => e
50
50
  Rails.logger.error("[StandardAudit] Error handling Rails.event: #{e.class}: #{e.message}")
51
+ report_error(e, name)
51
52
  end
52
53
 
53
54
  private
54
55
 
56
+ # The event handler rescues so a failed audit write cannot break the
57
+ # instrumented code path, but a log line alone is invisible to error
58
+ # tracking — so report it as handled too.
59
+ def report_error(error, event_name)
60
+ return unless Rails.respond_to?(:error) && Rails.error
61
+
62
+ Rails.error.report(
63
+ error,
64
+ handled: true,
65
+ context: { StandardAudit.config.audit_error_context_key => event_name,
66
+ subscriber: self.class.name }
67
+ )
68
+ rescue => report_failure
69
+ Rails.logger.error("[StandardAudit] Error reporting audit failure: #{report_failure.class}: #{report_failure.message}")
70
+ end
71
+
55
72
  def matches_subscription?(name)
56
73
  StandardAudit.config.subscriptions.any? { |pattern| pattern_match?(pattern, name) }
57
74
  end
@@ -25,6 +25,22 @@ module StandardAudit
25
25
 
26
26
  private
27
27
 
28
+ # The event handler rescues so a failed audit write cannot break the
29
+ # instrumented code path, but a log line alone is invisible to error
30
+ # tracking — so report it as handled too.
31
+ def report_error(error, event_name)
32
+ return unless Rails.respond_to?(:error) && Rails.error
33
+
34
+ Rails.error.report(
35
+ error,
36
+ handled: true,
37
+ context: { StandardAudit.config.audit_error_context_key => event_name,
38
+ subscriber: self.class.name }
39
+ )
40
+ rescue => report_failure
41
+ Rails.logger.error("[StandardAudit] Error reporting audit failure: #{report_failure.class}: #{report_failure.message}")
42
+ end
43
+
28
44
  def handle_event(event)
29
45
  return unless StandardAudit.config.enabled
30
46
 
@@ -68,6 +84,7 @@ module StandardAudit
68
84
  end
69
85
  rescue => e
70
86
  Rails.logger.error("[StandardAudit] Error creating audit log: #{e.class}: #{e.message}")
87
+ report_error(e, event.name)
71
88
  end
72
89
 
73
90
  def extract_metadata(payload, config)
@@ -1,3 +1,3 @@
1
1
  module StandardAudit
2
- VERSION = "0.11.0"
2
+ VERSION = "0.11.1"
3
3
  end
@@ -1,16 +1,44 @@
1
+ module StandardAudit
2
+ module RakeSupport
3
+ module_function
4
+
5
+ # Resolves the retention window for cleanup/archive: the task argument,
6
+ # else config.retention_days, else abort. nil retention means "keep
7
+ # forever", so there is deliberately no hard-coded fallback. Anything that
8
+ # is not a positive integer aborts — `cleanup[abc]` used to become 0 days,
9
+ # i.e. delete everything.
10
+ def resolve_days!(task_name, arg)
11
+ raw = arg.presence || StandardAudit.config.retention_days
12
+
13
+ if raw.nil?
14
+ abort "standard_audit:#{task_name}: no retention window. Pass days " \
15
+ "(rake \"standard_audit:#{task_name}[90]\") or set config.retention_days " \
16
+ "/ STANDARD_AUDIT_RETENTION_DAYS. A nil retention_days means keep forever."
17
+ end
18
+
19
+ days = Integer(raw.to_s.strip, 10, exception: false)
20
+ unless days&.positive?
21
+ abort "standard_audit:#{task_name}: days must be a positive integer, got #{raw.inspect}"
22
+ end
23
+
24
+ days
25
+ end
26
+ end
27
+ end
28
+
1
29
  namespace :standard_audit do
2
- desc "Delete audit logs older than specified days (default: 90)"
30
+ desc "Delete audit logs older than N days (default: config.retention_days; aborts if neither is set)"
3
31
  task :cleanup, [:days] => :environment do |_t, args|
4
- days = (args[:days] || StandardAudit.config.retention_days || 90).to_i
32
+ days = StandardAudit::RakeSupport.resolve_days!("cleanup", args[:days])
5
33
  cutoff = days.days.ago
6
34
 
7
35
  deleted = StandardAudit::AuditLog.where("occurred_at < ?", cutoff).delete_all
8
36
  puts "Deleted #{deleted} audit logs older than #{days} days"
9
37
  end
10
38
 
11
- desc "Archive audit logs to JSON file"
39
+ desc "Archive audit logs older than N days to a JSON file (default: config.retention_days; aborts if neither is set)"
12
40
  task :archive, [:days, :output] => :environment do |_t, args|
13
- days = (args[:days] || 90).to_i
41
+ days = StandardAudit::RakeSupport.resolve_days!("archive", args[:days])
14
42
  output = args[:output] || "audit_logs_archive_#{Date.current}.json"
15
43
  cutoff = days.days.ago
16
44
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_audit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim