Skip to content

[PM-38796] Create link confirmation endpoint#7907

Draft
JimmyVo16 wants to merge 15 commits into
mainfrom
ac/pm-38796/add-link-confirmation-endpoint
Draft

[PM-38796] Create link confirmation endpoint#7907
JimmyVo16 wants to merge 15 commits into
mainfrom
ac/pm-38796/add-link-confirmation-endpoint

Conversation

@JimmyVo16

@JimmyVo16 JimmyVo16 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@JimmyVo16 JimmyVo16 self-assigned this Jul 1, 2026
@JimmyVo16 JimmyVo16 changed the title [PM-38796] wip [PM-38796] Create link confirmation endpoint Jul 1, 2026
@sonarqubecloud

sonarqubecloud Bot commented Jul 1, 2026

Copy link
Copy Markdown

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.25000% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.40%. Comparing base (c71050b) to head (4db3ba3).

Files with missing lines Patch % Lines
...InviteLinks/ConfirmOrganizationInviteLinkErrors.cs 29.54% 31 Missing ⚠️
...nviteLinks/ConfirmOrganizationInviteLinkCommand.cs 95.37% 4 Missing and 1 partial ⚠️
...iteLinks/ConfirmOrganizationInviteLinkValidator.cs 88.88% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7907      +/-   ##
==========================================
+ Coverage   61.36%   61.40%   +0.04%     
==========================================
  Files        2244     2248       +4     
  Lines       98776    98975     +199     
  Branches     8928     8941      +13     
==========================================
+ Hits        60610    60778     +168     
- Misses      36019    36050      +31     
  Partials     2147     2147              

☔ 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.

@JimmyVo16 JimmyVo16 added ai-review Request a Claude code review t:feature Change Type - Feature Development labels Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the new invite-link confirmation endpoint (POST /organizations/users/invite-link/confirm), its command/validator/error types, DI registration, feature flag, and unit + integration tests. The command correctly separates read-only validation from write side effects, expands seats before creating a new confirmed membership, gates account-recovery enrollment before any writes, and pushes an org-keys sync to the user's devices. The RFC 7807 error-mapping approach is consistent and well tested. Prior review threads (audit logging, device push-sync, and the free-org admin limit) have been addressed in this PR or tracked via follow-up ticket PM-38799.

Code Review Details

No new blocking findings. Verified the operator precedence in ValidateFreeOrganizationAdminLimitAsync parses as (Type is Owner or Admin) && PlanType == Free && count > 0, which is correct. Seat-expansion, default-collection best-effort creation, and validation-error translation all behave as documented and are covered by tests.

Minor, non-blocking observation (not posted inline): DefaultUserCollectionName is [Required] on the new request model, whereas the sibling confirm/accept models treat it as optional/nullable, and the command only uses it when Organization Data Ownership applies. If clients may confirm into organizations without data ownership, consider whether the field should remain required.

Comment on lines +97 to +113
/// <summary>
/// Confirms an existing membership (a pending email invitation or an accepted membership) by linking
/// it to the user, releasing the org key, and moving it straight to <see cref="OrganizationUserStatusType.Confirmed"/>.
/// Persisting via <c>ReplaceAsync</c> bumps the user's account revision date so their other devices sync.
/// </summary>
private async Task<CommandResult<OrganizationUser>> ConfirmExistingMembershipAsync(
OrganizationUser existingOrganizationUser, User user, string orgUserKey)
{
existingOrganizationUser.Status = OrganizationUserStatusType.Confirmed;
existingOrganizationUser.UserId = user.Id;
existingOrganizationUser.Email = null;
existingOrganizationUser.Key = orgUserKey;

await organizationUserRepository.ReplaceAsync(existingOrganizationUser);

return existingOrganizationUser;
}

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: Is the omission of a device push-sync intentional here?

Details

The other confirm-to-Confirmed flows push an org-keys sync so the user's already-signed-in devices pick up the new org key immediately:

  • ConfirmOrganizationUserCommandPushSyncOrgKeysAsync (plus device-registration cleanup)
  • AutomaticallyConfirmOrganizationUserCommandPushSyncOrganizationKeysAsync

This command releases the org key (Key = orgUserKey) and moves the membership straight to Confirmed, but injects no IPushNotificationService. The doc comment notes the ReplaceAsync revision-date bump handles other-device sync, but that relies on the next poll rather than an immediate push, and CreateConfirmedMembershipAsync (new membership via CreateAsync) has no equivalent note. Was relying solely on the revision-date bump — with no PushSyncOrgKeysAsync — a deliberate choice for this endpoint?

Comment on lines +102 to +113
private async Task<CommandResult<OrganizationUser>> ConfirmExistingMembershipAsync(
OrganizationUser existingOrganizationUser, User user, string orgUserKey)
{
existingOrganizationUser.Status = OrganizationUserStatusType.Confirmed;
existingOrganizationUser.UserId = user.Id;
existingOrganizationUser.Email = null;
existingOrganizationUser.Key = orgUserKey;

await organizationUserRepository.ReplaceAsync(existingOrganizationUser);

return existingOrganizationUser;
}

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.

⚠️ IMPORTANT: Confirming an existing membership skips the "one free-org admin" limit that every other confirm/accept path enforces.

Details and fix

ConfirmExistingMembershipAsync moves any existing membership straight to Confirmed without checking the Free-plan admin limit. An email invitation can carry Admin/Owner Type, so a user who already administers one Free org could confirm a second Admin/Owner invite here and bypass the invariant.

The sibling AcceptOrganizationInviteLinkCommand.AcceptExistingInviteAsync guards exactly this case via ValidateFreeOrganizationAdminLimitAsync, with the comment "so the invite link can't bypass it." ConfirmOrganizationUserCommand enforces the same rule (GetCountByFreeOrganizationAdminUserAsync). Consider mirroring that check before confirming an existing Admin/Owner membership on a Free plan:

if (existingOrganizationUser.Type is OrganizationUserType.Owner or OrganizationUserType.Admin &&
    organization.PlanType == PlanType.Free &&
    await organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id) > 0)
{
    return new ConfirmOnlyOneFreeOrganizationAdminAllowed();
}

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.

1 participant