Class: StandardAudit::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/standard_audit/configuration.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfiguration

Returns a new instance of Configuration.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/standard_audit/configuration.rb', line 17

def initialize
  @subscriptions = []
  @async = false
  @queue_name = :default
  @enabled = true

  @actor_extractor = ->(payload) { payload[:actor] }
  @target_extractor = ->(payload) { payload[:target] }
  @scope_extractor = ->(payload) { payload[:scope] }

  @current_actor_resolver = -> {
    defined?(Current) && Current.respond_to?(:user) ? Current.user : nil
  }
  @current_request_id_resolver = -> {
    defined?(Current) && Current.respond_to?(:request_id) ? Current.request_id : nil
  }
  @current_ip_address_resolver = -> {
    defined?(Current) && Current.respond_to?(:ip_address) ? Current.ip_address : nil
  }
  @current_user_agent_resolver = -> {
    defined?(Current) && Current.respond_to?(:user_agent) ? Current.user_agent : nil
  }
  @current_session_id_resolver = -> {
    defined?(Current) && Current.respond_to?(:session_id) ? Current.session_id : nil
  }

  # Note: :authorization filters the HTTP Authorization header value.
  # If you use "authorization" as a metadata key for policy decisions,
  # rename it (e.g. :authorization_policy) to avoid accidental filtering.
  @sensitive_keys = %i[
    password password_confirmation token secret
    api_key access_token refresh_token
    private_key certificate_chain
    ssn credit_card authorization
  ]
  # Regexps matched against every metadata key name, in addition to the
  # exact-match `sensitive_keys` list. This is the supported way to catch a
  # family of keys: `/secret/i` redacts `client_secret`, `webhook_secret`,
  # and `secret` alike.
  #
  # There is deliberately NO substring *mode* for `sensitive_keys` — see
  # the note in MetadataFilter. Patterns are opt-in and per-app, which is
  # the only safe shape for a rule applied to append-only rows.
  @sensitive_key_patterns = []

  # Key names (String/Symbol, exact) or Regexps that are never redacted,
  # even when `sensitive_keys` or `sensitive_key_patterns` matches. Lets an
  # app adopt a broad pattern while keeping the handful of real audit keys
  # it would otherwise swallow, e.g.
  # `sensitive_key_patterns = [/token/i]` with
  # `sensitive_key_exceptions = %i[input_tokens output_tokens]`.
  @sensitive_key_exceptions = []

  # When true, redaction descends into nested Hashes (and Hashes inside
  # Arrays), so `metadata: { stripe: { client_secret: … } }` is caught.
  # Defaults to false: it changes what gets written, and audit rows are
  # append-only. Reserved keys are never descended into.
  @filter_nested_metadata = false

  # When true (the default), an ActiveRecord object appearing anywhere in
  # audit metadata is replaced by a reference —
  # `{ "gid" => …, "type" => …, "id" => … }` — instead of being serialised
  # with all of its attributes. See StandardAudit::RecordReference.
  #
  # Defaults to the SAFE behaviour, unlike `filter_nested_metadata`,
  # because the values this catches are ones no host asked to record:
  # `password_digest`, `token_digest`, `lookup_hash` and PKCE verifiers
  # arriving as a side effect of a payload carrying `account:` or
  # `session:`. Audit rows are append-only, so an unsafe default cannot be
  # walked back — the rows already written keep whatever was in them.
  #
  # Set to false ONLY if an app genuinely depends on record attributes in
  # metadata and has satisfied itself that no secret-bearing column can
  # reach a row. Preferred alternative: keep this on and use
  # `metadata_builder` to pull the specific attributes you want, which runs
  # BEFORE dereferencing and still sees the record.
  @dereference_record_metadata = true

  @metadata_builder = nil

  # Callables (or Symbols naming an AuditLog instance method) run on
  # `before_create` AFTER the UUID is assigned and BEFORE the checksum is
  # computed. See Configuration#before_checksum.
  @before_checksum_hooks = []

  @anonymizable_metadata_keys = %i[email name ip_address]

  # ── StandardAudit::Operation (the operation-audit DSL) ────────────────

  # The host's canonical action vocabulary, used by `audit!` to reject an
  # action absent from it. Normally a CALLABLE, because referencing an
  # autoloadable constant eagerly from an initializer pins the first-loaded
  # copy and breaks Zeitwerk reloading:
  #
  #   config.audit_catalogue = -> { AuditCatalogue::ACTIONS }
  #
  # A plain Array is accepted when the vocabulary really is a frozen
  # literal. `nil` (the default) skips the membership check entirely, so
  # the DSL is adoptable before an app has a catalogue.
  #
  # Membership is the ONLY rule applied — see StandardAudit::Operation.
  @audit_catalogue = nil

  # Whether `audit!` verifies the declaration before writing. A callable
  # (or a plain boolean). Defaults to local environments only: in
  # production `audit!` just writes, because a developer's declaration
  # mistake must not 500 a user — the host's meta-spec is the real gate.
  @verify_audit_declarations = -> {
    defined?(Rails) && Rails.respond_to?(:env) && Rails.env.respond_to?(:local?) && Rails.env.local?
  }

  # Whether a failed audit *write* aborts the operation. `false` (report
  # and swallow) matches four of the five apps this DSL was extracted from.
  # Set `true` where an unaudited state change is itself a compliance
  # failure — one app deliberately does not rescue, and defaulting to
  # swallow would have silently downgraded that posture.
  #
  # StandardAudit::Operation::DeclarationError is NEVER governed by this;
  # it always propagates.
  @raise_on_audit_write_error = false

  # Optional callable invoked instead of the built-in logger/Rails.error
  # reporting when an audit write fails:
  #
  #   config.audit_write_error_handler =
  #     ->(error, action:, operation:) { ErrorReporting.notify(error, ...) }
  #
  # Runs before `raise_on_audit_write_error` is applied, so it sees every
  # write failure under either policy.
  @audit_write_error_handler = nil

  # The `Rails.error.report` context key naming the audit action, for the
  # built-in reporter. Exists because two apps independently overrode
  # `audit_write_error_handler` for nothing but this key: both tag every
  # OTHER audit-error report site with `audit_event:` (four sites in one
  # app, twelve across ten files in the other), so adopting the gem's name
  # left the operations layer as the only place forking the convention.
  #
  # That divergence fails silently and asymmetrically — a saved error-tracker
  # search grouped on the host's key keeps working and simply stops
  # containing operation write failures. Nothing errors, nothing goes red,
  # and the search still looks healthy.
  #
  # A whole handler to rename one key is a lot of ceremony, and a handler
  # written for that reason also silently opts out of every future
  # improvement to the built-in reporter. Default keeps existing behaviour.
  @audit_error_context_key = :audit_action

  # Retention defaults from ENV so it can be set per-environment without a
  # code change. Unset/blank/non-positive => nil (infinite retention, the
  # compliance-safe default that never auto-deletes). A host app can still
  # override with `config.retention_days = N` in its initializer.
  @retention_days = self.class.retention_days_from_env
