Class: StandardAudit::AuditLog
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- StandardAudit::AuditLog
- Defined in:
- app/models/standard_audit/audit_log.rb
Constant Summary collapse
- CHECKSUM_FIELDS =
%w[ id event_type actor_gid actor_type target_gid target_type scope_gid scope_type metadata request_id ip_address user_agent session_id occurred_at ].freeze
Class Method Summary collapse
-
.anonymize_actor!(record) ⇒ Object
– GDPR methods –.
-
.backfill_checksums!(batch_size: 1000) ⇒ Object
Backfills checksums for records that don’t have them (e.g. pre-existing records before the checksum feature was added).
- .compute_checksum_value(attrs, previous_checksum: nil) ⇒ Object
- .export_for_actor(record) ⇒ Object
-
.verify_chain(scope: nil, batch_size: 1000) ⇒ Object
Verifies the integrity of the audit log chain.
Instance Method Summary collapse
- #actor ⇒ Object
-
#actor=(record) ⇒ Object
– Actor assignment via GlobalID –.
-
#compute_checksum_value(previous_checksum: nil) ⇒ Object
Recomputes the checksum from the record’s current field values and the given previous checksum.
- #scope ⇒ Object
-
#scope=(record) ⇒ Object
– Scope assignment via GlobalID –.
- #target ⇒ Object
-
#target=(record) ⇒ Object
– Target assignment via GlobalID –.
Class Method Details
.anonymize_actor!(record) ⇒ Object
– GDPR methods –
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'app/models/standard_audit/audit_log.rb', line 111 def self.anonymize_actor!(record) gid = record.to_global_id.to_s logs = where("actor_gid = ? OR target_gid = ?", gid, gid) count = logs.count anonymizable_keys = StandardAudit.config..map(&:to_s) logs.find_each do |log| attrs = { ip_address: nil, user_agent: nil, session_id: nil } attrs[:actor_gid] = "[anonymized]" if log.actor_gid == gid attrs[:actor_type] = "[anonymized]" if log.actor_gid == gid attrs[:target_gid] = "[anonymized]" if log.target_gid == gid attrs[:target_type] = "[anonymized]" if log.target_gid == gid if log..present? && anonymizable_keys.any? = log..reject { |k, _| anonymizable_keys.include?(k.to_s) } attrs[:metadata] = end log.update_columns(attrs) end count end |
.backfill_checksums!(batch_size: 1000) ⇒ Object
Backfills checksums for records that don’t have them (e.g. pre-existing records before the checksum feature was added).
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'app/models/standard_audit/audit_log.rb', line 232 def self.backfill_checksums!(batch_size: 1000) previous_checksum = nil count = 0 in_batches(of: batch_size) do |batch| batch.order(created_at: :asc, id: :asc).each do |record| if record.checksum.present? previous_checksum = record.checksum next end new_checksum = compute_checksum_value( record.attributes.slice(*CHECKSUM_FIELDS), previous_checksum: previous_checksum ) record.update_columns(checksum: new_checksum) previous_checksum = new_checksum count += 1 end end count end |
.compute_checksum_value(attrs, previous_checksum: nil) ⇒ Object
177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'app/models/standard_audit/audit_log.rb', line 177 def self.compute_checksum_value(attrs, previous_checksum: nil) canonical = CHECKSUM_FIELDS.map { |f| value = attrs[f] value = value.to_json if value.is_a?(Hash) value = value.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ") if value.respond_to?(:strftime) && value.respond_to?(:utc) "#{f}=#{value}" }.join("|") canonical = "#{previous_checksum}|#{canonical}" if previous_checksum.present? OpenSSL::Digest::SHA256.hexdigest(canonical) end |
.export_for_actor(record) ⇒ Object
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'app/models/standard_audit/audit_log.rb', line 141 def self.export_for_actor(record) gid = record.to_global_id.to_s logs = where("actor_gid = ? OR target_gid = ?", gid, gid).chronological records = logs.map do |log| { id: log.id, event_type: log.event_type, actor_gid: log.actor_gid, target_gid: log.target_gid, scope_gid: log.scope_gid, metadata: log., occurred_at: log.occurred_at.iso8601, ip_address: log.ip_address, user_agent: log.user_agent, request_id: log.request_id } end { subject: gid, exported_at: Time.current.iso8601, total_records: records.size, records: records } end |
.verify_chain(scope: nil, batch_size: 1000) ⇒ Object
Verifies the integrity of the audit log chain. Returns a result hash with :valid (boolean), :verified (count), and :failures (array of hashes).
Records are processed in (created_at, id) order. Records without a checksum (pre-feature data) reset the chain — the next checksummed record starts a new independent chain segment.
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'app/models/standard_audit/audit_log.rb', line 196 def self.verify_chain(scope: nil, batch_size: 1000) relation = scope ? where(scope_gid: scope.to_global_id.to_s) : all previous_checksum = nil verified = 0 failures = [] relation.in_batches(of: batch_size) do |batch| batch.order(created_at: :asc, id: :asc).each do |record| if record.checksum.blank? previous_checksum = nil next end expected = record.compute_checksum_value(previous_checksum: previous_checksum) if record.checksum != expected failures << { id: record.id, event_type: record.event_type, created_at: record.created_at, expected: expected, actual: record.checksum } end verified += 1 previous_checksum = record.checksum end end { valid: failures.empty?, verified: verified, failures: failures } end |
Instance Method Details
#actor ⇒ Object
39 40 41 42 43 44 |
# File 'app/models/standard_audit/audit_log.rb', line 39 def actor return nil if actor_gid.blank? GlobalID::Locator.locate(actor_gid) rescue ActiveRecord::RecordNotFound nil end |
#actor=(record) ⇒ Object
– Actor assignment via GlobalID –
29 30 31 32 33 34 35 36 37 |
# File 'app/models/standard_audit/audit_log.rb', line 29 def actor=(record) if record.nil? self.actor_gid = nil self.actor_type = nil else self.actor_gid = record.to_global_id.to_s self.actor_type = record.class.name end end |
#compute_checksum_value(previous_checksum: nil) ⇒ Object
Recomputes the checksum from the record’s current field values and the given previous checksum. Useful for verification without saving.
170 171 172 173 174 175 |
# File 'app/models/standard_audit/audit_log.rb', line 170 def compute_checksum_value(previous_checksum: nil) self.class.compute_checksum_value( attributes.slice(*CHECKSUM_FIELDS), previous_checksum: previous_checksum ) end |
#scope ⇒ Object
77 78 79 80 81 82 |
# File 'app/models/standard_audit/audit_log.rb', line 77 def scope return nil if scope_gid.blank? GlobalID::Locator.locate(scope_gid) rescue ActiveRecord::RecordNotFound nil end |
#scope=(record) ⇒ Object
– Scope assignment via GlobalID –
67 68 69 70 71 72 73 74 75 |
# File 'app/models/standard_audit/audit_log.rb', line 67 def scope=(record) if record.nil? self.scope_gid = nil self.scope_type = nil else self.scope_gid = record.to_global_id.to_s self.scope_type = record.class.name end end |
#target ⇒ Object
58 59 60 61 62 63 |
# File 'app/models/standard_audit/audit_log.rb', line 58 def target return nil if target_gid.blank? GlobalID::Locator.locate(target_gid) rescue ActiveRecord::RecordNotFound nil end |
#target=(record) ⇒ Object
– Target assignment via GlobalID –
48 49 50 51 52 53 54 55 56 |
# File 'app/models/standard_audit/audit_log.rb', line 48 def target=(record) if record.nil? self.target_gid = nil self.target_type = nil else self.target_gid = record.to_global_id.to_s self.target_type = record.class.name end end |