[PM-39894] fix: Exclude provider-linked organizations from add-existing-organization flow#7988
Conversation
…ng-organization flow The Provider Portal's "Add existing organization" flow never checked whether the target organization already had a ProviderOrganization row, so a reseller-linked org (Status stays Created) could be absorbed into an MSP's consolidated billing, leaving the org linked to two providers. Two layers: - Organization_ReadAddableToProviderByUserId (Dapper sproc + EF twin) now excludes any org with an existing ProviderOrganization row, so linked orgs never appear in the picker and the endpoint's eligibility re-check returns 404 for them. - ProviderBillingService.AddExistingOrganization guards first with the same check as the legacy ProviderService.AddOrganization path and throws ConflictException (409) before any Stripe or repository side effects, covering the check-then-act race and direct service callers.
…g-organization-allows-absorbing-an-organization
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the fix excluding provider-linked organizations from the "Add existing organization" flow. The change guards at two layers: the eligibility query (Dapper stored procedure plus its EF Core twin) now excludes organizations with an existing No findings meeting the confidence threshold. Code Review DetailsNo blocking or actionable findings. Notes verified during review:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7988 +/- ##
===========================================
+ Coverage 14.86% 66.64% +51.78%
===========================================
Files 1386 2284 +898
Lines 60245 99672 +39427
Branches 4799 9017 +4218
===========================================
+ Hits 8957 66431 +57474
+ Misses 51137 30970 -20167
- Partials 151 2271 +2120 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
mkincaid-bw
left a comment
There was a problem hiding this comment.
DB changes look good. The formatting of the code doesn't match our style guide but since you are only adding a small piece, I don't expect you to change it (unless you want to 😄).
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-39894
📔 Objective
The Provider Portal's "Add existing organization" flow (
POST /providers/{id}/clients/existing) never checked whether the target organization already had aProviderOrganizationrow. A reseller-linked org keepsStatus = Createdand its own gateway fields, so it passed every filter in the addable query and could be absorbed into an MSP's consolidated billing — leaving the org linked to two providers (confirmed in production; see ticket).This adds the missing guard at two layers:
Organization_ReadAddableToProviderByUserId(Dapper stored procedure + EF twin) now excludes any organization with an existingProviderOrganizationrow viaNOT EXISTS. Linked orgs no longer appear in the picker, and the endpoint's eligibility re-check returns 404 for them.ProviderBillingService.AddExistingOrganizationnow guards first with the same check as the legacyProviderService.AddOrganizationpath (GetByOrganizationId) and throwsConflictException(409"Organization already belongs to a provider.") before any Stripe or repository side effects. This covers the check-then-act race and direct service callers. Note: this intentionally returns 409 rather than the legacy path's 400 — the caller has no control over whether aProviderOrganizationrow exists.Includes a dated idempotent migration (
CREATE OR ALTER), unit tests asserting the guard fires before any billing side effect, and integration tests for both ORM tracks covering standalone (included), reseller-linked, MSP-linked, and BusinessUnit-branch (excluded) cases.Verified end-to-end locally against a running Api + Identity with the ticket's replication fixture (reseller-linked org with a real Stripe test-mode trialing subscription, billable MSP, owner-who-is-provider-admin):
[]Org row, both providers, the single
ProviderOrganizationlink, and the Stripe subscription were verified untouched after every rejection.QA note:
GetAddableOrganizationsalso filters on live Stripe subscription status (Active/Trialing/PastDue). The target org must have a real test-mode subscription, or it vanishes from the addable list regardless of this fix and can masquerade as a pass.Out of scope / follow-ups: cleanup of existing dual-linked production rows and a unique index on
ProviderOrganization.OrganizationId(requires the cleanup first) — tracked with dbops via the ticket.