[PM-36963] Bulk cohort assignment#7792
Conversation
…an read (PM-36963)
…ent over-posting (PM-36963)
The migration was named 2026-06-03_00 but base already occupies 2026-06-03_00 and _01. Bump to _02 so it sorts chronologically after the base migrations while staying before the 2026-06-08 update.
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the bulk cohort assignment (CSV upload) workflow: the Admin controller/views, the Code Review DetailsNo new findings. Notes from verification:
|
BTreston
left a comment
There was a problem hiding this comment.
Approved for AC owned files
mkincaid-bw
left a comment
There was a problem hiding this comment.
Naming issue and performance-related question.
mkincaid-bw
left a comment
There was a problem hiding this comment.
The temp table changes look good but I missed the usage of GETUTCDATE().
|



🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-36963
📔 Objective
Adds the operator-facing bulk cohort assignment (CSV upload) workflow under the Tools menu of the Bitwarden Portal (
src/Admin), gated behind the same business-plan price-migration feature flag as the Cohorts CRUD UI (#7732).What this adds:
Index): upload a 2-column CSV (OrganizationId,CohortName) with a header row. An empty cohort cell un-assigns the organization, organizations not listed are left unchanged, and cohort names match case-insensitively. File uploads are capped at 25 MB via a newMaxFileSizeAttribute+FileSize25mbconstant.Core(CohortBulkAssignmentCsvParser): malformed rows, unknown organizations, and unknown cohort names are collected with line numbers and surfaced back on the upload page as a line-by-line error list. Nothing is written to the database when the file has errors.OPENJSONMERGEstored procedure (OrganizationPlanMigrationCohortAssignment_SyncMany) that inserts, updates, and un-assigns in one statement and returnsInserted/Updated/Unassigned/Skippedcounts.Organization_ReadPlanTypesByIds(backed by a newOrganizationPlanTypeView) andOrganizationPlanMigrationCohort_ReadManyByNames. Dapper + EF Core parity for the reads; the bulk sync is MSSQL/Dapper-only and the EF implementation intentionally throwsNotSupportedException.BulkSyncCohortAssignmentsCommandorchestrating parse → validate → resolve → sync, DI registration, the controller (RequirePermission(Tools_ManagePlanMigrationCohorts), feature-flag gated), and a nav entry under Tools.CsvHelper.OrganizationPlanMigrationCohortAssignment_SyncMany,Organization_ReadPlanTypesByIds,OrganizationPlanMigrationCohort_ReadManyByNames; plus an update to the existingOrganizationPlanMigrationCohortAssignment_ReadNonPendingCountByCohortIdto align its lock predicate with the locked-assignment behavior (adds a new dated migration since that proc is already deployed).Product decision: locking already scheduled / migrated assignments
After discussing with product, we decided that assignments whose organization is already scheduled to migrate or has already been migrated (and, for Churn-only cohorts, has had a churn discount applied) should be treated as locked and skipped during a bulk upload, rather than reassigned or un-assigned. This mirrors the single-organization edit path, which already protects these organizations, and prevents a bulk CSV from disturbing in-flight or completed migrations. The bulk sync skips these rows, reports the count as Skipped on the result page with a short explanation, and the same lock definition is now shared across both SQL lock gates and the C#
IsLocked()entity method.Where this deviates from the mockups and why
📸 Screenshots