Skip to content

Commit

Permalink
Adds redacted option
Browse files Browse the repository at this point in the history
* In order to record that changes occurred, without storing sensitive
values, pass the `redacted` option.

* Redacted values default to `'[REDACTED]'` but can be customized with
the `redaction_value` option.

```
class User
    audited redacted: [:password, :ssn], redaction_value:
SecureRandom.uuid
end
```

* A lot of this was based on the work done
[here](#339).

* Resolves #475.
  • Loading branch information
JonathanWThom committed Feb 16, 2019
1 parent 7eb075e commit eb53b24
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 0 deletions.
28 changes: 28 additions & 0 deletions lib/audited/auditor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ module ClassMethods
# * +require_comment+ - Ensures that audit_comment is supplied before
# any create, update or destroy operation.
# * +max_audits+ - Limits the number of stored audits.

# * +redacted+ - Changes to these fields will be logged, but the values
# will not. This is useful, for example, if you wish to audit when a
# password is changed, without saving the actual password in the log.
# To store values as something other than '[REDACTED]', pass an argument
# to the redaction_value option.
#
# class User < ActiveRecord::Base
# audited redacted: :password, redaction_value: SecureRandom.uuid
# end
#
# * +if+ - Only audit the model when the given function returns true
# * +unless+ - Only audit the model when the given function returns false
Expand Down Expand Up @@ -90,6 +100,7 @@ def has_associated_audits
end

module AuditedInstanceMethods
REDACTED = '[REDACTED]'
# Deprecate version attribute in favor of audit_version attribute – preparing for eventual removal.
def method_missing(method_name, *args, &block)
if method_name == :version
Expand Down Expand Up @@ -214,6 +225,7 @@ def audited_changes
all_changes.except(*self.class.non_audited_columns)
end

filtered_changes = redact_values(filtered_changes)
filtered_changes = normalize_enum_changes(filtered_changes)
filtered_changes.to_hash
end
Expand All @@ -234,6 +246,22 @@ def normalize_enum_changes(changes)
changes
end

def redact_values(filtered_changes)
[audited_options[:redacted]].flatten.compact.each do |option|
changes = filtered_changes[option.to_s]
new_value = audited_options[:redaction_value] || REDACTED
if changes.is_a? Array
values = changes.map { |c| new_value }
else
values = new_value
end
hash = Hash[option.to_s, values]
filtered_changes.merge!(hash)
end

filtered_changes
end

def rails_below?(rails_version)
Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new(rails_version)
end
Expand Down
33 changes: 33 additions & 0 deletions spec/audited/auditor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,39 @@ def non_column_attr=(val)
expect(user.audits.last.audited_changes.keys).to eq(%w{non_column_attr})
end

it "should redact columns specified in 'redacted' option" do
redacted = Audited::Auditor::AuditedInstanceMethods::REDACTED
user = Models::ActiveRecord::UserRedactedPassword.create(password: "password")
user.save!
expect(user.audits.last.audited_changes['password']).to eq(redacted)
user.password = "new_password"
user.save!
expect(user.audits.last.audited_changes['password']).to eq([redacted, redacted])
end

it "should redact columns specified in 'redacted' option when there are multiple specified" do
redacted = Audited::Auditor::AuditedInstanceMethods::REDACTED
user =
Models::ActiveRecord::UserMultipleRedactedAttributes.create(
password: "password",
ssn: 123456789
)
user.save!
expect(user.audits.last.audited_changes['password']).to eq(redacted)
expect(user.audits.last.audited_changes['ssn']).to eq(redacted)
user.password = "new_password"
user.ssn = 987654321
user.save!
expect(user.audits.last.audited_changes['password']).to eq([redacted, redacted])
expect(user.audits.last.audited_changes['ssn']).to eq([redacted, redacted])
end

it "should redact columns in 'redacted' column with custom option" do
user = Models::ActiveRecord::UserRedactedPasswordCustomRedaction.create(password: "password")
user.save!
expect(user.audits.last.audited_changes['password']).to eq(["My", "Custom", "Value", 7])
end

if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
describe "'json' and 'jsonb' audited_changes column type" do
let(:migrations_path) { SPEC_ROOT.join("support/active_record/postgres") }
Expand Down
15 changes: 15 additions & 0 deletions spec/support/active_record/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ class UserOnlyPassword < ::ActiveRecord::Base
audited only: :password
end

class UserRedactedPassword < ::ActiveRecord::Base
self.table_name = :users
audited redacted: :password
end

class UserMultipleRedactedAttributes < ::ActiveRecord::Base
self.table_name = :users
audited redacted: [:password, :ssn]
end

class UserRedactedPasswordCustomRedaction < ::ActiveRecord::Base
self.table_name = :users
audited redacted: :password, redaction_value: ["My", "Custom", "Value", 7]
end

class CommentRequiredUser < ::ActiveRecord::Base
self.table_name = :users
audited comment_required: true
Expand Down
1 change: 1 addition & 0 deletions spec/support/active_record/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
t.column :created_at, :datetime
t.column :updated_at, :datetime
t.column :favourite_device, :string
t.column :ssn, :integer
end

create_table :companies do |t|
Expand Down

0 comments on commit eb53b24

Please sign in to comment.