rails_audit_log 1.2.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: 40a23f8ea1990a26ea7f575102fb5ddd5f1a7323c796315672de57f0154faec3
4
- data.tar.gz: 8b0563eebeeb21fe9836f3ff091640ae4a1c5484b45dc515abfc4647510cfae9
3
+ metadata.gz: fb6120f7ce9f0f2a2fb19553a77ff4c6bf5c24a1945270e2e6393403b9ffc3a0
4
+ data.tar.gz: a978f7082c30a68c37344e0b819af1e70d077b3092337c5ea31c4e35473ffef6
5
5
  SHA512:
6
- metadata.gz: 29cc8682459e2d7968555a49faa77e47c00da42a54bf633f286dd01e89979be8fff6790aa0880de5d7516d23f1fc16f4a88c10b1e4aa086921c6052fdb812784
7
- data.tar.gz: f1a1ba92cfe1c9a1ff0640b9b8a78f94275195fb018da97d89557b9612d631c31293b228e0715d059b790a3f0d2888069066f9897f376e43a40467c8c0c6dbd2
6
+ metadata.gz: 8670a4ceb147508b45d03679eb3765c6984393c144434f4ad90b40bf69c64478ff8b92b0ab3bdb0b054cb0f64d2794f05044d546f81fa2fde17496ff65fde4de
7
+ data.tar.gz: 5ba060114539abc8555a5873aa7671c226cdc2c19a3bd66a4549595f68ea170372a968de324485dc0bf2eabb0bd253fa14e5c14c98b7b4ad9fe691bc15973fbb
data/README.md CHANGED
@@ -26,6 +26,7 @@ Audit logging for Rails. Tracks `create`, `update`, and `destroy` events as stru
26
26
  - [Time-based retention](#time-based-retention)
27
27
  - [Scheduled and manual pruning](#scheduled-and-manual-pruning)
28
28
  - [Encrypting audit data](#encrypting-audit-data)
29
+ - [Multi-tenancy](#multi-tenancy)
29
30
  - [Selective tracking](#selective-tracking)
30
31
  - [Disabling auditing](#disabling-auditing)
31
32
  - [Object reconstruction](#object-reconstruction)
@@ -427,6 +428,55 @@ The generator creates:
427
428
  - `config/initializers/rails_audit_log_encryption.rb` — reads the generated keys from credentials and passes them to `ActiveRecord::Encryption`
428
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`
429
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
+
430
480
  ### Selective tracking
431
481
 
432
482
  Track only specific attributes, or exclude noisy ones:
@@ -31,6 +31,7 @@ module RailsAuditLog
31
31
  class_attribute :_audit_log_retain_for, default: nil
32
32
  class_attribute :_audit_log_async, default: false
33
33
  class_attribute :_audit_log_encrypt, default: nil
34
+ class_attribute :_audit_log_tenant, default: nil
34
35
 
35
36
  _warn_if_audit_table_missing
36
37
 
@@ -119,18 +120,21 @@ module RailsAuditLog
119
120
  # host app to configure +config.active_record.encryption+; decryption is
120
121
  # transparent — {AuditLogEntry#diff}, {AuditLogEntry#reify}, and
121
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
122
126
  # @return [void]
123
127
  # @example
124
128
  # class Article < ApplicationRecord
125
129
  # include RailsAuditLog::Auditable
126
130
  # audit_log only: %i[title body published_at],
127
- # meta: { tenant_id: -> { Current.tenant_id } },
131
+ # tenant: -> { Current.tenant_id },
128
132
  # associations: %i[tags],
129
133
  # version_limit: 100,
130
134
  # retain_for: 30.days,
131
135
  # encrypt: true
132
136
  # end
133
- def audit_log(only: nil, ignore: nil, meta: nil, associations: nil, version_limit: nil, retain_for: nil, async: nil, encrypt: 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)
134
138
  self._audit_log_only = only.map(&:to_s) if only
135
139
  self._audit_log_ignore = ignore.map(&:to_s) if ignore
136
140
  self._audit_log_meta = meta if meta
@@ -139,6 +143,7 @@ module RailsAuditLog
139
143
  self._audit_log_retain_for = retain_for unless retain_for.nil?
140
144
  self._audit_log_async = async unless async.nil?
141
145
  self._audit_log_encrypt = encrypt unless encrypt.nil?
146
+ self._audit_log_tenant = tenant unless tenant.nil?
142
147
  end
143
148
  end
144
149
 
@@ -183,6 +188,7 @@ module RailsAuditLog
183
188
  object: nil,
184
189
  reason: RailsAuditLog.reason,
185
190
  metadata: meta.presence,
191
+ tenant_id: resolve_tenant_id,
186
192
  whodunnit_snapshot: actor ? RailsAuditLog.whodunnit_display.call(actor) : nil,
187
193
  actor_type: actor&.class&.name,
188
194
  actor_id: actor.respond_to?(:id) ? actor.id : nil
@@ -205,6 +211,7 @@ module RailsAuditLog
205
211
  object: maybe_encrypt(snapshot),
206
212
  reason: RailsAuditLog.reason,
207
213
  metadata: meta.presence,
214
+ tenant_id: resolve_tenant_id,
208
215
  whodunnit_snapshot: actor ? RailsAuditLog.whodunnit_display.call(actor) : nil,
209
216
  actor_type: actor&.class&.name,
210
217
  actor_id: actor.respond_to?(:id) ? actor.id : nil
@@ -248,6 +255,11 @@ module RailsAuditLog
248
255
  end
249
256
  end
250
257
 
258
+ def resolve_tenant_id
259
+ tenant_proc = self.class._audit_log_tenant || RailsAuditLog.current_tenant
260
+ tenant_proc&.call
261
+ end
262
+
251
263
  def build_audit_metadata
252
264
  meta = {}
253
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
  )
@@ -91,6 +91,16 @@ module RailsAuditLog
91
91
  end
92
92
  }
93
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
+
94
104
  # @!endgroup
95
105
 
96
106
  # @!group Time scopes
@@ -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.2.0"
2
+ VERSION = "1.3.0"
3
3
  end
@@ -114,6 +114,35 @@ module RailsAuditLog
114
114
  yield self
115
115
  end
116
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
+
117
146
  # Sets or returns the authentication block used to gate the web dashboard.
118
147
  # The block is evaluated in controller context, so controller helpers
119
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.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -118,6 +118,8 @@ files:
118
118
  - lib/generators/rails_audit_log/install/templates/create_audit_log_entries.rb
119
119
  - lib/generators/rails_audit_log/migrate_from_paper_trail/migrate_from_paper_trail_generator.rb
120
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
121
123
  - lib/rails_audit_log.rb
122
124
  - lib/rails_audit_log/engine.rb
123
125
  - lib/rails_audit_log/matchers.rb