Skip to content

bugfix: ensure to mask SSN numbers - #1822

Merged
framitdavid merged 6 commits into
mainfrom
feat/mask-ssn-numbers
Jun 19, 2026
Merged

bugfix: ensure to mask SSN numbers#1822
framitdavid merged 6 commits into
mainfrom
feat/mask-ssn-numbers

Conversation

@framitdavid

@framitdavid framitdavid commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Description

What

Masks Norwegian national identity numbers (SSNs) in the party data returned to the frontend, so the full number is never exposed in HTTP responses (e.g. 01039012345010390*****).

Why

Several endpoints returned full Party objects — including Party.SSN and Party.Person.SSN — in the clear. This reduces unnecessary exposure of personal identifiers to the browser while keeping the birth-date portion (DDMMYY) visible for display purposes.

Endpoints covered

Endpoint Verb What is masked
/{org}/{app}/api/v1/parties GET Each Party in the list (incl. Person + ChildParties)
/{org}/{app}/api/v1/parties/validateInstantiation POST ValidParties in the result
/{org}/{app}/api/authorization/parties/current?returnPartyObject=true GET The returned Party

How

  • The masking rule lives in the domain layer next to the existing national-identity-number helpers: NationalIdentityNumberExtensions.Mask(string?) (+ ToMaskedString() for an already-validated NationalIdentityNumber). One source of truth, reusable elsewhere (logging
    etc.).
  • PartySsnMasking (API layer) returns masked copies of the party graph (PartyPersonChildParties). It copies every field generically via reflection and only transforms the SSN, so new fields on the platform model are carried over automatically.

Testing

  • Unit tests for the masking rule, including a case proving a checksum-invalid number is still masked.
  • Unit tests for the party-graph cloning (Party / Person / ChildParties masked, non-SSN fields preserved, source not mutated, org parties untouched).
  • HTTP-level test for GET api/authorization/parties/current asserting a masked ssn in the response.
  • Full solution test suite green locally.

Related Issue(s)

  • #{issue number}

