Module: Axn::Core::Contract::Redaction

Included in:
ClassMethods
Defined in:
lib/axn/core/contract/redaction.rb

Constant Summary collapse

SENSITIVE_FILTERED_MASK =

The mask a sensitive value is replaced with — matches ActiveSupport::ParameterFilter's default so wholesale-masked values read identically to per-key-filtered ones.

"[FILTERED]"

Instance Method Summary collapse

Instance Method Details

#_build_instance_filter(action_instance) ⇒ Object



213
214
215
# File 'lib/axn/core/contract/redaction.rb', line 213

def _build_instance_filter(action_instance)
  ActiveSupport::ParameterFilter.new(_resolve_sensitive_fields(action_instance))
end

#_context_slice(data:, direction: nil, action_instance: nil) ⇒ Object

Internal method for filtering context data by direction Used by instance methods (inputs_for_logging, outputs_for_logging) and async exception reporting When action_instance is provided, dynamic sensitive fields are resolved against that instance.



220
221
222
223
224
225
226
227
228
# File 'lib/axn/core/contract/redaction.rb', line 220

def _context_slice(data:, direction: nil, action_instance: nil)
  filter = if action_instance && _has_dynamic_sensitive_fields?
             _build_instance_filter(action_instance)
           else
             inspection_filter
           end
  sliced = _mask_unfilterable_shapes(data.slice(*_declared_fields(direction)), _sensitive_shape_paths(action_instance), action_instance)
  _filter_tolerating_cycles(filter, sliced)
end

#_has_dynamic_sensitive_fields?Boolean

Whether resolving this contract's sensitive: needs an action instance — and therefore the ONE question every reuse decision below turns on. It can be one question because the value space is closed at declaration (see Contract.validate_sensitive!): over true/false/nil/Symbol/Proc, "carries a Proc or Symbol" and "would resolve differently with an instance than without" are the same set. A second, stricter predicate existed here to cover a truthy value that is neither — which the instanceless path counted as not-sensitive while the per-instance path truthiness-tested it as sensitive, observably so for a non-Hash shaped value — and rejecting that value at declaration is what removes the divergence instead of guarding it.

false is one of the two answers, so the memo is guarded on nil rather than written with ||=: an ||= here re-ran the whole candidate walk on every logged call for precisely the contracts that have no dynamic sensitive: — which is nearly all of them.

Returns:

  • (Boolean)


147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/axn/core/contract/redaction.rb', line 147

def _has_dynamic_sensitive_fields?
  memo = _contract_redaction
  return memo.dynamic unless memo.dynamic.nil?

  memo.dynamic = _sensitive_candidate_configs.any? do |config|
    # `case`/`when` consults the real class, so a caller-supplied member's value cannot decide whether
    # it needs an instance — the same non-dispatching test the declaration guard uses.
    case _config_sensitive(config)
    when ::Proc, ::Symbol then true
    else false
    end
  end
end

#_mask_unfilterable_shape_value(field, value, action_instance) ⇒ Object

Single-field entry — inspect renders one field at a time. Reuses the whole-hash pass on a one-key hash so a subfield shape rooted under field is masked at its nested position too.



264
265
266
# File 'lib/axn/core/contract/redaction.rb', line 264

def _mask_unfilterable_shape_value(field, value, action_instance)
  _mask_unfilterable_shapes({ field => value }, _sensitive_shape_paths(action_instance), action_instance)[field]
end

#_mask_unfilterable_shapes(data, shape_paths, action_instance) ⇒ Object

Per-element sensitive: redaction works by adding the member's key name to an ActiveSupport::ParameterFilter, which redacts Hash keys at any depth — so a member inside a Hash (or an Array of Hashes) is filtered precisely, siblings preserved. But the filter only descends into Hashes: an object-backed shape value (a Data/Struct/PORO), or a malformed non-Hash value where a Hash was expected (which reaches logging before inbound validation can reject it), is opaque to it and would print whole. So for every field/subfield whose shape carries a sensitive member, this walks to the shaped value and replaces a non-Hash value in a member-bearing position with the mask — over-redacting the whole value (its non-sensitive siblings included) rather than risk leaking the secret. Applied to logs, exception context, and inspect.



