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 +4 -4
- data/README.md +50 -0
- data/app/concerns/rails_audit_log/auditable.rb +14 -2
- data/app/controllers/rails_audit_log/application_controller.rb +5 -0
- data/app/controllers/rails_audit_log/audit_log_entries_controller.rb +1 -1
- data/app/controllers/rails_audit_log/resources_controller.rb +1 -1
- data/app/models/rails_audit_log/audit_log_entry.rb +10 -0
- data/lib/generators/rails_audit_log/tenant/templates/add_tenant_id_to_audit_log_entries.rb +6 -0
- data/lib/generators/rails_audit_log/tenant/tenant_generator.rb +33 -0
- data/lib/rails_audit_log/version.rb +1 -1
- data/lib/rails_audit_log.rb +29 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb6120f7ce9f0f2a2fb19553a77ff4c6bf5c24a1945270e2e6393403b9ffc3a0
|
|
4
|
+
data.tar.gz: a978f7082c30a68c37344e0b819af1e70d077b3092337c5ea31c4e35473ffef6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
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 =
|
|
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
|
|
@@ -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,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
|
data/lib/rails_audit_log.rb
CHANGED
|
@@ -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.
|
|
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
|