A: 

CRUD operations must be logged, including what the operation was, who the actor was, and when the operation occurred

This can be addressed with an ActiveRecord::Callbacks and an attr_accessor field.

In any of the models that need to be logged add the following.

  attr_accessor :modifier_id, :modifier

  valiadate :valid_user
  before_validate :populate_modifier
  before_save :write_save_attempted_to_audit_log
  after_save :write_save_completed_to_audit_log

  def populate_modifier
    self.modifier = User.find_by_id(modifier_id) unless modifier
  end 

  def valid_user
    unless modifier
      errors.add(:modifiers_user_id, "Unknown user attempted to modify this record") 
      write_unauthorized_modification_to_audit_log
    end
  end

  def write_save_attempted_to_audit_log
    # announce that user is attempting to save a record with timestamp to audit log
    # use ActiveRecord::Dirty.changes to show the change the might be made
  end

  def write_save_competed_to_audit_log
    # announce that user has successfully changed the record with timestamp to audit log
  end

  def write_unauthorized_modification
    # announce that a change was attempted without a user
  end

Because you're likely to use this in a few models you can abstract it into a plugin, and add it only with needed with a method call like audit_changes. See any of the acts_as plugins for inspiration on how to accomplish this.

In the controllers you will need to remember to add @thing.modifier = @current_user before attempting to save.


There needs to be some way to validate the data (i.e. recording MD5 checksums of records)

As for a checksum... of an operation? You could override inspect to print a string containing all the information in the record in a consistent fashion and then generate a checksum for that. While you're at it, might as well add it to the to the audit log as part of the writing to log methods.


Some data should be write-once (i.e. the app can create an audit log entry, but that log cannot be edited or deleted from within the application thereafter)

Write each access log as a separate file with a deterministic name ("/logs/audits/#{class}/#{id}/#{timestamp}") and remove the write permission once it's saved. File.chmod(0555, access_log_file)


Changes to associated objects probably should be logged throughout the nesting. For example, adding a CustodyLog to a piece of Evidence should have a log for itself, a log for it's Evidence, and a log for the parent Case. This is to ensure that the last update timestamp for the Case accurately reflects the real last update, and not just the last time that the Case model data itself changed.

As for the 4th requirement. That will automatically get rolled into my solution for the first if you use accepts_nested_attributes_for on any of your nested relationships. And :autosave => true for belongs_to relationships.


If you're saving checksums into audit logs, you can roll a check into the before_save method to ensure the object you're working on, was has not been tampered with. Just by checking the latest audit log for the object and matching up the checksums.

EmFi