Skip to content

[PM-40216] feat: Data-protect OrganizationInviteLink.Code at rest#7969

Open
r-tome wants to merge 7 commits into
mainfrom
pm-40216/data-protect-invite-link-codes
Open

[PM-40216] feat: Data-protect OrganizationInviteLink.Code at rest#7969
r-tome wants to merge 7 commits into
mainfrom
pm-40216/data-protect-invite-link-codes

Conversation

@r-tome

@r-tome r-tome commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-40216

📔 Objective

The invite link Code is a bearer secret — anyone with DB read access (breach, backup dump, self-hosted operator) could obtain every org's join capability and walk a compliant email to Accepted. This PR data-protects Code at rest via IDataProtector, matching the existing pattern in UserRepository and SendRepository.

Key changes:

  • OrganizationInviteLink.Code changes from Guid to string (wire type stays Guid); stored as "P|" + protector.Protect(code) in a new NVARCHAR(300) column
  • GetByCodeAsync is removed; all anonymous endpoints now require both OrganizationId and Code — fetch by org, then validate Code via CryptographicOperations.FixedTimeEquals (constant-time, no oracle)
  • Unique index on Code dropped (encrypted values are non-deterministic)
  • InviteLinkCodeValidator static helper centralises the constant-time comparison
  • SQL Server: coordinated-deploy migration (must run after all instances are on the new version); EF Core migrations for MySQL, PostgreSQL, SQLite
  • URL format becomes /#/join/{orgId}/{code}?key=... (clients PR companion)

Security properties after this change:

  • DB compromise no longer yields usable invite codes
  • Code mismatch and missing-org both return 404 with the same shape (no enumeration oracle)
  • Admin re-display of the invite URL continues to work (reversible protection, not a hash)

Stores invite link Code as IDataProtector-encrypted NVARCHAR(300) with a
"P|" prefix, replacing the plaintext UNIQUEIDENTIFIER. All anonymous
endpoints now require OrganizationId + Code; lookup by Code is replaced
with lookup-by-org + constant-time FixedTimeEquals comparison, eliminating
the Code as a DB lookup key and closing the DB-read bearer-secret exposure.

Includes SQL Server migration (coordinated deploy), EF Core migrations for
MySQL/PostgreSQL/SQLite, SSDT schema update, and the InviteLinkCodeValidator
helper for constant-time comparisons.
@r-tome
r-tome requested review from a team as code owners July 13, 2026 14:49
@r-tome
r-tome requested a review from sven-bitwarden July 13, 2026 14:49
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the data-protection change for OrganizationInviteLink.Code: the entity/type change from Guid to string, both Dapper and EF Core repositories (protect/unprotect with the P| prefix and IDataProtector), the constant-time CodeMatches comparison, the anonymous query/command flows now keyed by OrganizationId + Code, the MSSQL migration/SSDT schema, and the MySQL/PostgreSQL/SQLite EF migrations. The implementation mirrors the established UserRepository/SendRepository protection pattern, restores the plaintext code in finally blocks so callers never see the protected value, and returns a uniform 404 for both code mismatch and missing org (no enumeration oracle). Test coverage is thorough, including case-sensitivity and null/empty edge cases.

No findings at or above the confidence threshold. The two existing review threads (lowercase normalization on EF providers, EDD/no-rollback concern on the coordinated-deploy migration) were satisfactorily resolved by the author: the feature is unreleased with zero existing records, so the intentionally non-backwards-compatible column-type change and dropped ReadByCode sproc are acceptable given the documented coordinated-deploy requirement.

Code Review Details

No findings at or above the confidence threshold.

migrationBuilder.DropIndex(
name: "IX_OrganizationInviteLink_Code",
table: "OrganizationInviteLink");
}

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.

QUESTION: Are pre-existing invite-link codes normalized to lowercase on SQLite/MySQL/PostgreSQL?

Details

The MSSQL migration deliberately runs UPDATE ... SET [Code] = LOWER([Code]) with the comment "SQL Server casts UNIQUEIDENTIFIER to uppercase NVARCHAR; LOWER() normalizes for FixedTimeEquals". This is because InviteLinkCodeValidator.CodesMatch uses CryptographicOperations.FixedTimeEquals on raw UTF-8 bytes — a case-sensitive comparison — while the incoming request.Code.ToString() is always lowercase (Guid.ToString() in .NET is lowercase).

This SQLite migration (and the MySQL/Postgres ones) only drops the index / alters the column type; none of them lowercase existing rows. For legacy invite links created before this PR, Code was stored via EF's Guid-to-TEXT conversion — the same conversion used for Id/OrganizationId, which EF renders in uppercase in SQLite.

If SQLite stored those legacy codes uppercase, they will no longer match the lowercase incoming code after migration, silently breaking every pre-existing invite link on self-hosted SQLite deployments (Postgres uuid→text and MySQL char(36) are lowercase, so likely unaffected).

Can you confirm the stored casing on each EF provider, and add a normalization step for any provider that stores uppercase — mirroring the MSSQL LOWER() — so existing links keep working?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There are no pre-existing invite-link codes. This feature has not been released yet.

@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.68786% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.61%. Comparing base (846372d) to head (74ffe65).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...e/Repositories/OrganizationInviteLinkRepository.cs 97.36% 0 Missing and 2 partials ⚠️
...e/Repositories/OrganizationInviteLinkRepository.cs 96.49% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7969      +/-   ##
==========================================
+ Coverage   62.05%   66.61%   +4.56%     
==========================================
  Files        2277     2277              
  Lines       99452    99556     +104     
  Branches     8983     8990       +7     
==========================================
+ Hits        61718    66323    +4605     
+ Misses      35561    30961    -4600     
- Partials     2173     2272      +99     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@r-tome
r-tome marked this pull request as draft July 13, 2026 15:00
@r-tome r-tome added the ai-review Request a Claude code review label Jul 13, 2026
@r-tome r-tome changed the title [PM-40216] Data-protect OrganizationInviteLink.Code at rest [PM-40216] feat: Data-protect OrganizationInviteLink.Code at rest Jul 13, 2026
@r-tome r-tome added the t:feature Change Type - Feature Development label Jul 13, 2026
r-tome added 5 commits July 14, 2026 11:16
…-invite-link-codes

# Conflicts:
#	test/Api.Test/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs
#	test/Core.Test/AdminConsole/OrganizationFeatures/InviteLinks/GetOrganizationInviteLinkStatusQueryTests.cs
…ation

CI's migration order check failed because main gained
2026-07-14_00_AddCreationDateToOrganizationUserUserDetailsView.sql
after this branch's migration was authored.
…eValidator

Delegates to the codebase's existing constant-time comparison helper instead of duplicating the raw CryptographicOperations call.
…onInviteLink

Replaces the standalone InviteLinkCodeValidator with an instance method on
OrganizationInviteLink, matching the AuthRequest.IsValidForAuthentication
pattern of entities comparing their own fields against external input.
The table has zero rows in every environment, so there's no existing
data to normalize before the column type change.
@r-tome
r-tome marked this pull request as ready for review July 14, 2026 15:42

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No records exist whatsoever so no EDD rules are broken. This was confirmed here #7896 (review)

…-invite-link-codes

# Conflicts:
#	test/Core.Test/AdminConsole/OrganizationFeatures/InviteLinks/GetOrganizationInviteLinkPoliciesQueryTests.cs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants