* Add email domain validation to OrganizationInviteLink
- Introduced IsEmailDomainAllowed method to check if an email's domain is permitted based on allowed domains.
- Added necessary using directives for MailAddress and domain sanitization utilities.
* Refactor OrganizationInviteLink by removing email domain validation
- Removed the IsEmailDomainAllowed method and associated using directives for MailAddress.
- Cleaned up the code by eliminating unused methods related to email domain validation.
* Add InviteLinkDomainValidator for email domain validation
- Introduced InviteLinkDomainValidator class with IsEmailDomainAllowed method to validate if an email's domain is in the list of allowed domains.
- Utilized MailAddress for email parsing and added domain sanitization logic.
* Add email domain validation endpoint for organization invite links
- Implemented ValidateEmailDomain method in OrganizationInviteLinksController to check if an email's domain is allowed based on the invite link's permitted domains.
- Created OrganizationInviteLinkValidateEmailDomainRequestModel for request validation and OrganizationInviteLinkValidateEmailDomainResponseModel for response formatting.
- Integrated IOrganizationInviteLinkRepository to retrieve invite link details by code.
* Add unit tests for InviteLinkDomainValidator
- Created InviteLinkDomainValidatorTests class to validate email domain functionality.
- Added tests for various scenarios including invalid emails, empty domain lists, and matching domains.
- Ensured comprehensive coverage of the IsEmailDomainAllowed method's behavior.
* Add integration test for email domain validation in OrganizationInviteLinksController
- Implemented a test to validate that an allowed email domain returns the expected result when checked against an organization invite link.
- Ensured the test verifies the creation of an invite link and the subsequent validation of an email domain against the allowed domains list.
* Add validation query and interface for organization invite link email domain
- Introduced ValidateOrganizationInviteLinkEmailDomainQuery class to validate if an email's domain is allowed based on the invite link's permitted domains.
- Created IValidateOrganizationInviteLinkEmailDomainQuery interface to define the validation method.
- Added unit tests for the validation query to ensure correct behavior for various scenarios, including link not found and domain matching.
* Refactor OrganizationInviteLinksController to use validation query for email domain
- Updated ValidateEmailDomain method to utilize IValidateOrganizationInviteLinkEmailDomainQuery for domain validation instead of directly accessing the repository.
- Removed unnecessary repository dependency and streamlined the response handling for validation results.
- Registered the new validation query in OrganizationServiceCollectionExtensions for dependency injection.
* Refactor InviteLinkDomainValidator and replace MailAddress usage with existing email validation method
* test(org-user-request-model): Add model validation tests.
* feat(request-models): Add Authentication and Unlock Data fields with annotations.
* test(recover-command): Add tests for Authentication and Unlock Data payload signature.
* feat(recover-command): Add overload for Authentication and Unlock Data payload signature.
* test(recover-command): Add tests for behavior with authentication and unlock data.
* feat(recover-command): Add impl for hash and key, authentication and unlock data inputs.
* test(org-users-controller): Add controller tests for dispatch.
* feat(org-users-controller): Add controller impl for dispatch for both request payload variants.
* chore: lint.
* fix(request-model): Validation method drifted in base; rename.
* test(request-model): Update validation tests.
* feat(request-model): Support 2FA-only validation at the boundary.
* test(request-model): Express handling of v1 vs v2 requests.
* PM-35394 - Per reviewer's request, mark AdminRecoverAccountCommand.RecoverAccountAsync that doesn't accept new models obselete
* PM-35394 - Fix using directive after model namespace move
Merge from main moved OrganizationUserResetPasswordRequestModel to the
AdminConsole namespace; update the test's using directive to match,
restoring both the build and dotnet format checks.
---------
Co-authored-by: Jared Snider <jsnider@bitwarden.com>
* feat(admin-console): Add InjectOrganizationAttribute and OrganizationModelBinder for automatic organization parameter binding
* feat(admin-console): Introduce BindOrganizationAttribute and OrganizationModelBinder for organization parameter binding with unit tests
* feat(admin-console): Update GetResetPasswordDetails to use BindOrganization for organization parameter
* fix(admin-console): Correct organization ID check in GetResetPasswordDetails method to use bound organization
* Refactor OrganizationUsersControllerTests to use bound organization in GetResetPasswordDetails method
- Updated test cases to pass the organization directly instead of relying on repository calls.
- Ensured that the tests correctly assert NotFoundException when the organization user does not match the bound organization.
- Improved clarity in test setup by explicitly binding the organization to the method calls.
* Fix UTF-8 BOM issue in BindOrganizationAttribute.cs
* Add integration tests for OrganizationUsersController's BindOrganization functionality
- Introduced OrganizationUsersControllerBindOrganizationTests to validate the behavior of the GET reset-password-details endpoint.
- Implemented tests for successful retrieval of reset password details, handling of non-existent organization users, and cases where the user belongs to a different organization.
- Ensured comprehensive coverage of scenarios to verify correct status responses and organization binding logic.
* feat(admin-console): Add bulk confirmation and pending auto-confirmation methods for organization users
- Implemented ConfirmManyOrganizationUsersAsync to confirm multiple users in a single operation.
- Added GetManyPendingAutoConfirmAsync to retrieve users pending automatic confirmation.
- Created stored procedures for bulk confirmation and fetching pending users.
- Updated relevant repository interfaces and implementations across Dapper and Entity Framework.
* refactor(admin-console): Change parameter type for ConfirmManyOrganizationUsersAsync to IReadOnlyCollection
- Updated the ConfirmManyOrganizationUsersAsync method signature in the IOrganizationUserRepository and its implementations to use IReadOnlyCollection instead of IEnumerable for better performance and clarity.
- Adjusted related repository methods in both Dapper and Entity Framework implementations to reflect this change.
- Added unit tests to ensure the new implementation behaves as expected, including scenarios for mixed batches and idempotency.
* Remove OrganizationUser_ReadByOrganizationIdStatus stored procedure as part of database cleanup.
* Add integration tests for ConfirmManyOrganizationUsers and GetManyPendingAutoConfirm methods
- Introduced ConfirmManyOrganizationUsersTests to validate the confirmation of multiple organization users, ensuring only accepted users are confirmed and idempotency is maintained.
- Added GetManyPendingAutoConfirmTests to verify retrieval of pending auto-confirm users, ensuring only eligible users are returned based on specific criteria.
- Removed duplicate test implementations from OrganizationUserRepositoryTests to maintain clarity and organization in the test suite.
* Implement OrganizationUser_UpdateStatusKey stored procedure and update related repository method
- Added OrganizationUser_UpdateStatusKey stored procedure to handle updating the status and key of organization users based on a JSON input.
- Updated OrganizationUserRepository to call the new stored procedure instead of the previous confirmation procedure.
- Modified OrganizationUser_ReadByPendingAutoConfirm stored procedure to filter users by a new type value.
- Enhanced integration tests to verify the correct behavior of the updated confirmation logic, ensuring revision dates are accurately tracked.
* Refactor OrganizationUser_UpdateStatusKey to OrganizationUser_UpdateManyStatusKey
- Renamed the stored procedure to OrganizationUser_UpdateManyStatusKey to better reflect its functionality of updating multiple organization users' statuses.
- Updated the OrganizationUserRepository to call the new stored procedure.
- Adjusted the migration script to create or alter the procedure accordingly.
* Update data type for Key column in AddOrganizationUserUpdateStatusKey migration script
- Changed the data type of the Key column from NVARCHAR(MAX) to VARCHAR(MAX) in the UsersToUpdate table and the corresponding JSON parsing logic to improve compatibility and performance.
* Updated spacing
* Add stored procedures for organization user status updates and retrieval
- Created OrganizationUser_UpdateManyStatusKey to update multiple organization users' statuses based on a JSON input, including handling revision dates and tracking updated IDs for idempotency.
- Added OrganizationUser_ReadByPendingAutoConfirm to retrieve organization users pending auto-confirmation based on organization ID and specific status and type filters.
---------
Co-authored-by: mkincaid-bw <mkincaid@bitwarden.com>
feat: add ChangeEmailCommand [PM-35830]
- Add ChangeEmailCommand to encapsulate core email change logic
- Add OrganizationDomainAllowEmailChangeQuery returning OrganizationDomainAllowEmailChangeDenialReason enum so callers can branch on the specific denial reason, if any
- Surface tailored BadRequestException messages per denial reason via a switch expression that fails closed on unknown values
- Push sync-only notification (no logout)
- Add unit tests covering denial reasons, domain case invariance, and the null-customerId-with-gateway path
* fix(billing): guard US customers from missing tax ID warning when automatic tax flag is enabled
US has no customer-facing VAT/Tax ID equivalent, so the warning should never appear for US customers regardless of the PM37597 flag state.
* fix(billing): fix provider warnings test asserting buggy US tax ID warning behavior
* Implement UpdateCollectionManagementSettingsCommand and associated interface for managing organization collection settings
* Add UpdateCollectionManagementSettingsCommand to handle updates to organization collection management settings.
* Create IUpdateCollectionManagementSettingsCommand interface to define the update method.
* Implement unit tests for UpdateCollectionManagementSettingsCommand to verify event logging and exception handling.
* Add IUpdateCollectionManagementSettingsCommand to service collection
* Register IUpdateCollectionManagementSettingsCommand and its implementation, UpdateCollectionManagementSettingsCommand, in the service collection for managing organization collection settings.
* Rename command
* Update OrganizationsController to use IOrganizationUpdateCollectionManagementCommand
* Added IOrganizationUpdateCollectionManagementCommand to the OrganizationsController for managing collection settings updates.
* Updated the constructor to inject the new command and modified the PutCollectionManagement method to utilize it.
* Adjusted unit tests to reflect the changes in the command used for updating collection management settings.
* Refactor IOrganizationService and OrganizationService to remove UpdateCollectionManagementSettingsAsync method
* Removed the UpdateCollectionManagementSettingsAsync method from IOrganizationService and its implementation in OrganizationService.
* Cleaned up unused usings related to collection management settings in both service files.
* Updated unit tests to reflect the removal of the collection management settings update functionality.
* feat(billing): add Organization.ChangePlan extension for structural plan shape
Pure helper that writes plan-derived structural columns (PlanType, Plan,
Use* capability flags, UsersGetPremium, MaxCollections) without touching
customer-purchase columns. Preserves the existing UseKeyConnector carve-out.
Behavior-preserving extraction of the field-copy block at lines 287-310
of UpgradeOrganizationPlanCommand.UpgradePlanAsync.
* refactor(billing): use Organization.ChangePlan in UpgradePlanAsync
Replaces the inline structural field-copy block with a call to the new
ChangePlan helper. Customer-purchase columns (Seats, MaxStorageGb,
Enabled, UseSecretsManager) and the PremiumAccessAddon override on
UsersGetPremium stay inline at the call site. Behavior-preserving.
* feat(billing): register Teams 2020 -> current migration paths
Appends Teams2020AnnualToCurrent (byte 3) and Teams2020MonthlyToCurrent
(byte 4) to MigrationPathId and the MigrationPaths registry. Required for
the business migration handler to resolve cohorts mapped to Teams source
plans; without these entries MigrationPaths.FromId returns null and the
handler no-ops on Teams orgs in Cohort A1. Snapshot tests updated.
* chore(billing): inject migration cohort repositories into SubscriptionUpdatedHandler
Adds IOrganizationPlanMigrationCohortRepository and
IOrganizationPlanMigrationCohortAssignmentRepository as constructor
dependencies in preparation for HandleScheduleTriggeredBusinessMigrationAsync.
No behavior change.
* feat(billing): scaffold business plan Phase-2 migration handler
Adds HandleScheduleTriggeredBusinessMigrationAsync as a sibling call in
HandleAsync's organization branch, gated on PM35215_BusinessPlanPriceMigration.
Initial implementation short-circuits when ScheduleId is null. Locks the
no-op behaviour with NoScheduleId + FeatureFlagOff tests. Full handler body
lands in subsequent commits.
* feat(billing): gate business migration handler on registered source price IDs
Builds the source-price allowlist from MigrationPaths.All, using the
seat-vs-non-seat pattern (HasNonSeatBasedPasswordManagerPlan). All four
Track A 2020 plans register automatically. Skips when the previous
subscription items don't include any registered 2020 source price.
* feat(billing): resolve cohort via assignment row for business migration handler
Reads assignment by organization id (DB is source of truth), then resolves
the cohort and migration path. Stripe subscription.Metadata['migration_cohort_id']
remains stamped by PriceIncreaseScheduler for dashboard attribution but is
not consulted by the handler. Skips with a warning when the assignment is
missing, the cohort is missing, or MigrationPathId references an unregistered
path. Idempotent: skips with info-level log when assignment.MigratedDate is
already set, before any further DB reads.
* feat(billing): defensive target-price sanity check for business migration
After resolving the target plan from cohort.MigrationPath.ToPlan, verifies
the current subscription items contain the target's PM price ID (seat-aware).
Skips with a warning on mismatch to protect against operator data errors or
off-path schedule transitions.
* feat(billing): apply plan shape and mark assignment migrated on Phase 2
Completes HandleScheduleTriggeredBusinessMigrationAsync: loads the org,
calls Organization.ChangePlan(targetPlan), persists via ReplaceAsync, and
sets assignment.MigratedDate + RevisionDate before persisting the
assignment. Happy-path coverage for all four Track A pairs (Teams +
Enterprise, monthly + annual). Teams tests assert the UseScim flip - the
load-bearing capability gain for Teams 2020 -> current.
* Add more unit test
* fix: add UTF-8 BOM to .cs files for editorconfig charset compliance
* Add exception handle
* Code refactoring
* Add more unit testing
* Resolve the pr comment
* feat(billing): introduce unified subscription price increase scheduler API
* feat(billing): implement unified subscription price increase scheduler logic
* refactor(billing): update subscription handlers to use unified scheduler
* feat(billing): extend price migration feature flag checks
* test(billing): add and update tests for unified price increase scheduler
* fix(billing): run dotnet format
* feat(billing): expand customer and customer.discount on subscription fetch
* refactor(ReinstateSubscriptionCommandTests): rename test method for broader scope
* feat(billing): expand customer.discount in update handler
* test(billing): update test name
* feat(billing): add test clock waiting mechanism for upcoming invoices
* feat(billing): introduce cancelling user ID metadata key
* feat(billing): store cancelling user ID on subscription cancellation
* feat(billing): clear cancelling user ID on subscription reinstatement
* test(billing): update subscriber service tests for cancelling user ID
* style(SubscriberService): use 'is not null' pattern matching
* feat(SubscriberService): add PM35215 migration cohort metadata handling
* feat(SubscriberService): extend price migration deferral to PM35215
* test(SubscriberService): add and update tests for PM35215 feature
* feat(billing): Introduce OrganizationPriceIncreaseOptions
* refactor(billing): Centralize price increase eligibility in scheduler
* refactor(billing): Delegate price increase validation from UpcomingInvoiceHandler
* feat(billing): Manage price increase schedules during subscription lifecycle events
* test(billing): Update UpcomingInvoiceHandlerTests for centralized validation
* test(billing): Add PriceIncreaseScheduler tests for SkipIfAlreadyScheduled option
* test(billing): Add SubscriberService tests for price increase schedule management
* fix(billing): run dotnet format
* fix(billing): remove redundant customer expansion
* fix(billing): expand discounts for customer and subscription
* refactor(billing): Rename method to clarify dispatching role for organization scheduling
* fix(billing): Prevent clearing migration cohort metadata on cancellation
* fix(billing): Fallback to standard email when price increase migration fails
* feat(billing): improve observability for missing migration path data
* refactor(billing): simplify business plan type identification
* Implementation desktop and browser checkout
* Fixed the failing test
* Add a logger to see gobal settings in qa
* Add log
* fix the lint error
* Removed the log
* added event type and control flow
* add EventType, control flow, and test coverage
* fix failing test and de-dupe enums
* access event log traps on auth and anon endpoints
* prioritize FF check in conditional statements
* Refactor InitPendingOrganizationValidator to remove IPolicyService dependency and replace with IPolicyRequirementQuery for policy checks. Update related tests to reflect changes in policy validation logic.
* Refactor AccountsController and related validators to replace IPolicyService with IPolicyRequirementQuery for policy checks. Update tests accordingly to reflect changes in policy validation logic.
* Remove IPolicyService and related implementations from the codebase, updating PolicyServiceCollectionExtensions and deleting associated tests. This change streamlines policy management by relying on IPolicyRequirementQuery for policy checks.
* Refactor OrganizationUserRepository to remove GetByUserIdWithPolicyDetailsAsync method and associated tests.
* Remove unused stored procedures: OrganizationUser_ReadByUserIdWithPolicyDetails and PolicyDetails_ReadByUserId, as they are no longer called in the codebase.
* Remove OrganizationUserPolicyDetails class and associated test fixtures, as they are no longer needed in the codebase.
* Refactor BaseRequestValidatorTests to replace IPolicyService with IPolicyRequirementQuery for SSO validation checks. Update related test logic to ensure accurate policy validation outcomes. Clean up unused test fixtures in PolicyFixtures.cs to streamline the codebase.
* Refactor BaseRequestValidator and SsoRequestValidator to improve readability by storing policy requirement results in local variables before returning values. This change enhances code clarity while maintaining existing functionality.
* Refactor AccountsController to improve clarity by storing the result of the policy requirement query in a local variable before returning the enforced options. This change enhances code readability while preserving existing functionality.
* Revert "Remove unused stored procedures: OrganizationUser_ReadByUserIdWithPolicyDetails and PolicyDetails_ReadByUserId, as they are no longer called in the codebase."
This reverts commit 0f4fdca6e7.
* [PM-37083] feat: Add per-phase price resolution to UpdateOrganizationSubscriptionCommand
Resolve source vs. target plan pricing per schedule phase so item changes
target the correct phase-specific price ID. Move cohort metadata onto the
schedule phases themselves to avoid Stripe normalization triggered by
direct subscription metadata updates. Filter the schedule-aware update
path to phases where EndDate > now, and drop the feature-flag gate on
PriceIncreaseScheduler.Release so schedule existence is the gate.
* Add defensive guard for source-priced single-phase migration schedules
* Implement GetOrganizationInviteLinkStatusQuery to retrieve invite link status
- Added GetOrganizationInviteLinkStatusQuery class to handle fetching the status of an organization invite link based on its code.
- Introduced OrganizationInviteLinkStatus and OrganizationInviteLinkSsoStatus records to encapsulate the invite link status and SSO information.
- Created IGetOrganizationInviteLinkStatusQuery interface to define the contract for the query implementation.
* Add unit tests for GetOrganizationInviteLinkStatusQuery
- Introduced comprehensive unit tests for GetOrganizationInviteLinkStatusQuery to validate various scenarios including successful retrieval of invite link status, handling of not found errors, and seat availability checks.
- Utilized Xunit and NSubstitute for testing and mocking dependencies, ensuring robust coverage of the query's functionality.
* Add IGetOrganizationInviteLinkStatusQuery to service collection
- Registered IGetOrganizationInviteLinkStatusQuery with the service collection to enable retrieval of organization invite link status.
- This addition supports the recently implemented GetOrganizationInviteLinkStatusQuery functionality.
* Add OrganizationInviteLinksPublicController and response models
- Introduced OrganizationInviteLinksPublicController to handle requests for organization invite link status.
- Implemented GetStatus endpoint to retrieve the status of an invite link using its GUID code.
- Added OrganizationInviteLinkStatusResponseModel and OrganizationInviteLinkSsoResponseModel to structure the response data for the invite link status.
- Ensured the endpoint is accessible to anonymous users while requiring application authorization for other actions.
* Add integration tests for OrganizationInviteLinksPublicController
- Introduced integration tests for OrganizationInviteLinksPublicController to validate the GetStatus endpoint functionality.
- Implemented tests to ensure correct handling of existing invite links and appropriate responses for valid and not found scenarios.
- Utilized Xunit and NSubstitute for testing and mocking dependencies, enhancing test coverage for invite link status retrieval.
* Updated GetOrganizationInviteLinkStatusQuery to return SSO status based on organization settings, including UseSso and UsePolicies
* Move status endpoint into OrganizationInviteLinksController as POST
* Refactor OrganizationInviteLinkStatusResponseModel and OrganizationInviteLinkStatus to remove OrganizationId property
- Removed OrganizationId property from both OrganizationInviteLinkStatusResponseModel and OrganizationInviteLinkStatus records to streamline the data model.
- Updated constructors accordingly to reflect the changes in the response models.
* Refactor GetOrganizationInviteLinkStatusQuery to simplify organization checks
- Updated the logic in GetOrganizationInviteLinkStatusQuery to streamline organization validation by combining null and enabled checks.
- Removed the dependency on IApplicationCacheService and adjusted the seat availability logic to enhance clarity and efficiency.
- Modified the return statement to use organization name directly instead of organization ID.
* Add integration tests for OrganizationInviteLinksController
- Introduced a new test method to validate the GetStatus functionality for existing invite links in OrganizationInviteLinksControllerTests.
- Enhanced existing tests to ensure correct responses for valid and not found scenarios.
- Removed OrganizationInviteLinksPublicControllerTests as its functionality is now covered in the OrganizationInviteLinksControllerTests.
* Refactor OrganizationInviteLinksControllerTests
- Updated test methods in OrganizationInviteLinksControllerTests to utilize GetOrganizationInviteLinkStatusRequestModel instead of individual parameters.
- Added a new test case to handle scenarios where the invite link status is not available, returning a BadRequest response.
- Enhanced existing tests to ensure consistent handling of valid and not found scenarios.
* Update GetOrganizationInviteLinkStatusQueryTests to enable organization for invite link tests
* [PM-36949] Add OrganizationPlanMigrationCohort schema and Core domain types
Add the foundation for cohort-based plan migrations:
- Two tables: OrganizationPlanMigrationCohort and OrganizationPlanMigrationCohortAssignment
- Two views and nine stored procedures (four CRUD on cohort, four CRUD plus
ReadByOrganizationId on assignment)
- Single Migrator script for MSSQL deployment
- Core entities, MigrationPath value object and its registry, and bare repository
interfaces under Bit.Core.Billing.Organizations.PlanMigration
The cohort table holds the human-managed metadata (name, discount coupons,
MigrationPathId byte) and the assignment table records each organization's
position in the migration lifecycle (scheduled, migrated, churn-mitigated).
Both Update SPs follow the accept-but-don't-assign pattern: immutable columns
(OrganizationId, CohortId, CreatedAt) are parameters but not SET clauses.
* [PM-36949] Add Dapper repositories for plan migration cohort tables
OrganizationPlanMigrationCohortRepository inherits the base Repository<T, TId>
CRUD methods unchanged. OrganizationPlanMigrationCohortAssignmentRepository
also relies on the base for CRUD and adds GetByOrganizationIdAsync which
returns at most one row (the UNIQUE constraint on OrganizationId at the
database layer guarantees this).
* [PM-36949] Add EF Core configurations, repositories, and provider migrations for plan migration cohort tables
- EF models wrap the Core entities; the assignment model exposes nav properties
for Organization and Cohort so the FK + cascade-delete is inferred by EF.
- EntityTypeConfiguration classes pin ID generation to application code
(ValueGeneratedNever) and declare the UNIQUE indexes plus the composite
(CohortId, ScheduledAt, MigratedAt) index.
- Repositories follow the OrganizationInstallationRepository template; the
assignment repo adds GetByOrganizationIdAsync to mirror the SP exposed on
the MSSQL side.
- DatabaseContext gets two DbSet properties; auto-discovery picks up the
configuration classes.
- Generated migrations for MySQL, Postgres, and SQLite create matching schemas;
EF truncates FK and index names on providers with 64-char identifier limits,
which is consistent with the rest of the codebase.
* [PM-36949] Wire up DI and add tests for plan migration cohort repositories
Register both Dapper and EF Core repositories in their respective service
collection extensions, following the existing AddSingleton convention in
these files.
Add tests:
- MigrationPathIdsSnapshotTests guards the immortal byte IDs that downstream
code pins on. The class- and method-level comments document why these
values can never be renumbered.
- MigrationPathTests covers the FromId round-trip and the null-on-unknown
behavior the registry promises to callers.
- OrganizationPlanMigrationCohortRepositoryTests exercises CRUD, the UNIQUE
Name constraint, and verifies that ReplaceAsync ignores CreatedAt
mutations (per the accept-but-don't-assign Update SP).
- OrganizationPlanMigrationCohortAssignmentRepositoryTests exercises CRUD,
GetByOrganizationIdAsync, the UNIQUE OrganizationId constraint,
cascade-delete from both Organization and Cohort, and verifies that
ReplaceAsync ignores OrganizationId, CohortId, and CreatedAt mutations.
* [PM-36949] Use PlanType and MigrationPathId enums on MigrationPath
Replace the byte Id with a byte-backed MigrationPathId enum and replace
the string FromPlan/ToPlan fields with PlanType. Persistence is
unchanged -- EF normalises enum-backed properties to their underlying
type in the model snapshot, and Dapper handles enum-to-byte parameter
mapping automatically.
* [PM-36949] Add *.lscache to .gitignore
* [PM-36949] fix: Override ReplaceAsync on EF cohort repositories for immutability parity
The Dapper _Update SPs accept-but-don't-assign certain columns (CreatedAt
on cohort; OrganizationId, CohortId, and CreatedAt on assignment), but
the base EF Repository<T,TEntity,Guid>.ReplaceAsync uses SetValues which
writes every scalar. Override on both repos and mark the immutable
properties as IsModified = false so MySQL/Postgres/Sqlite match MSSQL
behavior. Mirrors the existing DeviceRepository.ReplaceAsync pattern.
* [PM-36949] fix: Bound cohort string columns and widen Name to 255 chars
Add [MaxLength] attributes to the three cohort string properties so the
EF providers (MySQL/Postgres/Sqlite) enforce the same limits as MSSQL,
where the columns were already NVARCHAR-capped. Widen Name from 64 to
255 chars across MSSQL DDL, both _Create/_Update SP signatures, the
Migrator script, the entity, and all three regenerated EF migrations.
Coupon codes stay at 64 (Stripe IDs are short).
* [PM-36949] test: Lock FromPlan and ToPlan per MigrationPath
The snapshot test class doc says "byte N means a specific FromPlan ->
ToPlan transition forever", but only the byte value was being asserted.
A silent refactor of the registry's PlanType references would not have
been caught. Add per-path FromPlan/ToPlan assertions to close the gap.
* [PM-36949] chore: Apply dotnet format
* [PM-36949] test: Use LaxDateTimeComparer for round-tripped DateTimes
Postgres timestamp and MySQL datetime(6) store microsecond precision (6
fractional digits), but .NET DateTime is 100ns ticks (7 digits). Exact
Assert.Equal fails by a single tick on round-trip. Switch the three
DateTime comparisons in ReplaceAsync_UpdatesMutableColumns_AndIgnoresImmutableOnes
to LaxDateTimeComparer.Default -- the same 2ms-tolerance comparer used
by SendRepositoryTests and InstallationRepositoryTests for the same
precision issue.
* Add implementation for dropdown
* Reconcile dropdown work with renamed PM-36949 schema
After merging origin/main, the cohort schema now uses *Date suffixes
(ScheduledDate / MigratedDate / ChurnDiscountAppliedDate / CreationDate).
Rename references in the dropdown work, restore IsLocked() on the merged
entity, and re-add GetManyAsync() to the cohort repository (interface +
Dapper + EF) since the merge took main's pre-dropdown versions.
Also remove the six superseded provider migration files (20260515*) -- the
20260518* renames from origin/main are now the only cohort migrations.
* Fix UTF-8 BOM on cohort assignment unit test file
* Resolve the FF and permission issue
* Extract migration cohort resolution into a helper
Addresses PR feedback to keep the Edit action focused: the cohort
resolution/validation now lives in ResolveMigrationCohortAssignmentChangeAsync
and returns a MigrationCohortAssignmentChange record. The endpoint still
owns the page return.
---------
Co-authored-by: Alex Morask <amorask@bitwarden.com>
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
* feat(kdf-settings-validator): Enforce salt cannot be empty string.
* fix(kdf-settings-validator): Prefer IsNullOrWhitespace.
* feat(salt): Make AllowEmptyStrings false for request models.
* [PM-37068] feat: Add business plan cohort branch to UpcomingInvoiceHandler
Wires the caller side of the Teams/Enterprise 2020 → current price
migration. AlignOrganizationSubscriptionConcernsAsync becomes a
ProductTier dispatcher; the new business branch loads the cohort
assignment, runs eligibility guards (assignment unscheduled, cohort
active, org PlanType matches cohort source), and invokes PM-37064's
ScheduleBusinessPriceIncrease. The renewal email is a placeholder until
PM-37070 lands.
Also expands subscriptions.data.customer on the customer load so
PM-37064 can preserve customer-level discounts into Phase 2.
Incidentally strips pre-existing #region markers in
UpcomingInvoiceHandlerTests.cs per the no-regions rule.
* Apply review feedback: gate FF in handler, split MigrationPath checks, silence scheduler churn-only warning
* Add cohort metadata to subscription on migration schedule
---------
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
* feat(billing): inject feature service into billing warning queries
* test(billing): add provider tax warning tests for automatic tax flag
* test(billing): add organization tax warning tests for automatic tax flag
* feat(billing): modify provider tax id warning based on automatic tax feature flag
* feat(billing): modify organization tax id warning based on automatic tax feature flag
* refactor(billing): clean up unused usings and file encoding
* test(billing): add tax id verification warnings for providers
* test(billing): add tax id verification warnings for organizations
* [PM-37064] feat: Add ScheduleBusinessPriceIncrease scheduler entry point
Extends PriceIncreaseScheduler (PM-32645) with a parallel entry point for
Teams/Enterprise 2020 → current plan migrations under epic PM-35215. The
business path consumes a cohort directly, preserves existing discounts,
optionally appends the cohort's proactive coupon, and stamps ScheduledDate
on the assignment row on success.
Existing Schedule renames to SchedulePersonalPriceIncrease across four
call sites; ResolvePhase2Async becomes private. Shared concerns extract
into ActiveScheduleExistsAsync and CreateAndConfigureScheduleAsync helpers.
ScheduleBusinessPriceIncrease has zero production callers — the
UpcomingInvoiceHandler dispatcher branch lands in a follow-up. The new
PM35215_BusinessPlanPriceMigration feature flag defaults off.
* Address PR review: prevent orphan schedules and fix Release flag gate
* feat(billing): add feature flag for automatic tax enforcement
* refactor(billing): remove unused SubscriptionUpdateOptionsExtensions
* refactor(billing): inject IFeatureService into billing services and commands
* feat(billing): conditionalize customer tax exemption logic with feature flag
* feat(billing): conditionally enable Stripe automatic tax in OrganizationBillingService
* test(billing): add unit tests for Stripe automatic tax feature flag
* fix(billing): Run dotnet format
* test(Premium): use class-level IFeatureService mock in UpgradePremiumToOrganizationCommandTests
* refactor(billing): consolidate customer return conditions for automatic tax
* refactor(billing): broaden postal code validation for organization creation
* refactor(billing): remove PM37597 feature flag for automatic tax logic
* Revert "refactor(billing): broaden postal code validation for organization creation"
This reverts commit cddbda838c.
* fix(test): remove outdated test
* Fix null warnings and add more descriptive log in the case or non-successful HTTP calls.
* Add RelayPushRegistration tests
* Skip Push Registration when the client is not a mobile client
* Formatting
* Pin New Dependency
* Assert more in mobile test and use actual mobile device type
* [PM-36949] Add OrganizationPlanMigrationCohort schema and Core domain types
Add the foundation for cohort-based plan migrations:
- Two tables: OrganizationPlanMigrationCohort and OrganizationPlanMigrationCohortAssignment
- Two views and nine stored procedures (four CRUD on cohort, four CRUD plus
ReadByOrganizationId on assignment)
- Single Migrator script for MSSQL deployment
- Core entities, MigrationPath value object and its registry, and bare repository
interfaces under Bit.Core.Billing.Organizations.PlanMigration
The cohort table holds the human-managed metadata (name, discount coupons,
MigrationPathId byte) and the assignment table records each organization's
position in the migration lifecycle (scheduled, migrated, churn-mitigated).
Both Update SPs follow the accept-but-don't-assign pattern: immutable columns
(OrganizationId, CohortId, CreatedAt) are parameters but not SET clauses.
* [PM-36949] Add Dapper repositories for plan migration cohort tables
OrganizationPlanMigrationCohortRepository inherits the base Repository<T, TId>
CRUD methods unchanged. OrganizationPlanMigrationCohortAssignmentRepository
also relies on the base for CRUD and adds GetByOrganizationIdAsync which
returns at most one row (the UNIQUE constraint on OrganizationId at the
database layer guarantees this).
* [PM-36949] Add EF Core configurations, repositories, and provider migrations for plan migration cohort tables
- EF models wrap the Core entities; the assignment model exposes nav properties
for Organization and Cohort so the FK + cascade-delete is inferred by EF.
- EntityTypeConfiguration classes pin ID generation to application code
(ValueGeneratedNever) and declare the UNIQUE indexes plus the composite
(CohortId, ScheduledAt, MigratedAt) index.
- Repositories follow the OrganizationInstallationRepository template; the
assignment repo adds GetByOrganizationIdAsync to mirror the SP exposed on
the MSSQL side.
- DatabaseContext gets two DbSet properties; auto-discovery picks up the
configuration classes.
- Generated migrations for MySQL, Postgres, and SQLite create matching schemas;
EF truncates FK and index names on providers with 64-char identifier limits,
which is consistent with the rest of the codebase.
* [PM-36949] Wire up DI and add tests for plan migration cohort repositories
Register both Dapper and EF Core repositories in their respective service
collection extensions, following the existing AddSingleton convention in
these files.
Add tests:
- MigrationPathIdsSnapshotTests guards the immortal byte IDs that downstream
code pins on. The class- and method-level comments document why these
values can never be renumbered.
- MigrationPathTests covers the FromId round-trip and the null-on-unknown
behavior the registry promises to callers.
- OrganizationPlanMigrationCohortRepositoryTests exercises CRUD, the UNIQUE
Name constraint, and verifies that ReplaceAsync ignores CreatedAt
mutations (per the accept-but-don't-assign Update SP).
- OrganizationPlanMigrationCohortAssignmentRepositoryTests exercises CRUD,
GetByOrganizationIdAsync, the UNIQUE OrganizationId constraint,
cascade-delete from both Organization and Cohort, and verifies that
ReplaceAsync ignores OrganizationId, CohortId, and CreatedAt mutations.
* [PM-36949] Use PlanType and MigrationPathId enums on MigrationPath
Replace the byte Id with a byte-backed MigrationPathId enum and replace
the string FromPlan/ToPlan fields with PlanType. Persistence is
unchanged -- EF normalises enum-backed properties to their underlying
type in the model snapshot, and Dapper handles enum-to-byte parameter
mapping automatically.
* [PM-36949] Add *.lscache to .gitignore
* [PM-36949] fix: Override ReplaceAsync on EF cohort repositories for immutability parity
The Dapper _Update SPs accept-but-don't-assign certain columns (CreatedAt
on cohort; OrganizationId, CohortId, and CreatedAt on assignment), but
the base EF Repository<T,TEntity,Guid>.ReplaceAsync uses SetValues which
writes every scalar. Override on both repos and mark the immutable
properties as IsModified = false so MySQL/Postgres/Sqlite match MSSQL
behavior. Mirrors the existing DeviceRepository.ReplaceAsync pattern.
* [PM-36949] fix: Bound cohort string columns and widen Name to 255 chars
Add [MaxLength] attributes to the three cohort string properties so the
EF providers (MySQL/Postgres/Sqlite) enforce the same limits as MSSQL,
where the columns were already NVARCHAR-capped. Widen Name from 64 to
255 chars across MSSQL DDL, both _Create/_Update SP signatures, the
Migrator script, the entity, and all three regenerated EF migrations.
Coupon codes stay at 64 (Stripe IDs are short).
* [PM-36949] test: Lock FromPlan and ToPlan per MigrationPath
The snapshot test class doc says "byte N means a specific FromPlan ->
ToPlan transition forever", but only the byte value was being asserted.
A silent refactor of the registry's PlanType references would not have
been caught. Add per-path FromPlan/ToPlan assertions to close the gap.
* [PM-36949] chore: Apply dotnet format
* [PM-36949] test: Use LaxDateTimeComparer for round-tripped DateTimes
Postgres timestamp and MySQL datetime(6) store microsecond precision (6
fractional digits), but .NET DateTime is 100ns ticks (7 digits). Exact
Assert.Equal fails by a single tick on round-trip. Switch the three
DateTime comparisons in ReplaceAsync_UpdatesMutableColumns_AndIgnoresImmutableOnes
to LaxDateTimeComparer.Default -- the same 2ms-tolerance comparer used
by SendRepositoryTests and InstallationRepositoryTests for the same
precision issue.
* [PM-36949] refactor: Rename DateTime columns to Date suffix per naming convention
Addresses PR review feedback. Also tightens DATETIME2(7) / NVARCHAR(N) spacing
per the SQL style guide, drops *.lscache from .gitignore (handled by #7648),
and regenerates EF migrations for MySQL, Postgres, and SQLite.
* Apply dotnet format to regenerated migrations
* [PM-36949] chore: Align NULL/NOT NULL columns in cohort tables
* PM-37621 - Fix Device.LastActivityDate surfacing legacy NULL rows as DateTime.UtcNow
Dapper's deserializer skips the property setter when a nullable column is
DBNull, leaving the property at its CLR default. The field initializer
`public DateTime? LastActivityDate { get; internal set; } = DateTime.UtcNow`
poisoned that default, so rows whose LastActivityDate column was NULL (e.g.
devices created before the column existed) read back as the current time.
Drop the initializer, relax `internal set` to `set`, and stamp
LastActivityDate explicitly at the two creation call sites
(DeviceValidator.GetDeviceFromRequest and DeviceRequestModel.ToDevice). Adds
an integration regression test that creates a device with an explicit null
LastActivityDate and asserts the read path surfaces null. Augments
DeviceValidatorTests.GetDeviceFromRequest_RawDeviceInfoValid_ReturnsDevice
to lock in the creation-time stamp.
* PM-37621 - DeviceSeeder - creation should set LastActivityDate
* PM-37166 - Add ClientVersion to Device entity and repository contract
* PM-37166 - Add ClientVersion SQL schema and refactor bump stored procedures
* PM-37166 - Implement combined bump in repositories and add EF migrations
EF snapshot regeneration also absorbs Collection / CollectionGroup /
CollectionUser namespace moves (Bit.Infrastructure.EntityFramework.Models
-> Bit.Infrastructure.EntityFramework.AdminConsole.Models) that were left
un-regenerated by PR #7523 (PM-35489). Namespace-only, no SQL impact;
flagged with the AC team for awareness.
* PM-37166 - Replace DeviceLastActivityCacheService with DeviceDataCacheService
* PM-37166 - Replace BumpDeviceLastActivityDateCommand with BumpDeviceDataCommand
* PM-37166 - Pass ClientVersion through identity request validators
* PM-37166 - Align migration script with SQL style guide
Refresh Device_ReadBy* sprocs after DeviceView change so their cached
schema picks up ClientVersion, swap retired-sproc drops to
DROP PROCEDURE IF EXISTS, and tighten the ALTER TABLE indent in step 1.
* PM-37166 - Rename BumpData to UpdateLastActivity across device write pathway
The "BumpData" naming was vague — "data" named a category, not the thing
being written. Rename to "UpdateLastActivity" everywhere: SP, repositories,
command, cache, validators, tests. "Last activity" names the event of the
device's most recent appearance; LastActivityDate (when) and ClientVersion
(what was running) are facts we observed about that event. ClientVersion is
treated as a property of the activity event rather than an independent value,
so future last-observed properties (last IP, OS, etc.) slot in without renaming.
The SQL layer uses Update* per architect guidance on bitwarden/server#7302;
the Bump* SPs in this codebase are legacy and not being extended. The
extensibility note lives on IUpdateDeviceLastActivityCommand with short
pointers from the SP, repo, and cache.
Cache key prefix changes from device:data: to device:last-activity: — safe
because the cache is only a write-suppression optimization (SP guards ensure
correctness) and entries TTL out within 24h.
Migration renamed to 2026-05-14 to reflect the rewrite.
* PM-37166 - Add UTF-8 BOM to device last activity cache files
Aligns file encoding with the repo's .editorconfig (charset = utf-8-bom for .cs) so dotnet format --verify-no-changes passes.
* PM-37166 - Compare LastActivityDate at second precision in device creation test
GetManyByUserIdWithDeviceAuth_ReturnsLastActivityDate_ForNewDeviceAsync was
flaking on SqlServer: Dapper binds DateTime params as legacy `datetime`
(~3.33ms granularity), so the entity initializer's UtcNow can be rounded a
few ms earlier than the in-memory `beforeCreation` capture, making a strict
>= comparison occasionally false. Truncate both sides to the second to
absorb that drift while still rejecting stale or defaulted values.
* PM-37166 - Rename Device.ClientVersion EF migration
Renames migration class/files from AddDeviceClientVersionRefactorDeviceDataBump
to AddDeviceClientVersion to drop stale "Bump" terminology and the misleading
"RefactorDeviceData" prefix. The EF migration only adds the ClientVersion
column; the BumpData -> UpdateLastActivity SP refactor lives in MSSQL .sql
files and has no EF representation.
* PM-37166 - Document null-is-no-op semantics for ClientVersion on IUpdateDeviceLastActivityCommand
Tighten the interface-level summary and add a <param> note clarifying that
a null clientVersion is treated as "no opinion" and will not clear an
existing stored value.
* PM-37166 - Regenerate Device.ClientVersion EF migration on post-#7634 baseline
PR #7634 merged AddLastApiKeyRotationDateToUserTable into main while this
branch was open. The prior AddDeviceClientVersion migration's frozen model
snapshot (its .Designer.cs) was generated before that PR landed, so it did
not include User.LastApiKeyRotationDate. Applying migrations incrementally
against that stale snapshot would produce an inconsistent model graph.
Regenerated AddDeviceClientVersion on top of the merged-from-main baseline
so the new .Designer.cs files include both columns. The migration body
itself still only adds Device.ClientVersion; the top-level
DatabaseContextModelSnapshot.cs files were already correct from git's
three-way merge.
New timestamps (20260514192xxx) come after the User migration
(20260514011xxx), preserving migration order.
* PM-37166 - util/Migrator/DbScripts/2026-05-14_00_AddDeviceClientVersionAndUpdateLastActivitySp.sql - fix wrong comment
* PM-37166 - Bump Device.ClientVersion column width from 20 to 43
43 is the upper bound of Version.ToString() for any input parseable by
Version.TryParse — four Int32 components (Int32.MaxValue = 10 digits)
joined by 3 dots. Sizing to the type's mathematical max prevents
SQL Server error 8152 on malformed/hostile Bitwarden-Client-Version
headers without paying the cost of normalization at the call sites.
Real Bitwarden CalVer (YYYY.M.B) remains well within bounds at ~9 chars.
- Device.cs [MaxLength] + entity doc comment
- SSDT table + 4 stored procedures
- Cloud migration ALTER TABLE + SP parameters
- EF migrations regenerated for MySQL / Postgres / SQLite
* PM-37166 - Defer dropping old single-column UpdateLastActivityDate SPs to follow-up
Server and DB deploys are decoupled, so dropping the old SPs in the same migration that
introduces the new combined ones would break server rollback. Per discussion on PR #7632:
- Remove DROP PROCEDURE statements from the migration; replace with a note explaining the deferral.
- Restore the old Device_UpdateLastActivityDate{ById,ByIdentifierUserId}.sql files in src/Sql/dbo
so the SSDT source-of-truth stays aligned with deployed schema (EDD).
A follow-up ticket will drop the old SPs and delete the .sql files together once we're
confident no deployed server version still calls them.
* PM-37166 - Pass @LastActivityDate into Device_UpdateLastActivity SPs
Bitwarden convention is to compute timestamps in the application layer
and pass them as DATETIME2(7) params, not call GETUTCDATE() inside SPs.
Dapper repo now computes DateTime.UtcNow locally (matching the EF repo
and UserRepository.cs precedent) and passes LastActivityDate through.
* PM-37165 - Add LastApiKeyRotationDate column to User
Adds a nullable DATETIME2(7) LastApiKeyRotationDate column on the User
table alongside the other Last*Date audit columns. Covers the MSSQL
table, view, User_Create / User_Update stored procedures (new optional
parameter, EDD-safe with default NULL), the SSDT source-of-truth, and
EF migrations for MySql, Postgres, and Sqlite.
Repository round-trip integration tests verify that CreateAsync
defaults the column to NULL and ReplaceAsync persists it across all
four providers.
* PM-37165 - Add RotateUserApiKeyCommand under Auth/UserFeatures
Extracts user API key rotation out of UserService into a new CQS
command at src/Core/Auth/UserFeatures/UserApiKey/, mirroring the
existing decomposition pattern for other Auth user features. The
command generates a new 30-char ApiKey, bumps RevisionDate, sets
LastApiKeyRotationDate, and persists via IUserRepository.ReplaceAsync.
Adds the PM37165_RotateUserApiKeyCommand feature flag so the new path
can be rolled out behind a flag in a follow-up commit. Registers the
command via AddUserApiKeyCommands inside AddUserServices.
Unit tests verify the command assigns a fresh key, updates both
RevisionDate and LastApiKeyRotationDate to the same recent UTC value,
and calls ReplaceAsync exactly once.
* PM-37165 - Flag-gate rotate-api-key endpoint to new command
Wires AccountsController.RotateApiKey to dispatch between
IRotateUserApiKeyCommand (flag on) and the legacy
UserService.RotateApiKeyAsync (flag off) based on
PM37165_RotateUserApiKeyCommand. Both paths preserve the existing
auth and secret-verification guards, which run before the flag
branch.
Marks IUserService.RotateApiKeyAsync and its implementation [Obsolete]
pointing callers at IRotateUserApiKeyCommand, with TODOs tying their
removal to the flag cleanup. The body of the legacy method is
deliberately unchanged so it does NOT write LastApiKeyRotationDate
while the flag is off; that genuinely gates the new behavior so the
ramp is observable and reversible. The single remaining call site
(the controller fallback) is wrapped in #pragma warning disable
CS0618 so the attribute continues to flag any new callers.
Tests:
- AccountsControllerTests: dispatch tests for both flag states; the
auth and bad-secret guard tests are parameterized over flag state.
Pre-existing typo in two tests that called _sut.ApiKey() instead of
_sut.RotateApiKey() is fixed.
- UserServiceTests: regression test locks in the legacy non-write
behavior so it cannot drift before the flag is removed.
- AccountsControllerTest (integration): three endpoint tests cover
flag-off (LastApiKeyRotationDate stays NULL), flag-on (column is
populated), and bad-secret over both flag states (no rotation
occurs).
Each flag-state-specific test carries a TODO breadcrumb describing
the exact rename or deletion when the flag is cleaned up.
* PM-37165 - Tweak comment
* PM-37165 - Move LastApiKeyRotationDate to end of User schema
Append the new column to the end of User.sql, UserView.sql, the
matching CREATE OR ALTER VIEW in the migrator script, and the User
entity so SSDT mirrors what ALTER TABLE ADD produces in production.
* expose the ExemptFromBillingAutomation to client
* Fix the failing database
* Rename the file to resolve chronological order error
* Renamed the migration file name
* fix the failing database
* Suppress warnings for exempt orgs at the query level
Gate InactiveSubscription and ResellerRenewal warnings inside
GetOrganizationWarningsQuery on Organization.ExemptFromBillingAutomation
instead of plumbing the field through the profile response, view models,
EF queries, and SQL views.
* Add the ExemptFromBillingAutomation property
---------
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
This produces inherently flaky tests because it works by
fully migrating a database, and then re-running a specified
migration (the data migration) out of order. This means that
subsequent changes to the db schema will cause the migration
tests to fail, because the data migration will be re-run after
the schema has changed. Needs to be fixed before use.
* [PM-31781] skip unpaid automations for exempt orgs
* removing check for subscription create since it's impossible to be unpaid with that billing reason
* pr feedback
* Add subscriber existence guard to SubscriptionUpdatedHandler
* conflict resolution fixup
* pr feedback fix: don't update stale entities
---------
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>