Skip to content

DC-394: honor per-case-type address+view IdProofingRequirements when masking household data#301

Open
ShaynaCummings wants to merge 1 commit into
mainfrom
fix/dc-394
Open

DC-394: honor per-case-type address+view IdProofingRequirements when masking household data#301
ShaynaCummings wants to merge 1 commit into
mainfrom
fix/dc-394

Conversation

@ShaynaCummings
Copy link
Copy Markdown
Member

@ShaynaCummings ShaynaCummings commented May 11, 2026

🔗 Jira ticket

https://codeforamerica.atlassian.net/browse/DC-394

✍️ Description

PII visibility for address/email/phone was resolved against an empty case list every time, which is correct only for uniform requirements. For per-case-type IdProofingRequirements (e.g., the granular address+view form CO plans to adopt when co-loading begins in 2027) it silently degraded to IAL1 for every request — IalRequirement.Resolve([]) short-circuits to IAL1 ("no cases = no case-derived reason to require elevated IAL"), so the per-case-type "highest wins" rule was never applied. household+view IdProofingRequirements didn't have this gap because its evaluator already received the real cases; this change brings PII visibility into the same shape.

Changes:

  • Add IPiiVisibilityService.GetVisibility(UserIalLevel, IReadOnlyList<SummerEbtCase>) overload; existing case-less overload delegates by passing [] so callers without case context keep their prior behavior.
  • IdProofingService.EvaluateView now takes cases and passes them to IalRequirement.Resolve, so per-case-type view requirements actually resolve against the user's cases.
  • Extract masking from HouseholdRepository.ApplyPiiVisibility into a shared HouseholdPiiFilter.Apply utility (Core.Utilities) so the use-case layer can re-apply visibility after fetching.
  • GetHouseholdDataQueryHandler now fetches with full PII, runs the existing household+view check against real cases, then resolves case-aware PII visibility and applies masking before returning. The visibility decision now happens once, against the data we're actually about to return.

Out of scope (worth a follow-up if we want full symmetry):

  • UpdateAddressCommandHandler still uses the case-less overload. It doesn't expose household PII back to the client, so its visibility decision is functionally inert.
  • MockHouseholdRepository.CreateCopy duplicates the masking logic alongside its defensive deep copy; left untouched here to keep the diff scoped to the bug.

Tests:

  • New GetVisibility_PerCaseTypeAddressView_ResolvesAgainstActualCases documents the gap (was a compile-error red before the overload was added; green now).
  • Handle_WhenIdentifierResolvedAndHouseholdExistsButNotIdVerified_ReturnsSuccessWithoutAddress reworked to provide a real address and assert it gets masked to "****" through the new code path, instead of relying on the test's pre-null AddressOnFile to make masking a no-op.
  • Default mocks added in GetHouseholdDataQueryHandlerTests and HouseholdControllerTests so existing tests pick up full PII from the new overload without per-test wiring.
  • A few Assert.Same checks against the repository's returned instance replaced with behavior-focused assertions, since the handler now produces a new record instance via with.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

✅ Completion tasks

  • Added relevant tests
  • Meets acceptance criteria in ticket
  • Configuration changes:
    • If new environment variables are added, update in Tofu
    • If new environment secrets are added, update in Tofu and set in AWS Secret Manager
    • [] If you're adding an appsetting, add it to the requisite example file, and update it in the AWS AppConfig
    • If appsetttings are changed, update in AWS AppConfig

…usehold data

PII visibility for address/email/phone was resolved against an empty case
list every time, which is correct only for uniform requirements. For
per-case-type IdProofingRequirements (e.g., the granular `address+view` form CO
plans to adopt when co-loading begins in 2027) it silently degraded to
IAL1 for every request — `IalRequirement.Resolve([])` short-circuits to
IAL1 ("no cases = no case-derived reason to require elevated IAL"), so
the per-case-type "highest wins" rule was never applied. `household+view`
didn't have this gap because its evaluator already received the real
cases; this change brings PII visibility into the same shape.

Changes:
- Add `IPiiVisibilityService.GetVisibility(UserIalLevel, IReadOnlyList<SummerEbtCase>)`
  overload; existing case-less overload delegates by passing `[]` so
  callers without case context keep their prior behavior.
- `IdProofingService.EvaluateView` now takes cases and passes them to
  `IalRequirement.Resolve`, so per-case-type view requirements actually
  resolve against the user's cases.
- Extract masking from `HouseholdRepository.ApplyPiiVisibility` into a
  shared `HouseholdPiiFilter.Apply` utility (Core.Utilities) so the
  use-case layer can re-apply visibility after fetching.
- `GetHouseholdDataQueryHandler` now fetches with full PII, runs the
  existing `household+view` check against real cases, then resolves
  case-aware PII visibility and applies masking before returning. The
  visibility decision now happens once, against the data we're actually
  about to return.

Out of scope (worth a follow-up if we want full symmetry):
- `UpdateAddressCommandHandler` still uses the case-less overload. It
  doesn't expose household PII back to the client, so its visibility
  decision is functionally inert.
- `MockHouseholdRepository.CreateCopy` duplicates the masking logic
  alongside its defensive deep copy; left untouched here to keep the
  diff scoped to the bug.

Tests:
- New `GetVisibility_PerCaseTypeAddressView_ResolvesAgainstActualCases`
  documents the gap (was a compile-error red before the overload was
  added; green now).
- `Handle_WhenIdentifierResolvedAndHouseholdExistsButNotIdVerified_ReturnsSuccessWithoutAddress`
  reworked to provide a real address and assert it gets masked to "****"
  through the new code path, instead of relying on the test's pre-null
  AddressOnFile to make masking a no-op.
- Default mocks added in `GetHouseholdDataQueryHandlerTests` and
  `HouseholdControllerTests` so existing tests pick up full PII from the
  new overload without per-test wiring.
- A few `Assert.Same` checks against the repository's returned instance
  replaced with behavior-focused assertions, since the handler now
  produces a new record instance via `with`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ShaynaCummings ShaynaCummings changed the title DC-394: honor per-case-type address+view requirements when masking household data DC-394: honor per-case-type address+view IdProofingRequirements when masking household data May 11, 2026
@mtodd-cfa mtodd-cfa marked this pull request as ready for review May 14, 2026 17:09
// can be resolved against the household's actual cases below. The
// case-aware visibility computed after the fetch decides what's masked
// before returning to the caller.
var fullPiiVisibility = new PiiVisibility(IncludeAddress: true, IncludeEmail: true, IncludePhone: true);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small/nit sort of thing, but this feels like a special case deserving of a singleton instance or a factory (i.e., PiiVisibility.Full or PiiVisibility.Full())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants