Compare commits

...

450 commits

Author SHA1 Message Date
Todd Martin
94cd6fbff6
chore(flags): [PM-28337] Remove account recovery permission feature flag
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Removed pm-24425-send-2fa-failed-email

* Remove feature flag

* Linting

* Removed tests and cleaned up comment.
2026-01-11 12:04:10 -05:00
Ike
5320878295
[PM-25949] ExternalCallback Integration tests for SSO Project (#6809)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* feat: add new integration test project

* test: add factory for SSO application; ExternalCallback integration tests.

* test: modified Integration tests to use seeded data instead of service substitutes with mocked responses, where possible.

* fix: re-organize projects in solution. SsoFactory now in its owning project with SSO integration test which match the integration test factory pattern more closely.

* claude: better naming of class fields.
2026-01-10 09:02:50 -05:00
cyprain-okeke
e705fe3f3f
[PM-29598] Create Subscription Upgrade Endpoint (#6787)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Add the ticket implementation

* Add the unit test

* Fix the lint and test issues

* resolve pr comments

* Fix the error on the test file

* Review suggestion and fixes

* resolve the api access comments

* Gte the key from the client

* Add the gateway type as stripe

* Address the legacy plans issues

* Resolve the misunderstanding

* Add additional storage that we will need if they revert

* Add the previous premium UserId
2026-01-09 16:34:06 +01:00
Jimmy Vo
b1cf59b1bf
[PM-27882] Fix the subject line. (#6818) 2026-01-09 10:04:52 -05:00
Maciej Zieniuk
2e92a53f11
[PM-27281] Support v2 account encryption on JIT master password signups (#6777)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
* V2 prep, rename existing SSO JIT MP command to V1

* set initial master password for account registraton V2

* later removel docs

* TDE MP onboarding split

* revert separate TDE onboarding controller api

* Server side hash of the user master password hash

* use `ValidationResult` instead for validation errors

* unit test coverage

* integration test coverage

* update sql migration script date

* revert validate password change

* better requests validation

* explicit error message when org sso identifier invalid

* more unit test coverage

* renamed onboarding to set, hash naming clarifications

* update db sql script, formatting

* use raw json as request instead of request models for integration test

* v1 integration test coverage

* change of name
2026-01-09 09:17:45 +01:00
Matt Bishop
62ae828143
Fix pattern matching when retrieving database migrations (#6815)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2026-01-08 13:56:17 -05:00
Vince Grassia
ce4b906bdf
Update job names (#6814)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2026-01-08 12:29:47 -05:00
Alex Morask
6d69c9bb99
Make PayPalIPNTransactionModel currency culture invariant (#6811) 2026-01-08 08:40:45 -06:00
Patrick-Pimentel-Bitwarden
8387996844
chore(flags): Add pm-23801-prefetch-password-prelogin feature flag
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2026-01-07 17:28:34 -05:00
Jordan Aasen
02c03f4493
[PM-27884][PM-27886][PM-27885] - Add Cipher Archives (#6578)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
* add Archives column to ciphers table

* add archives column

* update cipher archive/unarchive and cipher deatils query

* add migrations

* add missing migrations

* fixes

* update tests. cleanup

* syntax fix

* fix sql syntax

* fix sql

* fix CreateWithCollections

* fix sql

* fix migration file

* fix migration

* add go

* add missing go

* fix migrations

* add missing proc

* fix migrations

* implement claude suggestions

* fix test

* update cipher service and tests

* updates to soft delete

* update UserCipherDetailsQuery and migration

* update migration

* update archive ciphers command to allow org ciphers to be archived

* updates to archivedDate

* revert change to UserCipherDetails

* updates to migration and procs

* remove archivedDate from Cipher_CreateWithCollections

* remove trailing comma

* fix syntax errors

* fix migration

* add double quotes around datetime

* fix syntax error

* remove archivedDate from cipher entity

* re-add ArchivedDate into cipher

* fix migration

* do not set Cipher.ArchivedDate in CipherRepository

* re-add ArchivedDate until removed from the db

* set defaults

* change to CREATE OR ALTER

* fix migration

* fix migration file

* quote datetime

* fix existing archiveAsync test. add additional test

* quote datetime

* update migration

* do not wrap datetime in quotes

* do not wrap datetime in quotes

* fix migration

* clean up archives and archivedDate from procs

* fix UserCipherDetailsQuery

* fix setting date in JSON_MODIFY

* prefer cast over convert

* fix cipher response model

* re-add ArchivedDate

* add new keyword

* remove ArchivedDate from entity

* use custom parameters for CipherDetails_CreateWithCollections

* remove reference to archivedDate

* add missing param

* add missing param

* fix params

* fix cipher repository

* fix migration file

* update request/response models

* update migration

* remove Archives from Cipher_CreateWithCollections

* revert last change

* clean up

* remove comment

* remove column in migration

* change language in drop

* wrap in brackets

* put drop column in separate migration

* remove archivedDate column

* re-add archivedDate

* add refresh module

* bump migration name

* fix proc and migration

* do not require edit permission for archiving ciphers

* do not require edit permission for unarchiving ciphers
2026-01-07 09:29:10 -08:00
cd-bitwarden
afd47ad085
[SM-1570] Adding new item to organization license to disable SM ads for users (#6482)
* Adding new item to organization license

* fixing whitespace issues

* fixing missing comment

* fixing merge conflicts

* merge fix

* db merge fixes

* fix

* Updating SM to Sm, and adding more view refreshes

* fixing merge conflicts

* Redoing migration

* Update OrganizationLicense.cs

* Update OrganizationLicense.cs

* fixes

* fixes

* fixing db issues

* fix

* rearranging sql after merge conflicts

* Merge conflicts with dbscripts are fixed, adding missing usedisableSMadsForUsers where needed

* removing incorrect merge fix

* fixes

* adding feature flag to disable sm ads

---------

Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com>
2026-01-07 09:42:10 -07:00
Kyle Denney
3f2ee5b029
[PM-30460] update storage job to also update database max storage (#6803)
* [PM-30460] update storage job to also update database max storage

* dry run logs

* more logging fixes and pr feedback, forgot sql scripts

* claude feedback

* pr feedback, redesign of entity id reverse lookup

* claude feedback
2026-01-07 10:38:27 -06:00
renovate[bot]
f2aa742f76
[deps]: Update dtolnay/rust-toolchain digest to f7ccc83 (#6497)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 13:23:02 +01:00
renovate[bot]
d86717eedb
[deps]: Update actions/setup-dotnet action to v5.0.1 (#6805)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 12:09:12 +01:00
renovate[bot]
46e9b18905
[deps]: Update github/codeql-action action to v4.31.9 (#6806)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 11:08:26 +00:00
renovate[bot]
ad6555c221
[deps] Tools: Update MailKit to 4.14.1 (#6621)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 11:32:15 +01:00
Nik Gilmore
a6e034a88c
[PM-27632] Add feature flag pm-27632-cipher-crud-operations-to-sdk (#6790)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2026-01-06 14:26:32 -08:00
Jimmy Vo
63784e1f5f
[PM-27882] Add SendOrganizationConfirmationCommand (#6743) 2026-01-06 16:43:36 -05:00
Stephon Brown
530d946857
[PM-29061] Remove Feature Flag 24996 (#6744)
* refactor(billing): Remove flag uses

* refactor(billing): remove redundant endpoint

* chore(billing): remove the flag

* fix(billing): revert changes

* Revert "fix(billing): revert changes"

This reverts commit 92271b380c.

* test(billing): update tests
2026-01-06 20:51:43 +00:00
aj-bw
5e735c8474
after a convo with bre & sre, remove long failing workflow. registry cleanup has moved into automated processes in azure (#6804) 2026-01-06 20:09:28 +00:00
Jeffrey Holland
2026ca103b
Remove unused feature flags for Autofill (#6749)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2026-01-06 12:05:48 -05:00
Vijay Oommen
f797825de1
PM-28947 remove feature flag (#6799) 2026-01-06 09:00:12 -06:00
Rui Tomé
1b17d99bfd
[PM-29555] Add self-revoke endpoint for declining organization data ownership policy (#6739)
* Add OrganizationUser_SelfRevoked event type to EventType enum

* Add SelfRevokeOrganizationUserCommand implementation and interface for user self-revocation from organizations

* Add unit tests for SelfRevokeOrganizationUserCommand to validate user self-revocation logic, including success scenarios and various failure conditions.

* Add ISelfRevokeOrganizationUserCommand registration to OrganizationServiceCollectionExtensions for user self-revocation functionality

* Add self-revoke user functionality to OrganizationUsersController with new endpoint for user-initiated revocation

* Add integration tests for self-revoke functionality in OrganizationUsersController, covering scenarios for eligible users, non-members, and users with owner/admin roles.

* Add unit test for SelfRevokeOrganizationUserCommand to validate behavior when a user attempts to self-revoke without confirmation. This test checks for a BadRequestException with an appropriate message.

* Add MemberRequirement class for organization membership authorization

- Implemented MemberRequirement to check if a user is a member of the organization.
- Added unit tests for MemberRequirement to validate authorization logic for different user types.

* Update authorization requirement for self-revoke endpoint and add integration test for provider users

- Changed authorization attribute from MemberOrProviderRequirement to MemberRequirement in the RevokeSelfAsync method.
- Added a new integration test to verify that provider users who are not members receive a forbidden response when attempting to revoke themselves.

* Add EligibleForSelfRevoke method to OrganizationDataOwnershipPolicyRequirement

- Implemented the EligibleForSelfRevoke method to determine if a user can self-revoke their data ownership based on their membership status and policy state.
- Added unit tests to validate the eligibility logic for confirmed, invited, and non-policy users, as well as for different organization IDs.

* Refactor self-revoke user command to enhance eligibility checks

- Updated the SelfRevokeOrganizationUserCommand to utilize policy requirements for determining user eligibility for self-revocation.
- Implemented checks to prevent the last owner from revoking themselves, ensuring organizational integrity.
- Modified unit tests to reflect changes in eligibility logic and added scenarios for confirmed owners and admins.
- Removed deprecated policy checks and streamlined the command's dependencies.

* Use CommandResult pattern in self-revoke command

* Clearer documentation
2026-01-06 11:25:14 +00:00
Vincent Salucci
35868c2a65
[PM-22434] Remove CreateDefaultLocation feature flag references (#6758)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* chore: remove ff ref from PoliciesController

* chore: remove ff ref from OrganizationExportController, refs PM-22434

* chore: remove ff ref from CiphersController, refs PM-22434

* chore: remove ff ref from ConfirmOrganizationUserCommand, refs PM-22434

* chore: remove ff refs from OrganizationDataOwnershipPolicyValidator, refs PM-22434

* chore: remove ff ref from OrganizationUserControllerTests, refs PM-22434

* chore: remove ff refs from ConfirmOrganizationUserCommandTests, refs PM-22434

* chore: remove ff refs from OrganizationDataOwnershipPolicyValidatorTests, refs PM-22434

* chore: format, refs PM-22434
2026-01-05 17:27:17 -06:00
Kyle Spearrin
2442d2dabc
[PM-30391] fix for org context on sso provisioning (#6797)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* fix for org context on sso provisioning

* tests are no longer needed since there is no logic on feature flag

* lint fixes
2026-01-05 11:56:31 -05:00
cyprain-okeke
76a8f0fd95
[PM 29610]Update Account Storage Endpoint (#6750)
* update account storage endpoint

* Fix the failing test

* Added flag and refactor base on pr comments

* fix the lint error

* Resolve the pr comments

* Fix the failing test

* Fix the failing test

* Return none

* Resolve the lint error

* Fix the failing test

* Add the missing test

* Formatting issues fixed
2026-01-05 10:52:52 -06:00
Thomas Rittson
e9d53c0c6b
[PM-30298] Initial documentation for OrganizationAbility pattern (#6781)
Some checks failed
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
2026-01-03 07:48:34 +10:00
Ike
a2ba5289fa
[PM-29568] Footer Links lack correct styling (#6784)
* fix: color weight of links in footer.

* fix: css styling
2026-01-02 12:02:57 -05:00
✨ Audrey ✨
484a8e42dc
[PM-21918] update send api models to support new email field (#5895)
Some checks failed
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
* update send api models to support new `email` field

* normalize authentication field evaluation order

* document send response converters

* add FIXME to remove unused constructor argument

* add FIXME to remove unused constructor argument

* introduce `tools-send-email-otp-listing` feature flag

* add `ISendOwnerQuery` to dependency graph

* fix broken tests

* added AuthType prop to send related models with test coverage and debt cleanup

* dotnet format

* add migrations

* dotnet format

* make SendsController null safe (tech debt)

* add AuthType col to Sends table, change Emails col length to 4000, and run migrations

* dotnet format

* update SPs to expect AuthType

* include SP updates in migrations

* remove migrations not intended for merge

* Revert "remove migrations not intended for merge"

This reverts commit 7df56e346a.

undo migrations removal

* extract AuthType inference to util method and remove SQLite file

* fix lints

* address review comments

* fix incorrect assignment and adopt SQL conventions

* fix column assignment order in Send_Update.sql

* remove space added to email list

* assign SQL default value of NULL to AuthType

* update SPs to match migration changes

---------

Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Co-authored-by: Alex Dragovich <46065570+itsadrago@users.noreply.github.com>
Co-authored-by: John Harrington <84741727+harr1424@users.noreply.github.com>
2025-12-31 13:37:42 -07:00
Brant DeBow
cc03842f5f
Disable database distributed cache in non-self-hosted environments (#6783)
* Disable database distributed cache in non-self-hosted environments

* Added distributed memory cache as a final fallback option
2025-12-31 11:59:59 -05:00
cyprain-okeke
665be6bfb0
[PM-29611] Decouple License from Subscription Response (#6768)
* implement the ticket request

* resolve the build lint error

* Resolve the build lint error

* Address review comments

* Fixt the lint and failing unit test

* Fix NSubstitute mock - use concrete ClaimsPrincipal instead of Arg.Any in Returns()

* resolve InjectUser issues

* Fix the failing testing

* Fix the failing unit test
2025-12-31 17:30:41 +01:00
Mick Letofsky
bf08bd279c
Revert review Code Triggered by labeled event (#6785) 2025-12-31 16:56:03 +01:00
Ike
f82552fba9
[PM-29568] Fix footer styling (#6722)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Database testing / Run tests (push) Waiting to run
Database testing / Run validation (push) Waiting to run
Database testing / Validate new migration naming and order (push) Waiting to run
Testing / Run tests (push) Waiting to run
* fix: update footer background color to match UIF Tailwind standards.

* fix: modify spacing for footer

* chore: build templates

* fix: update social icon assets

* fix: update footer image source

* fix: update send access
2025-12-30 14:08:10 -05:00
Mick Letofsky
c98f31a9f7
Review Code Triggered by labeled event (#6782) 2025-12-30 18:22:09 +01:00
Brant DeBow
86a68ab637
Move all event integration code to Dirt (#6757)
* Move all event integration code to Dirt

* Format to fix lint
2025-12-30 10:59:19 -05:00
Isaiah Inuwa
9a340c0fdd
Allow mobile clients to create passkeys (#6383) [PM-26177]
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Allow mobile clients to create vault passkeys

* Document uses for authorization policies
2025-12-30 07:31:26 -06:00
Jason Ng
34b4dc3985
[PM-29650] retain item archive date on softdelete (#6771)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-12-29 13:30:57 -05:00
Shane Melton
3b5bb76800
[PM-28747] Storage limit bypass for enforce organization ownership policy (#6759)
* [PM-28747] Bypass storage limit when enforce organization data ownership policy is enabled

* [PM-28747] Unit tests for storage limit enforcement

* [PM-28747] Add feature flag check

* [PM-28747] Simplify ignore storage limits policy enforcement

* [PM-28747] Add additional test cases
2025-12-29 09:30:22 -08:00
Dave
2dc4e9a420
feat(2fa-webauthn) [PM-20109]: Increase 2FA WebAuthn Security Key Limit (#6751)
* feat(global-settings) [PM-20109]: Add WebAuthN global settings.

* feat(webauthn) [PM-20109]: Update maximum allowed WebAuthN credentials to use new settings.

* test(webauthn) [PM-20109]: Update command tests to use global configs.

* feat(global-settings) [PM-20109]: Set defaults for maximum allowed credentials.

* feat(two-factor-request-model) [PM-20109]: Remove hard-coded 5 limit on ID validation.

* Revert "test(webauthn) [PM-20109]: Update command tests to use global configs."

This reverts commit ba9f0d5fb6.

* Revert "feat(webauthn) [PM-20109]: Update maximum allowed WebAuthN credentials to use new settings."

This reverts commit d2faef0c13.

* feat(global-settings) [PM-20109]: Add WebAuthNSettings to interface for User Service consumption.

* feat(user-service) [PM-20109]: Add boundary and persistence-time validation for maximum allowed WebAuthN 2FA credentials.

* test(user-service) [PM-20109]: Update tests for WebAuthN limit scenarios.

* refactor(user-service) [PM-20109]: Typo in variable name.

* refactor(user-service) [PM-20109]: Remove unnecessary pending check.

* refactor(user-service) [PM-20109]: Pending check is necessary.

* refactor(webauthn) [PM-20109]: Re-spell WebAuthN => WebAuthn.

* refactor(user-service) [PM-20109]: Re-format pending checks for consistency.

* refactor(user-service) [PM-20109]: Fix type spelling in comments.

* test(user-service) [PM-20109]: Combine premium and non-premium test cases with AutoData.

* refactor(user-service) [PM-20109]: Swap HasPremiumAccessQuery in for CanAccessPremium.

* refactor(user-service) [PM-20109]: Convert limit check to positive, edit comments.
2025-12-29 11:55:05 -05:00
Todd Martin
0f104af921
chore(deps): Move Cosmos cache to Auth ownership 2025-12-29 10:00:05 -05:00
renovate[bot]
8a79bfa673
[deps]: Update actions/upload-artifact action to v6 (#6766)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 12:59:46 +01:00
Todd Martin
bf5cacdfc5
chore(dependencies): Ignore minor updates for Platform deps
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-12-28 13:26:07 -05:00
Derek Nance
0cfb68336b
[PM-28025] Revert "chore(feature-flag): [PM-19665] Remove web-push feature flag" (#6779)
Some checks failed
Scan / Check PR run (push) Has been cancelled
Collect code references / Check for secret access (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
This reverts commit 1c60b805bf.
2025-12-26 16:44:34 -06:00
Thomas Rittson
67534e2cda
[PM-29556] Fix: changing organization plan nulls out public and private keys (#6738)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Main fix: only assign new key value where old keys are not set
and new keys have been provided.

Refactors:
- use consistent DTO model for keypairs
- delete duplicate property assignment for new orgs
2025-12-26 10:13:12 +10:00
renovate[bot]
96622d7928
[deps]: Update github-action minor (#6327)
Some checks failed
Scan / Check PR run (push) Has been cancelled
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Collect code references / Check for secret access (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 16:34:19 -05:00
Tyler
f80a5696a1
BRE-1005 docs(README): add dynamic badges for container image digests (#6769)
BRE-1005 docs(README): add dynamic badges for container image digests
* remove links to packages
2025-12-23 16:02:17 -05:00
Jordan Aasen
3486d29330
remove RemoveCardItemTypePolicy flag (#6760)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-12-23 09:12:14 -08:00
renovate[bot]
c632a9490a
[deps] Platform: Update Azure.Messaging.EventGrid to v5 (#6215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 10:51:54 -06:00
Andy Pixley
fafc61d7b9
[BRE-1439] Removing obsolete Server image from publish workflow (#6774)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-12-23 00:00:17 -05:00
Jared Snider
a82365b5df
PM-30125 - IdentityTokenResponse - mark fields as deprecated (#6773)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-12-22 15:29:28 -05:00
Alex Morask
2dce8722d6
Remove unused FF (#6709) 2025-12-22 14:14:18 -06:00
sneakernuts
ae3c8317e3
SRE-3582 billing cleanup (#6772) 2025-12-22 13:09:23 -07:00
cyprain-okeke
eb360ffec1
[PM-29930]Fix [Defect] Automatic Sync - Sync License throws error on Self Host (#6770)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Restore the mistakenly remove controller

* Fix the lint build error
2025-12-22 17:28:27 +01:00
Thomas Rittson
69d72c2ad3
[PM-28485] Move organization events domain to DIRT code ownership (#6685)
Some checks failed
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
2025-12-20 07:32:51 +10:00
renovate[bot]
bc800a788e
[deps]: Update actions/checkout action to v6 (#6706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 16:06:33 -05:00
Jordan Aasen
457e293fdc
[PM-29017] - improve logic for cipher SaveDetailsAsync validation (#6731)
* improve logic for cipher SaveDetailsAsync validation. fix tests

* revert change

* fix test

* remove duplicate semicolon
2025-12-19 11:35:01 -08:00
Jared Snider
e6c97bd850
Revert "refactor(IdentityTokenResponse): [Auth/PM-3287] Remove deprecated res…" (#6755)
Some checks failed
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
This reverts commit bbe682dae9.
2025-12-18 15:10:40 -05:00
Thomas Rittson
cc2d69e1fe
[PM-28487] Move Events and EventsProcessor to DIRT ownership (#6678)
* Move Events and EventsProcessor to DIRT ownership

* include test projs

* sort lines alphabetically within group

* fix order

---------

Co-authored-by: Graham Walker <ghwtx@icloud.com>
2025-12-18 14:05:18 -06:00
Mike Amirault
1b41a06e32
[PM-29780] Add feature flag for Send email OTP verification (#6742) 2025-12-18 14:12:56 -05:00
Maciej Zieniuk
a92d7ac129
[PM-27280] Support v2 encryption on key-connector signups (#6712)
* account v2 registration for key connector

* use new user repository functions

* test coverage

* integration test coverage

* documentation

* code review

* missing test coverage

* fix failing test

* failing test

* incorrect ticket number

* moved back request model to Api, created dedicated data class in Core

* sql stored procedure type mismatch, simplification

* key connector authorization handler
2025-12-18 13:43:03 -05:00
cyprain-okeke
2b742b0343
[PM-27605] Populate MaxStorageGbIncreased for storage increase from 1GB to 5GB. (#6579)
* Add transition migration to populate MaxStorageGbIncreased

This migration populates the MaxStorageGbIncreased column for Users and
Organizations by setting it to MaxStorageGb + 4, representing the storage
increase from 1GB to 5GB.

This migration depends on PM-27603 being deployed first to create the
MaxStorageGbIncreased column.

Target release: January 6th, 2026

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Using batched updates to reduce lock

* Add changes base on ticket adjustment

* Added the dependency check

* Add temporary index for performance

* Resolved the conflict

* resolve the syntax error

* Update the migration script

* Rename the file to updated date

* Revert the existing merge file change

* revert the change

* revert renaming

* rename file to updated date

* Add the column after renaming

* Rename other migration file to set current date

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alex Morask <amorask@bitwarden.com>
Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
2025-12-18 13:32:03 -05:00
Shane Melton
3511ece899
[PM-28746] Add support for Organization_ItemOrganization_Accepted/Declined event types (#6747) 2025-12-18 10:20:46 -08:00
Mick Letofsky
2707a965de
Remove additional code review prompt file (#6754) 2025-12-18 19:19:19 +01:00
cyprain-okeke
25eface1b9
Remove the feature flag (#6720) 2025-12-18 17:35:56 +01:00
Alex Morask
982957a2be
[PM-21421] Support legacy > current plan transition when resubscribing (#6728)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Refactor RestartSubscriptionCommand to support legacy > modern plan transition

* Run dotnet format

* Claude feedback

* Claude feedback
2025-12-18 09:12:16 -06:00
renovate[bot]
d03277323f
[deps]: Update actions/stale action to v10 (#6335)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Database testing / Run tests (push) Waiting to run
Database testing / Run validation (push) Waiting to run
Database testing / Validate new migration naming and order (push) Waiting to run
Testing / Run tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 15:56:13 -05:00
neuronull
8aa8bba9a6
Add feature flag for windows desktop autotype GA (#6717) 2025-12-17 13:29:06 -07:00
aj-bw
3cb8472fd2
adding platform tag to optimze build, avoiding unnecessary emulation (#6745) 2025-12-17 14:31:21 -05:00
Brant DeBow
b3437b3b30
Update requirements for RabbitMQ and Azure Service Bus configuration (#6741) 2025-12-17 13:00:05 -05:00
renovate[bot]
19ee4a0054
[deps] BRE: Update rabbitmq Docker tag to v4.2.0 (#4026)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 12:42:33 -05:00
Jared McCannon
de504d800b
[PM-24055] - Collection Users and Groups null on Public response (#6713)
* Integration test around getting and saving collection with group/user permissions

* This adds groups to the collections returned.

* Added new stored procedures so we don't accidentally wipe out access due to null parameters.

* wrapping all calls in transaction in the event that there is an error.
2025-12-17 11:34:17 -06:00
Brant DeBow
886ba9ae6d
Refactor IntegrationHandlerResult to provide more detail around failures (#6736)
* Refactor IntegrationHandlerResult to provide more detail around failures

* ServiceUnavailable now retryable, more explicit http status handling, more consistency with different handlers, additional xmldocs

* Address PR feedback
2025-12-17 11:43:53 -05:00
Jared Snider
bbe682dae9
refactor(IdentityTokenResponse): [Auth/PM-3287] Remove deprecated resetMasterPassword property from IdentityTokenResponse (#6676)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-12-17 10:34:12 -05:00
Jared McCannon
00c4ac2df1
[PM-29840] Correcting Auto-Confirm Org Accept User Flow (#6740)
* Populating org user userId and adding to allOrgUser list.

* Having validator check organization user existence based off email or userid.
2025-12-17 09:19:37 -06:00
Alex Morask
04efe402be
[PM-28128] Create transaction for bank transfer charges (#6691)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Create transaction for charges that were the result of a bank transfer

* Claude feedback

* Run dotnet format
2025-12-16 10:12:56 -06:00
Kyle Denney
794240f108
[PM-29732] (fix) storage job no longer ignores trialing and past_due statuses (#6737) 2025-12-16 09:58:57 -06:00
Alex Morask
39a6719361
[PM-27117] Sync Stripe Customer details for Organizations and Providers in API & Admin (#6679)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
* Sync Stripe customer details for Provider / Organization in API & Admin

* Remove unnecessary var

* Fix logical operator

* Remove customer ID check from callers

* Fix failing tests

* Missed conflicts
2025-12-16 07:59:05 -06:00
cyprain-okeke
2ecd6c8d5f
Fix the duplicate issue (#6711) 2025-12-16 13:15:53 +01:00
Rui Tomé
f7c615cc01
[PM-21411] Refactor interface for determining premium status and features (#6688)
* Removed 2FA user interface from premium method signatures

* Added some more comments for clarity and small touchups.

* Add PremiumAccessCacheCheck feature flag to Constants.cs

* Add IPremiumAccessQuery interface and PremiumAccessQuery implementation for checking user premium access status

* Add unit tests for PremiumAccessQuery to validate user premium access logic

* Add XML documentation to Premium in OrganizationUserUserDetails and User classes

* Add PremiumAccessQueries to UserServiceCollectionExtensions

* Refactor TwoFactorIsEnabledQuery to incorporate PremiumAccessQuery and feature flag for premium access checks. Enhanced user premium status retrieval logic and improved handling of user details based on feature flag state.

* Mark methods in IUserRepository and IUserService as obsolete, directing users to new methods in IPremiumAccessQuery for premium access checks.

* Rename CanAccessPremiumBulkAsync to CanAccessPremiumAsync in IPremiumAccessQuery

* Update TwoFactorIsEnabledQuery to use CanAccessPremiumAsync for premium status checks

* Refactor TwoFactorIsEnabledQuery to introduce VNextAsync methods for improved premium access checks and user detail handling. Removed obsolete feature service dependency and enhanced test coverage for new functionality.

* Refactor IPremiumAccessQuery and PremiumAccessQuery to remove the overloaded CanAccessPremiumAsync method. Update related methods to streamline premium access checks using the User object directly. Enhance test coverage by removing obsolete tests and ensuring proper functionality with the new method signatures.

* Add new sync static method to determine if TwoFactor is enabled

* Enhance XML documentation for Premium property in OrganizationUserUserDetails and User classes to clarify its usage and limitations regarding personal and organizational premium access.

* Refactor IPremiumAccessQuery and PremiumAccessQuery to replace User parameter with Guid for user ID in CanAccessPremiumAsync methods. Update related methods and tests to streamline premium access checks and improve clarity in method signatures.

* Update feature flag references in IUserRepository and IUserService to use 'PremiumAccessQuery' instead of 'PremiumAccessCacheCheck'. Adjust related XML documentation for clarity on premium access methods.

* Rename IPremiumAccessQuery to IHasPremiumAccessQuery and move to Billing owned folder

* Remove unnecessary whitespace from IHasPremiumAccessQuery interface.

* Refactor HasPremiumAccessQuery to throw NotFoundException for null users

* Add NotFoundException handling in HasPremiumAccessQuery for mismatched user counts

* Refactor TwoFactorIsEnabledQuery to optimize premium access checks and improve two-factor provider handling. Introduced bulk fetching of premium status for users with only premium providers and streamlined the logic for determining if two-factor authentication is enabled.

* Refactor TwoFactorIsEnabledQueryTests to enhance clarity and optimize test scenarios. Consolidated test cases for two-factor authentication, improved naming conventions, and ensured premium access checks are only performed when necessary.

* Add UserPremiumAccess model to represent user premium access status from personal subscriptions and memberships

* Add User_ReadPremiumAccessByIds stored procedure and UserPremiumAccessView view to enhance premium access retrieval. Updated Organization table index to include UsersGetPremium for optimized queries.

* Add SQL migration script

* Add premium access retrieval methods to IUserRepository and implementations in UserRepository classes. Introduced GetPremiumAccessByIdsAsync and GetPremiumAccessAsync methods to fetch premium status for multiple users and a single user, respectively. Updated using directives to include necessary models.

* Refactor HasPremiumAccessQuery and IHasPremiumAccessQuery to streamline premium access checks. Updated method names for clarity and improved documentation. Adjusted test cases to reflect changes in user premium access retrieval logic.

* Update IUserRepository to reflect new method names for premium access retrieval. Changed obsolete method messages to point to GetPremiumAccessByIdsAsync and GetPremiumAccessAsync. Added internal use notes for IHasPremiumAccessQuery. Improved documentation for clarity.

* Refactor TwoFactorIsEnabledQuery to utilize IFeatureService for premium access checks.

* Enhance EF UserRepository to improve premium access retrieval by including related organization data.

* Add unit tests for premium access retrieval in UserRepositoryTests.

* Optimize HasPremiumAccessQuery to eliminate duplicate user IDs before checking premium access. Updated logic to ensure accurate comparison of premium users against distinct user IDs.

* Refactor TwoFactorIsEnabledQuery to improve handling of users without two-factor providers. Added early exit for users lacking providers and streamlined premium status checks for enabled two-factor authentication.

* Update HasPremiumAccessQueryTests to use simplified exception handling and improve test clarity

* Replaced fully qualified exception references with simplified ones.
* Refactored test setup to use individual user variables for better readability.
* Ensured assertions reflect the updated user variable structure.

* Enhance TwoFactorIsEnabledQuery to throw NotFoundException for non-existent users

* Updated TwoFactorIsEnabledQuery to throw NotFoundException when a user is not found instead of returning false.
* Added a new unit test to verify that the NotFoundException is thrown when a user is not found while premium access query is enabled.

* Move premium access query to Billing owned ServiceCollectionExtensions

* Refactor IUserService to enhance premium access checks

* Updated CanAccessPremium and HasPremiumFromOrganization methods to clarify usage with the new premium access query.
* Integrated IHasPremiumAccessQuery into UserService for improved premium access handling based on feature flag.
* Adjusted method documentation to reflect changes in premium access logic.

* Update IUserRepository to clarify usage of premium access methods

* Modified Obsolete attribute messages for GetManyWithCalculatedPremiumAsync and GetCalculatedPremiumAsync to indicate that callers should use the new methods when the 'PremiumAccessQuery' feature flag is enabled.
* Enhanced documentation to improve clarity regarding premium access handling.

* Update IUserRepository and IUserService to clarify deprecation of premium access methods

* Modified Obsolete attribute messages for GetManyWithCalculatedPremiumAsync and GetCalculatedPremiumAsync in IUserRepository to indicate these methods will be removed in a future version.
* Updated Obsolete attribute message for HasPremiumFromOrganization in IUserService to reflect the same deprecation notice.

* Refactor TwoFactorIsEnabledQuery to streamline user ID retrieval

* Consolidated user ID retrieval logic to avoid redundancy.
* Ensured consistent handling of user ID checks for premium access queries.
* Improved code readability by reducing duplicate code blocks.

* Rename migration script to fix the date

* Update migration script to create the index with DROP_EXISTING = ON

* Refactor UserPremiumAccessView to use LEFT JOINs and GROUP BY for improved performance and clarity

* Update HasPremiumAccessQueryTests to return null for GetPremiumAccessAsync instead of throwing NotFoundException

* Add unit tests for premium access scenarios in UserRepositoryTests

- Implement tests for GetPremiumAccessAsync to cover various user and organization premium access combinations.
- Validate behavior when users belong to multiple organizations, including cases with and without premium access.
- Update email generation for user creation to ensure uniqueness without specific prefixes.
- Enhance assertions to verify expected premium access results across different test cases.

* Bump date on migration script

* Update OrganizationEntityTypeConfiguration to include UsersGetPremium in index properties

* Add migration scripts for OrganizationUsersGetPremiumIndex across MySQL, PostgreSQL, and SQLite

- Introduced new migration files to create the OrganizationUsersGetPremiumIndex.
- Updated the DatabaseContextModelSnapshot to include UsersGetPremium in index properties for all database types.
- Ensured consistency in index creation across different database implementations.

---------

Co-authored-by: Todd Martin <tmartin@bitwarden.com>
Co-authored-by: Patrick Pimentel <ppimentel@bitwarden.com>
2025-12-16 10:31:56 +00:00
Jared McCannon
e646b91a50
[PM-27131] Auto confirm policy requirement (#6649)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Added Auto confirm policy enforcement requirement. Includes strict single org enforcement along with blocking provider users from joining orgs with auto confirm enabled.
2025-12-15 15:40:00 -06:00
Kyle Spearrin
bead4f1d5a
validate and email on sso privisioning (#6734) 2025-12-15 15:19:17 -05:00
Thomas Avery
3c44430979
[PM-29161] Remove ReturnErrorOnExistingKeypair feature flag (#6726)
* Remove feature flag

* Add unit test coverage
2025-12-15 13:52:34 -06:00
Graham Walker
7cfdb4ddfc
PM-23358 removing phishing blocker code (#6668) 2025-12-15 12:12:07 -06:00
Thomas Avery
d554e4ef15
[PM-29203] Remove UserkeyRotationV2 feature flag (#6716) 2025-12-15 12:05:10 -06:00
Todd Martin
acfe0d7223
chore(README): Adjust README header level for better formatting
* Adjust headers for better formatting.

* Fixed formatting.
2025-12-15 12:38:53 -05:00
Bernd Schoolmann
4f7e76dac7
[PM-27279] Implement TDE Registration with V2 Keys (#6671)
* Implement TDE v2 signup

* Clean up fallback logic for account keys

* Fix broken v2 logic

* Add comment

* Update comment
2025-12-15 17:48:37 +01:00
Justin Baur
e9ba7ba315
Add the Server SDK to Billing (#6727) 2025-12-15 10:45:01 -05:00
Github Actions
4caf89f139 Bumped version to 2025.12.2 2025-12-15 15:42:14 +00:00
Github Actions
082233f761 Bumped version to 2025.12.1
Some checks failed
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
2025-12-15 15:36:38 +00:00
Brant DeBow
ed76fe2ab6
Refactor configuration for azure queue service for events to include queue name (#6724)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Refactor configuration for azure queue service for events to include queue name

* Address PR feedback

* Add check for queue name before writing to Azure Queue Service

* Fix file encoding (lint error)
2025-12-15 08:49:32 -05:00
Kyle Denney
99e1326039
[PM-24616] refactor stripe adapter (#6527)
Some checks failed
Scan / Check PR run (push) Has been cancelled
Collect code references / Check for secret access (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
* move billing services+tests to billing namespaces

* reorganized methods in file and added comment headers

* renamed StripeAdapter methods for better clarity

* clean up redundant qualifiers

* Upgrade Stripe.net to v48.4.0

* Update PreviewTaxAmountCommand

* Remove unused UpcomingInvoiceOptionExtensions

* Added SubscriptionExtensions with GetCurrentPeriodEnd

* Update PremiumUserBillingService

* Update OrganizationBillingService

* Update GetOrganizationWarningsQuery

* Update BillingHistoryInfo

* Update SubscriptionInfo

* Remove unused Sql Billing folder

* Update StripeAdapter

* Update StripePaymentService

* Update InvoiceCreatedHandler

* Update PaymentFailedHandler

* Update PaymentSucceededHandler

* Update ProviderEventService

* Update StripeEventUtilityService

* Update SubscriptionDeletedHandler

* Update SubscriptionUpdatedHandler

* Update UpcomingInvoiceHandler

* Update ProviderSubscriptionResponse

* Remove unused Stripe Subscriptions Admin Tool

* Update RemoveOrganizationFromProviderCommand

* Update ProviderBillingService

* Update RemoveOrganizatinoFromProviderCommandTests

* Update PreviewTaxAmountCommandTests

* Update GetCloudOrganizationLicenseQueryTests

* Update GetOrganizationWarningsQueryTests

* Update StripePaymentServiceTests

* Update ProviderBillingControllerTests

* Update ProviderEventServiceTests

* Update SubscriptionDeletedHandlerTests

* Update SubscriptionUpdatedHandlerTests

* Resolve Billing test failures

I completely removed tests for the StripeEventService as they were using a system I setup a while back that read JSON files of the Stripe event structure. I did not anticipate how frequently these structures would change with each API version and the cost of trying to update these specific JSON files to test a very static data retrieval service far outweigh the benefit.

* Resolve Core test failures

* Run dotnet format

* Remove unused provider migration

* Fixed failing tests

* Run dotnet format

* Replace the old webhook secret key with new one (#6223)

* Fix compilation failures in additions

* Run dotnet format

* Bump Stripe API version

* Fix recent addition: CreatePremiumCloudHostedSubscriptionCommand

* Fix new code in main according to Stripe update

* Fix InvoiceExtensions

* Bump SDK version to match API Version

* cleanup

* fixing items missed after the merge

* use expression body for all simple returns

* forgot fixes, format, and pr feedback

* claude pr feedback

* pr feedback and cleanup

* more claude feedback

---------

Co-authored-by: Alex Morask <amorask@bitwarden.com>
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
2025-12-12 15:32:43 -06:00
Brant DeBow
196e555116
Refactor event integration service collection extensions into their own extension (#6714)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Add CQRS and caching support for OrganizationIntegrationConfigurations

* Refactor event integration service collection extensions into their own extension
2025-12-12 16:17:43 -05:00
Dave
4fdc4b1b49
refactor(base-request-validator) [PM-28621] Unwind pm-21153 Feature Flag (#6730)
* refactor(base-request-validator) [PM-28621]: Remove feature flagged logic and constant.

* refactor(base-request-validator) [PM-28621]: Update tests to reflect unwound feature flag.
2025-12-12 15:38:21 -05:00
cyprain-okeke
5ac8536855
[PM-28662] Fix Individual Premium automatically disabled due to duplicate subscription leftover from failed payment (#6663)
* Fix the Bug

* Address the hardcode issue

* Fix the tailing test

* resolve the lint issue
2025-12-12 13:19:09 -06:00
rr-bw
84b138f431
(Auth) [PM-27108] Add OrgId checks in SSO Process (#6710) 2025-12-12 10:26:29 -08:00
Brant DeBow
72c8967937
Add CQRS and caching support for OrganizationIntegrationConfigurations (#6690) 2025-12-12 11:52:32 -05:00
Jason Ng
3de2f98681
[PM-28754] add accepted and decline types (#6721)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
2025-12-11 14:50:25 -05:00
Nick Krantz
20755f6c2f
[PM-25947] Add folders and favorites when sharing a cipher (#6402)
* add folders and favorites when sharing a cipher

* refactor folders and favorites assignment to consider existing folders/favorite assignments on a cipher

* remove unneeded string manipulation

* remove comment

* add unit test for folder/favorite sharing

* add migration for sharing a cipher to org and collect reprompt, favorite and folders

* update date timestamp of migration
2025-12-11 12:31:12 -06:00
Brant DeBow
e3d54060fe
Add configurable queue name support to AzureQueueHostedService (#6718) 2025-12-11 08:38:19 -05:00
Bernd Schoolmann
919d0be6d2
Add UpdateAccountCryptographicState repository function (#6669)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Database testing / Run tests (push) Waiting to run
Database testing / Run validation (push) Waiting to run
Database testing / Validate new migration naming and order (push) Waiting to run
Testing / Run tests (push) Waiting to run
* Add user repository update function for account cryptographic state

* Remove comment

* Remove transaction logic

* Fix security version

* Apply feedback

* Update tests

* Add support for external actions
2025-12-11 12:10:50 +01:00
Daniel James Smith
1aad410128
Remove epic link for bitwarden lite issue template (#6719)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
2025-12-10 19:55:16 -05:00
gitclonebrian
742280c999
[repository-management.yml] Implement least privilege permissions (#6646)
- Add empty permission set at workflow level to remove default GITHUB_TOKEN permissions
- Add empty permission set to setup job as it only runs bash commands
- Add contents:write to GitHub App tokens in bump_version and cut_branch jobs for git operations
- Add empty permission set to move_edd_db_scripts job as called workflow declares its own permissions
- Remove secrets:inherit as called workflow accesses Azure secrets directly
2025-12-10 17:47:54 -05:00
Thomas Avery
f86d1a51dd
[PM-25652] Add endpoint to fetch key connector confirmation details (#6635)
* Add new endpoint and query for key connector

* Add unit tests
2025-12-10 14:53:38 -06:00
renovate[bot]
8064ae1e05
[deps]: Update MarkDig to 0.44.0 (#6390)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-10 09:37:00 +01:00
Jimmy Vo
6d5d7e58a6
Ac/pm 21742/update confirmed to org email templates (#6683)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
2025-12-09 10:59:18 -05:00
Mike Amirault
dd74e966e5
[PM-28177] Add feature flag for Send UI refresh (#6708) 2025-12-09 10:37:09 -05:00
Alex Morask
579d8004ff
[PM-29224] Remove unused billing endpoints and code paths (#6692)
* Remove unused endpoints and code paths

* MOAR DELETE

* Run dotnet format
2025-12-09 08:46:15 -06:00
cyprain-okeke
3e12cfc6df
Resolve the failing test (#6622) 2025-12-09 15:45:03 +01:00
Dave
d1ae1fffd6
[PM-24211]: 2FA Send Email Login validation should use AuthRequest.IsValidForAuthentication (#6695)
* fix(two-factor-controller) [PM-24211]: Update send email validation to use auth request's IsValidForAuthentication.

* refactor(login-features) [PM-24211]: Remove Core.LoginFeatures as no longer used; AuthRequest.IsValidForAuthentication should be used for any applicable use cases.

* feat(auth-request) [PM-24211]: Add tests for AuthRequest.IsValidForAuthentication.

* fix(two-factor-controller) [PM-24211]: Branching logic should return on successful send.

* chore(auth-request) [PM-24211]: Remove some old comments (solved-for).

* fix(two-factor-controller) [PM-24211]: Update some comments (clarification/naming).

* fix(two-factor-controller) [PM-24211]: Rephrase a comment (accuracy).
2025-12-09 09:30:06 -05:00
renovate[bot]
d26b5fa029
[deps]: Update Microsoft.NET.Test.Sdk to v18 (#6449)
* [deps]: Update Microsoft.NET.Test.Sdk to v18

* Use MicrosoftNetTestSdkVersion variable instead of fixed versions

* Bump Microsoft.NET.Test.Sdk from 17.8.0 to 18.0.1 in Directory.Build.props

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
2025-12-09 14:40:46 +01:00
SmithThe4th
2e0a4161be
Added feature flag (#6694)
Some checks failed
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Database testing / Run tests (push) Waiting to run
Database testing / Run validation (push) Waiting to run
Database testing / Validate new migration naming and order (push) Waiting to run
Testing / Run tests (push) Waiting to run
Collect code references / Check for secret access (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
2025-12-08 22:08:23 -05:00
Todd Martin
b5f7f9f6a0
chore(premium): [PM-29186] Remove 2FA user interface from premium method signatures
* Removed 2FA user interface from premium method signatures

* Added some more comments for clarity and small touchups.

* Suggested documentation updates.

---------

Co-authored-by: Patrick Pimentel <ppimentel@bitwarden.com>
2025-12-08 17:54:55 -05:00
renovate[bot]
acc2529353
[deps]: Update Divergic.Logging.Xunit to 4.3.1 (#4821)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* [deps]: Update Divergic.Logging.Xunit to 4.3.1

* Switch to Neovolve.Logging.Xunit and clean up test file

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
Co-authored-by: Alex Morask <amorask@bitwarden.com>
2025-12-08 12:58:41 -06:00
Todd Martin
014376b545
chore(docs): Document email asset process 2025-12-08 13:47:28 -05:00
Todd Martin
bd75c71d10
chore(feature-flag): [PM-28331] Remove pm-24425-send-2fa-failed-email feature flag
* Removed pm-24425-send-2fa-failed-email

* Removed flagged logic.
2025-12-08 13:42:54 -05:00
Jordan Aasen
d687e8a84b
[PM-25675] - fix NormalCipherPermissions.CanDelete (#6666)
* fix NormalCipherPermissions.CanDelete

* fix test

* fix tests
2025-12-08 09:21:09 -08:00
renovate[bot]
01da3c91a7
[deps] Billing: Update Braintree to 5.36.0 (#5864)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
2025-12-08 10:51:43 -06:00
Oscar Hinton
b1390c9dfe
Require approvals for sdk renovate (#6707) 2025-12-08 14:01:40 +01:00
renovate[bot]
f8ec5fa0b2
[deps] Tools: Update aws-sdk-net monorepo to 4.0.2.5 (#6620)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 11:58:49 +01:00
Brant DeBow
2504fd9de4
Add CQRS and caching support for OrganizationIntegrations (#6689)
Some checks failed
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
* Add CQRS and caching support for OrganizationIntegrations

* Use primary constructor for Delete command, per Claude suggestion

* Fix namespace

* Add XMLDoc for new commands / queries

* Remove unnecessary extra call to AddExtendedCache in Startup (call in EventIntegrationsServiceCollectionExtensions handles this instead)

* Alter strategy to use one cache / database call to retrieve all configurations for an event (including wildcards)

* Updated README documentation to reflect updated Caching doc and updated CQRS approach
2025-12-05 15:28:07 -05:00
Jordan Aasen
3ff59021ae
[PM-20206][PM-22372] - remove end user notifications feature flags (#6642)
* remove end user notifications feature flag

* remove end user notifications feature flag
2025-12-05 12:00:51 -08:00
Jordan Aasen
b18506a0c1
add feature flag (#6693) 2025-12-05 11:06:09 -08:00
Brant DeBow
813fad8021
Use extended cache for caching integration configuration details (#6650)
* Use extended cache for caching integration configuration details

* Alter strategy to use one cache / database call to retrieve all configurations for an event (including wildcards)

* Renamed migration per @withinfocus suggestion
2025-12-05 13:12:27 -05:00
Jared McCannon
2f893768f5
[PM-18718] Refactor Bulk Revoke Users (#6601)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Database testing / Run tests (push) Waiting to run
Database testing / Run validation (push) Waiting to run
Database testing / Validate new migration naming and order (push) Waiting to run
Testing / Run tests (push) Waiting to run
2025-12-05 11:19:26 -06:00
Ike
d5f39eac91
[PM-28769] [PM-28768] [PM-28772] Welcome email bug fixes (#6644)
Fix: fix bugs reported by QA for Welcome emails
* test: add test for new plan type in welcome email

* fix: change to headStyle so styling is only included once

* fix: update MJML templates to have correct copy text

* chore: move build artifacts for updated email templates

* fix: add setting for SMTP to SSO project

* fix: update component css styling

* chore: rebuild hbs templates

* fix: using billing extension method to fetch Correct PlanType.
2025-12-05 11:35:37 -05:00
Rui Tomé
5469d8be0e
[PM-28260] Optimize bulk reinvite endpoint (#6670)
* Implement optimized bulk invite resend command

- Added IBulkResendOrganizationInvitesCommand interface to define the bulk resend operation.
- Created BulkResendOrganizationInvitesCommand class to handle the logic for resending invites to multiple organization users.
- Integrated logging and validation to ensure only valid users receive invites.
- Included error handling for non-existent organizations and invalid user statuses.

* Add unit tests for BulkResendOrganizationInvitesCommand

- Implemented comprehensive test cases for the BulkResendOrganizationInvitesCommand class.
- Validated user statuses and ensured correct handling of valid and invalid users during bulk invite resends.
- Included tests for scenarios such as organization not found and empty user lists.
- Utilized Xunit and NSubstitute for effective testing and mocking of dependencies.

* Add IBulkResendOrganizationInvitesCommand to service collection

- Registered IBulkResendOrganizationInvitesCommand in the service collection for dependency injection.

* Update OrganizationUsersController to utilize IBulkResendOrganizationInvitesCommand

- Added IBulkResendOrganizationInvitesCommand to the OrganizationUsersController for handling bulk invite resends based on feature flag.
- Updated BulkReinvite method to conditionally use the new command or the legacy service based on the feature flag status.
- Enhanced unit tests to verify correct command usage depending on feature flag state, ensuring robust testing for both scenarios.
2025-12-05 16:28:04 +00:00
Jared McCannon
18a8829476
[PM-26377] Correcting Auto Confirm Handler Provider Check (#6681)
* Fixed bug where providers weren't being checked correctly in auto confirm handler.
2025-12-05 08:28:42 -06:00
Rui Tomé
80ee31b4fe
[PM-25015] Add performance tests for Admin Console endpoints (#6235)
* Add GroupsRecipe to manage group creation and user relationships in organizations

* Add CollectionsRecipe to manage collection creation and user relationships in organizations

* Refactor OrganizationUsersControllerPerformanceTests to enhance performance testing and add new test cases

* Add OrganizationDomainRecipe to add verified domains for organizations

* Add more tests to OrganizationUsersControllerPerformanceTests and enhance seeding logic for organizations

- Updated performance tests to use dynamic domain generation for organization users.
- Refactored seeding methods in OrganizationWithUsersRecipe to accept user status and type.
- Modified AddToOrganization methods in CollectionsRecipe and GroupsRecipe to return created IDs.
- Adjusted DbSeederUtility to align with new seeding method signatures.

* Enhance OrganizationSeeder with additional configuration options and update seat calculation in OrganizationWithUsersRecipe to ensure a minimum of 1000 seats.

* Add performance tests for Groups, Organizations, Organization Users, and Provider Organizations controllers

- Introduced `GroupsControllerPerformanceTests` to validate the performance of the PutGroupAsync method.
- Added `OrganizationsControllerPerformanceTests` with multiple tests including DeleteOrganizationAsync, DeleteOrganizationWithTokenAsync, PostStorageAsync, and CreateWithoutPaymentAsync.
- Enhanced `OrganizationUsersControllerPerformanceTests` with DeleteSingleUserAccountAsync and InviteUsersAsync methods to test user account deletion and bulk invitations.
- Created `ProviderOrganizationsControllerPerformanceTests` to assess the performance of deleting provider organizations.

These tests ensure the reliability and efficiency of the respective controller actions under various scenarios.

* Refactor GroupsControllerPerformanceTests to use parameterized tests

- Renamed `GroupsControllerPerformanceTest` to `GroupsControllerPerformanceTests` for consistency.
- Updated `PutGroupAsync` method to use `[Theory]` with `InlineData` for dynamic user and collection counts.
- Adjusted organization user and collection seeding logic to utilize the new parameters.
- Enhanced logging to provide clearer performance metrics during tests.

* Update domain generation in GroupsControllerPerformanceTests for improved test consistency

* Remove ProviderOrganizationsControllerPerformanceTests

* Refactor performance tests for Groups, Organizations, and Organization Users controllers

- Updated method names for clarity and consistency, e.g., `PutGroupAsync` to `UpdateGroup_WithUsersAndCollections`.
- Enhanced test documentation with XML comments to describe the purpose of each test.
- Improved domain generation logic for consistency across tests.
- Adjusted logging to provide detailed performance metrics during test execution.
- Renamed several test methods to better reflect their functionality.

* Refactor performance tests in Organizations and Organization Users controllers

- Updated tests to use parameterized `[Theory]` attributes with `InlineData` for dynamic user, collection, and group counts.
- Enhanced logging to include detailed metrics such as user and collection counts during test execution.
- Marked several tests as skipped for performance considerations.
- Removed unused code and improved organization of test methods for clarity.

* Add bulk reinvite users performance test to OrganizationUsersControllerPerformanceTests

- Implemented a new performance test for the POST /organizations/{orgId}/users/reinvite endpoint.
- Utilized parameterized testing with `[Theory]` and `InlineData` to evaluate performance with varying user counts.
- Enhanced logging to capture request duration and response status for better performance insights.
- Updated OrganizationSeeder to conditionally set email based on user status during seeding.

* Refactor domain generation in performance tests to use OrganizationTestHelpers

- Updated domain generation logic in GroupsControllerPerformanceTests, OrganizationsControllerPerformanceTests, and OrganizationUsersControllerPerformanceTests to utilize the new GenerateRandomDomain method from OrganizationTestHelpers.
- This change enhances consistency and readability across the tests by centralizing domain generation logic.

* Update CollectionsRecipe to have better readability

* Update GroupsRecipe to have better readability

* Refactor authentication in performance tests to use centralized helper method. This change reduces code duplication across Groups, Organizations, and OrganizationUsers controller tests by implementing the `AuthenticateClientAsync` method in a new `PerformanceTestHelpers` class.

* Refactor OrganizationUsersControllerPerformanceTests to filter organization users by OrganizationId.

* Refactor CreateOrganizationUser method to improve handling of user status and key assignment based on invitation and confirmation states.

* Add XML documentation for CreateOrganizationUser method to clarify user status handling
2025-12-05 14:22:00 +00:00
Brant DeBow
3605b4d2ff
Upgrade ExtendedCache to support non-Redis distributed cache (#6682)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Upgrade ExtendedCache to support non-Redis distributed cache

* Update CACHING.md to use UseSharedDistributedCache setting

Updated documentation to reflect the setting rename from UseSharedRedisCache
to UseSharedDistributedCache in the ExtendedCache configuration examples.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Matt Bishop <withinfocus@users.noreply.github.com>
2025-12-04 16:37:51 -05:00
Alex Morask
101ff9d6ed
[PM-28423] Add latest_invoice expansion / logging to SubscriptionCancellationJob (#6603)
* Added latest_invoice expansion / logging to cancellation job

* Run dotnet format

* Claude feedback

* Run dotnet format
2025-12-04 13:10:13 -06:00
Jimmy Vo
d88fff4262
[PM-21742] Fix MJML validation error. (#6687)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-12-04 11:30:26 -05:00
cyprain-okeke
d619a49998
[PM-28508] Fix No validation occurs for Expiration date on Self Host licenses (#6655)
* Fix the license validation bug

* resolve the failing test

* fix the failing test

* Revert changes and Add the ui display fix

* remove empty spaces

* revert the changes on licensing file

* revert changes to the test signup

* Revert the org license file changes

* revert the empty spaces

* revert the empty spaces changes

* remove the empty spaces

* revert

* Remove the duplicate code

* Add the expire date fix for premium

* Fix the failing test

* Fix the lint error
2025-12-04 16:28:01 +01:00
Jared Snider
655054aa56
refactor(IdentityTokenResponse): [Auth/PM-3537] Remove deprecated "KeyConnectorUrl" from root of IdentityTokenResponse (#6627)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* PM-3537 - Remove "KeyConnectorUrl" from root of IdentityTokenResponse

* PM-3537 - CustomTokenRequestValidator.cs - update comment to be accurate
2025-12-03 16:57:01 -05:00
Ike
b0f6b22b3d
chore: update duende license (#6680) 2025-12-03 13:50:01 -05:00
Bernd Schoolmann
ed7a234eeb
Add data recovery tool flag (#6659) 2025-12-03 19:19:46 +01:00
cd-bitwarden
98212a7f49
[SM-1592] API for Secret Versioning, adding controller, repository and tests (#6444)
* Adding SecretVersion table to server

* making the names singular not plural for new table

* removing migration

* fixing migration

* Adding indexes for serviceacct and orguserId

* indexes for sqllite

* fixing migrations

* adding indexes to secretVeriosn.sql

* tests

* removing tests

* adding GO

* api repository and controller additions for SecretVersion table, as well as tests

* test fix sqllite

* improvements

* removing comments

* making files nullable safe

* Justin Baurs suggested changes

* claude suggestions

* Claude fixes

* test fixes
2025-12-03 12:17:29 -05:00
Vincent Salucci
ded1c58c27
[PM-26426] [PM-26427] Remove feature flag - policy validators/requirements refactor (#6674)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* chore: remove ff from PoliciesController, refs PM-26426

* chore: remove ff from public PoliciesController, refs PM-26426

* chore: remove ff from VerifyOrganizationDomainCommands, refs PM-26426

* chore: remove ff from SsoConfigService, refs PM-26426

* chore: remove ff from public PoliciesControllerTests, refs PM-26426

* chore: remove ff from PoliciesControllerTests, refs PM-26426

* chore: remove ff from VerifyOrganizationDomainCommandTests, refs PM-26426

* chore: remove ff from SsoConfigServiceTests, refs PM-26426

* chore: remove ff definition, refs PM-26427

* chore: dotnet format

* chore: remove unused constructor parameters, refs PM-26426

* chore: fix failing tests for VerifyOrganizationDomainCommandTests and SsoConfigServiceTests, refs PM-26426
2025-12-03 10:42:54 -06:00
Kyle Spearrin
1566a6d587
[PM-28871] Default startIndex and count values on SCIM groups list API (#6648)
* default startindex and count values on SCIM groups list api

* convert params to a model, like users

* review feedback

* fix file name to be plural

* added integration test
2025-12-03 15:52:09 +00:00
Vincent Salucci
28e9c24f33
[PM-25584] [PM-25585] Remove feature flag - recover provider accounts (#6673)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* chore: remove ff from OrganizationUsersController, refs PM-25584

* chore: update tests with reference to ff, refs PM-25584

* chore: remove ff definition, refs PM-25585

* chore: dotnet format, refs PM-25584
2025-12-02 23:23:58 -06:00
Thomas Rittson
ee26a701e9
[BEEEP] [PM-28808] Fix invalid identity URL in Swagger (#6653)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
- in generated JSON (used in help center), only show cloud options
  (with corrected identity URL)
- in self-host and dev, only show local option
2025-12-02 23:20:56 +00:00
Kyle Denney
89a2eab32a
[PM-23717] premium renewal email (#6672)
* [PM-23717] premium renewal email

* pr feedback

* pr feedback
2025-12-02 16:38:28 -06:00
Bernd Schoolmann
de5a81bdc4
Move request models to core (#6667)
* Move request models to core

* Fix build

* Fix

* Undo changes
2025-12-02 19:54:40 +01:00
Nick Krantz
5b8b394982
allow for archived ciphers to be shared into an organization (#6626) 2025-12-02 11:43:22 -06:00
Alex Morask
71be3865ea
[PM-24558] Remove FF: pm-21821-provider-portal-takeover (#6613)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Database testing / Validate new migration naming and order (push) Has been cancelled
* Remove FF: pm-21821-provider-portal-takeover

* Run dotnet format
2025-12-02 10:16:37 -06:00
Matt Bishop
b3573c15fd
Validate any SQL migration scripts are the most recent (#6652)
* Validate any SQL migration scripts are the most recent

* Make string checks more robust

* Clarify script location

* Remove need given the additional validations that are immediately valuable

* Allow past incorrectly-named migrations but now enforce

* Centralize validation logic to PowerShell script
2025-12-02 08:15:47 -05:00
Kyle Spearrin
63855cbb5a
Add BlockClaimedDomainAccountCreationPolicyValidator to AddPolicyValidators (#6665)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Database testing / Run tests (push) Waiting to run
Database testing / Run validation (push) Waiting to run
Testing / Run tests (push) Waiting to run
2025-12-01 17:49:52 -05:00
Alex
aa3172e24f
[PM-6979] correct REST semantics (#6661)
* fix: Return 200 OK with empty array for HIBP breach endpoint when no breaches found

Changes the HIBP breach check endpoint to return HTTP 200 OK with an empty
JSON array `[]` instead of 404 Not Found when no breaches are found. This
follows proper REST API semantics where 404 should indicate the endpoint
doesn't exist, not that a query returned no results.

Changes:
- src/Api/Dirt/Controllers/HibpController.cs: Lines 67-71
- Changed: return new NotFoundResult(); → return Content("[]", "application/json");

Backward Compatible:
- Clients handle both 200 with [] (new) and 404 (old)
- No breaking changes
- Safe to deploy independently

API Response Changes:
- Before: GET /api/hibp/breach?username=safe@example.com → 404 Not Found
- After:  GET /api/hibp/breach?username=safe@example.com → 200 OK, Body: []

Impact:
- No user-facing changes
- Correct REST semantics
- Industry-standard API response pattern

* Address PR feedback: enhance comment and add comprehensive unit tests

Addresses feedback from PR #6661:

1. Enhanced comment per @prograhamming's feedback (lines 69-71):
   - Added date stamp (12/1/2025)
   - Explained HIBP API behavior: returns 404 when no breaches found
   - Clarified HIBP API specification about 404 meaning
   - Maintained REST semantics justification

2. Created comprehensive unit tests per Claude bot's Finding 1:
   - New file: test/Api.Test/Dirt/HibpControllerTests.cs
   - 9 test cases covering all critical scenarios:
     * Missing API key validation
     * No breaches found (404 → 200 with []) - KEY TEST FOR PR CHANGE
     * Breaches found (200 with data)
     * Rate limiting with retry logic
     * Server error handling (500, 400)
     * URL encoding of special characters
     * Required headers validation
     * Self-hosted vs cloud User-Agent differences

Test Coverage:
- Before: 0% coverage for HibpController
- After: ~90% coverage (all public methods and major paths)
- Uses xUnit, NSubstitute, BitAutoData patterns
- Matches existing Dirt controller test conventions

Changes:
- src/Api/Dirt/Controllers/HibpController.cs: Enhanced comment (+3 lines)
- test/Api.Test/Dirt/HibpControllerTests.cs: New test file (327 lines, 9 tests)

Addresses:
- @prograhamming's comment about enhancing the code comment
- Claude bot's Finding 1: Missing unit tests for HibpController

Related: PM-6979

* fix test/formating errors
2025-12-01 12:37:31 -08:00
Brandon Treston
20efb5eb5e
add readme (#6664) 2025-12-01 15:31:30 -05:00
Kyle Denney
02568c8e7c
[PM-28100] families 2019 email (#6645)
* [PM-28100] families 2019 email

* pr feedback
2025-12-01 14:01:26 -06:00
Vince Grassia
267759db45
Update token permissions to properly trigger workflows (#6662) 2025-12-01 14:19:40 -05:00
Vijay Oommen
599fbc0efd
[PM-28616] Add flag UsePhishingBlocker to dbo.Organization (#6625)
* PM-28616 Add flag UsePhishingBlocker to dbo.Organization

* PM-28616 updated as per comments from claude

* PM-28616 updated ToLicense Method to copy the license file

* PM-28616 allow phishing blocker to be imported via license files for self-hosted

* PM-28616 updated PR comments - added more views to be refreshed

* PM-28616 removed proeprty from constructor as it is not used anymore. We have moved to claims based properties
2025-12-01 13:31:36 -05:00
Eli Grubb
c3301ce475
[PM-22275] Remove encryption-related feature flags (#6654)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-12-01 08:33:56 -07:00
Brandon Treston
a5ea603817
[PM-24011] Create new policy sync push notification (#6594)
* create new policy sync push notification

* CR feedback

* add tests, fix typo
2025-12-01 10:21:44 -05:00
Jared McCannon
62cbe36ce1
Forgot to add AutomaticUserConfirmationPolicyEventHandler to the IPolicyValidator implementation registration. (#6637) 2025-12-01 09:11:43 -06:00
Github Actions
5af060fbf5 Bumped version to 2025.12.0
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
2025-12-01 11:46:27 +00:00
Vince Grassia
3df68ea36f
BRE-1355 - Fix lite naming and remove PAT (#6658)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Database testing / Run tests (push) Waiting to run
Database testing / Run validation (push) Waiting to run
Testing / Run tests (push) Waiting to run
2025-12-01 08:46:51 +01:00
Ike
8a67aafbe5
[PM-1632] Redirect on SsoRequired - return SsoOrganizationIdentifier (#6597)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
feat: add SSO request validation and organization identifier lookup

- Implement SsoRequestValidator to validate SSO requirements
- Add UserSsoOrganizationIdentifierQuery to fetch organization identifiers
- Create SsoOrganizationIdentifier custom response for SSO redirects
- Add feature flag (RedirectOnSsoRequired) for gradual rollout
- Register validators and queries in dependency injection
- Create RequestValidationConstants to reduce magic strings
- Add comprehensive test coverage for validation logic
- Update BaseRequestValidator to consume SsoRequestValidator
2025-11-30 16:55:47 -05:00
sneakernuts
f151abee54
SRE-3494 cleanup (#6657)
Some checks failed
Scan / Check PR run (push) Has been cancelled
Collect code references / Check for secret access (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
2025-11-28 14:02:00 -07:00
Jim Hays
480c20a480
[PM-1968] Spellcheck bugs (#2877)
* Bug fix: 'captchResponse' -> 'captchaResponse'

* Bug fix: 'GoupUser' -> 'GroupUser'

* Bug fix: 'Cateogry' -> 'Category'

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
2025-11-28 15:45:23 +00:00
Usman
7cbc50de98
Removed unused global settings and listenerlogger and passed cancelationtoken to Task.Delay methods (#1810)
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
2025-11-28 15:15:06 +00:00
renovate[bot]
a17f94e150
[deps] Billing: Update xunit.runner.visualstudio to v3 (#5742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 16:10:39 +01:00
renovate[bot]
6a5430ff0a
[deps] Platform: Update quartznet monorepo to 3.15.1 (#6211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 15:46:08 +01:00
renovate[bot]
3133dc91ae
[deps]: Update MartinCostello.Logging.XUnit to 0.7.0 (#6046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 15:36:01 +01:00
renovate[bot]
3ad486068d
[deps] Platform: Update azure azure-sdk-for-net monorepo (#5735)
* [deps] Platform: Update azure azure-sdk-for-net monorepo

* Remove bump of Azure.Extensions.AspNetCore.DataProtection.Blobs because it require Microsoft.AspNetCore.DataProtection >= 8.0.11

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
2025-11-28 12:33:51 +01:00
Todd Martin
eed856cc04
Move Azure.Extensions.AspNetCore.DataProtection.Blobs to Platform (#5442) 2025-11-28 12:10:50 +01:00
renovate[bot]
d3e4ac76b4
[deps]: Update CommandDotNet to 7.0.5 (#5063)
Some checks failed
Scan / Check PR run (push) Has been cancelled
Collect code references / Check for secret access (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 20:46:43 +01:00
renovate[bot]
1334ed89f0
[deps]: Update github/codeql-action action to v4.31.4 (#6618)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 18:34:24 +01:00
Alex Morask
219993cc2e
[PM-26461] Send F2020 renewal email (#6638)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Send F2020 renewal email

* Implement and use simple hero

* Cy's feedback
2025-11-26 08:37:48 -06:00
Nick Krantz
9fcedd5b91
remove nudge feature flag (#6643)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-11-25 16:32:46 -06:00
Matt Bishop
35a0b675a2
Add full-featured caching docs (#6639)
* Add full-featured caching docs

* Improve some things from a weak README port

* Implement Claude suggestions

* Address Brant's comments

* Revise recommendations toward ExtendedCache in more situations

* Provide forward-looking documentation on how the application cache will also work

* Address minor feedback points
2025-11-25 16:53:50 -05:00
Thomas Rittson
35b4b0754c
[PM-25913] Fix owners unable to rename provider-managed organization (#6599)
And other refactors:
- move update organization method to a command
- separate authorization from business logic
- add tests
- move Billing Team logic into their service
2025-11-26 07:38:01 +10:00
Vicki League
3559759f4b
[CL-923] Add feature flag for router focus mgmt (#6623) 2025-11-25 13:13:36 -05:00
Brant DeBow
1413dd7689
[PM-12642] Add new ExtendedCache to add caching to template parameters (#6608)
* Add new ExtendedCache to add caching to template parameters

* Added Cache constants for building consistent keys/name, clarified that we are using defaults including TTL, removed as much fusion cache references as possible
2025-11-25 10:58:39 -05:00
renovate[bot]
f0f10bcb95
[deps] Billing: Update xunit-dotnet monorepo (#5732)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
2025-11-25 15:34:59 +01:00
Todd Martin
ebe5306fd2
chore(docs): Updated docs for IMailer and MJML
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
* Updated docs for IMailer.

* More changes.

* Added deprecation context.

* ViewModel corrections.

* Updated link.

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Updated link.

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Updated steps for clarity.

* Update src/Core/MailTemplates/Mjml/README.md

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Grammar fix.

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-11-24 23:48:53 -05:00
rr-bw
5fb69e42b0
feat(marketing-initiated-premium): (Auth) [PM-27541] Add optional marketing param to email verification link (#6604)
Adds an optional `&fromMarketing=premium` query parameter to the verification email link.

Feature flag: `"pm-26140-marketing-initiated-premium-flow"`
2025-11-24 15:06:16 -08:00
SmithThe4th
9131427622
Added bump account revision date to sproc (#6640) 2025-11-24 14:57:06 -08:00
Kyle Denney
931f0c65af
[PM-28265] storage reconciliation job (#6615)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Database testing / Run tests (push) Waiting to run
Database testing / Run validation (push) Waiting to run
Testing / Run tests (push) Waiting to run
2025-11-24 16:11:52 -06:00
Brant DeBow
9573cab37e
Add template properties for Datadog (#6528)
* Add template properites for Datadog

* Add test and implementation for including User and ActingUser when only the Type is referenced

* Refactored database calls to fetch the user details in a single DB call

* Refactor to use a dedicated stored procedure for Dapper

* Remove TOP 1 from stored procedure

* Accept Claude's optimization of SingleOrDefaultAsync to unify Dapper/EF

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Revert earlier change and add TOP 1 back into stored procedure

* Change go to GO

* Revert back to version that assumes uniqueness, remove TOP 1

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-11-24 10:30:45 -05:00
Rui Tomé
fcc879bd23
[PM-28252] Add new feature flag for increasing bulk reinvite limit for cloud (#6624)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-11-24 11:41:01 +00:00
Justin Baur
fdfec0ac4d
Remove deprecated logging methods (#6516)
Some checks failed
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
2025-11-21 14:39:26 -05:00
rr-bw
042279682a
feat(marketing-initiated-premium): (Auth) [PM-27540] Add optional Marketing Property to RegisterSendVerificationEmailRequestModel (#6598)
Adds an optional `FromMarketing` property to the RegisterSendVerificationEmailRequestModel.
2025-11-21 09:38:59 -08:00
Alex Morask
62705917ab
Remove unreferenced FF (#6612) 2025-11-21 11:12:33 -06:00
Jimmy Vo
1cc787f1f0
[PM-27879] Add AC shared MJML components (#6605) 2025-11-21 10:01:39 -05:00
renovate[bot]
08ce693fbf
[deps] Platform: Update LaunchDarkly.ServerSdk to 8.10.4 (#6616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-21 14:44:19 +01:00
Patrick-Pimentel-Bitwarden
59b6cd0cdb
feat(prelogin): [Auth/PM-27062] Prelogin New Response (#6583)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* feat(prelogin): [PM-27062] Prelogin New Response - Initial changes to support new data coming back from prelogin.

* test(prelogin): [PM-27062] Prelogin New Response - Added tests.
2025-11-20 15:43:01 -05:00
Alex Morask
c81b0dcd49
[PM-28249] Write PlanType claim as an integer (#6610)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Write PlanType claim as an integer

* Accept FamiliesAnnually2025 for FamiliesAnnually plan type
2025-11-20 14:08:20 -06:00
Ike
8b169551f5
Update MjmlWelcomeEmailTemplates Feature Flag Key (#6611) 2025-11-20 14:51:38 -05:00
renovate[bot]
a434419313
[deps]: Update sdk-internal to 7080159 (#6609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 18:28:01 +01:00
Oscar Hinton
6653b9802e
Group sdk dependencies take 2 (#6607) 2025-11-20 17:36:37 +01:00
Kyle Spearrin
c0700a6946
[PM-27766] Add policy for blocking account creation from claimed domains. (#6537)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
* Add policy for blocking account creation from claimed domains.

* dotnet format

* check as part of email verification

* add feature flag

* fix tests

* try to fix dates on database integration tests

* PR feedback from claude

* remove claude local settings

* pr feedback

* format

* fix test

* create or alter

* PR feedback

* PR feedback

* Update src/Core/Constants.cs

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* fix merge issues

* fix tests

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
2025-11-20 11:25:50 +10:00
Shane Melton
55fb80b2fc
[PM-27662] Add revision date to policy response model (#6602) 2025-11-20 09:55:19 +10:00
Nik Gilmore
d81c61637e
[PM-24314] Remove feature flag pm-9111-extension-persist-add-edit-form (#6561) 2025-11-19 15:31:34 -08:00
Brant DeBow
3c874646e8
Upgrade ExtendedCache with support for named caches (#6591)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Database testing / Run tests (push) Waiting to run
Database testing / Run validation (push) Waiting to run
Testing / Run tests (push) Waiting to run
* Upgrade ExtendedCache with support for named caches

* Addressed Claude PR suggestions - defensive mux creation, defend empty cache name, added tests

* Addressed PR suggestions; Fixed issue where IDistributedCache was missing when using the shared route; Added more unit tests

* Revert to TryAdd, document expectation that AddDistributedCache is called first
2025-11-19 14:42:03 -05:00
renovate[bot]
4c543fa824
[deps]: Update github/codeql-action action to v4 (#6500)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
2025-11-19 20:09:01 +01:00
Alex Morask
f595818ede
[PM-24549] Remove feature flag: use-pricing-service (#6567)
* Remove feature flag and move StaticStore plans to MockPlans for tests

* Remove old plan models / move sponsored plans out of StaticStore

* Run dotnet format

* Add pricing URI to Development appsettings for local development and integration tests

* Updated Api Integration tests to get current plan type

* Run dotnet format

* Fix failing tests
2025-11-19 09:53:30 -06:00
Jared McCannon
1eb396cb40
[PM-26636] - Auto Confirm Org User Command (#6488)
* Adding auto confirm endpoint and initial command work.

* Adding validator

* Finished command implementation.

* Enabled the feature renomved used method. Enabled the policy in the tests.

* Added extension functions to allow for railroad programming.

* Removed guid from route template. Added xml docs

* Added validation for command.

* Added default collection creation to command.

* formatting.

* Added additional error types and mapped to appropriate results.

* Added tests for auto confirm validator

* Adding tests

* fixing file name

* Cleaned up OrgUserController. Added integration tests.

* Consolidated CommandResult and validation result stuff into a v2 directory.

* changing result to match handle method.

* Moves validation thenasync method.

* Added brackets.

* Updated XML comment

* Adding idempotency comment.

* Fixed up merge problems. Fixed return types for handle.

* Renamed to ValidationRequest

* I added some methods for CommandResult to cover some future use cases. Added ApplyAsync method to execute multiple functions against CommandResult without an error stopping the workflow for side-effects.

* Fixed up logic around should create default colleciton. Added more methods for chaining ValidationResult together. Added logic for user type.

* Clearing nullable enable.

* Fixed up validator tests.

* Tests for auto confirm command

* Fixed up command result and AutoConfirmCommand.

* Removed some unused methods.

* Moved autoconfirm tests to their own class.

* Moved some stuff around. Need to clean up creation of accepted org user yet.

* Moved some more code around. Folded Key into accepted constructor. removed unneeded tests since key and accepted are now a part of AcceptedOrgUser Creation.

* Clean up clean up everybody everywhere. Clean up clean up everybody do your share.

* Another quick one

* Removed aggregate Errors.cs

* Cleaned up validator and fixed up tests.

* Fixed auto confirm repo

* Cleaned up command tests.

* Unused method.

* Restoring Bulk command back to what it was. deleted handle method for bulk.

* Remove unused method.

* removed unnecssary lines and comments

* fixed layout.

* Fixed test.

* fixed spelling mistake. removed unused import.

* Update test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs

Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>

* Ensuring collection is created before full sync. Cleaning up tests and added a few more. Added check that the policy is enabled.

* Added org cleanup

* Lowering to 5 to see if  that helps the runner.

* 🤷

* Trying this

* Maybe this time will be different.

* seeing if awaiting and checking independently will work in ci

* I figured it out. Locally, it would be fast enough to all return NoContent, however in CI, its slow enough for it to return 400 due to the user already being confirmed via validation.

* Updated tests and validator

* Fixed name

---------

Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
2025-11-19 08:09:48 -06:00
cyprain-okeke
a6f87c3f72
[PM 27603]Add MaxStorageGbIncreased Column to User and Organization Tables (#6570)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Database testing / Run tests (push) Waiting to run
Database testing / Run validation (push) Waiting to run
Testing / Run tests (push) Waiting to run
* Add changes for initial migration

* Fix the failing Database test

* Fix the failing database test

* Address the pr comments

* revert some change sthat are not

* Address the remaining comments

* Remove unused object

* remove MaxStorageGbIncreased in the views

* refresh commands will ensure all stored procedures

* Fix the typo
2025-11-19 12:02:23 +01:00
Vince Grassia
33842d6d4f
Change runners to ubuntu-22.04 until GitHub fixes timeout issue (#6587) 2025-11-19 08:39:29 +01:00
Matt Gibson
20cd3f5ceb
Use consistent naming of containers (#6584)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
The specified names are override compose project naming conventions. Each specified name was the same as the service name, with the exceptions of mysql and redis, which break our naming convention.
2025-11-18 22:08:38 -05:00
Kyle Denney
5c1d586a25
[PM-28370] fix defect for self-hosted metadata (#6593)
* [PM-28370] fix defect for self-hosted metadata

* dotnet format
2025-11-18 15:24:34 -06:00
Vince Grassia
2b926ef1c5
BRE-1355 - Rename Bitwarden Unified to Bitwarden Lite (#6592)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Database testing / Run tests (push) Waiting to run
Database testing / Run validation (push) Waiting to run
Testing / Run tests (push) Waiting to run
2025-11-18 12:24:01 -05:00
Nik Gilmore
a724c933dc
[PM-28285] Bugfix: Fix attachment uploads on selfhosted instances (#6590)
* [PM-28285] Remove check for LastKnownRevisionDate when uploading attachment file directly.

Remove accidental commit files

* Remove tests that are no longer relevant

* Remove unecessary lastKnownRevisionDate check from attachment share operation
2025-11-18 07:44:40 -08:00
Kyle Denney
75d8250f3a
[PM-24298] milestone3 families 2025 (#6586)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* [PM-24298] milestone3 families2025

# Conflicts:
#	src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs
#	test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs

* update test name
2025-11-17 16:03:35 -06:00
John Harrington
f52a630197
PM-27248 [Defect] An unhandled error is returned when a MSP tries to import data (#6563)
* Revert previous commits and implement logic to avoid null references:

• reverted previous commits and force pushed to remove inconsistent formatting
• implemented logic to avoid null reference access (with comments)

* fix typo in comment

* fix logical error and add test coverage

* extracted logic common to all code paths per review comment
2025-11-17 13:40:30 -07:00
rr-bw
c2075781ca
feat(marketing-initiated-premium): Auth [PM-27551] Create feature flag (#6581)
Create a `"pm-26140-marketing-initiated-premium-flow"` feature flag in `Constants.cs`
2025-11-17 11:25:22 -08:00
Github Actions
902fcb3704 Bumped version to 2025.11.1 2025-11-17 19:06:29 +00:00
Kyle Denney
a9bb01031a
remove hardcoded storage values (#6571)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Testing / Run tests (push) Waiting to run
2025-11-17 11:16:02 -06:00
Alex Morask
c620ec2aca
[PM-28250] Fix seat add on defect (#6580)
* Handle seat add on

* Remove old price instead
2025-11-17 10:50:10 -06:00
cd-bitwarden
1274fe6562
[SM-1575] Add ability to retrieve events based on projectId/SecretId (#6316)
Some checks failed
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
* adding event filters

* allow user to see deleted secret event logs through public api

* nullable changes to event controller

* fixing tests

* fixing permissions issues with public API

* fix for bug

* Update src/Api/AdminConsole/Public/Controllers/EventsController.cs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Update src/Api/AdminConsole/Public/Controllers/EventsController.cs

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-11-14 18:21:48 -05:00
Patrick-Pimentel-Bitwarden
7eaca9bb7d
Revert "feat(prelogin): [Auth/PM-27062] Prelogin New Response (#6577)" (#6582)
This reverts commit 92e511284b.

Merged without feature flag code and before QA could get their review done.
2025-11-14 21:42:14 +00:00
Patrick-Pimentel-Bitwarden
92e511284b
feat(prelogin): [Auth/PM-27062] Prelogin New Response (#6577)
* feat(prelogin): [PM-27062] Prelogin New Response - Initial changes to support new data coming back from prelogin.

* test(prelogin): [PM-27062] Prelogin New Response - Added tests.
2025-11-14 19:33:31 +00:00
Brant DeBow
b4c7ab8773
Add FusionCache to service collection (#6575)
* Add FusionCache to service collection

* Refactored to it's own service collection extension, added full unit tests, added TryAdd style support

* Move to ExtendedCache instead of FusionCache, re-use exsting DistributedCache if present, expose backplane to DI

* Reworked builders to reuse multiplexer if present
2025-11-14 12:45:45 -05:00
Ike
9b3adf0ddc
[PM-21741] Welcome email updates (#6479)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
feat(PM-21741): implement MJML welcome email templates with feature flag support

- Add MJML templates for individual, family, and organization welcome emails
- Track *.hbs artifacts from MJML build
- Implement feature flag for gradual rollout of new email templates
- Update RegisterUserCommand and HandlebarsMailService to support new templates
- Add text versions and sanitization for all welcome emails
- Fetch organization data from database for welcome emails
- Add comprehensive test coverage for registration flow

Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
2025-11-14 07:46:33 -05:00
Stephon Brown
30ff175f8e
Milestone 2 Handler Update (#6564)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* fix(billing): update discount id

* test(billing) update test
2025-11-13 17:47:06 +00:00
Jared McCannon
e7b4837be9
[PM-26377] Add Auto Confirm Policy (#6552)
* First pass at adding Automatic User Confirmation Policy.
* Adding edge case tests. Adding side effect of updating organization feature. Removing account recovery restriction from validation.
* Added implementation for the vnext save
* Added documentation to different event types with remarks. Updated IPolicyValidator xml docs.
2025-11-13 11:33:24 -06:00
Alex Morask
59a64af345
[PM-26435] Milestone 3 / F19R (#6574)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
* Re-organize UpcomingInvoiceHandler for readability

* Milestone 3 renewal

* Map premium access data from additonal data in pricing

* Feedback

* Fix test
2025-11-13 09:09:01 -06:00
Mick Letofsky
de4955a875
[PM-27181] - Grant additional permissions for review code (#6576) 2025-11-13 14:06:26 +00:00
Vince Grassia
a03994d16a
Update build workflow (#6572) 2025-11-13 13:52:26 +01:00
renovate[bot]
0311807951
[deps]: Update actions/upload-artifact action to v5 (#6558)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
2025-11-13 11:31:52 +01:00
SmithThe4th
a836ada6a7
[PM-23059] Provider Users who are also Organization Members cannot edit or delete items via Admin Console when admins can manage all items (#6573)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* removed providers check

* Fixed lint issues
2025-11-12 17:56:17 -05:00
renovate[bot]
7f04830f77
[deps]: Update actions/setup-node action to v6 (#6499) 2025-11-12 14:49:15 -06:00
cyprain-okeke
f0ec201745
[PM 26682]milestone 2d display discount on subscription page (#6542)
* The discount badge implementation

* Address the claude pr comments

* Add more unit testing

* Add more test

* used existing flag

* Add the coupon Ids

* Add more code documentation

* Add some recommendation from claude

* Fix addition comments and prs

* Add more integration test

* Fix some comment and add more test

* rename the test methods

* Add more unit test and comments

* Resolve the null issues

* Add more test

* reword the comments

* Rename Variable

* Some code refactoring

* Change the coupon ID to milestone-2c

* Fix the failing Test
2025-11-12 20:38:21 +01:00
SmithThe4th
de90108e0f
[PM-27579] bw sync does not pull in new items stored in a collection (#6562)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
* Updated sproc to update account revision date after cipher has been created in collection

* check response from update collection success

* removed the org check
2025-11-11 18:44:18 -05:00
Alex Morask
691047039b
[PM-27849] Check for sm-standalone on subscription (#6545)
* Fix coupon check

* Fixed in FF off scenario

* Run dotnet format
2025-11-11 16:01:48 -06:00
Nick Krantz
ea233580d2
add vault skeleton loader feature flag (#6566) 2025-11-11 14:29:55 -06:00
Stephon Brown
db36c52c62
Milestone 2C Update (#6560)
Some checks failed
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
* fix(billing): milestone update

* tests(billing): update tests
2025-11-10 16:07:14 -05:00
Jared McCannon
4de10c830d
[PM-26636] Set Key when Confirming (#6550)
* When confirming a uesr, we need to set the key. :face_palm:

* Adding default value.
2025-11-10 14:46:52 -06:00
Brant DeBow
4fac635272
Remove EventBasedOrganizationIntegrations feature flag (#6538)
* Remove EventBasedOrganizationIntegrations feature flag

* Remove unnecessary nullable enable

* Refactored service collection extensions to follow a more direct path: ASB, RabbitMQ, Azure Queue, Repository, No-op

* Use TryAdd instead of Add
2025-11-10 14:57:04 -05:00
Brant DeBow
212f10d22b
Extend Unit Test Coverage of Event Integrations (#6517)
* Extend Unit Test Coverage of Event Integrations

* Expanded SlackService error handling and tests

* Cleaned up a few issues noted by Claude
2025-11-10 14:55:36 -05:00
Ike
746b413cff
[PM-21741] MJML welcome emails (#6549)
feat: Implement welcome email using MJML templating

- Implement MJML templates for welcome emails (individual, family, org)
- Create reusable MJML components (mj-bw-icon-row, mj-bw-learn-more-footer)
- Update documentation for MJML development process
2025-11-10 13:37:11 -05:00
Kyle Denney
b2543b5c0f
[PM-24284] - milestone 3 (#6543)
* new feature flag

* first pass at changes

* safeguard against billing-pricing not being deployed yet

* handle families pre migration plan

* wrong stripe id

* tests

* unit tests
2025-11-10 11:51:00 -06:00
Rui Tomé
e7f3b6b12f
[PM-26430] Remove Type property from PolicyRequestModel to use route parameter only (#6472)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Enhance PolicyRequestModel and SavePolicyRequest with validation for policy data and metadata.

* Add integration tests for policy updates to validate handling of invalid data types in PolicyRequestModel and SavePolicyRequest.

* Add missing using

* Update PolicyRequestModel for null safety by making Data and ValidateAndSerializePolicyData nullable

* Add integration tests for public PoliciesController to validate handling of invalid data types in policy updates.

* Add PolicyDataValidator class for validating and serializing policy data and metadata based on policy type.

* Refactor PolicyRequestModel, SavePolicyRequest, and PolicyUpdateRequestModel to utilize PolicyDataValidator for data validation and serialization, removing redundant methods and improving code clarity.

* Update PolicyRequestModel and SavePolicyRequest to initialize Data and Metadata properties with empty dictionaries.

* Refactor PolicyDataValidator to remove null checks for input data in validation methods

* Rename test methods in SavePolicyRequestTests to reflect handling of empty data and metadata, and remove null assignments in test cases for improved clarity.

* Remove Type property from PolicyRequestModel to use route parameter only

* Run dotnet format

* Enhance error handling in PolicyDataValidator to include field-specific details in BadRequestException messages.

* Enhance PoliciesControllerTests to verify error messages for BadRequest responses by checking for specific field names in the response content.

* refactor: Update PolicyRequestModel and SavePolicyRequest to use nullable dictionaries for Data and Metadata properties; enhance validation methods in PolicyDataValidator to handle null cases.

* test: Add integration tests for handling policies with null data in PoliciesController

* fix: Catch specific JsonException in PolicyDataValidator to improve error handling

* test: Add unit tests for PolicyDataValidator to validate and serialize policy data and metadata

* test: Remove PolicyType from PolicyRequestModel in PoliciesControllerTests

* test: Update PolicyDataValidatorTests to validate organization data ownership metadata

* Refactor PoliciesControllerTests to include policy type in PutVNext method calls
2025-11-10 15:27:44 +00:00
Oscar Hinton
7d39efe29f
[PM-27575] Add support for loading Mailer templates from disk (#6520)
Adds support for overloading mail templates from disk.
2025-11-10 08:40:40 +01:00
Alex Morask
22fe50c67a
Expand coupon.applies_to (#6554)
Some checks failed
Scan / Check PR run (push) Has been cancelled
Collect code references / Check for secret access (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
2025-11-07 13:05:05 -06:00
Vincent Salucci
d1fecc2a0f
chore: remove custom permissions feature flag definition, refs PM-20168 (#6551) 2025-11-07 09:48:19 -06:00
Oscar Hinton
356e4263d2
Add feature flags for desktop-ui migration (#6548)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-11-07 11:04:27 +01:00
Patrick-Pimentel-Bitwarden
43d14971f5
fix(prevent-bad-existing-sso-user): [PM-24579] Fix Prevent Existing Non Confirmed and Accepted SSO Users (#6529)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
* fix(prevent-bad-existing-sso-user): [PM-24579] Precent Existing Non Confirmed and Accepted SSO Users - Fixed bad code and added comments.

* test(prevent-bad-existing-sso-user): [PM-24579] Precent Existing Non Confirmed and Accepted SSO Users - Added new test to make sure invited users aren't allowed through at the appropriate time.
2025-11-06 13:24:59 -05:00
Stephon Brown
5dbce33f74
[PM-24273] Milestone 2C (#6544)
* feat(billing): add mjml template and updated templates

* feat(billing): update maileservices

* feat(billing): add milestone2 discount

* feat(billing): add milestone 2 updates and stripe constants

* tests(billing): add handler tests

* fix(billing): update mailer view and templates

* fix(billing): revert mailservice changes

* fix(billing): swap mailer service in handler

* test(billing): update handler tests
2025-11-06 13:21:29 -05:00
Nick Krantz
087c6915e7
when ciphers are soft deleted, complete any associated security tasks (#6492) 2025-11-06 08:28:13 -06:00
Oscar Hinton
a1be1ae40b
Group sdk-internal dep (#6530)
* Disable renovate for updates to internal sdk-internal

* Group instead

* Add trailing comma
2025-11-06 14:44:44 +01:00
Rui Tomé
4aed97b76b
[PM-26690] Wire VNextSavePolicyCommand behind PolicyValidatorsRefactor feature flag (#6483)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Add PolicyValidatorsRefactor constant to FeatureFlagKeys in Constants.cs

* Add Metadata property and ToSavePolicyModel method to PolicyUpdateRequestModel

* Refactor PoliciesController to utilize IVNextSavePolicyCommand based on feature flag

- Added IFeatureService and IVNextSavePolicyCommand dependencies to PoliciesController.
- Updated PutVNext method to conditionally use VNextSavePolicyCommand or SavePolicyCommand based on the PolicyValidatorsRefactor feature flag.
- Enhanced unit tests to verify behavior for both enabled and disabled states of the feature flag.

* Update public PoliciesController to to utilize IVNextSavePolicyCommand based on feature flag

- Introduced IFeatureService and IVNextSavePolicyCommand to manage policy saving based on the PolicyValidatorsRefactor feature flag.
- Updated the Put method to conditionally use the new VNextSavePolicyCommand or the legacy SavePolicyCommand.
- Added unit tests to validate the behavior of the Put method for both enabled and disabled states of the feature flag.

* Refactor VerifyOrganizationDomainCommand to utilize IVNextSavePolicyCommand based on feature flag

- Added IFeatureService and IVNextSavePolicyCommand dependencies to VerifyOrganizationDomainCommand.
- Updated EnableSingleOrganizationPolicyAsync method to conditionally use VNextSavePolicyCommand or SavePolicyCommand based on the PolicyValidatorsRefactor feature flag.
- Enhanced unit tests to validate the behavior when the feature flag is enabled.

* Enhance SsoConfigService to utilize IVNextSavePolicyCommand based on feature flag

- Added IFeatureService and IVNextSavePolicyCommand dependencies to SsoConfigService.
- Updated SaveAsync method to conditionally use VNextSavePolicyCommand or SavePolicyCommand based on the PolicyValidatorsRefactor feature flag.
- Added unit tests to validate the behavior when the feature flag is enabled.

* Refactor SavePolicyModel to simplify constructor usage by removing EmptyMetadataModel parameter. Update related usages across the codebase to reflect the new constructor overloads.

* Update PolicyUpdateRequestModel to make Metadata property nullable for improved null safety
2025-11-06 11:35:07 +00:00
Todd Martin
3668a445e5
chore(docs): Add docs for legacy mail service
Some checks failed
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
* Added docs for legacy mail service.

* Updated namespaces.

* Consolidated under Platform.Mail namespace

* Updated obsolete comment.

* Linting

* Linting

* Replaced documentation in original readme after accidental deletion.
2025-11-04 11:54:39 -05:00
Alex Morask
04ed8abf5a
Re-add missing checkbox (#6532) 2025-11-04 07:25:42 -06:00
Bernd Schoolmann
7e54773a6e
Add summary comments for MasterKeyWrappedUserKey in response models (#6531)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-11-04 12:42:07 +01:00
Vincent Salucci
b4d6f3cb35
chore: fix provider account recovery flag key, refs PM-24192 (#6533)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-11-03 13:32:09 -06:00
Dave
bda2bd8ac1
fix(base-request-validator) [PM-21153] Recovery Code Not Functioning for SSO-required Users (#6481)
* chore(feature-flag-keys) [PM-21153]: Add feature flag key for BaseRequestValidator changes.

* fix(base-request-validator) [PM-21153]: Add validation state model for composable validation scenarios.

* fix(base-request-validator) [PM-21153]: Update BaseRequestValidator to allow validation scenarios to be composable.

* fix(base-request-validator) [PM-21153]: Remove validation state object in favor of validator context, per team discussion.

* feat(base-request-validator) [PM-21153]: Update tests to use issue feature flag, both execution paths.

* fix(base-request-validator) [PM-21153]: Fix a null dictionary check.

* chore(base-request-validator) [PM-21153]: Add unit tests around behavior addressed in this feature.

* chore(base-request-validator) [PM-21153]: Update comments for clarity.

* chore(base-request-validator-tests) [PM-21153]: Update verbiage for tests.

* fix(base-request-validator) [PM-21153]: Update validators to no longer need completed scheme management, use 2FA flag for recovery scenarios.

* fix(base-request-validator-tests) [PM-21153]: Customize CustomValidatorRequestContext fixture to allow for setting of request-specific flags as part of the request validation (not eagerly truthy).
2025-11-03 12:24:00 -05:00
Robyn MacCallum
b329305b77
Update description for AutomaticAppLogIn policy (#6522) 2025-11-03 11:11:42 -05:00
Rui Tomé
1e2e4b9d4d
[PM-26429] Add validation to policy data and metadata (#6460)
* Enhance PolicyRequestModel and SavePolicyRequest with validation for policy data and metadata.

* Add integration tests for policy updates to validate handling of invalid data types in PolicyRequestModel and SavePolicyRequest.

* Add missing using

* Update PolicyRequestModel for null safety by making Data and ValidateAndSerializePolicyData nullable

* Add integration tests for public PoliciesController to validate handling of invalid data types in policy updates.

* Add PolicyDataValidator class for validating and serializing policy data and metadata based on policy type.

* Refactor PolicyRequestModel, SavePolicyRequest, and PolicyUpdateRequestModel to utilize PolicyDataValidator for data validation and serialization, removing redundant methods and improving code clarity.

* Update PolicyRequestModel and SavePolicyRequest to initialize Data and Metadata properties with empty dictionaries.

* Refactor PolicyDataValidator to remove null checks for input data in validation methods

* Rename test methods in SavePolicyRequestTests to reflect handling of empty data and metadata, and remove null assignments in test cases for improved clarity.

* Enhance error handling in PolicyDataValidator to include field-specific details in BadRequestException messages.

* Enhance PoliciesControllerTests to verify error messages for BadRequest responses by checking for specific field names in the response content.

* refactor: Update PolicyRequestModel and SavePolicyRequest to use nullable dictionaries for Data and Metadata properties; enhance validation methods in PolicyDataValidator to handle null cases.

* test: Add integration tests for handling policies with null data in PoliciesController

* fix: Catch specific JsonException in PolicyDataValidator to improve error handling

* test: Add unit tests for PolicyDataValidator to validate and serialize policy data and metadata

* test: Update PolicyDataValidatorTests to validate organization data ownership metadata
2025-11-03 15:44:44 +00:00
Rui Tomé
de56b7f327
[PM-26099] Update public list members endpoint to include collections (#6503)
* Add CreateCollectionAsync method to OrganizationTestHelpers for collection creation with user and group associations

* Update public MembersController List endpoint to include associated collections in member response model

* Update MembersControllerTests to validate collection associations in List endpoint. Add JsonConstructor to AssociationWithPermissionsResponseModel

* Refactor MembersController by removing unused IUserService and IApplicationCacheService dependencies.

* Remove nullable disable directive from Public MembersController
2025-11-03 15:24:40 +00:00
Github Actions
0ea9e2e48a Bumped version to 2025.11.0
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
2025-11-03 14:29:04 +00:00
Thomas Rittson
e11458196c
[PM-24192] Move account recovery logic to command (#6184)
Some checks failed
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
* Move account recovery logic to command
  (temporarily duplicated behind feature flag)

* Move permission checks to authorization handler

* Prevent user from recovering provider member account
  unless they are also provider member
2025-11-01 07:55:25 +10:00
Github Actions
09564947e8 Bumped version to 2025.10.2 2025-10-31 21:38:53 +00:00
Jimmy Vo
21cc0b38b0
[PM-26401] Add logging logic (#6523)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
2025-10-31 14:47:22 -04:00
Daniel James Smith
d40d705aac
Revert feature flag removal for Chromium importers (#6526)
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
2025-10-31 18:40:54 +01:00
Alex Morask
410e754cd9
[PM-27553] Resolve premium purchase for user with account credit that used payment method (#6514)
* Update payment method for customer purchasing premium who has account credit but used a payment method

* Claude feedback + dotnet run format
2025-10-31 12:37:01 -05:00
Vijay Oommen
e102a7488e
[PM-26967] Added new metric properties (#6519)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
2025-10-30 16:54:05 -05:00
MtnBurrit0
b8325414bf
Disable environment synchronization in workflow (#6525) 2025-10-30 12:55:28 -07:00
Jared McCannon
07a18d31a9
[PM-27594] - Update Org and License with Token (#6518)
* Updating the license and org with claims when updating via license token.

* Removing the fature flag check and adding a null check.

* Added to method.
2025-10-30 14:34:18 -05:00
Stephon Brown
cfe818e0aa
Milestone 2b Update (#6515)
Some checks failed
Collect code references / Check for secret access (push) Has been cancelled
Scan / Check PR run (push) Has been cancelled
Testing / Run tests (push) Has been cancelled
Scan / Checkmarx (push) Has been cancelled
Scan / Sonar (push) Has been cancelled
Collect code references / Code reference collection (push) Has been cancelled
* feat(billing): add feature flag

* feat(billing): implement feature flag

* fix(billing): update logic

* fix(billing): revert spacing
2025-10-29 13:12:16 -04:00
Kyle Denney
ca0d5bf8cb
[PM-23713] plans controller needs app authorize so desktop and browser can use (#6512) 2025-10-29 10:18:49 -05:00
mkincaid-bw
4b1685d346
Change recovery model for db's in full mode with no t-log backups (#6474) 2025-10-29 08:10:17 -07:00
Ben Brooks
d97593e91d
Add validation to URI Match Default Policy for Single Org prerequisite (#6454)
* Add validation to URI Match Default Policy for Single Org prerequisite

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

* Remove nullable enable; Replace Task.FromResult(0) with Task.CompletedTask

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

* Add unit test for our new validator

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

* Improve comments and whitespace for unit test

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

* Remove unnecessary whitespace in unit test

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

* Remove unneccessary unit tets

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

* Re-add using NSubstitute

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

* Revert unintended changes to AccountControllerTest.cs

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

* Revert unintended changes to AccountControllerTest.cs

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

* Revert unintended changes to HubHelpersTest.cs

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

* Add IEnforceDependentPoliciesEvent interface to UriMatchDefaultPolicyValidator

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

---------

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>
2025-10-29 07:53:48 -07:00
Alex Morask
5f0e0383a5
Remove FF (#6456) 2025-10-29 07:41:42 -05:00
Jordan Aasen
8f2f2046b7
[PM-27554] - add autofill confirm from search flag (#6511)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* add autofill confirm from search flag

* move flag
2025-10-28 15:08:03 -07:00
Matt Bishop
394e91d639
Handle null cipher or organization with event submission (#6509)
* Handle null cipher

* Check for an org being null too

* Add unit and integration tests

* Clean up unused members
2025-10-28 16:31:05 -04:00
renovate[bot]
880a1fd13d
[deps] Auth: Update webpack to v5.102.1 (#6445)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 15:10:25 -04:00
Bryan Cunningham
d307b843f9
Revert "[deps]: Update mjml to v4.16.1 (#6391)" (#6510)
This reverts commit a111aa9fcd.
2025-10-28 14:33:48 -04:00
renovate[bot]
a111aa9fcd
[deps]: Update mjml to v4.16.1 (#6391)
Some checks failed
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
Database testing / Run tests (push) Has been cancelled
Database testing / Run validation (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Bryan Cunningham <bryan.cunningham@me.com>
2025-10-28 12:22:02 -04:00
Graham Walker
2b10907ef3
PM-26966 added new metric columns to organization report table (#6486)
* PM-26966 added new metric columns to organization report table

* PM-26966 fixing migration

* PM-26966 fixing formatting issue.

* PM-26966 updating unit tests

* PM-26966 fixing SQL to read from view
2025-10-28 11:17:45 -05:00
Oscar Hinton
653de07bd7
[PM-23493] Generic mailer proposal (#5958)
This implements a new Mailer service which supersedes the `HandlebarsMailService`. It allows teams to create emails without having to extend a generic service.

The `IMailer` only contains a single method, `SendEmail`, which sends an instance of `BaseMail`.
2025-10-28 15:55:36 +01:00
Alex Morask
62a0936c2e
[PM-25183] Update the BitPay purchasing procedure (#6396)
* Revise BitPay controller

* Run dotnet format

* Kyle's feedback

* Run dotnet format

* Temporary logging

* Whoops

* Undo temporary logging
2025-10-28 09:31:59 -05:00
Kyle Spearrin
02be34159d
fix(vuln): Change OTP and Email providers to use time-constant equality operators
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
2025-10-28 09:51:24 -04:00
renovate[bot]
76d7534d85
[deps] Auth: Update sass to v1.93.2 (#6324)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Testing / Run tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Patrick-Pimentel-Bitwarden <ppimentel@bitwarden.com>
2025-10-27 21:42:18 -04:00
Patrick-Pimentel-Bitwarden
a71eaeaed2
feat(prevent-bad-existing-sso-user): [PM-24579] Prevent Existing Non Confirmed and Accepted SSO Users (#6348)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* feat(prevent-bad-existing-sso-user): [PM-24579] Precent Existing Non Confirmed and Accepted SSO Users - Added in logic to block existing sso org users who are not in the confirmed or accepted state.

* fix(prevent-bad-existing-sso-user): [PM-24579] Precent Existing Non Confirmed and Accepted SSO Users - Added docs as well as made clear what statuses are permissible.

* test(prevent-bad-existing-sso-user): [PM-24579] Precent Existing Non Confirmed and Accepted SSO Users - Added tests.
2025-10-27 14:21:24 -04:00
Brant DeBow
df1d7184f8
Add template context fields for Elastic integration (#6504) 2025-10-27 13:23:01 -04:00
cyprain-okeke
427600d0cc
[PM-26194] Fix: Provider Portal not automatically disabled, when subscription is cancelled (#6480)
Some checks are pending
Collect code references / Check for secret access (push) Waiting to run
Collect code references / Code reference collection (push) Blocked by required conditions
Scan / Check PR run (push) Waiting to run
Scan / Checkmarx (push) Blocked by required conditions
Scan / Sonar (push) Blocked by required conditions
Testing / Run tests (push) Waiting to run
* Add the fix for the bug

* Move the org disable to job
2025-10-27 13:19:42 +01:00
Rui Tomé
9b313d9c0a
[PM-25923] Simplify and align response models for Organization members and Provider users (#6385)
* Update ProviderUserOrganizationDetailsView to include SSO configuration data

* Updated the ProviderUserOrganizationDetailsViewQuery to join with SsoConfigs and select SSO-related fields.
* Modified the SQL view to reflect the inclusion of SSO configuration data.
* Added a new migration script for the updated view structure.

* Add SSO configuration properties to ProviderUserOrganizationDetails model

* Add SSO configuration handling to ProfileProviderOrganizationResponseModel

* Introduced properties for SSO configuration, including SSO enabled status and KeyConnector details.
* Implemented deserialization of SSO configuration data to populate new fields in the response model.

* Add integration tests for ProviderUserRepository.GetManyOrganizationDetailsByUserAsync

* Add BaseUserOrganizationDetails model to encapsulate common properties

* Introduced a new abstract class to define shared properties for organization users and provider organization users

* Add BaseProfileOrganizationResponseModel to encapsulate organization response properties

* Introduced a new abstract class that ensures all properties are fully populated for profile organization responses.

* Update ProviderUserOrganizationDetailsViewQuery to include missing ProviderUserId

* Refactor OrganizationUserOrganizationDetails and ProviderUserOrganizationDetails to inherit from BaseUserOrganizationDetails

* Updated both models to extend BaseUserOrganizationDetails, promoting code reuse and ensure they have the same base properties

* Refactor ProfileOrganizationResponseModel and ProfileProviderOrganizationResponseModel to inherit from BaseProfileOrganizationResponseModel

* Refactor ProviderUserRepositoryTests to improve organization detail assertions

* Consolidated assertions for organization details into a new method, AssertProviderOrganizationDetails, enhancing code readability and maintainability.
* Updated test cases to verify all relevant properties for organizations with and without SSO configurations.

* Add integration test for GetManyDetailsByUserAsync to verify SSO properties

* Implemented a new test case to ensure that the SSO properties are correctly populated for organizations with and without SSO configurations.
* The test verifies the expected behavior of the method when interacting with the user and organization repositories, including cleanup of created entities after the test execution.

* Add unit tests for ProfileOrganizationResponseModel and ProfileProviderOrganizationResponseModel

* Introduced tests to validate the constructors of ProfileOrganizationResponseModel and ProfileProviderOrganizationResponseModel, ensuring that all properties are populated correctly based on the provided organization details.
* Verified expected behavior for both organization and provider models, including SSO configurations and relevant properties.

* Update SyncControllerTests.Get_ProviderPlanTypeProperlyPopulated to nullify SSO configurations in provider user organization details

* Refactor BaseProfileOrganizationResponseModel and ProfileOrganizationResponseModel for null safety

Updated properties in BaseProfileOrganizationResponseModel and ProfileOrganizationResponseModel to support null safety by introducing nullable types where appropriate.

* Enhance null safety in BaseUserOrganizationDetails and OrganizationUserOrganizationDetails

Updated properties in BaseUserOrganizationDetails and OrganizationUserOrganizationDetails to support null safety by introducing nullable types where appropriate, ensuring better handling of potential null values.

* Move common properties from ProfileOrganizationResponseModel to BaseProfileOrganizationResponseModel

* Refactor organization details: Remove BaseUserOrganizationDetails and introduce IProfileMemberOrganizationDetails interface for improved structure and clarity in organization user data management.

* Enhance OrganizationUserOrganizationDetails: Implement IProfileMemberOrganizationDetails interface

* Refactor ProviderUserOrganizationDetails: Implement IProfileMemberOrganizationDetails interface

* Refactor ProfileOrganizationResponseModelTests and ProfileProviderOrganizationResponseModelTests: Update constructors to utilize Organization and ProviderUserOrganizationDetails, enhancing property population and test coverage.

* Enhance ProviderUserOrganizationDetails: Add UseResetPassword, UseSecretsManager, and UsePasswordManager properties to the query and SQL views

* Update BaseProfileOrganizationResponseModel documentation: Clarify purpose and usage of organization properties for OrganizationUsers and ProviderUsers.

* Rename ProfileOrganizationResponseModel to ProfileMemberOrganizationResponseModel, update references and update related test names

* Add XML documentation for ProfileMemberOrganizationResponseModel and ProfileProviderOrganizationResponseModel to clarify their purpose and relationships

* Remove unnecessary cleanup code from OrganizationUserRepositoryTests

* Remove unnecessary cleanup code from ProviderUserRepositoryTests

* Rename test method in ProviderUserRepositoryTests to improve clarity on property population

* Add CreateFullOrganization method to ProviderUserRepositoryTests for improved organization setup in tests

* Refactor organization creation in tests to use CreateTestOrganizationAsync for consistency and improved setup

* Rename IProfileMemberOrganizationDetails to IProfileOrganizationDetails

* Rename ProfileMemberOrganizationResponseModel back to ProfileOrganizationResponseModel

* Refactor organization response models to remove Family Sponsorship properties from BaseProfileOrganizationResponseModel and reintroduce them in ProfileOrganizationResponseModel. Update related interfaces and tests accordingly.

* Bump date on migration script

* Update OrganizationUserOrganizationDetailsViewQuery to include UseAutomaticUserConfirmation property
2025-10-24 20:42:28 +01:00
Mick Letofsky
bd52cf56e7
Implement reusable Claude code review workflow (#6476) 2025-10-24 18:18:27 +02:00
Mick Letofsky
86eb86dac5
Update Claude owners (#6493) 2025-10-24 16:04:22 +02:00
Shane Melton
0b4ce8765e
[PM-23904] Add risk insights for premium feature flag (#6491) 2025-10-24 09:42:10 -04:00
Stephon Brown
ff4b3eb9e5
[PM-27123] Account Credit not Showing for Premium Upgrade Payment (#6484)
* feat(billing): add PaymentMethod union

* feat(billing):  add nontokenized payment method

* feat(billing): add validation for tokinized and nontokenized payments

* feat(billing): update and add payment method requests

* feat(billing): update command with new union object

* test(billing): add tests for account credit for user.

* feat(billing): update premium cloud hosted subscription request

* fix(billing): dotnet format

* tests(billing): include payment method tests

* fix(billing): clean up tests and converter method
2025-10-23 14:47:23 -04:00
Conner Turnbull
b15913ce73
Fix HubHelpers field references causing build error (#6487) 2025-10-23 16:04:47 +02:00
Maciej Zieniuk
dd1f0a120a
Notifications service unit test coverage with small refactor (#6126) 2025-10-23 14:40:57 +02:00
Brant DeBow
69f0464e05
Refactor Azure Service Bus to use the organization id as a partition key (#6477)
* Refactored Azure Service Bus to use the organization id as a partition key

* Use null for partition key instead of empty string when organization id is null
2025-10-23 08:08:09 -04:00
Nik Gilmore
76de64263c
[PM-22992] Check cipher revision date when handling attachments (#6451)
* Add lastKnownRevisionDate to Attachment functions

* Add lastKnownRevisionDate to attachment endpoints

* Change lastKnownCipherRevisionDate to lastKnownRevisionDate for consistency

* Add tests for RevisionDate checks in Attachment endpoints

* Improve validation on lastKnownRevisionDate

* Harden datetime parsing

* Rename ValidateCipherLastKnownRevisionDate - removed 'Async' suffix

* Cleanup and address PR feedback
2025-10-22 16:19:43 -07:00
Ike
9ce1ecba49
[PM-25240] Send Access OTP email in MJML format (#6411)
feat: Add MJML email templates for Send Email OTP
feat: Implement MJML-based email templates for Send OTP functionality
feat: Add feature flag support for Send Email OTP v2 emails
feat: Update email view models and call sites for Send Email OTP

fix: Modify the directory structure for MJML templates to have Auth directory for better team ownership
fix: Rename `hero.js` to `mj-bw-hero.js`

---
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
2025-10-22 15:13:31 -04:00
Alex Morask
6a3fc08957
[PM-26793] Fetch premium plan from pricing service (#6450)
* Fetch premium plan from pricing service

* Run dotnet format
2025-10-22 14:13:16 -05:00
Stephon Brown
0a7e6ae3ca
Billing/pm 27217/update secrets manager test fix (#6478)
* fix(billing): Add comments for clarity

* fix(billing): Update generated properties for test
2025-10-22 13:42:56 -04:00
Matt Andreko
f82125f416
Clean up workflow files from Zizmor output (#6409) 2025-10-22 12:53:46 -04:00
Vijay Oommen
c58f3d590c
PM-27204 feature flag for dataDog and crowdStrike (#6473) 2025-10-22 10:10:44 -05:00
Nick Krantz
b63fdfab0d
[PM-26772] Importing Archived Ciphers (#6452)
* persist archive date for importing ciphers

* throw error if a user imports archived ciphers into an organization

* remove extra semi-colon

* set archive date for initial tests to avoid error thrown

* refactor ArchivedDate query

* add test for throwing for importing archived ciphers into a organization

* remove folder and organization id from test

* remove unneeded org id and null out the folderid
2025-10-22 08:42:29 -05:00
Rui Tomé
3866bc5155
[PM-23134] Update PolicyDetails sprocs for performance (#6421)
* Add integration tests for GetByUserIdWithPolicyDetailsAsync in OrganizationUserRepository

- Implemented multiple test cases to verify the behavior of GetByUserIdWithPolicyDetailsAsync for different user statuses (Confirmed, Accepted, Invited, Revoked).
- Ensured that the method returns correct policy details based on user status and organization.
- Added tests for scenarios with multiple organizations and non-existing policy types.
- Included checks for provider users and custom user permissions.

These tests enhance coverage and ensure the correctness of policy retrieval logic.

* Add UserProviderAccessView to identify which organizations a user can access as a provider

* Refactor PolicyDetails_ReadByUserId stored procedure to improve user access logic

- Introduced a Common Table Expression (CTE) for organization users to streamline the selection process based on user status and email.
- Added a CTE for providers to enhance clarity and maintainability.
- Updated the main query to utilize the new CTEs, improving readability and performance.
- Ensured that the procedure correctly identifies provider access based on user permissions.

* Refactor OrganizationUser_ReadByUserIdWithPolicyDetails stored procedure to enhance user access logic

- Introduced a Common Table Expression (CTE) for organization users to improve selection based on user status and email.
- Updated the main query to utilize the new CTEs, enhancing readability and performance.
- Adjusted the logic for identifying provider access to ensure accurate policy retrieval based on user permissions.

* Add new SQL migration script to refactor policy details queries

- Created a new view, UserProviderAccessView, to streamline user access to provider organizations.
- Introduced two stored procedures: PolicyDetails_ReadByUserId and OrganizationUser_ReadByUserIdWithPolicyDetails, enhancing the logic for retrieving policy details based on user ID and policy type.
- Utilized Common Table Expressions (CTEs) to improve query readability and performance, ensuring accurate policy retrieval based on user permissions and organization status.

* Remove GetPolicyDetailsByUserIdTests

* Refactor PolicyRequirementQuery to use GetPolicyDetailsByUserIdsAndPolicyType and update unit tests

* Remove GetPolicyDetailsByUserId method from IPolicyRepository and its implementations in PolicyRepository classes

* Revert changes to PolicyDetails_ReadByUserId stored procedure

* Refactor OrganizationUser_ReadByUserIdWithPolicyDetails stored procedure to use UNION instead of OR

* Reduce UserEmail variable size from NVARCHAR(320) to NVARCHAR(256) for consistency in stored procedures

* Bump date on migration script
2025-10-22 13:20:53 +01:00
Bernd Schoolmann
0a205722b4
Add SSHAgentV2 constant to Constants.cs (#6461) 2025-10-22 01:11:40 +02:00
Oscar Hinton
44a82d3b22
[PM-22263] Integate Rust SDK to Seeder (#6150)
Adds a Rust SDK for performing seed related cryptograhic operations. It depends on internal portions of our Rust SDK. Primarily parts of the bitwarden-crypto crate.
2025-10-21 23:46:37 +02:00
Alex Morask
9c51c9971b
[PM-21638] Stripe .NET v48 (#6202)
* Upgrade Stripe.net to v48.4.0

* Update PreviewTaxAmountCommand

* Remove unused UpcomingInvoiceOptionExtensions

* Added SubscriptionExtensions with GetCurrentPeriodEnd

* Update PremiumUserBillingService

* Update OrganizationBillingService

* Update GetOrganizationWarningsQuery

* Update BillingHistoryInfo

* Update SubscriptionInfo

* Remove unused Sql Billing folder

* Update StripeAdapter

* Update StripePaymentService

* Update InvoiceCreatedHandler

* Update PaymentFailedHandler

* Update PaymentSucceededHandler

* Update ProviderEventService

* Update StripeEventUtilityService

* Update SubscriptionDeletedHandler

* Update SubscriptionUpdatedHandler

* Update UpcomingInvoiceHandler

* Update ProviderSubscriptionResponse

* Remove unused Stripe Subscriptions Admin Tool

* Update RemoveOrganizationFromProviderCommand

* Update ProviderBillingService

* Update RemoveOrganizatinoFromProviderCommandTests

* Update PreviewTaxAmountCommandTests

* Update GetCloudOrganizationLicenseQueryTests

* Update GetOrganizationWarningsQueryTests

* Update StripePaymentServiceTests

* Update ProviderBillingControllerTests

* Update ProviderEventServiceTests

* Update SubscriptionDeletedHandlerTests

* Update SubscriptionUpdatedHandlerTests

* Resolve Billing test failures

I completely removed tests for the StripeEventService as they were using a system I setup a while back that read JSON files of the Stripe event structure. I did not anticipate how frequently these structures would change with each API version and the cost of trying to update these specific JSON files to test a very static data retrieval service far outweigh the benefit.

* Resolve Core test failures

* Run dotnet format

* Remove unused provider migration

* Fixed failing tests

* Run dotnet format

* Replace the old webhook secret key with new one (#6223)

* Fix compilation failures in additions

* Run dotnet format

* Bump Stripe API version

* Fix recent addition: CreatePremiumCloudHostedSubscriptionCommand

* Fix new code in main according to Stripe update

* Fix InvoiceExtensions

* Bump SDK version to match API Version

* Fix provider invoice generation validation

* More QA fixes

* Fix tests

* QA defect resolutions

* QA defect resolutions

* Run dotnet format

* Fix tests

---------

Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
2025-10-21 14:07:55 -05:00
Maciej Zieniuk
6324f692b8
[PM-23249] Prevent log-out when changing KDF settings (#6349)
* Prevent log-out when changing KDF settings with feature flag.

* validate salt unchanged for user to throw bad request (400), not internal server error (500)

* change kdf integration tests

* failing tests

* iuncorrect tests wording

* conditional logout

* log out reason as enum

* explicit naming
2025-10-21 19:03:25 +02:00
Maciej Zieniuk
8d52ae869c
Add pm-26056-consolidated-session-timeout-component feature flag (#6470) 2025-10-20 17:52:38 +02:00
Dave
629672c4b0
Add tests for ProfileService (#6466)
* feat(profile-service) [PM-24621]: Add ProfileService test fixtures.

* feat(profile-service) [PM-24621]: Add ProfileService test suite.

* feat(profile-servie) [PM-24621]: Re-spell to more consistently use constants across tests.
2025-10-20 11:45:11 -04:00
Justin Baur
c6f1acede9
[BEEEP] Fix all CA2254 occurrences (#6357)
* Fix all CA2254 occurrences

* Fix tests
2025-10-20 11:34:31 -04:00
Stephon Brown
43779cf2e8
refactor(billing): Remove unused request model (#6463) 2025-10-20 10:17:38 -04:00
Jared McCannon
dbce45291c
[PM-26361] Add User Auto Confirmation (#6436)
* Adding AutoConfrim and migrations.

* Add value to Admin Page and update sproc to correct name.

* Correcting license constant.

* Adding feature check back in.

* Fixing sprocs :face_palm:

* Remove Coalesce

* Adding property to plan and model constructor

* Correcting name of column.  Cascading change throughout. Updating response models. Updating sprocs and views. Updating migrations

* fixing sproc

* Fixing up license stuff.

* Updating org view

* Code review changes and renames :face_palm:

* Refershing additional views

* Last two fixes.
2025-10-20 07:27:18 -05:00
Bernd Schoolmann
4bf7cf956b
[PM-21034] Feature Branch - "User Crypto V2" (#5982)
* [PM-21034] Database changes for signature keypairs (#5906)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Drop view if exists

* Enable nullable

* Replace with create or alter view

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* [PM-21034] Implement api changes to retreive signing keys (#5932)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Add initial get keys endpoint

* Add sync response

* Cleanup

* Add query and fix types

* Add tests and cleanup

* Fix test

* Drop view if exists

* Add km queries

* Cleanup

* Enable nullable

* Cleanup

* Cleanup

* Enable nullable

* Fix incorrect namespace

* Remove unused using

* Fix test build

* Fix build error

* Fix build

* Attempt to fix tests

* Attempt to fix tests

* Replace with create or alter view

* Attempt to fix tests

* Attempt to fix build

* Rename to include async suffix

* Fix test

* Rename repo

* Attempt to fix tests

* Cleanup

* Test

* Undo test

* Fix tests

* Fix test

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Fix build

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Cleanup and move query to core

* Fix test

* Fix build

* Fix tests

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Switch away from primary constructor

* Use argumentNullException

* Add test

* Pass user account keys directly to profileresponsemodel

* Move registration to core

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove empty line

* Apply suggestions

* Fix tests

* Fix tests

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* [PM-22384] Implement key-rotation based enrollment to user-crypto v2 (#5934)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Add initial get keys endpoint

* Add sync response

* Cleanup

* Add query and fix types

* Add tests and cleanup

* Fix test

* Drop view if exists

* Add km queries

* Cleanup

* Enable nullable

* Cleanup

* Cleanup

* Enable nullable

* Fix incorrect namespace

* Remove unused using

* Fix test build

* Fix build error

* Fix build

* Attempt to fix tests

* Attempt to fix tests

* Replace with create or alter view

* Attempt to fix tests

* Attempt to fix build

* Rename to include async suffix

* Fix test

* Rename repo

* Attempt to fix tests

* Cleanup

* Test

* Undo test

* Fix tests

* Fix test

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Fix build

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Cleanup

* Add migration to user encryption v2

* Fix build

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Cleanup

* Cleanup

* Fix build

* Fix test

* Add validation

* Fix test

* Apply fixes

* Fix tests

* Improve tests

* Add tests

* Add error message validation

* Fix tests

* Fix tests

* Fix test

* Add test

* Fix tests and errors

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Cleanup and move query to core

* Fix test

* Fix build

* Fix tests

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Switch away from primary constructor

* Use argumentNullException

* Add test

* Pass user account keys directly to profileresponsemodel

* Fix build

* Fix namespace

* Make signedpublickey optional

* Remove unused file

* Fix cases for request data conversion

* Revert constructor change

* Undo comments change

* Apply fixes

* Move registration to core

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove empty line

* Apply suggestions

* Fix tests

* Fix tests

* Fix build of integration tests

* Attempt to fix tests

* Add test

* Move v2 encryption user async below public functions

* Add todo

* Rename to have async suffix

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Address feedback

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test coverage

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Split up validation from rotation

* Fix tests

* Increase test coverage

* Rename tests

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test for no signature keypair data

* Fix build

* Enable nullable

* Fix build

* Clean up data model

* Fix tests

* Cleanup

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Fix build

* [PM-22862] Account security version (#5995)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Add initial get keys endpoint

* Add sync response

* Cleanup

* Add query and fix types

* Add tests and cleanup

* Fix test

* Drop view if exists

* Add km queries

* Cleanup

* Enable nullable

* Cleanup

* Cleanup

* Enable nullable

* Fix incorrect namespace

* Remove unused using

* Fix test build

* Fix build error

* Fix build

* Attempt to fix tests

* Attempt to fix tests

* Replace with create or alter view

* Attempt to fix tests

* Attempt to fix build

* Rename to include async suffix

* Fix test

* Rename repo

* Attempt to fix tests

* Cleanup

* Test

* Undo test

* Fix tests

* Fix test

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Fix build

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Cleanup

* Add migration to user encryption v2

* Fix build

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Cleanup

* Cleanup

* Fix build

* Fix test

* Add validation

* Fix test

* Apply fixes

* Fix tests

* Improve tests

* Add tests

* Add error message validation

* Fix tests

* Fix tests

* Fix test

* Add test

* Fix tests and errors

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Cleanup and move query to core

* Fix test

* Fix build

* Fix tests

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Switch away from primary constructor

* Use argumentNullException

* Add test

* Pass user account keys directly to profileresponsemodel

* Fix build

* Fix namespace

* Make signedpublickey optional

* Remove unused file

* Fix cases for request data conversion

* Revert constructor change

* Undo comments change

* Apply fixes

* Move registration to core

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove empty line

* Apply suggestions

* Fix tests

* Fix tests

* Fix build of integration tests

* Attempt to fix tests

* Add test

* Move v2 encryption user async below public functions

* Add todo

* Rename to have async suffix

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Address feedback

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test coverage

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Split up validation from rotation

* Fix tests

* Increase test coverage

* Rename tests

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test for no signature keypair data

* Fix build

* Enable nullable

* Fix build

* Clean up data model

* Fix tests

* Merge branch 'km/signing-upgrade-rotation' into km/account-security-version

* Add security state to rotation

* Update tests

* Update tests and check for security state in v2 model

* Cleanup

* Add tests

* Add security state data to integration test

* Re-sort and remove limit

* Update migrations

* Fix sql

* Fix sql

* Fix sql

* Fix fixture

* Fix test

* Fix test

* Fix test

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* [PM-22853] Add feature flag (#6090)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Add initial get keys endpoint

* Add sync response

* Cleanup

* Add query and fix types

* Add tests and cleanup

* Fix test

* Drop view if exists

* Add km queries

* Cleanup

* Enable nullable

* Cleanup

* Cleanup

* Enable nullable

* Fix incorrect namespace

* Remove unused using

* Fix test build

* Fix build error

* Fix build

* Attempt to fix tests

* Attempt to fix tests

* Replace with create or alter view

* Attempt to fix tests

* Attempt to fix build

* Rename to include async suffix

* Fix test

* Rename repo

* Attempt to fix tests

* Cleanup

* Test

* Undo test

* Fix tests

* Fix test

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Fix build

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Cleanup

* Add migration to user encryption v2

* Fix build

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Cleanup

* Cleanup

* Fix build

* Fix test

* Add validation

* Fix test

* Apply fixes

* Fix tests

* Improve tests

* Add tests

* Add error message validation

* Fix tests

* Fix tests

* Fix test

* Add test

* Fix tests and errors

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Cleanup and move query to core

* Fix test

* Fix build

* Fix tests

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Switch away from primary constructor

* Use argumentNullException

* Add test

* Pass user account keys directly to profileresponsemodel

* Fix build

* Fix namespace

* Make signedpublickey optional

* Remove unused file

* Fix cases for request data conversion

* Revert constructor change

* Undo comments change

* Apply fixes

* Move registration to core

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove empty line

* Apply suggestions

* Fix tests

* Fix tests

* Fix build of integration tests

* Attempt to fix tests

* Add test

* Move v2 encryption user async below public functions

* Add todo

* Rename to have async suffix

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Address feedback

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test coverage

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Split up validation from rotation

* Fix tests

* Increase test coverage

* Rename tests

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test for no signature keypair data

* Fix build

* Enable nullable

* Fix build

* Clean up data model

* Fix tests

* Merge branch 'km/signing-upgrade-rotation' into km/account-security-version

* Add security state to rotation

* Update tests

* Add feature flag

* Update tests and check for security state in v2 model

* Cleanup

* Add tests

* Add security state data to integration test

* Re-sort and remove limit

* Update migrations

* Fix sql

* Fix sql

* Fix sql

* Fix fixture

* Fix test

* Fix test

* Fix test

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* [PM-23222] Update revision date on key rotation (#6038)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Add initial get keys endpoint

* Add sync response

* Cleanup

* Add query and fix types

* Add tests and cleanup

* Fix test

* Drop view if exists

* Add km queries

* Cleanup

* Enable nullable

* Cleanup

* Cleanup

* Enable nullable

* Fix incorrect namespace

* Remove unused using

* Fix test build

* Fix build error

* Fix build

* Attempt to fix tests

* Attempt to fix tests

* Replace with create or alter view

* Attempt to fix tests

* Attempt to fix build

* Rename to include async suffix

* Fix test

* Rename repo

* Attempt to fix tests

* Cleanup

* Test

* Undo test

* Fix tests

* Fix test

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Fix build

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Cleanup

* Add migration to user encryption v2

* Fix build

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Cleanup

* Cleanup

* Fix build

* Fix test

* Add validation

* Fix test

* Apply fixes

* Fix tests

* Improve tests

* Add tests

* Add error message validation

* Fix tests

* Fix tests

* Fix test

* Add test

* Fix tests and errors

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Cleanup and move query to core

* Fix test

* Fix build

* Fix tests

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Switch away from primary constructor

* Use argumentNullException

* Add test

* Pass user account keys directly to profileresponsemodel

* Fix build

* Fix namespace

* Make signedpublickey optional

* Remove unused file

* Fix cases for request data conversion

* Revert constructor change

* Undo comments change

* Apply fixes

* Move registration to core

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove empty line

* Apply suggestions

* Fix tests

* Fix tests

* Fix build of integration tests

* Attempt to fix tests

* Add test

* Move v2 encryption user async below public functions

* Add todo

* Rename to have async suffix

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Address feedback

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test coverage

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Split up validation from rotation

* Fix tests

* Increase test coverage

* Rename tests

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test for no signature keypair data

* Fix build

* Enable nullable

* Fix build

* Clean up data model

* Fix tests

* Merge branch 'km/signing-upgrade-rotation' into km/account-security-version

* Add security state to rotation

* Update tests

* Update revision date on key rotation

* Update tests and check for security state in v2 model

* Cleanup

* Add tests

* Add security state data to integration test

* Re-sort and remove limit

* Update migrations

* Fix sql

* Fix sql

* Fix sql

* Fix fixture

* Fix test

* Fix test

* Fix test

* Add test for change date

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Fix signing keys

* Update sql migrations

* Fix tests

* Add keys to identity token response

* Fix tests

* Fix tests

* Fix formatting

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Requests/SignatureKeyPairRequestModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Requests/PublicKeyEncryptionKeyPairRequestModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Data/PublicKeyEncryptionKeyPairData.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Data/SecurityStateData.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Request/SecurityStateModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Response/PublicKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Response/PublicKeyEncryptionKeyPairResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Queries/Interfaces/IUserAcountKeysQuery.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Response/SignatureKeyPairResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove unnecessary file

* Add eof spacing

* Move models

* Fix build

* Move models to API subdirectory

* Rename model

* Remove migrations

* Add new ef migrations

* Remove empty line

* Only query account keys if the user has keys

* Dotnet format

* Fix test

* Update test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Apply suggestion

* Fix whitespace

* Force camel case on response models

* Address feedback for sql files

* Fix build

* Make index unique

* Add contstraints

* Fix sql

* Fix order

* Cleanup

* Fix build

* Update migrations

* Update EF migrations

* Change parameters to nvarchar

* Update to Varchar

* Apply feedback

* Move refresh view

* Attempt to fix build

* Undo sql changes

* Apply feedback about varchar

* Apply feedback about refresh view

* Apply feedback about new lines

* Address SQL feedback

* Re-sort columns

* Fix build

* Fix order

* Fix build

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
2025-10-20 12:51:08 +02:00
Github Actions
a68e2b9eb5 Bumped version to 2025.10.1 2025-10-20 10:34:57 +00:00
Daniel James Smith
39f38f5031
Add feature flag for chromium importer with ABE (#6464)
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
2025-10-17 20:44:04 +02:00
mpbw2
652da9644d
[PM-20036][PM-20153][PM-20156][PM-26075] remove mobile feature flags (#6380) 2025-10-17 12:33:01 -03:00
cd-bitwarden
2965b499e9
[SM-1591] Adding SecretVersion table to server (#6406)
* Adding SecretVersion table to server

* making the names singular not plural for new table

* removing migration

* fixing migration

* Adding indexes for serviceacct and orguserId

* indexes for sqllite

* fixing migrations

* adding indexes to secretVeriosn.sql

* tests

* removing tests

* adding GO
2025-10-16 15:35:14 -04:00
Jared McCannon
449603d180
[PM-26632] - Adding Idempotent Confirm User (#6459)
* Added repo call for idempotent user confirm. PLUS TESTS!

* Code review changes
2025-10-16 11:19:48 -05:00
Rui Tomé
132db95fb7
[PM-26683] Migrate individual policy handlers/validators to the new Policy Update Events pattern (#6458)
* Implement IOnPolicyPreUpdateEvent for FreeFamiliesForEnterprisePolicyValidator and add corresponding unit tests

* Implement IEnforceDependentPoliciesEvent in MaximumVaultTimeoutPolicyValidator

* Rename test methods in FreeFamiliesForEnterprisePolicyValidatorTests for consistency

* Implement IPolicyValidationEvent and IEnforceDependentPoliciesEvent in RequireSsoPolicyValidator and enhance unit tests

* Implement IPolicyValidationEvent and IEnforceDependentPoliciesEvent in ResetPasswordPolicyValidator and add unit tests

* Implement IOnPolicyPreUpdateEvent in TwoFactorAuthenticationPolicyValidator and add unit tests

* Implement IPolicyValidationEvent and IOnPolicyPreUpdateEvent in SingleOrgPolicyValidator with corresponding unit tests

* Implement IOnPolicyPostUpdateEvent in OrganizationDataOwnershipPolicyValidator and add unit tests for ExecutePostUpsertSideEffectAsync

* Refactor policy validation logic in VNextSavePolicyCommand to simplify enabling and disabling requirements checks

* Refactor VNextSavePolicyCommand to replace IEnforceDependentPoliciesEvent with IPolicyUpdateEvent and update related tests

* Add AddPolicyUpdateEvents method and update service registration for policy update events
2025-10-16 10:18:37 +01:00
Bernd Schoolmann
0fb7099620
Add feature flag (#6453) 2025-10-15 14:36:51 +02:00
Jordan Aasen
018ecc05ed
remove feature flag (#6419) 2025-10-14 16:43:18 -07:00
Alex Morask
58e9b4aa9c
Remove unused FF (#6455) 2025-10-14 18:42:29 +00:00
Alex Morask
f743d5e853
Save organization's customer ID before invoking hasPaymentMethodQuery (#6442) 2025-10-14 13:11:19 -05:00
Bernd Schoolmann
c2302c0dad
Add feature flag (#6399) 2025-10-14 10:41:27 -05:00
renovate[bot]
2d41e64a8b
[deps]: Update actions/setup-dotnet action to v5 (#6333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 10:39:57 +02:00
Kyle Denney
fedc6b865b
[PM-25379] Refactor org metadata (#6441)
* ignore serena

* removing unused properties from org metadata

* removing further properties that can already be fetched on the client side using available data

* new vnext endpoint for org metadata plus caching metadata first pass

including new feature flag

# Conflicts:
#	src/Core/Constants.cs

* [PM-25379] decided against cache and new query shouldn't use the service

* pr feedback

removing unneeded response model

* run dotnet format
2025-10-13 10:49:55 -05:00
renovate[bot]
d3fbfc75f0
[deps]: Update anchore/scan-action action to v7 (#6393)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 15:36:04 +02:00
Nick Krantz
42568b6494
[PM-26316] Prevent users from sharing archived cipher (#6443)
* prevent users from sharing an archived cipher

* move check outside of encrypted check

* add check for cipher stored in the DB does not have an archive date
2025-10-13 08:03:57 -05:00
cyprain-okeke
84534eb8f9
Add the PremiumUpgradeNewDesign flag (#6438) 2025-10-13 13:14:49 +01:00
John Harrington
ac20c84611
[PM-24747] Remove chromium importer feature flag (#6415) 2025-10-10 13:29:47 -07:00
Ike
d722314e2d
[PM-26551] MJML build script (#6417)
* docs: update readme for MJML
* docs: add readme for MailTemplates directory
* feat: add node build script for cross platform build support
2025-10-10 12:15:29 -04:00
Jimmy Vo
6072104153
Ac/pm 25823/vnext policy upsert pattern (#6426) 2025-10-10 11:23:02 -04:00
Brant DeBow
a565fd9ee4
Add Microsoft Teams integration (#6410)
* Add Microsoft Teams integration

* Fix method naming error

* Expand and clean up unit test coverage

* Update with PR feedback

* Add documentation, add In Progress logic/tests for Teams

* Fixed lowercase Slack

* Added docs; Updated PR suggestions;

* Fix broken tests
2025-10-10 10:39:31 -04:00
Kyle Denney
3272586e31
Revert "[PM-25379] Refactor org metadata (#6418)" (#6439)
This reverts commit 3bef57259d.
2025-10-10 09:06:58 -05:00
Alex Morask
c9970a0782
Resolve tax estimation for Families scenarios (#6437) 2025-10-10 08:19:45 -05:00
Justin Baur
b39e486e08
Switch to using built in source link feature (#6297)
* Switch to using built in source link feature

* Switch to using types assembly

* Formatting

* Make version retrieval safer

* Add debug message

* Apply suggestions from code review

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>

---------

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
2025-10-10 09:14:27 -04:00
Kyle Denney
3bef57259d
[PM-25379] Refactor org metadata (#6418)
* ignore serena

* removing unused properties from org metadata

* removing further properties that can already be fetched on the client side using available data

* new vnext endpoint for org metadata plus caching metadata first pass

including new feature flag

# Conflicts:
#	src/Core/Constants.cs

* [PM-25379] decided against cache and new query shouldn't use the service

* pr feedback

removing unneeded response model

* run dotnet format
2025-10-09 15:50:07 -05:00
Alex Morask
34f5ffd981
[PM-26692] Count unverified setup intent as payment method during organization subscription creation (#6433)
* Updated check that determines whether org has payment method to include bank account when determining how to set trial_settings

* Run dotnet format
2025-10-09 13:20:28 -05:00
Mick Letofsky
712926996e
PM-26727 - First iteration of Claude Code Review tuning (#6435)
* Sticky comments + pre-load specific project files + align prompts with clients repo
2025-10-09 16:54:14 +00:00
Vijay Oommen
a6726d2e04
PM-26208 updated api endpoint (#6431) 2025-10-09 10:47:54 -05:00
renovate[bot]
fe32e885c6
[deps] Auth: Update Sustainsys.Saml2.AspNetCore2 to 2.11.0 (#6207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 15:47:29 -04:00
Vincent Salucci
0fbc314fb4
[PM-24954] [PM-24955] Remove BulkResourceCreationService Feature Flag (#6428)
* chore: remove ff implementation and superflous tests, refs PM-24954

* chore: remove UpdateForKeyRotation vNext, refs PM-24954

* chore: remove CreateAsync vNext for ImportCiphersCommand, refs PM-24954

* chore: remove UpdateCiphersAsync vNext from CipherService, refs PM-24954

* chore: formatting, refs PM-24954

* chore: fix CipherRepositoryTests by removing vNext references, refs PM-24954

* chore: remove bulk cipher copy ff key, refs PM-24954
2025-10-08 12:21:02 -05:00
Vincent Salucci
14aa450e7c
chore: remove limit item deletion feature flag reference, refs PM-17155 (#6423) 2025-10-08 10:04:02 -05:00
Kyle Denney
e191ae9651
consolidating 2 feature flags into one (#6430)
it was determined we don't need the one that was removed
2025-10-08 09:21:23 -05:00
Vincent Salucci
0e1edadeb7
chore: remove sso details feature flag reference, refs PM-21472 (#6422) 2025-10-08 09:15:01 -05:00
renovate[bot]
98210b5e9c
[deps]: Update actions/github-script action to v8 (#6332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
2025-10-08 11:44:07 +02:00
Kyle Denney
876a2133b3
[PM-23713] new feature flag for premium badge interaction (#6425) 2025-10-07 16:08:02 -05:00
Thomas Rittson
474e3f163c
Fix MariaDB support for development (#6420)
- fix volume error when starting Docker container
- fix overlapping indexes of integration test databases
- add ef profile to start all EF containers at once
2025-10-08 06:59:43 +10:00
Jordan Aasen
6834cf72ba
remove feature flag (#5979) 2025-10-07 11:42:46 -07:00
Conner Turnbull
7ceccafa7e
Add license regression tests with frozen versions (#6408) 2025-10-07 18:37:07 +00:00
Kyle Denney
8f41379548
[PM-24032] new feature flag for premium button (#6404) 2025-10-07 09:09:04 -05:00
cyprain-okeke
b0cc2d23e2
Resolve the returned error message (#6414) 2025-10-07 11:49:00 +01:00
Jared McCannon
4450d43c5b
Adding AutoConfirm policy type to support future work. (#6416) 2025-10-06 15:04:50 -05:00
Anders Åberg
f75ad36770
PM-13632: Add support for configuring multiple allowed origins (#6317)
* Add support for configuring multiple allowed origins

* Use if/else instead of union

* Add conditionals

* Added Chromium based extension ID's

* format

* Update src/Core/Constants.cs

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>

* remove chromedevelopmentid

* format

---------

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
2025-10-06 14:15:05 +00:00
Github Actions
a15974029e Bumped version to 2025.10.0 2025-10-06 10:45:33 +00:00
renovate[bot]
baf53d1a5a
[deps] Tools: Update MailKit to 4.14.0 (#6413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 11:51:07 +02:00
renovate[bot]
53d9d52405
[deps] Tools: Update aws-sdk-net monorepo (#6412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 11:50:18 +02:00
Kyle Spearrin
60d701c945
[PM-25427] Allow reading mail templates from disk (#6123)
* Allow reading mail templates from self host disk

* Update src/Core/Services/Implementations/HandlebarsMailService.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/Core/Services/Implementations/HandlebarsMailService.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* tweak logic

* some error handling reading templates from disk

* fix: broken test

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
2025-10-06 10:13:56 +02:00
cd-bitwarden
d2577f670e
[Sm-1615] ma identifiers (#6407)
* Adding new logging for secrets

* fixing secrest controller tests

* fixing the tests

* Server side changes for adding ProjectId to Event table, adding Project event logging to projectsController

* Rough draft with TODO's need to work on EventRepository.cs, and ProjectRepository.cs

* Undoing changes to make projects soft delete, we want those to be fully deleted still. Adding GetManyTrashedSecretsByIds to secret repo so we can get soft deleted secrets, getSecrets in eventsController takes in orgdId, so that we can check the permission even if the secret was permanently deleted and doesn' thave the org Id set. Adding Secret Perm Deleted, and Restored to event logs

* db changes

* fixing the way we log events

* Trying to undo some manual changes that should have been migrations

* adding migration files

* fixing test

* setting up userid for project controller tests

* adding sql

* sql

* Rename file

* Trying to get it to for sure add the column before we try and update sprocs

* Adding code to refresh the view to include ProjectId I hope

* code improvements

* Suggested changes

* suggested changes

* trying to fix sql issues

* fixing swagger issue

* Update src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Suggested changes

* Adding event logging for machine accounts

* fixing two tests

* trying to fix all tests

* trying to fix tests

* fixing test

* Migrations

* fix

* updating eps

* adding migration

* Adding missing SQL changes

* updating sql

* fixing sql

* running migration again

* fixing sql

* adding query to add grantedSErviceAccountId to event table

* Suggested improvements

* removing more migrations

* more removal

* removing all migrations to them redo them

* redoing migration

* Copilot suggested fixes

* fixing view

* fixing test

* fix?

* trying to fix failing test

* fixing db test

* Fix

* "fix"

* last "fix" before I lose my mind

* ef migrations

* Removing bad migration and adding indexes properly

* order results chronologically

* order by desc on machine account events

* Changing the queries to not unecessarily create the event view again

* Fixing dapper for grantedServiceAccountId

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
2025-10-03 12:50:01 -04:00
Rui Tomé
a14cc9f932
[PM-25315] Mark enum EventType.Organization_CollectionManagement_Updated as obsolete for historical data reference (#6373) 2025-10-03 14:32:31 +01:00
Brant DeBow
cde458760c
Refactor Slack Callback Mechanism (#6388)
* Refactor Slack Callback

* Add more safety to state param, clarify if logic, update tests

* Added an additional 2 possible cases to test: integration is not a slack integration, and the integration has already been claimed

* Implement SonarQube suggestion

* Adjusted org hash to include timestamp; addressed PR feedback
2025-10-03 09:30:29 -04:00
cyprain-okeke
1dc4c327e4
Add the feature flag (#6405) 2025-10-02 16:11:04 +01:00
Brant DeBow
fe88275cd0
Add default values for MaxConcurrentCalls and PrefetchCount across event and integration ASB listeners (#6403)
* Add default values for MaxConcurrentCalls and PrefetchCount across all event and integration ASB listeners

* Fix test failure
2025-10-02 08:34:51 -04:00
renovate[bot]
28fa3037a4
[deps] BRE: Update rabbitmq Docker tag to v4.1.3 (#6205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 13:04:44 -04:00
rr-bw
e2f96be4dc
refactor(sso-config-tweaks): [Auth/PM-933] Make Single Sign-On URL required regardless of EntityId (#6314)
Makes the Single Sign-On URL required regardless of the EntityId
2025-10-01 08:55:03 -07:00
Alex Morask
61265c7533
[PM-25463] Work towards complete usage of Payments domain (#6363)
* Use payment domain

* Run dotnet format and remove unused code

* Fix swagger

* Stephon's feedback

* Run dotnet format
2025-10-01 10:26:39 -05:00
Rui Tomé
7cefca330b
[PM-26050] Migrate all DefaultUserCollection when claimed user is deleted (#6366)
* feat: migrate DefaultUserCollection to SharedCollection during user deletion

- Implemented migration of DefaultUserCollection to SharedCollection in EF UserRepository before deleting organization users.
- Updated stored procedures User_DeleteById and User_DeleteByIds to include migration logic.
- Added new migration script for updating stored procedures.

* Add unit test for user deletion and DefaultUserCollection migration

- Implemented a new test to verify the migration of DefaultUserCollection to SharedCollection during user deletion in UserRepository.
- The test ensures that the user is deleted and the associated collection is updated correctly.

* Refactor user deletion process in UserRepository

- Moved migrating DefaultUserCollection to SharedCollection to happen before the deletion of user-related entities.
- Updated the deletion logic to use ExecuteDeleteAsync for improved performance and clarity.
- Ensured that all related entities are removed in a single transaction to maintain data integrity.

* Add unit test for DeleteManyAsync in UserRepository

- Implemented a new test to verify the deletion of multiple users and the migration of their DefaultUserCollections to SharedCollections.
- Ensured that both users are deleted and their associated collections are updated correctly in a single transaction.

* Refactor UserRepositoryTests to use test user creation methods and streamline collection creation

* Ensure changes are saved after deleting users in bulk

* Refactor UserRepository to simplify migration queries and remove unnecessary loops for better performance

* Refactor UserRepository to encapsulate DefaultUserCollection migration logic in a separate method

* Refactor UserRepository to optimize deletion queries by using joins instead of subqueries for improved performance

* Refactor UserRepositoryTest DeleteManyAsync_Works to ensure GroupUser and CollectionUser deletion

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
2025-10-01 14:28:19 +01:00
cd-bitwarden
bca1d585c5
[SM-1489] machine account events (#6187)
* Adding new logging for secrets

* fixing secrest controller tests

* fixing the tests

* Server side changes for adding ProjectId to Event table, adding Project event logging to projectsController

* Rough draft with TODO's need to work on EventRepository.cs, and ProjectRepository.cs

* Undoing changes to make projects soft delete, we want those to be fully deleted still. Adding GetManyTrashedSecretsByIds to secret repo so we can get soft deleted secrets, getSecrets in eventsController takes in orgdId, so that we can check the permission even if the secret was permanently deleted and doesn' thave the org Id set. Adding Secret Perm Deleted, and Restored to event logs

* db changes

* fixing the way we log events

* Trying to undo some manual changes that should have been migrations

* adding migration files

* fixing test

* setting up userid for project controller tests

* adding sql

* sql

* Rename file

* Trying to get it to for sure add the column before we try and update sprocs

* Adding code to refresh the view to include ProjectId I hope

* code improvements

* Suggested changes

* suggested changes

* trying to fix sql issues

* fixing swagger issue

* Update src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Suggested changes

* Adding event logging for machine accounts

* fixing two tests

* trying to fix all tests

* trying to fix tests

* fixing test

* Migrations

* fix

* updating eps

* adding migration

* Adding missing SQL changes

* updating sql

* fixing sql

* running migration again

* fixing sql

* adding query to add grantedSErviceAccountId to event table

* Suggested improvements

* removing more migrations

* more removal

* removing all migrations to them redo them

* redoing migration

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
2025-10-01 13:13:49 +00:00
Maciej Zieniuk
721fda0aaa
[PM-25473] Non-encryption passkeys prevent key rotation (#6359)
* use webauthn credentials that have encrypted user key for user key rotation

* where condition simplification
2025-09-30 17:30:00 +02:00
Nick Krantz
12303b3acf
When deleting an archived clear the archived date so it will be restored to the vault (#6398) 2025-09-30 10:04:11 -05:00
John Harrington
fc07dec3a6
PM-25915 tools exclude items in my items collections and my items collection from org vault export endpoint (#6362)
Exclude MyItems and MyItems collection from Organizational Exports when CreateDefaultLocation feature flag is enabled
2025-09-30 07:43:43 -07:00
Alexey Zilber
718d96cc58
Increased usable port range for ephemeral ports from 26,669 to 59,976 (#6394) 2025-09-30 12:28:30 +08:00
rr-bw
8784907736
chore(flag-removal): [Auth/PM20439] Remove Flagging Logic for BrowserExtensionLoginApproval (#6368) 2025-09-29 13:29:18 -07:00
Maciej Zieniuk
f6b99a7906
adds pm-23995-no-logout-on-kdf-change feature flag (#6397) 2025-09-29 21:00:09 +02:00
Tyler
ca3d05c723
BRE-1040 Dockerfiles shared ownership (#6257)
* Include AppSec team and BRE dept for repository-level ownership of Dockerfile, and Dockerfile related, files.
2025-09-29 14:40:20 -04:00
Nick Krantz
46958cc838
[PM-25982] Restrict Ciphers being assigned to Default from Shared collections (#6382)
* validate that any change in collection does not allow only shared ciphers to migrate to a default cipher

* refactor order of checks to avoid any unnecessary calls

* remove unneeded conditional
2025-09-29 13:06:52 -05:00
Brandon Treston
f1af331a0c
remove feature flag (#6395) 2025-09-29 13:22:39 -04:00
Patrick-Pimentel-Bitwarden
e0ccd7f578
chore(global-settings): [PM-24717] New Global Settings For New Device Verification - Updated secrets in the example secrets.json (#6387) 2025-09-29 13:06:52 -04:00
renovate[bot]
a36340e9ad
[deps]: Update prettier to v3.6.2 (#6212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 09:17:13 -04:00
Mick Letofsky
3dd4ee7a07
Create new Action for Claude code review of Vault Team code (#6379)
Create new action for Claude Code Review of Vault Team Code. Worked to align what we have here with the initial `mcp-server` repo's code review action.
2025-09-29 08:31:56 +02:00
Jordan Aasen
3a6b9564d5
[PM-26004] - fix DeleteByOrganizationIdAsync_ExcludesDefaultCollectionCiphers test (#6389)
* fix test

* fix test
2025-09-26 14:30:34 -07:00
Jordan Aasen
b9e8b11311
update collections admin proc and repo (#6374) 2025-09-26 10:05:56 -07:00
renovate[bot]
80e7f4d85c
[deps] Billing: Update BenchmarkDotNet to 0.15.3 (#6041)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
2025-09-26 15:05:23 +00:00
renovate[bot]
9e0b767c98
[deps] Billing: Update CsvHelper to 33.1.0 (#6042)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
2025-09-26 09:56:28 -05:00
Mick Letofsky
ef54bc814d
Fix a couple broken links found during self-onboarding (#6386)
* Fix a couple broken links found during self-onboarding
2025-09-26 15:46:57 +02:00
Kyle Spearrin
0df22ff581
null coalesce collections to an empty array (#6381) 2025-09-25 19:05:48 -04:00
Dave
6466c00acd
fix(user-decryption-options) [PM-23174]: ManageAccountRecovery Permission Forces Master Password Set (#6230)
* fix(user-decryption-options): ManageAccountRecovery Permission Forces MP Set - Update tests, add OrganizationUser fixture customization for Permissions

* fix(user-decryption-options): ManageAccountRecovery Permission Forces MP Set - Update hasManageResetPasswordPermission evaluation.

* PM-23174 - Add TODO for endpoint per sync discussion with Dave

* fix(user-decryption-options): ManageAccountRecovery Permission Forces MP Set - Clean up comments.

* fix(user-decryption-options): ManageAccountRecovery Permission Forces MP Set - Remove an outdated comment.

* fix(user-decryption-options): ManageAccountRecovery Permission Forces MP Set - Elaborate on comments around Organization User invite-time evaluation.

* fix(user-decryption-options): Use currentContext for Provider relationships, update comments, and feature flag the change.

* fix(user-decryption-options): Update test suite and provide additional comments for future flag removal.

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>
2025-09-25 13:37:36 -04:00
Matt Bishop
222436589c
Enhance Claude instructions (#6378)
* Enhance Claude instructions

* Further simplify language
2025-09-25 12:37:29 -04:00
Mick Letofsky
179684a9e6
Begin pilot program for Claude code reviews with initial system prompt (#6371)
* Rough draft of a markdown file to give context to Claude.
2025-09-25 07:46:59 +02:00
Thomas Rittson
b83f95f78c
[PM-25097] Remove DeleteClaimedUserAccountRefactor flag (#6364)
* Remove feature flag
* Remove old code
2025-09-25 10:14:02 +10:00
Nik Gilmore
f0953ed6b0
[PM-26126] Add includeMemberItems query param to GET /organization-details (#6376) 2025-09-24 15:25:40 -07:00
Patrick-Pimentel-Bitwarden
4b10c16419
fix(global-settings): [PM-26092] Token Refresh Doc Enhancement (#6367)
* fix(global-settings): [PM-26092] Token Refresh Doc Enhancement - Enhanced documentation and wording for token refresh.
2025-09-24 18:23:15 -04:00
Todd Martin
68f7e8c15c
chore(feature-flag) Added feature flag for pm-22110-disable-alternate-login-methods 2025-09-24 19:30:43 +00:00
Nick Krantz
6edab46d97
[PM-24357] Do not purge ciphers in the default collection (#6320)
* do not purge ciphers in the default collection

* Update `DeleteByOrganizationId` procedure to be more performant based on PR review feedback

* update EF integration for purge to match new SQL implementation

* update Cipher_DeleteByOrganizationId based on PR feedback from dbops team
2025-09-24 12:52:04 -05:00
Ike
6e4f05ebd3
fix: change policies to static strings and update auth owned endpoints (#6296) 2025-09-24 08:42:56 -04:00
Bernd Schoolmann
ff092a031e
[PM-23229] Add extra validation to kdf changes + authentication data + unlock data (#6121)
* Added MasterPasswordUnlock to UserDecryptionOptions as part of identity response

* Implement support for authentication data and unlock data in kdf change

* Extract to kdf command and add tests

* Fix namespace

* Delete empty file

* Fix build

* Clean up tests

* Fix tests

* Add comments

* Cleanup

* Cleanup

* Cleanup

* Clean-up and fix build

* Address feedback; force new parameters on KDF change request

* Clean-up and add tests

* Re-add logger

* Update logger to interface

* Clean up, remove Kdf Request Model

* Remove kdf request model tests

* Fix types in test

* Address feedback to rename request model and re-add tests

* Fix namespace

* Move comments

* Rename InnerKdfRequestModel to KdfRequestModel

---------

Co-authored-by: Maciej Zieniuk <mzieniuk@bitwarden.com>
2025-09-23 16:10:46 -04:00
Todd Martin
744f11733d
Revert "Bumped version to 2025.9.3" (#6369)
This reverts commit 0b4b605524.
2025-09-23 13:07:42 -04:00
Github Actions
0b4b605524 Bumped version to 2025.9.3 2025-09-23 15:52:56 +00:00
Ike
3b54fea309
[PM-22696] send enumeration protection (#6352)
* feat: add static enumeration helper class
* test: add enumeration helper class unit tests

* feat: implement NeverAuthenticateValidator
* test: unit and integration tests SendNeverAuthenticateValidator

* test: use static class for common integration test setup for Send Access unit and integration tests
* test: update tests to use static helper
2025-09-23 06:38:22 -04:00
Daniel Riera
c6f5d5e36e
[PM-25986] Add server side enum type for AutotypeDefaultSetting policy (#6356)
* PM-25986 Add server side enum type for AutotypeDefaultSetting policy

* Update PolicyType.cs

remove space
2025-09-22 15:39:15 -04:00
Stephon Brown
ed5e4271df
[PM-25123] Remove VerifyBankAsync Code (#6355)
* refactor: remove VerifyBankAsync from interface and implementation

* refactor: remove controller endpoint
2025-09-22 13:51:36 -04:00
Addison Beck
8c238ce08d
fix: adjust permissions of repo management workflow (#6130)
- Specify permissions needed for the repo_management job
- Add required permissions (actions: read, contents: write, id-token: write, pull-requests: write) to the move_edd_db_scripts job
2025-09-22 13:46:35 -04:00
Thomas Avery
0b6b93048b
[PM-25373] Add feature flag (#6358) 2025-09-22 11:05:16 -05:00
Vijay Oommen
fe7e96eb6a
PM-25870 Activity tab feature flag (#6360) 2025-09-22 10:36:19 -05:00
Github Actions
dc2828291b Bumped version to 2025.9.2 2025-09-22 15:02:24 +00:00
SmithThe4th
d384c0cfe6
[PM-7730] Deprecate type-specific cipher properties in favor of opaque Data string (#6354)
* Marked structured fields as obsolete and add Data field to the request model

* Fixed lint issues

* Deprecated properties

* Changed to 1mb
2025-09-19 16:17:32 -04:00
Alex Morask
3ac3b8c8d9
Remove FF (#6302) 2025-09-19 12:27:12 -05:00
Alex Morask
14b307c15b
[PM-25205] Don't respond with a tax ID warning for US customers (#6310)
* Don't respond with a Tax ID warning for US customers

* Only show provider tax ID warning for non-US based providers
2025-09-19 10:26:22 -05:00
rr-bw
d2c2ae5b4d
fix(invalid-auth-request-approvals): Auth/[PM-3387] Better Error Handling for Invalid Auth Request Approval (#6264)
If a user approves an invalid auth request, on the Requesting Device they currently they get stuck on the `LoginViaAuthRequestComponent` with a spinning wheel.

This PR makes it so that when an Approving Device attempts to approve an invalid auth request, the Approving Device receives an error toast and the `UpdateAuthRequestAsync()` operation is blocked.
2025-09-18 17:30:05 -07:00
Vincent Salucci
7e4dac9837
chore: remove FF, references, and restructure code, refs PM-24373 (#6353) 2025-09-18 15:08:47 -05:00
Jared McCannon
9d3d35e0bf
removing status from org name. (#6350) 2025-09-18 11:22:22 -05:00
renovate[bot]
c93c346473
[deps] Platform: Update LaunchDarkly.ServerSdk to 8.10.1 (#6210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 13:50:24 +02:00
Daniel García
866a572d26
Enable custom IDs for bindings (#6340)
* Enable custom IDs for bindings

* Remove description
2025-09-18 13:41:19 +02:00
Thomas Rittson
780400fcf9
[PM-25138] Reduce db locking when creating default collections (#6308)
* Use single method for default collection creation

* Use GenerateComb to create sequential guids

* Pre-sort data for SqlBulkCopy

* Add SqlBulkCopy options per dbops recommendations
2025-09-18 14:50:36 +10:00
Ben Brooks
e46365ac20
feat(policies): add URI Match Defaults organizational policy (#6294)
* feat(policies): add URI Match Defaults organizational policy

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

* feat(policies): remove unecessary model and org feature

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>

---------

Signed-off-by: Ben Brooks <bbrooks@bitwarden.com>
2025-09-17 14:36:00 -07:00
Patrick-Pimentel-Bitwarden
26e574e8d7
Auth/pm 25453/support n users auth request (#6347)
* fix(pending-auth-request-view): [PM-25453] Bugfix Auth Requests Multiple Users Same Device - fixed view to allow for multiple users for each device when partitioning for the auth request view.
2025-09-17 17:14:00 -04:00
Rui Tomé
d83395aeb0
[PM-25372] Filter out DefaultUserCollections from CiphersController.GetAssignedOrganizationCiphers (#6274)
Co-authored-by: Jimmy Vo <huynhmaivo82@gmail.com>
2025-09-17 12:43:27 -04:00
Dave
57f891f391
feat(sso): [auth/pm-17719] Make SSO identifier errors consistent (#6345)
* feat(sso-account-controller): Make SSO identifiers consistent - align all return messages from prevalidate.

* feat(shared-resources): Make SSO identifiers consistent - remove unused string resources, add new consistent error message.

* feat(sso-account-controller): Make SSO identifiers consistent - Add logging.
2025-09-16 15:01:23 -04:00
Jordan Aasen
6e309c6e04
fix cipher org details with collections task (#6342) 2025-09-16 09:00:32 -07:00
SmithThe4th
da48603c18
Revert "Remove archive date from create request (#6341)" (#6346)
This reverts commit 2dd89b488d.
2025-09-16 11:16:00 -04:00
Vincent Salucci
4b3ac2ea61
chore: resolve merge conflict to delete dc user removal feature flag, refs PM-24596 (#6344) 2025-09-15 16:00:07 -05:00
Álison Fernandes
6c512f1bc2
Add mobile CXP feature flags (#6343) 2025-09-15 20:57:13 +01:00
SmithThe4th
2dd89b488d
Remove archive date from create request (#6341) 2025-09-15 14:11:25 -04:00
MtnBurrit0
b9f58946a3
Fix load test scheduled default path (#6339) 2025-09-15 12:23:29 -04:00
Daniel García
a173e7e2da
[PM-25182] Improve Swagger OperationIDs for Vault (#6240)
* Improve Swagger OperationIDs for Vault

* Some renames
2025-09-15 18:05:06 +02:00
Conner Turnbull
0ee307a027
[PM-25533][BEEEP] Refactor license date calculations into extensions (#6295)
* Refactor license date calculations into extensions

* `dotnet format`

* Handling case when expirationWithoutGracePeriod is null

* Removed extra UseAdminSponsoredFamilies claim
2025-09-15 10:56:33 -04:00
Daniel García
981ff51d57
Update swashbuckle to fix API docs (#6319) 2025-09-15 16:49:46 +02:00
Vijay Oommen
b249c4e4d7
[PM-23761] Auto-reply to tickets in Freskdesk with help from Onyx AI (#6315) 2025-09-15 08:22:39 -05:00
Oscar Hinton
b4a0555a72
Change swagger docs to refer to main (#6337) 2025-09-15 15:02:40 +02:00
Jordan Aasen
6ade09312f
[PM-21044] - optimize security task ReadByUserIdStatus (#5779)
* optimize security task ReadByUserIdStatus

* fix AccessibleCiphers query

* fix error

* add migrator file

* fix migration

* update sproc

* mirror sprocs

* revert change to sproc

* add indexes. update filename. add GO statement

* move index declarations to appropriate files

* add missing GO statement

* select view. add existance checks for index

* update indexes

* revert changes

* rename file

* update security task

* update sproc

* update script file

* bump migration date

* add filtered index. update statistics, update description with perf metics

* rename file

* reordering

* remove update statistics

* remove update statistics

* add missing index

* fix sproc

* update timestamp

* improve sproc with de-dupe and views

* fix syntax error

* add missing inner join

* sync up index

* fix indentation

* update file timestamp

* remove unnecessary indexes. update sql to match guidelines.

* add comment for status

* add comment for status
2025-09-12 11:49:40 -07:00
Jimmy Vo
854abb0993
[PM-23845] Update cache service to handle concurrency (#6170) 2025-09-12 13:44:19 -04:00
Patrick-Pimentel-Bitwarden
4e64d35f89
[PM-19151] [PM-19161] Innovation/archive/server (#5672)
* Added the ArchivedDate to cipher entity and response model
* Created migration scripts for sqlserver and ef core migration to add the ArchivedDate column

---------

Co-authored-by: gbubemismith <gsmithwalter@gmail.com>
Co-authored-by: SmithThe4th <gsmith@bitwarden.com>
Co-authored-by: Shane <smelton@bitwarden.com>
Co-authored-by: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com>
Co-authored-by: jng <jng@bitwarden.com>
2025-09-12 13:24:30 -04:00
Jared McCannon
18aed0bd79
Added conditional subject and button text to invite email. (#6304)
* Added conditional subject and button text to invite email.

* Added feature flag.
2025-09-12 10:41:53 -05:00
Ike
7eb5035d94
[PM-22740] Update current context to jive with Send Access Tokens (#6307)
* feat: modify current context to not include user information
* fix: circular dependency for feature check in current context. Successfully tested client isn't affected with feature flag off.
* test: whole bunch of tests for current context
2025-09-12 10:53:11 -04:00
MtnBurrit0
ba57ca5f67
BRE-1075: Migrate k6 loadtests to Datadog (#6293)
* Remove external loadImpact option that is being replaced by DataDog

* Add load test workflow

Keep otel encrypted, but skip verification

Go back to what was working from Billing-Relay

Tune test configuration based on last test output.

Tune config loadtest

Tune tests a bit more by removing preAllocatedVUs

Revert "Tune tests a bit more by removing preAllocatedVUs"

This reverts commit ab1d170e7a3a6b4296f2c44ed741656a75979c80.

Revert "Tune config loadtest"

This reverts commit 5bbd551421658e8eb0e2651fb1e005c7f1d52c99.

Tune config.js by reducing the amount of pAV

Revert "Tune config.js by reducing the amount of pAV"

This reverts commit 1e238d335c27ebf46992541ca3733178e165b3aa.

Drop MaxVUs

* Update .github/workflows/load-test.yml

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>

* Fix newline at end of load-test.yml file

* Fix github PR accepted code suggestion

---------

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
2025-09-11 15:04:37 -06:00
cyprain-okeke
c2cf290054
[PM-21938] Fix: Invoice Payment Issues After Payment Method Updates (#6306)
* Resolve the unpaid issue after valid payment method is added

* Removed the draft status

* Remove draft from the logger msg
2025-09-11 16:04:05 +01:00
Vincent Salucci
aab50ef5c4
[PM-24595] [PM-24596] Remove feature flag usage/definition for deleting users with no mp on import (#6313)
* chore: remove dc prevent non-mp users from being deleted feature flag, refs PM-24596

* chore: format, refs PM-24596
2025-09-11 08:25:57 -05:00
Nick Krantz
51c9958ff1
update global settings for icons service so URIs are available internally (#6303) 2025-09-11 08:21:04 -05:00
Thomas Rittson
2c860df34b
[PM-15621] Refactor delete claimed user command (#6221)
- create vNext command
- restructure command to simplify logic
- move validation to a separate class
- implement result types using OneOf library and demo
  their use here
2025-09-11 13:58:32 +10:00
Thomas Rittson
bd1745a50d
[PM-24192] Add OrganizationContext in API project (#6291) 2025-09-11 07:37:45 +10:00
Derek Nance
04cb7820a6
[PM-25088] Fix collision with PM-24964 (#6312)
`ISetupIntentCache.Remove` (used in #6262) was renamed to
`RemoveSetupIntentForSubscriber` with 3dd5acc in #6263.
2025-09-10 16:34:10 -05:00
Brant DeBow
e57569ad57
Alter Integration Template processing to remove keys when encountering null values (#6309) 2025-09-10 14:17:45 -04:00
Kyle Denney
a458db319e
[PM-25088] - refactor premium purchase endpoint (#6262)
* [PM-25088] add feature flag for new premium subscription flow

* [PM-25088] refactor premium endpoint

* forgot the punctuation change in the test

* [PM-25088] - pr feedback

* [PM-25088] - pr feedback round two
2025-09-10 10:08:22 -05:00
Jimmy Vo
d43b00dad9
[PM-24279] Add vnext policy endpoint (#6253) 2025-09-10 10:13:04 -04:00
renovate[bot]
52045b89fa
[deps]: Lock file maintenance (#5876)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com>
2025-09-10 08:55:21 -04:00
Daniel García
5f76804f47
Improve Swagger OperationIDs for AC (#6236) 2025-09-10 01:00:07 +02:00
renovate[bot]
48a262ff1e
[deps] Auth: Update sass to v1.91.0 (#6206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 17:15:47 -04:00
renovate[bot]
3283e6c1a6
[deps] Auth: Update webpack to v5.101.3 (#6208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com>
2025-09-09 17:14:44 -04:00
renovate[bot]
4f4b35e4bf
[deps] Auth: Update DuoUniversal to 1.3.1 (#5862)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 16:55:31 -04:00
Kyle Spearrin
c4f22a4508
[PM-25381] Add env variables for controlling refresh token lifetimes (#6276)
* add env variables for controlling refresh token lifetimes

* fix whitespace

* added setting for adjusting refresh token expiration policy

* format
2025-09-09 15:30:58 -04:00
Jared McCannon
2986a883eb
[PM-25126] Add Bulk Policy Details (#6256)
* Added new bulk get for policy details

* Query improvements to avoid unnecessary look-ups.
2025-09-09 13:43:14 -05:00
Alex Morask
3dd5accb56
[PM-24964] Stripe-hosted bank account verification (#6263)
* Implement bank account hosted URL verification with webhook handling notification

* Fix tests

* Run dotnet format

* Remove unused VerifyBankAccount operation

* Stephon's feedback

* Removing unused test

* TEMP: Add logging for deployment check

* Run dotnet format

* fix test

* Revert "fix test"

This reverts commit b8743ab3b5.

* Revert "Run dotnet format"

This reverts commit 5c861b0b72.

* Revert "TEMP: Add logging for deployment check"

This reverts commit 0a88acd6a1.

* Resolve GetPaymentMethodQuery order of operations
2025-09-09 12:22:42 -05:00
cyprain-okeke
ac718351a8
Fix UseKeyConnector is set to true when upgrading to Enterprise (#6281) 2025-09-09 20:33:22 +05:30
Brant DeBow
d0778a8a7b
Clean up OrgnizationIntegrationRequestModel validations and nullable declarations (#6301)
* Clean up OrgnizationIntegrationRequestModel validations; remove unnecessary nullable enables

* Fix weird line break
2025-09-09 10:02:10 -04:00
Graham Walker
226f274a72
Organization report tables, repos, services, and endpoints (#6158)
* PM-23754 initial commit

* pm-23754 fixing controller tests

* pm-23754 adding commands and queries

* pm-23754 adding endpoints, command/queries, repositories, and sql migrations

* pm-23754 add new sql scripts

* PM-23754 adding sql scripts

* pm-23754

* PM-23754 fixing migration script

* PM-23754 fixing migration script again

* PM-23754 fixing migration script validation

* PM-23754 fixing db validation script issue

* PM-23754 fixing endpoint and db validation

* PM-23754 fixing unit tests

* PM-23754 fixing implementation based on comments and tests

* PM-23754 updating logging statements

* PM-23754 making changes based on PR comments.

* updating migration scripts

* removing old migration files

* update code based testing for whole data object for OrganizationReport and add a stored procedure.

* updating services, unit tests, repository tests

* fixing unit tests

* fixing migration script

* fixing migration script again

* fixing migration script

* another fix

* fixing sql file, updating controller to account for different orgIds in the url and body.

* updating error message in controllers without a body

* making a change to the command

* Refactor ReportsController by removing organization reports

The IDropOrganizationReportCommand is no longer needed

* will code based on PR comments.

* fixing unit test

* fixing migration script based on last changes.

* adding another check in endpoint and adding unit tests

* fixing route parameter.

* PM-23754 updating data fields to return just the column

* PM-23754 fixing repository method signatures

* PM-23754 making change to orgId parameter through out code to align with api naming

---------

Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com>
2025-09-08 15:06:13 -05:00
Github Actions
cb0d5a5ba6 Bumped version to 2025.9.1 2025-09-08 19:45:06 +00:00
Brant DeBow
747e212b1b
Add Datadog integration (#6289)
* Event integration updates and cleanups

* Add Datadog integration

* Update README to include link to Datadog PR

* Move doc update into the Datadog PR; Fix empty message on ArgumentException

* Adjust exception message

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>

* Removed unnecessary nullable enable; Moved Docs link to PR into this PR

* Remove unnecessary nullable enable calls

---------

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
2025-09-08 12:39:59 -04:00
Jordan Aasen
39ad020418
[PM-22219] - [Vault] [Server] Exclude items in default collections from Admin Console (#5992)
* add GetAllOrganizationCiphersExcludingDefaultUserCollections

* add sproc

* update sproc and feature flag name

* add sproc. update tests

* rename sproc

* rename sproc

* use single sproc

* revert change

* remove unused code. update sproc

* remove joins from proc

* update migration filename

* fix syntax

* fix indentation

* remove unnecessary feature flag and go statements. clean up code

* update sproc, view, and index

* update sproc

* update index

* update timestamp

* update filename. update sproc to match EF filter

* match only enabled organizations. make index creation idempotent

* update file timestamp

* update timestamp

* use square brackets

* add square brackets

* formatting fixes

* rename view

* remove index
2025-09-08 08:23:08 -07:00
Brant DeBow
0fbbb6a984
Event integration updates and cleanups (#6288)
* Event integration updates and cleanups

* Fix empty message on ArgumentException

* Adjust exception message

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>

---------

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
2025-09-08 10:54:43 -04:00
Todd Martin
7e50a46d3b
chore(feature-flag): Remove persist-popup-view feature flag 2025-09-08 10:12:43 -04:00
1690 changed files with 275983 additions and 24046 deletions

77
.claude/CLAUDE.md Normal file
View file

@ -0,0 +1,77 @@
# Bitwarden Server - Claude Code Configuration
## Project Context Files
**Read these files before reviewing to ensure that you fully understand the project and contributing guidelines**
1. @README.md
2. @CONTRIBUTING.md
3. @.github/PULL_REQUEST_TEMPLATE.md
## Critical Rules
- **NEVER** use code regions: If complexity suggests regions, refactor for better readability
- **NEVER** compromise zero-knowledge principles: User vault data must remain encrypted and inaccessible to Bitwarden
- **NEVER** log or expose sensitive data: No PII, passwords, keys, or vault data in logs or error messages
- **ALWAYS** use secure communication channels: Enforce confidentiality, integrity, and authenticity
- **ALWAYS** encrypt sensitive data: All vault data must be encrypted at rest, in transit, and in use
- **ALWAYS** prioritize cryptographic integrity and data protection
- **ALWAYS** add unit tests (with mocking) for any new feature development
## Project Structure
- **Source Code**: `/src/` - Services and core infrastructure
- **Tests**: `/test/` - Test logic aligning with the source structure, albeit with a `.Test` suffix
- **Utilities**: `/util/` - Migration tools, seeders, and setup scripts
- **Dev Tools**: `/dev/` - Local development helpers
- **Configuration**: `appsettings.{Environment}.json`, `/dev/secrets.json` for local development
## Security Requirements
- **Compliance**: SOC 2 Type II, SOC 3, HIPAA, ISO 27001, GDPR, CCPA
- **Principles**: Zero-knowledge, end-to-end encryption, secure defaults
- **Validation**: Input sanitization, parameterized queries, rate limiting
- **Logging**: Structured logs, no PII/sensitive data in logs
## Common Commands
- **Build**: `dotnet build`
- **Test**: `dotnet test`
- **Run locally**: `dotnet run --project src/Api`
- **Database update**: `pwsh dev/migrate.ps1`
- **Generate OpenAPI**: `pwsh dev/generate_openapi_files.ps1`
## Development Workflow
- Security impact assessed
- xUnit tests added / updated
- Performance impact considered
- Error handling implemented
- Breaking changes documented
- CI passes: build, test, lint
- Feature flags considered for new features
- CODEOWNERS file respected
### Key Architectural Decisions
- Use .NET nullable reference types (ADR 0024)
- TryAdd dependency injection pattern (ADR 0026)
- Authorization patterns (ADR 0022)
- OpenTelemetry for observability (ADR 0020)
- Log to standard output (ADR 0021)
## References
- [Server architecture](https://contributing.bitwarden.com/architecture/server/)
- [Architectural Decision Records (ADRs)](https://contributing.bitwarden.com/architecture/adr/)
- [Contributing guidelines](https://contributing.bitwarden.com/contributing/)
- [Setup guide](https://contributing.bitwarden.com/getting-started/server/guide/)
- [Code style](https://contributing.bitwarden.com/contributing/code-style/)
- [Bitwarden security whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/)
- [Bitwarden security definitions](https://contributing.bitwarden.com/architecture/security/definitions)

View file

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
"version": "9.0.2",
"version": "9.0.4",
"commands": ["swagger"]
},
"dotnet-ef": {

View file

@ -123,3 +123,12 @@ csharp_style_namespace_declarations = file_scoped:warning
# Switch expression
dotnet_diagnostic.CS8509.severity = error # missing switch case for named enum value
dotnet_diagnostic.CS8524.severity = none # missing switch case for unnamed enum value
# CA2253: Named placeholders should nto be numeric values
dotnet_diagnostic.CA2253.severity = suggestion
# CA2254: Template should be a static expression
dotnet_diagnostic.CA2254.severity = warning
# CA1727: Use PascalCase for named placeholders
dotnet_diagnostic.CA1727.severity = suggestion

27
.github/CODEOWNERS vendored
View file

@ -4,11 +4,12 @@
#
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
## Docker files have shared ownership ##
**/Dockerfile
**/*.Dockerfile
**/.dockerignore
**/entrypoint.sh
## Docker-related files
**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
**/*.Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
## BRE team owns these workflows ##
.github/workflows/publish.yml @bitwarden/dept-bre
@ -35,6 +36,7 @@ util/Setup/** @bitwarden/dept-bre @bitwarden/team-platform-dev
# UIF
src/Core/MailTemplates/Mjml @bitwarden/team-ui-foundation # Teams are expected to own sub-directories of this project
src/Core/MailTemplates/Mjml/.mjmlconfig # This change allows teams to add components within their own subdirectories without requiring a code review from UIF.
# Auth team
**/Auth @bitwarden/team-auth-dev
@ -51,6 +53,11 @@ src/Core/IdentityServer @bitwarden/team-auth-dev
# Dirt (Data Insights & Reporting) team
**/Dirt @bitwarden/team-data-insights-and-reporting-dev
src/Events @bitwarden/team-data-insights-and-reporting-dev
src/EventsProcessor @bitwarden/team-data-insights-and-reporting-dev
test/Events.IntegrationTest @bitwarden/team-data-insights-and-reporting-dev
test/Events.Test @bitwarden/team-data-insights-and-reporting-dev
test/EventsProcessor.Test @bitwarden/team-data-insights-and-reporting-dev
# Vault team
**/Vault @bitwarden/team-vault-dev
@ -61,8 +68,6 @@ src/Core/IdentityServer @bitwarden/team-auth-dev
bitwarden_license/src/Scim @bitwarden/team-admin-console-dev
bitwarden_license/src/test/Scim.IntegrationTest @bitwarden/team-admin-console-dev
bitwarden_license/src/test/Scim.ScimTest @bitwarden/team-admin-console-dev
src/Events @bitwarden/team-admin-console-dev
src/EventsProcessor @bitwarden/team-admin-console-dev
# Billing team
**/*billing* @bitwarden/team-billing-dev
@ -95,6 +100,14 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
# The PushType enum is expected to be editted by anyone without need for Platform review
src/Core/Platform/Push/PushType.cs
# SDK
util/RustSdk @bitwarden/team-sdk-sme
# Multiple owners - DO NOT REMOVE (BRE)
**/packages.lock.json
Directory.Build.props
# Claude related files
.claude/ @bitwarden/team-ai-sme
.github/workflows/respond.yml @bitwarden/team-ai-sme
.github/workflows/review-code.yml @bitwarden/team-ai-sme

View file

@ -1,6 +1,6 @@
name: Bitwarden Unified Deployment Bug Report
name: Bitwarden lite Deployment Bug Report
description: File a bug report
labels: [bug, bw-unified-deploy]
labels: [bug, bw-lite-deploy]
body:
- type: markdown
attributes:
@ -70,15 +70,6 @@ body:
mariadb:10
# Postgres Example
postgres:14
- type: textarea
id: epic-label
attributes:
label: Issue-Link
description: Link to our pinned issue, tracking all Bitwarden Unified
value: |
https://github.com/bitwarden/server/issues/2480
validations:
required: true
- type: checkboxes
id: issue-tracking-info
attributes:

114
.github/renovate.json5 vendored
View file

@ -2,6 +2,7 @@
$schema: "https://docs.renovatebot.com/renovate-schema.json",
extends: ["github>bitwarden/renovate-config"], // Extends our default configuration for pinned dependencies
enabledManagers: [
"cargo",
"dockerfile",
"docker-compose",
"github-actions",
@ -9,32 +10,7 @@
"nuget",
],
packageRules: [
{
groupName: "dockerfile minor",
matchManagers: ["dockerfile"],
matchUpdateTypes: ["minor"],
},
{
groupName: "docker-compose minor",
matchManagers: ["docker-compose"],
matchUpdateTypes: ["minor"],
},
{
groupName: "github-action minor",
matchManagers: ["github-actions"],
matchUpdateTypes: ["minor"],
addLabels: ["hold"],
},
{
// For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates.
// This overrides the default that ignores patch updates for nuget dependencies.
matchPackageNames: [
"/^Microsoft\\.Extensions\\./",
"/^Microsoft\\.AspNetCore\\./",
],
matchUpdateTypes: ["patch"],
dependencyDashboardApproval: false,
},
// ==================== Team Ownership Rules ====================
{
matchManagers: ["dockerfile", "docker-compose"],
commitMessagePrefix: "[deps] BRE:",
@ -53,11 +29,11 @@
},
{
matchPackageNames: [
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
"DuoUniversal",
"Fido2.AspNet",
"Duende.IdentityServer",
"Microsoft.AspNetCore.Authentication.JwtBearer",
"Microsoft.Extensions.Caching.Cosmos",
"Microsoft.Extensions.Identity.Stores",
"Otp.NET",
"Sustainsys.Saml2.AspNetCore2",
@ -80,11 +56,7 @@
"Microsoft.AspNetCore.Mvc.Testing",
"Newtonsoft.Json",
"NSubstitute",
"Sentry.Serilog",
"Serilog.AspNetCore",
"Serilog.Extensions.Logging",
"Serilog.Extensions.Logging.File",
"Serilog.Sinks.SyslogMessages",
"Stripe.net",
"Swashbuckle.AspNetCore",
"Swashbuckle.AspNetCore.SwaggerGen",
@ -95,11 +67,6 @@
commitMessagePrefix: "[deps] Billing:",
reviewers: ["team:team-billing-dev"],
},
{
matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"],
groupName: "EntityFrameworkCore",
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
},
{
matchPackageNames: [
"Dapper",
@ -131,6 +98,7 @@
"AspNetCoreRateLimit",
"AspNetCoreRateLimit.Redis",
"Azure.Data.Tables",
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
"Azure.Messaging.EventGrid",
"Azure.Messaging.ServiceBus",
"Azure.Storage.Blobs",
@ -146,7 +114,6 @@
"Microsoft.Extensions.DependencyInjection",
"Microsoft.Extensions.Logging",
"Microsoft.Extensions.Logging.Console",
"Microsoft.Extensions.Caching.Cosmos",
"Microsoft.Extensions.Caching.SqlServer",
"Microsoft.Extensions.Caching.StackExchangeRedis",
"Quartz",
@ -155,6 +122,12 @@
commitMessagePrefix: "[deps] Platform:",
reviewers: ["team:team-platform-dev"],
},
{
matchUpdateTypes: ["lockFileMaintenance"],
description: "Platform owns lock file maintenance",
commitMessagePrefix: "[deps] Platform:",
reviewers: ["team:team-platform-dev"],
},
{
matchPackageNames: [
"AutoMapper.Extensions.Microsoft.DependencyInjection",
@ -184,6 +157,73 @@
commitMessagePrefix: "[deps] Vault:",
reviewers: ["team:team-vault-dev"],
},
// ==================== Grouping Rules ====================
// These come after any specific team assignment rules to ensure
// that grouping is not overridden by subsequent rule definitions.
{
groupName: "cargo minor",
matchManagers: ["cargo"],
matchUpdateTypes: ["minor"],
},
{
groupName: "dockerfile minor",
matchManagers: ["dockerfile"],
matchUpdateTypes: ["minor"],
},
{
groupName: "docker-compose minor",
matchManagers: ["docker-compose"],
matchUpdateTypes: ["minor"],
},
{
groupName: "github-action minor",
matchManagers: ["github-actions"],
matchUpdateTypes: ["minor"],
addLabels: ["hold"],
},
{
matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"],
groupName: "EntityFrameworkCore",
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
},
{
matchPackageNames: ["https://github.com/bitwarden/sdk-internal.git"],
groupName: "sdk-internal",
dependencyDashboardApproval: true
},
// ==================== Dashboard Rules ====================
{
// For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates.
// This overrides the default that ignores patch updates for nuget dependencies.
matchPackageNames: [
"/^Microsoft\\.Extensions\\./",
"/^Microsoft\\.AspNetCore\\./",
],
matchUpdateTypes: ["patch"],
dependencyDashboardApproval: false,
},
{
// For the Platform-owned dependencies below, we have decided we will only be creating PRs
// for major updates, and sending minor (as well as patch, inherited from base config) to the dashboard.
// This rule comes AFTER grouping rules so that groups are respected while still
// sending minor/patch updates to the dependency dashboard for approval.
matchPackageNames: [
"AspNetCoreRateLimit",
"AspNetCoreRateLimit.Redis",
"Azure.Data.Tables",
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
"Azure.Messaging.EventGrid",
"Azure.Messaging.ServiceBus",
"Azure.Storage.Blobs",
"Azure.Storage.Queues",
"LaunchDarkly.ServerSdk",
"Quartz",
],
matchUpdateTypes: ["minor"],
dependencyDashboardApproval: true,
},
],
ignoreDeps: ["dotnet-sdk"],
}

View file

@ -38,21 +38,22 @@ jobs:
uses: bitwarden/gh-actions/azure-logout@main
- name: Check out branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
persist-credentials: false
- name: Get script prefix
id: prefix
run: echo "prefix=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
run: echo "prefix=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
- name: Check if any files in DB transition or finalization directories
id: check-script-existence
run: |
if [ -f util/Migrator/DbScripts_transition/* -o -f util/Migrator/DbScripts_finalization/* ]; then
echo "copy_edd_scripts=true" >> $GITHUB_OUTPUT
echo "copy_edd_scripts=true" >> "$GITHUB_OUTPUT"
else
echo "copy_edd_scripts=false" >> $GITHUB_OUTPUT
echo "copy_edd_scripts=false" >> "$GITHUB_OUTPUT"
fi
move-scripts:
@ -67,20 +68,21 @@ jobs:
if: ${{ needs.setup.outputs.copy_edd_scripts == 'true' }}
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: true
- name: Generate branch name
id: branch_name
env:
PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }}
run: echo "branch_name=move_edd_db_scripts_$PREFIX" >> $GITHUB_OUTPUT
run: echo "branch_name=move_edd_db_scripts_$PREFIX" >> "$GITHUB_OUTPUT"
- name: "Create branch"
env:
BRANCH: ${{ steps.branch_name.outputs.branch_name }}
run: git switch -c $BRANCH
run: git switch -c "$BRANCH"
- name: Move scripts and finalization database schema
id: move-files
@ -120,7 +122,7 @@ jobs:
# sync finalization schema back to dbo, maintaining structure
rsync -r "$src_dir/" "$dest_dir/"
rm -rf $src_dir/*
rm -rf "${src_dir}"/*
# Replace any finalization references due to the move
find ./src/Sql/dbo -name "*.sql" -type f -exec sed -i \
@ -131,7 +133,7 @@ jobs:
moved_files="$moved_files \n $file"
done
echo "moved_files=$moved_files" >> $GITHUB_OUTPUT
echo "moved_files=$moved_files" >> "$GITHUB_OUTPUT"
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@ -162,18 +164,20 @@ jobs:
- name: Commit and push changes
id: commit
env:
BRANCH_NAME: ${{ steps.branch_name.outputs.branch_name }}
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
if [ -n "$(git status --porcelain)" ]; then
git add .
git commit -m "Move EDD database scripts" -a
git push -u origin ${{ steps.branch_name.outputs.branch_name }}
echo "pr_needed=true" >> $GITHUB_OUTPUT
git push -u origin "${BRANCH_NAME}"
echo "pr_needed=true" >> "$GITHUB_OUTPUT"
else
echo "No changes to commit!";
echo "pr_needed=false" >> $GITHUB_OUTPUT
echo "### :mega: No changes to commit! PR was ommited." >> $GITHUB_STEP_SUMMARY
echo "pr_needed=false" >> "$GITHUB_OUTPUT"
echo "### :mega: No changes to commit! PR was ommited." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Create PR for ${{ steps.branch_name.outputs.branch_name }}
@ -195,7 +199,7 @@ jobs:
Files moved:
$(echo -e "$MOVED_FILES")
")
echo "pr_url=${PR_URL}" >> $GITHUB_OUTPUT
echo "pr_url=${PR_URL}" >> "$GITHUB_OUTPUT"
- name: Notify Slack about creation of PR
if: ${{ steps.commit.outputs.pr_needed == 'true' }}

View file

@ -22,29 +22,30 @@ env:
jobs:
lint:
name: Lint
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
- name: Verify format
run: dotnet format --verify-no-changes
build-artifacts:
name: Build Docker images
runs-on: ubuntu-24.04
needs:
- lint
runs-on: ubuntu-22.04
needs: lint
outputs:
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
permissions:
security-events: write
id-token: write
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
@ -97,30 +98,31 @@ jobs:
id: check-secrets
run: |
has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }}
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
echo "has_secrets=$has_secrets" >> "$GITHUB_OUTPUT"
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Check branch to publish
env:
PUBLISH_BRANCHES: "main,rc,hotfix-rc"
id: publish-branch-check
run: |
IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES
IFS="," read -a publish_branches <<< "$PUBLISH_BRANCHES"
if [[ " ${publish_branches[*]} " =~ " ${GITHUB_REF:11} " ]]; then
echo "is_publish_branch=true" >> $GITHUB_ENV
echo "is_publish_branch=true" >> "$GITHUB_ENV"
else
echo "is_publish_branch=false" >> $GITHUB_ENV
echo "is_publish_branch=false" >> "$GITHUB_ENV"
fi
- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: "npm"
cache-dependency-path: "**/package-lock.json"
@ -157,7 +159,7 @@ jobs:
ls -atlh ../../../
- name: Upload project artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: ${{ matrix.dotnet }}
with:
name: ${{ matrix.project_name }}.zip
@ -166,10 +168,10 @@ jobs:
########## Set up Docker ##########
- name: Set up QEMU emulators
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
########## ACRs ##########
- name: Log in to Azure
@ -182,13 +184,6 @@ jobs:
- name: Log in to ACR - production subscription
run: az acr login -n bitwardenprod
- name: Retrieve GitHub PAT secrets
id: retrieve-secret-pat
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
########## Generate image tag and build Docker image ##########
- name: Generate Docker image tag
id: tag
@ -209,8 +204,8 @@ jobs:
IMAGE_TAG=dev
fi
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "### :mega: Docker Image Tag: $IMAGE_TAG" >> $GITHUB_STEP_SUMMARY
echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
echo "### :mega: Docker Image Tag: $IMAGE_TAG" >> "$GITHUB_STEP_SUMMARY"
- name: Set up project name
id: setup
@ -218,7 +213,7 @@ jobs:
PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}')
echo "Matrix name: ${{ matrix.project_name }}"
echo "PROJECT_NAME: $PROJECT_NAME"
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
echo "project_name=$PROJECT_NAME" >> "$GITHUB_OUTPUT"
- name: Generate image tags(s)
id: image-tags
@ -228,12 +223,12 @@ jobs:
SHA: ${{ github.sha }}
run: |
TAGS="${_AZ_REGISTRY}/${PROJECT_NAME}:${IMAGE_TAG}"
echo "primary_tag=$TAGS" >> $GITHUB_OUTPUT
echo "primary_tag=$TAGS" >> "$GITHUB_OUTPUT"
if [[ "${IMAGE_TAG}" == "dev" ]]; then
SHORT_SHA=$(git rev-parse --short ${SHA})
SHORT_SHA=$(git rev-parse --short "${SHA}")
TAGS=$TAGS",${_AZ_REGISTRY}/${PROJECT_NAME}:dev-${SHORT_SHA}"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
- name: Build Docker image
id: build-artifacts
@ -247,12 +242,10 @@ jobs:
linux/arm64
push: true
tags: ${{ steps.image-tags.outputs.tags }}
secrets: |
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
- name: Install Cosign
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
- name: Sign image with Cosign
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
@ -260,23 +253,24 @@ jobs:
DIGEST: ${{ steps.build-artifacts.outputs.digest }}
TAGS: ${{ steps.image-tags.outputs.tags }}
run: |
IFS="," read -a tags <<< "${TAGS}"
images=""
for tag in "${tags[@]}"; do
images+="${tag}@${DIGEST} "
IFS=',' read -r -a tags_array <<< "${TAGS}"
images=()
for tag in "${tags_array[@]}"; do
images+=("${tag}@${DIGEST}")
done
cosign sign --yes ${images}
cosign sign --yes ${images[@]}
echo "images=${images[*]}" >> "$GITHUB_OUTPUT"
- name: Scan Docker image
id: container-scan
uses: anchore/scan-action@2c901ab7378897c01b8efaa2d0c9bf519cc64b9e # v6.2.0
uses: anchore/scan-action@3c9a191a0fbab285ca6b8530b5de5a642cba332f # v7.2.2
with:
image: ${{ steps.image-tags.outputs.primary_tag }}
fail-build: false
output-format: sarif
- name: Upload Grype results to GitHub
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with:
sarif_file: ${{ steps.container-scan.outputs.sarif }}
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
@ -287,19 +281,20 @@ jobs:
upload:
name: Upload
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs: build-artifacts
permissions:
id-token: write
actions: read
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@ -309,7 +304,7 @@ jobs:
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Log in to ACR - production subscription
run: az acr login -n $_AZ_REGISTRY --only-show-errors
run: az acr login -n "$_AZ_REGISTRY" --only-show-errors
- name: Make Docker stubs
if: |
@ -332,26 +327,26 @@ jobs:
STUB_OUTPUT=$(pwd)/docker-stub
# Run setup
docker run -i --rm --name setup -v $STUB_OUTPUT/US:/bitwarden $SETUP_IMAGE \
docker run -i --rm --name setup -v "$STUB_OUTPUT/US:/bitwarden" "$SETUP_IMAGE" \
/app/Setup -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region US
docker run -i --rm --name setup -v $STUB_OUTPUT/EU:/bitwarden $SETUP_IMAGE \
docker run -i --rm --name setup -v "$STUB_OUTPUT/EU:/bitwarden" "$SETUP_IMAGE" \
/app/Setup -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region EU
sudo chown -R $(whoami):$(whoami) $STUB_OUTPUT
sudo chown -R "$(whoami):$(whoami)" "$STUB_OUTPUT"
# Remove extra directories and files
rm -rf $STUB_OUTPUT/US/letsencrypt
rm -rf $STUB_OUTPUT/EU/letsencrypt
rm $STUB_OUTPUT/US/env/uid.env $STUB_OUTPUT/US/config.yml
rm $STUB_OUTPUT/EU/env/uid.env $STUB_OUTPUT/EU/config.yml
rm -rf "$STUB_OUTPUT/US/letsencrypt"
rm -rf "$STUB_OUTPUT/EU/letsencrypt"
rm "$STUB_OUTPUT/US/env/uid.env" "$STUB_OUTPUT/US/config.yml"
rm "$STUB_OUTPUT/EU/env/uid.env" "$STUB_OUTPUT/EU/config.yml"
# Create uid environment files
touch $STUB_OUTPUT/US/env/uid.env
touch $STUB_OUTPUT/EU/env/uid.env
touch "$STUB_OUTPUT/US/env/uid.env"
touch "$STUB_OUTPUT/EU/env/uid.env"
# Zip up the Docker stub files
cd docker-stub/US; zip -r ../../docker-stub-US.zip *; cd ../..
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../..
cd docker-stub/US; zip -r ../../docker-stub-US.zip ./*; cd ../..
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip ./*; cd ../..
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
@ -360,7 +355,7 @@ jobs:
if: |
github.event_name != 'pull_request'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: docker-stub-US.zip
path: docker-stub-US.zip
@ -370,7 +365,7 @@ jobs:
if: |
github.event_name != 'pull_request'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: docker-stub-EU.zip
path: docker-stub-EU.zip
@ -382,21 +377,21 @@ jobs:
pwsh ./generate_openapi_files.ps1
- name: Upload Public API Swagger artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: swagger.json
path: api.public.json
if-no-files-found: error
- name: Upload Internal API Swagger artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: internal.json
path: api.json
if-no-files-found: error
- name: Upload Identity Swagger artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: identity.json
path: identity.json
@ -404,9 +399,8 @@ jobs:
build-mssqlmigratorutility:
name: Build MSSQL migrator utility
runs-on: ubuntu-24.04
needs:
- lint
runs-on: ubuntu-22.04
needs: lint
defaults:
run:
shell: bash
@ -420,12 +414,13 @@ jobs:
- win-x64
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
- name: Print environment
run: |
@ -441,7 +436,7 @@ jobs:
- name: Upload project artifact for Windows
if: ${{ contains(matrix.target, 'win') == true }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: MsSqlMigratorUtility-${{ matrix.target }}
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe
@ -449,20 +444,19 @@ jobs:
- name: Upload project artifact
if: ${{ contains(matrix.target, 'win') == false }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: MsSqlMigratorUtility-${{ matrix.target }}
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility
if-no-files-found: error
self-host-build:
name: Trigger self-host build
bitwarden-lite-build:
name: Trigger Bitwarden lite build
if: |
github.event_name != 'pull_request'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
runs-on: ubuntu-24.04
needs:
- build-artifacts
runs-on: ubuntu-22.04
needs: build-artifacts
permissions:
id-token: write
steps:
@ -473,25 +467,34 @@ jobs:
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve GitHub PAT secrets
id: retrieve-secret-pat
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
keyvault: gh-org-bitwarden
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Trigger self-host build
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
- name: Generate GH App token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
id: app-token
with:
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
owner: ${{ github.repository_owner }}
repositories: self-host
- name: Trigger Bitwarden lite build
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'bitwarden',
repo: 'self-host',
workflow_id: 'build-unified.yml',
workflow_id: 'build-bitwarden-lite.yml',
ref: 'main',
inputs: {
server_branch: process.env.GITHUB_REF
@ -499,11 +502,10 @@ jobs:
});
trigger-k8s-deploy:
name: Trigger k8s deploy
name: Trigger K8s deploy
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-22.04
needs:
- build-artifacts
needs: build-artifacts
permissions:
id-token: write
steps:
@ -514,20 +516,29 @@ jobs:
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve GitHub PAT secrets
id: retrieve-secret-pat
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
keyvault: gh-org-bitwarden
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Trigger k8s deploy
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
- name: Generate GH App token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
id: app-token
with:
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
owner: ${{ github.repository_owner }}
repositories: devops
- name: Trigger K8s deploy
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'bitwarden',
@ -542,8 +553,7 @@ jobs:
setup-ephemeral-environment:
name: Setup Ephemeral Environment
needs:
- build-artifacts
needs: build-artifacts
if: |
needs.build-artifacts.outputs.has_secrets == 'true'
&& github.event_name == 'pull_request'
@ -566,7 +576,7 @@ jobs:
- build-artifacts
- upload
- build-mssqlmigratorutility
- self-host-build
- bitwarden-lite-build
- trigger-k8s-deploy
permissions:
id-token: write

View file

@ -1,71 +0,0 @@
name: Container registry cleanup
on:
pull_request:
types: [closed]
env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
jobs:
build-docker:
name: Remove branch-specific Docker images
runs-on: ubuntu-22.04
permissions:
id-token: write
steps:
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Log in to Azure ACR
run: az acr login -n $_AZ_REGISTRY --only-show-errors
########## Remove Docker images ##########
- name: Remove the Docker image from ACR
env:
REF: ${{ github.event.pull_request.head.ref }}
SERVICES: |
services:
- Admin
- Api
- Attachments
- Events
- EventsProcessor
- Icons
- Identity
- K8S-Proxy
- MsSql
- Nginx
- Notifications
- Server
- Setup
- Sso
run: |
for SERVICE in $(echo "${{ env.SERVICES }}" | yq e ".services[]" - )
do
SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}')
IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name
echo "[*] Checking if remote exists: $_AZ_REGISTRY/$SERVICE_NAME:$IMAGE_TAG"
TAG_EXISTS=$(
az acr repository show-tags --name $_AZ_REGISTRY --repository $SERVICE_NAME \
| jq --arg $TAG "$IMAGE_TAG" -e '. | any(. == "$TAG")'
)
if [[ "$TAG_EXISTS" == "true" ]]; then
echo "[*] Tag exists. Removing tag"
az acr repository delete --name $_AZ_REGISTRY --image $SERVICE_NAME:$IMAGE_TAG --yes
else
echo "[*] Tag does not exist. No action needed"
fi
done
- name: Log out of Docker
run: docker logout
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main

View file

@ -31,10 +31,12 @@ jobs:
uses: bitwarden/gh-actions/azure-logout@main
- name: Checkout main
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: main
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
persist-credentials: false
fetch-depth: 0
- name: Check if a RC branch exists
id: branch-check
@ -43,11 +45,11 @@ jobs:
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
echo "hotfix-rc branch exists." | tee -a "$GITHUB_STEP_SUMMARY"
echo "name=hotfix-rc" >> "$GITHUB_OUTPUT"
elif [[ "${rc_branch_check}" -gt 0 ]]; then
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=rc" >> $GITHUB_OUTPUT
echo "rc branch exists." | tee -a "$GITHUB_STEP_SUMMARY"
echo "name=rc" >> "$GITHUB_OUTPUT"
fi
- name: Delete RC branch
@ -55,6 +57,6 @@ jobs:
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
run: |
if ! [[ -z "$BRANCH_NAME" ]]; then
git push --quiet origin --delete $BRANCH_NAME
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
git push --quiet origin --delete "$BRANCH_NAME"
echo "Deleted $BRANCH_NAME branch." | tee -a "$GITHUB_STEP_SUMMARY"
fi

View file

@ -19,9 +19,9 @@ jobs:
id: check-secret-access
run: |
if [ "${{ secrets.AZURE_CLIENT_ID }}" != '' ]; then
echo "available=true" >> $GITHUB_OUTPUT;
echo "available=true" >> "$GITHUB_OUTPUT";
else
echo "available=false" >> $GITHUB_OUTPUT;
echo "available=false" >> "$GITHUB_OUTPUT";
fi
refs:
@ -36,7 +36,9 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
@ -57,7 +59,7 @@ jobs:
- name: Collect
id: collect
uses: launchdarkly/find-code-references@e3e9da201b87ada54eb4c550c14fb783385c5c8a # v2.13.0
uses: launchdarkly/find-code-references@89a7d362d1d4b3725fe0fe0ccd0dc69e3bdcba58 # v2.14.0
with:
accessToken: ${{ steps.get-kv-secrets.outputs.LD-ACCESS-TOKEN }}
projKey: default
@ -65,14 +67,14 @@ jobs:
- name: Add label
if: steps.collect.outputs.any-changed == 'true'
run: gh pr edit $PR_NUMBER --add-label feature-flag
run: gh pr edit "$PR_NUMBER" --add-label feature-flag
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
- name: Remove label
if: steps.collect.outputs.any-changed == 'false'
run: gh pr edit $PR_NUMBER --remove-label feature-flag
run: gh pr edit "$PR_NUMBER" --remove-label feature-flag
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}

View file

@ -17,5 +17,5 @@ jobs:
- name: Check for label
run: |
echo "PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged"
echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> $GITHUB_STEP_SUMMARY
echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> "$GITHUB_STEP_SUMMARY"
exit 1

View file

@ -16,5 +16,5 @@ jobs:
with:
project: server
pull_request_number: ${{ github.event.number }}
sync_environment: true
sync_environment: false
secrets: inherit

View file

@ -1,13 +1,114 @@
name: Test Stub
name: Load test
on:
schedule:
- cron: "0 0 * * 1" # Run every Monday at 00:00
workflow_dispatch:
inputs:
test-id:
type: string
description: "Identifier label for Datadog metrics"
default: "server-load-test"
k6-test-path:
type: string
description: "Path to load test files"
default: "perf/load/*.js"
k6-flags:
type: string
description: "Additional k6 flags"
api-env-url:
type: string
description: "URL of the API environment"
default: "https://api.qa.bitwarden.pw"
identity-env-url:
type: string
description: "URL of the Identity environment"
default: "https://identity.qa.bitwarden.pw"
permissions:
contents: read
id-token: write
env:
# Secret configuration
AZURE_KEY_VAULT_NAME: gh-server
AZURE_KEY_VAULT_SECRETS: DD-API-KEY, K6-CLIENT-ID, K6-AUTH-USER-EMAIL, K6-AUTH-USER-PASSWORD-HASH
# Specify defaults for scheduled runs
TEST_ID: ${{ inputs.test-id || 'server-load-test' }}
K6_TEST_PATH: ${{ inputs.k6-test-path || 'perf/load/*.js' }}
API_ENV_URL: ${{ inputs.api-env-url || 'https://api.qa.bitwarden.pw' }}
IDENTITY_ENV_URL: ${{ inputs.identity-env-url || 'https://identity.qa.bitwarden.pw' }}
jobs:
test:
permissions:
contents: read
name: Test
run-tests:
name: Run load tests
runs-on: ubuntu-24.04
steps:
- name: Test
run: exit 0
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: ${{ env.AZURE_KEY_VAULT_NAME }}
secrets: ${{ env.AZURE_KEY_VAULT_SECRETS }}
- name: Log out of Azure
uses: bitwarden/gh-actions/azure-logout@main
# Datadog agent for collecting OTEL metrics from k6
- name: Start Datadog agent
env:
DD_API_KEY: ${{ steps.get-kv-secrets.outputs.DD-API-KEY }}
run: |
docker run --detach \
--name datadog-agent \
-p 4317:4317 \
-p 5555:5555 \
-e DD_SITE=us3.datadoghq.com \
-e DD_API_KEY="${DD_API_KEY}" \
-e DD_DOGSTATSD_NON_LOCAL_TRAFFIC=1 \
-e DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317 \
-e DD_HEALTH_PORT=5555 \
-e HOST_PROC=/proc \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
--volume /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \
--health-cmd "curl -f http://localhost:5555/health || exit 1" \
--health-interval 10s \
--health-timeout 5s \
--health-retries 10 \
--health-start-period 30s \
--pid host \
datadog/agent:7-full@sha256:7ea933dec3b8baa8c19683b1c3f6f801dbf3291f748d9ed59234accdaac4e479
- name: Check out repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up k6
uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2 # v1.1.0
- name: Run k6 tests
uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d # v1.3.1
continue-on-error: false
env:
K6_OTEL_METRIC_PREFIX: k6_
K6_OTEL_GRPC_EXPORTER_INSECURE: true
# Load test specific environment variables
API_URL: ${{ env.API_ENV_URL }}
IDENTITY_URL: ${{ env.IDENTITY_ENV_URL }}
CLIENT_ID: ${{ steps.get-kv-secrets.outputs.K6-CLIENT-ID }}
AUTH_USER_EMAIL: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-EMAIL }}
AUTH_USER_PASSWORD_HASH: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-PASSWORD-HASH }}
with:
flags: >-
--tag test-id=${{ env.TEST_ID }}
-o experimental-opentelemetry
${{ inputs.k6-flags }}
path: ${{ env.K6_TEST_PATH }}

View file

@ -31,9 +31,10 @@ jobs:
label: "DB-migrations-changed"
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 2
persist-credentials: false
- name: Check for file changes
id: check-changes
@ -43,9 +44,9 @@ jobs:
for file in $MODIFIED_FILES
do
if [[ $file == *"${{ matrix.path }}"* ]]; then
echo "changes_detected=true" >> $GITHUB_OUTPUT
echo "changes_detected=true" >> "$GITHUB_OUTPUT"
break
else echo "changes_detected=false" >> $GITHUB_OUTPUT
else echo "changes_detected=false" >> "$GITHUB_OUTPUT"
fi
done

View file

@ -36,21 +36,23 @@ jobs:
steps:
- name: Version output
id: version-output
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
if [[ "${{ inputs.version }}" == "latest" || "${{ inputs.version }}" == "" ]]; then
if [[ "${INPUT_VERSION}" == "latest" || "${INPUT_VERSION}" == "" ]]; then
VERSION=$(curl "https://api.github.com/repos/bitwarden/server/releases" | jq -c '.[] | select(.tag_name) | .tag_name' | head -1 | grep -ohE '20[0-9]{2}\.([1-9]|1[0-2])\.[0-9]+')
echo "Latest Released Version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
else
echo "Release Version: ${{ inputs.version }}"
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
echo "Release Version: ${INPUT_VERSION}"
echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
fi
- name: Get branch name
id: branch
run: |
BRANCH_NAME=$(basename ${{ github.ref }})
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
BRANCH_NAME=$(basename "${GITHUB_REF}")
echo "branch-name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
- name: Create GitHub deployment
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
@ -89,7 +91,6 @@ jobs:
- project_name: Nginx
- project_name: Notifications
- project_name: Scim
- project_name: Server
- project_name: Setup
- project_name: Sso
steps:
@ -104,7 +105,10 @@ jobs:
echo "Github Release Option: $RELEASE_OPTION"
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Set up project name
id: setup
@ -112,7 +116,7 @@ jobs:
PROJECT_NAME=$(echo "${{ matrix.project_name }}" | awk '{print tolower($0)}')
echo "Matrix name: ${{ matrix.project_name }}"
echo "PROJECT_NAME: $PROJECT_NAME"
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
echo "project_name=$PROJECT_NAME" >> "$GITHUB_OUTPUT"
########## ACR PROD ##########
- name: Log in to Azure
@ -123,16 +127,16 @@ jobs:
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Log in to Azure ACR
run: az acr login -n $_AZ_REGISTRY --only-show-errors
run: az acr login -n "$_AZ_REGISTRY" --only-show-errors
- name: Pull latest project image
env:
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
run: |
if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then
docker pull $_AZ_REGISTRY/$PROJECT_NAME:latest
docker pull "$_AZ_REGISTRY/$PROJECT_NAME:latest"
else
docker pull $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME
docker pull "$_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME"
fi
- name: Tag version and latest
@ -140,10 +144,10 @@ jobs:
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
run: |
if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then
docker tag $_AZ_REGISTRY/$PROJECT_NAME:latest $_AZ_REGISTRY/$PROJECT_NAME:dryrun
docker tag "$_AZ_REGISTRY/$PROJECT_NAME:latest" "$_AZ_REGISTRY/$PROJECT_NAME:dryrun"
else
docker tag $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
docker tag $_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME $_AZ_REGISTRY/$PROJECT_NAME:latest
docker tag "$_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME" "$_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION"
docker tag "$_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME" "$_AZ_REGISTRY/$PROJECT_NAME:latest"
fi
- name: Push version and latest image
@ -151,10 +155,10 @@ jobs:
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
run: |
if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then
docker push $_AZ_REGISTRY/$PROJECT_NAME:dryrun
docker push "$_AZ_REGISTRY/$PROJECT_NAME:dryrun"
else
docker push $_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION
docker push $_AZ_REGISTRY/$PROJECT_NAME:latest
docker push "$_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION"
docker push "$_AZ_REGISTRY/$PROJECT_NAME:latest"
fi
- name: Log out of Docker

View file

@ -39,7 +39,10 @@ jobs:
fi
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Check release version
id: version
@ -52,8 +55,8 @@ jobs:
- name: Get branch name
id: branch
run: |
BRANCH_NAME=$(basename ${{ github.ref }})
echo "branch-name=$BRANCH_NAME" >> $GITHUB_OUTPUT
BRANCH_NAME=$(basename "${GITHUB_REF}")
echo "branch-name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
release:
name: Create GitHub release
@ -86,7 +89,7 @@ jobs:
- name: Create release
if: ${{ inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
with:
artifacts: "docker-stub-US.zip,
docker-stub-EU.zip,

View file

@ -30,6 +30,7 @@ jobs:
runs-on: ubuntu-24.04
outputs:
branch: ${{ steps.set-branch.outputs.branch }}
permissions: {}
steps:
- name: Set branch
id: set-branch
@ -44,7 +45,7 @@ jobs:
BRANCH="hotfix-rc"
fi
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
bump_version:
name: Bump Version
@ -82,17 +83,19 @@ jobs:
version: ${{ inputs.version_number_override }}
- name: Generate GH App token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
id: app-token
with:
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
permission-contents: write
- name: Check out branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: main
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
- name: Configure Git
run: |
@ -108,7 +111,7 @@ jobs:
id: current-version
run: |
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
- name: Verify input version
if: ${{ inputs.version_number_override != '' }}
@ -118,16 +121,15 @@ jobs:
run: |
# Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Specified override version is the same as the current version." >> $GITHUB_STEP_SUMMARY
echo "Specified override version is the same as the current version." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
# Check if version is newer.
printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V
if [ $? -eq 0 ]; then
if printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V; then
echo "Version is newer than the current version."
else
echo "Version is older than the current version." >> $GITHUB_STEP_SUMMARY
echo "Version is older than the current version." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
@ -158,15 +160,20 @@ jobs:
id: set-final-version-output
env:
VERSION: ${{ inputs.version_number_override }}
BUMP_VERSION_OVERRIDE_OUTCOME: ${{ steps.bump-version-override.outcome }}
BUMP_VERSION_AUTOMATIC_OUTCOME: ${{ steps.bump-version-automatic.outcome }}
CALCULATE_NEXT_VERSION: ${{ steps.calculate-next-version.outputs.version }}
run: |
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
echo "version=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
if [[ "${BUMP_VERSION_OVERRIDE_OUTCOME}" = "success" ]]; then
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
elif [[ "${BUMP_VERSION_AUTOMATIC_OUTCOME}" = "success" ]]; then
echo "version=${CALCULATE_NEXT_VERSION}" >> "$GITHUB_OUTPUT"
fi
- name: Commit files
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
env:
FINAL_VERSION: ${{ steps.set-final-version-output.outputs.version }}
run: git commit -m "Bumped version to $FINAL_VERSION" -a
- name: Push changes
run: git push
@ -200,24 +207,27 @@ jobs:
uses: bitwarden/gh-actions/azure-logout@main
- name: Generate GH App token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
id: app-token
with:
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
permission-contents: write
- name: Check out target ref
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ inputs.target_ref }}
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
fetch-depth: 0
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
env:
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
run: |
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
if [[ $(git ls-remote --heads origin "$BRANCH_NAME") ]]; then
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
@ -225,11 +235,11 @@ jobs:
env:
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
run: |
git switch --quiet --create $BRANCH_NAME
git push --quiet --set-upstream origin $BRANCH_NAME
git switch --quiet --create "$BRANCH_NAME"
git push --quiet --set-upstream origin "$BRANCH_NAME"
move_edd_db_scripts:
name: Move EDD database scripts
needs: cut_branch
permissions: {}
uses: ./.github/workflows/_move_edd_db_scripts.yml
secrets: inherit

28
.github/workflows/respond.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Respond
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
permissions: {}
jobs:
respond:
name: Respond
uses: bitwarden/gh-actions/.github/workflows/_respond.yml@main
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
actions: read
contents: write
id-token: write
issues: write
pull-requests: write

21
.github/workflows/review-code.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: Code Review
on:
pull_request:
types: [opened, synchronize, reopened]
permissions: {}
jobs:
review:
name: Review
uses: bitwarden/gh-actions/.github/workflows/_review-code.yml@main
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
actions: read
contents: read
id-token: write
pull-requests: write

View file

@ -15,7 +15,7 @@ jobs:
pull-requests: write
steps:
- name: Check
uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
stale-issue-label: "needs-reply"
stale-pr-label: "needs-changes"

View file

@ -44,10 +44,12 @@ jobs:
checks: write
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
- name: Restore tools
run: dotnet tool restore
@ -60,7 +62,7 @@ jobs:
docker compose --profile mssql --profile postgres --profile mysql up -d
shell: pwsh
- name: Add MariaDB for unified
- name: Add MariaDB for Bitwarden lite
# Use a different port than MySQL
run: |
docker run --detach --name mariadb --env MARIADB_ROOT_PASSWORD=mariadb-password -p 4306:3306 mariadb:10
@ -131,7 +133,7 @@ jobs:
# Default Sqlite
BW_TEST_DATABASES__3__TYPE: "Sqlite"
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
# Unified MariaDB
# Bitwarden lite MariaDB
BW_TEST_DATABASES__4__TYPE: "MySql"
BW_TEST_DATABASES__4__CONNECTIONSTRING: "server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true"
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
@ -139,31 +141,31 @@ jobs:
- name: Print MySQL Logs
if: failure()
run: 'docker logs $(docker ps --quiet --filter "name=mysql")'
run: 'docker logs "$(docker ps --quiet --filter "name=mysql")"'
- name: Print MariaDB Logs
if: failure()
run: 'docker logs $(docker ps --quiet --filter "name=mariadb")'
run: 'docker logs "$(docker ps --quiet --filter "name=mariadb")"'
- name: Print Postgres Logs
if: failure()
run: 'docker logs $(docker ps --quiet --filter "name=postgres")'
run: 'docker logs "$(docker ps --quiet --filter "name=postgres")"'
- name: Print MSSQL Logs
if: failure()
run: 'docker logs $(docker ps --quiet --filter "name=mssql")'
run: 'docker logs "$(docker ps --quiet --filter "name=mssql")"'
- name: Report test results
uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
with:
name: Test Results
path: "**/*-test-results.trx"
path: "./**/*-test-results.trx"
reporter: dotnet-trx
fail-on-error: true
- name: Upload to codecov.io
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
- name: Docker Compose down
if: always()
@ -176,10 +178,12 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
- name: Print environment
run: |
@ -193,7 +197,7 @@ jobs:
shell: pwsh
- name: Upload DACPAC
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: sql.dacpac
path: Sql.dacpac
@ -219,7 +223,7 @@ jobs:
shell: pwsh
- name: Report validation results
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: report.xml
path: |
@ -258,3 +262,26 @@ jobs:
working-directory: "dev"
run: docker compose down
shell: pwsh
validate-migration-naming:
name: Validate new migration naming and order
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Validate new migrations for pull request
if: github.event_name == 'pull_request'
run: |
git fetch origin main:main
pwsh dev/verify_migrations.ps1 -BaseRef main
shell: pwsh
- name: Validate new migrations for push
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
run: pwsh dev/verify_migrations.ps1 -BaseRef HEAD~1
shell: pwsh

View file

@ -27,10 +27,20 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
- name: Install rust
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable
with:
toolchain: stable
- name: Cache cargo registry
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
- name: Print environment
run: |
@ -49,7 +59,7 @@ jobs:
run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
- name: Report test results
uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
with:
name: Test Results
@ -58,4 +68,4 @@ jobs:
fail-on-error: true
- name: Upload to codecov.io
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2

7
.gitignore vendored
View file

@ -215,6 +215,9 @@ bitwarden_license/src/Sso/wwwroot/assets
**/**.swp
.mono
src/Core/MailTemplates/Mjml/out
src/Core/MailTemplates/Mjml/out-hbs
NativeMethods.g.cs
util/RustSdk/rust/target
src/Admin/Admin.zip
src/Api/Api.zip
@ -231,3 +234,7 @@ bitwarden_license/src/Sso/Sso.zip
/identity.json
/api.json
/api.public.json
.serena/
# Serena
.serena/

View file

@ -3,12 +3,10 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2025.9.0</Version>
<Version>2025.12.2</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<IsTestProject Condition="'$(IsTestProject)' == '' and ($(MSBuildProjectName.EndsWith('.Test')) or $(MSBuildProjectName.EndsWith('.IntegrationTest')))">true</IsTestProject>
<Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' == 'true'">annotations</Nullable>
<Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' != 'true'">enable</Nullable>
@ -18,7 +16,7 @@
<PropertyGroup>
<MicrosoftNetTestSdkVersion>17.8.0</MicrosoftNetTestSdkVersion>
<MicrosoftNetTestSdkVersion>18.0.1</MicrosoftNetTestSdkVersion>
<XUnitVersion>2.6.6</XUnitVersion>
@ -32,19 +30,4 @@
<AutoFixtureAutoNSubstituteVersion>4.18.1</AutoFixtureAutoNSubstituteVersion>
</PropertyGroup>
<Target Name="SetSourceRevisionId" BeforeTargets="CoreGenerateAssemblyInfo">
<Exec Command="git describe --long --always --dirty --exclude=* --abbrev=8" ConsoleToMSBuild="True" IgnoreExitCode="False">
<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput" />
</Exec>
</Target>
<Target Name="WriteRevision" AfterTargets="SetSourceRevisionId">
<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>GitHash</_Parameter1>
<_Parameter2>$(SourceRevisionId)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Target>
</Project>

View file

@ -58,6 +58,42 @@ Invoke-RestMethod -OutFile bitwarden.ps1 `
.\bitwarden.ps1 -start
```
## Production Container Images
<details>
<summary><b>View Current Production Image Hashes</b> (click to expand)</summary>
<br>
### US Production Cluster
| Service | Image Hash |
|---------|------------|
| **Admin** | ![admin](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.admin&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **API** | ![api](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.api&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **Billing** | ![billing](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.billing&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **Events** | ![events](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.events&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **EventsProcessor** | ![eventsprocessor](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.eventsprocessor&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **Identity** | ![identity](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.identity&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **Notifications** | ![notifications](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.notifications&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **SCIM** | ![scim](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.scim&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **SSO** | ![sso](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.sso&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
### EU Production Cluster
| Service | Image Hash |
|---------|------------|
| **Admin** | ![admin](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.admin&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **API** | ![api](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.api&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **Billing** | ![billing](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.billing&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **Events** | ![events](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.events&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **EventsProcessor** | ![eventsprocessor](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.eventsprocessor&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **Identity** | ![identity](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.identity&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **Notifications** | ![notifications](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.notifications&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **SCIM** | ![scim](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.scim&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
| **SSO** | ![sso](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.sso&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |
</details>
## We're Hiring!
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.

View file

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
# Visual Studio Version 17
VisualStudioVersion = 17.14.36705.20 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src - AGPL", "src - AGPL", "{DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}"
EndProject
@ -11,19 +11,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{DD5BD056-4
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{458155D3-BCBC-481D-B37A-40D2ED10F0A4}"
ProjectSection(SolutionItems) = preProject
.dockerignore = .dockerignore
.editorconfig = .editorconfig
.gitignore = .gitignore
CONTRIBUTING.md = CONTRIBUTING.md
Directory.Build.props = Directory.Build.props
global.json = global.json
.gitignore = .gitignore
README.md = README.md
.editorconfig = .editorconfig
TRADEMARK_GUIDELINES.md = TRADEMARK_GUIDELINES.md
SECURITY.md = SECURITY.md
LICENSE_FAQ.md = LICENSE_FAQ.md
LICENSE_BITWARDEN.txt = LICENSE_BITWARDEN.txt
LICENSE_AGPL.txt = LICENSE_AGPL.txt
LICENSE.txt = LICENSE.txt
CONTRIBUTING.md = CONTRIBUTING.md
.dockerignore = .dockerignore
LICENSE_AGPL.txt = LICENSE_AGPL.txt
LICENSE_BITWARDEN.txt = LICENSE_BITWARDEN.txt
LICENSE_FAQ.md = LICENSE_FAQ.md
README.md = README.md
SECURITY.md = SECURITY.md
TRADEMARK_GUIDELINES.md = TRADEMARK_GUIDELINES.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "src\Core\Core.csproj", "{3973D21B-A692-4B60-9B70-3631C057423A}"
@ -133,8 +133,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seede
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RustSdk", "util\RustSdk\RustSdk.csproj", "{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb.Test", "test\SharedWeb.Test\SharedWeb.Test.csproj", "{AD59537D-5259-4B7A-948F-0CF58E80B359}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSO.Test", "bitwarden_license\test\SSO.Test\SSO.Test.csproj", "{7D98784C-C253-43FB-9873-25B65C6250D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sso.IntegrationTest", "bitwarden_license\test\Sso.IntegrationTest\Sso.IntegrationTest.csproj", "{FFB09376-595B-6F93-36F0-70CAE90AFECB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -339,10 +345,22 @@ Global
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.Build.0 = Release|Any CPU
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.Build.0 = Release|Any CPU
{7D98784C-C253-43FB-9873-25B65C6250D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D98784C-C253-43FB-9873-25B65C6250D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D98784C-C253-43FB-9873-25B65C6250D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D98784C-C253-43FB-9873-25B65C6250D6}.Release|Any CPU.Build.0 = Release|Any CPU
{FFB09376-595B-6F93-36F0-70CAE90AFECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FFB09376-595B-6F93-36F0-70CAE90AFECB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FFB09376-595B-6F93-36F0-70CAE90AFECB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FFB09376-595B-6F93-36F0-70CAE90AFECB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -397,7 +415,10 @@ Global
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{AD59537D-5259-4B7A-948F-0CF58E80B359} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{7D98784C-C253-43FB-9873-25B65C6250D6} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
{FFB09376-595B-6F93-36F0-70CAE90AFECB} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}

View file

@ -113,7 +113,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
await _providerBillingService.CreateCustomerForClientOrganization(provider, organization);
}
var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
var customer = await _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{
Description = string.Empty,
Email = organization.BillingEmail,
@ -138,7 +138,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
var subscription = await _stripeAdapter.CreateSubscriptionAsync(subscriptionCreateOptions);
organization.GatewaySubscriptionId = subscription.Id;
organization.Status = OrganizationStatusType.Created;
@ -148,22 +148,29 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
}
else if (organization.IsStripeEnabled())
{
var subscription = await _stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId);
var subscription = await _stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId, new SubscriptionGetOptions
{
Expand = ["customer"]
});
if (subscription.Status is StripeConstants.SubscriptionStatus.Canceled or StripeConstants.SubscriptionStatus.IncompleteExpired)
{
return;
}
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
await _stripeAdapter.UpdateCustomerAsync(subscription.CustomerId, new CustomerUpdateOptions
{
Coupon = string.Empty,
Email = organization.BillingEmail
});
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, new SubscriptionUpdateOptions
if (subscription.Customer.Discount?.Coupon != null)
{
await _stripeAdapter.DeleteCustomerDiscountAsync(subscription.CustomerId);
}
await _stripeAdapter.UpdateSubscriptionAsync(organization.GatewaySubscriptionId, new SubscriptionUpdateOptions
{
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
DaysUntilDue = 30
DaysUntilDue = 30,
});
await _subscriberService.RemovePaymentSource(organization);

View file

@ -9,12 +9,16 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -35,8 +39,9 @@ public class ProviderService : IProviderService
{
private static readonly PlanType[] _resellerDisallowedOrganizationTypes = [
PlanType.Free,
PlanType.FamiliesAnnually,
PlanType.FamiliesAnnually2019
PlanType.FamiliesAnnually2025,
PlanType.FamiliesAnnually2019,
PlanType.FamiliesAnnually
];
private readonly IDataProtector _dataProtector;
@ -58,6 +63,7 @@ public class ProviderService : IProviderService
private readonly IProviderBillingService _providerBillingService;
private readonly IPricingClient _pricingClient;
private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
@ -67,7 +73,8 @@ public class ProviderService : IProviderService
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient,
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand)
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand,
IPolicyRequirementQuery policyRequirementQuery)
{
_providerRepository = providerRepository;
_providerUserRepository = providerUserRepository;
@ -88,9 +95,10 @@ public class ProviderService : IProviderService
_providerBillingService = providerBillingService;
_pricingClient = pricingClient;
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
_policyRequirementQuery = policyRequirementQuery;
}
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null)
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress)
{
var owner = await _userService.GetUserByIdAsync(ownerUserId);
if (owner == null)
@ -115,21 +123,19 @@ public class ProviderService : IProviderService
throw new BadRequestException("Invalid owner.");
}
if (taxInfo == null || string.IsNullOrEmpty(taxInfo.BillingAddressCountry) || string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode))
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
throw new BadRequestException("Both address and postal code are required to set up your provider.");
}
var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerUserId);
if (tokenizedPaymentSource is not
if (organizationAutoConfirmPolicyRequirement
.CannotCreateProvider())
{
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
Token: not null and not ""
})
{
throw new BadRequestException("A payment method is required to set up your provider.");
throw new BadRequestException(new UserCannotJoinProvider().Message);
}
}
var customer = await _providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
var customer = await _providerBillingService.SetupCustomer(provider, paymentMethod, billingAddress);
provider.GatewayCustomerId = customer.Id;
var subscription = await _providerBillingService.SetupSubscription(provider);
provider.GatewaySubscriptionId = subscription.Id;
@ -261,6 +267,18 @@ public class ProviderService : IProviderService
throw new BadRequestException("User email does not match invite.");
}
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
if (organizationAutoConfirmPolicyRequirement
.CannotJoinProvider())
{
throw new BadRequestException(new UserCannotJoinProvider().Message);
}
}
providerUser.Status = ProviderUserStatusType.Accepted;
providerUser.UserId = user.Id;
providerUser.Email = null;
@ -306,6 +324,19 @@ public class ProviderService : IProviderService
throw new BadRequestException("Invalid user.");
}
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
if (organizationAutoConfirmPolicyRequirement
.CannotJoinProvider())
{
result.Add(Tuple.Create(providerUser, new UserCannotJoinProvider().Message));
continue;
}
}
providerUser.Status = ProviderUserStatusType.Confirmed;
providerUser.Key = keys[providerUser.Id];
providerUser.Email = null;
@ -440,7 +471,7 @@ public class ProviderService : IProviderService
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
{
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
await _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{
Email = provider.BillingEmail
});
@ -500,7 +531,7 @@ public class ProviderService : IProviderService
private async Task<SubscriptionItem> GetSubscriptionItemAsync(string subscriptionId, string oldPlanId)
{
var subscriptionDetails = await _stripeAdapter.SubscriptionGetAsync(subscriptionId);
var subscriptionDetails = await _stripeAdapter.GetSubscriptionAsync(subscriptionId);
return subscriptionDetails.Items.Data.FirstOrDefault(item => item.Price.Id == oldPlanId);
}
@ -510,7 +541,7 @@ public class ProviderService : IProviderService
{
if (subscriptionItem.Price.Id != extractedPlanType)
{
await _stripeAdapter.SubscriptionUpdateAsync(subscriptionItem.Subscription,
await _stripeAdapter.UpdateSubscriptionAsync(subscriptionItem.Subscription,
new Stripe.SubscriptionUpdateOptions
{
Items = new List<Stripe.SubscriptionItemOptions>

View file

@ -4,12 +4,12 @@ using Bit.Core.Billing.Providers.Models;
using Bit.Core.Billing.Providers.Queries;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Services;
using Stripe;
using Stripe.Tax;
namespace Bit.Commercial.Core.Billing.Providers.Queries;
using static Bit.Core.Constants;
using static StripeConstants;
using SuspensionWarning = ProviderWarnings.SuspensionWarning;
using TaxIdWarning = ProviderWarnings.TaxIdWarning;
@ -61,6 +61,11 @@ public class GetProviderWarningsQuery(
Provider provider,
Customer customer)
{
if (customer.Address?.Country == CountryAbbreviations.UnitedStates)
{
return null;
}
if (!currentContext.ProviderProviderAdmin(provider.Id))
{
return null;
@ -70,12 +75,12 @@ public class GetProviderWarningsQuery(
// Get active and scheduled registrations
var registrations = (await Task.WhenAll(
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }),
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled })))
stripeAdapter.ListTaxRegistrationsAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }),
stripeAdapter.ListTaxRegistrationsAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled })))
.SelectMany(registrations => registrations.Data);
// Find the matching registration for the customer
var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address.Country);
var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address?.Country);
// If we're not registered in their country, we don't need a warning
if (registration == null)

View file

@ -101,7 +101,7 @@ public class BusinessUnitConverter(
providerUser.Status = ProviderUserStatusType.Confirmed;
// Stripe requires that we clear all the custom fields from the invoice settings if we want to replace them.
await stripeAdapter.CustomerUpdateAsync(subscription.CustomerId, new CustomerUpdateOptions
await stripeAdapter.UpdateCustomerAsync(subscription.CustomerId, new CustomerUpdateOptions
{
InvoiceSettings = new CustomerInvoiceSettingsOptions
{
@ -116,7 +116,7 @@ public class BusinessUnitConverter(
["convertedFrom"] = organization.Id.ToString()
};
var updateCustomer = stripeAdapter.CustomerUpdateAsync(subscription.CustomerId, new CustomerUpdateOptions
var updateCustomer = stripeAdapter.UpdateCustomerAsync(subscription.CustomerId, new CustomerUpdateOptions
{
InvoiceSettings = new CustomerInvoiceSettingsOptions
{
@ -148,7 +148,7 @@ public class BusinessUnitConverter(
// Replace the existing password manager price with the new business unit price.
var updateSubscription =
stripeAdapter.SubscriptionUpdateAsync(subscription.Id,
stripeAdapter.UpdateSubscriptionAsync(subscription.Id,
new SubscriptionUpdateOptions
{
Items = [

View file

@ -14,6 +14,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Models;
@ -21,10 +22,8 @@ using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Billing.Tax.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -38,6 +37,9 @@ using Subscription = Stripe.Subscription;
namespace Bit.Commercial.Core.Billing.Providers.Services;
using static Constants;
using static StripeConstants;
public class ProviderBillingService(
IBraintreeGateway braintreeGateway,
IEventService eventService,
@ -51,8 +53,7 @@ public class ProviderBillingService(
IProviderUserRepository providerUserRepository,
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService,
ITaxService taxService)
ISubscriberService subscriberService)
: IProviderBillingService
{
public async Task AddExistingOrganization(
@ -60,14 +61,11 @@ public class ProviderBillingService(
Organization organization,
string key)
{
await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
new SubscriptionUpdateOptions
{
CancelAtPeriodEnd = false
});
await stripeAdapter.UpdateSubscriptionAsync(organization.GatewaySubscriptionId,
new SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
var subscription =
await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId,
await stripeAdapter.CancelSubscriptionAsync(organization.GatewaySubscriptionId,
new SubscriptionCancelOptions
{
CancellationDetails = new SubscriptionCancellationDetailsOptions
@ -83,9 +81,9 @@ public class ProviderBillingService(
var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now;
if (!wasTrialing && subscription.LatestInvoice.Status == StripeConstants.InvoiceStatus.Draft)
if (!wasTrialing && subscription.LatestInvoice.Status == InvoiceStatus.Draft)
{
await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId,
await stripeAdapter.FinalizeInvoiceAsync(subscription.LatestInvoiceId,
new InvoiceFinalizeOptions { AutoAdvance = true });
}
@ -140,7 +138,7 @@ public class ProviderBillingService(
if (clientCustomer.Balance != 0)
{
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
await stripeAdapter.CreateCustomerBalanceTransactionAsync(provider.GatewayCustomerId,
new CustomerBalanceTransactionCreateOptions
{
Amount = clientCustomer.Balance,
@ -184,25 +182,18 @@ public class ProviderBillingService(
{
Items =
[
new SubscriptionItemOptions
{
Price = newPriceId,
Quantity = oldSubscriptionItem!.Quantity
},
new SubscriptionItemOptions
{
Id = oldSubscriptionItem.Id,
Deleted = true
}
new SubscriptionItemOptions { Price = newPriceId, Quantity = oldSubscriptionItem!.Quantity },
new SubscriptionItemOptions { Id = oldSubscriptionItem.Id, Deleted = true }
]
};
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, updateOptions);
await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId, updateOptions);
// Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId)
// 1. Retrieve PlanType and PlanName for ProviderPlan
// 2. Assign PlanType & PlanName to Organization
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerPlan.ProviderId);
var providerOrganizations =
await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerPlan.ProviderId);
var newPlan = await pricingClient.GetPlanOrThrow(newPlanType);
@ -213,6 +204,7 @@ public class ProviderBillingService(
{
throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
}
organization.PlanType = newPlanType;
organization.Plan = newPlan.Name;
await organizationRepository.ReplaceAsync(organization);
@ -228,15 +220,15 @@ public class ProviderBillingService(
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
{
logger.LogWarning("Client organization ({ID}) already has a populated {FieldName}", organization.Id, nameof(organization.GatewayCustomerId));
logger.LogWarning("Client organization ({ID}) already has a populated {FieldName}", organization.Id,
nameof(organization.GatewayCustomerId));
return;
}
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
{
Expand = ["tax", "tax_ids"]
});
var providerCustomer =
await subscriberService.GetCustomerOrThrow(provider,
new CustomerGetOptions { Expand = ["tax", "tax_ids"] });
var providerTaxId = providerCustomer.TaxIds.FirstOrDefault();
@ -269,26 +261,21 @@ public class ProviderBillingService(
}
]
},
Metadata = new Dictionary<string, string>
{
{ "region", globalSettings.BaseServiceUri.CloudRegion }
},
TaxIdData = providerTaxId == null ? null :
[
new CustomerTaxIdDataOptions
{
Type = providerTaxId.Type,
Value = providerTaxId.Value
}
]
Metadata = new Dictionary<string, string> { { "region", globalSettings.BaseServiceUri.CloudRegion } },
TaxIdData = providerTaxId == null
? null
:
[
new CustomerTaxIdDataOptions { Type = providerTaxId.Type, Value = providerTaxId.Value }
]
};
if (providerCustomer.Address is not { Country: Constants.CountryAbbreviations.UnitedStates })
if (providerCustomer.Address is not { Country: CountryAbbreviations.UnitedStates })
{
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
customerCreateOptions.TaxExempt = TaxExempt.Reverse;
}
var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
var customer = await stripeAdapter.CreateCustomerAsync(customerCreateOptions);
organization.GatewayCustomerId = customer.Id;
@ -347,9 +334,9 @@ public class ProviderBillingService(
.Where(pair => pair.subscription is
{
Status:
StripeConstants.SubscriptionStatus.Active or
StripeConstants.SubscriptionStatus.Trialing or
StripeConstants.SubscriptionStatus.PastDue
SubscriptionStatus.Active or
SubscriptionStatus.Trialing or
SubscriptionStatus.PastDue
}).ToList();
if (active.Count == 0)
@ -474,36 +461,25 @@ public class ProviderBillingService(
// Below the limit to above the limit
(currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) ||
// Above the limit to further above the limit
(currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > currentlyAssignedSeatTotal);
(currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum &&
newlyAssignedSeatTotal > currentlyAssignedSeatTotal);
}
public async Task<Customer> SetupCustomer(
Provider provider,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource)
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
ArgumentNullException.ThrowIfNull(tokenizedPaymentSource);
if (taxInfo is not
{
BillingAddressCountry: not null and not "",
BillingAddressPostalCode: not null and not ""
})
{
logger.LogError("Cannot create customer for provider ({ProviderID}) without both a country and postal code", provider.Id);
throw new BillingException();
}
var options = new CustomerCreateOptions
{
Address = new AddressOptions
{
Country = taxInfo.BillingAddressCountry,
PostalCode = taxInfo.BillingAddressPostalCode,
Line1 = taxInfo.BillingAddressLine1,
Line2 = taxInfo.BillingAddressLine2,
City = taxInfo.BillingAddressCity,
State = taxInfo.BillingAddressState
Country = billingAddress.Country,
PostalCode = billingAddress.PostalCode,
Line1 = billingAddress.Line1,
Line2 = billingAddress.Line2,
City = billingAddress.City,
State = billingAddress.State
},
Description = provider.DisplayBusinessName(),
Email = provider.BillingEmail,
@ -520,93 +496,61 @@ public class ProviderBillingService(
}
]
},
Metadata = new Dictionary<string, string>
{
{ "region", globalSettings.BaseServiceUri.CloudRegion }
}
Metadata = new Dictionary<string, string> { { "region", globalSettings.BaseServiceUri.CloudRegion } },
TaxExempt = billingAddress.Country != CountryAbbreviations.UnitedStates ? TaxExempt.Reverse : TaxExempt.None
};
if (taxInfo.BillingAddressCountry is not Constants.CountryAbbreviations.UnitedStates)
if (billingAddress.TaxId != null)
{
options.TaxExempt = StripeConstants.TaxExempt.Reverse;
}
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
{
var taxIdType = taxService.GetStripeTaxCode(
taxInfo.BillingAddressCountry,
taxInfo.TaxIdNumber);
if (taxIdType == null)
{
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
taxInfo.BillingAddressCountry,
taxInfo.TaxIdNumber);
throw new BadRequestException("billingTaxIdTypeInferenceError");
}
options.TaxIdData =
[
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
new CustomerTaxIdDataOptions { Type = billingAddress.TaxId.Code, Value = billingAddress.TaxId.Value }
];
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
if (billingAddress.TaxId.Code == TaxIdType.SpanishNIF)
{
options.TaxIdData.Add(new CustomerTaxIdDataOptions
{
Type = StripeConstants.TaxIdType.EUVAT,
Value = $"ES{taxInfo.TaxIdNumber}"
Type = TaxIdType.EUVAT,
Value = $"ES{billingAddress.TaxId.Value}"
});
}
}
if (!string.IsNullOrEmpty(provider.DiscountId))
{
options.Coupon = provider.DiscountId;
}
var braintreeCustomerId = "";
if (tokenizedPaymentSource is not
{
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
Token: not null and not ""
})
{
logger.LogError("Cannot create customer for provider ({ProviderID}) with invalid payment method", provider.Id);
throw new BillingException();
}
var (type, token) = tokenizedPaymentSource;
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (type)
switch (paymentMethod.Type)
{
case PaymentMethodType.BankAccount:
case TokenizablePaymentMethodType.BankAccount:
{
var setupIntent =
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token }))
(await stripeAdapter.ListSetupIntentsAsync(new SetupIntentListOptions
{
PaymentMethod = paymentMethod.Token
}))
.FirstOrDefault();
if (setupIntent == null)
{
logger.LogError("Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", provider.Id);
logger.LogError(
"Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account",
provider.Id);
throw new BillingException();
}
await setupIntentCache.Set(provider.Id, setupIntent.Id);
break;
}
case PaymentMethodType.Card:
case TokenizablePaymentMethodType.Card:
{
options.PaymentMethod = token;
options.InvoiceSettings.DefaultPaymentMethod = token;
options.PaymentMethod = paymentMethod.Token;
options.InvoiceSettings.DefaultPaymentMethod = paymentMethod.Token;
break;
}
case PaymentMethodType.PayPal:
case TokenizablePaymentMethodType.PayPal:
{
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, token);
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, paymentMethod.Token);
options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
break;
}
@ -614,10 +558,9 @@ public class ProviderBillingService(
try
{
return await stripeAdapter.CustomerCreateAsync(options);
return await stripeAdapter.CreateCustomerAsync(options);
}
catch (StripeException stripeException) when (stripeException.StripeError?.Code ==
StripeConstants.ErrorCodes.TaxIdInvalid)
catch (StripeException stripeException) when (stripeException.StripeError?.Code == ErrorCodes.TaxIdInvalid)
{
await Revert();
throw new BadRequestException(
@ -632,17 +575,17 @@ public class ProviderBillingService(
async Task Revert()
{
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (tokenizedPaymentSource.Type)
switch (paymentMethod.Type)
{
case PaymentMethodType.BankAccount:
case TokenizablePaymentMethodType.BankAccount:
{
var setupIntentId = await setupIntentCache.Get(provider.Id);
await stripeAdapter.SetupIntentCancel(setupIntentId,
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
await stripeAdapter.CancelSetupIntentAsync(setupIntentId,
new SetupIntentCancelOptions { CancellationReason = "abandoned" });
await setupIntentCache.Remove(provider.Id);
await setupIntentCache.RemoveSetupIntentForSubscriber(provider.Id);
break;
}
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
{
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
break;
@ -661,9 +604,10 @@ public class ProviderBillingService(
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
if (providerPlans == null || providerPlans.Count == 0)
if (providerPlans.Count == 0)
{
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured plans", provider.Id);
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured plans",
provider.Id);
throw new BillingException();
}
@ -676,7 +620,9 @@ public class ProviderBillingService(
if (!providerPlan.IsConfigured())
{
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", provider.Id, plan.Name);
logger.LogError(
"Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan",
provider.Id, plan.Name);
throw new BillingException();
}
@ -689,19 +635,17 @@ public class ProviderBillingService(
});
}
var setupIntentId = await setupIntentCache.Get(provider.Id);
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
var setupIntent = !string.IsNullOrEmpty(setupIntentId)
? await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
{
Expand = ["payment_method"]
})
? await stripeAdapter.GetSetupIntentAsync(setupIntentId,
new SetupIntentGetOptions { Expand = ["payment_method"] })
: null;
var usePaymentMethod =
!string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) ||
(customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true) ||
(setupIntent?.IsUnverifiedBankAccount() == true);
customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true ||
setupIntent?.IsUnverifiedBankAccount() == true;
int? trialPeriodDays = provider.Type switch
{
@ -712,30 +656,28 @@ public class ProviderBillingService(
var subscriptionCreateOptions = new SubscriptionCreateOptions
{
CollectionMethod = usePaymentMethod ?
StripeConstants.CollectionMethod.ChargeAutomatically : StripeConstants.CollectionMethod.SendInvoice,
CollectionMethod =
usePaymentMethod
? CollectionMethod.ChargeAutomatically
: CollectionMethod.SendInvoice,
Customer = customer.Id,
DaysUntilDue = usePaymentMethod ? null : 30,
Discounts = !string.IsNullOrEmpty(provider.DiscountId) ? [new SubscriptionDiscountOptions { Coupon = provider.DiscountId }] : null,
Items = subscriptionItemOptionsList,
Metadata = new Dictionary<string, string>
{
{ "providerId", provider.Id.ToString() }
},
Metadata = new Dictionary<string, string> { { "providerId", provider.Id.ToString() } },
OffSession = true,
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
TrialPeriodDays = trialPeriodDays
ProrationBehavior = ProrationBehavior.CreateProrations,
TrialPeriodDays = trialPeriodDays,
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
};
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
try
{
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
var subscription = await stripeAdapter.CreateSubscriptionAsync(subscriptionCreateOptions);
if (subscription is
{
Status: StripeConstants.SubscriptionStatus.Active or StripeConstants.SubscriptionStatus.Trialing
Status: SubscriptionStatus.Active or SubscriptionStatus.Trialing
})
{
return subscription;
@ -749,9 +691,11 @@ public class ProviderBillingService(
throw new BillingException();
}
catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.CustomerTaxLocationInvalid)
catch (StripeException stripeException) when (stripeException.StripeError?.Code ==
ErrorCodes.CustomerTaxLocationInvalid)
{
throw new BadRequestException("Your location wasn't recognized. Please ensure your country and postal code are valid.");
throw new BadRequestException(
"Your location wasn't recognized. Please ensure your country and postal code are valid.");
}
}
@ -764,8 +708,8 @@ public class ProviderBillingService(
subscriberService.UpdatePaymentSource(provider, tokenizedPaymentSource),
subscriberService.UpdateTaxInformation(provider, taxInformation));
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
new SubscriptionUpdateOptions { CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically });
await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
new SubscriptionUpdateOptions { CollectionMethod = CollectionMethod.ChargeAutomatically });
}
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
@ -847,11 +791,49 @@ public class ProviderBillingService(
if (subscriptionItemOptionsList.Count > 0)
{
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList });
}
}
public async Task UpdateProviderNameAndEmail(Provider provider)
{
if (string.IsNullOrWhiteSpace(provider.GatewayCustomerId))
{
logger.LogWarning(
"Provider ({ProviderId}) has no Stripe customer to update",
provider.Id);
return;
}
var newDisplayName = provider.DisplayName();
// Provider.DisplayName() can return null - handle gracefully
if (string.IsNullOrWhiteSpace(newDisplayName))
{
logger.LogWarning(
"Provider ({ProviderId}) has no name to update in Stripe",
provider.Id);
return;
}
await stripeAdapter.UpdateCustomerAsync(provider.GatewayCustomerId,
new CustomerUpdateOptions
{
Email = provider.BillingEmail,
Description = newDisplayName,
InvoiceSettings = new CustomerInvoiceSettingsOptions
{
CustomFields = [
new CustomerInvoiceSettingsCustomFieldOptions
{
Name = provider.SubscriberType(),
Value = newDisplayName
}]
},
});
}
private Func<int, Task> CurrySeatScalingUpdate(
Provider provider,
ProviderPlan providerPlan,
@ -863,15 +845,11 @@ public class ProviderBillingService(
var item = subscription.Items.First(item => item.Price.Id == priceId);
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, new SubscriptionUpdateOptions
await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId, new SubscriptionUpdateOptions
{
Items = [
new SubscriptionItemOptions
{
Id = item.Id,
Price = priceId,
Quantity = newlySubscribedSeats
}
Items =
[
new SubscriptionItemOptions { Id = item.Id, Price = priceId, Quantity = newlySubscribedSeats }
]
});
@ -894,7 +872,8 @@ public class ProviderBillingService(
var plan = await pricingClient.GetPlanOrThrow(planType);
return providerOrganizations
.Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed)
.Where(providerOrganization => providerOrganization.Plan == plan.Name &&
providerOrganization.Status == OrganizationStatusType.Managed)
.Sum(providerOrganization => providerOrganization.Seats ?? 0);
}

View file

@ -5,7 +5,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="CsvHelper" Version="33.1.0" />
</ItemGroup>
</Project>

View file

@ -1,10 +1,13 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
namespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
@ -13,15 +16,21 @@ public class CreateServiceAccountCommand : ICreateServiceAccountCommand
private readonly IAccessPolicyRepository _accessPolicyRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IEventService _eventService;
private readonly ICurrentContext _currentContext;
public CreateServiceAccountCommand(
IAccessPolicyRepository accessPolicyRepository,
IOrganizationUserRepository organizationUserRepository,
IServiceAccountRepository serviceAccountRepository)
IServiceAccountRepository serviceAccountRepository,
IEventService eventService,
ICurrentContext currentContext)
{
_accessPolicyRepository = accessPolicyRepository;
_organizationUserRepository = organizationUserRepository;
_serviceAccountRepository = serviceAccountRepository;
_eventService = eventService;
_currentContext = currentContext;
}
public async Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount, Guid userId)
@ -38,6 +47,7 @@ public class CreateServiceAccountCommand : ICreateServiceAccountCommand
Write = true,
};
await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { accessPolicy });
await _eventService.LogServiceAccountPeopleEventAsync(user.Id, accessPolicy, EventType.ServiceAccount_UserAdded, _currentContext.IdentityClientType);
return createdServiceAccount;
}
}

View file

@ -0,0 +1,94 @@
using AutoMapper;
using Bit.Core.SecretsManager.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;
public class SecretVersionRepository : Repository<Core.SecretsManager.Entities.SecretVersion, SecretVersion, Guid>, ISecretVersionRepository
{
public SecretVersionRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
: base(serviceScopeFactory, mapper, db => db.SecretVersion)
{ }
public override async Task<Core.SecretsManager.Entities.SecretVersion?> GetByIdAsync(Guid id)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var secretVersion = await dbContext.SecretVersion
.Where(sv => sv.Id == id)
.FirstOrDefaultAsync();
return Mapper.Map<Core.SecretsManager.Entities.SecretVersion>(secretVersion);
}
public async Task<IEnumerable<Core.SecretsManager.Entities.SecretVersion>> GetManyBySecretIdAsync(Guid secretId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var secretVersions = await dbContext.SecretVersion
.Where(sv => sv.SecretId == secretId)
.OrderByDescending(sv => sv.VersionDate)
.ToListAsync();
return Mapper.Map<List<Core.SecretsManager.Entities.SecretVersion>>(secretVersions);
}
public async Task<IEnumerable<Core.SecretsManager.Entities.SecretVersion>> GetManyByIdsAsync(IEnumerable<Guid> ids)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var versionIds = ids.ToList();
var secretVersions = await dbContext.SecretVersion
.Where(sv => versionIds.Contains(sv.Id))
.OrderByDescending(sv => sv.VersionDate)
.ToListAsync();
return Mapper.Map<List<Core.SecretsManager.Entities.SecretVersion>>(secretVersions);
}
public override async Task<Core.SecretsManager.Entities.SecretVersion> CreateAsync(Core.SecretsManager.Entities.SecretVersion secretVersion)
{
const int maxVersionsToKeep = 10;
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
// Get the IDs of the most recent (maxVersionsToKeep - 1) versions to keep
var versionsToKeepIds = await dbContext.SecretVersion
.Where(sv => sv.SecretId == secretVersion.SecretId)
.OrderByDescending(sv => sv.VersionDate)
.Take(maxVersionsToKeep - 1)
.Select(sv => sv.Id)
.ToListAsync();
// Delete all versions for this secret that are not in the "keep" list
if (versionsToKeepIds.Any())
{
await dbContext.SecretVersion
.Where(sv => sv.SecretId == secretVersion.SecretId && !versionsToKeepIds.Contains(sv.Id))
.ExecuteDeleteAsync();
}
secretVersion.SetNewId();
var entity = Mapper.Map<SecretVersion>(secretVersion);
await dbContext.AddAsync(entity);
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return secretVersion;
}
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var secretVersionIds = ids.ToList();
await dbContext.SecretVersion
.Where(sv => secretVersionIds.Contains(sv.Id))
.ExecuteDeleteAsync();
}
}

View file

@ -10,6 +10,7 @@ public static class SecretsManagerEfServiceCollectionExtensions
{
services.AddSingleton<IAccessPolicyRepository, AccessPolicyRepository>();
services.AddSingleton<ISecretRepository, SecretRepository>();
services.AddSingleton<ISecretVersionRepository, SecretVersionRepository>();
services.AddSingleton<IProjectRepository, ProjectRepository>();
services.AddSingleton<IServiceAccountRepository, ServiceAccountRepository>();
}

View file

@ -61,17 +61,15 @@ public class GroupsController : Controller
[HttpGet("")]
public async Task<IActionResult> Get(
Guid organizationId,
[FromQuery] string filter,
[FromQuery] int? count,
[FromQuery] int? startIndex)
[FromQuery] GetGroupsQueryParamModel model)
{
var groupsListQueryResult = await _getGroupsListQuery.GetGroupsListAsync(organizationId, filter, count, startIndex);
var groupsListQueryResult = await _getGroupsListQuery.GetGroupsListAsync(organizationId, model);
var scimListResponseModel = new ScimListResponseModel<ScimGroupResponseModel>
{
Resources = groupsListQueryResult.groupList.Select(g => new ScimGroupResponseModel(g)).ToList(),
ItemsPerPage = count.GetValueOrDefault(groupsListQueryResult.groupList.Count()),
ItemsPerPage = model.Count,
TotalResults = groupsListQueryResult.totalResults,
StartIndex = startIndex.GetValueOrDefault(1),
StartIndex = model.StartIndex,
};
return Ok(scimListResponseModel);
}

View file

@ -3,6 +3,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;

View file

@ -4,6 +4,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Scim.Groups.Interfaces;
using Bit.Scim.Models;
namespace Bit.Scim.Groups;
@ -16,10 +17,16 @@ public class GetGroupsListQuery : IGetGroupsListQuery
_groupRepository = groupRepository;
}
public async Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, string filter, int? count, int? startIndex)
public async Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(
Guid organizationId, GetGroupsQueryParamModel groupQueryParams)
{
string nameFilter = null;
string externalIdFilter = null;
int count = groupQueryParams.Count;
int startIndex = groupQueryParams.StartIndex;
string filter = groupQueryParams.Filter;
if (!string.IsNullOrWhiteSpace(filter))
{
if (filter.StartsWith("displayName eq "))
@ -53,11 +60,11 @@ public class GetGroupsListQuery : IGetGroupsListQuery
}
totalResults = groupList.Count;
}
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
else if (string.IsNullOrWhiteSpace(filter))
{
groupList = groups.OrderBy(g => g.Name)
.Skip(startIndex.Value - 1)
.Take(count.Value)
.Skip(startIndex - 1)
.Take(count)
.ToList();
totalResults = groups.Count;
}

View file

@ -1,8 +1,9 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Scim.Models;
namespace Bit.Scim.Groups.Interfaces;
public interface IGetGroupsListQuery
{
Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, string filter, int? count, int? startIndex);
Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, GetGroupsQueryParamModel model);
}

View file

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Scim.Models;
public class GetGroupsQueryParamModel
{
public string Filter { get; init; } = string.Empty;
[Range(1, int.MaxValue)]
public int Count { get; init; } = 50;
[Range(1, int.MaxValue)]
public int StartIndex { get; init; } = 1;
}

View file

@ -1,5 +1,7 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Scim.Models;
public class GetUsersQueryParamModel
{
public string Filter { get; init; } = string.Empty;

View file

@ -11,21 +11,8 @@ public class Program
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureLogging((hostingContext, logging) =>
logging.AddSerilog(hostingContext, (e, globalSettings) =>
{
var context = e.Properties["SourceContext"].ToString();
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
{
return false;
}
return e.Level >= globalSettings.MinLogLevel.ScimSettings.Default;
}));
})
.AddSerilogFileLogging()
.Build()
.Run();
}

View file

@ -94,11 +94,8 @@ public class Startup
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IHostApplicationLifetime appLifetime,
GlobalSettings globalSettings)
{
app.UseSerilog(env, appLifetime, globalSettings);
// Add general security headers
app.UseMiddleware<SecurityHeadersMiddleware>();

View file

@ -3,6 +3,7 @@
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Scim.Models;
using Bit.Scim.Users.Interfaces;
namespace Bit.Scim.Users;

View file

@ -1,4 +1,5 @@
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Scim.Models;
namespace Bit.Scim.Users.Interfaces;

View file

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;

View file

@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.E
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.Utilities.Commands;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@ -24,7 +25,7 @@ public class PostUserCommand(
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService,
IPaymentService paymentService,
IStripePaymentService paymentService,
IScimContext scimContext,
IFeatureService featureService,
IInviteOrganizationUsersCommand inviteOrganizationUsersCommand,

View file

@ -30,6 +30,7 @@
},
"storage": {
"connectionString": "UseDevelopmentStorage=true"
}
},
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
}
}

View file

@ -30,9 +30,6 @@
"connectionString": "SECRET",
"applicationCacheTopicName": "SECRET"
},
"sentry": {
"dsn": "SECRET"
},
"notificationHub": {
"connectionString": "SECRET",
"hubName": "SECRET"

View file

@ -1,7 +1,4 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Security.Claims;
using System.Security.Claims;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
@ -57,6 +54,7 @@ public class AccountController : Controller
private readonly IDataProtectorTokenFactory<SsoTokenable> _dataProtector;
private readonly IOrganizationDomainRepository _organizationDomainRepository;
private readonly IRegisterUserCommand _registerUserCommand;
private readonly IFeatureService _featureService;
public AccountController(
IAuthenticationSchemeProvider schemeProvider,
@ -77,7 +75,8 @@ public class AccountController : Controller
Core.Services.IEventService eventService,
IDataProtectorTokenFactory<SsoTokenable> dataProtector,
IOrganizationDomainRepository organizationDomainRepository,
IRegisterUserCommand registerUserCommand)
IRegisterUserCommand registerUserCommand,
IFeatureService featureService)
{
_schemeProvider = schemeProvider;
_clientStore = clientStore;
@ -98,46 +97,43 @@ public class AccountController : Controller
_dataProtector = dataProtector;
_organizationDomainRepository = organizationDomainRepository;
_registerUserCommand = registerUserCommand;
_featureService = featureService;
}
[HttpGet]
public async Task<IActionResult> PreValidate(string domainHint)
public async Task<IActionResult> PreValidateAsync(string domainHint)
{
try
{
// Validate domain_hint provided
if (string.IsNullOrWhiteSpace(domainHint))
{
return InvalidJson("NoOrganizationIdentifierProvidedError");
_logger.LogError(new ArgumentException("domainHint is required."), "domainHint not specified.");
return InvalidJson("SsoInvalidIdentifierError");
}
// Validate organization exists from domain_hint
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
if (organization == null)
if (organization is not { UseSso: true })
{
return InvalidJson("OrganizationNotFoundByIdentifierError");
}
if (!organization.UseSso)
{
return InvalidJson("SsoNotAllowedForOrganizationError");
_logger.LogError("Organization not configured to use SSO.");
return InvalidJson("SsoInvalidIdentifierError");
}
// Validate SsoConfig exists and is Enabled
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
if (ssoConfig == null)
if (ssoConfig is not { Enabled: true })
{
return InvalidJson("SsoConfigurationNotFoundForOrganizationError");
}
if (!ssoConfig.Enabled)
{
return InvalidJson("SsoNotEnabledForOrganizationError");
_logger.LogError("SsoConfig not enabled.");
return InvalidJson("SsoInvalidIdentifierError");
}
// Validate Authentication Scheme exists and is loaded (cache)
var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString());
if (scheme == null || !(scheme is IDynamicAuthenticationScheme dynamicScheme))
if (scheme is not IDynamicAuthenticationScheme dynamicScheme)
{
return InvalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError");
_logger.LogError("Invalid authentication scheme for organization.");
return InvalidJson("SsoInvalidIdentifierError");
}
// Run scheme validation
@ -147,13 +143,8 @@ public class AccountController : Controller
}
catch (Exception ex)
{
var translatedException = _i18nService.GetLocalizedHtmlString(ex.Message);
var errorKey = "InvalidSchemeConfigurationError";
if (!translatedException.ResourceNotFound)
{
errorKey = ex.Message;
}
return InvalidJson(errorKey, translatedException.ResourceNotFound ? ex : null);
_logger.LogError(ex, "An error occurred while validating SSO dynamic scheme.");
return InvalidJson("SsoInvalidIdentifierError");
}
var tokenable = new SsoTokenable(organization, _globalSettings.Sso.SsoTokenLifetimeInSeconds);
@ -163,15 +154,18 @@ public class AccountController : Controller
}
catch (Exception ex)
{
return InvalidJson("PreValidationError", ex);
_logger.LogError(ex, "An error occurred during SSO prevalidation.");
return InvalidJson("SsoInvalidIdentifierError");
}
}
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
public async Task<IActionResult> LoginAsync(string returnUrl)
{
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
if (!context.Parameters.AllKeys.Contains("domain_hint") ||
string.IsNullOrWhiteSpace(context.Parameters["domain_hint"]))
{
@ -187,6 +181,7 @@ public class AccountController : Controller
var domainHint = context.Parameters["domain_hint"];
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
#nullable restore
if (organization == null)
{
@ -206,12 +201,15 @@ public class AccountController : Controller
returnUrl,
state = context.Parameters["state"],
userIdentifier = context.Parameters["session_state"],
ssoToken
});
}
[HttpGet]
public IActionResult ExternalChallenge(string scheme, string returnUrl, string state, string userIdentifier)
public IActionResult ExternalChallenge(string scheme, string returnUrl, string state, string userIdentifier, string ssoToken)
{
ValidateSchemeAgainstSsoToken(scheme, ssoToken);
if (string.IsNullOrEmpty(returnUrl))
{
returnUrl = "~/";
@ -240,40 +238,101 @@ public class AccountController : Controller
return Challenge(props, scheme);
}
/// <summary>
/// Validates the scheme (organization ID) against the organization ID found in the ssoToken.
/// </summary>
/// <param name="scheme">The authentication scheme (organization ID) to validate.</param>
/// <param name="ssoToken">The SSO token to validate against.</param>
/// <exception cref="Exception">Thrown if the scheme (organization ID) does not match the organization ID found in the ssoToken.</exception>
private void ValidateSchemeAgainstSsoToken(string scheme, string ssoToken)
{
SsoTokenable tokenable;
try
{
tokenable = _dataProtector.Unprotect(ssoToken);
}
catch
{
throw new Exception(_i18nService.T("InvalidSsoToken"));
}
if (!Guid.TryParse(scheme, out var schemeOrgId) || tokenable.OrganizationId != schemeOrgId)
{
throw new Exception(_i18nService.T("SsoOrganizationIdMismatch"));
}
}
[HttpGet]
public async Task<IActionResult> ExternalCallback()
{
// Feature flag (PM-24579): Prevent SSO on existing non-compliant users.
var preventOrgUserLoginIfStatusInvalid =
_featureService.IsEnabled(FeatureFlagKeys.PM24579_PreventSsoOnExistingNonCompliantUsers);
// Read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(
AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception(_i18nService.T("ExternalAuthenticationError"));
}
// Debugging
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {@claims}", externalClaims);
if (preventOrgUserLoginIfStatusInvalid)
{
if (!result.Succeeded)
{
throw new Exception(_i18nService.T("ExternalAuthenticationError"));
}
}
else
{
if (result?.Succeeded != true)
{
throw new Exception(_i18nService.T("ExternalAuthenticationError"));
}
}
// See if the user has logged in with this SSO provider before and has already been provisioned.
// This is signified by the user existing in the User table and the SSOUser table for the SSO provider they're using.
var (user, provider, providerUserId, claims, ssoConfigData) = await FindUserFromExternalProviderAsync(result);
var (possibleSsoLinkedUser, provider, providerUserId, claims, ssoConfigData) = await FindUserFromExternalProviderAsync(result);
// We will look these up as required (lazy resolution) to avoid multiple DB hits.
Organization? organization = null;
OrganizationUser? orgUser = null;
// The user has not authenticated with this SSO provider before.
// They could have an existing Bitwarden account in the User table though.
if (user == null)
if (possibleSsoLinkedUser == null)
{
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
// If we're manually linking to SSO, the user's external identifier will be passed as query string parameter.
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier") ?
result.Properties.Items["user_identifier"] : null;
user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier, ssoConfigData);
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier")
? result.Properties.Items["user_identifier"]
: null;
var (resolvedUser, foundOrganization, foundOrCreatedOrgUser) =
await CreateUserAndOrgUserConditionallyAsync(
provider,
providerUserId,
claims,
userIdentifier,
ssoConfigData);
#nullable restore
possibleSsoLinkedUser = resolvedUser;
if (preventOrgUserLoginIfStatusInvalid)
{
organization = foundOrganization;
orgUser = foundOrCreatedOrgUser;
}
}
// Either the user already authenticated with the SSO provider, or we've just provisioned them.
// Either way, we have associated the SSO login with a Bitwarden user.
// We will now sign the Bitwarden user in.
if (user != null)
if (preventOrgUserLoginIfStatusInvalid)
{
User resolvedSsoLinkedUser = possibleSsoLinkedUser
?? throw new Exception(_i18nService.T("UserShouldBeFound"));
await PreventOrgUserLoginIfStatusInvalidAsync(organization, provider, orgUser, resolvedSsoLinkedUser);
// This allows us to collect any additional claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
@ -286,19 +345,52 @@ public class AccountController : Controller
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// Issue authentication cookie for user
await HttpContext.SignInAsync(new IdentityServerUser(user.Id.ToString())
await HttpContext.SignInAsync(
new IdentityServerUser(resolvedSsoLinkedUser.Id.ToString())
{
DisplayName = resolvedSsoLinkedUser.Email,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims.ToArray()
}, localSignInProps);
}
else
{
// PM-24579: remove this else block with feature flag removal.
// Either the user already authenticated with the SSO provider, or we've just provisioned them.
// Either way, we have associated the SSO login with a Bitwarden user.
// We will now sign the Bitwarden user in.
if (possibleSsoLinkedUser != null)
{
DisplayName = user.Email,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims.ToArray()
}, localSignInProps);
// This allows us to collect any additional claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1)
};
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// Issue authentication cookie for user
await HttpContext.SignInAsync(
new IdentityServerUser(possibleSsoLinkedUser.Id.ToString())
{
DisplayName = possibleSsoLinkedUser.Email,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims.ToArray()
}, localSignInProps);
}
}
// Delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
// Retrieve return URL
var returnUrl = result.Properties.Items["return_url"] ?? "~/";
#nullable restore
// Check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
@ -317,8 +409,10 @@ public class AccountController : Controller
return Redirect(returnUrl);
}
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
public async Task<IActionResult> LogoutAsync(string logoutId)
{
// Build a model so the logged out page knows what to display
var (updatedLogoutId, redirectUri, externalAuthenticationScheme) = await GetLoggedOutDataAsync(logoutId);
@ -341,6 +435,7 @@ public class AccountController : Controller
// This triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, externalAuthenticationScheme);
}
if (redirectUri != null)
{
return View("Redirect", new RedirectViewModel { RedirectUrl = redirectUri });
@ -350,15 +445,24 @@ public class AccountController : Controller
return Redirect("~/");
}
}
#nullable restore
/// <summary>
/// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`.
/// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`.
/// The claims on the external identity are used to determine an `externalId`, and that is used to find the appropriate `SsoUser` and `User` records.
/// </summary>
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims, SsoConfigurationData config)>
FindUserFromExternalProviderAsync(AuthenticateResult result)
private async Task<(
User? possibleSsoUser,
string provider,
string providerUserId,
IEnumerable<Claim> claims,
SsoConfigurationData config
)> FindUserFromExternalProviderAsync(AuthenticateResult result)
{
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
var provider = result.Properties.Items["scheme"];
//Todo: Validate provider is a valid GUID with TryParse instead. When this is invalid it throws an exception
var orgId = new Guid(provider);
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);
if (ssoConfig == null || !ssoConfig.Enabled)
@ -382,9 +486,10 @@ public class AccountController : Controller
// Ensure the NameIdentifier used is not a transient name ID, if so, we need a different attribute
// for the user identifier.
static bool nameIdIsNotTransient(Claim c) => c.Type == ClaimTypes.NameIdentifier
&& (c.Properties == null
|| !c.Properties.TryGetValue(SamlPropertyKeys.ClaimFormat, out var claimFormat)
|| claimFormat != SamlNameIdFormats.Transient);
&& (c.Properties == null
|| !c.Properties.TryGetValue(SamlPropertyKeys.ClaimFormat,
out var claimFormat)
|| claimFormat != SamlNameIdFormats.Transient);
// Try to determine the unique id of the external user (issued by the provider)
// the most common claim type for that are the sub claim and the NameIdentifier
@ -399,6 +504,7 @@ public class AccountController : Controller
externalUser.FindFirst("upn") ??
externalUser.FindFirst("eppn") ??
throw new Exception(_i18nService.T("UnknownUserId"));
#nullable restore
// Remove the user id claim so we don't include it as an extra claim if/when we provision the user
var claims = externalUser.Claims.ToList();
@ -407,13 +513,15 @@ public class AccountController : Controller
// find external user
var providerUserId = userIdClaim.Value;
var user = await _userRepository.GetBySsoUserAsync(providerUserId, orgId);
var possibleSsoUser = await _userRepository.GetBySsoUserAsync(providerUserId, orgId);
return (user, provider, providerUserId, claims, ssoConfigData);
return (possibleSsoUser, provider, providerUserId, claims, ssoConfigData);
}
/// <summary>
/// Provision an SSO-linked Bitwarden user.
/// This function seeks to set up the org user record or create a new user record based on the conditions
/// below.
///
/// This handles three different scenarios:
/// 1. Creating an SsoUser link for an existing User and OrganizationUser
/// - User is a member of the organization, but hasn't authenticated with the org's SSO provider before.
@ -426,77 +534,100 @@ public class AccountController : Controller
/// <param name="providerUserId">The external identity provider's user identifier.</param>
/// <param name="claims">The claims from the external IdP.</param>
/// <param name="userIdentifier">The user identifier used for manual SSO linking.</param>
/// <param name="config">The SSO configuration for the organization.</param>
/// <returns>The User to sign in.</returns>
/// <param name="ssoConfigData">The SSO configuration for the organization.</param>
/// <returns>Guaranteed to return the user to sign in as well as the found organization and org user.</returns>
/// <exception cref="Exception">An exception if the user cannot be provisioned as requested.</exception>
private async Task<User> AutoProvisionUserAsync(string provider, string providerUserId,
IEnumerable<Claim> claims, string userIdentifier, SsoConfigurationData config)
private async Task<(User resolvedUser, Organization foundOrganization, OrganizationUser foundOrgUser)> CreateUserAndOrgUserConditionallyAsync(
string provider,
string providerUserId,
IEnumerable<Claim> claims,
string userIdentifier,
SsoConfigurationData ssoConfigData
)
{
var name = GetName(claims, config.GetAdditionalNameClaimTypes());
var email = GetEmailAddress(claims, config.GetAdditionalEmailClaimTypes());
if (string.IsNullOrWhiteSpace(email) && providerUserId.Contains("@"))
{
email = providerUserId;
}
// Try to get the email from the claims as we don't know if we have a user record yet.
var name = GetName(claims, ssoConfigData.GetAdditionalNameClaimTypes());
var email = TryGetEmailAddress(claims, ssoConfigData, providerUserId);
if (!Guid.TryParse(provider, out var orgId))
{
// TODO: support non-org (server-wide) SSO in the future?
throw new Exception(_i18nService.T("SSOProviderIsNotAnOrgId", provider));
}
User existingUser = null;
User? possibleExistingUser;
if (string.IsNullOrWhiteSpace(userIdentifier))
{
if (string.IsNullOrWhiteSpace(email))
{
throw new Exception(_i18nService.T("CannotFindEmailClaim"));
}
existingUser = await _userRepository.GetByEmailAsync(email);
possibleExistingUser = await _userRepository.GetByEmailAsync(email);
}
else
{
existingUser = await GetUserFromManualLinkingData(userIdentifier);
possibleExistingUser = await GetUserFromManualLinkingDataAsync(userIdentifier);
}
// Try to find the OrganizationUser if it exists.
var (organization, orgUser) = await FindOrganizationUser(existingUser, email, orgId);
// Find the org (we error if we can't find an org because no org is not valid)
var organization = await GetOrganizationByProviderAsync(provider);
// Try to find an org user (null org user possible and valid here)
var possibleOrgUser = await GetOrganizationUserByUserAndOrgIdOrEmailAsync(possibleExistingUser, organization.Id, email);
//----------------------------------------------------
// Scenario 1: We've found the user in the User table
//----------------------------------------------------
if (existingUser != null)
if (possibleExistingUser != null)
{
if (existingUser.UsesKeyConnector &&
(orgUser == null || orgUser.Status == OrganizationUserStatusType.Invited))
User guaranteedExistingUser = possibleExistingUser;
if (guaranteedExistingUser.UsesKeyConnector &&
(possibleOrgUser == null || possibleOrgUser.Status == OrganizationUserStatusType.Invited))
{
throw new Exception(_i18nService.T("UserAlreadyExistsKeyConnector"));
}
// If the user already exists in Bitwarden, we require that the user already be in the org,
// and that they are either Accepted or Confirmed.
if (orgUser == null)
OrganizationUser guaranteedOrgUser = possibleOrgUser ?? throw new Exception(_i18nService.T("UserAlreadyExistsInviteProcess"));
/*
* ----------------------------------------------------
* Critical Code Check Here
*
* We want to ensure a user is not in the invited state
* explicitly. User's in the invited state should not
* be able to authenticate via SSO.
*
* See internal doc called "Added Context for SSO Login
* Flows" for further details.
* ----------------------------------------------------
*/
if (guaranteedOrgUser.Status == OrganizationUserStatusType.Invited)
{
// Org User is not created - no invite has been sent
throw new Exception(_i18nService.T("UserAlreadyExistsInviteProcess"));
// Org User is invited must accept via email first
throw new Exception(
_i18nService.T("AcceptInviteBeforeUsingSSO", organization.DisplayName()));
}
EnsureOrgUserStatusAllowed(orgUser.Status, organization.DisplayName(),
allowedStatuses: [OrganizationUserStatusType.Accepted, OrganizationUserStatusType.Confirmed]);
// If the user already exists in Bitwarden, we require that the user already be in the org,
// and that they are either Accepted or Confirmed.
EnforceAllowedOrgUserStatus(
guaranteedOrgUser.Status,
allowedStatuses: [
OrganizationUserStatusType.Accepted,
OrganizationUserStatusType.Confirmed
],
organization.DisplayName());
// Since we're in the auto-provisioning logic, this means that the user exists, but they have not
// Since we're in the auto-provisioning logic, this means that the user exists, but they have not
// authenticated with the org's SSO provider before now (otherwise we wouldn't be auto-provisioning them).
// We've verified that the user is Accepted or Confnirmed, so we can create an SsoUser link and proceed
// We've verified that the user is Accepted or Confirmed, so we can create an SsoUser link and proceed
// with authentication.
await CreateSsoUserRecord(providerUserId, existingUser.Id, orgId, orgUser);
return existingUser;
await CreateSsoUserRecordAsync(providerUserId, guaranteedExistingUser.Id, organization.Id, guaranteedOrgUser);
return (guaranteedExistingUser, organization, guaranteedOrgUser);
}
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
if (orgUser == null && organization.Seats.HasValue)
if (possibleOrgUser == null && organization.Seats.HasValue)
{
var occupiedSeats = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var occupiedSeats =
await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var initialSeatCount = organization.Seats.Value;
var availableSeats = initialSeatCount - occupiedSeats.Total;
if (availableSeats < 1)
@ -514,8 +645,10 @@ public class AccountController : Controller
{
if (organization.Seats.Value != initialSeatCount)
{
await _organizationService.AdjustSeatsAsync(orgId, initialSeatCount - organization.Seats.Value);
await _organizationService.AdjustSeatsAsync(organization.Id,
initialSeatCount - organization.Seats.Value);
}
_logger.LogInformation(e, "SSO auto provisioning failed");
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.DisplayName()));
}
@ -523,40 +656,50 @@ public class AccountController : Controller
}
// If the email domain is verified, we can mark the email as verified
if (string.IsNullOrWhiteSpace(email))
{
throw new Exception(_i18nService.T("CannotFindEmailClaim"));
}
var emailVerified = false;
var emailDomain = CoreHelpers.GetEmailDomain(email);
if (!string.IsNullOrWhiteSpace(emailDomain))
{
var organizationDomain = await _organizationDomainRepository.GetDomainByOrgIdAndDomainNameAsync(orgId, emailDomain);
var organizationDomain =
await _organizationDomainRepository.GetDomainByOrgIdAndDomainNameAsync(organization.Id, emailDomain);
emailVerified = organizationDomain?.VerifiedDate.HasValue ?? false;
}
//--------------------------------------------------
// Scenarios 2 and 3: We need to register a new user
//--------------------------------------------------
var user = new User
var newUser = new User
{
Name = name,
Email = email,
EmailVerified = emailVerified,
ApiKey = CoreHelpers.SecureRandomString(30)
};
await _registerUserCommand.RegisterUser(user);
// Always use RegisterSSOAutoProvisionedUserAsync to ensure organization context is available
// for domain validation (BlockClaimedDomainAccountCreation policy) and welcome emails.
// The feature flag logic for welcome email templates is handled internally by RegisterUserCommand.
await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization);
// If the organization has 2fa policy enabled, make sure to default jit user 2fa to email
var twoFactorPolicy =
await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.TwoFactorAuthentication);
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.TwoFactorAuthentication);
if (twoFactorPolicy != null && twoFactorPolicy.Enabled)
{
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
newUser.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new TwoFactorProvider
{
MetaData = new Dictionary<string, object> { ["Email"] = user.Email.ToLowerInvariant() },
MetaData = new Dictionary<string, object> { ["Email"] = newUser.Email.ToLowerInvariant() },
Enabled = true
}
});
await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
await _userService.UpdateTwoFactorProviderAsync(newUser, TwoFactorProviderType.Email);
}
//-----------------------------------------------------------------
@ -564,17 +707,18 @@ public class AccountController : Controller
// This means that an invitation was not sent for this user and we
// need to establish their invited status now.
//-----------------------------------------------------------------
if (orgUser == null)
if (possibleOrgUser == null)
{
orgUser = new OrganizationUser
possibleOrgUser = new OrganizationUser
{
OrganizationId = orgId,
UserId = user.Id,
OrganizationId = organization.Id,
UserId = newUser.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Invited
};
await _organizationUserRepository.CreateAsync(orgUser);
await _organizationUserRepository.CreateAsync(possibleOrgUser);
}
//-----------------------------------------------------------------
// Scenario 3: There is already an existing OrganizationUser
// That was established through an invitation. We just need to
@ -582,24 +726,68 @@ public class AccountController : Controller
//-----------------------------------------------------------------
else
{
orgUser.UserId = user.Id;
await _organizationUserRepository.ReplaceAsync(orgUser);
possibleOrgUser.UserId = newUser.Id;
await _organizationUserRepository.ReplaceAsync(possibleOrgUser);
}
// Create the SsoUser record to link the user to the SSO provider.
await CreateSsoUserRecord(providerUserId, user.Id, orgId, orgUser);
await CreateSsoUserRecordAsync(providerUserId, newUser.Id, organization.Id, possibleOrgUser);
return user;
return (newUser, organization, possibleOrgUser);
}
private async Task<User> GetUserFromManualLinkingData(string userIdentifier)
/// <summary>
/// Validates an organization user is allowed to log in via SSO and blocks invalid statuses.
/// Lazily resolves the organization and organization user if not provided.
/// </summary>
/// <param name="organization">The target organization; if null, resolved from provider.</param>
/// <param name="provider">The SSO scheme provider value (organization id as a GUID string).</param>
/// <param name="orgUser">The organization-user record; if null, looked up by user/org or user email for invited users.</param>
/// <param name="user">The user attempting to sign in (existing or newly provisioned).</param>
/// <exception cref="Exception">Thrown if the organization cannot be resolved from provider;
/// the organization user cannot be found; or the organization user status is not allowed.</exception>
private async Task PreventOrgUserLoginIfStatusInvalidAsync(
Organization? organization,
string provider,
OrganizationUser? orgUser,
User user)
{
User user = null;
// Lazily get organization if not already known
organization ??= await GetOrganizationByProviderAsync(provider);
// Lazily get the org user if not already known
orgUser ??= await GetOrganizationUserByUserAndOrgIdOrEmailAsync(
user,
organization.Id,
user.Email);
if (orgUser != null)
{
// Invited is allowed at this point because we know the user is trying to accept an org invite.
EnforceAllowedOrgUserStatus(
orgUser.Status,
allowedStatuses: [
OrganizationUserStatusType.Invited,
OrganizationUserStatusType.Accepted,
OrganizationUserStatusType.Confirmed,
],
organization.DisplayName());
}
else
{
throw new Exception(_i18nService.T("CouldNotFindOrganizationUser", user.Id, organization.Id));
}
}
private async Task<User?> GetUserFromManualLinkingDataAsync(string userIdentifier)
{
User? user = null;
var split = userIdentifier.Split(",");
if (split.Length < 2)
{
throw new Exception(_i18nService.T("InvalidUserIdentifier"));
}
var userId = split[0];
var token = split[1];
@ -619,64 +807,94 @@ public class AccountController : Controller
throw new Exception(_i18nService.T("UserIdAndTokenMismatch"));
}
}
return user;
}
private async Task<(Organization, OrganizationUser)> FindOrganizationUser(User existingUser, string email, Guid orgId)
/// <summary>
/// Tries to get the organization by the provider which is org id for us as we use the scheme
/// to identify organizations - not identity providers.
/// </summary>
/// <param name="provider">Org id string from SSO scheme property</param>
/// <exception cref="Exception">Errors if the provider string is not a valid org id guid or if the org cannot be found by the id.</exception>
private async Task<Organization> GetOrganizationByProviderAsync(string provider)
{
OrganizationUser orgUser = null;
var organization = await _organizationRepository.GetByIdAsync(orgId);
if (!Guid.TryParse(provider, out var organizationId))
{
// TODO: support non-org (server-wide) SSO in the future?
throw new Exception(_i18nService.T("SSOProviderIsNotAnOrgId", provider));
}
var organization = await _organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
{
throw new Exception(_i18nService.T("CouldNotFindOrganization", orgId));
throw new Exception(_i18nService.T("CouldNotFindOrganization", organizationId));
}
return organization;
}
/// <summary>
/// Attempts to get an <see cref="OrganizationUser"/> for a given organization
/// by first checking for an existing user relationship, and if none is found,
/// by looking up an invited user via their email address.
/// </summary>
/// <param name="user">The existing user entity to be looked up in OrganizationUsers table.</param>
/// <param name="organizationId">Organization id from the provider data.</param>
/// <param name="email">Email to use as a fallback in case of an invited user not in the Org Users
/// table yet.</param>
private async Task<OrganizationUser?> GetOrganizationUserByUserAndOrgIdOrEmailAsync(
User? user,
Guid organizationId,
string? email)
{
OrganizationUser? orgUser = null;
// Try to find OrgUser via existing User Id.
// This covers any OrganizationUser state after they have accepted an invite.
if (existingUser != null)
if (user != null)
{
var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id);
orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgId);
var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(user.Id);
orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == organizationId);
}
// If no Org User found by Existing User Id - search all the organization's users via email.
// This covers users who are Invited but haven't accepted their invite yet.
orgUser ??= await _organizationUserRepository.GetByOrganizationEmailAsync(orgId, email);
if (email != null)
{
orgUser ??= await _organizationUserRepository.GetByOrganizationEmailAsync(organizationId, email);
}
return (organization, orgUser);
return orgUser;
}
private void EnsureOrgUserStatusAllowed(
OrganizationUserStatusType status,
string organizationDisplayName,
params OrganizationUserStatusType[] allowedStatuses)
private void EnforceAllowedOrgUserStatus(
OrganizationUserStatusType statusToCheckAgainst,
OrganizationUserStatusType[] allowedStatuses,
string organizationDisplayNameForLogging)
{
// if this status is one of the allowed ones, just return
if (allowedStatuses.Contains(status))
if (allowedStatuses.Contains(statusToCheckAgainst))
{
return;
}
// otherwise throw the appropriate exception
switch (status)
switch (statusToCheckAgainst)
{
case OrganizationUserStatusType.Invited:
// Org User is invited must accept via email first
throw new Exception(
_i18nService.T("AcceptInviteBeforeUsingSSO", organizationDisplayName));
case OrganizationUserStatusType.Revoked:
// Revoked users may not be (auto)provisioned
throw new Exception(
_i18nService.T("OrganizationUserAccessRevoked", organizationDisplayName));
_i18nService.T("OrganizationUserAccessRevoked", organizationDisplayNameForLogging));
default:
// anything else is “unknown”
throw new Exception(
_i18nService.T("OrganizationUserUnknownStatus", organizationDisplayName));
_i18nService.T("OrganizationUserUnknownStatus", organizationDisplayNameForLogging));
}
}
private IActionResult InvalidJson(string errorMessageKey, Exception ex = null)
private IActionResult InvalidJson(string errorMessageKey, Exception? ex = null)
{
Response.StatusCode = ex == null ? 400 : 500;
return Json(new ErrorResponseModel(_i18nService.T(errorMessageKey))
@ -687,13 +905,13 @@ public class AccountController : Controller
});
}
private string GetEmailAddress(IEnumerable<Claim> claims, IEnumerable<string> additionalClaimTypes)
private string? TryGetEmailAddressFromClaims(IEnumerable<Claim> claims, IEnumerable<string> additionalClaimTypes)
{
var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value) && c.Value.Contains("@"));
var email = filteredClaims.GetFirstMatch(additionalClaimTypes.ToArray()) ??
filteredClaims.GetFirstMatch(JwtClaimTypes.Email, ClaimTypes.Email,
SamlClaimTypes.Email, "mail", "emailaddress");
filteredClaims.GetFirstMatch(JwtClaimTypes.Email, ClaimTypes.Email,
SamlClaimTypes.Email, "mail", "emailaddress");
if (!string.IsNullOrWhiteSpace(email))
{
return email;
@ -709,13 +927,15 @@ public class AccountController : Controller
return null;
}
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
private string GetName(IEnumerable<Claim> claims, IEnumerable<string> additionalClaimTypes)
{
var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value));
var name = filteredClaims.GetFirstMatch(additionalClaimTypes.ToArray()) ??
filteredClaims.GetFirstMatch(JwtClaimTypes.Name, ClaimTypes.Name,
SamlClaimTypes.DisplayName, SamlClaimTypes.CommonName, "displayname", "cn");
filteredClaims.GetFirstMatch(JwtClaimTypes.Name, ClaimTypes.Name,
SamlClaimTypes.DisplayName, SamlClaimTypes.CommonName, "displayname", "cn");
if (!string.IsNullOrWhiteSpace(name))
{
return name;
@ -732,8 +952,10 @@ public class AccountController : Controller
return null;
}
#nullable restore
private async Task CreateSsoUserRecord(string providerUserId, Guid userId, Guid orgId, OrganizationUser orgUser)
private async Task CreateSsoUserRecordAsync(string providerUserId, Guid userId, Guid orgId,
OrganizationUser orgUser)
{
// Delete existing SsoUser (if any) - avoids error if providerId has changed and the sso link is stale
var existingSsoUser = await _ssoUserRepository.GetByUserIdOrganizationIdAsync(orgId, userId);
@ -748,15 +970,12 @@ public class AccountController : Controller
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_FirstSsoLogin);
}
var ssoUser = new SsoUser
{
ExternalId = providerUserId,
UserId = userId,
OrganizationId = orgId,
};
var ssoUser = new SsoUser { ExternalId = providerUserId, UserId = userId, OrganizationId = orgId, };
await _ssoUserRepository.CreateAsync(ssoUser);
}
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
private void ProcessLoginCallback(AuthenticateResult externalResult,
List<Claim> localClaims, AuthenticationProperties localSignInProps)
{
@ -777,18 +996,6 @@ public class AccountController : Controller
}
}
private async Task<string> GetProviderAsync(string returnUrl)
{
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
{
return context.IdP;
}
var schemes = await _schemeProvider.GetAllSchemesAsync();
var providers = schemes.Select(x => x.Name).ToList();
return providers.FirstOrDefault();
}
private async Task<(string, string, string)> GetLoggedOutDataAsync(string logoutId)
{
// Get context information (client name, post logout redirect URI and iframe for federated signout)
@ -819,10 +1026,31 @@ public class AccountController : Controller
return (logoutId, logout?.PostLogoutRedirectUri, externalAuthenticationScheme);
}
#nullable restore
/**
* Tries to get a user's email from the claims and SSO configuration data or the provider user id if
* the claims email extraction returns null.
*/
private string? TryGetEmailAddress(
IEnumerable<Claim> claims,
SsoConfigurationData config,
string providerUserId)
{
var email = TryGetEmailAddressFromClaims(claims, config.GetAdditionalEmailClaimTypes());
// If email isn't populated from claims and providerUserId has @, assume it is the email.
if (string.IsNullOrWhiteSpace(email) && providerUserId.Contains("@"))
{
email = providerUserId;
}
return email;
}
public bool IsNativeClient(DIM.AuthorizationRequest context)
{
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
}
}

View file

@ -1,5 +1,4 @@
using Bit.Core.Utilities;
using Serilog;
namespace Bit.Sso;
@ -13,19 +12,8 @@ public class Program
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureLogging((hostingContext, logging) =>
logging.AddSerilog(hostingContext, (e, globalSettings) =>
{
var context = e.Properties["SourceContext"].ToString();
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
{
return false;
}
return e.Level >= globalSettings.MinLogLevel.SsoSettings.Default;
}));
})
.AddSerilogFileLogging()
.Build()
.Run();
}

View file

@ -10,7 +10,7 @@
<!-- This is a transitive dependency to Sustainsys.Saml2.AspNetCore2 -->
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.10.0" />
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.11.0" />
</ItemGroup>
<ItemGroup>

View file

@ -100,8 +100,6 @@ public class Startup
IdentityModelEventSource.ShowPII = true;
}
app.UseSerilog(env, appLifetime, globalSettings);
// Add general security headers
app.UseMiddleware<SecurityHeadersMiddleware>();
@ -157,6 +155,6 @@ public class Startup
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
// Log startup
logger.LogInformation(Constants.BypassFiltersEventId, globalSettings.ProjectName + " started.");
logger.LogInformation(Constants.BypassFiltersEventId, "{Project} started.", globalSettings.ProjectName);
}
}

View file

@ -24,6 +24,13 @@
"storage": {
"connectionString": "UseDevelopmentStorage=true"
},
"developmentDirectory": "../../../dev"
"developmentDirectory": "../../../dev",
"pricingUri": "https://billingpricing.qa.bitwarden.pw",
"mail": {
"smtp": {
"host": "localhost",
"port": 10250
}
}
}
}

View file

@ -13,7 +13,11 @@
"mail": {
"sendGridApiKey": "SECRET",
"amazonConfigSetName": "Email",
"replyToEmail": "no-reply@bitwarden.com"
"replyToEmail": "no-reply@bitwarden.com",
"smtp": {
"host": "localhost",
"port": 10250
}
},
"identityServer": {
"certificateThumbprint": "SECRET"

View file

@ -17,9 +17,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2",
"sass": "1.89.2",
"sass": "1.93.2",
"sass-loader": "16.0.5",
"webpack": "5.99.8",
"webpack": "5.102.1",
"webpack-cli": "5.1.4"
}
},
@ -34,18 +34,14 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@ -58,20 +54,10 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -80,16 +66,16 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"version": "0.3.30",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -441,9 +427,9 @@
}
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
@ -455,13 +441,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"version": "24.3.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
"integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
"undici-types": "~7.10.0"
}
},
"node_modules/@webassemblyjs/ast": {
@ -687,11 +673,12 @@
"license": "Apache-2.0"
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -699,12 +686,26 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-import-phases": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13.0"
},
"peerDependencies": {
"acorn": "^8.14.0"
}
},
"node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@ -747,6 +748,16 @@
"ajv": "^8.8.2"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.18",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz",
"integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/bootstrap": {
"version": "5.3.6",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
@ -781,9 +792,9 @@
}
},
"node_modules/browserslist": {
"version": "4.24.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
"version": "4.26.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
"integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
"dev": true,
"funding": [
{
@ -800,10 +811,12 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.149",
"node-releases": "^2.0.19",
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
"electron-to-chromium": "^1.5.227",
"node-releases": "^2.0.21",
"update-browserslist-db": "^1.1.3"
},
"bin": {
@ -821,9 +834,9 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
"version": "1.0.30001718",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
"integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"version": "1.0.30001751",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
"integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
"dev": true,
"funding": [
{
@ -975,16 +988,16 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.155",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
"integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
"version": "1.5.237",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz",
"integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==",
"dev": true,
"license": "ISC"
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1107,9 +1120,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true,
"funding": [
{
@ -1241,9 +1254,9 @@
}
},
"node_modules/immutable": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
"integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
"dev": true,
"license": "MIT"
},
@ -1528,9 +1541,9 @@
"optional": true
},
"node_modules/node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"version": "2.0.26",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
"integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==",
"dev": true,
"license": "MIT"
},
@ -1635,9 +1648,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
@ -1654,8 +1667,9 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@ -1860,11 +1874,12 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.89.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
"integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
"version": "1.93.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@ -1922,9 +1937,9 @@
}
},
"node_modules/schema-utils": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2061,24 +2076,28 @@
}
},
"node_modules/tapable": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/terser": {
"version": "5.39.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
"integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.14.0",
"acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
@ -2139,9 +2158,9 @@
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"dev": true,
"license": "MIT"
},
@ -2198,22 +2217,24 @@
}
},
"node_modules/webpack": {
"version": "5.99.8",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
"version": "5.102.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
"@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.14.0",
"browserslist": "^4.24.0",
"acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.26.3",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.1",
"enhanced-resolve": "^5.17.3",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@ -2223,11 +2244,11 @@
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^4.3.2",
"tapable": "^2.1.1",
"schema-utils": "^4.3.3",
"tapable": "^2.3.0",
"terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1",
"webpack-sources": "^3.2.3"
"watchpack": "^2.4.4",
"webpack-sources": "^3.3.3"
},
"bin": {
"webpack": "bin/webpack.js"
@ -2251,6 +2272,7 @@
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",
@ -2317,9 +2339,9 @@
}
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
"dev": true,
"license": "MIT",
"engines": {

View file

@ -16,9 +16,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2",
"sass": "1.89.2",
"sass": "1.93.2",
"sass-loader": "16.0.5",
"webpack": "5.99.8",
"webpack": "5.102.1",
"webpack-cli": "5.1.4"
}
}

View file

@ -13,7 +13,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Test.Billing.Mocks;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@ -131,7 +131,7 @@ public class RemoveOrganizationFromProviderCommandTests
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
}
[Theory, BitAutoData]
@ -156,18 +156,22 @@ public class RemoveOrganizationFromProviderCommandTests
"b@example.com"
]);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(GetSubscription(organization.GatewaySubscriptionId));
sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(organization.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(
options => options.Expand.Contains("customer")))
.Returns(GetSubscription(organization.GatewaySubscriptionId, organization.GatewayCustomerId));
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
await stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId,
Arg.Is<CustomerUpdateOptions>(options =>
options.Coupon == string.Empty && options.Email == "a@example.com"));
await stripeAdapter.Received(1).UpdateCustomerAsync(organization.GatewayCustomerId,
Arg.Is<CustomerUpdateOptions>(options => options.Email == "a@example.com"));
await stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
await stripeAdapter.Received(1).DeleteCustomerDiscountAsync(organization.GatewayCustomerId);
await stripeAdapter.Received(1).DeleteCustomerDiscountAsync(organization.GatewayCustomerId);
await stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options =>
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
options.DaysUntilDue == 30));
@ -205,7 +209,7 @@ public class RemoveOrganizationFromProviderCommandTests
organization.PlanType = PlanType.TeamsMonthly;
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
@ -224,7 +228,7 @@ public class RemoveOrganizationFromProviderCommandTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Description == string.Empty &&
options.Email == organization.BillingEmail &&
options.Expand[0] == "tax" &&
@ -237,14 +241,14 @@ public class RemoveOrganizationFromProviderCommandTests
}
});
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
{
Id = "subscription_id"
});
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
await stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == organization.GatewayCustomerId &&
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
options.DaysUntilDue == 30 &&
@ -294,7 +298,7 @@ public class RemoveOrganizationFromProviderCommandTests
organization.PlanType = PlanType.TeamsMonthly;
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
@ -313,7 +317,7 @@ public class RemoveOrganizationFromProviderCommandTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Description == string.Empty &&
options.Email == organization.BillingEmail &&
options.Expand[0] == "tax" &&
@ -326,14 +330,14 @@ public class RemoveOrganizationFromProviderCommandTests
}
});
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
{
Id = "subscription_id"
});
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
await stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == organization.GatewayCustomerId &&
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
options.DaysUntilDue == 30 &&
@ -368,10 +372,21 @@ public class RemoveOrganizationFromProviderCommandTests
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
}
private static Subscription GetSubscription(string subscriptionId) =>
private static Subscription GetSubscription(string subscriptionId, string customerId) =>
new()
{
Id = subscriptionId,
CustomerId = customerId,
Customer = new Customer
{
Discount = new Discount
{
Coupon = new Coupon
{
Id = "coupon-id"
}
}
},
Status = StripeConstants.SubscriptionStatus.Active,
Items = new StripeList<SubscriptionItem>
{
@ -403,7 +418,7 @@ public class RemoveOrganizationFromProviderCommandTests
organization.PlanType = PlanType.TeamsMonthly;
organization.Enabled = false; // Start with a disabled organization
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
@ -421,7 +436,7 @@ public class RemoveOrganizationFromProviderCommandTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Any<CustomerUpdateOptions>())
stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerUpdateOptions>())
.Returns(new Customer
{
Id = "customer_id",
@ -431,7 +446,7 @@ public class RemoveOrganizationFromProviderCommandTests
}
});
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
{
Id = "new_subscription_id"
});

View file

@ -1,17 +1,23 @@
using Bit.Commercial.Core.AdminConsole.Services;
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -20,6 +26,7 @@ using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Test.Billing.Mocks;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
@ -41,7 +48,7 @@ public class ProviderServiceTests
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null));
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null, null));
Assert.Contains("Invalid owner.", exception.Message);
}
@ -53,83 +60,12 @@ public class ProviderServiceTests
userService.GetUserByIdAsync(user.Id).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null));
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null, null));
Assert.Contains("Invalid token.", exception.Message);
}
[Theory, BitAutoData]
public async Task CompleteSetupAsync_InvalidTaxInfo_ThrowsBadRequestException(
User user,
Provider provider,
string key,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource,
[ProviderUser] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
providerUser.ProviderId = provider.Id;
providerUser.UserId = user.Id;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByIdAsync(user.Id).Returns(user);
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
sutProvider.Create();
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
taxInfo.BillingAddressCountry = null;
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
Assert.Equal("Both address and postal code are required to set up your provider.", exception.Message);
}
[Theory, BitAutoData]
public async Task CompleteSetupAsync_InvalidTokenizedPaymentSource_ThrowsBadRequestException(
User user,
Provider provider,
string key,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource,
[ProviderUser] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
providerUser.ProviderId = provider.Id;
providerUser.UserId = user.Id;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByIdAsync(user.Id).Returns(user);
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
sutProvider.Create();
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
Assert.Equal("A payment method is required to set up your provider.", exception.Message);
}
[Theory, BitAutoData]
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource,
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TokenizedPaymentMethod tokenizedPaymentMethod, BillingAddress billingAddress,
[ProviderUser] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
@ -149,7 +85,7 @@ public class ProviderServiceTests
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
var customer = new Customer { Id = "customer_id" };
providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource).Returns(customer);
providerBillingService.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress).Returns(customer);
var subscription = new Subscription { Id = "subscription_id" };
providerBillingService.SetupSubscription(provider).Returns(subscription);
@ -158,7 +94,7 @@ public class ProviderServiceTests
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource);
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, tokenizedPaymentMethod, billingAddress);
await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(Arg.Is<Provider>(
p =>
@ -170,6 +106,57 @@ public class ProviderServiceTests
.ReplaceAsync(Arg.Is<ProviderUser>(pu => pu.UserId == user.Id && pu.ProviderId == provider.Id && pu.Key == key));
}
[Theory, BitAutoData]
public async Task CompleteSetupAsync_WithAutoConfirmEnabled_ThrowsUserCannotJoinProviderError(User user, Provider provider,
string key,
TokenizedPaymentMethod tokenizedPaymentMethod, BillingAddress billingAddress,
[ProviderUser] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
providerUser.ProviderId = provider.Id;
providerUser.UserId = user.Id;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByIdAsync(user.Id).Returns(user);
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
var customer = new Customer { Id = "customer_id" };
providerBillingService.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress).Returns(customer);
var subscription = new Subscription { Id = "subscription_id" };
providerBillingService.SetupSubscription(provider).Returns(subscription);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
var policyDetails = new List<PolicyDetails> { new() { OrganizationId = Guid.NewGuid(), IsProvider = false } };
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
.Returns(policyRequirement);
sutProvider.Create();
var token = protector.Protect(
$"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, tokenizedPaymentMethod,
billingAddress));
Assert.Equal(new UserCannotJoinProvider().Message, exception.Message);
}
[Theory, BitAutoData]
public async Task UpdateAsync_ProviderIdIsInvalid_Throws(Provider provider, SutProvider<ProviderService> sutProvider)
{
@ -649,6 +636,132 @@ public class ProviderServiceTests
Assert.Equal(user.Id, pu.UserId);
}
[Theory, BitAutoData]
public async Task AcceptUserAsync_WithAutoConfirmEnabledAndPolicyExists_Throws(
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,
User user,
SutProvider<ProviderService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IProviderUserRepository>()
.GetByIdAsync(providerUser.Id)
.Returns(providerUser);
var protector = DataProtectionProvider
.Create("ApplicationName")
.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>()
.CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
sutProvider.Create();
providerUser.Email = user.Email;
var token = protector.Protect($"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
var policyDetails = new List<PolicyDetails>
{
new() { OrganizationId = Guid.NewGuid(), IsProvider = false }
};
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
.Returns(policyRequirement);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token));
Assert.Equal(new UserCannotJoinProvider().Message, exception.Message);
}
[Theory, BitAutoData]
public async Task AcceptUserAsync_WithAutoConfirmEnabledButNoPolicyExists_Success(
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,
User user,
SutProvider<ProviderService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IProviderUserRepository>()
.GetByIdAsync(providerUser.Id)
.Returns(providerUser);
var protector = DataProtectionProvider
.Create("ApplicationName")
.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>()
.CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
sutProvider.Create();
providerUser.Email = user.Email;
var token = protector.Protect($"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement([]);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
.Returns(policyRequirement);
// Act
var pu = await sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token);
// Assert
Assert.Null(pu.Email);
Assert.Equal(ProviderUserStatusType.Accepted, pu.Status);
Assert.Equal(user.Id, pu.UserId);
}
[Theory, BitAutoData]
public async Task AcceptUserAsync_WithAutoConfirmDisabled_Success(
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,
User user,
SutProvider<ProviderService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IProviderUserRepository>()
.GetByIdAsync(providerUser.Id)
.Returns(providerUser);
var protector = DataProtectionProvider
.Create("ApplicationName")
.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>()
.CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
sutProvider.Create();
providerUser.Email = user.Email;
var token = protector.Protect($"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(false);
// Act
var pu = await sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token);
// Assert
Assert.Null(pu.Email);
Assert.Equal(ProviderUserStatusType.Accepted, pu.Status);
Assert.Equal(user.Id, pu.UserId);
// Verify that policy check was never called when feature flag is disabled
await sutProvider.GetDependency<IPolicyRequirementQuery>()
.DidNotReceive()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
}
[Theory, BitAutoData]
public async Task ConfirmUsersAsync_NoValid(
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser pu1,
@ -695,13 +808,131 @@ public class ProviderServiceTests
Assert.Equal("Invalid user.", result[2].Item2);
}
[Theory, BitAutoData]
public async Task ConfirmUsersAsync_WithAutoConfirmEnabledAndPolicyExists_ReturnsError(
[ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,
Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)
{
// Arrange
pu1.ProviderId = provider.Id;
pu1.UserId = u1.Id;
var providerUsers = new[] { pu1 };
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
var policyDetails = new List<PolicyDetails>
{
new() { OrganizationId = Guid.NewGuid(), IsProvider = false }
};
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(u1.Id)
.Returns(policyRequirement);
var dict = providerUsers.ToDictionary(pu => pu.Id, _ => "key");
// Act
var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);
// Assert
Assert.Single(result);
Assert.Equal(new UserCannotJoinProvider().Message, result[0].Item2);
// Verify user was not confirmed
await providerUserRepository.DidNotReceive().ReplaceAsync(Arg.Any<ProviderUser>());
}
[Theory, BitAutoData]
public async Task ConfirmUsersAsync_WithAutoConfirmEnabledButNoPolicyExists_Success(
[ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,
Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)
{
// Arrange
pu1.ProviderId = provider.Id;
pu1.UserId = u1.Id;
var providerUsers = new[] { pu1 };
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(true);
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(new List<PolicyDetails>());
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(u1.Id)
.Returns(policyRequirement);
var dict = providerUsers.ToDictionary(pu => pu.Id, _ => "key");
// Act
var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);
// Assert
Assert.Single(result);
Assert.Equal("", result[0].Item2);
// Verify user was confirmed
await providerUserRepository.Received(1).ReplaceAsync(Arg.Is<ProviderUser>(pu =>
pu.Status == ProviderUserStatusType.Confirmed));
}
[Theory, BitAutoData]
public async Task ConfirmUsersAsync_WithAutoConfirmDisabled_Success(
[ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,
Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)
{
// Arrange
pu1.ProviderId = provider.Id;
pu1.UserId = u1.Id;
var providerUsers = new[] { pu1 };
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
.Returns(false);
var dict = providerUsers.ToDictionary(pu => pu.Id, _ => "key");
// Act
var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);
// Assert
Assert.Single(result);
Assert.Equal("", result[0].Item2);
// Verify user was confirmed
await providerUserRepository.Received(1).ReplaceAsync(Arg.Is<ProviderUser>(pu =>
pu.Status == ProviderUserStatusType.Confirmed));
// Verify that policy check was never called when feature flag is disabled
await sutProvider.GetDependency<IPolicyRequirementQuery>()
.DidNotReceive()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task SaveUserAsync_UserIdIsInvalid_Throws(ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
providerUser.Id = default;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveUserAsync(providerUser, default));
providerUser.Id = Guid.Empty;
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SaveUserAsync(providerUser, Guid.Empty));
Assert.Equal("Invite the user first.", exception.Message);
}
@ -827,7 +1058,7 @@ public class ProviderServiceTests
await organizationRepository.Received(1)
.ReplaceAsync(Arg.Is<Organization>(org => org.BillingEmail == provider.BillingEmail));
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerUpdateAsync(
await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateCustomerAsync(
organization.GatewayCustomerId,
Arg.Is<CustomerUpdateOptions>(options => options.Email == provider.BillingEmail));
@ -882,12 +1113,12 @@ public class ProviderServiceTests
organization.Plan = "Enterprise (Monthly)";
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
.Returns(MockPlans.Get(organization.PlanType));
var expectedPlanType = PlanType.EnterpriseMonthly2020;
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(expectedPlanType)
.Returns(StaticStore.GetPlan(expectedPlanType));
.Returns(MockPlans.Get(expectedPlanType));
var expectedPlanId = "2020-enterprise-org-seat-monthly";
@ -898,9 +1129,9 @@ public class ProviderServiceTests
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(organization.GatewaySubscriptionId)
.Returns(GetSubscription(organization.GatewaySubscriptionId));
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
await sutProvider.GetDependency<IStripeAdapter>().UpdateSubscriptionAsync(
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);

View file

@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@ -58,12 +57,12 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration> { Data = [] });
var response = await sutProvider.Sut.Run(provider);
@ -90,12 +89,12 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration> { Data = [] });
var response = await sutProvider.Sut.Run(provider);
@ -124,12 +123,12 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration> { Data = [] });
var response = await sutProvider.Sut.Run(provider);
@ -158,12 +157,12 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration> { Data = [] });
var response = await sutProvider.Sut.Run(provider);
@ -191,7 +190,7 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@ -219,15 +218,15 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "CA" }]
Data = [new Registration { Country = "GB" }]
});
var response = await sutProvider.Sut.Run(provider);
@ -252,15 +251,15 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
@ -291,15 +290,15 @@ public class GetProviderWarningsQueryTests
{
Data = [new TaxId { Verification = null }]
},
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
@ -333,15 +332,15 @@ public class GetProviderWarningsQueryTests
}
}]
},
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
@ -378,15 +377,15 @@ public class GetProviderWarningsQueryTests
}
}]
},
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
@ -423,15 +422,15 @@ public class GetProviderWarningsQueryTests
}
}]
},
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
@ -461,7 +460,7 @@ public class GetProviderWarningsQueryTests
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Active))
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Active))
.Returns(new StripeList<Registration>
{
Data = [
@ -470,7 +469,7 @@ public class GetProviderWarningsQueryTests
new Registration { Country = "FR" }
]
});
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Scheduled))
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Scheduled))
.Returns(new StripeList<Registration> { Data = [] });
var response = await sutProvider.Sut.Run(provider);
@ -500,15 +499,15 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
@ -520,4 +519,37 @@ public class GetProviderWarningsQueryTests
});
Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);
}
[Theory, BitAutoData]
public async Task Run_USCustomer_NoTaxIdWarning(
Provider provider,
SutProvider<GetProviderWarningsQuery> sutProvider)
{
provider.Enabled = true;
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.Returns(new Subscription
{
Status = SubscriptionStatus.Active,
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
});
var response = await sutProvider.Sut.Run(provider);
Assert.Null(response!.TaxId);
}
}

View file

@ -18,6 +18,7 @@ using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Test.Billing.Mocks;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.DataProtection;
@ -72,7 +73,7 @@ public class BusinessUnitConverterTests
{
organization.PlanType = PlanType.EnterpriseAnnually2020;
var enterpriseAnnually2020 = StaticStore.GetPlan(PlanType.EnterpriseAnnually2020);
var enterpriseAnnually2020 = MockPlans.Get(PlanType.EnterpriseAnnually2020);
var subscription = new Subscription
{
@ -134,7 +135,7 @@ public class BusinessUnitConverterTests
_pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually2020)
.Returns(enterpriseAnnually2020);
var enterpriseAnnually = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
var enterpriseAnnually = MockPlans.Get(PlanType.EnterpriseAnnually);
_pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually)
.Returns(enterpriseAnnually);
@ -143,11 +144,11 @@ public class BusinessUnitConverterTests
await businessUnitConverter.FinalizeConversion(organization, userId, token, providerKey, organizationKey);
await _stripeAdapter.Received(2).CustomerUpdateAsync(subscription.CustomerId, Arg.Any<CustomerUpdateOptions>());
await _stripeAdapter.Received(2).UpdateCustomerAsync(subscription.CustomerId, Arg.Any<CustomerUpdateOptions>());
var updatedPriceId = ProviderPriceAdapter.GetActivePriceId(provider, enterpriseAnnually.Type);
await _stripeAdapter.Received(1).SubscriptionUpdateAsync(subscription.Id, Arg.Is<SubscriptionUpdateOptions>(
await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id, Arg.Is<SubscriptionUpdateOptions>(
arguments =>
arguments.Items.Count == 2 &&
arguments.Items[0].Id == "subscription_item_id" &&
@ -242,7 +243,7 @@ public class BusinessUnitConverterTests
argument.Status == ProviderStatusType.Pending &&
argument.Type == ProviderType.BusinessUnit)).Returns(provider);
var plan = StaticStore.GetPlan(organization.PlanType);
var plan = MockPlans.Get(organization.PlanType);
_pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);

View file

@ -6,7 +6,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Core.Test.Billing.Mocks;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@ -69,7 +69,7 @@ public class MaxProjectsQueryTests
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
.Returns(MockPlans.Get(organization.PlanType));
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
@ -114,7 +114,7 @@ public class MaxProjectsQueryTests
.Returns(projects);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
.Returns(MockPlans.Get(organization.PlanType));
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);

View file

@ -0,0 +1,130 @@
using Bit.Core.SecretsManager.Entities;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Repositories;
public class SecretVersionRepositoryTests
{
[Theory]
[BitAutoData]
public void SecretVersion_EntityCreation_Success(SecretVersion secretVersion)
{
// Arrange & Act
secretVersion.SetNewId();
// Assert
Assert.NotEqual(Guid.Empty, secretVersion.Id);
Assert.NotEqual(Guid.Empty, secretVersion.SecretId);
Assert.NotNull(secretVersion.Value);
Assert.NotEqual(default, secretVersion.VersionDate);
}
[Theory]
[BitAutoData]
public void SecretVersion_WithServiceAccountEditor_Success(SecretVersion secretVersion, Guid serviceAccountId)
{
// Arrange & Act
secretVersion.EditorServiceAccountId = serviceAccountId;
secretVersion.EditorOrganizationUserId = null;
// Assert
Assert.Equal(serviceAccountId, secretVersion.EditorServiceAccountId);
Assert.Null(secretVersion.EditorOrganizationUserId);
}
[Theory]
[BitAutoData]
public void SecretVersion_WithOrganizationUserEditor_Success(SecretVersion secretVersion, Guid organizationUserId)
{
// Arrange & Act
secretVersion.EditorOrganizationUserId = organizationUserId;
secretVersion.EditorServiceAccountId = null;
// Assert
Assert.Equal(organizationUserId, secretVersion.EditorOrganizationUserId);
Assert.Null(secretVersion.EditorServiceAccountId);
}
[Theory]
[BitAutoData]
public void SecretVersion_NullableEditors_Success(SecretVersion secretVersion)
{
// Arrange & Act
secretVersion.EditorServiceAccountId = null;
secretVersion.EditorOrganizationUserId = null;
// Assert
Assert.Null(secretVersion.EditorServiceAccountId);
Assert.Null(secretVersion.EditorOrganizationUserId);
}
[Theory]
[BitAutoData]
public void SecretVersion_VersionDateSet_Success(SecretVersion secretVersion)
{
// Arrange
var versionDate = DateTime.UtcNow;
// Act
secretVersion.VersionDate = versionDate;
// Assert
Assert.Equal(versionDate, secretVersion.VersionDate);
}
[Theory]
[BitAutoData]
public void SecretVersion_ValueEncrypted_Success(SecretVersion secretVersion, string encryptedValue)
{
// Arrange & Act
secretVersion.Value = encryptedValue;
// Assert
Assert.Equal(encryptedValue, secretVersion.Value);
Assert.NotEmpty(secretVersion.Value);
}
[Theory]
[BitAutoData]
public void SecretVersion_MultipleVersions_DifferentIds(List<SecretVersion> secretVersions, Guid secretId)
{
// Arrange & Act
foreach (var version in secretVersions)
{
version.SecretId = secretId;
version.SetNewId();
}
// Assert
var distinctIds = secretVersions.Select(v => v.Id).Distinct();
Assert.Equal(secretVersions.Count, distinctIds.Count());
Assert.All(secretVersions, v => Assert.Equal(secretId, v.SecretId));
}
[Theory]
[BitAutoData]
public void SecretVersion_VersionDateOrdering_Success(SecretVersion version1, SecretVersion version2, SecretVersion version3, Guid secretId)
{
// Arrange
var now = DateTime.UtcNow;
version1.SecretId = secretId;
version1.VersionDate = now.AddDays(-2);
version2.SecretId = secretId;
version2.VersionDate = now.AddDays(-1);
version3.SecretId = secretId;
version3.VersionDate = now;
var versions = new List<SecretVersion> { version2, version3, version1 };
// Act
var orderedVersions = versions.OrderByDescending(v => v.VersionDate).ToList();
// Assert
Assert.Equal(version3.Id, orderedVersions[0].Id); // Most recent
Assert.Equal(version2.Id, orderedVersions[1].Id);
Assert.Equal(version1.Id, orderedVersions[2].Id); // Oldest
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sso\Sso.csproj" />
<ProjectReference Include="..\..\..\test\Common\Common.csproj" />
</ItemGroup>
</Project>

View file

@ -200,6 +200,38 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
}
[Fact]
public async Task GetList_SearchDisplayNameWithoutOptionalParameters_Success()
{
string filter = "displayName eq Test Group 2";
int? itemsPerPage = null;
int? startIndex = null;
var expectedResponse = new ScimListResponseModel<ScimGroupResponseModel>
{
ItemsPerPage = 50, //default value
TotalResults = 1,
StartIndex = 1, //default value
Resources = new List<ScimGroupResponseModel>
{
new ScimGroupResponseModel
{
Id = ScimApplicationFactory.TestGroupId2,
DisplayName = "Test Group 2",
ExternalId = "B",
Schemas = new List<string> { ScimConstants.Scim2SchemaGroup }
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }
};
var context = await _factory.GroupsGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimGroupResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
}
[Fact]
public async Task Post_Success()
{

View file

@ -0,0 +1,36 @@
{
"globalSettings": {
"baseServiceUri": {
"vault": "https://localhost:8080",
"api": "http://localhost:4000",
"identity": "http://localhost:33656",
"admin": "http://localhost:62911",
"notifications": "http://localhost:61840",
"sso": "http://localhost:51822",
"internalNotifications": "http://localhost:61840",
"internalAdmin": "http://localhost:62911",
"internalIdentity": "http://localhost:33656",
"internalApi": "http://localhost:4000",
"internalVault": "https://localhost:8080",
"internalSso": "http://localhost:51822",
"internalScim": "http://localhost:44559"
},
"mail": {
"smtp": {
"host": "localhost",
"port": 10250
}
},
"attachment": {
"connectionString": "UseDevelopmentStorage=true",
"baseUrl": "http://localhost:4000/attachments/"
},
"events": {
"connectionString": "UseDevelopmentStorage=true"
},
"storage": {
"connectionString": "UseDevelopmentStorage=true"
},
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
}
}

View file

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Scim.Groups;
using Bit.Scim.Models;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
@ -24,7 +25,7 @@ public class GetGroupsListCommandTests
.GetManyByOrganizationIdAsync(organizationId)
.Returns(groups);
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, null, count, startIndex);
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Count = count, StartIndex = startIndex });
AssertHelper.AssertPropertyEqual(groups.Skip(startIndex - 1).Take(count).ToList(), result.groupList);
AssertHelper.AssertPropertyEqual(groups.Count, result.totalResults);
@ -47,7 +48,7 @@ public class GetGroupsListCommandTests
.GetManyByOrganizationIdAsync(organizationId)
.Returns(groups);
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
@ -67,7 +68,7 @@ public class GetGroupsListCommandTests
.GetManyByOrganizationIdAsync(organizationId)
.Returns(groups);
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
@ -90,7 +91,7 @@ public class GetGroupsListCommandTests
.GetManyByOrganizationIdAsync(organizationId)
.Returns(groups);
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);
@ -112,7 +113,7 @@ public class GetGroupsListCommandTests
.GetManyByOrganizationIdAsync(organizationId)
.Returns(groups);
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });
AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);
AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);

View file

@ -436,7 +436,7 @@ public class PatchGroupCommandTests
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
// Assert: logging
sutProvider.GetDependency<ILogger<PatchGroupCommand>>().ReceivedWithAnyArgs().LogWarning(default);
sutProvider.GetDependency<ILogger<PatchGroupCommand>>().ReceivedWithAnyArgs().LogWarning("");
}
[Theory]

View file

@ -1,5 +1,6 @@
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Scim.Models;
using Bit.Scim.Users;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;

View file

@ -1,6 +1,6 @@
using System.Text.Json;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;

View file

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
@ -36,7 +37,7 @@ public class PostUserCommandTests
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);
sutProvider.GetDependency<IPaymentService>().HasSecretsManagerStandalone(organization).Returns(true);
sutProvider.GetDependency<IStripePaymentService>().HasSecretsManagerStandalone(organization).Returns(true);
sutProvider.GetDependency<IOrganizationService>()
.InviteUserAsync(organizationId,

View file

@ -0,0 +1,952 @@
using System.Net;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Sso.IntegrationTest.Utilities;
using Bit.Test.Common.AutoFixture.Attributes;
using Bitwarden.License.Test.Sso.IntegrationTest.Utilities;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Testing;
using NSubstitute;
using Xunit;
using AuthenticationSchemes = Bit.Core.AuthenticationSchemes;
namespace Bit.Sso.IntegrationTest.Controllers;
public class AccountControllerTests(SsoApplicationFactory factory) : IClassFixture<SsoApplicationFactory>
{
private readonly SsoApplicationFactory _factory = factory;
/*
* Test to verify the /Account/ExternalCallback endpoint exists and is reachable.
*/
[Fact]
public async Task ExternalCallback_EndpointExists_ReturnsExpectedStatusCode()
{
// Arrange
var client = _factory.CreateClient();
// Act - Verify the endpoint is accessible (even if it fails due to missing auth)
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - The endpoint should exist and return 500 (not 404) due to missing authentication
Assert.NotEqual(HttpStatusCode.NotFound, response.StatusCode);
}
/*
* Test to verify calling /Account/ExternalCallback without an authentication cookie
* results in an error as expected.
*/
[Fact]
public async Task ExternalCallback_WithNoAuthenticationCookie_ReturnsError()
{
// Arrange
var client = _factory.CreateClient();
// Act - Call ExternalCallback without proper authentication setup
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because there's no external authentication cookie
Assert.False(response.IsSuccessStatusCode);
// The endpoint will throw an exception when authentication fails
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify behavior of /Account/ExternalCallback with PM24579 feature flag
*/
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task ExternalCallback_WithPM24579FeatureFlag_AndNoAuthCookie_ReturnsError
(
bool featureFlagEnabled
)
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM24579_PreventSsoOnExistingNonCompliantUsers).Returns(featureFlagEnabled);
services.AddSingleton(featureService);
});
}).CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert
Assert.False(response.IsSuccessStatusCode);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify behavior of /Account/ExternalCallback simulating failed authentication.
*/
[Fact]
public async Task ExternalCallback_WithMockedAuthenticationService_FailedAuth_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithFailedAuthentication()
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert
Assert.False(response.IsSuccessStatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when SSO config exists but is disabled.
*/
[Fact]
public async Task ExternalCallback_WithDisabledSsoConfig_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig(ssoConfig => ssoConfig!.Enabled = false)
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because SSO config is disabled
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("Organization not found or SSO configuration not enabled", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
[Fact]
public async Task ExternalCallback_FindUserFromExternalProviderAsync_OrganizationOrSsoConfigNotFound_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because user has invalid status
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("Organization not found or SSO configuration not enabled", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when SSO config expects an ACR value
* but the authentication response has a missing or invalid ACR claim.
*/
[Fact]
public async Task ExternalCallback_WithExpectedAcrValue_AndInvalidAcr_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig(ssoConfig => ssoConfig!.SetData(
new SsoConfigurationData
{
ExpectedReturnAcrValue = "urn:expected:acr:value"
}))
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because ACR claim is missing or invalid
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("Expected authentication context class reference (acr) was not returned with the authentication response or is invalid", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when the authentication response
* does not contain any recognizable user ID claim (sub, NameIdentifier, uid, upn, eppn).
*/
[Fact]
public async Task ExternalCallback_WithNoUserIdClaim_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.OmitProviderUserId()
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback"); ;
// Assert - Should fail because no user ID claim was found
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("Unknown userid", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when no email claim is found
* and the providerUserId cannot be used as a fallback email (doesn't contain @).
*/
[Fact]
public async Task ExternalCallback_WithNoEmailClaim_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithNullEmail()
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because no email claim was found
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("Cannot find email claim", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when an existing user
* uses Key Connector but has no org user record (was removed from organization).
*/
[Fact]
public async Task ExternalCallback_WithExistingKeyConnectorUser_AndNoOrgUser_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser(user =>
{
user.UsesKeyConnector = true;
})
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because user uses Key Connector but has no org user record
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("You were removed from the organization managing single sign-on for your account", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when an existing user
* uses Key Connector and has an org user record in the invited status.
*/
[Fact]
public async Task ExternalCallback_WithExistingKeyConnectorUser_AndInvitedOrgUser_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig(ssoConfig => { })
.WithUser(user =>
{
user.UsesKeyConnector = true;
})
.WithOrganizationUser(orgUser =>
{
orgUser.Status = OrganizationUserStatusType.Invited;
})
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because user uses Key Connector but the Org user is in the invited status
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("You were removed from the organization managing single sign-on for your account", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when an existing user
* (not using Key Connector) has no org user record - they were removed from the organization.
*/
[Fact]
public async Task ExternalCallback_WithExistingUser_AndNoOrgUser_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser()
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because user exists but has no org user record
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("You were removed from the organization managing single sign-on for your account. Contact the organization administrator", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when an existing user
* has an org user record with Invited status - they must accept the invite first.
*/
[Fact]
public async Task ExternalCallback_WithExistingUser_AndInvitedOrgUserStatus_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser()
.WithOrganizationUser(orgUser =>
{
orgUser.Status = OrganizationUserStatusType.Invited;
})
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because user must accept invite before using SSO
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("you must first log in using your master password", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when organization has no available seats
* and cannot auto-scale because it's a self-hosted instance.
*/
[Fact]
public async Task ExternalCallback_WithNoAvailableSeats_OnSelfHosted_ReturnsError()
{
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithOrganization(org =>
{
org.Seats = 5; // Organization has seat limit
})
.AsSelfHosted()
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because no seats available and cannot auto-scale on self-hosted
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("No seats available for organization", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when organization has no available seats
* and auto-scaling fails (e.g., billing issue, max seats reached).
*/
[Fact]
public async Task ExternalCallback_WithNoAvailableSeats_AndAutoAddSeatsFails_ReturnsError()
{
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithOrganization(org =>
{
org.Seats = 5;
org.MaxAutoscaleSeats = 5;
})
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because auto-adding seats failed
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("No seats available for organization", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when email cannot be found
* during new user provisioning (Scenario 2) after bypassing the first email check
* via manual linking path (userIdentifier is set).
*/
[Fact]
public async Task ExternalCallback_WithUserIdentifier_AndNoEmail_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUserIdentifier("")
.WithNullEmail()
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because email cannot be found during new user provisioning
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("Cannot find email claim", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when org user has an unknown/invalid status.
* This tests defensive code that handles future enum values or data corruption scenarios.
* We simulate this by casting an invalid integer to OrganizationUserStatusType.
*/
[Fact]
public async Task ExternalCallback_WithUnknownOrgUserStatus_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser()
.WithOrganizationUser(orgUser =>
{
orgUser.Status = (OrganizationUserStatusType)99; // Invalid enum value - simulates future status or data corruption
})
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because org user status is unknown/invalid
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("is in an unknown state", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
// Note: "User should be found ln 304" appears to be unreachable defensive code.
// CreateUserAndOrgUserConditionallyAsync always returns a non-null user or throws an exception,
// so possibleSsoLinkedUser cannot be null when the feature flag check executes.
/*
* Test to verify /Account/ExternalCallback returns error when userIdentifier
* is malformed (doesn't contain comma separator for userId,token format).
* There is only a single test case here but in the future we may need to expand the
* tests to cover other invalid formats.
*/
[Theory]
[BitAutoData("No-Comas-Identifier")]
public async Task ExternalCallback_WithInvalidUserIdentifierFormat_ReturnsError(
string UserIdentifier
)
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUserIdentifier(UserIdentifier)
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because userIdentifier format is invalid
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("Invalid user identifier", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when userIdentifier
* contains valid userId but invalid/mismatched token.
*
* NOTE: This test uses the substitute pattern instead of SsoTestDataBuilder because:
* - The userIdentifier in the auth result must contain a userId that matches a user in the system
* - User.SetNewId() always overwrites the Id (unlike Organization.SetNewId() which has a guard)
* - This means we cannot pre-set a User.Id before database insertion
* - The auth mock must be configured BEFORE accessing factory.Services (required by SubstituteService)
* - Therefore, we cannot coordinate the userId between the auth mock and the seeded user
* - Using substitutes allows us to control the exact userId and mock UserManager.VerifyUserTokenAsync
*/
[Fact]
public async Task ExternalCallback_WithUserIdentifier_AndInvalidToken_ReturnsError()
{
// Arrange
var organizationId = Guid.NewGuid();
var providerUserId = Guid.NewGuid().ToString();
var userId = Guid.NewGuid();
var testEmail = "test_user@integration.test";
var testName = "Test User";
// Valid format but token won't verify
var userIdentifier = $"{userId},invalid-token";
var claimedUser = new User
{
Id = userId,
Email = testEmail,
Name = testName
};
var organization = new Organization
{
Id = organizationId,
Name = "Test Organization",
Enabled = true,
UseSso = true
};
var ssoConfig = new SsoConfig
{
OrganizationId = organizationId,
Enabled = true
};
ssoConfig.SetData(new SsoConfigurationData());
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var featureService = Substitute.For<IFeatureService>();
featureService.IsEnabled(FeatureFlagKeys.PM24579_PreventSsoOnExistingNonCompliantUsers).Returns(true);
services.AddSingleton(featureService);
// Mock organization repository
var orgRepo = Substitute.For<IOrganizationRepository>();
orgRepo.GetByIdAsync(organizationId).Returns(organization);
orgRepo.GetByIdentifierAsync(organizationId.ToString()).Returns(organization);
services.AddSingleton(orgRepo);
// Mock SSO config repository
var ssoConfigRepo = Substitute.For<ISsoConfigRepository>();
ssoConfigRepo.GetByOrganizationIdAsync(organizationId).Returns(ssoConfig);
services.AddSingleton(ssoConfigRepo);
// Mock user repository - no existing user via SSO
var userRepo = Substitute.For<IUserRepository>();
userRepo.GetBySsoUserAsync(providerUserId, organizationId).Returns((User?)null);
services.AddSingleton(userRepo);
// Mock user service - returns user for manual linking lookup
var userService = Substitute.For<IUserService>();
userService.GetUserByIdAsync(userId.ToString()).Returns(claimedUser);
services.AddSingleton(userService);
// Mock UserManager to return false for token verification
var userManager = Substitute.For<UserManager<User>>(
Substitute.For<IUserStore<User>>(), null, null, null, null, null, null, null, null);
userManager.VerifyUserTokenAsync(
claimedUser,
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<string>())
.Returns(false);
services.AddSingleton(userManager);
// Mock authentication service with userIdentifier that has valid format but invalid token
var authService = Substitute.For<IAuthenticationService>();
authService.AuthenticateAsync(
Arg.Any<HttpContext>(),
AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)
.Returns(MockSuccessfulAuthResult.Build(organizationId, providerUserId, testEmail, testName, null, userIdentifier));
services.AddSingleton(authService);
});
}).CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because token verification failed
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("Supplied userId and token did not match", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error for revoked org user when PM24579 feature flag is enabled.
*/
[Fact]
public async Task ExternalCallback_WithRevokedOrgUser_WithPM24579FeatureFlagEnabled_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser()
.WithOrganizationUser(orgUser =>
{
orgUser.Status = OrganizationUserStatusType.Revoked;
})
.WithFeatureFlags(factoryService =>
{
factoryService.SubstituteService<IFeatureService>(srv =>
{
srv.IsEnabled(FeatureFlagKeys.PM24579_PreventSsoOnExistingNonCompliantUsers).Returns(true);
});
})
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because user state is invalid
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains(
$"Your access to organization {testData.Organization?.DisplayName()} has been revoked. Please contact your administrator for assistance.",
stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error for revoked org user when PM24579 feature flag is disabled.
*/
[Fact]
public async Task ExternalCallback_WithRevokedOrgUserStatus_WithPM24579FeatureFlagDisabled_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser()
.WithOrganizationUser(orgUser =>
{
orgUser.Status = OrganizationUserStatusType.Revoked;
})
.WithFeatureFlags(factoryService =>
{
factoryService.SubstituteService<IFeatureService>(srv =>
{
srv.IsEnabled(FeatureFlagKeys.PM24579_PreventSsoOnExistingNonCompliantUsers).Returns(false);
});
})
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because user has invalid status
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains(
$"Your access to organization {testData.Organization?.DisplayName()} has been revoked. Please contact your administrator for assistance.",
stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error for invited org user when PM24579 feature flag is disabled.
*/
[Fact]
public async Task ExternalCallback_WithInvitedOrgUserStatus_WithPM24579FeatureFlagDisabled_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser()
.WithOrganizationUser(orgUser =>
{
orgUser.Status = OrganizationUserStatusType.Invited;
})
.WithFeatureFlags(factoryService =>
{
factoryService.SubstituteService<IFeatureService>(srv =>
{
srv.IsEnabled(FeatureFlagKeys.PM24579_PreventSsoOnExistingNonCompliantUsers).Returns(false);
});
})
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because user has invalid status
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains(
$"To accept your invite to {testData.Organization?.DisplayName()}, you must first log in using your master password. Once your invite has been accepted, you will be able to log in using SSO.",
stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when user is found via SSO
* but has no organization user record (with feature flag enabled).
*/
[Fact]
public async Task ExternalCallback_WithSsoUser_AndNoOrgUser_WithFeatureFlagEnabled_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser()
.WithSsoUser()
.WithFeatureFlags(factoryService =>
{
factoryService.SubstituteService<IFeatureService>(srv =>
{
srv.IsEnabled(FeatureFlagKeys.PM24579_PreventSsoOnExistingNonCompliantUsers).Returns(true);
});
})
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because org user cannot be found
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("Could not find organization user", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when the provider scheme
* is not a valid GUID (SSOProviderIsNotAnOrgId).
*
* NOTE: This test uses the substitute pattern instead of SsoTestDataBuilder because:
* - Organization.Id is of type Guid and cannot be set to a non-GUID value
* - The auth mock scheme must be a non-GUID string to trigger this error path
* - This cannot be tested since ln 438 in AccountController.FindUserFromExternalProviderAsync throws a different exception
* before reaching the organization lookup exception.
*/
[Fact(Skip = "This test cannot be executed because the organization ID must be a GUID. See note in test summary.")]
public async Task ExternalCallback_WithInvalidProviderGuid_ReturnsError()
{
// Arrange
var invalidScheme = "not-a-valid-guid";
var providerUserId = Guid.NewGuid().ToString();
var testEmail = "test@example.com";
var testName = "Test User";
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Mock authentication service with invalid (non-GUID) scheme
var authService = Substitute.For<IAuthenticationService>();
authService.AuthenticateAsync(
Arg.Any<HttpContext>(),
AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)
.Returns(MockSuccessfulAuthResult.Build(invalidScheme, providerUserId, testEmail, testName));
services.AddSingleton(authService);
});
}).CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because provider is not a valid organization GUID
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("Organization not found from identifier.", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* Test to verify /Account/ExternalCallback returns error when the organization ID
* in the auth result does not match any organization in the database.
* NOTE: This code path is unreachable because the SsoConfig must exist to proceed, but there is a circular dependency:
* - SsoConfig cannot exist without a valid Organization but the test is testing that an Organization cannot be found.
*/
[Fact(Skip = "This code path is unreachable because the SsoConfig must exist to proceed. But the SsoConfig cannot exist without a valid Organization.")]
public async Task ExternalCallback_WithNonExistentOrganization_ReturnsError()
{
// Arrange
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithNonExistentOrganizationInAuth()
.BuildAsync();
var client = testData.Factory.CreateClient();
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should fail because organization cannot be found by the ID in auth result
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Contains("Could not find organization", stringResponse);
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
/*
* SUCCESS PATH: Test to verify /Account/ExternalCallback succeeds when an existing
* SSO-linked user logs in (user exists in SsoUser table).
*/
[Fact]
public async Task ExternalCallback_WithExistingSsoUser_ReturnsSuccess()
{
// Arrange - User with SSO link already exists
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser()
.WithOrganizationUser()
.WithSsoUser()
.BuildAsync();
var client = testData.Factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false // Prevent auto-redirects to capture initial response
});
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should succeed and redirect
Assert.True(
response.StatusCode == HttpStatusCode.Redirect,
$"Expected success/redirect but got {response.StatusCode}");
Assert.NotNull(response.Headers.Location);
}
/*
* SUCCESS PATH: Test to verify /Account/ExternalCallback succeeds when JIT provisioning
* a new user (user doesn't exist, gets created automatically).
*/
[Fact]
public async Task ExternalCallback_WithJitProvisioning_ReturnsSuccess()
{
// Arrange - No user, no org user - JIT provisioning will create both
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.BuildAsync();
var client = testData.Factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false // Prevent auto-redirects to capture initial response
});
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should succeed and redirect
Assert.True(
response.StatusCode == HttpStatusCode.Redirect,
$"Expected success/redirect but got {response.StatusCode}");
Assert.NotNull(response.Headers.Location);
}
/*
* SUCCESS PATH: Test to verify /Account/ExternalCallback succeeds when an existing user
* with a valid (Confirmed) organization user status logs in via SSO for the first time.
*/
[Fact]
public async Task ExternalCallback_WithExistingUserAndConfirmedOrgUser_ReturnsSuccess()
{
// Arrange - Existing user with confirmed org user status, no SSO link yet
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser()
.WithOrganizationUser(orgUser =>
{
orgUser.Status = OrganizationUserStatusType.Confirmed;
})
.BuildAsync();
var client = testData.Factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false // Prevent auto-redirects to capture initial response
});
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should succeed and redirect
Assert.True(
response.StatusCode == HttpStatusCode.Redirect,
$"Expected success/redirect but got {response.StatusCode}");
Assert.NotNull(response.Headers.Location);
}
/*
* SUCCESS PATH: Test to verify /Account/ExternalCallback succeeds when an existing user
* with Accepted organization user status logs in via SSO.
*/
[Fact]
public async Task ExternalCallback_WithExistingUserAndAcceptedOrgUser_ReturnsSuccess()
{
// Arrange - Existing user with accepted org user status
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser()
.WithOrganizationUser(orgUser =>
{
orgUser.Status = OrganizationUserStatusType.Accepted;
})
.BuildAsync();
var client = testData.Factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false // Prevent auto-redirects to capture initial response
});
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Should succeed and redirect
Assert.True(
response.StatusCode == HttpStatusCode.Redirect,
$"Expected success/redirect but got {response.StatusCode}");
Assert.NotNull(response.Headers.Location);
}
/*
* SUCCESS PATH: Test to verify /Account/ExternalCallback returns a View with 200 status
* when the client is a native application (uses custom URI scheme like "bitwarden://callback").
* Native clients get a different response for better UX - a 200 with redirect view instead of 302.
* See AccountController lines 371-378.
*/
[Fact]
public async Task ExternalCallback_WithNativeClient_ReturnsViewWith200Status()
{
// Arrange - Existing SSO user with native client context
var testData = await new SsoTestDataBuilder()
.WithSsoConfig()
.WithUser()
.WithOrganizationUser()
.WithSsoUser()
.AsNativeClient()
.BuildAsync();
var client = testData.Factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/Account/ExternalCallback");
// Assert - Native clients get 200 status with a redirect view instead of 302
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// The Location header should be empty for native clients (set in controller)
// and the response should contain the redirect view
var content = await response.Content.ReadAsStringAsync();
Assert.NotEmpty(content); // View content should be present
}
}

View file

@ -0,0 +1,12 @@
{
"profiles": {
"Sso.IntegrationTest": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:59973;http://localhost:59974"
}
}
}

View file

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="AutoFixture.Xunit2" Version="$(AutoFixtureXUnit2Version)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sso\Sso.csproj" />
<ProjectReference Include="..\..\..\test\Common\Common.csproj" />
<ProjectReference Include="..\..\..\test\IntegrationTestCommon\IntegrationTestCommon.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="Properties\launchSettings.json">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project>

View file

@ -0,0 +1,11 @@
using Bit.IntegrationTestCommon.Factories;
namespace Bit.Sso.IntegrationTest.Utilities;
public class SsoApplicationFactory : WebApplicationFactoryBase<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
}
}

View file

@ -0,0 +1,327 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bitwarden.License.Test.Sso.IntegrationTest.Utilities;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Microsoft.AspNetCore.Authentication;
using NSubstitute;
using AuthenticationSchemes = Bit.Core.AuthenticationSchemes;
namespace Bit.Sso.IntegrationTest.Utilities;
/// <summary>
/// Contains the factory and all entities created by <see cref="SsoTestDataBuilder"/> for use in integration tests.
/// </summary>
public record SsoTestData(
SsoApplicationFactory Factory,
Organization? Organization,
User? User,
OrganizationUser? OrganizationUser,
SsoConfig? SsoConfig,
SsoUser? SsoUser);
/// <summary>
/// Builder for creating SSO test data with seeded database entities.
/// </summary>
public class SsoTestDataBuilder
{
/// <summary>
/// This UserIdentifier is a mock for the UserIdentifier we get from the External Identity Provider.
/// </summary>
private string? _userIdentifier;
private Action<Organization>? _organizationConfig;
private Action<User>? _userConfig;
private Action<OrganizationUser>? _orgUserConfig;
private Action<SsoConfig>? _ssoConfigConfig;
private Action<SsoUser>? _ssoUserConfig;
private Action<SsoApplicationFactory>? _featureFlagConfig;
private bool _includeUser = false;
private bool _includeSsoUser = false;
private bool _includeOrganizationUser = false;
private bool _includeSsoConfig = false;
private bool _successfulAuth = true;
private bool _withNullEmail = false;
private bool _isSelfHosted = false;
private bool _includeProviderUserId = true;
private bool _useNonExistentOrgInAuth = false;
private bool _isNativeClient = false;
public SsoTestDataBuilder WithOrganization(Action<Organization> configure)
{
_organizationConfig = configure;
return this;
}
public SsoTestDataBuilder WithUser(Action<User>? configure = null)
{
_includeUser = true;
_userConfig = configure;
return this;
}
public SsoTestDataBuilder WithOrganizationUser(Action<OrganizationUser>? configure = null)
{
_includeOrganizationUser = true;
_orgUserConfig = configure;
return this;
}
public SsoTestDataBuilder WithSsoConfig(Action<SsoConfig>? configure = null)
{
_includeSsoConfig = true;
_ssoConfigConfig = configure;
return this;
}
public SsoTestDataBuilder WithSsoUser(Action<SsoUser>? configure = null)
{
_includeSsoUser = true;
_ssoUserConfig = configure;
return this;
}
public SsoTestDataBuilder WithFeatureFlags(Action<SsoApplicationFactory> configure)
{
_featureFlagConfig = configure;
return this;
}
public SsoTestDataBuilder WithFailedAuthentication()
{
_successfulAuth = false;
return this;
}
public SsoTestDataBuilder WithNullEmail()
{
_withNullEmail = true;
return this;
}
public SsoTestDataBuilder WithUserIdentifier(string userIdentifier)
{
_userIdentifier = userIdentifier;
return this;
}
public SsoTestDataBuilder OmitProviderUserId()
{
_includeProviderUserId = false;
return this;
}
public SsoTestDataBuilder AsSelfHosted()
{
_isSelfHosted = true;
return this;
}
/// <summary>
/// Causes the auth result to use a different (non-existent) organization ID than what is seeded
/// in the database. This simulates the "organization not found" scenario.
/// </summary>
public SsoTestDataBuilder WithNonExistentOrganizationInAuth()
{
_useNonExistentOrgInAuth = true;
return this;
}
/// <summary>
/// Configures the test to simulate a native client (non-browser) OIDC flow.
/// Native clients use custom URI schemes (e.g., "bitwarden://callback") instead of http/https.
/// This causes ExternalCallback to return a View with 200 status instead of a redirect.
/// </summary>
public SsoTestDataBuilder AsNativeClient()
{
_isNativeClient = true;
return this;
}
public async Task<SsoTestData> BuildAsync()
{
// Create factory
var factory = new SsoApplicationFactory();
// Pre-generate IDs and values needed for auth mock (before accessing Services)
var organizationId = Guid.NewGuid();
// Use a different org ID in auth if testing "organization not found" scenario
var authOrganizationId = _useNonExistentOrgInAuth ? Guid.NewGuid() : organizationId;
var providerUserId = _includeProviderUserId ? Guid.NewGuid().ToString() : "";
var userEmail = _withNullEmail ? null : $"user_{Guid.NewGuid()}@test.com";
var userName = "TestUser";
// 1. Configure mocked authentication service BEFORE accessing Services
factory.SubstituteService<IAuthenticationService>(authService =>
{
if (_successfulAuth)
{
authService.AuthenticateAsync(
Arg.Any<HttpContext>(),
AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)
.Returns(MockSuccessfulAuthResult.Build(
authOrganizationId,
providerUserId,
userEmail,
userName,
acrValue: null,
_userIdentifier));
}
else
{
authService.AuthenticateAsync(
Arg.Any<HttpContext>(),
AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)
.Returns(AuthenticateResult.Fail("External authentication error"));
}
});
// 1.a Configure GlobalSettings for Self-Hosted and seat limit
factory.SubstituteService<IGlobalSettings>(globalSettings =>
{
globalSettings.SelfHosted.Returns(_isSelfHosted);
});
// 1.b configure setting feature flags
_featureFlagConfig?.Invoke(factory);
// 1.c Configure IIdentityServerInteractionService for native client flow
if (_isNativeClient)
{
factory.SubstituteService<IIdentityServerInteractionService>(interaction =>
{
// Native clients have redirect URIs that don't start with http/https
// e.g., "bitwarden://callback" or "com.bitwarden.app://callback"
var authorizationRequest = new AuthorizationRequest
{
RedirectUri = "bitwarden://sso-callback"
};
interaction.GetAuthorizationContextAsync(Arg.Any<string>())
.Returns(authorizationRequest);
});
}
if (!_successfulAuth)
{
return new SsoTestData(factory, null!, null!, null!, null!, null!);
}
// 2. Create Organization with defaults (using pre-generated ID)
var organization = new Organization
{
Id = organizationId,
Name = "Test Organization",
BillingEmail = "billing@test.com",
Plan = "Enterprise",
Enabled = true,
UseSso = true
};
_organizationConfig?.Invoke(organization);
var orgRepo = factory.Services.GetRequiredService<IOrganizationRepository>();
organization = await orgRepo.CreateAsync(organization);
// 3. Create User with defaults (using pre-generated values)
User? user = null;
if (_includeUser)
{
user = new User
{
Email = userEmail ?? $"email_{Guid.NewGuid()}@test.dev",
Name = userName,
ApiKey = Guid.NewGuid().ToString(),
SecurityStamp = Guid.NewGuid().ToString()
};
_userConfig?.Invoke(user);
var userRepo = factory.Services.GetRequiredService<IUserRepository>();
user = await userRepo.CreateAsync(user);
}
// 4. Create OrganizationUser linking them
OrganizationUser? orgUser = null;
if (_includeOrganizationUser)
{
orgUser = new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user!.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User
};
_orgUserConfig?.Invoke(orgUser);
var orgUserRepo = factory.Services.GetRequiredService<IOrganizationUserRepository>();
orgUser = await orgUserRepo.CreateAsync(orgUser);
}
// 4.a Create many OrganizationUser to test seat count logic
if (organization.Seats > 1)
{
var orgUserRepo = factory.Services.GetRequiredService<IOrganizationUserRepository>();
var userRepo = factory.Services.GetRequiredService<IUserRepository>();
var additionalOrgUsers = new List<OrganizationUser>();
for (var i = 1; i <= organization.Seats; i++)
{
var additionalUser = new User
{
Email = $"additional_user_{i}_{Guid.NewGuid()}@test.dev",
Name = $"AdditionalUser{i}",
ApiKey = Guid.NewGuid().ToString(),
SecurityStamp = Guid.NewGuid().ToString()
};
var createdAdditionalUser = await userRepo.CreateAsync(additionalUser);
var additionalOrgUser = new OrganizationUser
{
OrganizationId = organization.Id,
UserId = createdAdditionalUser.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User
};
additionalOrgUsers.Add(additionalOrgUser);
}
await orgUserRepo.CreateManyAsync(additionalOrgUsers);
}
// 5. Create SsoConfig, if ssoConfigConfig is not null
SsoConfig? ssoConfig = null;
if (_includeSsoConfig)
{
ssoConfig = new SsoConfig
{
OrganizationId = authOrganizationId,
Enabled = true
};
ssoConfig.SetData(new SsoConfigurationData());
_ssoConfigConfig?.Invoke(ssoConfig);
var ssoConfigRepo = factory.Services.GetRequiredService<ISsoConfigRepository>();
ssoConfig = await ssoConfigRepo.CreateAsync(ssoConfig);
}
// 6. Optionally create SsoUser (using pre-generated providerUserId as ExternalId)
SsoUser? ssoUser = null;
if (_includeSsoUser)
{
ssoUser = new SsoUser
{
OrganizationId = organization.Id,
UserId = user!.Id,
ExternalId = providerUserId
};
_ssoUserConfig?.Invoke(ssoUser);
var ssoUserRepo = factory.Services.GetRequiredService<ISsoUserRepository>();
ssoUser = await ssoUserRepo.CreateAsync(ssoUser);
}
return new SsoTestData(factory, organization, user, orgUser, ssoConfig, ssoUser);
}
}

View file

@ -0,0 +1,88 @@
using System.Security.Claims;
using Bit.Core;
using Duende.IdentityModel;
using Microsoft.AspNetCore.Authentication;
namespace Bitwarden.License.Test.Sso.IntegrationTest.Utilities;
/// <summary>
/// Creates a mock for use in tests requiring a valid external authentication result.
/// </summary>
internal static class MockSuccessfulAuthResult
{
/// <summary>
/// Since this tests the external Authentication flow, only the OrganizationId is strictly required.
/// However, some tests may require additional claims to be present, so they can be optionally added.
/// </summary>
/// <param name="organizationId"></param>
/// <param name="providerUserId"></param>
/// <param name="email"></param>
/// <param name="name"></param>
/// <param name="acrValue"></param>
/// <param name="userIdentifier"></param>
/// <returns></returns>
public static AuthenticateResult Build(
Guid organizationId,
string? providerUserId,
string? email,
string? name = null,
string? acrValue = null,
string? userIdentifier = null)
{
return Build(organizationId.ToString(), providerUserId, email, name, acrValue, userIdentifier);
}
/// <summary>
/// Overload that accepts a custom scheme string. Useful for testing invalid provider scenarios
/// where the scheme is not a valid GUID.
/// </summary>
public static AuthenticateResult Build(
string scheme,
string? providerUserId,
string? email,
string? name = null,
string? acrValue = null,
string? userIdentifier = null)
{
var claims = new List<Claim>();
if (!string.IsNullOrEmpty(email))
{
claims.Add(new Claim(JwtClaimTypes.Email, email));
}
if (!string.IsNullOrEmpty(providerUserId))
{
claims.Add(new Claim(JwtClaimTypes.Subject, providerUserId));
}
if (!string.IsNullOrEmpty(name))
{
claims.Add(new Claim(JwtClaimTypes.Name, name));
}
if (!string.IsNullOrEmpty(acrValue))
{
claims.Add(new Claim(JwtClaimTypes.AuthenticationContextClassReference, acrValue));
}
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "External"));
var properties = new AuthenticationProperties
{
Items =
{
["scheme"] = scheme,
["return_url"] = "~/",
["state"] = "test-state",
["user_identifier"] = userIdentifier ?? string.Empty
}
};
var ticket = new AuthenticationTicket(
principal,
properties,
AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
return AuthenticateResult.Success(ticket);
}
}

View file

@ -53,10 +53,10 @@ services:
- ./.data/postgres/log:/var/log/postgresql
profiles:
- postgres
- ef
mysql:
image: mysql:8.0
container_name: bw-mysql
ports:
- "3306:3306"
command:
@ -69,6 +69,7 @@ services:
- mysql_dev_data:/var/lib/mysql
profiles:
- mysql
- ef
mariadb:
image: mariadb:10
@ -76,17 +77,16 @@ services:
- 4306:3306
environment:
MARIADB_USER: maria
MARIADB_PASSWORD: ${MARIADB_ROOT_PASSWORD}
MARIADB_DATABASE: vault_dev
MARIADB_RANDOM_ROOT_PASSWORD: "true"
volumes:
- mariadb_dev_data:/var/lib/mysql
profiles:
- mariadb
- ef
idp:
image: kenchan0130/simplesamlphp:1.19.8
container_name: idp
ports:
- "8090:8080"
environment:
@ -99,8 +99,7 @@ services:
- idp
rabbitmq:
image: rabbitmq:4.1.0-management
container_name: rabbitmq
image: rabbitmq:4.2.0-management
ports:
- "5672:5672"
- "15672:15672"
@ -114,7 +113,6 @@ services:
reverse-proxy:
image: nginx:alpine
container_name: reverse-proxy
volumes:
- "./reverse-proxy.conf:/etc/nginx/conf.d/default.conf"
ports:
@ -124,7 +122,6 @@ services:
- proxy
service-bus:
container_name: service-bus
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
pull_policy: always
volumes:
@ -140,7 +137,6 @@ services:
redis:
image: redis:alpine
container_name: bw-redis
ports:
- "6379:6379"
volumes:
@ -153,5 +149,6 @@ volumes:
mssql_dev_data:
postgres_dev_data:
mysql_dev_data:
mariadb_dev_data:
rabbitmq_data:
redis_data:

View file

@ -18,11 +18,11 @@ if ($LASTEXITCODE -ne 0) {
# Api internal & public
Set-Location "../../src/Api"
dotnet build
dotnet swagger tofile --output "../../api.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "internal"
dotnet swagger tofile --output "../../api.json" "./bin/Debug/net8.0/Api.dll" "internal"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
dotnet swagger tofile --output "../../api.public.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "public"
dotnet swagger tofile --output "../../api.public.json" "./bin/Debug/net8.0/Api.dll" "public"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}

View file

@ -70,7 +70,7 @@ Foreach ($item in @(
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
# MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context.
# However they can still be run independently for integration tests.
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 3)
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 4)
)) {
if (!$item[0] -and !$all) {
continue

View file

@ -33,6 +33,12 @@
"id": "<your Installation Id>",
"key": "<your Installation Key>"
},
"licenseDirectory": "<full path to license directory>"
"events": {
"connectionString": "",
"queueName": "event"
},
"licenseDirectory": "<full path to license directory>",
"enableNewDeviceVerification": true,
"enableEmailVerification": true
}
}

View file

@ -3,22 +3,6 @@
"Namespaces": [
{
"Name": "sbemulatorns",
"Queues": [
{
"Name": "queue.1",
"Properties": {
"DeadLetteringOnMessageExpiration": false,
"DefaultMessageTimeToLive": "PT1H",
"DuplicateDetectionHistoryTimeWindow": "PT20S",
"ForwardDeadLetteredMessagesTo": "",
"ForwardTo": "",
"LockDuration": "PT1M",
"MaxDeliveryCount": 3,
"RequiresDuplicateDetection": false,
"RequiresSession": false
}
}
],
"Topics": [
{
"Name": "event-logging",
@ -34,6 +18,12 @@
},
{
"Name": "events-hec-subscription"
},
{
"Name": "events-datadog-subscription"
},
{
"Name": "events-teams-subscription"
}
]
},
@ -81,6 +71,34 @@
}
}
]
},
{
"Name": "integration-datadog-subscription",
"Rules": [
{
"Name": "datadog-integration-filter",
"Properties": {
"FilterType": "Correlation",
"CorrelationFilter": {
"Label": "datadog"
}
}
}
]
},
{
"Name": "integration-teams-subscription",
"Rules": [
{
"Name": "teams-integration-filter",
"Properties": {
"FilterType": "Correlation",
"CorrelationFilter": {
"Label": "teams"
}
}
}
]
}
]
}

132
dev/verify_migrations.ps1 Normal file
View file

@ -0,0 +1,132 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Validates that new database migration files follow naming conventions and chronological order.
.DESCRIPTION
This script validates migration files in util/Migrator/DbScripts/ to ensure:
1. New migrations follow the naming format: YYYY-MM-DD_NN_Description.sql
2. New migrations are chronologically ordered (filename sorts after existing migrations)
3. Dates use leading zeros (e.g., 2025-01-05, not 2025-1-5)
4. A 2-digit sequence number is included (e.g., _00, _01)
.PARAMETER BaseRef
The base git reference to compare against (e.g., 'main', 'HEAD~1')
.PARAMETER CurrentRef
The current git reference (defaults to 'HEAD')
.EXAMPLE
# For pull requests - compare against main branch
.\verify_migrations.ps1 -BaseRef main
.EXAMPLE
# For pushes - compare against previous commit
.\verify_migrations.ps1 -BaseRef HEAD~1
#>
param(
[Parameter(Mandatory = $true)]
[string]$BaseRef,
[Parameter(Mandatory = $false)]
[string]$CurrentRef = "HEAD"
)
# Use invariant culture for consistent string comparison
[System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::InvariantCulture
$migrationPath = "util/Migrator/DbScripts"
# Get list of migrations from base reference
try {
$baseMigrations = git ls-tree -r --name-only $BaseRef -- "$migrationPath/" 2>$null | Where-Object { $_ -like "*.sql" } | Sort-Object
if ($LASTEXITCODE -ne 0) {
Write-Host "Warning: Could not retrieve migrations from base reference '$BaseRef'"
$baseMigrations = @()
}
}
catch {
Write-Host "Warning: Could not retrieve migrations from base reference '$BaseRef'"
$baseMigrations = @()
}
# Get list of migrations from current reference
$currentMigrations = git ls-tree -r --name-only $CurrentRef -- "$migrationPath/" | Where-Object { $_ -like "*.sql" } | Sort-Object
# Find added migrations
$addedMigrations = $currentMigrations | Where-Object { $_ -notin $baseMigrations }
if ($addedMigrations.Count -eq 0) {
Write-Host "No new migration files added."
exit 0
}
Write-Host "New migration files detected:"
$addedMigrations | ForEach-Object { Write-Host " $_" }
Write-Host ""
# Get the last migration from base reference
if ($baseMigrations.Count -eq 0) {
Write-Host "No previous migrations found (initial commit?). Skipping validation."
exit 0
}
$lastBaseMigration = Split-Path -Leaf ($baseMigrations | Select-Object -Last 1)
Write-Host "Last migration in base reference: $lastBaseMigration"
Write-Host ""
# Required format regex: YYYY-MM-DD_NN_Description.sql
$formatRegex = '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}_.+\.sql$'
$validationFailed = $false
foreach ($migration in $addedMigrations) {
$migrationName = Split-Path -Leaf $migration
# Validate NEW migration filename format
if ($migrationName -notmatch $formatRegex) {
Write-Host "ERROR: Migration '$migrationName' does not match required format"
Write-Host "Required format: YYYY-MM-DD_NN_Description.sql"
Write-Host " - YYYY: 4-digit year"
Write-Host " - MM: 2-digit month with leading zero (01-12)"
Write-Host " - DD: 2-digit day with leading zero (01-31)"
Write-Host " - NN: 2-digit sequence number (00, 01, 02, etc.)"
Write-Host "Example: 2025-01-15_00_MyMigration.sql"
$validationFailed = $true
continue
}
# Compare migration name with last base migration (using ordinal string comparison)
if ([string]::CompareOrdinal($migrationName, $lastBaseMigration) -lt 0) {
Write-Host "ERROR: New migration '$migrationName' is not chronologically after '$lastBaseMigration'"
$validationFailed = $true
}
else {
Write-Host "OK: '$migrationName' is chronologically after '$lastBaseMigration'"
}
}
Write-Host ""
if ($validationFailed) {
Write-Host "FAILED: One or more migrations are incorrectly named or not in chronological order"
Write-Host ""
Write-Host "All new migration files must:"
Write-Host " 1. Follow the naming format: YYYY-MM-DD_NN_Description.sql"
Write-Host " 2. Use leading zeros in dates (e.g., 2025-01-05, not 2025-1-5)"
Write-Host " 3. Include a 2-digit sequence number (e.g., _00, _01)"
Write-Host " 4. Have a filename that sorts after the last migration in base"
Write-Host ""
Write-Host "To fix this issue:"
Write-Host " 1. Locate your migration file(s) in util/Migrator/DbScripts/"
Write-Host " 2. Rename to follow format: YYYY-MM-DD_NN_Description.sql"
Write-Host " 3. Ensure the date is after $lastBaseMigration"
Write-Host ""
Write-Host "Example: 2025-01-15_00_AddNewFeature.sql"
exit 1
}
Write-Host "SUCCESS: All new migrations are correctly named and in chronological order"
exit 0

View file

@ -5,6 +5,7 @@
},
"msbuild-sdks": {
"Microsoft.Build.Traversal": "4.1.0",
"Microsoft.Build.Sql": "1.0.0"
"Microsoft.Build.Sql": "1.0.0",
"Bitwarden.Server.Sdk": "1.2.0"
}
}

View file

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.3" />
</ItemGroup>
<ItemGroup>

View file

@ -9,12 +9,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
export const options = {
ext: {
loadimpact: {
projectID: 3639465,
name: "Config",
},
},
scenarios: {
constant_load: {
executor: "constant-arrival-rate",

View file

@ -10,12 +10,6 @@ const AUTH_CLIENT_ID = __ENV.AUTH_CLIENT_ID;
const AUTH_CLIENT_SECRET = __ENV.AUTH_CLIENT_SECRET;
export const options = {
ext: {
loadimpact: {
projectID: 3639465,
name: "Groups",
},
},
scenarios: {
constant_load: {
executor: "constant-arrival-rate",

View file

@ -6,12 +6,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
export const options = {
ext: {
loadimpact: {
projectID: 3639465,
name: "Login",
},
},
scenarios: {
constant_load: {
executor: "constant-arrival-rate",

View file

@ -9,12 +9,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
export const options = {
ext: {
loadimpact: {
projectID: 3639465,
name: "Sync",
},
},
scenarios: {
constant_load: {
executor: "constant-arrival-rate",

View file

@ -14,8 +14,10 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Organizations.Services;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Enums;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
@ -41,7 +43,7 @@ public class OrganizationsController : Controller
private readonly ICollectionRepository _collectionRepository;
private readonly IGroupRepository _groupRepository;
private readonly IPolicyRepository _policyRepository;
private readonly IPaymentService _paymentService;
private readonly IStripePaymentService _paymentService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly GlobalSettings _globalSettings;
private readonly IProviderRepository _providerRepository;
@ -56,6 +58,7 @@ public class OrganizationsController : Controller
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
private readonly IPricingClient _pricingClient;
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
private readonly IOrganizationBillingService _organizationBillingService;
public OrganizationsController(
IOrganizationRepository organizationRepository,
@ -66,7 +69,7 @@ public class OrganizationsController : Controller
ICollectionRepository collectionRepository,
IGroupRepository groupRepository,
IPolicyRepository policyRepository,
IPaymentService paymentService,
IStripePaymentService paymentService,
IApplicationCacheService applicationCacheService,
GlobalSettings globalSettings,
IProviderRepository providerRepository,
@ -80,7 +83,8 @@ public class OrganizationsController : Controller
IProviderBillingService providerBillingService,
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
IPricingClient pricingClient,
IResendOrganizationInviteCommand resendOrganizationInviteCommand)
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
IOrganizationBillingService organizationBillingService)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -105,6 +109,7 @@ public class OrganizationsController : Controller
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
_pricingClient = pricingClient;
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
_organizationBillingService = organizationBillingService;
}
[RequirePermission(Permission.Org_List_View)]
@ -241,6 +246,8 @@ public class OrganizationsController : Controller
var existingOrganizationData = new Organization
{
Id = organization.Id,
Name = organization.Name,
BillingEmail = organization.BillingEmail,
Status = organization.Status,
PlanType = organization.PlanType,
Seats = organization.Seats
@ -286,6 +293,22 @@ public class OrganizationsController : Controller
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
// Sync name/email changes to Stripe
if (existingOrganizationData.Name != organization.Name || existingOrganizationData.BillingEmail != organization.BillingEmail)
{
try
{
await _organizationBillingService.UpdateOrganizationNameAndEmail(organization);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to update Stripe customer for organization {OrganizationId}. Database was updated successfully.",
organization.Id);
TempData["Warning"] = "Organization updated successfully, but Stripe customer name/email synchronization failed.";
}
}
return RedirectToAction("Edit", new { id });
}
@ -472,6 +495,9 @@ public class OrganizationsController : Controller
organization.UseRiskInsights = model.UseRiskInsights;
organization.UseOrganizationDomains = model.UseOrganizationDomains;
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
organization.UseAutomaticUserConfirmation = model.UseAutomaticUserConfirmation;
organization.UseDisableSmAdsForUsers = model.UseDisableSmAdsForUsers;
organization.UsePhishingBlocker = model.UsePhishingBlocker;
//secrets
organization.SmSeats = model.SmSeats;

View file

@ -56,6 +56,7 @@ public class ProvidersController : Controller
private readonly IStripeAdapter _stripeAdapter;
private readonly IAccessControlService _accessControlService;
private readonly ISubscriberService _subscriberService;
private readonly ILogger<ProvidersController> _logger;
public ProvidersController(IOrganizationRepository organizationRepository,
IResellerClientOrganizationSignUpCommand resellerClientOrganizationSignUpCommand,
@ -72,7 +73,8 @@ public class ProvidersController : Controller
IPricingClient pricingClient,
IStripeAdapter stripeAdapter,
IAccessControlService accessControlService,
ISubscriberService subscriberService)
ISubscriberService subscriberService,
ILogger<ProvidersController> logger)
{
_organizationRepository = organizationRepository;
_resellerClientOrganizationSignUpCommand = resellerClientOrganizationSignUpCommand;
@ -92,6 +94,7 @@ public class ProvidersController : Controller
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
_subscriberService = subscriberService;
_logger = logger;
}
[RequirePermission(Permission.Provider_List_View)]
@ -296,6 +299,9 @@ public class ProvidersController : Controller
var originalProviderStatus = provider.Enabled;
// Capture original billing email before modifications for Stripe sync
var originalBillingEmail = provider.BillingEmail;
model.ToProvider(provider);
// validate the stripe ids to prevent saving a bad one
@ -321,6 +327,22 @@ public class ProvidersController : Controller
await _providerService.UpdateAsync(provider);
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
// Sync billing email changes to Stripe
if (!string.IsNullOrEmpty(provider.GatewayCustomerId) && originalBillingEmail != provider.BillingEmail)
{
try
{
await _providerBillingService.UpdateProviderNameAndEmail(provider);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to update Stripe customer for provider {ProviderId}. Database was updated successfully.",
provider.Id);
TempData["Warning"] = "Provider updated successfully, but Stripe customer email synchronization failed.";
}
}
if (!provider.IsBillable())
{
return RedirectToAction("Edit", new { id });
@ -339,11 +361,11 @@ public class ProvidersController : Controller
]);
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId);
var customer = await _stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId);
if (model.PayByInvoice != customer.ApprovedToPayByInvoice())
{
var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0";
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
await _stripeAdapter.UpdateCustomerAsync(customer.Id, new CustomerUpdateOptions
{
Metadata = new Dictionary<string, string>
{

View file

@ -106,6 +106,10 @@ public class OrganizationEditModel : OrganizationViewModel
SmServiceAccounts = org.SmServiceAccounts;
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
UseOrganizationDomains = org.UseOrganizationDomains;
UseAutomaticUserConfirmation = org.UseAutomaticUserConfirmation;
UseDisableSmAdsForUsers = org.UseDisableSmAdsForUsers;
UsePhishingBlocker = org.UsePhishingBlocker;
_plans = plans;
}
@ -158,6 +162,8 @@ public class OrganizationEditModel : OrganizationViewModel
public new bool UseSecretsManager { get; set; }
[Display(Name = "Risk Insights")]
public new bool UseRiskInsights { get; set; }
[Display(Name = "Phishing Blocker")]
public new bool UsePhishingBlocker { get; set; }
[Display(Name = "Admin Sponsored Families")]
public bool UseAdminSponsoredFamilies { get; set; }
[Display(Name = "Self Host")]
@ -191,7 +197,11 @@ public class OrganizationEditModel : OrganizationViewModel
public int? MaxAutoscaleSmServiceAccounts { get; set; }
[Display(Name = "Use Organization Domains")]
public bool UseOrganizationDomains { get; set; }
[Display(Name = "Disable SM Ads For Users")]
public new bool UseDisableSmAdsForUsers { get; set; }
[Display(Name = "Automatic User Confirmation")]
public bool UseAutomaticUserConfirmation { get; set; }
/**
* Creates a Plan[] object for use in Javascript
* This is mapped manually below to provide some type safety in case the plan objects change
@ -231,6 +241,7 @@ public class OrganizationEditModel : OrganizationViewModel
LegacyYear = p.LegacyYear,
Disabled = p.Disabled,
SupportsSecretsManager = p.SupportsSecretsManager,
AutomaticUserConfirmation = p.AutomaticUserConfirmation,
PasswordManager =
new
{
@ -322,6 +333,8 @@ public class OrganizationEditModel : OrganizationViewModel
existingOrganization.SmServiceAccounts = SmServiceAccounts;
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
existingOrganization.UseOrganizationDomains = UseOrganizationDomains;
existingOrganization.UseDisableSmAdsForUsers = UseDisableSmAdsForUsers;
existingOrganization.UsePhishingBlocker = UsePhishingBlocker;
return existingOrganization;
}
}

View file

@ -75,6 +75,8 @@ public class OrganizationViewModel
public int OccupiedSmSeatsCount { get; set; }
public bool UseSecretsManager => Organization.UseSecretsManager;
public bool UseRiskInsights => Organization.UseRiskInsights;
public bool UsePhishingBlocker => Organization.UsePhishingBlocker;
public bool UseDisableSmAdsForUsers => Organization.UseDisableSmAdsForUsers;
public IEnumerable<OrganizationUserUserDetails> OwnersDetails { get; set; }
public IEnumerable<OrganizationUserUserDetails> AdminsDetails { get; set; }
}

View file

@ -152,11 +152,19 @@
<input type="checkbox" class="form-check-input" asp-for="UseCustomPermissions" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseCustomPermissions"></label>
</div>
@if(FeatureService.IsEnabled(FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseAdminSponsoredFamilies" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseAdminSponsoredFamilies"></label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UsePhishingBlocker" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UsePhishingBlocker"></label>
</div>
@if(FeatureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
{
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseAdminSponsoredFamilies" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseAdminSponsoredFamilies"></label>
<input type="checkbox" class="form-check-input" asp-for="UseAutomaticUserConfirmation" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseAutomaticUserConfirmation"></label>
</div>
}
</div>
@ -177,6 +185,13 @@
<input type="checkbox" class="form-check-input" asp-for="UseSecretsManager" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseSecretsManager"></label>
</div>
@if (FeatureService.IsEnabled(FeatureFlagKeys.SM1719_RemoveSecretsManagerAds))
{
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseDisableSmAdsForUsers" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseDisableSmAdsForUsers"></label>
</div>
}
</div>
<div class="col-2">
<h3>Access Intelligence</h3>

View file

@ -1,83 +0,0 @@
using Bit.Admin.Billing.Models;
using Bit.Admin.Enums;
using Bit.Admin.Utilities;
using Bit.Core.Billing.Providers.Migration.Models;
using Bit.Core.Billing.Providers.Migration.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Admin.Billing.Controllers;
[Authorize]
[Route("migrate-providers")]
[SelfHosted(NotSelfHostedOnly = true)]
public class MigrateProvidersController(
IProviderMigrator providerMigrator) : Controller
{
[HttpGet]
[RequirePermission(Permission.Tools_MigrateProviders)]
public IActionResult Index()
{
return View(new MigrateProvidersRequestModel());
}
[HttpPost]
[RequirePermission(Permission.Tools_MigrateProviders)]
[ValidateAntiForgeryToken]
public async Task<IActionResult> PostAsync(MigrateProvidersRequestModel request)
{
var providerIds = GetProviderIdsFromInput(request.ProviderIds);
if (providerIds.Count == 0)
{
return RedirectToAction("Index");
}
foreach (var providerId in providerIds)
{
await providerMigrator.Migrate(providerId);
}
return RedirectToAction("Results", new { ProviderIds = string.Join("\r\n", providerIds) });
}
[HttpGet("results")]
[RequirePermission(Permission.Tools_MigrateProviders)]
public async Task<IActionResult> ResultsAsync(MigrateProvidersRequestModel request)
{
var providerIds = GetProviderIdsFromInput(request.ProviderIds);
if (providerIds.Count == 0)
{
return View(Array.Empty<ProviderMigrationResult>());
}
var results = await Task.WhenAll(providerIds.Select(providerMigrator.GetResult));
return View(results);
}
[HttpGet("results/{providerId:guid}")]
[RequirePermission(Permission.Tools_MigrateProviders)]
public async Task<IActionResult> DetailsAsync([FromRoute] Guid providerId)
{
var result = await providerMigrator.GetResult(providerId);
if (result == null)
{
return RedirectToAction("Index");
}
return View(result);
}
private static List<Guid> GetProviderIdsFromInput(string text) => !string.IsNullOrEmpty(text)
? text.Split(
["\r\n", "\r", "\n"],
StringSplitOptions.TrimEntries
)
.Select(id => new Guid(id))
.ToList()
: [];
}

View file

@ -1,13 +0,0 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.ComponentModel.DataAnnotations;
namespace Bit.Admin.Billing.Models;
public class MigrateProvidersRequestModel
{
[Required]
[Display(Name = "Provider IDs")]
public string ProviderIds { get; set; }
}

View file

@ -1,39 +0,0 @@
@using System.Text.Json
@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult
@{
ViewData["Title"] = "Results";
}
<h1>Migrate Providers</h1>
<h2>Migration Details: @Model.ProviderName</h2>
<dl class="row">
<dt class="col-sm-4 col-lg-3">Id</dt>
<dd class="col-sm-8 col-lg-9"><code>@Model.ProviderId</code></dd>
<dt class="col-sm-4 col-lg-3">Result</dt>
<dd class="col-sm-8 col-lg-9">@Model.Result</dd>
</dl>
<h3>Client Organizations</h3>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Result</th>
<th>Previous State</th>
</tr>
</thead>
<tbody>
@foreach (var clientResult in Model.Clients)
{
<tr>
<td>@clientResult.OrganizationId</td>
<td>@clientResult.OrganizationName</td>
<td>@clientResult.Result</td>
<td><pre>@Html.Raw(JsonSerializer.Serialize(clientResult.PreviousState))</pre></td>
</tr>
}
</tbody>
</table>
</div>

Some files were not shown because too many files have changed in this diff Show more