end

Instance Attribute Details

#actor_extractorObject

Returns the value of attribute actor_extractor.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def actor_extractor
  @actor_extractor
end

#anonymizable_metadata_keysObject

Returns the value of attribute anonymizable_metadata_keys.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def 
  @anonymizable_metadata_keys
end

#asyncObject

Returns the value of attribute async.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def async
  @async
end

#audit_catalogueObject

Returns the value of attribute audit_catalogue.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def audit_catalogue
  @audit_catalogue
end

#audit_error_context_keyObject

Returns the value of attribute audit_error_context_key.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def audit_error_context_key
  @audit_error_context_key
end

#audit_write_error_handlerObject

Returns the value of attribute audit_write_error_handler.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def audit_write_error_handler
  @audit_write_error_handler
end

#before_checksum_hooksObject

Returns the value of attribute before_checksum_hooks.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def before_checksum_hooks
  @before_checksum_hooks
end

#current_actor_resolverObject

Returns the value of attribute current_actor_resolver.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def current_actor_resolver
  @current_actor_resolver
end

#current_ip_address_resolverObject

Returns the value of attribute current_ip_address_resolver.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def current_ip_address_resolver
  @current_ip_address_resolver
end

#current_request_id_resolverObject

Returns the value of attribute current_request_id_resolver.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def current_request_id_resolver
  @current_request_id_resolver
end

#current_session_id_resolverObject

