Skip to content

Commit

Permalink
Add conditional and exclusionary auditing feature
Browse files Browse the repository at this point in the history
* Adds conditional (:if) and exclusionary (:unless) auditing feature
* Updates docs for `audited` to reflect new features

Upstream collectiveidea#167
  • Loading branch information
Valentino authored and ledermann committed Jun 1, 2018
1 parent 08c9958 commit acb9759
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Added

- Add `inverse_of: auditable` definition to audit relation
[#413](https://github.com/collectiveidea/audited/pull/413)
- Add functionality to conditionally audit models
[#414](https://github.com/collectiveidea/audited/pull/414)

Changed

Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,32 @@ user.audits.last.associated # => #<Company name: "Collective Idea">
company.associated_audits.last.auditable # => #<User name: "Steve Richert">
```

### Conditional auditing

If you want to audit only under specific conditions, you can provide conditional options (similar to ActiveModel callbacks) that will ensure your model is only audited for these conditions.

```ruby
class User < ActiveRecord::Base
audited if: :active?

private

def active?
last_login > 6.months.ago
end
end
```

Just like in ActiveModel, you can use an inline Proc in your conditions:

```ruby
class User < ActiveRecord::Base
audited unless: Proc.new { |u| u.ninja? }
end
```

In the above case, the user will only be audited when `User#ninja` is `false`.

### Disabling auditing

If you want to disable auditing temporarily doing certain tasks, there are a few
Expand Down
26 changes: 24 additions & 2 deletions lib/audited/auditor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,25 @@ module ClassMethods
# * +require_comment+ - Ensures that audit_comment is supplied before
# any create, update or destroy operation.
#
# * +if+ - Only audit the model when the given function returns true
# * +unless+ - Only audit the model when the given function returns false
#
# class User < ActiveRecord::Base
# audited :if => :active?
#
# def active?
# self.status == 'active'
# end
# end
#
def audited(options = {})
# don't allow multiple calls
return if included_modules.include?(Audited::Auditor::AuditedInstanceMethods)

extend Audited::Auditor::AuditedClassMethods
include Audited::Auditor::AuditedInstanceMethods

class_attribute :audit_associated_with, instance_writer: false
class_attribute :audit_associated_with, instance_writer: false
class_attribute :audited_options, instance_writer: false
attr_accessor :version, :audit_comment

Expand Down Expand Up @@ -249,7 +260,18 @@ def require_comment
end

def auditing_enabled
self.class.auditing_enabled
return run_conditional_check(audited_options[:if]) &&
run_conditional_check(audited_options[:unless], matching: false) &&
self.class.auditing_enabled
end

def run_conditional_check(condition, matching: true)
return true if condition.blank?

return condition.call(self) == matching if condition.respond_to?(:call)
return send(condition) == matching if respond_to?(condition.to_sym)

true
end

def auditing_enabled=(val)
Expand Down
108 changes: 108 additions & 0 deletions spec/audited/auditor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,114 @@
end
end

context "should be configurable which conditions are audited" do
subject { ConditionalCompany.new.send(:auditing_enabled) }

context "when passing a method name" do
before do
class ConditionalCompany < ::ActiveRecord::Base
self.table_name = 'companies'

audited if: :public?

def public?; end
end
end

context "when conditions are true" do
before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(true) }
it { is_expected.to be_truthy }
end

context "when conditions are false" do
before { allow_any_instance_of(ConditionalCompany).to receive(:public?).and_return(false) }
it { is_expected.to be_falsey }
end
end

context "when passing a Proc" do
context "when conditions are true" do
before do
class InclusiveCompany < ::ActiveRecord::Base
self.table_name = 'companies'
audited if: Proc.new { true }
end
end

subject { InclusiveCompany.new.send(:auditing_enabled) }

it { is_expected.to be_truthy }
end

context "when conditions are false" do
before do
class ExclusiveCompany < ::ActiveRecord::Base
self.table_name = 'companies'
audited if: Proc.new { false }
end
end
subject { ExclusiveCompany.new.send(:auditing_enabled) }
it { is_expected.to be_falsey }
end
end
end

context "should be configurable which conditions aren't audited" do
context "when using a method name" do
before do
class ExclusionaryCompany < ::ActiveRecord::Base
self.table_name = 'companies'

audited unless: :non_profit?

def non_profit?; end
end
end

subject { ExclusionaryCompany.new.send(:auditing_enabled) }

context "when conditions are true" do
before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(true) }
it { is_expected.to be_falsey }
end

context "when conditions are false" do
before { allow_any_instance_of(ExclusionaryCompany).to receive(:non_profit?).and_return(false) }
it { is_expected.to be_truthy }
end
end

context "when using a proc" do
context "when conditions are true" do
before do
class ExclusionaryCompany < ::ActiveRecord::Base
self.table_name = 'companies'
audited unless: Proc.new { |c| c.exclusive? }

def exclusive?
true
end
end
end

subject { ExclusionaryCompany.new.send(:auditing_enabled) }
it { is_expected.to be_falsey }
end

context "when conditions are false" do
before do
class InclusiveCompany < ::ActiveRecord::Base
self.table_name = 'companies'
audited unless: Proc.new { false }
end
end

subject { InclusiveCompany.new.send(:auditing_enabled) }
it { is_expected.to be_truthy }
end
end
end

it "should be configurable which attributes are not audited via ignored_attributes" do
Audited.ignored_attributes = ['delta', 'top_secret', 'created_at']
class Secret < ::ActiveRecord::Base
Expand Down

0 comments on commit acb9759

Please sign in to comment.