rails_audit_log 1.1.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26fdf8335990094a7278cbcf9921e123fbf44f6ec464b974b1c73e1f41047691
4
- data.tar.gz: 9700fef384bb8f23eb929b0c62cb82880ba8217a44f64660f97823c4ffe6f1c0
3
+ metadata.gz: fb6120f7ce9f0f2a2fb19553a77ff4c6bf5c24a1945270e2e6393403b9ffc3a0
4
+ data.tar.gz: a978f7082c30a68c37344e0b819af1e70d077b3092337c5ea31c4e35473ffef6
5
5
  SHA512:
6
- metadata.gz: 8c0450b8f9228ec6ea98e7e026e0554d73004accf1a284433fa6260b5acd941d8314fd33b9bf873156e214f65f8bcd1d687613077f60c310dacd2a2b4e1f98e8
7
- data.tar.gz: 733c560b234f96900a760cc76b74a18bd39303179e36ae8272c0f150560139272eb3fb0de9ee435883fbd2c128bd408b373601f407818ff9e5c72cb9c465e503
6
+ metadata.gz: 8670a4ceb147508b45d03679eb3765c6984393c144434f4ad90b40bf69c64478ff8b92b0ab3bdb0b054cb0f64d2794f05044d546f81fa2fde17496ff65fde4de
7
+ data.tar.gz: 5ba060114539abc8555a5873aa7671c226cdc2c19a3bd66a4549595f68ea170372a968de324485dc0bf2eabb0bd253fa14e5c14c98b7b4ad9fe691bc15973fbb
data/README.md CHANGED
@@ -25,6 +25,8 @@ Audit logging for Rails. Tracks `create`, `update`, and `destroy` events as stru
25
25
  - [Capping history per record](#capping-history-per-record)
26
26
  - [Time-based retention](#time-based-retention)
27
27
  - [Scheduled and manual pruning](#scheduled-and-manual-pruning)
28
+ - [Encrypting audit data](#encrypting-audit-data)
29
+ - [Multi-tenancy](#multi-tenancy)
28
30
  - [Selective tracking](#selective-tracking)
29
31
  - [Disabling auditing](#disabling-auditing)
30
32
  - [Object reconstruction](#object-reconstruction)
@@ -369,6 +371,112 @@ Or run it once manually via the rake task:
369
371
  bin/rails rails_audit_log:prune
370
372
  ```
371
373
 
374
+ ### Encrypting audit data
375
+
376
+ Encrypt `object_changes` and `object` at write time using `ActiveRecord::Encryption` (Rails 7.1+). Pass `encrypt: true` to `audit_log` in the model:
377
+
378
+ ```ruby
379
+ class Payment < ApplicationRecord
380
+ include RailsAuditLog::Auditable
381
+ audit_log encrypt: true
382
+ end
383
+ ```
384
+
385
+ The host app must configure `ActiveRecord::Encryption` with primary, deterministic, and key-derivation-salt keys — typically in `config/initializers/rails_audit_log.rb` or `config/application.rb`:
386
+
387
+ ```ruby
388
+ config.active_record.encryption.primary_key = Rails.application.credentials.ral_primary_key
389
+ config.active_record.encryption.deterministic_key = Rails.application.credentials.ral_deterministic_key
390
+ config.active_record.encryption.key_derivation_salt = Rails.application.credentials.ral_kdf_salt
391
+ ```
392
+
393
+ Enable encryption globally so every audited model encrypts by default:
394
+
395
+ ```ruby
396
+ # config/initializers/rails_audit_log.rb
397
+ RailsAuditLog.encrypt = true
398
+ ```
399
+
400
+ Opt a specific model out when the global default is on:
401
+
402
+ ```ruby
403
+ class PublicLog < ApplicationRecord
404
+ include RailsAuditLog::Auditable
405
+ audit_log encrypt: false # plain JSON even when RailsAuditLog.encrypt = true
406
+ end
407
+ ```
408
+
409
+ Decryption is transparent — `#diff`, `#reify`, `#changed_attributes`, and all other instance methods work without any changes.
410
+
411
+ > **Note:** The `touching` scope uses database-level JSON extraction (`json_extract` / `->>`) and will not match encrypted entries. All Ruby-side query methods work normally.
412
+
413
+ #### Setting up encryption keys
414
+
415
+ Run the Rails built-in task to generate keys and store them in `config/credentials.yml.enc`:
416
+
417
+ ```bash
418
+ bin/rails db:encryption:init
419
+ ```
420
+
421
+ Then run the encryption generator to produce a wired-up initializer and a re-encryption migration for existing entries:
422
+
423
+ ```bash
424
+ bin/rails generate rails_audit_log:encryption
425
+ ```
426
+
427
+ The generator creates:
428
+ - `config/initializers/rails_audit_log_encryption.rb` — reads the generated keys from credentials and passes them to `ActiveRecord::Encryption`
429
+ - `db/migrate/TIMESTAMP_encrypt_rails_audit_log_entries.rb` — re-encrypts existing plain-text audit entries; edit `ENCRYPTED_MODELS` to list your model class names, then run `bin/rails db:migrate`
430
+
431
+ ### Multi-tenancy
432
+
433
+ Store the current tenant on every audit entry so queries are naturally isolated per tenant.
434
+
435
+ Run the generator to add the `tenant_id` column:
436
+
437
+ ```bash
438
+ bin/rails generate rails_audit_log:tenant
439
+ bin/rails db:migrate
440
+ ```
441
+
442
+ Set a global resolver in your initializer — the block is called at write time:
443
+
444
+ ```ruby
445
+ # config/initializers/rails_audit_log.rb
446
+ RailsAuditLog.current_tenant { Current.tenant_id }
447
+ ```
448
+
449
+ Or override per model:
450
+
451
+ ```ruby
452
+ class Order < ApplicationRecord
453
+ include RailsAuditLog::Auditable
454
+ audit_log tenant: -> { Current.tenant_id }
455
+ end
456
+ ```
457
+
458
+ The per-model lambda takes precedence over the global resolver. Both accept zero-argument lambdas and store whatever the block returns in the `tenant_id` string column.
459
+
460
+ Scope queries to a single tenant with `for_tenant`:
461
+
462
+ ```ruby
463
+ AuditLogEntry.for_tenant("acme")
464
+ AuditLogEntry.for_tenant(Current.tenant_id).updated_events.since(1.week.ago)
465
+ ```
466
+
467
+ The web dashboard (`/audit`) automatically applies `for_tenant` when `current_tenant` is configured, so entries from other tenants are never exposed.
468
+
469
+ #### Acts As Tenant integration
470
+
471
+ Wire the resolver to `ActsAsTenant` in one line:
472
+
473
+ ```ruby
474
+ # config/initializers/rails_audit_log.rb
475
+ RailsAuditLog.acts_as_tenant!
476
+ ```
477
+
478
+ This is equivalent to `RailsAuditLog.current_tenant { ActsAsTenant.current_tenant&.id }`.
479
+
372
480
  ### Selective tracking
373
481
 
374
482
  Track only specific attributes, or exclude noisy ones:
@@ -754,7 +862,7 @@ bundle exec rake benchmark
754
862
 
755
863
  [↑ Table of contents](#table-of-contents)
756
864
 
757
- Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/rails_audit_log).
865
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/eclectic-coding/rails_audit_log). See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions, branch workflow, and CHANGELOG conventions.
758
866
 
759
867
  ## License
760
868
 
@@ -30,6 +30,8 @@ module RailsAuditLog
30
30
  class_attribute :_audit_log_version_limit, default: nil
31
31
  class_attribute :_audit_log_retain_for, default: nil
32
32
  class_attribute :_audit_log_async, default: false
33
+ class_attribute :_audit_log_encrypt, default: nil
34
+ class_attribute :_audit_log_tenant, default: nil
33
35
 
34
36
  _warn_if_audit_table_missing
35
37
 
@@ -111,17 +113,28 @@ module RailsAuditLog
111
113
  # {RailsAuditLog.retention_period} for this model
112
114
  # @param async [Boolean, nil] when +true+, writes are dispatched via
113
115
  # +WriteAuditLogJob+; overrides {RailsAuditLog.async} for this model
116
+ # @param encrypt [Boolean, nil] when +true+, encrypts +object_changes+ and
117
+ # +object+ at write time using +ActiveRecord::Encryption+; when +false+,
118
+ # opts this model out even if {RailsAuditLog.encrypt} is +true+; when
119
+ # +nil+ (default), falls back to {RailsAuditLog.encrypt}; requires the
120
+ # host app to configure +config.active_record.encryption+; decryption is
121
+ # transparent — {AuditLogEntry#diff}, {AuditLogEntry#reify}, and
122
+ # {AuditLogEntry.touching} work unchanged for non-SQL access paths
123
+ # @param tenant [Proc, nil] zero-argument lambda evaluated at write time;
124
+ # return value is stored in the +tenant_id+ column; overrides
125
+ # {RailsAuditLog.current_tenant} for this model
114
126
  # @return [void]
115
127
  # @example
116
128
  # class Article < ApplicationRecord
117
129
  # include RailsAuditLog::Auditable
118
130
  # audit_log only: %i[title body published_at],
119
- # meta: { tenant_id: -> { Current.tenant_id } },
131
+ # tenant: -> { Current.tenant_id },
120
132
  # associations: %i[tags],
121
133
  # version_limit: 100,
122
- # retain_for: 30.days
134
+ # retain_for: 30.days,
135
+ # encrypt: true
123
136
  # end
124
- def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, retain_for: nil, async: nil)
137
+ def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, retain_for: nil, async: nil, encrypt: nil, tenant: nil)
125
138
  self._audit_log_only = only.map(&:to_s) if only
126
139
  self._audit_log_ignore = ignore.map(&:to_s) if ignore
127
140
  self._audit_log_meta = meta if meta
@@ -129,6 +142,8 @@ module RailsAuditLog
129
142
  self._audit_log_version_limit = version_limit unless version_limit.nil?
130
143
  self._audit_log_retain_for = retain_for unless retain_for.nil?
131
144
  self._audit_log_async = async unless async.nil?
145
+ self._audit_log_encrypt = encrypt unless encrypt.nil?
146
+ self._audit_log_tenant = tenant unless tenant.nil?
132
147
  end
133
148
  end
134
149
 
@@ -169,10 +184,11 @@ module RailsAuditLog
169
184
  event: "update",
170
185
  item_type: self.class.name,
171
186
  item_id: id,
172
- object_changes: { assoc_name => [before, after] },
187
+ object_changes: maybe_encrypt({ assoc_name => [before, after] }),
173
188
  object: nil,
174
189
  reason: RailsAuditLog.reason,
175
190
  metadata: meta.presence,
191
+ tenant_id: resolve_tenant_id,
176
192
  whodunnit_snapshot: actor ? RailsAuditLog.whodunnit_display.call(actor) : nil,
177
193
  actor_type: actor&.class&.name,
178
194
  actor_id: actor.respond_to?(:id) ? actor.id : nil
@@ -191,16 +207,25 @@ module RailsAuditLog
191
207
  event: event,
192
208
  item_type: self.class.name,
193
209
  item_id: id,
194
- object_changes: filtered,
195
- object: snapshot,
210
+ object_changes: maybe_encrypt(filtered),
211
+ object: maybe_encrypt(snapshot),
196
212
  reason: RailsAuditLog.reason,
197
213
  metadata: meta.presence,
214
+ tenant_id: resolve_tenant_id,
198
215
  whodunnit_snapshot: actor ? RailsAuditLog.whodunnit_display.call(actor) : nil,
199
216
  actor_type: actor&.class&.name,
200
217
  actor_id: actor.respond_to?(:id) ? actor.id : nil
201
218
  )
202
219
  end
203
220
 
221
+ def maybe_encrypt(value)
222
+ return value unless value
223
+ encrypt = self.class._audit_log_encrypt.nil? ? RailsAuditLog.encrypt : self.class._audit_log_encrypt
224
+ return value unless encrypt
225
+ ciphertext = ActiveRecord::Encryption.encryptor.encrypt(value.to_json)
226
+ { RailsAuditLog::AuditLogEntry::ENCRYPTION_MARKER => ciphertext }
227
+ end
228
+
204
229
  def write_audit_entry(entry_attrs)
205
230
  if (buffer = RailsAuditLog.batch_audit_buffer)
206
231
  buffer << entry_attrs.stringify_keys.merge("created_at" => Time.current)
@@ -230,6 +255,11 @@ module RailsAuditLog
230
255
  end
231
256
  end
232
257
 
258
+ def resolve_tenant_id
259
+ tenant_proc = self.class._audit_log_tenant || RailsAuditLog.current_tenant
260
+ tenant_proc&.call
261
+ end
262
+
233
263
  def build_audit_metadata
234
264
  meta = {}
235
265
  if self.class._audit_log_meta
@@ -13,5 +13,10 @@ module RailsAuditLog
13
13
 
14
14
  instance_exec(self, &auth) || request_http_basic_authentication("Audit Log")
15
15
  end
16
+
17
+ def base_audit_scope
18
+ tenant_id = RailsAuditLog.current_tenant&.call
19
+ tenant_id ? AuditLogEntry.for_tenant(tenant_id) : AuditLogEntry.all
20
+ end
16
21
  end
17
22
  end
@@ -21,7 +21,7 @@ module RailsAuditLog
21
21
  end
22
22
 
23
23
  def filtered_scope
24
- scope = AuditLogEntry.order(created_at: :desc)
24
+ scope = base_audit_scope.order(created_at: :desc)
25
25
  scope = scope.where(event: @event) if @event
26
26
  scope = scope.where(item_type: @item_type) if @item_type
27
27
  scope = scope.for_period(@period) if @period
@@ -5,7 +5,7 @@ module RailsAuditLog
5
5
  @item_type = params[:item_type]
6
6
  @item_id = params[:item_id]
7
7
  @pagy, @entries = pagy(
8
- AuditLogEntry
8
+ base_audit_scope
9
9
  .where(item_type: @item_type, item_id: @item_id)
10
10
  .order(created_at: :asc)
11
11
  )
@@ -21,9 +21,10 @@ module RailsAuditLog
21
21
  class AuditLogEntry < ApplicationRecord
22
22
  self.table_name = "audit_log_entries"
23
23
 
24
- EVENTS = %w[create update destroy].freeze
25
- BLOB_COLUMNS = %w[object_changes object metadata].freeze
26
- PERIODS = { "1h" => 1.hour, "24h" => 24.hours, "7d" => 7.days }.freeze
24
+ EVENTS = %w[create update destroy].freeze
25
+ BLOB_COLUMNS = %w[object_changes object metadata].freeze
26
+ PERIODS = { "1h" => 1.hour, "24h" => 24.hours, "7d" => 7.days }.freeze
27
+ ENCRYPTION_MARKER = "__ral_enc__"
27
28
 
28
29
  # @api private
29
30
  def self.configure_connection!
@@ -90,6 +91,16 @@ module RailsAuditLog
90
91
  end
91
92
  }
92
93
 
94
+ # Entries belonging to a specific tenant.
95
+ # Composable with all other scopes.
96
+ #
97
+ # @param id [String, Integer] the tenant identifier stored in +tenant_id+
98
+ # @return [ActiveRecord::Relation]
99
+ # @example
100
+ # AuditLogEntry.for_tenant("acme")
101
+ # AuditLogEntry.for_tenant(Current.tenant_id).updated_events
102
+ scope :for_tenant, ->(id) { where(tenant_id: id) }
103
+
93
104
  # @!endgroup
94
105
 
95
106
  # @!group Time scopes
@@ -192,6 +203,21 @@ module RailsAuditLog
192
203
  self.class.where(item_type: item_type, item_id: item_id).where("id > ?", id).order(id: :asc).first
193
204
  end
194
205
 
206
+ # Returns the decrypted +object_changes+ hash. Transparent to callers —
207
+ # encrypted and non-encrypted entries behave identically.
208
+ #
209
+ # @return [Hash, nil]
210
+ def object_changes
211
+ decrypt_if_encrypted(super)
212
+ end
213
+
214
+ # Returns the decrypted +object+ snapshot hash. Transparent to callers.
215
+ #
216
+ # @return [Hash, nil]
217
+ def object
218
+ decrypt_if_encrypted(super)
219
+ end
220
+
195
221
  # Returns the list of attribute (and association) names that changed in
196
222
  # this entry, derived from the keys of +object_changes+.
197
223
  #
@@ -215,6 +241,12 @@ module RailsAuditLog
215
241
 
216
242
  private
217
243
 
244
+ def decrypt_if_encrypted(value)
245
+ return value unless value.is_a?(Hash) && value.keys == [ENCRYPTION_MARKER]
246
+ json = ActiveRecord::Encryption.encryptor.decrypt(value[ENCRYPTION_MARKER])
247
+ JSON.parse(json)
248
+ end
249
+
218
250
  def metadata_must_be_a_hash
219
251
  errors.add(:metadata, "must be a Hash") if metadata.present? && !metadata.is_a?(Hash)
220
252
  end
@@ -0,0 +1,41 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module RailsAuditLog
5
+ module Generators
6
+ class EncryptionGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Creates an ActiveRecord::Encryption config initializer and a data migration " \
12
+ "that re-encrypts existing audit entries for models using audit_log encrypt: true."
13
+
14
+ def create_initializer_file
15
+ template "rails_audit_log_encryption.rb",
16
+ "config/initializers/rails_audit_log_encryption.rb"
17
+ end
18
+
19
+ def create_migration_file
20
+ migration_template(
21
+ "encrypt_rails_audit_log_entries.rb",
22
+ "db/migrate/encrypt_rails_audit_log_entries.rb"
23
+ )
24
+ end
25
+
26
+ def print_next_steps
27
+ say ""
28
+ say "Next steps:", :green
29
+ say " 1. Run `bin/rails db:encryption:init` to generate encryption keys"
30
+ say " and store them in config/credentials.yml.enc."
31
+ say " 2. Review config/initializers/rails_audit_log_encryption.rb and wire"
32
+ say " the generated keys into ActiveRecord::Encryption."
33
+ say " 3. Add `audit_log encrypt: true` (or set RailsAuditLog.encrypt = true)"
34
+ say " on the models whose audit data you want to protect."
35
+ say " 4. Edit the generated migration — add your encrypted model class names"
36
+ say " to ENCRYPTED_MODELS — then run `bin/rails db:migrate`."
37
+ say ""
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,85 @@
1
+ # Data migration: re-encrypts existing plain-text audit entries for every
2
+ # model listed in ENCRYPTED_MODELS.
3
+ #
4
+ # == Before running
5
+ #
6
+ # 1. Add the class names of every model that uses `audit_log encrypt: true`
7
+ # (or that will be covered by `RailsAuditLog.encrypt = true`) to the
8
+ # ENCRYPTED_MODELS constant below.
9
+ #
10
+ # 2. Ensure ActiveRecord::Encryption is configured with your production keys
11
+ # (config/initializers/rails_audit_log_encryption.rb).
12
+ #
13
+ # 3. Run: bin/rails db:migrate
14
+ #
15
+ # Entries that are already encrypted (detected by the internal envelope format)
16
+ # are skipped automatically — the migration is safe to re-run.
17
+ #
18
+ # This migration is irreversible.
19
+
20
+ class EncryptRailsAuditLogEntries < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
21
+ BATCH_SIZE = 500
22
+ ENCRYPTION_MARKER = RailsAuditLog::AuditLogEntry::ENCRYPTION_MARKER
23
+
24
+ # Add the class names of every model whose audit entries should be encrypted:
25
+ ENCRYPTED_MODELS = %w[
26
+ # Post
27
+ # Payment
28
+ # User
29
+ ].freeze
30
+
31
+ # Isolated AR class — avoids coupling to host-app model overrides.
32
+ class Entry < ActiveRecord::Base # @api private
33
+ self.table_name = "audit_log_entries"
34
+ end
35
+
36
+ def up
37
+ if ENCRYPTED_MODELS.empty?
38
+ say " No ENCRYPTED_MODELS configured — nothing to re-encrypt. " \
39
+ "Edit the migration and add your model class names, then re-run."
40
+ return
41
+ end
42
+
43
+ ENCRYPTED_MODELS.each do |model_name|
44
+ say_with_time "Re-encrypting audit entries for #{model_name}" do
45
+ re_encrypt_for(model_name)
46
+ end
47
+ end
48
+ end
49
+
50
+ def down
51
+ raise ActiveRecord::IrreversibleMigration,
52
+ "Cannot reverse encryption migration — decryption would require the original " \
53
+ "keys and there is no way to distinguish re-encrypted rows from native ones."
54
+ end
55
+
56
+ private
57
+
58
+ def re_encrypt_for(item_type)
59
+ count = 0
60
+ Entry.where(item_type: item_type).find_in_batches(batch_size: BATCH_SIZE) do |batch|
61
+ batch.each do |entry|
62
+ changes = entry.read_attribute(:object_changes)
63
+ object = entry.read_attribute(:object)
64
+
65
+ updates = {}
66
+ updates[:object_changes] = encrypt(changes) if changes && !encrypted?(changes)
67
+ updates[:object] = encrypt(object) if object && !encrypted?(object)
68
+ next if updates.empty?
69
+
70
+ entry.update_columns(updates)
71
+ count += 1
72
+ end
73
+ end
74
+ count
75
+ end
76
+
77
+ def encrypted?(value)
78
+ value.is_a?(Hash) && value.keys == [ENCRYPTION_MARKER]
79
+ end
80
+
81
+ def encrypt(value)
82
+ ciphertext = ActiveRecord::Encryption.encryptor.encrypt(value.to_json)
83
+ { ENCRYPTION_MARKER => ciphertext }
84
+ end
85
+ end
@@ -0,0 +1,41 @@
1
+ # config/initializers/rails_audit_log_encryption.rb
2
+ # Generated by `bin/rails generate rails_audit_log:encryption`
3
+ #
4
+ # == Setup
5
+ #
6
+ # 1. Run `bin/rails db:encryption:init` to generate encryption keys and store
7
+ # them in config/credentials.yml.enc under the :active_record_encryption key.
8
+ #
9
+ # Rails adds entries like:
10
+ # active_record_encryption:
11
+ # primary_key: <generated>
12
+ # deterministic_key: <generated>
13
+ # key_derivation_salt: <generated>
14
+ #
15
+ # 2. Wire the keys into ActiveRecord::Encryption (already done below).
16
+ #
17
+ # 3. Enable encryption globally or per model:
18
+ #
19
+ # # Global — every audited model encrypts by default:
20
+ # # RailsAuditLog.encrypt = true
21
+ #
22
+ # # Per model — opt individual models in:
23
+ # # class Payment < ApplicationRecord
24
+ # # include RailsAuditLog::Auditable
25
+ # # audit_log encrypt: true
26
+ # # end
27
+ #
28
+ # # Per model — opt a model out when the global default is on:
29
+ # # class PublicLog < ApplicationRecord
30
+ # # include RailsAuditLog::Auditable
31
+ # # audit_log encrypt: false
32
+ # # end
33
+
34
+ Rails.application.configure do
35
+ config.active_record.encryption.primary_key =
36
+ Rails.application.credentials.dig(:active_record_encryption, :primary_key)
37
+ config.active_record.encryption.deterministic_key =
38
+ Rails.application.credentials.dig(:active_record_encryption, :deterministic_key)
39
+ config.active_record.encryption.key_derivation_salt =
40
+ Rails.application.credentials.dig(:active_record_encryption, :key_derivation_salt)
41
+ end
@@ -31,6 +31,13 @@ RailsAuditLog.configure do |config|
31
31
  # Per-model `audit_log async: true` also works.
32
32
  # config.async = true
33
33
 
34
+ # Encrypt object_changes and object for all audited models using
35
+ # ActiveRecord::Encryption (Rails 7.1+). Requires config.active_record.encryption
36
+ # to be configured — run `bin/rails generate rails_audit_log:encryption` for the
37
+ # setup initializer and re-encryption migration. Default: false
38
+ # Per-model `audit_log encrypt: false` opts a specific model out.
39
+ # config.encrypt = true
40
+
34
41
  # Route AuditLogEntry to a dedicated database (Rails multi-DB). Default: nil
35
42
  # config.connects_to = { database: { writing: :audit_log, reading: :audit_log } }
36
43
 
@@ -0,0 +1,6 @@
1
+ class AddTenantIdToAuditLogEntries < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ add_column :audit_log_entries, :tenant_id, :string
4
+ add_index :audit_log_entries, :tenant_id
5
+ end
6
+ end
@@ -0,0 +1,33 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module RailsAuditLog
5
+ module Generators
6
+ class TenantGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Creates a migration that adds a tenant_id column and index to audit_log_entries."
12
+
13
+ def create_migration_file
14
+ migration_template(
15
+ "add_tenant_id_to_audit_log_entries.rb",
16
+ "db/migrate/add_tenant_id_to_audit_log_entries.rb"
17
+ )
18
+ end
19
+
20
+ def print_next_steps
21
+ say ""
22
+ say "Next steps:", :green
23
+ say " 1. Run `bin/rails db:migrate` to add the tenant_id column."
24
+ say " 2. Set a global resolver in your initializer:"
25
+ say " RailsAuditLog.current_tenant { Current.tenant_id }"
26
+ say " or per-model:"
27
+ say " audit_log tenant: -> { Current.tenant_id }"
28
+ say " 3. Use AuditLogEntry.for_tenant(id) to scope queries to a tenant."
29
+ say ""
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsAuditLog
2
- VERSION = "1.1.0"
2
+ VERSION = "1.3.0"
3
3
  end
@@ -69,6 +69,14 @@ module RailsAuditLog
69
69
  # @return [Boolean]
70
70
  mattr_accessor :async, default: false
71
71
 
72
+ # When +true+, encrypts +object_changes+ and +object+ for all audited models
73
+ # using +ActiveRecord::Encryption+. Requires the host app to configure
74
+ # +config.active_record.encryption+. Override per-model with
75
+ # <tt>audit_log encrypt: false</tt> to opt a specific model out.
76
+ #
77
+ # @return [Boolean]
78
+ mattr_accessor :encrypt, default: false
79
+
72
80
  # Passes +connects_to+ options directly to {AuditLogEntry} so audit entries
73
81
  # can be stored on a separate database.
74
82
  #
@@ -106,6 +114,35 @@ module RailsAuditLog
106
114
  yield self
107
115
  end
108
116
 
117
+ # Sets or returns the global tenant resolver block. The block is called at
118
+ # write time and its return value is stored in the +tenant_id+ column of each
119
+ # {AuditLogEntry}. Override per-model with <tt>audit_log tenant: -> { ... }</tt>.
120
+ #
121
+ # @yield block called with no arguments at write time; return the tenant id
122
+ # @return [Proc, nil] the stored block, or +nil+ when not configured
123
+ # @example
124
+ # RailsAuditLog.current_tenant { Current.tenant_id }
125
+ def self.current_tenant(&block)
126
+ @current_tenant = block if block_given?
127
+ @current_tenant
128
+ end
129
+
130
+ # Wires {.current_tenant} to +ActsAsTenant.current_tenant&.id+ so audit
131
+ # entries are automatically scoped to the Acts As Tenant context.
132
+ # Call once in an initializer after the gem is loaded.
133
+ #
134
+ # @raise [RuntimeError] if the +acts_as_tenant+ gem is not loaded
135
+ # @return [void]
136
+ # @example
137
+ # RailsAuditLog.acts_as_tenant!
138
+ def self.acts_as_tenant!
139
+ unless defined?(ActsAsTenant)
140
+ raise "ActsAsTenant is not loaded. Add the `acts_as_tenant` gem to your Gemfile."
141
+ end
142
+
143
+ current_tenant { ActsAsTenant.current_tenant&.id }
144
+ end
145
+
109
146
  # Sets or returns the authentication block used to gate the web dashboard.
110
147
  # The block is evaluated in controller context, so controller helpers
111
148
  # (e.g. +current_user+) are available directly.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_audit_log
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -109,12 +109,17 @@ files:
109
109
  - app/views/rails_audit_log/resources/show.html.erb
110
110
  - config/importmap.rb
111
111
  - config/routes.rb
112
+ - lib/generators/rails_audit_log/encryption/encryption_generator.rb
113
+ - lib/generators/rails_audit_log/encryption/templates/encrypt_rails_audit_log_entries.rb
114
+ - lib/generators/rails_audit_log/encryption/templates/rails_audit_log_encryption.rb
112
115
  - lib/generators/rails_audit_log/initializer/initializer_generator.rb
113
116
  - lib/generators/rails_audit_log/initializer/templates/rails_audit_log.rb
114
117
  - lib/generators/rails_audit_log/install/install_generator.rb
115
118
  - lib/generators/rails_audit_log/install/templates/create_audit_log_entries.rb
116
119
  - lib/generators/rails_audit_log/migrate_from_paper_trail/migrate_from_paper_trail_generator.rb
117
120
  - lib/generators/rails_audit_log/migrate_from_paper_trail/templates/migrate_from_paper_trail.rb
121
+ - lib/generators/rails_audit_log/tenant/templates/add_tenant_id_to_audit_log_entries.rb
122
+ - lib/generators/rails_audit_log/tenant/tenant_generator.rb
118
123
  - lib/rails_audit_log.rb
119
124
  - lib/rails_audit_log/engine.rb
120
125
  - lib/rails_audit_log/matchers.rb