254
255
256
257
258
259
260
# File 'lib/axn/core/contract/redaction.rb', line 254

def _mask_unfilterable_shapes(data, shape_paths, action_instance)
  return data unless data.is_a?(Hash)

  shape_paths.reduce(data) do |acc, (wire_path, shape)|
    _mask_value_at_path(acc, wire_path, shape, action_instance)
  end
end

#_resolve_sensitive_value(sensitive, action_instance) ⇒ Object

The first three arms are the whole declared grammar (see Contract.validate_sensitive!); !! is what turns a predicate's arbitrary return value into a decision. The else is now reachable only by nil — which _config_sensitive already normalizes to false on every path that reads a config — and is kept fail-SAFE rather than dropped: were an out-of-grammar value ever to arrive here, redacting a truthy one is the direction that cannot leak. This is the branch whose truthiness test used to disagree with the instanceless path about sensitive: "yes", and closing the value space is what removed the disagreement rather than a second predicate guarding it.



200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/axn/core/contract/redaction.rb', line 200

def _resolve_sensitive_value(sensitive, action_instance)
  case sensitive
  when true, false
    sensitive
  when Symbol
    !!action_instance.send(sensitive)
  when Proc
    !!action_instance.instance_exec(&sensitive)
  else
    !!sensitive
  end
end

#_sensitive_ambient_shape_paths(action_instance) ⇒ Object

The ambient analog of _sensitive_shape_paths: [(wire_path_within_ambient, shape)] for every ambient subfield whose shape carries a sensitive member. Ambient shapes are leaf nodes (_check_ambient_shape_placement!), so their value is copied whole and may be a non-Hash the ParameterFilter can't descend into — this feeds _mask_unfilterable_shapes to mask it. The ambient tree's wire paths are rooted at the synthetic :ambient_context segment; drop it, since the mask applies to the ambient VALUE the reader returns, not a hash wrapped under an :ambient_context key.



305
306
307
308
309
310
# File 'lib/axn/core/contract/redaction.rb', line 305

def _sensitive_ambient_shape_paths(action_instance)
  return _derive_sensitive_ambient_shape_paths(action_instance) if _has_dynamic_sensitive_fields?

  memo = _contract_redaction
  memo.static_ambient_shape_paths ||= _derive_sensitive_ambient_shape_paths(nil)
end

#_sensitive_member_names(config, action_instance) ⇒ Object

The names of the sensitive: members reachable inside one shape-bearing config's value — the flat key set inspect hands to a ParameterFilter, since a member redacts by NAME wherever it appears (every array element, any nesting depth), unlike the precise wire path a subfield contributes. Memoized on the same condition as _sensitive_shape_paths — no sensitive: resolving against the action — since inspect asks it once per displayed field, and the walk covers the whole stored graph.

Lives here rather than with the inspector because it is a question about the CONTRACT, and because sensitivity is read through _config_sensitive — the same seam logging reads — so a member that defines sensitive: cannot escape redaction in inspect by denying the reader while logging redacts it anyway.



498
499
500
501
502
# File 'lib/axn/core/contract/redaction.rb', line 498

def _sensitive_member_names(config, action_instance)
  return _derive_sensitive_member_names(config, action_instance) if _has_dynamic_sensitive_fields?

  _contract_redaction.member_names_for(config) { _derive_sensitive_member_names(config, nil) }
end

#inspection_filterObject



82
83
84
85
# File 'lib/axn/core/contract/redaction.rb', line 82

def inspection_filter
  memo = _contract_redaction
  memo.filter ||= ActiveSupport::ParameterFilter.new(sensitive_fields)
end

#sensitive_fieldsObject



87
88
89
# File 'lib/axn/core/contract/redaction.rb', line 87

def sensitive_fields
  _static_sensitive_fields
end