audit_log_rails 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +276 -634
- data/lib/audit_log_rails.rb +4 -0
- data/lib/audit_logger/audit_log.rb +13 -1
- data/lib/audit_logger/change_extractor.rb +15 -3
- data/lib/audit_logger/config_validator.rb +22 -0
- data/lib/audit_logger/configuration.rb +19 -1
- data/lib/audit_logger/humanizer.rb +47 -1
- data/lib/audit_logger/model_config.rb +27 -2
- data/lib/audit_logger/owner_resolver.rb +45 -0
- data/lib/audit_logger/persistence.rb +30 -0
- data/lib/audit_logger/railtie.rb +1 -0
- data/lib/audit_logger/record_audit_entry.rb +13 -20
- data/lib/audit_logger/record_event.rb +73 -0
- data/lib/audit_logger/version.rb +1 -1
- data/lib/audit_logger.rb +35 -0
- data/lib/generators/audit_logger/templates/add_owner_to_audit_logs.rb.tt +18 -0
- data/lib/generators/audit_logger/templates/audit_logger_initializer.rb.tt +13 -0
- data/lib/generators/audit_logger/templates/create_audit_logs.rb.tt +3 -0
- data/lib/generators/audit_logger/upgrade_generator.rb +30 -0
- metadata +19 -8
|
@@ -6,6 +6,18 @@ module AuditLogger
|
|
|
6
6
|
|
|
7
7
|
JSON_FIELDS = %w[changed_by_other audited_changes audited_changes_humanize].freeze
|
|
8
8
|
|
|
9
|
+
# Historico de um agregado: tudo o que tem o registro como dono, do mais recente ao mais antigo.
|
|
10
|
+
scope :of_owner, lambda { |owner|
|
|
11
|
+
where(OwnerResolver.attributes_for(owner)).order(created_at: :desc, id: :desc)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# As colunas de dono chegaram na 0.2.0; a migration de upgrade cria as duas.
|
|
15
|
+
def self.owner_columns?
|
|
16
|
+
column_names.include?("owner_type") && column_names.include?("owner_id")
|
|
17
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::NoDatabaseError
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
9
21
|
# Mantem compatibilidade entre bancos nativos JSON/JSONB e colunas texto sem
|
|
10
22
|
# depender de conexao ativa no carregamento da classe.
|
|
11
23
|
JSON_FIELDS.each do |field_name|
|
|
@@ -39,7 +51,7 @@ module AuditLogger
|
|
|
39
51
|
|
|
40
52
|
# A verificacao do tipo fica em runtime, quando a conexao ja deve estar disponivel.
|
|
41
53
|
def json_like_column?(field_name)
|
|
42
|
-
self.class.type_for_attribute(field_name.to_s).type.in?([
|
|
54
|
+
self.class.type_for_attribute(field_name.to_s).type.in?(%i[json jsonb])
|
|
43
55
|
rescue ActiveRecord::ConnectionNotDefined, ActiveRecord::NoDatabaseError
|
|
44
56
|
false
|
|
45
57
|
end
|
|
@@ -38,21 +38,29 @@ module AuditLogger
|
|
|
38
38
|
# Usa snapshot completo em create e destroy para preservar contexto.
|
|
39
39
|
def snapshot_fields
|
|
40
40
|
filtered_attributes.each_with_object({}) do |(attribute, value), result|
|
|
41
|
-
result[attribute] = { "value" => value }
|
|
41
|
+
result[attribute] = { "value" => mask(attribute, value) }
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Em update salva apenas delta com old/new para reduzir ruido.
|
|
46
|
+
# Atributo mascarado entra no delta (a troca de senha fica registrada), mas sem os valores.
|
|
46
47
|
def update_fields
|
|
47
48
|
filtered_previous_changes.each_with_object({}) do |(attribute, values), result|
|
|
48
49
|
old_value, new_value = values
|
|
49
50
|
result[attribute] = {
|
|
50
|
-
"old_value" => old_value,
|
|
51
|
-
"new_value" => new_value
|
|
51
|
+
"old_value" => mask(attribute, old_value),
|
|
52
|
+
"new_value" => mask(attribute, new_value)
|
|
52
53
|
}
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
|
|
57
|
+
# Valor nulo continua nulo: mostra que o campo estava vazio sem revelar nada.
|
|
58
|
+
def mask(attribute, value)
|
|
59
|
+
return value if value.nil? || !masked_attributes.include?(attribute)
|
|
60
|
+
|
|
61
|
+
configuration.mask_value
|
|
62
|
+
end
|
|
63
|
+
|
|
56
64
|
# Remove do snapshot os atributos que nao devem ser auditados.
|
|
57
65
|
def filtered_attributes
|
|
58
66
|
record.attributes.reject do |attribute, _value|
|
|
@@ -71,5 +79,9 @@ module AuditLogger
|
|
|
71
79
|
def ignored_attributes
|
|
72
80
|
@ignored_attributes ||= model_config.effective_ignored_attributes(configuration)
|
|
73
81
|
end
|
|
82
|
+
|
|
83
|
+
def masked_attributes
|
|
84
|
+
@masked_attributes ||= model_config.effective_masked_attributes(configuration)
|
|
85
|
+
end
|
|
74
86
|
end
|
|
75
87
|
end
|
|
@@ -10,6 +10,7 @@ module AuditLogger
|
|
|
10
10
|
uuid_resolver
|
|
11
11
|
ip_resolver
|
|
12
12
|
humanizer
|
|
13
|
+
error_handler
|
|
13
14
|
].freeze
|
|
14
15
|
|
|
15
16
|
def self.validate!(configuration)
|
|
@@ -26,6 +27,8 @@ module AuditLogger
|
|
|
26
27
|
validate_humanize_by_default!
|
|
27
28
|
validate_i18n_scopes!
|
|
28
29
|
validate_ignored_attributes!
|
|
30
|
+
validate_masked_attributes!
|
|
31
|
+
validate_booleans!
|
|
29
32
|
|
|
30
33
|
configuration
|
|
31
34
|
end
|
|
@@ -63,6 +66,25 @@ module AuditLogger
|
|
|
63
66
|
end
|
|
64
67
|
end
|
|
65
68
|
|
|
69
|
+
# Mesma regra dos ignorados: nomes de atributo como string ou symbol.
|
|
70
|
+
def validate_masked_attributes!
|
|
71
|
+
value = configuration.masked_attributes
|
|
72
|
+
|
|
73
|
+
unless value.is_a?(Array) && value.all? { |item| item.is_a?(String) || item.is_a?(Symbol) }
|
|
74
|
+
raise ConfigurationError,
|
|
75
|
+
"masked_attributes deve ser um Array de strings ou symbols"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Flags simples nao aceitam valores "truthy" ambiguos.
|
|
80
|
+
def validate_booleans!
|
|
81
|
+
%i[resolve_associations raise_errors].each do |field_name|
|
|
82
|
+
next if [true, false].include?(configuration.public_send(field_name))
|
|
83
|
+
|
|
84
|
+
raise ConfigurationError, "#{field_name} deve ser true ou false"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
66
88
|
# Mantem a comparacao de atributos ignorados previsivel.
|
|
67
89
|
def validate_ignored_attributes!
|
|
68
90
|
value = configuration.ignored_attributes
|
|
@@ -11,7 +11,12 @@ module AuditLogger
|
|
|
11
11
|
:humanize_by_default,
|
|
12
12
|
:i18n_scopes,
|
|
13
13
|
:ignored_attributes,
|
|
14
|
-
:
|
|
14
|
+
:masked_attributes,
|
|
15
|
+
:mask_value,
|
|
16
|
+
:humanizer,
|
|
17
|
+
:resolve_associations,
|
|
18
|
+
:raise_errors,
|
|
19
|
+
:error_handler
|
|
15
20
|
|
|
16
21
|
def initialize
|
|
17
22
|
reset!
|
|
@@ -28,7 +33,15 @@ module AuditLogger
|
|
|
28
33
|
self.humanize_by_default = true
|
|
29
34
|
self.i18n_scopes = default_i18n_scopes
|
|
30
35
|
self.ignored_attributes = default_ignored_attributes
|
|
36
|
+
# Atributos sensiveis: a alteracao e registrada, mas o valor vira mask_value.
|
|
37
|
+
self.masked_attributes = default_masked_attributes
|
|
38
|
+
self.mask_value = "[FILTRADO]"
|
|
31
39
|
self.humanizer = nil
|
|
40
|
+
# Troca o id de belongs_to pelo rotulo do registro associado no payload humanizado.
|
|
41
|
+
self.resolve_associations = true
|
|
42
|
+
# Por padrao a auditoria nunca derruba a operacao principal; nos testes vale ligar.
|
|
43
|
+
self.raise_errors = false
|
|
44
|
+
self.error_handler = nil
|
|
32
45
|
|
|
33
46
|
self
|
|
34
47
|
end
|
|
@@ -44,5 +57,10 @@ module AuditLogger
|
|
|
44
57
|
def default_ignored_attributes
|
|
45
58
|
%i[created_at updated_at lock_version]
|
|
46
59
|
end
|
|
60
|
+
|
|
61
|
+
# Senhas e tokens comuns em apps Rails (Devise, has_secure_password, tokens de API).
|
|
62
|
+
def default_masked_attributes
|
|
63
|
+
%i[password password_digest encrypted_password reset_password_token confirmation_token unlock_token]
|
|
64
|
+
end
|
|
47
65
|
end
|
|
48
66
|
end
|
|
@@ -65,6 +65,8 @@ module AuditLogger
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# Mantem o label traduzido e preserva os dados do payload bruto.
|
|
68
|
+
# Em belongs_to, acrescenta o rotulo do registro associado: "r_cargo_id: 3 -> 7" vira
|
|
69
|
+
# old_value_label "PROFESSOR" e new_value_label "ASSESSOR".
|
|
68
70
|
def default_field_payload(attribute, values)
|
|
69
71
|
result = { "label" => translate_attribute(attribute) }
|
|
70
72
|
|
|
@@ -72,7 +74,51 @@ module AuditLogger
|
|
|
72
74
|
result[key] = value
|
|
73
75
|
end
|
|
74
76
|
|
|
75
|
-
result
|
|
77
|
+
result.merge(association_labels(attribute, values))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
VALUE_KEYS = %w[old_value new_value value].freeze
|
|
81
|
+
|
|
82
|
+
def association_labels(attribute, values)
|
|
83
|
+
return {} unless configuration.resolve_associations
|
|
84
|
+
|
|
85
|
+
reflection = belongs_to_reflection(attribute)
|
|
86
|
+
return {} unless reflection
|
|
87
|
+
|
|
88
|
+
values.each_with_object({}) do |(key, value), labels|
|
|
89
|
+
next unless VALUE_KEYS.include?(key) && !value.nil?
|
|
90
|
+
|
|
91
|
+
label = association_label(reflection, value)
|
|
92
|
+
labels["#{key}_label"] = label unless label.nil?
|
|
93
|
+
end
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
log_warning(attribute, e)
|
|
96
|
+
{}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def belongs_to_reflection(attribute)
|
|
100
|
+
record.class.reflect_on_all_associations(:belongs_to).find do |reflection|
|
|
101
|
+
!reflection.polymorphic? && reflection.foreign_key.to_s == attribute.to_s
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Procura tambem registros excluidos (soft delete): o historico precisa do nome antigo.
|
|
106
|
+
def association_label(reflection, id)
|
|
107
|
+
klass = reflection.klass
|
|
108
|
+
scope = klass.respond_to?(:with_deleted) ? klass.with_deleted : klass.unscoped
|
|
109
|
+
associated = scope.find_by(reflection.association_primary_key => id)
|
|
110
|
+
associated && display_label(associated)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
LABEL_METHODS = %i[display_name descricao nome name title].freeze
|
|
114
|
+
|
|
115
|
+
# Prefere um to_s definido na propria model; depois metodos comuns de rotulo.
|
|
116
|
+
def display_label(associated)
|
|
117
|
+
owner = associated.class.instance_method(:to_s).owner
|
|
118
|
+
return associated.to_s if owner.is_a?(Class) && owner < ActiveRecord::Base
|
|
119
|
+
|
|
120
|
+
method_name = LABEL_METHODS.find { |name| associated.respond_to?(name) }
|
|
121
|
+
method_name && associated.public_send(method_name)&.to_s
|
|
76
122
|
end
|
|
77
123
|
|
|
78
124
|
# Faz o fallback de traducao por escopo ate encontrar um label valido.
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
module AuditLogger
|
|
4
4
|
class ModelConfig
|
|
5
5
|
# Guarda apenas a configuracao local da model, sem misturar com a global.
|
|
6
|
-
attr_reader :humanize, :i18n_scopes, :humanizer, :ignored_attributes
|
|
6
|
+
attr_reader :humanize, :i18n_scopes, :humanizer, :ignored_attributes, :masked_attributes, :owner
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# owner: nome da associacao (Symbol) ou Proc(record) que devolve o registro "dono" do log,
|
|
9
|
+
# para consultar num lugar so o historico de um agregado (ex.: pessoa + contratos).
|
|
10
|
+
# Sem owner, o dono e o proprio registro.
|
|
11
|
+
def initialize(humanize: nil, i18n_scopes: nil, humanizer: nil, ignored_attributes: [], masked_attributes: [],
|
|
12
|
+
owner: nil)
|
|
9
13
|
@humanize = humanize
|
|
10
14
|
@i18n_scopes = i18n_scopes
|
|
11
15
|
@humanizer = humanizer
|
|
12
16
|
@ignored_attributes = Array(ignored_attributes)
|
|
17
|
+
@masked_attributes = Array(masked_attributes)
|
|
18
|
+
@owner = owner
|
|
13
19
|
|
|
14
20
|
validate!
|
|
15
21
|
end
|
|
@@ -38,6 +44,11 @@ module AuditLogger
|
|
|
38
44
|
(global_configuration.ignored_attributes + ignored_attributes).map(&:to_s).uniq
|
|
39
45
|
end
|
|
40
46
|
|
|
47
|
+
# Combina masked attributes globais e locais em uma lista unica.
|
|
48
|
+
def effective_masked_attributes(global_configuration)
|
|
49
|
+
(global_configuration.masked_attributes + masked_attributes).map(&:to_s).uniq
|
|
50
|
+
end
|
|
51
|
+
|
|
41
52
|
private
|
|
42
53
|
|
|
43
54
|
# Concentra as validacoes do contrato aceito pela DSL `auditable`.
|
|
@@ -46,6 +57,8 @@ module AuditLogger
|
|
|
46
57
|
validate_i18n_scopes!
|
|
47
58
|
validate_humanizer!
|
|
48
59
|
validate_ignored_attributes!
|
|
60
|
+
validate_masked_attributes!
|
|
61
|
+
validate_owner!
|
|
49
62
|
end
|
|
50
63
|
|
|
51
64
|
# Limita o override de humanizacao a valores simples e previsiveis.
|
|
@@ -70,6 +83,18 @@ module AuditLogger
|
|
|
70
83
|
raise ConfigurationError, "humanizer da model deve ser um Proc/Lambda ou nil"
|
|
71
84
|
end
|
|
72
85
|
|
|
86
|
+
def validate_masked_attributes!
|
|
87
|
+
return if masked_attributes.all? { |item| item.is_a?(String) || item.is_a?(Symbol) }
|
|
88
|
+
|
|
89
|
+
raise ConfigurationError, "masked_attributes da model deve ser um Array de strings ou symbols"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def validate_owner!
|
|
93
|
+
return if owner.nil? || owner.is_a?(Symbol) || owner.is_a?(Proc)
|
|
94
|
+
|
|
95
|
+
raise ConfigurationError, "owner da model deve ser um Symbol (associacao), um Proc ou nil"
|
|
96
|
+
end
|
|
97
|
+
|
|
73
98
|
# Normaliza a entrada para facilitar o filtro de atributos depois.
|
|
74
99
|
def validate_ignored_attributes!
|
|
75
100
|
return if ignored_attributes.all? { |item| item.is_a?(String) || item.is_a?(Symbol) }
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AuditLogger
|
|
4
|
+
# Descobre o registro "dono" do log, para consultar o historico de um agregado num lugar so
|
|
5
|
+
# (ex.: alteracoes da pessoa, dos contratos e dos dependentes dela).
|
|
6
|
+
class OwnerResolver
|
|
7
|
+
def self.call(record, model_config)
|
|
8
|
+
new(record, model_config).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(record, model_config)
|
|
12
|
+
@record = record
|
|
13
|
+
@model_config = model_config
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Sem owner configurado, ou com associacao vazia, o dono e o proprio registro.
|
|
17
|
+
def call
|
|
18
|
+
attributes_for(resolve_owner || record)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Atributos owner_type/owner_id de um registro qualquer (tambem usado por record_event).
|
|
22
|
+
def self.attributes_for(owner)
|
|
23
|
+
return {} if owner.nil?
|
|
24
|
+
|
|
25
|
+
klass = owner.class
|
|
26
|
+
type = klass.respond_to?(:base_class) ? klass.base_class.name : klass.name
|
|
27
|
+
{ owner_type: type, owner_id: owner.id.to_s }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :record, :model_config
|
|
33
|
+
|
|
34
|
+
def resolve_owner
|
|
35
|
+
case model_config.owner
|
|
36
|
+
when Symbol then record.public_send(model_config.owner)
|
|
37
|
+
when Proc then model_config.owner.call(record)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def attributes_for(owner)
|
|
42
|
+
self.class.attributes_for(owner)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AuditLogger
|
|
4
|
+
# Grava uma linha em audit_logs com o contexto do ator (usuario, uuid, ip).
|
|
5
|
+
# Compartilhado pelo log automatico (auditable) e pelos eventos manuais (record_event).
|
|
6
|
+
module Persistence
|
|
7
|
+
def self.create!(model_class_name:, id_object:, action:, audited_changes:, audited_changes_humanize:, owner: {})
|
|
8
|
+
actor_context = ActorContextResolver.call(AuditLogger.configuration)
|
|
9
|
+
now = Time.current
|
|
10
|
+
attributes = {
|
|
11
|
+
model_class_name: model_class_name,
|
|
12
|
+
id_object: id_object.to_s,
|
|
13
|
+
action: action.to_s,
|
|
14
|
+
uuid: actor_context[:uuid].to_s,
|
|
15
|
+
changed_by_id: actor_context[:changed_by_id],
|
|
16
|
+
changed_by_type: actor_context[:changed_by_type],
|
|
17
|
+
changed_by_other: actor_context[:changed_by_other],
|
|
18
|
+
audited_changes: audited_changes,
|
|
19
|
+
audited_changes_humanize: audited_changes_humanize,
|
|
20
|
+
ip_remote: actor_context[:ip_remote],
|
|
21
|
+
created_at: now,
|
|
22
|
+
updated_at: now
|
|
23
|
+
}
|
|
24
|
+
# Instalacoes da 0.1.x ainda sem as colunas de dono continuam funcionando.
|
|
25
|
+
attributes.merge!(owner) if AuditLog.owner_columns?
|
|
26
|
+
|
|
27
|
+
AuditLog.create!(attributes)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/audit_logger/railtie.rb
CHANGED
|
@@ -14,7 +14,16 @@ module AuditLogger
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
# Orquestra toda a montagem do log e persiste o resultado final.
|
|
17
|
+
# Roda depois do commit: uma falha aqui nao pode virar erro da requisicao que ja gravou.
|
|
17
18
|
def call
|
|
19
|
+
AuditLogger.safely(record: record, action: action) { persist }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :record, :action, :configuration, :model_config
|
|
25
|
+
|
|
26
|
+
def persist
|
|
18
27
|
payload = ChangeExtractor.new(
|
|
19
28
|
record: record,
|
|
20
29
|
action: action,
|
|
@@ -24,8 +33,7 @@ module AuditLogger
|
|
|
24
33
|
|
|
25
34
|
return if payload.fetch("fields", {}).empty?
|
|
26
35
|
|
|
27
|
-
#
|
|
28
|
-
actor_context = ActorContextResolver.call(configuration)
|
|
36
|
+
# Gera a versao amigavel do payload antes de persistir.
|
|
29
37
|
humanized_payload = Humanizer.new(
|
|
30
38
|
record: record,
|
|
31
39
|
payload: payload,
|
|
@@ -33,29 +41,14 @@ module AuditLogger
|
|
|
33
41
|
configuration: configuration
|
|
34
42
|
).call
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
AuditLogger::Persistence.create!(
|
|
37
45
|
model_class_name: record.class.name,
|
|
38
|
-
id_object:
|
|
46
|
+
id_object: record.id.to_s,
|
|
39
47
|
action: action,
|
|
40
|
-
uuid: actor_context[:uuid].to_s,
|
|
41
|
-
changed_by_id: actor_context[:changed_by_id],
|
|
42
|
-
changed_by_type: actor_context[:changed_by_type],
|
|
43
|
-
changed_by_other: actor_context[:changed_by_other],
|
|
44
48
|
audited_changes: payload,
|
|
45
49
|
audited_changes_humanize: humanized_payload,
|
|
46
|
-
|
|
47
|
-
created_at: Time.current,
|
|
48
|
-
updated_at: Time.current
|
|
50
|
+
owner: OwnerResolver.call(record, model_config)
|
|
49
51
|
)
|
|
50
52
|
end
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
|
|
54
|
-
attr_reader :record, :action, :configuration, :model_config
|
|
55
|
-
|
|
56
|
-
# Normaliza o id como string para manter consistencia entre tipos de PK.
|
|
57
|
-
def resolve_record_id
|
|
58
|
-
record.id.to_s
|
|
59
|
-
end
|
|
60
53
|
end
|
|
61
54
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AuditLogger
|
|
4
|
+
# Evento registrado a mao, para o que nao passa pelos callbacks do model:
|
|
5
|
+
# pedido de reset de senha, login, alteracao em massa (update_all) etc.
|
|
6
|
+
class RecordEvent
|
|
7
|
+
def self.call(**options)
|
|
8
|
+
new(**options).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# fields usa o mesmo formato do log automatico: { "campo" => { "old_value" =>, "new_value" => } }
|
|
12
|
+
# ou { "campo" => { "value" => } }. metadata guarda contexto livre (origem, quantidade...).
|
|
13
|
+
def initialize(action:, record: nil, model_class_name: nil, id_object: nil, owner: nil, fields: {}, metadata: {})
|
|
14
|
+
@action = action.to_s
|
|
15
|
+
@record = record
|
|
16
|
+
@model_class_name = model_class_name
|
|
17
|
+
@id_object = id_object
|
|
18
|
+
@owner = owner
|
|
19
|
+
@fields = stringify(fields)
|
|
20
|
+
@metadata = metadata || {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call
|
|
24
|
+
AuditLogger.safely(record: record, action: action) { persist }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :action, :record, :model_class_name, :id_object, :owner, :fields, :metadata
|
|
30
|
+
|
|
31
|
+
def persist
|
|
32
|
+
payload = { "type" => action, "fields" => fields }
|
|
33
|
+
payload["metadata"] = stringify(metadata) unless metadata.empty?
|
|
34
|
+
|
|
35
|
+
AuditLogger::Persistence.create!(
|
|
36
|
+
model_class_name: model_class_name || record&.class&.name || "AuditLogger::Event",
|
|
37
|
+
id_object: id_object || record&.id,
|
|
38
|
+
action: action,
|
|
39
|
+
audited_changes: payload,
|
|
40
|
+
audited_changes_humanize: humanize(payload),
|
|
41
|
+
owner: owner_attributes
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Com registro, reaproveita a humanizacao (labels e associacoes) da model dele.
|
|
46
|
+
def humanize(payload)
|
|
47
|
+
return payload unless record
|
|
48
|
+
|
|
49
|
+
humanized = Humanizer.new(
|
|
50
|
+
record: record, payload: payload, model_config: model_config, configuration: AuditLogger.configuration
|
|
51
|
+
).call
|
|
52
|
+
humanized.merge(payload.slice("metadata"))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def owner_attributes
|
|
56
|
+
return OwnerResolver.attributes_for(owner) if owner
|
|
57
|
+
return OwnerResolver.call(record, model_config) if record
|
|
58
|
+
|
|
59
|
+
{}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def model_config
|
|
63
|
+
klass = record.class
|
|
64
|
+
klass.respond_to?(:audit_logger_model_config) ? klass.audit_logger_model_config : ModelConfig.new
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def stringify(hash)
|
|
68
|
+
hash.to_h.each_with_object({}) do |(key, value), result|
|
|
69
|
+
result[key.to_s] = value.is_a?(Hash) ? stringify(value) : value
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/audit_logger/version.rb
CHANGED
data/lib/audit_logger.rb
CHANGED
|
@@ -14,7 +14,10 @@ require_relative "audit_logger/audit_log"
|
|
|
14
14
|
require_relative "audit_logger/actor_context_resolver"
|
|
15
15
|
require_relative "audit_logger/change_extractor"
|
|
16
16
|
require_relative "audit_logger/humanizer"
|
|
17
|
+
require_relative "audit_logger/owner_resolver"
|
|
18
|
+
require_relative "audit_logger/persistence"
|
|
17
19
|
require_relative "audit_logger/record_audit_entry"
|
|
20
|
+
require_relative "audit_logger/record_event"
|
|
18
21
|
require_relative "audit_logger/auditable"
|
|
19
22
|
|
|
20
23
|
module AuditLogger
|
|
@@ -38,6 +41,38 @@ module AuditLogger
|
|
|
38
41
|
def reset_configuration!
|
|
39
42
|
@configuration = Configuration.new
|
|
40
43
|
end
|
|
44
|
+
|
|
45
|
+
# Registra um evento que nao passa pelos callbacks (reset de senha, login, update_all).
|
|
46
|
+
# Ver AuditLogger::RecordEvent para as opcoes.
|
|
47
|
+
def record_event(**options)
|
|
48
|
+
RecordEvent.call(**options)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A auditoria nunca derruba a operacao principal: registra o erro, chama o error_handler
|
|
52
|
+
# e segue. Com raise_errors = true (util em teste) o erro sobe.
|
|
53
|
+
def safely(record: nil, action: nil)
|
|
54
|
+
yield
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
raise if configuration.raise_errors
|
|
57
|
+
|
|
58
|
+
report_error(e, record, action)
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def report_error(error, record, action)
|
|
65
|
+
alvo = record ? " de #{record.class.name}##{record.id}" : ""
|
|
66
|
+
message = "[AuditLogger] Falha ao registrar #{action}#{alvo}: #{error.class} - #{error.message}"
|
|
67
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
68
|
+
Rails.logger.error(message)
|
|
69
|
+
else
|
|
70
|
+
warn(message)
|
|
71
|
+
end
|
|
72
|
+
configuration.error_handler&.call(error, record, action)
|
|
73
|
+
rescue StandardError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
41
76
|
end
|
|
42
77
|
end
|
|
43
78
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Upgrade da 0.1.x: colunas do registro "dono", para consultar o historico de um agregado.
|
|
4
|
+
class AddOwnerToAuditLogs < ActiveRecord::Migration<%= migration_version %>
|
|
5
|
+
def up
|
|
6
|
+
add_column :audit_logs, :owner_type, :string unless column_exists?(:audit_logs, :owner_type)
|
|
7
|
+
add_column :audit_logs, :owner_id, :string unless column_exists?(:audit_logs, :owner_id)
|
|
8
|
+
return if index_exists?(:audit_logs, [:owner_type, :owner_id, :created_at])
|
|
9
|
+
|
|
10
|
+
add_index :audit_logs, [:owner_type, :owner_id, :created_at]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def down
|
|
14
|
+
remove_index :audit_logs, [:owner_type, :owner_id, :created_at] if index_exists?(:audit_logs, [:owner_type, :owner_id, :created_at])
|
|
15
|
+
remove_column :audit_logs, :owner_id if column_exists?(:audit_logs, :owner_id)
|
|
16
|
+
remove_column :audit_logs, :owner_type if column_exists?(:audit_logs, :owner_type)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -42,6 +42,19 @@ AuditLogger.configure do |config|
|
|
|
42
42
|
# Campos que normalmente nao agregam valor na auditoria.
|
|
43
43
|
config.ignored_attributes = [:created_at, :updated_at, :lock_version]
|
|
44
44
|
|
|
45
|
+
# Campos sensiveis: a alteracao e registrada (ex.: "senha alterada"), mas o valor vira mask_value.
|
|
46
|
+
# Acrescente tokens proprios do projeto. Por model: auditable masked_attributes: [:token]
|
|
47
|
+
config.masked_attributes = [:password, :password_digest, :encrypted_password, :reset_password_token, :confirmation_token, :unlock_token]
|
|
48
|
+
config.mask_value = "[FILTRADO]"
|
|
49
|
+
|
|
50
|
+
# Em belongs_to, grava no payload humanizado o rotulo do registro associado (old_value_label/new_value_label).
|
|
51
|
+
config.resolve_associations = true
|
|
52
|
+
|
|
53
|
+
# A auditoria roda depois do commit e nunca derruba a requisicao: falha vira log de erro.
|
|
54
|
+
# Ligue raise_errors no ambiente de teste para enxergar problemas.
|
|
55
|
+
config.raise_errors = false
|
|
56
|
+
# config.error_handler = ->(error, record, action) { Sentry.capture_exception(error) }
|
|
57
|
+
|
|
45
58
|
# Humanizer opcional.
|
|
46
59
|
# Se retornar nil, a gem usa o fallback padrao baseado em I18n.
|
|
47
60
|
config.humanizer = ->(_model_klass, _attribute, _old_value, _new_value) { nil }
|
|
@@ -14,6 +14,8 @@ class CreateAuditLogs < ActiveRecord::Migration<%= migration_version %>
|
|
|
14
14
|
t.jsonb :audited_changes, null: false, default: {}
|
|
15
15
|
t.jsonb :audited_changes_humanize, null: false, default: {}
|
|
16
16
|
t.string :ip_remote
|
|
17
|
+
t.string :owner_type
|
|
18
|
+
t.string :owner_id
|
|
17
19
|
t.datetime :deleted_at
|
|
18
20
|
t.timestamps
|
|
19
21
|
end
|
|
@@ -21,6 +23,7 @@ class CreateAuditLogs < ActiveRecord::Migration<%= migration_version %>
|
|
|
21
23
|
add_index :audit_logs, [:model_class_name, :id_object]
|
|
22
24
|
add_index :audit_logs, :action
|
|
23
25
|
add_index :audit_logs, [:changed_by_type, :changed_by_id]
|
|
26
|
+
add_index :audit_logs, [:owner_type, :owner_id, :created_at]
|
|
24
27
|
add_index :audit_logs, :created_at
|
|
25
28
|
add_index :audit_logs, :deleted_at
|
|
26
29
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module AuditLogger
|
|
7
|
+
module Generators
|
|
8
|
+
# Para quem instalou a 0.1.x: cria a migration das colunas owner_type/owner_id.
|
|
9
|
+
class UpgradeGenerator < Rails::Generators::Base
|
|
10
|
+
include ActiveRecord::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
desc "Cria a migration que acrescenta owner_type/owner_id em audit_logs (upgrade da 0.1.x)."
|
|
14
|
+
|
|
15
|
+
def copy_migration
|
|
16
|
+
migration_template "add_owner_to_audit_logs.rb.tt", "db/migrate/add_owner_to_audit_logs.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def migration_version
|
|
22
|
+
"[#{ActiveRecord::VERSION::STRING.to_f}]"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.next_migration_number(path)
|
|
26
|
+
ActiveRecord::Generators::Base.next_migration_number(path)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|