Verification

  • Your code builds clean without any errors or warnings
  • Manual testing done (required)
  • Relevant automated test added (if you find this hard, leave it and we'll help out)
  • All tests run green

Documentation

  • User documentation is updated with a separate linked PR in altinn-studio-docs. (if applicable)

Summary by CodeRabbit

Summary by CodeRabbit

  • Bug Fixes

    • SSNs returned in party-related and user profile API responses are now partially masked (only the first 6 characters shown; the rest replaced with asterisks), including nested person and child party information.
  • New Features

    • Added support for consistent national identity number masking across responses.
  • Tests

    • Expanded automated tests to verify masking behavior and that original data is not mutated, including edge cases.

@framitdavid framitdavid changed the title ensure to mask ssn feat: ensure to mask SSN numbers Jun 19, 2026
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds SSN masking infrastructure to preserve national identity number privacy in Party API responses. Introduces NationalIdentityNumberExtensions.Mask and ToMaskedString to mask strings by showing the first 6 characters and replacing the remainder with asterisks. Implements PartySsnMasking helper that clones Party, Person, and UserProfile objects via reflection-based property copying while selectively masking all SSN-bearing fields and nested party hierarchies. Applies masking across AuthorizationController.GetCurrentParty, PartiesController.Get and ValidateInstantiation, and ProfileController.GetUser for all authentication contexts.

Changes

Party SSN masking across API responses

Layer / File(s) Summary
SSN string masking primitives
src/Altinn.App.Core/Extensions/NationalIdentityNumberExtensions.cs, test/Altinn.App.Core.Tests/Extensions/NationalIdentityNumberExtensionsTest.cs
Adds VisibleDigits constant (6 leading characters), Mask(string?) helper that preserves first 6 characters and replaces remainder with *, and ToMaskedString(NationalIdentityNumber) extension. Tests verify masking for standard/short/empty/null inputs, checksum-invalid numbers, and valid identity numbers.
Party cloning and SSN masking helper
src/Altinn.App.Api/Helpers/PartySsnMasking.cs, test/Altinn.App.Api.Tests/Helpers/PartySsnMaskingTests.cs
Implements MaskUserProfile, MaskParties, and MaskParty methods that clone Party/Person/UserProfile objects using reflection-based property copying while masking SSN-bearing fields at all nesting levels (top-level, nested Person, recursive ChildParties). Includes reflection utilities with cached property lists. Tests verify masking across nested objects, non-mutation, preservation of non-SSN fields, and correctness in all authentication contexts.
Controller integration
src/Altinn.App.Api/Controllers/AuthorizationController.cs, src/Altinn.App.Api/Controllers/PartiesController.cs, src/Altinn.App.Api/Controllers/ProfileController.cs, test/Altinn.App.Api.Tests/Controllers/ProfileControllerTests.User.verified.txt
Applies PartySsnMasking.MaskParty/MaskParties/MaskUserProfile to all party-returning responses in AuthorizationController.GetCurrentParty (when returnPartyObject is true), PartiesController.Get and ValidateInstantiation (user, org, system-user branches), and ProfileController.GetUser across all authentication contexts. Verified fixture updated to reflect masked SSN format.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'bugfix: ensure to mask SSN numbers' directly summarizes the main change: implementing SSN masking across party data endpoints.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mask-ssn-numbers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/Altinn.App.Api/Helpers/PartySsnMasking.cs Fixed
@danielskovli

Copy link
Copy Markdown
Contributor

We have a struct type for national identity numbers here, if that's relevant: https://github.com/Altinn/app-lib-dotnet/blob/main/src/Altinn.App.Core/Models/NationalIdentityNumber.cs

Associated extensions: https://github.com/Altinn/app-lib-dotnet/blob/main/src/Altinn.App.Core/Extensions/NationalIdentityNumberExtensions.cs

Food for thought 🥫 🧠

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/Altinn.App.Api/Helpers/PartySsnMasking.cs (1)

26-34: ⚡ Quick win

Pre-size maskedParties to avoid avoidable reallocations.

MaskParties can cheaply initialize capacity when the source is a collection.

♻️ Proposed change
 public static List<Party> MaskParties(IEnumerable<Party> parties)
 {
-    List<Party> maskedParties = new List<Party>();
+    List<Party> maskedParties = parties is ICollection<Party> collection
+        ? new List<Party>(collection.Count)
+        : new List<Party>();
     foreach (Party party in parties)
     {
         maskedParties.Add(MaskParty(party));
     }

     return maskedParties;
 }
As per coding guidelines, `**/*.cs`: "Write efficient code - don't allocate unnecessarily (e.g., avoid calling ToString twice, prefer for loops over LINQ when appropriate)".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.App.Api/Helpers/PartySsnMasking.cs` around lines 26 - 34, The
MaskParties method creates a List without pre-sizing its capacity, causing
unnecessary reallocations as items are added during iteration. To fix this,
check if the input parameter `parties` can be cast to ICollection<Party> (or use
a similar approach to determine the count efficiently), and then initialize the
`maskedParties` list with that capacity by passing the count to the List
constructor. This ensures the list has sufficient capacity from the start and
eliminates avoidable memory reallocations as MaskParty results are added in the
foreach loop.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Altinn.App.Api/Helpers/PartySsnMasking.cs`:
- Around line 50-57: Replace the blanket property cloning approach using
CopyProperties with explicit allowlisted field mapping in the Party cloning
logic (around line 50-57) and in any related Person/ChildParty masking methods
(around lines 117-136). Instead of copying all writable properties and then
selectively masking sensitive ones, explicitly map only the non-sensitive fields
that are safe to expose in the API response, ensuring that any future sensitive
fields added to the Party, Person, or related models will require explicit code
changes to include them, providing compile-time safety against PII leaks.

---

Nitpick comments:
In `@src/Altinn.App.Api/Helpers/PartySsnMasking.cs`:
- Around line 26-34: The MaskParties method creates a List without pre-sizing
its capacity, causing unnecessary reallocations as items are added during
iteration. To fix this, check if the input parameter `parties` can be cast to
ICollection<Party> (or use a similar approach to determine the count
efficiently), and then initialize the `maskedParties` list with that capacity by
passing the count to the List constructor. This ensures the list has sufficient
capacity from the start and eliminates avoidable memory reallocations as
MaskParty results are added in the foreach loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44bd1d04-0c3d-4a21-a06b-34625025e264

📥 Commits

Reviewing files that changed from the base of the PR and between 79ad1a0 and 70def4b.

📒 Files selected for processing (4)
  • src/Altinn.App.Api/Controllers/AuthorizationController.cs
  • src/Altinn.App.Api/Controllers/PartiesController.cs
  • src/Altinn.App.Api/Helpers/PartySsnMasking.cs
  • test/Altinn.App.Api.Tests/Helpers/PartySsnMaskingTests.cs

Comment thread src/Altinn.App.Api/Helpers/PartySsnMasking.cs
@framitdavid

Copy link
Copy Markdown
Contributor Author

Test fra localtest:
image

…o use Where'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@framitdavid framitdavid added the feature Label Pull requests with new features. Used when generation releasenotes label Jun 19, 2026
@framitdavid framitdavid changed the title feat: ensure to mask SSN numbers bugfix: ensure to mask SSN numbers Jun 19, 2026
@framitdavid framitdavid added bugfix Label Pull requests with bugfix. Used when generation releasenotes and removed feature Label Pull requests with new features. Used when generation releasenotes labels Jun 19, 2026
@framitdavid framitdavid moved this to 🔎 In review in Team Altinn Studio Jun 19, 2026
@framitdavid framitdavid added the squad/utforming Issues that belongs to the named squad. label Jun 19, 2026
@framitdavid

Copy link
Copy Markdown
Contributor Author

/publish

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

PR release:

⚙️ Building...
✅ Done!

@danielskovli danielskovli left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀

@framitdavid

Copy link
Copy Markdown
Contributor Author

/publish

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

PR release:

⚙️ Building...
✅ Done!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Altinn.App.Api/Controllers/ProfileController.cs (1)

30-42: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Use an explicit ...Response DTO for GET /profile/user.

This action still returns UserProfile directly from a controller endpoint. Please map the masked profile into an API-owned response DTO to keep the HTTP contract decoupled and compliant.

Suggested direction
-[ProducesResponseType(typeof(UserProfile), StatusCodes.Status200OK)]
+[ProducesResponseType(typeof(GetUserResponse), StatusCodes.Status200OK)]
...
- return Ok(PartySsnMasking.MaskUserProfile(details.Profile));
+ var maskedProfile = PartySsnMasking.MaskUserProfile(details.Profile);
+ return Ok(GetUserResponse.From(maskedProfile));

As per coding guidelines, **/src/Altinn.App.Api/**/*.cs: For HTTP APIs, provide ...Request and ...Response DTOs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.App.Api/Controllers/ProfileController.cs` around lines 30 - 42,
The GetUser action in ProfileController returns UserProfile directly instead of
using an explicit response DTO. Create an API-owned response DTO (such as
UserProfileResponse) to represent the HTTP contract, then modify the GetUser
method to map the masked profile from
PartySsnMasking.MaskUserProfile(details.Profile) into this new DTO before
returning it with Ok(). This ensures the HTTP API contract remains decoupled
from internal domain models as per the coding guidelines for src/Altinn.App.Api.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/Altinn.App.Api/Controllers/ProfileController.cs`:
- Around line 30-42: The GetUser action in ProfileController returns UserProfile
directly instead of using an explicit response DTO. Create an API-owned response
DTO (such as UserProfileResponse) to represent the HTTP contract, then modify
the GetUser method to map the masked profile from
PartySsnMasking.MaskUserProfile(details.Profile) into this new DTO before
returning it with Ok(). This ensures the HTTP API contract remains decoupled
from internal domain models as per the coding guidelines for src/Altinn.App.Api.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b7ae78f7-d200-4284-ae06-c26de74b3a8e

📥 Commits

Reviewing files that changed from the base of the PR and between 70def4b and 63cd464.

📒 Files selected for processing (6)
  • src/Altinn.App.Api/Controllers/ProfileController.cs
  • src/Altinn.App.Api/Helpers/PartySsnMasking.cs
  • src/Altinn.App.Core/Extensions/NationalIdentityNumberExtensions.cs
  • test/Altinn.App.Api.Tests/Controllers/ProfileControllerTests.User.verified.txt
  • test/Altinn.App.Api.Tests/Helpers/PartySsnMaskingTests.cs
  • test/Altinn.App.Core.Tests/Extensions/NationalIdentityNumberExtensionsTest.cs
✅ Files skipped from review due to trivial changes (1)
  • test/Altinn.App.Api.Tests/Controllers/ProfileControllerTests.User.verified.txt

@framitdavid framitdavid added the backport This PR should be cherry-picked onto older release branches label Jun 19, 2026
@framitdavid
framitdavid enabled auto-merge (squash) June 19, 2026 12:50
@framitdavid
framitdavid merged commit 9b485ef into main Jun 19, 2026
11 checks passed
@framitdavid
framitdavid deleted the feat/mask-ssn-numbers branch June 19, 2026 12:53
@github-project-automation github-project-automation Bot moved this from 🔎 In review to ✅ Done in Team Altinn Studio Jun 19, 2026
@github-actions

Copy link
Copy Markdown

Automatic backport successful!

A backport PR has been automatically created for the release/v8.12 release branch.

The release branch release/v8.12 already existed and was updated.

The cherry-pick was clean with no conflicts. Please review the backport PR when it appears.

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

backport This PR should be cherry-picked onto older release branches bugfix Label Pull requests with bugfix. Used when generation releasenotes squad/utforming Issues that belongs to the named squad.

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

3 participants