3scale/porta

View on GitHub
config/initializers/audited_hacks.rb

Summary

Maintainability
A
1 hr
Test Coverage
module AuditHacks
  extend ActiveSupport::Concern

  TTL = 3.months

  included do
    # this could also validate provider_id, but unfortunately we have to much going on
    # and factories destroy the whole thing
    validates :kind, :presence => true

    alias_attribute :association_id, :associated_id
    alias_attribute :association_type, :associated_type

    after_commit :log_to_stdout, on: :create, if: :logging_to_stdout?

    def self.delete_old
      where('created_at < ?', TTL.ago).delete_all
    end

    def self.logging_to_stdout?
      Features::LoggingConfig.config.audits_to_stdout
    end

    delegate :logging_to_stdout?, to: :class

    def log_to_stdout
      logger.tagged('audit', kind, action) { logger.info log_trail }
    end

    def obfuscated
      dup.tap do |copy|
        copy.send(:write_attribute, :id, id)
        copy.send(:write_attribute, :created_at, created_at)
        copy.audited_changes = ThreeScale::FilterArguments.new(audited_changes).filter if audited_changes
      end
    end

    protected

    def log_trail
      to_h_safe.to_json
    end

    alias_method :to_s, :log_trail

    def to_h_safe
      attrs = %w[auditable_type auditable_id action audited_changes version provider_id user_id user_type request_uuid remote_address created_at]
      hash = obfuscated.attributes.slice(*attrs)
      hash['user_role'] = user&.role
      hash['audit_id'] = id
      hash
    end
  end

  def audited_changes_for_destroy_list
    changes = audited_changes.extract!(*kind.constantize.attributes_for_destroy_list)
    changes.merge('id' => auditable_id)
  end

  # runs all before_create callbacks except version, which performs a database call redundant for background auditing
  def before_background_callbacks(*args)
    set_audit_user
    set_request_uuid
    set_remote_address
  end

  def enqueue_job
    AuditedWorker.perform_async(attributes.as_json)
  end
end

module AuditedHacks
  extend ActiveSupport::Concern

  included do
    extend ClassMethods
  end

  module ClassMethods
    def audited(options = {})
      super

      # this disables auditing only for current thread
      self.disable_auditing if Rails.env.test?

      include InstanceMethods
      include AfterCommitQueue
      class << self
        prepend ClassMethods
      end
    end

    def synchronous_audits
      original = Thread.current.thread_variable_get(:audit_hacks_synchronous)

      Thread.current.thread_variable_set(:audit_hacks_synchronous, true)
      yield if block_given?

      original
    ensure
      Thread.current.thread_variable_set(:audit_hacks_synchronous, original)
    end

    # override method to also enable auditing globally
    def with_auditing
      auditing_was_enabled = Audited.auditing_enabled
      Audited.auditing_enabled = true
      super
    ensure
      Audited.auditing_enabled = false unless auditing_was_enabled
    end

    def with_synchronous_auditing(&block)
      synchronous_audits { with_auditing(&block) }
    end
  end

  module InstanceMethods
    def auditing_enabled?
      auditing_enabled
    end

    private

    def write_audit(attrs)
      return unless auditing_enabled

      provider_id = respond_to?(:tenant_id) && self.tenant_id
      provider_id ||= respond_to?(:provider_account_id) && self.provider_account_id
      provider_id ||= respond_to?(:provider_id) && self.provider_id
      provider_id ||= respond_to?(:provider_account) && self.provider_account.try!(:id)
      provider_id ||= self.provider_id_for_audits

      attrs[:provider_id] = provider_id
      attrs[:kind] = self.class.to_s

      Audited.audit_class.as_user(User.current) do
        if self.class.synchronous_audits
          super
        else
          # this is basically a copy of super that enqueues audit instead of creating it
          self.audit_comment = nil

          attrs[:associated] = send(audit_associated_with) unless audit_associated_with.nil?

          run_callbacks(:audit) {
            audit = audits.build(attrs)
            audit.before_background_callbacks
            audit.destroy # avoid autosave logic
            run_after_commit { audit.enqueue_job }
            # if needed to combine audits, that can be moved to the background job
            # combine_audits_if_needed if attrs[:action] != "create"
            audit
          }
        end
      end
    end

    protected
    # Overwrite this in your auditable models to return something for audit's provider_id
    #
    # for example:
    #   class Mojo < ActiveRecord::Base
    #     auditable
    #
    #     def provider_id_for_audits
    #       42
    #     end
    #   end
    def provider_id_for_audits
      nil
    end
  end
end

ActiveSupport.on_load(:active_record) do
  # Unlike above, this disables auditing globally
  Audited.auditing_enabled = false if Rails.env.test?

  # we want to audit created_at field
  Audited.ignored_attributes = %w(lock_version updated_at created_on updated_on)

  Audited.audit_class.class_eval do
    include AuditHacks
  end

  # This fixes issues with overloading current_user in our controllers
  Audited::Sweeper.prepend(Module.new do
    def current_user
      User.current
    end
  end)

  ::ActiveRecord::Base.class_eval do
    include AuditedHacks
  end
end