Open mgraham opened 10 years ago
Hello @mgraham, I'm facing the same issue. Have you found a solution since making this issue with storing extra admin user information?
Thanks!
Perhaps add an after_create
callback on audit that adds that information in the audit comment, or add another field in your audit table and fill that in similarly
Hi @tbrisker, I think that is a good place to start but this requires me to store the current AdminUser into the Thread.current
hash for saving into the Audit in the after_create
callback. I rather modify the Audited::Sweeper
class to get the current AdminUser directly from the controller.
However, there doesn't seem to be any other possibility at the moment (apart from coding everything myself). Thanks for your comment and pointer, much appreciated :-)
I'll see if I can get this working. Once it's working, I'll post my results here.
Unfortunately it seems the 3.0 version doesn't allow subclassing the Audited::Audit
for a custom Audit
model as it's a Module.
So far, I have managed to get it working using the after_audit
callback. I put it in a concern Module for ease of use.
I put all the code in this gist: https://gist.github.com/werdlerk/3cf429b7e2495c125749ce5260dffdf5
It's not the best solution but it works for now.
I'm trying to implement this idiomatically. IE not hackishly. Notes:
I added an impersonator_id
column to the audits table.
I noticed that Sweeper has a list of the attributes that will be taken from the environment in Audited::Sweeper::STORED_DATA
. It isn't so much designed to be extended since it's a const, but it's Ruby so... 😂 I gave it a shot using the pattern for current_user
Audited::Sweeper::STORED_DATA[:impersonator] = :impersonator
module Audited
class Sweeper
def impersonator
lambda {
Rails.logger.info("[ GETTING IMPERSONATOR ] =========================== \n#{{session_id: controller.try(:user_session).try(:id), impersonator: controller.try(:impersonator)}.inspect}\n\n")
controller.try(:impersonator)
}
end
end
end
The impersonator
method was called but the lambda was not.
But then:
jw@logopolis:/projects/client/project$ bin/rails c
Running via Spring preloader in process 28340
Loading development environment (Rails 6.1.4.6)
>> Audited::Sweeper::STORED_DATA
=>
{:current_remote_address=>:remote_ip,
:current_request_uuid=>:request_uuid,
:current_user=>:current_user}
>> exit
jw@logopolis:/projects/client/project$ spring stop
Spring stopped.
jw@logopolis:/projects/client/project$ bin/rails c
Running via Spring preloader in process 28454
Loading development environment (Rails 6.1.4.6)
>> Audited::Sweeper::STORED_DATA
=>
{:current_remote_address=>:remote_ip,
:current_request_uuid=>:request_uuid,
:current_user=>:current_user,
:impersonator=>:impersonator}
ðŸ˜
Anyway... the impersonator
lambda still wasn't called. I copied this method and named it current_user
and then it stopped tracking the current user, also, so at least I felt like I was headed vaguely in the correct direction.
Next I tried to (a) see if auditor just returned a user, would it be included? (b) make my version of current_user
work.
def current_user
lambda {
Rails.logger.info("[ audit / current_user ] =========================== \n#{{session_id: controller.send(:user_session).try(:id), current_user: controller.send(Audited.current_user_method)}.inspect}\n\n")
controller.send(Audited.current_user_method) if controller.respond_to?(Audited.current_user_method, true)
}
end
def impersonator
lambda {
User.last
}
end
Interesting... 🤔 I learned that controller.try(:whatever) results in nil, but controller.send(:whatever) results in a value... 🤷 Seems wrong to me. My current_user method was now being used, and current_user was being audited again but impersonator was not. Time to dig into this hash to see where it's used. Disappointingly, it is only used to gather data.
Sweeper is used in an around_action. The data is then used here... making progress!
module Audited
class Audit < ::ActiveRecord::Base
before_create :set_impersonator
belongs_to :impersonator, required: false, class_name: 'User'
def set_impersonator
self.impersonator ||= ::Audited.store[:impersonator].try!(:call) # from Sweeper
nil # prevent stopping callback chains
end
end
And then...
Audited::Audit Create (1.2ms) INSERT INTO "audits" ("auditable_id", "auditable_type", "user_id", "user_type", "action", "audited_changes", "version", "remote_address", "request_uuid", "created_at", "impersonator_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING "id" [["auditable_id", 4], ["auditable_type", "SomeThing"], ["user_id", 3], ["user_type", "User"], ["action", "update"], ["audited_changes", "{\"status_id\":[3,9]}"], ["version", 32], ["remote_address", "192.168.1.22"], ["request_uuid", "7c4d1bb7-8d36-4851-add6-a41f5a66249f"], ["created_at", "2022-04-20 17:18:39.712769"], ["impersonator_id", 7]]
and that did it! Impersonator is now being stored in the audits table.
So the final code. We're defining it in config/initializers/audited.rb
. Note that means you need to restart Rails for it to take effect... not necessarily what I would have chosen but there was already some audited-gem-related stuff here so I'm going with our convention.
Audited::Sweeper::STORED_DATA[:impersonator] = :impersonator
module Audited
class Sweeper
def impersonator
lambda {
controller.send(:impersonator)
}
end
end
end
module Audited
class Audit < ::ActiveRecord::Base
before_create :set_impersonator
belongs_to :impersonator, required: false, class_name: 'User'
def set_impersonator
self.impersonator ||= ::Audited.store[:impersonator].try!(:call) # from Sweeper
nil # prevent stopping callback chains
end
end
end
In our system, an admin user can impersonate another user. In our audit trail, we'd like to store both the effective user and the real user (admin).
What would be the best way to approach this?