Returns the value of attribute current_session_id_resolver.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def current_session_id_resolver
  @current_session_id_resolver
end

#current_user_agent_resolverObject

Returns the value of attribute current_user_agent_resolver.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def current_user_agent_resolver
  @current_user_agent_resolver
end

#dereference_record_metadataObject

Returns the value of attribute dereference_record_metadata.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def 
  @dereference_record_metadata
end

#enabledObject

Returns the value of attribute enabled.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def enabled
  @enabled
end

#filter_nested_metadataObject

Returns the value of attribute filter_nested_metadata.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def 
  @filter_nested_metadata
end

#metadata_builderObject

Returns the value of attribute metadata_builder.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def 
  @metadata_builder
end

#queue_nameObject

Returns the value of attribute queue_name.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def queue_name
  @queue_name
end

#raise_on_audit_write_errorObject

Returns the value of attribute raise_on_audit_write_error.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def raise_on_audit_write_error
  @raise_on_audit_write_error
end

#retention_daysObject

Returns the value of attribute retention_days.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def retention_days
  @retention_days
end

#scope_extractorObject

Returns the value of attribute scope_extractor.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def scope_extractor
  @scope_extractor
end

#sensitive_key_exceptionsObject

Returns the value of attribute sensitive_key_exceptions.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def sensitive_key_exceptions
  @sensitive_key_exceptions
end

#sensitive_key_patternsObject

Returns the value of attribute sensitive_key_patterns.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def sensitive_key_patterns
  @sensitive_key_patterns
end

#sensitive_keysObject

Returns the value of attribute sensitive_keys.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def sensitive_keys
  @sensitive_keys
end

#target_extractorObject

Returns the value of attribute target_extractor.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def target_extractor
  @target_extractor
end

#verify_audit_declarationsObject

Returns the value of attribute verify_audit_declarations.



3
4
5
# File 'lib/standard_audit/configuration.rb', line 3

def verify_audit_declarations
  @verify_audit_declarations
end

Class Method Details

.retention_days_from_envObject

Parses STANDARD_AUDIT_RETENTION_DAYS into a positive Integer, or nil when unset/blank/zero/negative/non-numeric (=> infinite retention).



174
175
176
177
178
179
180
# File 'lib/standard_audit/configuration.rb', line 174

def self.retention_days_from_env
  raw = ENV["STANDARD_AUDIT_RETENTION_DAYS"]
  return nil if raw.nil? || raw.strip.empty?

  days = Integer(raw, exception: false)
  days&.positive? ? days : nil
end

Instance Method Details

#before_checksum(hook = nil, &block) ⇒ Object

Registers a hook to run between assign_uuid and compute_checksum on every audit write that instantiates a model.

config.before_checksum { |log| log.scope = derive_scope(log) }
config.before_checksum :backfill_organization_scope

A hook may set a CHECKSUM_FIELDS member (scope_gid, metadata, …) and the row will still verify, because the checksum is computed afterwards. That is the whole point: before this existed, hosts had to register their own before_create ..., prepend: true to beat the gem's checksum callback, which is fragile ordering knowledge no host should need.

Hooks accumulate and run in registration order. Each is rescued individually — a failing hook logs and is skipped; it never fails the audit write.

A Symbol/String is sent to the AuditLog instance (use this for methods supplied by a concern mixed into the model). A callable is passed the instance.

NOTE: batched writes (StandardAudit.batch { … }insert_all!) never instantiate a model, so hooks do not run there. A batched writer that needs a derived column has to set it on the buffered attrs.

Raises:

  • (ArgumentError)


205
206
207
208
209
210
211
# File 'lib/standard_audit/configuration.rb', line 205

def before_checksum(hook = nil, &block)
  hook ||= block
  raise ArgumentError, "before_checksum needs a callable, a Symbol, or a block" if hook.nil?

  @before_checksum_hooks << hook
  hook
end

#subscribe_to(pattern) ⇒ Object



213
214
215
# File 'lib/standard_audit/configuration.rb', line 213

def subscribe_to(pattern)
  @subscriptions << pattern
end

#subscriptionsObject



217
218
219
# File 'lib/standard_audit/configuration.rb', line 217

def subscriptions
  @subscriptions.dup.freeze
end