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
- #_build_instance_filter(action_instance) ⇒ Object
-
#_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.
-
#_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. -
#_mask_unfilterable_shape_value(field, value, action_instance) ⇒ Object
Single-field entry — inspect renders one field at a time.
-
#_mask_unfilterable_shapes(data, shape_paths, action_instance) ⇒ Object
Per-element
sensitive:redaction works by adding the member's key name to anActiveSupport::ParameterFilter, which redacts Hash keys at any depth — so a member inside a Hash (or an Array of Hashes) is filtered precisely, siblings preserved. -
#_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. -
#_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. -
#_sensitive_member_names(config, action_instance) ⇒ Object
The names of the
sensitive:members reachable inside one shape-bearing config's value — the flat key setinspecthands to aParameterFilter, since a member redacts by NAME wherever it appears (every array element, any nesting depth), unlike the precise wire path a subfield contributes. - #inspection_filter ⇒ Object
- #sensitive_fields ⇒ Object
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, 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.
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_filter ⇒ Object
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_fields ⇒ Object
87 88 89 |
# File 'lib/axn/core/contract/redaction.rb', line 87 def sensitive_fields _static_sensitive_fields end |